46 lines
1.8 KiB
HTML
46 lines
1.8 KiB
HTML
{{/*
|
|
Stat tiles. Values that start with a number ("80%+", "100+", "30X") count
|
|
up from zero when the row scrolls into view; others show as written.
|
|
Without JS or with reduced motion, every value simply shows.
|
|
*/}}
|
|
{{ $s := . }}
|
|
{{ partial "sections/head.html" $s }}
|
|
<div class="stat-row">
|
|
{{ range $s.items }}
|
|
<div class="stat">
|
|
<span class="stat-value">{{ .value }}</span>
|
|
<span class="stat-label">{{ .label }}</span>
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var row = document.currentScript.previousElementSibling;
|
|
if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
var counters = [];
|
|
row.querySelectorAll('.stat-value').forEach(function (el) {
|
|
var m = el.textContent.trim().match(/^(\d+)(.*)$/);
|
|
if (!m) return;
|
|
var shown = document.createElement('span'), full = el.textContent.trim();
|
|
shown.setAttribute('aria-hidden', 'true');
|
|
shown.textContent = '0' + m[2];
|
|
el.innerHTML = '<span class="visually-hidden"></span>';
|
|
el.firstChild.textContent = full;
|
|
el.appendChild(shown);
|
|
counters.push({ node: shown, to: +m[1], suffix: m[2] });
|
|
});
|
|
if (!counters.length) return;
|
|
new IntersectionObserver(function (entries, io) {
|
|
if (!entries[0].isIntersecting) return;
|
|
io.disconnect();
|
|
var start = performance.now(), dur = 1200;
|
|
(function tick(now) {
|
|
var t = Math.min(1, (now - start) / dur), eased = 1 - Math.pow(1 - t, 3);
|
|
counters.forEach(function (c) { c.node.textContent = Math.round(c.to * eased) + c.suffix; });
|
|
if (t < 1) requestAnimationFrame(tick);
|
|
})(start);
|
|
}, { threshold: 0.6 }).observe(row);
|
|
})();
|
|
</script>
|
|
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
|