diff --git a/.hallmark/log.json b/.hallmark/log.json new file mode 100644 index 000000000..1a6bfadfc --- /dev/null +++ b/.hallmark/log.json @@ -0,0 +1,20 @@ +[ + { + "date": "2026-07-31", + "macrostructure": "Workbench", + "theme": "Aurora", + "enrichment": "product-demo + interactive-dot-field", + "brief": "ArkEnv landing · interactive faded dots (open-slide DNA)", + "nav": "N5", + "footer": "Ft5" + }, + { + "date": "2026-07-31", + "macrostructure": "Workbench", + "theme": "Aurora", + "enrichment": "product-demo", + "brief": "ArkEnv landing · Aurora redesign", + "nav": "N5", + "footer": "Ft5" + } +] diff --git a/.hallmark/preflight.json b/.hallmark/preflight.json new file mode 100644 index 000000000..ab4fe12b8 --- /dev/null +++ b/.hallmark/preflight.json @@ -0,0 +1,22 @@ +{ + "scannedAt": "2026-07-31", + "fontStack": "Geist + JetBrains Mono (next/font)", + "palette": "fumadocs ocean + custom light/dark theme CSS", + "motion": "CSS keyframes only (marquee, spin) — no framer-motion", + "spacing": "Tailwind 4 defaults", + "framework": "Next.js app router (apps/www)", + "preserve": [ + "Geist display", + "JetBrains Mono", + "fumadocs docs theming", + "home route ownership", + "product copy" + ], + "introduce": [ + "Aurora tokens scoped to home", + "Sentient body", + "Workbench macrostructure", + "N5/Ft5", + "microinteraction discipline" + ] +} diff --git a/apps/www/app/(home)/aurora.css b/apps/www/app/(home)/aurora.css new file mode 100644 index 000000000..d112ac2f8 --- /dev/null +++ b/apps/www/app/(home)/aurora.css @@ -0,0 +1,2354 @@ +/* Hallmark · genre: atmospheric · macrostructure: Workbench · tone: atmospheric + * theme: Aurora · anchor hue: 200 · nav: N5 · footer: Ft5 + * enrichment: product-demo + interactive-dot-field (open-slide DNA) + * axes: dark / geist-sans + geist-mono + geist-pixel-grid / cool-cyan + * fonts: Geist · Geist Mono · Geist Pixel Grid + * Pre-emit critique: P4 H4 E4 S4 R4 V5 + */ + +@import "../../tokens.css"; + +/* Load-time rise (open-slide). `backwards` avoids pinning filter after the run. */ +@keyframes home-aurora-rise { + from { + opacity: 0; + transform: translateY(14px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes home-aurora-rise-blur { + from { + opacity: 0; + transform: translateY(16px); + filter: blur(8px); + } + to { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + +.home-aurora .rise { + animation: home-aurora-rise 900ms var(--ease-out-strong) backwards; +} + +.home-aurora .rise-blur { + animation: home-aurora-rise-blur 1s var(--ease-out-strong) backwards; +} + +/* Scroll reveals - ScrollReveal only adds .reveal-hidden below the fold */ +.home-aurora [data-reveal].reveal-hidden { + opacity: 0; + transform: translateY(18px); +} + +.home-aurora [data-reveal="blur"].reveal-hidden { + filter: blur(7px); +} + +.home-aurora [data-reveal="fade"].reveal-hidden { + transform: none; +} + +.home-aurora [data-reveal].reveal-shown { + transition: + opacity var(--dur-reveal) var(--ease-out-strong), + transform var(--dur-reveal) var(--ease-out-strong), + filter 700ms var(--ease-out-strong); + transition-delay: var(--reveal-delay, 0ms); +} + +.home-aurora { + --background: var(--color-paper); + --foreground: var(--color-ink); + --primary: var(--color-accent); + --primary-foreground: var(--color-accent-ink); + --muted-foreground: var(--color-muted); + --border: var(--color-rule); + --ring: var(--color-focus); + --fd-primary: var(--color-accent); + --color-fd-primary: var(--color-accent); + + position: relative; + isolation: isolate; + min-height: 100dvh; + color: var(--color-ink); + background-color: var(--color-paper); + font-family: var(--font-body); + overflow-x: clip; +} + +.home-aurora__atmosphere { + position: absolute; + inset: 0 0 auto; + height: min(48rem, 85vh); + z-index: 0; + pointer-events: none; + overflow: hidden; + background-image: radial-gradient(ellipse 42% 36% at 12% 18%, var(--color-bloom-a), transparent 70%), radial-gradient(ellipse 38% 32% at 88% 8%, var(--color-bloom-b), transparent 72%); + /* Strong near the header, gone by the workbench */ + mask-image: linear-gradient(to bottom, #000 0%, #000 28%, rgb(0 0 0 / 0.55) 48%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 28%, rgb(0 0 0 / 0.55) 48%, transparent 100%); +} + +.home-aurora > :not(.home-aurora__atmosphere) { + position: relative; + z-index: 1; +} + +/* Static CSS dots until the canvas hydrates and clears the image */ +.home-aurora__dot-grid { + display: block; + width: 100%; + height: 100%; + color: color-mix(in oklch, var(--color-ink) 14%, transparent); + background-image: radial-gradient(currentColor 1px, transparent 1.5px); + background-size: 24px 24px; +} + +@media (prefers-reduced-motion: reduce) { + .home-aurora__dot-grid { + /* Keep the static field; physics stays off in the component */ + opacity: 0.85; + } + + .home-aurora .rise, + .home-aurora .rise-blur { + animation: none; + } + + .home-aurora__validator-body { + animation: none; + } + + .home-aurora [data-reveal].reveal-hidden, + .home-aurora [data-reveal="blur"].reveal-hidden { + opacity: 1; + transform: none; + filter: none; + } +} + +.home-aurora :is(h1, h2, h3) { + font-style: normal; + font-weight: 600; + letter-spacing: -0.03em; + font-family: var(--font-display); + overflow-wrap: anywhere; + min-width: 0; +} + +.home-aurora__wordmark { + display: inline-flex; + align-items: center; + flex-shrink: 0; + font-family: var(--font-body); + font-weight: 500; + font-style: normal; + letter-spacing: -0.01em; + color: var(--color-ink); + text-decoration: none; + white-space: nowrap; +} + +.home-aurora__wordmark span { + font-family: var(--font-body); + font-weight: 500; + font-size: var(--text-md); + letter-spacing: -0.02em; + white-space: nowrap; +} + +/* N5 · Floating pill */ +.home-aurora__nav { + position: fixed; + inset: var(--space-sm) auto auto 50%; + transform: translateX(-50%); + z-index: 40; + display: inline-flex; + align-items: center; + gap: var(--space-sm); + max-width: calc(100vw - 2 * var(--page-gutter)); + padding: 0.4rem 0.55rem 0.4rem 0.85rem; + background: color-mix(in oklch, var(--color-paper) 72%, transparent); + backdrop-filter: blur(14px) saturate(120%); + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-pill); + box-shadow: var(--shadow-nav); +} + +@media (min-width: 40rem) { + .home-aurora__nav { + inset-block-start: var(--space-md); + } +} + +.home-aurora__nav-links { + display: none; + list-style: none; + margin: 0; + padding: 0; + gap: var(--space-xs); +} + +@media (min-width: 40rem) { + .home-aurora__nav-links { + display: flex; + } +} + +.home-aurora__nav-links a { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.65rem; + border-radius: var(--radius-pill); + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 500; + text-decoration: none; + white-space: nowrap; + transition: + color var(--dur-micro) var(--ease-out), + background-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__nav-links a:hover { + color: var(--color-ink); + background: color-mix(in oklch, var(--color-paper-3) 70%, transparent); +} + +.home-aurora__nav-links a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__nav-actions { + display: flex; + align-items: center; + gap: var(--space-xs); + margin-left: auto; +} + +.home-aurora__nav-utils { + display: flex; + align-items: center; + gap: 0.15rem; +} + +.home-aurora__nav-github { + display: none; + height: 2rem; + padding: 0 0.55rem 0 0.45rem; + border-radius: var(--radius-pill); + color: var(--color-ink-2); + text-decoration: none; + gap: 0.4rem; +} + +@media (min-width: 48rem) { + .home-aurora__nav-github { + display: inline-flex; + } +} + +.home-aurora__nav-github:hover { + color: var(--color-ink); + background: color-mix(in oklch, var(--color-paper-3) 70%, transparent); +} + +.home-aurora__nav-github:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__nav-github svg:first-child { + width: 1.05rem; + height: 1.05rem; +} + +/* Keep the floating pill content-sized - icon search only */ +.home-aurora__nav [data-search-full] { + display: none !important; +} + +.home-aurora__nav [data-search] { + display: inline-flex !important; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: var(--radius-pill); + color: var(--color-ink-2); +} + +.home-aurora__nav [data-search]:hover { + color: var(--color-ink); + background: color-mix(in oklch, var(--color-paper-3) 70%, transparent); +} + +.home-aurora__nav [data-search]:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__nav-cta { + display: none; + align-items: center; + padding: 0.45rem 0.95rem; + border-radius: var(--radius-pill); + background: linear-gradient(135deg, oklch(82% 0.14 190) 0%, oklch(70% 0.15 205) 100%); + color: oklch(12% 0.02 200); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 600; + text-decoration: none; + white-space: nowrap; + box-shadow: + 0 0 0 1px oklch(90% 0.1 195 / 0.3) inset, + 0 2px 10px oklch(74% 0.13 195 / 0.25); + transition: + transform var(--dur-micro) var(--ease-out), + box-shadow var(--dur-short) var(--ease-out), + filter var(--dur-short) var(--ease-out); +} + +@media (min-width: 40rem) { + .home-aurora__nav-cta { + display: inline-flex; + } +} + +.home-aurora__nav-cta:hover { + filter: brightness(1.08); + box-shadow: + 0 0 0 1px oklch(95% 0.1 195 / 0.5) inset, + 0 4px 18px oklch(74% 0.16 195 / 0.45); + transform: translateY(-1px); +} + +.home-aurora__nav-cta:hover span { + transform: translateX(3px); +} + +.home-aurora__nav-cta span { + display: inline-block; + margin-left: 0.2rem; + transition: transform var(--dur-short) var(--ease-out); +} + +.home-aurora__nav-cta:active { + transform: translateY(1px); +} + +.home-aurora__nav-cta:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__nav-cta[aria-disabled="true"], +.home-aurora__nav-cta:disabled { + opacity: 0.45; + pointer-events: none; +} + +.home-aurora__shell { + width: min(90rem, 100%); + max-width: 100%; + min-width: 0; + margin-inline: auto; + /* Mobile: clear the floating pill without a tall empty band */ + padding: calc(3.75rem + var(--space-xs)) clamp(0.75rem, 2.5vw, 1.75rem) var(--space-md); +} + +@media (min-width: 40rem) { + .home-aurora__shell { + padding-top: calc(var(--space-3xl) + var(--space-sm)); + } +} + +/* Workbench · hanging intro */ +.home-aurora__intro { + display: grid; + gap: var(--space-sm); + width: 100%; + max-width: 100%; + min-width: 0; + padding-block: var(--space-2xs) var(--space-md); +} + +/* Marquee rails (and other wide kids) must not inflate the grid track */ +.home-aurora__intro > * { + min-width: 0; + max-width: 100%; +} + +@media (min-width: 40rem) { + .home-aurora__intro { + gap: var(--space-md); + padding-block: var(--space-xl) var(--space-lg); + } +} + +.home-aurora__badge { + justify-self: start; +} + +.home-aurora__announce { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.25rem 0.25rem 0.25rem 0.25rem; + border-radius: var(--radius-pill); + border: var(--rule-hair) solid color-mix(in oklch, var(--color-accent) 35%, transparent); + background: color-mix(in oklch, var(--color-accent) 10%, transparent); + color: var(--color-ink); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 500; + text-decoration: none; + backdrop-filter: blur(8px); + transition: + background-color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__announce:hover { + background: color-mix(in oklch, var(--color-accent) 16%, transparent); + border-color: color-mix(in oklch, var(--color-accent) 50%, transparent); +} + +.home-aurora__announce:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__announce-new { + height: 1.25rem; + padding-inline: 0.45rem; + border-radius: var(--radius-pill); + border: var(--rule-hair) solid color-mix(in oklch, var(--color-accent) 30%, transparent); + background: color-mix(in oklch, var(--color-accent) 22%, transparent); + color: var(--color-accent); + font-size: var(--text-xs); + font-weight: 600; + box-shadow: none; +} + +@media (max-width: 40rem) { + .home-aurora__announce-new { + width: 0.5rem; + height: 0.5rem; + min-width: 0.5rem; + padding: 0; + margin-inline-start: 0.4rem; + border: none; + background: var(--color-accent); + border-radius: 50%; + box-shadow: 0 0 8px color-mix(in oklch, var(--color-accent) 60%, transparent); + } + + .home-aurora__announce-label { + margin-inline-start: 0.25rem; + } +} + +.home-aurora__announce-new:hover { + background: color-mix(in oklch, var(--color-accent) 30%, transparent); +} + +.home-aurora__announce-label { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-inline-end: 0.35rem; +} + +.home-aurora__announce-label--solo { + margin-inline-start: 0.5rem; +} + +.home-aurora__announce-arrow { + width: 1rem; + height: 1rem; + opacity: 0.45; + transition: + opacity var(--dur-micro) var(--ease-out), + transform var(--dur-micro) var(--ease-out); +} + +.home-aurora__announce:hover .home-aurora__announce-arrow { + opacity: 1; +} + +.home-aurora__announce:hover .home-aurora__announce-arrow--right { + transform: translateX(0.125rem); +} + +.home-aurora__intro h1, +.home-aurora__tagline { + margin: 0; + max-width: 100%; + min-width: 0; + font-family: var(--font-display); + font-size: clamp(2.5rem, 9.5vw + 0.5rem, 5.85rem); + line-height: 1.05; + letter-spacing: -0.035em; + color: var(--color-ink); + font-weight: 500; + overflow-wrap: anywhere; +} + +/* Keep the Geist lead on one line once the viewport can host it */ +@media (min-width: 75rem) { + .home-aurora__lead { + white-space: nowrap; + } +} + +.home-aurora__digital { + font-family: var(--font-geist-pixel-grid), "Geist Pixel Grid", ui-monospace, monospace; + font-weight: 500; + letter-spacing: -0.06em; + color: inherit; +} + +/* Allow wrapping between the two words when the viewport is too narrow for single line */ +.home-aurora__digital-accent { + color: var(--color-accent); + display: inline; + white-space: normal; +} + +@media (min-width: 62rem) { + .home-aurora__digital-accent { + white-space: nowrap; + } +} + +.home-aurora__summary { + margin: 0; + max-width: min(54ch, 100%); + min-width: 0; + font-family: var(--font-body); + font-size: clamp(1.05rem, 1vw + 0.8rem, 1.25rem); + line-height: 1.5; + color: oklch(72% 0.02 200); + font-weight: 400; + overflow-wrap: anywhere; +} + +.home-aurora__summary code { + font-family: var(--font-mono); + font-size: 0.9em; + font-weight: 400; + color: var(--color-ink-2); + padding: 0.1em 0.35em; + border-radius: 0.25rem; + background: color-mix(in oklch, var(--color-paper-3) 60%, transparent); + border: var(--rule-hair) solid var(--color-rule); +} + +.home-aurora__install-row { + display: grid; + gap: var(--space-sm); + margin-top: var(--space-2xs); + width: 100%; + max-width: 100%; +} + +@media (min-width: 40rem) { + .home-aurora__install-row { + width: fit-content; + } +} + +.home-aurora__install-row--outro { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: var(--space-sm); +} + +/* FAQ - end-of-page accordion: +/− spin · panel expand · docs SoT */ +.home-aurora__faq-section { + margin-top: var(--space-2xl); + display: grid; + gap: var(--space-md); + scroll-margin-top: 6rem; +} + +.home-aurora__faq-head { + display: grid; + gap: var(--space-2xs); + max-width: 36rem; +} + +.home-aurora__faq-head h2 { + margin: 0; + font-size: var(--text-display-s); + line-height: 1.1; + color: var(--color-ink); + font-style: normal; +} + +.home-aurora__faq-head p { + margin: 0; + font-family: var(--font-body); + font-size: clamp(1rem, 0.4vw + 0.85rem, 1.15rem); + line-height: 1.5; + color: var(--color-ink-2); +} + +.home-aurora__faq-head a, +.home-aurora__faq-more a { + color: var(--color-accent); + text-decoration: none; + border-bottom: var(--rule-hair) solid color-mix(in oklch, var(--color-accent) 45%, transparent); + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__faq-head a:hover, +.home-aurora__faq-more a:hover { + color: var(--color-ink); + border-color: var(--color-accent); +} + +.home-aurora__faq-head a:focus-visible, +.home-aurora__faq-more a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__faq { + display: grid; + gap: 0; + width: 100%; + max-width: 42rem; + border-top: var(--rule-hair) solid var(--color-rule); +} + +.home-aurora__faq-item { + border-bottom: var(--rule-hair) solid var(--color-rule); + scroll-margin-top: 6rem; +} + +.home-aurora__faq-trigger { + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-sm); + width: 100%; + padding: 1.05rem 0; + border: 0; + background: transparent; + font-family: var(--font-display); + font-size: clamp(1rem, 0.4vw + 0.85rem, 1.15rem); + font-weight: 500; + color: var(--color-ink); + text-align: left; + transition: color var(--dur-micro) var(--ease-out); +} + +.home-aurora__faq-trigger:hover { + color: var(--color-accent); +} + +.home-aurora__faq-trigger:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__faq-trigger:active { + opacity: 0.92; +} + +/* + morphs to −: 90° spin; hide the bar that would become | after rotate */ +.home-aurora__faq-icon { + position: relative; + display: block; + flex-shrink: 0; + width: 0.75rem; + height: 0.75rem; + color: var(--color-muted); + transition: + transform var(--dur-short) var(--ease-out), + color var(--dur-micro) var(--ease-out); +} + +.home-aurora__faq-icon-h, +.home-aurora__faq-icon-v { + position: absolute; + left: 50%; + top: 50%; + border-radius: 1px; + background: currentColor; + transform: translate(-50%, -50%); + transition: + opacity var(--dur-short) var(--ease-out), + transform var(--dur-short) var(--ease-out); +} + +.home-aurora__faq-icon-h { + width: 100%; + height: 1.5px; +} + +.home-aurora__faq-icon-v { + width: 1.5px; + height: 100%; +} + +.home-aurora__faq-item[data-open="true"] .home-aurora__faq-icon { + color: var(--color-accent); + transform: rotate(90deg); +} + +/* After 90°, the vertical bar is horizontal (= −); drop the old horizontal */ +.home-aurora__faq-item[data-open="true"] .home-aurora__faq-icon-h { + opacity: 0; + transform: translate(-50%, -50%) scaleX(0); +} + +.home-aurora__faq-panel { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transition: + grid-template-rows var(--dur-long) var(--ease-out), + opacity var(--dur-short) var(--ease-out); +} + +.home-aurora__faq-item[data-open="true"] .home-aurora__faq-panel { + grid-template-rows: 1fr; + opacity: 1; + transition: + grid-template-rows var(--dur-long) var(--ease-out), + opacity var(--dur-short) var(--ease-out); +} + +.home-aurora__faq-panel-inner { + overflow: hidden; + min-height: 0; +} + +.home-aurora__faq-panel-inner p { + margin: 0 0 var(--space-sm); + max-width: 48ch; + font-family: var(--font-body); + font-size: clamp(0.95rem, 0.35vw + 0.8rem, 1.05rem); + font-weight: 400; + line-height: 1.55; + color: color-mix(in oklch, var(--color-ink-2) 80%, var(--color-muted)); + transform: translateY(-0.35rem); + opacity: 0.4; + transition: + transform var(--dur-long) var(--ease-out), + opacity var(--dur-long) var(--ease-out); +} + +.home-aurora__faq-item[data-open="true"] .home-aurora__faq-panel-inner p { + transform: none; + opacity: 1; +} + +.home-aurora__faq-more { + margin-top: calc(var(--space-2xs) * -1); +} + +.home-aurora__faq-panel-inner code { + font-family: var(--font-outlier); + font-size: 0.92em; + color: var(--color-ink); + padding: 0.05em 0.3em; + border-radius: 0.25rem; + background: color-mix(in oklch, var(--color-paper-3) 80%, transparent); + border: var(--rule-hair) solid var(--color-rule); +} + +/* Mobile secondary CTA - below Star us; hidden with install panel on ≥40rem */ +.home-aurora__docs-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + width: 100%; + min-height: 3rem; + padding: 0.75rem 1.1rem; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-pill); + background: color-mix(in oklch, var(--color-paper-2) 90%, transparent); + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 500; + text-decoration: none; + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out), + background-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__docs-cta:hover { + color: var(--color-ink); + border-color: color-mix(in oklch, var(--color-accent) 40%, var(--color-rule)); + background: var(--color-paper-3); +} + +.home-aurora__docs-cta:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__docs-cta:active { + opacity: 0.9; +} + +@media (min-width: 40rem) { + .home-aurora__docs-cta { + display: none; + } +} + +/* Install · Command / Prompt tabs - desktop/tablet only */ +.home-aurora__install { + display: none; + gap: var(--space-xs); + width: fit-content; + max-width: 100%; +} + +@media (min-width: 40rem) { + .home-aurora__install { + display: grid; + } +} + +.home-aurora__install-tabs { + display: flex; + align-items: center; + justify-content: flex-start; + gap: var(--space-xs); + width: 100%; + padding: 0; + border: 0; + background: transparent; +} + +.home-aurora__install-tab { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.4rem; + border: 0; + border-radius: 0; + background: transparent; + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 600; + letter-spacing: 0.01em; + cursor: pointer; + white-space: nowrap; + opacity: 0.7; + transition: + color var(--dur-micro) var(--ease-out), + opacity var(--dur-micro) var(--ease-out); +} + +.home-aurora__install-tab:hover { + color: var(--color-ink); + opacity: 1; +} + +.home-aurora__install-tab[data-active="true"] { + background: transparent; + color: var(--color-accent); + opacity: 1; + box-shadow: inset 0 -2px 0 var(--color-accent); +} + +.home-aurora__install-tab:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__install-copy { + display: flex; + align-items: center; + gap: var(--space-md); + width: max-content; + max-width: 100%; + min-height: 3rem; + padding: 0.65rem 1.15rem 0.65rem 1.25rem; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: color-mix(in oklch, var(--color-paper-2) 88%, transparent); + color: var(--color-ink); + text-align: left; + cursor: pointer; + transition: + border-color var(--dur-short) var(--ease-out), + background-color var(--dur-short) var(--ease-out), + transform var(--dur-micro) var(--ease-out); +} + +.home-aurora__install-copy:hover { + border-color: color-mix(in oklch, var(--color-accent) 45%, var(--color-rule)); + background: var(--color-paper-3); +} + +.home-aurora__install-copy:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__install-copy:active { + transform: translateY(1px); +} + +.home-aurora__install-copy[data-state="success"] { + border-color: var(--color-success); +} + +/* Command ↔ Prompt: box stays put; only label/icon crossfade */ +.home-aurora__install-swap { + position: relative; + display: grid; + grid-template-areas: "swap"; + flex: 0 1 auto; + min-width: 0; + align-items: center; +} + +.home-aurora__install-swap-item { + grid-area: swap; + display: flex; + align-items: center; + gap: var(--space-sm); + min-width: 0; + opacity: 0; + transform: translateY(0.25rem); + visibility: hidden; + pointer-events: none; + transition: + opacity var(--dur-short) var(--ease-out), + transform var(--dur-short) var(--ease-out), + visibility var(--dur-short) var(--ease-out); +} + +.home-aurora__install-swap-item[data-active="true"] { + opacity: 1; + transform: none; + visibility: visible; + pointer-events: auto; +} + +.home-aurora__install-icon { + width: 0.9rem; + height: 0.9rem; + flex-shrink: 0; + color: var(--color-accent); + opacity: 0.75; + stroke-width: 1.5px; +} + +.home-aurora__install-prompt-symbol { + font-family: var(--font-outlier); + font-size: 0.9rem; + font-weight: 400; + line-height: 1; + flex-shrink: 0; + color: var(--color-accent); + opacity: 0.75; + user-select: none; +} + +.home-aurora__install-code { + flex: 1; + min-width: 0; + font-family: var(--font-outlier); + font-size: clamp(0.8125rem, 0.25vw + 0.8rem, 0.9375rem); + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.home-aurora__install-label { + flex: 1; + min-width: 0; + font-family: var(--font-display); + font-size: clamp(0.8125rem, 0.25vw + 0.8rem, 0.9375rem); + font-weight: 400; + line-height: 1.4; + white-space: nowrap; +} + +.home-aurora__install-prompt-token { + color: var(--color-accent); +} + +.home-aurora__install-copy-affordance { + display: inline-flex; + align-items: center; + gap: 0.4rem; + flex-shrink: 0; + padding-left: var(--space-md); + border-left: var(--rule-hair) solid var(--color-rule); + color: var(--color-muted); + align-self: stretch; +} + +/* C3 · Typographic primary CTA (nav / legacy) */ +.home-aurora__cta { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-family: var(--font-display); + font-size: var(--text-md); + font-weight: 600; + color: var(--color-ink); + text-decoration: none; + border-bottom: var(--rule-hair) solid var(--color-accent); + padding-block: 0.15rem; + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out), + opacity var(--dur-micro) var(--ease-out); +} + +.home-aurora__cta:hover { + color: var(--color-accent); +} + +.home-aurora__cta:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +.home-aurora__cta:active { + opacity: 0.85; +} + +.home-aurora__cta[aria-disabled="true"], +.home-aurora__cta:disabled { + opacity: 0.4; + pointer-events: none; +} + +.home-aurora__cta[data-state="loading"] { + opacity: 0.7; +} + +.home-aurora__cta[data-state="error"] { + border-color: var(--color-danger); + color: var(--color-danger-ink); +} + +.home-aurora__cta[data-state="success"] { + border-color: var(--color-success); + color: var(--color-success); +} + +.home-aurora__bench { + margin-top: var(--space-xl); + display: grid; + gap: var(--space-md); +} + +@media (min-width: 40rem) { + .home-aurora__bench { + margin-top: var(--space-2xl); + } +} + +.home-aurora__bench-head { + display: grid; + gap: var(--space-2xs); + max-width: 36rem; +} + +.home-aurora__bench-head h2 { + margin: 0; + font-size: var(--text-display-s); + line-height: 1.1; + color: var(--color-ink); +} + +.home-aurora__bench-head p { + margin: 0; + color: var(--color-ink-2); + font-size: var(--text-md); + line-height: 1.5; + max-width: 42ch; +} + +/* Pitch sections - code-first proof */ +.home-aurora__pitch { + margin-top: var(--space-3xl); + display: grid; + gap: var(--space-md); + scroll-margin-top: 6rem; + width: 100%; + max-width: 100%; + min-width: 0; +} + +.home-aurora__pitch-head { + display: grid; + gap: var(--space-2xs); + max-width: 40rem; +} + +.home-aurora__pitch-label { + margin: 0; + font-family: var(--font-outlier); + font-size: var(--text-xs); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-muted); +} + +.home-aurora__pitch-head h2 { + margin: 0; + font-size: var(--text-display-s); + line-height: 1.1; + color: var(--color-ink); +} + +.home-aurora__pitch-head h2 a { + color: inherit; + text-decoration: none; + border-bottom: var(--rule-hair) solid color-mix(in oklch, var(--color-ink) 28%, transparent); + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__pitch-head h2 a:hover { + border-color: var(--color-accent); +} + +.home-aurora__pitch-head h2 a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +.home-aurora__pitch-struck { + color: color-mix(in oklch, var(--color-ink) 55%, transparent); +} + +.home-aurora__pitch-struck s { + text-decoration-thickness: 1.5px; + text-decoration-color: color-mix(in oklch, var(--color-ink) 45%, transparent); + color: inherit; +} + +.home-aurora__pitch-head p { + margin: 0; + color: var(--color-ink-2); + font-size: var(--text-md); + line-height: 1.5; + max-width: 46ch; +} + +.home-aurora__pitch-head p a { + color: var(--color-accent); + text-decoration: none; + border-bottom: var(--rule-hair) solid color-mix(in oklch, var(--color-accent) 45%, transparent); + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__pitch-head p a:hover { + color: var(--color-ink); + border-color: var(--color-accent); +} + +.home-aurora__pitch-head p a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__compare-stat { + margin: var(--space-2xs) 0 0 !important; + font-family: var(--font-outlier); + font-size: var(--text-sm) !important; + letter-spacing: 0.02em; + color: var(--color-muted) !important; + max-width: none !important; +} + +.home-aurora__compare-stat span { + color: var(--color-accent); + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.home-aurora__pitch-head code { + font-family: var(--font-mono); + font-size: 0.92em; + font-weight: 500; + color: var(--color-ink); + padding: 0.1em 0.35em; + border-radius: 0.25rem; + background: color-mix(in oklch, var(--color-paper-3) 80%, transparent); + border: var(--rule-hair) solid var(--color-rule); +} + +/* Before / after compare - window frame + drag reveal */ +.home-aurora__compare-toggle { + display: none; +} + +.home-aurora__compare-window { + width: 100%; + max-width: min(56rem, 100%); + min-width: 0; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: var(--color-paper-2); + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; +} + +.home-aurora__compare { + --compare: 52%; + position: relative; + isolation: isolate; + width: 100%; + min-width: 0; + background: var(--color-paper-2); + overflow: hidden; + user-select: none; + touch-action: none; +} + +/* Drives frame height from the longer (old-way) snippet */ +.home-aurora__compare-sizer { + visibility: hidden; + pointer-events: none; + padding: var(--space-md) calc(var(--space-md) + 0.35rem); + display: grid; + align-content: start; + gap: var(--space-xs); + min-width: 0; +} + +.home-aurora__compare-pane { + position: absolute; + inset: 0; + padding: var(--space-md) calc(var(--space-md) + 0.35rem); + display: grid; + align-content: start; + gap: var(--space-xs); + min-width: 0; +} + +.home-aurora__compare-pane--before { + z-index: 2; + clip-path: inset(0 calc(100% - var(--compare)) 0 0); + background: var(--color-paper); +} + +.home-aurora__compare-pane--after { + z-index: 1; +} + +/* Shiki (github-*-high-contrast) — shared with 01 DECLARATIVE */ +.home-aurora__shiki { + min-width: 0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.home-aurora__shiki .shiki, +.home-aurora__compare .shiki { + margin: 0; + padding: var(--space-md); + overflow: auto; + background: transparent !important; + font-family: var(--font-outlier); + font-size: clamp(0.78rem, 0.2vw + 0.72rem, 0.9rem); + line-height: 1.55; + white-space: pre; + max-width: 100%; + min-width: 0; + tab-size: 2; +} + +.home-aurora__compare .shiki { + padding: 0; +} + +.home-aurora__shiki .shiki code, +.home-aurora__compare .shiki code { + display: grid; + font-family: inherit; + font-size: inherit; + line-height: inherit; + background: transparent; +} + +.home-aurora__shiki .shiki .line, +.home-aurora__compare .shiki .line { + padding-left: 0; + padding-right: 0; +} + +.home-aurora__shiki .shiki .line:empty, +.home-aurora__compare .shiki .line:empty { + height: 1.55em; +} + +/* Hand-rolled tokens aligned to github-dark-high-contrast */ +.home-aurora__tok-kw { + color: #ff9492; +} + +.home-aurora__tok-id { + color: #91cbff; +} + +.home-aurora__tok-fn { + color: #dbb7ff; +} + +.home-aurora__tok-str { + color: #addcff; +} + +.home-aurora__tok-punct { + color: #f0f3f6; +} + +.home-aurora__tok-muted { + color: #bdc4cc; +} + +.home-aurora__tok-ok { + color: var(--color-success); +} + +/* Arc-style split: hairline + soft side bloom + thin | grip */ +.home-aurora__compare-handle { + position: absolute; + top: 0; + bottom: 0; + left: var(--compare); + z-index: 3; + width: 1px; + background: color-mix(in oklch, var(--color-ink) 35%, var(--color-rule)); + pointer-events: none; + transform: translateX(-50%); + box-shadow: + -1px 0 0 color-mix(in oklch, var(--color-accent) 28%, transparent), + 1px 0 0 color-mix(in oklch, var(--color-accent) 40%, transparent), + 0 0 12px color-mix(in oklch, var(--color-accent) 22%, transparent); +} + +.home-aurora__compare-grip { + position: absolute; + top: 50%; + left: 50%; + width: 3px; + height: 1.75rem; + border-radius: 999px; + background: color-mix(in oklch, var(--color-ink) 72%, var(--color-muted)); + box-shadow: 0 0 0 1px color-mix(in oklch, var(--color-paper) 55%, transparent); + transform: translate(-50%, -50%); +} + +.home-aurora__compare-range { + position: absolute; + inset: 0; + z-index: 4; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: ew-resize; + appearance: none; + background: transparent; +} + +.home-aurora__compare-range:focus-visible { + opacity: 1; + outline: 2px solid var(--color-focus); + outline-offset: -2px; +} + +@media (max-width: 39.99rem) { + /* One snippet at a time - toggle instead of stacked panes / clip slider */ + .home-aurora__compare-toggle { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-xs); + margin-top: calc(var(--space-2xs) * -1); + } + + .home-aurora__compare-toggle-btn { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.15rem; + border: 0; + border-radius: 0; + background: transparent; + color: var(--color-muted); + font-family: var(--font-display); + font-size: var(--text-xs); + font-weight: 500; + letter-spacing: 0.02em; + cursor: pointer; + white-space: nowrap; + opacity: 0.75; + transition: + color var(--dur-micro) var(--ease-out), + opacity var(--dur-micro) var(--ease-out); + } + + .home-aurora__compare-toggle-btn:hover { + color: var(--color-ink-2); + opacity: 1; + } + + .home-aurora__compare-toggle-btn[data-active="true"] { + color: var(--color-ink-2); + opacity: 1; + box-shadow: inset 0 -1px 0 color-mix(in oklch, var(--color-accent) 55%, transparent); + } + + .home-aurora__compare-toggle-btn:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; + } + + .home-aurora__compare { + --compare: 100%; + min-height: 0; + display: block; + touch-action: auto; + } + + .home-aurora__compare-sizer { + display: none; + } + + .home-aurora__compare-pane { + position: relative; + inset: auto; + clip-path: none; + } + + .home-aurora__compare[data-mobile-view="arkenv"] .home-aurora__compare-pane--before, + .home-aurora__compare[data-mobile-view="old"] .home-aurora__compare-pane--after { + display: none; + } + + .home-aurora__compare-pane--before { + z-index: auto; + background: var(--color-paper); + } + + .home-aurora__compare-pane--after { + z-index: auto; + } + + .home-aurora__compare-handle, + .home-aurora__compare-range { + display: none; + } + + .home-aurora__compare .shiki, + .home-aurora__shiki .shiki { + white-space: pre-wrap; + overflow-wrap: anywhere; + overflow: visible; + } +} + +/* IDE autocomplete mock - stacked chrome / code / menu (no absolute overlay) */ +.home-aurora__ide { + position: relative; + display: grid; + grid-template-rows: auto auto auto; + width: 100%; + max-width: min(40rem, 100%); + min-width: 0; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: var(--color-paper-2); + overflow: hidden; +} + +/* Shared window chrome - CLI, IDE, playground */ +.home-aurora__window-chrome { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.55rem 0.85rem; + border-bottom: var(--rule-hair) solid var(--color-rule); + background: color-mix(in oklch, var(--color-paper-2) 78%, transparent); + min-width: 0; +} + +.home-aurora__window-traffic { + display: inline-flex; + align-items: center; + gap: 0.35rem; + flex-shrink: 0; + z-index: 1; +} + +.home-aurora__window-traffic span { + width: 0.7rem; + height: 0.7rem; + border-radius: 999px; +} + +.home-aurora__window-traffic [data-tone="close"] { + background: var(--color-traffic-close); +} + +.home-aurora__window-traffic [data-tone="min"] { + background: var(--color-traffic-min); +} + +.home-aurora__window-traffic [data-tone="max"] { + background: var(--color-traffic-max); +} + +.home-aurora__window-title { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding-inline: 0.85rem; + min-width: 0; + text-align: center; + font-family: var(--font-outlier); + font-size: var(--text-xs); + color: var(--color-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: none; + pointer-events: none; +} + +.home-aurora__window-url-bar { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + margin-left: 0.5rem; + padding: 0.2rem 0.75rem; + border-radius: var(--radius-pill); + background: color-mix(in oklch, var(--color-paper-3) 80%, transparent); + border: var(--rule-hair) solid var(--color-rule); + min-width: 0; +} + +.home-aurora__window-url { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--color-ink-2); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.home-aurora__ide-body { + margin: 0; + padding: var(--space-sm) var(--space-md); + font-family: var(--font-outlier); + font-size: clamp(0.8rem, 0.2vw + 0.75rem, 0.98rem); + line-height: 1.6; + color: var(--color-ink-2); + min-width: 0; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.home-aurora__tok-cmd, +.home-aurora__transcript-prompt .home-aurora__tok-cmd { + color: var(--color-cmd, oklch(78% 0.19 55)); +} + +.home-aurora__tok-caret { + display: inline-block; + width: 1.5px; + height: 1.05em; + margin-left: 1px; + margin-right: 1px; + vertical-align: text-bottom; + background: var(--color-ink); +} + +.home-aurora__ide-menu { + position: relative; + z-index: 1; + margin: 0 var(--space-sm) var(--space-sm); + padding: 0.35rem; + list-style: none; + width: auto; + max-width: 100%; + min-width: 0; + border: var(--rule-hair) solid var(--color-rule); + border-radius: 0.5rem; + background: color-mix(in oklch, var(--color-paper) 94%, transparent); + backdrop-filter: blur(10px); + box-shadow: var(--shadow-nav); + animation: home-aurora-menu-in var(--dur-short) var(--ease-out) both; +} + +.home-aurora__ide-menu li { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-xs); + padding: 0.4rem 0.55rem; + border-radius: 0.3rem; + font-family: var(--font-outlier); + font-size: var(--text-sm); + color: var(--color-ink); + min-width: 0; +} + +.home-aurora__ide-menu li[data-active="true"] { + background: color-mix(in oklch, var(--color-accent) 18%, transparent); +} + +.home-aurora__ide-name { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--color-ink); +} + +.home-aurora__ide-type { + flex: 0 1 auto; + min-width: 0; + max-width: 55%; + color: #bdc4cc; + font-size: var(--text-xs); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: right; +} + +@keyframes home-aurora-menu-in { + from { + opacity: 0; + transform: translateY(0.35rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Typesafe split (IDE Autocomplete + Terminal Error) */ +.home-aurora__typesafe-split { + display: grid; + gap: var(--space-sm); + width: 100%; + max-width: 56rem; +} + +@media (min-width: 48rem) { + .home-aurora__typesafe-split { + grid-template-columns: 1fr 1fr; + align-items: stretch; + } +} + +/* Agent split */ +.home-aurora__agent-split { + display: grid; + gap: var(--space-sm); + width: 100%; + max-width: 56rem; +} + +@media (min-width: 48rem) { + .home-aurora__agent-split { + grid-template-columns: 1fr 1fr; + align-items: start; + } +} + +/* 04 · Bring your own validator — tabbed schema window */ +.home-aurora__validator { + display: grid; + gap: var(--space-xs); + width: 100%; + max-width: min(40rem, 100%); + min-width: 0; +} + +.home-aurora__validator-tabs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-xs); +} + +.home-aurora__validator-tab { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.4rem; + border: 0; + border-radius: 0; + background: transparent; + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 600; + letter-spacing: 0.01em; + cursor: pointer; + white-space: nowrap; + opacity: 0.7; + transition: + color var(--dur-micro) var(--ease-out), + opacity var(--dur-micro) var(--ease-out); +} + +.home-aurora__validator-tab:hover { + color: var(--color-ink); + opacity: 1; +} + +.home-aurora__validator-tab[data-active="true"] { + background: transparent; + color: var(--color-accent); + opacity: 1; + box-shadow: inset 0 -2px 0 var(--color-accent); +} + +.home-aurora__validator-tab:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__validator-tab:active { + opacity: 0.9; +} + +.home-aurora__validator-frame { + margin: 0; + padding: 0; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: var(--color-paper-2); + display: grid; + grid-template-rows: auto 1fr; + overflow: hidden; + min-width: 0; +} + +.home-aurora__validator-body { + min-width: 0; + animation: home-aurora-rise 420ms var(--ease-out-strong) both; +} + +.home-aurora__validator-note { + margin: 0; + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: 1.45; + color: var(--color-ink-2); +} + +.home-aurora__validator-note code { + font-family: var(--font-outlier); + font-size: 0.92em; + color: var(--color-ink); +} + +.home-aurora__validator-note a { + color: var(--color-accent); + text-decoration: none; + border-bottom: var(--rule-hair) solid color-mix(in oklch, var(--color-accent) 45%, transparent); + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__validator-note a:hover { + color: var(--color-ink); + border-color: var(--color-accent); +} + +.home-aurora__validator-note a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +/* 05 · Secure — client / server schema boundary */ +.home-aurora__secure { + display: grid; + gap: var(--space-sm); + width: 100%; + max-width: 56rem; +} + +@media (min-width: 48rem) { + .home-aurora__secure { + grid-template-columns: 1fr 1fr; + align-items: stretch; + } +} + +.home-aurora__secure-pane { + margin: 0; + display: flex; + flex-direction: column; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: color-mix(in oklch, var(--color-paper) 88%, black); + overflow: hidden; + min-width: 0; + transition: border-color var(--dur-short) var(--ease-out); +} + +.home-aurora__secure-pane[data-side="server"] { + border-color: color-mix(in oklch, var(--color-success) 30%, var(--color-rule)); +} + +.home-aurora__secure-pane-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.85rem; + border-bottom: var(--rule-hair) solid var(--color-rule); + background: color-mix(in oklch, var(--color-paper-2) 60%, transparent); +} + +.home-aurora__secure-pane-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--font-display); + font-size: var(--text-xs); + font-weight: 600; + color: var(--color-ink); +} + +.home-aurora__secure-chip { + padding: 0.15rem 0.5rem; + border-radius: var(--radius-pill); + font-family: var(--font-outlier); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.home-aurora__secure-chip--public { + background: color-mix(in oklch, var(--color-accent) 15%, transparent); + color: var(--color-accent); + border: var(--rule-hair) solid color-mix(in oklch, var(--color-accent) 30%, transparent); +} + +.home-aurora__secure-chip--private { + background: color-mix(in oklch, var(--color-success) 15%, transparent); + color: var(--color-success); + border: var(--rule-hair) solid color-mix(in oklch, var(--color-success) 30%, transparent); +} + +.home-aurora__secure-chip--error { + background: color-mix(in oklch, var(--color-danger) 15%, transparent); + color: var(--color-danger-ink); + border: var(--rule-hair) solid color-mix(in oklch, var(--color-danger) 30%, transparent); +} + +.home-aurora__secure-vault-footer { + margin-top: auto; + padding: 0.55rem 0.85rem; + border-top: var(--rule-hair) solid var(--color-rule); + background: color-mix(in oklch, var(--color-paper-2) 40%, transparent); +} + +.home-aurora__secure-status { + display: flex; + align-items: center; + gap: 0.45rem; + font-family: var(--font-display); + font-size: var(--text-xs); + font-weight: 500; + color: var(--color-ink-2); +} + +.home-aurora__terminal { + margin: 0; + padding: 0; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: color-mix(in oklch, var(--color-paper) 88%, black); + display: grid; + grid-template-rows: auto 1fr; + overflow: hidden; + min-height: 14rem; +} + +.home-aurora__terminal pre { + margin: 0; + padding: var(--space-md); + font-family: var(--font-outlier); + font-size: clamp(0.8rem, 0.2vw + 0.75rem, 0.92rem); + line-height: 1.55; + color: var(--color-ink-2); + white-space: pre-wrap; +} + +/* Cursor-style agent transcript - framed prompt / status / open reply */ +.home-aurora__transcript { + display: grid; + gap: var(--space-xs); + min-width: 0; + align-content: start; + padding: var(--space-md); + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + background: var(--color-paper-2); + min-height: 14rem; +} + +.home-aurora__transcript-prompt { + margin: 0; + padding: 0.7rem 0.85rem; + border-radius: calc(var(--radius-frame) - 0.15rem); + background: var(--color-paper-3); + border: var(--rule-hair) solid var(--color-rule); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: 1.45; +} + +.home-aurora__transcript-status { + margin: 0; + padding-inline: 0.15rem; + font-family: var(--font-body); + font-size: var(--text-sm); + color: var(--color-muted); + line-height: 1.4; +} + +.home-aurora__transcript-reply { + display: grid; + gap: var(--space-2xs); + padding-inline: 0.15rem; + color: var(--color-ink-2); + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: 1.55; +} + +.home-aurora__transcript-reply p { + margin: 0; +} + +.home-aurora__transcript-reply code { + font-family: var(--font-outlier); + font-size: 0.92em; + color: var(--color-ink); + padding: 0.05em 0.3em; + border-radius: 0.25rem; + background: color-mix(in oklch, var(--color-paper-3) 80%, transparent); + border: var(--rule-hair) solid var(--color-rule); +} + +.home-aurora__frame { + position: relative; + width: 100%; + max-width: 56rem; + margin-inline: auto; + border: var(--rule-hair) solid var(--color-rule); + border-radius: var(--radius-frame); + overflow: hidden; + background: color-mix(in oklch, var(--color-paper) 70%, black); + box-shadow: var(--shadow-frame); +} + +.home-aurora__frame-button { + display: flex; + flex-direction: column; + position: relative; + width: 100%; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + text-align: left; + color: inherit; + transition: transform var(--dur-short) var(--ease-out); +} + +.home-aurora__frame-button:hover { + transform: scale(1.01); +} + +.home-aurora__frame-chrome { + display: none; +} + +@media (min-width: 64rem) { + .home-aurora__frame-chrome { + display: flex; + } +} + +.home-aurora__frame-media { + position: relative; + flex: 1; + min-height: 0; + width: 100%; +} + +.home-aurora__frame-button:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.home-aurora__frame-button:hover .home-aurora__frame-hint { + opacity: 1; + transform: translateY(0); +} + +.home-aurora__frame-hint { + position: absolute; + inset: 1.5rem 0 auto 0; + display: flex; + justify-content: center; + pointer-events: none; + opacity: 0; + transform: translateY(-0.35rem); + transition: + opacity var(--dur-short) var(--ease-out), + transform var(--dur-short) var(--ease-out); +} + +.home-aurora__frame-hint span { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.45rem 0.85rem; + border-radius: var(--radius-pill); + background: color-mix(in oklch, var(--color-paper) 88%, transparent); + border: var(--rule-hair) solid var(--color-rule); + color: var(--color-ink); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 500; + backdrop-filter: blur(8px); + box-shadow: var(--shadow-nav); +} + +/* Bookend · callback to the hero promise + install CTAs (desktop only) */ +.home-aurora__outro { + display: none; +} + +@media (min-width: 40rem) { + .home-aurora__outro { + margin-top: var(--space-2xl); + padding-block: var(--space-xl) var(--space-md); + border-top: var(--rule-hair) solid var(--color-rule); + display: grid; + gap: var(--space-sm); + } +} + +.home-aurora__outro-title { + margin: 0; + max-width: none; + font-family: var(--font-display); + font-size: clamp(1.85rem, 4.5vw + 0.55rem, 3.25rem); + line-height: 1.08; + letter-spacing: -0.03em; + font-weight: 500; + font-style: normal; + color: var(--color-ink); + overflow-wrap: normal; +} + +.home-aurora__outro-docs { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + width: fit-content; + min-height: 3rem; + padding: 0.55rem 0.9rem; + border: var(--rule-hair) solid transparent; + border-radius: var(--radius-frame); + background: transparent; + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 500; + text-decoration: none; + transition: + color var(--dur-micro) var(--ease-out), + border-color var(--dur-micro) var(--ease-out), + background-color var(--dur-micro) var(--ease-out); +} + +.home-aurora__outro-docs:hover { + color: var(--color-ink); + border-color: var(--color-rule); + background: color-mix(in oklch, var(--color-paper-2) 70%, transparent); +} + +.home-aurora__outro-docs:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__outro-docs:active { + opacity: 0.9; +} + +/* Ft3 · Index-style footer */ +.home-aurora__footer { + margin-top: var(--space-xl); + padding: var(--space-xl) 0 0; + display: grid; + gap: var(--space-lg); +} + +.home-aurora__footer-grid { + display: grid; + gap: var(--space-lg); +} + +@media (min-width: 40rem) { + .home-aurora__footer-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 64rem) { + .home-aurora__footer-grid { + grid-template-columns: 1.4fr repeat(3, minmax(0, 1fr)); + gap: var(--space-xl); + } +} + +.home-aurora__footer-brand { + display: grid; + gap: var(--space-xs); + align-content: start; +} + +.home-aurora__footer-brand p { + margin: 0; + max-width: 28ch; + color: var(--color-ink-2); + font-size: var(--text-sm); + line-height: 1.45; +} + +.home-aurora__footer-license { + color: var(--color-muted) !important; + font-family: var(--font-outlier); + font-size: var(--text-xs) !important; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.home-aurora__footer-grid h3 { + margin: 0 0 var(--space-xs); + font-family: var(--font-outlier); + font-size: var(--text-xs); + font-weight: 500; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-muted); +} + +.home-aurora__footer-grid ul { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0.45rem; +} + +.home-aurora__footer-grid a { + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-sm); + text-decoration: none; + transition: color var(--dur-micro) var(--ease-out); +} + +.home-aurora__footer-grid a:hover { + color: var(--color-ink); +} + +.home-aurora__footer-grid a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; +} + +.home-aurora__footer-meta { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: baseline; + gap: var(--space-sm); + padding-block-start: var(--space-sm); + border-top: var(--rule-hair) solid var(--color-rule); + color: var(--color-muted); + font-family: var(--font-display); + font-size: var(--text-sm); +} + +@media (prefers-reduced-motion: reduce) { + .home-aurora__frame-hint, + .home-aurora__cta, + .home-aurora__docs-cta, + .home-aurora__outro-docs, + .home-aurora__nav-cta, + .home-aurora__nav-links a, + .home-aurora__install-tab, + .home-aurora__install-copy, + .home-aurora__validator-tab, + .home-aurora__ide-menu, + .home-aurora__faq-icon, + .home-aurora__faq-icon-h, + .home-aurora__faq-icon-v, + .home-aurora__faq-panel, + .home-aurora__faq-panel-inner p, + .home-aurora__install-swap-item { + transition: opacity 120ms linear; + } + + .home-aurora__ide-menu { + animation: none; + } + + .home-aurora__frame-button:hover .home-aurora__frame-hint { + transform: none; + } + + .home-aurora__install-copy:active { + transform: none; + } + + /* No spin → keep horizontal bar, drop vertical (= −) */ + .home-aurora__faq-item[data-open="true"] .home-aurora__faq-icon { + transform: none; + } + + .home-aurora__faq-item[data-open="true"] .home-aurora__faq-icon-h { + opacity: 1; + transform: translate(-50%, -50%); + } + + .home-aurora__faq-item[data-open="true"] .home-aurora__faq-icon-v { + opacity: 0; + transform: translate(-50%, -50%) scaleY(0); + } + + .home-aurora__faq-panel-inner p, + .home-aurora__faq-item[data-open="true"] .home-aurora__faq-panel-inner p, + .home-aurora__install-swap-item, + .home-aurora__install-swap-item[data-active="true"] { + transform: none; + } +} + +/* Compatibility rails - restyle existing component under Aurora */ +.home-aurora .home-aurora__rails-host { + display: flex; + flex-direction: column; + gap: var(--space-xs); + width: 100%; + max-width: min(42rem, 100%); + min-width: 0; + margin: var(--space-sm) 0 0; + padding: 0; + overflow: hidden; +} + +.home-aurora .home-aurora__rails-host > div { + min-width: 0; + max-width: 100%; +} + +.home-aurora .home-aurora__rails-host span.text-\[10px\] { + color: var(--color-ink-2); + font-family: var(--font-display); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.05em; + opacity: 0.9; +} + +.home-aurora .home-aurora__rails-host a { + background: color-mix(in oklch, var(--color-paper-2) 80%, transparent); + border-color: var(--color-rule); + color: var(--color-ink-2); +} + +.home-aurora .home-aurora__rails-host a:hover { + color: var(--color-ink); + border-color: color-mix(in oklch, var(--color-accent) 40%, var(--color-rule)); + background: var(--color-paper-3); +} + +/* StarUs mobile - quiet under Aurora */ +.home-aurora .home-aurora__install-row a[href*="github.com"] { + width: 100% !important; + justify-content: center !important; + background: color-mix(in oklch, var(--color-paper-2) 90%, transparent) !important; + border: var(--rule-hair) solid var(--color-rule) !important; + color: var(--color-ink) !important; + border-radius: var(--radius-pill) !important; + box-shadow: none !important; +} + +.home-aurora .home-aurora__install-row a[href*="github.com"] .text-yellow-600, +.home-aurora .home-aurora__install-row a[href*="github.com"] .dark\:text-yellow-400, +.home-aurora .home-aurora__install-row a[href*="github.com"] .text-yellow-700, +.home-aurora .home-aurora__install-row a[href*="github.com"] .dark\:text-yellow-300 { + color: var(--color-accent) !important; +} + +/* Logo accent */ +.home-aurora .stroke-blue-500, +.home-aurora .dark\:stroke-blue-400, +.home-aurora .fill-blue-500, +.home-aurora .dark\:fill-blue-400 { + stroke: var(--color-accent); + fill: var(--color-accent); + color: var(--color-accent); +} + +.home-aurora .fill-blue-500, +.home-aurora .dark\:fill-blue-400 { + fill: var(--color-accent); +} diff --git a/apps/www/app/(home)/home-nav.tsx b/apps/www/app/(home)/home-nav.tsx new file mode 100644 index 000000000..8bd5b4f8e --- /dev/null +++ b/apps/www/app/(home)/home-nav.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { HeaderGithubLink } from "~/components/page/header-github-link"; +import { Logo } from "~/components/page/logo"; +import { SearchToggle } from "~/components/ui/search-toggle"; +import { ThemeToggle } from "~/components/ui/theme-toggle"; + +export function HomeNav() { + return ( + + ); +} diff --git a/apps/www/app/(home)/layout.tsx b/apps/www/app/(home)/layout.tsx index e16e90cfa..6d092fbf1 100644 --- a/apps/www/app/(home)/layout.tsx +++ b/apps/www/app/(home)/layout.tsx @@ -1,49 +1,30 @@ -import { Header } from "@arkenv/fumadocs-ui/components"; +import "./aurora.css"; import { HomeLayout } from "fumadocs-ui/layouts/home"; import type { CSSProperties, ReactNode } from "react"; -import { HeaderGithubLink } from "~/components/page/header-github-link"; -import { Logo } from "~/components/page/logo"; -import { SearchToggle } from "~/components/ui/search-toggle"; -import { ThemeToggle } from "~/components/ui/theme-toggle"; +import { DotGrid, ScrollReveal } from "~/components/page"; +import { HomeNav } from "./home-nav"; export default function Layout({ children }: { children: ReactNode }) { return ( - } - links={[ - { text: "Documentation", url: "/docs/arkenv" }, - { - text: "Roadmap", - url: "https://github.com/yamcodes/arkenv/issues/683", - }, - ]} - actions={[ - , -
- -
, - , - ]} - menuActions={[]} - menuSocialActions={[ - , - ]} - /> - ), - }} - > - {children} -
+
+ + + + {children} + +
); } diff --git a/apps/www/app/(home)/page.tsx b/apps/www/app/(home)/page.tsx index 2d4c755b0..64729d0d7 100644 --- a/apps/www/app/(home)/page.tsx +++ b/apps/www/app/(home)/page.tsx @@ -1,113 +1,221 @@ -import { ExternalLink } from "@arkenv/fumadocs-ui/components"; import type { Metadata } from "next"; -import Image from "next/image"; import { AnnouncementBadge } from "~/components/announcement-badge"; -import { HeroGradientOverlay } from "~/components/hero-gradient-overlay"; -import { HeroVisual } from "~/components/hero-visual"; import { - CLICommand, + AgentNativePitch, + BeforeAfterCompare, + BringYourOwnValidator, CompatibilityRails, + HeroFaq, + InstallPanel, QuickstartButton, + SecureBoundary, StarUsButton, + TypeSafetyShowcase, VideoDemo, } from "~/components/page"; +import { Logo } from "~/components/page/logo"; export const metadata: Metadata = { title: "ArkEnv", - description: "Environment variable validation from editor to runtime", + description: "The simple way to validate environment variables.", }; export default function HomePage() { return ( -
- {/* Top gradient overlay for dark mode - SVG version */} - - -
-
-
- - Next.js, Netlify presets - -
-

-
- Better{" "} - - typesafe - {" "} - than sorry -
-
- Environment variable validation from editor to runtime -
-

- -
- - - -
+
+
+
+ + Next.js, Netlify presets +
-
- +

+ The simple way to validate{" "} + + + environment variables + + +

+

+ One function validates, parses, and keeps your env vars + typesafe. Use ArkType, Zod, or any Standard Schema. Fail fast, ship + faster. +

+
+ +
+
+ + + +
+
+ +
+
+
-
+ -
- -
+ + + + + - {/* Homepage Footer with Fade Gradient and Stylized Separator */} -
- {/* Background fade gradient */} + + +
+

+ Try ArkEnv now. +

+
-
); } diff --git a/apps/www/app/globals.css b/apps/www/app/globals.css index cb7119a61..2e3173455 100644 --- a/apps/www/app/globals.css +++ b/apps/www/app/globals.css @@ -18,6 +18,11 @@ background-image: none !important; } +html, +body { + overflow-x: clip; +} + @theme { @keyframes spin-slow { from { diff --git a/apps/www/app/layout.tsx b/apps/www/app/layout.tsx index 0f53b0da8..2f6a5af7b 100644 --- a/apps/www/app/layout.tsx +++ b/apps/www/app/layout.tsx @@ -2,20 +2,13 @@ import "./globals.css"; import { Analytics } from "@vercel/analytics/next"; import { SpeedInsights } from "@vercel/speed-insights/next"; import { RootProvider } from "fumadocs-ui/provider/next"; +import { GeistMono } from "geist/font/mono"; +import { GeistPixelGrid } from "geist/font/pixel"; +import { GeistSans } from "geist/font/sans"; import type { Metadata } from "next"; -import { Geist, JetBrains_Mono } from "next/font/google"; import type { ReactNode } from "react"; import { Toaster } from "~/components/ui/toaster"; -const geist = Geist({ - subsets: ["latin"], -}); - -const mono = JetBrains_Mono({ - variable: "--font-mono", - subsets: ["latin"], -}); - export const metadata: Metadata = { metadataBase: new URL("https://arkenv.js.org"), icons: { @@ -40,7 +33,7 @@ export default function Layout({ children }: { children: ReactNode }) { return ( @@ -54,6 +47,7 @@ export default function Layout({ children }: { children: ReactNode }) { theme={{ enableColorScheme: true, enableSystem: true, + defaultTheme: "dark", themes: ["system", "light", "dark"], }} > diff --git a/apps/www/app/styles/base.css b/apps/www/app/styles/base.css index b1dba246e..8157f568e 100644 --- a/apps/www/app/styles/base.css +++ b/apps/www/app/styles/base.css @@ -11,7 +11,7 @@ code, pre { - font-family: var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-family: var(--font-geist-mono), var(--font-mono), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } button:not(:disabled) { diff --git a/apps/www/app/styles/theme/dark.css b/apps/www/app/styles/theme/dark.css index d088afb66..8db66cdfd 100644 --- a/apps/www/app/styles/theme/dark.css +++ b/apps/www/app/styles/theme/dark.css @@ -1,28 +1,48 @@ .dark { - --background: #081021; - --foreground: hsl(0 0% 98%); - --card: hsl(240 10% 3.9%); - --card-foreground: hsl(0 0% 98%); - --popover: hsl(240 10% 3.9%); - --popover-foreground: hsl(0 0% 98%); - --primary: rgb(158, 203, 255); - --primary-foreground: rgb(10, 52, 93); - --fd-primary: rgb(158, 203, 255); - --color-fd-primary: rgb(158, 203, 255); - /** can be rgb(3, 47, 98) */ - --secondary: color-mix(in srgb, var(--background) 80%, transparent); - --secondary-foreground: hsl(0 0% 98%); - --muted: hsl(240 3.7% 15.9%); - --muted-foreground: hsl(240 5% 70%); - /* WCAG AA compliant: 8.88:1 contrast */ - --accent: hsl(240 3.7% 15.9%); - --accent-foreground: hsl(0 0% 98%); - --destructive: hsl(0 62.8% 30.6%); - --destructive-foreground: hsl(0 0% 98%); - --border: var(--color-fd-border); - --input: hsl(240 3.7% 15.9%); - --ring: hsl(240 4.9% 83.9%); - --chart-1: hsl(220 70% 50%); + /* Surfaces — Aurora paper (hue 200), not ocean navy */ + --background: oklch(13% 0.018 200); + --foreground: oklch(95% 0.01 200); + --card: oklch(17% 0.02 200); + --card-foreground: oklch(95% 0.01 200); + --popover: oklch(17% 0.02 200); + --popover-foreground: oklch(95% 0.01 200); + + /* Brand accent */ + --primary: oklch(74% 0.13 195); + --primary-foreground: oklch(14% 0.02 200); + --fd-primary: oklch(74% 0.13 195); + --color-fd-primary: oklch(74% 0.13 195); + --color-fd-primary-foreground: oklch(14% 0.02 200); + + --secondary: oklch(21% 0.022 200); + --secondary-foreground: oklch(78% 0.02 200); + --muted: oklch(17% 0.02 200); + --muted-foreground: oklch(62% 0.025 200); + --accent: oklch(21% 0.022 200); + --accent-foreground: oklch(95% 0.01 200); + --destructive: oklch(65% 0.18 25); + --destructive-foreground: oklch(95% 0.01 200); + --border: oklch(30% 0.02 200); + --input: oklch(21% 0.022 200); + --ring: oklch(78% 0.12 195); + + /* fumadocs ocean overrides — search dialog + docs chrome */ + --color-fd-background: oklch(13% 0.018 200); + --color-fd-foreground: oklch(95% 0.01 200); + --color-fd-muted: oklch(17% 0.02 200); + --color-fd-muted-foreground: oklch(62% 0.025 200); + --color-fd-popover: oklch(17% 0.02 200); + --color-fd-popover-foreground: oklch(95% 0.01 200); + --color-fd-card: oklch(17% 0.02 200); + --color-fd-card-foreground: oklch(95% 0.01 200); + --color-fd-border: oklch(30% 0.02 200); + --color-fd-secondary: oklch(21% 0.022 200); + --color-fd-secondary-foreground: oklch(78% 0.02 200); + --color-fd-accent: oklch(21% 0.022 200); + --color-fd-accent-foreground: oklch(95% 0.01 200); + --color-fd-ring: oklch(78% 0.12 195); + + --chart-1: hsl(195 70% 50%); --chart-2: hsl(160 60% 45%); --chart-3: hsl(30 80% 55%); --chart-4: hsl(280 65% 60%); diff --git a/apps/www/app/styles/theme/light.css b/apps/www/app/styles/theme/light.css index fd850e53c..f95a734ca 100644 --- a/apps/www/app/styles/theme/light.css +++ b/apps/www/app/styles/theme/light.css @@ -1,26 +1,47 @@ :root { - --background: #fefefe; - --foreground: hsl(240 10% 3.9%); - --card: hsl(0 0% 100%); - --card-foreground: hsl(240 10% 3.9%); - --popover: hsl(0 0% 100%); - --popover-foreground: hsl(240 10% 3.9%); - --primary: #1e40af; - --primary-foreground: hsl(0 0% 98%); - --fd-primary: var(--primary); - --color-fd-primary: var(--primary); - --secondary: rgb(247, 249, 252); - --secondary-foreground: hsl(240 5.9% 10%); - --muted: hsl(240 4.8% 95.9%); - --muted-foreground: hsl(240 4% 45%); - --accent: rgb(244, 244, 245); - --accent-foreground: hsl(240 5.9% 10%); - --destructive: hsl(0 84.2% 60.2%); - --destructive-foreground: hsl(0 0% 98%); - --border: hsl(240 5.9% 90%); - --input: hsl(240 5.9% 90%); - --ring: hsl(240 10% 3.9%); - --chart-1: hsl(12 76% 61%); + --background: oklch(99% 0.005 200); + --foreground: oklch(18% 0.02 200); + --card: oklch(100% 0 0); + --card-foreground: oklch(18% 0.02 200); + --popover: oklch(100% 0 0); + --popover-foreground: oklch(18% 0.02 200); + + /* Aurora accent — darker for light surfaces */ + --primary: oklch(48% 0.11 195); + --primary-foreground: oklch(98% 0.01 195); + --fd-primary: oklch(48% 0.11 195); + --color-fd-primary: oklch(48% 0.11 195); + --color-fd-primary-foreground: oklch(98% 0.01 195); + + --secondary: oklch(96% 0.01 200); + --secondary-foreground: oklch(22% 0.02 200); + --muted: oklch(96% 0.01 200); + --muted-foreground: oklch(45% 0.02 200); + --accent: oklch(96% 0.01 200); + --accent-foreground: oklch(22% 0.02 200); + --destructive: oklch(55% 0.2 25); + --destructive-foreground: oklch(98% 0.01 200); + --border: oklch(90% 0.015 200); + --input: oklch(90% 0.015 200); + --ring: oklch(48% 0.11 195); + + /* fumadocs ocean overrides */ + --color-fd-background: oklch(99% 0.005 200); + --color-fd-foreground: oklch(18% 0.02 200); + --color-fd-muted: oklch(96% 0.01 200); + --color-fd-muted-foreground: oklch(45% 0.02 200); + --color-fd-popover: oklch(100% 0 0); + --color-fd-popover-foreground: oklch(18% 0.02 200); + --color-fd-card: oklch(100% 0 0); + --color-fd-card-foreground: oklch(18% 0.02 200); + --color-fd-border: oklch(90% 0.015 200); + --color-fd-secondary: oklch(96% 0.01 200); + --color-fd-secondary-foreground: oklch(22% 0.02 200); + --color-fd-accent: oklch(96% 0.01 200); + --color-fd-accent-foreground: oklch(22% 0.02 200); + --color-fd-ring: oklch(48% 0.11 195); + + --chart-1: hsl(195 70% 40%); --chart-2: hsl(173 58% 39%); --chart-3: hsl(197 37% 24%); --chart-4: hsl(43 74% 66%); diff --git a/apps/www/components/announcement-badge.tsx b/apps/www/components/announcement-badge.tsx index 7b371467a..11544b495 100644 --- a/apps/www/components/announcement-badge.tsx +++ b/apps/www/components/announcement-badge.tsx @@ -10,7 +10,6 @@ export function AnnouncementBadge({ href, children, }: PropsWithChildren<{ - // TODO: we can detect the arrow kind (right facing, or top right facing) based on whether `href` is internal or external /** * Show an arrow next to the badge */ @@ -25,24 +24,18 @@ export function AnnouncementBadge({ href: ArkenvUrl; }>) { return ( - - {newBadge && ( - - )} + + {newBadge ? : null} {children} {arrow && href && (isExternalUrl(href) ? ( - + ) : ( - + ))} diff --git a/apps/www/components/page/agent-native-pitch.tsx b/apps/www/components/page/agent-native-pitch.tsx new file mode 100644 index 000000000..c781a19fc --- /dev/null +++ b/apps/www/components/page/agent-native-pitch.tsx @@ -0,0 +1,85 @@ +import { WindowChrome } from "./window-chrome"; + +/** + * Agent-native pitch: terminal CLI + Cursor-style agent transcript in a frame. + */ +export function AgentNativePitch() { + return ( +
+
+

+ 05 / AGENT-READY +

+

+ For humans.{" "} + And their agents. +

+

+ Run the interactive CLI, or point the{" "} + + agent skill + {" "} + at your .env.example in Cursor, Claude Code, or Codex. +

+
+ +
+
+ +
+						
+							yam@dev
+							:
+							~/app
+							$ npx
+							arkenv@latest init{"\n"}
+							 Detected Next.js
+							{"\n"}
+							 Wrote{" "}
+							env.ts
+							{"\n"}
+							 Wired{" "}
+							@arkenv/nextjs
+							{"\n"}
+							yam@dev
+							:
+							~/app
+							$
+						
+					
+
+ +
+

+ Set up{" "} + + /arkenv + {" "} + for my .env.example +

+

Worked for 2.4s

+
+

+ Running npx arkenv@latest init --agent. Schema + matches your .env keys; validation works from editor to runtime. +

+
+
+
+
+ ); +} diff --git a/apps/www/components/page/before-after-compare-view.tsx b/apps/www/components/page/before-after-compare-view.tsx new file mode 100644 index 000000000..19ba2ecc2 --- /dev/null +++ b/apps/www/components/page/before-after-compare-view.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { type CSSProperties, useEffect, useId, useRef, useState } from "react"; +import { WindowChrome } from "./window-chrome"; + +type BeforeAfterCompareViewProps = { + beforeHtml: string; + afterHtml: string; + reduction: number; +}; + +type MobileView = "arkenv" | "old"; + +const NARROW_MQ = "(max-width: 39.99rem)"; + +/** + * Desktop: scroll-driven before/after reveal (drag still works as override). + * Mobile: ArkEnv snippet by default, with a toggle for the old way. + */ +export function BeforeAfterCompareView({ + beforeHtml, + afterHtml, + reduction, +}: BeforeAfterCompareViewProps) { + const [compare, setCompare] = useState(100); + const [manual, setManual] = useState(false); + const [mobileView, setMobileView] = useState("arkenv"); + const compareRef = useRef(null); + const labelId = useId(); + const toggleId = useId(); + + useEffect(() => { + if (manual) return; + + const narrow = window.matchMedia(NARROW_MQ); + if (narrow.matches) return; + + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + if (reduceMotion.matches) { + setCompare(50); + return; + } + + const updateFromScroll = () => { + const el = compareRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const view = window.innerHeight || 1; + const start = view * 0.85; + const end = view * 0.2; + const raw = (start - rect.top) / (start - end); + const progress = Math.min(1, Math.max(0, raw)); + setCompare(Math.round((1 - progress) * 100)); + }; + + updateFromScroll(); + window.addEventListener("scroll", updateFromScroll, { passive: true }); + window.addEventListener("resize", updateFromScroll, { passive: true }); + return () => { + window.removeEventListener("scroll", updateFromScroll); + window.removeEventListener("resize", updateFromScroll); + }; + }, [manual]); + + return ( +
+
+

+ 01 / DECLARATIVE +

+

+ + + Simplify, simplify, + {" "} + simplify. + +

+

+ Replace your manual process.env checks with one schema. + Skip the separate interfaces and error handling. +

+

+ {reduction}% less code, same checks +

+
+ +
+

+ Code sample +

+ + +
+ +
+ +
+ {/* Invisible sizer so the frame fits the full old-way snippet */} +
+ ); +} diff --git a/apps/www/components/page/before-after-compare.tsx b/apps/www/components/page/before-after-compare.tsx new file mode 100644 index 000000000..1e10dad5e --- /dev/null +++ b/apps/www/components/page/before-after-compare.tsx @@ -0,0 +1,59 @@ +import { BeforeAfterCompareView } from "./before-after-compare-view"; +import { highlightTs } from "./highlight-ts"; + +const BEFORE = `// The old way +const DATABASE_URL = process.env.DATABASE_URL; +if (!DATABASE_URL) throw new Error("DATABASE_URL missing"); +if (!/^https?:\\/\\//.test(DATABASE_URL)) { + throw new Error("DATABASE_URL must be http(s)"); +} + +const portRaw = process.env.PORT ?? "3000"; +const PORT = Number.parseInt(portRaw, 10); +if (Number.isNaN(PORT)) throw new Error("PORT invalid"); +if (PORT < 0 || PORT > 65535) { + throw new Error("PORT out of range"); +} + +const NODE_ENV = process.env.NODE_ENV ?? "development"; +if (!["development", "production"].includes(NODE_ENV)) { + throw new Error("NODE_ENV invalid"); +} + +export const env = { DATABASE_URL, PORT, NODE_ENV };`; + +const AFTER = `// The ArkEnv way +import arkenv from "@arkenv/core"; + +export const env = arkenv({ + DATABASE_URL: "string.url", + PORT: "0 <= number.integer <= 65535 = 3000", + NODE_ENV: "'development' | 'production'", +});`; + +function countLines(source: string) { + return source.trimEnd().split("\n").length; +} + +/** + * Server entry: Shiki-highlights the snippets, then hands off to the + * interactive client reveal. + */ +export async function BeforeAfterCompare() { + const [beforeHtml, afterHtml] = await Promise.all([ + highlightTs(BEFORE), + highlightTs(AFTER), + ]); + + const reduction = Math.round( + (1 - countLines(AFTER) / countLines(BEFORE)) * 100, + ); + + return ( + + ); +} diff --git a/apps/www/components/page/bring-your-own-validator-view.tsx b/apps/www/components/page/bring-your-own-validator-view.tsx new file mode 100644 index 000000000..18a54c9bf --- /dev/null +++ b/apps/www/components/page/bring-your-own-validator-view.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useId, useState } from "react"; +import { WindowChrome } from "./window-chrome"; + +type ValidatorExample = { + id: "arktype" | "zod" | "valibot"; + label: string; + importLine: string; + html: string; +}; + +type BringYourOwnValidatorViewProps = { + examples: ValidatorExample[]; +}; + +/** + * Client tabs over server-highlighted validator snippets. + */ +export function BringYourOwnValidatorView({ + examples, +}: BringYourOwnValidatorViewProps) { + const [active, setActive] = useState("arktype"); + const baseId = useId(); + const example = examples.find((item) => item.id === active) ?? examples[0]; + const panelId = `${baseId}-panel`; + + return ( +
+
+

+ 04 / MODULAR +

+

+ Bring your own validator. +

+

+ Use ArkType, Zod, Valibot, or any{" "} + + Standard Schema + {" "} + library. Either way, you get the same env API. +

+
+ +
+
+ {examples.map((item) => ( + + ))} +
+ +
+ +
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: static Shiki HTML from server */} +
+
+
+ +

+ via{" "} + {example.importLine} + {" · "} + + See Standard Schema docs → + +

+
+
+ ); +} diff --git a/apps/www/components/page/bring-your-own-validator.tsx b/apps/www/components/page/bring-your-own-validator.tsx new file mode 100644 index 000000000..09d309f41 --- /dev/null +++ b/apps/www/components/page/bring-your-own-validator.tsx @@ -0,0 +1,56 @@ +import { BringYourOwnValidatorView } from "./bring-your-own-validator-view"; +import { highlightTs } from "./highlight-ts"; + +const EXAMPLES = [ + { + id: "arktype" as const, + label: "ArkType", + importLine: "@arkenv/core", + code: `import arkenv from "@arkenv/core"; + +export const env = arkenv({ + DATABASE_URL: "string.url", + PORT: "number.port", +});`, + }, + { + id: "zod" as const, + label: "Zod", + importLine: "@arkenv/standard", + code: `import arkenv from "@arkenv/standard"; +import { z } from "zod"; + +export const env = arkenv({ + DATABASE_URL: z.url(), + PORT: z.coerce.number().int().min(0).max(65535), +});`, + }, + { + id: "valibot" as const, + label: "Valibot", + importLine: "@arkenv/standard", + code: `import arkenv from "@arkenv/standard"; +import * as v from "valibot"; + +export const env = arkenv({ + DATABASE_URL: v.pipe(v.string(), v.url()), + PORT: v.pipe(v.string(), v.toNumber(), v.integer(), v.minValue(0), v.maxValue(65535)), +});`, + }, +]; + +/** + * Pitch: same ArkEnv API across ArkType / Zod / Valibot via Standard Schema. + */ +export async function BringYourOwnValidator() { + const examples = await Promise.all( + EXAMPLES.map(async (example) => ({ + id: example.id, + label: example.label, + importLine: example.importLine, + html: await highlightTs(example.code), + })), + ); + + return ; +} diff --git a/apps/www/components/page/compatibility-rails.tsx b/apps/www/components/page/compatibility-rails.tsx index c46e761d9..e50fc9c60 100644 --- a/apps/www/components/page/compatibility-rails.tsx +++ b/apps/www/components/page/compatibility-rails.tsx @@ -105,11 +105,11 @@ function MarqueeRow({ : "animate-marquee"; return ( -
+
{label} -
+
- - + +
); } diff --git a/apps/www/components/page/dot-grid.tsx b/apps/www/components/page/dot-grid.tsx new file mode 100644 index 000000000..81abd19e0 --- /dev/null +++ b/apps/www/components/page/dot-grid.tsx @@ -0,0 +1,266 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +/** Physics tuned for a quiet Aurora field - push ripples, then settle. */ +const SPACING = 24; +const DOT_RADIUS = 1; +const INFLUENCE = 110; +const STIFFNESS = 130; +const DAMPING = 9; +const PUSH = 1100; +const DRAG = 1.6; +const MAX_OFFSET = 14; +const MAX_POINTER_SPEED = 2600; +const REST_THRESHOLD = 0.01; + +/** + * Interactive faded-dot atmosphere for the home hero. + * Static CSS dots paint first; the canvas takes over after hydration. + */ +export function DotGrid() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + let width = 0; + let height = 0; + let cols = 0; + let rows = 0; + // ox, oy, vx, vy per dot + let dots = new Float32Array(0); + let color = ""; + let raf = 0; + let running = false; + let lastTime = 0; + const pointer = { + x: 0, + y: 0, + prevX: 0, + prevY: 0, + vx: 0, + vy: 0, + active: false, + }; + + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + + const resolveColor = () => { + color = getComputedStyle(canvas).color; + }; + + const draw = () => { + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = color; + ctx.beginPath(); + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const idx = (row * cols + col) * 4; + const x = SPACING / 2 + col * SPACING + dots[idx]; + const y = SPACING / 2 + row * SPACING + dots[idx + 1]; + ctx.moveTo(x + DOT_RADIUS, y); + ctx.arc(x, y, DOT_RADIUS, 0, Math.PI * 2); + } + } + ctx.fill(); + }; + + const step = (dt: number) => { + const influenceSq = INFLUENCE * INFLUENCE; + let moving = false; + for (let row = 0; row < rows; row++) { + const baseY = SPACING / 2 + row * SPACING; + for (let col = 0; col < cols; col++) { + const idx = (row * cols + col) * 4; + const baseX = SPACING / 2 + col * SPACING; + let ox = dots[idx]; + let oy = dots[idx + 1]; + let vx = dots[idx + 2]; + let vy = dots[idx + 3]; + + let ax = -STIFFNESS * ox - DAMPING * vx; + let ay = -STIFFNESS * oy - DAMPING * vy; + + if (pointer.active) { + const dx = baseX + ox - pointer.x; + const dy = baseY + oy - pointer.y; + const distSq = dx * dx + dy * dy; + if (distSq < influenceSq) { + const dist = Math.sqrt(distSq) || 1; + const falloff = 1 - dist / INFLUENCE; + ax += ((dx / dist) * PUSH + pointer.vx * DRAG) * falloff; + ay += ((dy / dist) * PUSH + pointer.vy * DRAG) * falloff; + } + } + + vx += ax * dt; + vy += ay * dt; + ox += vx * dt; + oy += vy * dt; + + const offsetSq = ox * ox + oy * oy; + if (offsetSq > MAX_OFFSET * MAX_OFFSET) { + const scale = MAX_OFFSET / Math.sqrt(offsetSq); + ox *= scale; + oy *= scale; + } + + if (offsetSq > REST_THRESHOLD || vx * vx + vy * vy > REST_THRESHOLD) { + moving = true; + } else { + ox = 0; + oy = 0; + vx = 0; + vy = 0; + } + + dots[idx] = ox; + dots[idx + 1] = oy; + dots[idx + 2] = vx; + dots[idx + 3] = vy; + } + } + return moving; + }; + + const frame = (time: number) => { + const dt = Math.min(Math.max((time - lastTime) / 1000, 0.001), 1 / 30); + lastTime = time; + + if (pointer.active) { + const clamp = (v: number) => + Math.max(-MAX_POINTER_SPEED, Math.min(MAX_POINTER_SPEED, v)); + pointer.vx = clamp((pointer.x - pointer.prevX) / dt); + pointer.vy = clamp((pointer.y - pointer.prevY) / dt); + pointer.prevX = pointer.x; + pointer.prevY = pointer.y; + } + + const moving = step(dt); + draw(); + + if (moving || pointer.active) { + raf = requestAnimationFrame(frame); + } else { + running = false; + } + }; + + const wake = () => { + if (running || reduceMotion.matches) return; + running = true; + lastTime = performance.now(); + raf = requestAnimationFrame(frame); + }; + + const sleep = () => { + cancelAnimationFrame(raf); + running = false; + }; + + const resize = () => { + const rect = canvas.getBoundingClientRect(); + width = rect.width; + height = rect.height; + const dpr = Math.min(window.devicePixelRatio || 1, 2); + canvas.width = Math.round(width * dpr); + canvas.height = Math.round(height * dpr); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + cols = Math.ceil(width / SPACING); + rows = Math.ceil(height / SPACING); + dots = new Float32Array(cols * rows * 4); + draw(); + }; + + const deactivatePointer = () => { + pointer.active = false; + pointer.vx = 0; + pointer.vy = 0; + }; + + const onPointerMove = (event: PointerEvent) => { + if (reduceMotion.matches) return; + const rect = canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + const inRange = + x > -INFLUENCE && + y > -INFLUENCE && + x < rect.width + INFLUENCE && + y < rect.height + INFLUENCE; + if (!inRange) { + deactivatePointer(); + return; + } + if (!pointer.active) { + pointer.prevX = x; + pointer.prevY = y; + } + pointer.active = true; + pointer.x = x; + pointer.y = y; + wake(); + }; + + const onPointerEnd = (event: PointerEvent) => { + if (event.pointerType !== "mouse") deactivatePointer(); + }; + + const onWindowOut = (event: PointerEvent) => { + if (!event.relatedTarget) deactivatePointer(); + }; + + resolveColor(); + resize(); + canvas.style.backgroundImage = "none"; + + const resizeObserver = new ResizeObserver(resize); + resizeObserver.observe(canvas); + + const themeObserver = new MutationObserver(() => { + resolveColor(); + draw(); + }); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "data-theme", "style"], + }); + + const onMotionPreferenceChange = () => { + if (!reduceMotion.matches) return; + sleep(); + deactivatePointer(); + dots.fill(0); + draw(); + }; + + window.addEventListener("pointermove", onPointerMove, { passive: true }); + window.addEventListener("pointerup", onPointerEnd, { passive: true }); + window.addEventListener("pointercancel", onPointerEnd, { passive: true }); + window.addEventListener("pointerout", onWindowOut, { passive: true }); + window.addEventListener("blur", deactivatePointer); + window.addEventListener("scroll", deactivatePointer, { passive: true }); + reduceMotion.addEventListener("change", onMotionPreferenceChange); + + return () => { + sleep(); + resizeObserver.disconnect(); + themeObserver.disconnect(); + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", onPointerEnd); + window.removeEventListener("pointercancel", onPointerEnd); + window.removeEventListener("pointerout", onWindowOut); + window.removeEventListener("blur", deactivatePointer); + window.removeEventListener("scroll", deactivatePointer); + reduceMotion.removeEventListener("change", onMotionPreferenceChange); + }; + }, []); + + return ( + + ); +} diff --git a/apps/www/components/page/header-github-link.tsx b/apps/www/components/page/header-github-link.tsx index 44c05c0bb..802e81000 100644 --- a/apps/www/components/page/header-github-link.tsx +++ b/apps/www/components/page/header-github-link.tsx @@ -3,8 +3,12 @@ import { SiGithub } from "@icons-pack/react-simple-icons"; import { Star } from "lucide-react"; import { useEffect, useState } from "react"; +import { cn } from "~/lib/utils"; import { breakDownGithubUrl } from "~/lib/utils/github"; +/** + * Single GitHub badge: icon + star count in one hit target. + */ export function HeaderGithubLink({ className, iconClassName, @@ -34,21 +38,32 @@ export function HeaderGithubLink({ fetchStarCount(); }, []); + const label = + starCount !== null + ? `GitHub · ${starCount.toLocaleString()} stars` + : "GitHub"; + return ( - - {starCount !== null && ( -
- - {starCount.toLocaleString()} -
+ aria-label={label} + className={cn( + "inline-flex items-center justify-center gap-1.5 transition-colors", + className, )} + > +
); } diff --git a/apps/www/components/page/hero-faq.tsx b/apps/www/components/page/hero-faq.tsx new file mode 100644 index 000000000..c31ae5432 --- /dev/null +++ b/apps/www/components/page/hero-faq.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useCallback, useEffect, useId, useState } from "react"; + +/** + * Homepage FAQ picks - questions + anchors match `/docs/arkenv/faq` (source of truth). + * Keep answers as short teasers; link out for the full write-up. + */ +const FAQ: readonly { + id: string; + href: string; + question: React.ReactNode; + teaser: React.ReactNode; +}[] = [ + { + id: "why", + href: "/docs/arkenv/faq#why-do-i-need-arkenv", + question: "Why do I need ArkEnv?", + teaser: ( + <> +

+ When environment variables fail, the results can be silent, + unexpected, and catastrophic. CI deployments miss required keys, + stringified booleans like "false" evaluate as + truthy, and schema updates break downstream code. +

+

+ ArkEnv protects against this by validating the environment against + your schema before a single line of code runs. +

+ + ), + }, + { + id: "env-files", + href: "/docs/arkenv/faq#does-arkenv-load-my-env-files", + question: ( + <> + Does ArkEnv load my .env files? + + ), + teaser: ( + <> + No. ArkEnv validates the env your framework already loaded. It does not + read .env files itself. + + ), + }, + { + id: "arktype", + href: "/docs/arkenv/faq#do-i-have-to-use-arktype", + question: "Do I have to use ArkType?", + teaser: + "No. Use any Standard Schema validator: Zod, Valibot, Typia, and others.", + }, + { + id: "zod", + href: "/docs/arkenv/faq#does-arkenv-work-with-zod", + question: "Does ArkEnv work with Zod?", + teaser: + "Yes. Zod has a first-class example, and the CLI can scaffold ArkEnv + Zod for you.", + }, + { + id: "agents", + href: "/docs/arkenv/faq#does-arkenv-support-ai-development-tools-like-claude-code-or-cursor", + question: + "Does ArkEnv support AI development tools like Claude Code or Cursor?", + teaser: ( + <> + Yes. Point the agent skill at your .env.example to + intelligently set up validation and guide your assistant through schema + maintenance. + + ), + }, +]; + +const DOCS_FAQ_HREF = "/docs/arkenv/faq"; + +/** + * Landing FAQ - select questions from the docs FAQ, with links back to SoT. + * `#why` expands the first item and scrolls to the section. + */ +export function HeroFaq() { + const [openId, setOpenId] = useState(null); + const baseId = useId(); + + const openWhy = useCallback(() => { + if (window.location.hash !== "#why" && window.location.hash !== "#faq") { + return; + } + if (window.location.hash === "#why") { + setOpenId("why"); + } + requestAnimationFrame(() => { + const target = + window.location.hash === "#why" + ? document.getElementById("why") + : document.getElementById("faq"); + target?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }); + }, []); + + useEffect(() => { + openWhy(); + window.addEventListener("hashchange", openWhy); + + const onClick = (event: MouseEvent) => { + const target = event.target; + if (!(target instanceof Element)) return; + const link = target.closest( + 'a[href="/#why"], a[href="#why"], a[href="/#faq"], a[href="#faq"]', + ); + if (!link) return; + requestAnimationFrame(openWhy); + }; + document.addEventListener("click", onClick); + + return () => { + window.removeEventListener("hashchange", openWhy); + document.removeEventListener("click", onClick); + }; + }, [openWhy]); + + return ( +
+
+

+ Frequently asked questions. +

+

+ Picked from the docs. Read the full FAQ → +

+
+ +
+ {FAQ.map((item) => { + const isOpen = openId === item.id; + const panelId = `${baseId}-${item.id}-panel`; + const buttonId = `${baseId}-${item.id}-button`; + + return ( +
+ +
+
+

{item.teaser}

+

+ Read full answer → +

+
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/www/components/page/highlight-ts.ts b/apps/www/components/page/highlight-ts.ts new file mode 100644 index 000000000..4f465f388 --- /dev/null +++ b/apps/www/components/page/highlight-ts.ts @@ -0,0 +1,16 @@ +import { codeToHtml } from "shiki"; + +/** + * Shared landing-page highlighter — matches docs + 01 DECLARATIVE + * (github-*-high-contrast dual theme). + */ +export async function highlightTs(code: string) { + return codeToHtml(code, { + lang: "ts", + themes: { + light: "github-light-high-contrast", + dark: "github-dark-high-contrast", + }, + defaultColor: false, + }); +} diff --git a/apps/www/components/page/index.ts b/apps/www/components/page/index.ts index 935e63958..6620aa86e 100644 --- a/apps/www/components/page/index.ts +++ b/apps/www/components/page/index.ts @@ -1,7 +1,17 @@ +export * from "./agent-native-pitch"; +export * from "./before-after-compare"; +export * from "./bring-your-own-validator"; export * from "./cli-command"; export * from "./compatibility-rails"; export * from "./copy-button"; +export * from "./dot-grid"; +export * from "./hero-faq"; +export * from "./install-panel"; export * from "./logo"; export * from "./quickstart-button"; +export * from "./scroll-reveal"; +export * from "./secure-boundary"; export * from "./star-us-button"; +export * from "./type-safety-showcase"; export * from "./video-demo"; +export * from "./window-chrome"; diff --git a/apps/www/components/page/install-panel.tsx b/apps/www/components/page/install-panel.tsx new file mode 100644 index 000000000..e36810638 --- /dev/null +++ b/apps/www/components/page/install-panel.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { Check, Copy, Sparkles } from "lucide-react"; +import { useId, useState } from "react"; +import { useCopyCommand } from "~/hooks/use-copy-command"; + +const INSTALL_COMMAND = "npx arkenv@latest init"; + +const INSTALL_PROMPT = + "Set up ArkEnv with `npx arkenv@latest init --agent`. Install any missing dependencies, wire the env schema into the app entry, start the app, and tell me when validation works from editor to runtime."; + +type Tab = "command" | "prompt"; + +export function InstallPanel() { + const [tab, setTab] = useState("command"); + const baseId = useId(); + const panelId = `${baseId}-panel`; + const commandTabId = `${baseId}-tab-command`; + const promptTabId = `${baseId}-tab-prompt`; + + const commandCopy = useCopyCommand(INSTALL_COMMAND, { + successDescription: "Command copied to clipboard!", + }); + const promptCopy = useCopyCommand(INSTALL_PROMPT, { + successDescription: "Prompt copied to clipboard!", + }); + + const active = tab === "command" ? commandCopy : promptCopy; + const ariaLabel = + tab === "command" + ? active.copied + ? "Copied" + : "Copy install command" + : active.copied + ? "Copied" + : "Copy prompt"; + + return ( +
+
+ + +
+ +
+ +
+
+ ); +} diff --git a/apps/www/components/page/logo.test.tsx b/apps/www/components/page/logo.test.tsx index 285a9e2a4..1bcb36837 100644 --- a/apps/www/components/page/logo.test.tsx +++ b/apps/www/components/page/logo.test.tsx @@ -14,6 +14,12 @@ describe("Logo", () => { expect(logoText).toBeInTheDocument(); }); + it("hides the wordmark when wordmark is false", () => { + render(); + + expect(screen.queryByText("ArkEnv")).not.toBeInTheDocument(); + }); + it("accepts custom className", () => { render(); diff --git a/apps/www/components/page/logo.tsx b/apps/www/components/page/logo.tsx index 2df64bdcf..efca15318 100644 --- a/apps/www/components/page/logo.tsx +++ b/apps/www/components/page/logo.tsx @@ -4,9 +4,16 @@ import { cn } from "~/lib/utils"; * Logo component * * @param className - Optional className for custom styling + * @param wordmark - When false, renders the icon only (default true) * @returns Logo component */ -export function Logo({ className }: { className?: string }) { +export function Logo({ + className, + wordmark = true, +}: { + className?: string; + wordmark?: boolean; +}) { return (
- ArkEnv + {wordmark ? ( + ArkEnv + ) : null}
); } diff --git a/apps/www/components/page/quickstart-button.test.tsx b/apps/www/components/page/quickstart-button.test.tsx index 47852505c..859606fcb 100644 --- a/apps/www/components/page/quickstart-button.test.tsx +++ b/apps/www/components/page/quickstart-button.test.tsx @@ -3,14 +3,14 @@ import { describe, expect, it } from "vitest"; import { QuickstartButton } from "./quickstart-button"; describe("QuickstartButton", () => { - it("renders quickstart button with correct text", () => { + it("renders read the docs button with correct text", () => { render(); - expect(screen.getByText("Get Started")).toBeInTheDocument(); + expect(screen.getByText("Read the docs")).toBeInTheDocument(); }); - it("renders as a link to docs/quickstart", () => { + it("renders as a link to docs", () => { render(); const link = screen.getByRole("link"); - expect(link).toHaveAttribute("href", "/docs/arkenv/quickstart"); + expect(link).toHaveAttribute("href", "/docs/arkenv"); }); }); diff --git a/apps/www/components/page/quickstart-button.tsx b/apps/www/components/page/quickstart-button.tsx index 02212caa9..15d707b3e 100644 --- a/apps/www/components/page/quickstart-button.tsx +++ b/apps/www/components/page/quickstart-button.tsx @@ -1,22 +1,10 @@ "use client"; -import { Button } from "~/components/ui/button"; - export function QuickstartButton() { return ( - + + Read the docs + + ); } diff --git a/apps/www/components/page/scroll-reveal.tsx b/apps/www/components/page/scroll-reveal.tsx new file mode 100644 index 000000000..8499b5ea7 --- /dev/null +++ b/apps/www/components/page/scroll-reveal.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect } from "react"; + +/** + * Open-slide–style scroll reveals: only hide `[data-reveal]` nodes still + * below the fold, then clear blur/opacity via IntersectionObserver. + * No-JS and above-the-fold content stay visible (no flash). + */ +export function ScrollReveal() { + useEffect(() => { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + return; + } + + const els = Array.from( + document.querySelectorAll("[data-reveal]"), + ); + const foldLine = window.innerHeight * 0.92; + const pending = els.filter( + (el) => el.getBoundingClientRect().top > foldLine, + ); + if (pending.length === 0) return; + + for (const el of pending) el.classList.add("reveal-hidden"); + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + entry.target.classList.add("reveal-shown"); + entry.target.classList.remove("reveal-hidden"); + observer.unobserve(entry.target); + } + }, + { rootMargin: "0px 0px -8% 0px" }, + ); + + for (const el of pending) observer.observe(el); + + return () => { + observer.disconnect(); + for (const el of pending) el.classList.remove("reveal-hidden"); + }; + }, []); + + return null; +} diff --git a/apps/www/components/page/secure-boundary-view.tsx b/apps/www/components/page/secure-boundary-view.tsx new file mode 100644 index 000000000..db3aff0f7 --- /dev/null +++ b/apps/www/components/page/secure-boundary-view.tsx @@ -0,0 +1,168 @@ +"use client"; + +import { useId, useState } from "react"; +import { WindowChrome } from "./window-chrome"; + +type SecureBoundaryViewProps = { + envHtml: string; + bundleHtml: string; +}; + +type Tab = "schema" | "bundle"; + +export function SecureBoundaryView({ + envHtml, + bundleHtml, +}: SecureBoundaryViewProps) { + const [activeTab, setActiveTab] = useState("schema"); + const baseId = useId(); + + return ( +
+
+

+ 03 / SECURE +

+

+ Keep secrets off the client. +

+

+ Define a single{" "} + + flat schema + + . Secrets are stripped at build time; accidental client access throws + during SSR. +

+
+ +
+ {/* Left Column: Window with integrated tab switcher */} +
+
+ +
+
+
+
+
+ +
+

+ Define server & client variables in one flat schema. Non- + NEXT_PUBLIC_ keys are automatically identified. +

+

+ Secret values like DATABASE_URL{" "} + ("postgresql://db...") are stripped at build time. Only + public values reach the client. +

+
+
+ + {/* Right Column: Anchored SSR Error Overlay */} +
+ +
+
+ + Unhandled Runtime Error + + SSR Exception +
+
+
+ Uncaught Error: +
+
+ ArkEnvError: Cannot access server-only key + "DATABASE_URL" on client. +
+
+
+
+ Call Stack +
+
+ at{" "} + + Proxy.<anonymous> + {" "} + (src/env.ts:14) +
+
+ at Header{" "} + (app/components/header.tsx:5) +
+
+
+
+
+
+ ); +} diff --git a/apps/www/components/page/secure-boundary.tsx b/apps/www/components/page/secure-boundary.tsx new file mode 100644 index 000000000..e757f73e3 --- /dev/null +++ b/apps/www/components/page/secure-boundary.tsx @@ -0,0 +1,27 @@ +import { highlightTs } from "./highlight-ts"; +import { SecureBoundaryView } from "./secure-boundary-view"; + +const FLAT_ENV_CODE = `import arkenv from "@/generated/env.gen"; + +export const env = arkenv({ + NEXT_PUBLIC_API_URL: "string.url", + DATABASE_URL: "string.url", + STRIPE_SECRET_KEY: "string", +});`; + +const COMPILED_BUNDLE_CODE = `export const env = { + NEXT_PUBLIC_API_URL: "https://api.acme.com", + // 🔒 Secrets omitted from client bundle! +};`; + +/** + * Client / server env boundary pitch showcasing the recommended Flat Layout. + */ +export async function SecureBoundary() { + const [envHtml, bundleHtml] = await Promise.all([ + highlightTs(FLAT_ENV_CODE), + highlightTs(COMPILED_BUNDLE_CODE), + ]); + + return ; +} diff --git a/apps/www/components/page/type-safety-showcase.tsx b/apps/www/components/page/type-safety-showcase.tsx new file mode 100644 index 000000000..5cc0f68db --- /dev/null +++ b/apps/www/components/page/type-safety-showcase.tsx @@ -0,0 +1,104 @@ +/** + * Simulated IDE autocomplete + Terminal fail-fast runtime output side-by-side. + */ +import { WindowChrome } from "./window-chrome"; + +export function TypeSafetyShowcase() { + return ( +
+
+

+ 02 / TYPESAFE +

+

+ Editor autocomplete with fail-fast at runtime. +

+

+ Your editor gets perfect types. ArkEnv rejects missing or invalid keys + at build or startup, before they reach production. +

+
+ +
+ {/* Left Window: IDE Autocomplete */} +
+ +
+						
+							import
+							{" { "}
+							env
+							{" } "}
+							from
+							{` "./env"`}
+							{";\n\n"}
+							const
+							{" db = "}
+							env
+							.
+							
+					
+ +
+ + {/* Right Window: Terminal Fail-Fast Runtime Output */} +
+ +
+
+ + Runtime Error + + + Process Exited (1) + +
+
+ ArkEnvError: Errors found while validating environment variables +
+
+
+ + DATABASE_URL + {" "} + must be a valid URL starting with postgresql:// or mysql:// (was + "localhost/db") +
+
+ PORT must + be an integer between 0 and 65535 (was "8080a") +
+
+
+
+
+
+ ); +} diff --git a/apps/www/components/page/video-demo.tsx b/apps/www/components/page/video-demo.tsx index 741889bfd..f557a2e79 100644 --- a/apps/www/components/page/video-demo.tsx +++ b/apps/www/components/page/video-demo.tsx @@ -1,12 +1,12 @@ "use client"; -import { ArrowUpRight } from "lucide-react"; +import { ArrowRight, ArrowUpRight } from "lucide-react"; import Image from "next/image"; import BackgroundVideo from "next-video/background-video"; import { useState } from "react"; import demo from "~/videos/demo.mp4"; +import { WindowChrome } from "./window-chrome"; -// Actual video dimensions const WIDTH = 2048; const HEIGHT = 1672; @@ -14,8 +14,8 @@ const getAspectRatio = (width: number, height: number) => `${width} / ${height}`; /** - * VideoDemo component displays an interactive video demonstration with a browser-style frame. - * Clicking the video opens the demo in StackBlitz. Falls back to a GIF if video fails to load. + * Product demo in a faux window frame (main-branch pattern, Aurora tokens). + * Click opens the StackBlitz playground. */ export function VideoDemo() { const [videoError, setVideoError] = useState(false); @@ -31,58 +31,58 @@ export function VideoDemo() { }; return ( -
-
- + + + + + View more examples + +
); } diff --git a/apps/www/components/page/window-chrome.tsx b/apps/www/components/page/window-chrome.tsx new file mode 100644 index 000000000..8b3dea7f3 --- /dev/null +++ b/apps/www/components/page/window-chrome.tsx @@ -0,0 +1,35 @@ +/** + * Shared faux-window traffic lights + optional title or URL address bar. + * Used by CLI, IDE mock, before/after compare, and playground frame. + */ +export function WindowChrome({ + title, + url, + className, +}: { + title?: string; + url?: string; + className?: string; +}) { + return ( + + ); +} diff --git a/apps/www/components/ui/new-badge.tsx b/apps/www/components/ui/new-badge.tsx index 326e0b2bf..b3cf9f5d3 100644 --- a/apps/www/components/ui/new-badge.tsx +++ b/apps/www/components/ui/new-badge.tsx @@ -4,9 +4,12 @@ import { Badge } from "./badge"; export function NewBadge({ className }: { className?: string }) { return ( - New + New ); } diff --git a/apps/www/components/ui/theme-toggle.tsx b/apps/www/components/ui/theme-toggle.tsx index 7a8f6eb18..1bf827063 100644 --- a/apps/www/components/ui/theme-toggle.tsx +++ b/apps/www/components/ui/theme-toggle.tsx @@ -9,6 +9,8 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/ui/tooltip"; +import { useFeatureFlag } from "~/hooks/use-feature-flag"; +import { FeatureFlag } from "~/lib/posthog/feature-flags"; import { cn } from "~/lib/utils"; type Option = { @@ -22,6 +24,7 @@ let hydrated = false; let cachedTheme: string | undefined; export function ThemeToggle({ className }: { className?: string }) { + const themeToggleEnabled = useFeatureFlag(FeatureFlag.THEME_TOGGLE); const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(hydrated); @@ -35,6 +38,10 @@ export function ThemeToggle({ className }: { className?: string }) { setMounted(true); }, []); + if (themeToggleEnabled !== true) { + return null; + } + const options = [ { value: "light", icon: Sun, label: "Light" }, { value: "system", icon: Monitor, label: "System" }, diff --git a/apps/www/content/docs/arkenv/faq.mdx b/apps/www/content/docs/arkenv/faq.mdx index e54926c16..7030831da 100644 --- a/apps/www/content/docs/arkenv/faq.mdx +++ b/apps/www/content/docs/arkenv/faq.mdx @@ -149,7 +149,7 @@ Read the [CLI reference docs](/docs/cli) for a complete list of flags and capabi ## Does ArkEnv support AI development tools like Claude Code or Cursor? -Yes. ArkEnv is optimized for modern development workflows and features a **dedicated AI Agent Skill**. Providing this context to LLM-driven tools (like Claude Code, Cursor, Codex or Antigravity) allows your AI assistant to **automatically maintain, update, and tighten environment schemas** as your application code evolves, matching schema changes to code changes seamlessly. +Yes. ArkEnv is optimized for modern development workflows and features a dedicated [**AI Agent Skill**](/docs/arkenv/integrations/ai/skills). Providing this context to LLM-driven tools (like Claude Code, Cursor, Codex or Antigravity) allows your AI assistant to **automatically maintain, update, and tighten environment schemas** as your application code evolves, matching schema changes to code changes seamlessly. ## Is it safe to use in production? diff --git a/apps/www/hooks/use-copy-command.ts b/apps/www/hooks/use-copy-command.ts index 6cd69b919..b180e5ec8 100644 --- a/apps/www/hooks/use-copy-command.ts +++ b/apps/www/hooks/use-copy-command.ts @@ -11,9 +11,14 @@ import { useToast } from "./use-toast"; * @param command - The string command to be copied. * @returns An object containing the `copied` state and the `copy` function. */ -export function useCopyCommand(command: string) { +export function useCopyCommand( + command: string, + options?: { successDescription?: string }, +) { const [copied, setCopied] = useState(false); const { toast } = useToast(); + const successDescription = + options?.successDescription ?? "Command copied to clipboard!"; const copy = async () => { try { @@ -21,7 +26,7 @@ export function useCopyCommand(command: string) { setCopied(true); setTimeout(() => setCopied(false), 2000); toast({ - description: "Command copied to clipboard!", + description: successDescription, duration: 2000, }); } catch (error) { diff --git a/apps/www/hooks/use-feature-flag.test.ts b/apps/www/hooks/use-feature-flag.test.ts new file mode 100644 index 000000000..d864ca1ec --- /dev/null +++ b/apps/www/hooks/use-feature-flag.test.ts @@ -0,0 +1,48 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import posthog from "posthog-js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { FeatureFlag } from "~/lib/posthog/feature-flags"; +import { useFeatureFlag } from "./use-feature-flag"; + +vi.mock("posthog-js", () => ({ + default: { + isFeatureEnabled: vi.fn(), + onFeatureFlags: vi.fn(), + }, +})); + +describe("useFeatureFlag", () => { + beforeEach(() => { + vi.mocked(posthog.isFeatureEnabled).mockReset(); + vi.mocked(posthog.onFeatureFlags).mockReset(); + vi.mocked(posthog.onFeatureFlags).mockReturnValue(() => undefined); + }); + + it("returns false until the flag is explicitly true", async () => { + vi.mocked(posthog.isFeatureEnabled).mockReturnValue(undefined); + const { result } = renderHook(() => + useFeatureFlag(FeatureFlag.THEME_TOGGLE), + ); + expect(result.current).toBe(false); + }); + + it("returns true when PostHog reports the flag enabled", async () => { + vi.mocked(posthog.isFeatureEnabled).mockReturnValue(true); + const { result } = renderHook(() => + useFeatureFlag(FeatureFlag.THEME_TOGGLE), + ); + await waitFor(() => { + expect(result.current).toBe(true); + }); + }); + + it("returns false for non-true flag values", async () => { + vi.mocked(posthog.isFeatureEnabled).mockReturnValue(false); + const { result } = renderHook(() => + useFeatureFlag(FeatureFlag.THEME_TOGGLE), + ); + await waitFor(() => { + expect(result.current).toBe(false); + }); + }); +}); diff --git a/apps/www/hooks/use-feature-flag.ts b/apps/www/hooks/use-feature-flag.ts new file mode 100644 index 000000000..f7c9af5a1 --- /dev/null +++ b/apps/www/hooks/use-feature-flag.ts @@ -0,0 +1,24 @@ +"use client"; + +import posthog from "posthog-js"; +import { useEffect, useState } from "react"; +import type { FeatureFlag } from "~/lib/posthog/feature-flags"; + +/** + * Subscribe to a PostHog boolean feature flag. + * Returns true only when the flag is explicitly enabled. + */ +export function useFeatureFlag(flag: FeatureFlag): boolean { + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + const sync = () => { + setEnabled(posthog.isFeatureEnabled(flag) === true); + }; + + sync(); + return posthog.onFeatureFlags(sync); + }, [flag]); + + return enabled; +} diff --git a/apps/www/lib/posthog/feature-flags.ts b/apps/www/lib/posthog/feature-flags.ts new file mode 100644 index 000000000..f6fbb0727 --- /dev/null +++ b/apps/www/lib/posthog/feature-flags.ts @@ -0,0 +1,8 @@ +/** + * PostHog feature flag keys. + * Keep each flag consumed in as few places as possible. + */ +export enum FeatureFlag { + /** When true, show the light/dark/system theme switcher. Default: off. */ + THEME_TOGGLE = "theme-toggle", +} diff --git a/apps/www/lib/posthog/index.ts b/apps/www/lib/posthog/index.ts index 5c62e04f5..32f83d228 100644 --- a/apps/www/lib/posthog/index.ts +++ b/apps/www/lib/posthog/index.ts @@ -1 +1,2 @@ export * from "./config"; +export * from "./feature-flags"; diff --git a/apps/www/package.json b/apps/www/package.json index 821184cb1..1f07b3a08 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -45,6 +45,7 @@ "fumadocs-twoslash": "3.2.0", "fumadocs-typescript": "^5.2.6", "fumadocs-ui": "16.8.7", + "geist": "^1.7.2", "import-in-the-middle": "2.0.6", "lucide-react": "1.14.0", "next": "catalog:", diff --git a/apps/www/public/assets/icon.svg b/apps/www/public/assets/icon.svg index a02e45c3b..dfaaf9a1f 100644 --- a/apps/www/public/assets/icon.svg +++ b/apps/www/public/assets/icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/www/tokens.css b/apps/www/tokens.css new file mode 100644 index 000000000..5b7295632 --- /dev/null +++ b/apps/www/tokens.css @@ -0,0 +1,69 @@ +/* Hallmark · genre: atmospheric · macrostructure: Workbench · tone: atmospheric + * theme: Aurora · anchor hue: 200 · nav: N5 · footer: Ft5 · enrichment: product-demo + * axes: dark / geist-sans + geist-mono + geist-pixel-grid / cool-cyan + * fonts: Geist · Geist Mono · Geist Pixel Grid + */ + +:root { + --color-paper: oklch(13% 0.018 200); + --color-paper-2: oklch(17% 0.02 200); + --color-paper-3: oklch(21% 0.022 200); + --color-ink: oklch(95% 0.01 200); + --color-ink-2: oklch(78% 0.02 200); + --color-muted: oklch(62% 0.025 200); + --color-rule: oklch(30% 0.02 200); + --color-accent: oklch(74% 0.13 195); + --color-accent-ink: oklch(14% 0.02 200); + --color-focus: oklch(78% 0.12 195); + --color-bloom-a: oklch(52% 0.11 195 / 0.32); + --color-bloom-b: oklch(42% 0.09 220 / 0.22); + --color-success: oklch(72% 0.14 155); + --color-danger: oklch(65% 0.18 25); + --color-danger-ink: oklch(78% 0.14 25); + --color-cmd: oklch(78% 0.19 55); + --color-traffic-close: oklch(68% 0.18 25); + --color-traffic-min: oklch(78% 0.14 85); + --color-traffic-max: oklch(72% 0.16 145); + + --font-display: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-body: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-outlier: var(--font-geist-mono), ui-monospace, monospace; + --font-mono: var(--font-geist-mono), ui-monospace, monospace; + /* --font-geist-pixel-grid is provided by geist/font/pixel (GeistPixelGrid) */ + + --space-3xs: 0.25rem; + --space-2xs: 0.5rem; + --space-xs: 0.75rem; + --space-sm: 1rem; + --space-md: 1.5rem; + --space-lg: 2rem; + --space-xl: 3rem; + --space-2xl: 4.5rem; + --space-3xl: 7rem; + --page-gutter: clamp(1rem, 4vw, 2.5rem); + + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-md: 1.125rem; + --text-lg: 1.375rem; + --text-xl: 1.75rem; + --text-2xl: 2.25rem; + --text-display-s: clamp(2rem, 4vw + 0.5rem, 3rem); + --text-display: clamp(2.5rem, 5vw + 0.75rem, 4.25rem); + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + /* open-slide rise / blur-reveal */ + --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1); + --dur-micro: 120ms; + --dur-short: 220ms; + --dur-long: 420ms; + --dur-reveal: 800ms; + + --rule-hair: 1px; + --radius-pill: 999px; + --radius-frame: 0.75rem; + --shadow-frame: 0 24px 60px -28px oklch(0% 0 0 / 0.55); + --shadow-nav: 0 8px 24px -12px oklch(0% 0 0 / 0.35); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 510eec1ad..3639995ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -677,6 +677,9 @@ importers: fumadocs-ui: specifier: 16.8.7 version: 16.8.7(@tailwindcss/oxide@4.2.4)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.1))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4) + geist: + specifier: ^1.7.2 + version: 1.7.2(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) import-in-the-middle: specifier: 2.0.6 version: 2.0.6 @@ -999,7 +1002,7 @@ importers: devDependencies: '@fumadocs/ui': specifier: 16.5.0 - version: 16.5.0(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4) + version: 16.5.0(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4) '@radix-ui/react-collapsible': specifier: 1.1.12 version: 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1014,10 +1017,10 @@ importers: version: 19.2.3(@types/react@19.2.14) fumadocs-core: specifier: 16.8.7 - version: 16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) + version: 16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) fumadocs-ui: specifier: 16.8.7 - version: 16.8.7(@tailwindcss/oxide@4.2.4)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4) + version: 16.8.7(@tailwindcss/oxide@4.2.4)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4) lucide-react: specifier: 1.14.0 version: 1.14.0(react@19.2.5) @@ -7709,6 +7712,11 @@ packages: fzf@0.5.2: resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + geist@1.7.2: + resolution: {integrity: sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==} + peerDependencies: + next: '>=13.2.0' + gemoji@8.1.0: resolution: {integrity: sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw==} @@ -12715,9 +12723,9 @@ snapshots: '@tailwindcss/oxide': 4.2.4 tailwindcss: 4.2.4 - '@fumadocs/ui@16.5.0(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4)': + '@fumadocs/ui@16.5.0(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4)': dependencies: - fumadocs-core: 16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) + fumadocs-core: 16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) next-themes: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) postcss-selector-parser: 7.1.1 react: 19.2.5 @@ -16658,7 +16666,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/expect@4.1.5': dependencies: @@ -16720,7 +16728,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(@vitest/ui@4.1.5)(jsdom@29.1.1(@noble/hashes@1.8.0))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/utils@4.1.5': dependencies: @@ -18412,7 +18420,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3): + fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 @@ -18556,7 +18564,7 @@ snapshots: - '@types/react-dom' - tailwindcss - fumadocs-ui@16.8.7(@tailwindcss/oxide@4.2.4)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4): + fumadocs-ui@16.8.7(@tailwindcss/oxide@4.2.4)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tailwindcss@4.2.4): dependencies: '@fumadocs/tailwind': 0.0.5(@tailwindcss/oxide@4.2.4)(tailwindcss@4.2.4) '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -18570,7 +18578,7 @@ snapshots: '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) class-variance-authority: 0.7.1 - fumadocs-core: 16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) + fumadocs-core: 16.8.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.14.0(react@19.2.5))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) lucide-react: 1.14.0(react@19.2.5) motion: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) next-themes: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -18598,6 +18606,10 @@ snapshots: fzf@0.5.2: {} + geist@1.7.2(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)): + dependencies: + next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + gemoji@8.1.0: {} gensync@1.0.0-beta.2: {} diff --git a/tokens.css b/tokens.css new file mode 100644 index 000000000..10d7ffd34 --- /dev/null +++ b/tokens.css @@ -0,0 +1,62 @@ +/* Hallmark · genre: atmospheric · macrostructure: Workbench · tone: atmospheric + * theme: Aurora · anchor hue: 200 · nav: N5 · footer: Ft5 · enrichment: product-demo + * axes: dark / geist-sans + geist-mono + geist-pixel-grid / cool-cyan + * fonts: Geist · Geist Mono · Geist Pixel Grid + */ + +:root { + --color-paper: oklch(13% 0.018 200); + --color-paper-2: oklch(17% 0.02 200); + --color-paper-3: oklch(21% 0.022 200); + --color-ink: oklch(95% 0.01 200); + --color-ink-2: oklch(78% 0.02 200); + --color-muted: oklch(62% 0.025 200); + --color-rule: oklch(30% 0.02 200); + --color-accent: oklch(74% 0.13 195); + --color-accent-ink: oklch(14% 0.02 200); + --color-focus: oklch(78% 0.12 195); + --color-bloom-a: oklch(52% 0.11 195 / 0.32); + --color-bloom-b: oklch(42% 0.09 220 / 0.22); + --color-success: oklch(72% 0.14 155); + --color-danger: oklch(65% 0.18 25); + --color-danger-ink: oklch(78% 0.14 25); + + --font-display: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-body: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; + --font-outlier: var(--font-geist-mono), ui-monospace, monospace; + --font-mono: var(--font-geist-mono), ui-monospace, monospace; + /* --font-geist-pixel-grid is provided by geist/font/pixel (GeistPixelGrid) */ + + --space-3xs: 0.25rem; + --space-2xs: 0.5rem; + --space-xs: 0.75rem; + --space-sm: 1rem; + --space-md: 1.5rem; + --space-lg: 2rem; + --space-xl: 3rem; + --space-2xl: 4.5rem; + --space-3xl: 7rem; + --page-gutter: clamp(1rem, 4vw, 2.5rem); + + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-md: 1.125rem; + --text-lg: 1.375rem; + --text-xl: 1.75rem; + --text-2xl: 2.25rem; + --text-display-s: clamp(2rem, 4vw + 0.5rem, 3rem); + --text-display: clamp(2.5rem, 5vw + 0.75rem, 4.25rem); + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dur-micro: 120ms; + --dur-short: 220ms; + --dur-long: 420ms; + + --rule-hair: 1px; + --radius-pill: 999px; + --radius-frame: 0.75rem; + --shadow-frame: 0 24px 60px -28px oklch(0% 0 0 / 0.55); + --shadow-nav: 0 8px 24px -12px oklch(0% 0 0 / 0.35); +}