{"schemaVersion":1,"slug":"glass-folder","framework":"react","component":"GlassFolder","dependencies":["react","react-dom"],"install":"npm install react react-dom","usage":"import { GlassFolder } from \"./GlassFolder\";\n\nexport default function App() {\n  return <GlassFolder />;\n}","source":{"kind":"project-source","provider":"LeiBox","implementation":"LeiBox component package","designReference":null,"designAuthor":null,"license":"Retain source notices and design-reference attribution; no blanket license is assigned by the catalog."},"files":[{"path":"GlassFolder.tsx","content":"\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport { mountGlassFolder, type GlassFolderOptions } from \"./GlassFolderEngine.js\";\nimport type { MotionController } from \"./motion-runtime\";\n\nexport function GlassFolder({ options, className }: { options?: GlassFolderOptions; className?: string }) {\n  const host = useRef<HTMLDivElement>(null);\n  const controller = useRef<MotionController | null>(null);\n  useEffect(() => {\n    const instance = mountGlassFolder(host.current!, options);\n    controller.current = instance;\n    return () => { instance.destroy(); controller.current = null; };\n    // Options are updated below without remounting the state machine.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n  useEffect(() => { controller.current?.update(options ?? {}); }, [options]);\n  return <div ref={host} className={className} data-motion-component=\"glass-folder\" />;\n}\n","sha256":"8b5adf5dc3d2e9709e9fa0b508209ea08a1d9d255e7cfa9336c3b3334941b590"},{"path":"GlassFolderEngine.js","content":"// Native DOM port of the preserved reference HTML. No iframe or runtime eval.\nimport { createSurface } from \"./motion-runtime\";\nexport function mountGlassFolder(target, options = {}) {\nconst scope = createSurface(target, {\"markup\":\"<main class=\\\"glass-folder-demo\\\" aria-label=\\\"Glass folder cards\\\"><div class=\\\"gf-demo-slot\\\"></div><div class=\\\"gf-demo-slot\\\"></div><div class=\\\"gf-demo-slot\\\"></div></main>\",\"css\":\"html,body{margin:0;min-height:100%;background:#ececee}body{font-family:Arial,sans-serif}.glass-folder-demo{box-sizing:border-box;min-height:600px;width:100%;padding:95px 45px 65px;background:#ececee;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:28px;align-items:center;justify-items:center;overflow:hidden}.gf-demo-slot{position:relative;width:340px;min-width:0;max-width:100%;padding-top:30px}.gf-feedback{position:absolute;top:100%;left:0;right:0;margin-top:24px;font-size:12px;line-height:1.6;text-align:center;color:#53535c;min-height:20px}.gf-root .gf-sheet:disabled{cursor:default}@media(max-width:800px){.glass-folder-demo{grid-template-columns:1fr;padding:50px 24px 85px;gap:45px}.gf-demo-slot{padding-top:25px}}\\n\\n.gf-root {\\n  --gf-px: 0;\\n  --gf-py: 0;\\n  --gf-ease: cubic-bezier(.2, .8, .2, 1);\\n  --gf-spring: cubic-bezier(.34, 1.36, .5, 1);\\n  --gf-hinge: 0deg;\\n  --gf-lift: 42px;\\n  --gf-spread: 0px;\\n  --gf-fan: 0deg;\\n  --gf-stack: 15px;\\n  --gf-ink: #141416;\\n  --gf-ink-soft: rgba(20, 20, 22, .55);\\n  position: relative;\\n  display: inline-block;\\n  flex: none;\\n  vertical-align: top;\\n  font-family: ui-sans-serif, system-ui, -apple-system, \\\"SF Pro Text\\\", \\\"Segoe UI\\\", Roboto, \\\"Helvetica Neue\\\", Arial, sans-serif;\\n  -webkit-font-smoothing: antialiased;\\n  -webkit-tap-highlight-color: transparent;\\n  isolation: isolate;\\n}\\n.gf-root[data-tone=\\\"dark\\\"] { --gf-ink: #f5f5f7; --gf-ink-soft: rgba(245, 245, 247, .58); }\\n.gf-root:hover, .gf-root:focus-within { --gf-hinge: -11deg; --gf-lift: -34px; --gf-spread: 8px; --gf-fan: 1.4deg; --gf-stack: 10px; }\\n.gf-root[data-open] { --gf-hinge: -24deg; --gf-lift: -96px; --gf-spread: 40px; --gf-fan: 5deg; --gf-stack: 30px; }\\n\\n.gf-stage {\\n  position: absolute;\\n  left: 0;\\n  top: 0;\\n  width: 340px;\\n  height: 272px;\\n  transform-origin: 0 0;\\n}\\n.gf-tilt {\\n  position: absolute;\\n  inset: 0;\\n  transform: perspective(1400px) rotateX(calc(var(--gf-py) * -3deg)) rotateY(calc(var(--gf-px) * 4deg));\\n  transition: transform .9s var(--gf-ease);\\n}\\n\\n.gf-shadow {\\n  position: absolute;\\n  left: 18px;\\n  right: 18px;\\n  bottom: -6px;\\n  height: 60px;\\n  border-radius: 40px;\\n  background: rgba(16, 18, 24, .34);\\n  filter: blur(22px);\\n  transform: translateX(calc(var(--gf-px) * -8px));\\n  transition: transform .9s var(--gf-ease), opacity .9s var(--gf-ease);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-shadow { background: rgba(0, 0, 0, .62); }\\n\\n.gf-back {\\n  position: absolute;\\n  inset: 0;\\n  overflow: hidden;\\n  background: linear-gradient(180deg, #f3f3f4 0%, #e3e3e6 100%);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-back { background: linear-gradient(180deg, #2a2a2e 0%, #121214 100%); }\\n.gf-art {\\n  position: absolute;\\n  left: -12px;\\n  top: -12px;\\n  max-width: none;\\n  object-fit: cover;\\n  transform: translate(calc(var(--gf-px) * -7px), calc(var(--gf-py) * -6px)) scale(1.02);\\n  transition: transform 1.1s var(--gf-ease), opacity 1.2s var(--gf-ease), filter 1.2s var(--gf-ease);\\n}\\ncanvas.gf-art { opacity: 0; filter: blur(14px) saturate(.6); }\\ncanvas.gf-art[data-ready] { opacity: 1; filter: none; }\\n.gf-back-sheen {\\n  position: absolute;\\n  inset: 0;\\n  background:\\n    linear-gradient(180deg, rgba(255, 255, 255, .22) 0%, rgba(255, 255, 255, 0) 22%),\\n    radial-gradient(120% 80% at 80% 0%, rgba(255, 255, 255, .16), rgba(255, 255, 255, 0) 60%);\\n  pointer-events: none;\\n}\\n\\n.gf-rim {\\n  position: absolute;\\n  left: 0;\\n  top: 0;\\n  max-width: none;\\n  overflow: visible;\\n  pointer-events: none;\\n  fill: none;\\n}\\n.gf-back-rim { width: 340px; height: 272px; }\\n.gf-back-rim path { stroke: rgba(255, 255, 255, .45); stroke-width: 1; }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-back-rim path { stroke: rgba(255, 255, 255, .16); }\\n\\n/* ---- files ---------------------------------------------------------------- */\\n.gf-files {\\n  position: absolute;\\n  inset: 0;\\n  z-index: 5;\\n  pointer-events: none;\\n  /* nothing hangs out of the bottom of the pocket, however tall the stack */\\n  clip-path: inset(-400px -400px 10px -400px);\\n}\\n.gf-sheet-slot { display: contents; }\\n.gf-sheet {\\n  all: unset;\\n  box-sizing: border-box;\\n  position: absolute;\\n  left: 34px;\\n  top: 96px;\\n  width: 272px;\\n  height: 184px;\\n  border-radius: 16px;\\n  overflow: hidden;\\n  cursor: pointer;\\n  pointer-events: auto;\\n  background: linear-gradient(180deg, #ffffff 0%, #f2f2f0 100%);\\n  box-shadow:\\n    inset 0 1px 0 rgba(255, 255, 255, .9),\\n    0 0 0 .5px rgba(0, 0, 0, .1),\\n    0 10px 26px -8px rgba(10, 12, 20, .28);\\n  transform-origin: 50% 100%;\\n  transform:\\n    translate(calc(var(--gf-jx) + var(--gf-slot) * var(--gf-spread)), calc(var(--gf-lift) + var(--gf-depth) * var(--gf-stack) * -1))\\n    rotate(calc(var(--gf-jr) + var(--gf-slot) * var(--gf-fan)));\\n  transition:\\n    transform .75s var(--gf-spring) calc(var(--gf-depth) * 40ms),\\n    box-shadow .4s var(--gf-ease),\\n    translate .9s var(--gf-ease),\\n    opacity .9s var(--gf-ease);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-sheet { background: linear-gradient(180deg, #2c2c30 0%, #1c1c1f 100%); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14), 0 0 0 .5px rgba(255, 255, 255, .08), 0 12px 28px -8px rgba(0, 0, 0, .7); }\\n.gf-sheet--art { background: #1a1a1d; }\\n.gf-root[data-open] .gf-sheet:hover,\\n.gf-root[data-open] .gf-sheet:focus-visible {\\n  transform:\\n    translate(calc(var(--gf-jx) + var(--gf-slot) * var(--gf-spread)), calc(var(--gf-lift) + var(--gf-depth) * var(--gf-stack) * -1 - 18px))\\n    rotate(calc(var(--gf-slot) * var(--gf-fan) * .6));\\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9), 0 0 0 .5px rgba(0, 0, 0, .1), 0 22px 40px -12px rgba(10, 12, 20, .42);\\n  transition-delay: 0s;\\n}\\n.gf-sheet:focus-visible { outline: 2px solid #4c8dff; outline-offset: 2px; }\\n.gf-sheet-art {\\n  position: absolute;\\n  left: 0;\\n  top: 0;\\n  width: 272px;\\n  height: 184px;\\n  max-width: none;\\n  object-fit: cover;\\n  transition: opacity .9s var(--gf-ease), filter .9s var(--gf-ease);\\n}\\ncanvas.gf-sheet-art { opacity: 0; filter: blur(10px); }\\ncanvas.gf-sheet-art[data-ready] { opacity: 1; filter: none; }\\n.gf-sheet--art::after {\\n  content: \\\"\\\";\\n  position: absolute;\\n  inset: 0;\\n  border-radius: inherit;\\n  background: linear-gradient(180deg, rgba(0, 0, 0, .42) 0%, rgba(0, 0, 0, 0) 34%);\\n  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .2);\\n  pointer-events: none;\\n}\\n.gf-sheet-head {\\n  position: absolute;\\n  left: 14px;\\n  right: 14px;\\n  top: 11px;\\n  z-index: 1;\\n  display: flex;\\n  align-items: baseline;\\n  justify-content: space-between;\\n  gap: 10px;\\n  white-space: nowrap;\\n}\\n.gf-sheet-name {\\n  overflow: hidden;\\n  text-overflow: ellipsis;\\n  font-size: 12.5px;\\n  font-weight: 560;\\n  letter-spacing: -.005em;\\n  color: #18181b;\\n}\\n.gf-sheet-meta {\\n  flex: none;\\n  font-family: ui-monospace, \\\"SF Mono\\\", SFMono-Regular, Menlo, Consolas, monospace;\\n  font-size: 10px;\\n  letter-spacing: .02em;\\n  color: rgba(24, 24, 27, .5);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-sheet-name, .gf-sheet--art .gf-sheet-name { color: #fafafa; }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-sheet-meta, .gf-sheet--art .gf-sheet-meta { color: rgba(250, 250, 250, .66); }\\n.gf-lines {\\n  position: absolute;\\n  left: 14px;\\n  right: 14px;\\n  top: 40px;\\n  display: flex;\\n  flex-direction: column;\\n  gap: 9px;\\n}\\n.gf-lines i {\\n  display: block;\\n  height: 5px;\\n  border-radius: 3px;\\n  background: rgba(24, 24, 27, .08);\\n}\\n.gf-lines i:first-child { height: 9px; margin-bottom: 4px; background: rgba(24, 24, 27, .14); }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-lines i { background: rgba(255, 255, 255, .08); }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-lines i:first-child { background: rgba(255, 255, 255, .16); }\\n\\n.gf-sheet--drop { animation: gf-drop .85s var(--gf-ease) both; }\\n@keyframes gf-drop {\\n  0% { translate: 0 -190px; rotate: -9deg; opacity: 0; }\\n  35% { opacity: 1; }\\n  72% { translate: 0 10px; rotate: 1deg; }\\n  100% { translate: 0 0; rotate: 0deg; opacity: 1; }\\n}\\n\\n/* ---- front glass ----------------------------------------------------------- */\\n.gf-front {\\n  position: absolute;\\n  left: 0;\\n  top: 74px;\\n  width: 340px;\\n  height: 198px;\\n  z-index: 10;\\n  -webkit-backdrop-filter: blur(22px) saturate(1.5);\\n  backdrop-filter: blur(22px) saturate(1.5);\\n  transform-origin: 50% 100%;\\n  transform: perspective(760px) rotateX(var(--gf-hinge));\\n  transition: transform .8s var(--gf-spring), translate 1s var(--gf-ease), opacity 1s var(--gf-ease);\\n}\\n.gf-front--bump { animation: gf-bump .7s var(--gf-ease) .38s both; }\\n@keyframes gf-bump {\\n  0% { scale: 1; }\\n  35% { scale: 1.018 .975; }\\n  70% { scale: .996 1.006; }\\n  100% { scale: 1; }\\n}\\n.gf-tint {\\n  position: absolute;\\n  inset: 0;\\n  background:\\n    linear-gradient(180deg, rgba(255, 255, 255, .16) 0%, rgba(255, 255, 255, .5) 42%, rgba(252, 252, 252, .93) 100%);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-tint {\\n  background: linear-gradient(180deg, rgba(18, 18, 20, .18) 0%, rgba(14, 14, 16, .62) 46%, rgba(8, 8, 10, .94) 100%);\\n}\\n.gf-spec {\\n  position: absolute;\\n  inset: 0;\\n  background: radial-gradient(260px 200px at calc(50% + var(--gf-px) * 46%) calc(30% + var(--gf-py) * 40%), rgba(255, 255, 255, .5), rgba(255, 255, 255, 0) 70%);\\n  mix-blend-mode: soft-light;\\n  opacity: .9;\\n  transition: background-position .6s var(--gf-ease);\\n  pointer-events: none;\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-spec { mix-blend-mode: screen; opacity: .16; }\\n.gf-front-rim { width: 340px; height: 198px; }\\n.gf-front-rim path { stroke-width: 2.4; transition: stroke-width .3s var(--gf-ease); }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-front-rim { opacity: .55; }\\n\\n.gf-hit {\\n  all: unset;\\n  position: absolute;\\n  inset: 0;\\n  z-index: 1;\\n  cursor: pointer;\\n}\\n.gf-hit:focus-visible ~ .gf-label .gf-title { text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 4px; }\\n.gf-front:has(.gf-hit:focus-visible) .gf-front-rim path { stroke-width: 5; }\\n\\n.gf-label {\\n  position: absolute;\\n  z-index: 2;\\n  display: flex;\\n  flex-direction: column;\\n  gap: 4px;\\n  max-width: 200px;\\n  pointer-events: none;\\n  color: var(--gf-ink);\\n}\\n.gf-root[data-place=\\\"bottom-left\\\"] .gf-label { left: 28px; bottom: 32px; }\\n.gf-root[data-place=\\\"top-left\\\"] .gf-label { left: 28px; top: 34px; }\\n.gf-root[data-place=\\\"top-right\\\"] .gf-label { right: 32px; top: 52px; align-items: flex-start; }\\n.gf-title {\\n  font-size: 18px;\\n  font-weight: 500;\\n  line-height: 1.15;\\n  letter-spacing: -.012em;\\n  overflow: hidden;\\n  text-overflow: ellipsis;\\n  white-space: nowrap;\\n}\\n.gf-meta {\\n  font-family: ui-monospace, \\\"SF Mono\\\", SFMono-Regular, Menlo, Consolas, monospace;\\n  font-size: 12px;\\n  letter-spacing: .03em;\\n  color: var(--gf-ink-soft);\\n  white-space: nowrap;\\n  animation: gf-roll .5s var(--gf-ease) both;\\n}\\n@keyframes gf-roll {\\n  from { opacity: 0; translate: 0 7px; filter: blur(3px); }\\n  to { opacity: 1; translate: 0 0; filter: blur(0); }\\n}\\n\\n/* ---- the key: + or ··· ------------------------------------------------------ */\\n.gf-key {\\n  all: unset;\\n  box-sizing: border-box;\\n  position: absolute;\\n  right: 22px;\\n  bottom: 22px;\\n  z-index: 12;\\n  width: 58px;\\n  height: 58px;\\n  border-radius: 18px;\\n  display: grid;\\n  place-items: center;\\n  cursor: pointer;\\n  transition: transform .35s var(--gf-spring), box-shadow .35s var(--gf-ease), background .35s var(--gf-ease);\\n}\\n.gf-key:hover { transform: translateY(-2px); }\\n.gf-key:active { transform: translateY(0) scale(.93); transition-duration: .12s; }\\n.gf-key:focus-visible { outline: 2px solid #4c8dff; outline-offset: 3px; }\\n.gf-key--add {\\n  color: #fff;\\n  background: radial-gradient(120% 90% at 50% 0%, #3a3a3f 0%, #121214 55%, #050506 100%);\\n  box-shadow:\\n    inset 0 1px 0 rgba(255, 255, 255, .22),\\n    inset 0 -3px 8px rgba(0, 0, 0, .7),\\n    0 0 0 1px rgba(0, 0, 0, .75),\\n    0 14px 22px -8px rgba(0, 0, 0, .55),\\n    0 3px 6px -2px rgba(0, 0, 0, .35);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-key--add {\\n  color: #111113;\\n  background: radial-gradient(120% 90% at 50% 0%, #ffffff 0%, #ececee 55%, #d2d2d6 100%);\\n  box-shadow: inset 0 1px 0 #fff, inset 0 -3px 8px rgba(0, 0, 0, .14), 0 0 0 1px rgba(0, 0, 0, .4), 0 14px 22px -8px rgba(0, 0, 0, .7);\\n}\\n.gf-key--more {\\n  color: #111113;\\n  background: linear-gradient(180deg, rgba(255, 255, 255, .92) 0%, rgba(226, 226, 230, .82) 100%);\\n  -webkit-backdrop-filter: blur(10px);\\n  backdrop-filter: blur(10px);\\n  box-shadow:\\n    inset 0 1px 0 #fff,\\n    inset 0 -2px 4px rgba(0, 0, 0, .06),\\n    0 0 0 1px rgba(0, 0, 0, .16),\\n    0 10px 18px -8px rgba(0, 0, 0, .3);\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-key--more {\\n  color: #f5f5f7;\\n  background: linear-gradient(180deg, rgba(255, 255, 255, .16) 0%, rgba(255, 255, 255, .06) 100%);\\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), 0 0 0 1px rgba(255, 255, 255, .1), 0 10px 18px -8px rgba(0, 0, 0, .6);\\n}\\n.gf-key--more[aria-expanded=\\\"true\\\"] { transform: scale(.96); }\\n.gf-glyph { display: block; max-width: none; fill: none; stroke: currentColor; stroke-linecap: round; }\\n.gf-glyph--plus { width: 26px; height: 26px; stroke-width: 1.6; animation: gf-turn .6s var(--gf-spring); }\\n.gf-glyph--more { width: 30px; height: 12px; stroke-width: 1.7; }\\n@keyframes gf-turn { from { rotate: -90deg; } to { rotate: 0deg; } }\\n\\n/* ---- menu ------------------------------------------------------------------- */\\n.gf-menu {\\n  position: absolute;\\n  right: 22px;\\n  bottom: 90px;\\n  z-index: 20;\\n  min-width: 176px;\\n  padding: 6px;\\n  border-radius: 16px;\\n  display: flex;\\n  flex-direction: column;\\n  background: rgba(252, 252, 253, .72);\\n  -webkit-backdrop-filter: blur(24px) saturate(1.6);\\n  backdrop-filter: blur(24px) saturate(1.6);\\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9), 0 0 0 .5px rgba(0, 0, 0, .14), 0 24px 48px -16px rgba(10, 12, 20, .4);\\n  transform-origin: 100% 100%;\\n  opacity: 0;\\n  visibility: hidden;\\n  transform: translateY(6px) scale(.94);\\n  transition: opacity .2s var(--gf-ease), transform .32s var(--gf-spring), visibility 0s .32s;\\n}\\n.gf-menu[data-open] { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-menu { background: rgba(30, 30, 34, .72); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 0 0 .5px rgba(255, 255, 255, .1), 0 24px 48px -16px rgba(0, 0, 0, .8); }\\n.gf-menu-item {\\n  all: unset;\\n  box-sizing: border-box;\\n  padding: 9px 12px;\\n  border-radius: 10px;\\n  font-size: 13.5px;\\n  font-weight: 450;\\n  color: #18181b;\\n  cursor: pointer;\\n}\\n.gf-root[data-tone=\\\"dark\\\"] .gf-menu-item { color: #f4f4f5; }\\n.gf-menu-item:hover, .gf-menu-item:focus-visible { background: rgba(24, 24, 27, .07); }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-menu-item:hover, .gf-root[data-tone=\\\"dark\\\"] .gf-menu-item:focus-visible { background: rgba(255, 255, 255, .09); }\\n.gf-menu-item--danger { color: #d92d20; }\\n.gf-root[data-tone=\\\"dark\\\"] .gf-menu-item--danger { color: #ff6b5e; }\\n\\n/* ---- entrance: the folder assembles itself ---------------------------------- */\\n.gf-root:not([data-entered]) .gf-back,\\n.gf-root:not([data-entered]) .gf-back-rim { translate: 0 22px; opacity: 0; }\\n.gf-root:not([data-entered]) .gf-sheet { translate: 0 -120px; opacity: 0; }\\n.gf-root:not([data-entered]) .gf-front { translate: 0 46px; opacity: 0; }\\n.gf-root:not([data-entered]) .gf-key { opacity: 0; scale: .6; }\\n.gf-root:not([data-entered]) .gf-shadow { opacity: 0; }\\n.gf-back, .gf-back-rim { transition: translate 1s var(--gf-ease), opacity .9s var(--gf-ease); }\\n.gf-root[data-entered] .gf-sheet { transition-delay: calc(var(--gf-depth) * 40ms), 0s, calc(.18s + var(--gf-depth) * 90ms), calc(.18s + var(--gf-depth) * 90ms); }\\n.gf-root[data-entered] .gf-front { transition-delay: 0s, .42s, .42s; }\\n.gf-key { transition: transform .35s var(--gf-spring), box-shadow .35s var(--gf-ease), opacity .6s var(--gf-ease) .7s, scale .7s var(--gf-spring) .7s; }\\n\\n@media (prefers-reduced-motion: reduce) {\\n  .gf-root *, .gf-root *::before, .gf-root *::after {\\n    transition-duration: 0s !important;\\n    transition-delay: 0s !important;\\n    animation: none !important;\\n  }\\n  .gf-tilt { transform: none; }\\n}\\n\",\"width\":1200,\"height\":600,\"responsive\":true}, options);\nconst { document, window, requestAnimationFrame, cancelAnimationFrame, setTimeout, clearTimeout, setInterval, clearInterval, ResizeObserver, IntersectionObserver, matchMedia } = scope;\n\n// Native DOM port of user-provided Glass Folder. Art math and styles preserved.\nconst ART_PRESETS = {\n    // a long exposure: blush, oxblood and steel pulled across the frame\n    aurora: {\n        style: \"streak\",\n        colors: [\"#f4efe9\", \"#e9c2b8\", \"#c2433f\", \"#2a1e2a\", \"#22395e\", \"#6f97c2\", \"#e5edf4\"],\n        scale: 0.7,\n        warp: 1.4,\n        angle: -0.62,\n        gloss: 0.18,\n        veins: 0,\n    },\n    // red ink folding through teal-black water\n    ember: {\n        style: \"marble\",\n        colors: [\"#07090c\", \"#16252c\", \"#3d5f68\", \"#1a1416\", \"#a3201a\", \"#ef4a2b\", \"#ff8a5c\"],\n        scale: 0.62,\n        warp: 3.2,\n        angle: -0.5,\n        gloss: 0.55,\n        veins: 0.6,\n    },\n    // plum and rust, swirled\n    dusk: {\n        style: \"marble\",\n        colors: [\"#120c18\", \"#3e2850\", \"#8f6c9c\", \"#2b1a24\", \"#b3502f\", \"#eab08a\", \"#fbe7d2\"],\n        scale: 0.75,\n        warp: 3.6,\n        angle: 0.8,\n        gloss: 0.5,\n        veins: 0.5,\n    },\n    // cold, pale, nearly white\n    glacier: {\n        style: \"streak\",\n        colors: [\"#f7f8fa\", \"#d9e2ea\", \"#9fb4c7\", \"#e8edf2\", \"#bccbd8\", \"#ffffff\"],\n        scale: 0.7,\n        warp: 1.6,\n        angle: 0.35,\n        gloss: 0.25,\n        veins: 0,\n    },\n    // moss and brass\n    verdant: {\n        style: \"marble\",\n        colors: [\"#070b08\", \"#173024\", \"#4f7a5a\", \"#141812\", \"#a7843a\", \"#e9cf87\", \"#fbf3d6\"],\n        scale: 0.7,\n        warp: 3.4,\n        angle: 2.1,\n        gloss: 0.5,\n        veins: 0.5,\n    },\n};\n/** `#rgb` or `#rrggbb` to a triple. Anything unreadable comes back mid-grey. */\nfunction hexToRgb(hex) {\n    let h = hex.trim().replace(/^#/, \"\");\n    if (h.length === 3)\n        h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n    const n = /^[0-9a-f]{6}$/i.test(h) ? parseInt(h, 16) : 0x808080;\n    return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n/** A small, fast, seedable PRNG (mulberry32). */\nfunction rng(seed) {\n    let a = seed >>> 0;\n    return () => {\n        a = (a + 0x6d2b79f5) >>> 0;\n        let t = a;\n        t = Math.imul(t ^ (t >>> 15), t | 1);\n        t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n    };\n}\n/** A stable small integer for a string, so a file's art follows its name. */\nfunction hashString(s) {\n    let h = 2166136261;\n    for (let i = 0; i < s.length; i++)\n        h = Math.imul(h ^ s.charCodeAt(i), 16777619);\n    return h >>> 0;\n}\n/** Seeded gradient noise, summed into fractal octaves with a rotation between each. */\nfunction makeFbm(seed) {\n    const r = rng(seed);\n    const perm = new Uint8Array(512);\n    const grad = new Float32Array(512);\n    const p = [];\n    for (let i = 0; i < 256; i++)\n        p.push(i);\n    for (let i = 255; i > 0; i--) {\n        const j = Math.floor(r() * (i + 1));\n        const t = p[i];\n        p[i] = p[j];\n        p[j] = t;\n    }\n    for (let i = 0; i < 512; i++)\n        perm[i] = p[i & 255];\n    for (let i = 0; i < 256; i++) {\n        const a = r() * Math.PI * 2;\n        grad[i * 2] = Math.cos(a);\n        grad[i * 2 + 1] = Math.sin(a);\n    }\n    const noise = (x, y) => {\n        const xi = Math.floor(x);\n        const yi = Math.floor(y);\n        const xf = x - xi;\n        const yf = y - yi;\n        const X = xi & 255;\n        const Y = yi & 255;\n        const h00 = perm[X + perm[Y]] * 2;\n        const h10 = perm[X + 1 + perm[Y]] * 2;\n        const h01 = perm[X + perm[Y + 1]] * 2;\n        const h11 = perm[X + 1 + perm[Y + 1]] * 2;\n        const d00 = grad[h00] * xf + grad[h00 + 1] * yf;\n        const d10 = grad[h10] * (xf - 1) + grad[h10 + 1] * yf;\n        const d01 = grad[h01] * xf + grad[h01 + 1] * (yf - 1);\n        const d11 = grad[h11] * (xf - 1) + grad[h11 + 1] * (yf - 1);\n        const u = xf * xf * xf * (xf * (xf * 6 - 15) + 10);\n        const v = yf * yf * yf * (yf * (yf * 6 - 15) + 10);\n        return d00 + u * (d10 - d00) + v * (d01 - d00 + u * (d11 - d01 - d10 + d00));\n    };\n    return (x, y, octaves) => {\n        let sum = 0;\n        let amp = 0.5;\n        for (let i = 0; i < octaves; i++) {\n            sum += amp * noise(x, y);\n            const nx = 1.6 * x + 1.2 * y + 7.1;\n            y = -1.2 * x + 1.6 * y + 3.3;\n            x = nx;\n            amp *= 0.5;\n        }\n        return sum;\n    };\n}\n/**\n * The artwork, as a job that can be spread over several frames.\n *\n * A field is domain-warped twice (each fold feeding the next), then\n * normalised by its own percentiles so every seed spans the whole palette.\n * The field is then treated as the height of a liquid surface: its slope\n * gives a normal, the normal gives diffuse light and a tight specular, and\n * fine striations run along the contours like combed ink. `streak` squeezes\n * the coordinates along one axis first, which turns the same folds into a\n * long-exposure smear.\n */\nfunction createArtJob(width, height, recipe, seed) {\n    const w = Math.max(1, Math.round(width));\n    const h = Math.max(1, Math.round(height));\n    const colors = recipe.colors.length >= 2 ? recipe.colors : [\"#111111\", \"#eeeeee\"];\n    const stops = colors.map((c, i) => ({ t: i / (colors.length - 1), c: hexToRgb(c) }));\n    const scale = recipe.scale ?? 0.7;\n    const warp = recipe.warp ?? 3;\n    const gloss = recipe.gloss ?? 0.4;\n    const veins = recipe.veins ?? 0;\n    const streak = recipe.style === \"streak\";\n    const fbm = makeFbm(seed * 977 + 13);\n    const r = rng(seed + 1);\n    const ox = r() * 50;\n    const oy = r() * 50;\n    const angle = (recipe.angle ?? 0) + (r() - 0.5) * 0.5;\n    const ca = Math.cos(angle);\n    const sa = Math.sin(angle);\n    const m = Math.min(w, h);\n    // The field is sampled on a half-resolution grid (with a one-cell apron) and\n    // brought up with Catmull-Rom, which keeps the slope continuous for the\n    // lighting: a quarter of the noise for no visible loss.\n    const gw = Math.ceil(w / 2) + 3;\n    const gh = Math.ceil(h / 2) + 3;\n    const G = new Float32Array(gw * gh);\n    const GQ = new Float32Array(gw * gh);\n    const F = new Float32Array(w * h);\n    const Q = new Float32Array(w * h);\n    const pixels = new Uint8ClampedArray(w * h * 4);\n    let row = 0;\n    const field = (gy) => {\n        for (let gx = 0; gx < gw; gx++) {\n            const u = ((gx - 1) * 2 - w / 2) / m;\n            const v = ((gy - 1) * 2 - h / 2) / m;\n            let px = (u * ca - v * sa) * scale;\n            let py = (u * sa + v * ca) * scale;\n            if (streak) {\n                px *= 0.14;\n                py *= 2.2;\n            }\n            px += ox;\n            py += oy;\n            const qx = fbm(px, py, 3);\n            const qy = fbm(px + 5.2, py + 1.3, 3);\n            const rx = fbm(px + warp * qx + 1.7, py + warp * qy + 9.2, 3);\n            const ry = fbm(px + warp * qx + 8.3, py + warp * qy + 2.8, 3);\n            G[gy * gw + gx] = fbm(px + warp * rx, py + warp * ry, 4);\n            GQ[gy * gw + gx] = Math.sqrt(qx * qx + qy * qy);\n        }\n    };\n    const upsample = () => {\n        // pixel x sits on grid column x/2 + 1: either exactly on a sample, or halfway\n        const cr = (a, b, c, d) => (-a + 9 * b + 9 * c - d) / 16;\n        const T = new Float32Array(w * gh);\n        for (let gy = 0; gy < gh; gy++) {\n            const r = gy * gw;\n            for (let x = 0; x < w; x++) {\n                const c = (x >> 1) + 1;\n                T[gy * w + x] = x & 1 ? cr(G[r + c - 1], G[r + c], G[r + c + 1], G[r + c + 2]) : G[r + c];\n            }\n        }\n        for (let y = 0; y < h; y++) {\n            const c = (y >> 1) + 1;\n            for (let x = 0; x < w; x++) {\n                F[y * w + x] =\n                    y & 1 ? cr(T[(c - 1) * w + x], T[c * w + x], T[(c + 1) * w + x], T[(c + 2) * w + x]) : T[c * w + x];\n                const gx = (x >> 1) + 1;\n                const q0 = GQ[c * gw + gx];\n                const qx = x & 1 ? (q0 + GQ[c * gw + gx + 1]) / 2 : q0;\n                const q1 = y & 1 ? (x & 1 ? (GQ[(c + 1) * gw + gx] + GQ[(c + 1) * gw + gx + 1]) / 2 : GQ[(c + 1) * gw + gx]) : qx;\n                Q[y * w + x] = (qx + q1) / 2;\n            }\n        }\n    };\n    const colorize = () => {\n        // percentiles from a sample, not a full sort: cheap and just as stable\n        const sample = [];\n        const stride = Math.max(1, Math.floor(F.length / 4096));\n        for (let i = 0; i < F.length; i += stride)\n            sample.push(F[i]);\n        sample.sort((a, b) => a - b);\n        const lo = sample[Math.floor(sample.length * 0.02)];\n        const hi = sample[Math.floor(sample.length * 0.98)];\n        const k = 1 / (hi - lo || 1);\n        const lx = -0.55;\n        const ly = -0.65;\n        const lz = 0.52;\n        const slope = k * m * 0.012;\n        for (let y = 0; y < h; y++) {\n            for (let x = 0; x < w; x++) {\n                const i = y * w + x;\n                let t = (F[i] - lo) * k;\n                t = t < 0 ? 0 : t > 1 ? 1 : t;\n                let j = 1;\n                while (j < stops.length - 1 && stops[j].t < t)\n                    j++;\n                const a = stops[j - 1];\n                const b = stops[j];\n                let f = (t - a.t) / (b.t - a.t || 1);\n                f = f < 0 ? 0 : f > 1 ? 1 : f;\n                f = f * f * (3 - 2 * f);\n                const gx = (F[x < w - 1 ? i + 1 : i] - F[x > 0 ? i - 1 : i]) * slope;\n                const gy = (F[y < h - 1 ? i + w : i] - F[y > 0 ? i - w : i]) * slope;\n                const nd = (-gx * lx - gy * ly + lz) / Math.sqrt(gx * gx + gy * gy + 1);\n                const spec = Math.pow(nd > 0 ? nd : 0, 24) * gloss * 255;\n                const silk = 1 + 0.035 * Math.sin(t * 150 + Q[i] * 24);\n                const vein = veins * Math.max(0, 1 - Math.abs(Q[i] - 0.42) * 14) * 0.6;\n                const lit = (0.82 + 0.28 * nd) * silk * (1 - vein);\n                pixels[i * 4] = (a.c[0] + (b.c[0] - a.c[0]) * f) * lit + spec;\n                pixels[i * 4 + 1] = (a.c[1] + (b.c[1] - a.c[1]) * f) * lit + spec;\n                pixels[i * 4 + 2] = (a.c[2] + (b.c[2] - a.c[2]) * f) * lit + spec;\n                pixels[i * 4 + 3] = 255;\n            }\n        }\n    };\n    const step = (rows) => {\n        if (row >= gh)\n            return true;\n        const end = Math.min(gh, row + Math.max(1, rows));\n        while (row < end)\n            field(row++);\n        if (row < gh)\n            return false;\n        upsample();\n        colorize();\n        return true;\n    };\n    return { width: w, height: h, pixels, step };\n}\n/** Where a file sits in the fan: -1 … 1 across the visible stack, 0 for one file. */\nfunction fanSlot(index, count) {\n    return count <= 1 ? 0 : (index / (count - 1)) * 2 - 1;\n}\n// #endregion\n\nconst W = 340;\nconst H = 272;\nconst HEADROOM = 54; // room above the back panel for the files to rise into\nconst FRONT_TOP = 74;\nconst FRONT_H = H - FRONT_TOP;\nconst SHEET_W = 272;\nconst SHEET_H = 184;\nconst BACK_PATH = \"M0 56A22 22 0 0 1 22 34L146 34C170 34 172 0 198 0L318 0A22 22 0 0 1 340 22L340 250A22 22 0 0 1 318 272L22 272A22 22 0 0 1 0 250Z\";\nconst FRONT_PATH = \"M0 22A22 22 0 0 1 22 0L118 0C144 0 146 32 172 32L318 32A22 22 0 0 1 340 54L340 \" +\n    (FRONT_H - 22) +\n    \"A22 22 0 0 1 318 \" +\n    FRONT_H +\n    \"L22 \" +\n    FRONT_H +\n    \"A22 22 0 0 1 0 \" +\n    (FRONT_H - 22) +\n    \"Z\";\nconst MAX_VISIBLE = 4;\nconst SAMPLE_FILES = [\n    { name: \"Cover — final\", meta: \"PNG · 4.8 MB\", art: \"ember\" },\n    { name: \"Moodboard\", meta: \"24 images\", art: \"dusk\" },\n    { name: \"Brief.pdf\", meta: \"PDF · 1.2 MB\" },\n];\n\n// Included in the standalone HTML and the shared six-framework engine.\nconst recipeOf=art=>typeof art==='string'?ART_PRESETS[art]:art;\nconst artCache=new Map();\nconst paintQueue=[];\nlet painting=false,backsReady=0;\nconst reduceMotion=()=>window.matchMedia('(prefers-reduced-motion: reduce)').matches;\nfunction paint(recipe,seed,w,h,priority=0){\n  const key=JSON.stringify([recipe,seed,w,h]);\n  if(artCache.has(key))return artCache.get(key);\n  const promise=new Promise(done=>{\n    let at=paintQueue.length;while(at>1&&paintQueue[at-1].priority>priority)at--;\n    paintQueue.splice(at,0,{job:createArtJob(w,h,recipe,seed),priority,done});\n    if(painting)return;painting=true;\n    function tick(){\n      const until=performance.now()+7;\n      while(paintQueue.length&&performance.now()<until){\n        const head=paintQueue[0];if(!head.job.step(4))continue;\n        paintQueue.shift();const c=document.createElement('canvas');c.width=head.job.width;c.height=head.job.height;\n        c.getContext('2d').putImageData(new ImageData(head.job.pixels,c.width,c.height),0,0);head.done(c);\n      }\n      if(paintQueue.length){if(reduceMotion())setTimeout(tick,0);else requestAnimationFrame(tick);}else painting=false;\n    }\n    if(reduceMotion())setTimeout(tick,0);else requestAnimationFrame(tick);\n  });\n  artCache.set(key,promise);return promise;\n}\nfunction artCanvas(className,art,seed,w,h,priority){\n  const canvas=document.createElement('canvas');canvas.className=className;canvas.setAttribute('aria-hidden','true');\n  canvas.style.cssText=`width:${w}px;height:${h}px;max-width:none`;\n  const density=Math.min(1.5,window.devicePixelRatio||1);\n  paint(recipeOf(art),seed,Math.round(w*density),Math.round(h*density),priority).then(src=>{\n    if(!canvas.isConnected)return;canvas.width=src.width;canvas.height=src.height;\n    canvas.getContext('2d').drawImage(src,0,0);canvas.dataset.ready='';\n    if(className==='gf-art'&&++backsReady===3)document.body.dataset.motionReady='true';\n  });\n  return canvas;\n}\nfunction makeFolder(host,index,settings){\n  const uid='gf-port-'+index;\n  const root=document.createElement('div');root.className='gf-root';root.dataset.tone=settings.tone;root.dataset.place='bottom-left';root.dataset.folder=String(index);\n  root.innerHTML=`<div class=\"gf-stage\"><div class=\"gf-tilt\"><div class=\"gf-shadow\" aria-hidden=\"true\"></div><div class=\"gf-back\" aria-hidden=\"true\"><div class=\"gf-back-sheen\"></div></div><svg class=\"gf-rim gf-back-rim\" viewBox=\"0 0 340 272\" aria-hidden=\"true\"><path d=\"${BACK_PATH}\"/></svg><div class=\"gf-front\"><div class=\"gf-tint\" aria-hidden=\"true\"></div><div class=\"gf-spec\" aria-hidden=\"true\"></div><svg class=\"gf-rim gf-front-rim\" viewBox=\"0 0 340 198\" aria-hidden=\"true\"><defs><linearGradient id=\"${uid}-rim\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".95\"/><stop offset=\".35\" stop-color=\"#fff\" stop-opacity=\".35\"/><stop offset=\"1\" stop-color=\"#fff\" stop-opacity=\".12\"/></linearGradient></defs><path d=\"${FRONT_PATH}\" stroke=\"url(#${uid}-rim)\"/></svg><button class=\"gf-hit\" type=\"button\" aria-expanded=\"false\"></button><div class=\"gf-label\"><span class=\"gf-title\"></span><span class=\"gf-meta\"></span></div></div><div class=\"gf-files\" role=\"list\" inert></div><button class=\"gf-key gf-key--${settings.action}\" type=\"button\">${settings.action==='add'?'<svg class=\"gf-glyph gf-glyph--plus\" viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M12 4.5v15M4.5 12h15\"/></svg>':'<svg class=\"gf-glyph gf-glyph--more\" viewBox=\"0 0 30 12\" aria-hidden=\"true\"><rect x=\"1\" y=\"1.5\" width=\"8\" height=\"9\" rx=\"3.4\"/><rect x=\"11\" y=\"1.5\" width=\"8\" height=\"9\" rx=\"3.4\"/><rect x=\"21\" y=\"1.5\" width=\"8\" height=\"9\" rx=\"3.4\"/></svg>'}</button><div class=\"gf-menu\" role=\"menu\" aria-label=\"Folder options\" inert></div></div></div>`;\n  const q=s=>root.querySelector(s),front=q('.gf-front'),back=q('.gf-back'),filesNode=q('.gf-files'),hit=q('.gf-hit'),key=q('.gf-key'),menu=q('.gf-menu');\n  front.style.clipPath=`path('${FRONT_PATH}')`;back.style.clipPath=`path('${BACK_PATH}')`;\n  back.prepend(artCanvas('gf-art',settings.art,settings.seed,W+24,H+24,0));\n  q('.gf-title').textContent=settings.label;filesNode.setAttribute('aria-label',settings.label+' contents');\n  let files=SAMPLE_FILES.map((file,id)=>({...file,id})),nextId=files.length,open=false,menuOpen=false;\n  const sheetNodes=new Map();\n  const feedback=document.createElement('output');feedback.className='gf-feedback';feedback.setAttribute('aria-live','polite');\n  host.append(root,feedback);\n  function setOpen(value){\n    open=value;root.toggleAttribute('data-open',open);hit.setAttribute('aria-expanded',String(open));filesNode.inert=!open;setMenu(false);renderLabel();\n  }\n  function setMenu(value){\n    menuOpen=value;menu.toggleAttribute('data-open',value);menu.inert=!value;key.setAttribute('aria-expanded',String(value));\n    if(value){menu.firstElementChild.textContent=open?'Close folder':'Open folder';menu.firstElementChild.focus();}\n  }\n  function renderLabel(){\n    hit.setAttribute('aria-label',`${open?'Close':'Open'} ${settings.label}, ${files.length} ${files.length===1?'item':'items'}`);\n    const text=document.createElement('span');text.className='gf-meta';text.textContent=files.length+(files.length===1?' item':' items');q('.gf-meta').replaceWith(text);\n  }\n  function renderFiles(dropped){\n    const start=Math.max(0,files.length-MAX_VISIBLE),visible=files.slice(start);\n    const visibleIds=new Set(visible.map(file=>file.id));\n    // Keep retained sheets attached so their in-flight CSS transitions survive.\n    for(const [id,node]of sheetNodes)if(!visibleIds.has(id)){node.remove();sheetNodes.delete(id);}\n    visible.forEach((file,v)=>{\n      let node=sheetNodes.get(file.id);\n      if(!node){\n        node=document.createElement('div');node.className='gf-sheet-slot';node.setAttribute('role','listitem');\n        const sheet=document.createElement('button');sheet.type='button';sheet.className='gf-sheet'+(file.art?' gf-sheet--art':'');\n        if(file.art)sheet.append(artCanvas('gf-sheet-art',file.art,hashString(file.name)%997,SHEET_W,SHEET_H,1));\n        else{const lines=document.createElement('span');lines.className='gf-lines';lines.setAttribute('aria-hidden','true');lines.innerHTML=[86,92,64,88,42].map(w=>`<i style=\"width:${w}%\"></i>`).join('');sheet.append(lines);}\n        const header=document.createElement('span');header.className='gf-sheet-head';const name=document.createElement('span');name.className='gf-sheet-name';name.textContent=file.name;const meta=document.createElement('span');meta.className='gf-sheet-meta';meta.textContent=file.meta||'';header.append(name,meta);sheet.append(header);\n        sheet.addEventListener('click',()=>{feedback.textContent='Selected: '+file.name;});\n        sheet.addEventListener('animationend',event=>{if(event.target===sheet&&event.animationName==='gf-drop')sheet.classList.remove('gf-sheet--drop');});\n        node.append(sheet);sheetNodes.set(file.id,node);\n      }\n      const sheet=node.firstElementChild,jitter=rng(hashString(file.name)+file.id);\n      sheet.style.setProperty('--gf-slot',fanSlot(v,visible.length));sheet.style.setProperty('--gf-depth',visible.length-1-v);\n      sheet.style.setProperty('--gf-jx',((jitter()-.5)*10).toFixed(2)+'px');sheet.style.setProperty('--gf-jr',((jitter()-.5)*3.2).toFixed(2)+'deg');sheet.style.zIndex=v+1;\n      // A later add supersedes the previous drop, matching the supplied component.\n      sheet.classList.toggle('gf-sheet--drop',file.id===dropped&&!reduceMotion());\n      if(node.parentNode!==filesNode)filesNode.append(node);\n    });\n    renderLabel();\n  }\n  function add(){\n    const id=nextId++;files.push({id,name:'Untitled '+(files.length+1),meta:'Just now'});renderFiles(id);\n    front.classList.remove('gf-front--bump');\n    if(!reduceMotion()){void front.offsetWidth;front.classList.add('gf-front--bump');}\n    // The original keys the glyph by add count; replay it without replacing the button.\n    const glyph=key.querySelector('.gf-glyph--plus');if(glyph)glyph.replaceWith(glyph.cloneNode(true));\n    feedback.textContent='Added a file · '+files.length+' items';\n  }\n  front.addEventListener('animationend',event=>{if(event.target===front&&event.animationName==='gf-bump')front.classList.remove('gf-front--bump');});\n  hit.addEventListener('click',()=>setOpen(!open));\n  if(settings.action==='add'){key.setAttribute('aria-label','Add a file to '+settings.label);key.addEventListener('click',add);}\n  else{\n    key.setAttribute('aria-label','More options for '+settings.label);key.setAttribute('aria-haspopup','menu');key.setAttribute('aria-expanded','false');\n    key.addEventListener('click',()=>setMenu(!menuOpen));\n    for(const [label,action,danger]of [['Open folder',()=>setOpen(!open),false],['Add a file',add,false],['Reset contents',()=>{files=SAMPLE_FILES.map((f,id)=>({...f,id}));nextId=files.length;renderFiles();feedback.textContent='Contents reset';},false]]){\n      const item=document.createElement('button');item.type='button';item.className='gf-menu-item'+(danger?' gf-menu-item--danger':'');item.setAttribute('role','menuitem');item.tabIndex=-1;item.textContent=label;\n      item.addEventListener('click',()=>{setMenu(false);action();key.focus();});menu.append(item);\n    }\n  }\n  menu.addEventListener('keydown',event=>{\n    const items=[...menu.children],at=items.indexOf(document.activeElement);let next;\n    if(event.key==='ArrowDown')next=(at+1)%items.length;else if(event.key==='ArrowUp')next=(at+items.length-1)%items.length;else if(event.key==='Home')next=0;else if(event.key==='End')next=items.length-1;\n    if(next!==undefined){event.preventDefault();items[next].focus();}else if(event.key==='Tab')setMenu(false);\n  });\n  document.addEventListener('keydown',event=>{\n    if(event.key!=='Escape')return;\n    if(menuOpen){setMenu(false);key.focus();}else if(open&&root.contains(event.composedPath()[0])){setOpen(false);hit.focus();}\n  });\n  document.addEventListener('pointerdown',event=>{\n    const target=event.composedPath()[0];if(!root.contains(target)){if(menuOpen)setMenu(false);else if(open)setOpen(false);}\n  });\n  let frame=0,px=0,py=0;\n  function write(){frame=0;root.style.setProperty('--gf-px',px.toFixed(3));root.style.setProperty('--gf-py',py.toFixed(3));}\n  root.addEventListener('pointermove',event=>{\n    if(event.pointerType==='touch'||reduceMotion())return;const bounds=scope.rect(root);px=(scope.point(event).x-bounds.left)/bounds.width*2-1;py=(scope.point(event).y-bounds.top)/bounds.height*2-1;if(!frame)frame=requestAnimationFrame(write);\n  });\n  root.addEventListener('pointerleave',()=>{px=py=0;if(!frame)frame=requestAnimationFrame(write);});\n  const resize=new ResizeObserver(()=>{const width=Math.min(340,host.clientWidth),k=width/W;root.style.width=width+'px';root.style.height=(H+HEADROOM)*k+'px';q('.gf-stage').style.transform=`translateY(${HEADROOM*k}px) scale(${k})`;});resize.observe(host);\n  const enter=new IntersectionObserver(entries=>{if(entries.some(e=>e.isIntersecting)){enter.disconnect();setTimeout(()=>{root.dataset.entered='';},index*100);}},{threshold:.25});enter.observe(root);\n  renderFiles();\n}\ndocument.querySelectorAll('.gf-demo-slot').forEach((host,index)=>makeFolder(host,index,[\n  {label:'My Files',tone:'light',art:'aurora',seed:3,action:'add'},\n  {label:'Studio',tone:'dark',art:'ember',seed:7,action:'more'},\n  {label:'References',tone:'light',art:'glacier',seed:11,action:'add'},\n][index]));\n\n\n\nscope.ready();\nreturn scope.controller;\n}\n","sha256":"65a252ce146e5037c2a332d24c92d8e9bbee7bdf28076517a7d7e1234413bb9a"},{"path":"motion-runtime.ts","content":"/** Shared native DOM lifecycle. No iframe, global IDs, or executable HTML. */\nexport interface MotionOptions { height?: number; reducedMotion?: boolean }\nexport interface MotionController {\n  element: HTMLElement;\n  root: ShadowRoot;\n  update(options: MotionOptions): void;\n  destroy(): void;\n}\nexport interface SurfaceConfig { markup: string; css: string; width: number; height: number; responsive?: boolean }\nconst fonts = new Map<string, { face: FontFace; users: number }>();\n\nexport function createSurface(target: HTMLElement, config: SurfaceConfig, initial: MotionOptions = {}) {\n  let options = { ...initial };\n  let destroyed = false;\n  let scale = 1;\n  const owner = target.ownerDocument;\n  const browser = owner.defaultView!;\n  const element = owner.createElement(\"div\");\n  element.dataset.motionSurface = \"\";\n  element.style.cssText = \"display:block;position:relative;width:100%;min-width:0;overflow:hidden;isolation:isolate;\";\n  target.appendChild(element);\n  const root = element.attachShadow({ mode: \"open\" });\n  const style = owner.createElement(\"style\");\n  style.textContent = `:host{display:block}[hidden]{display:none!important}*{box-sizing:border-box}.motion-frame{position:relative;transform-origin:top left;overflow:hidden}.motion-body{position:relative;width:100%;min-height:100%;margin:0}.motion-body>.leibox-otp.otp-stage{height:var(--motion-height)}.motion-body>.leibox-share.share-stage{min-height:var(--motion-height)}:host([data-reduced-motion=true]) *{animation-duration:0s!important;animation-iteration-count:1!important;transition-duration:0s!important}` + config.css\n    .replace(/(^|[,\\s}>+~])(?:html|body)(?=[\\s,{.#:[>])/g, \"$1.motion-body\")\n    .replace(/:root\\b/g, \".motion-body\")\n    .replace(/100(?:s|d|l)?vh/g, \"var(--motion-height)\")\n    .replace(/position\\s*:\\s*fixed/g, \"position:absolute\");\n  const frame = owner.createElement(\"div\");\n  frame.className = \"motion-frame\";\n  const body = owner.createElement(\"div\");\n  body.className = \"motion-body\";\n  body.innerHTML = config.markup;\n  frame.appendChild(body);\n  root.append(style, frame);\n  const frames = new Set<number>();\n  const deferredFrames = new Set<FrameRequestCallback>();\n  let painted = false;\n  const timeouts = new Set<number>();\n  const intervals = new Set<number>();\n  const observers = new Set<ResizeObserver | IntersectionObserver>();\n  const cleanups: (() => void)[] = [];\n  for (const rule of config.css.matchAll(/@font-face\\s*\\{([^}]+)\\}/g)) {\n    const name = rule[1].match(/font-family:\\s*[\"']?([^;\"']+)/)?.[1]?.trim();\n    const source = rule[1].match(/src:\\s*(url\\([^)]*\\))/)?.[1];\n    if (!name || !source) continue;\n    const key = `${name}:${source}`;\n    let cached = fonts.get(key);\n    if (!cached) { const face = new FontFace(name, source, { weight: \"100 900\" }); owner.fonts.add(face); void face.load().catch(() => {}); cached = { face, users: 0 }; fonts.set(key, cached); }\n    cached.users++;\n    cleanups.push(() => { const entry = fonts.get(key); if (entry && --entry.users === 0) { owner.fonts.delete(entry.face); fonts.delete(key); } });\n  }\n  const ready: EventListenerOrEventListenerObject[] = [];\n  const listeners: { target: EventTarget; type: string; listener: EventListenerOrEventListenerObject; options?: boolean | AddEventListenerOptions }[] = [];\n  const on = (target: EventTarget, type: string, listener: EventListenerOrEventListenerObject, settings?: boolean | AddEventListenerOptions) => {\n    target.addEventListener(type, listener, settings);\n    listeners.push({ target, type, listener, options: settings });\n  };\n  const requestFrame: typeof requestAnimationFrame = (callback) => {\n    if (destroyed) return 0;\n    if (owner.hidden || (painted && reduced())) { deferredFrames.add(callback); return 0; }\n    const id = browser.requestAnimationFrame(time => { frames.delete(id); painted = true; if (!destroyed) callback(time); });\n    frames.add(id); return id;\n  };\n  const cancelFrame = (id: number) => { browser.cancelAnimationFrame(id); frames.delete(id); };\n  const timeout = (callback: () => void, ms?: number) => {\n    const id = browser.setTimeout(() => { timeouts.delete(id); if (!destroyed) callback(); }, ms);\n    timeouts.add(id); return id;\n  };\n  const interval = (callback: () => void, ms?: number) => {\n    const id = browser.setInterval(() => { if (!destroyed) callback(); }, ms);\n    intervals.add(id); return id;\n  };\n  const clearTimeout = (id: number) => { browser.clearTimeout(id); timeouts.delete(id); };\n  const clearInterval = (id: number) => { browser.clearInterval(id); intervals.delete(id); };\n  const query = (selector: string) => selector === \"body\" || selector === \"html\" ? body : root.querySelector(selector);\n  const document = new Proxy(owner, {\n    get(original, key) {\n      if (key === \"body\" || key === \"documentElement\") return body;\n      if (key === \"activeElement\") return root.activeElement;\n      if (key === \"getElementById\") return (id: string) => root.getElementById(id);\n      if (key === \"querySelector\") return query;\n      if (key === \"querySelectorAll\") return (selector: string) => root.querySelectorAll(selector);\n      if (key === \"addEventListener\") return (type: string, listener: EventListenerOrEventListenerObject, settings?: AddEventListenerOptions) => {\n        if (type === \"DOMContentLoaded\") ready.push(listener);\n        else on(owner, type, listener, settings);\n      };\n      if (key === \"removeEventListener\") return owner.removeEventListener.bind(owner);\n      const value = Reflect.get(original, key, original);\n      return typeof value === \"function\" ? value.bind(original) : value;\n    },\n  });\n  const reduced = () => options.reducedMotion ?? browser.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  const resume = () => { if (!owner.hidden && !reduced()) { const callbacks = [...deferredFrames]; deferredFrames.clear(); callbacks.forEach(requestFrame); } };\n  on(owner, \"visibilitychange\", resume);\n  const matchMedia = (query: string) => {\n    const media = browser.matchMedia(query);\n    return new Proxy(media, { get(original, key) {\n      if (key === \"matches\" && query.includes(\"prefers-reduced-motion\")) return reduced();\n      if (key === \"addEventListener\") return (type: string, listener: EventListener) => on(media, type, listener);\n      const value = Reflect.get(original, key, original);\n      return typeof value === \"function\" ? value.bind(original) : value;\n    } });\n  };\n  const window = new Proxy(browser, { get(original, key) {\n    if (key === \"document\") return document;\n    if (key === \"innerWidth\") return frame.offsetWidth;\n    if (key === \"innerHeight\") return config.height;\n    if (key === \"requestAnimationFrame\") return requestFrame;\n    if (key === \"cancelAnimationFrame\") return cancelFrame;\n    if (key === \"setTimeout\") return timeout;\n    if (key === \"clearTimeout\") return clearTimeout;\n    if (key === \"setInterval\") return interval;\n    if (key === \"clearInterval\") return clearInterval;\n    if (key === \"matchMedia\") return matchMedia;\n    if (key === \"addEventListener\") return (type: string, listener: EventListener, settings?: AddEventListenerOptions) => on(browser, type, listener, settings);\n    const value = Reflect.get(original, key, original);\n    return typeof value === \"function\" ? value.bind(original) : value;\n  } });\n  const resize = () => {\n    const available = element.clientWidth || config.width;\n    scale = config.responsive ? 1 : Math.min(1, available / config.width);\n    const height = options.height ?? config.height;\n    element.style.setProperty(\"--motion-height\", `${height}px`);\n    frame.style.width = `${config.responsive ? available : Math.max(config.width, available)}px`;\n    frame.style.height = config.responsive ? \"auto\" : `${height}px`;\n    frame.style.minHeight = config.responsive ? `${height}px` : \"0\";\n    frame.style.transform = `scale(${scale})`;\n    body.style.minHeight = `${height}px`;\n    element.style.height = config.responsive ? \"auto\" : `${height * scale}px`;\n    element.dataset.reducedMotion = String(reduced());\n  };\n  const resizeObserver = new browser.ResizeObserver(resize);\n  resizeObserver.observe(element);\n  resize();\n  const TrackedResizeObserver = class extends browser.ResizeObserver {\n    constructor(callback: ResizeObserverCallback) { super(callback); observers.add(this); }\n  };\n  const TrackedIntersectionObserver = class extends browser.IntersectionObserver {\n    constructor(callback: IntersectionObserverCallback, init?: IntersectionObserverInit) { super(callback, init); observers.add(this); }\n  };\n  const controller: MotionController = {\n    element, root,\n    update(next) { if (destroyed) return; options = { ...options, ...next }; resize(); resume(); },\n    destroy() {\n      if (destroyed) return;\n      destroyed = true;\n      frames.forEach(id => browser.cancelAnimationFrame(id));\n      timeouts.forEach(id => browser.clearTimeout(id));\n      intervals.forEach(id => browser.clearInterval(id));\n      resizeObserver.disconnect(); observers.forEach(observer => observer.disconnect());\n      listeners.forEach(({ target, type, listener, options }) => target.removeEventListener(type, listener, options));\n      cleanups.reverse().forEach(cleanup => cleanup());\n      root.querySelectorAll(\"dialog[open]\").forEach(dialog => (dialog as HTMLDialogElement).close());\n      element.remove();\n      frames.clear(); deferredFrames.clear(); timeouts.clear(); intervals.clear();\n    },\n  };\n  return {\n    ...controller, controller, body, frame, document, window, on, reduced,\n    requestAnimationFrame: requestFrame, cancelAnimationFrame: cancelFrame,\n    setTimeout: timeout, clearTimeout, setInterval: interval, clearInterval,\n    ResizeObserver: TrackedResizeObserver, IntersectionObserver: TrackedIntersectionObserver, matchMedia,\n    addCleanup(cleanup: () => void) { cleanups.push(cleanup); },\n    ready() { ready.splice(0).forEach(listener => { const event = new Event(\"DOMContentLoaded\"); if (typeof listener === \"function\") listener.call(document, event); else listener.handleEvent(event); }); },\n    point(event: { clientX: number; clientY: number }) { const rect = frame.getBoundingClientRect(); return { x: (event.clientX - rect.x) / scale, y: (event.clientY - rect.y) / scale }; },\n    rect(node: Element) { const rect = node.getBoundingClientRect(); const origin = frame.getBoundingClientRect(); return { x: (rect.x - origin.x) / scale, y: (rect.y - origin.y) / scale, left: (rect.left - origin.left) / scale, top: (rect.top - origin.top) / scale, right: (rect.right - origin.left) / scale, bottom: (rect.bottom - origin.top) / scale, width: rect.width / scale, height: rect.height / scale }; },\n  };\n}\n","sha256":"17c1eb5d22c3b554dd54ba5489f2724b69ff1a1c78ebb0abc480555c7c88ffb8"},{"path":"GlassFolderEngine.d.ts","content":"import type { MotionOptions, MotionController } from \"./motion-runtime\";\nexport type GlassFolderOptions = MotionOptions;\nexport function mountGlassFolder(target: HTMLElement, options?: GlassFolderOptions): MotionController;\n","sha256":"4b67c6d913fedc5fa05c0ae5c0aa64962f9e94300452b555cda2953d12ee36df"}]}