← All sections

Cursor Trail Hero

Products spill out behind the cursor, so visitors browse the range just by moving.

FreeHero & banners33 settingsInstall
Full screen ↗

Install

One file: sections/mf-cursor-trail-hero.liquid. It appears in the theme editor as EX · Cursor trail hero.

  1. Connect EXPERAISE Sections to Claude (once). In Claude Code:
    terminal
    claude mcp add --transport http experaise-sections https://sections.experaise.com/api/mcp
    In the Claude app: Settings → Connectors → Add custom connector → https://sections.experaise.com/api/mcp. Full setup
  2. Ask Claude. It fetches sections/mf-cursor-trail-hero.liquid, pushes only that file to your theme with Shopify CLI, and tells you where to find it.
    prompt
    Install the EXPERAISE Sections section "Cursor Trail Hero" (cursor-trail-hero) on my Shopify store your-store.myshopify.com. Put it on an unpublished copy of my live theme, then tell me how to add it in the theme editor.
  3. Customize. Online Store → Themes → Customize → Add section → EX · Cursor trail hero. Every text, color and product is a setting.
sections/mf-cursor-trail-hero.liquidDownload
{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign pool = s.pool | default: 12
  assign sep = '|MFTRAIL|'
  assign tags = ''
  assign count = 0
  assign first_title = ''
  assign first_price = ''
  assign first_url = ''
  assign cta_link = s.button_link | default: s.collection.url | default: routes.all_products_collection_url
-%}

{%- comment -%} Sources: Trail image blocks first, then the collection's products. {%- endcomment -%}
{%- for block in section.blocks -%}
  {%- if block.settings.image != blank -%}
    {%- liquid
      assign n = count | plus: 1
      if n < 10
        assign n_pad = n | prepend: '0'
      else
        assign n_pad = n
      endif
      assign item_title = block.settings.title | escape
      assign item_url = block.settings.link | default: cta_link
    -%}
    {%- capture tag -%}
      <div class="mf-trail__item" data-n="{{ n_pad }}" data-title="{{ item_title }}" data-price="" data-url="{{ item_url }}">
        {{- block.settings.image | image_url: width: 640 | image_tag: loading: 'lazy', alt: '', sizes: '(min-width: 750px) 300px, 180px', widths: '240, 320, 480, 640', class: 'mf-trail__img' -}}
      </div>
    {%- endcapture -%}
    {%- liquid
      if count == 0
        assign first_title = item_title
        assign first_url = item_url
      else
        assign tags = tags | append: sep
      endif
      assign tags = tags | append: tag
      assign count = count | plus: 1
    -%}
  {%- endif -%}
{%- endfor -%}

{%- if count == 0 and s.collection != blank -%}
  {%- for item in s.collection.products limit: 16 -%}
    {%- liquid
      assign media = item.featured_media
      if s.card_image == 'second' and item.media.size > 1
        assign media = item.media[1]
      endif
    -%}
    {%- if media != blank -%}
      {%- liquid
        assign n = count | plus: 1
        if n < 10
          assign n_pad = n | prepend: '0'
        else
          assign n_pad = n
        endif
        assign item_title = item.title | escape
        assign item_price = item.price | money | strip_html | escape
      -%}
      {%- capture tag -%}
        <div class="mf-trail__item" data-n="{{ n_pad }}" data-title="{{ item_title }}" data-price="{{ item_price }}" data-url="{{ item.url }}">
          {{- media | image_url: width: 640 | image_tag: loading: 'lazy', alt: '', sizes: '(min-width: 750px) 300px, 180px', widths: '240, 320, 480, 640', class: 'mf-trail__img' -}}
        </div>
      {%- endcapture -%}
      {%- liquid
        if count == 0
          assign first_title = item_title
          assign first_price = item_price
          assign first_url = item.url
        else
          assign tags = tags | append: sep
        endif
        assign tags = tags | append: tag
        assign count = count | plus: 1
      -%}
    {%- endif -%}
  {%- endfor -%}
{%- endif -%}

{%- liquid
  assign items = tags | split: sep
  if count == 0
    assign total = 8
  else
    assign total = count
  endif
  if total < 10
    assign total_pad = total | prepend: '0'
  else
    assign total_pad = total
  endif
-%}

<mf-cursor-trail-hero
  class="mf-trail mf-trail--accent-{{ s.accent_style }}{% if s.blend %} mf-trail--blend{% endif %}"
  data-spacing="{{ s.spacing }}"
  data-life="{{ s.lifetime }}"
  data-autoplay="{{ s.autoplay }}"
  data-hint="{{ s.hint_desktop | escape }}"
  data-hint-touch="{{ s.hint_touch | escape }}"
  style="--mf-trail-bg: {{ s.color_bg }}; --mf-trail-fg: {{ s.color_text }}; --mf-trail-accent: {{ s.color_accent }}; --mf-trail-min-h: {{ s.min_height }}px; --mf-trail-pad: {{ s.padding }}px; --mf-trail-w: {{ s.size }}px; --mf-trail-radius: {{ s.radius }}px;"
>
  <div class="mf-trail__bg" aria-hidden="true"></div>

  <div class="mf-trail__layer" aria-hidden="true">
    {%- for i in (1..pool) -%}
      {%- if count == 0 -%}
        {%- assign art = i | minus: 1 | modulo: 8 | plus: 1 -%}
        <div class="mf-trail__item mf-trail__item--art" data-n="0{{ art }}"><span class="mf-trail__art mf-trail__art--{{ art }}"></span></div>
      {%- else -%}
        {%- assign idx = i | minus: 1 | modulo: count -%}
        {{- items[idx] -}}
      {%- endif -%}
    {%- endfor -%}
  </div>

  <div class="mf-trail__marks" aria-hidden="true"></div>

  <div class="mf-trail__frame">
    <div class="mf-trail__row mf-trail__row--top">
      {%- if s.eyebrow != blank -%}
        <p class="mf-trail__eyebrow"><i aria-hidden="true"></i><span>{{ s.eyebrow | escape }}</span></p>
      {%- endif -%}
      {%- if s.counter_label != blank -%}
        <p class="mf-trail__count" aria-hidden="true">
          <span>{{ s.counter_label | escape }}</span>
          <b class="mf-trail__count-num">01</b>
          <span>/ {{ total_pad }}</span>
        </p>
      {%- endif -%}
    </div>

    <div class="mf-trail__center">
      <{{ heading_tag }} class="mf-trail__title">
        <span class="mf-trail__line"><span class="mf-trail__line-in">{{ s.heading }}</span></span>
        {%- if s.heading_accent != blank -%}
          <span class="mf-trail__line mf-trail__line--accent"><span class="mf-trail__line-in">{{ s.heading_accent }}</span></span>
        {%- endif -%}
      </{{ heading_tag }}>
      {%- if s.text != blank -%}
        <div class="mf-trail__text">{{ s.text }}</div>
      {%- endif -%}
      {%- if s.button_label != blank or s.link_label != blank -%}
        <div class="mf-trail__actions">
          {%- if s.button_label != blank -%}
            <a class="mf-trail__button" href="{{ cta_link }}">
              {{- s.button_label | escape }} <span aria-hidden="true">→</span>
            </a>
          {%- endif -%}
          {%- if s.link_label != blank -%}
            <a class="mf-trail__link" href="{{ s.link_url | default: routes.all_products_collection_url }}">{{ s.link_label | escape }}</a>
          {%- endif -%}
        </div>
      {%- endif -%}
    </div>

    <div class="mf-trail__row mf-trail__row--bottom">
      {%- if s.now_label != blank and first_title != blank -%}
        <a class="mf-trail__now" href="{{ first_url }}">
          <i class="mf-trail__now-dot" aria-hidden="true"></i>
          <span class="mf-trail__now-label">{{ s.now_label | escape }}</span>
          <span class="mf-trail__now-title">{{ first_title }}</span>
          <span class="mf-trail__now-price">{{ first_price }}</span>
          <span class="mf-trail__now-arrow" aria-hidden="true">↗</span>
        </a>
      {%- else -%}
        <span></span>
      {%- endif -%}
      {%- if s.proof != blank -%}
        <p class="mf-trail__proof">
          {%- if s.show_stars -%}<span class="mf-trail__stars" aria-hidden="true">★★★★★</span>{%- endif -%}
          <span class="mf-trail__proof-text">{{ s.proof | escape }}</span>
        </p>
      {%- endif -%}
    </div>
  </div>

  <div class="mf-trail__ghost" aria-hidden="true">
    <i></i>
    <span class="mf-trail__ghost-label">{{ s.hint_desktop | escape }}</span>
  </div>
</mf-cursor-trail-hero>

{% stylesheet %}
  /* Image trail hero. As the pointer travels, product images are dropped
     along its path from a pool of reused elements: each one pops in with a
     little scale and rotation, holds, then fades and falls away. Images are
     only ever moved with transform/opacity (Web Animations API). Without
     JavaScript, or with reduced motion, the pool settles into a still
     composition around the headline. */

  .mf-trail {
    position: relative;
    isolation: isolate;
    display: block;
    overflow: hidden;
    background: var(--mf-trail-bg, #0d0c0b);
    color: var(--mf-trail-fg, #f3efe8);
    font-family: inherit;
    touch-action: pan-y;
  }
  .mf-trail *,
  .mf-trail *::before,
  .mf-trail *::after {
    box-sizing: border-box;
  }

  /* ---- atmosphere: accent glow, vignette, fine grain ---- */
  .mf-trail__bg {
    position: absolute;
    z-index: 0;
    inset: 0;
    pointer-events: none;
    background:
      radial-gradient(38% 34% at 50% 48%, color-mix(in srgb, var(--mf-trail-accent, #ff5a24) 15%, transparent), transparent 72%),
      radial-gradient(120% 95% at 50% 50%, transparent 55%, color-mix(in srgb, #000 45%, transparent) 100%),
      radial-gradient(color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 7%, transparent) 0.6px, transparent 1px) 0 0 / 3px 3px;
  }
  .mf-trail__marks {
    position: absolute;
    z-index: 0;
    inset: 14px;
    pointer-events: none;
    opacity: 0.42;
    --mf-trail-mark: linear-gradient(var(--mf-trail-fg, #f3efe8), var(--mf-trail-fg, #f3efe8));
    background:
      var(--mf-trail-mark) left 0 top 5px / 11px 1px no-repeat,
      var(--mf-trail-mark) left 5px top 0 / 1px 11px no-repeat,
      var(--mf-trail-mark) right 0 top 5px / 11px 1px no-repeat,
      var(--mf-trail-mark) right 5px top 0 / 1px 11px no-repeat,
      var(--mf-trail-mark) left 0 bottom 5px / 11px 1px no-repeat,
      var(--mf-trail-mark) left 5px bottom 0 / 1px 11px no-repeat,
      var(--mf-trail-mark) right 0 bottom 5px / 11px 1px no-repeat,
      var(--mf-trail-mark) right 5px bottom 0 / 1px 11px no-repeat;
  }

  /* ---- the trail ---- */
  .mf-trail__layer {
    position: absolute;
    z-index: 0;
    inset: 0;
    overflow: hidden;
    pointer-events: none;
  }
  .mf-trail__item {
    position: absolute;
    left: 0;
    top: 0;
    width: var(--mf-trail-w, 220px);
    aspect-ratio: 4 / 5;
    overflow: hidden;
    border-radius: var(--mf-trail-radius, 6px);
    background: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 10%, var(--mf-trail-bg, #0d0c0b));
    box-shadow:
      0 30px 60px -24px rgba(0, 0, 0, 0.7),
      0 0 0 1px color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 10%, transparent);
    opacity: 0;
    backface-visibility: hidden;
    will-change: transform, opacity;
  }
  .mf-trail__item::after {
    content: attr(data-n);
    position: absolute;
    left: 10px;
    bottom: 9px;
    color: #fff;
    font: 600 10px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.08em;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.45);
    opacity: 0.85;
  }
  .mf-trail__img {
    display: block;
    width: 100%;
    height: 100%;
    max-width: none;
    margin: 0;
    object-fit: cover;
  }

  /* Still composition: no JavaScript yet, or reduced motion. */
  mf-cursor-trail-hero:not(:defined) .mf-trail__item,
  .mf-trail.is-still .mf-trail__item {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) rotate(var(--mf-trail-r, 0deg)) scale(var(--mf-trail-s, 1));
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(-n + 6),
  .mf-trail.is-still .mf-trail__item:nth-child(-n + 6) {
    opacity: 1;
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(1),
  .mf-trail.is-still .mf-trail__item:nth-child(1) {
    left: 12%;
    top: 34%;
    --mf-trail-r: -8deg;
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(2),
  .mf-trail.is-still .mf-trail__item:nth-child(2) {
    left: 88%;
    top: 30%;
    --mf-trail-r: 7deg;
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(3),
  .mf-trail.is-still .mf-trail__item:nth-child(3) {
    left: 20%;
    top: 80%;
    --mf-trail-r: 5deg;
    --mf-trail-s: 0.78;
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(4),
  .mf-trail.is-still .mf-trail__item:nth-child(4) {
    left: 81%;
    top: 79%;
    --mf-trail-r: -6deg;
    --mf-trail-s: 0.82;
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(5),
  .mf-trail.is-still .mf-trail__item:nth-child(5) {
    left: 31%;
    top: 12%;
    --mf-trail-r: 4deg;
    --mf-trail-s: 0.62;
  }
  mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(6),
  .mf-trail.is-still .mf-trail__item:nth-child(6) {
    left: 70%;
    top: 11%;
    --mf-trail-r: -5deg;
    --mf-trail-s: 0.58;
  }

  /* ---- art tiles: the designed fallback when no images are set ---- */
  .mf-trail__art {
    position: absolute;
    inset: 0;
    display: block;
  }
  .mf-trail__art::before,
  .mf-trail__art::after {
    content: "";
    position: absolute;
  }
  /* 1 · amber dropper */
  .mf-trail__art--1 {
    background:
      radial-gradient(46% 7% at 50% 80%, rgba(0, 0, 0, 0.28), transparent 70%),
      radial-gradient(70% 50% at 50% 30%, rgba(255, 255, 255, 0.6), transparent 70%),
      linear-gradient(180deg, #f4e4d2, #e3c2a2);
  }
  .mf-trail__art--1::before {
    left: 36%;
    width: 28%;
    top: 40%;
    height: 40%;
    border-radius: 18% / 12%;
    background: linear-gradient(90deg, #a8611f, #e7a35c 38%, #d38a3e 70%, #9a5419);
    box-shadow: inset 6px 0 0 -2px rgba(255, 255, 255, 0.35);
  }
  .mf-trail__art--1::after {
    left: 44%;
    width: 12%;
    top: 22%;
    height: 19%;
    border-radius: 40% 40% 8% 8%;
    background: linear-gradient(90deg, #15130f, #3b352d 45%, #15130f);
  }
  /* 2 · sage jar */
  .mf-trail__art--2 {
    background:
      radial-gradient(50% 7% at 50% 78%, rgba(0, 0, 0, 0.22), transparent 70%),
      radial-gradient(70% 50% at 50% 28%, rgba(255, 255, 255, 0.55), transparent 70%),
      linear-gradient(180deg, #e2e9dd, #bfcfb8);
  }
  .mf-trail__art--2::before {
    left: 22%;
    width: 56%;
    top: 52%;
    height: 26%;
    border-radius: 14%;
    background: linear-gradient(90deg, #d9d3c4, #f6f3ec 40%, #ece6da 70%, #cfc8b7);
  }
  .mf-trail__art--2::after {
    left: 20%;
    width: 60%;
    top: 43%;
    height: 11%;
    border-radius: 12px;
    background: linear-gradient(90deg, #45573f, #6f8566 45%, #45573f);
  }
  /* 3 · ember candle */
  .mf-trail__art--3 {
    background:
      radial-gradient(30% 22% at 50% 34%, rgba(255, 196, 102, 0.75), transparent 70%),
      radial-gradient(50% 7% at 50% 80%, rgba(0, 0, 0, 0.4), transparent 70%),
      linear-gradient(180deg, #3a2a20, #1d1510);
  }
  .mf-trail__art--3::before {
    left: 26%;
    width: 48%;
    top: 46%;
    height: 34%;
    border-radius: 10%;
    background: linear-gradient(90deg, #8e4523, #c9774a 40%, #b5643c 70%, #7d3a1c);
  }
  .mf-trail__art--3::after {
    left: 46.5%;
    width: 7%;
    top: 31%;
    height: 11%;
    border-radius: 50% 50% 50% 50% / 64% 64% 36% 36%;
    background: radial-gradient(circle at 50% 70%, #fff6d6, #ffb347 60%, #ff7a1a);
    box-shadow: 0 0 22px 6px rgba(255, 170, 70, 0.55);
  }
  /* 4 · lilac sphere */
  .mf-trail__art--4 {
    background:
      radial-gradient(40% 6% at 50% 76%, rgba(40, 20, 80, 0.3), transparent 70%),
      linear-gradient(160deg, #ece6f6, #c9bdea);
  }
  .mf-trail__art--4::before {
    left: 24%;
    width: 52%;
    top: 30%;
    aspect-ratio: 1;
    border-radius: 50%;
    background: radial-gradient(circle at 34% 28%, #fff, #d8ccf4 22%, #9b86d4 62%, #5a4592);
  }
  /* 5 · sky pump */
  .mf-trail__art--5 {
    background:
      radial-gradient(40% 7% at 50% 80%, rgba(10, 40, 80, 0.22), transparent 70%),
      radial-gradient(70% 50% at 50% 28%, rgba(255, 255, 255, 0.6), transparent 70%),
      linear-gradient(180deg, #e3edf6, #bcd2e6);
  }
  .mf-trail__art--5::before {
    left: 34%;
    width: 32%;
    top: 34%;
    height: 46%;
    border-radius: 22% / 12%;
    background: linear-gradient(90deg, #dfe8f0, #ffffff 40%, #f3f7fb 70%, #cfdbe6);
  }
  .mf-trail__art--5::after {
    left: 40%;
    width: 22%;
    top: 21%;
    height: 14%;
    border-radius: 6px 26px 4px 4px;
    background: linear-gradient(90deg, #22568c, #3a7cc0 50%, #22568c);
  }
  /* 6 · blush tube */
  .mf-trail__art--6 {
    background:
      radial-gradient(40% 7% at 50% 82%, rgba(120, 40, 30, 0.2), transparent 70%),
      linear-gradient(180deg, #f8e7e3, #ecc8c1);
  }
  .mf-trail__art--6::before {
    left: 36%;
    width: 28%;
    top: 18%;
    height: 54%;
    border-radius: 6px 6px 3px 3px;
    background: linear-gradient(90deg, #d99a91, #f6cfc8 40%, #f2c4bd 70%, #cc8a80);
    clip-path: polygon(0 0, 100% 0, 88% 100%, 12% 100%);
  }
  .mf-trail__art--6::after {
    left: 39%;
    width: 22%;
    top: 71%;
    height: 11%;
    border-radius: 4px 4px 10px 10px;
    background: linear-gradient(90deg, #e8ddd6, #fffaf6 50%, #e8ddd6);
  }
  /* 7 · olive oil */
  .mf-trail__art--7 {
    background:
      radial-gradient(40% 7% at 50% 82%, rgba(30, 30, 10, 0.26), transparent 70%),
      radial-gradient(70% 50% at 50% 28%, rgba(255, 255, 255, 0.45), transparent 70%),
      linear-gradient(180deg, #edecdf, #d1cfb6);
  }
  .mf-trail__art--7::before {
    left: 33%;
    width: 34%;
    top: 36%;
    height: 46%;
    border-radius: 30% 30% 10% 10% / 16% 16% 6% 6%;
    background: linear-gradient(90deg, #3f4326, #7a8050 40%, #6f7446 70%, #3a3d22);
  }
  .mf-trail__art--7::after {
    left: 44%;
    width: 12%;
    top: 20%;
    height: 17%;
    border-radius: 4px;
    background: linear-gradient(90deg, #cbbf98, #efe6c9 50%, #cbbf98);
  }
  /* 8 · ink compact */
  .mf-trail__art--8 {
    background:
      radial-gradient(55% 8% at 50% 72%, rgba(0, 0, 0, 0.35), transparent 70%),
      linear-gradient(170deg, #dfe3ea, #aeb7c6);
  }
  .mf-trail__art--8::before {
    left: 18%;
    width: 64%;
    top: 50%;
    height: 18%;
    border-radius: 50% / 40%;
    background: linear-gradient(180deg, #2b3142, #0e1119);
  }
  .mf-trail__art--8::after {
    left: 18%;
    width: 64%;
    top: 42%;
    height: 17%;
    border-radius: 50%;
    background: radial-gradient(60% 50% at 50% 46%, #e8d3a4, #c9a86a 60%, #8d7340);
  }

  /* ---- content frame ---- */
  /* No z-index here on purpose: the headline blends with the trail below,
     and a stacking context on any ancestor would cut it off from it. */
  .mf-trail__frame {
    position: relative;
    display: grid;
    grid-template-rows: auto 1fr auto;
    gap: 28px;
    width: 100%;
    min-height: var(--mf-trail-min-h, 760px);
    padding: var(--mf-trail-pad, 36px) clamp(24px, 4vw, 52px);
    pointer-events: none;
  }
  .mf-trail__frame a,
  .mf-trail__frame p,
  .mf-trail__text {
    pointer-events: auto;
  }
  .mf-trail__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 14px 24px;
    min-width: 0;
  }
  .mf-trail__row--top {
    grid-row: 1;
  }
  .mf-trail__center {
    grid-row: 2;
    align-self: center;
    justify-self: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    max-width: 1100px;
    text-align: center;
  }
  .mf-trail__row--bottom {
    grid-row: 3;
    align-items: flex-end;
  }

  .mf-trail__eyebrow,
  .mf-trail__count {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    margin: 0;
    color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 72%, transparent);
    font: 500 11px/1.3 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.16em;
    text-transform: uppercase;
  }
  .mf-trail__eyebrow span {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .mf-trail__eyebrow i {
    flex: none;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--mf-trail-accent, #ff5a24);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--mf-trail-accent, #ff5a24) 24%, transparent);
  }
  .mf-trail__count {
    flex: none;
    gap: 8px;
    font-variant-numeric: tabular-nums;
  }
  .mf-trail__count b {
    color: var(--mf-trail-fg, #f3efe8);
    font-weight: 700;
  }

  .mf-trail__title {
    margin: 0;
    color: var(--mf-trail-fg, #f3efe8);
    font-family: inherit;
    font-size: clamp(56px, 10.4vw, 164px);
    font-weight: 600;
    line-height: 0.9;
    letter-spacing: -0.05em;
    text-transform: none;
    text-wrap: balance;
  }
  /* Invert: the headline is drawn as the inverse of whatever sits beneath
     it, so it stays legible over any product image. */
  .mf-trail--blend .mf-trail__title {
    color: #ffffff;
    mix-blend-mode: difference;
  }
  /* Supporting copy gets the same treatment, softened: a translucent
     white in difference mode reads light on the background and dark on a
     pale image. */
  .mf-trail--blend .mf-trail__text,
  .mf-trail--blend .mf-trail__link,
  .mf-trail--blend .mf-trail__proof-text,
  .mf-trail--blend .mf-trail__eyebrow span,
  .mf-trail--blend .mf-trail__count {
    color: rgba(255, 255, 255, 0.76);
    mix-blend-mode: difference;
  }
  .mf-trail--blend .mf-trail__link,
  .mf-trail--blend .mf-trail__count b {
    color: #ffffff;
  }
  .mf-trail--blend .mf-trail__link {
    text-decoration-color: rgba(255, 255, 255, 0.45);
  }
  .mf-trail__line {
    display: block;
  }
  .mf-trail__line-in {
    display: inline-block;
    padding: 0 0.04em;
  }
  .mf-trail--accent-serif .mf-trail__line--accent {
    font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
    font-style: italic;
    font-weight: 400;
    letter-spacing: -0.035em;
  }
  .mf-trail--accent-outline .mf-trail__line--accent {
    color: transparent;
    -webkit-text-stroke: 1.5px var(--mf-trail-fg, #f3efe8);
  }
  .mf-trail--blend.mf-trail--accent-outline .mf-trail__line--accent {
    -webkit-text-stroke-color: #ffffff;
  }

  .mf-trail__text {
    max-width: 46ch;
    margin-top: clamp(20px, 2.4vw, 30px);
    color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 74%, transparent);
    font-size: 17px;
    line-height: 1.55;
    letter-spacing: 0;
    text-wrap: pretty;
  }
  .mf-trail__text p {
    margin: 0;
    color: inherit;
    font-size: inherit;
    line-height: inherit;
    letter-spacing: inherit;
  }

  .mf-trail__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 14px 26px;
    margin-top: 30px;
  }
  .mf-trail__button {
    display: inline-flex;
    align-items: center;
    gap: 12px;
    min-height: 52px;
    margin: 0;
    padding: 0 26px;
    border-radius: 999px;
    background: var(--mf-trail-fg, #f3efe8);
    color: var(--mf-trail-bg, #0d0c0b);
    font-family: inherit;
    font-size: 15px;
    font-weight: 600;
    line-height: 1;
    letter-spacing: 0;
    text-decoration: none;
    text-transform: none;
    box-shadow: 0 14px 44px -14px color-mix(in srgb, var(--mf-trail-accent, #ff5a24) 80%, transparent);
    transition: transform 0.25s ease, box-shadow 0.25s ease;
  }
  .mf-trail__button span {
    transition: transform 0.25s ease;
  }
  .mf-trail__button:hover {
    transform: translateY(-2px);
    box-shadow: 0 18px 52px -12px color-mix(in srgb, var(--mf-trail-accent, #ff5a24) 95%, transparent);
  }
  .mf-trail__button:hover span {
    transform: translateX(3px);
  }
  .mf-trail__link {
    margin: 0;
    padding: 8px 0;
    color: var(--mf-trail-fg, #f3efe8);
    font-family: inherit;
    font-size: 15px;
    font-weight: 500;
    line-height: 1.2;
    letter-spacing: 0;
    text-decoration: underline;
    text-decoration-color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 40%, transparent);
    text-decoration-thickness: 1px;
    text-underline-offset: 6px;
    text-transform: none;
    transition: text-decoration-color 0.25s ease;
  }
  .mf-trail__link:hover {
    text-decoration-color: var(--mf-trail-fg, #f3efe8);
  }

  /* "Now showing" chip: a real link to the product last dropped. */
  .mf-trail__now {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    max-width: 100%;
    min-height: 46px;
    margin: 0;
    padding: 6px 18px 6px 14px;
    border: 1px solid color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 16%, transparent);
    border-radius: 999px;
    background: color-mix(in srgb, var(--mf-trail-bg, #0d0c0b) 62%, transparent);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    color: var(--mf-trail-fg, #f3efe8);
    font-family: inherit;
    font-size: 14px;
    line-height: 1.2;
    letter-spacing: 0;
    text-decoration: none;
    text-transform: none;
    transition: border-color 0.25s ease;
  }
  .mf-trail__now:hover {
    border-color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 45%, transparent);
  }
  .mf-trail__now-dot {
    flex: none;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--mf-trail-accent, #ff5a24);
    box-shadow: 0 0 12px var(--mf-trail-accent, #ff5a24);
  }
  .mf-trail__now-label {
    flex: none;
    color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 58%, transparent);
    font: 500 10px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    letter-spacing: 0.16em;
    text-transform: uppercase;
  }
  .mf-trail__now-title {
    min-width: 0;
    overflow: hidden;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .mf-trail__now-price {
    flex: none;
    color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 64%, transparent);
    font-variant-numeric: tabular-nums;
  }
  .mf-trail__now-price:empty {
    display: none;
  }
  .mf-trail__now-arrow {
    flex: none;
    color: var(--mf-trail-accent, #ff5a24);
    transition: transform 0.25s ease;
  }
  .mf-trail__now:hover .mf-trail__now-arrow {
    transform: translate(2px, -2px);
  }

  .mf-trail__proof {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    color: color-mix(in srgb, var(--mf-trail-fg, #f3efe8) 66%, transparent);
    font-size: 13px;
    line-height: 1.3;
    letter-spacing: 0;
    text-transform: none;
  }
  .mf-trail__stars {
    color: var(--mf-trail-accent, #ff5a24);
    font-size: 13px;
    letter-spacing: 2px;
  }

  .mf-trail__button:focus-visible,
  .mf-trail__link:focus-visible,
  .mf-trail__now:focus-visible {
    outline: 2px solid var(--mf-trail-accent, #ff5a24);
    outline-offset: 4px;
  }

  /* ---- self-playing cursor (idle demo) ---- */
  .mf-trail__ghost {
    position: absolute;
    z-index: 3;
    left: 0;
    top: 0;
    display: flex;
    align-items: flex-start;
    gap: 4px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.45s ease;
    will-change: transform;
  }
  .mf-trail.is-ghost .mf-trail__ghost {
    opacity: 1;
  }
  .mf-trail__ghost i {
    flex: none;
    width: 18px;
    height: 20px;
    background: var(--mf-trail-accent, #ff5a24);
    clip-path: polygon(0 0, 100% 64%, 54% 68%, 30% 100%);
    filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.4));
  }
  .mf-trail__ghost-label {
    margin-top: 16px;
    padding: 7px 12px;
    border-radius: 4px 999px 999px 999px;
    background: var(--mf-trail-accent, #ff5a24);
    color: #ffffff;
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    letter-spacing: 0;
    white-space: nowrap;
    box-shadow: 0 8px 20px -8px rgba(0, 0, 0, 0.5);
  }

  @media screen and (max-width: 749px) {
    /* Phones: the top of the hero becomes the playground the trail draws
       in, and the copy is anchored underneath it. */
    .mf-trail__frame {
      grid-template-rows: auto minmax(230px, 1fr) auto auto;
      gap: 22px;
      min-height: max(700px, calc(var(--mf-trail-min-h, 760px) * 0.95));
      padding: max(18px, calc(var(--mf-trail-pad, 36px) * 0.6)) 20px max(22px, calc(var(--mf-trail-pad, 36px) * 0.7));
    }
    .mf-trail__center {
      grid-row: 3;
      align-self: end;
      width: 100%;
    }
    .mf-trail__row--bottom {
      grid-row: 4;
      flex-direction: column;
      align-items: stretch;
      gap: 14px;
    }
    .mf-trail__marks {
      inset: 8px;
    }
    .mf-trail__eyebrow,
    .mf-trail__count {
      font-size: 10px;
      letter-spacing: 0.12em;
    }
    .mf-trail__title {
      font-size: clamp(46px, 13.6vw, 84px);
      line-height: 0.92;
    }
    .mf-trail__text {
      margin-top: 16px;
      font-size: 15px;
    }
    .mf-trail__actions {
      margin-top: 22px;
      gap: 10px 20px;
    }
    .mf-trail__button {
      min-height: 50px;
      padding: 0 22px;
    }
    .mf-trail__now {
      justify-content: flex-start;
      gap: 8px;
      width: 100%;
      padding-left: 12px;
    }
    .mf-trail__now-label {
      letter-spacing: 0.1em;
    }
    .mf-trail__now-arrow {
      margin-left: auto;
    }
    .mf-trail__proof {
      justify-content: center;
      font-size: 12px;
    }
    .mf-trail__item {
      width: calc(var(--mf-trail-w, 220px) * 0.6);
    }
    .mf-trail__ghost-label {
      font-size: 11px;
    }
    mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(n + 4),
    .mf-trail.is-still .mf-trail__item:nth-child(n + 4) {
      opacity: 0;
    }
    mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(1),
    .mf-trail.is-still .mf-trail__item:nth-child(1) {
      left: 25%;
      top: 23%;
      --mf-trail-r: -10deg;
      --mf-trail-s: 0.92;
    }
    mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(2),
    .mf-trail.is-still .mf-trail__item:nth-child(2) {
      left: 75%;
      top: 22%;
      --mf-trail-r: 9deg;
      --mf-trail-s: 0.92;
    }
    mf-cursor-trail-hero:not(:defined) .mf-trail__item:nth-child(3),
    .mf-trail.is-still .mf-trail__item:nth-child(3) {
      left: 50%;
      top: 19%;
      --mf-trail-r: -1deg;
      --mf-trail-s: 1;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .mf-trail__ghost {
      display: none;
    }
    .mf-trail__button,
    .mf-trail__button span,
    .mf-trail__now-arrow,
    .mf-trail__link,
    .mf-trail__now {
      transition: none;
    }
    .mf-trail__button:hover,
    .mf-trail__button:hover span,
    .mf-trail__now:hover .mf-trail__now-arrow {
      transform: none;
    }
  }

{% endstylesheet %}

{% javascript %}
  if (!customElements.get('mf-cursor-trail-hero')) {
    customElements.define('mf-cursor-trail-hero', class extends HTMLElement {
      connectedCallback() {
        this.items = Array.prototype.slice.call(this.querySelectorAll('.mf-trail__item'));
        if (!this.items.length) return;

        const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        if (reduce || typeof this.items[0].animate !== 'function') {
          this.classList.add('is-still');
          return;
        }
        this.classList.add('is-live');

        this.preview = Boolean(window.__MF_PREVIEW__);
        this.autoplay = this.preview || this.dataset.autoplay === 'true';
        this.spacingBase = Number(this.dataset.spacing) || 130;
        this.life = Number(this.dataset.life) || 1600;
        this.ghost = this.querySelector('.mf-trail__ghost');
        this.countEl = this.querySelector('.mf-trail__count-num');
        this.now = this.querySelector('.mf-trail__now');
        this.nowTitle = this.querySelector('.mf-trail__now-title');
        this.nowPrice = this.querySelector('.mf-trail__now-price');
        this.nowDot = this.querySelector('.mf-trail__now-dot');
        this.next = 0;
        this.z = 1;
        this.last = null;
        this.down = false;
        this.visible = false;
        this.raf = 0;
        this.prevT = 0;
        this.theta = -2.54;
        this.prefilled = false;
        this.userAt = -1e9;
        // First ghost run starts right after the headline lands; later runs
        // wait for the visitor to be idle for a while.
        this.idleDelay = this.preview ? 0 : 500;
        this.ghostOn = false;

        const label = this.querySelector('.mf-trail__ghost-label');
        if (label && window.matchMedia('(pointer: coarse)').matches && this.dataset.hintTouch) {
          label.textContent = this.dataset.hintTouch;
        }

        this.measure = () => {
          this.w = this.clientWidth;
          this.h = this.clientHeight;
          this.iw = this.items[0].offsetWidth;
          this.ih = this.items[0].offsetHeight;
          this.mobile = this.w < 750;
          this.spacing = this.mobile ? this.spacingBase * 0.45 : this.spacingBase;
        };
        this.measure();
        if ('ResizeObserver' in window) {
          this.ro = new ResizeObserver(this.measure);
          this.ro.observe(this);
        }

        this.onMove = (event) => {
          if (event.pointerType === 'touch' && !this.down) return;
          this.user(event);
        };
        this.onDown = (event) => {
          if (event.pointerType !== 'touch') return;
          this.down = true;
          this.last = null;
          this.user(event);
        };
        this.onUp = () => {
          this.down = false;
        };
        this.onLeave = () => {
          this.last = null;
        };
        this.addEventListener('pointermove', this.onMove, { passive: true });
        this.addEventListener('pointerdown', this.onDown, { passive: true });
        this.addEventListener('pointerup', this.onUp, { passive: true });
        this.addEventListener('pointercancel', this.onUp, { passive: true });
        this.addEventListener('pointerleave', this.onLeave, { passive: true });

        if (this.autoplay && this.w) {
          const p0 = this.pathAt(this.theta);
          if (this.ghost) {
            this.ghost.style.transition = 'none';
            this.ghost.style.transform = 'translate3d(' + p0.x.toFixed(1) + 'px,' + p0.y.toFixed(1) + 'px,0)';
          }
          this.setGhost(true);
          if (this.ghost) {
            void this.ghost.offsetWidth;
            this.ghost.style.transition = '';
          }
        }

        this.loop = this.loop.bind(this);
        if ('IntersectionObserver' in window) {
          this.io = new IntersectionObserver((entries) => {
            entries.forEach((entry) => {
              this.visible = entry.isIntersecting;
              if (this.visible) this.start();
            });
          }, { rootMargin: '80px 0px' });
          this.io.observe(this);
        } else {
          this.visible = true;
          this.start();
        }
      }

      disconnectedCallback() {
        if (this.io) this.io.disconnect();
        if (this.ro) this.ro.disconnect();
        if (this.raf) cancelAnimationFrame(this.raf);
        if (this.timer) clearTimeout(this.timer);
        this.raf = 0;
        if (this.onMove) {
          this.removeEventListener('pointermove', this.onMove);
          this.removeEventListener('pointerdown', this.onDown);
          this.removeEventListener('pointerup', this.onUp);
          this.removeEventListener('pointercancel', this.onUp);
          this.removeEventListener('pointerleave', this.onLeave);
        }
        (this.items || []).forEach((el) => {
          if (el.mfAnim) el.mfAnim.cancel();
        });
      }

      user(event) {
        const rect = this.getBoundingClientRect();
        this.userAt = performance.now();
        this.idleDelay = this.preview ? 1600 : 3500;
        if (this.ghostOn) this.setGhost(false);
        this.track(event.clientX - rect.left, event.clientY - rect.top, 0);
      }

      track(x, y, age) {
        if (!this.last) {
          this.spawn(x, y, 0, age);
          this.last = { x: x, y: y };
          return;
        }
        const dx = x - this.last.x;
        const dy = y - this.last.y;
        if (dx * dx + dy * dy < this.spacing * this.spacing) return;
        this.spawn(x, y, dx, age);
        this.last = { x: x, y: y };
      }

      spawn(x, y, dx, age) {
        const el = this.items[this.next % this.items.length];
        this.next += 1;
        if (el.mfAnim) el.mfAnim.cancel();
        this.z += 1;
        el.style.zIndex = this.z;

        const tx = x - this.iw / 2;
        const ty = y - this.ih / 2;
        const lean = Math.max(-14, Math.min(14, dx * 0.12));
        const rot = lean + (Math.random() - 0.5) * 10;
        const fall = this.ih * (0.28 + Math.random() * 0.22);
        const drift = dx * 0.18;
        const at = (px, py, sc, r) => 'translate3d(' + px.toFixed(1) + 'px,' + py.toFixed(1) + 'px,0) rotate(' + r.toFixed(2) + 'deg) scale(' + sc + ')';

        el.mfAnim = el.animate([
          { transform: at(tx, ty, 0.3, rot * 1.8), opacity: 0, easing: 'cubic-bezier(0.2, 1.4, 0.4, 1)' },
          { transform: at(tx, ty, 1, rot), opacity: 1, offset: 0.16 },
          { transform: at(tx + drift * 0.3, ty + fall * 0.12, 0.98, rot), opacity: 1, offset: 0.56, easing: 'cubic-bezier(0.5, 0, 0.75, 0.4)' },
          { transform: at(tx + drift, ty + fall, 0.84, rot + (rot >= 0 ? 7 : -7)), opacity: 0 }
        ], { duration: this.life, fill: 'none' });
        // Pin the start time instead of waiting for the next frame, so the
        // timing holds even when frames are throttled (background tabs,
        // headless captures) and prefilled images can be back-dated.
        el.mfAnim.startTime = performance.now() - Math.min(age || 0, this.life);

        const n = el.dataset.n;
        if (this.countEl && n) this.countEl.textContent = n;
        const title = el.dataset.title;
        if (this.now && title) {
          if (this.nowTitle) this.nowTitle.textContent = title;
          if (this.nowPrice) this.nowPrice.textContent = el.dataset.price || '';
          if (el.dataset.url) this.now.setAttribute('href', el.dataset.url);
          if (this.nowDot) this.nowDot.animate([{ transform: 'scale(2.2)', opacity: 0.4 }, { transform: 'scale(1)', opacity: 1 }], { duration: 420, easing: 'ease-out' });
        }
      }

      setGhost(on) {
        this.ghostOn = on;
        this.classList.toggle('is-ghost', on);
        if (on && this.userAt > 0) this.last = null;
      }

      start() {
        if (!this.autoplay || this.raf) return;
        // Open on a trail already in flight instead of an empty hero: replay
        // the last moments of the idle path with back-dated animations.
        if (!this.prefilled && this.w) {
          this.prefilled = true;
          const step = 16;
          const steps = Math.floor(this.life / step);
          const period = this.period();
          let theta = this.theta - (steps * step / period) * Math.PI * 2;
          this.last = null;
          for (let k = 0; k <= steps; k += 1) {
            const p = this.pathAt(theta);
            this.track(p.x, p.y, (steps - k) * step);
            theta += (step / period) * Math.PI * 2;
          }
        }
        this.prevT = 0;
        this.schedule();
      }

      schedule() {
        // The library preview ticks on timers so headless captures and
        // background iframes keep drawing; real pages use animation frames.
        if (this.preview) {
          this.raf = 1;
          this.timer = setTimeout(() => this.loop(performance.now()), 16);
        } else {
          this.raf = requestAnimationFrame(this.loop);
        }
      }

      period() {
        return this.mobile ? 4800 : 6400;
      }

      // A figure-eight (1:2 Lissajous) around the headline; on phones it
      // stays in the playground above the copy.
      pathAt(theta) {
        const cx = this.w * 0.5;
        const cy = this.h * (this.mobile ? 0.27 : 0.5);
        const ax = this.w * (this.mobile ? 0.33 : 0.37);
        const ay = this.h * (this.mobile ? 0.12 : 0.3);
        return { x: cx + ax * Math.sin(theta), y: cy + ay * Math.sin(theta * 2) };
      }

      loop(t) {
        this.raf = 0;
        if (!this.visible) {
          if (this.ghostOn) this.setGhost(false);
          return;
        }
        const dt = this.prevT ? Math.min(50, t - this.prevT) : 16;
        this.prevT = t;
        const idle = performance.now() - this.userAt > this.idleDelay;
        if (idle && !this.down && this.w) {
          if (!this.ghostOn) this.setGhost(true);
          this.theta += (dt / this.period()) * Math.PI * 2;
          const p = this.pathAt(this.theta);
          this.track(p.x, p.y, 0);
          if (this.ghost) this.ghost.style.transform = 'translate3d(' + p.x.toFixed(1) + 'px,' + p.y.toFixed(1) + 'px,0)';
        }
        this.schedule();
      }
    });
  }
{% endjavascript %}

{% schema %}
{
  "name": "EX · Cursor trail hero",
  "tag": "section",
  "class": "mf-section",
  "disabled_on": {
    "groups": ["header", "footer"]
  },
  "settings": [
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "eyebrow",
      "label": "Eyebrow",
      "default": "The autumn index · new season"
    },
    {
      "type": "inline_richtext",
      "id": "heading",
      "label": "Heading",
      "default": "Every piece,"
    },
    {
      "type": "inline_richtext",
      "id": "heading_accent",
      "label": "Heading second line",
      "default": "one move away."
    },
    {
      "type": "select",
      "id": "accent_style",
      "label": "Second line style",
      "options": [
        { "value": "plain", "label": "Plain" },
        { "value": "serif", "label": "Italic serif" },
        { "value": "outline", "label": "Outline" }
      ],
      "default": "serif"
    },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Heading HTML tag",
      "info": "Use H1 only when this is the first section of the page.",
      "options": [
        { "value": "h1", "label": "H1" },
        { "value": "h2", "label": "H2" }
      ],
      "default": "h2"
    },
    {
      "type": "richtext",
      "id": "text",
      "label": "Text",
      "default": "<p>Move across the page and the collection follows you. When something catches your eye, it is one click away.</p>"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Shop the collection"
    },
    {
      "type": "url",
      "id": "button_link",
      "label": "Button link",
      "info": "Leave empty to link to the collection chosen below."
    },
    {
      "type": "text",
      "id": "link_label",
      "label": "Secondary link label",
      "default": "New arrivals"
    },
    {
      "type": "url",
      "id": "link_url",
      "label": "Secondary link"
    },
    {
      "type": "text",
      "id": "proof",
      "label": "Social proof line",
      "default": "Rated 4.9/5 by 2,340 customers"
    },
    {
      "type": "checkbox",
      "id": "show_stars",
      "label": "Show stars before the proof line",
      "default": true
    },
    {
      "type": "text",
      "id": "now_label",
      "label": "Now showing label",
      "info": "Links to the product of the last image dropped. Leave empty to hide.",
      "default": "Now showing"
    },
    {
      "type": "text",
      "id": "counter_label",
      "label": "Counter label",
      "info": "Shown top right with the number of the last image. Leave empty to hide.",
      "default": "Index"
    },
    {
      "type": "header",
      "content": "Trail"
    },
    {
      "type": "collection",
      "id": "collection",
      "label": "Collection",
      "info": "The trail shows this collection's products (up to 16). Trail image blocks, if added, are used instead."
    },
    {
      "type": "select",
      "id": "card_image",
      "label": "Product image",
      "options": [
        { "value": "first", "label": "Main image" },
        { "value": "second", "label": "Second image (often lifestyle)" }
      ],
      "default": "first"
    },
    {
      "type": "range",
      "id": "pool",
      "label": "Images on screen at once",
      "min": 8,
      "max": 16,
      "step": 1,
      "default": 12
    },
    {
      "type": "range",
      "id": "size",
      "label": "Image width",
      "info": "Phones use 60% of this width.",
      "min": 140,
      "max": 320,
      "step": 10,
      "unit": "px",
      "default": 200
    },
    {
      "type": "range",
      "id": "spacing",
      "label": "Distance between images",
      "min": 40,
      "max": 240,
      "step": 10,
      "unit": "px",
      "default": 130
    },
    {
      "type": "range",
      "id": "lifetime",
      "label": "Image lifetime",
      "min": 800,
      "max": 3000,
      "step": 100,
      "unit": "ms",
      "default": 1600
    },
    {
      "type": "range",
      "id": "radius",
      "label": "Image corner radius",
      "min": 0,
      "max": 24,
      "step": 1,
      "unit": "px",
      "default": 6
    },
    {
      "type": "checkbox",
      "id": "blend",
      "label": "Invert the heading over images",
      "info": "The heading is drawn as the inverse of what is beneath it, so it reads over any image. Off: it uses the text color.",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "autoplay",
      "label": "Draw the trail by itself when the visitor is idle",
      "info": "A labelled cursor traces a figure-eight until the visitor moves or touches the hero.",
      "default": true
    },
    {
      "type": "text",
      "id": "hint_desktop",
      "label": "Cursor label (mouse)",
      "default": "Move your cursor"
    },
    {
      "type": "text",
      "id": "hint_touch",
      "label": "Cursor label (touch)",
      "default": "Drag to explore"
    },
    {
      "type": "header",
      "content": "Layout"
    },
    {
      "type": "range",
      "id": "min_height",
      "label": "Minimum height",
      "min": 560,
      "max": 1000,
      "step": 20,
      "unit": "px",
      "default": 760
    },
    {
      "type": "range",
      "id": "padding",
      "label": "Top and bottom spacing",
      "min": 16,
      "max": 96,
      "step": 4,
      "unit": "px",
      "default": 36
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "color_bg",
      "label": "Background",
      "default": "#0d0c0b"
    },
    {
      "type": "color",
      "id": "color_text",
      "label": "Text",
      "default": "#f3efe8"
    },
    {
      "type": "color",
      "id": "color_accent",
      "label": "Accent",
      "default": "#ff5a24"
    }
  ],
  "blocks": [
    {
      "type": "image",
      "name": "Trail image",
      "settings": [
        {
          "type": "image_picker",
          "id": "image",
          "label": "Image",
          "info": "Portrait 4:5 works best."
        },
        {
          "type": "text",
          "id": "title",
          "label": "Label",
          "info": "Shown in the Now showing chip when this image is dropped."
        },
        {
          "type": "url",
          "id": "link",
          "label": "Link"
        }
      ]
    }
  ],
  "max_blocks": 16,
  "presets": [
    {
      "name": "EX · Cursor trail hero"
    }
  ]
}
{% endschema %}
Why it converts

Playful first impression: the catalog reveals itself as the visitor explores, raising dwell time and the odds of a first click.

Specs

File: sections/mf-cursor-trail-hero.liquidIn the theme editor: EX · Cursor trail heroBest on: index, collection, page templatesSettings: 33 · blocks: Trail imageValidated: Shopify Theme Check, no external assets

About this section

Cursor Trail Hero is a Shopify Online Store 2.0 hero section that turns the visitor's cursor into a way of browsing. As the pointer moves, product images drop along its path: each one pops in with a tilt that follows the direction of travel, then fades and falls away. Images come from a collection the merchant picks, using the main or the second product image, or from up to sixteen Trail image blocks with their own label and link. With neither, eight CSS-drawn studio still lifes fill the trail. A Now showing chip links to the product of the last image dropped, with its title and price, and a mono counter numbers each image. The copy sits in the middle: eyebrow, a two-line heading whose second line can be plain, italic serif or outline, rich text, a button that defaults to the chosen collection, a secondary link and a star-rated proof line. The heading can be drawn as the inverse of what lies beneath it, so it stays readable over any image. The theme editor controls image count, width, spacing, lifetime, radius, height and colors. When the visitor is idle, a labelled cursor can trace a figure-eight so the hero is never empty. On phones, a tap or a sideways drag drops images and the copy is anchored below a dedicated playground. With reduced motion, the images settle into a still composition. The conversion principle is a playful first impression: the catalog reveals itself as the visitor explores. One self-contained .liquid file.

Questions

Where do the trail images come from?

Trail image blocks come first: if you add blocks with images, the trail uses only those, with each block's label and link in the Now showing chip. Otherwise it uses up to 16 products from the collection selected under Trail, with the main or second product image. With neither, it shows CSS-drawn still lifes.

How does it work on phones and tablets?

A tap drops an image where the finger lands and a sideways drag draws a trail; vertical swipes still scroll the page. Images shrink to 60% of the desktop width and the copy sits below a dedicated playground at the top of the hero, where the idle cursor plays.

Will it slow down my store?

No library is loaded. A fixed pool of image elements is reused, so the page never grows, and every movement is a transform or opacity change on the GPU. Images are lazy-loaded with responsive widths, and the idle loop stops whenever the section is off-screen.

Is it accessible?

The trail is decorative and hidden from screen readers; the heading, text, button, links and Now showing chip are regular focusable elements with visible focus rings. Visitors who prefer reduced motion get a still composition of the images and no idle cursor.

More sections