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