{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stack-cards",
  "type": "registry:block",
  "title": "Stack Cards",
  "description": "Sticky cards that stack into a deck as you scroll: each buried card sinks back on a spring and leaves its index tab showing, which you can click to scroll back to it. The card in flight leans with your scroll speed; it is transform and opacity only, with layout read once.",
  "docs": "Stack Cards was added to public/vividsites/stack-cards.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/stack-cards",
  "categories": [
    "ui",
    "cards-layout"
  ],
  "files": [
    {
      "path": "components/vividsites/stack-cards.html",
      "type": "registry:file",
      "target": "public/vividsites/stack-cards.html",
      "content": "<!--\n  vs-stack-cards: sticky cards that stack into a deck as you scroll. Each card pins a little\n  lower than the last, so buried cards leave their index tab showing; as the next card slides\n  over, the one below sinks back (scale, lift, dim) on a spring, and the card in flight leans\n  a few degrees with your scroll speed.\n\n  - Layout is read once (and on resize or prop change); scrolling does transform/opacity writes\n    only, in one rAF that sleeps when the springs settle and while the stack is off screen.\n  - A buried card's tab is a button: click it (or Tab + Enter) to scroll back to that card.\n  - root.vsStackCards.go(i) scrolls to card i (e.g. go(2, 'instant') to open mid-stack).\n  - Each card's copy rises in once when it reaches its pin; the picture drifts as it arrives.\n  - prefers-reduced-motion: cards still stack (it is just sticky), with no scale, lean or reveal.\n\n  CSS variables (on .vs-stack-cards):\n    --vs-stack-cards-top      where the first card pins, from the top of the viewport\n    --vs-stack-cards-offset   how much lower each next card pins = the tab left showing\n    --vs-stack-cards-gap      scroll distance between cards\n    --vs-stack-cards-h        card height\n    --vs-stack-cards-dwell    extra scroll with the finished deck pinned before it leaves\n    --vs-stack-cards-depth    how much a buried card shrinks per card above it (0-0.12)\n    --vs-stack-cards-dim      how dark a buried card gets (0-1)\n    --vs-stack-cards-blur     max blur of buried cards, in px (0 = off; costs GPU)\n    --vs-stack-cards-radius   card radius\n    --vs-stack-cards-paper / -ink / -accent   card colours\n-->\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Gilda+Display&family=Mulish:wght@400;500&display=swap\" rel=\"stylesheet\">\n<style>\n.vs-stack-cards{\n  --vs-stack-cards-gap: 34vh;\n  --vs-stack-cards-paper: #f3ebdc;\n  --vs-stack-cards-ink: #16203a;\n  --vs-stack-cards-accent: #c8412b;\n  --vs-stack-cards-radius: 24px;\n  --vs-stack-cards-depth: .05;\n  --vs-stack-cards-dim: .45;\n  --vs-stack-cards-blur: 0;\n  position: relative; width: min(920px, 100%); margin: 0 auto;\n  font-family: \"Mulish\", system-ui, sans-serif; color: var(--vs-stack-cards-ink);\n}\n/* a spacer after the last card: sticky cards live inside the parent's CONTENT box, so padding would not\n   keep them pinned; this keeps the whole deck (last card included) pinned for --vs-stack-cards-dwell */\n.vs-stack-cards::after{ content: \"\"; display: block; height: calc((var(--vs-stack-cards-n, 4) - 1) * var(--vs-stack-cards-offset, 34px) + var(--vs-stack-cards-dwell, 12vh)); }\n.vs-stack-cards-card{\n  position: sticky; top: calc(var(--vs-stack-cards-top, 64px) + var(--i, 0) * var(--vs-stack-cards-offset, 34px));\n  height: var(--vs-stack-cards-h, min(62vh, 520px));\n}\n/* spacing sits ABOVE each next card: a margin below a sticky card would unstick it early */\n.vs-stack-cards-card + .vs-stack-cards-card{ margin-top: var(--vs-stack-cards-gap); }\n.vs-stack-cards-inner{\n  position: relative; height: 100%; border-radius: var(--vs-stack-cards-radius); overflow: hidden; isolation: isolate;\n  display: flex; flex-direction: column;\n  background: var(--vs-stack-cards-paper); transform-origin: 50% 0; will-change: transform;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,.75), 0 0 0 .5px rgba(8,14,28,.35), 0 1px 2px rgba(5,10,22,.3), 0 18px 36px -18px rgba(3,8,20,.55), 0 50px 90px -40px rgba(3,8,20,.8);\n  translate: 0 0; transition: translate .5s cubic-bezier(.16,1,.3,1);\n}\n.vs-stack-cards-inner::before{ /* paper tooth */\n  content: \"\"; position: absolute; inset: 0; z-index: -1; opacity: .45; pointer-events: none;\n  background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .45  0 0 0 0 .38  0 0 0 0 .28  0 0 0 .18 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\");\n}\n/* the index tab: exactly the strip a buried card leaves showing */\n.vs-stack-cards-tab{\n  all: unset; box-sizing: border-box; flex: none; height: var(--vs-stack-cards-offset, 34px); min-height: 22px; padding: 0 22px 0 20px;\n  display: flex; align-items: center; justify-content: space-between; gap: 16px; cursor: default; position: relative;\n  font: 500 10.5px/1 \"Mulish\", system-ui, sans-serif; letter-spacing: .22em; text-transform: uppercase; font-variant-numeric: tabular-nums;\n  color: color-mix(in srgb, var(--vs-stack-cards-ink) 62%, transparent); -webkit-tap-highlight-color: transparent;\n  transition: color .25s cubic-bezier(.2,0,0,1);\n}\n.vs-stack-cards-tab::after{ content: \"\"; position: absolute; left: 14px; right: 14px; bottom: 0; border-bottom: 1px dashed color-mix(in srgb, var(--vs-stack-cards-ink) 20%, transparent); }\n.vs-stack-cards-tab b{ font-weight: 500; color: var(--vs-stack-cards-accent); margin-right: 12px; letter-spacing: .1em; }\n.vs-stack-cards-tab:focus-visible{ box-shadow: inset 0 0 0 2px var(--vs-stack-cards-accent); border-radius: var(--vs-stack-cards-radius) var(--vs-stack-cards-radius) 6px 6px; }\n.vs-stack-cards-card.is-buried .vs-stack-cards-tab{ cursor: pointer; }\n.vs-stack-cards-card.is-buried .vs-stack-cards-tab:hover{ color: var(--vs-stack-cards-ink); }\n@media (hover: hover){ .vs-stack-cards-card.is-buried:has(.vs-stack-cards-tab:hover) .vs-stack-cards-inner{ translate: 0 -6px; } }\n.vs-stack-cards-card.is-buried .vs-stack-cards-tab:active{ color: var(--vs-stack-cards-accent); }\n\n.vs-stack-cards-grid{ flex: 1; min-height: 0; display: grid; grid-template-columns: 47% 1fr; gap: clamp(20px, 3vw, 40px); padding: 12px 14px 14px; }\n.vs-stack-cards-shade{ position: absolute; inset: 0; z-index: 5; background: #0a1222; opacity: 0; pointer-events: none; will-change: opacity; }\n.vs-stack-cards-media{ position: relative; border-radius: calc(var(--vs-stack-cards-radius) - 12px); overflow: hidden; min-height: 0;\n  box-shadow: inset 0 0 0 .5px rgba(8,14,28,.25); }\n.vs-stack-cards-media img{ position: absolute; inset: -4% 0; width: 100%; height: 108%; object-fit: cover; display: block; will-change: transform; user-select: none; -webkit-user-drag: none; }\n.vs-stack-cards-body{ display: flex; flex-direction: column; padding: clamp(8px, 1.2vw, 16px) clamp(6px, 1.6vw, 22px) 6px 0; min-width: 0; }\n.vs-stack-cards-top{ display: flex; justify-content: space-between; align-items: flex-start; }\n.vs-stack-cards-num{ font: 400 clamp(64px, 7.4vw, 104px)/.78 \"Gilda Display\", Georgia, serif; letter-spacing: -.045em; color: var(--vs-stack-cards-accent); }\n.vs-stack-cards-seal{\n  width: 44px; height: 44px; display: grid; place-items: center; border-radius: 6px; flex: none;\n  color: var(--vs-stack-cards-paper); background: var(--vs-stack-cards-accent);\n  font: 400 21px/1 \"Hiragino Mincho ProN\", \"Yu Mincho\", \"Noto Serif JP\", serif; transform: rotate(-4deg);\n  box-shadow: inset 0 0 0 2px rgba(243,235,220,.25), 0 1px 1px rgba(80,20,10,.25);\n}\n.vs-stack-cards-title{ margin: auto 0 0; padding-top: 18px; font: 400 clamp(30px, 3.4vw, 48px)/.98 \"Gilda Display\", Georgia, serif; letter-spacing: -.02em; }\n.vs-stack-cards-title span{ color: color-mix(in srgb, var(--vs-stack-cards-ink) 45%, transparent); }\n.vs-stack-cards-text{ margin: 14px 0 0; max-width: 40ch; font-size: 14.5px; line-height: 1.58; color: color-mix(in srgb, var(--vs-stack-cards-ink) 76%, transparent); }\n.vs-stack-cards-meta{ display: flex; gap: 18px; margin-top: 22px; padding-top: 14px; border-top: 1px solid color-mix(in srgb, var(--vs-stack-cards-ink) 14%, transparent);\n  font-size: 10.5px; font-weight: 500; letter-spacing: .16em; text-transform: uppercase; font-variant-numeric: tabular-nums; color: color-mix(in srgb, var(--vs-stack-cards-ink) 62%, transparent); }\n/* copy rises in once the card reaches its pin */\n.vs-stack-cards-title, .vs-stack-cards-text, .vs-stack-cards-meta{\n  opacity: 0; transform: translate3d(0,18px,0); transition: opacity .7s cubic-bezier(.2,0,0,1), transform 1.1s cubic-bezier(.16,1,.3,1); }\n.vs-stack-cards-card.is-in .vs-stack-cards-title{ opacity: 1; transform: none; transition-delay: .06s; }\n.vs-stack-cards-card.is-in .vs-stack-cards-text{ opacity: 1; transform: none; transition-delay: .14s; }\n.vs-stack-cards-card.is-in .vs-stack-cards-meta{ opacity: 1; transform: none; transition-delay: .22s; }\n\n@media (max-width: 760px){\n  .vs-stack-cards{ --vs-stack-cards-radius: 20px; }\n  .vs-stack-cards-tab{ padding: 0 16px; letter-spacing: .18em; }\n  .vs-stack-cards-grid{ grid-template-columns: 1fr; grid-template-rows: 50% 1fr; gap: 12px; padding: 10px; }\n  .vs-stack-cards-body{ padding: 2px 8px 6px; }\n  .vs-stack-cards-num{ font-size: 52px; }\n  .vs-stack-cards-seal{ width: 36px; height: 36px; font-size: 17px; }\n  .vs-stack-cards-title{ font-size: 30px; padding-top: 10px; }\n  .vs-stack-cards-text{ font-size: 13.5px; margin-top: 8px; line-height: 1.5; }\n  .vs-stack-cards-meta{ margin-top: 12px; padding-top: 10px; gap: 12px; letter-spacing: .12em; }\n  .vs-stack-cards-meta span:nth-child(3){ display: none; }\n}\n@media (prefers-reduced-motion: reduce){\n  .vs-stack-cards-title, .vs-stack-cards-text, .vs-stack-cards-meta{ opacity: 1; transform: none; transition: none; }\n  .vs-stack-cards-inner{ transition: none; }\n}\n</style>\n\n<section class=\"vs-stack-cards\" aria-label=\"Four stages of the brew\">\n  <article class=\"vs-stack-cards-card\" aria-label=\"Stage 1 of 4, Washing\">\n    <div class=\"vs-stack-cards-inner\">\n      <button class=\"vs-stack-cards-tab\" type=\"button\" aria-label=\"Bring stage 1, Washing, to the top\"><span><b>01</b>Washing</span><span>Day 1</span></button>\n      <div class=\"vs-stack-cards-grid\">\n        <div class=\"vs-stack-cards-media\"><img src=\"assets/stage-1.webp\" alt=\"Brewers washing polished rice in cedar tubs by the sea\" draggable=\"false\" loading=\"eager\"></div>\n        <div class=\"vs-stack-cards-body\">\n          <div class=\"vs-stack-cards-top\"><span class=\"vs-stack-cards-num\">01</span><span class=\"vs-stack-cards-seal\" aria-hidden=\"true\">洗</span></div>\n          <h3 class=\"vs-stack-cards-title\">Polish, <span>then wash.</span></h3>\n          <p class=\"vs-stack-cards-text\">The rice is milled to sixty percent of the grain, then washed by hand in well water, timed to the second so each grain drinks just enough.</p>\n          <div class=\"vs-stack-cards-meta\"><span>Day 1</span><span>8&thinsp;&deg;C water</span><span>Milled 60%</span></div>\n        </div>\n      </div>\n      <div class=\"vs-stack-cards-shade\" aria-hidden=\"true\"></div>\n    </div>\n  </article>\n  <article class=\"vs-stack-cards-card\" aria-label=\"Stage 2 of 4, Koji\">\n    <div class=\"vs-stack-cards-inner\">\n      <button class=\"vs-stack-cards-tab\" type=\"button\" aria-label=\"Bring stage 2, Koji, to the top\"><span><b>02</b>Koji</span><span>Days 2&ndash;3</span></button>\n      <div class=\"vs-stack-cards-grid\">\n        <div class=\"vs-stack-cards-media\"><img src=\"assets/stage-2.webp\" alt=\"Two brewers spreading steamed rice on a table under a paper lantern\" draggable=\"false\" loading=\"eager\"></div>\n        <div class=\"vs-stack-cards-body\">\n          <div class=\"vs-stack-cards-top\"><span class=\"vs-stack-cards-num\">02</span><span class=\"vs-stack-cards-seal\" aria-hidden=\"true\">麹</span></div>\n          <h3 class=\"vs-stack-cards-title\">Grow <span>the mould.</span></h3>\n          <p class=\"vs-stack-cards-text\">Steamed rice is spread across cedar beds in the warm room, dusted with koji spores and turned by lantern light every four hours for two nights.</p>\n          <div class=\"vs-stack-cards-meta\"><span>Days 2&ndash;3</span><span>35&thinsp;&deg;C room</span><span>Turned 12&times;</span></div>\n        </div>\n      </div>\n      <div class=\"vs-stack-cards-shade\" aria-hidden=\"true\"></div>\n    </div>\n  </article>\n  <article class=\"vs-stack-cards-card\" aria-label=\"Stage 3 of 4, Moromi\">\n    <div class=\"vs-stack-cards-inner\">\n      <button class=\"vs-stack-cards-tab\" type=\"button\" aria-label=\"Bring stage 3, Moromi, to the top\"><span><b>03</b>Moromi</span><span>Days 4&ndash;30</span></button>\n      <div class=\"vs-stack-cards-grid\">\n        <div class=\"vs-stack-cards-media\"><img src=\"assets/stage-3.webp\" alt=\"Brewers on ladders stirring tall cedar vats of rice mash\" draggable=\"false\" loading=\"eager\"></div>\n        <div class=\"vs-stack-cards-body\">\n          <div class=\"vs-stack-cards-top\"><span class=\"vs-stack-cards-num\">03</span><span class=\"vs-stack-cards-seal\" aria-hidden=\"true\">醪</span></div>\n          <h3 class=\"vs-stack-cards-title\">Build <span>the mash.</span></h3>\n          <p class=\"vs-stack-cards-text\">Koji, water and yeast go into the tall vats in three additions over four days. The brewers climb ladders at dawn to stir it with long oars.</p>\n          <div class=\"vs-stack-cards-meta\"><span>Days 4&ndash;30</span><span>10&thinsp;&deg;C</span><span>3 additions</span></div>\n        </div>\n      </div>\n      <div class=\"vs-stack-cards-shade\" aria-hidden=\"true\"></div>\n    </div>\n  </article>\n  <article class=\"vs-stack-cards-card\" aria-label=\"Stage 4 of 4, Shipping\">\n    <div class=\"vs-stack-cards-inner\">\n      <button class=\"vs-stack-cards-tab\" type=\"button\" aria-label=\"Bring stage 4, Shipping, to the top\"><span><b>04</b>Shipping</span><span>Month 6</span></button>\n      <div class=\"vs-stack-cards-grid\">\n        <div class=\"vs-stack-cards-media\"><img src=\"assets/stage-4.webp\" alt=\"A brewer carrying a cask to the shore, a boat waiting in the bay\" draggable=\"false\" loading=\"eager\"></div>\n        <div class=\"vs-stack-cards-body\">\n          <div class=\"vs-stack-cards-top\"><span class=\"vs-stack-cards-num\">04</span><span class=\"vs-stack-cards-seal\" aria-hidden=\"true\">出</span></div>\n          <h3 class=\"vs-stack-cards-title\">Carry it <span>to the boats.</span></h3>\n          <p class=\"vs-stack-cards-text\">Pressed and rested through summer, the casks leave the kura on shoulders, down to the harbour for the autumn boats, as they have since 1861.</p>\n          <div class=\"vs-stack-cards-meta\"><span>Month 6</span><span>Autumn tide</span><span>Cask 72 L</span></div>\n        </div>\n      </div>\n      <div class=\"vs-stack-cards-shade\" aria-hidden=\"true\"></div>\n    </div>\n  </article>\n</section>\n\n<script>\n(() => {\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)');\n  const clamp = (v, a, b) => Math.max(a, Math.min(b, v));\n  function init(root) {\n    if (root.dataset.vsReady) return; root.dataset.vsReady = '1';\n    const cards = [...root.querySelectorAll('.vs-stack-cards-card')].map((el, i) => {\n      el.style.setProperty('--i', i);\n      return { el, i, inner: el.querySelector('.vs-stack-cards-inner'), shade: el.querySelector('.vs-stack-cards-shade'),\n        img: el.querySelector('.vs-stack-cards-media img'), tab: el.querySelector('.vs-stack-cards-tab'),\n        d: 0, dv: 0, t: 0, tv: 0, blur: -1, buried: false };\n    });\n    const n = cards.length; if (!n) return;\n    root.style.setProperty('--vs-stack-cards-n', n);\n    // geometry, all from layout once (no reads while scrolling)\n    let pins = [], spans = [], depthK = .05, dimMax = .55, blurMax = 0, lift = 7;\n    const measure = () => {\n      const cs = getComputedStyle(root);\n      depthK = parseFloat(cs.getPropertyValue('--vs-stack-cards-depth')); if (isNaN(depthK)) depthK = .05;\n      dimMax = parseFloat(cs.getPropertyValue('--vs-stack-cards-dim')); if (isNaN(dimMax)) dimMax = .45;\n      blurMax = parseFloat(cs.getPropertyValue('--vs-stack-cards-blur')) || 0;\n      const H = cards[0].el.offsetHeight, G = n > 1 ? parseFloat(getComputedStyle(cards[1].el).marginTop) || 0 : 0;\n      const top0 = root.getBoundingClientRect().top + scrollY;\n      const tops = cards.map(c => parseFloat(getComputedStyle(c.el).top) || 0);\n      pins = cards.map((c, i) => top0 + i * (H + G) - tops[i]);           // scrollY at which card i pins\n      spans = cards.map((c, i) => Math.max(1, i ? (H + G) - (tops[i] - tops[i - 1]) : 1));\n      lift = Math.max(2, (tops[1] - tops[0]) * .2 || 6);\n    };\n    // scroll velocity -> a small lean on the card in flight (spring-damped)\n    let sy = scrollY, vel = 0, lastT = 0, raf = 0, on = false, settleFrames = 0;\n    const K = 260, C = 28, KT = 180, CT = 20;\n    const frame = now => {\n      raf = 0;\n      const dt = Math.min(.05, lastT ? (now - lastT) / 1000 : 1 / 60); lastT = now;\n      const s = scrollY, inst = (s - sy) / Math.max(dt, 1e-3); sy = s;\n      vel += (inst - vel) * Math.min(1, dt * 12);\n      const prog = cards.map((c, i) => i === 0 ? (s >= pins[0] - 1 ? 1 : clamp(1 - (pins[0] - s) / 400, 0, 1)) : clamp(1 - (pins[i] - s) / spans[i], 0, 1));\n      let moving = false, top = 0;\n      const steps = Math.max(1, Math.ceil(dt * 240)), h = dt / steps;\n      cards.forEach((c, i) => {\n        let target = 0; for (let j = i + 1; j < n; j++) target += prog[j];\n        const inFlight = prog[i] > 0 && prog[i] < 1;\n        const tiltT = inFlight ? clamp(-vel * 0.0024, -5, 5) : 0;\n        if (reduce.matches) { c.d = target; c.dv = 0; c.t = 0; }\n        else for (let k = 0; k < steps; k++) {\n          c.dv += (-K * (c.d - target) - C * c.dv) * h; c.d += c.dv * h;\n          c.tv += (-KT * (c.t - tiltT) - CT * c.tv) * h; c.t += c.tv * h;\n        }\n        if (Math.abs(c.d - target) > 1e-4 || Math.abs(c.dv) > 1e-3 || Math.abs(c.t - tiltT) > 1e-3 || Math.abs(c.tv) > 1e-2) moving = true;\n        if (prog[i] > .985) top = i;\n        if (!c.el.classList.contains('is-in') && prog[i] > .45) c.el.classList.add('is-in');\n        const d = Math.max(0, c.d);\n        c.inner.style.transform = reduce.matches ? '' :\n          `translate3d(0,${(-d * lift).toFixed(2)}px,0) perspective(1600px) rotateX(${c.t.toFixed(3)}deg) scale(${(1 - d * depthK).toFixed(4)})`;\n        c.shade.style.opacity = Math.min(dimMax, d * dimMax * .55).toFixed(3);\n        if (blurMax > 0) { const b = Math.round(Math.min(blurMax, d * blurMax * .6) * 4) / 4; if (b !== c.blur) { c.blur = b; c.inner.style.filter = b ? `blur(${b}px)` : ''; } }\n        else if (c.blur !== -1) { c.blur = -1; c.inner.style.filter = ''; }\n        if (c.img && !reduce.matches) c.img.style.transform = `translate3d(0,${((1 - prog[i]) * 4 - 2).toFixed(2)}%,0)`;\n        const buried = d > .5; if (buried !== c.buried) { c.buried = buried; c.el.classList.toggle('is-buried', buried); c.tab.tabIndex = buried ? 0 : -1; }\n      });\n      root.dataset.top = top;\n      if (Math.abs(vel) > 2) moving = true;\n      // keep running until the springs settle, then sleep until the next scroll\n      if (moving || settleFrames-- > 0) raf = requestAnimationFrame(frame); else lastT = 0;\n    };\n    const wake = () => { settleFrames = 3; if (!raf) raf = requestAnimationFrame(frame); };\n    new IntersectionObserver(([e]) => {\n      if (e.isIntersecting && !on) { on = true; addEventListener('scroll', wake, { passive: true }); wake(); }\n      else if (!e.isIntersecting && on) { on = false; removeEventListener('scroll', wake); }\n    }, { rootMargin: '20% 0px' }).observe(root);\n    const go = (i, behavior) => scrollTo({ top: pins[clamp(i, 0, n - 1)] + 1, behavior: behavior || (reduce.matches ? 'auto' : 'smooth') });\n    cards.forEach(c => { c.tab.tabIndex = -1; c.tab.addEventListener('click', () => { if (c.buried) go(c.i); }); });\n    // props can change live (offset, depth, dim, blur)\n    new MutationObserver(() => { measure(); wake(); }).observe(root, { attributes: true, attributeFilter: ['style'] });\n    new ResizeObserver(() => { measure(); wake(); }).observe(root);\n    measure(); sy = scrollY; frame(performance.now());\n    root.vsStackCards = { go, measure };\n  }\n  const boot = () => document.querySelectorAll('.vs-stack-cards').forEach(init);\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', boot) : boot();\n})();\n</script>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Cards & layout",
    "tags": [
      "sticky",
      "scroll",
      "stacking cards",
      "deck",
      "tabs",
      "spring",
      "process",
      "steps",
      "storytelling"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [
      "https://fonts.googleapis.com/css2?family=Gilda+Display&family=Mulish:wght@400;500&display=swap"
    ],
    "source": "ukiyo",
    "free": true,
    "accent": "#c8412b",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/stack-cards/index.html",
    "thumbnail": "https://vividsites.app/api/component/stack-cards/thumb.jpg",
    "video": "https://vividsites.app/api/component/stack-cards/thumb.mp4",
    "page": "https://vividsites.app/c/stack-cards"
  }
}