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
|
# Brainstorming / visual companion previews
|
||||||
.superpowers/
|
.superpowers/
|
||||||
|
|
||||||
|
# Claude Code session state (local tooling)
|
||||||
|
.claude/
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.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">
|
<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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user