Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};

export default config;
10 changes: 1 addition & 9 deletions src/components/sections/FeatureGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,9 @@ export function FeatureGrid() {
<section id="about" className="scroll-mt-28 px-6 py-24 md:py-32">
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-3xl text-center">
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-gold">
About OpsWarden
</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-text md:text-5xl">
The command room for incidents, releases, and shared operational context.
Coordinate faster under pressure.
</h2>
<p className="mt-6 text-lg leading-8 text-muted">
OpsWarden is built for teams that need one operational surface, not
five disconnected ones. The goal is simple: less context loss, less
handoff friction, and faster resolution under pressure.
</p>
</div>

<div className="mt-14 grid gap-6 lg:grid-cols-3">
Expand Down
58 changes: 30 additions & 28 deletions src/components/sections/HeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,42 @@ export function HeroSection() {
className="pointer-events-none absolute left-1/2 top-10 -z-10 h-[480px] w-[820px] -translate-x-1/2 rounded-full bg-[radial-gradient(ellipse_at_center,rgba(251,192,45,0.10),transparent_60%)] blur-[70px]"
/>

{/* Copy — on top */}
<div className="relative z-10 mx-auto flex max-w-3xl flex-col items-center gap-6 text-center">
<h1 className="animate-appear text-balance text-4xl font-semibold leading-[1.1] tracking-tight md:text-6xl">
Ship fearlessly, <span className="text-gold">resolve instantly.</span>
</h1>
<div className="relative z-10 mx-auto max-w-6xl">
<div className="grid items-center gap-16 lg:grid-cols-[minmax(0,35rem)_minmax(22rem,25rem)] lg:justify-between">
<div className="flex max-w-[35rem] flex-col items-center gap-6 text-center lg:items-start lg:text-left">
<h1 className="animate-appear text-4xl font-semibold leading-[1.02] tracking-tight md:text-6xl">
<span className="block whitespace-nowrap">
Ship <span className="text-gold">fearlessly</span>,
</span>
<span className="mt-1 block whitespace-nowrap">
resolve <span className="text-gold">instantly.</span>
</span>
</h1>

<p className="animate-appear max-w-3xl text-base leading-relaxed text-muted md:text-lg [animation-delay:100ms]">
OpsWarden coordinates your incidents and releases in real time — with an
AI SRE{' '}
<br className="hidden md:inline" />
that proposes the root cause and the runbook, straight in the incident
timeline.
</p>
<p className="animate-appear max-w-[34rem] text-base leading-8 text-muted md:text-[1.2rem] [animation-delay:100ms]">
OpsWarden coordinates your incidents and releases in real time — with an AI
SRE that proposes the root cause and the runbook, straight in the incident
timeline.
</p>

<div className="animate-appear pt-2 [animation-delay:200ms]">
<Link
href="http://localhost:4242/en/signup"
className="flex w-fit items-center gap-2 rounded-md bg-gold px-6 py-3 text-sm font-bold text-bg transition-all hover:scale-105 hover:bg-gold-hover"
>
Start now
</Link>
<div className="animate-appear pt-2 [animation-delay:200ms]">
<Link
href="http://localhost:4242/en/signup"
className="flex w-fit items-center gap-2 rounded-md bg-gold px-6 py-3 text-sm font-bold text-bg transition-all hover:scale-105 hover:bg-gold-hover"
>
Start now
</Link>
</div>
</div>

<div className="flex justify-center lg:justify-end">
<DisplayCards />
</div>
</div>
</div>

{/* Faux app window — below */}
<div className="relative z-10 mx-auto mt-16 w-full max-w-5xl md:mt-20">
<div className="pointer-events-none absolute right-2 top-[-5rem] hidden md:block">
<DisplayCards />
</div>
<div className="relative z-10 mx-auto mt-20 w-full max-w-6xl md:mt-24">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-10 top-8 -z-10 h-64 rounded-full bg-[radial-gradient(ellipse_at_center,rgba(251,192,45,0.08),transparent_70%)] blur-[70px]"
Expand Down Expand Up @@ -94,11 +101,6 @@ export function HeroSection() {
</div>
</div>
</div>
<div className="mt-8 flex flex-wrap justify-center gap-3 text-xs uppercase tracking-[0.24em] text-muted md:justify-start">
<span className="rounded-full bg-white/[0.05] px-4 py-2">Incident timeline</span>
<span className="rounded-full bg-white/[0.05] px-4 py-2">Release-aware</span>
<span className="rounded-full bg-white/[0.05] px-4 py-2">AI runbooks</span>
</div>
</div>
</section>
);
Expand Down
70 changes: 42 additions & 28 deletions src/components/ui/display-cards.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client";

import React from "react";
import { cn } from "@/lib/utils";
import { Bot, GitBranch, ShieldAlert } from "lucide-react";
Expand All @@ -14,74 +12,90 @@ interface DisplayCardProps {
}

function DisplayCard({
className,
icon = <Bot className="size-4 text-gold" />,
title = "Featured",
description = "Discover amazing content",
date = "Just now",
titleClassName = "text-gold",
}: DisplayCardProps) {
return (
<div
className={cn(
"glass relative flex h-36 w-[20rem] -skew-y-[8deg] select-none flex-col justify-between rounded-2xl px-4 py-3 shadow-[0_22px_60px_rgba(0,0,0,0.22)] transition-all duration-700 after:absolute after:-right-1 after:top-[-5%] after:h-[110%] after:w-[12rem] after:bg-gradient-to-l after:from-bg after:to-transparent after:content-[''] [&>*]:flex [&>*]:items-center [&>*]:gap-2",
className
)}
>
<div>
<span className="relative inline-block rounded-full bg-gold/12 p-1.5">
<div className="relative flex h-[5.5rem] flex-col gap-1.5 overflow-hidden rounded-2xl border border-white/12 bg-white/[0.07] px-5 py-4 shadow-[inset_0_1px_1px_rgba(255,255,255,0.12),0_16px_32px_-12px_rgba(0,0,0,0.55)] backdrop-blur-[14px]">
<div className="flex items-center gap-2.5">
<span className="grid size-7 shrink-0 place-items-center rounded-full bg-gold/12">
{icon}
</span>
<p className={cn("text-lg font-medium", titleClassName)}>{title}</p>
<p className={cn("text-[0.95rem] font-medium", titleClassName)}>{title}</p>
<span className="ml-auto shrink-0 text-xs text-muted">{date}</span>
</div>
<p className="text-base text-text/90">{description}</p>
<p className="text-sm text-muted">{date}</p>
<p className="truncate pl-[2.625rem] text-sm leading-snug text-text/75">
{description}
</p>
</div>
);
}

// Applied to the product: incidents, releases, AI SRE.
// Order matters — this reads top→bottom as the AI-SRE story.
const PRODUCT_CARDS: DisplayCardProps[] = [
{
icon: <ShieldAlert className="size-4 text-gold" />,
title: "Incident opened",
description: "API latency spike detected on eu-west.",
description: "API latency spike on eu-west.",
date: "2 min ago",
titleClassName: "text-[#ff8c69]",
className:
"[grid-area:stack] -translate-x-8 -translate-y-6 rotate-[-6deg] hover:-translate-y-10",
},
{
icon: <Bot className="size-4 text-gold" />,
title: "AI hypothesis",
description: "Likely root cause: database connection saturation.",
description: "Root cause: DB connection saturation.",
date: "Now",
className:
"[grid-area:stack] translate-x-3 translate-y-8 rotate-[4deg] hover:-translate-y-1",
},
{
icon: <GitBranch className="size-4 text-gold" />,
title: "Runbook attached",
description: "Rollback checkout-worker and drain stuck jobs.",
description: "Rollback checkout-worker, drain jobs.",
date: "Suggested",
titleClassName: "text-[#8ad5a3]",
className:
"[grid-area:stack] translate-x-16 translate-y-20 rotate-[10deg] hover:translate-y-12",
},
];

interface DisplayCardsProps {
cards?: DisplayCardProps[];
}

// Stacked-deck cascade. Each card is OPAQUE so the one in front cleanly
// occludes the one behind (no text bleed). z grows with index so the deck
// reads top→bottom while every card's header + description stays visible;
// the front card only ever covers the bottom padding of the card behind it.
// Rear cards are nudged + scaled for depth, so it reads as a deck, not a list.
const LAYERS = [
{ top: "0rem", x: "1.25rem", scale: 0.955, z: 10, opacity: 0.9 },
{ top: "4.5rem", x: "0.625rem", scale: 0.978, z: 20, opacity: 0.96 },
{ top: "9rem", x: "0rem", scale: 1, z: 30, opacity: 1 },
];

export default function DisplayCards({ cards }: DisplayCardsProps) {
const displayCards = cards || PRODUCT_CARDS;
const baseCards = cards || PRODUCT_CARDS;

return (
<div className="animate-appear grid place-items-center [grid-template-areas:'stack'] [animation-delay:200ms]">
{displayCards.map((cardProps, index) => (
<DisplayCard key={index} {...cardProps} />
))}
<div className="animate-appear group relative h-[15.5rem] w-full max-w-[24rem] [animation-delay:200ms]">
{baseCards.map((card, index) => {
const layer = LAYERS[index] ?? LAYERS[LAYERS.length - 1];
return (
<div
key={index}
className="absolute inset-x-0 origin-top transition-all duration-500 ease-out group-hover:[--lift:-0.6rem]"
style={{
top: layer.top,
zIndex: layer.z,
opacity: layer.opacity,
transform: `translate3d(${layer.x}, calc(var(--lift, 0rem) * ${LAYERS.length - 1 - index}), 0) scale(${layer.scale})`,
}}
>
<DisplayCard {...card} />
</div>
);
})}
</div>
);
}