Saltmere UI Template - Let's templates
Saltmere preview 1
Free
0 2

Saltmere

By Priya Shah

Saltmere is a warm, art-deco ecommerce template for grooming and skincare brands, with an emblem hero, a monthly box builder, a bestseller rail, ingredient hover list, membership card pricing, a catalog, product page and working cart.

Live Demo
saltmere-tailwind.html
<%# letstemplates: access=premium category=ecommerce %>
<!DOCTYPE html>
<html lang="en">
<head>
  <script>
    window.FontAwesomeConfig = { autoReplaceSvg: 'nest' };
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Saltmere | Tailwind CSS Ecommerce Template</title>
  <meta name="description" content="Saltmere is a warm, art-deco Tailwind CSS ecommerce template for grooming and skincare brands, with an emblem hero, a monthly box builder, a bestseller rail, ingredient hover list, membership card pricing, a catalog, product page and working cart.">
  <meta name="keywords" content="Saltmere, Tailwind CSS template, HTML website template, ecommerce template, skincare shop, grooming products, subscription box, beauty store website, product page, shopping cart">
  <meta name="author" content="Let's templates">
  <link rel="canonical" href="">

  <meta property="og:type" content="product">
  <meta property="og:site_name" content="Let's templates">
  <meta property="og:locale" content="en_US">
  <meta property="og:url" content="">
  <meta property="og:title" content="Saltmere | Tailwind CSS Ecommerce Template">
  <meta property="og:description" content="A warm, art-deco Tailwind CSS ecommerce template for grooming and skincare brands, with a monthly box builder, membership pricing and a working cart.">
  <meta property="og:image" content="">
  <meta property="og:image:alt" content="Saltmere template preview">

  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:url" content="">
  <meta name="twitter:title" content="Saltmere | Tailwind CSS Ecommerce Template">
  <meta name="twitter:description" content="A warm, art-deco Tailwind CSS ecommerce template for grooming and skincare brands, with a monthly box builder, membership pricing and a working cart.">
  <meta name="twitter:image" content="">
  <meta name="twitter:image:alt" content="Saltmere template preview">
  <script src="https://cdn.tailwindcss.com/3.4.17"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link href="https://fonts.googleapis.com/css2?family=Federo&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: { sand: '#ecd5b5', peach: '#f3cdb8', cream: '#f5e6be', ink: '#1d1b19', rust: { DEFAULT: '#a8443a', dark: '#87322a' }, sage: '#617d58' },
          fontFamily: { sans: ['"Space Grotesk"', 'sans-serif'], display: ['Federo', 'serif'] }
        }
      }
    }
  </script>
  <style>
    @view-transition { navigation: auto; }
    ::view-transition-old(root) { animation: 180ms ease-out both page-fade-out; }
    ::view-transition-new(root) { animation: 220ms ease-in both page-fade-in; }
    @keyframes page-fade-out { to { opacity: 0; } }
    @keyframes page-fade-in { from { opacity: 0; } }
    html { scroll-behavior: smooth; }
    [hidden] { display: none !important; }
    body { background: radial-gradient(circle at 0% 0%, #f3c4b4 0, transparent 40%), radial-gradient(circle at 100% 100%, #f5e6be 0, transparent 45%), #ecd5b5; background-attachment: fixed; }
    .grain { position: fixed; inset: 0; pointer-events: none; z-index: 60; opacity: .18; mix-blend-mode: multiply; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.6'/%3E%3C/svg%3E"); }
    .scribble path { stroke-dasharray: 420; stroke-dashoffset: 0; transition: stroke-dashoffset .7s ease; }
    .group:hover .scribble path, .group:focus-within .scribble path { stroke-dashoffset: 840; }
    .rail { scrollbar-width: none; }
    .rail::-webkit-scrollbar { display: none; }
    .ring-fill { transition: stroke-dashoffset .7s ease; }
    .hover-image { transition: opacity .3s ease, transform .3s ease; }
    .reveal-ready [data-reveal] { opacity: 0; transform: translateY(24px); transition: opacity .6s ease, transform .6s ease; transition-delay: var(--reveal-delay, 0ms); }
    .reveal-ready [data-reveal].is-visible { opacity: 1; transform: none; }
    @media (prefers-reduced-motion: reduce) {
      html { scroll-behavior: auto; }
      .scribble path, .ring-fill, .hover-image { transition: none; }
      ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
    }
  </style>
</head>
<body class="font-sans text-ink antialiased">
  <div class="grain" aria-hidden="true"></div>
  <% active_page = "index" %>
  <% nav = { "catalog" => "Catalog", "monthly-box" => "Monthly box", "about" => "About", "contact" => "Contact" } %>
  <!-- block:start navbar -->
  <header class="sticky top-0 z-40 border-b border-ink bg-peach/80 backdrop-blur">
    <nav class="flex h-16 items-stretch md:h-[5.5rem]" aria-label="Main">
      <button type="button" data-menu-toggle aria-controls="site-menu" aria-expanded="false" aria-label="Open menu" class="grid w-16 shrink-0 place-items-center border-r border-ink transition hover:bg-ink hover:text-peach md:w-[5.5rem]"><i class="fa-solid fa-bars text-xl" aria-hidden="true"></i></button>
      <a href="contact.html" <%= 'aria-current="page"'.html_safe if active_page == "contact" %> class="hidden items-center border-r border-ink px-11 font-medium transition hover:bg-ink hover:text-peach md:flex <%= 'underline underline-offset-8' if active_page == 'contact' %>">Contact</a>
      <a href="index.html" class="flex flex-1 items-center justify-center font-sans text-lg font-bold uppercase tracking-[0.08em] md:text-2xl" aria-label="Saltmere home">Saltmere</a>
      <a href="catalog.html" <%= 'aria-current="page"'.html_safe if active_page == "catalog" %> class="hidden items-center border-l border-ink px-11 font-medium transition hover:bg-ink hover:text-peach md:flex <%= 'underline underline-offset-8' if active_page == 'catalog' %>">Catalog</a>
      <a href="cart.html" <%= 'aria-current="page"'.html_safe if active_page == "cart" %> class="relative flex w-16 shrink-0 items-center justify-center border-l border-ink transition hover:bg-ink hover:text-peach md:w-[5.5rem]" aria-label="Cart"><i class="fa-solid fa-bag-shopping text-xl" aria-hidden="true"></i><span data-cart-count class="absolute right-2 top-2 grid h-5 min-w-5 place-items-center rounded-full bg-rust px-1 text-[11px] font-bold text-cream md:right-4 md:top-4">2</span></a>
      <div class="relative hidden border-l border-ink sm:block">
        <button type="button" data-lang-toggle aria-controls="lang-menu" aria-expanded="false" class="flex h-full items-center gap-2 px-6 font-medium transition hover:bg-ink hover:text-peach"><span data-lang-label>EN</span><i class="fa-solid fa-chevron-down text-xs" aria-hidden="true"></i></button>
        <ul id="lang-menu" hidden class="absolute right-0 top-full grid min-w-full border border-ink bg-cream">
          <% %w[EN ES FR DE].each do |lang| %><li><button type="button" data-lang="<%= lang %>" class="w-full px-6 py-3 text-left font-medium hover:bg-ink hover:text-cream"><%= lang %></button></li><% end %>
        </ul>
      </div>
    </nav>
  </header>
  <div id="site-menu" hidden class="fixed inset-0 z-50 grid bg-ink text-cream lg:grid-cols-[1.3fr_1fr]">
    <div class="flex flex-col overflow-y-auto px-6 py-6 md:px-16">
      <button type="button" data-menu-close aria-label="Close menu" class="grid h-12 w-12 place-items-center self-end border border-cream/40 transition hover:bg-cream hover:text-ink"><i class="fa-solid fa-xmark text-xl" aria-hidden="true"></i></button>
      <ul class="my-auto grid gap-2 py-10">
        <% { "index" => "Home" }.merge(nav).merge("cart" => "Cart").each do |slug, label| %>
          <% active = active_page == slug %>
          <li><a href="<%= slug %>.html" <%= 'aria-current="page"'.html_safe if active %> class="group flex items-baseline gap-5 font-display text-5xl uppercase transition md:text-7xl <%= active ? 'text-peach' : 'hover:text-peach' %>"><span class="font-sans text-sm text-cream/50"><%= active ? '●' : '→' %></span><%= label %></a></li>
        <% end %>
      </ul>
      <p class="text-sm text-cream/60">hello@saltmere.shop · Free shipping over $60</p>
    </div>
    <img src="https://images.unsplash.com/photo-1503951914875-452162b0f3f1?auto=format&fit=crop&w=1000&q=80" alt="Barber giving a client a hot-towel shave" class="hidden h-full w-full object-cover lg:block">
  </div>
  <!-- block:end navbar -->

  <!-- block:start hero -->
  <section class="mx-auto max-w-[96rem] overflow-x-clip px-5 pb-16 pt-14 md:px-10 lg:pt-20">
    <div class="grid grid-cols-1 items-start gap-14 lg:grid-cols-[1.15fr_1fr] lg:gap-10">
      <div class="lg:pl-[8%]">
        <h1 class="font-display text-[15vw] uppercase leading-none tracking-[0.02em] sm:text-8xl xl:text-[8.6rem]">
          <span class="flex items-center justify-between gap-6 border-b-2 border-ink pb-4">Sharp
            <svg class="h-14 w-11 shrink-0 sm:h-20 sm:w-16 md:h-32 md:w-24" viewBox="0 0 60 80" aria-hidden="true"><path d="M30 4 56 40 30 76 4 40Z" fill="none" stroke="#1d1b19" stroke-width="2"/><path d="M4 40h52L30 76Z" fill="#1d1b19" opacity=".15"/><circle cx="30" cy="28" r="7" fill="none" stroke="#1d1b19" stroke-width="1.6"/><path d="M30 35v14M24 44l6 5 6-5" fill="none" stroke="#1d1b19" stroke-width="1.6"/><path d="M56 40 30 76" stroke="#1d1b19" stroke-width="4"/></svg>
          </span>
          <span class="flex items-center justify-between gap-6 border-b-2 border-ink py-4">
            <svg class="h-14 w-14 shrink-0 sm:h-20 sm:w-20 md:h-32 md:w-32" viewBox="0 0 80 80" aria-hidden="true"><defs><path id="badge-arc" d="M14 40a26 26 0 0 1 52 0"/></defs><circle cx="40" cy="40" r="34" fill="none" stroke="#1d1b19" stroke-width="2"/><text font-family="Space Grotesk" font-size="7" font-weight="700" letter-spacing="2" fill="#1d1b19"><textPath href="#badge-arc" startOffset="50%" text-anchor="middle">SEA · SALT · CO</textPath></text><circle cx="40" cy="34" r="5" fill="#1d1b19"/><path d="M8 48c6-4 10-4 16 0s10 4 16 0 10-4 16 0 10 4 16 0V60a34 34 0 0 1-64 0Z" fill="#1d1b19" opacity=".2"/><path d="M8 48c6-4 10-4 16 0s10 4 16 0 10-4 16 0 10 4 16 0" fill="none" stroke="#1d1b19" stroke-width="2"/></svg>
            Steady
          </span>
          <span class="flex items-center justify-between gap-6 pt-4">Self
            <svg class="h-14 w-11 shrink-0 sm:h-20 sm:w-16 md:h-32 md:w-24" viewBox="0 0 60 80" aria-hidden="true"><path d="M6 78V30a24 24 0 0 1 48 0v48Z" fill="none" stroke="#1d1b19" stroke-width="2"/><path d="M6 62h48v16H6Z" fill="#1d1b19" opacity=".15"/><path d="M18 40a12 12 0 0 1 24 0Z" fill="#1d1b19"/><path d="M16 44h28M18 48h24" stroke="#1d1b19" stroke-width="1.5"/><path d="M16 62V50M16 50l-5 3M16 50l5 3M44 62V50M44 50l-5 3M44 50l5 3" stroke="#1d1b19" stroke-width="1.5" fill="none"/></svg>
          </span>
        </h1>
      </div>
      <div class="relative mx-auto w-full max-w-md lg:mt-8">
        <div class="relative">
          <span class="absolute inset-x-0 -bottom-2 top-6 -rotate-[4deg] bg-sage md:top-10" aria-hidden="true"></span>
          <img src="https://images.unsplash.com/photo-1531384441138-2736e62e0919?auto=format&fit=crop&w=800&h=950&q=80" alt="Smiling man with a beard in glasses and a dark turtleneck" class="relative aspect-[4/5] w-full object-cover mix-blend-normal">
          <span class="relative block h-4 bg-ink" aria-hidden="true"></span>
          <a href="monthly-box.html" class="absolute -right-3 -top-8 w-40 bg-rust p-5 text-cream shadow-xl transition hover:-translate-y-1 md:-right-10 md:w-48 md:p-6">
            <span class="block font-display text-lg uppercase leading-tight md:text-xl">Monthly care box</span>
            <span class="mt-3 block h-px w-12 bg-cream/70" aria-hidden="true"></span>
            <span class="mt-4 flex items-start font-sans text-5xl font-medium md:text-6xl">29<span class="ml-1 text-lg">$</span></span>
          </a>
        </div>
      </div>
    </div>
    <div class="mt-20 grid grid-cols-1 gap-10 lg:grid-cols-[1.15fr_1fr] lg:gap-10">
      <div class="grid gap-8 sm:grid-cols-[auto_1fr] sm:gap-16 lg:pl-[8%]">
        <p class="font-display text-3xl uppercase leading-tight">Care you've<br>long since<br>earned</p>
        <p class="text-lg leading-relaxed sm:text-right sm:text-xl">Small-batch grooming and skincare, made to fit how you actually live. <strong>Honest formulas, no fuss</strong> <span class="inline-flex translate-y-[-3px] items-center gap-1 align-middle" aria-hidden="true"><span class="h-1.5 w-1.5 rounded-full bg-ink"></span><span class="h-1 w-14 bg-ink"></span></span></p>
      </div>
      <form data-contact-form data-error="Please add a valid email." data-success="You're in. We'll email you the day the first batch ships." class="mx-auto w-full max-w-md" novalidate>
        <label for="preorder-email" class="font-display text-xl uppercase">Join the first batch</label>
        <div class="group relative mt-4 flex items-center bg-white shadow-sm">
          <input id="preorder-email" type="email" name="email" required autocomplete="email" placeholder="Your email for early access" class="min-w-0 flex-1 bg-transparent px-6 py-6 placeholder:text-ink/45 focus:outline-none">
          <button type="submit" class="relative px-8 py-6 font-display text-2xl uppercase">Send
            <svg class="scribble pointer-events-none absolute -inset-3" viewBox="0 0 140 100" preserveAspectRatio="none" aria-hidden="true"><path d="M110 12C70-2 14 14 10 50s48 46 86 40 38-36 24-58C110 16 86 10 70 12" fill="none" stroke="#a8443a" stroke-width="2.5" stroke-linecap="round"/></svg>
          </button>
        </div>
        <p data-form-status class="mt-3 text-sm font-medium text-rust-dark" role="status" aria-live="polite"></p>
      </form>
    </div>
  </section>
  <!-- block:end hero -->

  <% products = [["sea-salt-wash", "Sea salt face wash", "Cleanser", 18, "1620917669809-1af0497965de", "Cream tube of face wash on a white background"], ["cedar-oil", "Cedar beard oil", "Beard", 24, "1617897903246-719242758050", "Glass dropper bottle of oil on a wooden board with eucalyptus"], ["night-serum", "Night repair serum", "Serum", 32, "1608571423902-eed4a5ad8108", "Amber serum bottle on a wooden stand"], ["daily-lotion", "Daily light lotion", "Moisturiser", 26, "1631729371254-42c2892f0e6e", "Three white pump bottles beside a plant"], ["lip-balm", "Lip & brow balm", "Balm", 12, "1599305090598-fe179d501227", "Small open jar of balm on a stone surface"], ["olive-clay", "Olive clay bar", "Soap", 10, "1614806687007-2215a9db3b1c", "Stacked green olive soap bars with dried flowers"], ["lavender-bar", "Lavender bar", "Soap", 9, "1600857544200-b2f666a9a2ec", "Handmade lavender soap bars tied with string"], ["gel-mask", "Cooling gel mask", "Mask", 22, "1619451334792-150fd785ee74", "Hands holding an open jar of blue gel mask"]] %>
  <!-- block:start products -->
  <section class="border-y-2 border-ink bg-cream/60 py-20" data-rail-root>
    <div class="mx-auto flex max-w-[96rem] flex-wrap items-end justify-between gap-6 px-5 md:px-10">
      <div>
        <p class="text-sm font-bold uppercase tracking-[0.2em] text-rust">Bestsellers</p>
        <h2 class="mt-2 font-display text-5xl uppercase md:text-6xl">On every shelf</h2>
      </div>
      <div class="flex gap-2">
        <button type="button" data-rail-prev class="grid h-14 w-14 place-items-center border-2 border-ink transition hover:bg-ink hover:text-cream" aria-label="Previous products"><i class="fa-solid fa-arrow-left" aria-hidden="true"></i></button>
        <button type="button" data-rail-next class="grid h-14 w-14 place-items-center border-2 border-ink bg-ink text-cream transition hover:bg-rust hover:border-rust" aria-label="Next products"><i class="fa-solid fa-arrow-right" aria-hidden="true"></i></button>
      </div>
    </div>
    <ul data-rail-track class="rail mt-10 flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth px-5 pb-2 md:px-10" tabindex="0" aria-label="Bestselling products">
      <% products.each do |id, name, kind, price, photo, alt| %>
        <li class="w-[78%] shrink-0 snap-start sm:w-[45%] lg:w-[23%]">
          <article class="group flex h-full flex-col border-2 border-ink bg-white/70">
            <a href="monthly-box.html" class="block overflow-hidden border-b-2 border-ink"><img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=700&h=700&q=75" alt="<%= alt %>" class="aspect-square w-full object-cover transition duration-500 group-hover:scale-105" loading="lazy"></a>
            <div class="flex flex-1 flex-col p-5">
              <p class="text-xs font-bold uppercase tracking-[0.2em] text-ink/55"><%= kind %></p>
              <h3 class="mt-1 font-display text-2xl uppercase"><%= name %></h3>
              <div class="mt-auto flex items-center justify-between pt-6">
                <p class="text-xl font-medium">$<%= price %></p>
                <button type="button" data-add-to-cart data-name="<%= name %>" class="border-2 border-ink px-4 py-2 text-sm font-bold uppercase tracking-[0.1em] transition hover:bg-ink hover:text-cream">Add</button>
              </div>
            </div>
          </article>
        </li>
      <% end %>
    </ul>
    <p data-cart-toast class="mx-auto mt-6 max-w-[96rem] px-5 text-sm font-medium text-rust-dark md:px-10" role="status" aria-live="polite"></p>
  </section>
  <!-- block:end products -->

  <!-- block:start demo -->
  <section class="mx-auto max-w-[96rem] px-5 py-24 md:px-10" data-box-builder data-slots="4" data-discount="0.15">
    <div class="grid gap-14 lg:grid-cols-[1fr_1.1fr]">
      <div>
        <p class="text-sm font-bold uppercase tracking-[0.2em] text-rust">Build your box</p>
        <h2 class="mt-2 font-display text-5xl uppercase leading-none md:text-6xl">Four things,<br>every month</h2>
        <p class="mt-6 max-w-md text-lg">Pick any four. Members save 15% and can swap items until the 20th of each month.</p>
        <ul class="mt-10 grid grid-cols-2 gap-3 sm:grid-cols-4" aria-label="Choose products for your box">
          <% products.each do |id, name, kind, price, photo, alt| %>
            <li>
              <button type="button" data-box-item="<%= id %>" data-name="<%= name %>" data-price="<%= price %>" data-photo="<%= photo %>" aria-pressed="false" class="group relative block w-full border-2 border-ink/20 bg-white/60 p-2 text-left transition hover:border-ink aria-pressed:border-rust aria-pressed:bg-rust aria-pressed:text-cream">
                <img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=300&h=300&q=60" alt="" class="aspect-square w-full object-cover">
                <span class="mt-2 block text-sm font-medium leading-tight"><%= name %></span>
                <span class="block text-xs opacity-70">$<%= price %></span>
                <span class="absolute right-3 top-3 hidden h-7 w-7 place-items-center rounded-full bg-cream text-rust group-aria-pressed:grid" aria-hidden="true"><i class="fa-solid fa-check text-xs"></i></span>
              </button>
            </li>
          <% end %>
        </ul>
      </div>
      <div class="relative self-start border-2 border-ink bg-white/70 p-6 md:p-10 lg:sticky lg:top-28">
        <div class="flex items-center justify-between gap-4">
          <p class="font-display text-3xl uppercase">Your box</p>
          <p class="text-sm font-bold uppercase tracking-[0.15em]"><span data-box-count>0</span> / 4</p>
        </div>
        <ol data-box-slots class="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4">
          <% 4.times do |i| %>
            <li data-box-slot class="grid aspect-square place-items-center border-2 border-dashed border-ink/30 bg-sand/50 text-center text-xs uppercase tracking-[0.15em] text-ink/50">Slot <%= i + 1 %></li>
          <% end %>
        </ol>
        <div class="mt-6 h-2 bg-ink/10" aria-hidden="true"><div data-box-progress class="h-full w-0 bg-rust transition-all"></div></div>
        <dl class="mt-8 grid gap-3 text-lg">
          <div class="flex justify-between"><dt>Shelf price</dt><dd data-box-full>$0</dd></div>
          <div class="flex justify-between text-rust-dark"><dt>Member saving</dt><dd data-box-saving>−$0</dd></div>
          <div class="flex justify-between border-t-2 border-ink pt-3 font-display text-3xl uppercase"><dt>Monthly</dt><dd data-box-total>$0</dd></div>
        </dl>
        <button type="button" data-box-add disabled class="mt-8 w-full bg-ink px-6 py-5 font-bold uppercase tracking-[0.15em] text-cream transition hover:bg-rust disabled:cursor-not-allowed disabled:opacity-40">Add box to cart</button>
        <p data-box-status class="mt-3 text-sm font-medium text-rust-dark" role="status" aria-live="polite">Pick four products to fill your box.</p>
      </div>
    </div>
  </section>
  <!-- block:end demo -->

  <% ingredients = [["Sea salt", "Gently buffs and wakes up tired skin.", "1556760544-74068565f05c", "Hands applying oil from a small dropper bottle"], ["Cedar", "Warm, woody and calming for beard and scalp.", "1617897903246-719242758050", "Dropper bottle of oil on a wooden board"], ["Oat milk", "Soothes razor burn and redness.", "1596755389378-c31d21fd1273", "Person in a face mask holding half an avocado"], ["Green clay", "Draws out oil without stripping.", "1614806687007-2215a9db3b1c", "Green clay soap bars stacked on linen"], ["Rosehip", "Rich in vitamin A for overnight repair.", "1600428877878-1a0fd85beda8", "Serum bottle with a stone face roller and gua sha"]] %>
  <!-- block:start features -->
  <section class="relative bg-ink py-24 text-cream" data-hover-list>
    <div class="mx-auto max-w-[96rem] px-5 md:px-10">
      <div class="flex flex-wrap items-end justify-between gap-6">
        <h2 class="font-display text-5xl uppercase md:text-6xl">What goes in</h2>
        <p class="max-w-sm text-cream/70">Five hero ingredients, sourced from small growers. Hover a row to see it at work.</p>
      </div>
      <ol class="mt-12 border-t border-cream/25">
        <% ingredients.each_with_index do |(name, text, photo, alt), index| %>
          <li>
            <a href="catalog.html" data-hover-row data-image="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=600&h=750&q=70" data-alt="<%= alt %>" class="group grid items-baseline gap-2 border-b border-cream/25 py-6 transition md:grid-cols-[5rem_1fr_1fr] md:gap-8 md:py-8">
              <span class="text-sm text-cream/50">0<%= index + 1 %></span>
              <span class="font-display text-4xl uppercase transition group-hover:translate-x-3 group-hover:text-peach group-focus-visible:text-peach md:text-7xl"><%= name %></span>
              <span class="text-cream/75 md:text-right md:text-lg"><%= text %></span>
            </a>
          </li>
        <% end %>
      </ol>
    </div>
    <img data-hover-image src="https://images.unsplash.com/photo-<%= ingredients.first[2] %>?auto=format&fit=crop&w=600&h=750&q=70" alt="" class="hover-image pointer-events-none fixed left-0 top-0 z-30 hidden w-56 -rotate-3 border-4 border-cream opacity-0 shadow-2xl md:block" style="aspect-ratio: 4 / 5; object-fit: cover">
  </section>
  <!-- block:end features -->

  <% results = [[94, "said skin felt softer after two weeks"], [88, "noticed less razor burn by week three"], [100, "plastic-free outer packaging"]] %>
  <!-- block:start stats -->
  <section class="mx-auto max-w-[96rem] px-5 py-24 md:px-10" data-rings>
    <div class="grid gap-12 lg:grid-cols-[1fr_2fr] lg:items-center">
      <div>
        <p class="text-sm font-bold uppercase tracking-[0.2em] text-rust">Tested by 300 men</p>
        <h2 class="mt-2 font-display text-5xl uppercase leading-none md:text-6xl">Proof, not promises</h2>
        <a href="about.html" class="mt-8 inline-block border-b-2 border-ink pb-1 font-bold uppercase tracking-[0.1em] transition hover:border-rust hover:text-rust">How we test</a>
      </div>
      <ul class="grid gap-10 sm:grid-cols-3">
        <% results.each do |value, label| %>
          <li class="text-center" data-reveal>
            <div class="relative mx-auto h-44 w-44">
              <svg class="h-full w-full -rotate-90" viewBox="0 0 120 120" aria-hidden="true"><circle cx="60" cy="60" r="52" fill="none" stroke="rgba(29,27,25,.12)" stroke-width="8"/><circle class="ring-fill" data-ring="<%= value %>" cx="60" cy="60" r="52" fill="none" stroke="#a8443a" stroke-width="8" stroke-dasharray="326.73" stroke-dashoffset="<%= (326.73 * (1 - value / 100.0)).round(2) %>"/></svg>
              <p class="absolute inset-0 grid place-items-center font-display text-5xl"><%= value %>%</p>
            </div>
            <p class="mx-auto mt-5 max-w-[14rem] text-lg"><%= label %></p>
          </li>
        <% end %>
      </ul>
    </div>
  </section>
  <!-- block:end stats -->

  <% plans = [["monthly", "Every month", 29, "4 full-size items, swap anytime"], ["quarterly", "Every 3 months", 79, "12 items, ships as one bigger box"], ["yearly", "Once a year", 290, "2 months free and a travel kit"]] %>
  <!-- block:start pricing -->
  <section class="border-y-2 border-ink bg-sage/90 py-24 text-cream" data-membership>
    <div class="mx-auto grid max-w-[96rem] items-center gap-14 px-5 md:px-10 lg:grid-cols-2">
      <div>
        <p class="text-sm font-bold uppercase tracking-[0.2em] text-cream/80">The Saltmere club</p>
        <h2 class="mt-2 font-display text-5xl uppercase leading-none md:text-6xl">Pick a rhythm</h2>
        <fieldset class="mt-10 grid gap-3">
          <legend class="sr-only">Delivery frequency</legend>
          <% plans.each_with_index do |(id, label, price, note), index| %>
            <label class="flex cursor-pointer items-center gap-4 border-2 border-cream/40 p-5 transition hover:border-cream has-[:checked]:border-cream has-[:checked]:bg-cream has-[:checked]:text-ink">
              <input type="radio" name="plan" value="<%= id %>" data-label="<%= label %>" data-price="<%= price %>" data-note="<%= note %>" class="h-5 w-5 accent-rust" <%= 'checked' if index.zero? %>>
              <span class="flex-1"><span class="block font-display text-2xl uppercase"><%= label %></span><span class="text-sm opacity-80"><%= note %></span></span>
              <span class="text-2xl font-medium">$<%= price %></span>
            </label>
          <% end %>
        </fieldset>
      </div>
      <div class="mx-auto w-full max-w-md" aria-live="polite">
        <div class="relative aspect-[1.586] w-full rotate-[-3deg] overflow-hidden bg-rust p-7 shadow-2xl transition hover:rotate-0">
          <svg class="absolute -right-10 -top-10 h-56 w-56 opacity-25" viewBox="0 0 80 80" aria-hidden="true"><circle cx="40" cy="40" r="34" fill="none" stroke="#f5e6be" stroke-width="2"/><path d="M8 48c6-4 10-4 16 0s10 4 16 0 10-4 16 0 10 4 16 0" fill="none" stroke="#f5e6be" stroke-width="2"/><circle cx="40" cy="34" r="5" fill="#f5e6be"/></svg>
          <p class="text-sm font-bold uppercase tracking-[0.25em]">Saltmere · member</p>
          <p class="mt-6 font-display text-3xl uppercase" data-plan-label><%= plans.first[1] %></p>
          <p class="mt-1 text-sm text-cream/80" data-plan-note><%= plans.first[3] %></p>
          <p class="absolute bottom-6 left-7 flex items-start font-medium"><span class="text-6xl" data-plan-price><%= plans.first[2] %></span><span class="ml-1 text-xl">$</span></p>
          <p class="absolute bottom-7 right-7 text-sm tracking-[0.3em]">NO. 0001</p>
        </div>
        <a href="monthly-box.html" class="mt-12 block bg-ink px-6 py-5 text-center font-bold uppercase tracking-[0.15em] text-cream transition hover:bg-cream hover:text-ink">Start my membership</a>
      </div>
    </div>
  </section>
  <!-- block:end pricing -->

  <% reviews = [["Finally a face wash that doesn't leave my skin tight.", "Andre", "1500648767791-00dcc994a43e", "bg-white/70"], ["The beard oil smells like a forest after rain. My partner steals it.", "Tomás", "1507003211169-0a1dd7228f2d", "bg-peach"], ["Swapped two items this month in under a minute.", "Liam", "1539571696357-5a69c17a67c6", "bg-white/70"], ["Bought it for my brother, now we both subscribe. The gel mask is the hero.", "Nia", "1519699047748-de8e457a634e", "bg-cream"], ["Packaging is all paper and tins. Nothing to feel bad about.", "Marcus", "1506794778202-cad84cf45f1d", "bg-white/70"], ["Razor burn gone by week two.", "Ivo", "1531384441138-2736e62e0919", "bg-rust text-cream"]] %>
  <!-- block:start reviews -->
  <section class="mx-auto max-w-[96rem] px-5 py-24 md:px-10">
    <div class="flex flex-wrap items-end justify-between gap-6">
      <h2 class="font-display text-5xl uppercase md:text-6xl">Overheard</h2>
      <p class="text-lg"><i class="fa-solid fa-star text-rust" aria-hidden="true"></i> 4.8 from 2,140 reviews</p>
    </div>
    <ul class="mt-12 gap-5 sm:columns-2 lg:columns-3">
      <% reviews.each_with_index do |(quote, name, photo, tone), index| %>
        <li class="mb-5 break-inside-avoid border-2 border-ink p-7 <%= tone %>" data-reveal style="--reveal-delay: <%= (index % 3) * 90 %>ms">
          <p class="font-display <%= quote.length < 50 ? 'text-4xl' : 'text-2xl' %> uppercase leading-tight">&ldquo;<%= quote %>&rdquo;</p>
          <p class="mt-6 flex items-center gap-3"><img src="https://images.unsplash.com/photo-<%= photo %>?auto=format&fit=crop&w=96&h=96&q=70" alt="" class="h-10 w-10 rounded-full object-cover"><span class="font-bold"><%= name %></span><span class="text-sm opacity-70">· verified buyer</span></p>
        </li>
      <% end %>
    </ul>
  </section>
  <!-- block:end reviews -->

  <!-- block:start footer -->
  <footer class="border-t-2 border-ink bg-ink text-cream">
    <div class="mx-auto grid max-w-[96rem] gap-10 px-5 py-16 sm:grid-cols-2 md:px-10 lg:grid-cols-4">
      <div>
        <a href="index.html" class="text-2xl font-bold uppercase tracking-[0.08em]" aria-label="Saltmere home">Saltmere</a>
        <p class="mt-4 max-w-xs text-cream/70">Small-batch grooming and skincare. Mixed, filled and packed by hand in Porto.</p>
      </div>
      <div>
        <p class="font-display text-xl uppercase">Shop</p>
        <ul class="mt-4 grid gap-2 text-cream/75">
          <% [["catalog", "All products"], ["monthly-box", "Monthly box"], ["cart", "Cart"]].each do |slug, label| %><li><a href="<%= slug %>.html" class="hover:text-peach"><%= label %></a></li><% end %>
        </ul>
      </div>
      <div>
        <p class="font-display text-xl uppercase">Studio</p>
        <ul class="mt-4 grid gap-2 text-cream/75">
          <% [["about", "Our story"], ["contact", "Book a consult"], ["contact", "Shipping & returns"]].each do |slug, label| %><li><a href="<%= slug %>.html" class="hover:text-peach"><%= label %></a></li><% end %>
        </ul>
      </div>
      <div>
        <p class="font-display text-xl uppercase">Follow</p>
        <ul class="mt-4 flex gap-3">
          <% [["instagram", "Instagram", "https://www.instagram.com"], ["tiktok", "TikTok", "https://www.tiktok.com"], ["youtube", "YouTube", "https://www.youtube.com"]].each do |icon, label, url| %>
            <li><a href="<%= url %>" aria-label="<%= label %>" class="grid h-11 w-11 place-items-center border border-cream/40 transition hover:bg-cream hover:text-ink"><i class="fa-brands fa-<%= icon %>" aria-hidden="true"></i></a></li>
          <% end %>
        </ul>
      </div>
    </div>
    <p class="border-t border-cream/15 px-5 py-6 text-center text-sm text-cream/50">&copy; <span data-current-year>2026</span> Saltmere Goods. Free shipping over $60.</p>
  </footer>
  <!-- block:end footer -->

  <script>
    const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
    const watchMotion = !reducedMotion && !navigator.webdriver && 'IntersectionObserver' in window;
    const store = { get: (key, fallback) => { try { return sessionStorage.getItem(key) ?? fallback; } catch (e) { return fallback; } }, set: (key, value) => { try { sessionStorage.setItem(key, value); } catch (e) {} } };

    document.querySelectorAll('[data-menu-toggle]').forEach((button) => {
      const menu = document.getElementById(button.getAttribute('aria-controls'));
      const close = menu.querySelector('[data-menu-close]');
      const set = (open) => { menu.hidden = !open; button.setAttribute('aria-expanded', String(open)); document.body.style.overflow = open ? 'hidden' : ''; (open ? close : button).focus(); };
      button.addEventListener('click', () => set(menu.hidden));
      close.addEventListener('click', () => set(false));
      document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !menu.hidden) set(false); });
    });

    document.querySelectorAll('[data-lang-toggle]').forEach((button) => {
      const menu = document.getElementById(button.getAttribute('aria-controls'));
      const set = (open) => { menu.hidden = !open; button.setAttribute('aria-expanded', String(open)); };
      button.addEventListener('click', () => set(menu.hidden));
      menu.querySelectorAll('[data-lang]').forEach((item) => item.addEventListener('click', () => { button.querySelector('[data-lang-label]').textContent = item.dataset.lang; set(false); button.focus(); }));
      document.addEventListener('click', (event) => { if (!button.parentElement.contains(event.target)) set(false); });
    });

    const cartCount = () => Number(store.get('saltmere-cart', '2'));
    const paintCart = () => document.querySelectorAll('[data-cart-count]').forEach((badge) => { badge.textContent = cartCount(); });
    const addToCart = (amount) => { store.set('saltmere-cart', String(cartCount() + amount)); paintCart(); };
    paintCart();
    document.querySelectorAll('[data-add-to-cart]').forEach((button) => button.addEventListener('click', () => {
      addToCart(Number(button.dataset.qty || 1));
      const toast = document.querySelector('[data-cart-toast]');
      if (toast) toast.textContent = `${button.dataset.name} added to your cart.`;
      const label = button.textContent;
      button.textContent = 'Added ✓';
      setTimeout(() => { button.textContent = label; }, 1400);
    }));

    document.querySelectorAll('[data-rail-root]').forEach((root) => {
      const track = root.querySelector('[data-rail-track]');
      const step = () => track.firstElementChild.getBoundingClientRect().width + 20;
      root.querySelector('[data-rail-prev]').addEventListener('click', () => track.scrollBy({ left: -step(), behavior: reducedMotion ? 'auto' : 'smooth' }));
      root.querySelector('[data-rail-next]').addEventListener('click', () => track.scrollBy({ left: step(), behavior: reducedMotion ? 'auto' : 'smooth' }));
    });

    document.querySelectorAll('[data-box-builder]').forEach((root) => {
      const max = Number(root.dataset.slots);
      const discount = Number(root.dataset.discount);
      const items = [...root.querySelectorAll('[data-box-item]')];
      const slots = [...root.querySelectorAll('[data-box-slot]')];
      const status = root.querySelector('[data-box-status]');
      const add = root.querySelector('[data-box-add]');
      let picked = [];
      const money = (value) => `$${value.toFixed(2).replace(/\.00$/, '')}`;
      const paint = () => {
        items.forEach((item) => item.setAttribute('aria-pressed', String(picked.includes(item))));
        slots.forEach((slot, i) => {
          const item = picked[i];
          slot.innerHTML = item ? `<img src="https://images.unsplash.com/photo-${item.dataset.photo}?auto=format&fit=crop&w=300&h=300&q=60" alt="${item.dataset.name}" class="h-full w-full object-cover">` : `Slot ${i + 1}`;
          slot.classList.toggle('border-solid', !!item);
        });
        const full = picked.reduce((sum, item) => sum + Number(item.dataset.price), 0);
        root.querySelector('[data-box-count]').textContent = picked.length;
        root.querySelector('[data-box-progress]').style.width = `${(picked.length / max) * 100}%`;
        root.querySelector('[data-box-full]').textContent = money(full);
        root.querySelector('[data-box-saving]').textContent = `−${money(full * discount)}`;
        root.querySelector('[data-box-total]').textContent = money(full * (1 - discount));
        add.disabled = picked.length !== max;
        status.textContent = picked.length === max ? 'Box full. Swap an item by tapping it again.' : `Pick ${max - picked.length} more to fill your box.`;
      };
      items.forEach((item) => item.addEventListener('click', () => {
        if (picked.includes(item)) picked = picked.filter((other) => other !== item);
        else if (picked.length < max) picked.push(item);
        else { status.textContent = 'Your box is full. Remove an item first.'; return; }
        paint();
      }));
      add.addEventListener('click', () => { addToCart(1); status.textContent = 'Monthly box added to your cart.'; });
      paint();
    });

    document.querySelectorAll('[data-hover-list]').forEach((root) => {
      const image = root.querySelector('[data-hover-image]');
      root.querySelectorAll('[data-hover-row]').forEach((row) => {
        const show = () => { image.src = row.dataset.image; image.alt = row.dataset.alt; image.style.opacity = '1'; };
        row.addEventListener('mouseenter', show);
        row.addEventListener('mousemove', (event) => { image.style.transform = `translate(${event.clientX + 24}px, ${event.clientY - 140}px) rotate(-3deg)`; });
        row.addEventListener('mouseleave', () => { image.style.opacity = '0'; });
      });
    });

    document.querySelectorAll('[data-rings]').forEach((root) => {
      const rings = root.querySelectorAll('[data-ring]');
      if (!watchMotion) return;
      const full = 326.73;
      rings.forEach((ring) => { ring.style.strokeDashoffset = full; });
      const observer = new IntersectionObserver((entries) => {
        if (!entries.some((entry) => entry.isIntersecting)) return;
        rings.forEach((ring) => { ring.style.strokeDashoffset = full * (1 - Number(ring.dataset.ring) / 100); });
        observer.disconnect();
      }, { threshold: 0.3 });
      observer.observe(root);
    });

    document.querySelectorAll('[data-membership]').forEach((root) => {
      root.addEventListener('change', (event) => {
        const plan = event.target;
        if (plan.name !== 'plan') return;
        root.querySelector('[data-plan-label]').textContent = plan.dataset.label;
        root.querySelector('[data-plan-note]').textContent = plan.dataset.note;
        root.querySelector('[data-plan-price]').textContent = plan.dataset.price;
      });
    });

    document.querySelectorAll('[data-catalog]').forEach((root) => {
      const grid = root.querySelector('[data-catalog-grid]');
      const items = [...grid.children];
      const count = root.querySelector('[data-catalog-count]');
      let kind = 'all';
      const apply = () => {
        const sort = root.querySelector('[data-catalog-sort]').value;
        const sorted = [...items].sort((a, b) => sort === 'low' ? a.dataset.price - b.dataset.price : sort === 'high' ? b.dataset.price - a.dataset.price : a.dataset.order - b.dataset.order);
        sorted.forEach((item) => { item.hidden = kind !== 'all' && item.dataset.kind !== kind; grid.append(item); });
        count.textContent = sorted.filter((item) => !item.hidden).length;
      };
      root.querySelectorAll('[data-kind-tab]').forEach((tab) => tab.addEventListener('click', () => {
        kind = tab.dataset.kindTab;
        root.querySelectorAll('[data-kind-tab]').forEach((other) => { other.setAttribute('aria-selected', String(other === tab)); other.classList.toggle('active', other === tab); });
        apply();
      }));
      root.querySelector('[data-catalog-sort]').addEventListener('change', apply);
      apply();
    });

    document.querySelectorAll('[data-product]').forEach((root) => {
      const main = root.querySelector('[data-product-main]');
      root.querySelectorAll('[data-product-thumb]').forEach((thumb) => thumb.addEventListener('click', () => {
        main.src = thumb.dataset.src; main.alt = thumb.dataset.alt;
        root.querySelectorAll('[data-product-thumb]').forEach((other) => other.setAttribute('aria-pressed', String(other === thumb)));
      }));
      const qty = root.querySelector('[data-qty]');
      const price = root.querySelector('[data-product-price]');
      const button = root.querySelector('[data-add-to-cart]');
      const paint = () => {
        const option = [...root.querySelectorAll('[name="purchase"]')].find((radio) => radio.checked);
        const total = Number(option.dataset.price) * Number(qty.value);
        price.textContent = `$${total}`;
        button.dataset.qty = qty.value;
      };
      root.querySelectorAll('[data-qty-step]').forEach((step) => step.addEventListener('click', () => { qty.value = Math.min(9, Math.max(1, Number(qty.value) + Number(step.dataset.qtyStep))); paint(); }));
      root.addEventListener('change', paint);
      paint();
    });

    document.querySelectorAll('[data-tabs]').forEach((tabs) => {
      const buttons = [...tabs.querySelectorAll('[role="tab"]')];
      const select = (tab) => {
        buttons.forEach((other) => { const on = other === tab; other.setAttribute('aria-selected', String(on)); other.tabIndex = on ? 0 : -1; other.classList.toggle('active', on); document.getElementById(other.getAttribute('aria-controls')).hidden = !on; });
      };
      buttons.forEach((tab, index) => {
        tab.addEventListener('click', () => select(tab));
        tab.addEventListener('keydown', (event) => {
          const move = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
          if (!move) return;
          const next = buttons[(index + move + buttons.length) % buttons.length];
          select(next); next.focus();
        });
      });
    });

    document.querySelectorAll('[data-cart]').forEach((root) => {
      const threshold = Number(root.dataset.freeShipping);
      const paint = () => {
        const lines = [...root.querySelectorAll('[data-line]')];
        let subtotal = 0; let units = 0;
        lines.forEach((line) => { const qty = Number(line.querySelector('[data-line-qty]').value); units += qty; const total = qty * Number(line.dataset.price); subtotal += total; line.querySelector('[data-line-total]').textContent = `$${total}`; });
        const discount = root.dataset.promo === 'on' ? Math.round(subtotal * 0.1) : 0;
        const shipping = subtotal - discount >= threshold || subtotal === 0 ? 0 : 6;
        root.querySelector('[data-subtotal]').textContent = `$${subtotal}`;
        root.querySelector('[data-discount]').textContent = discount ? `−$${discount}` : '$0';
        root.querySelector('[data-shipping]').textContent = shipping ? `$${shipping}` : 'Free';
        root.querySelector('[data-total]').textContent = `$${subtotal - discount + shipping}`;
        const left = Math.max(0, threshold - (subtotal - discount));
        root.querySelector('[data-ship-progress]').style.width = `${Math.min(100, ((subtotal - discount) / threshold) * 100)}%`;
        root.querySelector('[data-ship-note]').textContent = left ? `Add $${left} more for free shipping.` : 'You get free shipping.';
        root.querySelector('[data-cart-empty]').hidden = lines.length > 0;
        store.set('saltmere-cart', String(units)); paintCart();
      };
      root.addEventListener('click', (event) => {
        const step = event.target.closest('[data-line-step]');
        const remove = event.target.closest('[data-line-remove]');
        if (step) { const input = step.closest('[data-line]').querySelector('[data-line-qty]'); input.value = Math.min(9, Math.max(1, Number(input.value) + Number(step.dataset.lineStep))); paint(); }
        if (remove) { remove.closest('[data-line]').remove(); paint(); }
      });
      const promo = root.querySelector('[data-promo-form]');
      promo.addEventListener('submit', (event) => {
        event.preventDefault();
        const code = promo.querySelector('[name="code"]').value.trim().toUpperCase();
        const status = promo.querySelector('[data-form-status]');
        if (code === 'SALT10') { root.dataset.promo = 'on'; status.textContent = 'SALT10 applied: 10% off.'; } else { status.textContent = 'That code does not work. Try SALT10.'; }
        paint();
      });
      paint();
    });

    document.querySelectorAll('[data-slot-picker]').forEach((root) => {
      const summary = root.querySelector('[data-slot-summary]');
      root.addEventListener('change', () => {
        const day = root.querySelector('[name="day"]:checked');
        const time = root.querySelector('[name="time"]:checked');
        summary.textContent = day && time ? `${day.dataset.label} at ${time.value}` : day ? `${day.dataset.label}, pick a time` : 'Pick a day and a time';
      });
    });

    document.querySelectorAll('[data-faq-index]').forEach((index) => {
      const links = [...index.querySelectorAll('[data-faq-link]')];
      const mark = (id) => links.forEach((link) => { const on = link.getAttribute('href') === `#${id}`; link.classList.toggle('border-rust', on); link.classList.toggle('font-bold', on); link.classList.toggle('border-transparent', !on); });
      links.forEach((link) => link.addEventListener('click', () => mark(link.getAttribute('href').slice(1))));
      if (!('IntersectionObserver' in window)) return;
      const observer = new IntersectionObserver((entries) => entries.forEach((entry) => { if (entry.isIntersecting) mark(entry.target.id); }), { rootMargin: '-30% 0px -60% 0px' });
      document.querySelectorAll('[data-faq-item]').forEach((item) => observer.observe(item));
    });

    document.querySelectorAll('[data-contact-form]').forEach((form) => {
      form.addEventListener('submit', (event) => {
        event.preventDefault();
        const status = form.querySelector('[data-form-status]');
        if (!form.checkValidity()) {
          status.textContent = form.dataset.error;
          form.querySelector(':invalid')?.focus();
          return;
        }
        status.textContent = form.dataset.success;
        form.reset();
      });
    });

    document.querySelectorAll('[data-timeline]').forEach((root) => {
      const fill = root.querySelector('[data-timeline-fill]');
      if (!watchMotion) return;
      const update = () => {
        const box = root.getBoundingClientRect();
        const progress = Math.min(1, Math.max(0, (innerHeight * 0.6 - box.top) / box.height));
        fill.style.transform = `scaleY(${progress})`;
      };
      update();
      addEventListener('scroll', update, { passive: true });
    });

    document.querySelectorAll('[data-current-year]').forEach((el) => { el.textContent = new Date().getFullYear(); });

    (() => {
      const items = document.querySelectorAll('[data-reveal]');
      if (!items.length || !watchMotion) return;
      document.documentElement.classList.add('reveal-ready');
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          entry.target.classList.add('is-visible');
          observer.unobserve(entry.target);
        });
      }, { rootMargin: '0px 0px -10% 0px' });
      items.forEach((item) => observer.observe(item));
    })();
  </script>
</body>
</html>

Unlimited access to all templates

Unlock our entire library of premium UI kits, dashboards, and landing pages. Renews automatically, cancel anytime.

$20 / 3 months or $60 / year
Get Unlimited Access

More E-commerce Templates