mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
Each .ex-card div is now an <a target="_blank" rel="noopener"> wrapping the same content. The hover-play video, prompt line, and .ex-card styles all work unchanged (the class already had color: inherit + focus-visible). Added one CSS line — text-decoration: none on .ex-card — so the <a> doesn't underline the card. Linked destinations: - examples/tally/ — SaaS, modern-minimal (from /Users/youssef/test-hallmark-9) - examples/wayfare/ — travel booking, atmospheric (from test-hallmark-12) - _tests/09-slow-pour/ — small-batch coffee subscription (already in site) - examples/bananastudio/ — creative studio with playful motion (from test-hallmark-6) - _tests/06-anya-portfolio/ — software architect personal site (already in site) - examples/najm/ — Moroccan fashion (from test-hallmark-7) - _tests/11-soroe-ceramics/ — small ceramics studio (already in site) - examples/hyperlane/ — developer infrastructure (from test-hallmark-15) site/examples/ adds ~308 KB total across the 5 copied projects. The other 3 already live under site/_tests/ so no copying needed for them.
85 lines
2.8 KiB
JavaScript
85 lines
2.8 KiB
JavaScript
/* Hallmark · motion primitives: counter · marquee (CSS) · hover-tilt
|
|
* respects prefers-reduced-motion
|
|
*/
|
|
|
|
(function () {
|
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
|
|
/* ── counter · live ticker for hero status + invoice + bench stats ── */
|
|
const liveEvents = document.getElementById("live-events");
|
|
const benchEps = document.getElementById("bench-eps");
|
|
const invCalls = document.getElementById("inv-calls");
|
|
const invTotal = document.getElementById("inv-total");
|
|
const benchMrr = document.getElementById("bench-mrr");
|
|
|
|
let eps = 12847;
|
|
let calls = 1284512;
|
|
let total = 5142.86;
|
|
let mrr = 412.6;
|
|
|
|
const fmt = (n) => n.toLocaleString("en-US");
|
|
|
|
function tick() {
|
|
// small jitter, bounded
|
|
eps += Math.round((Math.random() - 0.4) * 80);
|
|
if (eps < 11000) eps = 11200;
|
|
if (eps > 14500) eps = 14200;
|
|
|
|
calls += Math.round(Math.random() * 24 + 4);
|
|
total += Math.random() * 0.18;
|
|
mrr += (Math.random() - 0.5) * 0.4;
|
|
if (mrr < 410) mrr = 410.3;
|
|
if (mrr > 416) mrr = 415.8;
|
|
|
|
if (liveEvents) liveEvents.textContent = fmt(eps);
|
|
if (benchEps) benchEps.textContent = fmt(eps);
|
|
if (invCalls) invCalls.textContent = fmt(calls);
|
|
if (invTotal) invTotal.textContent = total.toFixed(2);
|
|
if (benchMrr) benchMrr.textContent = mrr.toFixed(1);
|
|
}
|
|
|
|
if (!reduceMotion) {
|
|
tick();
|
|
setInterval(tick, 1100);
|
|
}
|
|
|
|
/* ── pricing toggle ── */
|
|
const pricingButtons = document.querySelectorAll(".pricing__toggle button");
|
|
const pricedSpans = document.querySelectorAll("[data-price-monthly]");
|
|
|
|
pricingButtons.forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const period = btn.dataset.period;
|
|
pricingButtons.forEach((b) => b.setAttribute("aria-pressed", b === btn ? "true" : "false"));
|
|
pricedSpans.forEach((span) => {
|
|
span.textContent = period === "annual" ? span.dataset.priceAnnual : span.dataset.priceMonthly;
|
|
});
|
|
});
|
|
});
|
|
|
|
/* ── hover-tilt on feature cards ── */
|
|
const tiltCards = document.querySelectorAll("[data-tilt]");
|
|
|
|
if (!reduceMotion) {
|
|
tiltCards.forEach((card) => {
|
|
let raf = null;
|
|
|
|
card.addEventListener("mousemove", (e) => {
|
|
const rect = card.getBoundingClientRect();
|
|
const x = (e.clientX - rect.left) / rect.width - 0.5;
|
|
const y = (e.clientY - rect.top) / rect.height - 0.5;
|
|
if (raf) cancelAnimationFrame(raf);
|
|
raf = requestAnimationFrame(() => {
|
|
card.style.transform =
|
|
`perspective(900px) rotateX(${(-y * 4).toFixed(2)}deg) rotateY(${(x * 5).toFixed(2)}deg) translateY(-2px)`;
|
|
});
|
|
});
|
|
|
|
card.addEventListener("mouseleave", () => {
|
|
if (raf) cancelAnimationFrame(raf);
|
|
card.style.transform = "";
|
|
});
|
|
});
|
|
}
|
|
})();
|