Svenn skrytetall Kalkulasjon

by SvennMats

HTML

<section class="kpi-section">
  
  <div class="kpi-grid">
    
    <div class="kpi-card">
      <div class="kpi-card-top">
        <div class="kpi-icon-wrapper">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
        </div>
        <h3 class="kpi-title">Gjennomsnittlig tidsbruk</h3>
      </div>
      
      <div class="kpi-value-wrapper">
        <span class="kpi-number counter" data-target="27">0</span>
        <span class="kpi-suffix">min</span>
      </div>
      
      <div class="kpi-trend positive">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg>
        Redusert fra 3 timer
      </div>
      <p class="kpi-description">Fra du setter deg ned til et ferdig og profft tilbud er sendt til kunden.</p>
    </div>

    <div class="kpi-card">
      <div class="kpi-card-top">
        <div class="kpi-icon-wrapper">
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
        </div>
        <h3 class="kpi-title">Anbud vunnet</h3>
      </div>
      
      <div class="kpi-value-wrapper">
        <span class="kpi-number counter" data-target="75">0</span>
        <span class="kpi-suffix">%</span>
      </div>
      
      <div class="kpi-trend positive">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12"...

CSS

/* --- Sikrer at padding ikke ødelegger bredden --- */
*, *::before, *::after {
  box-sizing: border-box;
}

/* --- Brand Variabler --- */
:root {
  --svenn-orange: #F96529;
  --svenn-dark: #241D2D;
  --svenn-res-bg: #EDF3E3;
  --svenn-bg: #F8F9FA;
}

/* --- Grunnleggende oppsett --- */
.kpi-section {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background-color: #ffffff; 
  padding: 60px 20px; 
  color: var(--svenn-dark);
  width: 100%; /* Sikrer at den ikke går utenfor skjermen */
  overflow-x: hidden; /* Ekstra sikkerhet mot horisontal scroll */
}

/* --- Layout for kortene --- */
.kpi-grid {
  display: grid;
  /* min(100%, 280px) er magien her. Den tvinger kortet til å krympe hvis skjermen er bitteliten */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  gap: 24px;
  max-width: 1100px;
  margin: 0 auto;
  width: 100%;
}

/* --- Selve designet på kortet --- */
.kpi-card {
  background: #ffffff; 
  border: 1px solid #E5E7EB;
  border-radius: 16px;
  padding: 32px 24px;
  box-shadow: 0 4px 6px -1px rgba(36, 29, 45, 0.05), 0 2px 4px -1px rgba(36, 29, 45, 0.03);
  display: flex;
  flex-direction: column;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  
  /* For animasjonen fra JavaScript */
  opacity: 0;
  transform: translateY(30px);
}

.kpi-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 10px 15px -3px rgba(36, 29, 45, 0.08), 0 4px 6px -2px rgba(36, 29, 45, 0.04);
}

/* Toppseksjon med ikon og tittel */
.kpi-card-top {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 20px;
}

.kpi-icon-wrapper {
  background: var(--svenn-res-bg); 
  color: var(--svenn-orange);
  width: 44px;
  height: 44px;
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0; /* Sikrer at ikonet ikke klemmes på små skjermer */
}

.kpi-title...

JavaScript

document.addEventListener("DOMContentLoaded", () => {
  // Avansert tellefunksjon for mykere (ease-out) animasjon
  const easeOutQuart = (t) => 1 - Math.pow(1 - t, 4);

  const animateValue = (obj, start, end, duration) => {
    let startTimestamp = null;
    
    const step = (timestamp) => {
      if (!startTimestamp) startTimestamp = timestamp;
      const progress = Math.min((timestamp - startTimestamp) / duration, 1);
      
      // Bruker ease-out for at tellingen skal bremse naturlig opp mot slutten
      const currentCount = Math.floor(easeOutQuart(progress) * (end - start) + start);
      obj.innerHTML = currentCount;
      
      if (progress < 1) {
        window.requestAnimationFrame(step);
      } else {
        obj.innerHTML = end; // Sikrer at den treffer nøyaktig
      }
    };
    window.requestAnimationFrame(step);
  };

  // Trigger animasjonene når kortene er i viewport
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // Fade inn kortet
        entry.target.classList.add('is-visible');
        
        // Finn tallet og start opptellingen (bruker 2 sekunder for god effekt)
        const counterElement = entry.target.querySelector('.counter');
        if (counterElement) {
          const target = parseInt(counterElement.getAttribute('data-target'));
          animateValue(counterElement, 0, target, 2000);
        }
        
        // Slutt å observere (animeres kun 1 gang)
        observer.unobserve(entry.target);
      }
    });
  }, { 
    threshold: 0.1, // Starter når 10% av kortet vises
    rootMargin: "0px 0px -50px 0px" // Trigger litt før man er helt på elementet
  });

  // Knytt observeren til alle kort
  document.querySelectorAll('.kpi-card').forEach(card => {
    observer.observe(card);
  });
});