diff --git a/landing/next-env.d.ts b/landing/next-env.d.ts index ce4e94a..a419cbe 100644 --- a/landing/next-env.d.ts +++ b/landing/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; -import "./.next/types/root-params.d.ts"; +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/landing/package-lock.json b/landing/package-lock.json index 0477d1d..d1b39fe 100644 --- a/landing/package-lock.json +++ b/landing/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "next": "16.4.3", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "framer-motion": "^12.43.0", + "lucide-react": "^0.488.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.4", @@ -6858,6 +6860,57 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/framer-motion": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.43.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.488.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.488.0.tgz", + "integrity": "sha512-ronlL0MyKut4CEzBY/ai2ZpKPxyWO4jUqdAkm2GNK5Zn3Rj+swDz+3lvyAUXN0PNqPKIX6XM9Xadwz/skLs/pQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } } } } diff --git a/landing/package.json b/landing/package.json index 94f96ee..39daaef 100644 --- a/landing/package.json +++ b/landing/package.json @@ -12,7 +12,9 @@ "dependencies": { "next": "16.4.3", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "framer-motion": "^12.43.0", + "lucide-react": "^0.488.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.4", diff --git a/landing/public/screenshots/app/dashboard-overview.png b/landing/public/screenshots/app/dashboard-overview.png new file mode 100644 index 0000000..26489ce Binary files /dev/null and b/landing/public/screenshots/app/dashboard-overview.png differ diff --git a/landing/public/screenshots/app/dependency-analysis-detail.png b/landing/public/screenshots/app/dependency-analysis-detail.png new file mode 100644 index 0000000..35fc347 Binary files /dev/null and b/landing/public/screenshots/app/dependency-analysis-detail.png differ diff --git a/landing/public/screenshots/app/dependency-graph.png b/landing/public/screenshots/app/dependency-graph.png new file mode 100644 index 0000000..1c00b7e Binary files /dev/null and b/landing/public/screenshots/app/dependency-graph.png differ diff --git a/landing/public/screenshots/app/migration-assistant.png b/landing/public/screenshots/app/migration-assistant.png new file mode 100644 index 0000000..716e766 Binary files /dev/null and b/landing/public/screenshots/app/migration-assistant.png differ diff --git a/landing/public/screenshots/app/pydantic-upgrade-simulation-in-progress.png b/landing/public/screenshots/app/pydantic-upgrade-simulation-in-progress.png new file mode 100644 index 0000000..8335b59 Binary files /dev/null and b/landing/public/screenshots/app/pydantic-upgrade-simulation-in-progress.png differ diff --git a/landing/public/screenshots/app/repository-inventory.png b/landing/public/screenshots/app/repository-inventory.png new file mode 100644 index 0000000..f4bbee9 Binary files /dev/null and b/landing/public/screenshots/app/repository-inventory.png differ diff --git a/landing/public/screenshots/app/security-findings.png b/landing/public/screenshots/app/security-findings.png new file mode 100644 index 0000000..e7ed7a1 Binary files /dev/null and b/landing/public/screenshots/app/security-findings.png differ diff --git a/landing/public/screenshots/app/upgrade-simulation-babel-in-progress.png b/landing/public/screenshots/app/upgrade-simulation-babel-in-progress.png new file mode 100644 index 0000000..8dc2083 Binary files /dev/null and b/landing/public/screenshots/app/upgrade-simulation-babel-in-progress.png differ diff --git a/landing/public/screenshots/app/upgrade-simulation-babel-results.png b/landing/public/screenshots/app/upgrade-simulation-babel-results.png new file mode 100644 index 0000000..d173b8b Binary files /dev/null and b/landing/public/screenshots/app/upgrade-simulation-babel-results.png differ diff --git a/landing/src/app/globals.css b/landing/src/app/globals.css index 3e445eb..4cf12a3 100644 --- a/landing/src/app/globals.css +++ b/landing/src/app/globals.css @@ -1,120 +1,208 @@ @import "tailwindcss"; +@custom-variant dark (&:is(.dark *)); + :root { - --ink: #070b12; - --panel: #0a1018; - --panel-raised: #0d151f; - --line: rgba(220, 234, 245, .095); - --text: #e8eef6; - --muted: #91a0b2; - --quiet: #657488; - --teal: #3dd6c6; - --teal-dim: #163b39; - --mono: "IBM Plex Mono", ui-monospace, monospace; - --sans: "DM Sans", ui-sans-serif, system-ui, sans-serif; - --display: "Syne", "DM Sans", ui-sans-serif, sans-serif; -} - -* { box-sizing: border-box; } -html { scroll-behavior: smooth; scroll-padding-top: 88px; color-scheme: dark; } -body { margin: 0; min-width: 320px; background: var(--ink); color: var(--text); font-family: var(--sans); -webkit-font-smoothing: antialiased; } -body::before { position: fixed; z-index: -1; inset: 0; content: ""; pointer-events: none; background: radial-gradient(ellipse at 12% -10%, rgba(61,214,198,.09), transparent 36%), radial-gradient(ellipse at 88% 5%, rgba(48,111,122,.08), transparent 31%); } -a { color: inherit; text-decoration: none; } -button { font: inherit; } -::selection { background: rgba(61,214,198,.28); color: white; } -.container { width: min(1160px, calc(100% - 64px)); margin-inline: auto; } -.skip-link { position: fixed; z-index: 100; top: 8px; left: 8px; transform: translateY(-150%); border-radius: 8px; background: var(--teal); padding: 12px 16px; color: #04201c; font-weight: 700; } -.skip-link:focus { transform: translateY(0); } -.site-header { position: sticky; z-index: 50; top: 0; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(7,11,18,.86); backdrop-filter: blur(16px); } -.nav-inner { width: min(1320px, calc(100% - 64px)); height: 76px; margin: auto; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 28px; } -.brand { display: inline-flex; width: fit-content; align-items: center; gap: 10px; color: #fff; font-family: var(--display); font-size: 17px; font-weight: 650; letter-spacing: -.04em; } -.brand-mark { display: grid; width: 34px; height: 34px; place-items: center; border: 1px solid rgba(61,214,198,.3); border-radius: 10px; background: rgba(61,214,198,.11); color: var(--teal); } -.brand-mark svg { width: 21px; height: 21px; } -.desktop-nav { display: flex; align-items: center; gap: clamp(14px, 2vw, 29px); } -.desktop-nav a, .nav-github { color: #aeb9c7; font-size: 13px; font-weight: 550; white-space: nowrap; transition: color .18s ease; } -.desktop-nav a:hover, .nav-github:hover { color: #fff; } -.nav-github { justify-self: end; display: inline-flex; align-items: center; gap: 9px; border: 1px solid rgba(61,214,198,.28); border-radius: 999px; background: rgba(61,214,198,.08); padding: 10px 16px; color: #c6f8f1; } -.nav-github span { color: var(--teal); } -.menu-toggle, .mobile-nav { display: none; } -.menu-toggle:focus-visible, a:focus-visible, button:focus-visible { outline: 2px solid #7af2e4; outline-offset: 4px; } - -.hero { position: relative; overflow: hidden; padding: 94px 0 76px; } -.section-grid { background-image: linear-gradient(rgba(255,255,255,.022) 1px, transparent 1px), linear-gradient(90deg,rgba(255,255,255,.022) 1px,transparent 1px); background-size: 68px 68px; background-position: center -1px; } -.hero::after { position: absolute; inset: 0; content: ""; pointer-events: none; background: radial-gradient(ellipse at center 18%, transparent 4%, rgba(7,11,18,.28) 69%, var(--ink) 100%); } -.hero-glow { position: absolute; top: 60px; left: 50%; width: 780px; height: 520px; transform: translateX(-50%); border-radius: 50%; background: rgba(27,159,148,.09); filter: blur(100px); } -.hero-content { position: relative; z-index: 1; } -.hero-copy { max-width: 910px; margin: auto; text-align: center; } -.eyebrow { display: inline-flex; align-items: center; gap: 9px; color: #85d8d0; font-family: var(--mono); font-size: 10px; font-weight: 500; letter-spacing: .14em; line-height: 1.5; text-transform: uppercase; } -.status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--teal); box-shadow: 0 0 12px rgba(61,214,198,.65); } -h1, h2, h3, p { margin-top: 0; } -.hero h1 { margin: 24px 0 20px; font-family: var(--display); font-size: clamp(38px, 5.3vw, 67px); font-weight: 600; letter-spacing: -.065em; line-height: 1.1; } -.hero h1 span, .hosting-copy h2 span, .final-cta h2 span { color: var(--teal); } -.hero-lede { max-width: 740px; margin: 0 auto; color: #9eacbc; font-size: 16px; line-height: 1.78; } -.hero-actions, .hosting-actions { display: flex; justify-content: center; flex-wrap: wrap; gap: 12px; margin-top: 30px; } -.button { min-height: 47px; display: inline-flex; justify-content: center; align-items: center; gap: 11px; border: 1px solid transparent; border-radius: 999px; padding: 0 20px; font-size: 13px; font-weight: 700; transition: background .18s, border-color .18s, transform .18s; } -.button:hover { transform: translateY(-1px); } -.button-primary { background: var(--teal); color: #04201c; box-shadow: 0 7px 28px rgba(61,214,198,.14); } -.button-primary:hover { background: #6de5d9; } -.button-secondary { border-color: rgba(255,255,255,.14); background: rgba(255,255,255,.025); color: #e4eaf0; } -.button-secondary:hover { border-color: rgba(61,214,198,.38); background: rgba(61,214,198,.06); } -.product-attributes { display: flex; justify-content: center; flex-wrap: wrap; gap: 19px; margin: 25px 0 0; padding: 0; list-style: none; color: #8a98a9; font-family: var(--mono); font-size: 10px; } -.product-attributes li { display: flex; align-items: center; gap: 6px; } -.product-attributes li span { color: #4da99f; font-size: 9px; } -.hero-preview { position: relative; min-height: 380px; margin: 61px auto 0; display: grid; grid-template-columns: 1.3fr .77fr; align-items: center; gap: 16px; } -.preview-shadow { position: absolute; top: 15%; left: 14%; width: 72%; height: 70%; border-radius: 50%; background: rgba(25,117,111,.16); filter: blur(68px); } -.window { position: relative; overflow: hidden; border: 1px solid rgba(214,230,241,.13); border-radius: 15px; background: rgba(10,16,24,.96); box-shadow: 0 24px 75px -42px rgba(0,0,0,.95), 0 4px 12px rgba(0,0,0,.18); } -.window-title { min-height: 42px; display: flex; align-items: center; gap: 11px; border-bottom: 1px solid rgba(255,255,255,.07); background: rgba(255,255,255,.018); padding: 0 14px; color: #8190a1; font-family: var(--mono); font-size: 9px; } -.window-title > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.window-title > b { margin-left: auto; flex-shrink: 0; border: 1px solid rgba(61,214,198,.2); border-radius: 4px; padding: 3px 6px; color: #79c9c0; font-size: 8px; font-weight: 500; letter-spacing: .05em; } -.window-dots { display: flex; gap: 5px; flex-shrink: 0; } -.window-dots i { width: 7px; height: 7px; border-radius: 50%; background: #f07178a6; } -.window-dots i:nth-child(2) { background: #e6b450a6; }.window-dots i:nth-child(3) { background: #7fd962a6; } -.terminal-window { z-index: 1; transform: perspective(1400px) rotateY(3deg) rotateX(1deg); } -.terminal-body { min-height: 300px; display: grid; grid-template-columns: 168px 1fr; } -.terminal-sidebar { position: relative; display: flex; flex-direction: column; gap: 12px; border-right: 1px solid rgba(255,255,255,.06); padding: 17px 12px; color: #768598; font-size: 9px; } -.side-label { margin: 0 0 2px 5px; color: #586779; font-family: var(--mono); font-size: 8px; letter-spacing: .11em; } -.side-active { border-radius: 5px; background: rgba(61,214,198,.1); padding: 7px 5px; color: #a6e8e1; } -.side-user { position: absolute; right: 12px; bottom: 13px; left: 12px; display: flex; align-items: center; gap: 7px; border-top: 1px solid rgba(255,255,255,.06); padding-top: 11px; color: #647286; font-size: 8px; } -.user-dot { display: grid; width: 19px; height: 19px; place-items: center; border-radius: 50%; background: #213b3b; color: #8edbd2; font-family: var(--mono); } -.terminal-main { min-width: 0; padding: 17px 16px; font-family: var(--mono); } -.terminal-command { overflow: hidden; color: #cbd5df; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }.terminal-prompt { margin-right: 7px; color: var(--teal); } -.terminal-divider { height: 1px; margin: 14px 0 11px; background: rgba(255,255,255,.07); } -.terminal-step { display: grid; grid-template-columns: 15px minmax(0,1fr) auto; align-items: center; gap: 6px; min-height: 35px; border-bottom: 1px solid rgba(255,255,255,.035); color: #91a0af; font-size: 9px; } -.step-check { color: #54d4c3; }.step-state { color: #5c6c7d; font-size: 8px; white-space: nowrap; } -.scan-progress { position: relative; display: flex; justify-content: space-between; margin-top: 16px; color: #667587; font-size: 8px; letter-spacing: .06em; }.scan-progress i { position: absolute; top: 17px; left: 0; width: 100%; height: 3px; border-radius: 4px; background: linear-gradient(90deg,var(--teal) 0 100%,#17202b 100%); box-shadow: 0 0 8px rgba(61,214,198,.25); } -.metrics-window { z-index: 2; margin-left: -47px; transform: translateY(27px); } -.metrics-content { padding: 17px 16px 19px; }.metric-top { display: flex; align-items: center; justify-content: space-between; }.metric-top > div { display: grid; gap: 5px; }.metric-label, .metric-card span, .bar-row span { color: #6d7a8b; font-family: var(--mono); font-size: 8px; letter-spacing: .07em; }.metric-top strong { color: white; font-family: var(--display); font-size: 36px; letter-spacing: -.06em; }.metric-pill { border: 1px solid rgba(61,214,198,.16); border-radius: 999px; padding: 4px 7px; color: #73bdb5; font-family: var(--mono); font-size: 8px; } -.metric-cards { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; margin: 14px 0 18px; }.metric-card { display: grid; gap: 6px; border: 1px solid rgba(255,255,255,.06); border-radius: 7px; background: rgba(255,255,255,.02); padding: 8px 7px; }.metric-card span { font-size: 7px; letter-spacing: 0; }.metric-card b { color: #dce7ee; font-family: var(--mono); font-size: 14px; font-weight: 500; } -.metric-bars { display: grid; gap: 10px; }.bar-row { display: grid; grid-template-columns: 1fr auto; gap: 4px 8px; align-items: center; }.bar-row b { color: #a2afbd; font-family: var(--mono); font-size: 8px; font-weight: 400; }.bar-row i { grid-column: 1 / -1; height: 3px; overflow: hidden; border-radius: 3px; background: rgba(255,255,255,.06); }.bar-row em { display: block; height: 100%; border-radius: inherit; background: var(--teal); } -.illustrative-note { display: flex; justify-content: center; gap: 7px; margin: 22px auto 0; color: #6f7c8d; font-size: 10px; }.illustrative-note span { color: #69c9c0; } - -.section { border-top: 1px solid rgba(255,255,255,.06); padding: 104px 0; } -.section-muted { background: linear-gradient(180deg, rgba(255,255,255,.014), rgba(255,255,255,.009)); } -.section-heading { max-width: 690px; margin-bottom: 43px; }.section-heading h2, .activity-row h2 { margin: 12px 0 14px; color: #f0f4f7; font-family: var(--display); font-size: clamp(30px, 3.3vw, 43px); font-weight: 550; letter-spacing: -.055em; line-height: 1.16; }.section-heading p, .activity-row p { max-width: 620px; margin: 0; color: var(--muted); font-size: 15px; line-height: 1.75; } -.capability-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 13px; }.capability-card { position: relative; min-height: 208px; border: 1px solid var(--line); border-radius: 13px; background: linear-gradient(145deg,rgba(255,255,255,.033),rgba(255,255,255,.012)); padding: 22px; transition: border-color .2s, transform .2s, background .2s; }.capability-card:hover { transform: translateY(-2px); border-color: rgba(61,214,198,.26); background: linear-gradient(145deg,rgba(61,214,198,.055),rgba(255,255,255,.012)); }.capability-icon { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid rgba(61,214,198,.2); border-radius: 9px; background: rgba(61,214,198,.07); color: #70d8cd; font-family: var(--mono); font-size: 17px; }.card-index { position: absolute; top: 26px; right: 22px; color: #4c5a6c; font-family: var(--mono); font-size: 9px; }.capability-card h3 { margin: 23px 0 8px; color: #e7edf2; font-size: 15px; font-weight: 600; }.capability-card p, .workflow-card p { margin: 0; color: #8997a8; font-size: 12px; line-height: 1.7; } -.split-layout { display: grid; grid-template-columns: .86fr 1.14fr; align-items: center; gap: 70px; }.split-layout .section-heading { margin-bottom: 19px; }.split-layout .section-heading h2 { font-size: clamp(30px, 3vw, 39px); }.reverse-on-wide { grid-template-columns: 1.05fr .95fr; }.reverse-on-wide > :first-child { order: 1; }.reverse-on-wide > :last-child { order: 2; }.body-copy { color: #94a1b1; font-size: 13px; line-height: 1.8; }.check-list { display: grid; gap: 12px; margin: 25px 0 0; padding: 0; list-style: none; color: #b2bdc9; font-size: 12px; }.check-list li { display: flex; align-items: center; gap: 10px; }.check-list li::before { content: "✓"; color: var(--teal); font-family: var(--mono); } -.scan-window, .upgrade-window, .migration-window, .insight-window { width: 100%; }.scan-timeline { padding: 18px 21px 10px; }.scan-line { min-height: 43px; display: grid; grid-template-columns: 28px 1fr auto; align-items: center; gap: 10px; border-bottom: 1px solid rgba(255,255,255,.045); }.scan-line > span { color: #59706f; font-family: var(--mono); font-size: 9px; }.scan-line b { color: #c4ced7; font-size: 11px; font-weight: 500; }.scan-line em { color: #76cfc5; font-family: var(--mono); font-size: 8px; font-style: normal; }.scan-footer { display: flex; align-items: center; gap: 8px; border-top: 1px solid rgba(255,255,255,.06); background: rgba(255,255,255,.015); padding: 13px 20px; color: #728192; font-size: 9px; } -.upgrade-body { padding: 18px; }.package-select { display: grid; gap: 8px; }.package-select > span, .version-compare span, .security-summary span { color: #68778a; font-family: var(--mono); font-size: 8px; letter-spacing: .08em; }.package-select b { display: flex; justify-content: space-between; border: 1px solid rgba(255,255,255,.08); border-radius: 7px; background: rgba(255,255,255,.02); padding: 10px 12px; color: #e1eaf0; font-family: var(--mono); font-size: 11px; font-weight: 400; }.package-select i { color: #76879a; font-style: normal; }.version-compare { display: grid; grid-template-columns: 1fr 24px 1fr; align-items: center; gap: 10px; margin: 17px 0; border: 1px solid rgba(255,255,255,.06); border-radius: 8px; background: rgba(255,255,255,.015); padding: 12px; }.version-compare div { display: grid; gap: 7px; }.version-compare strong { color: #d7e1ea; font-family: var(--mono); font-size: 13px; font-weight: 500; }.compare-arrow { color: var(--teal); text-align: center; }.evidence-box { display: grid; gap: 8px; }.evidence-box > div { display: grid; grid-template-columns: 20px 1fr; align-items: center; gap: 2px 7px; border: 1px solid rgba(255,255,255,.055); border-radius: 7px; padding: 9px; }.evidence-icon { grid-row: span 2; color: #72d6cb; font-family: var(--mono); }.muted-icon { color: #a6ad74; }.evidence-box b { color: #c7d0da; font-size: 10px; font-weight: 550; }.evidence-box small { color: #718092; font-size: 9px; }.risk-note { display: flex; gap: 9px; margin-top: 12px; border: 1px solid rgba(230,180,80,.16); border-radius: 7px; background: rgba(230,180,80,.04); padding: 10px; }.risk-note > span { color: #e6b450; font-family: var(--mono); font-size: 11px; }.risk-note p { margin: 0; color: #a5a08a; font-size: 9px; line-height: 1.55; }.demo-tag { display: inline-flex; border: 1px solid rgba(61,214,198,.18); border-radius: 999px; padding: 7px 10px; color: #77c3bb; font-family: var(--mono); font-size: 8px; } -.migration-content { padding: 17px; }.migration-context { display: grid; gap: 7px; }.migration-context > span { color: #68bfb6; font-family: var(--mono); font-size: 8px; letter-spacing: .06em; }.migration-context b { color: #d9e2e9; font-size: 11px; font-weight: 550; }.migration-context small { color: #718092; font-size: 9px; }.code-diff { overflow: auto; margin-top: 15px; border: 1px solid rgba(255,255,255,.06); border-radius: 7px; background: #080d13; padding: 9px 0; }.code-diff > div { min-width: max-content; display: flex; gap: 12px; padding: 7px 12px; color: #bcbfc3; font-family: var(--mono); font-size: 9px; }.code-diff > div span { color: #eb8589; }.code-diff .diff-add { background: rgba(61,214,198,.08); color: #b4e6df; }.code-diff .diff-add span { color: #64dacb; }.code-diff code { font: inherit; }.migration-explain { display: flex; gap: 10px; margin-top: 12px; border: 1px solid rgba(61,214,198,.12); border-radius: 8px; background: rgba(61,214,198,.035); padding: 11px; }.ai-spark { color: var(--teal); }.migration-explain p { margin: 0; color: #8897a7; font-size: 9px; line-height: 1.6; }.migration-explain b { color: #d0e2df; font-weight: 550; }.migration-foot { display: flex; justify-content: space-between; gap: 8px; margin-top: 14px; color: #68798a; font-family: var(--mono); font-size: 8px; }.migration-foot span:last-child { color: #72cfc5; } -.insights-grid { display: grid; grid-template-columns: 1.07fr .93fr; align-items: stretch; gap: 15px; }.insight-window { display: flex; flex-direction: column; }.graph-canvas { padding: 15px 12px 6px; }.graph-canvas svg { width: 100%; overflow: visible; }.graph-edges path { fill: none; stroke: rgba(61,214,198,.27); stroke-width: 1.2; }.graph-node rect { fill: #121c27; stroke: rgba(255,255,255,.14); }.graph-node text { fill: #bec9d3; font: 10px var(--mono); }.graph-node.root rect { fill: #1b4743; stroke: rgba(61,214,198,.65); }.graph-node.root text { fill: #bdf6ee; }.graph-node.risk rect { fill: #321d23; stroke: rgba(240,113,120,.58); }.graph-node.risk text { fill: #f09ca0; }.graph-legend { display: flex; justify-content: center; flex-wrap: wrap; gap: 17px; color: #788697; font-family: var(--mono); font-size: 8px; }.graph-legend span { display: inline-flex; align-items: center; gap: 6px; }.graph-legend i { width: 7px; height: 7px; border: 1px solid #8392a2; border-radius: 50%; }.graph-legend .legend-root { border-color: var(--teal); background: #358b81; }.graph-legend .legend-risk { border-color: #e27c82; background: #8c4247; }.card-caption { margin: auto 0 0; border-top: 1px solid rgba(255,255,255,.06); padding: 13px 16px; color: #69788a; font-size: 9px; line-height: 1.5; }.security-body { display: grid; gap: 14px; padding: 17px; }.security-summary { display: grid; grid-template-columns: repeat(3,1fr); gap: 7px; }.security-summary > div { display: grid; gap: 7px; border: 1px solid rgba(255,255,255,.06); border-radius: 7px; background: rgba(255,255,255,.018); padding: 9px; }.security-summary b { color: #d5dfe7; font-family: var(--mono); font-size: 10px; font-weight: 500; }.security-summary .severity { color: #f18d93; }.finding-card { border: 1px solid rgba(240,113,120,.18); border-radius: 8px; background: rgba(240,113,120,.025); padding: 12px; }.finding-card > div:first-child { display: flex; align-items: center; gap: 7px; }.finding-dot { width: 6px; height: 6px; border-radius: 50%; background: #ed777f; box-shadow: 0 0 8px #ed777f88; }.finding-card b { color: #d8dfe5; font-size: 10px; font-weight: 550; }.finding-id { margin-left: auto; color: #8e7378; font-family: var(--mono); font-size: 8px; }.finding-card p { margin: 10px 0 14px; color: #9a8990; font-size: 9px; line-height: 1.5; }.range-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; color: #788596; font-size: 8px; }.range-row code { color: #dfadb0; font-family: var(--mono); font-size: 8px; }.range-line { width: 25px; height: 1px; background: #b86f76; }.security-foot { color: #718092; font-size: 9px; line-height: 1.5; } -.ecosystem-layout { display: grid; grid-template-columns: 1.04fr .96fr; align-items: center; gap: 78px; }.ecosystem-layout .section-heading { margin-bottom: 17px; }.text-link { display: inline-flex; gap: 7px; margin-top: 10px; color: #7dd8cf; font-size: 12px; font-weight: 550; }.text-link:hover { color: #b3f3ec; }.ecosystem-card { border: 1px solid var(--line); border-radius: 12px; background: rgba(10,16,24,.72); padding: 20px 22px 17px; }.ecosystem-card-head { display: flex; justify-content: space-between; border-bottom: 1px solid rgba(255,255,255,.065); padding-bottom: 14px; color: #728193; font-family: var(--mono); font-size: 8px; letter-spacing: .1em; }.ecosystem-card-head .mono { color: #506171; }.ecosystem-card ul { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; margin: 5px 0 0; padding: 0; list-style: none; }.ecosystem-card li { display: grid; grid-template-columns: 23px 1fr 12px; align-items: center; gap: 8px; min-height: 47px; border-bottom: 1px solid rgba(255,255,255,.045); color: #c4ced7; font-size: 11px; }.ecosystem-number { color: #526274; font-family: var(--mono); font-size: 8px; }.ecosystem-arrow { color: #5cb7ae; font-size: 10px; }.ecosystem-card > p { margin: 13px 0 0; color: #718092; font-size: 9px; line-height: 1.5; } -.workflow-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 13px; }.workflow-card { position: relative; min-height: 190px; border: 1px solid var(--line); border-radius: 11px; background: rgba(255,255,255,.018); padding: 20px; }.workflow-num { color: #66cfc4; font-family: var(--mono); font-size: 11px; }.workflow-connector { position: absolute; top: 27px; right: -14px; z-index: 2; width: 14px; height: 1px; background: rgba(61,214,198,.38); }.workflow-card:last-child .workflow-connector { display: none; }.workflow-card h3 { margin: 24px 0 9px; color: #e0e8ee; font-size: 14px; font-weight: 600; } -.hosting-section { padding-block: 90px; }.hosting-panel { position: relative; overflow: hidden; display: grid; grid-template-columns: 1.15fr .85fr; align-items: center; gap: 60px; border: 1px solid rgba(61,214,198,.19); border-radius: 17px; background: linear-gradient(112deg,#0c171c 0%,#0b121a 60%,#0b141b 100%); padding: 57px 57px; }.hosting-orbit { position: absolute; top: -220px; right: -120px; width: 520px; height: 520px; border: 1px solid rgba(61,214,198,.08); border-radius: 50%; box-shadow: 0 0 0 43px rgba(61,214,198,.022), 0 0 0 90px rgba(61,214,198,.018); pointer-events: none; }.hosting-copy, .hosting-facts { position: relative; }.hosting-copy h2 { margin: 19px 0 15px; font-family: var(--display); font-size: clamp(35px, 4vw, 49px); font-weight: 550; letter-spacing: -.06em; line-height: 1.12; }.hosting-copy > p { max-width: 490px; margin: 0; color: #98a6b4; font-size: 13px; line-height: 1.8; }.hosting-actions { justify-content: flex-start; margin-top: 24px; }.hosting-actions .button { font-size: 11px; padding-inline: 16px; }.hosting-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }.hosting-fact { min-height: 91px; display: flex; flex-direction: column; justify-content: center; gap: 10px; border: 1px solid rgba(255,255,255,.075); border-radius: 9px; background: rgba(5,10,15,.25); padding: 14px; }.hosting-fact span { color: #78899a; font-family: var(--mono); font-size: 8px; letter-spacing: .04em; }.hosting-fact b { color: #d8e5e8; font-size: 12px; font-weight: 550; }.license-note { display: grid; grid-template-columns: 32px 1fr auto; align-items: start; gap: 14px; margin-top: 16px; border: 1px solid rgba(255,255,255,.065); border-radius: 10px; background: rgba(255,255,255,.015); padding: 18px 20px; }.license-mark { display: grid; width: 27px; height: 27px; place-items: center; border-radius: 7px; background: rgba(61,214,198,.08); color: #74cfc5; font-family: var(--mono); }.license-note p { margin: 0; color: #8795a5; font-size: 10px; line-height: 1.7; }.license-note code { color: #b4c3cf; font-family: var(--mono); font-size: 9px; }.license-note > div { display: flex; gap: 14px; white-space: nowrap; }.license-note a { color: #7bd2c9; font-size: 9px; } -.activity-row { display: grid; grid-template-columns: .85fr 1.15fr; align-items: center; gap: 72px; }.activity-row h2 { font-size: clamp(28px,3vw,38px); }.activity-row p { font-size: 13px; }.activity-links { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }.activity-link { display: flex; justify-content: space-between; align-items: center; min-height: 65px; border: 1px solid rgba(255,255,255,.07); border-radius: 9px; background: rgba(255,255,255,.018); padding: 12px 14px; transition: border-color .18s, background .18s; }.activity-link:hover { border-color: rgba(61,214,198,.28); background: rgba(61,214,198,.035); }.activity-link > span { display: grid; gap: 5px; }.activity-link b { color: #dbe3eb; font-size: 11px; font-weight: 550; }.activity-link small { color: #728193; font-size: 9px; }.activity-link i { color: #62c7bd; font-style: normal; } -.faq-layout { display: grid; grid-template-columns: .75fr 1.25fr; align-items: start; gap: 78px; }.faq-layout .section-heading { position: sticky; top: 115px; }.faq-list { border-top: 1px solid rgba(255,255,255,.09); }.faq-item { border-bottom: 1px solid rgba(255,255,255,.09); }.faq-item h3 { margin: 0; }.faq-item button { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 20px; border: 0; background: transparent; padding: 18px 0; color: #d8e1e8; text-align: left; font-size: 13px; font-weight: 550; cursor: pointer; }.faq-item button:hover { color: #fff; }.faq-plus { color: #6ed0c6; font-family: var(--mono); font-size: 18px; font-weight: 400; }.faq-answer p { max-width: 620px; margin: -2px 30px 19px 0; color: #8b99a9; font-size: 12px; line-height: 1.8; } -.final-cta { position: relative; overflow: hidden; border-top: 1px solid rgba(255,255,255,.06); background: #080e14; padding: 92px 0 100px; text-align: center; }.final-cta-inner { position: relative; }.cta-glow { position: absolute; top: -120px; left: 50%; z-index: 0; width: 540px; height: 320px; transform: translateX(-50%); border-radius: 50%; background: rgba(35,144,134,.12); filter: blur(90px); pointer-events: none; }.final-cta .eyebrow, .final-cta h2, .final-cta p, .final-cta .hero-actions { position: relative; }.final-cta h2 { margin: 18px 0 13px; font-family: var(--display); font-size: clamp(35px,4.4vw,52px); font-weight: 550; letter-spacing: -.06em; line-height: 1.14; }.final-cta p { margin: 0 auto; color: #97a4b3; font-size: 14px; line-height: 1.7; }.final-cta .hero-actions { margin-top: 25px; } -.footer { border-top: 1px solid rgba(255,255,255,.075); background: #05080d; padding: 56px max(32px,calc((100% - 1160px)/2)) 22px; }.footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1.25fr 1fr; gap: 40px; }.footer-brand-col > p { max-width: 210px; margin: 15px 0 14px; color: #788697; font-size: 11px; line-height: 1.7; }.footer-license { color: #637284; font-family: var(--mono); font-size: 8px; }.footer-column { display: flex; flex-direction: column; align-items: flex-start; gap: 11px; }.footer-column h2 { margin: 2px 0 6px; color: #d5dfe7; font-size: 11px; font-weight: 600; }.footer-column a { color: #7d8b9b; font-size: 10px; line-height: 1.5; transition: color .18s; }.footer-column a:hover { color: #a9eee6; }.footer-column a span { color: #4fa79f; }.footer-bottom { display: flex; justify-content: space-between; gap: 15px; margin-top: 46px; border-top: 1px solid rgba(255,255,255,.06); padding-top: 17px; color: #576577; font-size: 9px; } - -@media (max-width: 1100px) { - .nav-inner { grid-template-columns: 1fr auto auto; gap: 19px; }.desktop-nav { gap: 15px; }.desktop-nav a { font-size: 12px; }.container { width: min(100% - 56px, 1000px); }.split-layout { gap: 42px; }.hosting-panel { gap: 32px; padding: 42px; }.ecosystem-layout, .faq-layout { gap: 46px; } -} -@media (max-width: 850px) { - .nav-inner { width: calc(100% - 44px); height: 68px; grid-template-columns: 1fr auto auto; }.desktop-nav { display: none; }.nav-github { grid-column: 2; }.menu-toggle { grid-column: 3; width: 40px; height: 40px; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 5px; border: 1px solid rgba(255,255,255,.12); border-radius: 9px; background: transparent; cursor: pointer; }.menu-toggle span { width: 16px; height: 1.5px; background: #cbd7df; }.mobile-nav:not([hidden]) { display: grid; gap: 2px; border-top: 1px solid rgba(255,255,255,.07); background: #080d14; padding: 9px 22px 15px; }.mobile-nav a { min-height: 43px; display: flex; align-items: center; border-radius: 6px; padding: 0 11px; color: #aab6c3; font-size: 13px; }.mobile-nav a:hover { background: rgba(255,255,255,.04); color: white; }.container { width: calc(100% - 48px); }.hero { padding-top: 75px; }.hero-preview { min-height: unset; grid-template-columns: 1.18fr .82fr; }.terminal-body { grid-template-columns: 130px 1fr; }.terminal-sidebar { font-size: 8px; }.metrics-window { margin-left: -30px; }.section { padding-block: 78px; }.split-layout, .ecosystem-layout { grid-template-columns: 1fr 1fr; gap: 28px; }.section-heading h2 { font-size: 34px; }.capability-card { min-height: 225px; padding: 18px; }.hosting-panel { grid-template-columns: 1fr; gap: 30px; }.hosting-facts { grid-template-columns: repeat(4,1fr); }.hosting-fact { min-height: 77px; padding: 10px; }.hosting-fact b { font-size: 10px; }.license-note { grid-template-columns: 30px 1fr; }.license-note > div { grid-column: 2; }.activity-row { grid-template-columns: 1fr 1.1fr; gap: 32px; }.faq-layout { grid-template-columns: .7fr 1.3fr; gap: 35px; }.footer { padding-inline: 28px; } -} -@media (max-width: 620px) { - html { scroll-padding-top: 72px; }.container { width: calc(100% - 40px); }.nav-inner { width: calc(100% - 36px); height: 64px; }.brand { font-size: 16px; }.brand-mark { width: 31px; height: 31px; }.nav-github { padding: 9px 12px; font-size: 11px; }.menu-toggle { width: 37px; height: 37px; }.hero { padding: 63px 0 58px; }.hero h1 { margin-top: 22px; font-size: clamp(34px,9.2vw,48px); letter-spacing: -.06em; }.hero-lede { font-size: 13px; line-height: 1.75; }.hero-actions { gap: 9px; margin-top: 23px; }.button { min-height: 44px; padding-inline: 15px; font-size: 11px; }.product-attributes { gap: 11px 13px; margin-top: 19px; font-size: 8px; }.hero-preview { display: flex; min-height: unset; flex-direction: column; align-items: stretch; gap: 13px; margin-top: 39px; }.terminal-window { transform: none; }.terminal-body { min-height: 250px; grid-template-columns: 1fr; }.terminal-sidebar { display: none; }.terminal-main { padding: 14px 12px; }.terminal-step { min-height: 34px; grid-template-columns: 14px minmax(0,1fr) auto; font-size: 8px; }.step-state { font-size: 7px; }.metrics-window { width: 84%; align-self: flex-end; margin: -1px 2px 0 0; transform: none; }.metrics-content { padding: 12px; }.metric-top strong { font-size: 31px; }.illustrative-note { margin-top: 16px; font-size: 9px; text-align: center; } - .section { padding-block: 68px; }.section-heading { margin-bottom: 30px; }.section-heading h2, .split-layout .section-heading h2 { font-size: 30px; }.section-heading p { font-size: 13px; }.capability-grid { grid-template-columns: 1fr; gap: 9px; }.capability-card { min-height: unset; padding: 17px; }.capability-card h3 { margin-top: 17px; }.capability-card p { font-size: 11px; }.split-layout, .ecosystem-layout, .activity-row, .faq-layout { grid-template-columns: 1fr; gap: 31px; }.split-layout > div:first-child { order: 0; }.reverse-on-wide > :first-child { order: 1; }.reverse-on-wide > :last-child { order: 0; }.scan-line { min-height: 41px; grid-template-columns: 22px 1fr auto; gap: 7px; }.scan-line b { font-size: 9px; }.scan-line em { font-size: 7px; }.scan-timeline { padding: 14px 12px 7px; }.upgrade-body, .migration-content { padding: 13px; }.insights-grid { grid-template-columns: 1fr; gap: 12px; }.graph-canvas { padding-inline: 5px; }.graph-canvas svg { min-height: 180px; }.ecosystem-card ul { gap: 0 10px; }.ecosystem-card { padding: 16px; }.workflow-grid { grid-template-columns: 1fr 1fr; gap: 9px; }.workflow-card { min-height: 170px; padding: 15px; }.workflow-connector { display: none; }.workflow-card h3 { margin-top: 20px; font-size: 12px; }.workflow-card p { font-size: 10px; }.hosting-section { padding-block: 60px; }.hosting-panel { gap: 25px; border-radius: 13px; padding: 26px 20px; }.hosting-copy h2 { font-size: 36px; }.hosting-copy > p { font-size: 12px; }.hosting-actions { align-items: stretch; flex-direction: column; }.hosting-actions .button { width: 100%; }.hosting-facts { grid-template-columns: 1fr 1fr; }.hosting-fact { min-height: 72px; }.license-note { gap: 10px; padding: 14px; }.license-note p { font-size: 9px; }.license-note > div { flex-wrap: wrap; gap: 10px 14px; }.activity-row { gap: 20px; }.activity-links { gap: 7px; }.activity-link { min-height: 62px; padding: 10px; }.activity-link b { font-size: 10px; }.activity-link small { font-size: 8px; }.faq-layout { gap: 17px; }.faq-layout .section-heading { position: static; margin-bottom: 9px; }.faq-item button { padding: 16px 0; font-size: 12px; }.faq-answer p { font-size: 11px; }.final-cta { padding: 69px 0 74px; }.final-cta h2 { font-size: 36px; }.final-cta p { font-size: 12px; }.final-cta .hero-actions { flex-direction: column; align-items: stretch; }.footer { padding: 39px 20px 18px; }.footer-grid { grid-template-columns: 1fr 1fr; gap: 30px 18px; }.footer-brand-col { grid-column: 1 / -1; }.footer-bottom { flex-direction: column; gap: 8px; margin-top: 32px; font-size: 8px; } -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } + --background: #0c1117; + --foreground: #e8eef6; + --card: #121a24; + --card-foreground: #e8eef6; + --popover: #121a24; + --popover-foreground: #e8eef6; + --primary: #3dd6c6; + --primary-foreground: #04201c; + --secondary: #1a2433; + --secondary-foreground: #c5d0de; + --muted: #1a2433; + --muted-foreground: #8b9bb0; + --accent: #243044; + --accent-foreground: #e8eef6; + --destructive: #f07178; + --destructive-foreground: #1a0a0b; + --border: #243044; + --input: #243044; + --ring: #3dd6c6; + --success: #7fd962; + --warning: #e6b450; + --chart-1: #3dd6c6; + --chart-2: #6c9eff; + --chart-3: #e6b450; + --chart-4: #f07178; + --chart-5: #c3a6ff; + --radius: 0.625rem; + --sidebar: #0a0f14; + --font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif; + --font-display: "Syne", ui-sans-serif, system-ui, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; +} + +.light { + --background: #f4f7fb; + --foreground: #0c1117; + --card: #ffffff; + --card-foreground: #0c1117; + --popover: #ffffff; + --popover-foreground: #0c1117; + --primary: #0d9488; + --primary-foreground: #f0fdfa; + --secondary: #e8eef6; + --secondary-foreground: #1a2433; + --muted: #e8eef6; + --muted-foreground: #5a6b80; + --accent: #dce6f2; + --accent-foreground: #0c1117; + --destructive: #dc2626; + --destructive-foreground: #fff; + --border: #d0dae8; + --input: #d0dae8; + --ring: #0d9488; + --sidebar: #eef3f9; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-success: var(--success); + --color-warning: var(--warning); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); +} + +* { + border-color: var(--border); +} + +html { + color-scheme: dark; +} + +body { + background: + radial-gradient(1200px 600px at 10% -10%, rgba(61, 214, 198, 0.12), transparent 55%), + radial-gradient(900px 500px at 90% 0%, rgba(108, 158, 255, 0.10), transparent 50%), + var(--background); + color: var(--foreground); + font-family: var(--font-sans); + min-height: 100vh; +} + +.light body { + background: + radial-gradient(1200px 600px at 10% -10%, rgba(13, 148, 136, 0.08), transparent 55%), + radial-gradient(900px 500px at 90% 0%, rgba(59, 130, 246, 0.06), transparent 50%), + var(--background); +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse-soft { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.55; } +} + +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.animate-fade-up { + animation: fade-up 0.45s ease-out both; +} + +.animate-pulse-soft { + animation: pulse-soft 2s ease-in-out infinite; +} + +.skeleton-shimmer { + background-image: linear-gradient( + 90deg, + transparent 0%, + color-mix(in oklab, var(--foreground) 8%, transparent) 50%, + transparent 100% + ); + background-size: 200% 100%; + animation: shimmer 1.6s ease-in-out infinite; +} + +/* Instant click affordance for in-app navigation */ +.nav-interactive { + transition: + transform 110ms cubic-bezier(0.2, 0.8, 0.2, 1), + opacity 110ms ease, + background-color 140ms ease, + border-color 140ms ease; + will-change: transform, opacity; + transform-origin: center; + -webkit-tap-highlight-color: transparent; +} + +.nav-interactive:active, +.nav-interactive-pressed { + transform: scale(0.985); + opacity: 0.82; +} + +.nav-interactive-pending { + pointer-events: none !important; + cursor: default; + opacity: 0.68; + transform: scale(0.992); +} + +.nav-interactive-card:active, +.nav-interactive-card.nav-interactive-pressed, +.nav-interactive-card.nav-interactive-pending { + border-color: color-mix(in oklab, var(--primary) 35%, var(--border)); +} + +.nav-interactive-row:active, +.nav-interactive-row.nav-interactive-pressed { + background-color: color-mix(in oklab, var(--accent) 70%, transparent); +} + +.nav-interactive-nav:active, +.nav-interactive-nav.nav-interactive-pressed { + background-color: color-mix(in oklab, var(--primary) 12%, transparent); + transform: scale(0.99); +} + +.nav-interactive-nav.nav-interactive-pending { + transform: none; + opacity: 0.75; } diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index 9048aed..6f27634 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -1,194 +1,30 @@ -import Link from "next/link"; -import { Faq } from "@/components/faq"; -import { SiteFooter } from "@/components/site-footer"; -import { SiteNav } from "@/components/site-nav"; - -const repository = "https://github.com/ayushdoesdev/verspect"; -const install = `${repository}#quick-start-with-docker-compose`; - -const capabilities = [ - { icon: "↗", name: "Dependency intelligence", text: "Explore direct and transitive dependencies, package inventory, and relationships revealed by supported manifests and lockfiles." }, - { icon: "⌁", name: "Security analysis", text: "Match package versions with OSV advisories and review severity and affected-version evidence where available." }, - { icon: "§", name: "License analysis", text: "Inspect package license information and policy concerns, with gaps shown when source or registry metadata is incomplete." }, - { icon: "◷", name: "Package health", text: "Review signals such as release freshness and registry activity when the underlying metadata is available." }, - { icon: "⇄", name: "Upgrade simulator", text: "Compare current and target versions against detected usage and available upgrade rules. Findings depend on the evidence present." }, - { icon: "⌘", name: "Migration assistant", text: "Use structured rules and optional AI-assisted suggestions or patch drafts. Review and test every proposed change." }, -]; - -const ecosystems = ["Python", "JavaScript / TypeScript", "Go", "Java", "Rust", "PHP", "Ruby"]; +import { MarketingFooter, MarketingNav } from "@/components/marketing/nav"; +import { LandingHero } from "@/components/marketing/landing/hero"; +import { ScanDemoSection } from "@/components/marketing/landing/scan-demo"; +import { SimulatorDemoSection } from "@/components/marketing/landing/simulator-demo"; +import { MigrationDemoSection } from "@/components/marketing/landing/migration-demo"; +import { DependencyGraphSection } from "@/components/marketing/landing/dependency-graph"; +import { SecurityDashboardSection } from "@/components/marketing/landing/security-dashboard"; +import { ProductGallerySection } from "@/components/marketing/landing/product-gallery"; +import { FaqSection, FinalCtaSection, RoadmapTeaserSection } from "@/components/marketing/landing/social-sections"; export default function Home() { - return <> - Skip to content - -
-
-
-
-
-
Open-source dependency intelligence
-

Know your dependencies.
Understand your risks.
Upgrade with confidence.

-

Dependency intelligence for GitHub repositories. Discover what your software depends on, understand security and license risks, explore package health, and evaluate upgrade impact with evidence-led analysis and AI-assisted migration guidance.

- -
    -
  • ◈ Open Source
  • ◈ Apache-2.0
  • ◈ Self-hosted
  • ◈ Docker Compose
  • -
-
- -

Illustrative product preview · example repository data, not a live scan

-
-
- -
-
- -
- {capabilities.map((item, i) =>
- 0{i + 1} -

{item.name}

{item.text}

-
)} -
-
-
- -
-
-
-
  • Find supported manifests and lockfiles
  • Build inventory and dependency relationships
  • Map imports and usage evidence by ecosystem
  • Enrich with vulnerability, license, and health data
-
- -
-
- -
-
- -
-

A version jump is not automatically labeled breaking. The simulator reports what its rules and repository evidence support, and leaves uncertainty visible.

- Illustrative example · no live compatibility claim -
-
-
- -
-
-
-

Deterministic migration knowledge is currently deepest for selected Python packages. AI-generated guidance and patch drafts are suggestions: review, run, and test them before adoption.

-
-
-
- -
-
- -
-
-
- -
-
-
-

Python has the deepest deterministic usage and migration analysis. Other ecosystems provide varying levels of inventory, import or module usage, vulnerability, license, and health analysis. Full API-level and language-specific breaking-change analysis remains limited.

- Read the language support details -
-
SUPPORTED INPUTS01 — 07
    {ecosystems.map((name, index) =>
  • 0{index + 1}{name}↗
  • )}

Capabilities vary by ecosystem and available repository evidence.

-
-
- -
-
-
{[ - ["01", "Self-host", "Run Verspect on infrastructure you control with Docker Compose."], - ["02", "Connect GitHub", "Configure OAuth, then choose a repository to import."], - ["03", "Review evidence", "Scan dependencies and inspect inventory, usage, and findings."], - ["04", "Plan an upgrade", "Evaluate impact and review migration recommendations."], - ].map(([num, title, text]) =>
{num}
)}
-
-
- -
-
- -
§

The current main-branch source uses Apache-2.0. The original v0.1.0-beta.1 release remains under AGPL-3.0-or-later. Third-party dependencies keep their respective licenses. Verspect name, logo, and official branding are reserved; Apache-2.0 does not grant trademark rights.

-
-
- -
-
Project activity

Follow the project as it develops.

Explore releases, report an issue, contribute a change, or review the security policy.

- -
-
- -
-
-
- -
-
Explore the source

Explore the source.
Understand your dependencies.

Explore Verspect, inspect its implementation, and run it on your own infrastructure.

-
-
- - ; -} - -function SectionHeading({ eyebrow, title, description }: { eyebrow: string; title: string; description: string }) { - return
{eyebrow}

{title}

{description}

; -} - -function HeroPreview() { - return
-
-
- -
WORKSPACE◉   service-api⌕   Dependencies◇   Security▦   Upgrades
vlocal workspace
-
$ verspect scan service-api
- {["Repository import complete", "Manifest and lockfile discovery", "Dependency indexing", "Usage evidence collected", "OSV, license & health enrichment"].map((line, i) =>
{i < 5 ? "✓" : "·"}{line}{["READY", "12 FILES", "186 PKGS", "94 FILES", "READY"][i]}
)} -
EXAMPLE WORKFLOWCOMPLETE
-
-
+ return ( +
+ +
+ + + + + + + + + + +
+
-
DEPENDENCY INVENTORY186
example state
-
; -} - -function ScanPreview() { - const lines = [["01", "Repository imported", "done"], ["02", "Manifests & lockfiles found", "6 files"], ["03", "Dependency inventory built", "186 packages"], ["04", "Usage evidence indexed", "94 files"], ["05", "OSV and registry enrichment", "example"]]; - return
{lines.map(([num, text, status]) =>
{num}{text}{status}
)}
Example scan flow · no live connection
; -} - -function UpgradePreview() { - return
PACKAGEfastapi ⌄
CURRENT VERSION0.110.0
→
TARGET VERSION0.115.0
✓Usage evidenceImports found in 8 files
◌Migration rulesReview available package notes
i

Example only. Compatibility and impact depend on repository evidence and available rules.

; -} - -function MigrationPreview() { - return
PYTHON · SELECTED PACKAGE RULEExample configuration migrationEvidence: configuration usage found · rule availability varies
−from package import LegacyClient
+from package import Client
client = Client(timeout=30)
✳

Suggested draft
Review API compatibility and validate this change against the target release.

Human review requiredSave patch draft  →
; -} - -function GraphPreview() { - return
repository dependency finding example

Illustrative graph · relationships depend on available manifests and lockfiles.

; -} - -function GraphNode({ x, y, label, kind = "normal" }: { x: number; y: number; label: string; kind?: string }) { - return {label}; -} - -function SecurityPreview() { - return
PACKAGEexample-lib
INSTALLED1.2.0
SEVERITYHigh
Advisory matchOSV · illustrative

Version range and ecosystem are checked against vulnerability data.

Installed version1.2.0Fixed versioncheck source
A lookup result is evidence, not a guarantee of a clean dependency.
; -} - -function WindowTitle({ title, tag }: { title: string; tag?: string }) { - return
{title}{tag && {tag}}
; + ); } - -function Metric({ label, value }: { label: string; value: string }) { return
{label}{value}
; } -function Bar({ label, value, width }: { label: string; value: string; width: string }) { return
{label}{value}
; } -function Fact({ label, value }: { label: string; value: string }) { return
{label}{value}
; } -function ActivityLink({ href, label, detail }: { href: string; label: string; detail: string }) { return {label}{detail}; } diff --git a/landing/src/components/marketing/landing/dependency-graph.tsx b/landing/src/components/marketing/landing/dependency-graph.tsx new file mode 100644 index 0000000..6f9901b --- /dev/null +++ b/landing/src/components/marketing/landing/dependency-graph.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { motion, useReducedMotion } from "framer-motion"; +import { + FadeIn, + Section, + WindowChrome, +} from "@/components/marketing/landing/primitives"; + +type Node = { + id: string; + label: string; + x: number; + y: number; + kind: "root" | "direct" | "transitive" | "vuln"; +}; + +const nodes: Node[] = [ + { id: "app", label: "payments-api", x: 280, y: 160, kind: "root" }, + { id: "django", label: "django", x: 120, y: 70, kind: "direct" }, + { id: "celery", label: "celery", x: 440, y: 70, kind: "direct" }, + { id: "pydantic", label: "pydantic", x: 90, y: 220, kind: "vuln" }, + { id: "requests", label: "requests", x: 470, y: 220, kind: "direct" }, + { id: "urllib3", label: "urllib3", x: 520, y: 300, kind: "transitive" }, + { id: "sqlalchemy", label: "sqlalchemy", x: 280, y: 300, kind: "direct" }, + { id: "greenlet", label: "greenlet", x: 200, y: 360, kind: "transitive" }, +]; + +const edges: Array<[string, string]> = [ + ["app", "django"], + ["app", "celery"], + ["app", "pydantic"], + ["app", "requests"], + ["app", "sqlalchemy"], + ["requests", "urllib3"], + ["sqlalchemy", "greenlet"], + ["celery", "requests"], +]; + +const kindStyle: Record = { + root: { fill: "#3dd6c6", ring: "rgba(61,214,198,0.45)", text: "#04201c" }, + direct: { fill: "#1a2433", ring: "rgba(255,255,255,0.16)", text: "#e8eef6" }, + transitive: { fill: "#121a24", ring: "rgba(255,255,255,0.10)", text: "#8b9bb0" }, + vuln: { fill: "#3a1a1d", ring: "rgba(240,113,120,0.45)", text: "#f07178" }, +}; + +export function DependencyGraphSection() { + const reduce = useReducedMotion(); + const [active, setActive] = useState("pydantic"); + const byId = useMemo(() => Object.fromEntries(nodes.map((n) => [n.id, n])), []); + + const connected = useMemo(() => { + if (!active) return new Set(); + const set = new Set([active]); + edges.forEach(([a, b]) => { + if (a === active || b === active) { + set.add(a); + set.add(b); + } + }); + return set; + }, [active]); + + return ( +
+ + + + + +
+ } + > +
+ + {edges.map(([a, b], i) => { + const na = byId[a]; + const nb = byId[b]; + const lit = + !active || (connected.has(a) && connected.has(b)); + return ( + + ); + })} + {nodes.map((n, i) => { + const style = kindStyle[n.kind]; + const lit = !active || connected.has(n.id); + return ( + setActive(n.id)} + onMouseLeave={() => setActive("pydantic")} + onFocus={() => setActive(n.id)} + onBlur={() => setActive("pydantic")} + tabIndex={0} + role="button" + aria-label={n.label} + > + + + {n.label} + + + ); + })} + + {active && ( +
+
{byId[active].label}
+
+ {byId[active].kind === "vuln" + ? "CVE-linked · upgrade recommended" + : byId[active].kind === "root" + ? "Repository root" + : `${byId[active].kind} dependency`} +
+
+ )} +
+ + + + ); +} + +function Legend({ color, label }: { color: string; label: string }) { + return ( + + + {label} + + ); +} diff --git a/landing/src/components/marketing/landing/hero.tsx b/landing/src/components/marketing/landing/hero.tsx new file mode 100644 index 0000000..a37bc8f --- /dev/null +++ b/landing/src/components/marketing/landing/hero.tsx @@ -0,0 +1,283 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { motion, useReducedMotion } from "framer-motion"; +import { ArrowRight, ShieldAlert, Sparkles, Workflow } from "lucide-react"; +import { + CountUp, + ProgressBar, + TypewriterLines, + WindowChrome, +} from "@/components/marketing/landing/primitives"; + +const terminalLines = [ + "verspect scan acme/payments-api --branch main", + "✓ cloning repository", + "✓ indexing 142 manifests · 1,840 AST nodes", + "✓ security · 3 critical · 11 high", + "✓ upgrade simulator · django 4.2 → 5.1", + "✓ migration assistant · 18 patches ready", +]; + +export function LandingHero() { + const reduce = useReducedMotion(); + const [scanProgress, setScanProgress] = useState(reduce ? 100 : 12); + const [phase, setPhase] = useState(0); + + useEffect(() => { + if (reduce) return; + const id = window.setInterval(() => { + setScanProgress((p) => { + if (p >= 100) return 18; + return Math.min(100, p + (p > 70 ? 3 : 7)); + }); + }, 380); + return () => window.clearInterval(id); + }, [reduce]); + + useEffect(() => { + if (reduce) return; + const id = window.setInterval(() => setPhase((p) => (p + 1) % 4), 3200); + return () => window.clearInterval(id); + }, [reduce]); + + return ( +
+
+
+
+
+ +
+
+ +

+ Verspect +

+
+ Open Source + Apache-2.0 + Self-hosted + Docker Compose +
+
+ + Know every upgrade before you ship it. + + + Explore dependency usage, security and license findings, package health, and upgrade impact. Optional AI assistance can help draft migration guidance for review. + +

+ Product previews use illustrative example data and are not live repository scans. +

+ + + View on GitHub + + + See product demos + + +
+ + {/* Product stage — full-bleed interactive preview */} + +
+ + illustrative + + } + > +
+
+ +
+
+ Repository index + {scanProgress}% +
+ +
+
+
+ + + +
+
+
+ + + +
+
+
+
+ Health score +
+
+ + /100 +
+
+
+ improving +
+
+
+ {[ + { label: "Deps", value: 248 }, + { label: "Outdated", value: 37 }, + { label: "CVEs", value: 14 }, + ].map((m) => ( +
+
+ {m.label} +
+
+ +
+
+ ))} +
+
+ + + +
+
+
+
+
+
+
+
+ ); +} + +function HeroMetric({ + icon: Icon, + label, + value, + suffix = "", + hint, + active, +}: { + icon: typeof ShieldAlert; + label: string; + value: number; + suffix?: string; + hint: string; + active: boolean; +}) { + return ( +
+
+ + {label} +
+
+ + {suffix} +
+
{hint}
+
+ ); +} + +function MiniBar({ + label, + value, + tone, +}: { + label: string; + value: number; + tone: "teal" | "amber" | "rose"; +}) { + return ( +
+
+ {label} + {value}% +
+ +
+ ); +} diff --git a/landing/src/components/marketing/landing/migration-demo.tsx b/landing/src/components/marketing/landing/migration-demo.tsx new file mode 100644 index 0000000..197195b --- /dev/null +++ b/landing/src/components/marketing/landing/migration-demo.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; +import { Sparkles } from "lucide-react"; +import { + FadeIn, + ProgressBar, + Section, + WindowChrome, +} from "@/components/marketing/landing/primitives"; + +const beforeCode = `from django.conf.urls import url +from django.utils.translation import ugettext_lazy as _ + +urlpatterns = [ + url(r'^health/$', views.health), +]`; + +const afterCode = `from django.urls import re_path +from django.utils.translation import gettext_lazy as _ + +urlpatterns = [ + re_path(r'^health/$', views.health), +]`; + +const analysisSteps = [ + "Parsing AST usages…", + "Matching migration rules…", + "Generating exact patches…", + "Scoring confidence…", +]; + +export function MigrationDemoSection() { + const reduce = useReducedMotion(); + const [step, setStep] = useState(0); + const [showDiff, setShowDiff] = useState(!!reduce); + const [confidence, setConfidence] = useState(reduce ? 94 : 0); + + useEffect(() => { + if (reduce) return; + const id = window.setInterval(() => { + setStep((s) => { + const next = (s + 1) % (analysisSteps.length + 2); + if (next >= analysisSteps.length) { + setShowDiff(true); + setConfidence(94); + } else { + setShowDiff(false); + setConfidence(20 + next * 22); + } + return next; + }); + }, 1600); + return () => window.clearInterval(id); + }, [reduce]); + + return ( +
+ + + Example + + } + > +
+
+
+ Before +
+ +
+
+ AI analysis +
+
+ {analysisSteps.map((line, i) => ( +
+ + {line} +
+ ))} +
+
+
+ Confidence + {confidence}% +
+ +
+
+
+ +
+
+
+ Example patch +
+
+ side-by-side diff +
+
+ + {showDiff ? ( + + + + + ) : ( + +
+ +

+ Generating patch from AST match… +

+
+
+ )} +
+
+
+
+
+
+ ); +} + +function CodeBlock({ code, tone }: { code: string; tone: "rose" | "teal" }) { + const border = + tone === "teal" ? "border-teal-400/20 bg-teal-400/[0.04]" : "border-white/10 bg-secondary/40"; + return ( +
+      {code}
+    
+ ); +} + +function DiffLines() { + const rows = [ + { type: "del", text: "- from django.conf.urls import url" }, + { type: "add", text: "+ from django.urls import re_path" }, + { type: "del", text: "- from django.utils.translation import ugettext_lazy as _" }, + { type: "add", text: "+ from django.utils.translation import gettext_lazy as _" }, + { type: "del", text: "- url(r'^health/$', views.health)," }, + { type: "add", text: "+ re_path(r'^health/$', views.health)," }, + ]; + return ( +
+ {rows.map((r, i) => ( + + {r.text} + + ))} +
+ ); +} diff --git a/landing/src/components/marketing/landing/primitives.tsx b/landing/src/components/marketing/landing/primitives.tsx new file mode 100644 index 0000000..6674ce5 --- /dev/null +++ b/landing/src/components/marketing/landing/primitives.tsx @@ -0,0 +1,254 @@ +"use client"; + +import { + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import { + motion, + useInView, + useReducedMotion, + animate, + type HTMLMotionProps, +} from "framer-motion"; +import { cn } from "@/lib/utils"; + +export const sectionViewport = { once: true, margin: "-10% 0px" as const }; + +export function FadeIn({ + children, + className, + delay = 0, + y = 20, + ...props +}: HTMLMotionProps<"div"> & { delay?: number; y?: number }) { + const reduce = useReducedMotion(); + return ( + + {children} + + ); +} + +export function Section({ + id, + children, + className, + eyebrow, + title, + description, +}: { + id?: string; + children: ReactNode; + className?: string; + eyebrow?: string; + title?: string; + description?: string; +}) { + return ( +
+
+ {(eyebrow || title) && ( + + {eyebrow && ( +

+ {eyebrow} +

+ )} + {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ )} + {children} +
+
+ ); +} + +export function WindowChrome({ + title, + children, + className, + trailing, +}: { + title: string; + children: ReactNode; + className?: string; + trailing?: ReactNode; +}) { + return ( +
+
+
+ + + +
+
+ {title} +
+ {trailing} +
+ {children} +
+ ); +} + +export function CountUp({ + value, + duration = 1.4, + suffix = "", + prefix = "", + className, +}: { + value: number; + duration?: number; + suffix?: string; + prefix?: string; + className?: string; +}) { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: "-40px" }); + const reduce = useReducedMotion(); + const [display, setDisplay] = useState(reduce ? value : 0); + + useEffect(() => { + if (!inView) return; + if (reduce) { + setDisplay(value); + return; + } + const controls = animate(0, value, { + duration, + ease: "easeOut", + onUpdate: (v) => setDisplay(Math.round(v)), + }); + return () => controls.stop(); + }, [inView, value, duration, reduce]); + + return ( + + {prefix} + {display.toLocaleString()} + {suffix} + + ); +} + +export function ProgressBar({ + value, + className, + tone = "teal", +}: { + value: number; + className?: string; + tone?: "teal" | "amber" | "rose" | "sky"; +}) { + const colors = { + teal: "bg-teal-400", + amber: "bg-amber-400", + rose: "bg-rose-400", + sky: "bg-sky-400", + }; + return ( +
+ +
+ ); +} + +export function TypewriterLines({ + lines, + className, + lineDelay = 700, + active = true, + onComplete, +}: { + lines: string[]; + className?: string; + lineDelay?: number; + active?: boolean; + onComplete?: () => void; +}) { + const reduce = useReducedMotion(); + const [visible, setVisible] = useState(reduce ? lines.length : 0); + + useEffect(() => { + if (!active) return; + if (reduce) { + setVisible(lines.length); + onComplete?.(); + return; + } + setVisible(0); + let i = 0; + const id = window.setInterval(() => { + i += 1; + setVisible(i); + if (i >= lines.length) { + window.clearInterval(id); + onComplete?.(); + } + }, lineDelay); + return () => window.clearInterval(id); + }, [active, lines, lineDelay, onComplete, reduce]); + + return ( +
+ {lines.slice(0, visible).map((line, idx) => ( + + › + {line} + + ))} + {visible < lines.length && active && !reduce && ( + + )} +
+ ); +} + +export function SkeletonPulse({ className }: { className?: string }) { + return ( +
+ ); +} diff --git a/landing/src/components/marketing/landing/product-gallery.tsx b/landing/src/components/marketing/landing/product-gallery.tsx new file mode 100644 index 0000000..3eed004 --- /dev/null +++ b/landing/src/components/marketing/landing/product-gallery.tsx @@ -0,0 +1,198 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { X, ZoomIn } from "lucide-react"; +import { Section } from "@/components/marketing/landing/primitives"; + +const screenshots = [ + { + src: "/screenshots/app/dashboard-overview.png", + title: "Dashboard overview", + description: "A high-level view of repository and dependency health.", + alt: "Verspect application dashboard overview.", + }, + { + src: "/screenshots/app/repository-inventory.png", + title: "Repository inventory", + description: "Browse detected packages and their repository context.", + alt: "Verspect repository dependency inventory screen.", + }, + { + src: "/screenshots/app/dependency-analysis-detail.png", + title: "Dependency analysis", + description: "Inspect package-level details and analysis signals.", + alt: "Verspect dependency analysis detail screen.", + }, + { + src: "/screenshots/app/dependency-graph.png", + title: "Dependency graph", + description: "Explore relationships across the dependency tree.", + alt: "Verspect interactive dependency graph screen.", + }, + { + src: "/screenshots/app/security-findings.png", + title: "Security findings", + description: "Review vulnerability findings and affected packages.", + alt: "Verspect security findings screen.", + }, + { + src: "/screenshots/app/migration-assistant.png", + title: "Migration assistant", + description: "Review migration guidance and proposed code changes.", + alt: "Verspect migration assistant screen.", + }, + { + src: "/screenshots/app/upgrade-simulation-babel-in-progress.png", + title: "Upgrade simulation", + description: "Follow an upgrade analysis while it is running.", + alt: "Verspect Babel upgrade simulation in progress.", + }, + { + src: "/screenshots/app/upgrade-simulation-babel-results.png", + title: "Simulation results", + description: "Inspect the impact summary after an upgrade analysis.", + alt: "Verspect Babel upgrade simulation results.", + }, + { + src: "/screenshots/app/pydantic-upgrade-simulation-in-progress.png", + title: "Python package upgrade", + description: "Review high-confidence patches and proposed changes for a Python package upgrade.", + alt: "Verspect Python package upgrade patch preview with proposed repository code changes.", + }, +] as const; + +type Screenshot = (typeof screenshots)[number]; + +export function ProductGallerySection() { + const [activeScreenshot, setActiveScreenshot] = useState(null); + const closeButtonRef = useRef(null); + const originButtonRef = useRef(null); + + useEffect(() => { + if (!activeScreenshot) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + closeButtonRef.current?.focus(); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setActiveScreenshot(null); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => { + document.body.style.overflow = previousOverflow; + window.removeEventListener("keydown", handleKeyDown); + originButtonRef.current?.focus(); + }; + }, [activeScreenshot]); + + return ( + <> +
+
+ {screenshots.map((screenshot, index) => ( + { + originButtonRef.current = event.currentTarget; + setActiveScreenshot(screenshot); + }} + aria-label={"Open " + screenshot.title + " screenshot"} + className="group overflow-hidden rounded-2xl border border-white/10 bg-[#0a1018] text-left shadow-[0_18px_55px_-35px_rgba(0,0,0,0.9)] transition duration-300 hover:-translate-y-1 hover:border-teal-300/30 hover:shadow-[0_24px_60px_-32px_rgba(45,212,191,0.2)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-teal-300" + > + + {screenshot.alt} + + + + + + {screenshot.title} + {screenshot.description} + + + ))} +
+
+ + + {activeScreenshot && ( + + +
+
+ {activeScreenshot.alt} +
+ + + )} + + + ); +} diff --git a/landing/src/components/marketing/landing/scan-demo.tsx b/landing/src/components/marketing/landing/scan-demo.tsx new file mode 100644 index 0000000..830891a --- /dev/null +++ b/landing/src/components/marketing/landing/scan-demo.tsx @@ -0,0 +1,233 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; +import { + Check, + GitBranch, + Loader2, + PackageSearch, + ShieldAlert, + Workflow, +} from "lucide-react"; +import { + FadeIn, + ProgressBar, + Section, + SkeletonPulse, + WindowChrome, +} from "@/components/marketing/landing/primitives"; + +const steps = [ + { + id: "import", + title: "GitHub import", + detail: "oauth · acme/payments-api · main", + icon: GitBranch, + }, + { + id: "index", + title: "Repository indexing", + detail: "pyproject.toml · requirements · 184 source files", + icon: PackageSearch, + }, + { + id: "deps", + title: "Dependency detection", + detail: "248 packages · 1,840 AST call sites", + icon: PackageSearch, + }, + { + id: "security", + title: "Security analysis", + detail: "OSV + advisories mapped to installed versions", + icon: ShieldAlert, + }, + { + id: "upgrade", + title: "Upgrade analysis", + detail: "Breaking APIs · migration effort · health score", + icon: Workflow, + }, +]; + +const detected = [ + { name: "django", from: "4.2.11", to: "5.1.4", risk: "high" }, + { name: "requests", from: "2.31.0", to: "2.32.3", risk: "low" }, + { name: "pydantic", from: "1.10.13", to: "2.10.3", risk: "high" }, + { name: "celery", from: "5.3.6", to: "5.4.1", risk: "med" }, +]; + +export function ScanDemoSection() { + const reduce = useReducedMotion(); + const [active, setActive] = useState(0); + const [progress, setProgress] = useState(0); + + useEffect(() => { + if (reduce) { + setActive(steps.length - 1); + setProgress(100); + return; + } + const id = window.setInterval(() => { + setActive((i) => { + const next = (i + 1) % steps.length; + setProgress(((next + 1) / steps.length) * 100); + return next; + }); + }, 2200); + setProgress(((active + 1) / steps.length) * 100); + return () => window.clearInterval(id); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reduce]); + + return ( +
+
+ +
    + {steps.map((step, idx) => { + const done = idx < active; + const current = idx === active; + const Icon = step.icon; + return ( +
  1. +
    + {done ? ( + + ) : current ? ( + + ) : ( + + )} +
    +
    +
    {step.title}
    +
    + {step.detail} +
    +
    +
  2. + ); + })} +
+
+ + + + {Math.round(progress)}% + + } + > +
+ + + + {active <= 1 && ( +
+ + + +
+ + + +
+
+ )} + {active >= 2 && ( +
+ {detected.map((dep, i) => ( + +
+
{dep.name}
+
+ {dep.from} → {dep.to} +
+
+ +
+ ))} + {active >= 3 && ( +
+ {[ + { label: "Critical", value: "3", tone: "text-rose-300" }, + { label: "High", value: "11", tone: "text-amber-300" }, + { label: "Breaking APIs", value: "9", tone: "text-teal-300" }, + ].map((s) => ( +
+
+ {s.label} +
+
+ {s.value} +
+
+ ))} +
+ )} +
+ )} +
+
+
+
+
+
+
+ ); +} + +function RiskPill({ risk }: { risk: string }) { + const styles = + risk === "high" + ? "bg-rose-400/10 text-rose-300 ring-rose-400/20" + : risk === "med" + ? "bg-amber-400/10 text-amber-300 ring-amber-400/20" + : "bg-emerald-400/10 text-emerald-300 ring-emerald-400/20"; + return ( + + {risk} + + ); +} diff --git a/landing/src/components/marketing/landing/security-dashboard.tsx b/landing/src/components/marketing/landing/security-dashboard.tsx new file mode 100644 index 0000000..a8518d6 --- /dev/null +++ b/landing/src/components/marketing/landing/security-dashboard.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { motion } from "framer-motion"; +import { + CountUp, + FadeIn, + ProgressBar, + Section, + WindowChrome, +} from "@/components/marketing/landing/primitives"; + +const severities = [ + { + label: "Critical", + count: 3, + tone: "rose" as const, + examples: ["CVE-2024-27304 · django", "CVE-2023-32681 · requests"], + }, + { + label: "High", + count: 11, + tone: "amber" as const, + examples: ["GHSA-xxx · urllib3", "OSV · pydantic < 2.4"], + }, + { + label: "Medium", + count: 27, + tone: "sky" as const, + examples: ["outdated transitive pins", "license policy warnings"], + }, + { + label: "Low", + count: 48, + tone: "teal" as const, + examples: ["informational advisories", "non-blocking bumps"], + }, +]; + +export function SecurityDashboardSection() { + return ( +
+
+ {severities.map((s, i) => ( + + +
+ {s.label} +
+
+ +
+ +
    + {s.examples.map((ex) => ( +
  • + {ex} +
  • + ))} +
+
+
+ ))} +
+ + + +
+ {[ + { label: "Packages scanned", value: 248 }, + { label: "Advisories matched", value: 89 }, + { label: "Fixable this week", value: 22 }, + ].map((m) => ( +
+
+ {m.label} +
+
+ +
+
+ ))} +
+
+
+
+ ); +} diff --git a/landing/src/components/marketing/landing/simulator-demo.tsx b/landing/src/components/marketing/landing/simulator-demo.tsx new file mode 100644 index 0000000..42c039d --- /dev/null +++ b/landing/src/components/marketing/landing/simulator-demo.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { motion, useReducedMotion } from "framer-motion"; +import { + FadeIn, + ProgressBar, + Section, + WindowChrome, +} from "@/components/marketing/landing/primitives"; + +const versions = ["4.2.11", "5.0.9", "5.1.4"]; + +const findings = [ + { api: "django.conf.urls.url", severity: "breaking", files: 12 }, + { api: "ugettext_lazy", severity: "breaking", files: 7 }, + { api: "FORCE_SCRIPT_NAME default", severity: "warning", files: 3 }, + { api: "DEFAULT_AUTO_FIELD", severity: "info", files: 1 }, +]; + +export function SimulatorDemoSection() { + const reduce = useReducedMotion(); + const [targetIdx, setTargetIdx] = useState(2); + const [risk, setRisk] = useState(reduce ? 72 : 0); + const [phase, setPhase] = useState(0); + + useEffect(() => { + if (reduce) return; + const id = window.setInterval(() => { + setPhase((p) => (p + 1) % 5); + setTargetIdx((i) => (i === 2 ? 1 : 2)); + }, 2800); + return () => window.clearInterval(id); + }, [reduce]); + + useEffect(() => { + const next = targetIdx === 2 ? 72 : 48; + if (reduce) { + setRisk(next); + return; + } + setRisk(0); + const t = window.setTimeout(() => setRisk(next), 120); + return () => window.clearTimeout(t); + }, [targetIdx, reduce]); + + const bars = useMemo( + () => [ + { label: "API breaks", value: targetIdx === 2 ? 68 : 42, tone: "rose" as const }, + { label: "File impact", value: targetIdx === 2 ? 54 : 31, tone: "amber" as const }, + { label: "Effort", value: targetIdx === 2 ? 61 : 38, tone: "sky" as const }, + { label: "Confidence", value: 88, tone: "teal" as const }, + ], + [targetIdx], + ); + + return ( +
+ + + analysis + + } + > +
+
+
+
+ Version selection +
+
+ + current 4.2.11 + + {versions.slice(1).map((v, i) => { + const selected = targetIdx === i + 1; + return ( + + ); + })} +
+
+ +
+
+ + Risk calculation + + + {risk}/100 + +
+ +
+ +
+
+ Impact charts +
+
+ {bars.map((b) => ( +
+
+ {b.label} + {b.value}% +
+ +
+ ))} +
+
+
+ +
+
+
+ Breaking API detection +
+
+ phase {phase + 1}/5 +
+
+
+ {findings.map((f, i) => ( + +
+ + {f.api} + + +
+
+ {f.files} affected files +
+
+ ))} +
+ + = 3 ? 1 : 0.45, + }} + className="mt-4 rounded-xl border border-teal-400/20 bg-teal-400/[0.05] p-4" + > +
+ Migration summary +
+

+ {targetIdx === 2 + ? "9 breaking APIs across 28 files. Estimated effort: 2–3 engineer days. Suggested patches available for 18 call sites." + : "4 breaking APIs across 11 files. Intermediate hop recommended before 5.1."} +

+
+
+
+
+
+
+ ); +} + +function Severity({ severity }: { severity: string }) { + const map: Record = { + breaking: "bg-rose-400/10 text-rose-300 ring-rose-400/20", + warning: "bg-amber-400/10 text-amber-300 ring-amber-400/20", + info: "bg-sky-400/10 text-sky-300 ring-sky-400/20", + }; + return ( + + {severity} + + ); +} diff --git a/landing/src/components/marketing/landing/social-sections.tsx b/landing/src/components/marketing/landing/social-sections.tsx new file mode 100644 index 0000000..d649c75 --- /dev/null +++ b/landing/src/components/marketing/landing/social-sections.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { ArrowRight, ChevronDown, ExternalLink } from "lucide-react"; +import { FadeIn, Section } from "@/components/marketing/landing/primitives"; + +const repository = "https://github.com/ayushdoesdev/verspect"; +const faqs = [ + { q: "What is Verspect?", a: "Verspect is a dependency intelligence tool for GitHub repositories. It helps you inspect dependency inventory and usage, identify vulnerability and license signals, understand package health, explore dependency relationships, and evaluate upgrade impact." }, + { q: "Is Verspect open source?", a: "Yes. The current main-branch project source is licensed under Apache-2.0. The earlier v0.1.0-beta.1 release remains under AGPL-3.0-or-later, and third-party dependencies retain their own licenses and notices." }, + { q: "Is Verspect a hosted service?", a: "No official hosted SaaS service is currently provided. Verspect is self-hosted, so you run and manage the application on infrastructure you control. Docker Compose is the recommended setup." }, + { q: "How do I install Verspect?", a: "Follow the Docker Compose quick-start in the GitHub repository. The application is self-hosted and requires the services described in its installation documentation." }, + { q: "Which ecosystems are supported?", a: "The scanner supports Python, JavaScript and TypeScript, Go, Java, Rust, PHP, and Ruby. Analysis depth varies by ecosystem and by the manifests, lockfiles, and source evidence available." }, + { q: "Do all ecosystems have the same analysis depth?", a: "No. Python has the deepest deterministic usage and migration analysis. Other ecosystems provide varying levels of dependency inventory, import or module usage, vulnerability, license, and package-health analysis. Advanced language-specific breaking-change analysis is more limited outside the strongest supported Python package rules." }, + { q: "Does Verspect require an AI provider?", a: "No. AI integrations are optional. Deterministic dependency, security, license, and upgrade-analysis capabilities can be used without configuring an AI provider. AI-assisted migration suggestions require a configured provider." }, + { q: "How does vulnerability detection work?", a: "Verspect matches detected package names and versions against OSV vulnerability data. Results depend on package identification, available version evidence, and upstream advisory coverage. A scan should not be treated as a guarantee that a repository has no vulnerabilities." }, + { q: "Can Verspect automatically create GitHub pull requests?", a: "Migration suggestions and patch drafts can be reviewed in the application. Creating a pull request requires reviewing the proposed change and explicitly confirming the action; unattended automatic pull requests are not enabled." }, + { q: "Are the landing-page demonstrations live scans?", a: "No. The animated terminal, scan pipeline, simulator, migration diff, dependency graph, and dashboard use illustrative example data. They do not scan a visitor's repository or connect to the Verspect backend." }, + { q: "Can I contribute to Verspect?", a: "Yes. The source is public on GitHub. Read CONTRIBUTING.md for contribution guidance, or use the repository's Issues page to report a problem or suggest an improvement." }, +]; + +export function RoadmapTeaserSection() { + return ( +
+
+ +
+

Open source · Built in the open

+

Explore the project. Follow its progress. Help shape what comes next.

+

Browse the source, follow releases, report issues, and contribute improvements. Project changes are tracked publicly.

+ +
+
+
+
+ ); +} + +export function FaqSection() { + const [open, setOpen] = useState(0); + return ( +
+
+ {faqs.map((faq, index) => { + const isOpen = open === index; + const answerId = `verspect-faq-answer-${index}`; + return ( + +
+ + + {isOpen && +

{faq.a}

+
} +
+
+
+ ); + })} +
+
+ ); +} + +export function FinalCtaSection() { + return ( +
+
+
+ +

Verspect

+

Understand your dependencies before the next upgrade.

+

Explore the source, read the installation guide, and run Verspect on infrastructure you control.

+ +
+
+
+ ); +} diff --git a/landing/src/components/marketing/nav.tsx b/landing/src/components/marketing/nav.tsx new file mode 100644 index 0000000..c99e92d --- /dev/null +++ b/landing/src/components/marketing/nav.tsx @@ -0,0 +1,144 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { Activity, ChevronDown, Menu, X } from "lucide-react"; + +const repository = "https://github.com/ayushdoesdev/verspect"; +const documentation = `${repository}/tree/main/docs`; +const primaryLinks = [ + { href: "#scan-demo", label: "Product" }, + { href: "#simulator", label: "Simulator" }, + { href: "#migration", label: "Migration" }, + { href: "#screenshots", label: "Gallery" }, + { href: "#faq", label: "FAQ" }, +] as const; +const moreLinks = [ + { href: `${repository}/releases`, label: "Releases" }, + { href: `${repository}/issues`, label: "Issues & feedback" }, + { href: `${repository}/blob/main/CONTRIBUTING.md`, label: "Contributing" }, +] as const; + +export function MarketingNav() { + const [open, setOpen] = useState(false); + const [moreOpen, setMoreOpen] = useState(false); + const menuButtonRef = useRef(null); + const firstMobileLinkRef = useRef(null); + + useEffect(() => { + if (!open) return; + firstMobileLinkRef.current?.focus(); + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setOpen(false); + menuButtonRef.current?.focus(); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [open]); + + useEffect(() => { + if (!moreOpen) return; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setMoreOpen(false); + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [moreOpen]); + + const closeMenus = () => { + setOpen(false); + setMoreOpen(false); + }; + + return ( +
+
+ + + Verspect + + + + + + + +
+ +
+ +
+
+ ); +} + +export function MarketingFooter() { + const columns = [ + { title: "Product", links: [ + { href: "#scan-demo", label: "Repository scan" }, + { href: "#simulator", label: "Upgrade Simulator" }, + { href: "#migration", label: "Migration Assistant" }, + { href: "#screenshots", label: "App screenshots" }, + { href: "#graph", label: "Dependency graph" }, + { href: "#faq", label: "FAQ" }, + ]}, + { title: "Resources", links: [ + { href: documentation, label: "Documentation" }, + { href: `${repository}/releases`, label: "Releases" }, + { href: `${repository}/issues`, label: "Issues & feedback" }, + { href: `${repository}/blob/main/CONTRIBUTING.md`, label: "Contributing" }, + ]}, + { title: "Project", links: [ + { href: repository, label: "GitHub repository" }, + { href: `${repository}/blob/main/LICENSE`, label: "Apache-2.0 license" }, + { href: `${repository}/blob/main/THIRD_PARTY_NOTICES.md`, label: "Third-party notices" }, + { href: `${repository}/blob/main/SECURITY.md`, label: "Security policy" }, + ]}, + ]; + + return ( +
+
+
+
Verspect
+

Dependency intelligence for GitHub repositories. Self-host it, inspect the source, and understand your upgrades.

+

Open source · Apache-2.0 on main

+
+ {columns.map((column) =>
+
{column.title}
+
    {column.links.map((link) =>
  • + {link.href.startsWith("#") ? {link.label} : {link.label} ↗} +
  • )}
+
)} +
+
© {new Date().getFullYear()} Verspect contributors. Verspect name, logo, and official branding are reserved.
+
+ ); +} diff --git a/landing/src/lib/utils.ts b/landing/src/lib/utils.ts new file mode 100644 index 0000000..a935c00 --- /dev/null +++ b/landing/src/lib/utils.ts @@ -0,0 +1,3 @@ +export function cn(...values: Array): string { + return values.filter(Boolean).join(" "); +}