feat(www): improvements to mobile design, layout of diagrams

This commit is contained in:
Musa 2025-11-20 17:08:14 -08:00
parent 52c6ccb9a9
commit fa956962f0
15 changed files with 877 additions and 251 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -159,16 +159,7 @@
--radius-xl: calc(var(--radius) + 4px);
}
/* Global letter-spacing for fonts */
.font-sans,
[class*="font-sans"] {
letter-spacing: -4.56px;
}
.font-mono,
[class*="font-mono"] {
letter-spacing: -0.96px;
}
/* Global letter-spacing removed - use Tailwind tracking utilities (tracking-tight, tracking-[-1.92px], etc.) instead */
body {
font-family: var(--font-sans);

View file

@ -16,19 +16,19 @@ const footerLinks = {
export function Footer() {
return (
<footer className="relative overflow-hidden py-24 px-6 lg:px-[102px] pb-48" style={{ background: 'linear-gradient(to top right, #ffffff, #dcdfff)' }}>
<footer className="relative overflow-hidden pt-20 px-6 lg:px-[102px] pb-48" style={{ background: 'linear-gradient(to top right, #ffffff, #dcdfff)' }}>
<div className="max-w-[81rem] mx-auto relative z-10">
{/* Main Grid Layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
{/* Left Column - Tagline and Copyright */}
<div className="flex flex-col">
<p className="font-sans text-2xl text-black tracking-[-1.7px]! leading-8 mb-8">
<p className="font-sans text-lg sm:text-xl lg:text-2xl text-black tracking-[-1.2px] sm:tracking-[-1.5px] lg:tracking-[-1.7px]! leading-7 sm:leading-8 mb-6 sm:mb-8">
Plano is the powerful, intelligent platform that empowers teams to seamlessly build, automate, and scale agentic systems with ease.
</p>
{/* Copyright */}
<div className="mt-auto">
<p className="font-sans text-base text-black/63 tracking-[-0.8px]!">
<p className="font-sans text-sm sm:text-base text-black/63 tracking-[-0.6px] sm:tracking-[-0.8px]!">
© Katanemo Labs, Inc. 2025 / Plano by Katanemo Labs, Inc.
</p>
</div>
@ -38,15 +38,15 @@ export function Footer() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
{/* Company Links */}
<div>
<h3 className="font-sans font-medium text-3xl text-black tracking-[-1.6px]! mb-6">
<h3 className="font-sans font-medium text-2xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
Company
</h3>
<nav className="space-y-4">
<nav className="space-y-3 sm:space-y-4">
{footerLinks.company.map((link) => (
<Link
key={link.href}
href={link.href}
className="block font-sans text-xl text-black tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
className="block font-sans text-base sm:text-lg lg:text-xl text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
>
{link.label}
</Link>
@ -56,15 +56,15 @@ export function Footer() {
{/* Developer Resources */}
<div>
<h3 className="font-sans font-medium text-3xl text-black tracking-[-1.6px]! mb-6">
<h3 className="font-sans font-medium text-2xl sm:text-2xl lg:text-3xl text-black tracking-[-1.2px] sm:tracking-[-1.4px] lg:tracking-[-1.6px]! mb-4 sm:mb-6">
Developer Resources
</h3>
<nav className="space-y-4">
<nav className="space-y-3 sm:space-y-4">
{footerLinks.developerResources.map((link) => (
<Link
key={link.href}
href={link.href}
className="block font-sans text-xl text-black tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
className="block font-sans text-base sm:text-lg lg:text-xl text-black tracking-[-0.8px] sm:tracking-[-0.9px] lg:tracking-[-1px]! hover:text-[var(--primary)] transition-colors"
>
{link.label}
</Link>

View file

@ -5,40 +5,43 @@ import { NetworkAnimation } from "./NetworkAnimation";
export function Hero() {
return (
<section className="relative pt-15 pb-6 px-6 lg:px-8">
<section className="relative pt-8 sm:pt-12 lg:pt-15 pb-6 px-4 sm:px-6 lg:px-8">
<div className="hidden lg:block">
<NetworkAnimation />
</div>
<div className="max-w-[81rem] mx-auto relative z-10">
<div className="max-w-3xl mb-4">
<div className="max-w-3xl mb-6 sm:mb-4">
{/* Version Badge */}
<div className="mb-6">
<div className="inline-flex items-center gap-2 px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
<span className="text-sm font-medium text-black/65">v0.4</span>
<span className="text-sm font-medium text-black"></span>
<span className="text-sm font-[600] tracking-[-0.6px]! text-black">Unified /v1/responses API with state management</span>
<div className="mb-4 sm:mb-6">
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
<span className="text-xs sm:text-sm font-medium text-black/65">v0.4</span>
<span className="text-xs sm:text-sm font-medium text-black hidden sm:inline"></span>
<span className="text-xs sm:text-sm font-[600] tracking-[-0.6px]! text-black leading-tight">
<span className="hidden sm:inline">Unified /v1/responses API with state management</span>
<span className="sm:hidden">Unified /v1/responses API</span>
</span>
</div>
</div>
{/* Main Heading */}
<h1 className="text-5xl lg:text-7xl font-normal leading-tight tracking-tight text-black mb-4 flex flex-col -space-y-3">
<h1 className="text-4xl sm:text-4xl md:text-5xl lg:text-7xl font-normal leading-tight tracking-tighter text-black mb-4 sm:mb-6 flex flex-col gap-0 sm:-space-y-2 lg:-space-y-3">
<span className="font-sans">Models-native </span>
<span className="font-sans font-medium text-[var(--secondary)]">dataplane for agents</span>
</h1>
</div>
{/* Subheading with CTA Buttons on the right */}
{/* Subheading with CTA Buttons */}
<div className="max-w-7xl flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
<p className="text-xl lg:text-2xl font-sans font-[500] tracking-[-1.92px]! text-black max-w-4xl">
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-sans font-[500] tracking-[-1.2px] sm:tracking-[-1.92px]! text-black max-w-4xl">
Build agents faster, and scale them reliably by offloading the plumbing work in AI agents.
</p>
{/* CTA Buttons */}
<div className="justify-self-end flex justify-end items-center gap-4">
<Button asChild>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 sm:gap-4 w-full sm:w-auto lg:justify-end">
<Button asChild className="w-full sm:w-auto">
<Link href="/get-started">Get started</Link>
</Button>
<Button variant="secondary" asChild>
<Button variant="secondary" asChild className="w-full sm:w-auto">
<Link href="/docs">Documentation</Link>
</Button>
</div>

View file

@ -5,28 +5,49 @@ import Image from "next/image";
export function HowItWorksSection() {
return (
<section className="bg-[#1a1a1a] text-white pb-28 px-6 lg:px-[102px]">
<div className="max-w-324 mx-auto">
<div className="flex flex-col gap-16">
<section className="bg-[#1a1a1a] text-white pb-16 sm:pb-20 lg:pb-28 sm:pt-3 pt-20">
<div className="max-w-324 mx-auto sm:pl-0">
<div className="flex flex-col gap-8 sm:gap-12 lg:gap-16">
{/* Header and Description */}
<div className="max-w-4xl">
<h2 className="font-sans font-normal text-3xl lg:text-4xl tracking-[-2.4px]! text-white leading-[1.03] mb-8">
<div className="max-w-4xl lg:-ml-[102px] lg:pl-[102px] sm:pl-0 pl-4">
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.4px]! text-white leading-[1.03] mb-6 sm:mb-8">
A high-level overview of how Plano works
</h2>
<div className="font-mono text-white text-xl lg:text-lg leading-10 tracking-[-1.2px]!">
<div className="font-mono text-white w-100 sm:w-full text-sm sm:text-lg lg:text-lg leading-7 sm:leading-9 lg:leading-10 tracking-[-0.8px] sm:tracking-[-1.2px]!">
<p className="mb-0">
Plano offers a delightful developer experience with a simple configuration file that describes the types of prompts your agentic app supports, a set of APIs that need to be plugged in for agentic scenarios (including retrieval queries) and your choice of LLMs.
</p>
</div>
</div>
{/* Large Diagram */}
<div className="w-full">
<Image
{/* Large Diagram - Scrollable on mobile, normal on desktop */}
{/* Mobile: Full-width scrollable container that extends to viewport edges */}
<div className="mt-5 lg:hidden relative left-1/2 right-1/2 -ml-[50vw] -mr-[50vw] w-screen overflow-x-auto overflow-y-visible" style={{ scrollbarWidth: "none", msOverflowStyle: "none", WebkitOverflowScrolling: "touch" }}>
<style jsx>{`
.diagram-scroll-container::-webkit-scrollbar {
display: none;
}
`}</style>
<div className="diagram-scroll-container inline-block">
<Image
src="/HowItWorks.svg"
alt="How Plano Works Diagram"
width={1200}
height={600}
className="h-auto"
style={{ width: "1200px", maxWidth: "none", display: "block" }}
priority
/>
</div>
</div>
{/* Desktop: Extends to container edges */}
<div className="hidden lg:block -w-[calc(10%+20px)] -mx-[10px]">
<Image
src="/HowItWorks.svg"
alt="How Plano Works Diagram"
width={1200}
height={600}
width={10}
height={10}
className="w-full h-auto"
priority
/>

View file

@ -65,21 +65,24 @@ export function IdeaToAgentSection() {
};
return (
<section className="relative py-24 px-6 lg:px-[102px]">
<section className="relative py-12 sm:py-16 lg:py-24 px-4 sm:px-6 lg:px-[102px]">
<div className="max-w-[81rem] mx-auto">
{/* Main Heading */}
<h2 className="font-sans font-normal text-4xl tracking-[-2.96px]! text-black mb-10">
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.96px]! text-black mb-6 sm:mb-8 lg:mb-10">
Idea to agent without overhead
</h2>
{/* Progress Indicators */}
<div className="flex gap-2 mb-12">
<div className="flex gap-1.5 sm:gap-2 mb-4 sm:mb-6 lg:mb-6 w-full">
{carouselData.map((_, index) => (
<button
key={index}
onClick={() => handleSlideClick(index)}
className="relative h-2 rounded-full overflow-hidden transition-all duration-300 hover:opacity-80"
style={{ width: index === currentSlide ? '292px' : '293px' }}
className={`relative h-1.5 sm:h-2 rounded-full overflow-hidden transition-all duration-300 hover:opacity-80 ${
index === currentSlide
? 'flex-1 sm:w-16 md:w-20 lg:w-[292px]'
: 'flex-1 sm:w-16 md:w-20 lg:w-[293px]'
}`}
>
{/* Background */}
<div className="absolute inset-0 bg-black/6 rounded-full" />
@ -103,51 +106,74 @@ export function IdeaToAgentSection() {
))}
</div>
{/* Carousel Content */}
<div className="relative min-h-[300px] lg:min-h-[400px]">
{/* Carousel Content - Fixed height to prevent layout shift */}
<div className="relative h-[500px] sm:h-[550px] md:h-[600px] lg:h-[500px]">
<AnimatePresence mode="wait">
<motion.div
key={currentSlide}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
className="absolute inset-0"
>
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center lg:gap-12">
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center lg:gap-12 h-full">
{/* Left Content */}
<div className="flex-1">
<div className="max-w-[692px] mt-6">
<div className="flex-1 order-1 lg:order-1 flex flex-col justify-center">
<div className="max-w-[692px] mt-0 lg:mt-0">
{/* Category */}
<p className="font-mono font-bold text-[#2a3178] text-xl tracking-[1.92px]! mb-4 leading-[1.102]">
<p className="font-mono font-bold text-[#2a3178] text-sm sm:text-base lg:text-xl tracking-[1.44px] sm:tracking-[1.92px]! mb-3 sm:mb-4 leading-[1.102]">
{carouselData[currentSlide].category}
</p>
{/* Title */}
<h3 className="font-sans font-medium text-[#9797ea] text-5xl tracking-[-2.96px]! mb-7">
<h3 className="font-sans font-medium text-[#9797ea] text-2xl sm:text-3xl lg:text-5xl tracking-tight sm:tracking-[-2.96px]! mb-4 sm:mb-6 lg:mb-7">
{carouselData[currentSlide].title}
</h3>
{/* Description */}
<div className="font-mono text-black text-lg leading-8 max-w-140">
<div className="font-mono text-black text-sm sm:text-base lg:text-lg leading-6 sm:leading-7 lg:leading-8 max-w-full lg:max-w-140 tracking-[-0.8px] sm:tracking-[-1.2px]!">
<p className="mb-0">
{carouselData[currentSlide].description}
</p>
</div>
<Button className="mt-8">
<Button className="mt-6 sm:mt-8 w-full sm:w-auto">
Learn more
</Button>
</div>
</div>
{/* Right Image - Only show if current slide has an image */}
{/* Image - Show below on mobile, right side on desktop */}
{carouselData[currentSlide].image && (
<div className="hidden lg:flex shrink-0 w-[400px] xl:w-[500px] justify-end items-center ">
<div className="flex lg:hidden shrink-0 w-full justify-center items-center mb-6 sm:mb-8 order-0 lg:order-2">
<img
src={carouselData[currentSlide].image}
alt={carouselData[currentSlide].category}
className="w-full h-auto max-h-[450px] object-contain"
className={`w-full h-auto object-contain ${
carouselData[currentSlide].image === "/Telemetry.svg"
? "max-w-md sm:max-w-lg max-h-[300px] sm:max-h-[350px]"
: "max-w-sm sm:max-w-md max-h-[250px] sm:max-h-[300px]"
}`}
/>
</div>
)}
{/* Right Image - Desktop only */}
{carouselData[currentSlide].image && (
<div className={`hidden lg:flex shrink-0 justify-end items-center order-2 ${
carouselData[currentSlide].image === "/Telemetry.svg"
? "w-[500px] xl:w-[600px]"
: "w-[400px] xl:w-[500px]"
}`}>
<img
src={carouselData[currentSlide].image}
alt={carouselData[currentSlide].category}
className={`w-full h-auto object-contain ${
carouselData[currentSlide].image === "/Telemetry.svg"
? "max-h-[550px]"
: "max-h-[450px]"
}`}
/>
</div>
)}

View file

@ -23,8 +23,10 @@ export function IntroSection() {
Plano is a framework-friendly proxy server and dataplane for agents,
deployed as a sidecar. Plano handles the critical plumbing work in AI
like agent routing and orchestration, comprehensive traces for agentic
interactions, guardrail hooks, unified APIs for LLMs so <strong> developers </strong>
can focus more on modeling workflows, <strong> product teams </strong> can accelerate
interactions, guardrail hooks, unified APIs for LLMs
</p>
<p className="mb-0 leading-8 mt-4">
<strong>Developers</strong> can focus more on modeling workflows, <strong>product teams</strong> can accelerate
feedback loops for reinforcement learning and <strong>engineering teams</strong> can
standardize policies and access controls across every agent and LLM for
safer, more reliable scaling.

View file

@ -26,23 +26,28 @@ const customerLogos = [
export function LogoCloud() {
return (
<section className="relative py-8 px-6 lg:px-8">
<section className="relative py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-[81rem] mx-auto">
<div className="flex items-center justify-center gap-8 lg:gap-12 flex-wrap lg:flex-nowrap">
{customerLogos.map((logo) => (
<div
key={logo.name}
className="flex items-center justify-center mx-auto opacity-60 hover:opacity-80 transition-opacity duration-300 w-48 h-12"
>
<Image
src={logo.src}
alt={`${logo.name} logo`}
width={128}
height={40}
className="w-full h-full object-contain filter grayscale hover:grayscale-0 transition-all duration-300"
/>
</div>
))}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 sm:gap-6 md:gap-8 lg:gap-12 items-center justify-items-center">
{customerLogos.map((logo, index) => {
const isLast = index === customerLogos.length - 1;
return (
<div
key={logo.name}
className={`flex items-center justify-center opacity-60 hover:opacity-80 transition-opacity duration-300 w-full max-w-32 sm:max-w-40 md:max-w-48 h-10 sm:h-12 md:h-16 ${
isLast ? 'col-span-2 md:col-span-3 lg:col-span-1' : ''
}`}
>
<Image
src={logo.src}
alt={`${logo.name} logo`}
width={128}
height={40}
className="w-full h-full object-contain filter grayscale hover:grayscale-0 transition-all duration-300"
/>
</div>
);
})}
</div>
</div>
</section>

View file

@ -1,10 +1,12 @@
"use client";
import React from "react";
import React, { useState, useEffect } from "react";
import Link from "next/link";
import { Logo } from "./Logo";
import { Button } from "./ui/button";
import { cn } from "@/lib/utils";
import { motion, AnimatePresence } from "framer-motion";
import { X, Menu } from "lucide-react";
const navItems = [
{ href: "/start", label: "start locally" },
@ -15,6 +17,147 @@ const navItems = [
];
export function Navbar() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isDarkBackground, setIsDarkBackground] = useState(false);
// Detect background color behind dropdown menu
useEffect(() => {
if (!isMenuOpen) {
setIsDarkBackground(false);
return;
}
const detectBackground = () => {
// Small delay to ensure DOM is ready
setTimeout(() => {
const nav = document.querySelector("nav");
if (!nav) return;
const navRect = nav.getBoundingClientRect();
const dropdownBottom = navRect.bottom;
const checkY = dropdownBottom + 20; // Just below the dropdown
// First, try to find section elements directly
const main = document.querySelector("main");
if (main) {
const sections = main.querySelectorAll("section");
let foundDarkSection = false;
sections.forEach((section) => {
const rect = section.getBoundingClientRect();
// Check if this section is visible below the navbar
if (rect.top <= checkY && rect.bottom > checkY) {
// Check for dark background classes
const classList = Array.from(section.classList);
const hasDarkBg = classList.some(cls =>
cls.includes('bg-[#1a1a1a]') ||
cls.includes('bg-black') ||
cls.includes('bg-gray-900') ||
cls.includes('bg-neutral-900') ||
cls.includes('dark')
);
if (hasDarkBg) {
foundDarkSection = true;
setIsDarkBackground(true);
return;
}
// Also check computed background
const computed = window.getComputedStyle(section);
const bg = computed.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
const rgbMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1]);
const g = parseInt(rgbMatch[2]);
const b = parseInt(rgbMatch[3]);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
setIsDarkBackground(luminance < 0.5);
foundDarkSection = true;
return;
}
}
}
});
if (foundDarkSection) return;
}
// Fallback: Check element at point
const centerX = window.innerWidth / 2;
const elementBelow = document.elementFromPoint(centerX, checkY);
if (elementBelow) {
let current: HTMLElement | null = elementBelow as HTMLElement;
let backgroundColor = "";
// Walk up the DOM tree
let levels = 0;
while (current && !backgroundColor && current !== document.body && levels < 15) {
const computed = window.getComputedStyle(current);
const bg = computed.backgroundColor;
if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
const rgbaMatch = bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (rgbaMatch) {
const alpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1;
if (alpha > 0.1) {
backgroundColor = bg;
break;
}
} else {
backgroundColor = bg;
break;
}
}
current = current.parentElement;
levels++;
}
if (!backgroundColor) {
const bodyBg = window.getComputedStyle(document.body).backgroundColor;
backgroundColor = bodyBg;
}
if (backgroundColor) {
const rgbMatch = backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (rgbMatch) {
const r = parseInt(rgbMatch[1]);
const g = parseInt(rgbMatch[2]);
const b = parseInt(rgbMatch[3]);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
setIsDarkBackground(luminance < 0.5);
} else {
const darkColors = ['black', '#000', '#000000', 'rgb(0,0,0)', 'rgba(0,0,0', '#1a1a1a'];
const isDark = darkColors.some(color => backgroundColor.toLowerCase().includes(color.toLowerCase()));
setIsDarkBackground(isDark);
}
}
}
}, 100);
};
// Detect on open and on scroll
detectBackground();
const scrollHandler = () => detectBackground();
const resizeHandler = () => detectBackground();
window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("scroll", scrollHandler);
window.removeEventListener("resize", resizeHandler);
};
}, [isMenuOpen]);
// Close menu when route changes
const handleLinkClick = () => {
setIsMenuOpen(false);
};
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-gradient-to-b from-transparent to-white/5 backdrop-blur border-b border-neutral-200/5">
<div className="max-w-[85rem] mx-auto px-6 lg:px-8">
@ -44,26 +187,82 @@ export function Navbar() {
{/* Mobile Menu Button */}
<div className="md:hidden">
<button
className="p-2 rounded-md text-[var(--muted)] hover:text-[var(--primary)]"
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(!isMenuOpen);
}}
className="p-2 rounded-md text-[var(--muted)] hover:text-[var(--primary)] transition-colors"
aria-label="Toggle menu"
aria-expanded={isMenuOpen}
>
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<AnimatePresence mode="wait" initial={false}>
{isMenuOpen ? (
<motion.div
key="close"
initial={{ opacity: 0, rotate: -90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: 90 }}
transition={{ duration: 0.2 }}
>
<X className="h-6 w-6" />
</motion.div>
) : (
<motion.div
key="menu"
initial={{ opacity: 0, rotate: 90 }}
animate={{ opacity: 1, rotate: 0 }}
exit={{ opacity: 0, rotate: -90 }}
transition={{ duration: 0.2 }}
>
<Menu className="h-6 w-6" />
</motion.div>
)}
</AnimatePresence>
</button>
</div>
</div>
</div>
{/* Mobile Dropdown Menu - Outside constrained container for full width */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
className="md:hidden overflow-hidden bg-[#7580DF]/70"
>
<div className="max-w-[85rem] mx-auto px-6 lg:px-8 py-3">
<div className="flex flex-col gap-0.5">
{navItems.map((item, index) => (
<motion.div
key={item.href}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.3,
delay: index * 0.05,
ease: [0.16, 1, 0.3, 1],
}}
>
<Link
href={item.href}
onClick={handleLinkClick}
className={cn(
"block px-0 py-1.5 border-b border-dashed transition-colors font-mono tracking-tighter",
"text-sm font-medium",
"text-white",
)}>
{item.label}
</Link>
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</nav>
);
}

View file

@ -1,9 +1,9 @@
"use client";
import React, { useState } from "react";
import { ArrowRightIcon } from "lucide-react";
import { ArrowRightIcon, Network, Filter, TrendingUp, Shield, Server, XIcon } from "lucide-react";
import { Button } from "./ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogClose } from "./ui/dialog";
import { motion, AnimatePresence } from "framer-motion";
interface UseCase {
@ -12,6 +12,8 @@ interface UseCase {
title: string;
summary: string;
fullContent: string;
icon: React.ComponentType<{ className?: string }>;
gradient: string;
}
const useCasesData: UseCase[] = [
@ -20,35 +22,45 @@ const useCasesData: UseCase[] = [
category: "AGENT ORCHESTRATION",
title: "Multi-agent systems without framework lock-in",
summary: "Seamless routing and orchestration for complex agent interactions",
fullContent: "Plano manages agent routing and orchestration without framework dependencies, allowing seamless multi-agent interactions. This is ideal for building complex systems like automated customer support or data processing pipelines, where agents hand off tasks efficiently to deliver end-to-end solutions faster."
fullContent: "Plano manages agent routing and orchestration without framework dependencies, allowing seamless multi-agent interactions. This is ideal for building complex systems like automated customer support or data processing pipelines, where agents hand off tasks efficiently to deliver end-to-end solutions faster.",
icon: Network,
gradient: "from-[rgba(119,128,217,0.15)] via-[rgba(119,128,217,0.08)] to-[rgba(17,28,132,0.05)]"
},
{
id: 2,
category: "CONTEXT ENGINEERING",
title: "Reusable filters for smarter agents",
summary: "Inject data, reformulate queries, and enforce policies efficiently",
fullContent: "Plano's filter chain encourages reuse and decoupling for context engineering tasks like injecting data, reformulating queries, and enforcing policy before calls reach an agent or LLM. This means faster debugging, cleaner architecture, and more accurate, on-policy agents —without bespoke glue code."
fullContent: "Plano's filter chain encourages reuse and decoupling for context engineering tasks like injecting data, reformulating queries, and enforcing policy before calls reach an agent or LLM. This means faster debugging, cleaner architecture, and more accurate, on-policy agents —without bespoke glue code.",
icon: Filter,
gradient: "from-[rgba(177,184,255,0.15)] via-[rgba(177,184,255,0.08)] to-[rgba(17,28,132,0.05)]"
},
{
id: 3,
category: "REINFORCEMENT LEARNING",
title: "Production signals for continuous improvement",
summary: "Capture rich traces to accelerate training and refinement",
fullContent: "Plano captures hyper-rich tracing and log samples from production traffic, feeding into reinforcement learning and fine-tuning cycles. This accelerates iteration in areas like recommendation engines, helping teams quickly identify failures, refine prompts, and boost agent effectiveness based on real-user signals."
fullContent: "Plano captures hyper-rich tracing and log samples from production traffic, feeding into reinforcement learning and fine-tuning cycles. This accelerates iteration in areas like recommendation engines, helping teams quickly identify failures, refine prompts, and boost agent effectiveness based on real-user signals.",
icon: TrendingUp,
gradient: "from-[rgba(185,191,255,0.15)] via-[rgba(185,191,255,0.08)] to-[rgba(17,28,132,0.05)]"
},
{
id: 4,
category: "SECURITY",
title: "Built-in guardrails and centralized policies",
summary: "Safe scaling with jailbreak detection and access controls",
fullContent: "With built-in guardrails, centralized policies, and access controls, Plano ensures safe scaling across LLMs, detecting issues like jailbreak attempts. This is critical for deployments in regulated fields like finance or healthcare, and minimizing risks while standardizing reliability and security of agents."
fullContent: "With built-in guardrails, centralized policies, and access controls, Plano ensures safe scaling across LLMs, detecting issues like jailbreak attempts. This is critical for deployments in regulated fields like finance or healthcare, and minimizing risks while standardizing reliability and security of agents.",
icon: Shield,
gradient: "from-[rgba(119,128,217,0.15)] via-[rgba(119,128,217,0.08)] to-[rgba(17,28,132,0.05)]"
},
{
id: 5,
category: "ON-PREMISES",
title: "Full data control in regulated environments",
summary: "Deploy on private infrastructure without compromising features",
fullContent: "Plano's lightweight sidecar model deploys effortlessly on your private infrastructure, empowering teams in regulated sectors to maintain full data control while benefiting from unified LLM access, custom filter chains, and production-grade tracing—without compromising on security or scalability."
fullContent: "Plano's lightweight sidecar model deploys effortlessly on your private infrastructure, empowering teams in regulated sectors to maintain full data control while benefiting from unified LLM access, custom filter chains, and production-grade tracing—without compromising on security or scalability.",
icon: Server,
gradient: "from-[rgba(177,184,255,0.15)] via-[rgba(177,184,255,0.08)] to-[rgba(17,28,132,0.05)]"
}
];
@ -56,23 +68,23 @@ export function UseCasesSection() {
const [selectedUseCase, setSelectedUseCase] = useState<UseCase | null>(null);
return (
<section className="relative py-24 px-6 lg:px-[102px]">
<section className="relative py-12 sm:py-16 lg:py-10 px-4 sm:px-6 lg:px-[102px]">
<div className="max-w-[81rem] mx-auto">
{/* Section Header */}
<div className="mb-14">
<div className="mb-8 sm:mb-12 lg:mb-14">
{/* USE CASES Badge */}
<div className="mb-6">
<div className="inline-flex items-center gap-2 px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
<span className="font-mono font-bold text-[#2a3178] text-sm tracking-[1.62px]!">USE CASES</span>
<div className="mb-4 sm:mb-6">
<div className="inline-flex items-center gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur">
<span className="font-mono font-bold text-[#2a3178] text-xs sm:text-sm tracking-[1.44px] sm:tracking-[1.62px]!">USE CASES</span>
</div>
</div>
{/* Main Heading and CTA Button */}
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
<h2 className="font-sans font-normal text-3xl lg:text-4xl tracking-[-2.88px]! text-black leading-[1.03]">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 sm:gap-6">
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.88px]! text-black leading-[1.03]">
What's possible with Plano
</h2>
<Button>
<Button className="hidden lg:block">
Start building
</Button>
</div>
@ -85,65 +97,143 @@ export function UseCasesSection() {
key={useCase.id}
whileHover={{ y: -4 }}
transition={{ duration: 0.2 }}
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-8 h-90 flex flex-col justify-between cursor-pointer"
className="bg-gradient-to-b from-[rgba(177,184,255,0.16)] to-[rgba(17,28,132,0.035)] border-2 border-[rgba(171,178,250,0.27)] rounded-md p-4 sm:p-6 lg:p-8 h-auto sm:h-64 md:h-72 lg:h-90 flex flex-col justify-between cursor-pointer"
onClick={() => setSelectedUseCase(useCase)}
>
{/* Category */}
<div className="mb-6">
<p className="font-mono font-bold text-[#2a3178] text-base tracking-[1.92px]! mb-4">
<div className="mb-4 sm:mb-6">
<p className="font-mono font-bold text-[#2a3178] text-sm sm:text-base tracking-[1.44px] sm:tracking-[1.92px]! mb-3 sm:mb-4">
{useCase.category}
</p>
{/* Title */}
<h3 className="font-sans font-medium text-black text-xl lg:text-2xl tracking-[-1.2px]! leading-[1.102]">
<h3 className="font-sans font-medium text-black text-lg sm:text-xl lg:text-2xl tracking-[-1.2px]! leading-[1.102]">
{useCase.title}
</h3>
</div>
{/* Learn More Link */}
<div className="mt-auto">
<button className="group flex items-center gap-2 font-mono font-bold text-[var(--primary)] text-base tracking-[1.92px]! leading-[1.45] hover:text-[var(--primary-dark)] transition-colors">
<button className="group flex items-center gap-2 font-mono font-bold text-[var(--primary)] text-sm sm:text-base tracking-[1.44px] sm:tracking-[1.92px]! leading-[1.45] hover:text-[var(--primary-dark)] transition-colors">
LEARN MORE
<ArrowRightIcon className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
<ArrowRightIcon className="w-3.5 h-3.5 sm:w-4 sm:h-4 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</motion.div>
))}
</div>
{/* Start building button - Mobile only, appears last */}
<div className="lg:hidden mt-8">
<Button className="w-full">
Start building
</Button>
</div>
</div>
{/* Modal */}
<Dialog open={selectedUseCase !== null} onOpenChange={(open) => !open && setSelectedUseCase(null)}>
<AnimatePresence>
{selectedUseCase && (
<DialogContent className="max-w-2xl">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
<DialogHeader>
<div className="mb-4">
<p className="font-mono font-bold text-[#2a3178] text-sm tracking-[1.62px]! mb-3">
USE CASE
</p>
<DialogTitle className="font-sans font-medium text-3xl tracking-[-1.5px]! text-black leading-[1.1]">
{selectedUseCase.category}
</DialogTitle>
</div>
<DialogDescription className="font-mono text-[#494949] text-base leading-relaxed pt-2">
{selectedUseCase.fullContent}
</DialogDescription>
</DialogHeader>
<div className="mt-6 flex justify-end">
<Button onClick={() => setSelectedUseCase(null)}>
Close
</Button>
{selectedUseCase && (() => {
const IconComponent = selectedUseCase.icon;
return (
<DialogContent key={selectedUseCase.id} className="max-w-[90rem]! p-0 overflow-hidden" showCloseButton={false}>
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 8 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.98, y: 8 }}
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
className="relative"
>
{/* Gradient Background */}
<div className={`absolute inset-0 bg-gradient-to-br ${selectedUseCase.gradient} opacity-50`} />
{/* Decorative Border */}
<div className="absolute inset-0 border-2 border-[rgba(171,178,250,0.3)] rounded-lg pointer-events-none" />
{/* Custom Close Button */}
<DialogClose className="absolute top-4 right-4 z-50 rounded-xs opacity-70 hover:opacity-100 transition-opacity focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgba(171,178,250,0.5)] bg-white/80 backdrop-blur-sm p-2 hover:bg-white/90">
<XIcon className="w-5 h-5 text-[#2a3178]" />
<span className="sr-only">Close</span>
</DialogClose>
{/* Content Container */}
<div className="relative z-10 p-5 sm:p-8 md:p-10 lg:p-14">
{/* Header Section with Icon */}
<DialogHeader className="mb-4">
<div className="flex flex-col sm:flex-row sm:items-start gap-4 sm:gap-8 mb-8">
{/* Icon Container - hidden on mobile */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1], delay: 0.1 }}
className="hidden sm:flex shrink-0 w-14 h-14 sm:w-16 sm:h-16 rounded-xl bg-gradient-to-br from-[rgba(119,128,217,0.2)] to-[rgba(17,28,132,0.1)] border-2 border-[rgba(171,178,250,0.4)] items-center justify-center shadow-lg backdrop-blur-sm mx-0"
>
<IconComponent className="w-8 h-8 text-[#2a3178]" />
</motion.div>
{/* Title Section */}
<div className="flex-1 text-left mt-4 sm:mt-0">
<motion.p
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1], delay: 0.15 }}
className="font-mono font-bold text-[#2a3178] text-xs tracking-[1.62px]! mb-1 uppercase"
>
USE CASE
</motion.p>
<motion.div
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1], delay: 0.2 }}
>
<DialogTitle className="font-sans font-medium text-2xl sm:text-3xl lg:text-4xl xl:text-4xl tracking-[-1.5px]! text-black leading-[1.1] mb-4">
{selectedUseCase.title}
</DialogTitle>
<div className="inline-flex items-center px-3 py-1 rounded-full bg-[rgba(185,191,255,0.3)] border border-[rgba(171,178,250,0.4)] backdrop-blur-sm">
<span className="font-mono font-bold text-[#2a3178] text-xs tracking-[1.44px]!">
{selectedUseCase.category}
</span>
</div>
</motion.div>
</div>
</div>
</DialogHeader>
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1], delay: 0.3 }}
className="mb-10"
>
<DialogDescription className="font-mono text-[#494949] text-base lg:text-base xl:text-lg leading-relaxed tracking-tight max-w-none mb-0">
{selectedUseCase.fullContent}
</DialogDescription>
</motion.div>
{/* Footer with CTA - mobile friendly */}
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1], delay: 0.35 }}
className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 pt-8 border-t border-[rgba(171,178,250,0.2)]"
>
{/* "Ready to get started?" is now first in column on mobile */}
<div className="flex items-center gap-2 text-sm font-mono text-[#494949] justify-center sm:justify-start order-0">
<span>Ready to get started?</span>
</div>
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto order-1">
<Button className="w-full sm:w-auto">
Start building
<ArrowRightIcon className="w-4 h-4" />
</Button>
</div>
</motion.div>
</div>
</motion.div>
</DialogContent>
)}
);
})()}
</AnimatePresence>
</Dialog>
</section>

View file

@ -11,7 +11,7 @@ const verticalCarouselData = [
category: "INTRODUCTION",
title: "Simple to revolutionary",
description: "Plano is an intelligent (edge and LLM) proxy server designed for agents - to help you focus on core business objectives. Arch handles critical but the pesky tasks related to the handling and processing of prompts, which includes detecting and rejecting jailbreak attempts, intelligent task routing for improved accuracy, mapping user requests into 'backend' functions, and managing the observability of prompts and LLM in a centralized way.",
diagram: "/Introduction.svg"
diagram: "/IntroDiagram.svg"
},
{
id: 2,
@ -32,7 +32,7 @@ const verticalCarouselData = [
category: "PURPOSE-BUILT",
title: "Task-optimized, efficient LLMs",
description: "Unlike generic API gateways, Plano is purpose-built for AI agent workloads. Every feature is designed with prompt processing, model routing, and agent orchestration in mind, providing optimal performance for your AI applications.",
diagram: "/PurposeBuiltLLMs.svg"
diagram: "/PurposeBuilt.svg"
},
{
id: 5,
@ -51,19 +51,52 @@ export function VerticalCarouselSection() {
};
return (
<section className="relative bg-[#1a1a1a] text-white py-24 px-6 lg:px-[102px]">
<section className="relative bg-[#1a1a1a] text-white py-12 sm:py-16 lg:py-24 px-4 sm:px-6 lg:px-[102px]">
<div className="max-w-324 mx-auto">
{/* Main Heading */}
<h2 className="font-sans font-normal text-3xl lg:text-4xl tracking-[-2.88px]! text-white leading-[1.03] mb-16 max-w-4xl">
<h2 className="font-sans font-normal text-2xl sm:text-3xl lg:text-4xl tracking-[-2px] sm:tracking-[-2.88px]! text-white leading-[1.03] mb-8 sm:mb-12 lg:mb-12 max-w-4xl">
Basic scenarios to powerful agentic apps in minutes
</h2>
{/* Vertical Carousel Layout */}
<div className="flex flex-col lg:flex-row">
{/* Left Sidebar Navigation */}
<div className="lg:w-72 shrink-0">
{/* Mobile: Horizontal Scroller Navigation */}
<div className="lg:hidden mb-8 -mx-4 sm:mx-0 px-4 sm:px-0">
<div
className="relative overflow-x-auto pb-2"
style={{
scrollbarWidth: "none",
msOverflowStyle: "none"
}}
>
<style jsx>{`
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
`}</style>
<div className="flex gap-4 min-w-max hide-scrollbar">
{verticalCarouselData.map((item, index) => (
<button
key={item.id}
onClick={() => handleSlideClick(index)}
className={`relative px-4 py-2 rounded transition-all duration-300 whitespace-nowrap ${
index === activeSlide
? 'bg-[#6363d2]/90 text-[#f9faff]'
: 'bg-[#6363d2]/10 text-[rgba(182,188,255,0.71)] hover:bg-[#6363d2]/15'
}`}
>
<span className="font-mono font-bold text-sm tracking-[1.44px]!">
{item.category}
</span>
</button>
))}
</div>
</div>
</div>
{/* Desktop: Vertical Carousel Layout */}
<div className="flex flex-col lg:flex-row lg:items-start">
{/* Left Sidebar Navigation - Desktop Only */}
<div className="hidden lg:block lg:w-72 shrink-0 lg:pt-0">
<div className="relative space-y-6">
{/* Sliding Rectangle Indicator */}
<motion.div
className="absolute left-0 top-0 w-2 h-4 bg-[#6363d2] z-10 rounded-xs"
animate={{
@ -96,44 +129,45 @@ export function VerticalCarouselSection() {
</div>
</div>
{/* Right Content Area */}
<div className="flex-1 min-h-[600px] relative lg:-ml-8">
{/* Right Content Area - Fixed height to prevent layout shift */}
<div className="flex-1 h-[600px] sm:h-[650px] lg:h-[600px] relative lg:-ml-8">
<AnimatePresence mode="wait">
<motion.div
key={activeSlide}
initial={{ opacity: 0, x: 50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -50 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
className="absolute inset-0"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.4, ease: "easeInOut" }}
className="w-full h-full"
>
<div className="flex flex-col lg:flex-row gap-12 items-start h-full">
<div className="flex flex-col lg:flex-row gap-6 sm:gap-8 lg:gap-12 items-start h-full">
{/* Diagram - Above on mobile, Right Side on desktop */}
<div className="w-full lg:flex-1 flex items-center justify-center lg:justify-start order-first lg:order-last shrink-0">
<div className="relative w-full max-w-full sm:max-w-md lg:max-w-[600px] aspect-4/3">
<Image
src={verticalCarouselData[activeSlide].diagram}
alt={verticalCarouselData[activeSlide].title}
fill
className="object-contain object-top"
priority
/>
</div>
</div>
{/* Text Content */}
<div className="flex-1 max-w-2xl">
<div className="flex-1 max-w-2xl order-last lg:order-first flex flex-col justify-start">
{/* Title */}
<h3 className="font-sans font-medium text-primary text-2xl lg:text-[34px] tracking-[-2.4px]! leading-[1.03] mb-4">
<h3 className="font-sans font-medium text-primary text-xl sm:text-2xl lg:text-[34px] tracking-[-1px]! leading-[1.03] mb-4 sm:mb-6">
{verticalCarouselData[activeSlide].title}
</h3>
{/* Description */}
<div className="font-mono text-white text-xl lg:text-lg leading-10 tracking-[-1.2px]! max-w-md">
<div className="font-mono text-white text-sm sm:text-base lg:text-lg leading-6 sm:leading-8 lg:leading-10 tracking-[-0.8px] sm:tracking-[-1.2px]! max-w-full lg:max-w-md">
<p className="mb-0">
{verticalCarouselData[activeSlide].description}
</p>
</div>
</div>
{/* Diagram - Right Side */}
<div className="flex-1 w-full flex items-start justify-center lg:justify-start pt-2">
<Image
src={verticalCarouselData[activeSlide].diagram}
alt={verticalCarouselData[activeSlide].title}
width={600}
height={400}
className="w-full max-w-[600px] h-auto object-contain"
priority
/>
</div>
</div>
</motion.div>
</AnimatePresence>