@utopia-studio-design/design-system-cli 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/bin/utopia-ds-mcp.mjs +6 -2
- package/bin/utopia-ds.mjs +94 -21
- package/data/docs/arabic-friendly.md +19 -2
- package/data/docs/billing-setup.md +80 -0
- package/data/docs/cer-003-032-033-round-8-repair.md +41 -0
- package/data/docs/cer-007-checkout.md +51 -0
- package/data/docs/cer-008-011-qa-repair.md +64 -0
- package/data/docs/cer-016-023-round-6-repair.md +92 -0
- package/data/docs/cer-024-031-round-7-repair.md +51 -0
- package/data/docs/cer-033-035-036-round-12-repair.md +37 -0
- package/data/docs/cer-035-037-round-10-repair.md +34 -0
- package/data/docs/charts.md +34 -3
- package/data/docs/chat-service-methodology.md +181 -0
- package/data/docs/code-block.md +20 -0
- package/data/docs/creative-production.md +149 -0
- package/data/docs/designer-agent-backend-contract.md +552 -0
- package/data/docs/email-resend.md +29 -0
- package/data/docs/foundations.md +46 -8
- package/data/docs/guide.md +9 -9
- package/data/docs/handover-billing-aiden.md +132 -0
- package/data/docs/logo-maker-methodology.md +109 -0
- package/data/docs/presentation-asset-repository.md +53 -0
- package/data/docs/product-architecture.md +42 -0
- package/data/docs/quick-start-ai.md +4 -4
- package/data/docs/shadcn-conversion.md +1 -1
- package/data/docs/sidebar-placement.md +56 -0
- package/data/docs/theme-authoring.md +6 -3
- package/data/manifests/catalog.json +42 -46
- package/data/manifests/components.json +317 -66
- package/data/manifests/logo-skills.json +174 -0
- package/data/manifests/motion-profiles.json +2 -2
- package/data/manifests/patterns.json +201 -7
- package/data/manifests/presentation-assets.schema.json +44 -0
- package/data/manifests/templates.json +87 -1
- package/data/manifests/theme-dextrum.json +7 -7
- package/data/manifests/theme-prism-mono.json +19 -0
- package/data/manifests/theme-utopia-cloudblur.json +135 -0
- package/data/manifests/theme-utopia-default.json +31 -31
- package/data/manifests/theme-vyapti.json +17 -1
- package/data/manifests/theme-y2k-pop.json +18 -0
- package/data/manifests/themes.json +285 -57
- package/data/templates/analytics-dashboard/main.tsx +14 -1
- package/data/templates/calendar-application/main.tsx +14 -1
- package/data/templates/chat-workspace/README.md +12 -0
- package/data/templates/chat-workspace/index.html +11 -0
- package/data/templates/chat-workspace/main.tsx +97 -0
- package/data/templates/chat-workspace/styles.css +19 -0
- package/data/templates/chat-workspace/template.manifest.json +13 -0
- package/data/templates/composition-registry-lab/main.tsx +12 -1
- package/data/templates/dashboard-composer/main.tsx +14 -1
- package/data/templates/database-workspace/main.tsx +14 -1
- package/data/templates/interaction-lab/main.tsx +12 -1
- package/data/templates/quantum-verification-story/README.md +12 -0
- package/data/templates/quantum-verification-story/components.json +21 -0
- package/data/templates/quantum-verification-story/eslint.config.mjs +13 -0
- package/data/templates/quantum-verification-story/next-env.d.ts +7 -0
- package/data/templates/quantum-verification-story/next.config.mjs +11 -0
- package/data/templates/quantum-verification-story/package-lock.json +7535 -0
- package/data/templates/quantum-verification-story/package.json +46 -0
- package/data/templates/quantum-verification-story/postcss.config.js +6 -0
- package/data/templates/quantum-verification-story/src/app/assessment/page.tsx +138 -0
- package/data/templates/quantum-verification-story/src/app/ceramic-theme.css +48 -0
- package/data/templates/quantum-verification-story/src/app/deployment/page.tsx +178 -0
- package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Bold.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Medium.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Regular.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/ReadexPro-Variable.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-350.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-500.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-700.ttf +0 -0
- package/data/templates/quantum-verification-story/src/app/globals.css +175 -0
- package/data/templates/quantum-verification-story/src/app/layout.tsx +45 -0
- package/data/templates/quantum-verification-story/src/app/not-found.tsx +39 -0
- package/data/templates/quantum-verification-story/src/app/page.tsx +24 -0
- package/data/templates/quantum-verification-story/src/app/products/page.tsx +132 -0
- package/data/templates/quantum-verification-story/src/app/utopia-default.css +312 -0
- package/data/templates/quantum-verification-story/src/components/RotatingText.css +36 -0
- package/data/templates/quantum-verification-story/src/components/RotatingText.tsx +189 -0
- package/data/templates/quantum-verification-story/src/components/brand-mark.tsx +14 -0
- package/data/templates/quantum-verification-story/src/components/console-preview-section.tsx +57 -0
- package/data/templates/quantum-verification-story/src/components/console-preview.tsx +72 -0
- package/data/templates/quantum-verification-story/src/components/fade-up.tsx +54 -0
- package/data/templates/quantum-verification-story/src/components/floating-network.tsx +125 -0
- package/data/templates/quantum-verification-story/src/components/footer.tsx +83 -0
- package/data/templates/quantum-verification-story/src/components/harvest-timeline.tsx +70 -0
- package/data/templates/quantum-verification-story/src/components/lattice-diagram.tsx +73 -0
- package/data/templates/quantum-verification-story/src/components/lattice-field.tsx +129 -0
- package/data/templates/quantum-verification-story/src/components/nav.tsx +104 -0
- package/data/templates/quantum-verification-story/src/components/presentation-layout.tsx +91 -0
- package/data/templates/quantum-verification-story/src/components/readiness-estimator.tsx +135 -0
- package/data/templates/quantum-verification-story/src/components/scramble-text.tsx +60 -0
- package/data/templates/quantum-verification-story/src/components/section-backdrop.tsx +38 -0
- package/data/templates/quantum-verification-story/src/components/sections/clients.tsx +140 -0
- package/data/templates/quantum-verification-story/src/components/sections/contact.tsx +125 -0
- package/data/templates/quantum-verification-story/src/components/sections/hero.tsx +92 -0
- package/data/templates/quantum-verification-story/src/components/sections/moat.tsx +73 -0
- package/data/templates/quantum-verification-story/src/components/sections/offer.tsx +113 -0
- package/data/templates/quantum-verification-story/src/components/sections/pilot.tsx +63 -0
- package/data/templates/quantum-verification-story/src/components/sections/problem.tsx +53 -0
- package/data/templates/quantum-verification-story/src/components/text-particle.tsx +204 -0
- package/data/templates/quantum-verification-story/src/components/ticker.tsx +27 -0
- package/data/templates/quantum-verification-story/src/components/ui/badge.tsx +29 -0
- package/data/templates/quantum-verification-story/src/components/ui/button.tsx +55 -0
- package/data/templates/quantum-verification-story/src/components/ui/card.tsx +46 -0
- package/data/templates/quantum-verification-story/src/components/ui/cursor-driven-particle-typography.tsx +288 -0
- package/data/templates/quantum-verification-story/src/components/ui/input.tsx +20 -0
- package/data/templates/quantum-verification-story/src/components/ui/label.tsx +21 -0
- package/data/templates/quantum-verification-story/src/components/ui/separator.tsx +25 -0
- package/data/templates/quantum-verification-story/src/components/ui/textarea.tsx +19 -0
- package/data/templates/quantum-verification-story/src/lib/utils.ts +6 -0
- package/data/templates/quantum-verification-story/tailwind.config.js +95 -0
- package/data/templates/quantum-verification-story/template.manifest.json +23 -0
- package/data/templates/quantum-verification-story/tsconfig.json +42 -0
- package/data/templates/saas-solution-homepage/main.tsx +91 -14
- package/data/templates/saas-solution-homepage/privacy/index.html +1 -0
- package/data/templates/saas-solution-homepage/styles.css +47 -2
- package/data/templates/saas-solution-homepage/team/index.html +1 -0
- package/data/templates/saas-solution-homepage/template.manifest.json +41 -0
- package/data/templates/saas-solution-homepage/terms/index.html +1 -0
- package/lib/api.mjs +52 -3
- package/lib/template-runtime.mjs +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortest Vector Problem diagram — the lattice hardness assumption that
|
|
3
|
+
* underpins ML-KEM (FIPS-203) and ML-DSA (FIPS-204). Pure SVG, deterministic.
|
|
4
|
+
*/
|
|
5
|
+
const O = { x: 78, y: 250 }
|
|
6
|
+
const b1 = { x: 60, y: -16 }
|
|
7
|
+
const b2 = { x: 22, y: -52 }
|
|
8
|
+
|
|
9
|
+
const points: { x: number; y: number; origin: boolean }[] = []
|
|
10
|
+
for (let i = -1; i <= 5; i++) {
|
|
11
|
+
for (let j = -1; j <= 5; j++) {
|
|
12
|
+
const x = O.x + i * b1.x + j * b2.x
|
|
13
|
+
const y = O.y + i * b1.y + j * b2.y
|
|
14
|
+
if (x >= 8 && x <= 372 && y >= 8 && y <= 292) {
|
|
15
|
+
points.push({ x, y, origin: i === 0 && j === 0 })
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// shortest non-zero vector ≈ b1 − b2
|
|
21
|
+
const sv = { x: O.x + b1.x - b2.x, y: O.y + b1.y - b2.y }
|
|
22
|
+
|
|
23
|
+
export function LatticeDiagram() {
|
|
24
|
+
return (
|
|
25
|
+
<div className="rounded-2xl border border-white/10 bg-[var(--deep-950)] p-7 sm:p-8">
|
|
26
|
+
<div className="font-mono text-[10px] tracking-[0.15em] text-[var(--primary-400)] uppercase font-semibold mb-5">
|
|
27
|
+
Why quantum can't break it
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<svg viewBox="0 0 380 300" className="w-full h-auto" role="img"
|
|
31
|
+
aria-label="Lattice diagram illustrating the shortest vector problem">
|
|
32
|
+
<defs>
|
|
33
|
+
<marker id="ah-primary" markerWidth="7" markerHeight="7" refX="5.5" refY="3" orient="auto">
|
|
34
|
+
<path d="M0 0 L6 3 L0 6 z" fill="var(--primary-400)" />
|
|
35
|
+
</marker>
|
|
36
|
+
<marker id="ah-sv" markerWidth="7" markerHeight="7" refX="5.5" refY="3" orient="auto">
|
|
37
|
+
<path d="M0 0 L6 3 L0 6 z" fill="#6ee7a8" />
|
|
38
|
+
</marker>
|
|
39
|
+
</defs>
|
|
40
|
+
|
|
41
|
+
{/* lattice points */}
|
|
42
|
+
{points.map((p, k) => (
|
|
43
|
+
<circle key={k} cx={p.x} cy={p.y} r={p.origin ? 3.4 : 2.4}
|
|
44
|
+
fill={p.origin ? "var(--primary-300)" : "color-mix(in srgb, var(--primary-500) 40%, transparent)"} />
|
|
45
|
+
))}
|
|
46
|
+
|
|
47
|
+
{/* basis vectors */}
|
|
48
|
+
<line x1={O.x} y1={O.y} x2={O.x + b1.x} y2={O.y + b1.y}
|
|
49
|
+
stroke="var(--primary-400)" strokeWidth="1.6" markerEnd="url(#ah-primary)" opacity="0.85" />
|
|
50
|
+
<line x1={O.x} y1={O.y} x2={O.x + b2.x} y2={O.y + b2.y}
|
|
51
|
+
stroke="var(--primary-400)" strokeWidth="1.6" markerEnd="url(#ah-primary)" opacity="0.85" />
|
|
52
|
+
<text x={O.x + b1.x + 6} y={O.y + b1.y + 2} fill="var(--primary-300)"
|
|
53
|
+
className="font-mono" fontSize="11" fontStyle="italic">b₁</text>
|
|
54
|
+
<text x={O.x + b2.x - 18} y={O.y + b2.y + 4} fill="var(--primary-300)"
|
|
55
|
+
className="font-mono" fontSize="11" fontStyle="italic">b₂</text>
|
|
56
|
+
|
|
57
|
+
{/* shortest vector */}
|
|
58
|
+
<line x1={O.x} y1={O.y} x2={sv.x} y2={sv.y}
|
|
59
|
+
stroke="#6ee7a8" strokeWidth="2.2" markerEnd="url(#ah-sv)" />
|
|
60
|
+
<text x={sv.x + 8} y={sv.y + 4} fill="#6ee7a8" className="font-mono" fontSize="11">shortest vector</text>
|
|
61
|
+
</svg>
|
|
62
|
+
|
|
63
|
+
<p className="mt-5 text-[13px] text-white/55 leading-[1.7]">
|
|
64
|
+
Finding the shortest vector in a high-dimensional lattice stays hard even for a large-scale
|
|
65
|
+
quantum computer — Shor's algorithm, the very attack that shatters RSA and ECC, simply
|
|
66
|
+
doesn't apply here. That asymmetry is exactly what{" "}
|
|
67
|
+
<strong className="text-[var(--primary-400)]">ML-KEM (FIPS-203)</strong> and{" "}
|
|
68
|
+
<strong className="text-[var(--primary-400)]">ML-DSA (FIPS-204)</strong> are built on. Migrating
|
|
69
|
+
to them is what keeps your data safe after Y2Q.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Animated lattice field — evokes lattice-based post-quantum cryptography
|
|
7
|
+
* (the math behind NIST FIPS-203 / FIPS-204). A grid of nodes drifts slowly
|
|
8
|
+
* and connects to nearby neighbours; a faint pulse travels through the lattice.
|
|
9
|
+
* Subtle by design so text stays readable. Respects reduced-motion and pauses
|
|
10
|
+
* the animation loop while off-screen, so multiple instances stay cheap.
|
|
11
|
+
*/
|
|
12
|
+
export function LatticeField({
|
|
13
|
+
spacing = 64,
|
|
14
|
+
className = "",
|
|
15
|
+
}: {
|
|
16
|
+
spacing?: number
|
|
17
|
+
className?: string
|
|
18
|
+
}) {
|
|
19
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const canvas = canvasRef.current
|
|
23
|
+
if (!canvas) return
|
|
24
|
+
const ctx = canvas.getContext("2d")
|
|
25
|
+
if (!ctx) return
|
|
26
|
+
|
|
27
|
+
const still = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("still")
|
|
28
|
+
const reduce = still || window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
29
|
+
const SPACING = spacing
|
|
30
|
+
const LINK = spacing * 1.45 // max distance to draw a connection
|
|
31
|
+
const GOLD = "208,160,85"
|
|
32
|
+
|
|
33
|
+
let w = 0, h = 0, dpr = 1
|
|
34
|
+
let nodes: { x: number; y: number; bx: number; by: number; ph: number; sp: number }[] = []
|
|
35
|
+
let raf = 0
|
|
36
|
+
let t = 0
|
|
37
|
+
|
|
38
|
+
const build = () => {
|
|
39
|
+
const rect = canvas.getBoundingClientRect()
|
|
40
|
+
w = rect.width; h = rect.height
|
|
41
|
+
dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
42
|
+
canvas.width = w * dpr; canvas.height = h * dpr
|
|
43
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
44
|
+
nodes = []
|
|
45
|
+
const cols = Math.ceil(w / SPACING) + 2
|
|
46
|
+
const rows = Math.ceil(h / SPACING) + 2
|
|
47
|
+
for (let r = 0; r < rows; r++) {
|
|
48
|
+
for (let c = 0; c < cols; c++) {
|
|
49
|
+
const bx = (c - 1) * SPACING
|
|
50
|
+
const by = (r - 1) * SPACING
|
|
51
|
+
// deterministic phase offset so the drift looks organic, not uniform
|
|
52
|
+
const ph = ((c * 13 + r * 7) % 100) / 100 * Math.PI * 2
|
|
53
|
+
const sp = 0.5 + ((c * 5 + r * 11) % 50) / 100
|
|
54
|
+
nodes.push({ x: bx, y: by, bx, by, ph, sp })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const draw = () => {
|
|
60
|
+
ctx.clearRect(0, 0, w, h)
|
|
61
|
+
const amp = reduce ? 0 : 7
|
|
62
|
+
|
|
63
|
+
for (const n of nodes) {
|
|
64
|
+
n.x = n.bx + Math.sin(t * 0.0006 * n.sp + n.ph) * amp
|
|
65
|
+
n.y = n.by + Math.cos(t * 0.0005 * n.sp + n.ph) * amp
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// a slow diagonal pulse front travelling across the lattice
|
|
69
|
+
const pulse = ((t * 0.04) % (w + h + 400)) - 200
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
72
|
+
const a = nodes[i]
|
|
73
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
74
|
+
const b = nodes[j]
|
|
75
|
+
const dx = a.x - b.x, dy = a.y - b.y
|
|
76
|
+
const d = Math.hypot(dx, dy)
|
|
77
|
+
if (d < LINK) {
|
|
78
|
+
const base = (1 - d / LINK) * 0.16
|
|
79
|
+
ctx.strokeStyle = `rgba(${GOLD},${base})`
|
|
80
|
+
ctx.lineWidth = 0.6
|
|
81
|
+
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// node dot, brighter near the pulse front
|
|
85
|
+
const near = 1 - Math.min(Math.abs((a.x + a.y) - pulse), 160) / 160
|
|
86
|
+
const glow = reduce ? 0.22 : 0.18 + near * 0.55
|
|
87
|
+
ctx.fillStyle = `rgba(${GOLD},${glow})`
|
|
88
|
+
ctx.beginPath(); ctx.arc(a.x, a.y, near > 0.6 && !reduce ? 2.2 : 1.4, 0, Math.PI * 2); ctx.fill()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const loop = (now: number) => {
|
|
93
|
+
t = now
|
|
94
|
+
draw()
|
|
95
|
+
raf = requestAnimationFrame(loop)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let visible = true
|
|
99
|
+
const start = () => { if (!reduce && visible && !raf) raf = requestAnimationFrame(loop) }
|
|
100
|
+
const stop = () => { if (raf) { cancelAnimationFrame(raf); raf = 0 } }
|
|
101
|
+
|
|
102
|
+
build()
|
|
103
|
+
draw()
|
|
104
|
+
|
|
105
|
+
// pause the loop while the canvas is scrolled out of view
|
|
106
|
+
const io = new IntersectionObserver(
|
|
107
|
+
([e]) => {
|
|
108
|
+
visible = e.isIntersecting
|
|
109
|
+
if (visible) start()
|
|
110
|
+
else stop()
|
|
111
|
+
},
|
|
112
|
+
{ threshold: 0 }
|
|
113
|
+
)
|
|
114
|
+
io.observe(canvas)
|
|
115
|
+
start()
|
|
116
|
+
|
|
117
|
+
const onResize = () => { build(); draw() }
|
|
118
|
+
window.addEventListener("resize", onResize)
|
|
119
|
+
return () => { stop(); io.disconnect(); window.removeEventListener("resize", onResize) }
|
|
120
|
+
}, [spacing])
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<canvas
|
|
124
|
+
ref={canvasRef}
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
className={`absolute inset-0 w-full h-full pointer-events-none ${className}`}
|
|
127
|
+
/>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
import { BrandMark } from "@/components/brand-mark"
|
|
6
|
+
import { Menu, X } from "lucide-react"
|
|
7
|
+
import { Button } from "@/components/ui/button"
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
|
|
10
|
+
const links = [
|
|
11
|
+
{ label: "Products", href: "/products" },
|
|
12
|
+
{ label: "NIST Estimator", href: "/assessment" },
|
|
13
|
+
{ label: "Verification", href: "/deployment" },
|
|
14
|
+
{ label: "Pricing", href: "#pilot" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export function Nav() {
|
|
18
|
+
const [open, setOpen] = useState(false)
|
|
19
|
+
const [scrolled, setScrolled] = useState(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const fn = () => setScrolled(window.scrollY > 4)
|
|
23
|
+
window.addEventListener("scroll", fn, { passive: true })
|
|
24
|
+
return () => window.removeEventListener("scroll", fn)
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<header className={cn(
|
|
29
|
+
"sticky top-0 z-50 w-full bg-[var(--gray-950)] transition-shadow duration-200",
|
|
30
|
+
scrolled && "shadow-lg shadow-black/30"
|
|
31
|
+
)}>
|
|
32
|
+
<div className="container-main h-[64px] flex items-center justify-between gap-6">
|
|
33
|
+
|
|
34
|
+
{/* Logo */}
|
|
35
|
+
<Link href="/" className="flex items-center gap-2.5 shrink-0">
|
|
36
|
+
<BrandMark />
|
|
37
|
+
<span className="text-[15px] font-semibold tracking-tight text-white">
|
|
38
|
+
Your Company
|
|
39
|
+
</span>
|
|
40
|
+
</Link>
|
|
41
|
+
|
|
42
|
+
{/* Desktop center links */}
|
|
43
|
+
<nav className="hidden md:flex items-center gap-0.5">
|
|
44
|
+
{links.map(({ label, href }) => (
|
|
45
|
+
<Link
|
|
46
|
+
key={href}
|
|
47
|
+
href={href}
|
|
48
|
+
className="px-3 py-1.5 text-[14px] font-medium text-white/65 hover:text-white rounded-md hover:bg-white/5 transition-colors"
|
|
49
|
+
>
|
|
50
|
+
{label}
|
|
51
|
+
</Link>
|
|
52
|
+
))}
|
|
53
|
+
</nav>
|
|
54
|
+
|
|
55
|
+
{/* Desktop right */}
|
|
56
|
+
<div className="hidden md:flex items-center gap-2 shrink-0">
|
|
57
|
+
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full border border-white/20 text-[12px] font-mono text-white/55">
|
|
58
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--primary-400)] animate-[pulse-dot_1.8s_infinite]" />
|
|
59
|
+
<span>Lattice active</span>
|
|
60
|
+
</div>
|
|
61
|
+
<Button asChild size="sm"
|
|
62
|
+
className="rounded-full bg-white text-[var(--gray-950)] hover:bg-white/90 h-8 px-4 text-[13px] font-semibold"
|
|
63
|
+
>
|
|
64
|
+
<Link href="#contact">Contact sales</Link>
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Mobile toggle */}
|
|
69
|
+
<button
|
|
70
|
+
className="md:hidden p-1.5 rounded-md text-white/80 hover:bg-white/10 transition-colors"
|
|
71
|
+
onClick={() => setOpen(v => !v)}
|
|
72
|
+
aria-label="Toggle menu"
|
|
73
|
+
aria-expanded={open}
|
|
74
|
+
aria-controls="mobile-navigation"
|
|
75
|
+
>
|
|
76
|
+
{open ? <X size={20} /> : <Menu size={20} />}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Mobile drawer */}
|
|
81
|
+
{open && (
|
|
82
|
+
<div id="mobile-navigation" className="md:hidden bg-[var(--gray-950)] border-t border-white/10">
|
|
83
|
+
<div className="container-main py-4 flex flex-col gap-0.5">
|
|
84
|
+
{links.map(({ label, href }) => (
|
|
85
|
+
<Link
|
|
86
|
+
key={href}
|
|
87
|
+
href={href}
|
|
88
|
+
onClick={() => setOpen(false)}
|
|
89
|
+
className="px-3 py-2.5 text-[15px] font-medium text-white/80 hover:bg-white/5 rounded-md transition-colors"
|
|
90
|
+
>
|
|
91
|
+
{label}
|
|
92
|
+
</Link>
|
|
93
|
+
))}
|
|
94
|
+
<div className="pt-3 mt-2 border-t border-white/10">
|
|
95
|
+
<Button asChild className="w-full rounded-full bg-white text-[var(--gray-950)] hover:bg-white/90">
|
|
96
|
+
<Link href="#contact" onClick={() => setOpen(false)}>Contact sales</Link>
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</header>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useEffect, useState, ReactNode } from "react"
|
|
3
|
+
import { motion } from "framer-motion"
|
|
4
|
+
|
|
5
|
+
const NAV_H = 64
|
|
6
|
+
|
|
7
|
+
function Slide({ children, snap }: { children: ReactNode; snap: boolean }) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
style={snap ? {
|
|
11
|
+
height: `calc(100vh - ${NAV_H}px)`,
|
|
12
|
+
scrollSnapAlign: "start",
|
|
13
|
+
scrollSnapStop: "always",
|
|
14
|
+
overflow: "hidden",
|
|
15
|
+
position: "relative",
|
|
16
|
+
} : {
|
|
17
|
+
minHeight: "100svh",
|
|
18
|
+
position: "relative",
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<motion.div
|
|
22
|
+
style={snap ? { height: "100%" } : {}}
|
|
23
|
+
initial={false}
|
|
24
|
+
animate={{ opacity: 1, y: 0 }}
|
|
25
|
+
transition={{ duration: 0.65, ease: [0.22, 1, 0.36, 1] }}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</motion.div>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PresentationLayout({ sections }: { sections: ReactNode[] }) {
|
|
34
|
+
const [snap, setSnap] = useState(false)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const check = () => setSnap(window.innerWidth >= 768)
|
|
38
|
+
check()
|
|
39
|
+
window.addEventListener("resize", check)
|
|
40
|
+
return () => window.removeEventListener("resize", check)
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
// Lock body scroll only on desktop snap mode
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!snap) return
|
|
46
|
+
document.documentElement.style.overflow = "hidden"
|
|
47
|
+
document.documentElement.style.height = "100%"
|
|
48
|
+
document.body.style.overflow = "hidden"
|
|
49
|
+
document.body.style.height = "100%"
|
|
50
|
+
return () => {
|
|
51
|
+
document.documentElement.style.overflow = ""
|
|
52
|
+
document.documentElement.style.height = ""
|
|
53
|
+
document.body.style.overflow = ""
|
|
54
|
+
document.body.style.height = ""
|
|
55
|
+
}
|
|
56
|
+
}, [snap])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{/* Black frame — desktop only */}
|
|
61
|
+
{snap && (
|
|
62
|
+
<div
|
|
63
|
+
style={{
|
|
64
|
+
position: "fixed",
|
|
65
|
+
top: NAV_H,
|
|
66
|
+
left: 0,
|
|
67
|
+
right: 0,
|
|
68
|
+
bottom: 0,
|
|
69
|
+
pointerEvents: "none",
|
|
70
|
+
zIndex: 40,
|
|
71
|
+
boxShadow: "inset 0 0 0 10px #000",
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Scroll container */}
|
|
77
|
+
<div
|
|
78
|
+
style={snap ? {
|
|
79
|
+
height: `calc(100vh - ${NAV_H}px)`,
|
|
80
|
+
overflowY: "scroll",
|
|
81
|
+
scrollSnapType: "y mandatory",
|
|
82
|
+
scrollBehavior: "smooth",
|
|
83
|
+
} : {}}
|
|
84
|
+
>
|
|
85
|
+
{sections.map((section, i) => (
|
|
86
|
+
<Slide key={i} snap={snap}>{section}</Slide>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
</>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Readiness estimator based on Mosca's inequality:
|
|
7
|
+
* if (data shelf-life X) + (migration time Y) > (years until quantum Z)
|
|
8
|
+
* then you are already exposed to harvest-now-decrypt-later.
|
|
9
|
+
* Z is anchored to an estimated Y2Q of 2031.
|
|
10
|
+
*/
|
|
11
|
+
const CRYPTO = [
|
|
12
|
+
{ k: "rsa2048", l: "RSA-2048", mult: 1.0 },
|
|
13
|
+
{ k: "ecc", l: "ECC (P-256)", mult: 1.0 },
|
|
14
|
+
{ k: "rsa4096", l: "RSA-4096", mult: 1.1 },
|
|
15
|
+
{ k: "mixed", l: "Mixed legacy", mult: 1.4 },
|
|
16
|
+
]
|
|
17
|
+
const Z = 5 // years from 2026 to an estimated Y2Q of 2031
|
|
18
|
+
|
|
19
|
+
export function ReadinessEstimator() {
|
|
20
|
+
const [shelf, setShelf] = useState(7)
|
|
21
|
+
const [systems, setSystems] = useState(40)
|
|
22
|
+
const [crypto, setCrypto] = useState("rsa2048")
|
|
23
|
+
|
|
24
|
+
const mult = CRYPTO.find((c) => c.k === crypto)!.mult
|
|
25
|
+
const migration = Math.max(0.5, Math.round((systems / 60) * mult * 10) / 10)
|
|
26
|
+
const total = Math.round((shelf + migration) * 10) / 10
|
|
27
|
+
const margin = Math.round((total - Z) * 10) / 10
|
|
28
|
+
const exposed = total > Z
|
|
29
|
+
|
|
30
|
+
const tier =
|
|
31
|
+
!exposed ? { label: "Narrow margin", tone: "ok" } :
|
|
32
|
+
margin > 10 ? { label: "Critical exposure", tone: "crit" } :
|
|
33
|
+
margin > 4 ? { label: "High exposure", tone: "high" } :
|
|
34
|
+
{ label: "Elevated exposure", tone: "elev" }
|
|
35
|
+
|
|
36
|
+
const toneColor =
|
|
37
|
+
tier.tone === "ok" ? "#6ee7a8" :
|
|
38
|
+
tier.tone === "crit" ? "#e8705a" :
|
|
39
|
+
tier.tone === "high" ? "#e8975a" :
|
|
40
|
+
tier.tone === "elev" ? "var(--primary-400)" : "var(--primary-400)"
|
|
41
|
+
|
|
42
|
+
// bar scaling
|
|
43
|
+
const max = Math.max(total, Z) * 1.1
|
|
44
|
+
const pct = (v: number) => `${Math.min((v / max) * 100, 100)}%`
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="glow-border rounded-2xl border border-white/10 bg-[var(--deep-900)] text-white overflow-hidden">
|
|
48
|
+
<div className="px-7 sm:px-9 pt-8 pb-6 border-b border-white/10">
|
|
49
|
+
<div className="font-mono text-[10px] tracking-[0.15em] uppercase text-[var(--primary-400)] font-semibold mb-1.5">
|
|
50
|
+
Mosca Exposure Estimator
|
|
51
|
+
</div>
|
|
52
|
+
<p className="text-[13px] text-white/55 leading-relaxed">
|
|
53
|
+
A 30-second version of the math we run in a full assessment. Adjust the inputs to see whether
|
|
54
|
+
your data is already inside the harvest-now window.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="grid lg:grid-cols-[1fr_1fr]">
|
|
59
|
+
{/* Inputs */}
|
|
60
|
+
<div className="px-7 sm:px-9 py-8 space-y-7 border-b lg:border-b-0 lg:border-r border-white/10">
|
|
61
|
+
<div>
|
|
62
|
+
<div className="flex justify-between items-baseline mb-2">
|
|
63
|
+
<label className="text-[12px] font-mono uppercase tracking-[0.08em] text-white/55">Data must stay secret for</label>
|
|
64
|
+
<span className="font-mono text-[15px] font-bold text-[var(--primary-400)]">{shelf} yrs</span>
|
|
65
|
+
</div>
|
|
66
|
+
<input type="range" min={1} max={25} value={shelf} onChange={(e) => setShelf(+e.target.value)}
|
|
67
|
+
className="w-full h-1 cursor-grab" style={{ accentColor: "var(--primary-500)" }} />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div>
|
|
71
|
+
<div className="flex justify-between items-baseline mb-2">
|
|
72
|
+
<label className="text-[12px] font-mono uppercase tracking-[0.08em] text-white/55">Systems in scope</label>
|
|
73
|
+
<span className="font-mono text-[15px] font-bold text-[var(--primary-400)]">{systems}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<input type="range" min={1} max={300} value={systems} onChange={(e) => setSystems(+e.target.value)}
|
|
76
|
+
className="w-full h-1 cursor-grab" style={{ accentColor: "var(--primary-500)" }} />
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div>
|
|
80
|
+
<label className="block text-[12px] font-mono uppercase tracking-[0.08em] text-white/55 mb-2">Primary public-key crypto</label>
|
|
81
|
+
<div className="grid grid-cols-2 gap-2">
|
|
82
|
+
{CRYPTO.map((c) => (
|
|
83
|
+
<button key={c.k} onClick={() => setCrypto(c.k)}
|
|
84
|
+
className={`px-3 py-2 text-[12px] font-mono rounded-lg border transition-colors cursor-pointer ${
|
|
85
|
+
crypto === c.k
|
|
86
|
+
? "border-[var(--primary-500)] bg-[var(--primary-500)]/15 text-[var(--primary-300)]"
|
|
87
|
+
: "border-white/12 text-white/55 hover:border-white/30"
|
|
88
|
+
}`}>
|
|
89
|
+
{c.l}
|
|
90
|
+
</button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Output */}
|
|
97
|
+
<div className="px-7 sm:px-9 py-8 flex flex-col justify-center bg-white/[0.02]">
|
|
98
|
+
<div className="space-y-3 mb-6">
|
|
99
|
+
{[
|
|
100
|
+
{ l: "Data shelf-life (X)", v: `${shelf} yrs`, w: shelf, c: "var(--primary-400)" },
|
|
101
|
+
{ l: "Migration time (Y)", v: `${migration} yrs`, w: migration, c: "var(--primary-300)" },
|
|
102
|
+
{ l: "Years to Y2Q (Z)", v: `${Z} yrs`, w: Z, c: "rgba(255,255,255,0.45)" },
|
|
103
|
+
].map((row) => (
|
|
104
|
+
<div key={row.l}>
|
|
105
|
+
<div className="flex justify-between text-[11px] mb-1">
|
|
106
|
+
<span className="text-white/50 font-mono">{row.l}</span>
|
|
107
|
+
<span className="text-white/80 font-mono font-semibold">{row.v}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div className="h-1.5 rounded-full bg-white/8 overflow-hidden">
|
|
110
|
+
<div className="h-full rounded-full" style={{ width: pct(row.w), background: row.c }} />
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="rounded-xl border p-5" style={{ borderColor: toneColor + "55", background: toneColor + "12" }}>
|
|
117
|
+
<div className="font-mono text-[11px] tracking-[0.1em] uppercase font-bold mb-1.5" style={{ color: toneColor }}>
|
|
118
|
+
{tier.label}
|
|
119
|
+
</div>
|
|
120
|
+
<p className="text-[13px] text-white/75 leading-[1.6]">
|
|
121
|
+
{exposed ? (
|
|
122
|
+
<>X + Y = <strong className="text-white">{total} yrs</strong> exceeds Z = {Z} yrs by{" "}
|
|
123
|
+
<strong style={{ color: toneColor }}>{margin} yrs</strong>. Data you encrypt today can be
|
|
124
|
+
harvested now and decrypted after Y2Q. Migration should start immediately.</>
|
|
125
|
+
) : (
|
|
126
|
+
<>X + Y = <strong className="text-white">{total} yrs</strong> is within Z = {Z} yrs — but the
|
|
127
|
+
margin is thin. Any delay or scope growth pushes you into exposure.</>
|
|
128
|
+
)}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react"
|
|
4
|
+
|
|
5
|
+
const GLYPHS = "ABCDEF0123456789<>/\\{}[]=+*#$%&"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes its text from random cipher-glyphs into the final string when it
|
|
9
|
+
* scrolls into view — a nod to encryption/decryption. Static (final text) for
|
|
10
|
+
* reduced-motion users and during SSR, so it's always readable.
|
|
11
|
+
*/
|
|
12
|
+
export function ScrambleText({
|
|
13
|
+
text,
|
|
14
|
+
className = "",
|
|
15
|
+
duration = 700,
|
|
16
|
+
}: {
|
|
17
|
+
text: string
|
|
18
|
+
className?: string
|
|
19
|
+
duration?: number
|
|
20
|
+
}) {
|
|
21
|
+
const [display, setDisplay] = useState(text)
|
|
22
|
+
const ref = useRef<HTMLSpanElement>(null)
|
|
23
|
+
const done = useRef(false)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const el = ref.current
|
|
27
|
+
if (!el) return
|
|
28
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
|
29
|
+
|
|
30
|
+
const run = () => {
|
|
31
|
+
if (done.current) return
|
|
32
|
+
done.current = true
|
|
33
|
+
const start = performance.now()
|
|
34
|
+
let raf = 0
|
|
35
|
+
const tick = (now: number) => {
|
|
36
|
+
const p = Math.min((now - start) / duration, 1)
|
|
37
|
+
const reveal = Math.floor(p * text.length)
|
|
38
|
+
let out = ""
|
|
39
|
+
for (let i = 0; i < text.length; i++) {
|
|
40
|
+
if (i < reveal || text[i] === " ") out += text[i]
|
|
41
|
+
else out += GLYPHS[Math.floor(Math.random() * GLYPHS.length)]
|
|
42
|
+
}
|
|
43
|
+
setDisplay(out)
|
|
44
|
+
if (p < 1) raf = requestAnimationFrame(tick)
|
|
45
|
+
else setDisplay(text)
|
|
46
|
+
}
|
|
47
|
+
raf = requestAnimationFrame(tick)
|
|
48
|
+
return () => cancelAnimationFrame(raf)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const io = new IntersectionObserver(
|
|
52
|
+
([e]) => { if (e.isIntersecting) { run(); io.disconnect() } },
|
|
53
|
+
{ threshold: 0.4 }
|
|
54
|
+
)
|
|
55
|
+
io.observe(el)
|
|
56
|
+
return () => io.disconnect()
|
|
57
|
+
}, [text, duration])
|
|
58
|
+
|
|
59
|
+
return <span ref={ref} className={className}>{display}</span>
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtle decorative backdrop for the light sections — a faint dotted lattice
|
|
3
|
+
* grid plus a soft brand glow, radially masked so it fades into the page.
|
|
4
|
+
* Pure CSS/SVG, no JS, render-cheap. Breaks up large white areas without
|
|
5
|
+
* competing with the foreground text.
|
|
6
|
+
*/
|
|
7
|
+
export function SectionBackdrop({
|
|
8
|
+
variant = "light",
|
|
9
|
+
className = "",
|
|
10
|
+
}: {
|
|
11
|
+
variant?: "light" | "dark"
|
|
12
|
+
className?: string
|
|
13
|
+
}) {
|
|
14
|
+
const dot = variant === "dark" ? "color-mix(in srgb, var(--primary-500) 18%, transparent)" : "color-mix(in srgb, var(--foreground) 10%, transparent)"
|
|
15
|
+
const glow =
|
|
16
|
+
variant === "dark"
|
|
17
|
+
? "radial-gradient(48% 60% at 85% 12%, color-mix(in srgb, var(--primary-500) 10%, transparent) 0%, transparent 70%)"
|
|
18
|
+
: "radial-gradient(46% 58% at 88% 10%, color-mix(in srgb, var(--primary-500) 7%, transparent) 0%, transparent 72%)"
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div aria-hidden className={`absolute inset-0 overflow-hidden pointer-events-none ${className}`}>
|
|
22
|
+
{/* dotted lattice */}
|
|
23
|
+
<div
|
|
24
|
+
className="absolute inset-0"
|
|
25
|
+
style={{
|
|
26
|
+
backgroundImage: `radial-gradient(${dot} 1.2px, transparent 1.3px)`,
|
|
27
|
+
backgroundSize: "26px 26px",
|
|
28
|
+
maskImage:
|
|
29
|
+
"radial-gradient(70% 70% at 80% 8%, black 0%, transparent 75%)",
|
|
30
|
+
WebkitMaskImage:
|
|
31
|
+
"radial-gradient(70% 70% at 80% 8%, black 0%, transparent 75%)",
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
{/* soft glow */}
|
|
35
|
+
<div className="absolute inset-0" style={{ background: glow }} />
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|