Compare commits

...

6 Commits

Author SHA1 Message Date
hsiegeln
b7b58dd948 feat(design): click-to-enlarge on product screenshots
Some checks failed
ci / build-test (push) Failing after 3m41s
Lightbox.astro — reusable native HTMLDialogElement wrapper:
- Trigger: <button> wrapping the <img>, cursor: zoom-in, amber zoom-pill
  badge fades in on hover/focus.
- Dialog: showModal() opens a full-viewport modal (≤1800x1200 cap) with
  blurred amber-tinted backdrop.
- Close paths: native form[method=dialog] submit (Escape + close button),
  click on backdrop, click on the image itself.
- Accessibility: aria-labelledby + visually-hidden heading avoids both
  aria-label-misuse and no-redundant-role validator conflicts. Focus
  returns to trigger on close (native HTMLDialogElement behavior).
- Motion: 220ms fade+scale open, disabled under prefers-reduced-motion.
- CSP: <script> is Astro-bundled to an external file — script-src 'self'
  respected.

Hero and ProductShowcase now use <Lightbox> instead of a raw <img> for
the product screenshots. The existing frame styling (border, glow, ring
overlay) is untouched — the lightbox trigger is a block-level button
that fills the frame.
2026-04-25 00:31:48 +02:00
hsiegeln
4d4c072834 feat(design): atmosphere + WhyUs editorial 3-AM treatment
TopographicBg now actually reads:
- Per-line stroke width varies (triangle wave — contour-interval feel)
- Per-line opacity varies by vertical depth (darker mid-section, lighter
  edges)
- One line in four rendered in cyan (echo of cross-route correlation)
- Radial-mask soft edge fade so lines dissolve into the section boundary
- Default opacity bumped from 0.12 to 0.35; section callers still scale it
  down via the opacity prop, but the new internal variation makes the
  atmosphere visible where before it was invisible

WhyUs second tile: 3-AM storytelling moment now lands typographically:
- Decorative 03:00 glyph (amber/4% alpha) in the top-right corner
- Eyebrow log-entry treatment: pulsing amber dot + mono 03:00:47.218
  timestamp + OPS DESK label — reads like a product UI log row
- The rest of the tile unchanged

ProductShowcase figure: figcaption moved to last child (HTML spec
requires figcaption to be first or last in a figure; a div after it was
a validation error).
2026-04-25 00:26:16 +02:00
hsiegeln
c4395eb245 feat(design): card motion + Pricing MID tier hierarchy
- DualValueProps: 110ms staggered rise-in on load (cubic-bezier ease),
  reduced-motion users see cards pre-populated, no animation.
- All card sections (DualValueProps, HowItWorks, WhyUs, Pricing) gain a
  subtle hover lift: -translate-y-0.5, amber/40 border, soft amber drop
  shadow. 200ms ease-out — tactile but not noisy.
- Pricing MID tier now looks like the highlighted option: ring-2 accent,
  amber-tinted drop shadow, lg:-translate-y-2 (sits above the others),
  and a 'MOST POPULAR' ribbon pill. The 1px border swap was invisible.
2026-04-25 00:23:54 +02:00
hsiegeln
073ff2ad48 feat(design): new ProductShowcase section — 'When something breaks'
Editorial section between DualValueProps and HowItWorks. Breaks the
identical-rectangle cascade with an asymmetric 8/4 grid: large
error-detail screenshot with subtle cyan/amber backlight on the left,
three numbered callout captions on the right.

The screenshot (cross-route correlation chain + circuit breaker +
fallback + Java stack trace) makes the 'deep tracing, replay, live
control' claims concrete in a way the abstract RouteDiagram never did.

Cyan kicker on this section (vs. amber elsewhere) signals 'this one is
different' and echoes the cross-route correlation color in the product.
2026-04-25 00:22:28 +02:00
hsiegeln
ad8312b7f0 chore: gitignore .claude/ session state
Accidentally committed .claude/scheduled_tasks.lock in the previous
commit. Untrack it and add .claude/ to .gitignore so local Claude Code
session state does not leak into the repo.
2026-04-25 00:21:07 +02:00
hsiegeln
8c77db02ac feat(design): Hero asymmetric layout with real product UI + bug fixes
- Hero restructured from stacked to 2-col grid on lg+ (copy left, product
  screenshot right). Replaces the abstract RouteDiagram with the actual
  exchange-detail view — the product doing the thing the copy promises.
- Kicker broken out of the shared uppercase-mono pattern: italic pill with
  a soft amber fill/border, scaled up to 14px. The humor now wears a
  different costume from the other section kickers.
- Hero brand mark scaled to 64px and given a slow 7s sway (reduced-motion
  guarded) — living atmosphere, not ambient animation.
- H1 min-height raised to 2.5em to absorb the 2-line wrap of line 1 at
  mobile sizes without layout shift on rotation.
- Amber radial glow behind the product shot + subtle bevel + frame ring.
- Footer placeholder 3-wavy-lines SVG replaced with real camel logo
  (spec gap from earlier refresh — header got swapped, footer didn't).
- Screenshot assets imported under public/product/.
2026-04-25 00:20:39 +02:00
13 changed files with 534 additions and 68 deletions

3
.gitignore vendored
View File

@@ -22,6 +22,9 @@ Thumbs.db
# Brainstorming / visual companion previews # Brainstorming / visual companion previews
.superpowers/ .superpowers/
# Claude Code session state (local tooling)
.claude/
# Logs # Logs
*.log *.log
npm-debug.log* npm-debug.log*

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

View 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>

View File

@@ -4,14 +4,14 @@ const year = new Date().getFullYear();
<footer class="border-t border-border mt-24"> <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="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"> <div class="flex items-center gap-3">
<svg width="24" height="24" viewBox="0 0 32 32" aria-hidden="true"> <img
<rect width="32" height="32" rx="6" fill="#0c111a"/> src="/cameleer-logo.svg"
<g fill="none" stroke="#f0b429" stroke-width="1.6" stroke-linecap="round"> width="24"
<path d="M4 10 Q10 6 16 12 T28 10"/> height="24"
<path d="M4 16 Q10 12 16 18 T28 16"/> alt=""
<path d="M4 22 Q10 18 16 24 T28 22"/> decoding="async"
</g> class="shrink-0 opacity-80"
</svg> />
<span class="text-text-muted text-sm">© {year} Cameleer</span> <span class="text-text-muted text-sm">© {year} Cameleer</span>
</div> </div>
<nav class="flex items-center gap-8 text-sm text-text-muted" aria-label="Footer"> <nav class="flex items-center gap-8 text-sm text-text-muted" aria-label="Footer">

View File

@@ -3,24 +3,66 @@ interface Props {
opacity?: number; opacity?: number;
lines?: 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); const stepY = 100 / (lines + 1);
for (let i = 1; i <= lines; i++) { for (let i = 1; i <= lines; i++) {
const y = i * stepY; const y = i * stepY;
const amp = 4 + (i % 3) * 2; // Mix two frequencies so adjacent lines don't read parallel.
paths.push(`M0,${y} Q25,${y - amp} 50,${y + amp * 0.6} T100,${y}`); 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 <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" viewBox="0 0 100 100"
preserveAspectRatio="none" preserveAspectRatio="none"
aria-hidden="true"
style={`opacity:${opacity}`}
> >
<g fill="none" stroke="#f0b429" stroke-width="0.15" vector-effect="non-scaling-stroke"> <g fill="none" vector-effect="non-scaling-stroke" stroke-linecap="round">
{paths.map((d) => <path d={d} />)} {out.map((l) => (
<path
d={l.d}
stroke={l.tone === 'cyan' ? '#5cc8ff' : '#f0b429'}
stroke-width={l.width}
stroke-opacity={l.lineOpacity}
/>
))}
</g> </g>
</svg> </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>

View File

@@ -27,8 +27,11 @@ const tiles: Tile[] = [
<section class="border-b border-border"> <section class="border-b border-border">
<div class="max-w-content mx-auto px-6 py-20 md:py-24"> <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"> <div class="grid md:grid-cols-3 gap-6 md:gap-8">
{tiles.map((tile) => ( {tiles.map((tile, i) => (
<div class="rounded-lg border border-border bg-bg-elevated p-7 md:p-8 hover:border-border-strong transition-colors"> <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"> <h2 class="text-xl md:text-2xl font-bold text-text mb-3 leading-snug">
{tile.outcome} {tile.outcome}
</h2> </h2>
@@ -38,3 +41,22 @@ const tiles: Tile[] = [
</div> </div>
</div> </div>
</section> </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>

View File

@@ -1,27 +1,29 @@
--- ---
import CTAButtons from '../CTAButtons.astro'; import CTAButtons from '../CTAButtons.astro';
import RouteDiagram from '../RouteDiagram.astro';
import TopographicBg from '../TopographicBg.astro'; import TopographicBg from '../TopographicBg.astro';
import Lightbox from '../Lightbox.astro';
--- ---
<section class="relative overflow-hidden border-b border-border"> <section class="relative overflow-hidden border-b border-border">
<TopographicBg opacity={0.14} lines={10} /> <TopographicBg opacity={0.22} lines={11} />
<div class="relative max-w-content mx-auto px-6 pt-20 pb-24 md:pt-28 md:pb-32"> <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="max-w-3xl"> <div class="grid lg:grid-cols-12 gap-10 lg:gap-14 items-center">
<div class="flex items-center gap-3 mb-6"> <div class="lg:col-span-5">
<img <img
src="/cameleer-logo.svg" src="/cameleer-logo.svg"
width="48" width="64"
height="48" height="64"
alt="" alt=""
decoding="async" 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. Your camels called. They want a GPS.
</p> </p>
</div>
<h1 <h1
class="text-display font-bold text-text mb-6 hero-rotator" class="font-bold text-text mb-6 hero-rotator"
aria-live="off" aria-live="off"
data-hero-rotator data-hero-rotator
> >
@@ -34,25 +36,39 @@ import TopographicBg from '../TopographicBg.astro';
</p> </p>
<CTAButtons size="lg" /> <CTAButtons size="lg" />
</div> </div>
<div class="mt-16 md:mt-20"> <div class="lg:col-span-7 relative">
<RouteDiagram /> <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>
</div> </div>
</section> </section>
<style> <style>
/* Rotating H1 — fluid size + fade transition */
.hero-rotator { .hero-rotator {
font-size: clamp(2.25rem, 4.5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.02em;
position: relative; position: relative;
display: block; display: block;
/* Reserve height for the tallest line so no layout shift on swap. /* Reserve enough vertical space that a 2-line wrap of the longest line
Two lines at current H1 size handles all three on most viewports. */ does not push the page on swap (mobile wraps line 1 to 2 lines). */
min-height: 2.2em; min-height: 2.5em;
} }
.hero-line { .hero-line {
display: block; display: block;
opacity: 0; opacity: 0;
transition: opacity 700ms ease-in-out; transition: opacity 700ms ease-in-out;
/* Stack all lines on top of each other — only [data-active] is visible. */
position: absolute; position: absolute;
inset: 0; inset: 0;
} }
@@ -60,10 +76,39 @@ import TopographicBg from '../TopographicBg.astro';
opacity: 1; opacity: 1;
position: relative; position: relative;
} }
@media (prefers-reduced-motion: reduce) {
.hero-line { /* Product screenshot frame — subtle dropshadow + amber glow behind */
transition: none; .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> </style>

View File

@@ -33,7 +33,7 @@ const steps: Step[] = [
</div> </div>
<ol class="grid md:grid-cols-3 gap-6 md:gap-8"> <ol class="grid md:grid-cols-3 gap-6 md:gap-8">
{steps.map((step) => ( {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> <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> <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> <p class="text-text-muted leading-relaxed mb-4">{step.body}</p>

View File

@@ -54,11 +54,22 @@ const tiers: Tier[] = [
<a href="/pricing" class="text-accent hover:underline">See full comparison →</a> <a href="/pricing" class="text-accent hover:underline">See full comparison →</a>
</p> </p>
</div> </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) => ( {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="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 class="text-2xl font-bold text-text">{tier.price}</div>
</div> </div>
<p class="text-text-muted text-sm leading-relaxed flex-grow mb-5">{tier.sub}</p> <p class="text-text-muted text-sm leading-relaxed flex-grow mb-5">{tier.sub}</p>

View 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>

View File

@@ -11,7 +11,7 @@
</h2> </h2>
</div> </div>
<div class="grid md:grid-cols-2 gap-8 md:gap-12"> <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> <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"> <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. 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. 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> </p>
</div> </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> <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"> <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. 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>
</div> </div>
</div>
</section> </section>

View File

@@ -4,6 +4,7 @@ import SiteHeader from '../components/SiteHeader.astro';
import SiteFooter from '../components/SiteFooter.astro'; import SiteFooter from '../components/SiteFooter.astro';
import Hero from '../components/sections/Hero.astro'; import Hero from '../components/sections/Hero.astro';
import DualValueProps from '../components/sections/DualValueProps.astro'; import DualValueProps from '../components/sections/DualValueProps.astro';
import ProductShowcase from '../components/sections/ProductShowcase.astro';
import HowItWorks from '../components/sections/HowItWorks.astro'; import HowItWorks from '../components/sections/HowItWorks.astro';
import WhyUs from '../components/sections/WhyUs.astro'; import WhyUs from '../components/sections/WhyUs.astro';
import PricingTeaser from '../components/sections/PricingTeaser.astro'; import PricingTeaser from '../components/sections/PricingTeaser.astro';
@@ -17,6 +18,7 @@ import FinalCTA from '../components/sections/FinalCTA.astro';
<main> <main>
<Hero /> <Hero />
<DualValueProps /> <DualValueProps />
<ProductShowcase />
<HowItWorks /> <HowItWorks />
<WhyUs /> <WhyUs />
<PricingTeaser /> <PricingTeaser />