{
  "id": "serif-motion-study-hero",
  "kind": "element",
  "title": "Serif motion study hero",
  "summary": "A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.",
  "url": "https://enrichr.dev/elements/serif-motion-study-hero.html",
  "section": "hero",
  "techniques": [
    "text-animation",
    "3d-css",
    "pointer-interaction",
    "stagger",
    "reveal"
  ],
  "stack": [
    "Vanilla JS",
    "CSS",
    "Canvas"
  ],
  "frameworks": [
    "React",
    "Next.js",
    "Vanilla"
  ],
  "mood": [
    "cinematic",
    "editorial"
  ],
  "intents": [
    "serif headline blur in animation",
    "3d card wall drag gallery",
    "curved card wall css 3d",
    "dark green starfield hero",
    "infinite drag card archive"
  ],
  "status": "tested",
  "params": [
    {
      "key": "eyebrow",
      "label": "Small caps line",
      "type": "text",
      "value": "THE MOTION ARCHIVE"
    },
    {
      "key": "headline",
      "label": "Headline (*word* is underlined, / starts a new line)",
      "type": "text",
      "value": "Great motion is *studied* / before it is *built*."
    },
    {
      "key": "body",
      "label": "Text",
      "type": "text",
      "value": "Thirty-three small studies from the Enrichr Library, each rebuilt by hand in plain code. Open the wall and drag through what inspired them."
    },
    {
      "key": "cta",
      "label": "Button",
      "type": "text",
      "value": "Open the motion archive"
    },
    {
      "key": "open",
      "label": "Open the wall",
      "type": "select",
      "options": [
        [
          "click",
          "On button click"
        ],
        [
          "auto",
          "Automatically after the intro"
        ]
      ],
      "value": "click"
    },
    {
      "key": "sound",
      "label": "Ambient sound",
      "type": "select",
      "options": [
        [
          "off",
          "Off until switched on"
        ],
        [
          "on",
          "Starts with the wall"
        ]
      ],
      "value": "off"
    },
    {
      "key": "curve",
      "label": "Wall curve (px)",
      "type": "range",
      "min": 0,
      "max": 400,
      "step": 10,
      "value": 170
    },
    {
      "key": "tiltX",
      "label": "Tilt up and down (deg)",
      "type": "range",
      "min": 0,
      "max": 30,
      "step": 1,
      "value": 11
    },
    {
      "key": "tiltY",
      "label": "Turn left and right (deg)",
      "type": "range",
      "min": 0,
      "max": 45,
      "step": 1,
      "value": 21
    },
    {
      "key": "bob",
      "label": "Bob (px)",
      "type": "range",
      "min": 0,
      "max": 20,
      "step": 1,
      "value": 5
    },
    {
      "key": "stars",
      "label": "Stars",
      "type": "range",
      "min": 0,
      "max": 400,
      "step": 10,
      "value": 150
    },
    {
      "key": "accent",
      "label": "Accent",
      "type": "color",
      "value": "#e6ff55"
    },
    {
      "key": "background",
      "label": "Background",
      "type": "color",
      "value": "#0c1e18"
    },
    {
      "key": "card",
      "label": "Cards",
      "type": "color",
      "value": "#0f3628"
    },
    {
      "key": "glow",
      "label": "Glow",
      "type": "color",
      "value": "#1a7a4c"
    },
    {
      "key": "text",
      "label": "Text",
      "type": "color",
      "value": "#fbf8f1"
    }
  ],
  "license": "Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.",
  "contract": "function mount(root, opts) -> { update(partialOpts), destroy() }; root must contain the element markup",
  "exports": {
    "claude": "Add the \"Serif motion study hero\" element to this project.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nSteps:\n1. Look at the project first to see its framework and where components, styles and static files live. Follow its conventions.\n2. Fetch https://enrichr.dev/elements/serif-motion-study-hero.json if you can read URLs (fields: source.markup, source.css, source.js, params); otherwise use the source at the end of this message.\n3. Add the files, mount it where I tell you (default: the top of the home page), and pass the settings below as the opts.\n4. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n5. Run the dev server and check it. with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "chatgpt": "I want the \"Serif motion study hero\" element on my website. Below is its complete source. Please:\n1. Build one self-contained index.html that I can open locally to see it working (put the CSS in a <style> tag and the JavaScript in a <script> tag, with the markup in the body).\n2. Then explain, step by step, which parts go where in my own site, and how to mount it. My stack is: [tell me yours, or say \"plain HTML\"].\n3. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}\n\nVerify: with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.",
    "cursor": "Add the \"Serif motion study hero\" element to this codebase.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nUse Composer / agent mode. First search the repo for how components, styles and scripts are organised, then add the files in the matching places, mount it on the page I name, and run the app to confirm it works. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}\n\nVerify: with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.\n\nOptional: save this as .cursor/rules/enrichr-serif-motion-study-hero.mdc so the rule stays with the project:\n---\ndescription: How to use the Enrichr Serif motion study hero element\nalwaysApply: false\n---\nmount(root, opts) expects the element markup inside root and returns { update, destroy }. Always call destroy() on unmount and never edit the ef- class names.",
    "v0": "Create a Next.js (App Router) client component for the \"Serif motion study hero\" element. Tailwind is available but this component needs no utility classes.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles to create:\n- serif-motion-study-hero.js : the JavaScript below, with one line appended at the end: export { mount };  (and the markup exported as: export const markup = \"<the markup string>\";). If the project uses TypeScript and does not allow JS imports, put // @ts-nocheck on the first line.\n- the CSS below goes in your global stylesheet (or a plain .css file imported once at the app root). The class names are global and start with ef-.\n\nUse this component as the wrapper (adjust the import paths):\n```tsx\n\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./serif-motion-study-hero\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function SerifMotionStudyHero(props: Record<string, unknown>) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fx = useRef<{ update: (o: object) => void; destroy: () => void } | null>(null);\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    root.innerHTML = markup;\n    fx.current = mount(root, props);\n    return () => { fx.current?.destroy(); fx.current = null; root.innerHTML = \"\"; };\n  }, []);\n  useEffect(() => { fx.current?.update(props); }, [JSON.stringify(props)]);\n  return <div ref={ref} />;\n}\n```\n\nRender it on the home page so I can see it. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "lovable": "Add the \"Serif motion study hero\" element to this React + Vite + TypeScript + Tailwind project. Do not install any packages.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles to create:\n- serif-motion-study-hero.js : the JavaScript below, with one line appended at the end: export { mount };  (and the markup exported as: export const markup = \"<the markup string>\";). If the project uses TypeScript and does not allow JS imports, put // @ts-nocheck on the first line.\n- the CSS below goes in your global stylesheet (or a plain .css file imported once at the app root). The class names are global and start with ef-.\n\nWrapper component (src/components/SerifMotionStudyHero.tsx):\n```tsx\n\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./serif-motion-study-hero\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function SerifMotionStudyHero(props: Record<string, unknown>) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fx = useRef<{ update: (o: object) => void; destroy: () => void } | null>(null);\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    root.innerHTML = markup;\n    fx.current = mount(root, props);\n    return () => { fx.current?.destroy(); fx.current = null; root.innerHTML = \"\"; };\n  }, []);\n  useEffect(() => { fx.current?.update(props); }, [JSON.stringify(props)]);\n  return <div ref={ref} />;\n}\n```\n\nRemove the \"use client\" line if it is not needed. Show it on the home page. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "bolt": "Create a small Vite project (vanilla JavaScript, no framework unless I already have one) that shows the \"Serif motion study hero\" element.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles: index.html (a container element, then a script tag), style.css (the CSS below), serif-motion-study-hero.js (the JavaScript below) and main.js that sets the container's innerHTML to the markup and calls mount(container, opts) with the settings below. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}\n\nVerify: with reduced motion on, nothing moves; it works at 375px wide and with touch only; there are no console errors; scrolling stays smooth; destroy() is called when it unmounts.",
    "replit": "Create a new HTML, CSS and JavaScript Repl that shows the \"Serif motion study hero\" element. No packages, no server code.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nFiles: index.html (a container element, link style.css, load serif-motion-study-hero.js and then main.js), style.css (the CSS below), serif-motion-study-hero.js (the JavaScript below), main.js (sets the container's innerHTML to the markup, then calls mount(container, opts) with the settings below). Press Run to preview. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "figma": "Designer handoff notes for the \"Serif motion study hero\" element (Enrichr Library). Figma cannot run this JavaScript inside a design file, so use it as a motion reference and hand the code to a developer.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nWhat it does: The intro is the original GSAP timeline as start times: the small caps line rises 14 px (0.9 s expo.out), each headline word fades up 26 px out of a 14 px blur (1.15 s expo.out, 0.09 s apart), the underlines draw with stroke-dashoffset (0.85 s power2.inOut), then the text and the button rise. The button blurs the words away and sends 33 cards in from 1,500 to 2,400 px deep, tumbling, centre first (1.6 s power3.out each). Every frame a card's tilt, depth, blur and brightness are worked out from where it sits on screen: further from the centre means turned more, pushed back on a curve, blurred up to 5.5 px and dimmed. The wall wraps around forever. Dragging, the wheel and the arrow keys move a target that the wall eases toward, and a fling keeps gliding with friction. Behind it a canvas draws 150 twinkling stars and an occasional shooting streak.\nAccessibility: The heading, text and button are real elements; the headline words are hidden from screen readers and the heading carries the whole sentence as its label. In the wall every card is a focusable button: Tab moves from card to card and brings the focused one to the centre, Enter or Space marks it as seen (the counter is announced), and the left and right arrow keys pan. Home returns to the hero and puts focus back on its button. The glow cursor replaces the system cursor only on mouse devices. With prefers-reduced-motion the text is shown at rest, the wall opens without the flight, and the bob, twinkle and streaks stop. Sound is off by default and only ever starts from a click.\n\nTunable settings:\n- Small caps line: text, default THE MOTION ARCHIVE\n- Headline (*word* is underlined, / starts a new line): text, default Great motion is *studied* / before it is *built*.\n- Text: text, default Thirty-three small studies from the Enrichr Library, each rebuilt by hand in plain code. Open the wall and drag through what inspired them.\n- Button: text, default Open the motion archive\n- Open the wall: select (On button click / Automatically after the intro), default click\n- Ambient sound: select (Off until switched on / Starts with the wall), default off\n- Wall curve (px): range 0 to 400, step 10, default 170\n- Tilt up and down (deg): range 0 to 30, step 1, default 11\n- Turn left and right (deg): range 0 to 45, step 1, default 21\n- Bob (px): range 0 to 20, step 1, default 5\n- Stars: range 0 to 400, step 10, default 150\n- Accent: color, default #e6ff55\n- Background: color, default #0c1e18\n- Cards: color, default #0f3628\n- Glow: color, default #1a7a4c\n- Text: color, default #fbf8f1\n\nCurrent settings:\n{{SETTINGS}}\n\nFor the developer: Element contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\nIn Figma:\n- Draw the resting state as a frame and the end state as a second frame; link them with Smart Animate to show the motion in a prototype.\n- Put the settings above in the frame's notes or Dev Mode annotations so the developer matches them.\n- Link https://enrichr.dev/elements/serif-motion-study-hero.html in the handoff: the live demo and the copy-ready code are there. Developers can paste the prompts for Claude, Cursor or v0 from the Export menu.\nNothing is copied from another designer's file; this is original code from Enrichr.",
    "framer": "Add the \"Serif motion study hero\" element to Framer as a code component. Framer code components are React, so wrap the vanilla code like this. Check it in the Framer editor, as Framer's component API can change.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\n1. In Framer: Assets > Code > New component. Name it SerifMotionStudyHero.\n2. Paste the JavaScript below into a separate code file named serif-motion-study-hero.js in the same project, with  export { mount };  and  export const markup = \"<the markup string>\";  added. Put the CSS into the component as a <style> tag.\n3. Component:\n```tsx\nimport { addPropertyControls, ControlType } from \"framer\"\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./serif-motion-study-hero\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function SerifMotionStudyHero(props: any) {\n  const ref = useRef<HTMLDivElement>(null);\n  const fx = useRef<{ update: (o: object) => void; destroy: () => void } | null>(null);\n  useEffect(() => {\n    const root = ref.current;\n    if (!root) return;\n    root.innerHTML = markup;\n    fx.current = mount(root, props);\n    return () => { fx.current?.destroy(); fx.current = null; root.innerHTML = \"\"; };\n  }, []);\n  useEffect(() => { fx.current?.update(props); }, [JSON.stringify(props)]);\n  return <div ref={ref} />;\n}\n\naddPropertyControls(SerifMotionStudyHero, {\n  eyebrow: { type: ControlType.String, title: \"Small caps line\", defaultValue: \"THE MOTION ARCHIVE\" },\n  headline: { type: ControlType.String, title: \"Headline (*word* is underlined, / starts a new line)\", defaultValue: \"Great motion is *studied* / before it is *built*.\" },\n  body: { type: ControlType.String, title: \"Text\", defaultValue: \"Thirty-three small studies from the Enrichr Library, each rebuilt by hand in plain code. Open the wall and drag through what inspired them.\" },\n  cta: { type: ControlType.String, title: \"Button\", defaultValue: \"Open the motion archive\" },\n  open: { type: ControlType.Enum, title: \"Open the wall\", options: [\"click\",\"auto\"], optionTitles: [\"On button click\",\"Automatically after the intro\"], defaultValue: \"click\" },\n  sound: { type: ControlType.Enum, title: \"Ambient sound\", options: [\"off\",\"on\"], optionTitles: [\"Off until switched on\",\"Starts with the wall\"], defaultValue: \"off\" },\n  curve: { type: ControlType.Number, title: \"Wall curve\", min: 0, max: 400, step: 10, defaultValue: 170 },\n  tiltX: { type: ControlType.Number, title: \"Tilt up and down\", min: 0, max: 30, step: 1, defaultValue: 11 },\n  tiltY: { type: ControlType.Number, title: \"Turn left and right\", min: 0, max: 45, step: 1, defaultValue: 21 },\n  bob: { type: ControlType.Number, title: \"Bob\", min: 0, max: 20, step: 1, defaultValue: 5 },\n  stars: { type: ControlType.Number, title: \"Stars\", min: 0, max: 400, step: 10, defaultValue: 150 },\n  accent: { type: ControlType.Color, title: \"Accent\", defaultValue: \"#e6ff55\" },\n  background: { type: ControlType.Color, title: \"Background\", defaultValue: \"#0c1e18\" },\n  card: { type: ControlType.Color, title: \"Cards\", defaultValue: \"#0f3628\" },\n  glow: { type: ControlType.Color, title: \"Glow\", defaultValue: \"#1a7a4c\" },\n  text: { type: ControlType.Color, title: \"Text\", defaultValue: \"#fbf8f1\" },\n})\n```\n4. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}",
    "webflow": "Add the \"Serif motion study hero\" element to a Webflow site with an Embed element. Test it on the published or preview site: scripts in embeds may not run inside the Designer canvas.\n\nThis is the \"Serif motion study hero\" element from the Enrichr Library: A starlit green hero whose serif headline blurs into place with hand-drawn underlines, then opens onto an endless curved wall of cards you can drag through.\nPage: https://enrichr.dev/elements/serif-motion-study-hero.html\nMachine-readable spec: https://enrichr.dev/elements/serif-motion-study-hero.json\n\nElement contract (vanilla JavaScript, no dependencies):\n- The JavaScript defines a function mount(root, opts).\n- root is an empty container element. Set root.innerHTML to the markup first, then call mount(root, opts).\n- It returns an object with update(partialOpts) and destroy(). Call destroy() when the element is removed. Some elements also return replay().\n- The CSS classes all start with \"ef-\" so they will not collide with your styles.\n\n1. Add an Embed element where it should appear. Paste a <style> block with the CSS, one container <div id=\"enrichr-serif-motion-study-hero\"></div>, and a <script> block with the JavaScript followed by:\n   mount(document.getElementById(\"enrichr-serif-motion-study-hero\"), { ...your settings... });\n   (set the container's innerHTML to the markup first, as in the contract).\n2. Embed elements have a size limit (check Webflow's current number). If the code is longer, host the JS and CSS as files and link them from Custom code in the page settings, then keep only the container and the mount call in the Embed.\n3. Keep the behaviour of the source unchanged. Do not add dependencies. Respect prefers-reduced-motion. Keep text as real HTML.\n\nSettings:\n{{SETTINGS}}\n\nSource:\n{{CODE}}"
  },
  "exportTools": [
    "claude",
    "chatgpt",
    "cursor",
    "v0",
    "lovable",
    "bolt",
    "replit",
    "figma",
    "framer",
    "webflow"
  ],
  "accessibility": "The heading, text and button are real elements; the headline words are hidden from screen readers and the heading carries the whole sentence as its label. In the wall every card is a focusable button: Tab moves from card to card and brings the focused one to the centre, Enter or Space marks it as seen (the counter is announced), and the left and right arrow keys pan. Home returns to the hero and puts focus back on its button. The glow cursor replaces the system cursor only on mouse devices. With prefers-reduced-motion the text is shown at rest, the wall opens without the flight, and the bob, twinkle and streaks stop. Sound is off by default and only ever starts from a click.",
  "performance": "The 33 cards move with transforms only. The blur and brightness filter on each card is rounded to half-pixel steps, so it changes now and then instead of every frame. The starfield is one canvas and the card images are generated once at start. Pixel ratio is capped at 2 and the loop pauses off screen and in hidden tabs.",
  "dependencies": [],
  "codepen": "https://codepen.io/Enrichr-Dev/pen/yyMGqZY",
  "source": {
    "markup": "<div class=\"ef-smh\">\n  <div class=\"ef-smh__bg\"></div>\n  <canvas class=\"ef-smh__stars\" aria-hidden=\"true\"></canvas>\n  <div class=\"ef-smh__field\" role=\"region\" aria-label=\"Motion archive: use the left and right arrow keys or Tab to browse the studies, Enter marks one as seen\" tabindex=\"-1\"></div>\n  <header class=\"ef-smh__head\">\n    <div class=\"ef-smh__logo\"><svg class=\"ef-smh__mark\" viewBox=\"0 0 50 50\" aria-hidden=\"true\" focusable=\"false\"><path fill=\"currentColor\" d=\"m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z\"/></svg><span>Enrichr</span></div>\n    <div class=\"ef-smh__top\"><span><span class=\"ef-smh__g\">33 studies</span> in motion</span><span class=\"ef-smh__inf\" aria-hidden=\"true\">i</span></div>\n    <div class=\"ef-smh__lang\">EN<span class=\"ef-smh__caret\" aria-hidden=\"true\">&#9662;</span></div>\n  </header>\n  <section class=\"ef-smh__hero\">\n    <p class=\"ef-smh__eyebrow\">THE MOTION ARCHIVE</p>\n    <h2 class=\"ef-smh__h\"></h2>\n    <p class=\"ef-smh__body\">Thirty-three small studies from the Enrichr Library, each rebuilt by hand in plain code. Open the wall and drag through what inspired them.</p>\n    <button class=\"ef-smh__cta\" type=\"button\">Open the motion archive</button>\n  </section>\n  <div class=\"ef-smh__bottom\">\n    <button class=\"ef-smh__audio\" type=\"button\" aria-pressed=\"false\" aria-label=\"Ambient sound\">\n      <svg viewBox=\"0 0 34 14\" aria-hidden=\"true\" focusable=\"false\"><path class=\"ef-smh__w1\" d=\"M1 7 Q 5 2, 9 7 T 17 7 T 25 7 T 33 7\"/><path class=\"ef-smh__w2\" d=\"M1 7 Q 5 12, 9 7 T 17 7 T 25 7 T 33 7\"/></svg><span class=\"ef-smh__albl\">OFF</span>\n    </button>\n    <div class=\"ef-smh__nav\">\n      <span class=\"ef-smh__navi\"><span class=\"ef-smh__sl\" aria-hidden=\"true\">/</span> <button class=\"ef-smh__home\" type=\"button\">Home</button></span>\n      <a class=\"ef-smh__a\" href=\"#\">Library</a><a class=\"ef-smh__a\" href=\"#\">Tools</a><a class=\"ef-smh__a\" href=\"#\">Contact</a>\n      <button class=\"ef-smh__share\" type=\"button\">Share a study</button>\n    </div>\n    <div class=\"ef-smh__count\" aria-live=\"polite\"><span class=\"ef-smh__d\"></span><span>STUDIES SEEN <b class=\"ef-smh__n\">0</b>/33</span></div>\n  </div>\n  <div class=\"ef-smh__cursor\" aria-hidden=\"true\"></div>\n  <div class=\"ef-smh__hint\" aria-hidden=\"true\">DRAG / SCROLL TO EXPLORE</div>\n  <div class=\"ef-smh__grain\" aria-hidden=\"true\"></div>\n</div>",
    "css": "/* Enrichr Library · https://enrichr.dev */\n.ef-smh { --ef-smh-bg: #0c1e18; --ef-smh-acc: #e6ff55; --ef-smh-ink: #0c1e18; --ef-smh-txt: #fbf8f1; --ef-smh-dim: #9db8ad; --ef-smh-card: #0f3628; --ef-smh-glow: #1a7a4c; --ef-smh-fs: 97px; --ef-smh-k: 1; --ef-smh-f: 1; --ef-smh-serif: 'Instrument Serif', 'Iowan Old Style', 'Palatino Linotype', Palatino, Georgia, serif;\n  position: relative; box-sizing: border-box; width: 100%; height: 100%; min-height: 28rem; overflow: hidden; border-radius: 1rem; background: #050d0a; color: var(--ef-smh-txt); font-family: 'Instrument Sans', system-ui, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; touch-action: pan-y; isolation: isolate; }\n.ef-smh.is-fine { cursor: none; }\n.ef-smh.is-fine .ef-smh__cta, .ef-smh.is-fine .ef-smh__audio, .ef-smh.is-fine .ef-smh__home, .ef-smh.is-fine .ef-smh__share, .ef-smh.is-fine .ef-smh__a, .ef-smh.is-fine .ef-smh__card { cursor: none; }\n/* atmosphere */\n.ef-smh .ef-smh__bg { position: absolute; inset: 0; z-index: 0; background: radial-gradient(120% 90% at 50% 118%, color-mix(in srgb, var(--ef-smh-glow) 34%, transparent) 0%, color-mix(in srgb, #005236 22%, transparent) 28%, transparent 62%), radial-gradient(85% 70% at 50% 42%, color-mix(in srgb, var(--ef-smh-bg) 72%, var(--ef-smh-glow)) 0%, var(--ef-smh-bg) 55%, #050d0a 100%); }\n.ef-smh .ef-smh__bg::after { content: \"\"; position: absolute; inset: 0; background: radial-gradient(120% 120% at 50% 50%, transparent 52%, rgba(0, 0, 0, 0.55) 100%); }\n.ef-smh .ef-smh__stars { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; display: block; pointer-events: none; }\n.ef-smh .ef-smh__grain { position: absolute; inset: 0; z-index: 40; pointer-events: none; opacity: 0.05; mix-blend-mode: overlay; 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'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\"); }\n/* cursor and hint */\n.ef-smh .ef-smh__cursor { position: absolute; left: 0; top: 0; z-index: 200; width: 30px; height: 30px; margin: -15px 0 0 -15px; border-radius: 50%; pointer-events: none; opacity: 0; background: radial-gradient(circle, color-mix(in srgb, var(--ef-smh-acc) 55%, transparent) 0%, color-mix(in srgb, var(--ef-smh-acc) 18%, transparent) 45%, transparent 70%); transition: width 0.3s, height 0.3s, margin 0.3s; }\n.ef-smh.is-fine.is-in .ef-smh__cursor { opacity: 1; }\n.ef-smh .ef-smh__cursor.is-big { width: 52px; height: 52px; margin: -26px 0 0 -26px; }\n.ef-smh .ef-smh__hint { position: absolute; left: 0; top: 0; z-index: 199; white-space: nowrap; pointer-events: none; font-size: 10px; font-weight: 600; letter-spacing: 0.12em; color: #fff; text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8); opacity: 0; }\n/* chrome */\n.ef-smh .ef-smh__head { position: absolute; top: 0; left: 0; right: 0; z-index: 60; display: flex; justify-content: space-between; align-items: center; padding: calc(30px * var(--ef-smh-k)) 44px; }\n.ef-smh .ef-smh__logo { display: inline-flex; align-items: center; gap: 0.35rem; font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-size: 22px; font-weight: 700; letter-spacing: -0.01em; }\n.ef-smh .ef-smh__mark { width: 24px; height: 24px; color: var(--ef-smh-acc); }\n.ef-smh .ef-smh__top { position: absolute; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 8px; font-family: var(--ef-smh-serif); font-size: 16px; white-space: nowrap; opacity: 0; }\n.ef-smh .ef-smh__g { color: var(--ef-smh-acc); }\n.ef-smh .ef-smh__inf { display: grid; place-items: center; width: 15px; height: 15px; border: 1px solid var(--ef-smh-dim); border-radius: 50%; font-family: 'Instrument Sans', sans-serif; font-size: 9px; color: var(--ef-smh-dim); }\n.ef-smh .ef-smh__lang { font-size: 13px; letter-spacing: 0.06em; }\n.ef-smh .ef-smh__caret { margin-left: 5px; font-size: 9px; color: var(--ef-smh-dim); }\n/* hero */\n.ef-smh .ef-smh__hero { position: absolute; inset: 0; z-index: 20; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 0 20px; text-align: center; }\n.ef-smh .ef-smh__eyebrow { margin: 0 0 calc(26px * var(--ef-smh-k)); font-size: 11px; font-weight: 500; letter-spacing: 0.28em; line-height: 1.4; color: var(--ef-smh-dim); opacity: 0; }\n.ef-smh .ef-smh__h { margin: 0; font-family: var(--ef-smh-serif); font-weight: 400; font-size: var(--ef-smh-fs); line-height: 1.06; letter-spacing: -0.01em; color: var(--ef-smh-txt); opacity: 1; }\n.ef-smh .ef-smh__w { display: inline-block; opacity: 0; will-change: transform, filter, opacity; }\n.ef-smh .ef-smh__ul { position: relative; }\n.ef-smh .ef-smh__ul svg { position: absolute; left: -1%; bottom: -0.1em; width: 102%; height: 0.14em; overflow: visible; }\n.ef-smh .ef-smh__ul path { fill: none; stroke: var(--ef-smh-acc); stroke-width: 2.4; stroke-linecap: round; }\n.ef-smh .ef-smh__body { max-width: 520px; margin: calc(30px * var(--ef-smh-k)) 0 0; font-size: 13.5px; line-height: 1.7; color: color-mix(in srgb, var(--ef-smh-txt) 80%, var(--ef-smh-glow)); opacity: 0; }\n.ef-smh .ef-smh__cta { position: relative; margin-top: calc(38px * var(--ef-smh-k)); padding: 16px 30px; border: none; border-radius: 7px; background: var(--ef-smh-acc); color: var(--ef-smh-ink); font-family: 'Instrument Sans', sans-serif; font-size: 14px; font-weight: 600; letter-spacing: 0.01em; cursor: pointer; opacity: 0; box-shadow: 0 0 34px color-mix(in srgb, var(--ef-smh-acc) 35%, transparent), 0 0 90px color-mix(in srgb, var(--ef-smh-acc) 12%, transparent); transition: scale 0.35s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.35s; }\n.ef-smh .ef-smh__cta:hover { scale: 1.04; box-shadow: 0 0 46px color-mix(in srgb, var(--ef-smh-acc) 55%, transparent), 0 0 130px color-mix(in srgb, var(--ef-smh-acc) 20%, transparent); }\n.ef-smh .ef-smh__cta:focus-visible, .ef-smh .ef-smh__audio:focus-visible, .ef-smh .ef-smh__home:focus-visible, .ef-smh .ef-smh__share:focus-visible, .ef-smh .ef-smh__a:focus-visible { outline: 2px solid var(--ef-smh-acc); outline-offset: 3px; }\n/* the card wall */\n.ef-smh .ef-smh__field { position: absolute; inset: 0; z-index: 10; perspective: 1150px; perspective-origin: 50% 45%; visibility: hidden; outline: none; }\n.ef-smh .ef-smh__card { position: absolute; left: 0; top: 0; opacity: 0; will-change: transform, filter; outline: none; }\n.ef-smh .ef-smh__cin { position: relative; box-sizing: border-box; display: flex; flex-direction: column; width: 100%; height: 100%; padding: calc(17px * var(--ef-smh-f)); overflow: hidden; border: 1px solid rgba(190, 235, 215, 0.09); border-radius: calc(15px * var(--ef-smh-f)); background: linear-gradient(168deg, var(--ef-smh-card) 0%, color-mix(in srgb, var(--ef-smh-card) 72%, #050d0a) 100%); box-shadow: 0 18px 50px rgba(0, 0, 0, 0.42); transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.45s, border-color 0.45s; }\n.ef-smh .ef-smh__card:hover .ef-smh__cin, .ef-smh .ef-smh__card:focus-visible .ef-smh__cin { transform: translateZ(0) scale(1.045); border-color: color-mix(in srgb, var(--ef-smh-acc) 40%, transparent); box-shadow: 0 24px 70px rgba(0, 0, 0, 0.55), 0 0 30px color-mix(in srgb, var(--ef-smh-acc) 12%, transparent); }\n.ef-smh .ef-smh__card:focus-visible .ef-smh__cin { border-color: var(--ef-smh-acc); }\n.ef-smh .ef-smh__cin::before, .ef-smh .ef-smh__cin::after { content: \"\"; position: absolute; width: 14px; height: 14px; opacity: 0; pointer-events: none; transition: opacity 0.35s; }\n.ef-smh .ef-smh__cin::before { left: 7px; bottom: 7px; border-left: 1.5px solid var(--ef-smh-acc); border-bottom: 1.5px solid var(--ef-smh-acc); }\n.ef-smh .ef-smh__cin::after { right: 7px; top: 7px; border-right: 1.5px solid var(--ef-smh-acc); border-top: 1.5px solid var(--ef-smh-acc); }\n.ef-smh .ef-smh__card:hover .ef-smh__cin::before, .ef-smh .ef-smh__card:hover .ef-smh__cin::after, .ef-smh .ef-smh__card:focus-visible .ef-smh__cin::before, .ef-smh .ef-smh__card:focus-visible .ef-smh__cin::after { opacity: 0.9; }\n.ef-smh .ef-smh__ph { flex-shrink: 0; margin-bottom: calc(13px * var(--ef-smh-f)); overflow: hidden; border-radius: calc(9px * var(--ef-smh-f)); }\n.ef-smh .ef-smh__ph canvas { display: block; width: 100%; height: 100%; object-fit: cover; }\n.ef-smh .ef-smh__ct { margin: 0; font-family: var(--ef-smh-serif); font-weight: 400; font-size: calc(21.5px * var(--ef-smh-f)); line-height: 1.17; letter-spacing: 0.005em; color: color-mix(in srgb, var(--ef-smh-txt) 92%, var(--ef-smh-glow)); opacity: 1; }\n.ef-smh .ef-smh__ct.is-top { margin-top: 6px; }\n.ef-smh .ef-smh__who { display: flex; align-items: center; gap: calc(9px * var(--ef-smh-f)); margin-top: auto; padding-top: calc(14px * var(--ef-smh-f)); }\n.ef-smh .ef-smh__av { flex-shrink: 0; width: calc(27px * var(--ef-smh-f)); height: calc(27px * var(--ef-smh-f)); border-radius: 50%; }\n.ef-smh .ef-smh__wn { display: block; font-size: calc(11.5px * var(--ef-smh-f)); font-weight: 500; line-height: 1.25; color: color-mix(in srgb, var(--ef-smh-txt) 92%, var(--ef-smh-glow)); }\n.ef-smh .ef-smh__wo { display: block; margin-top: 2px; font-style: normal; font-size: calc(8px * var(--ef-smh-f)); font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ef-smh-dim); }\n.ef-smh .ef-smh__vdot { position: absolute; top: 11px; right: 12px; width: 6px; height: 6px; border-radius: 50%; background: var(--ef-smh-acc); opacity: 0; box-shadow: 0 0 8px var(--ef-smh-acc); }\n.ef-smh .ef-smh__card.is-seen .ef-smh__vdot { opacity: 1; }\n/* bottom chrome */\n.ef-smh .ef-smh__bottom { position: absolute; left: 0; right: 0; bottom: 0; z-index: 60; display: flex; justify-content: space-between; align-items: center; padding: calc(24px * var(--ef-smh-k)) 44px; opacity: 0; visibility: hidden; }\n.ef-smh .ef-smh__audio { display: flex; align-items: center; gap: 9px; margin: 0; padding: 4px 0; border: 0; background: none; font-family: inherit; font-size: 9px; font-weight: 600; letter-spacing: 0.14em; color: var(--ef-smh-dim); cursor: pointer; }\n.ef-smh .ef-smh__audio svg { width: 34px; height: 14px; }\n.ef-smh .ef-smh__audio path { fill: none; stroke: var(--ef-smh-acc); stroke-width: 1.6; stroke-linecap: round; opacity: 0.85; }\n.ef-smh .ef-smh__audio.is-off path { stroke: var(--ef-smh-dim); opacity: 0.5; }\n.ef-smh .ef-smh__w1, .ef-smh .ef-smh__w2 { animation: ef-smh-wave 2.2s ease-in-out infinite alternate; }\n.ef-smh .ef-smh__w2 { animation-delay: -1.1s; opacity: 0.5; }\n.ef-smh .ef-smh__audio.is-off .ef-smh__w1, .ef-smh .ef-smh__audio.is-off .ef-smh__w2 { animation-play-state: paused; }\n@keyframes ef-smh-wave { from { transform: translateY(-1px); } to { transform: translateY(1px); } }\n.ef-smh .ef-smh__nav { display: flex; align-items: center; gap: 30px; font-size: 13.5px; }\n.ef-smh .ef-smh__sl { color: var(--ef-smh-acc); }\n.ef-smh .ef-smh__home, .ef-smh .ef-smh__a { margin: 0; padding: 0; border: 0; background: none; font: inherit; color: var(--ef-smh-txt); text-decoration: none; opacity: 0.88; cursor: pointer; }\n.ef-smh .ef-smh__home:hover, .ef-smh .ef-smh__a:hover { opacity: 1; }\n.ef-smh .ef-smh__share { padding: 12px 18px; border: 1px solid color-mix(in srgb, var(--ef-smh-txt) 28%, transparent); border-radius: 4px; background: rgba(5, 25, 18, 0.3); font-family: inherit; font-size: 13px; color: var(--ef-smh-txt); cursor: pointer; transition: border-color 0.3s, background 0.3s; }\n.ef-smh .ef-smh__share:hover { border-color: color-mix(in srgb, var(--ef-smh-acc) 55%, transparent); background: color-mix(in srgb, var(--ef-smh-acc) 8%, transparent); }\n.ef-smh .ef-smh__count { display: flex; align-items: center; gap: 8px; font-size: 10px; font-weight: 600; letter-spacing: 0.13em; color: color-mix(in srgb, var(--ef-smh-txt) 80%, var(--ef-smh-glow)); }\n.ef-smh .ef-smh__d { width: 7px; height: 7px; border-radius: 50%; background: var(--ef-smh-acc); box-shadow: 0 0 9px var(--ef-smh-acc); }\n.ef-smh .ef-smh__n { color: #fff; }\n/* small boxes: the original's phone layout */\n.ef-smh.is-compact .ef-smh__head, .ef-smh.is-compact .ef-smh__bottom { padding-left: 20px; padding-right: 20px; }\n.ef-smh.is-compact .ef-smh__nav, .ef-smh.is-compact .ef-smh__top { display: none; }",
    "js": "// Enrichr Library · https://enrichr.dev\n// Serif motion study hero: a dark green hero under a twinkling starfield. The small caps line rises in, each headline word\n// fades up out of a 14px blur (1.15 s expo.out, 0.09 s apart), hand-drawn underlines draw themselves, then the text and\n// the button rise in. The button opens a wall of 33 study cards: they tumble in from deep space, centre first, and land\n// on a concave wall where every card's tilt, depth, blur and brightness come from its place on screen. Drag, swipe,\n// scroll or use the arrow keys to pan the endless wall (with inertia); cards bob gently, lift on hover, and a click (or\n// Enter) marks one as seen. Home flies the cards away and plays the intro again. A glow cursor and an optional ambient\n// synth pad (off by default) come from the original. No GSAP: the tweens are requestAnimationFrame with the same eases.\n// Reduced motion shows everything at rest, opens the wall without the flight and stops the bob, twinkle and streaks.\n// The loop pauses off screen and in hidden tabs.\n// mount(root, { eyebrow, headline (*word* = underlined, / = line break), body, cta, open (\"click\" | \"auto\"),\n//               sound (\"off\" | \"on\"), curve (wall depth, px), tiltX (deg), tiltY (deg), bob (px), stars (count),\n//               accent, background, card, glow, text }) -> { update, open, close, destroy }\nfunction mount(root, o) {\n  var reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  var fine = window.matchMedia(\"(hover: hover) and (pointer: fine)\").matches;\n  var box = root.querySelector(\".ef-smh\");\n  var stars = box.querySelector(\".ef-smh__stars\"), sx = stars.getContext(\"2d\");\n  var field = box.querySelector(\".ef-smh__field\"), hero = box.querySelector(\".ef-smh__hero\");\n  var eyebrow = box.querySelector(\".ef-smh__eyebrow\"), head = box.querySelector(\".ef-smh__h\"), bodyEl = box.querySelector(\".ef-smh__body\"), cta = box.querySelector(\".ef-smh__cta\");\n  var top = box.querySelector(\".ef-smh__top\"), bottom = box.querySelector(\".ef-smh__bottom\");\n  var audioBtn = box.querySelector(\".ef-smh__audio\"), albl = box.querySelector(\".ef-smh__albl\"), homeBtn = box.querySelector(\".ef-smh__home\");\n  var cNum = box.querySelector(\".ef-smh__n\"), cDot = box.querySelector(\".ef-smh__d\");\n  var cursor = box.querySelector(\".ef-smh__cursor\"), hint = box.querySelector(\".ef-smh__hint\");\n  var TITLES = [\"Start with one moving dot\", \"Easing is the whole trick\", \"The pause as a tool\", \"It began as a CodePen\",\n    \"Make room for mistakes\", \"The brief nobody asked for\", \"Let go of the mouse\", \"Motion with a reason\",\n    \"Stillness directs too\", \"Rebuild it to learn it\", \"Version seven was the one\", \"A client who said no\",\n    \"The day I deleted everything\", \"Working with the lights off\", \"Sixty frames or nothing\", \"The best motion goes unseen\",\n    \"The project that chose me\", \"Slow is smooth\", \"Starting over at forty\", \"The first honest critique\",\n    \"A sketch on a napkin\", \"Saying yes without knowing\", \"The bug that paid the rent\", \"Look more, design less\",\n    \"Obsession as a compass\", \"Nothing new under the sun\", \"Delete the third animation\", \"Intuition arrives late\",\n    \"Play, but seriously\", \"The craft of waiting\", \"Respect reduced motion\", \"Designing for my mum\", \"The future was already here\"];\n  var TECH = [\"WEBGL\", \"CANVAS\", \"SVG\", \"CSS 3D\", \"SCROLL\", \"POINTER\", \"TYPE\", \"SHADER\", \"PHYSICS\", \"PARALLAX\", \"EASING\", \"NO LIBRARIES\"];\n  var COLS = 17, ROWS = 2, N = 33;\n\n  var opts = {}, raf = 0, last = 0, visible = true, destroyed = false, io = null, ro = null, hintTimer = 0;\n  var W = 1, H = 1, S = 1, PITCH = 1, TOTALW = 1, cards = [], starList = [], streak = null, nextStreak = 3500;\n  var ox = 0, oy = 0, tox = 0, toy = 0, vx2 = 0, inField = false, opening = false, dragging = false, dragDist = 0, lx = 0, lastDx = 0;\n  var mx = 0, my = 0, hintOn = false, seen = 0, words = [], paths = [], anims = [], actx = null, gainMaster = null, audioOn = false;\n\n  // ---------- eases ----------\n  function expoOut(p) { return p >= 1 ? 1 : 1 - Math.pow(2, -10 * p); }\n  function p2io(p) { return p < 0.5 ? Math.pow(p * 2, 3) / 2 : 1 - Math.pow((1 - p) * 2, 3) / 2; } // power2.inOut\n  function p2in(p) { return p * p * p; }                     // power2.in\n  function p2out(p) { return 1 - Math.pow(1 - p, 3); }       // power2.out\n  function p3out(p) { return 1 - Math.pow(1 - p, 4); }       // power3.out\n  function p1out(p) { return 1 - (1 - p) * (1 - p); }        // power1.out (GSAP default)\n  function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }\n  function hexRgb(h, d) { var m = /^#?([0-9a-f]{6})$/i.exec(h || \"\"); var v = parseInt(m ? m[1] : d, 16); return [v >> 16, (v >> 8) & 255, v & 255]; }\n\n  // ---------- tweens: anim(delay s, duration s, ease, fn(e), done) ----------\n  function anim(delay, dur, ease, fn, done) { var a = { t: -delay, dur: dur, ease: ease, fn: fn, done: done, dead: false }; anims.push(a); wake(); return a; }\n  function stepAnims(dt) {\n    var list = anims.slice();\n    for (var i = 0; i < list.length; i++) {\n      var a = list[i];\n      if (a.dead) continue;\n      a.t += dt;\n      if (a.t < 0) continue;\n      var p = a.dur > 0 ? Math.min(1, a.t / a.dur) : 1;\n      a.fn(a.ease(p));\n      if (p >= 1) { a.dead = true; if (a.done) a.done(); }\n    }\n    anims = anims.filter(function (a) { return !a.dead; });\n  }\n  function killAll(tag) { anims.forEach(function (a) { if (!tag || a.tag === tag) a.dead = true; }); }\n\n  // ---------- procedural duotone \"photos\" and avatars (same generator as the original, tinted green) ----------\n  function photo(seed, w, h) {\n    var c = document.createElement(\"canvas\"); c.width = w; c.height = h;\n    var x = c.getContext(\"2d\");\n    function r(n) { var s = Math.sin(seed * 91.7 + n * 47.3) * 43758.5453; return s - Math.floor(s); }\n    var base = 120 + r(1) * 70;\n    x.fillStyle = \"rgb(\" + base + \",\" + base + \",\" + base + \")\"; x.fillRect(0, 0, w, h);\n    var cx = w * (0.3 + r(2) * 0.4), cy = h * (0.34 + r(3) * 0.2), rad = Math.max(w, h) * (0.3 + r(4) * 0.25), dk = 26 + r(5) * 40;\n    var g = x.createRadialGradient(cx, cy, rad * 0.08, cx, cy, rad);\n    g.addColorStop(0, \"rgba(\" + dk + \",\" + dk + \",\" + dk + \",.95)\");\n    g.addColorStop(0.6, \"rgba(\" + (dk + 60) + \",\" + (dk + 60) + \",\" + (dk + 60) + \",.45)\");\n    g.addColorStop(1, \"rgba(0,0,0,0)\");\n    x.fillStyle = g; x.beginPath(); x.arc(cx, cy, rad, 0, 6.283); x.fill();\n    var g2 = x.createLinearGradient(0, 0, 0, h);\n    g2.addColorStop(0, \"rgba(255,255,255,.14)\"); g2.addColorStop(1, \"rgba(0,0,0,.5)\");\n    x.fillStyle = g2; x.fillRect(0, 0, w, h);\n    var img = x.getImageData(0, 0, w, h), d = img.data;\n    for (var i = 0; i < d.length; i += 4) { var n = (Math.random() - 0.5) * 30, v = d[i] + n; d[i] = v * 0.88; d[i + 1] = v * 0.99; d[i + 2] = v * 0.92; }\n    x.putImageData(img, 0, 0);\n    return c;\n  }\n\n  // ---------- the wall: 33 cards on a virtual plane, wrapped horizontally ----------\n  function buildCards() {\n    field.textContent = \"\"; cards = [];\n    for (var i = 0; i < N; i++) {\n      var hasPhoto = (i % 3) !== 2;\n      var el = document.createElement(\"div\");\n      el.className = \"ef-smh__card\"; el.tabIndex = -1; el.setAttribute(\"role\", \"button\"); el.setAttribute(\"aria-pressed\", \"false\");\n      var nn = (i < 9 ? \"0\" : \"\") + (i + 1), tech = TECH[i % TECH.length];\n      el.setAttribute(\"aria-label\", \"Study \" + nn + \": \" + TITLES[i] + \", \" + tech.toLowerCase());\n      var cin = document.createElement(\"div\"); cin.className = \"ef-smh__cin\";\n      var ph = null;\n      if (hasPhoto) { ph = document.createElement(\"div\"); ph.className = \"ef-smh__ph\"; ph.appendChild(photo(i + 1, 300, 170)); cin.appendChild(ph); }\n      var h3 = document.createElement(\"h3\"); h3.className = \"ef-smh__ct\" + (hasPhoto ? \"\" : \" is-top\"); h3.textContent = TITLES[i]; cin.appendChild(h3);\n      var who = document.createElement(\"div\"); who.className = \"ef-smh__who\";\n      var av = document.createElement(\"canvas\"); av.className = \"ef-smh__av\"; av.width = 54; av.height = 54; av.getContext(\"2d\").drawImage(photo(i + 40, 54, 54), 0, 0);\n      var wd = document.createElement(\"div\");\n      var b = document.createElement(\"b\"); b.className = \"ef-smh__wn\"; b.textContent = \"Study \" + nn;\n      var it = document.createElement(\"i\"); it.className = \"ef-smh__wo\"; it.textContent = tech;\n      wd.appendChild(b); wd.appendChild(it); who.appendChild(av); who.appendChild(wd); cin.appendChild(who);\n      var vd = document.createElement(\"span\"); vd.className = \"ef-smh__vdot\"; cin.appendChild(vd);\n      el.appendChild(cin); field.appendChild(el);\n      var c = { el: el, i: i, ph: ph, hasPhoto: hasPhoto, w: 1, h: 1, baseX: 0, baseY: 0,\n        jz: (Math.random() - 0.5) * 5, bph: Math.random() * 7, it: 0,\n        intro: { fx: (Math.random() - 0.5) * 560, fy: (Math.random() - 0.5) * 400, fz: -1500 - Math.random() * 900,\n          rx: (Math.random() - 0.5) * 140, ry: (Math.random() - 0.5) * 140, rz: (Math.random() - 0.5) * 100 },\n        fl: -1, fb: -1 };\n      el.addEventListener(\"click\", cardClick.bind(null, c));\n      el.addEventListener(\"keydown\", cardKey.bind(null, c));\n      el.addEventListener(\"focus\", cardFocus.bind(null, c));\n      cards.push(c);\n    }\n  }\n  function layoutCards() {\n    var COLW = 228 * S, GAPX = 66 * S, ROWPITCH = 345 * S;\n    PITCH = COLW + GAPX; TOTALW = COLS * PITCH;\n    cards.forEach(function (c) {\n      var col = Math.floor(c.i / ROWS), row = c.i % ROWS;\n      c.w = COLW; c.h = (c.hasPhoto ? 318 : 268) * S + (c.i % 2 ? 14 : -8) * S;\n      c.baseX = col * PITCH - TOTALW / 2 + PITCH / 2 + (row ? PITCH * 0.42 : 0);\n      c.baseY = (row - 0.5) * ROWPITCH + ((col % 2) ? 26 : -26) * S;\n      c.el.style.width = c.w.toFixed(1) + \"px\"; c.el.style.height = c.h.toFixed(1) + \"px\";\n      if (c.ph) c.ph.style.height = (128 * S).toFixed(1) + \"px\";\n    });\n  }\n  function wrapX(x) { var m = (x + TOTALW / 2) % TOTALW; if (m < 0) m += TOTALW; return m - TOTALW / 2; }\n  function wall(t, k60) {\n    if (!dragging) { tox += vx2 * k60; vx2 *= Math.pow(0.94, k60); if (Math.abs(vx2) < 0.04) vx2 = 0; }\n    toy += (Math.min(150, Math.max(-150, toy)) - toy) * (1 - Math.pow(0.86, k60));      // rubber-band y home\n    var f = 1 - Math.pow(1 - 0.085, k60);\n    ox += (tox - ox) * f; oy += (toy - oy) * f;\n    var cx = W / 2, cy = H / 2;\n    for (var i = 0; i < cards.length; i++) {\n      var c = cards[i];\n      var x = wrapX(c.baseX + ox), y = c.baseY + oy * 0.92 + (reduce ? 0 : Math.sin(t / 1400 + c.bph) * opts.bob);\n      var nx = x / (cx * 1.06), ny = y / (cy * 0.95);\n      var it = c.it, inv = 1 - it;\n      // wall pose comes from screen position: tilt toward the centre, push the edges back\n      var rx = -ny * opts.tiltX * it, ry = -nx * opts.tiltY * it, z = -(nx * nx + ny * ny) * opts.curve * it;\n      var px = x * it + c.intro.fx * inv, py = y * it + c.intro.fy * inv, pz = z * it + c.intro.fz * inv;\n      c.el.style.transform = \"translate3d(\" + (cx + px - c.w / 2).toFixed(2) + \"px,\" + (cy + py - c.h / 2).toFixed(2) + \"px,\" + pz.toFixed(1) + \"px) \" +\n        \"rotateX(\" + (rx + c.intro.rx * inv).toFixed(2) + \"deg) rotateY(\" + (ry + c.intro.ry * inv).toFixed(2) + \"deg) rotateZ(\" + (c.jz * it + c.intro.rz * inv).toFixed(2) + \"deg)\";\n      c.el.style.opacity = String(Math.min(1, it * 1.6));\n      // depth of field and edge falloff, rounded so the filter only changes in small steps\n      var r = Math.hypot(nx, ny * 1.15);\n      var bl = Math.round(Math.min(5.5, Math.max(0, (r - 0.68) * 10)) * 2) / 2;\n      var br = Math.round(Math.max(0.62, 1 - Math.max(0, (r - 0.55) * 0.6)) * 20) / 20;\n      if (bl !== c.fl || br !== c.fb) { c.el.style.filter = \"blur(\" + bl + \"px) brightness(\" + br + \")\"; c.fl = bl; c.fb = br; }\n    }\n  }\n\n  // ---------- starfield + shooting streaks ----------\n  function makeStars() {\n    starList = [];\n    for (var i = 0; i < opts.stars; i++) starList.push({ x: Math.random(), y: Math.random(), r: 0.4 + Math.random() * 1.1, a: 0.08 + Math.random() * 0.4, tw: 0.5 + Math.random() * 2, ph: Math.random() * 7, acc: Math.random() < 0.16 });\n  }\n  function drawStars(t, dtMs) {\n    sx.clearRect(0, 0, W, H);\n    var acc = opts.accent, pale = \"#dfe9e1\", k60 = dtMs / 16;\n    for (var i = 0; i < starList.length; i++) {\n      var s = starList[i];\n      sx.globalAlpha = s.a * (0.6 + 0.4 * Math.sin(t / 900 * s.tw + s.ph));\n      sx.fillStyle = s.acc ? acc : pale;\n      sx.fillRect(s.x * W, s.y * H, s.r, s.r);\n    }\n    if (!reduce && dtMs > 0) {\n      nextStreak -= dtMs;\n      if (nextStreak < 0 && !streak) {\n        streak = { x: W * (0.05 + Math.random() * 0.3), y: H * (0.55 + Math.random() * 0.3), t: 0, dx: 2.6 + Math.random() * 1.4, dy: 1.5 + Math.random() * 0.9, len: 90 + Math.random() * 70 };\n        nextStreak = 6000 + Math.random() * 7000;\n      }\n      if (streak) {\n        streak.t += dtMs; streak.x += streak.dx * k60; streak.y += streak.dy * k60;\n        var life = streak.t / 1100, al = Math.sin(Math.min(life, 1) * Math.PI) * 0.5, rgb = hexRgb(acc, \"e6ff55\").join(\",\");\n        var ex = streak.x - streak.dx * streak.len / 3, ey = streak.y - streak.dy * streak.len / 3;\n        var g = sx.createLinearGradient(streak.x, streak.y, ex, ey);\n        g.addColorStop(0, \"rgba(\" + rgb + \",\" + al + \")\"); g.addColorStop(1, \"rgba(\" + rgb + \",0)\");\n        sx.globalAlpha = 1; sx.strokeStyle = g; sx.lineWidth = 1.4;\n        sx.beginPath(); sx.moveTo(streak.x, streak.y); sx.lineTo(ex, ey); sx.stroke();\n        if (life > 1) streak = null;\n      }\n    }\n    sx.globalAlpha = 1;\n  }\n\n  // ---------- headline: words, underlines ----------\n  function buildHeadline() {\n    head.textContent = \"\"; words = []; paths = [];\n    var curves = [\"M2 6 C 28 3.5, 62 8, 98 5\", \"M2 5.5 C 30 8, 70 3.4, 98 6\"];\n    var label = String(opts.headline).replace(/\\*/g, \"\").replace(/\\s*\\/\\s*/g, \" \").replace(/\\s+/g, \" \").trim();\n    head.setAttribute(\"aria-label\", label);\n    var tokens = String(opts.headline).split(/\\s+/).filter(Boolean), first = true;\n    tokens.forEach(function (tok) {\n      if (tok === \"/\") { head.appendChild(document.createElement(\"br\")); first = true; return; }\n      if (!first) head.appendChild(document.createTextNode(\" \"));\n      first = false;\n      var m = /^\\*([^*]+)\\*([^\\w*]*)$/.exec(tok);\n      if (m) {\n        var w = document.createElement(\"span\"); w.className = \"ef-smh__w ef-smh__ul\"; w.setAttribute(\"aria-hidden\", \"true\"); w.textContent = m[1];\n        var svg = document.createElementNS(\"http://www.w3.org/2000/svg\", \"svg\"); svg.setAttribute(\"viewBox\", \"0 0 100 10\"); svg.setAttribute(\"preserveAspectRatio\", \"none\"); svg.setAttribute(\"aria-hidden\", \"true\");\n        var p = document.createElementNS(\"http://www.w3.org/2000/svg\", \"path\"); p.setAttribute(\"d\", curves[paths.length % 2]); svg.appendChild(p); w.appendChild(svg);\n        head.appendChild(w); words.push(w); paths.push(p);\n        if (m[2]) { var pw = document.createElement(\"span\"); pw.className = \"ef-smh__w\"; pw.setAttribute(\"aria-hidden\", \"true\"); pw.textContent = m[2]; head.appendChild(pw); words.push(pw); }\n      } else {\n        var s = document.createElement(\"span\"); s.className = \"ef-smh__w\"; s.setAttribute(\"aria-hidden\", \"true\"); s.textContent = tok.replace(/\\*/g, \"\"); head.appendChild(s); words.push(s);\n      }\n    });\n    paths.forEach(function (p) { var L = p.getTotalLength ? p.getTotalLength() : 100; p.__L = L; p.style.strokeDasharray = L; p.style.strokeDashoffset = L; });\n  }\n  function setRise(el, op, y, blur) {\n    el.style.opacity = String(op);\n    el.style.transform = y ? \"translateY(\" + y.toFixed(2) + \"px)\" : \"\";\n    if (blur !== undefined) el.style.filter = blur > 0.01 ? \"blur(\" + blur.toFixed(2) + \"px)\" : \"\";\n  }\n  // intro: the original GSAP timeline (delay .35), positions resolved to absolute seconds\n  function intro() {\n    killAll(\"intro\");\n    hero.style.display = \"\";\n    var D = 0.35, n = words.length, m = paths.length;\n    function a(delay, dur, ease, fn) { var x = anim(D + delay, dur, ease, fn); x.tag = \"intro\"; return x; }\n    if (reduce) {\n      setRise(eyebrow, 1, 0); words.forEach(function (w) { setRise(w, 1, 0, 0); }); paths.forEach(function (p) { p.style.strokeDashoffset = 0; });\n      setRise(bodyEl, 1, 0); setRise(cta, 1, 0); autoOpen(0.5); return;\n    }\n    setRise(eyebrow, 0, 14); words.forEach(function (w) { setRise(w, 0, 26, 14); }); paths.forEach(function (p) { p.style.strokeDashoffset = p.__L; });\n    setRise(bodyEl, 0, 16); setRise(cta, 0, 18);\n    a(0, 0.9, expoOut, function (e) { setRise(eyebrow, e, 14 * (1 - e)); });\n    var wStart = 0.9 - 0.55;                                            // '-=.55'\n    words.forEach(function (w, i) { a(wStart + i * 0.09, 1.15, expoOut, function (e) { setRise(w, e, 26 * (1 - e), 14 * (1 - e)); }); });\n    var wEnd = wStart + Math.max(0, n - 1) * 0.09 + 1.15, uStart = wEnd - 0.5;  // '-=.5'\n    paths.forEach(function (p, i) { a(uStart + i * 0.18, 0.85, p2io, function (e) { p.style.strokeDashoffset = (p.__L * (1 - e)).toFixed(2); }); });\n    var uEnd = uStart + Math.max(0, m - 1) * 0.18 + 0.85, bStart = Math.max(wEnd, uEnd) - 0.7;  // '-=.7'\n    a(bStart, 0.8, expoOut, function (e) { setRise(bodyEl, e, 16 * (1 - e)); });\n    var cStart = bStart + 0.8 - 0.55;                                   // '-=.55'\n    a(cStart, 0.8, expoOut, function (e) { setRise(cta, e, 18 * (1 - e)); });\n    autoOpen(D + cStart + 0.8);\n  }\n  function autoOpen(at) {\n    if (opts.open !== \"auto\") return;\n    var x = anim(at + 1.2, 0, p1out, function () {}, function () { open(); }); x.tag = \"intro\";\n  }\n\n  // ---------- open the wall (the original CTA handler) ----------\n  function open() {\n    if (inField || opening || destroyed) return;\n    killAll(\"intro\"); killAll(\"close\");\n    if (opts.sound === \"on\" && !actx) setAudio(true);\n    field.style.visibility = \"visible\";\n    var focusWall = box.contains(document.activeElement) && document.activeElement !== document.body;\n    if (!reduce) {\n      words.forEach(function (w, i) { anim(i * 0.03, 0.6, p2in, function (e) { setRise(w, 1 - e, -30 * e, 10 * e); }).tag = \"out\"; });\n      [eyebrow, bodyEl, cta].forEach(function (el) { var s0 = Number(el.style.opacity || 1); anim(0.1, 0.45, p1out, function (e) { el.style.opacity = String(s0 * (1 - e)); }).tag = \"out\"; });\n      anim(0, 0.6 + Math.max(0, words.length - 1) * 0.03, p1out, function () {}, function () { hero.style.display = \"none\"; }).tag = \"out\";\n    } else hero.style.display = \"none\";\n    // swarm: centre-out, from deep z with a tumble, onto the wall\n    var order = cards.slice().sort(function (a, b) { return Math.abs(a.baseX) - Math.abs(b.baseX); });\n    order.forEach(function (c, k) {\n      c.el.tabIndex = 0;\n      if (reduce) { c.it = 1; return; }\n      anim(0.45 + k * 0.028 + Math.random() * 0.06, 1.6, p3out, function (e) { c.it = e; }).tag = \"swarm\";\n    });\n    inField = true;\n    bottom.style.visibility = \"visible\";\n    if (reduce) { top.style.opacity = \"1\"; bottom.style.opacity = \"1\"; }\n    else {\n      anim(1.1, 0.9, expoOut, function (e) { top.style.opacity = String(e); top.style.transform = \"translate(-50%,\" + (-12 * (1 - e)).toFixed(2) + \"px)\"; }).tag = \"chrome\";\n      anim(1.3, 0.9, expoOut, function (e) { bottom.style.opacity = String(e); bottom.style.transform = \"translateY(\" + (16 * (1 - e)).toFixed(2) + \"px)\"; }).tag = \"chrome\";\n    }\n    if (fine && !reduce) { clearTimeout(hintTimer); hintTimer = setTimeout(showHint, 1600); }\n    if (focusWall) field.focus({ preventScroll: true });\n  }\n  // Home: the cards fall back into the distance, then the hero plays its intro again\n  function close() {\n    if (!inField || destroyed) return;\n    killAll(\"swarm\"); killAll(\"chrome\"); killAll(\"out\"); hideHint();\n    var homeFocus = document.activeElement === homeBtn;\n    var order = cards.slice().sort(function (a, b) { return Math.abs(b.baseX) - Math.abs(a.baseX); });\n    var dur = reduce ? 0 : 0.9;\n    order.forEach(function (c, k) { c.el.tabIndex = -1; var s0 = c.it; anim(reduce ? 0 : k * 0.012, dur, p2in, function (e) { c.it = s0 * (1 - e); }).tag = \"close\"; });\n    var t0 = Number(bottom.style.opacity || 1);\n    anim(0, reduce ? 0 : 0.4, p1out, function (e) { bottom.style.opacity = String(t0 * (1 - e)); top.style.opacity = String(t0 * (1 - e)); }).tag = \"close\";\n    anim(reduce ? 0 : 0.9 + N * 0.012, 0, p1out, function () {}, function () {\n      inField = false; field.style.visibility = \"hidden\"; bottom.style.visibility = \"hidden\";\n      tox = ox = 0; vx2 = 0;\n      intro();\n      if (homeFocus) cta.focus({ preventScroll: true });\n    }).tag = \"close\";\n  }\n\n  // ---------- cards: seen state, keyboard ----------\n  function markSeen(c) {\n    if (c.el.classList.contains(\"is-seen\")) return;\n    c.el.classList.add(\"is-seen\"); c.el.setAttribute(\"aria-pressed\", \"true\"); seen++;\n    cNum.textContent = String(seen);\n    if (!reduce) anim(0, 0.44, function (p) { return p; }, function (p) { var e = p < 0.5 ? p2out(p * 2) : p2out((1 - p) * 2); cDot.style.transform = \"scale(\" + (1 + 0.9 * e).toFixed(3) + \")\"; });\n  }\n  function cardClick(c) { if (dragDist > 6 || !inField) return; markSeen(c); }\n  function cardKey(c, e) { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); markSeen(c); } }\n  function cardFocus(c) { if (!inField || dragging) return; tox -= wrapX(c.baseX + tox); vx2 = 0; }   // bring the focused card to the centre\n  function fieldKey(e) {\n    if (!inField) return;\n    if (e.key === \"ArrowLeft\") { tox += PITCH; e.preventDefault(); hideHint(); }\n    else if (e.key === \"ArrowRight\") { tox -= PITCH; e.preventDefault(); hideHint(); }\n  }\n\n  // ---------- input: drag + wheel, cursor, hint ----------\n  function down(e) {\n    if (!inField || e.button > 0 || (e.target.closest && e.target.closest(\"button,a,.ef-smh__audio,.ef-smh__nav\"))) return;\n    dragging = true; dragDist = 0; lx = e.clientX; vx2 = 0; lastDx = 0; hideHint();\n    window.addEventListener(\"pointermove\", drag); window.addEventListener(\"pointerup\", up); window.addEventListener(\"pointercancel\", up);\n  }\n  function drag(e) { var dx = e.clientX - lx; lx = e.clientX; tox += dx; lastDx = dx; dragDist += Math.abs(dx); }\n  function up() {\n    if (dragging) { vx2 = lastDx * 0.9; lastDx = 0; }\n    dragging = false;\n    window.removeEventListener(\"pointermove\", drag); window.removeEventListener(\"pointerup\", up); window.removeEventListener(\"pointercancel\", up);\n  }\n  function wheel(e) { if (!inField) return; tox -= e.deltaY * 0.9 + e.deltaX * 0.9; hideHint(); }\n  function hover(e) {\n    var r = box.getBoundingClientRect(); mx = e.clientX - r.left; my = e.clientY - r.top;\n    box.classList.add(\"is-in\");\n    if (fine) {\n      cursor.style.transform = \"translate(\" + mx.toFixed(1) + \"px,\" + my.toFixed(1) + \"px)\";\n      if (hintOn) hint.style.transform = \"translate(\" + (mx + 24).toFixed(1) + \"px,\" + my.toFixed(1) + \"px) translateY(-50%)\";\n      cursor.classList.toggle(\"is-big\", !!(e.target.closest && e.target.closest(\".ef-smh__card,button,a,.ef-smh__audio\")));\n    }\n  }\n  function leave() { box.classList.remove(\"is-in\"); }\n  // tabbing to a card off to the side would scroll the clipped box; keep it pinned (the wall pans to the card instead)\n  function pin() { if (box.scrollLeft || box.scrollTop) { box.scrollLeft = 0; box.scrollTop = 0; } }\n  function showHint() {\n    hintOn = true;\n    hint.style.transform = \"translate(\" + (mx + 24).toFixed(1) + \"px,\" + my.toFixed(1) + \"px) translateY(-50%)\";\n    killAll(\"hint\"); var s0 = Number(hint.style.opacity || 0);\n    anim(0.9, 0.5, p1out, function (e) { hint.style.opacity = String(s0 + (1 - s0) * e); }).tag = \"hint\";\n  }\n  function hideHint() {\n    clearTimeout(hintTimer);\n    if (!hintOn) return; hintOn = false;\n    killAll(\"hint\"); var s0 = Number(hint.style.opacity || 0);\n    anim(0, 0.35, p1out, function (e) { hint.style.opacity = String(s0 * (1 - e)); }).tag = \"hint\";\n  }\n\n  // ---------- ambient pad: four detuned sines through a low-pass, about -30 dBFS, only after a click ----------\n  function setAudio(on) {\n    audioOn = on;\n    audioBtn.classList.toggle(\"is-off\", !on); audioBtn.setAttribute(\"aria-pressed\", on ? \"true\" : \"false\"); albl.textContent = on ? \"ON\" : \"OFF\";\n    try {\n      if (on && !actx) {\n        var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;\n        actx = new AC();\n        gainMaster = actx.createGain(); gainMaster.gain.value = 0;\n        var lp = actx.createBiquadFilter(); lp.type = \"lowpass\"; lp.frequency.value = 340;\n        gainMaster.connect(lp); lp.connect(actx.destination);\n        [[110, 0.5], [164.81, 0.32], [220, 0.22], [329.63, 0.12]].forEach(function (fg) {\n          var osc = actx.createOscillator(), og = actx.createGain();\n          osc.type = \"sine\"; osc.frequency.value = fg[0]; og.gain.value = fg[1] * 0.028;\n          osc.connect(og); og.connect(gainMaster); osc.start();\n          var lfo = actx.createOscillator(), lg = actx.createGain();\n          lfo.frequency.value = 0.06 + Math.random() * 0.05; lg.gain.value = fg[0] * 0.004;\n          lfo.connect(lg); lg.connect(osc.frequency); lfo.start();\n        });\n        gainMaster.gain.linearRampToValueAtTime(1, actx.currentTime + 3);\n      } else if (actx) {\n        gainMaster.gain.cancelScheduledValues(actx.currentTime);\n        gainMaster.gain.setValueAtTime(gainMaster.gain.value, actx.currentTime);\n        gainMaster.gain.linearRampToValueAtTime(on ? 1 : 0, actx.currentTime + 0.8);\n      }\n    } catch (err) { /* no audio: the toggle still works visually */ }\n  }\n  function audioClick() { setAudio(!audioOn); }\n\n  // ---------- loop ----------\n  function wake() { if (!raf && !destroyed) { last = performance.now(); raf = requestAnimationFrame(loop); } }\n  function loop(now) {\n    raf = 0;\n    var dtMs = Math.min(50, now - last || 0); last = now;\n    if (!visible || document.hidden) return;   // the IntersectionObserver wakes it again\n    stepAnims(dtMs / 1000);\n    if (!reduce) drawStars(now, dtMs);\n    if (inField) wall(now, dtMs / 16.667);\n    if (!reduce || inField || anims.length) raf = requestAnimationFrame(loop);\n  }\n  function layout() {\n    W = Math.max(1, box.clientWidth); H = Math.max(1, box.clientHeight);\n    var dpr = Math.min(window.devicePixelRatio || 1, 2);\n    stars.width = Math.round(W * dpr); stars.height = Math.round(H * dpr); sx.setTransform(dpr, 0, 0, dpr, 0, 0);\n    S = Math.min(1, Math.max(0.5, Math.min(W / 1500, H / 845)));\n    box.style.setProperty(\"--ef-smh-f\", Math.max(0.6, Math.min(1.17, S / 0.853)).toFixed(3));\n    box.style.setProperty(\"--ef-smh-fs\", Math.max(30, Math.min(104, 0.076 * W, 0.135 * H)).toFixed(1) + \"px\");\n    box.style.setProperty(\"--ef-smh-k\", Math.max(0.55, Math.min(1, H / 720)).toFixed(3));\n    box.classList.toggle(\"is-compact\", W < 760);\n    layoutCards();\n    if (reduce) { drawStars(0, 0); if (inField) wall(0, 1); }\n  }\n  function update(partial) {\n    var prev = opts;\n    opts = Object.assign({ eyebrow: \"THE MOTION ARCHIVE\", headline: \"Great motion is *studied* / before it is *built*.\", body: \"Thirty-three small studies from the Enrichr Library, each rebuilt by hand in plain code. Open the wall and drag through what inspired them.\",\n      cta: \"Open the motion archive\", open: \"click\", sound: \"off\", curve: 170, tiltX: 11, tiltY: 21, bob: 5, stars: 150,\n      accent: \"#e6ff55\", background: \"#0c1e18\", card: \"#0f3628\", glow: \"#1a7a4c\", text: \"#fbf8f1\" }, opts, partial || {});\n    opts.curve = num(opts.curve, 170); opts.tiltX = num(opts.tiltX, 11); opts.tiltY = num(opts.tiltY, 21); opts.bob = Math.max(0, num(opts.bob, 5));\n    opts.stars = Math.max(0, Math.min(600, Math.round(num(opts.stars, 150))));\n    box.style.setProperty(\"--ef-smh-acc\", opts.accent); box.style.setProperty(\"--ef-smh-bg\", opts.background);\n    box.style.setProperty(\"--ef-smh-card\", opts.card); box.style.setProperty(\"--ef-smh-glow\", opts.glow); box.style.setProperty(\"--ef-smh-txt\", opts.text);\n    box.style.setProperty(\"--ef-smh-ink\", opts.background);\n    eyebrow.textContent = opts.eyebrow; bodyEl.textContent = opts.body; cta.textContent = opts.cta;\n    if (prev.stars !== opts.stars) makeStars();\n    if (prev.headline !== opts.headline) {\n      buildHeadline();\n      if (prev.headline !== undefined && !destroyed) {\n        if (inField) words.forEach(function (w) { setRise(w, 0, 0, 0); });   // hidden while the wall is open; the next intro shows them\n        else intro();\n      }\n    }\n    if (prev.open !== undefined && prev.open !== opts.open && opts.open === \"auto\" && !inField && !destroyed) intro();\n    if (reduce && !destroyed) { drawStars(0, 0); if (inField) wall(0, 1); }\n  }\n\n  buildCards();\n  update(o);\n  layout();\n  if (fine) box.classList.add(\"is-fine\");\n  audioBtn.classList.add(\"is-off\");\n  cta.addEventListener(\"click\", open);\n  homeBtn.addEventListener(\"click\", close);\n  audioBtn.addEventListener(\"click\", audioClick);\n  box.addEventListener(\"pointerdown\", down);\n  box.addEventListener(\"pointermove\", hover);\n  box.addEventListener(\"pointerleave\", leave);\n  box.addEventListener(\"wheel\", wheel, { passive: true });\n  field.addEventListener(\"keydown\", fieldKey);\n  box.addEventListener(\"scroll\", pin);\n  intro();\n  if (reduce) drawStars(0, 0);\n  if (\"IntersectionObserver\" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; if (visible) wake(); }); io.observe(box); }\n  if (\"ResizeObserver\" in window) { ro = new ResizeObserver(function () { if (!destroyed) layout(); }); ro.observe(box); }\n  document.addEventListener(\"visibilitychange\", wake);\n  wake();\n  return {\n    update: update, open: open, close: close,\n    destroy: function () {\n      destroyed = true; cancelAnimationFrame(raf); raf = 0; clearTimeout(hintTimer); anims = [];\n      if (io) io.disconnect();\n      if (ro) ro.disconnect();\n      up();\n      cta.removeEventListener(\"click\", open); homeBtn.removeEventListener(\"click\", close); audioBtn.removeEventListener(\"click\", audioClick);\n      box.removeEventListener(\"pointerdown\", down); box.removeEventListener(\"pointermove\", hover); box.removeEventListener(\"pointerleave\", leave);\n      box.removeEventListener(\"wheel\", wheel); field.removeEventListener(\"keydown\", fieldKey); box.removeEventListener(\"scroll\", pin);\n      document.removeEventListener(\"visibilitychange\", wake);\n      if (actx) { try { actx.close(); } catch (err) {} actx = null; }\n    }\n  };\n}"
  }
}