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.
- 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/.
All three lines render in the DOM; CSS drives the fade via data-active.
Reduced-motion users see the first line only (no interval, no fade).
Rotation pauses on hover and keyboard focus. aria-live=off on the
rotator so AT does not announce every swap; aria-hidden flips per-swap
to avoid duplicate heading announcements.
Also set vite.build.assetsInlineLimit=0 in astro.config.mjs so Astro
emits the rotator script as a same-origin external file (dist/assets/)
rather than inlining it — required for CSP script-src 'self' compliance.