PagesStructures['id1789981756489'] = {"structures":[{"type":"page","id":"id1789981756489","elementProperties":{"tagName":"DIV","behaviours":null,"isLocked":true,"name":"On-Line Gallery Demo","baseName":"Precision Art Grid (Demo)","templateID":null,"templateName":null,"order":0,"customStates":null,"isElementWasEditedInMode":true,"showHoverEffects":true,"skinInformation":{"categoryId":245},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"width":0,"height":1068,"top":0,"left":0},"lastResizingSize":{"width":0,"height":1068,"top":0,"left":0},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"verticalAlign":"center","horizontalAlign":"center","horizontalContentAlign":"center","basicUserCanEdit":true,"overflow":false,"isGrowable":{"bottom":false,"right":false},"rowsItems":null,"childrenAnchor":"center","backgroundColor":{"mainColor":0,"variation":0,"opacity":1,"brightness":0},"imageUrl":"","imageLayout":"fitWhenBigger","imagePosition":{"horizontal":"center","vertical":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","gridLines":[],"fixedBackground":false,"statesGroups":null},"metaData":{"position":{"left":-598.5,"top":0},"size":{"width":1197,"height":2570}},"childElements":[{"type":"SectionElement","id":"id1789981756490","elementProperties":{"tagName":"SECTION","behaviours":null,"isLocked":false,"name":"Section 2","baseName":null,"templateID":null,"templateName":null,"order":3,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"height":418,"left":0,"width":1320,"top":0},"lastResizingSize":{"height":418,"left":0,"width":1320,"top":0},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"padding":{"top":0,"left":0,"bottom":0,"right":0},"basicUserCanEdit":true,"backgroundColor":"transparent","imageUrl":"","imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillVertical":false,"fillHorizontal":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"overflow":false,"isGrowable":{"bottom":false,"right":false},"rowsItems":null,"childrenAnchor":"center","resizingGripPosiotion":"s","sectionName":"Section A","pageContentType":1,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"bottomLeft":0,"topRight":0,"bottomRight":0},"borderColor":{"top":"Black","left":"Black","bottom":"Black","right":"Black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"}},"metaData":{"position":{"left":-598.5,"top":100},"size":{"width":1197,"height":418}}},{"type":"SectionElement","id":"id1790012004518","elementProperties":{"tagName":"SECTION","behaviours":null,"isLocked":false,"name":"Section 5","baseName":null,"templateID":null,"templateName":null,"order":4,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{"categoryId":199},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"height":306,"left":0,"width":1320,"top":0},"lastResizingSize":{"height":306,"left":0,"width":1320,"top":0},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"padding":{"top":0,"left":0,"bottom":0,"right":0},"basicUserCanEdit":true,"backgroundColor":"transparent","imageUrl":"","imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillVertical":false,"fillHorizontal":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"overflow":false,"isGrowable":{"bottom":false,"right":false},"rowsItems":null,"childrenAnchor":"center","resizingGripPosiotion":"s","sectionName":"Section 2","pageContentType":1,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"bottomLeft":0,"topRight":0,"bottomRight":0},"borderColor":{"top":"Black","left":"Black","bottom":"Black","right":"Black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"}},"metaData":{"position":{"left":-598.5,"top":518},"size":{"width":1197,"height":306}}},{"type":"SectionElement","id":"id1789981777514","elementProperties":{"tagName":"SECTION","behaviours":null,"isLocked":false,"name":"Section 4","baseName":null,"templateID":null,"templateName":null,"order":5,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{"categoryId":199},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"height":914,"left":0,"width":1261,"top":0},"lastResizingSize":{"height":914,"left":0,"width":1261,"top":0},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"padding":{"top":0,"left":0,"bottom":0,"right":0},"basicUserCanEdit":true,"backgroundColor":"transparent","imageUrl":"","imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillVertical":false,"fillHorizontal":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"overflow":false,"isGrowable":{"bottom":false,"right":false},"rowsItems":null,"childrenAnchor":"center","resizingGripPosiotion":"s","sectionName":"Section 1","pageContentType":1,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"bottomLeft":0,"topRight":0,"bottomRight":0},"borderColor":{"top":"Black","left":"Black","bottom":"Black","right":"Black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"}},"metaData":{"position":{"left":-598.5,"top":824},"size":{"width":1197,"height":914}},"childElements":[{"type":"userBox","id":"id1790011247470","elementProperties":{"tagName":"DIV","behaviours":null,"isLocked":false,"name":"Box 1","baseName":null,"templateID":"templateid1790011247471","templateName":null,"order":9,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"width":1320,"height":1591,"left":-655,"top":-943},"lastResizingSize":{"width":1320,"height":1591,"left":-655,"top":-943},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"display":"fixed","cellItem":null,"cellHorizontalAlignment":"none","padding":{"top":0,"left":0,"bottom":0,"right":0},"basicUserCanEdit":true,"backgroundColor":"rgb(245,240,232)","imageUrl":"","imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillHorizontal":false,"fillVertical":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"overflow":false,"isGrowable":{"bottom":false,"right":false},"rowsItems":null,"childrenAnchor":"left","startSelected":false,"urlAddress":null,"openInNewTab":false,"openInTopWindow":false,"shouldUnderlineChildren":false,"isVisible":true,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"bottomLeft":0,"topRight":0,"bottomRight":0},"borderColor":{"top":"Black","left":"Black","bottom":"Black","right":"Black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"},"rotationAngle":0},"metaData":{"position":{"left":-655,"top":-119},"size":{"width":1320,"height":1591}},"childElements":[{"type":"ExternalAppembed_html_app1","id":"id1789981789465","elementProperties":{"tagName":"DIV","behaviours":null,"isLocked":false,"name":"Embed Html 1","baseName":null,"templateID":"templateid1789981789466","templateName":null,"order":10,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"width":960,"height":644,"left":180,"top":847},"lastResizingSize":{"width":960,"height":644,"left":180,"top":847},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":10,"left":10,"spread":0,"blur":20,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"basicUserCanEdit":true,"backgroundColor":"transparent","padding":{"top":0,"left":0,"bottom":0,"right":0},"imageUrl":"","imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillHorizontal":false,"fillVertical":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"isVisible":true,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"topRight":0,"bottomLeft":0,"bottomRight":0},"borderColor":{"top":"Black","left":"Black","bottom":"Black","right":"Black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"},"rotationAngle":0,"display":"fixed","cellItem":null,"cellHorizontalAlignment":"none","verticalAlign":"center","horizontalAlign":"center","horizontalContentAlign":"center","embed_html_app1html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n  <title>Imagine Art Gallery — Professional Viewing Rooms for Artists</title>\n  <style>\n    :root {\n      --bg: #0f0f10;\n      --panel: rgba(22, 22, 24, 0.94);\n      --panel-border: rgba(255, 255, 255, 0.08);\n      --accent: #c9a86c;\n      --accent-hover: #dbb97a;\n      --accent-dim: rgba(201, 168, 108, 0.15);\n      --text: #f2f0eb;\n      --text-muted: #9a9690;\n      --text-dim: #6b6762;\n      --danger: #e05a5a;\n      --success: #6bbf8a;\n      --radius: 10px;\n      --radius-sm: 6px;\n      --shadow: 0 12px 40px rgba(0, 0, 0, 0.55);\n      --font: \"Segoe UI\", system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;\n    }\n\n    * { box-sizing: border-box; margin: 0; padding: 0; }\n    body, html {\n      width: 100vw; height: 100vh; overflow: hidden;\n      background: var(--bg);\n      font-family: var(--font);\n      color: var(--text);\n      -webkit-font-smoothing: antialiased;\n    }\n\n    /* Canvas layers */\n    #fx-canvas {\n      position: absolute; inset: 0;\n      width: 100vw; height: 100vh;\n      pointer-events: none; z-index: 5;\n    }\n    /* WebGL renderer canvas — must cover viewport or export/main can show black bars */\n    body > canvas:not(#fx-canvas) {\n      position: absolute !important;\n      inset: 0 !important;\n      width: 100% !important;\n      height: 100% !important;\n      display: block;\n      z-index: 1;\n    }\n\n    /* Live crop guide for non-native aspect recording */\n    #recordFrameOverlay {\n      display: none;\n      position: fixed;\n      inset: 0;\n      z-index: 9;\n      pointer-events: none;\n    }\n    #recordFrameOverlay.active { display: block; }\n    .rf-bar {\n      position: absolute;\n      background: rgba(0, 0, 0, 0.55);\n    }\n    .rf-guide {\n      position: absolute;\n      border: 1.5px solid rgba(201, 168, 108, 0.85);\n      box-shadow: 0 0 0 1px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(0,0,0,0.2);\n      box-sizing: border-box;\n    }\n    .rf-label {\n      position: absolute;\n      left: 50%;\n      transform: translateX(-50%);\n      top: 8px;\n      background: rgba(22,22,24,0.92);\n      border: 1px solid rgba(201,168,108,0.55);\n      color: var(--accent);\n      font-size: 10px;\n      font-weight: 700;\n      letter-spacing: 0.08em;\n      text-transform: uppercase;\n      padding: 4px 10px;\n      border-radius: 20px;\n      white-space: nowrap;\n    }\n\n    /* Top bar */\n    .top-bar {\n      position: absolute; top: 0; left: 0; right: 0;\n      height: 52px; z-index: 40;\n      display: flex; align-items: center; justify-content: space-between;\n      padding: 0 16px;\n      background: linear-gradient(to bottom, rgba(15,15,16,0.92), transparent);\n      pointer-events: none;\n    }\n    .top-bar > * { pointer-events: auto; }\n\n    .brand {\n      display: flex; align-items: center; gap: 10px;\n      font-weight: 600; font-size: 14px; letter-spacing: 0.04em;\n      color: var(--text);\n    }\n    .brand-mark {\n      width: 28px; height: 28px; border-radius: 8px;\n      background: linear-gradient(135deg, var(--accent), #a88b4a);\n      display: grid; place-items: center;\n      font-size: 14px; font-weight: 700; color: #1a1612;\n    }\n    .brand span { opacity: 0.9; }\n\n    .top-actions { display: flex; gap: 8px; align-items: center; }\n\n    .btn-icon {\n      background: rgba(255,255,255,0.06);\n      border: 1px solid var(--panel-border);\n      color: var(--text);\n      width: 36px; height: 36px; border-radius: var(--radius-sm);\n      cursor: pointer; display: grid; place-items: center;\n      font-size: 15px; transition: background 0.2s, border-color 0.2s;\n    }\n    .btn-icon:hover { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }\n\n    /* Menu toggle */\n    #menuToggleBtn {\n      background: rgba(22,22,24,0.9);\n      border: 1px solid var(--panel-border);\n      color: var(--text);\n      padding: 8px 14px; border-radius: var(--radius-sm);\n      font-weight: 600; font-size: 12px;\n      cursor: pointer; letter-spacing: 0.03em;\n      transition: background 0.2s, border-color 0.2s, color 0.2s;\n    }\n    #menuToggleBtn:hover {\n      background: var(--accent-dim);\n      border-color: var(--accent);\n      color: var(--accent);\n    }\n\n    /* Dropzone */\n    #dropzone-overlay {\n      display: none; position: fixed; inset: 0;\n      background: rgba(201, 168, 108, 0.12);\n      border: 3px dashed var(--accent);\n      z-index: 9999; pointer-events: none;\n      align-items: center; justify-content: center;\n      backdrop-filter: blur(6px);\n    }\n    #dropzone-overlay.active { display: flex; }\n    .dropzone-msg {\n      background: var(--panel); border: 1px solid var(--accent);\n      padding: 22px 40px; border-radius: var(--radius);\n      font-size: 16px; font-weight: 600; color: var(--accent);\n      letter-spacing: 0.04em; box-shadow: var(--shadow);\n    }\n\n    /* Editor panel */\n    .editor-panel {\n      position: absolute; top: 60px; left: 16px;\n      width: 360px; max-height: calc(100vh - 100px);\n      background: var(--panel);\n      backdrop-filter: blur(16px) saturate(1.2);\n      border: 1px solid var(--panel-border);\n      border-radius: 14px;\n      z-index: 30; overflow: hidden;\n      box-shadow: var(--shadow);\n      display: flex; flex-direction: column;\n      transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease;\n    }\n    .editor-panel.collapsed {\n      transform: translateX(calc(-100% - 30px));\n      opacity: 0; pointer-events: none;\n    }\n\n    .panel-header {\n      padding: 16px 18px 12px;\n      border-bottom: 1px solid var(--panel-border);\n      flex-shrink: 0;\n    }\n    .panel-header h1 {\n      font-size: 13px; font-weight: 700; letter-spacing: 0.06em;\n      text-transform: uppercase; color: var(--accent);\n      margin-bottom: 2px;\n    }\n    .panel-header p {\n      font-size: 11px; color: var(--text-muted);\n    }\n\n    /* Tabs */\n    .tabs {\n      display: flex; gap: 2px; padding: 10px 12px 0;\n      border-bottom: 1px solid var(--panel-border);\n      flex-shrink: 0; overflow-x: auto;\n    }\n    .tab {\n      flex: 1; min-width: 0;\n      background: transparent; border: none;\n      color: var(--text-muted);\n      padding: 10px 6px; font-size: 11px; font-weight: 600;\n      letter-spacing: 0.03em; text-transform: uppercase;\n      cursor: pointer; border-radius: var(--radius-sm) var(--radius-sm) 0 0;\n      transition: color 0.2s, background 0.2s;\n      white-space: nowrap;\n    }\n    .tab:hover { color: var(--text); }\n    .tab.active {\n      color: var(--accent);\n      background: var(--accent-dim);\n      box-shadow: inset 0 -2px 0 var(--accent);\n    }\n\n    .panel-body {\n      flex: 1; overflow-y: auto; padding: 14px 16px 20px;\n      scrollbar-width: thin;\n      scrollbar-color: rgba(201,168,108,0.35) transparent;\n    }\n    .panel-body::-webkit-scrollbar { width: 6px; }\n    .panel-body::-webkit-scrollbar-thumb {\n      background: rgba(201,168,108,0.35); border-radius: 3px;\n    }\n\n    .tab-content { display: none; }\n    .tab-content.active { display: block; }\n\n    .section-label {\n      font-size: 10px; font-weight: 700; letter-spacing: 0.08em;\n      text-transform: uppercase; color: var(--text-dim);\n      margin: 14px 0 8px; padding-top: 6px;\n      border-top: 1px solid var(--panel-border);\n    }\n    .section-label:first-child { margin-top: 0; border-top: none; padding-top: 0; }\n\n    .control-group { margin-bottom: 12px; }\n    .control-group label {\n      display: flex; justify-content: space-between; align-items: center;\n      font-size: 11px; font-weight: 600; color: var(--text-muted);\n      margin-bottom: 5px; letter-spacing: 0.02em;\n    }\n    .val-display {\n      font-size: 11px; color: var(--accent); font-weight: 500;\n    }\n\n    .control-group input[type=\"text\"],\n    .control-group select {\n      width: 100%;\n      background: rgba(0,0,0,0.35);\n      border: 1px solid var(--panel-border);\n      color: var(--text);\n      padding: 9px 11px; border-radius: var(--radius-sm);\n      font-size: 13px; outline: none;\n      transition: border-color 0.2s;\n    }\n    .control-group input[type=\"text\"]:focus,\n    .control-group select:focus {\n      border-color: var(--accent);\n    }\n    .control-group input[type=\"range\"] {\n      width: 100%; accent-color: var(--accent); cursor: pointer; height: 6px;\n    }\n    .control-group input[type=\"color\"] {\n      width: 100%; height: 34px; border: 1px solid var(--panel-border);\n      border-radius: var(--radius-sm); background: transparent; cursor: pointer;\n      padding: 2px;\n    }\n    .control-group input[type=\"file\"] {\n      width: 100%; font-size: 12px; color: var(--text-muted);\n    }\n\n    .checkbox-row {\n      display: flex; align-items: center; justify-content: space-between;\n      margin-bottom: 10px; padding: 6px 0;\n    }\n    .checkbox-row label {\n      font-size: 12px; color: var(--text); font-weight: 500; cursor: pointer;\n    }\n    .checkbox-row input[type=\"checkbox\"] {\n      accent-color: var(--accent); width: 17px; height: 17px; cursor: pointer;\n    }\n\n    .btn-row { display: flex; gap: 8px; margin-bottom: 10px; }\n    .btn-row.wrap { flex-wrap: wrap; }\n\n    .btn {\n      flex: 1; min-width: 0;\n      background: rgba(255,255,255,0.05);\n      border: 1px solid var(--panel-border);\n      color: var(--text);\n      padding: 9px 10px; border-radius: var(--radius-sm);\n      font-weight: 600; font-size: 11px; letter-spacing: 0.03em;\n      cursor: pointer; text-transform: uppercase;\n      transition: background 0.2s, border-color 0.2s, color 0.2s;\n      text-decoration: none; display: inline-flex;\n      align-items: center; justify-content: center; gap: 4px;\n    }\n    .btn:hover {\n      background: var(--accent-dim);\n      border-color: var(--accent);\n      color: var(--accent);\n    }\n    .btn-primary {\n      background: var(--accent); color: #1a1612; border-color: var(--accent);\n    }\n    .btn-primary:hover {\n      background: var(--accent-hover); border-color: var(--accent-hover); color: #1a1612;\n    }\n    .btn-danger {\n      border-color: rgba(224,90,90,0.5); color: var(--danger);\n    }\n    .btn-danger:hover {\n      background: rgba(224,90,90,0.15); border-color: var(--danger); color: var(--danger);\n    }\n    .btn-danger.recording {\n      background: var(--danger); color: #fff; border-color: var(--danger);\n      animation: pulse-rec 1.2s ease-in-out infinite;\n    }\n    @keyframes pulse-rec {\n      0%, 100% { opacity: 1; }\n      50% { opacity: 0.65; }\n    }\n    .btn-full { width: 100%; margin-top: 6px; }\n\n    .record-timer {\n      display: none; font-size: 12px; font-weight: 700;\n      color: var(--danger); text-align: center;\n      letter-spacing: 0.08em; margin-bottom: 8px;\n    }\n    .record-timer.active { display: block; }\n\n    /* Bottom control bar */\n    .arc-control-bar {\n      position: absolute; bottom: 20px; left: 50%;\n      transform: translateX(-50%);\n      width: min(620px, 94vw);\n      background: var(--panel);\n      backdrop-filter: blur(16px);\n      border: 1px solid var(--panel-border);\n      padding: 12px 18px 14px;\n      border-radius: 16px;\n      z-index: 15; text-align: center;\n      box-shadow: var(--shadow);\n    }\n    .arc-control-bar .bar-label {\n      display: block; font-size: 10px; font-weight: 700;\n      color: var(--accent); letter-spacing: 0.1em;\n      margin-bottom: 6px; text-transform: uppercase;\n    }\n    .arc-control-bar input[type=\"range\"] {\n      width: 100%; cursor: pointer; accent-color: var(--accent); margin-bottom: 8px;\n    }\n    .player-controls {\n      display: flex; justify-content: center; flex-wrap: wrap; gap: 6px;\n    }\n    .player-controls .btn {\n      flex: 0 1 auto; padding: 8px 12px; font-size: 10px;\n    }\n\n    /* Modal */\n    .modal-overlay {\n      display: none; position: fixed; inset: 0;\n      background: rgba(0,0,0,0.72); z-index: 100;\n      align-items: center; justify-content: center;\n      backdrop-filter: blur(4px);\n    }\n    .modal-card {\n      background: #1a1a1c; border: 1px solid var(--panel-border);\n      border-radius: 14px; padding: 28px;\n      width: 380px; max-width: 92%; color: var(--text);\n      position: relative; box-shadow: var(--shadow);\n    }\n    .modal-close {\n      position: absolute; top: 14px; right: 16px;\n      color: var(--text-muted); font-size: 22px; cursor: pointer;\n      line-height: 1; transition: color 0.2s;\n    }\n    .modal-close:hover { color: var(--text); }\n    .modal-title {\n      font-size: 20px; font-weight: 700; color: var(--accent);\n      margin-bottom: 6px; letter-spacing: -0.01em;\n    }\n    .modal-details {\n      font-size: 13px; color: var(--text-muted);\n      margin-bottom: 14px; line-height: 1.55;\n    }\n    .modal-price {\n      font-size: 22px; font-weight: 700; color: var(--text);\n      margin-bottom: 18px;\n    }\n    .btn-modal-buy {\n      display: block; width: 100%; text-align: center;\n      background: var(--accent); color: #1a1612;\n      padding: 13px; border-radius: var(--radius-sm);\n      font-weight: 700; text-decoration: none; font-size: 13px;\n      letter-spacing: 0.04em; transition: background 0.2s;\n    }\n    .btn-modal-buy:hover { background: var(--accent-hover); }\n\n    /* Quality badge */\n    .quality-badge {\n      font-size: 10px; padding: 3px 8px; border-radius: 20px;\n      background: var(--accent-dim); color: var(--accent);\n      font-weight: 600; letter-spacing: 0.04em;\n    }\n\n    #hiddenVideo { display: none; }\n\n    /* Empty state overlay in scene */\n    #emptyState {\n      position: absolute; inset: 0; z-index: 8;\n      display: flex; align-items: center; justify-content: center;\n      pointer-events: none;\n    }\n    #emptyState.hidden { display: none; }\n    .empty-card {\n      background: rgba(22,22,24,0.88);\n      border: 1.5px dashed rgba(201,168,108,0.55);\n      border-radius: 16px;\n      padding: 36px 48px;\n      text-align: center;\n      max-width: 380px;\n      box-shadow: 0 20px 50px rgba(0,0,0,0.45);\n      pointer-events: auto;\n    }\n    .empty-card h2 {\n      font-size: 18px; font-weight: 600; color: var(--accent);\n      margin-bottom: 8px; letter-spacing: 0.02em;\n    }\n    .empty-card p {\n      font-size: 13px; color: var(--text-muted); line-height: 1.5;\n      margin-bottom: 18px;\n    }\n    .empty-card .btn {\n      display: inline-flex; padding: 10px 18px; font-size: 12px;\n    }\n\n    /* Watermark — always visible on demo build */\n    #watermark {\n      position: absolute; bottom: 90px; right: 20px; z-index: 12;\n      font-size: 12px; font-weight: 600; letter-spacing: 0.06em;\n      color: rgba(255,255,255,0.35);\n      text-transform: uppercase;\n      pointer-events: none;\n      user-select: none;\n    }\n\n    /* Welcome / start modal */\n    #welcomeModal.modal-overlay {\n      display: flex;\n      z-index: 200;\n    }\n    #welcomeModal.hidden {\n      display: none;\n    }\n    .welcome-card {\n      background: #1a1a1c;\n      border: 1px solid var(--panel-border);\n      border-radius: 16px;\n      padding: 28px 28px 24px;\n      width: 420px;\n      max-width: 92%;\n      color: var(--text);\n      position: relative;\n      box-shadow: var(--shadow);\n    }\n    .welcome-card .welcome-mark {\n      width: 40px; height: 40px; border-radius: 10px;\n      background: linear-gradient(135deg, var(--accent), #a88b4a);\n      display: grid; place-items: center;\n      font-size: 18px; font-weight: 700; color: #1a1612;\n      margin-bottom: 14px;\n    }\n    .welcome-card h2 {\n      font-size: 20px; font-weight: 700; color: var(--accent);\n      margin-bottom: 8px; letter-spacing: -0.01em;\n    }\n    .welcome-card .welcome-lead {\n      font-size: 13px; color: var(--text-muted); line-height: 1.55;\n      margin-bottom: 16px;\n    }\n    .welcome-steps {\n      list-style: none;\n      margin: 0 0 20px;\n      padding: 0;\n    }\n    .welcome-steps li {\n      display: flex; gap: 10px; align-items: flex-start;\n      font-size: 13px; color: var(--text); line-height: 1.45;\n      margin-bottom: 10px;\n    }\n    .welcome-steps .step-num {\n      flex-shrink: 0;\n      width: 22px; height: 22px; border-radius: 50%;\n      background: var(--accent-dim); color: var(--accent);\n      font-size: 11px; font-weight: 700;\n      display: grid; place-items: center;\n      margin-top: 1px;\n    }\n    .welcome-steps strong { color: var(--accent); font-weight: 600; }\n    .welcome-card .btn-start {\n      display: block; width: 100%; text-align: center;\n      background: var(--accent); color: #1a1612;\n      padding: 13px; border-radius: var(--radius-sm);\n      font-weight: 700; font-size: 13px; letter-spacing: 0.04em;\n      border: none; cursor: pointer;\n      transition: background 0.2s;\n    }\n    .welcome-card .btn-start:hover { background: var(--accent-hover); }\n    .welcome-note {\n      margin-top: 12px; font-size: 11px; color: var(--text-dim);\n      text-align: center; line-height: 1.45;\n    }\n\n    /* Persistent tips bar */\n    #tipsBar {\n      position: absolute; top: 52px; left: 50%;\n      transform: translateX(-50%);\n      z-index: 25;\n      width: min(720px, calc(100vw - 32px));\n      background: rgba(22,22,24,0.9);\n      border: 1px solid var(--panel-border);\n      backdrop-filter: blur(12px);\n      border-radius: 10px;\n      padding: 8px 14px;\n      font-size: 11px; color: var(--text-muted);\n      line-height: 1.45;\n      text-align: center;\n      letter-spacing: 0.01em;\n      box-shadow: 0 8px 24px rgba(0,0,0,0.35);\n      pointer-events: none;\n    }\n    #tipsBar strong { color: var(--accent); font-weight: 600; }\n    #tipsBar.hidden { display: none; }\n\n    /* Mobile tweaks */\n    @media (max-width: 640px) {\n      .editor-panel {\n        width: calc(100vw - 24px); left: 12px;\n        max-height: calc(100vh - 90px);\n      }\n      .arc-control-bar { bottom: 12px; padding: 10px 12px; }\n      .player-controls .btn { padding: 7px 8px; font-size: 9px; }\n      .brand span { display: none; }\n      #tipsBar { top: 48px; font-size: 10px; padding: 7px 10px; }\n      #watermark { bottom: 100px; right: 12px; font-size: 10px; }\n    }\n  </style>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js\"></script>\n</head>\n<body>\n\n  <canvas id=\"fx-canvas\"></canvas>\n  <video id=\"hiddenVideo\" loop playsinline muted crossorigin=\"anonymous\"></video>\n\n  <div class=\"top-bar\">\n    <div class=\"brand\">\n      <div class=\"brand-mark\">I</div>\n      <span>Imagine Art Gallery</span>\n    </div>\n    <div class=\"top-actions\">\n      <span class=\"quality-badge\" id=\"qualityBadge\">Balanced</span>\n      <button id=\"menuToggleBtn\" type=\"button\">Hide Panel</button>\n    </div>\n  </div>\n\n  <div id=\"dropzone-overlay\">\n    <div class=\"dropzone-msg\">Drop Image or Video</div>\n  </div>\n\n  <div id=\"recordFrameOverlay\" aria-hidden=\"true\">\n    <div class=\"rf-bar\" id=\"rfTop\"></div>\n    <div class=\"rf-bar\" id=\"rfBottom\"></div>\n    <div class=\"rf-bar\" id=\"rfLeft\"></div>\n    <div class=\"rf-bar\" id=\"rfRight\"></div>\n    <div class=\"rf-guide\" id=\"rfGuide\"></div>\n    <div class=\"rf-label\" id=\"rfLabel\">Recording frame</div>\n  </div>\n\n  <div class=\"editor-panel\" id=\"editorPanel\">\n    <div class=\"panel-header\">\n      <h1>Studio Controls</h1>\n      <p>Create a gallery-ready presentation</p>\n    </div>\n\n    <div class=\"tabs\">\n      <button class=\"tab active\" data-tab=\"media\">Media</button>\n      <button class=\"tab\" data-tab=\"room\">Room</button>\n      <button class=\"tab\" data-tab=\"fx\">FX</button>\n      <button class=\"tab\" data-tab=\"camera\">Camera</button>\n      <button class=\"tab\" data-tab=\"export\">Export</button>\n    </div>\n\n    <div class=\"panel-body\">\n      <!-- MEDIA TAB -->\n      <div class=\"tab-content active\" id=\"tab-media\">\n        <div class=\"section-label\">Artwork Source</div>\n        <div class=\"control-group\">\n          <label>Media Mode</label>\n          <select id=\"mediaTypeSelector\">\n            <option value=\"image\">Static Artwork Image</option>\n            <option value=\"video\">Video Loop Texture</option>\n          </select>\n        </div>\n        <div class=\"control-group\" id=\"imageUploadGroup\">\n          <label>Artwork File</label>\n          <input type=\"file\" id=\"artUploader\" accept=\"image/*\">\n        </div>\n        <div class=\"control-group\" id=\"videoUploadGroup\" style=\"display:none;\">\n          <label>Video File (MP4 / WebM)</label>\n          <input type=\"file\" id=\"videoUploader\" accept=\"video/*\">\n        </div>\n\n        <div id=\"videoControlsGroup\" style=\"display:none;\">\n          <div class=\"btn-row\">\n            <button class=\"btn\" type=\"button\" onclick=\"toggleVideoPlay()\">Play / Pause</button>\n            <button class=\"btn\" type=\"button\" onclick=\"toggleVideoMute()\">Mute / Unmute</button>\n          </div>\n          <div class=\"checkbox-row\">\n            <label for=\"videoLoopToggle\">Loop Video</label>\n            <input type=\"checkbox\" id=\"videoLoopToggle\" checked>\n          </div>\n          <div class=\"control-group\">\n            <label>Playback Speed <span id=\"videoSpeedVal\" class=\"val-display\">1.0×</span></label>\n            <input type=\"range\" id=\"videoSpeedSlider\" min=\"0.25\" max=\"2.0\" step=\"0.25\" value=\"1.0\">\n          </div>\n        </div>\n\n        <div class=\"section-label\">Presentation</div>\n        <div class=\"control-group\">\n          <label>Canvas Scale <span id=\"scaleVal\" class=\"val-display\">1.00×</span></label>\n          <input type=\"range\" id=\"scaleSlider\" min=\"0.4\" max=\"2.0\" step=\"0.05\" value=\"1.0\">\n        </div>\n        <div class=\"control-group\">\n          <label>Frame Style</label>\n          <select id=\"styleSelector\">\n            <option value=\"floater\">Modern Floater</option>\n            <option value=\"classic\">Classic</option>\n            <option value=\"gallery\">Gallery Stretched</option>\n          </select>\n        </div>\n        <div class=\"checkbox-row\">\n          <label for=\"matToggle\">Digital Mat / Passepartout</label>\n          <input type=\"checkbox\" id=\"matToggle\">\n        </div>\n        <div class=\"control-group\">\n          <label>Mat Width <span id=\"matWidthVal\" class=\"val-display\">0.12</span></label>\n          <input type=\"range\" id=\"matWidthSlider\" min=\"0.04\" max=\"0.30\" step=\"0.01\" value=\"0.12\">\n        </div>\n        <div class=\"control-group\">\n          <label>Mat Color</label>\n          <input type=\"color\" id=\"matColorPicker\" value=\"#f4f1ea\">\n        </div>\n\n        <div class=\"section-label\">Artwork Details</div>\n        <div class=\"checkbox-row\">\n          <label for=\"placardToggle\">Wall Placard</label>\n          <input type=\"checkbox\" id=\"placardToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>Title</label>\n          <input type=\"text\" id=\"titleInput\" value=\"Artwork Title\">\n        </div>\n        <div class=\"control-group\">\n          <label>Medium / Year</label>\n          <input type=\"text\" id=\"mediumInput\" value=\"Medium, Year\">\n        </div>\n        <div class=\"control-group\">\n          <label>Dimensions</label>\n          <input type=\"text\" id=\"dimInput\" value=\"Dimensions\">\n        </div>\n        <div class=\"control-group\">\n          <label>Price</label>\n          <input type=\"text\" id=\"priceInput\" value=\"Price\">\n        </div>\n        <div class=\"control-group\">\n          <label>Buy Link</label>\n          <input type=\"text\" id=\"linkInput\" value=\"https://your-site.com\">\n        </div>\n      </div>\n\n      <!-- ROOM TAB -->\n      <div class=\"tab-content\" id=\"tab-room\">\n        <div class=\"section-label\">Room Template</div>\n        <div class=\"control-group\">\n          <label>Scene Style</label>\n          <select id=\"roomTemplateSelector\">\n            <option value=\"classic\">Classic Gallery</option>\n            <option value=\"whiteCube\">Clean White Cube</option>\n            <option value=\"warmTraditional\">Warm Traditional</option>\n            <option value=\"modernLoft\">Modern Loft</option>\n          </select>\n        </div>\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin:-4px 0 12px;\">\n          Templates set lighting, surfaces, and furniture in one click. Fine-tune anything below.\n        </p>\n\n        <div class=\"section-label\">Lighting</div>\n        <div class=\"control-group\">\n          <label>Preset</label>\n          <select id=\"lightPresetSelector\">\n            <option value=\"gallery\">Gallery Soft White</option>\n            <option value=\"sunset\">Sunset Warmth</option>\n            <option value=\"daylight\">Cool Daylight</option>\n            <option value=\"dramatic\">Dramatic Spotlight</option>\n          </select>\n        </div>\n        <div class=\"control-group\">\n          <label>Spotlight Intensity</label>\n          <input type=\"range\" id=\"spotIntensitySlider\" min=\"0\" max=\"4\" step=\"0.1\" value=\"1.2\">\n        </div>\n        <div class=\"control-group\">\n          <label>Spotlight Color</label>\n          <input type=\"color\" id=\"spotColorPicker\" value=\"#fffaed\">\n        </div>\n        <div class=\"control-group\">\n          <label>Cone Angle</label>\n          <input type=\"range\" id=\"spotAngleSlider\" min=\"0.2\" max=\"1.2\" step=\"0.05\" value=\"0.6\">\n        </div>\n        <div class=\"control-group\">\n          <label>Softness (Penumbra)</label>\n          <input type=\"range\" id=\"spotPenumbraSlider\" min=\"0\" max=\"1\" step=\"0.05\" value=\"0.8\">\n        </div>\n        <div class=\"control-group\">\n          <label>Ambient Light</label>\n          <input type=\"range\" id=\"ambientIntensitySlider\" min=\"0\" max=\"1\" step=\"0.05\" value=\"0.2\">\n        </div>\n\n        <div class=\"section-label\">Surfaces</div>\n        <div class=\"control-group\">\n          <label>Wall Color</label>\n          <input type=\"color\" id=\"wallColorPicker\" value=\"#8e8e8e\">\n        </div>\n        <div class=\"control-group\">\n          <label>Floor Gloss / Roughness</label>\n          <input type=\"range\" id=\"floorRoughnessSlider\" min=\"0.02\" max=\"0.8\" step=\"0.02\" value=\"0.12\">\n        </div>\n\n        <div class=\"section-label\">Furniture & Accents</div>\n        <div class=\"checkbox-row\">\n          <label for=\"baseboardToggle\">Baseboard</label>\n          <input type=\"checkbox\" id=\"baseboardToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>Baseboard Style</label>\n          <select id=\"baseboardStyleSelector\">\n            <option value=\"traditional\">Traditional Carved</option>\n            <option value=\"modern\">Modern Minimal</option>\n          </select>\n        </div>\n        <div class=\"control-group\">\n          <label>Baseboard Color</label>\n          <input type=\"color\" id=\"baseboardColorPicker\" value=\"#ffffff\">\n        </div>\n        <div class=\"checkbox-row\">\n          <label for=\"trackToggle\">Track Lighting</label>\n          <input type=\"checkbox\" id=\"trackToggle\" checked>\n        </div>\n        <div class=\"checkbox-row\">\n          <label for=\"benchToggle\">Gallery Bench</label>\n          <input type=\"checkbox\" id=\"benchToggle\" checked>\n        </div>\n        <div class=\"checkbox-row\">\n          <label for=\"stanchionToggle\">Rope Stanchions</label>\n          <input type=\"checkbox\" id=\"stanchionToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>Stanchion Finish</label>\n          <select id=\"metalFinishSelector\">\n            <option value=\"chrome\">Chrome / Silver</option>\n            <option value=\"gold\">Polished Gold</option>\n          </select>\n        </div>\n        <div class=\"control-group\">\n          <label>Rope Color</label>\n          <input type=\"color\" id=\"ropeColorPicker\" value=\"#880000\">\n        </div>\n      </div>\n\n      <!-- FX TAB -->\n      <div class=\"tab-content\" id=\"tab-fx\">\n        <div class=\"section-label\">Seasonal Atmosphere</div>\n        <div class=\"checkbox-row\">\n          <label for=\"fxToggle\">Enable Particle FX</label>\n          <input type=\"checkbox\" id=\"fxToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>FX Mode</label>\n          <select id=\"fxModeSelector\">\n            <option value=\"leaves\">Autumn Falling Leaves</option>\n            <option value=\"snow\">Winter Snowstorm</option>\n          </select>\n        </div>\n        <div class=\"checkbox-row\">\n          <label for=\"windToggle\">Wispy Wind</label>\n          <input type=\"checkbox\" id=\"windToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>Particle Count <span id=\"particleCountVal\" class=\"val-display\">35</span></label>\n          <input type=\"range\" id=\"particleCountSlider\" min=\"10\" max=\"200\" step=\"5\" value=\"35\">\n        </div>\n        <div class=\"control-group\">\n          <label>Particle Size <span id=\"particleSizeVal\" class=\"val-display\">1.0×</span></label>\n          <input type=\"range\" id=\"particleSizeSlider\" min=\"0.3\" max=\"2.0\" step=\"0.1\" value=\"1.0\">\n        </div>\n\n        <div class=\"section-label\">Performance</div>\n        <div class=\"control-group\">\n          <label>Quality Preset</label>\n          <select id=\"qualityPreset\">\n            <option value=\"performance\">Performance (faster)</option>\n            <option value=\"balanced\" selected>Balanced</option>\n            <option value=\"ultra\">Ultra (highest quality)</option>\n          </select>\n        </div>\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin-top:4px;\">\n          Performance mode lowers shadow resolution and particle load for smoother playback on older devices.\n        </p>\n      </div>\n\n      <!-- CAMERA TAB -->\n      <div class=\"tab-content\" id=\"tab-camera\">\n        <div class=\"section-label\">Playback</div>\n        <div id=\"recordTimerPanel\" class=\"record-timer\">REC: 00:00</div>\n        <div class=\"btn-row wrap\">\n          <button class=\"btn\" type=\"button\" onclick=\"playAnim()\">Play</button>\n          <button class=\"btn\" type=\"button\" onclick=\"pauseAnim()\">Pause</button>\n          <button class=\"btn\" type=\"button\" onclick=\"stopAnim()\">Stop</button>\n          <button class=\"btn btn-danger\" type=\"button\" id=\"recordBtn\" onclick=\"toggleRecord()\">Record</button>\n        </div>\n\n        <div class=\"section-label\">Zoom & Position</div>\n        <div class=\"control-group\">\n          <label>Smooth Zoom Target <span id=\"manualZoomVal\" class=\"val-display\">4.50</span></label>\n          <input type=\"range\" id=\"manualZoomSlider\" min=\"1.2\" max=\"7.0\" step=\"0.05\" value=\"4.5\">\n        </div>\n        <div class=\"control-group\">\n          <label>Zoom Speed <span id=\"smoothZoomSpeedVal\" class=\"val-display\">2.5×</span></label>\n          <input type=\"range\" id=\"smoothZoomSpeedSlider\" min=\"0.5\" max=\"8.0\" step=\"0.25\" value=\"2.5\">\n        </div>\n        <div class=\"btn-row\">\n          <button class=\"btn\" type=\"button\" onclick=\"triggerZoomIn()\">Zoom In</button>\n          <button class=\"btn\" type=\"button\" onclick=\"triggerZoomOut()\">Zoom Out</button>\n          <button class=\"btn\" type=\"button\" onclick=\"resetZoom()\">Reset</button>\n        </div>\n        <div class=\"control-group\">\n          <label>Vertical Pan <span id=\"manualPanYVal\" class=\"val-display\">1.80</span></label>\n          <input type=\"range\" id=\"manualPanYSlider\" min=\"0.8\" max=\"3.0\" step=\"0.05\" value=\"1.8\">\n        </div>\n        <div class=\"control-group\">\n          <label>Horizontal Pan <span id=\"manualPanXVal\" class=\"val-display\">0.00</span></label>\n          <input type=\"range\" id=\"manualPanXSlider\" min=\"-2.0\" max=\"2.0\" step=\"0.05\" value=\"0.0\">\n        </div>\n        <div class=\"control-group\">\n          <label>Orbit Pivot</label>\n          <select id=\"pivotSelector\">\n            <option value=\"painting\">Center on Painting</option>\n            <option value=\"viewer\">Center on Bench</option>\n          </select>\n        </div>\n\n        <div class=\"section-label\">Arc Limits & Path</div>\n        <div class=\"checkbox-row\">\n          <label for=\"arcSliderToggle\">Show Bottom Control Bar</label>\n          <input type=\"checkbox\" id=\"arcSliderToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>Min Arc <span id=\"minAngleVal\" class=\"val-display\">30°</span></label>\n          <input type=\"range\" id=\"minAngleSlider\" min=\"0\" max=\"80\" step=\"1\" value=\"30\">\n        </div>\n        <div class=\"control-group\">\n          <label>Max Arc <span id=\"maxAngleVal\" class=\"val-display\">150°</span></label>\n          <input type=\"range\" id=\"maxAngleSlider\" min=\"100\" max=\"180\" step=\"1\" value=\"150\">\n        </div>\n        <div class=\"checkbox-row\">\n          <label for=\"exportAnimToggle\">Auto-Play on Load</label>\n          <input type=\"checkbox\" id=\"exportAnimToggle\" checked>\n        </div>\n        <div class=\"control-group\">\n          <label>Animation Path</label>\n          <select id=\"animPatternSelector\">\n            <option value=\"pendulum\">Smooth Pendulum</option>\n            <option value=\"zoomSweep\">Zoom & Arc Sweep</option>\n            <option value=\"spiral\">Close-up Spiral</option>\n            <option value=\"singleSweep\">Single Slow Pass</option>\n            <option value=\"figure8\">Figure-8 Orbit</option>\n            <option value=\"flyby\">Dramatic Flyby</option>\n            <option value=\"elevator\">Cinematic Elevator</option>\n          </select>\n        </div>\n        <div class=\"control-group\">\n          <label>Cycle Duration <span id=\"animSpeedVal\" class=\"val-display\">12 s</span></label>\n          <input type=\"range\" id=\"animSpeedSlider\" min=\"4\" max=\"30\" step=\"1\" value=\"12\">\n        </div>\n        <div class=\"control-group\">\n          <label>Zoom Depth <span id=\"zoomDepthVal\" class=\"val-display\">2.2</span></label>\n          <input type=\"range\" id=\"zoomDepthSlider\" min=\"1.2\" max=\"4.0\" step=\"0.1\" value=\"2.2\">\n        </div>\n      </div>\n\n      <!-- EXPORT TAB -->\n      <div class=\"tab-content\" id=\"tab-export\">\n        <div class=\"section-label\">Project</div>\n        <div class=\"btn-row\">\n          <button class=\"btn\" type=\"button\" onclick=\"saveProject()\">Save .gallery</button>\n          <button class=\"btn\" type=\"button\" onclick=\"document.getElementById('projectLoader').click()\">Load Project</button>\n          <input type=\"file\" id=\"projectLoader\" accept=\".gallery,.json\" style=\"display:none;\" onchange=\"loadProject(event)\">\n        </div>\n\n        <div class=\"section-label\">Still Capture</div>\n        <div class=\"btn-row wrap\">\n          <button class=\"btn\" type=\"button\" onclick=\"captureScreenshot()\">Full PNG</button>\n          <button class=\"btn\" type=\"button\" onclick=\"exportPreset('instagram')\">Instagram 1:1</button>\n          <button class=\"btn\" type=\"button\" onclick=\"exportPreset('story')\">Story 9:16</button>\n          <button class=\"btn\" type=\"button\" onclick=\"exportPreset('landscape')\">Web 16:9</button>\n        </div>\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin:6px 0 12px;\">\n          Still images at the chosen aspect ratio. Watermark is always applied on free exports.\n        </p>\n\n        <div class=\"section-label\">Video Export</div>\n        <div class=\"control-group\">\n          <label>Aspect Ratio</label>\n          <select id=\"videoAspectSelector\">\n            <option value=\"native\">Native (window)</option>\n            <option value=\"16:9\">Landscape 16:9 (YouTube / Facebook)</option>\n            <option value=\"9:16\">Portrait 9:16 (TikTok / Reels / Stories)</option>\n            <option value=\"1:1\">Square 1:1</option>\n          </select>\n        </div>\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin:-4px 0 10px;\">\n          When not Native, a gold frame shows exactly what will be filmed. Dark areas outside the frame are cropped out.\n        </p>\n        <div class=\"btn-row\">\n          <button class=\"btn btn-danger\" type=\"button\" id=\"recordBtnExport\" onclick=\"toggleRecord()\">Record Video</button>\n        </div>\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin:6px 0 12px;\">\n          Records scene + FX with watermark. Prefers <strong>MP4</strong> (Facebook / Instagram ready) when your browser supports it; otherwise saves WebM. Safari and many Chromium builds output MP4; others may need a free WebM→MP4 convert (HandBrake, CloudConvert, etc.).\n        </p>\n\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin:0 0 12px;padding:8px 10px;background:var(--accent-dim);border-radius:6px;border:1px solid rgba(201,168,108,0.25);\">\n          <strong style=\"color:var(--accent);\">Demo watermark always on.</strong> Preview mark is visible on screen and is burned into every video, still, and HTML export.\n        </p>\n\n        <div class=\"section-label\">Finished Viewer</div>\n        <button class=\"btn btn-primary btn-full\" type=\"button\" onclick=\"exportHTML()\">Export Standalone Viewer HTML</button>\n        <p style=\"font-size:11px;color:var(--text-dim);line-height:1.45;margin-top:10px;\">\n          Creates a self-contained viewer of the finished gallery (no editor controls). Safe to host or share with collectors. Includes watermark.\n        </p>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"arc-control-bar\" id=\"arcControlBar\">\n    <span class=\"bar-label\">Camera Pan & Motion</span>\n    <div id=\"recordTimerArc\" class=\"record-timer\">REC: 00:00</div>\n    <input type=\"range\" id=\"cameraArcSlider\" min=\"30\" max=\"150\" value=\"90\" step=\"1\">\n    <div class=\"player-controls\">\n      <button class=\"btn\" type=\"button\" onclick=\"playAnim()\">Play</button>\n      <button class=\"btn\" type=\"button\" onclick=\"pauseAnim()\">Pause</button>\n      <button class=\"btn\" type=\"button\" onclick=\"stopAnim()\">Stop</button>\n      <button class=\"btn\" type=\"button\" onclick=\"triggerZoomIn()\">Zoom In</button>\n      <button class=\"btn\" type=\"button\" onclick=\"resetZoom()\">Reset</button>\n      <button class=\"btn btn-danger\" type=\"button\" id=\"recordBtnArc\" onclick=\"toggleRecord()\">Record</button>\n      <a href=\"#\" target=\"_blank\" class=\"btn btn-primary\" id=\"barBuyBtn\">Buy Original</a>\n    </div>\n  </div>\n\n\n  <!-- Empty state -->\n  <div id=\"emptyState\">\n    <div class=\"empty-card\">\n      <h2>Drop your artwork here</h2>\n      <p>Upload an image or video to place it in a professional viewing room. Drag &amp; drop anywhere, or use the Media tab.</p>\n      <button class=\"btn btn-primary\" type=\"button\" onclick=\"document.getElementById('artUploader').click()\">Choose File</button>\n    </div>\n  </div>\n\n  <!-- Watermark (always on for demo) -->\n  <div id=\"watermark\">Preview · Imagine Art Gallery</div>\n\n  <!-- Persistent tips (shown after welcome is dismissed) -->\n  <div id=\"tipsBar\" class=\"hidden\">\n    <strong>Quick path:</strong>\n    Media → upload image ·\n    Room → colors &amp; lighting ·\n    FX → Leaves or Snow ·\n    Export → aspect ratio → Record for Facebook / YouTube / Instagram\n  </div>\n\n  <!-- Welcome / start here modal -->\n  <div class=\"modal-overlay\" id=\"welcomeModal\">\n    <div class=\"welcome-card\">\n      <div class=\"welcome-mark\">I</div>\n      <h2>Start here</h2>\n      <p class=\"welcome-lead\">Create a professional gallery room for your artwork in a few steps. This is the free demo — a watermark stays on all previews and exports.</p>\n      <ul class=\"welcome-steps\">\n        <li><span class=\"step-num\">1</span><span><strong>Upload</strong> your image (or video) — drag &amp; drop anywhere, or use the Media tab.</span></li>\n        <li><span class=\"step-num\">2</span><span><strong>Frame &amp; room</strong> — pick frame style, wall color, lighting, and furniture under Media and Room.</span></li>\n        <li><span class=\"step-num\">3</span><span><strong>Effects</strong> — add falling Leaves or Snow in the FX tab.</span></li>\n        <li><span class=\"step-num\">4</span><span><strong>Export</strong> — choose 16:9 (Facebook / YouTube) or 9:16 (Reels / Stories), then Record or save a still.</span></li>\n      </ul>\n      <button type=\"button\" class=\"btn-start\" id=\"welcomeStartBtn\">Click here to start</button>\n      <p class=\"welcome-note\">Tips stay visible at the top after you begin. Open Studio Controls anytime to fine-tune.</p>\n    </div>\n  </div>\n\n  <div class=\"modal-overlay\" id=\"purchaseModal\">\n    <div class=\"modal-card\">\n      <span class=\"modal-close\" onclick=\"closeModal()\">&times;</span>\n      <div class=\"modal-title\" id=\"mTitle\">Untitled</div>\n      <div class=\"modal-details\" id=\"mDetails\"></div>\n      <div class=\"modal-price\" id=\"mPrice\"></div>\n      <a href=\"#\" target=\"_blank\" class=\"btn-modal-buy\" id=\"mBuyBtn\">Buy Original Painting</a>\n    </div>\n  </div>\n\n  <script>\n    // ─── UI: Tabs & Menu ───────────────────────────────────────────────\n    const menuToggleBtn = document.getElementById('menuToggleBtn');\n    const editorPanel = document.getElementById('editorPanel');\n\n    menuToggleBtn.addEventListener('click', () => {\n      editorPanel.classList.toggle('collapsed');\n      menuToggleBtn.textContent = editorPanel.classList.contains('collapsed') ? 'Show Panel' : 'Hide Panel';\n    });\n\n    document.querySelectorAll('.tab').forEach(tab => {\n      tab.addEventListener('click', () => {\n        document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n        document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));\n        tab.classList.add('active');\n        document.getElementById('tab-' + tab.dataset.tab).classList.add('active');\n      });\n    });\n\n    // ─── Seasonal FX Engine ────────────────────────────────────────────\n    class SeasonalFXOverlay {\n      constructor(canvasId, count = 35) {\n        this.canvas = document.getElementById(canvasId);\n        this.ctx = this.canvas.getContext('2d');\n        this.targetCount = count;\n        this.particles = [];\n        this.active = true;\n        this.mode = 'leaves';\n        this.globalScaleMultiplier = 1.0;\n        this.windEnabled = true;\n        this.windForceX = 0;\n        this.windForceY = 0;\n        this.gustTimer = 0;\n        this.resize();\n        window.addEventListener('resize', () => this.resize());\n        this.initParticles();\n      }\n\n      resize() {\n        this.canvas.width = window.innerWidth;\n        this.canvas.height = window.innerHeight;\n      }\n\n      setMode(mode) {\n        this.mode = mode;\n        this.initParticles();\n      }\n\n      createParticleObject() {\n        if (this.mode === 'snow') {\n          return {\n            x: Math.random() * (this.canvas.width + 200) - 100,\n            y: Math.random() * this.canvas.height - this.canvas.height,\n            baseScale: Math.random() * 0.6 + 0.3,\n            speedY: Math.random() * 0.8 + 0.4,\n            speedX: Math.random() * 0.3 - 0.15,\n            oscillation: Math.random() * Math.PI * 2,\n            oscillationSpeed: Math.random() * 0.02 + 0.008,\n            flipAngle: Math.random() * Math.PI * 2,\n            flipSpeed: Math.random() * 0.02 + 0.005,\n            angle: Math.random() * Math.PI * 2,\n            spin: (Math.random() - 0.5) * 0.015,\n            depth: Math.random() * 0.7 + 0.3,\n            type: Math.random() > 0.4 ? 'flake' : 'soft',\n            opacity: Math.random() * 0.4 + 0.5\n          };\n        }\n        const leafPalettes = [\n          { base: '#d9531e', dark: '#a3330e', vein: '#fca34d' },\n          { base: '#e67319', dark: '#ad4908', vein: '#ffd166' },\n          { base: '#f4a213', dark: '#c46d00', vein: '#fff1ad' },\n          { base: '#c13915', dark: '#7e1d04', vein: '#f28e6b' }\n        ];\n        return {\n          x: Math.random() * (this.canvas.width + 200) - 100,\n          y: Math.random() * this.canvas.height - this.canvas.height,\n          baseScale: Math.random() * 0.7 + 0.4,\n          speedY: Math.random() * 1.1 + 0.7,\n          speedX: Math.random() * 0.4 - 0.2,\n          oscillation: Math.random() * Math.PI * 2,\n          oscillationSpeed: Math.random() * 0.035 + 0.015,\n          flipAngle: Math.random() * Math.PI * 2,\n          flipSpeed: Math.random() * 0.04 + 0.01,\n          angle: Math.random() * Math.PI * 2,\n          spin: (Math.random() - 0.5) * 0.025,\n          depth: Math.random() * 0.6 + 0.4,\n          palette: leafPalettes[Math.floor(Math.random() * leafPalettes.length)]\n        };\n      }\n\n      initParticles() {\n        this.particles = [];\n        for (let i = 0; i < this.targetCount; i++) {\n          const p = this.createParticleObject();\n          p.y = Math.random() * this.canvas.height;\n          this.particles.push(p);\n        }\n      }\n\n      setCount(newCount) {\n        this.targetCount = newCount;\n        while (this.particles.length < this.targetCount) this.particles.push(this.createParticleObject());\n        if (this.particles.length > this.targetCount) this.particles.length = this.targetCount;\n      }\n\n      updateWind() {\n        if (!this.windEnabled) {\n          this.windForceX = 0;\n          this.windForceY = 0;\n          return;\n        }\n        this.gustTimer += 0.02;\n        const breeze = Math.sin(this.gustTimer * 0.8) * 0.6 + Math.cos(this.gustTimer * 0.3) * 0.4;\n        const gust = Math.pow(Math.max(0, Math.sin(this.gustTimer * 0.4)), 3) * 1.8;\n        this.windForceX = (breeze + gust) * 0.8;\n        this.windForceY = Math.sin(this.gustTimer * 1.2) * 0.2;\n      }\n\n      drawSnowflake(ctx, p) {\n        ctx.save();\n        ctx.translate(p.x, p.y);\n        ctx.rotate(p.angle);\n        const scaleX = p.baseScale * p.depth * this.globalScaleMultiplier;\n        const scaleY = scaleX * (0.8 + 0.2 * Math.cos(p.flipAngle));\n        ctx.scale(scaleX, scaleY);\n        if (p.depth < 0.5) ctx.filter = 'blur(1px)';\n        ctx.globalAlpha = p.opacity;\n        if (p.type === 'soft') {\n          const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, 10);\n          grad.addColorStop(0, 'rgba(255,255,255,0.95)');\n          grad.addColorStop(0.5, 'rgba(230,245,255,0.6)');\n          grad.addColorStop(1, 'rgba(255,255,255,0)');\n          ctx.fillStyle = grad;\n          ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.fill();\n        } else {\n          ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1.8; ctx.lineCap = 'round';\n          ctx.beginPath();\n          for (let i = 0; i < 6; i++) {\n            ctx.rotate(Math.PI / 3);\n            ctx.moveTo(0, 0); ctx.lineTo(0, 16);\n            ctx.moveTo(0, 8); ctx.lineTo(5, 12);\n            ctx.moveTo(0, 8); ctx.lineTo(-5, 12);\n          }\n          ctx.stroke();\n          ctx.fillStyle = '#ffffff';\n          ctx.beginPath(); ctx.arc(0, 0, 2.5, 0, Math.PI * 2); ctx.fill();\n        }\n        ctx.restore();\n      }\n\n      drawMapleLeaf(ctx, leaf) {\n        ctx.save();\n        ctx.translate(leaf.x, leaf.y);\n        ctx.rotate(leaf.angle);\n        const scaleX = leaf.baseScale * leaf.depth * this.globalScaleMultiplier;\n        const scaleY = scaleX * Math.cos(leaf.flipAngle);\n        ctx.scale(scaleX, scaleY);\n        if (leaf.depth < 0.65) ctx.filter = 'blur(1px)';\n        const grad = ctx.createRadialGradient(0, 0, 4, 0, 0, 30);\n        grad.addColorStop(0, leaf.palette.base);\n        grad.addColorStop(1, leaf.palette.dark);\n        ctx.fillStyle = grad;\n        ctx.beginPath(); ctx.moveTo(0, 10); ctx.lineTo(0, 30);\n        ctx.strokeStyle = '#6e2a0c'; ctx.lineWidth = 2; ctx.stroke();\n        ctx.beginPath();\n        ctx.moveTo(0, -32);\n        ctx.lineTo(6, -20); ctx.lineTo(12, -22); ctx.lineTo(22, -18); ctx.lineTo(16, -10); ctx.lineTo(30, -4); ctx.lineTo(20, 2);\n        ctx.lineTo(26, 12); ctx.lineTo(16, 14); ctx.lineTo(12, 22); ctx.lineTo(5, 12);\n        ctx.lineTo(0, 10);\n        ctx.lineTo(-5, 12); ctx.lineTo(-12, 22); ctx.lineTo(-16, 14); ctx.lineTo(-26, 12); ctx.lineTo(-20, 2);\n        ctx.lineTo(-30, -4); ctx.lineTo(-16, -10); ctx.lineTo(-22, -18); ctx.lineTo(-12, -22); ctx.lineTo(-6, -20);\n        ctx.closePath(); ctx.fill();\n        ctx.strokeStyle = leaf.palette.vein; ctx.lineWidth = 1; ctx.globalAlpha = 0.55;\n        ctx.beginPath();\n        ctx.moveTo(0, 10); ctx.lineTo(0, -22);\n        ctx.moveTo(0, 5); ctx.lineTo(20, -10);\n        ctx.moveTo(0, 5); ctx.lineTo(-20, -10);\n        ctx.moveTo(0, 8); ctx.lineTo(16, 8);\n        ctx.moveTo(0, 8); ctx.lineTo(-16, 8);\n        ctx.stroke();\n        ctx.restore();\n      }\n\n      render() {\n        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n        if (!this.active) return;\n        this.updateWind();\n        for (let p of this.particles) {\n          p.oscillation += p.oscillationSpeed;\n          p.flipAngle += p.flipSpeed;\n          p.angle += p.spin;\n          const sway = Math.sin(p.oscillation) * (this.mode === 'snow' ? 0.8 : 1.2);\n          p.x += p.speedX + sway + (this.windForceX * p.depth);\n          p.y += (p.speedY + (this.windForceY * p.depth)) * p.depth;\n          if (p.y > this.canvas.height + 50) {\n            p.y = -40;\n            p.x = Math.random() * (this.canvas.width + 200) - 100;\n          }\n          if (p.x > this.canvas.width + 100) p.x = -50;\n          if (p.x < -100) p.x = this.canvas.width + 50;\n          if (this.mode === 'snow') this.drawSnowflake(this.ctx, p);\n          else this.drawMapleLeaf(this.ctx, p);\n        }\n      }\n    }\n\n    const weatherFX = new SeasonalFXOverlay('fx-canvas', 35);\n\n    // ─── Empty state, watermark (always on), welcome ───────────────────\n    let hasArtwork = false;\n    let watermarkEnabled = true; // Demo build: always on — cannot be disabled\n\n    function showEmptyState(show) {\n      const el = document.getElementById('emptyState');\n      if (el) el.classList.toggle('hidden', !show);\n      hasArtwork = !show;\n    }\n\n    function setWatermark(_on) {\n      // Demo model: watermark is locked on for screen + all exports\n      watermarkEnabled = true;\n      const el = document.getElementById('watermark');\n      if (el) el.classList.remove('hidden');\n    }\n    setWatermark(true);\n\n    // Welcome modal + tips bar\n    (function initWelcome() {\n      const modal = document.getElementById('welcomeModal');\n      const startBtn = document.getElementById('welcomeStartBtn');\n      const tips = document.getElementById('tipsBar');\n      function dismissWelcome() {\n        if (modal) modal.classList.add('hidden');\n        if (tips) tips.classList.remove('hidden');\n        try { sessionStorage.setItem('iag_welcome_seen', '1'); } catch (_) {}\n      }\n      if (startBtn) startBtn.addEventListener('click', dismissWelcome);\n      // Optional: click backdrop to start\n      if (modal) {\n        modal.addEventListener('click', (e) => {\n          if (e.target === modal) dismissWelcome();\n        });\n      }\n      // Show every fresh page load for website demo visitors\n      if (modal) modal.classList.remove('hidden');\n    })();\n\n\n\n\n\n    // ─── Procedural textures ───────────────────────────────────────────\n    function createCanvasGrain() {\n      const canvas = document.createElement('canvas');\n      canvas.width = 256; canvas.height = 256;\n      const ctx = canvas.getContext('2d');\n      ctx.fillStyle = '#808080'; ctx.fillRect(0, 0, 256, 256);\n      ctx.fillStyle = '#a0a0a0';\n      for (let x = 0; x < 256; x += 2) {\n        for (let y = 0; y < 256; y += 2) {\n          if ((x + y) % 4 === 0) ctx.fillRect(x, y, 1, 1);\n        }\n      }\n      const texture = new THREE.CanvasTexture(canvas);\n      texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping;\n      texture.repeat.set(4, 4);\n      return texture;\n    }\n\n    function createMarbleTexture() {\n      const canvas = document.createElement('canvas');\n      canvas.width = 512; canvas.height = 512;\n      const ctx = canvas.getContext('2d');\n      ctx.fillStyle = '#f5f5f7'; ctx.fillRect(0, 0, 512, 512);\n      ctx.strokeStyle = 'rgba(120,120,120,0.25)'; ctx.lineWidth = 3;\n      for (let i = 0; i < 8; i++) {\n        ctx.beginPath();\n        let x = Math.random() * 512, y = 0;\n        ctx.moveTo(x, y);\n        while (y < 512) {\n          x += (Math.random() - 0.5) * 50;\n          y += Math.random() * 40;\n          ctx.lineTo(x, y);\n        }\n        ctx.stroke();\n      }\n      const texture = new THREE.CanvasTexture(canvas);\n      texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping;\n      return texture;\n    }\n\n    function createPlacardTexture(title, medium, dim) {\n      const canvas = document.createElement('canvas');\n      canvas.width = 1024; canvas.height = 512;\n      const ctx = canvas.getContext('2d');\n      ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 1024, 512);\n      ctx.fillStyle = '#111111'; ctx.font = 'bold 56px sans-serif'; ctx.fillText(title, 60, 140);\n      ctx.font = '40px sans-serif'; ctx.fillStyle = '#555555';\n      ctx.fillText(medium, 60, 240); ctx.fillText(dim, 60, 320);\n      const texture = new THREE.CanvasTexture(canvas);\n      texture.minFilter = THREE.LinearMipmapLinearFilter;\n      texture.generateMipmaps = true;\n      return texture;\n    }\n\n    // ─── Three.js scene ────────────────────────────────────────────────\n    const scene = new THREE.Scene();\n    scene.background = new THREE.Color(0x0f0f10);\n    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);\n\n    const DEFAULT_ZOOM = 4.5;\n    let targetRadius = DEFAULT_ZOOM;\n    let currentRadius = DEFAULT_ZOOM;\n    let smoothZoomSpeed = 2.5;\n    let manualPanY = 1.8;\n    let manualPanX = 0.0;\n    camera.position.set(manualPanX, manualPanY, currentRadius);\n\n    // Quality settings\n    let quality = 'balanced';\n    let shadowMapSize = 2048;\n    let maxPixelRatio = 2;\n\n    function applyQuality(q) {\n      quality = q;\n      const badge = document.getElementById('qualityBadge');\n      if (q === 'performance') {\n        shadowMapSize = 1024;\n        maxPixelRatio = 1.25;\n        badge.textContent = 'Performance';\n        if (weatherFX.targetCount > 40) {\n          weatherFX.setCount(25);\n          document.getElementById('particleCountSlider').value = 25;\n          document.getElementById('particleCountVal').textContent = '25';\n        }\n      } else if (q === 'ultra') {\n        shadowMapSize = 4096;\n        maxPixelRatio = Math.min(window.devicePixelRatio, 2.5);\n        badge.textContent = 'Ultra';\n      } else {\n        shadowMapSize = 2048;\n        maxPixelRatio = Math.min(window.devicePixelRatio, 2);\n        badge.textContent = 'Balanced';\n      }\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio, maxPixelRatio));\n      if (spotlight) {\n        spotlight.shadow.mapSize.set(shadowMapSize, shadowMapSize);\n        spotlight.shadow.map?.dispose();\n        spotlight.shadow.map = null;\n        spotlight.shadow.needsUpdate = true;\n      }\n    }\n\n    const renderer = new THREE.WebGLRenderer({\n      antialias: true,\n      powerPreference: 'high-performance',\n      preserveDrawingBuffer: true\n    });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, maxPixelRatio));\n    renderer.setSize(window.innerWidth, window.innerHeight);\n    renderer.shadowMap.enabled = true;\n    renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n    document.body.appendChild(renderer.domElement);\n    renderer.domElement.style.position = 'absolute';\n    renderer.domElement.style.inset = '0';\n    renderer.domElement.style.width = '100%';\n    renderer.domElement.style.height = '100%';\n    renderer.domElement.style.zIndex = '1';\n\n    const controls = new THREE.OrbitControls(camera, renderer.domElement);\n    controls.target.set(manualPanX, manualPanY, 0);\n    controls.enableDamping = true;\n    controls.dampingFactor = 0.05;\n\n    let minDeg = 30, maxDeg = 150;\n    let isAnimating = true;\n    let animTime = 0;\n    let animCycleDuration = 12;\n    let zoomTargetDepth = 2.2;\n    let animPattern = 'pendulum';\n\n    function updateBuyButtonLink() {\n      const linkVal = document.getElementById('linkInput').value || '#';\n      document.getElementById('barBuyBtn').href = linkVal;\n      document.getElementById('mBuyBtn').href = linkVal;\n    }\n\n    function setZoomTarget(dist) {\n      targetRadius = Math.max(1.2, Math.min(7.0, dist));\n      document.getElementById('manualZoomSlider').value = targetRadius;\n      document.getElementById('manualZoomVal').textContent = targetRadius.toFixed(2);\n    }\n    function triggerZoomIn() { setZoomTarget(1.8); }\n    function triggerZoomOut() { setZoomTarget(5.5); }\n    function resetZoom() { setZoomTarget(DEFAULT_ZOOM); }\n\n    // ─── Recording ─────────────────────────────────────────────────────\n    let mediaRecorder = null;\n    let recordedChunks = [];\n    let isRecording = false;\n    let recordStartTime = 0;\n    let recordTimerInterval = null;\n    let recordAspectMode = 'native'; // native | 16:9 | 9:16 | 1:1\n\n    // ── Live recording frame (crop guide) ───────────────────────────────\n    function getAspectNumber(mode) {\n      if (mode === '16:9') return 16 / 9;\n      if (mode === '9:16') return 9 / 16;\n      if (mode === '1:1') return 1;\n      return null;\n    }\n\n    function updateRecordFrameOverlay() {\n      const overlay = document.getElementById('recordFrameOverlay');\n      const modeEl = document.getElementById('videoAspectSelector');\n      if (!overlay || !modeEl) return;\n      const mode = modeEl.value || 'native';\n      const aspect = getAspectNumber(mode);\n      if (!aspect) {\n        overlay.classList.remove('active');\n        overlay.setAttribute('aria-hidden', 'true');\n        return;\n      }\n      const vw = window.innerWidth;\n      const vh = window.innerHeight;\n      let frameW, frameH, left, top;\n      if (vw / vh > aspect) {\n        // pillarbox — tall frame centered horizontally\n        frameH = vh;\n        frameW = Math.round(vh * aspect);\n        left = Math.round((vw - frameW) / 2);\n        top = 0;\n      } else {\n        // letterbox — wide frame centered vertically\n        frameW = vw;\n        frameH = Math.round(vw / aspect);\n        left = 0;\n        top = Math.round((vh - frameH) / 2);\n      }\n      const topBar = document.getElementById('rfTop');\n      const botBar = document.getElementById('rfBottom');\n      const leftBar = document.getElementById('rfLeft');\n      const rightBar = document.getElementById('rfRight');\n      const guide = document.getElementById('rfGuide');\n      const label = document.getElementById('rfLabel');\n      if (topBar) {\n        topBar.style.left = '0'; topBar.style.top = '0';\n        topBar.style.width = '100%'; topBar.style.height = top + 'px';\n      }\n      if (botBar) {\n        botBar.style.left = '0'; botBar.style.top = (top + frameH) + 'px';\n        botBar.style.width = '100%'; botBar.style.height = Math.max(0, vh - top - frameH) + 'px';\n      }\n      if (leftBar) {\n        leftBar.style.left = '0'; leftBar.style.top = top + 'px';\n        leftBar.style.width = left + 'px'; leftBar.style.height = frameH + 'px';\n      }\n      if (rightBar) {\n        rightBar.style.left = (left + frameW) + 'px'; rightBar.style.top = top + 'px';\n        rightBar.style.width = Math.max(0, vw - left - frameW) + 'px'; rightBar.style.height = frameH + 'px';\n      }\n      if (guide) {\n        guide.style.left = left + 'px';\n        guide.style.top = top + 'px';\n        guide.style.width = frameW + 'px';\n        guide.style.height = frameH + 'px';\n      }\n      if (label) {\n        const labels = {\n          '9:16': '9×16 portrait · TikTok / Reels / Stories',\n          '16:9': '16×9 landscape · YouTube / Facebook',\n          '1:1': '1×1 square'\n        };\n        label.textContent = labels[mode] || 'Recording frame';\n        // Keep label inside the guide area near the top\n        label.style.top = Math.max(8, top + 10) + 'px';\n      }\n      overlay.classList.add('active');\n      overlay.setAttribute('aria-hidden', 'false');\n    }\n\n    document.getElementById('videoAspectSelector').addEventListener('change', updateRecordFrameOverlay);\n    window.addEventListener('resize', updateRecordFrameOverlay);\n    // initial\n    setTimeout(updateRecordFrameOverlay, 0);\n\n\n    function formatTime(seconds) {\n      const mins = Math.floor(seconds / 60);\n      const secs = seconds % 60;\n      return `REC: ${mins < 10 ? '0' : ''}${mins}:${secs < 10 ? '0' : ''}${secs}`;\n    }\n\n    function updateRecordTimerDisplay() {\n      if (!isRecording) return;\n      const elapsed = Math.floor((Date.now() - recordStartTime) / 1000);\n      const timeStr = formatTime(elapsed);\n      ['recordTimerPanel', 'recordTimerArc'].forEach(id => {\n        const el = document.getElementById(id);\n        if (el) el.textContent = timeStr;\n      });\n    }\n\n    function getRecordDimensions() {\n      const srcW = renderer.domElement.width;\n      const srcH = renderer.domElement.height;\n      const mode = (document.getElementById('videoAspectSelector') || {}).value || 'native';\n      recordAspectMode = mode;\n      if (mode === 'native') return { outW: srcW, outH: srcH, sx: 0, sy: 0, sw: srcW, sh: srcH };\n      let targetAspect = 16 / 9;\n      if (mode === '9:16') targetAspect = 9 / 16;\n      else if (mode === '1:1') targetAspect = 1;\n      let outW, outH, sx, sy, sw, sh;\n      if (srcW / srcH > targetAspect) {\n        outH = srcH;\n        outW = Math.round(srcH * targetAspect);\n        sx = Math.max(0, Math.round((srcW - outW) / 2));\n        sy = 0;\n        sw = outW;\n        sh = srcH;\n      } else {\n        outW = srcW;\n        outH = Math.round(srcW / targetAspect);\n        sx = 0;\n        sy = Math.max(0, Math.round((srcH - outH) / 2));\n        sw = srcW;\n        sh = outH;\n      }\n      const maxDim = 1920;\n      if (outW > maxDim || outH > maxDim) {\n        const scale = maxDim / Math.max(outW, outH);\n        outW = Math.round(outW * scale);\n        outH = Math.round(outH * scale);\n      }\n      return { outW, outH, sx, sy, sw, sh };\n    }\n\n    function drawWatermarkOnCtx(ctx, w, h) {\n      // Scale mark for resolution + aspect (vertical 9:16 needs a larger, clearer brand)\n      const shortSide = Math.min(w, h);\n      const isPortrait = h > w * 1.05;\n      const scale = Math.max(0.85, Math.min(2.4, shortSide / 720));\n      const pad = Math.round(14 * scale);\n      const markSize = Math.round((isPortrait ? 28 : 22) * scale);\n      const fontPx = Math.round((isPortrait ? 15 : 13) * scale);\n      const gap = Math.round(8 * scale);\n      const label = 'PREVIEW · IMAGINE ART GALLERY';\n\n      ctx.save();\n      ctx.textAlign = 'left';\n      ctx.textBaseline = 'middle';\n      ctx.font = '600 ' + fontPx + 'px system-ui, -apple-system, BlinkMacSystemFont, sans-serif';\n      const textW = ctx.measureText(label).width;\n      const blockW = markSize + gap + textW;\n      const blockH = Math.max(markSize, fontPx + 4);\n      const x = w - pad - blockW;\n      const y = h - pad - blockH;\n\n      // Soft plate behind logo + text so it reads on light floors / snow\n      const platePadX = Math.round(8 * scale);\n      const platePadY = Math.round(6 * scale);\n      ctx.fillStyle = 'rgba(0,0,0,0.38)';\n      // rounded rect\n      const rx = x - platePadX, ry = y - platePadY;\n      const rw = blockW + platePadX * 2, rh = blockH + platePadY * 2;\n      const r = Math.round(6 * scale);\n      ctx.beginPath();\n      ctx.moveTo(rx + r, ry);\n      ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, r);\n      ctx.arcTo(rx + rw, ry + rh, rx, ry + rh, r);\n      ctx.arcTo(rx, ry + rh, rx, ry, r);\n      ctx.arcTo(rx, ry, rx + rw, ry, r);\n      ctx.closePath();\n      ctx.fill();\n\n      // Gold \"I\" logo mark (matches on-screen brand)\n      const grad = ctx.createLinearGradient(x, y, x + markSize, y + markSize);\n      grad.addColorStop(0, '#c9a86c');\n      grad.addColorStop(1, '#a88b4a');\n      const mr = Math.round(6 * scale);\n      ctx.fillStyle = grad;\n      ctx.beginPath();\n      ctx.moveTo(x + mr, y);\n      ctx.arcTo(x + markSize, y, x + markSize, y + markSize, mr);\n      ctx.arcTo(x + markSize, y + markSize, x, y + markSize, mr);\n      ctx.arcTo(x, y + markSize, x, y, mr);\n      ctx.arcTo(x, y, x + markSize, y, mr);\n      ctx.closePath();\n      ctx.fill();\n      ctx.fillStyle = '#1a1612';\n      ctx.font = '700 ' + Math.round(markSize * 0.55) + 'px system-ui, -apple-system, sans-serif';\n      ctx.textAlign = 'center';\n      ctx.textBaseline = 'middle';\n      ctx.fillText('I', x + markSize / 2, y + markSize / 2 + 0.5);\n\n      // Wordmark\n      ctx.font = '600 ' + fontPx + 'px system-ui, -apple-system, BlinkMacSystemFont, sans-serif';\n      ctx.fillStyle = 'rgba(255,255,255,0.88)';\n      ctx.textAlign = 'left';\n      ctx.textBaseline = 'middle';\n      ctx.fillText(label, x + markSize + gap, y + blockH / 2);\n      ctx.restore();\n    }\n\n    function toggleRecord() {\n      isRecording ? stopRecording() : startRecording();\n    }\n\n    function pickRecorderMime() {\n      // Prefer MP4/H.264 for Facebook, Instagram, TikTok. Fall back to WebM.\n      const candidates = [\n        'video/mp4;codecs=avc1.42E01E,mp4a.40.2',\n        'video/mp4;codecs=avc1.4D401E,mp4a.40.2',\n        'video/mp4;codecs=avc1',\n        'video/mp4',\n        'video/webm;codecs=vp9,opus',\n        'video/webm;codecs=vp8,opus',\n        'video/webm;codecs=vp9',\n        'video/webm'\n      ];\n      for (const m of candidates) {\n        try {\n          if (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(m)) {\n            return m;\n          }\n        } catch (_) {}\n      }\n      return '';\n    }\n\n    function extensionForMime(mime) {\n      if (!mime) return 'webm';\n      if (mime.indexOf('mp4') !== -1) return 'mp4';\n      return 'webm';\n    }\n\n    function startRecording() {\n      if (!hasArtwork) {\n        alert('Please add an artwork first.');\n        return;\n      }\n      if (typeof MediaRecorder === 'undefined') {\n        alert('Video recording is not supported in this browser. Try Chrome, Edge, or Safari.');\n        return;\n      }\n      const dims = getRecordDimensions();\n      const recordCanvas = document.createElement('canvas');\n      recordCanvas.width = dims.outW;\n      recordCanvas.height = dims.outH;\n      const recordCtx = recordCanvas.getContext('2d');\n      const stream = recordCanvas.captureStream(30);\n      recordedChunks = [];\n      const mime = pickRecorderMime();\n      try {\n        mediaRecorder = mime\n          ? new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 8_000_000 })\n          : new MediaRecorder(stream, { videoBitsPerSecond: 8_000_000 });\n      } catch (e) {\n        try {\n          mediaRecorder = new MediaRecorder(stream);\n        } catch (err) {\n          alert('Video recording is not supported in this browser. Try Chrome, Edge, or Safari.');\n          return;\n        }\n      }\n      const usedMime = mediaRecorder.mimeType || mime || 'video/webm';\n      const fileExt = extensionForMime(usedMime);\n      mediaRecorder.ondataavailable = (e) => {\n        if (e.data && e.data.size > 0) recordedChunks.push(e.data);\n      };\n      mediaRecorder.onstop = () => {\n        const blob = new Blob(recordedChunks, { type: usedMime.split(';')[0] || 'video/webm' });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.style.display = 'none';\n        a.href = url;\n        const titleVal = document.getElementById('titleInput').value || 'gallery_preview';\n        const aspectTag = recordAspectMode === 'native' ? '' : ('_' + recordAspectMode.replace(':', 'x'));\n        a.download = titleVal.replace(/[^a-z0-9]/gi, '_').toLowerCase() + aspectTag + '_preview.' + fileExt;\n        document.body.appendChild(a);\n        a.click();\n        setTimeout(() => {\n          document.body.removeChild(a);\n          URL.revokeObjectURL(url);\n        }, 100);\n      };\n      mediaRecorder.start(1000);\n      isRecording = true;\n      recordStartTime = Date.now();\n      ['recordTimerPanel', 'recordTimerArc'].forEach(id => {\n        const el = document.getElementById(id);\n        if (el) el.classList.add('active');\n      });\n      updateRecordTimerDisplay();\n      recordTimerInterval = setInterval(updateRecordTimerDisplay, 1000);\n      ['recordBtn', 'recordBtnArc', 'recordBtnExport'].forEach(id => {\n        const el = document.getElementById(id);\n        if (el) { el.textContent = 'Stop Rec'; el.classList.add('recording'); }\n      });\n      window._recordCanvas = recordCanvas;\n      window._recordCtx = recordCtx;\n      window._recordDims = dims;\n    }\n\n    function stopRecording() {\n      if (!isRecording || !mediaRecorder) return;\n      mediaRecorder.stop();\n      isRecording = false;\n      if (recordTimerInterval) {\n        clearInterval(recordTimerInterval);\n        recordTimerInterval = null;\n      }\n      ['recordTimerPanel', 'recordTimerArc'].forEach(id => {\n        const el = document.getElementById(id);\n        if (el) el.classList.remove('active');\n      });\n      ['recordBtn', 'recordBtnArc', 'recordBtnExport'].forEach(id => {\n        const el = document.getElementById(id);\n        if (el) { el.textContent = 'Record'; el.classList.remove('recording'); }\n      });\n      delete window._recordCanvas;\n      delete window._recordCtx;\n      delete window._recordDims;\n    }\n\n    function captureScreenshot(opts = {}) {\n      if (!hasArtwork && !opts.force) {\n        alert('Please add an artwork first.');\n        return;\n      }\n      const srcW = renderer.domElement.width;\n      const srcH = renderer.domElement.height;\n      let outW = srcW, outH = srcH;\n      const aspect = opts.aspect;\n      if (aspect) {\n        if (srcW / srcH > aspect) {\n          outH = srcH;\n          outW = Math.round(srcH * aspect);\n        } else {\n          outW = srcW;\n          outH = Math.round(srcW / aspect);\n        }\n      }\n      const off = document.createElement('canvas');\n      off.width = outW; off.height = outH;\n      const ctx = off.getContext('2d');\n      const sx = Math.max(0, (srcW - outW) / 2);\n      const sy = Math.max(0, (srcH - outH) / 2);\n      ctx.drawImage(renderer.domElement, sx, sy, outW, outH, 0, 0, outW, outH);\n      if (weatherFX.active) {\n        ctx.drawImage(weatherFX.canvas, sx, sy, outW, outH, 0, 0, outW, outH);\n      }\n      // Free version always burns watermark into still exports\n      drawWatermarkOnCtx(ctx, outW, outH);\n      const suffix = opts.suffix || 'still';\n      off.toBlob((blob) => {\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.href = url;\n        const titleVal = document.getElementById('titleInput').value || 'gallery';\n        a.download = titleVal.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '_' + suffix + '.png';\n        a.click();\n        URL.revokeObjectURL(url);\n      }, 'image/png');\n    }\n\n    function exportPreset(kind) {\n      if (!hasArtwork) {\n        alert('Please add an artwork first.');\n        return;\n      }\n      if (kind === 'instagram') captureScreenshot({ aspect: 1, suffix: 'instagram' });\n      else if (kind === 'story') captureScreenshot({ aspect: 9/16, suffix: 'story' });\n      else if (kind === 'landscape') captureScreenshot({ aspect: 16/9, suffix: 'web' });\n      else captureScreenshot();\n    }\n\n    function playAnim() { isAnimating = true; }\n    function pauseAnim() { isAnimating = false; }\n    function stopAnim() {\n      isAnimating = false;\n      animTime = 0;\n      resetZoom();\n      document.getElementById('cameraArcSlider').value = 90;\n      updateCameraFromSlider(90, currentRadius);\n    }\n\n    function updateCameraFromSlider(degVal, radius = currentRadius, height = manualPanY, xOffset = manualPanX) {\n      const rad = (degVal - 90) * (Math.PI / 180);\n      camera.position.x = controls.target.x + radius * Math.sin(rad) + xOffset;\n      camera.position.z = controls.target.z + radius * Math.cos(rad);\n      camera.position.y = height;\n      camera.lookAt(controls.target.x + xOffset, height, controls.target.z);\n      controls.update();\n    }\n\n    function applyAngleLimits() {\n      controls.minAzimuthAngle = (minDeg - 90) * (Math.PI / 180);\n      controls.maxAzimuthAngle = (maxDeg - 90) * (Math.PI / 180);\n      const cameraSlider = document.getElementById('cameraArcSlider');\n      cameraSlider.min = minDeg;\n      cameraSlider.max = maxDeg;\n      if (parseFloat(cameraSlider.value) < minDeg) cameraSlider.value = minDeg;\n      if (parseFloat(cameraSlider.value) > maxDeg) cameraSlider.value = maxDeg;\n      if (!isAnimating) updateCameraFromSlider(parseFloat(cameraSlider.value));\n    }\n\n    const cameraArcSlider = document.getElementById('cameraArcSlider');\n    cameraArcSlider.addEventListener('input', (e) => {\n      isAnimating = false;\n      updateCameraFromSlider(parseFloat(e.target.value));\n    });\n\n    controls.addEventListener('change', () => {\n      if (!isAnimating) {\n        const angleRad = Math.atan2(camera.position.x - controls.target.x, camera.position.z - controls.target.z);\n        let deg = angleRad * (180 / Math.PI) + 90;\n        if (deg < 0) deg += 360;\n        cameraArcSlider.value = Math.max(minDeg, Math.min(maxDeg, deg));\n      }\n    });\n\n    applyAngleLimits();\n\n    // Lights\n    const ambientLight = new THREE.AmbientLight(0xfff8ee, 0.2);\n    scene.add(ambientLight);\n\n    const spotlight = new THREE.SpotLight(0xfffaed, 1.2);\n    spotlight.position.set(0, 4.5, 3.5);\n    spotlight.target.position.set(0, 1.8, 0);\n    spotlight.angle = 0.6;\n    spotlight.penumbra = 0.8;\n    spotlight.castShadow = true;\n    spotlight.shadow.mapSize.width = shadowMapSize;\n    spotlight.shadow.mapSize.height = shadowMapSize;\n    spotlight.shadow.bias = -0.00005;\n    spotlight.shadow.normalBias = 0.015;\n    spotlight.shadow.radius = 6;\n    scene.add(spotlight);\n    scene.add(spotlight.target);\n\n    document.getElementById('lightPresetSelector').addEventListener('change', (e) => {\n      const preset = e.target.value;\n      if (preset === 'gallery') {\n        spotlight.intensity = 1.2; spotlight.color.setHex(0xfffaed);\n        spotlight.angle = 0.6; spotlight.penumbra = 0.8; ambientLight.intensity = 0.2;\n      } else if (preset === 'sunset') {\n        spotlight.intensity = 1.8; spotlight.color.setHex(0xffaa55);\n        spotlight.angle = 0.8; spotlight.penumbra = 0.9; ambientLight.intensity = 0.15;\n      } else if (preset === 'daylight') {\n        spotlight.intensity = 1.0; spotlight.color.setHex(0xeef5ff);\n        spotlight.angle = 0.7; spotlight.penumbra = 0.7; ambientLight.intensity = 0.35;\n      } else if (preset === 'dramatic') {\n        spotlight.intensity = 2.4; spotlight.color.setHex(0xffffff);\n        spotlight.angle = 0.45; spotlight.penumbra = 0.3; ambientLight.intensity = 0.05;\n      }\n      document.getElementById('spotIntensitySlider').value = spotlight.intensity;\n      document.getElementById('spotColorPicker').value = '#' + spotlight.color.getHexString();\n      document.getElementById('spotAngleSlider').value = spotlight.angle;\n      document.getElementById('spotPenumbraSlider').value = spotlight.penumbra;\n      document.getElementById('ambientIntensitySlider').value = ambientLight.intensity;\n    });\n\n    document.getElementById('spotIntensitySlider').addEventListener('input', (e) => { spotlight.intensity = parseFloat(e.target.value); });\n    document.getElementById('spotColorPicker').addEventListener('input', (e) => { spotlight.color.set(e.target.value); });\n    document.getElementById('spotAngleSlider').addEventListener('input', (e) => { spotlight.angle = parseFloat(e.target.value); });\n    document.getElementById('spotPenumbraSlider').addEventListener('input', (e) => { spotlight.penumbra = parseFloat(e.target.value); });\n    document.getElementById('ambientIntensitySlider').addEventListener('input', (e) => { ambientLight.intensity = parseFloat(e.target.value); });\n\n    document.getElementById('qualityPreset').addEventListener('change', (e) => applyQuality(e.target.value));\n\n    // ─── Room Templates ────────────────────────────────────────────────\n    function applyRoomTemplate(template) {\n      const set = (id, val, isCheck) => {\n        const el = document.getElementById(id);\n        if (!el) return;\n        if (isCheck) { el.checked = val; el.dispatchEvent(new Event('change')); }\n        else { el.value = val; el.dispatchEvent(new Event(el.type === 'range' || el.type === 'color' ? 'input' : 'change')); }\n      };\n\n      if (template === 'whiteCube') {\n        // Clean contemporary gallery: pure white walls, high ambient, minimal furniture\n        set('lightPresetSelector', 'daylight');\n        set('spotIntensitySlider', 0.9);\n        set('spotColorPicker', '#f5f8ff');\n        set('spotAngleSlider', 0.85);\n        set('spotPenumbraSlider', 0.9);\n        set('ambientIntensitySlider', 0.45);\n        set('wallColorPicker', '#f7f7f5');\n        set('floorRoughnessSlider', 0.35);\n        set('baseboardToggle', false, true);\n        set('trackToggle', false, true);\n        set('benchToggle', false, true);\n        set('stanchionToggle', false, true);\n        set('baseboardStyleSelector', 'modern');\n        set('baseboardColorPicker', '#ffffff');\n      } else if (template === 'warmTraditional') {\n        // Warm classical room with rich walls, gold accents, full furnishings\n        set('lightPresetSelector', 'sunset');\n        set('spotIntensitySlider', 1.6);\n        set('spotColorPicker', '#ffd4a0');\n        set('spotAngleSlider', 0.55);\n        set('spotPenumbraSlider', 0.7);\n        set('ambientIntensitySlider', 0.18);\n        set('wallColorPicker', '#5c4a3a');\n        set('floorRoughnessSlider', 0.22);\n        set('baseboardToggle', true, true);\n        set('baseboardStyleSelector', 'traditional');\n        set('baseboardColorPicker', '#f5efe6');\n        set('trackToggle', true, true);\n        set('benchToggle', true, true);\n        set('stanchionToggle', true, true);\n        set('metalFinishSelector', 'gold');\n        set('ropeColorPicker', '#6b1a1a');\n      } else if (template === 'modernLoft') {\n        // Industrial loft: dark walls, cooler light, chrome, no stanchions\n        set('lightPresetSelector', 'dramatic');\n        set('spotIntensitySlider', 2.0);\n        set('spotColorPicker', '#eef2ff');\n        set('spotAngleSlider', 0.5);\n        set('spotPenumbraSlider', 0.4);\n        set('ambientIntensitySlider', 0.12);\n        set('wallColorPicker', '#2a2c30');\n        set('floorRoughnessSlider', 0.45);\n        set('baseboardToggle', true, true);\n        set('baseboardStyleSelector', 'modern');\n        set('baseboardColorPicker', '#1a1a1c');\n        set('trackToggle', true, true);\n        set('benchToggle', true, true);\n        set('stanchionToggle', false, true);\n        set('metalFinishSelector', 'chrome');\n      } else {\n        // Classic Gallery (default)\n        set('lightPresetSelector', 'gallery');\n        set('spotIntensitySlider', 1.2);\n        set('spotColorPicker', '#fffaed');\n        set('spotAngleSlider', 0.6);\n        set('spotPenumbraSlider', 0.8);\n        set('ambientIntensitySlider', 0.2);\n        set('wallColorPicker', '#8e8e8e');\n        set('floorRoughnessSlider', 0.12);\n        set('baseboardToggle', true, true);\n        set('baseboardStyleSelector', 'traditional');\n        set('baseboardColorPicker', '#ffffff');\n        set('trackToggle', true, true);\n        set('benchToggle', true, true);\n        set('stanchionToggle', true, true);\n        set('metalFinishSelector', 'chrome');\n        set('ropeColorPicker', '#880000');\n      }\n    }\n\n    document.getElementById('roomTemplateSelector').addEventListener('change', (e) => {\n      applyRoomTemplate(e.target.value);\n    });\n\n    // Room geometry\n    const textureLoader = new THREE.TextureLoader();\n    const proceduralMarble = createMarbleTexture();\n    const canvasGrainBump = createCanvasGrain();\n\n    const backWallMat = new THREE.MeshStandardMaterial({ color: 0x8e8e8e, roughness: 0.9, side: THREE.DoubleSide });\n    const backWall = new THREE.Mesh(new THREE.PlaneGeometry(12, 6), backWallMat);\n    backWall.position.set(0, 3, -0.05);\n    backWall.receiveShadow = true;\n    scene.add(backWall);\n\n    let baseboardMesh = null;\n    const baseboardMat = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.4 });\n\n    function rebuildBaseboard() {\n      if (baseboardMesh) scene.remove(baseboardMesh);\n      if (!document.getElementById('baseboardToggle').checked) return;\n      const style = document.getElementById('baseboardStyleSelector').value;\n      const height = style === 'traditional' ? 0.15 : 0.10;\n      const depth = style === 'traditional' ? 0.025 : 0.015;\n      baseboardMesh = new THREE.Mesh(new THREE.BoxGeometry(12, height, depth), baseboardMat);\n      baseboardMesh.position.set(0, height / 2, -0.04);\n      baseboardMesh.castShadow = true;\n      baseboardMesh.receiveShadow = true;\n      scene.add(baseboardMesh);\n    }\n    rebuildBaseboard();\n\n    let trackGroup = new THREE.Group();\n    function rebuildTrackLights() {\n      scene.remove(trackGroup);\n      if (!document.getElementById('trackToggle').checked) return;\n      trackGroup = new THREE.Group();\n      const metalMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.3 });\n      const rail = new THREE.Mesh(new THREE.BoxGeometry(4.0, 0.04, 0.04), metalMat);\n      rail.position.set(0, 4.8, 1.5);\n      trackGroup.add(rail);\n      for (let x = -0.8; x <= 0.8; x += 1.6) {\n        const canister = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.07, 0.2, 16), metalMat);\n        canister.position.set(x, 4.7, 1.5);\n        canister.rotation.x = Math.PI / 4;\n        trackGroup.add(canister);\n      }\n      scene.add(trackGroup);\n    }\n    rebuildTrackLights();\n\n    const floorMat = new THREE.MeshStandardMaterial({\n      map: proceduralMarble, color: 0xffffff, roughness: 0.12, metalness: 0.1\n    });\n    floorMat.map.repeat.set(6, 6);\n    const floor = new THREE.Mesh(new THREE.PlaneGeometry(12, 10), floorMat);\n    floor.position.set(0, 0, 5);\n    floor.rotation.x = -Math.PI / 2;\n    floor.receiveShadow = true;\n    scene.add(floor);\n\n    let benchGroup = new THREE.Group();\n    function rebuildBench() {\n      scene.remove(benchGroup);\n      if (!document.getElementById('benchToggle').checked) return;\n      benchGroup = new THREE.Group();\n      const leatherMat = new THREE.MeshStandardMaterial({ color: 0x1f1a17, roughness: 0.7 });\n      const chromeMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, metalness: 0.9, roughness: 0.2 });\n      const cushion = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.12, 0.55), leatherMat);\n      cushion.position.set(0, 0.45, 2.6);\n      cushion.castShadow = true;\n      cushion.receiveShadow = true;\n      benchGroup.add(cushion);\n      const leg1 = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 0.45, 16), chromeMat);\n      leg1.position.set(-0.7, 0.225, 2.45);\n      leg1.castShadow = true;\n      benchGroup.add(leg1);\n      const leg2 = leg1.clone(); leg2.position.set(0.7, 0.225, 2.45); benchGroup.add(leg2);\n      const leg3 = leg1.clone(); leg3.position.set(-0.7, 0.225, 2.75); benchGroup.add(leg3);\n      const leg4 = leg1.clone(); leg4.position.set(0.7, 0.225, 2.75); benchGroup.add(leg4);\n      scene.add(benchGroup);\n    }\n    rebuildBench();\n\n    let placardMesh = null;\n    function rebuildPlacard() {\n      if (placardMesh) {\n        if (placardMesh.material.map) placardMesh.material.map.dispose();\n        placardMesh.material.dispose();\n        scene.remove(placardMesh);\n      }\n      if (!document.getElementById('placardToggle').checked) return;\n      const title = document.getElementById('titleInput').value || 'Untitled';\n      const medium = document.getElementById('mediumInput').value || '';\n      const dim = document.getElementById('dimInput').value || '';\n      const placardTex = createPlacardTexture(title, medium, dim);\n      const mat = new THREE.MeshStandardMaterial({ map: placardTex, roughness: 0.3 });\n      placardMesh = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.18, 0.008), mat);\n      placardMesh.position.set(1.45, 1.4, 0);\n      placardMesh.castShadow = true;\n      scene.add(placardMesh);\n    }\n    rebuildPlacard();\n\n    let stanchionGroup = new THREE.Group();\n    const stanchionMetalMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.2, metalness: 0.8 });\n    const ropeMat = new THREE.MeshStandardMaterial({ color: 0x880000, roughness: 0.8 });\n\n    function buildPost(x, z) {\n      const postGroup = new THREE.Group();\n      postGroup.position.set(x, 0, z);\n      const base = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.2, 0.03, 32), stanchionMetalMat);\n      base.position.y = 0.015; base.castShadow = true; postGroup.add(base);\n      const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.025, 0.95, 32), stanchionMetalMat);\n      pole.position.y = 0.5; pole.castShadow = true; postGroup.add(pole);\n      const topBall = new THREE.Mesh(new THREE.SphereGeometry(0.05, 32, 16), stanchionMetalMat);\n      topBall.position.y = 1.0; topBall.castShadow = true; postGroup.add(topBall);\n      return postGroup;\n    }\n\n    function rebuildStanchions() {\n      scene.remove(stanchionGroup);\n      if (!document.getElementById('stanchionToggle').checked) return;\n      stanchionGroup = new THREE.Group();\n      const leftX = -1.25, rightX = 1.25, distance = 1.2;\n      stanchionGroup.add(buildPost(leftX, distance));\n      stanchionGroup.add(buildPost(rightX, distance));\n      const curve = new THREE.CatmullRomCurve3([\n        new THREE.Vector3(leftX, 0.95, distance),\n        new THREE.Vector3(0, 0.65, distance + 0.15),\n        new THREE.Vector3(rightX, 0.95, distance)\n      ]);\n      const ropeMesh = new THREE.Mesh(new THREE.TubeGeometry(curve, 32, 0.02, 16, false), ropeMat);\n      ropeMesh.castShadow = true;\n      stanchionGroup.add(ropeMesh);\n      scene.add(stanchionGroup);\n    }\n    rebuildStanchions();\n\n    // ─── Artwork engine ────────────────────────────────────────────────\n    let artworkGroup = new THREE.Group();\n    let canvasMesh = null;\n    artworkGroup.position.set(0, 1.8, 0);\n    scene.add(artworkGroup);\n\n    let activeMediaType = 'image';\n    let currentImageDataURL = null;\n    let currentVideoURL = null;\n    let currentDisplayMode = 'floater';\n    let currentCanvasScale = 1.0;\n    let loadedTexture = null;\n    let imageAspect = 1.0;\n    const hiddenVideo = document.getElementById('hiddenVideo');\n\n    function disposeArtwork() {\n      if (canvasMesh) {\n        canvasMesh.geometry.dispose();\n        if (Array.isArray(canvasMesh.material)) {\n          canvasMesh.material.forEach(m => {\n            if (m.map && m.map !== loadedTexture) m.map.dispose();\n            m.dispose();\n          });\n        }\n      }\n      scene.remove(artworkGroup);\n      artworkGroup = new THREE.Group();\n      artworkGroup.position.set(0, 1.8, 0);\n    }\n\n    function buildArtwork() {\n      if (!loadedTexture) return;\n      disposeArtwork();\n\n      const maxDim = 2.0 * currentCanvasScale;\n      let canvasW, canvasH;\n      if (imageAspect >= 1) { canvasW = maxDim; canvasH = maxDim / imageAspect; }\n      else { canvasH = maxDim; canvasW = maxDim * imageAspect; }\n\n      const canvasMat = new THREE.MeshStandardMaterial({\n        map: loadedTexture,\n        bumpMap: activeMediaType === 'image' ? canvasGrainBump : null,\n        bumpScale: 0.003,\n        roughness: activeMediaType === 'video' ? 0.2 : 0.55\n      });\n      const canvasMaterials = [canvasMat, canvasMat, canvasMat, canvasMat, canvasMat, new THREE.MeshStandardMaterial({ color: 0x111111 })];\n      canvasMesh = new THREE.Mesh(new THREE.BoxGeometry(canvasW, canvasH, 0.05), canvasMaterials);\n      canvasMesh.castShadow = true;\n      canvasMesh.receiveShadow = true;\n\n      const showMat = document.getElementById('matToggle').checked;\n      const matWidth = parseFloat(document.getElementById('matWidthSlider').value);\n      const matColorHex = document.getElementById('matColorPicker').value;\n      let artworkTargetW = canvasW, artworkTargetH = canvasH;\n\n      if (showMat) {\n        artworkTargetW = canvasW + (matWidth * 2);\n        artworkTargetH = canvasH + (matWidth * 2);\n        const matMat = new THREE.MeshStandardMaterial({ color: matColorHex, roughness: 0.85 });\n        const matBoard = new THREE.Mesh(new THREE.BoxGeometry(artworkTargetW, artworkTargetH, 0.015), matMat);\n        matBoard.position.z = -0.005;\n        matBoard.castShadow = true;\n        matBoard.receiveShadow = true;\n        artworkGroup.add(matBoard);\n        const bevelMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, roughness: 0.5 });\n        const bevelTop = new THREE.Mesh(new THREE.BoxGeometry(canvasW + 0.01, 0.005, 0.016), bevelMat);\n        bevelTop.position.set(0, canvasH / 2 + 0.0025, -0.002);\n        artworkGroup.add(bevelTop);\n        const bevelBot = bevelTop.clone(); bevelBot.position.y = -canvasH / 2 - 0.0025; artworkGroup.add(bevelBot);\n        const bevelLeft = new THREE.Mesh(new THREE.BoxGeometry(0.005, canvasH + 0.01, 0.016), bevelMat);\n        bevelLeft.position.set(-canvasW / 2 - 0.0025, 0, -0.002);\n        artworkGroup.add(bevelLeft);\n        const bevelRight = bevelLeft.clone(); bevelRight.position.x = canvasW / 2 + 0.0025; artworkGroup.add(bevelRight);\n        canvasMesh.position.z = 0.01;\n      } else {\n        canvasMesh.position.z = 0;\n      }\n\n      if (currentDisplayMode === 'floater') {\n        const outerW = artworkTargetW + 0.06, outerH = artworkTargetH + 0.06;\n        const frameMat = new THREE.MeshStandardMaterial({ color: 0x181412, roughness: 0.6 });\n        const frameGroup = new THREE.Group();\n        const backer = new THREE.Mesh(new THREE.BoxGeometry(outerW, outerH, 0.01), frameMat);\n        backer.position.z = -0.025; backer.castShadow = true; frameGroup.add(backer);\n        const topEdge = new THREE.Mesh(new THREE.BoxGeometry(outerW, 0.02, 0.06), frameMat);\n        topEdge.position.set(0, outerH / 2 - 0.01, 0); topEdge.castShadow = true; frameGroup.add(topEdge);\n        const bottomEdge = topEdge.clone(); bottomEdge.position.y = -outerH / 2 + 0.01; frameGroup.add(bottomEdge);\n        const leftEdge = new THREE.Mesh(new THREE.BoxGeometry(0.02, outerH, 0.06), frameMat);\n        leftEdge.position.set(-outerW / 2 + 0.01, 0, 0); leftEdge.castShadow = true; frameGroup.add(leftEdge);\n        const rightEdge = leftEdge.clone(); rightEdge.position.x = outerW / 2 - 0.01; frameGroup.add(rightEdge);\n        if (!showMat) canvasMesh.position.z = 0.01;\n        artworkGroup.add(frameGroup);\n      } else if (currentDisplayMode === 'classic') {\n        const frameGroup = new THREE.Group();\n        const linerWidth = 0.12;\n        const linerW = artworkTargetW + (linerWidth * 2);\n        const linerH = artworkTargetH + (linerWidth * 2);\n        const linerMat = new THREE.MeshStandardMaterial({ color: 0xebe3d5, roughness: 0.9 });\n        const goldAccentMat = new THREE.MeshStandardMaterial({ color: 0xb8860b, roughness: 0.3, metalness: 0.8 });\n        const woodMat = new THREE.MeshStandardMaterial({ color: 0x2b1810, roughness: 0.4 });\n        const linerBacker = new THREE.Mesh(new THREE.BoxGeometry(linerW, linerH, 0.02), linerMat);\n        linerBacker.position.z = -0.01; linerBacker.castShadow = true; frameGroup.add(linerBacker);\n        const goldTrimTop = new THREE.Mesh(new THREE.BoxGeometry(artworkTargetW + 0.02, 0.01, 0.02), goldAccentMat);\n        goldTrimTop.position.set(0, artworkTargetH / 2 + 0.005, 0.005); frameGroup.add(goldTrimTop);\n        const goldTrimBot = goldTrimTop.clone(); goldTrimBot.position.y = -artworkTargetH / 2 - 0.005; frameGroup.add(goldTrimBot);\n        const goldTrimLeft = new THREE.Mesh(new THREE.BoxGeometry(0.01, artworkTargetH, 0.02), goldAccentMat);\n        goldTrimLeft.position.set(-artworkTargetW / 2 - 0.005, 0, 0.005); frameGroup.add(goldTrimLeft);\n        const goldTrimRight = goldTrimLeft.clone(); goldTrimRight.position.x = artworkTargetW / 2 + 0.005; frameGroup.add(goldTrimRight);\n        const outerW = linerW + 0.20, outerH = linerH + 0.20;\n        const outerTop = new THREE.Mesh(new THREE.BoxGeometry(outerW, 0.10, 0.08), woodMat);\n        outerTop.position.set(0, outerH / 2 - 0.05, 0.02); outerTop.castShadow = true; frameGroup.add(outerTop);\n        const outerBot = outerTop.clone(); outerBot.position.y = -outerH / 2 + 0.05; frameGroup.add(outerBot);\n        const outerLeft = new THREE.Mesh(new THREE.BoxGeometry(0.10, outerH - 0.20, 0.08), woodMat);\n        outerLeft.position.set(-outerW / 2 + 0.05, 0, 0.02); outerLeft.castShadow = true; frameGroup.add(outerLeft);\n        const outerRight = outerLeft.clone(); outerRight.position.x = outerW / 2 - 0.05; frameGroup.add(outerRight);\n        const goldFilletTop = new THREE.Mesh(new THREE.BoxGeometry(outerW, 0.02, 0.09), goldAccentMat);\n        goldFilletTop.position.set(0, linerH / 2 + 0.01, 0.025); frameGroup.add(goldFilletTop);\n        const goldFilletBot = goldFilletTop.clone(); goldFilletBot.position.y = -linerH / 2 - 0.01; frameGroup.add(goldFilletBot);\n        const goldFilletLeft = new THREE.Mesh(new THREE.BoxGeometry(0.02, linerH, 0.09), goldAccentMat);\n        goldFilletLeft.position.set(-linerW / 2 - 0.01, 0, 0.025); frameGroup.add(goldFilletLeft);\n        const goldFilletRight = goldFilletLeft.clone(); goldFilletRight.position.x = linerW / 2 + 0.01; frameGroup.add(goldFilletRight);\n        if (!showMat) canvasMesh.position.z = 0.005;\n        artworkGroup.add(frameGroup);\n      } else {\n        if (!showMat) canvasMesh.position.z = 0.025;\n      }\n\n      artworkGroup.add(canvasMesh);\n      scene.add(artworkGroup);\n    }\n\n    function loadArtwork(url) {\n      if (hiddenVideo) hiddenVideo.pause();\n      if (loadedTexture && loadedTexture.isVideoTexture) {\n        // keep video element, just swap texture reference\n      } else if (loadedTexture) {\n        loadedTexture.dispose();\n      }\n      textureLoader.load(url, (tex) => {\n        const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();\n        tex.anisotropy = maxAnisotropy;\n        tex.generateMipmaps = true;\n        tex.minFilter = THREE.LinearMipmapLinearFilter;\n        tex.magFilter = THREE.LinearFilter;\n        tex.needsUpdate = true;\n        loadedTexture = tex;\n        imageAspect = tex.image.width / tex.image.height;\n        buildArtwork();\n        showEmptyState(false);\n      });\n    }\n\n    function loadVideoTexture(src) {\n      if (loadedTexture && !loadedTexture.isVideoTexture) loadedTexture.dispose();\n      hiddenVideo.src = src;\n      hiddenVideo.load();\n      hiddenVideo.play().catch(() => {});\n      hiddenVideo.onloadedmetadata = () => {\n        imageAspect = hiddenVideo.videoWidth / hiddenVideo.videoHeight;\n        const videoTex = new THREE.VideoTexture(hiddenVideo);\n        const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();\n        videoTex.anisotropy = maxAnisotropy;\n        videoTex.minFilter = THREE.LinearFilter;\n        videoTex.magFilter = THREE.LinearFilter;\n        videoTex.format = THREE.RGBAFormat;\n        loadedTexture = videoTex;\n        buildArtwork();\n        showEmptyState(false);\n      };\n    }\n\n    function handleImageFile(file) {\n      if (file && file.type.startsWith('image/')) {\n        activeMediaType = 'image';\n        document.getElementById('mediaTypeSelector').value = 'image';\n        updateMediaUI();\n        const reader = new FileReader();\n        reader.onload = (event) => {\n          currentImageDataURL = event.target.result;\n          loadArtwork(currentImageDataURL);\n        };\n        reader.readAsDataURL(file);\n      }\n    }\n\n    function handleVideoFile(file) {\n      if (file && file.type.startsWith('video/')) {\n        activeMediaType = 'video';\n        document.getElementById('mediaTypeSelector').value = 'video';\n        updateMediaUI();\n        const url = URL.createObjectURL(file);\n        currentVideoURL = url;\n        loadVideoTexture(url);\n      }\n    }\n\n    function updateMediaUI() {\n      const isVideo = activeMediaType === 'video';\n      document.getElementById('imageUploadGroup').style.display = isVideo ? 'none' : 'block';\n      document.getElementById('videoUploadGroup').style.display = isVideo ? 'block' : 'none';\n      document.getElementById('videoControlsGroup').style.display = isVideo ? 'block' : 'none';\n    }\n\n    function toggleVideoPlay() {\n      if (!hiddenVideo) return;\n      hiddenVideo.paused ? hiddenVideo.play() : hiddenVideo.pause();\n    }\n    function toggleVideoMute() {\n      if (!hiddenVideo) return;\n      hiddenVideo.muted = !hiddenVideo.muted;\n    }\n\n    document.getElementById('mediaTypeSelector').addEventListener('change', (e) => {\n      activeMediaType = e.target.value;\n      updateMediaUI();\n      if (activeMediaType === 'image' && currentImageDataURL) loadArtwork(currentImageDataURL);\n      else if (activeMediaType === 'video' && currentVideoURL) loadVideoTexture(currentVideoURL);\n      else if (!currentImageDataURL && !currentVideoURL) showEmptyState(true);\n    });\n    document.getElementById('videoUploader').addEventListener('change', (e) => {\n      if (e.target.files?.[0]) handleVideoFile(e.target.files[0]);\n    });\n    document.getElementById('videoLoopToggle').addEventListener('change', (e) => {\n      hiddenVideo.loop = e.target.checked;\n    });\n    document.getElementById('videoSpeedSlider').addEventListener('input', (e) => {\n      const val = parseFloat(e.target.value);\n      hiddenVideo.playbackRate = val;\n      document.getElementById('videoSpeedVal').textContent = val.toFixed(2) + '×';\n    });\n\n    // Start empty — user drops or uploads artwork\n    showEmptyState(true);\n\n    // Drag & drop\n    const dropzoneOverlay = document.getElementById('dropzone-overlay');\n    let dragCounter = 0;\n    window.addEventListener('dragenter', (e) => {\n      e.preventDefault(); e.stopPropagation(); dragCounter++;\n      if (e.dataTransfer.items?.length > 0) dropzoneOverlay.classList.add('active');\n    });\n    window.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); });\n    window.addEventListener('dragleave', (e) => {\n      e.preventDefault(); e.stopPropagation(); dragCounter--;\n      if (dragCounter === 0) dropzoneOverlay.classList.remove('active');\n    });\n    window.addEventListener('drop', (e) => {\n      e.preventDefault(); e.stopPropagation(); dragCounter = 0;\n      dropzoneOverlay.classList.remove('active');\n      if (e.dataTransfer.files?.length > 0) {\n        const file = e.dataTransfer.files[0];\n        file.type.startsWith('video/') ? handleVideoFile(file) : handleImageFile(file);\n      }\n    });\n\n    // Click to open buy modal\n    const raycaster = new THREE.Raycaster();\n    const mouse = new THREE.Vector2();\n    function openModal() {\n      document.getElementById('mTitle').textContent = document.getElementById('titleInput').value || 'Untitled';\n      document.getElementById('mDetails').innerHTML =\n        (document.getElementById('mediumInput').value || '') + '<br>' +\n        (document.getElementById('dimInput').value || '');\n      document.getElementById('mPrice').textContent = document.getElementById('priceInput').value || '';\n      updateBuyButtonLink();\n      document.getElementById('purchaseModal').style.display = 'flex';\n    }\n    function closeModal() {\n      document.getElementById('purchaseModal').style.display = 'none';\n    }\n    window.addEventListener('click', (event) => {\n      if (event.target.closest('#menuToggleBtn') ||\n          event.target.closest('.editor-panel') ||\n          event.target.closest('.modal-card') ||\n          event.target.closest('.arc-control-bar') ||\n          event.target.closest('.top-bar')) return;\n      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;\n      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;\n      raycaster.setFromCamera(mouse, camera);\n      const targets = [];\n      if (canvasMesh) targets.push(canvasMesh);\n      if (placardMesh) targets.push(placardMesh);\n      if (targets.length && raycaster.intersectObjects(targets).length > 0) openModal();\n    });\n\n    document.getElementById('linkInput').addEventListener('input', updateBuyButtonLink);\n    updateBuyButtonLink();\n\n    // Control listeners\n    document.getElementById('fxToggle').addEventListener('change', (e) => { weatherFX.active = e.target.checked; });\n    document.getElementById('fxModeSelector').addEventListener('change', (e) => { weatherFX.setMode(e.target.value); });\n    document.getElementById('windToggle').addEventListener('change', (e) => { weatherFX.windEnabled = e.target.checked; });\n    document.getElementById('particleCountSlider').addEventListener('input', (e) => {\n      const val = parseInt(e.target.value);\n      document.getElementById('particleCountVal').textContent = val;\n      weatherFX.setCount(val);\n    });\n    document.getElementById('particleSizeSlider').addEventListener('input', (e) => {\n      const val = parseFloat(e.target.value);\n      document.getElementById('particleSizeVal').textContent = val.toFixed(1) + '×';\n      weatherFX.globalScaleMultiplier = val;\n    });\n    document.getElementById('scaleSlider').addEventListener('input', (e) => {\n      currentCanvasScale = parseFloat(e.target.value);\n      document.getElementById('scaleVal').textContent = currentCanvasScale.toFixed(2) + '×';\n      buildArtwork();\n    });\n    document.getElementById('manualZoomSlider').addEventListener('input', (e) => {\n      targetRadius = parseFloat(e.target.value);\n      document.getElementById('manualZoomVal').textContent = targetRadius.toFixed(2);\n    });\n    document.getElementById('smoothZoomSpeedSlider').addEventListener('input', (e) => {\n      smoothZoomSpeed = parseFloat(e.target.value);\n      document.getElementById('smoothZoomSpeedVal').textContent = smoothZoomSpeed.toFixed(2) + '×';\n    });\n    document.getElementById('manualPanYSlider').addEventListener('input', (e) => {\n      manualPanY = parseFloat(e.target.value);\n      document.getElementById('manualPanYVal').textContent = manualPanY.toFixed(2);\n      updateCameraFromSlider(parseFloat(cameraArcSlider.value));\n    });\n    document.getElementById('manualPanXSlider').addEventListener('input', (e) => {\n      manualPanX = parseFloat(e.target.value);\n      document.getElementById('manualPanXVal').textContent = manualPanX.toFixed(2);\n      updateCameraFromSlider(parseFloat(cameraArcSlider.value));\n    });\n    document.getElementById('matToggle').addEventListener('change', buildArtwork);\n    document.getElementById('matWidthSlider').addEventListener('input', (e) => {\n      document.getElementById('matWidthVal').textContent = parseFloat(e.target.value).toFixed(2);\n      buildArtwork();\n    });\n    document.getElementById('matColorPicker').addEventListener('input', buildArtwork);\n    document.getElementById('animPatternSelector').addEventListener('change', (e) => { animPattern = e.target.value; });\n    document.getElementById('animSpeedSlider').addEventListener('input', (e) => {\n      animCycleDuration = parseFloat(e.target.value);\n      document.getElementById('animSpeedVal').textContent = animCycleDuration + ' s';\n    });\n    document.getElementById('zoomDepthSlider').addEventListener('input', (e) => {\n      zoomTargetDepth = parseFloat(e.target.value);\n      document.getElementById('zoomDepthVal').textContent = zoomTargetDepth;\n    });\n    document.getElementById('minAngleSlider').addEventListener('input', (e) => {\n      minDeg = parseFloat(e.target.value);\n      document.getElementById('minAngleVal').textContent = minDeg + '°';\n      applyAngleLimits();\n    });\n    document.getElementById('maxAngleSlider').addEventListener('input', (e) => {\n      maxDeg = parseFloat(e.target.value);\n      document.getElementById('maxAngleVal').textContent = maxDeg + '°';\n      applyAngleLimits();\n    });\n    document.getElementById('arcSliderToggle').addEventListener('change', (e) => {\n      document.getElementById('arcControlBar').style.display = e.target.checked ? 'block' : 'none';\n    });\n    document.getElementById('pivotSelector').addEventListener('change', (e) => {\n      if (e.target.value === 'viewer') controls.target.set(manualPanX, 1.5, 2.6);\n      else controls.target.set(manualPanX, manualPanY, 0);\n      updateCameraFromSlider(parseFloat(cameraArcSlider.value));\n    });\n    document.getElementById('baseboardToggle').addEventListener('change', rebuildBaseboard);\n    document.getElementById('baseboardColorPicker').addEventListener('input', (e) => { baseboardMat.color.set(e.target.value); });\n    document.getElementById('baseboardStyleSelector').addEventListener('change', rebuildBaseboard);\n    document.getElementById('stanchionToggle').addEventListener('change', rebuildStanchions);\n    document.getElementById('benchToggle').addEventListener('change', rebuildBench);\n    document.getElementById('trackToggle').addEventListener('change', rebuildTrackLights);\n    document.getElementById('placardToggle').addEventListener('change', rebuildPlacard);\n    document.getElementById('titleInput').addEventListener('input', rebuildPlacard);\n    document.getElementById('mediumInput').addEventListener('input', rebuildPlacard);\n    document.getElementById('dimInput').addEventListener('input', rebuildPlacard);\n    document.getElementById('metalFinishSelector').addEventListener('change', (e) => {\n      stanchionMetalMat.color.set(e.target.value === 'gold' ? 0xd4af37 : 0xcccccc);\n    });\n    document.getElementById('ropeColorPicker').addEventListener('input', (e) => { ropeMat.color.set(e.target.value); });\n    document.getElementById('wallColorPicker').addEventListener('input', (e) => { backWallMat.color.set(e.target.value); });\n    document.getElementById('floorRoughnessSlider').addEventListener('input', (e) => {\n      floorMat.roughness = parseFloat(e.target.value);\n    });\n    document.getElementById('styleSelector').addEventListener('change', (e) => {\n      currentDisplayMode = e.target.value;\n      buildArtwork();\n    });\n    document.getElementById('artUploader').addEventListener('change', (e) => {\n      if (e.target.files?.[0]) handleImageFile(e.target.files[0]);\n    });\n\n    // Save / Load\n    function saveProject() {\n      const state = {\n        mediaType: activeMediaType,\n        artworkImage: currentImageDataURL,\n        canvasScale: currentCanvasScale,\n        style: document.getElementById('styleSelector').value,\n        matEnabled: document.getElementById('matToggle').checked,\n        matWidth: document.getElementById('matWidthSlider').value,\n        matColor: document.getElementById('matColorPicker').value,\n        fxEnabled: document.getElementById('fxToggle').checked,\n        fxMode: document.getElementById('fxModeSelector').value,\n        windEnabled: document.getElementById('windToggle').checked,\n        particleCount: document.getElementById('particleCountSlider').value,\n        particleSize: document.getElementById('particleSizeSlider').value,\n        quality: quality,\n        roomTemplate: document.getElementById('roomTemplateSelector').value,\n        lightPreset: document.getElementById('lightPresetSelector').value,\n        spotIntensity: document.getElementById('spotIntensitySlider').value,\n        spotColor: document.getElementById('spotColorPicker').value,\n        spotAngle: document.getElementById('spotAngleSlider').value,\n        spotPenumbra: document.getElementById('spotPenumbraSlider').value,\n        ambientIntensity: document.getElementById('ambientIntensitySlider').value,\n        wallColor: document.getElementById('wallColorPicker').value,\n        floorRoughness: document.getElementById('floorRoughnessSlider').value,\n        baseboardEnabled: document.getElementById('baseboardToggle').checked,\n        baseboardStyle: document.getElementById('baseboardStyleSelector').value,\n        baseboardColor: document.getElementById('baseboardColorPicker').value,\n        trackEnabled: document.getElementById('trackToggle').checked,\n        benchEnabled: document.getElementById('benchToggle').checked,\n        stanchionEnabled: document.getElementById('stanchionToggle').checked,\n        metalFinish: document.getElementById('metalFinishSelector').value,\n        ropeColor: document.getElementById('ropeColorPicker').value,\n        placardEnabled: document.getElementById('placardToggle').checked,\n        title: document.getElementById('titleInput').value,\n        medium: document.getElementById('mediumInput').value,\n        dim: document.getElementById('dimInput').value,\n        price: document.getElementById('priceInput').value,\n        link: document.getElementById('linkInput').value,\n        manualZoom: targetRadius,\n        smoothZoomSpeed: smoothZoomSpeed,\n        manualPanY: document.getElementById('manualPanYSlider').value,\n        manualPanX: document.getElementById('manualPanXSlider').value,\n        pivot: document.getElementById('pivotSelector').value,\n        arcControlVisible: document.getElementById('arcSliderToggle').checked,\n        minAngle: document.getElementById('minAngleSlider').value,\n        maxAngle: document.getElementById('maxAngleSlider').value,\n        autoPlay: document.getElementById('exportAnimToggle').checked,\n        animPattern: document.getElementById('animPatternSelector').value,\n        animSpeed: document.getElementById('animSpeedSlider').value,\n        zoomDepth: document.getElementById('zoomDepthSlider').value,\n        videoLoop: document.getElementById('videoLoopToggle').checked,\n        videoSpeed: document.getElementById('videoSpeedSlider').value\n      };\n      const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });\n      const a = document.createElement('a');\n      a.href = URL.createObjectURL(blob);\n      const filename = (state.title ? state.title.replace(/[^a-z0-9]/gi, '_').toLowerCase() : 'gallery_project') + '.gallery';\n      a.download = filename;\n      a.click();\n      URL.revokeObjectURL(a.href);\n    }\n\n    function loadProject(event) {\n      const file = event.target.files[0];\n      if (!file) return;\n      const reader = new FileReader();\n      reader.onload = (e) => {\n        try {\n          const state = JSON.parse(e.target.result);\n          activeMediaType = state.mediaType || 'image';\n          document.getElementById('mediaTypeSelector').value = activeMediaType;\n          updateMediaUI();\n          document.getElementById('scaleSlider').value = state.canvasScale || 1.0;\n          currentCanvasScale = parseFloat(state.canvasScale || 1.0);\n          document.getElementById('scaleVal').textContent = currentCanvasScale.toFixed(2) + '×';\n          let loadedStyle = state.style || 'floater';\n          if (loadedStyle === 'kinkade') loadedStyle = 'classic'; // legacy rename\n          document.getElementById('styleSelector').value = loadedStyle;\n          currentDisplayMode = loadedStyle;\n          document.getElementById('matToggle').checked = !!state.matEnabled;\n          document.getElementById('matWidthSlider').value = state.matWidth || 0.12;\n          document.getElementById('matWidthVal').textContent = parseFloat(state.matWidth || 0.12).toFixed(2);\n          document.getElementById('matColorPicker').value = state.matColor || '#f4f1ea';\n          document.getElementById('fxToggle').checked = !!state.fxEnabled;\n          weatherFX.active = !!state.fxEnabled;\n          document.getElementById('fxModeSelector').value = state.fxMode || 'leaves';\n          weatherFX.setMode(state.fxMode || 'leaves');\n          document.getElementById('windToggle').checked = !!state.windEnabled;\n          weatherFX.windEnabled = !!state.windEnabled;\n          document.getElementById('particleCountSlider').value = state.particleCount || 35;\n          document.getElementById('particleCountVal').textContent = state.particleCount || 35;\n          weatherFX.setCount(parseInt(state.particleCount || 35));\n          document.getElementById('particleSizeSlider').value = state.particleSize || 1.0;\n          document.getElementById('particleSizeVal').textContent = parseFloat(state.particleSize || 1.0).toFixed(1) + '×';\n          weatherFX.globalScaleMultiplier = parseFloat(state.particleSize || 1.0);\n          if (state.quality) {\n            document.getElementById('qualityPreset').value = state.quality;\n            applyQuality(state.quality);\n          }\n          if (state.roomTemplate) {\n            document.getElementById('roomTemplateSelector').value = state.roomTemplate;\n          }\n          document.getElementById('lightPresetSelector').value = state.lightPreset || 'gallery';\n          document.getElementById('spotIntensitySlider').value = state.spotIntensity || 1.2;\n          spotlight.intensity = parseFloat(state.spotIntensity || 1.2);\n          document.getElementById('spotColorPicker').value = state.spotColor || '#fffaed';\n          spotlight.color.set(state.spotColor || '#fffaed');\n          document.getElementById('spotAngleSlider').value = state.spotAngle || 0.6;\n          spotlight.angle = parseFloat(state.spotAngle || 0.6);\n          document.getElementById('spotPenumbraSlider').value = state.spotPenumbra || 0.8;\n          spotlight.penumbra = parseFloat(state.spotPenumbra || 0.8);\n          document.getElementById('ambientIntensitySlider').value = state.ambientIntensity || 0.2;\n          ambientLight.intensity = parseFloat(state.ambientIntensity || 0.2);\n          document.getElementById('wallColorPicker').value = state.wallColor || '#8e8e8e';\n          backWallMat.color.set(state.wallColor || '#8e8e8e');\n          document.getElementById('floorRoughnessSlider').value = state.floorRoughness || 0.12;\n          floorMat.roughness = parseFloat(state.floorRoughness || 0.12);\n          document.getElementById('baseboardToggle').checked = !!state.baseboardEnabled;\n          document.getElementById('baseboardStyleSelector').value = state.baseboardStyle || 'traditional';\n          document.getElementById('baseboardColorPicker').value = state.baseboardColor || '#ffffff';\n          baseboardMat.color.set(state.baseboardColor || '#ffffff');\n          rebuildBaseboard();\n          document.getElementById('trackToggle').checked = !!state.trackEnabled;\n          rebuildTrackLights();\n          document.getElementById('benchToggle').checked = !!state.benchEnabled;\n          rebuildBench();\n          document.getElementById('stanchionToggle').checked = !!state.stanchionEnabled;\n          document.getElementById('metalFinishSelector').value = state.metalFinish || 'chrome';\n          stanchionMetalMat.color.set((state.metalFinish || 'chrome') === 'gold' ? 0xd4af37 : 0xcccccc);\n          document.getElementById('ropeColorPicker').value = state.ropeColor || '#880000';\n          ropeMat.color.set(state.ropeColor || '#880000');\n          rebuildStanchions();\n          document.getElementById('placardToggle').checked = !!state.placardEnabled;\n          document.getElementById('titleInput').value = state.title || '';\n          document.getElementById('mediumInput').value = state.medium || '';\n          document.getElementById('dimInput').value = state.dim || '';\n          document.getElementById('priceInput').value = state.price || '';\n          document.getElementById('linkInput').value = state.link || '';\n          rebuildPlacard();\n          updateBuyButtonLink();\n          targetRadius = parseFloat(state.manualZoom || DEFAULT_ZOOM);\n          document.getElementById('manualZoomSlider').value = targetRadius;\n          document.getElementById('manualZoomVal').textContent = targetRadius.toFixed(2);\n          smoothZoomSpeed = parseFloat(state.smoothZoomSpeed || 2.5);\n          document.getElementById('smoothZoomSpeedSlider').value = state.smoothZoomSpeed || 2.5;\n          document.getElementById('smoothZoomSpeedVal').textContent = parseFloat(state.smoothZoomSpeed || 2.5).toFixed(2) + '×';\n          document.getElementById('manualPanYSlider').value = state.manualPanY || 1.8;\n          manualPanY = parseFloat(state.manualPanY || 1.8);\n          document.getElementById('manualPanYVal').textContent = manualPanY.toFixed(2);\n          document.getElementById('manualPanXSlider').value = state.manualPanX || 0.0;\n          manualPanX = parseFloat(state.manualPanX || 0.0);\n          document.getElementById('manualPanXVal').textContent = manualPanX.toFixed(2);\n          document.getElementById('pivotSelector').value = state.pivot || 'painting';\n          if ((state.pivot || 'painting') === 'viewer') controls.target.set(manualPanX, 1.5, 2.6);\n          else controls.target.set(manualPanX, manualPanY, 0);\n          document.getElementById('arcSliderToggle').checked = !!state.arcControlVisible;\n          document.getElementById('arcControlBar').style.display = !!state.arcControlVisible ? 'block' : 'none';\n          document.getElementById('minAngleSlider').value = state.minAngle || 30;\n          minDeg = parseFloat(state.minAngle || 30);\n          document.getElementById('minAngleVal').textContent = minDeg + '°';\n          document.getElementById('maxAngleSlider').value = state.maxAngle || 150;\n          maxDeg = parseFloat(state.maxAngle || 150);\n          document.getElementById('maxAngleVal').textContent = maxDeg + '°';\n          applyAngleLimits();\n          document.getElementById('exportAnimToggle').checked = !!state.autoPlay;\n          document.getElementById('animPatternSelector').value = state.animPattern || 'pendulum';\n          animPattern = state.animPattern || 'pendulum';\n          document.getElementById('animSpeedSlider').value = state.animSpeed || 12;\n          animCycleDuration = parseFloat(state.animSpeed || 12);\n          document.getElementById('animSpeedVal').textContent = animCycleDuration + ' s';\n          document.getElementById('zoomDepthSlider').value = state.zoomDepth || 2.2;\n          zoomTargetDepth = parseFloat(state.zoomDepth || 2.2);\n          document.getElementById('zoomDepthVal').textContent = zoomTargetDepth;\n          if (state.videoLoop !== undefined) {\n            document.getElementById('videoLoopToggle').checked = !!state.videoLoop;\n            if (hiddenVideo) hiddenVideo.loop = !!state.videoLoop;\n          }\n          if (state.videoSpeed !== undefined) {\n            document.getElementById('videoSpeedSlider').value = state.videoSpeed;\n            document.getElementById('videoSpeedVal').textContent = parseFloat(state.videoSpeed).toFixed(2) + '×';\n            if (hiddenVideo) hiddenVideo.playbackRate = parseFloat(state.videoSpeed);\n          }\n          if (activeMediaType === 'image' && state.artworkImage) {\n            currentImageDataURL = state.artworkImage;\n            loadArtwork(currentImageDataURL);\n          } else {\n            buildArtwork();\n          }\n          updateCameraFromSlider(parseFloat(cameraArcSlider.value));\n        } catch (err) {\n          alert('Failed to parse project file.');\n        }\n      };\n      reader.readAsText(file);\n      event.target.value = '';\n    }\n\n    // Animation loop\n\n    const clock = new THREE.Clock();\n    function animate() {\n      requestAnimationFrame(animate);\n      const delta = clock.getDelta();\n      const lerpFactor = 1 - Math.exp(-smoothZoomSpeed * delta * 4);\n      currentRadius += (targetRadius - currentRadius) * lerpFactor;\n\n      if (isAnimating) {\n        animTime += delta;\n        const progress = (animTime % animCycleDuration) / animCycleDuration;\n        let currentAngle = 90, animRadius = currentRadius, currentY = manualPanY, currentX = manualPanX;\n        const angleRange = maxDeg - minDeg;\n\n        if (animPattern === 'pendulum') {\n          const sine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          currentAngle = minDeg + sine * angleRange;\n        } else if (animPattern === 'zoomSweep') {\n          const turnSine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          const zoomSine = Math.sin(progress * Math.PI);\n          currentAngle = minDeg + turnSine * angleRange;\n          animRadius = currentRadius - (currentRadius - zoomTargetDepth) * zoomSine;\n        } else if (animPattern === 'spiral') {\n          currentAngle = minDeg + progress * angleRange;\n          animRadius = currentRadius - (currentRadius - zoomTargetDepth) * progress;\n        } else if (animPattern === 'singleSweep') {\n          currentAngle = minDeg + Math.sin(progress * Math.PI) * angleRange;\n        } else if (animPattern === 'figure8') {\n          const sine8 = Math.sin(progress * Math.PI * 2);\n          const cos8 = Math.cos(progress * Math.PI * 4);\n          currentAngle = minDeg + ((sine8 + 1) / 2) * angleRange;\n          currentY = manualPanY + cos8 * 0.35;\n        } else if (animPattern === 'flyby') {\n          const flySine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          currentAngle = minDeg + flySine * angleRange;\n          currentX = manualPanX + Math.sin(progress * Math.PI * 2) * 0.8;\n          animRadius = currentRadius - (currentRadius - zoomTargetDepth) * Math.sin(progress * Math.PI);\n        } else if (animPattern === 'elevator') {\n          const eleSine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          currentAngle = minDeg + eleSine * angleRange;\n          currentY = manualPanY + Math.sin(progress * Math.PI * 2) * 0.6;\n        }\n        updateCameraFromSlider(currentAngle, animRadius, currentY, currentX);\n        document.getElementById('cameraArcSlider').value = currentAngle;\n      } else {\n        updateCameraFromSlider(parseFloat(cameraArcSlider.value), currentRadius);\n        controls.update();\n      }\n\n      renderer.render(scene, camera);\n      weatherFX.render();\n\n      if (isRecording && window._recordCtx && window._recordCanvas && window._recordDims) {\n        const d = window._recordDims;\n        const ctx = window._recordCtx;\n        const c = window._recordCanvas;\n        ctx.clearRect(0, 0, c.width, c.height);\n        ctx.drawImage(renderer.domElement, d.sx, d.sy, d.sw, d.sh, 0, 0, c.width, c.height);\n        if (weatherFX.active) {\n          ctx.drawImage(weatherFX.canvas, d.sx, d.sy, d.sw, d.sh, 0, 0, c.width, c.height);\n        }\n        drawWatermarkOnCtx(ctx, c.width, c.height);\n      }\n    }\n    animate();\n\n    window.addEventListener('resize', () => {\n      camera.aspect = window.innerWidth / window.innerHeight;\n      camera.updateProjectionMatrix();\n      renderer.setSize(window.innerWidth, window.innerHeight);\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio, maxPixelRatio));\n    });\n\n    // Pause FX when tab is hidden (performance)\n    document.addEventListener('visibilitychange', () => {\n      if (document.hidden) weatherFX.active = false;\n      else weatherFX.active = document.getElementById('fxToggle').checked;\n    });\n\n    function exportHTML() {\n      if (!hasArtwork || !loadedTexture) {\n        alert('Please add an artwork first.');\n        return;\n      }\n      if (activeMediaType === 'video') {\n        alert('Standalone HTML export currently supports static images. Record a video instead for video artworks.');\n        return;\n      }\n      if (!currentImageDataURL) {\n        alert('Artwork data is not available for export. Please re-upload the image.');\n        return;\n      }\n\n      // Collect presentation state for the viewer\n      const state = {\n        title: document.getElementById('titleInput').value || 'Artwork',\n        medium: document.getElementById('mediumInput').value || '',\n        dim: document.getElementById('dimInput').value || '',\n        price: document.getElementById('priceInput').value || '',\n        link: document.getElementById('linkInput').value || '#',\n        artworkImage: currentImageDataURL,\n        canvasScale: currentCanvasScale,\n        style: currentDisplayMode,\n        matEnabled: document.getElementById('matToggle').checked,\n        matWidth: parseFloat(document.getElementById('matWidthSlider').value),\n        matColor: document.getElementById('matColorPicker').value,\n        wallColor: document.getElementById('wallColorPicker').value,\n        floorRoughness: parseFloat(document.getElementById('floorRoughnessSlider').value),\n        spotIntensity: parseFloat(document.getElementById('spotIntensitySlider').value),\n        spotColor: document.getElementById('spotColorPicker').value,\n        spotAngle: parseFloat(document.getElementById('spotAngleSlider').value),\n        spotPenumbra: parseFloat(document.getElementById('spotPenumbraSlider').value),\n        ambientIntensity: parseFloat(document.getElementById('ambientIntensitySlider').value),\n        baseboardEnabled: document.getElementById('baseboardToggle').checked,\n        baseboardStyle: document.getElementById('baseboardStyleSelector').value,\n        baseboardColor: document.getElementById('baseboardColorPicker').value,\n        trackEnabled: document.getElementById('trackToggle').checked,\n        benchEnabled: document.getElementById('benchToggle').checked,\n        stanchionEnabled: document.getElementById('stanchionToggle').checked,\n        metalFinish: document.getElementById('metalFinishSelector').value,\n        ropeColor: document.getElementById('ropeColorPicker').value,\n        placardEnabled: document.getElementById('placardToggle').checked,\n        fxEnabled: document.getElementById('fxToggle').checked,\n        fxMode: document.getElementById('fxModeSelector').value,\n        particleCount: Math.min(40, parseInt(document.getElementById('particleCountSlider').value) || 25),\n        animPattern: document.getElementById('animPatternSelector').value,\n        animSpeed: parseFloat(document.getElementById('animSpeedSlider').value),\n        zoomDepth: parseFloat(document.getElementById('zoomDepthSlider').value),\n        minAngle: parseFloat(document.getElementById('minAngleSlider').value),\n        maxAngle: parseFloat(document.getElementById('maxAngleSlider').value),\n        autoPlay: document.getElementById('exportAnimToggle').checked\n      };\n\n      const safeTitle = (state.title || 'viewing_room').replace(/[^a-z0-9]/gi, '_').toLowerCase();\n      const stateJSON = JSON.stringify(state);\n\n      const viewerHTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>${state.title.replace(/</g, '')} — Imagine Art Gallery</title>\n  <style>\n    * { box-sizing: border-box; margin: 0; padding: 0; }\n    html, body { width: 100%; height: 100%; overflow: hidden; background: #0f0f10; font-family: system-ui, -apple-system, sans-serif; color: #f2f0eb; }\n    canvas { display: block; }\n    #fx-canvas { position: absolute; inset: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 5; }\n    /* Critical: WebGL canvas must be absolute full-viewport or a black strip appears on the side */\n    body > canvas:not(#fx-canvas) {\n      position: absolute !important;\n      inset: 0 !important;\n      width: 100% !important;\n      height: 100% !important;\n      z-index: 1;\n    }\n    .top-bar {\n      position: absolute; top: 0; left: 0; right: 0; height: 48px; z-index: 20;\n      display: flex; align-items: center; justify-content: space-between; padding: 0 16px;\n      background: linear-gradient(to bottom, rgba(15,15,16,0.85), transparent); pointer-events: none;\n    }\n    .top-bar > * { pointer-events: auto; }\n    .brand { font-size: 13px; font-weight: 600; letter-spacing: 0.04em; opacity: 0.9; }\n    .brand span { color: #c9a86c; }\n    .controls {\n      position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);\n      display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; z-index: 20;\n      background: rgba(22,22,24,0.92); border: 1px solid rgba(255,255,255,0.08);\n      padding: 10px 14px; border-radius: 14px; backdrop-filter: blur(12px);\n      box-shadow: 0 12px 40px rgba(0,0,0,0.45);\n    }\n    .controls button, .controls a {\n      background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);\n      color: #f2f0eb; padding: 8px 12px; border-radius: 6px; font-size: 11px; font-weight: 600;\n      text-transform: uppercase; letter-spacing: 0.03em; cursor: pointer; text-decoration: none;\n    }\n    .controls button:hover, .controls a:hover { border-color: #c9a86c; color: #c9a86c; }\n    .controls a.buy { background: #c9a86c; color: #1a1612; border-color: #c9a86c; }\n    .controls a.buy:hover { background: #dbb97a; color: #1a1612; }\n    #watermark {\n      position: absolute; bottom: 78px; right: 16px; z-index: 12;\n      font-size: 11px; font-weight: 600; letter-spacing: 0.06em;\n      color: rgba(255,255,255,0.35); text-transform: uppercase; pointer-events: none; user-select: none;\n    }\n    .modal-overlay {\n      display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.72); z-index: 100;\n      align-items: center; justify-content: center; backdrop-filter: blur(4px);\n    }\n    .modal-card {\n      background: #1a1a1c; border: 1px solid rgba(255,255,255,0.08); border-radius: 14px;\n      padding: 28px; width: 360px; max-width: 92%; position: relative;\n    }\n    .modal-close { position: absolute; top: 12px; right: 14px; color: #9a9690; font-size: 22px; cursor: pointer; }\n    .modal-title { font-size: 18px; font-weight: 700; color: #c9a86c; margin-bottom: 6px; }\n    .modal-details { font-size: 13px; color: #9a9690; margin-bottom: 12px; line-height: 1.5; }\n    .modal-price { font-size: 20px; font-weight: 700; margin-bottom: 16px; }\n    .btn-buy {\n      display: block; width: 100%; text-align: center; background: #c9a86c; color: #1a1612;\n      padding: 12px; border-radius: 6px; font-weight: 700; text-decoration: none; font-size: 13px;\n    }\n  </style>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\"><\\/script>\n  <script src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js\"><\\/script>\n</head>\n<body>\n  <canvas id=\"fx-canvas\"></canvas>\n  <div class=\"top-bar\">\n    <div class=\"brand\"><span>Imagine</span> Art Gallery</div>\n  </div>\n  <div id=\"watermark\">Preview · Imagine Art Gallery</div>\n  <div class=\"controls\">\n    <button type=\"button\" id=\"btnPlay\">Play</button>\n    <button type=\"button\" id=\"btnPause\">Pause</button>\n    <button type=\"button\" id=\"btnReset\">Reset</button>\n    <a class=\"buy\" id=\"buyBtn\" href=\"${state.link.replace(/\"/g, '&quot;')}\" target=\"_blank\" rel=\"noopener\">Buy Original</a>\n  </div>\n  <div class=\"modal-overlay\" id=\"purchaseModal\">\n    <div class=\"modal-card\">\n      <span class=\"modal-close\" id=\"modalClose\">&times;</span>\n      <div class=\"modal-title\" id=\"mTitle\"></div>\n      <div class=\"modal-details\" id=\"mDetails\"></div>\n      <div class=\"modal-price\" id=\"mPrice\"></div>\n      <a href=\"#\" target=\"_blank\" rel=\"noopener\" class=\"btn-buy\" id=\"mBuyBtn\">Buy Original Painting</a>\n    </div>\n  </div>\n  <script>\n    const STATE = ${stateJSON};\n\n    // ── Seasonal FX (lightweight) ──\n    class SeasonalFXOverlay {\n      constructor(canvasId, count) {\n        this.canvas = document.getElementById(canvasId);\n        this.ctx = this.canvas.getContext('2d');\n        this.targetCount = count;\n        this.particles = [];\n        this.active = !!STATE.fxEnabled;\n        this.mode = STATE.fxMode || 'leaves';\n        this.globalScaleMultiplier = 1.0;\n        this.windEnabled = true;\n        this.windForceX = 0; this.windForceY = 0; this.gustTimer = 0;\n        this.resize();\n        window.addEventListener('resize', () => this.resize());\n        this.initParticles();\n      }\n      resize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; }\n      createParticleObject() {\n        if (this.mode === 'snow') {\n          return { x: Math.random() * (this.canvas.width + 200) - 100, y: Math.random() * this.canvas.height - this.canvas.height,\n            baseScale: Math.random() * 0.6 + 0.3, speedY: Math.random() * 0.8 + 0.4, speedX: Math.random() * 0.3 - 0.15,\n            oscillation: Math.random() * Math.PI * 2, oscillationSpeed: Math.random() * 0.02 + 0.008,\n            flipAngle: Math.random() * Math.PI * 2, flipSpeed: Math.random() * 0.02 + 0.005,\n            angle: Math.random() * Math.PI * 2, spin: (Math.random() - 0.5) * 0.015, depth: Math.random() * 0.7 + 0.3,\n            type: Math.random() > 0.4 ? 'flake' : 'soft', opacity: Math.random() * 0.4 + 0.5 };\n        }\n        const leafPalettes = [\n          { base: '#d9531e', dark: '#a3330e', vein: '#fca34d' },\n          { base: '#e67319', dark: '#ad4908', vein: '#ffd166' },\n          { base: '#f4a213', dark: '#c46d00', vein: '#fff1ad' },\n          { base: '#c13915', dark: '#7e1d04', vein: '#f28e6b' }\n        ];\n        return { x: Math.random() * (this.canvas.width + 200) - 100, y: Math.random() * this.canvas.height - this.canvas.height,\n          baseScale: Math.random() * 0.7 + 0.4, speedY: Math.random() * 1.1 + 0.7, speedX: Math.random() * 0.4 - 0.2,\n          oscillation: Math.random() * Math.PI * 2, oscillationSpeed: Math.random() * 0.035 + 0.015,\n          flipAngle: Math.random() * Math.PI * 2, flipSpeed: Math.random() * 0.04 + 0.01,\n          angle: Math.random() * Math.PI * 2, spin: (Math.random() - 0.5) * 0.025, depth: Math.random() * 0.6 + 0.4,\n          palette: leafPalettes[Math.floor(Math.random() * leafPalettes.length)] };\n      }\n      initParticles() {\n        this.particles = [];\n        for (let i = 0; i < this.targetCount; i++) {\n          const p = this.createParticleObject();\n          p.y = Math.random() * this.canvas.height;\n          this.particles.push(p);\n        }\n      }\n      updateWind() {\n        if (!this.windEnabled) { this.windForceX = 0; this.windForceY = 0; return; }\n        this.gustTimer += 0.02;\n        const breeze = Math.sin(this.gustTimer * 0.8) * 0.6 + Math.cos(this.gustTimer * 0.3) * 0.4;\n        const gust = Math.pow(Math.max(0, Math.sin(this.gustTimer * 0.4)), 3) * 1.8;\n        this.windForceX = (breeze + gust) * 0.8;\n        this.windForceY = Math.sin(this.gustTimer * 1.2) * 0.2;\n      }\n      drawSnowflake(ctx, p) {\n        ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.angle);\n        const scaleX = p.baseScale * p.depth * this.globalScaleMultiplier;\n        ctx.scale(scaleX, scaleX * (0.8 + 0.2 * Math.cos(p.flipAngle)));\n        if (p.depth < 0.5) ctx.filter = 'blur(1px)';\n        ctx.globalAlpha = p.opacity;\n        if (p.type === 'soft') {\n          const grad = ctx.createRadialGradient(0, 0, 0, 0, 0, 10);\n          grad.addColorStop(0, 'rgba(255,255,255,0.95)'); grad.addColorStop(1, 'rgba(255,255,255,0)');\n          ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(0, 0, 10, 0, Math.PI * 2); ctx.fill();\n        } else {\n          ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.8; ctx.lineCap = 'round';\n          ctx.beginPath();\n          for (let i = 0; i < 6; i++) { ctx.rotate(Math.PI / 3); ctx.moveTo(0, 0); ctx.lineTo(0, 16); ctx.moveTo(0, 8); ctx.lineTo(5, 12); ctx.moveTo(0, 8); ctx.lineTo(-5, 12); }\n          ctx.stroke();\n        }\n        ctx.restore();\n      }\n      drawMapleLeaf(ctx, leaf) {\n        ctx.save(); ctx.translate(leaf.x, leaf.y); ctx.rotate(leaf.angle);\n        const scaleX = leaf.baseScale * leaf.depth * this.globalScaleMultiplier;\n        ctx.scale(scaleX, scaleX * Math.cos(leaf.flipAngle));\n        if (leaf.depth < 0.65) ctx.filter = 'blur(1px)';\n        const grad = ctx.createRadialGradient(0, 0, 4, 0, 0, 30);\n        grad.addColorStop(0, leaf.palette.base); grad.addColorStop(1, leaf.palette.dark);\n        ctx.fillStyle = grad;\n        ctx.beginPath();\n        ctx.moveTo(0, -32);\n        ctx.lineTo(6, -20); ctx.lineTo(12, -22); ctx.lineTo(22, -18); ctx.lineTo(16, -10); ctx.lineTo(30, -4); ctx.lineTo(20, 2);\n        ctx.lineTo(26, 12); ctx.lineTo(16, 14); ctx.lineTo(12, 22); ctx.lineTo(5, 12); ctx.lineTo(0, 10);\n        ctx.lineTo(-5, 12); ctx.lineTo(-12, 22); ctx.lineTo(-16, 14); ctx.lineTo(-26, 12); ctx.lineTo(-20, 2);\n        ctx.lineTo(-30, -4); ctx.lineTo(-16, -10); ctx.lineTo(-22, -18); ctx.lineTo(-12, -22); ctx.lineTo(-6, -20);\n        ctx.closePath(); ctx.fill();\n        ctx.restore();\n      }\n      render() {\n        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n        if (!this.active) return;\n        this.updateWind();\n        for (let p of this.particles) {\n          p.oscillation += p.oscillationSpeed; p.flipAngle += p.flipSpeed; p.angle += p.spin;\n          const sway = Math.sin(p.oscillation) * (this.mode === 'snow' ? 0.8 : 1.2);\n          p.x += p.speedX + sway + (this.windForceX * p.depth);\n          p.y += (p.speedY + (this.windForceY * p.depth)) * p.depth;\n          if (p.y > this.canvas.height + 50) { p.y = -40; p.x = Math.random() * (this.canvas.width + 200) - 100; }\n          if (p.x > this.canvas.width + 100) p.x = -50;\n          if (p.x < -100) p.x = this.canvas.width + 50;\n          if (this.mode === 'snow') this.drawSnowflake(this.ctx, p); else this.drawMapleLeaf(this.ctx, p);\n        }\n      }\n    }\n    const weatherFX = new SeasonalFXOverlay('fx-canvas', STATE.particleCount || 25);\n\n    // ── Three.js scene ──\n    const scene = new THREE.Scene();\n    scene.background = new THREE.Color(0x0f0f10);\n    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);\n    const DEFAULT_ZOOM = 4.5;\n    let targetRadius = DEFAULT_ZOOM, currentRadius = DEFAULT_ZOOM;\n    let manualPanY = 1.8, manualPanX = 0.0;\n    camera.position.set(0, manualPanY, currentRadius);\n\n    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n    renderer.setSize(window.innerWidth, window.innerHeight);\n    renderer.shadowMap.enabled = true;\n    renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n    document.body.appendChild(renderer.domElement);\n    // Ensure canvas fills viewport (guards against black side bars on some browsers)\n    renderer.domElement.style.position = 'absolute';\n    renderer.domElement.style.inset = '0';\n    renderer.domElement.style.width = '100%';\n    renderer.domElement.style.height = '100%';\n    renderer.domElement.style.zIndex = '1';\n\n    const controls = new THREE.OrbitControls(camera, renderer.domElement);\n    controls.target.set(0, manualPanY, 0);\n    controls.enableDamping = true;\n    controls.dampingFactor = 0.05;\n    const minDeg = STATE.minAngle || 30, maxDeg = STATE.maxAngle || 150;\n    controls.minAzimuthAngle = (minDeg - 90) * Math.PI / 180;\n    controls.maxAzimuthAngle = (maxDeg - 90) * Math.PI / 180;\n\n    let isAnimating = !!STATE.autoPlay;\n    let animTime = 0;\n    const animCycleDuration = STATE.animSpeed || 12;\n    const zoomTargetDepth = STATE.zoomDepth || 2.2;\n    const animPattern = STATE.animPattern || 'pendulum';\n\n    const ambientLight = new THREE.AmbientLight(0xfff8ee, STATE.ambientIntensity || 0.2);\n    scene.add(ambientLight);\n    const spotlight = new THREE.SpotLight(STATE.spotColor || '#fffaed', STATE.spotIntensity || 1.2);\n    spotlight.position.set(0, 4.5, 3.5);\n    spotlight.target.position.set(0, 1.8, 0);\n    spotlight.angle = STATE.spotAngle || 0.6;\n    spotlight.penumbra = STATE.spotPenumbra || 0.8;\n    spotlight.castShadow = true;\n    spotlight.shadow.mapSize.set(1024, 1024);\n    scene.add(spotlight); scene.add(spotlight.target);\n\n    // Room\n    const backWallMat = new THREE.MeshStandardMaterial({ color: STATE.wallColor || '#8e8e8e', roughness: 0.9, side: THREE.DoubleSide });\n    const backWall = new THREE.Mesh(new THREE.PlaneGeometry(12, 6), backWallMat);\n    backWall.position.set(0, 3, -0.05); backWall.receiveShadow = true; scene.add(backWall);\n\n    if (STATE.baseboardEnabled) {\n      const h = STATE.baseboardStyle === 'traditional' ? 0.15 : 0.10;\n      const d = STATE.baseboardStyle === 'traditional' ? 0.025 : 0.015;\n      const bb = new THREE.Mesh(new THREE.BoxGeometry(12, h, d), new THREE.MeshStandardMaterial({ color: STATE.baseboardColor || '#ffffff', roughness: 0.4 }));\n      bb.position.set(0, h / 2, -0.04); scene.add(bb);\n    }\n\n    // Simple marble floor\n    const floorCanvas = document.createElement('canvas'); floorCanvas.width = 256; floorCanvas.height = 256;\n    const fctx = floorCanvas.getContext('2d'); fctx.fillStyle = '#f5f5f7'; fctx.fillRect(0,0,256,256);\n    fctx.strokeStyle = 'rgba(120,120,120,0.25)'; fctx.lineWidth = 2;\n    for (let i = 0; i < 6; i++) { fctx.beginPath(); let x = Math.random()*256, y = 0; fctx.moveTo(x,y); while (y < 256) { x += (Math.random()-0.5)*40; y += Math.random()*30; fctx.lineTo(x,y); } fctx.stroke(); }\n    const floorTex = new THREE.CanvasTexture(floorCanvas); floorTex.wrapS = floorTex.wrapT = THREE.RepeatWrapping; floorTex.repeat.set(6,6);\n    const floorMat = new THREE.MeshStandardMaterial({ map: floorTex, color: 0xffffff, roughness: STATE.floorRoughness || 0.12, metalness: 0.1 });\n    const floor = new THREE.Mesh(new THREE.PlaneGeometry(12, 10), floorMat);\n    floor.position.set(0, 0, 5); floor.rotation.x = -Math.PI / 2; floor.receiveShadow = true; scene.add(floor);\n\n    if (STATE.benchEnabled) {\n      const leather = new THREE.MeshStandardMaterial({ color: 0x1f1a17, roughness: 0.7 });\n      const chrome = new THREE.MeshStandardMaterial({ color: 0xdddddd, metalness: 0.9, roughness: 0.2 });\n      const cushion = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.12, 0.55), leather);\n      cushion.position.set(0, 0.45, 2.6); cushion.castShadow = true; scene.add(cushion);\n      [[-0.7,2.45],[0.7,2.45],[-0.7,2.75],[0.7,2.75]].forEach(([x,z]) => {\n        const leg = new THREE.Mesh(new THREE.CylinderGeometry(0.02,0.02,0.45,12), chrome);\n        leg.position.set(x, 0.225, z); scene.add(leg);\n      });\n    }\n\n    if (STATE.stanchionEnabled) {\n      const metalC = STATE.metalFinish === 'gold' ? 0xd4af37 : 0xcccccc;\n      const metal = new THREE.MeshStandardMaterial({ color: metalC, roughness: 0.2, metalness: 0.8 });\n      const rope = new THREE.MeshStandardMaterial({ color: STATE.ropeColor || '#880000', roughness: 0.8 });\n      function post(x,z) {\n        const g = new THREE.Group(); g.position.set(x,0,z);\n        const base = new THREE.Mesh(new THREE.CylinderGeometry(0.18,0.2,0.03,24), metal); base.position.y = 0.015; g.add(base);\n        const pole = new THREE.Mesh(new THREE.CylinderGeometry(0.025,0.025,0.95,16), metal); pole.position.y = 0.5; g.add(pole);\n        const ball = new THREE.Mesh(new THREE.SphereGeometry(0.05,16,12), metal); ball.position.y = 1.0; g.add(ball);\n        scene.add(g);\n      }\n      post(-1.25, 1.2); post(1.25, 1.2);\n      const curve = new THREE.CatmullRomCurve3([\n        new THREE.Vector3(-1.25, 0.95, 1.2), new THREE.Vector3(0, 0.65, 1.35), new THREE.Vector3(1.25, 0.95, 1.2)\n      ]);\n      const ropeMesh = new THREE.Mesh(new THREE.TubeGeometry(curve, 24, 0.02, 12, false), rope);\n      scene.add(ropeMesh);\n    }\n\n    if (STATE.trackEnabled) {\n      const metalMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.3 });\n      const rail = new THREE.Mesh(new THREE.BoxGeometry(4.0, 0.04, 0.04), metalMat);\n      rail.position.set(0, 4.8, 1.5); scene.add(rail);\n      for (let x = -0.8; x <= 0.8; x += 1.6) {\n        const can = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.07, 0.2, 12), metalMat);\n        can.position.set(x, 4.7, 1.5); can.rotation.x = Math.PI / 4; scene.add(can);\n      }\n    }\n\n    // Placard\n    let placardMesh = null;\n    if (STATE.placardEnabled) {\n      const pc = document.createElement('canvas'); pc.width = 1024; pc.height = 512;\n      const pctx = pc.getContext('2d');\n      pctx.fillStyle = '#ffffff'; pctx.fillRect(0,0,1024,512);\n      pctx.fillStyle = '#111'; pctx.font = 'bold 56px sans-serif'; pctx.fillText(STATE.title || 'Untitled', 60, 140);\n      pctx.font = '40px sans-serif'; pctx.fillStyle = '#555';\n      pctx.fillText(STATE.medium || '', 60, 240); pctx.fillText(STATE.dim || '', 60, 320);\n      const ptex = new THREE.CanvasTexture(pc);\n      placardMesh = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.18, 0.008), new THREE.MeshStandardMaterial({ map: ptex, roughness: 0.3 }));\n      placardMesh.position.set(1.45, 1.4, 0); scene.add(placardMesh);\n    }\n\n    // Artwork\n    let canvasMesh = null;\n    const artworkGroup = new THREE.Group();\n    artworkGroup.position.set(0, 1.8, 0);\n    scene.add(artworkGroup);\n\n    const loader = new THREE.TextureLoader();\n    loader.load(STATE.artworkImage, (tex) => {\n      tex.anisotropy = renderer.capabilities.getMaxAnisotropy();\n      tex.generateMipmaps = true;\n      tex.minFilter = THREE.LinearMipmapLinearFilter;\n      const aspect = tex.image.width / tex.image.height;\n      const maxDim = 2.0 * (STATE.canvasScale || 1);\n      let canvasW, canvasH;\n      if (aspect >= 1) { canvasW = maxDim; canvasH = maxDim / aspect; }\n      else { canvasH = maxDim; canvasW = maxDim * aspect; }\n\n      const canvasMat = new THREE.MeshStandardMaterial({ map: tex, roughness: 0.55 });\n      const mats = [canvasMat, canvasMat, canvasMat, canvasMat, canvasMat, new THREE.MeshStandardMaterial({ color: 0x111111 })];\n      canvasMesh = new THREE.Mesh(new THREE.BoxGeometry(canvasW, canvasH, 0.05), mats);\n      canvasMesh.castShadow = true;\n\n      let artworkTargetW = canvasW, artworkTargetH = canvasH;\n      if (STATE.matEnabled) {\n        const mw = STATE.matWidth || 0.12;\n        artworkTargetW = canvasW + mw * 2; artworkTargetH = canvasH + mw * 2;\n        const matBoard = new THREE.Mesh(new THREE.BoxGeometry(artworkTargetW, artworkTargetH, 0.015),\n          new THREE.MeshStandardMaterial({ color: STATE.matColor || '#f4f1ea', roughness: 0.85 }));\n        matBoard.position.z = -0.005; artworkGroup.add(matBoard);\n        canvasMesh.position.z = 0.01;\n      }\n\n      const style = STATE.style || 'floater';\n      if (style === 'floater') {\n        const outerW = artworkTargetW + 0.06, outerH = artworkTargetH + 0.06;\n        const frameMat = new THREE.MeshStandardMaterial({ color: 0x181412, roughness: 0.6 });\n        const backer = new THREE.Mesh(new THREE.BoxGeometry(outerW, outerH, 0.01), frameMat); backer.position.z = -0.025; artworkGroup.add(backer);\n        [['x', outerW, 0.02, outerH/2-0.01], ['x', outerW, 0.02, -(outerH/2-0.01)],\n         ['y', 0.02, outerH, -(outerW/2-0.01)], ['y', 0.02, outerH, outerW/2-0.01]].forEach(([axis, a, b, pos]) => {\n          const e = new THREE.Mesh(new THREE.BoxGeometry(axis==='x'?a:b, axis==='x'?b:a, 0.06), frameMat);\n          if (axis==='x') e.position.set(0, pos, 0); else e.position.set(pos, 0, 0);\n          artworkGroup.add(e);\n        });\n        if (!STATE.matEnabled) canvasMesh.position.z = 0.01;\n      } else if (style === 'classic' || style === 'kinkade') {\n        const linerW = artworkTargetW + 0.24, linerH = artworkTargetH + 0.24;\n        const linerMat = new THREE.MeshStandardMaterial({ color: 0xebe3d5, roughness: 0.9 });\n        const gold = new THREE.MeshStandardMaterial({ color: 0xb8860b, roughness: 0.3, metalness: 0.8 });\n        const wood = new THREE.MeshStandardMaterial({ color: 0x2b1810, roughness: 0.4 });\n        const liner = new THREE.Mesh(new THREE.BoxGeometry(linerW, linerH, 0.02), linerMat); liner.position.z = -0.01; artworkGroup.add(liner);\n        const outerW = linerW + 0.20, outerH = linerH + 0.20;\n        const ot = new THREE.Mesh(new THREE.BoxGeometry(outerW, 0.10, 0.08), wood); ot.position.set(0, outerH/2-0.05, 0.02); artworkGroup.add(ot);\n        const ob = ot.clone(); ob.position.y = -(outerH/2-0.05); artworkGroup.add(ob);\n        const ol = new THREE.Mesh(new THREE.BoxGeometry(0.10, outerH-0.20, 0.08), wood); ol.position.set(-(outerW/2-0.05), 0, 0.02); artworkGroup.add(ol);\n        const or = ol.clone(); or.position.x = outerW/2-0.05; artworkGroup.add(or);\n        if (!STATE.matEnabled) canvasMesh.position.z = 0.005;\n      } else {\n        if (!STATE.matEnabled) canvasMesh.position.z = 0.025;\n      }\n      artworkGroup.add(canvasMesh);\n    });\n\n    function updateCamera(degVal, radius, height, xOffset) {\n      radius = radius == null ? currentRadius : radius;\n      height = height == null ? manualPanY : height;\n      xOffset = xOffset == null ? manualPanX : xOffset;\n      const rad = (degVal - 90) * Math.PI / 180;\n      camera.position.x = controls.target.x + radius * Math.sin(rad) + xOffset;\n      camera.position.z = controls.target.z + radius * Math.cos(rad);\n      camera.position.y = height;\n      camera.lookAt(controls.target.x + xOffset, height, controls.target.z);\n      controls.update();\n    }\n\n    document.getElementById('btnPlay').onclick = () => { isAnimating = true; };\n    document.getElementById('btnPause').onclick = () => { isAnimating = false; };\n    document.getElementById('btnReset').onclick = () => {\n      isAnimating = false; animTime = 0; targetRadius = DEFAULT_ZOOM; currentRadius = DEFAULT_ZOOM;\n      updateCamera(90);\n    };\n\n    // Modal\n    function openModal() {\n      document.getElementById('mTitle').textContent = STATE.title || 'Untitled';\n      document.getElementById('mDetails').innerHTML = (STATE.medium || '') + '<br>' + (STATE.dim || '');\n      document.getElementById('mPrice').textContent = STATE.price || '';\n      document.getElementById('mBuyBtn').href = STATE.link || '#';\n      document.getElementById('purchaseModal').style.display = 'flex';\n    }\n    document.getElementById('modalClose').onclick = () => { document.getElementById('purchaseModal').style.display = 'none'; };\n    document.getElementById('buyBtn').href = STATE.link || '#';\n\n    const raycaster = new THREE.Raycaster();\n    const mouse = new THREE.Vector2();\n    window.addEventListener('click', (event) => {\n      if (event.target.closest('.controls') || event.target.closest('.modal-card') || event.target.closest('.top-bar')) return;\n      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;\n      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;\n      raycaster.setFromCamera(mouse, camera);\n      const targets = [];\n      if (canvasMesh) targets.push(canvasMesh);\n      if (placardMesh) targets.push(placardMesh);\n      if (targets.length && raycaster.intersectObjects(targets).length > 0) openModal();\n    });\n\n    const clock = new THREE.Clock();\n    function animate() {\n      requestAnimationFrame(animate);\n      const delta = clock.getDelta();\n      currentRadius += (targetRadius - currentRadius) * (1 - Math.exp(-2.5 * delta * 4));\n\n      if (isAnimating) {\n        animTime += delta;\n        const progress = (animTime % animCycleDuration) / animCycleDuration;\n        let currentAngle = 90, animRadius = currentRadius, currentY = manualPanY, currentX = manualPanX;\n        const angleRange = maxDeg - minDeg;\n        if (animPattern === 'pendulum') {\n          const sine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          currentAngle = minDeg + sine * angleRange;\n        } else if (animPattern === 'zoomSweep') {\n          const turnSine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          currentAngle = minDeg + turnSine * angleRange;\n          animRadius = currentRadius - (currentRadius - zoomTargetDepth) * Math.sin(progress * Math.PI);\n        } else if (animPattern === 'spiral') {\n          currentAngle = minDeg + progress * angleRange;\n          animRadius = currentRadius - (currentRadius - zoomTargetDepth) * progress;\n        } else if (animPattern === 'singleSweep') {\n          currentAngle = minDeg + Math.sin(progress * Math.PI) * angleRange;\n        } else if (animPattern === 'figure8') {\n          currentAngle = minDeg + ((Math.sin(progress * Math.PI * 2) + 1) / 2) * angleRange;\n          currentY = manualPanY + Math.cos(progress * Math.PI * 4) * 0.35;\n        } else if (animPattern === 'flyby') {\n          currentAngle = minDeg + ((Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2) * angleRange;\n          currentX = manualPanX + Math.sin(progress * Math.PI * 2) * 0.8;\n          animRadius = currentRadius - (currentRadius - zoomTargetDepth) * Math.sin(progress * Math.PI);\n        } else if (animPattern === 'elevator') {\n          currentAngle = minDeg + ((Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2) * angleRange;\n          currentY = manualPanY + Math.sin(progress * Math.PI * 2) * 0.6;\n        } else {\n          const sine = (Math.sin(progress * Math.PI * 2 - Math.PI / 2) + 1) / 2;\n          currentAngle = minDeg + sine * angleRange;\n        }\n        updateCamera(currentAngle, animRadius, currentY, currentX);\n      } else {\n        controls.update();\n      }\n      renderer.render(scene, camera);\n      weatherFX.render();\n    }\n    animate();\n\n    window.addEventListener('resize', () => {\n      camera.aspect = window.innerWidth / window.innerHeight;\n      camera.updateProjectionMatrix();\n      renderer.setSize(window.innerWidth, window.innerHeight);\n    });\n  <\\/script>\n</body>\n</html>`;\n\n      const blob = new Blob([viewerHTML], { type: 'text/html' });\n      const a = document.createElement('a');\n      a.href = URL.createObjectURL(blob);\n      a.download = safeTitle + '_viewer.html';\n      a.click();\n      URL.revokeObjectURL(a.href);\n    }\n\n    // Init quality\n    applyQuality('balanced');\n  </script>\n</body>\n</html>\n","embed_html_app1headerHtml":""},"metaData":{"position":{"left":-475,"top":728},"size":{"width":960,"height":644}}},{"type":"image","id":"id1790010577004","elementProperties":{"tagName":"DIV","behaviours":null,"isLocked":false,"name":"Image 3","baseName":null,"templateID":"templateid1790010577005","templateName":null,"order":11,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"width":960,"height":158,"left":180,"top":689},"lastResizingSize":{"width":960,"height":158,"left":180,"top":689},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":12,"left":12,"spread":0,"blur":24,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"display":"fixed","cellItem":null,"cellHorizontalAlignment":"none","basicUserCanEdit":true,"isVisible":true,"backgroundColor":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillHorizontal":false,"fillVertical":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"startSelected":false,"urlAddress":null,"openInNewTab":false,"openInTopWindow":false,"shouldUnderlineChildren":false,"templateOpener":false,"popupType":"DefaultPopup","imageUrl":{"id":155120782,"imagePath":"https://storage.googleapis.com/production-homestead-v1-0-0/170/2018170/S9EXuCtN/2e4362da94cf4445b8fe2f57c8472579","storageServer":0,"imageWidth":1794,"imageHeight":592},"imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"bottomLeft":0,"topRight":0,"bottomRight":0},"borderColor":{"top":"black","left":"black","bottom":"black","right":"black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"},"rotationAngle":0,"backgroundOverlay":"transparent","dataBinding":null,"imageLoadingOverlay":{"background":"rgba(255,255,255,0.7)","imageInfo":{}},"imageLoadingSettings":{"hideImage":false,"showOverlay":false,"overlayBackground":"rgba(255,255,255,0.7)"},"alt":null},"metaData":{"position":{"left":-475,"top":570},"size":{"width":960,"height":158}}}]}]},{"type":"SectionElement","id":"id1791230597571","elementProperties":{"tagName":"SECTION","behaviours":null,"isLocked":false,"name":"Section 7","baseName":null,"templateID":null,"templateName":null,"order":6,"customStates":null,"isElementWasEditedInMode":false,"showHoverEffects":true,"skinInformation":{"categoryId":199},"isSupportsMobile":false,"isVisibleInMode":true,"isFixed":false,"topZIndex":0,"elementOpacity":1,"staticMargin":{"horizontalMargin":0,"verticalMargin":0},"sizeAndPosition":{"height":556,"left":0,"width":1261,"top":0},"lastResizingSize":{"height":556,"left":0,"width":1261,"top":0},"isVisibleInLastSection":true,"isVisibleInLastDataItem":true,"attributes":null,"metaProps":null,"metaItemType":null,"metaTitle":null,"styleBindingId":null,"styleBindingFields":null,"shadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"glow":{"spread":0,"blur":0,"color":"black"},"isParallaxBackground":false,"parallaxBackgroundSpeed":0.5,"isParallaxElement":{"isActive":false,"speed":1,"delay":0},"effectsOptions":{"isParallax":false,"oneTimeAnimation":false,"duration":1200,"startDistance":200,"startVisibility":0.8,"distance":300,"measureDistanceType":3,"delay":0,"distanceDelay":0,"runInLoop":false,"flyDirection":2,"direction":"horizontal","movementDistanceHorizontally":200,"movementDistanceVertically":0,"autoStartDistanceSettings":true,"autoDistanceSettings":true},"effects":null,"binding":null,"innerGlow":{"spread":0,"blur":0,"color":"black"},"innerShadow":{"top":0,"left":0,"spread":0,"blur":0,"color":"black"},"padding":{"top":0,"left":0,"bottom":0,"right":0},"basicUserCanEdit":true,"backgroundColor":"transparent","imageUrl":"","imageLayout":"stretchedToFill","imagePosition":{"vertical":"center","horizontal":"center"},"origImage":"","imageOpacity":1,"backgroundOverlay":"transparent","verticalLayout":"normal","horizontalLayout":"normal","fillLayout":{"fillVertical":false,"fillHorizontal":false},"margin":{"top":0,"left":0,"bottom":0,"right":0},"overflow":false,"isGrowable":{"bottom":false,"right":false},"rowsItems":null,"childrenAnchor":"center","resizingGripPosiotion":"s","sectionName":"Section 3","pageContentType":1,"borderWidth":{"top":0,"left":0,"bottom":0,"right":0},"roundness":{"topLeft":0,"bottomLeft":0,"topRight":0,"bottomRight":0},"borderColor":{"top":"Black","left":"Black","bottom":"Black","right":"Black"},"borderStyle":{"top":"solid","left":"solid","bottom":"solid","right":"solid"}},"metaData":{"position":{"left":-598.5,"top":1738},"size":{"width":1197,"height":556}}}],"offset":{"left":231,"top":-1136}}],"connections":{"targetConnections":{},"sourceConnections":{}},"customStatesProperties":{},"pageId":"id1789981756489","version":17.004,"id":"id1791353777160","relativePoint":{"left":231,"top":-1136}}