{
  "id": "refractive-glass-lens",
  "kind": "element",
  "title": "Refractive glass lens",
  "summary": "A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.",
  "url": "https://enrichr.dev/elements/refractive-glass-lens.html",
  "section": "page",
  "techniques": [
    "pointer-interaction"
  ],
  "stack": [
    "Vanilla JS",
    "SVG filter",
    "CSS"
  ],
  "frameworks": [
    "React",
    "Next.js",
    "Vanilla"
  ],
  "mood": [
    "cinematic",
    "technical"
  ],
  "intents": [
    "liquid glass cursor lens",
    "chromatic aberration hover effect",
    "backdrop filter svg displacement",
    "refraction magnifier following the mouse",
    "prism lens cursor over text"
  ],
  "status": "tested",
  "params": [
    {
      "key": "title",
      "label": "Headline (*stars* colour a phrase)",
      "type": "text",
      "value": "Optical glass with *chromatic aberration*"
    },
    {
      "key": "mode",
      "label": "Lens",
      "type": "select",
      "options": [
        [
          "refract",
          "Refract (prism)"
        ],
        [
          "invert",
          "Invert"
        ]
      ],
      "value": "refract"
    },
    {
      "key": "warp",
      "label": "Refraction warp",
      "type": "range",
      "min": 0,
      "max": 120,
      "step": 1,
      "value": 35
    },
    {
      "key": "fringe",
      "label": "Colour fringe (px)",
      "type": "range",
      "min": 0,
      "max": 24,
      "step": 1,
      "value": 6
    },
    {
      "key": "noise",
      "label": "Turbulence frequency",
      "type": "range",
      "min": 0.002,
      "max": 0.06,
      "step": 0.001,
      "value": 0.018
    },
    {
      "key": "size",
      "label": "Lens size (px)",
      "type": "range",
      "min": 96,
      "max": 320,
      "step": 4,
      "value": 192
    },
    {
      "key": "shape",
      "label": "Shape",
      "type": "select",
      "options": [
        [
          "circle",
          "Circle"
        ],
        [
          "square",
          "Rounded square"
        ],
        [
          "pill",
          "Pill"
        ],
        [
          "leaf",
          "Leaf"
        ]
      ],
      "value": "circle"
    },
    {
      "key": "smoothing",
      "label": "Follow (1 = no lag)",
      "type": "range",
      "min": 0.05,
      "max": 1,
      "step": 0.01,
      "value": 0.16
    }
  ],
  "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 \"Refractive glass lens\" element to this project.\n\nThis is the \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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/refractive-glass-lens.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 \"Refractive glass lens\" 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 \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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 \"Refractive glass lens\" element to this codebase.\n\nThis is the \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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-refractive-glass-lens.mdc so the rule stays with the project:\n---\ndescription: How to use the Enrichr Refractive glass lens 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 \"Refractive glass lens\" element. Tailwind is available but this component needs no utility classes.\n\nThis is the \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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- refractive-glass-lens.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 \"./refractive-glass-lens\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function RefractiveGlassLens(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 \"Refractive glass lens\" element to this React + Vite + TypeScript + Tailwind project. Do not install any packages.\n\nThis is the \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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- refractive-glass-lens.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/RefractiveGlassLens.tsx):\n```tsx\n\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { markup, mount } from \"./refractive-glass-lens\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function RefractiveGlassLens(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 \"Refractive glass lens\" element.\n\nThis is the \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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), refractive-glass-lens.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 \"Refractive glass lens\" element. No packages, no server code.\n\nThis is the \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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 refractive-glass-lens.js and then main.js), style.css (the CSS below), refractive-glass-lens.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 \"Refractive glass lens\" 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 \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.json\n\nWhat it does: The lens is an empty element with backdrop-filter pointing at an inline SVG filter, so the filter runs on whatever is painted behind the lens. Inside the filter, feTurbulence makes a fractal noise map and feDisplacementMap uses it to shift the backdrop's pixels (the warp setting). The bent image is then split three ways: feColorMatrix keeps only red from a copy moved left by the fringe distance, only blue from a copy moved right, and only green from the centre. Two feBlend steps in screen mode add the channels back together, so edges show red and blue fringes like cheap glass. Each frame the lens eases toward the pointer; its speed and direction stretch it along the path of travel and rotate it to match, then it relaxes back to round. Box shadows draw the rim, glare and bevel.\nAccessibility: The lens is decorative, hidden from assistive technology and ignores pointer events, so text under it can still be selected and clicked. The content is real text. With reduced motion the lens jumps to the pointer without lag or squash. Refraction makes text under the lens hard to read on purpose, so don't rely on the lens to reveal anything important.\n\nTunable settings:\n- Headline (*stars* colour a phrase): text, default Optical glass with *chromatic aberration*\n- Lens: select (Refract (prism) / Invert), default refract\n- Refraction warp: range 0 to 120, step 1, default 35\n- Colour fringe (px): range 0 to 24, step 1, default 6\n- Turbulence frequency: range 0.002 to 0.06, step 0.001, default 0.018\n- Lens size (px): range 96 to 320, step 4, default 192\n- Shape: select (Circle / Rounded square / Pill / Leaf), default circle\n- Follow (1 = no lag): range 0.05 to 1, step 0.01, default 0.16\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/refractive-glass-lens.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 \"Refractive glass lens\" 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 \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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 RefractiveGlassLens.\n2. Paste the JavaScript below into a separate code file named refractive-glass-lens.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 \"./refractive-glass-lens\"; // markup is the HTML string; mount is the JavaScript's mount function\n\nexport default function RefractiveGlassLens(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(RefractiveGlassLens, {\n  title: { type: ControlType.String, title: \"Headline (*stars* colour a phrase)\", defaultValue: \"Optical glass with *chromatic aberration*\" },\n  mode: { type: ControlType.Enum, title: \"Lens\", options: [\"refract\",\"invert\"], optionTitles: [\"Refract (prism)\",\"Invert\"], defaultValue: \"refract\" },\n  warp: { type: ControlType.Number, title: \"Refraction warp\", min: 0, max: 120, step: 1, defaultValue: 35 },\n  fringe: { type: ControlType.Number, title: \"Colour fringe\", min: 0, max: 24, step: 1, defaultValue: 6 },\n  noise: { type: ControlType.Number, title: \"Turbulence frequency\", min: 0.002, max: 0.06, step: 0.001, defaultValue: 0.018 },\n  size: { type: ControlType.Number, title: \"Lens size\", min: 96, max: 320, step: 4, defaultValue: 192 },\n  shape: { type: ControlType.Enum, title: \"Shape\", options: [\"circle\",\"square\",\"pill\",\"leaf\"], optionTitles: [\"Circle\",\"Rounded square\",\"Pill\",\"Leaf\"], defaultValue: \"circle\" },\n  smoothing: { type: ControlType.Number, title: \"Follow\", min: 0.05, max: 1, step: 0.01, defaultValue: 0.16 },\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 \"Refractive glass lens\" 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 \"Refractive glass lens\" element from the Enrichr Library: A glass lens follows the pointer and bends the page behind it, splitting red, green and blue apart like a prism, and squashes as it moves.\nPage: https://enrichr.dev/elements/refractive-glass-lens.html\nMachine-readable spec: https://enrichr.dev/elements/refractive-glass-lens.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-refractive-glass-lens\"></div>, and a <script> block with the JavaScript followed by:\n   mount(document.getElementById(\"enrichr-refractive-glass-lens\"), { ...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 lens is decorative, hidden from assistive technology and ignores pointer events, so text under it can still be selected and clicked. The content is real text. With reduced motion the lens jumps to the pointer without lag or squash. Refraction makes text under the lens hard to read on purpose, so don't rely on the lens to reveal anything important.",
  "performance": "SVG filters in backdrop-filter repaint the area under the lens whenever it moves, which is the expensive part. The movement loop runs only while the lens is still travelling or squashed and pauses off screen. Keep the lens under about 260px and the turbulence octaves at 3 on low-end devices. Full refraction needs a Chromium browser (Chrome, Edge, Brave, Opera); Safari and Firefox do not render SVG filters as backdrop filters, so they get a frosted blur lens instead, and the invert mode works everywhere.",
  "dependencies": [],
  "source": {
    "markup": "<div class=\"ef-rgl\"><div class=\"ef-rgl__zone\"><div class=\"ef-rgl__scene\"><span class=\"ef-rgl__kicker\">Optical filter</span><h2 class=\"ef-rgl__t\"></h2><p class=\"ef-rgl__p\"></p><ul class=\"ef-rgl__chips\" aria-hidden=\"true\"><li>Red</li><li>Green</li><li>Blue</li><li>Glass</li></ul></div><div class=\"ef-rgl__lens\" aria-hidden=\"true\"></div></div></div>",
    "css": "/* Enrichr Library · https://enrichr.dev */\n.ef-rgl__zone { position: relative; isolation: isolate; overflow: hidden; min-height: 22rem; display: flex; align-items: center; border-radius: 1rem; touch-action: pan-y; container-type: inline-size; background-color: var(--ef-bg, #0b1120); background-image: linear-gradient(rgba(148, 163, 184, 0.09) 1px, transparent 1px), linear-gradient(90deg, rgba(148, 163, 184, 0.09) 1px, transparent 1px); background-size: 40px 40px; color: #e2e8f0; }\n.ef-rgl__scene { position: relative; z-index: 1; padding: 2.4rem clamp(1.2rem, 5vw, 3rem); max-width: 46rem; }\n.ef-rgl__kicker { display: inline-block; padding: 0.3rem 0.7rem; border: 1px solid rgba(34, 211, 238, 0.4); border-radius: 999px; color: var(--ef-accent, #22d3ee); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }\n.ef-rgl .ef-rgl__t { margin: 1rem 0 0.8rem; font-size: clamp(1.8rem, 6vw, 3.6rem); font-size: clamp(1.8rem, 7.5cqi, 3.6rem); font-weight: 800; line-height: 1.05; letter-spacing: -0.02em; }\n.ef-rgl__t em { font-style: normal; background: linear-gradient(90deg, var(--ef-accent, #22d3ee), #a78bfa 55%, #f472b6); -webkit-background-clip: text; background-clip: text; color: transparent; }\n.ef-rgl .ef-rgl__p { margin: 0; opacity: 1; max-width: 34rem; color: #94a3b8; font-size: 1.05rem; line-height: 1.55; }\n.ef-rgl .ef-rgl__chips { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 1.4rem 0 0; padding: 0; list-style: none; }\n.ef-rgl .ef-rgl__chips li { margin: 0; opacity: 1; padding: 0.55rem 1rem; border-radius: 0.6rem; font-size: 0.85rem; font-weight: 700; color: #0b1120; background: #f87171; }\n.ef-rgl .ef-rgl__chips li:nth-child(2) { background: #4ade80; }\n.ef-rgl .ef-rgl__chips li:nth-child(3) { background: #60a5fa; }\n.ef-rgl .ef-rgl__chips li:nth-child(4) { background: #f8fafc; }\n.ef-rgl__lens { position: absolute; left: 0; top: 0; z-index: 3; width: var(--ef-w, 192px); height: var(--ef-h, 192px); border-radius: var(--ef-radius, 50%); pointer-events: none; will-change: transform; background: rgba(255, 255, 255, 0.03); -webkit-backdrop-filter: blur(3px) saturate(1.6); backdrop-filter: blur(3px) saturate(1.6); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.35), inset 0 0 20px rgba(255, 255, 255, 0.25), inset 0 2px 4px rgba(255, 255, 255, 0.6), inset 0 -4px 8px rgba(0, 0, 0, 0.4); transition: width 0.2s cubic-bezier(0.16, 1, 0.3, 1), height 0.2s cubic-bezier(0.16, 1, 0.3, 1), border-radius 0.2s ease; }\n@media (prefers-reduced-motion: reduce) { .ef-rgl__lens { transition: none; } }",
    "js": "// Enrichr Library · https://enrichr.dev\n// Refractive glass lens: a glass disc follows the pointer and bends whatever is behind it. An SVG filter used as a\n// backdrop-filter warps the backdrop with turbulence noise, then splits it into red, green and blue copies shifted\n// sideways (chromatic aberration) and screens them back together. The lens squashes along its direction of travel.\n// Chromium browsers get the full refraction; others get a frosted-glass fallback. \"Invert\" mode swaps in an inverting lens.\n// mount(root, { title (wrap a phrase in *stars* to colour it), text, mode (\"refract\" | \"invert\"), warp, fringe (px), noise, size (px), shape, smoothing, accent, background })\nfunction mount(root, o) {\n  var reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  var zone = root.querySelector(\".ef-rgl__zone\"), lens = root.querySelector(\".ef-rgl__lens\");\n  var title = root.querySelector(\".ef-rgl__t\"), text = root.querySelector(\".ef-rgl__p\");\n  var svgOk = !!window.chrome && /Chrome\\//.test(navigator.userAgent);\n  var NS = \"http://www.w3.org/2000/svg\", id = \"ef-rgl-\" + Math.random().toString(36).slice(2, 8);\n  function el(tag, attrs, parent) {\n    var n = document.createElementNS(NS, tag);\n    for (var k in attrs) n.setAttribute(k, attrs[k]);\n    if (parent) parent.appendChild(n);\n    return n;\n  }\n  var svg = el(\"svg\", { \"aria-hidden\": \"true\", width: \"0\", height: \"0\" });\n  svg.style.cssText = \"position:absolute;width:0;height:0;pointer-events:none\";\n  var f = el(\"filter\", { id: id, x: \"-30%\", y: \"-30%\", width: \"160%\", height: \"160%\" }, svg);\n  var turb = el(\"feTurbulence\", { type: \"fractalNoise\", baseFrequency: \"0.018\", numOctaves: \"3\", seed: \"5\", result: \"noise\" }, f);\n  var disp = el(\"feDisplacementMap\", { \"in\": \"SourceGraphic\", in2: \"noise\", scale: \"35\", xChannelSelector: \"R\", yChannelSelector: \"G\", result: \"bent\" }, f);\n  var red = el(\"feOffset\", { \"in\": \"bent\", dx: \"-6\", dy: \"0\", result: \"rOff\" }, f);\n  el(\"feColorMatrix\", { \"in\": \"rOff\", type: \"matrix\", values: \"1 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 1 0\", result: \"r\" }, f);\n  var blue = el(\"feOffset\", { \"in\": \"bent\", dx: \"6\", dy: \"0\", result: \"bOff\" }, f);\n  el(\"feColorMatrix\", { \"in\": \"bOff\", type: \"matrix\", values: \"0 0 0 0 0  0 0 0 0 0  0 0 1 0 0  0 0 0 1 0\", result: \"b\" }, f);\n  el(\"feColorMatrix\", { \"in\": \"bent\", type: \"matrix\", values: \"0 0 0 0 0  0 1 0 0 0  0 0 0 0 0  0 0 0 1 0\", result: \"g\" }, f);\n  el(\"feBlend\", { \"in\": \"r\", in2: \"g\", mode: \"screen\", result: \"rg\" }, f);\n  el(\"feBlend\", { \"in\": \"rg\", in2: \"b\", mode: \"screen\" }, f);\n  zone.appendChild(svg);\n\n  var opts = {}, raf = 0, visible = true, io = null, ro = null, placed = false;\n  var tx = 0, ty = 0, x = 0, y = 0, sx = 1, sy = 1, ang = 0;\n\n  // Before the pointer arrives, park the lens over the headline so the effect is visible straight away.\n  function rest() {\n    if (placed) return;\n    var z = zone.getBoundingClientRect(), t = title.getBoundingClientRect();\n    tx = x = Math.min(z.width * 0.62, t.left - z.left + t.width * 0.55);\n    ty = y = t.top - z.top + t.height / 2;\n  }\n  function paint() {\n    lens.style.transform = \"translate3d(\" + x.toFixed(1) + \"px,\" + y.toFixed(1) + \"px,0) translate(-50%,-50%) rotate(\" + ang.toFixed(1) + \"deg) scale(\" + sx.toFixed(3) + \",\" + sy.toFixed(3) + \")\";\n  }\n  function frame() {\n    raf = 0;\n    if (!visible) return;\n    var k = reduce ? 1 : opts.smoothing;\n    var px = x, py = y;\n    x += (tx - x) * k;\n    y += (ty - y) * k;\n    var vx = x - px, vy = y - py, speed = Math.sqrt(vx * vx + vy * vy);\n    if (!reduce && speed > 0.5) {\n      var s = Math.min(speed * 0.015, 0.35);\n      sx = 1 + s; sy = 1 - s;\n      ang = Math.atan2(vy, vx) * 180 / Math.PI;\n    } else {\n      sx += (1 - sx) * 0.3; sy += (1 - sy) * 0.3;\n    }\n    paint();\n    if (Math.abs(tx - x) + Math.abs(ty - y) > 0.3 || Math.abs(1 - sx) > 0.002) raf = requestAnimationFrame(frame);\n  }\n  function wake() { if (!raf && visible) raf = requestAnimationFrame(frame); }\n  function move(e) {\n    var r = zone.getBoundingClientRect();\n    tx = e.clientX - r.left;\n    ty = e.clientY - r.top;\n    placed = true;\n    wake();\n  }\n  function heading(s) {\n    title.textContent = \"\";\n    String(s).split(\"*\").forEach(function (part, i) {\n      if (!part) return;\n      if (i % 2) { var em = document.createElement(\"em\"); em.textContent = part; title.appendChild(em); }\n      else title.appendChild(document.createTextNode(part));\n    });\n  }\n  function update(next) {\n    opts = Object.assign({ title: \"Optical glass with *chromatic aberration*\", text: \"Move the pointer across the text and chips: the lens bends the light and pulls red, green and blue apart.\", mode: \"refract\", warp: 35, fringe: 6, noise: 0.018, size: 192, shape: \"circle\", smoothing: 0.16, accent: \"#22d3ee\", background: \"#0b1120\" }, opts, next || {});\n    opts.smoothing = Math.max(0.02, Math.min(1, Number(opts.smoothing)));\n    heading(opts.title);\n    text.textContent = opts.text;\n    zone.style.setProperty(\"--ef-bg\", opts.background);\n    zone.style.setProperty(\"--ef-accent\", opts.accent);\n    disp.setAttribute(\"scale\", String(Number(opts.warp)));\n    turb.setAttribute(\"baseFrequency\", String(Number(opts.noise)));\n    red.setAttribute(\"dx\", String(-Number(opts.fringe)));\n    blue.setAttribute(\"dx\", String(Number(opts.fringe)));\n    var size = Number(opts.size), wide = opts.shape === \"pill\";\n    lens.style.setProperty(\"--ef-w\", (wide ? Math.round(size * 1.6) : size) + \"px\");\n    lens.style.setProperty(\"--ef-h\", size + \"px\");\n    lens.style.setProperty(\"--ef-radius\", { circle: \"50%\", square: \"24px\", pill: \"9999px\", leaf: \"40% 10% 40% 10%\" }[opts.shape] || \"50%\");\n    var bf = opts.mode === \"invert\" ? \"invert(1) hue-rotate(180deg)\" : svgOk ? \"url(#\" + id + \")\" : \"\";\n    lens.style.backdropFilter = bf;\n    lens.style.webkitBackdropFilter = bf;\n    rest();\n    paint();\n  }\n  update(o);\n  zone.addEventListener(\"pointermove\", move);\n  zone.addEventListener(\"pointerdown\", move);\n  if (\"ResizeObserver\" in window) { ro = new ResizeObserver(function () { rest(); paint(); }); ro.observe(zone); }\n  if (\"IntersectionObserver\" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; if (visible) wake(); }); io.observe(zone); }\n  return {\n    update: update,\n    destroy: function () {\n      cancelAnimationFrame(raf); raf = 0;\n      if (io) io.disconnect();\n      if (ro) ro.disconnect();\n      zone.removeEventListener(\"pointermove\", move);\n      zone.removeEventListener(\"pointerdown\", move);\n      if (svg.parentNode) svg.parentNode.removeChild(svg);\n    }\n  };\n}"
  }
}