@utopia-studio-design/design-system-cli 0.6.2 → 0.7.1

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.
Files changed (124) hide show
  1. package/README.md +28 -14
  2. package/bin/utopia-ds-mcp.mjs +6 -2
  3. package/bin/utopia-ds.mjs +94 -21
  4. package/data/docs/arabic-friendly.md +19 -2
  5. package/data/docs/billing-setup.md +80 -0
  6. package/data/docs/cer-003-032-033-round-8-repair.md +41 -0
  7. package/data/docs/cer-007-checkout.md +51 -0
  8. package/data/docs/cer-008-011-qa-repair.md +64 -0
  9. package/data/docs/cer-016-023-round-6-repair.md +92 -0
  10. package/data/docs/cer-024-031-round-7-repair.md +51 -0
  11. package/data/docs/cer-033-035-036-round-12-repair.md +37 -0
  12. package/data/docs/cer-035-037-round-10-repair.md +34 -0
  13. package/data/docs/ceramic-release-2026-09-06.md +11 -0
  14. package/data/docs/charts.md +34 -3
  15. package/data/docs/chat-service-methodology.md +181 -0
  16. package/data/docs/code-block.md +20 -0
  17. package/data/docs/creative-production.md +149 -0
  18. package/data/docs/designer-agent-backend-contract.md +552 -0
  19. package/data/docs/email-resend.md +29 -0
  20. package/data/docs/foundations.md +46 -8
  21. package/data/docs/guide.md +9 -9
  22. package/data/docs/handover-billing-aiden.md +132 -0
  23. package/data/docs/logo-maker-methodology.md +109 -0
  24. package/data/docs/presentation-asset-repository.md +53 -0
  25. package/data/docs/product-architecture.md +42 -0
  26. package/data/docs/quick-start-ai.md +4 -4
  27. package/data/docs/shadcn-conversion.md +1 -1
  28. package/data/docs/sidebar-placement.md +56 -0
  29. package/data/docs/theme-authoring.md +6 -3
  30. package/data/manifests/catalog.json +42 -46
  31. package/data/manifests/components.json +317 -66
  32. package/data/manifests/logo-skills.json +174 -0
  33. package/data/manifests/motion-profiles.json +2 -2
  34. package/data/manifests/patterns.json +201 -7
  35. package/data/manifests/presentation-assets.schema.json +44 -0
  36. package/data/manifests/templates.json +87 -1
  37. package/data/manifests/theme-dextrum.json +7 -7
  38. package/data/manifests/theme-prism-mono.json +19 -0
  39. package/data/manifests/theme-utopia-cloudblur.json +135 -0
  40. package/data/manifests/theme-utopia-default.json +31 -31
  41. package/data/manifests/theme-vyapti.json +17 -1
  42. package/data/manifests/theme-y2k-pop.json +18 -0
  43. package/data/manifests/themes.json +285 -57
  44. package/data/templates/analytics-dashboard/main.tsx +14 -1
  45. package/data/templates/calendar-application/main.tsx +14 -1
  46. package/data/templates/chat-workspace/README.md +12 -0
  47. package/data/templates/chat-workspace/index.html +11 -0
  48. package/data/templates/chat-workspace/main.tsx +97 -0
  49. package/data/templates/chat-workspace/styles.css +19 -0
  50. package/data/templates/chat-workspace/template.manifest.json +13 -0
  51. package/data/templates/composition-registry-lab/main.tsx +12 -1
  52. package/data/templates/dashboard-composer/main.tsx +14 -1
  53. package/data/templates/database-workspace/main.tsx +14 -1
  54. package/data/templates/interaction-lab/main.tsx +12 -1
  55. package/data/templates/quantum-verification-story/README.md +12 -0
  56. package/data/templates/quantum-verification-story/components.json +21 -0
  57. package/data/templates/quantum-verification-story/eslint.config.mjs +13 -0
  58. package/data/templates/quantum-verification-story/next-env.d.ts +7 -0
  59. package/data/templates/quantum-verification-story/next.config.mjs +11 -0
  60. package/data/templates/quantum-verification-story/package-lock.json +7535 -0
  61. package/data/templates/quantum-verification-story/package.json +46 -0
  62. package/data/templates/quantum-verification-story/postcss.config.js +6 -0
  63. package/data/templates/quantum-verification-story/src/app/assessment/page.tsx +138 -0
  64. package/data/templates/quantum-verification-story/src/app/ceramic-theme.css +48 -0
  65. package/data/templates/quantum-verification-story/src/app/deployment/page.tsx +178 -0
  66. package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Bold.ttf +0 -0
  67. package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Medium.ttf +0 -0
  68. package/data/templates/quantum-verification-story/src/app/fonts/IBMPlexSansArabic-Regular.ttf +0 -0
  69. package/data/templates/quantum-verification-story/src/app/fonts/ReadexPro-Variable.ttf +0 -0
  70. package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-350.ttf +0 -0
  71. package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-500.ttf +0 -0
  72. package/data/templates/quantum-verification-story/src/app/fonts/TWKLausanne-700.ttf +0 -0
  73. package/data/templates/quantum-verification-story/src/app/globals.css +175 -0
  74. package/data/templates/quantum-verification-story/src/app/layout.tsx +45 -0
  75. package/data/templates/quantum-verification-story/src/app/not-found.tsx +39 -0
  76. package/data/templates/quantum-verification-story/src/app/page.tsx +24 -0
  77. package/data/templates/quantum-verification-story/src/app/products/page.tsx +132 -0
  78. package/data/templates/quantum-verification-story/src/app/utopia-default.css +312 -0
  79. package/data/templates/quantum-verification-story/src/components/RotatingText.css +36 -0
  80. package/data/templates/quantum-verification-story/src/components/RotatingText.tsx +189 -0
  81. package/data/templates/quantum-verification-story/src/components/brand-mark.tsx +14 -0
  82. package/data/templates/quantum-verification-story/src/components/console-preview-section.tsx +57 -0
  83. package/data/templates/quantum-verification-story/src/components/console-preview.tsx +72 -0
  84. package/data/templates/quantum-verification-story/src/components/fade-up.tsx +54 -0
  85. package/data/templates/quantum-verification-story/src/components/floating-network.tsx +125 -0
  86. package/data/templates/quantum-verification-story/src/components/footer.tsx +83 -0
  87. package/data/templates/quantum-verification-story/src/components/harvest-timeline.tsx +70 -0
  88. package/data/templates/quantum-verification-story/src/components/lattice-diagram.tsx +73 -0
  89. package/data/templates/quantum-verification-story/src/components/lattice-field.tsx +129 -0
  90. package/data/templates/quantum-verification-story/src/components/nav.tsx +104 -0
  91. package/data/templates/quantum-verification-story/src/components/presentation-layout.tsx +91 -0
  92. package/data/templates/quantum-verification-story/src/components/readiness-estimator.tsx +135 -0
  93. package/data/templates/quantum-verification-story/src/components/scramble-text.tsx +60 -0
  94. package/data/templates/quantum-verification-story/src/components/section-backdrop.tsx +38 -0
  95. package/data/templates/quantum-verification-story/src/components/sections/clients.tsx +140 -0
  96. package/data/templates/quantum-verification-story/src/components/sections/contact.tsx +125 -0
  97. package/data/templates/quantum-verification-story/src/components/sections/hero.tsx +92 -0
  98. package/data/templates/quantum-verification-story/src/components/sections/moat.tsx +73 -0
  99. package/data/templates/quantum-verification-story/src/components/sections/offer.tsx +113 -0
  100. package/data/templates/quantum-verification-story/src/components/sections/pilot.tsx +63 -0
  101. package/data/templates/quantum-verification-story/src/components/sections/problem.tsx +53 -0
  102. package/data/templates/quantum-verification-story/src/components/text-particle.tsx +204 -0
  103. package/data/templates/quantum-verification-story/src/components/ticker.tsx +27 -0
  104. package/data/templates/quantum-verification-story/src/components/ui/badge.tsx +29 -0
  105. package/data/templates/quantum-verification-story/src/components/ui/button.tsx +55 -0
  106. package/data/templates/quantum-verification-story/src/components/ui/card.tsx +46 -0
  107. package/data/templates/quantum-verification-story/src/components/ui/cursor-driven-particle-typography.tsx +288 -0
  108. package/data/templates/quantum-verification-story/src/components/ui/input.tsx +20 -0
  109. package/data/templates/quantum-verification-story/src/components/ui/label.tsx +21 -0
  110. package/data/templates/quantum-verification-story/src/components/ui/separator.tsx +25 -0
  111. package/data/templates/quantum-verification-story/src/components/ui/textarea.tsx +19 -0
  112. package/data/templates/quantum-verification-story/src/lib/utils.ts +6 -0
  113. package/data/templates/quantum-verification-story/tailwind.config.js +95 -0
  114. package/data/templates/quantum-verification-story/template.manifest.json +23 -0
  115. package/data/templates/quantum-verification-story/tsconfig.json +42 -0
  116. package/data/templates/saas-solution-homepage/main.tsx +91 -14
  117. package/data/templates/saas-solution-homepage/privacy/index.html +1 -0
  118. package/data/templates/saas-solution-homepage/styles.css +47 -2
  119. package/data/templates/saas-solution-homepage/team/index.html +1 -0
  120. package/data/templates/saas-solution-homepage/template.manifest.json +41 -0
  121. package/data/templates/saas-solution-homepage/terms/index.html +1 -0
  122. package/lib/api.mjs +52 -3
  123. package/lib/template-runtime.mjs +56 -0
  124. 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&apos;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&apos;s algorithm, the very attack that shatters RSA and ECC, simply
66
+ doesn&apos;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
+ }