Compare commits
6 Commits
af7c61c203
...
b7b58dd948
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7b58dd948 | ||
|
|
4d4c072834 | ||
|
|
c4395eb245 | ||
|
|
073ff2ad48 | ||
|
|
ad8312b7f0 | ||
|
|
8c77db02ac |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,6 +22,9 @@ Thumbs.db
|
||||
# Brainstorming / visual companion previews
|
||||
.superpowers/
|
||||
|
||||
# Claude Code session state (local tooling)
|
||||
.claude/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
BIN
public/product/error-detail.png
Normal file
BIN
public/product/error-detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 644 KiB |
BIN
public/product/exchange-detail.png
Normal file
BIN
public/product/exchange-detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 551 KiB |
233
src/components/Lightbox.astro
Normal file
233
src/components/Lightbox.astro
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
interface Props {
|
||||
src: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
loading?: 'eager' | 'lazy';
|
||||
caption?: string;
|
||||
triggerClass?: string;
|
||||
imgClass?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
loading = 'lazy',
|
||||
caption,
|
||||
triggerClass = '',
|
||||
imgClass = 'block w-full h-auto',
|
||||
} = Astro.props;
|
||||
|
||||
// Unique per-instance id so multiple lightboxes on a page do not collide.
|
||||
const dialogId = `lb-${Math.random().toString(36).slice(2, 10)}`;
|
||||
---
|
||||
<button
|
||||
type="button"
|
||||
class={`lightbox-trigger group ${triggerClass}`.trim()}
|
||||
data-lightbox-open={dialogId}
|
||||
aria-label={`Enlarge: ${alt}`}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={height}
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
class={imgClass}
|
||||
/>
|
||||
<span aria-hidden="true" class="lightbox-zoom-badge">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="7.5"/>
|
||||
<path d="m20 20-3.5-3.5"/>
|
||||
<line x1="11" y1="8" x2="11" y2="14"/>
|
||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<dialog id={dialogId} class="lightbox-dialog" aria-modal="true" aria-labelledby={`${dialogId}-title`}>
|
||||
<h2 id={`${dialogId}-title`} class="sr-only">{alt}</h2>
|
||||
<form method="dialog" class="lightbox-close-form">
|
||||
<button type="submit" class="lightbox-close" aria-label="Close">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"/>
|
||||
<path d="m6 6 12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
<img src={src} alt={alt} class="lightbox-image" />
|
||||
{caption && <p class="lightbox-caption">{caption}</p>}
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
/* Trigger: reset native button chrome, keep block layout matching the old <img>. */
|
||||
.lightbox-trigger {
|
||||
all: unset;
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: zoom-in;
|
||||
width: 100%;
|
||||
}
|
||||
.lightbox-trigger:focus-visible {
|
||||
outline: 2px solid #f0b429;
|
||||
outline-offset: 4px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Zoom pill (fades in on hover / keyboard focus) */
|
||||
.lightbox-zoom-badge {
|
||||
position: absolute;
|
||||
top: 0.85rem;
|
||||
right: 0.85rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(240, 180, 41, 0.92);
|
||||
color: #060a13;
|
||||
opacity: 0;
|
||||
transform: translateY(-3px);
|
||||
transition: opacity 180ms ease-out, transform 180ms ease-out;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px -4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.lightbox-trigger:hover .lightbox-zoom-badge,
|
||||
.lightbox-trigger:focus-visible .lightbox-zoom-badge {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
.lightbox-dialog {
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: min(98vw, 1800px);
|
||||
height: min(96vh, 1200px);
|
||||
background: #060a13;
|
||||
color: #e8eaed;
|
||||
border: 1px solid rgba(240, 180, 41, 0.25);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 40px 100px -30px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
.lightbox-dialog::backdrop {
|
||||
background: rgba(6, 10, 19, 0.82);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.lightbox-close-form {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
margin: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.lightbox-close {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 9999px;
|
||||
color: #f0b429;
|
||||
background: rgba(12, 17, 26, 0.92);
|
||||
border: 1px solid rgba(240, 180, 41, 0.35);
|
||||
transition: background 160ms ease-out, transform 160ms ease-out, border-color 160ms ease-out;
|
||||
}
|
||||
.lightbox-close:hover {
|
||||
background: rgba(240, 180, 41, 0.18);
|
||||
border-color: rgba(240, 180, 41, 0.7);
|
||||
transform: scale(1.06);
|
||||
}
|
||||
.lightbox-close:focus-visible {
|
||||
outline: 2px solid #f0b429;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #060a13;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
.lightbox-caption {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0.5rem;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: #9aa3b2;
|
||||
font-size: 0.875rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Open animation */
|
||||
@keyframes lightbox-in {
|
||||
from { opacity: 0; transform: scale(0.985); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
@keyframes lightbox-backdrop-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.lightbox-dialog[open] {
|
||||
animation: lightbox-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.lightbox-dialog[open]::backdrop {
|
||||
animation: lightbox-backdrop-in 220ms ease-out;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lightbox-zoom-badge {
|
||||
transition: none;
|
||||
}
|
||||
.lightbox-dialog[open],
|
||||
.lightbox-dialog[open]::backdrop {
|
||||
animation: none;
|
||||
}
|
||||
.lightbox-close {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Open lightbox on trigger click. Close on click of the image itself or
|
||||
// the backdrop. Bundled by Astro — CSP script-src 'self'.
|
||||
const triggers = document.querySelectorAll<HTMLButtonElement>('[data-lightbox-open]');
|
||||
triggers.forEach((trigger) => {
|
||||
trigger.addEventListener('click', () => {
|
||||
const id = trigger.getAttribute('data-lightbox-open');
|
||||
if (!id) return;
|
||||
const dialog = document.getElementById(id);
|
||||
if (dialog instanceof HTMLDialogElement) {
|
||||
dialog.showModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog.lightbox-dialog');
|
||||
dialogs.forEach((dialog) => {
|
||||
dialog.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
// Click on the dialog element itself (the backdrop area) or the image
|
||||
// closes. Clicks on the close button are already handled by the native
|
||||
// form[method=dialog].
|
||||
if (target === dialog || target.classList.contains('lightbox-image')) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -4,14 +4,14 @@ const year = new Date().getFullYear();
|
||||
<footer class="border-t border-border mt-24">
|
||||
<div class="max-w-content mx-auto px-6 py-12 flex flex-col md:flex-row md:items-center md:justify-between gap-8">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true">
|
||||
<rect width="32" height="32" rx="6" fill="#0c111a"/>
|
||||
<g fill="none" stroke="#f0b429" stroke-width="1.6" stroke-linecap="round">
|
||||
<path d="M4 10 Q10 6 16 12 T28 10"/>
|
||||
<path d="M4 16 Q10 12 16 18 T28 16"/>
|
||||
<path d="M4 22 Q10 18 16 24 T28 22"/>
|
||||
</g>
|
||||
</svg>
|
||||
<img
|
||||
src="/cameleer-logo.svg"
|
||||
width="24"
|
||||
height="24"
|
||||
alt=""
|
||||
decoding="async"
|
||||
class="shrink-0 opacity-80"
|
||||
/>
|
||||
<span class="text-text-muted text-sm">© {year} Cameleer</span>
|
||||
</div>
|
||||
<nav class="flex items-center gap-8 text-sm text-text-muted" aria-label="Footer">
|
||||
|
||||
@@ -3,24 +3,66 @@ interface Props {
|
||||
opacity?: number;
|
||||
lines?: number;
|
||||
}
|
||||
const { opacity = 0.12, lines = 8 } = Astro.props;
|
||||
const { opacity = 0.35, lines = 9 } = Astro.props;
|
||||
|
||||
const paths: string[] = [];
|
||||
interface Line {
|
||||
d: string;
|
||||
width: number; // stroke width in CSS px (non-scaling)
|
||||
lineOpacity: number; // per-line opacity (0..1) — varies depth
|
||||
tone: 'amber' | 'cyan';
|
||||
}
|
||||
|
||||
const out: Line[] = [];
|
||||
const stepY = 100 / (lines + 1);
|
||||
for (let i = 1; i <= lines; i++) {
|
||||
const y = i * stepY;
|
||||
const amp = 4 + (i % 3) * 2;
|
||||
paths.push(`M0,${y} Q25,${y - amp} 50,${y + amp * 0.6} T100,${y}`);
|
||||
// Mix two frequencies so adjacent lines don't read parallel.
|
||||
const amp = 3 + (i % 3) * 2 + Math.sin(i * 1.7) * 1.2;
|
||||
const phase = (i * 13) % 25; // shift crests horizontally
|
||||
const d = `M0,${y} Q${25 + phase / 3},${y - amp} ${50 + phase / 5},${y + amp * 0.6} T100,${y + (i % 2 ? 1 : -1)}`;
|
||||
// Vary stroke weight with a triangle wave — gives the feel of cartographic contour intervals.
|
||||
const triangle = Math.abs(((i + 2) % 4) - 2) / 2;
|
||||
const width = 0.6 + triangle * 0.9;
|
||||
// Depth: middle lines darker, edges lighter.
|
||||
const depth = 1 - Math.abs((i - (lines + 1) / 2)) / ((lines + 1) / 2);
|
||||
const lineOpacity = 0.35 + depth * 0.65;
|
||||
// One cyan line roughly every 4th — echo of the cross-route correlation color.
|
||||
const tone: 'amber' | 'cyan' = i % 4 === 2 ? 'cyan' : 'amber';
|
||||
out.push({ d, width, lineOpacity, tone });
|
||||
}
|
||||
---
|
||||
<div
|
||||
class="topo-wrap absolute inset-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={`--topo-opacity:${opacity}`}
|
||||
>
|
||||
<svg
|
||||
class="absolute inset-0 w-full h-full pointer-events-none"
|
||||
class="topo-svg absolute inset-0 w-full h-full"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden="true"
|
||||
style={`opacity:${opacity}`}
|
||||
>
|
||||
<g fill="none" stroke="#f0b429" stroke-width="0.15" vector-effect="non-scaling-stroke">
|
||||
{paths.map((d) => <path d={d} />)}
|
||||
<g fill="none" vector-effect="non-scaling-stroke" stroke-linecap="round">
|
||||
{out.map((l) => (
|
||||
<path
|
||||
d={l.d}
|
||||
stroke={l.tone === 'cyan' ? '#5cc8ff' : '#f0b429'}
|
||||
stroke-width={l.width}
|
||||
stroke-opacity={l.lineOpacity}
|
||||
/>
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topo-wrap {
|
||||
opacity: var(--topo-opacity, 0.35);
|
||||
/* Soft edge fade — lines should feel like they dissolve at the section
|
||||
boundaries rather than hit them hard. */
|
||||
-webkit-mask-image: radial-gradient(ellipse at 50% 45%, black 55%, transparent 95%);
|
||||
mask-image: radial-gradient(ellipse at 50% 45%, black 55%, transparent 95%);
|
||||
}
|
||||
.topo-svg {
|
||||
filter: blur(0.15px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,8 +27,11 @@ const tiles: Tile[] = [
|
||||
<section class="border-b border-border">
|
||||
<div class="max-w-content mx-auto px-6 py-20 md:py-24">
|
||||
<div class="grid md:grid-cols-3 gap-6 md:gap-8">
|
||||
{tiles.map((tile) => (
|
||||
<div class="rounded-lg border border-border bg-bg-elevated p-7 md:p-8 hover:border-border-strong transition-colors">
|
||||
{tiles.map((tile, i) => (
|
||||
<div
|
||||
class="tile rounded-lg border border-border bg-bg-elevated p-7 md:p-8 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]"
|
||||
style={`--tile-delay:${i * 110}ms`}
|
||||
>
|
||||
<h2 class="text-xl md:text-2xl font-bold text-text mb-3 leading-snug">
|
||||
{tile.outcome}
|
||||
</h2>
|
||||
@@ -38,3 +41,22 @@ const tiles: Tile[] = [
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes tile-rise {
|
||||
from { opacity: 0; transform: translateY(18px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.tile {
|
||||
opacity: 0;
|
||||
animation: tile-rise 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
animation-delay: var(--tile-delay, 0ms);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.tile {
|
||||
opacity: 1;
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
---
|
||||
import CTAButtons from '../CTAButtons.astro';
|
||||
import RouteDiagram from '../RouteDiagram.astro';
|
||||
import TopographicBg from '../TopographicBg.astro';
|
||||
import Lightbox from '../Lightbox.astro';
|
||||
---
|
||||
<section class="relative overflow-hidden border-b border-border">
|
||||
<TopographicBg opacity={0.14} lines={10} />
|
||||
<div class="relative max-w-content mx-auto px-6 pt-20 pb-24 md:pt-28 md:pb-32">
|
||||
<div class="max-w-3xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<TopographicBg opacity={0.22} lines={11} />
|
||||
<div class="relative max-w-content mx-auto px-6 pt-16 pb-20 md:pt-24 md:pb-24 lg:pt-28">
|
||||
<div class="grid lg:grid-cols-12 gap-10 lg:gap-14 items-center">
|
||||
<div class="lg:col-span-5">
|
||||
<img
|
||||
src="/cameleer-logo.svg"
|
||||
width="48"
|
||||
height="48"
|
||||
width="64"
|
||||
height="64"
|
||||
alt=""
|
||||
decoding="async"
|
||||
class="shrink-0"
|
||||
class="shrink-0 mb-5 hero-mark"
|
||||
/>
|
||||
<p class="text-accent font-mono text-xs tracking-[0.25em] uppercase">
|
||||
<p
|
||||
class="inline-flex items-center gap-2 mb-7 rounded-full border border-accent/30 bg-accent/[0.08] text-accent px-3.5 py-1 text-sm italic font-medium"
|
||||
>
|
||||
<span aria-hidden="true" class="text-base">✦</span>
|
||||
Your camels called. They want a GPS.
|
||||
</p>
|
||||
</div>
|
||||
<h1
|
||||
class="text-display font-bold text-text mb-6 hero-rotator"
|
||||
class="font-bold text-text mb-6 hero-rotator"
|
||||
aria-live="off"
|
||||
data-hero-rotator
|
||||
>
|
||||
@@ -34,25 +36,39 @@ import TopographicBg from '../TopographicBg.astro';
|
||||
</p>
|
||||
<CTAButtons size="lg" />
|
||||
</div>
|
||||
<div class="mt-16 md:mt-20">
|
||||
<RouteDiagram />
|
||||
<div class="lg:col-span-7 relative">
|
||||
<div class="hero-shot relative rounded-lg border border-border-strong bg-bg-elevated overflow-hidden">
|
||||
<Lightbox
|
||||
src="/product/exchange-detail.png"
|
||||
alt="Cameleer Mission Control — route execution detail with processor-level trace"
|
||||
width={1920}
|
||||
height={945}
|
||||
loading="eager"
|
||||
/>
|
||||
<div class="absolute inset-0 ring-1 ring-inset ring-accent/10 pointer-events-none rounded-lg"></div>
|
||||
</div>
|
||||
<div aria-hidden="true" class="hero-shot-glow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Rotating H1 — fluid size + fade transition */
|
||||
.hero-rotator {
|
||||
font-size: clamp(2.25rem, 4.5vw, 4rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
position: relative;
|
||||
display: block;
|
||||
/* Reserve height for the tallest line so no layout shift on swap.
|
||||
Two lines at current H1 size handles all three on most viewports. */
|
||||
min-height: 2.2em;
|
||||
/* Reserve enough vertical space that a 2-line wrap of the longest line
|
||||
does not push the page on swap (mobile wraps line 1 to 2 lines). */
|
||||
min-height: 2.5em;
|
||||
}
|
||||
.hero-line {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transition: opacity 700ms ease-in-out;
|
||||
/* Stack all lines on top of each other — only [data-active] is visible. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
@@ -60,10 +76,39 @@ import TopographicBg from '../TopographicBg.astro';
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-line {
|
||||
transition: none;
|
||||
|
||||
/* Product screenshot frame — subtle dropshadow + amber glow behind */
|
||||
.hero-shot {
|
||||
box-shadow:
|
||||
0 1px 0 rgba(240, 180, 41, 0.08) inset,
|
||||
0 30px 60px -20px rgba(0, 0, 0, 0.6),
|
||||
0 10px 25px -10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.hero-shot-glow {
|
||||
position: absolute;
|
||||
inset: 10% -5% 10% -5%;
|
||||
background: radial-gradient(
|
||||
60% 60% at 50% 50%,
|
||||
rgba(240, 180, 41, 0.18),
|
||||
transparent 70%
|
||||
);
|
||||
filter: blur(40px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Slow sway on the mark — tasteful, not distracting */
|
||||
@keyframes hero-mark-sway {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-2px) rotate(-1.5deg); }
|
||||
}
|
||||
.hero-mark {
|
||||
animation: hero-mark-sway 7s ease-in-out infinite;
|
||||
transform-origin: 50% 90%;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-line { transition: none; }
|
||||
.hero-mark { animation: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const steps: Step[] = [
|
||||
</div>
|
||||
<ol class="grid md:grid-cols-3 gap-6 md:gap-8">
|
||||
{steps.map((step) => (
|
||||
<li class="relative rounded-lg border border-border bg-bg-elevated p-7">
|
||||
<li class="relative rounded-lg border border-border bg-bg-elevated p-7 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]">
|
||||
<div class="font-mono text-accent text-sm tracking-wider mb-3">{step.n}</div>
|
||||
<h3 class="text-lg font-bold text-text mb-3">{step.title}</h3>
|
||||
<p class="text-text-muted leading-relaxed mb-4">{step.body}</p>
|
||||
|
||||
@@ -54,11 +54,22 @@ const tiers: Tier[] = [
|
||||
<a href="/pricing" class="text-accent hover:underline">See full comparison →</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-5 lg:items-stretch">
|
||||
{tiers.map((tier) => (
|
||||
<div class={`rounded-lg border bg-bg-elevated p-6 flex flex-col ${tier.highlight ? 'border-accent' : 'border-border'}`}>
|
||||
<div
|
||||
class={`relative rounded-lg bg-bg-elevated p-6 flex flex-col transition-all duration-200 ease-out hover:-translate-y-0.5
|
||||
${tier.highlight
|
||||
? 'ring-2 ring-accent shadow-[0_20px_50px_-20px_rgba(240,180,41,0.35)] lg:-translate-y-2 lg:pt-8 lg:pb-7'
|
||||
: 'border border-border hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]'}`}
|
||||
>
|
||||
{tier.highlight && (
|
||||
<span class="absolute -top-3 left-6 inline-flex items-center gap-1.5 rounded-full bg-accent text-bg px-3 py-0.5 text-[11px] font-bold tracking-wide font-mono">
|
||||
<span aria-hidden="true">★</span>
|
||||
MOST POPULAR
|
||||
</span>
|
||||
)}
|
||||
<div class="mb-4">
|
||||
<div class="font-mono text-xs tracking-wider text-text-muted mb-2">{tier.name.toUpperCase()}</div>
|
||||
<div class={`font-mono text-xs tracking-wider mb-2 ${tier.highlight ? 'text-accent' : 'text-text-muted'}`}>{tier.name.toUpperCase()}</div>
|
||||
<div class="text-2xl font-bold text-text">{tier.price}</div>
|
||||
</div>
|
||||
<p class="text-text-muted text-sm leading-relaxed flex-grow mb-5">{tier.sub}</p>
|
||||
|
||||
91
src/components/sections/ProductShowcase.astro
Normal file
91
src/components/sections/ProductShowcase.astro
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
import TopographicBg from '../TopographicBg.astro';
|
||||
import Lightbox from '../Lightbox.astro';
|
||||
|
||||
interface Callout {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const callouts: Callout[] = [
|
||||
{
|
||||
title: 'Cross-service correlation.',
|
||||
body: 'Every exchange carries its correlation ID forward. One click jumps to what the downstream route did with the same message — 610 ms later.',
|
||||
},
|
||||
{
|
||||
title: 'Runtime detail, not guesswork.',
|
||||
body: 'Circuit breaker tripped. Fallback path ran. The request tried to reach backend:80. The kind of pieces a 3 AM page actually needs — already captured.',
|
||||
},
|
||||
{
|
||||
title: 'The whole story of a failure.',
|
||||
body: 'Exception class, message, stack trace, headers, payload — all pinned to the exchange. No log-grepping tour. No SSH into the pod.',
|
||||
},
|
||||
];
|
||||
---
|
||||
<section class="relative overflow-hidden border-b border-border bg-bg">
|
||||
<TopographicBg opacity={0.14} lines={7} />
|
||||
<div class="relative max-w-content mx-auto px-6 py-24 md:py-32">
|
||||
<div class="max-w-3xl mb-14 md:mb-20">
|
||||
<p class="text-cyan font-mono text-xs tracking-[0.25em] uppercase mb-4">When it breaks</p>
|
||||
<h2 class="text-hero font-bold text-text mb-5">
|
||||
When something breaks, the answer is already waiting.
|
||||
</h2>
|
||||
<p class="text-lg text-text-muted leading-relaxed">
|
||||
Follow a single exchange from ingestion to failure. See the route it took, the fallback that ran, the stack trace, the correlated downstream work — in one place. Without writing a line of tracing code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-12 gap-10 lg:gap-14 items-start">
|
||||
<figure class="lg:col-span-8 relative">
|
||||
<div class="showcase-shot relative rounded-lg border border-border-strong bg-bg-elevated overflow-hidden">
|
||||
<Lightbox
|
||||
src="/product/error-detail.png"
|
||||
alt="Cameleer Mission Control — complex fulfillment route with circuit breaker, fallback, correlated audit route, and full error context"
|
||||
width={1920}
|
||||
height={945}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="absolute inset-0 ring-1 ring-inset ring-accent/10 pointer-events-none rounded-lg"></div>
|
||||
</div>
|
||||
<div aria-hidden="true" class="showcase-shot-glow"></div>
|
||||
<figcaption class="sr-only">Screenshot of a failed exchange in Cameleer, showing the full execution graph, fallback path, and exception context.</figcaption>
|
||||
</figure>
|
||||
|
||||
<ul class="lg:col-span-4 space-y-7 lg:pt-4">
|
||||
{callouts.map((c, i) => (
|
||||
<li class="relative pl-10">
|
||||
<span
|
||||
class="absolute left-0 top-0 inline-flex items-center justify-center w-7 h-7 rounded-full border border-accent/40 bg-accent/10 text-accent font-mono text-xs"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<h3 class="text-text font-semibold mb-1.5">{c.title}</h3>
|
||||
<p class="text-text-muted leading-relaxed">{c.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.showcase-shot {
|
||||
box-shadow:
|
||||
0 1px 0 rgba(240, 180, 41, 0.08) inset,
|
||||
0 40px 80px -30px rgba(0, 0, 0, 0.7),
|
||||
0 15px 35px -15px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.showcase-shot-glow {
|
||||
position: absolute;
|
||||
inset: -5% -8% -5% -8%;
|
||||
background: radial-gradient(
|
||||
55% 55% at 45% 50%,
|
||||
rgba(92, 200, 255, 0.10),
|
||||
rgba(240, 180, 41, 0.08) 40%,
|
||||
transparent 75%
|
||||
);
|
||||
filter: blur(50px);
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-8 md:gap-12">
|
||||
<div class="rounded-lg border border-border bg-bg-elevated p-8">
|
||||
<div class="rounded-lg border border-border bg-bg-elevated p-8 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]">
|
||||
<h3 class="text-xl font-bold text-text mb-4">Generic APMs do not understand Camel. Cameleer does.</h3>
|
||||
<p class="text-text-muted leading-relaxed mb-4">
|
||||
Most monitoring tools see your app as a Java process and a pile of HTTP calls. Cameleer understands that you are running a Camel app — choices, splits, multicasts, error handlers, and every other EIP pattern as first-class citizens.
|
||||
@@ -20,7 +20,23 @@
|
||||
So when you ask "why did this exchange fail?", you get an answer, not a log tail. And you can reach back into a running app to replay a message, deep-trace a correlation ID, or toggle recording — observability that does things, not just shows them.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-bg-elevated p-8">
|
||||
<div class="relative overflow-hidden rounded-lg border border-border bg-bg-elevated p-8 transition-all duration-200 ease-out hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_12px_32px_-12px_rgba(240,180,41,0.18)]">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none select-none absolute -top-6 -right-4 font-mono font-bold leading-none tracking-tight text-accent/[0.04] text-[7rem] md:text-[9rem]"
|
||||
>
|
||||
03:00
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-2.5 mb-5 font-mono text-xs">
|
||||
<span aria-hidden="true" class="relative inline-flex w-1.5 h-1.5">
|
||||
<span class="absolute inset-0 rounded-full bg-accent"></span>
|
||||
<span class="absolute inset-0 rounded-full bg-accent/60 animate-ping [animation-duration:2.4s]"></span>
|
||||
</span>
|
||||
<span class="text-accent tabular-nums tracking-wide">03:00:47.218</span>
|
||||
<span class="text-text-faint">·</span>
|
||||
<span class="text-text-faint uppercase tracking-[0.2em]">ops desk</span>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-text mb-4">Built by people who know what 3 AM looks like.</h3>
|
||||
<p class="text-text-muted leading-relaxed mb-4">
|
||||
We spent years building integration monitoring for banks, insurers, and logistics operators — the kind of shops where a stuck exchange at 3 AM means someone's phone is ringing. We know what integration teams actually need then, and what they never use.
|
||||
@@ -31,4 +47,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,6 +4,7 @@ import SiteHeader from '../components/SiteHeader.astro';
|
||||
import SiteFooter from '../components/SiteFooter.astro';
|
||||
import Hero from '../components/sections/Hero.astro';
|
||||
import DualValueProps from '../components/sections/DualValueProps.astro';
|
||||
import ProductShowcase from '../components/sections/ProductShowcase.astro';
|
||||
import HowItWorks from '../components/sections/HowItWorks.astro';
|
||||
import WhyUs from '../components/sections/WhyUs.astro';
|
||||
import PricingTeaser from '../components/sections/PricingTeaser.astro';
|
||||
@@ -17,6 +18,7 @@ import FinalCTA from '../components/sections/FinalCTA.astro';
|
||||
<main>
|
||||
<Hero />
|
||||
<DualValueProps />
|
||||
<ProductShowcase />
|
||||
<HowItWorks />
|
||||
<WhyUs />
|
||||
<PricingTeaser />
|
||||
|
||||
Reference in New Issue
Block a user