{"product_id":"studio","title":"Studio","description":"\u003c!-- File: Shopify Product Description HTML\/code view OR Custom Liquid section --\u003e\n\n\u003cdiv id=\"usaf-pro-studio\"\u003e\n\n  \u003cstyle\u003e\n    @import url(\"https:\/\/fonts.googleapis.com\/css2?family=Anton\u0026family=Bebas+Neue\u0026family=Black+Ops+One\u0026family=Bungee\u0026family=Lobster\u0026family=Oswald:wght@700\u0026family=Pacifico\u0026family=Permanent+Marker\u0026display=swap\");\n\n    #usaf-pro-studio{\n      --yellow:#ffd400;\n      --line:#2a3542;\n      --text:#f7f8fb;\n      --muted:#919eab;\n      --green:#22d99a;\n      --red:#ff707c;\n      max-width:1600px;\n      margin:20px auto;\n      overflow:hidden;\n      border:1px solid #202a35;\n      border-radius:27px;\n      background:\n        radial-gradient(circle at 10% 0%,rgba(255,212,0,.07),transparent 26%),\n        linear-gradient(180deg,#05070b,#090d13);\n      color:var(--text);\n      font-family:Arial,Helvetica,sans-serif;\n      box-shadow:0 30px 80px rgba(0,0,0,.32);\n    }\n\n    #usaf-pro-studio *{\n      box-sizing:border-box;\n    }\n\n    #usaf-pro-studio button,\n    #usaf-pro-studio input,\n    #usaf-pro-studio select,\n    #usaf-pro-studio textarea{\n      font:inherit;\n    }\n\n    .usaf-hidden{\n      display:none!important;\n    }\n\n    .usaf-appbar{\n      min-height:80px;\n      padding:14px 18px;\n      display:flex;\n      justify-content:space-between;\n      align-items:center;\n      gap:12px;\n      flex-wrap:wrap;\n      border-bottom:1px solid var(--line);\n      background:linear-gradient(180deg,#111821,#090d13);\n    }\n\n    .usaf-brand{\n      display:flex;\n      align-items:center;\n      gap:12px;\n    }\n\n    .usaf-logo{\n      width:53px;\n      height:53px;\n      display:grid;\n      place-items:center;\n      border-radius:16px;\n      background:linear-gradient(145deg,#ffe95f,#ffd400);\n      color:#111;\n      font-size:25px;\n      font-weight:1000;\n    }\n\n    .usaf-brand strong{\n      display:block;\n      font-size:20px;\n    }\n\n    .usaf-brand span{\n      display:block;\n      margin-top:3px;\n      color:var(--muted);\n      font-size:9px;\n      font-weight:900;\n      letter-spacing:.14em;\n      text-transform:uppercase;\n    }\n\n    .usaf-mode{\n      padding:7px 11px;\n      border:1px solid rgba(255,212,0,.22);\n      border-radius:999px;\n      background:rgba(255,212,0,.06);\n      color:var(--yellow);\n      font-size:9px;\n      font-weight:900;\n    }\n\n    .usaf-nav{\n      display:flex;\n      gap:7px;\n      flex-wrap:wrap;\n      padding:10px 16px;\n      border-bottom:1px solid var(--line);\n      background:#080c12;\n    }\n\n    .usaf-tab,\n    .usaf-btn{\n      min-height:43px;\n      padding:9px 13px;\n      border:1px solid var(--line);\n      border-radius:12px;\n      background:linear-gradient(180deg,#18212c,#111821);\n      color:#fff;\n      font-size:11px;\n      font-weight:900;\n      cursor:pointer;\n      text-decoration:none;\n    }\n\n    .usaf-tab{\n      border-color:transparent;\n      background:transparent;\n      color:#aeb9c4;\n    }\n\n    .usaf-tab.active,\n    .usaf-btn.primary{\n      border-color:#ac9000;\n      background:linear-gradient(180deg,#ffe452,#ffd400);\n      color:#111;\n    }\n\n    .usaf-btn.green{\n      border-color:#23644e;\n      background:#15382e;\n      color:#96f0cf;\n    }\n\n    .usaf-btn.danger{\n      border-color:#632a33;\n      background:#29171c;\n      color:#ffb6bd;\n    }\n\n    .usaf-btn:disabled{\n      opacity:.4;\n      cursor:not-allowed;\n    }\n\n    .usaf-content{\n      padding:15px;\n    }\n\n    .usaf-section{\n      display:none;\n    }\n\n    .usaf-section.active{\n      display:block;\n    }\n\n    .usaf-panel{\n      border:1px solid var(--line);\n      border-radius:19px;\n      background:linear-gradient(180deg,#111923,#0b1118);\n      overflow:hidden;\n    }\n\n    .usaf-panel-head{\n      padding:14px 15px 11px;\n      border-bottom:1px solid var(--line);\n    }\n\n    .usaf-panel-body{\n      padding:14px;\n    }\n\n    .usaf-title{\n      font-size:17px;\n      font-weight:950;\n    }\n\n    .usaf-sub{\n      margin-top:4px;\n      color:var(--muted);\n      font-size:10px;\n      line-height:1.55;\n    }\n\n    .usaf-label{\n      display:block;\n      margin:10px 0 6px;\n      color:#d7dde3;\n      font-size:9px;\n      font-weight:900;\n      letter-spacing:.06em;\n      text-transform:uppercase;\n    }\n\n    .usaf-input,\n    .usaf-select,\n    .usaf-textarea{\n      width:100%;\n      min-height:43px;\n      padding:9px 10px;\n      border:1px solid var(--line);\n      border-radius:11px;\n      background:#080d13;\n      color:#fff;\n      outline:none;\n    }\n\n    .usaf-textarea{\n      min-height:82px;\n      resize:vertical;\n    }\n\n    .usaf-range{\n      width:100%;\n      accent-color:var(--yellow);\n    }\n\n    .usaf-row{\n      display:flex;\n      align-items:center;\n      gap:8px;\n      flex-wrap:wrap;\n    }\n\n    .usaf-two{\n      display:grid;\n      grid-template-columns:1fr 1fr;\n      gap:8px;\n    }\n\n    .usaf-three{\n      display:grid;\n      grid-template-columns:1fr 1fr 1fr;\n      gap:8px;\n    }\n\n    .usaf-note{\n      padding:10px;\n      border:1px solid rgba(255,212,0,.23);\n      border-radius:11px;\n      background:rgba(255,212,0,.055);\n      color:#ffe778;\n      font-size:10px;\n      line-height:1.5;\n    }\n\n    .usaf-note.good{\n      border-color:rgba(34,217,154,.28);\n      background:rgba(34,217,154,.06);\n      color:#90efcd;\n    }\n\n    .usaf-note.error{\n      border-color:rgba(255,112,124,.3);\n      background:rgba(255,112,124,.07);\n      color:#ffb5bd;\n    }\n\n    .usaf-chipbar{\n      display:flex;\n      flex-wrap:wrap;\n      gap:5px;\n    }\n\n    .usaf-chip{\n      padding:7px 9px;\n      border:1px solid var(--line);\n      border-radius:999px;\n      background:#121a24;\n      color:#d9dfe5;\n      font-size:9px;\n      font-weight:900;\n      cursor:pointer;\n    }\n\n    .usaf-chip.active{\n      border-color:#927900;\n      background:rgba(255,212,0,.07);\n      color:var(--yellow);\n    }\n\n    \/* LIBRARY *\/\n\n    .usaf-library-layout{\n      display:grid;\n      grid-template-columns:300px 1fr;\n      gap:14px;\n    }\n\n    .usaf-gallery{\n      display:grid;\n      grid-template-columns:repeat(4,minmax(0,1fr));\n      gap:11px;\n    }\n\n    .usaf-card{\n      position:relative;\n      overflow:hidden;\n      border:1px solid var(--line);\n      border-radius:15px;\n      background:#090e14;\n    }\n\n    .usaf-card.selected{\n      border:2px solid var(--yellow);\n    }\n\n    .usaf-card.selected:after{\n      content:\"SELECTED\";\n      position:absolute;\n      top:7px;\n      right:7px;\n      z-index:4;\n      padding:5px 7px;\n      border-radius:999px;\n      background:var(--yellow);\n      color:#111;\n      font-size:8px;\n      font-weight:900;\n    }\n\n    .usaf-thumb{\n      position:relative;\n      aspect-ratio:1\/1;\n      overflow:hidden;\n      background:#05080b;\n    }\n\n    .usaf-thumb img{\n      width:100%;\n      height:100%;\n      display:block;\n      object-fit:cover;\n      user-select:none;\n      -webkit-user-drag:none;\n    }\n\n    .usaf-cat{\n      position:absolute;\n      left:7px;\n      top:7px;\n      padding:5px 7px;\n      border-radius:999px;\n      background:rgba(0,0,0,.72);\n      color:#fff;\n      font-size:8px;\n      font-weight:900;\n    }\n\n    .usaf-card-body{\n      padding:9px;\n    }\n\n    .usaf-card-name{\n      display:block;\n      overflow:hidden;\n      white-space:nowrap;\n      text-overflow:ellipsis;\n      font-size:10px;\n      font-weight:900;\n    }\n\n    .usaf-card-meta{\n      min-height:22px;\n      margin-top:4px;\n      color:var(--muted);\n      font-size:8px;\n    }\n\n    .usaf-card-actions{\n      display:grid;\n      grid-template-columns:1fr 1fr;\n      gap:5px;\n      margin-top:7px;\n    }\n\n    .usaf-card-actions .usaf-btn{\n      min-height:34px;\n      padding:4px;\n      font-size:8px;\n    }\n\n    .usaf-pages{\n      display:flex;\n      justify-content:center;\n      gap:5px;\n      flex-wrap:wrap;\n      margin-top:13px;\n    }\n\n    .usaf-page{\n      width:36px;\n      height:36px;\n      border:1px solid var(--line);\n      border-radius:9px;\n      background:#121a24;\n      color:#fff;\n      cursor:pointer;\n    }\n\n    .usaf-page.active{\n      background:var(--yellow);\n      color:#111;\n      font-weight:900;\n    }\n\n    \/* PERSONALISATION *\/\n\n    .usaf-studio-layout{\n      display:grid;\n      grid-template-columns:minmax(0,1fr) 360px;\n      gap:14px;\n    }\n\n    .usaf-stack{\n      display:grid;\n      gap:11px;\n      align-content:start;\n    }\n\n    .usaf-stage,\n    .usaf-half-preview{\n      min-height:680px;\n      display:grid;\n      place-items:center;\n      overflow:auto;\n      padding:13px;\n      border-radius:16px;\n      background:\n        linear-gradient(45deg,#ddd 25%,transparent 25%),\n        linear-gradient(-45deg,#ddd 25%,transparent 25%),\n        linear-gradient(45deg,transparent 75%,#ddd 75%),\n        linear-gradient(-45deg,transparent 75%,#ddd 75%),\n        #f5f6f7;\n      background-size:24px 24px;\n      background-position:0 0,0 12px,12px -12px,-12px 0;\n    }\n\n    #usafMockCanvas,\n    #usafHalfCanvas{\n      max-width:100%;\n      height:auto;\n      display:block;\n      touch-action:none;\n    }\n\n    .usaf-product{\n      overflow:hidden;\n      border:1px solid var(--line);\n      border-radius:13px;\n      background:#080d13;\n    }\n\n    .usaf-product-top{\n      display:grid;\n      grid-template-columns:70px 1fr;\n      gap:9px;\n      padding:9px;\n      align-items:center;\n    }\n\n    .usaf-product-img{\n      width:70px;\n      height:70px;\n      object-fit:contain;\n      border-radius:9px;\n      background:#fff;\n    }\n\n    .usaf-price{\n      margin-top:4px;\n      color:var(--yellow);\n      font-size:18px;\n      font-weight:950;\n    }\n\n    .usaf-layer,\n    .usaf-gang-piece{\n      padding:8px;\n      margin-bottom:6px;\n      border:1px solid var(--line);\n      border-radius:10px;\n      background:#080d13;\n      font-size:9px;\n      cursor:pointer;\n    }\n\n    .usaf-layer.active{\n      border-color:#927900;\n      color:var(--yellow);\n    }\n\n    \/* HALFTONE *\/\n\n    .usaf-half-layout{\n      display:grid;\n      grid-template-columns:340px 1fr;\n      gap:14px;\n    }\n\n    \/* GANG *\/\n\n    .usaf-gang-layout{\n      display:grid;\n      grid-template-columns:300px minmax(0,1fr) 340px;\n      gap:13px;\n    }\n\n    .usaf-gang-workspace{\n      min-height:740px;\n      width:100%;\n      max-width:100%;\n      overflow:auto;\n      padding:13px;\n      border-radius:16px;\n      background:#05080c;\n    }\n\n    .usaf-gang-inner{\n      width:max-content;\n      margin:0 auto;\n    }\n\n    .usaf-width-badge{\n      width:max-content;\n      margin:0 auto 7px;\n      padding:5px 9px;\n      border-radius:999px;\n      background:var(--yellow);\n      color:#111;\n      font-size:9px;\n      font-weight:950;\n    }\n\n    #usafGangCanvas{\n      display:block;\n      max-width:none;\n      height:auto;\n      border:1px solid #46525e;\n      box-shadow:0 22px 55px rgba(0,0,0,.45);\n      touch-action:none;\n    }\n\n    .usaf-price-card{\n      padding:13px;\n      border:1px solid rgba(255,212,0,.23);\n      border-radius:13px;\n      background:rgba(255,212,0,.045);\n    }\n\n    .usaf-price-grid{\n      display:grid;\n      grid-template-columns:1fr auto;\n      gap:7px;\n      font-size:10px;\n    }\n\n    .usaf-price-grid span{\n      color:var(--muted);\n    }\n\n    .usaf-quality{\n      margin-top:8px;\n      padding:8px;\n      border-radius:9px;\n      font-size:9px;\n      font-weight:900;\n    }\n\n    .usaf-quality.good{\n      background:rgba(34,217,154,.08);\n      color:#91efcd;\n    }\n\n    .usaf-quality.warn{\n      background:rgba(255,212,0,.08);\n      color:#ffe676;\n    }\n\n    .usaf-quality.bad{\n      background:rgba(255,112,124,.08);\n      color:#ffb3bc;\n    }\n\n    \/* CAPTCHA *\/\n\n    #usaf-pro-studio .usaf-captcha-overlay{\n      position:fixed;\n      inset:0;\n      z-index:999999;\n      display:grid;\n      place-items:center;\n      padding:20px;\n      background:rgba(0,0,0,.86);\n      backdrop-filter:blur(8px);\n      -webkit-backdrop-filter:blur(8px);\n    }\n\n    #usaf-pro-studio .usaf-captcha-box{\n      width:min(430px,100%);\n      padding:22px;\n      border:1px solid var(--line);\n      border-radius:20px;\n      background:linear-gradient(180deg,#111923,#080d13);\n      box-shadow:0 30px 90px rgba(0,0,0,.6);\n    }\n\n    #usaf-pro-studio .usaf-captcha-icon{\n      width:48px;\n      height:48px;\n      display:grid;\n      place-items:center;\n      margin-bottom:13px;\n      border-radius:14px;\n      background:rgba(255,212,0,.09);\n      color:var(--yellow);\n      font-size:23px;\n      font-weight:900;\n    }\n\n    #usaf-pro-studio .usaf-captcha-widget{\n      min-height:72px;\n      display:flex;\n      align-items:center;\n      justify-content:center;\n      margin-top:15px;\n    }\n\n    \/* RESPONSIVE *\/\n\n    @media(max-width:1200px){\n      .usaf-library-layout,\n      .usaf-studio-layout,\n      .usaf-half-layout{\n        grid-template-columns:1fr;\n      }\n\n      .usaf-gang-layout{\n        grid-template-columns:280px 1fr;\n      }\n\n      .usaf-gang-right{\n        grid-column:1\/-1;\n        display:grid;\n        grid-template-columns:1fr 1fr;\n        gap:11px;\n      }\n\n      .usaf-gallery{\n        grid-template-columns:repeat(3,minmax(0,1fr));\n      }\n    }\n\n    @media(max-width:720px){\n      #usaf-pro-studio{\n        margin:0;\n        border-radius:0;\n        border-left:0;\n        border-right:0;\n      }\n\n      .usaf-appbar{\n        min-height:auto;\n        padding:11px;\n      }\n\n      .usaf-logo{\n        width:44px;\n        height:44px;\n        border-radius:13px;\n        font-size:21px;\n      }\n\n      .usaf-brand strong{\n        font-size:15px;\n      }\n\n      .usaf-brand span{\n        font-size:7px;\n      }\n\n      .usaf-content{\n        padding:8px;\n      }\n\n      .usaf-nav{\n        padding:8px;\n        gap:5px;\n      }\n\n      .usaf-tab{\n        flex:1 1 46%;\n        min-height:39px;\n        padding:7px;\n        font-size:9px;\n      }\n\n      .usaf-library-layout,\n      .usaf-studio-layout,\n      .usaf-half-layout,\n      .usaf-gang-layout{\n        display:block!important;\n        width:100%!important;\n      }\n\n      .usaf-stack,\n      .usaf-gang-right{\n        width:100%!important;\n        display:block!important;\n      }\n\n      .usaf-stack .usaf-panel,\n      .usaf-gang-right .usaf-panel{\n        margin-bottom:9px;\n      }\n\n      .usaf-gallery{\n        grid-template-columns:repeat(2,minmax(0,1fr))!important;\n        gap:6px!important;\n      }\n\n      .usaf-stage,\n      .usaf-half-preview{\n        min-height:410px;\n        padding:7px;\n      }\n\n      .usaf-gang-workspace{\n        width:100%!important;\n        max-width:100%!important;\n        min-height:430px!important;\n        padding:4px!important;\n      }\n\n      .usaf-gang-inner{\n        width:100%!important;\n        max-width:100%!important;\n        min-width:0!important;\n        margin:0 auto!important;\n      }\n\n      #usafGangCanvas{\n        display:block!important;\n        width:100%!important;\n        max-width:100%!important;\n        height:auto!important;\n        transform:none!important;\n        margin:0 auto!important;\n      }\n\n      .usaf-two,\n      .usaf-three{\n        grid-template-columns:1fr;\n      }\n    }\n  \u003c\/style\u003e\n\n\n  \u003c!-- SHOPIFY CUSTOMER DATA --\u003e\n\n  \u003cscript type=\"application\/json\" id=\"usafCustomerJson\"\u003e\n    {\n      \"loggedIn\": {% if customer %}true{% else %}false{% endif %},\n      \"id\": {% if customer %}{{ customer.id | json }}{% else %}\"\"{% endif %},\n      \"name\": {% if customer %}{{ customer.name | json }}{% else %}\"\"{% endif %},\n      \"email\": {% if customer %}{{ customer.email | json }}{% else %}\"\"{% endif %},\n      \"phone\": {% if customer and customer.phone %}{{ customer.phone | json }}{% else %}\"\"{% endif %}\n    }\n  \u003c\/script\u003e\n\n\n  \u003c!-- HEADER --\u003e\n\n  \u003cheader class=\"usaf-appbar\"\u003e\n\n    \u003cdiv class=\"usaf-brand\"\u003e\n\n      \u003cdiv class=\"usaf-logo\"\u003e\n        U\n      \u003c\/div\u003e\n\n      \u003cdiv\u003e\n        \u003cstrong\u003eUSASTARFASHION PRO Studio\u003c\/strong\u003e\n        \u003cspan\u003eLibrary · Personalise · Halftone · Gang Sheet\u003c\/span\u003e\n      \u003c\/div\u003e\n\n    \u003c\/div\u003e\n\n    \u003cdiv class=\"usaf-row\"\u003e\n\n      \u003cspan id=\"usafMode\" class=\"usaf-mode\"\u003e\n        Ready\n      \u003c\/span\u003e\n\n      \u003cbutton id=\"usafRefreshTop\" class=\"usaf-btn\" type=\"button\"\u003e\n        ↻ Refresh Library\n      \u003c\/button\u003e\n\n    \u003c\/div\u003e\n\n  \u003c\/header\u003e\n\n\n  \u003c!-- NAVIGATION --\u003e\n\n  \u003cnav class=\"usaf-nav\"\u003e\n\n    \u003cbutton class=\"usaf-tab active\" data-tab=\"library\" type=\"button\"\u003e\n      ▦ Library\n    \u003c\/button\u003e\n\n    \u003cbutton id=\"usafPersonaliseTab\" class=\"usaf-tab\" data-tab=\"studio\" type=\"button\"\u003e\n      ✦ Personalise\n    \u003c\/button\u003e\n\n    \u003cbutton class=\"usaf-tab\" data-tab=\"halftone\" type=\"button\"\u003e\n      ◉ Halftone\n    \u003c\/button\u003e\n\n    \u003cbutton class=\"usaf-tab\" data-tab=\"gang\" type=\"button\"\u003e\n      ▤ Gang Sheet\n    \u003c\/button\u003e\n\n  \u003c\/nav\u003e\n\n\n  \u003cdiv class=\"usaf-content\"\u003e\n\n\n    \u003c!-- LIBRARY --\u003e\n\n    \u003csection id=\"usafSection_library\" class=\"usaf-section active\"\u003e\n\n      \u003cdiv class=\"usaf-library-layout\"\u003e\n\n        \u003caside class=\"usaf-panel\"\u003e\n\n          \u003cdiv class=\"usaf-panel-head\"\u003e\n\n            \u003cdiv class=\"usaf-title\"\u003e\n              Artwork Library\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-sub\"\u003e\n              Google Drive artwork or local upload.\n              Drive connects automatically.\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n          \u003cdiv class=\"usaf-panel-body\"\u003e\n\n            \u003cinput id=\"usafSearch\" class=\"usaf-input\" placeholder=\"Search artwork...\"\u003e\n\n            \u003cdiv class=\"usaf-two\" style=\"margin-top:8px\"\u003e\n\n              \u003cbutton id=\"usafRefreshLibrary\" class=\"usaf-btn primary\" type=\"button\"\u003e\n                Refresh\n              \u003c\/button\u003e\n\n              \u003cbutton id=\"usafLibraryUploadBtn\" class=\"usaf-btn\" type=\"button\"\u003e\n                Upload\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n            \u003cinput id=\"usafLibraryUpload\" class=\"usaf-hidden\" type=\"file\" multiple accept=\"image\/png,image\/jpeg,image\/webp\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Categories\n            \u003c\/label\u003e\n\n            \u003cdiv id=\"usafFilters\" class=\"usaf-chipbar\"\u003e\u003c\/div\u003e\n\n            \u003cdiv id=\"usafLibraryStatus\" class=\"usaf-note\" style=\"margin-top:9px\"\u003e\n              Loading Google Drive…\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/aside\u003e\n\n\n        \u003cmain class=\"usaf-panel\"\u003e\n\n          \u003cdiv class=\"usaf-panel-head\"\u003e\n\n            \u003cdiv class=\"usaf-title\"\u003e\n              Design Gallery\n            \u003c\/div\u003e\n\n            \u003cdiv id=\"usafGalleryInfo\" class=\"usaf-sub\"\u003e\u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n          \u003cdiv class=\"usaf-panel-body\"\u003e\n\n            \u003cdiv id=\"usafGallery\" class=\"usaf-gallery\"\u003e\u003c\/div\u003e\n\n            \u003cdiv id=\"usafPages\" class=\"usaf-pages\"\u003e\u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/main\u003e\n\n      \u003c\/div\u003e\n\n    \u003c\/section\u003e\n\n\n    \u003c!-- PERSONALISATION --\u003e\n\n    \u003csection id=\"usafSection_studio\" class=\"usaf-section\"\u003e\n\n      \u003cdiv class=\"usaf-studio-layout\"\u003e\n\n\n        \u003cmain class=\"usaf-panel\"\u003e\n\n          \u003cdiv class=\"usaf-panel-head\"\u003e\n\n            \u003cdiv class=\"usaf-title\"\u003e\n              Personalisation Studio\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-sub\"\u003e\n              Use Shopify product image as mockup.\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n          \u003cdiv class=\"usaf-panel-body\"\u003e\n\n            \u003cdiv id=\"usafAreas\" class=\"usaf-chipbar\" style=\"margin-bottom:9px\"\u003e\u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-stage\"\u003e\n\n              \u003ccanvas id=\"usafMockCanvas\" width=\"860\" height=\"860\"\u003e\u003c\/canvas\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/main\u003e\n\n\n        \u003caside class=\"usaf-stack\"\u003e\n\n\n          \u003c!-- SHOPIFY PRODUCT --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Shopify Product\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv id=\"usafProductStatus\" class=\"usaf-note\"\u003e\n                Detecting product…\n              \u003c\/div\u003e\n\n              \u003cdiv id=\"usafProductBox\" class=\"usaf-product usaf-hidden\" style=\"margin-top:8px\"\u003e\n\n                \u003cdiv class=\"usaf-product-top\"\u003e\n\n                  \u003cimg id=\"usafProductImage\" class=\"usaf-product-img\" alt=\"\"\u003e\n\n                  \u003cdiv\u003e\n\n                    \u003cstrong id=\"usafProductTitle\"\u003e\u003c\/strong\u003e\n\n                    \u003cdiv id=\"usafProductPrice\" class=\"usaf-price\"\u003e\u003c\/div\u003e\n\n                    \u003cdiv id=\"usafProductStock\" style=\"color:var(--muted);font-size:9px\"\u003e\u003c\/div\u003e\n\n                  \u003c\/div\u003e\n\n                \u003c\/div\u003e\n\n                \u003cdiv id=\"usafVariantOptions\" style=\"padding:0 9px 9px\"\u003e\u003c\/div\u003e\n\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- PRODUCT \/ METHOD --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Product \/ Method\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv class=\"usaf-two\"\u003e\n\n                \u003cbutton class=\"usaf-btn primary\" data-product=\"tshirt\" type=\"button\"\u003e\n                  T-Shirt\n                \u003c\/button\u003e\n\n                \u003cbutton class=\"usaf-btn\" data-product=\"cap\" type=\"button\"\u003e\n                  Cap\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Print Method\n              \u003c\/label\u003e\n\n              \u003cdiv class=\"usaf-two\"\u003e\n\n                \u003cbutton class=\"usaf-btn primary\" data-method=\"dtf\" type=\"button\"\u003e\n                  DTF\n                \u003c\/button\u003e\n\n                \u003cbutton class=\"usaf-btn\" data-method=\"embroidery\" type=\"button\"\u003e\n                  Embroidery\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Mockup Colour\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafMockColour\" class=\"usaf-input\" type=\"color\" value=\"#f2f2f2\" style=\"padding:4px\"\u003e\n\n              \u003clabel style=\"display:flex;gap:8px;margin-top:9px;font-size:10px\"\u003e\n\n                \u003cinput id=\"usafUseVariant\" type=\"checkbox\" checked\u003e\n\n                Use Shopify variant image\n\n              \u003c\/label\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- ARTWORK --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Artwork\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv id=\"usafSelectedDesign\" class=\"usaf-note\"\u003e\n                No Library design selected.\n              \u003c\/div\u003e\n\n              \u003cdiv class=\"usaf-two\" style=\"margin-top:7px\"\u003e\n\n                \u003cbutton id=\"usafAddLibraryMock\" class=\"usaf-btn primary\" type=\"button\"\u003e\n                  Add Selected\n                \u003c\/button\u003e\n\n                \u003cbutton id=\"usafLibraryHalf\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Halftone\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Customer Artwork\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafCustomerUpload\" class=\"usaf-input\" type=\"file\" accept=\"image\/png,image\/jpeg,image\/webp\"\u003e\n\n              \u003cdiv class=\"usaf-two\" style=\"margin-top:7px\"\u003e\n\n                \u003cbutton id=\"usafAddCustomer\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Add\n                \u003c\/button\u003e\n\n                \u003cbutton id=\"usafCustomerHalf\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Halftone\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- TEXT --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Text\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cinput id=\"usafText\" class=\"usaf-input\" placeholder=\"Your text\"\u003e\n\n              \u003cdiv class=\"usaf-two\" style=\"margin-top:7px\"\u003e\n\n                \u003cselect id=\"usafFont\" class=\"usaf-select\"\u003e\n\n                  \u003coption value=\"Arial\"\u003eArial\u003c\/option\u003e\n                  \u003coption value=\"Impact\"\u003eImpact\u003c\/option\u003e\n                  \u003coption value=\"'Anton', Impact\"\u003eAnton\u003c\/option\u003e\n                  \u003coption value=\"'Bebas Neue', Impact\"\u003eBebas Neue\u003c\/option\u003e\n                  \u003coption value=\"'Black Ops One', Impact\"\u003eBlack Ops One\u003c\/option\u003e\n                  \u003coption value=\"'Bungee', Impact\"\u003eBungee\u003c\/option\u003e\n                  \u003coption value=\"'Oswald', Arial\"\u003eOswald\u003c\/option\u003e\n                  \u003coption value=\"'Permanent Marker', cursive\"\u003ePermanent Marker\u003c\/option\u003e\n                  \u003coption value=\"'Pacifico', cursive\"\u003ePacifico\u003c\/option\u003e\n                  \u003coption value=\"'Lobster', cursive\"\u003eLobster\u003c\/option\u003e\n                  \u003coption value=\"Georgia\"\u003eGeorgia\u003c\/option\u003e\n                  \u003coption value=\"Verdana\"\u003eVerdana\u003c\/option\u003e\n                  \u003coption value=\"'Courier New'\"\u003eCourier New\u003c\/option\u003e\n\n                \u003c\/select\u003e\n\n                \u003cinput id=\"usafTextSize\" class=\"usaf-input\" type=\"number\" value=\"56\" min=\"8\" max=\"300\"\u003e\n\n              \u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Text Style\n              \u003c\/label\u003e\n\n              \u003cselect id=\"usafTextStyle\" class=\"usaf-select\"\u003e\n\n                \u003coption value=\"normal\"\u003eNormal\u003c\/option\u003e\n                \u003coption value=\"sticker\"\u003eDTF Sticker\u003c\/option\u003e\n                \u003coption value=\"shadow\"\u003eShadow\u003c\/option\u003e\n                \u003coption value=\"glow\"\u003eGlow\u003c\/option\u003e\n                \u003coption value=\"retro\"\u003eRetro\u003c\/option\u003e\n                \u003coption value=\"stitch\"\u003eEmbroidery Stitch\u003c\/option\u003e\n\n              \u003c\/select\u003e\n\n              \u003cdiv class=\"usaf-two\" style=\"margin-top:7px\"\u003e\n\n                \u003cinput id=\"usafTextColour\" class=\"usaf-input\" type=\"color\" value=\"#000000\" style=\"padding:4px\"\u003e\n\n                \u003cinput id=\"usafOutlineColour\" class=\"usaf-input\" type=\"color\" value=\"#ffffff\" style=\"padding:4px\"\u003e\n\n              \u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Outline\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafOutline\" class=\"usaf-range\" type=\"range\" min=\"0\" max=\"24\" value=\"0\"\u003e\n\n              \u003cbutton id=\"usafAddText\" class=\"usaf-btn primary\" type=\"button\" style=\"width:100%;margin-top:7px\"\u003e\n                + Add Text\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- LAYERS --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Layers\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv id=\"usafLayers\"\u003e\u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Size\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafLayerSize\" class=\"usaf-range\" type=\"range\" min=\"20\" max=\"700\" value=\"220\"\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Rotation\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafLayerRotation\" class=\"usaf-range\" type=\"range\" min=\"-180\" max=\"180\" value=\"0\"\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Opacity\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafLayerOpacity\" class=\"usaf-range\" type=\"range\" min=\"0.1\" max=\"1\" step=\"0.05\" value=\"1\"\u003e\n\n              \u003cdiv class=\"usaf-three\"\u003e\n\n                \u003cbutton id=\"usafCenterLayer\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Center\n                \u003c\/button\u003e\n\n                \u003cbutton id=\"usafDuplicateLayer\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Copy\n                \u003c\/button\u003e\n\n                \u003cbutton id=\"usafDeleteLayer\" class=\"usaf-btn danger\" type=\"button\"\u003e\n                  Delete\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n              \u003cbutton id=\"usafMockToGang\" class=\"usaf-btn primary\" type=\"button\" style=\"width:100%;margin-top:7px\"\u003e\n                + Add Print Area to Gang\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- CHECKOUT --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Product Checkout\n              \u003c\/div\u003e\n\n              \u003cdiv class=\"usaf-sub\"\u003e\n                For personalised products only.\n                Gang Sheet is download only.\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Quantity\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafMockQty\" class=\"usaf-input\" type=\"number\" min=\"1\" value=\"1\"\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Customer Note\n              \u003c\/label\u003e\n\n              \u003ctextarea id=\"usafCustomerNote\" class=\"usaf-textarea\" placeholder=\"Example: Front center DTF, white logo.\"\u003e\u003c\/textarea\u003e\n\n              \u003cdiv class=\"usaf-two\" style=\"margin-top:9px\"\u003e\n\n                \u003cbutton id=\"usafMockAddCart\" class=\"usaf-btn green\" type=\"button\"\u003e\n                  Add to Cart\n                \u003c\/button\u003e\n\n                \u003cbutton id=\"usafMockBuyNow\" class=\"usaf-btn primary\" type=\"button\"\u003e\n                  Buy Now\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n              \u003cdiv id=\"usafMockCartStatus\" class=\"usaf-note\" style=\"margin-top:8px\"\u003e\n                Custom design will be saved with this product.\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/aside\u003e\n\n      \u003c\/div\u003e\n\n    \u003c\/section\u003e\n\n\n    \u003c!-- HALFTONE --\u003e\n\n    \u003csection id=\"usafSection_halftone\" class=\"usaf-section\"\u003e\n\n      \u003cdiv class=\"usaf-half-layout\"\u003e\n\n        \u003caside class=\"usaf-panel\"\u003e\n\n          \u003cdiv class=\"usaf-panel-head\"\u003e\n\n            \u003cdiv class=\"usaf-title\"\u003e\n              Halftone Studio\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n          \u003cdiv class=\"usaf-panel-body\"\u003e\n\n            \u003cinput id=\"usafHalfUpload\" class=\"usaf-input\" type=\"file\" accept=\"image\/png,image\/jpeg,image\/webp\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Output Width — inches\n            \u003c\/label\u003e\n\n            \u003cinput id=\"usafHalfWidth\" class=\"usaf-input\" type=\"number\" min=\"1\" max=\"22\" step=\".1\" value=\"11\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Dot Shape\n            \u003c\/label\u003e\n\n            \u003cselect id=\"usafHalfShape\" class=\"usaf-select\"\u003e\n              \u003coption value=\"circle\"\u003eRound\u003c\/option\u003e\n              \u003coption value=\"square\"\u003eSquare\u003c\/option\u003e\n              \u003coption value=\"diamond\"\u003eDiamond\u003c\/option\u003e\n            \u003c\/select\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Dot Size\n            \u003c\/label\u003e\n\n            \u003cinput id=\"usafHalfCell\" class=\"usaf-range\" type=\"range\" min=\"4\" max=\"40\" value=\"12\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Strength\n            \u003c\/label\u003e\n\n            \u003cinput id=\"usafHalfStrength\" class=\"usaf-range\" type=\"range\" min=\"20\" max=\"160\" value=\"100\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Brightness\n            \u003c\/label\u003e\n\n            \u003cinput id=\"usafHalfBrightness\" class=\"usaf-range\" type=\"range\" min=\"-100\" max=\"100\" value=\"0\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Contrast\n            \u003c\/label\u003e\n\n            \u003cinput id=\"usafHalfContrast\" class=\"usaf-range\" type=\"range\" min=\"-100\" max=\"100\" value=\"10\"\u003e\n\n            \u003clabel class=\"usaf-label\"\u003e\n              Dot Colour\n            \u003c\/label\u003e\n\n            \u003cinput id=\"usafHalfColour\" class=\"usaf-input\" type=\"color\" value=\"#000000\" style=\"padding:4px\"\u003e\n\n            \u003cdiv class=\"usaf-two\" style=\"margin-top:8px\"\u003e\n\n              \u003cbutton id=\"usafApplyHalf\" class=\"usaf-btn primary\" type=\"button\"\u003e\n                Apply\n              \u003c\/button\u003e\n\n              \u003cbutton id=\"usafResetHalf\" class=\"usaf-btn\" type=\"button\"\u003e\n                Reset\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-two\" style=\"margin-top:7px\"\u003e\n\n              \u003cbutton id=\"usafHalfToMock\" class=\"usaf-btn\" type=\"button\"\u003e\n                To Mockup\n              \u003c\/button\u003e\n\n              \u003cbutton id=\"usafHalfToGang\" class=\"usaf-btn\" type=\"button\"\u003e\n                To Gang\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n            \u003cbutton id=\"usafDownloadHalf\" class=\"usaf-btn\" type=\"button\" style=\"width:100%;margin-top:7px\"\u003e\n              Download PNG\n            \u003c\/button\u003e\n\n            \u003cdiv id=\"usafHalfStatus\" class=\"usaf-note\" style=\"margin-top:8px\"\u003e\n              Select or upload artwork.\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/aside\u003e\n\n\n        \u003cmain class=\"usaf-panel\"\u003e\n\n          \u003cdiv class=\"usaf-panel-head\"\u003e\n\n            \u003cdiv class=\"usaf-title\"\u003e\n              Halftone Preview\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n          \u003cdiv class=\"usaf-panel-body\"\u003e\n\n            \u003cdiv class=\"usaf-half-preview\"\u003e\n\n              \u003ccanvas id=\"usafHalfCanvas\" width=\"1000\" height=\"700\"\u003e\u003c\/canvas\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/main\u003e\n\n      \u003c\/div\u003e\n\n    \u003c\/section\u003e\n\n\n    \u003c!-- GANG SHEET --\u003e\n\n    \u003csection id=\"usafSection_gang\" class=\"usaf-section\"\u003e\n\n      \u003cdiv class=\"usaf-gang-layout\"\u003e\n\n        \u003caside class=\"usaf-stack\"\u003e\n\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Gang Artwork\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cbutton id=\"usafGangUploadBtn\" class=\"usaf-btn primary\" type=\"button\" style=\"width:100%\"\u003e\n                + Upload Artwork\n              \u003c\/button\u003e\n\n              \u003cinput id=\"usafGangUpload\" class=\"usaf-hidden\" type=\"file\" multiple accept=\"image\/png,image\/jpeg,image\/webp\"\u003e\n\n              \u003cbutton id=\"usafGangLibrary\" class=\"usaf-btn\" type=\"button\" style=\"width:100%;margin-top:7px\"\u003e\n                + Selected Library Design\n              \u003c\/button\u003e\n\n              \u003cdiv id=\"usafGangPieces\" style=\"margin-top:9px\"\u003e\u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Sheet Settings\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Film Width\n              \u003c\/label\u003e\n\n              \u003cinput class=\"usaf-input\" value=\"56 cm\" readonly\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Spacing — cm\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafGangSpacing\" class=\"usaf-input\" type=\"number\" min=\"0\" max=\"5\" step=\".1\" value=\".3\"\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Background\n              \u003c\/label\u003e\n\n              \u003cselect id=\"usafGangBackground\" class=\"usaf-select\"\u003e\n                \u003coption value=\"transparent\"\u003eTransparent\u003c\/option\u003e\n                \u003coption value=\"white\"\u003eWhite\u003c\/option\u003e\n                \u003coption value=\"black\"\u003eBlack\u003c\/option\u003e\n              \u003c\/select\u003e\n\n              \u003clabel style=\"display:flex;gap:8px;margin-top:9px;font-size:10px\"\u003e\n\n                \u003cinput id=\"usafGangSnap\" type=\"checkbox\" checked\u003e\n\n                Snap movement to 0.1 cm\n\n              \u003c\/label\u003e\n\n              \u003cbutton id=\"usafAutoNest\" class=\"usaf-btn primary\" type=\"button\" style=\"width:100%;margin-top:9px\"\u003e\n                Smart Auto Nest\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/aside\u003e\n\n\n        \u003cmain class=\"usaf-panel\"\u003e\n\n          \u003cdiv class=\"usaf-panel-head\"\u003e\n\n            \u003cdiv class=\"usaf-title\"\u003e\n              Gang Sheet Workspace\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-sub\"\u003e\n              Drag artwork anywhere on the 56 cm sheet.\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n          \u003cdiv class=\"usaf-panel-body\"\u003e\n\n            \u003cdiv class=\"usaf-gang-workspace\"\u003e\n\n              \u003cdiv class=\"usaf-gang-inner\"\u003e\n\n                \u003cdiv class=\"usaf-width-badge\"\u003e\n                  56 CM PRINT WIDTH\n                \u003c\/div\u003e\n\n                \u003ccanvas id=\"usafGangCanvas\" width=\"672\" height=\"1200\"\u003e\u003c\/canvas\u003e\n\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/main\u003e\n\n\n        \u003caside class=\"usaf-stack usaf-gang-right\"\u003e\n\n\n          \u003c!-- SELECTED --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Selected Artwork\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv id=\"usafGangSelected\" class=\"usaf-note\"\u003e\n                Tap artwork on the sheet.\n              \u003c\/div\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Width — cm\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafGangSelectedWidth\" class=\"usaf-input\" type=\"number\" min=\"1\" max=\"56\" step=\".1\" disabled\u003e\n\n              \u003clabel class=\"usaf-label\"\u003e\n                Quick Resize\n              \u003c\/label\u003e\n\n              \u003cinput id=\"usafGangWidthRange\" class=\"usaf-range\" type=\"range\" min=\"1\" max=\"56\" step=\".1\" disabled\u003e\n\n              \u003cdiv id=\"usafGangQuality\" class=\"usaf-quality good\"\u003e\n                Resolution appears here.\n              \u003c\/div\u003e\n\n              \u003cdiv class=\"usaf-two\" style=\"margin-top:7px\"\u003e\n\n                \u003cbutton id=\"usafGangRotate\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Rotate 90°\n                \u003c\/button\u003e\n\n                \u003cbutton id=\"usafGangDuplicate\" class=\"usaf-btn\" type=\"button\"\u003e\n                  Duplicate\n                \u003c\/button\u003e\n\n              \u003c\/div\u003e\n\n              \u003cbutton id=\"usafGangDelete\" class=\"usaf-btn danger\" type=\"button\" style=\"width:100%;margin-top:7px\"\u003e\n                Delete\n              \u003c\/button\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- DOWNLOAD ACCESS --\u003e\n\n          \u003cdiv id=\"usafDownloadAccessPanel\" class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Download Access\n              \u003c\/div\u003e\n\n              \u003cdiv class=\"usaf-sub\"\u003e\n                Login, complete the form, approve,\n                verify, then download.\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv id=\"usafGangLoginStatus\" class=\"usaf-note\"\u003e\n                Checking Shopify login…\n              \u003c\/div\u003e\n\n              \u003ca id=\"usafGangLoginLink\" class=\"usaf-btn primary usaf-hidden\" href=\"\/account\/login\" style=\"width:100%;display:block;text-align:center;margin-top:8px\"\u003e\n                Login To Download\n              \u003c\/a\u003e\n\n              \u003cdiv id=\"usafGangDownloadForm\" class=\"usaf-hidden\"\u003e\n\n                \u003clabel class=\"usaf-label\"\u003e\n                  Full Name\n                \u003c\/label\u003e\n\n                \u003cinput id=\"usafGangCustomerName\" class=\"usaf-input\" type=\"text\" placeholder=\"Customer full name\"\u003e\n\n                \u003clabel class=\"usaf-label\"\u003e\n                  Email\n                \u003c\/label\u003e\n\n                \u003cinput id=\"usafGangCustomerEmail\" class=\"usaf-input\" type=\"email\" placeholder=\"customer@email.com\"\u003e\n\n                \u003clabel class=\"usaf-label\"\u003e\n                  Phone\n                \u003c\/label\u003e\n\n                \u003cinput id=\"usafGangCustomerPhone\" class=\"usaf-input\" type=\"tel\" placeholder=\"Phone number\"\u003e\n\n                \u003clabel class=\"usaf-label\"\u003e\n                  Message \/ Notes\n                \u003c\/label\u003e\n\n                \u003ctextarea id=\"usafGangCustomerMessage\" class=\"usaf-textarea\" placeholder=\"Optional notes about this gang sheet\"\u003e\u003c\/textarea\u003e\n\n                \u003cdiv class=\"usaf-price-card\" style=\"margin-top:9px\"\u003e\n\n                  \u003cdiv class=\"usaf-price-grid\"\u003e\n\n                    \u003cspan\u003eFilm Width\u003c\/span\u003e\n                    \u003cstrong\u003e56 cm\u003c\/strong\u003e\n\n                    \u003cspan\u003eArtwork Length\u003c\/span\u003e\n                    \u003cstrong id=\"usafGangActual\"\u003e0 cm\u003c\/strong\u003e\n\n                    \u003cspan\u003eDownload Length\u003c\/span\u003e\n                    \u003cstrong id=\"usafGangCharged\"\u003e100 cm\u003c\/strong\u003e\n\n                    \u003cspan\u003eArtwork Count\u003c\/span\u003e\n                    \u003cstrong id=\"usafGangCount\"\u003e0\u003c\/strong\u003e\n\n                  \u003c\/div\u003e\n\n                  \u003cdiv class=\"usaf-hidden\"\u003e\n\n                    \u003cstrong id=\"usafGangUnitPrice\"\u003e\n                      Download only\n                    \u003c\/strong\u003e\n\n                    \u003cstrong id=\"usafGangUnits\"\u003e\n                      10\n                    \u003c\/strong\u003e\n\n                    \u003cstrong id=\"usafGangPrice\"\u003e\n                      Download only\n                    \u003c\/strong\u003e\n\n                  \u003c\/div\u003e\n\n                \u003c\/div\u003e\n\n                \u003clabel style=\"display:flex;gap:8px;margin-top:10px;font-size:10px;line-height:1.5\"\u003e\n\n                  \u003cinput id=\"usafGangTerms\" type=\"checkbox\"\u003e\n\n                  I confirm this file is ready and I approve\n                  the layout for download.\n\n                \u003c\/label\u003e\n\n                \u003cdiv style=\"margin-top:9px\"\u003e\n\n                  \u003cbutton id=\"usafGangProduction\" class=\"usaf-btn primary\" type=\"button\" style=\"width:100%\" disabled\u003e\n                    Download 300 DPI PNG\n                  \u003c\/button\u003e\n\n                \u003c\/div\u003e\n\n                \u003cdiv id=\"usafGangCartStatus\" class=\"usaf-note\" style=\"margin-top:8px\"\u003e\n                  Add artwork, fill the form,\n                  and approve the layout.\n                \u003c\/div\u003e\n\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n\n          \u003c!-- EXPORT INFO --\u003e\n\n          \u003cdiv class=\"usaf-panel\"\u003e\n\n            \u003cdiv class=\"usaf-panel-head\"\u003e\n\n              \u003cdiv class=\"usaf-title\"\u003e\n                Export Info\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n            \u003cdiv class=\"usaf-panel-body\"\u003e\n\n              \u003cdiv id=\"usafGangExportStatus\" class=\"usaf-note\"\u003e\n                Production width is exactly 56 cm at 300 DPI.\n              \u003c\/div\u003e\n\n            \u003c\/div\u003e\n\n          \u003c\/div\u003e\n\n        \u003c\/aside\u003e\n\n      \u003c\/div\u003e\n\n    \u003c\/section\u003e\n\n  \u003c\/div\u003e\n\n\n  \u003c!-- CAPTCHA --\u003e\n\n  \u003cdiv id=\"usafCaptchaOverlay\" class=\"usaf-captcha-overlay usaf-hidden\"\u003e\n\n    \u003cdiv class=\"usaf-captcha-box\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"usafCaptchaTitle\"\u003e\n\n      \u003cdiv class=\"usaf-captcha-icon\"\u003e\n        ✓\n      \u003c\/div\u003e\n\n      \u003cdiv id=\"usafCaptchaTitle\" class=\"usaf-title\"\u003e\n        Security Check\n      \u003c\/div\u003e\n\n      \u003cdiv class=\"usaf-sub\"\u003e\n        Complete the verification before downloading\n        your production artwork.\n      \u003c\/div\u003e\n\n      \u003cdiv id=\"usafTurnstile\" class=\"usaf-captcha-widget\"\u003e\u003c\/div\u003e\n\n      \u003cdiv id=\"usafCaptchaStatus\" class=\"usaf-note usaf-hidden\" style=\"margin-top:10px\"\u003e\u003c\/div\u003e\n\n      \u003cbutton id=\"usafCaptchaCancel\" class=\"usaf-btn\" type=\"button\" style=\"width:100%;margin-top:10px\"\u003e\n        Cancel\n      \u003c\/button\u003e\n\n    \u003c\/div\u003e\n\n  \u003c\/div\u003e\n\n\n  \u003cscript\u003e\n    (function(){\n      \"use strict\";\n\n      if (\n        window.USAF_PRO_STUDIO_DOWNLOAD_EMAIL_READY\n      ) {\n        return;\n      }\n\n      window.USAF_PRO_STUDIO_DOWNLOAD_EMAIL_READY =\n        true;\n\n\n      \/* =====================================================\n         GOOGLE CONNECTIONS\n      ====================================================== *\/\n\n      const DRIVE_API =\n        \"https:\/\/script.google.com\/macros\/s\/AKfycbwvSSSZqM3M8--3wfKTXy9mU3gYeBMvnaooIoK-IH9liZOrUSUUGRzY29QeALOO95zb\/exec\";\n\n      const GOOGLE_SHEET_LOG_URL =\n        \"https:\/\/script.google.com\/macros\/s\/AKfycbzOeWVW8NWMx942pKcrsDXpfAIDtqLGjb1KlBHqIcuXuPoGsXdoU8ASqvLZXElwkBh1vQ\/exec\";\n\n\n      \/* =====================================================\n         CAPTCHA CONFIGURATION\n\n         Replace these 2 values.\n\n         Keep your Turnstile SECRET on your Worker\/server.\n      ====================================================== *\/\n\n      const TURNSTILE_SITE_KEY =\n        \"YOUR_TURNSTILE_SITE_KEY\";\n\n      const TURNSTILE_VERIFY_URL =\n        \"https:\/\/YOUR-WORKER.workers.dev\/verify-turnstile\";\n\n      const CAPTCHA_VALID_MS =\n        10 * 60 * 1000;\n\n      let captchaVerifiedUntil =\n        0;\n\n      let captchaResolver =\n        null;\n\n      let captchaWidgetId =\n        null;\n\n      let captchaBusy =\n        false;\n\n\n      \/* =====================================================\n         GLOBAL CONSTANTS\n      ====================================================== *\/\n\n      const DPI =\n        300;\n\n      const GANG_WIDTH_CM =\n        56;\n\n      const GANG_MIN_CM =\n        100;\n\n      const GANG_STEP_CM =\n        10;\n\n      const PRINT_PX_CM =\n        DPI \/ 2.54;\n\n      const $ =\n        function(id){\n          return document.getElementById(id);\n        };\n\n\n      const mockCanvas =\n        $(\"usafMockCanvas\");\n\n      const mctx =\n        mockCanvas.getContext(\"2d\");\n\n      const halfCanvas =\n        $(\"usafHalfCanvas\");\n\n      const hctx =\n        halfCanvas.getContext(\"2d\");\n\n      const gangCanvas =\n        $(\"usafGangCanvas\");\n\n      const gctx =\n        gangCanvas.getContext(\"2d\");\n\n\n      \/* =====================================================\n         STATE\n      ====================================================== *\/\n\n      const state = {\n\n        library: [],\n        filtered: [],\n        category: \"All\",\n        search: \"\",\n        page: 1,\n        selected: null,\n\n        productType: \"tshirt\",\n        method: \"dtf\",\n        area: \"front\",\n        mockColour: \"#f2f2f2\",\n\n        scenes: {\n\n          tshirt: {\n            front: [],\n            back: [],\n            leftSleeve: [],\n            rightSleeve: [],\n            neck: []\n          },\n\n          cap: {\n            front: [],\n            leftSide: [],\n            rightSide: [],\n            back: []\n          }\n\n        },\n\n        activeLayer: null,\n        customerImage: null,\n        mockDrag: null,\n\n        product: null,\n        options: [],\n        variant: null,\n        variantImage: null,\n        variantLock: false,\n\n        gangProduct: false,\n\n        halfImage: null,\n        halfSourceCanvas: null,\n        halfResult: null,\n\n        gangPieces: [],\n        gangSelected: null,\n        gangDrag: null,\n        gangBackground: \"transparent\",\n        gangSnap: true,\n\n        customer: {\n          loggedIn: false,\n          id: \"\",\n          name: \"\",\n          email: \"\",\n          phone: \"\"\n        }\n\n      };\n\n\n      \/* =====================================================\n         HELPERS\n      ====================================================== *\/\n\n      function makeId(prefix) {\n\n        return (\n          prefix +\n          \"_\" +\n          Date.now() +\n          \"_\" +\n          Math.random()\n            .toString(36)\n            .slice(2,8)\n        );\n\n      }\n\n\n      function createDownloadId() {\n\n        return (\n          \"USAF-\" +\n          Date.now() +\n          \"-\" +\n          Math.random()\n            .toString(36)\n            .slice(2,8)\n            .toUpperCase()\n        );\n\n      }\n\n\n      function clamp(\n        value,\n        min,\n        max\n      ) {\n\n        return Math.max(\n          min,\n          Math.min(\n            max,\n            value\n          )\n        );\n\n      }\n\n\n      function esc(value) {\n\n        return String(\n          value || \"\"\n        )\n          .replace(\/\u0026\/g,\"\u0026amp;\")\n          .replace(\/\u003c\/g,\"\u0026lt;\")\n          .replace(\/\u003e\/g,\"\u0026gt;\")\n          .replace(\/\"\/g,\"\u0026quot;\");\n\n      }\n\n\n      function rootPath() {\n\n        return (\n          window.Shopify \u0026\u0026\n          Shopify.routes \u0026\u0026\n          Shopify.routes.root\n            ? Shopify.routes.root\n            : \"\/\"\n        );\n\n      }\n\n\n      function imageWidth(image) {\n\n        return (\n          image.naturalWidth ||\n          image.width ||\n          1\n        );\n\n      }\n\n\n      function imageHeight(image) {\n\n        return (\n          image.naturalHeight ||\n          image.height ||\n          1\n        );\n\n      }\n\n\n      function money(value) {\n\n        try {\n\n          return new Intl.NumberFormat(\n            \"en-US\",\n            {\n              style: \"currency\",\n              currency:\n                Shopify.currency.active ||\n                \"USD\"\n            }\n          ).format(\n            Number(value || 0)\n          );\n\n        } catch (error) {\n\n          return (\n            \"$\" +\n            Number(value || 0)\n              .toFixed(2)\n          );\n\n        }\n\n      }\n\n\n      function readFile(file) {\n\n        return new Promise(\n          function(\n            resolve,\n            reject\n          ) {\n\n            const reader =\n              new FileReader();\n\n            reader.onload =\n              function(event){\n\n                resolve(\n                  event.target.result\n                );\n\n              };\n\n            reader.onerror =\n              reject;\n\n            reader.readAsDataURL(\n              file\n            );\n\n          }\n        );\n\n      }\n\n\n      function loadImage(\n        src,\n        cors\n      ) {\n\n        return new Promise(\n          function(resolve){\n\n            const image =\n              new Image();\n\n            if (cors) {\n              image.crossOrigin =\n                \"anonymous\";\n            }\n\n            image.onload =\n              function(){\n                resolve(image);\n              };\n\n            image.onerror =\n              function(){\n                resolve(null);\n              };\n\n            image.src =\n              src;\n\n          }\n        );\n\n      }\n\n\n      function imageToCanvas(image) {\n\n        const canvas =\n          document.createElement(\n            \"canvas\"\n          );\n\n        canvas.width =\n          imageWidth(image);\n\n        canvas.height =\n          imageHeight(image);\n\n        canvas\n          .getContext(\"2d\")\n          .drawImage(\n            image,\n            0,\n            0\n          );\n\n        return canvas;\n\n      }\n\n\n      function cloneCanvas(source) {\n\n        const canvas =\n          document.createElement(\n            \"canvas\"\n          );\n\n        canvas.width =\n          source.width;\n\n        canvas.height =\n          source.height;\n\n        canvas\n          .getContext(\"2d\")\n          .drawImage(\n            source,\n            0,\n            0\n          );\n\n        return canvas;\n\n      }\n\n\n      function setStatus(\n        statusId,\n        className,\n        message\n      ) {\n\n        const status =\n          statusId\n            ? $(statusId)\n            : null;\n\n        if (!status) return;\n\n        status.className =\n          className;\n\n        status.textContent =\n          message;\n\n      }\n\n\n      function isMobileSafari() {\n\n        return (\n          \/iPhone|iPad|iPod\/i\n            .test(\n              navigator.userAgent\n            ) \u0026\u0026\n\n          \/Safari\/i\n            .test(\n              navigator.userAgent\n            ) \u0026\u0026\n\n          !\/CriOS|FxiOS|EdgiOS\/i\n            .test(\n              navigator.userAgent\n            )\n        );\n\n      }\n\n\n      function openBlobInNewTab(\n        blob,\n        statusId\n      ) {\n\n        const url =\n          URL.createObjectURL(\n            blob\n          );\n\n        const popup =\n          window.open(\n            url,\n            \"_blank\"\n          );\n\n        if (!popup) {\n\n          setStatus(\n            statusId,\n            \"usaf-note error\",\n            \"Popup blocked. Allow popups, then click download again.\"\n          );\n\n          setTimeout(\n            function(){\n              URL.revokeObjectURL(url);\n            },\n            15000\n          );\n\n          return false;\n\n        }\n\n        setStatus(\n          statusId,\n          \"usaf-note good\",\n          \"PNG opened. On mobile, hold image and choose Save to Photos or Save to Files.\"\n        );\n\n        setTimeout(\n          function(){\n            URL.revokeObjectURL(url);\n          },\n          60000\n        );\n\n        return true;\n\n      }\n\n\n      function normalDownload(\n        blob,\n        filename\n      ) {\n\n        const url =\n          URL.createObjectURL(\n            blob\n          );\n\n        const link =\n          document.createElement(\n            \"a\"\n          );\n\n        link.href =\n          url;\n\n        link.download =\n          filename;\n\n        link.rel =\n          \"noopener\";\n\n        link.style.display =\n          \"none\";\n\n        document.body\n          .appendChild(link);\n\n        link.click();\n\n        link.remove();\n\n        setTimeout(\n          function(){\n            URL.revokeObjectURL(url);\n          },\n          15000\n        );\n\n      }\n\n\n      function downloadCanvas(\n        canvas,\n        filename,\n        statusId\n      ) {\n\n        try {\n\n          canvas.toBlob(\n            function(blob){\n\n              if (!blob) {\n\n                setStatus(\n                  statusId,\n                  \"usaf-note error\",\n                  \"Could not create PNG. Try a shorter gang sheet or smaller artwork.\"\n                );\n\n                return;\n\n              }\n\n              if (\n                isMobileSafari()\n              ) {\n\n                openBlobInNewTab(\n                  blob,\n                  statusId\n                );\n\n                return;\n\n              }\n\n              normalDownload(\n                blob,\n                filename\n              );\n\n              setStatus(\n                statusId,\n                \"usaf-note good\",\n                \"Download started. If nothing appears, check browser Downloads.\"\n              );\n\n            },\n            \"image\/png\",\n            1\n          );\n\n        } catch (error) {\n\n          setStatus(\n            statusId,\n            \"usaf-note error\",\n            \"Browser blocked this export. Try Chrome or reduce sheet length.\"\n          );\n\n        }\n\n      }\n\n\n      \/* =====================================================\n         CAPTCHA\n      ====================================================== *\/\n\n      function captchaIsFresh() {\n\n        return (\n          Date.now() \u003c\n          captchaVerifiedUntil\n        );\n\n      }\n\n\n      function loadTurnstile() {\n\n        return new Promise(\n          function(\n            resolve,\n            reject\n          ) {\n\n            if (\n              window.turnstile\n            ) {\n\n              resolve();\n              return;\n\n            }\n\n            const existing =\n              document.querySelector(\n                \"script[data-usaf-turnstile]\"\n              );\n\n            if (existing) {\n\n              let checks =\n                0;\n\n              const timer =\n                setInterval(\n                  function(){\n\n                    checks++;\n\n                    if (\n                      window.turnstile\n                    ) {\n\n                      clearInterval(timer);\n                      resolve();\n                      return;\n\n                    }\n\n                    if (\n                      checks \u003e 120\n                    ) {\n\n                      clearInterval(timer);\n\n                      reject(\n                        new Error(\n                          \"Security check timed out.\"\n                        )\n                      );\n\n                    }\n\n                  },\n                  100\n                );\n\n              return;\n\n            }\n\n            const script =\n              document.createElement(\n                \"script\"\n              );\n\n            script.src =\n              \"https:\/\/challenges.cloudflare.com\/turnstile\/v0\/api.js?render=explicit\";\n\n            script.async =\n              true;\n\n            script.defer =\n              true;\n\n            script.dataset.usafTurnstile =\n              \"true\";\n\n            script.onload =\n              function(){\n\n                if (\n                  window.turnstile\n                ) {\n\n                  resolve();\n\n                } else {\n\n                  reject(\n                    new Error(\n                      \"Security check failed to initialise.\"\n                    )\n                  );\n\n                }\n\n              };\n\n            script.onerror =\n              function(){\n\n                reject(\n                  new Error(\n                    \"Security check could not load.\"\n                  )\n                );\n\n              };\n\n            document.head\n              .appendChild(script);\n\n          }\n        );\n\n      }\n\n\n      async function verifyCaptchaToken(\n        token\n      ) {\n\n        if (\n          !TURNSTILE_VERIFY_URL ||\n          TURNSTILE_VERIFY_URL.includes(\n            \"YOUR-WORKER\"\n          )\n        ) {\n\n          throw new Error(\n            \"Turnstile verification endpoint is not configured.\"\n          );\n\n        }\n\n        const response =\n          await fetch(\n            TURNSTILE_VERIFY_URL,\n            {\n              method:\n                \"POST\",\n\n              headers: {\n                \"Content-Type\":\n                  \"application\/json\"\n              },\n\n              body:\n                JSON.stringify({\n                  token:\n                    token,\n\n                  action:\n                    \"download\"\n                })\n            }\n          );\n\n        if (\n          !response.ok\n        ) {\n\n          return false;\n\n        }\n\n        const result =\n          await response.json();\n\n        return Boolean(\n          result \u0026\u0026\n          result.success\n        );\n\n      }\n\n\n      function showCaptchaStatus(\n        message,\n        isError\n      ) {\n\n        const status =\n          $(\"usafCaptchaStatus\");\n\n        status.className =\n          isError\n            ? \"usaf-note error\"\n            : \"usaf-note\";\n\n        status.textContent =\n          message;\n\n      }\n\n\n      function closeCaptcha(\n        result\n      ) {\n\n        $(\"usafCaptchaOverlay\")\n          .classList\n          .add(\n            \"usaf-hidden\"\n          );\n\n        if (\n          captchaWidgetId !== null \u0026\u0026\n          window.turnstile\n        ) {\n\n          try {\n\n            window.turnstile.remove(\n              captchaWidgetId\n            );\n\n          } catch (error) {}\n\n        }\n\n        captchaWidgetId =\n          null;\n\n        $(\"usafTurnstile\").innerHTML =\n          \"\";\n\n        $(\"usafCaptchaStatus\")\n          .classList\n          .add(\n            \"usaf-hidden\"\n          );\n\n        const resolver =\n          captchaResolver;\n\n        captchaResolver =\n          null;\n\n        captchaBusy =\n          false;\n\n        if (resolver) {\n\n          resolver(\n            Boolean(result)\n          );\n\n        }\n\n      }\n\n\n      async function requireCaptcha() {\n\n        if (\n          captchaIsFresh()\n        ) {\n\n          return true;\n\n        }\n\n        if (\n          captchaBusy\n        ) {\n\n          return false;\n\n        }\n\n        captchaBusy =\n          true;\n\n        return new Promise(\n          async function(resolve){\n\n            captchaResolver =\n              resolve;\n\n            $(\"usafCaptchaOverlay\")\n              .classList\n              .remove(\n                \"usaf-hidden\"\n              );\n\n            $(\"usafCaptchaStatus\")\n              .classList\n              .add(\n                \"usaf-hidden\"\n              );\n\n            try {\n\n              if (\n                !TURNSTILE_SITE_KEY ||\n                TURNSTILE_SITE_KEY.includes(\n                  \"YOUR_TURNSTILE\"\n                )\n              ) {\n\n                throw new Error(\n                  \"Turnstile site key is not configured.\"\n                );\n\n              }\n\n              await loadTurnstile();\n\n              captchaWidgetId =\n                window.turnstile.render(\n                  \"#usafTurnstile\",\n                  {\n\n                    sitekey:\n                      TURNSTILE_SITE_KEY,\n\n                    theme:\n                      \"dark\",\n\n                    action:\n                      \"download\",\n\n                    callback:\n                      async function(token){\n\n                        showCaptchaStatus(\n                          \"Verifying…\",\n                          false\n                        );\n\n                        try {\n\n                          const valid =\n                            await verifyCaptchaToken(\n                              token\n                            );\n\n                          if (!valid) {\n\n                            showCaptchaStatus(\n                              \"Verification failed. Please try again.\",\n                              true\n                            );\n\n                            window.turnstile.reset(\n                              captchaWidgetId\n                            );\n\n                            return;\n\n                          }\n\n                          captchaVerifiedUntil =\n                            Date.now() +\n                            CAPTCHA_VALID_MS;\n\n                          closeCaptcha(\n                            true\n                          );\n\n                        } catch (error) {\n\n                          showCaptchaStatus(\n                            error.message ||\n                            \"Verification could not be completed.\",\n                            true\n                          );\n\n                          try {\n\n                            window.turnstile.reset(\n                              captchaWidgetId\n                            );\n\n                          } catch (resetError) {}\n\n                        }\n\n                      },\n\n                    \"error-callback\":\n                      function(){\n\n                        showCaptchaStatus(\n                          \"Security check could not be completed. Please try again.\",\n                          true\n                        );\n\n                      },\n\n                    \"expired-callback\":\n                      function(){\n\n                        showCaptchaStatus(\n                          \"Verification expired. Please complete it again.\",\n                          true\n                        );\n\n                      }\n\n                  }\n                );\n\n            } catch (error) {\n\n              showCaptchaStatus(\n                error.message ||\n                \"Security check could not load.\",\n                true\n              );\n\n            }\n\n          }\n        );\n\n      }\n\n\n      $(\"usafCaptchaCancel\").onclick =\n        function(){\n\n          closeCaptcha(false);\n\n        };\n\n\n      \/* =====================================================\n         SHOPIFY CUSTOMER LOGIN\n      ====================================================== *\/\n\n      function loginUrl() {\n\n        return (\n          \"\/account\/login?return_url=\" +\n          encodeURIComponent(\n            location.pathname +\n            location.search +\n            \"#usaf-download\"\n          )\n        );\n\n      }\n\n\n      function readLiquidCustomer() {\n\n        const element =\n          $(\"usafCustomerJson\");\n\n        if (!element) return;\n\n        try {\n\n          const data =\n            JSON.parse(\n              element.textContent\n            );\n\n          if (\n            data \u0026\u0026\n            (\n              data.loggedIn ||\n              data.id\n            )\n          ) {\n\n            state.customer.loggedIn =\n              true;\n\n            state.customer.id =\n              data.id || \"\";\n\n            state.customer.name =\n              data.name || \"\";\n\n            state.customer.email =\n              data.email || \"\";\n\n            state.customer.phone =\n              data.phone || \"\";\n\n          }\n\n        } catch (error) {}\n\n      }\n\n\n      function readAnalyticsCustomer() {\n\n        try {\n\n          const id =\n            ShopifyAnalytics\n              .meta\n              .page\n              .customerId;\n\n          if (id) {\n\n            state.customer.loggedIn =\n              true;\n\n            state.customer.id =\n              id;\n\n          }\n\n        } catch (error) {}\n\n      }\n\n\n      async function verifyCustomerLogin() {\n\n        readLiquidCustomer();\n\n        readAnalyticsCustomer();\n\n        if (\n          !state.customer.loggedIn\n        ) {\n\n          try {\n\n            const response =\n              await fetch(\n                \"\/account\",\n                {\n                  credentials:\n                    \"include\"\n                }\n              );\n\n            const finalUrl =\n              response.url || \"\";\n\n            const text =\n              await response.text();\n\n            const lower =\n              text.toLowerCase();\n\n            if (\n              response.ok \u0026\u0026\n              !finalUrl.includes(\n                \"\/account\/login\"\n              ) \u0026\u0026\n              !lower.includes(\n                \"customer_login\"\n              ) \u0026\u0026\n              !lower.includes(\n                \"login to your account\"\n              )\n            ) {\n\n              state.customer.loggedIn =\n                true;\n\n            }\n\n          } catch (error) {}\n\n        }\n\n        if (\n          $(\"usafGangCustomerName\") \u0026\u0026\n          state.customer.name\n        ) {\n\n          $(\"usafGangCustomerName\").value =\n            state.customer.name;\n\n        }\n\n        if (\n          $(\"usafGangCustomerEmail\") \u0026\u0026\n          state.customer.email\n        ) {\n\n          $(\"usafGangCustomerEmail\").value =\n            state.customer.email;\n\n        }\n\n        if (\n          $(\"usafGangCustomerPhone\") \u0026\u0026\n          state.customer.phone\n        ) {\n\n          $(\"usafGangCustomerPhone\").value =\n            state.customer.phone;\n\n        }\n\n        updateGangButtons();\n\n      }\n\n\n      \/* =====================================================\n         PRODUCT MODE\n      ====================================================== *\/\n\n      function isGangText(text) {\n\n        text =\n          String(text || \"\")\n            .toLowerCase();\n\n        return (\n          text.includes(\"gang sheet\") ||\n          text.includes(\"gangsheet\") ||\n          text.includes(\"gang-sheet\") ||\n          text.includes(\"dtf gang\") ||\n          text.includes(\"gang dtf\")\n        );\n\n      }\n\n\n      function enableGangMode() {\n\n        state.gangProduct =\n          true;\n\n        $(\"usafMode\").textContent =\n          \"Gang Sheet Download\";\n\n        $(\"usafPersonaliseTab\")\n          .classList\n          .add(\n            \"usaf-hidden\"\n          );\n\n        $(\"usafSection_studio\")\n          .classList\n          .add(\n            \"usaf-hidden\"\n          );\n\n        openTab(\"gang\");\n\n      }\n\n\n      function detectGangFromPage() {\n\n        const text =\n          decodeURIComponent(\n            location.pathname\n          ) +\n          \" \" +\n          document.title;\n\n        if (\n          isGangText(text)\n        ) {\n\n          enableGangMode();\n\n        }\n\n      }\n\n\n      \/* =====================================================\n         TABS\n      ====================================================== *\/\n\n      function openTab(name) {\n\n        if (\n          name === \"studio\" \u0026\u0026\n          state.gangProduct\n        ) {\n\n          name =\n            \"gang\";\n\n        }\n\n        document\n          .querySelectorAll(\n            \"#usaf-pro-studio .usaf-section\"\n          )\n          .forEach(\n            function(section){\n\n              section.classList\n                .remove(\"active\");\n\n            }\n          );\n\n        const section =\n          $(\n            \"usafSection_\" +\n            name\n          );\n\n        if (section) {\n\n          section.classList\n            .add(\"active\");\n\n        }\n\n        document\n          .querySelectorAll(\n            \"#usaf-pro-studio [data-tab]\"\n          )\n          .forEach(\n            function(button){\n\n              button.classList.toggle(\n                \"active\",\n                button.dataset.tab ===\n                  name\n              );\n\n            }\n          );\n\n        if (\n          name === \"studio\"\n        ) {\n\n          renderAreas();\n          renderMock();\n          renderLayers();\n\n        }\n\n        if (\n          name === \"gang\"\n        ) {\n\n          renderGang();\n          updateGangButtons();\n\n        }\n\n      }\n\n\n      document\n        .querySelectorAll(\n          \"#usaf-pro-studio [data-tab]\"\n        )\n        .forEach(\n          function(button){\n\n            button.onclick =\n              function(){\n\n                openTab(\n                  button.dataset.tab\n                );\n\n              };\n\n          }\n        );\n\n\n      \/* =====================================================\n         GOOGLE DRIVE LIBRARY\n      ====================================================== *\/\n\n      function galleryPageSize() {\n\n        if (\n          innerWidth \u003c 720\n        ) {\n          return 4;\n        }\n\n        if (\n          innerWidth \u003c 1100\n        ) {\n          return 6;\n        }\n\n        return 8;\n\n      }\n\n\n      function loadLibrary() {\n\n        $(\"usafLibraryStatus\").className =\n          \"usaf-note\";\n\n        $(\"usafLibraryStatus\").textContent =\n          \"Loading Google Drive…\";\n\n        $(\"usafMode\").textContent =\n          \"Connecting Drive…\";\n\n        const callback =\n          \"USAF_LIBRARY_\" +\n          Date.now() +\n          \"_\" +\n          Math.random()\n            .toString(36)\n            .slice(2,8);\n\n        const script =\n          document.createElement(\n            \"script\"\n          );\n\n        let finished =\n          false;\n\n        const timeout =\n          setTimeout(\n            function(){\n\n              if (finished) {\n                return;\n              }\n\n              finished =\n                true;\n\n              $(\"usafLibraryStatus\").className =\n                \"usaf-note error\";\n\n              $(\"usafLibraryStatus\").textContent =\n                \"Google Drive timed out. Local artwork is still available.\";\n\n              $(\"usafMode\").textContent =\n                \"Drive Offline\";\n\n              try {\n\n                delete window[\n                  callback\n                ];\n\n              } catch (error) {}\n\n              script.remove();\n\n              renderFilters();\n              filterLibrary();\n\n            },\n            15000\n          );\n\n\n        window[callback] =\n          function(data){\n\n            if (finished) {\n              return;\n            }\n\n            finished =\n              true;\n\n            clearTimeout(\n              timeout\n            );\n\n            const items =\n              data \u0026\u0026\n              Array.isArray(\n                data.items\n              )\n                ? data.items\n                : [];\n\n            const localItems =\n              state.library.filter(\n                function(item){\n\n                  return (\n                    item.source ===\n                    \"local\"\n                  );\n\n                }\n              );\n\n            const driveItems =\n              items\n                .map(\n                  function(\n                    item,\n                    index\n                  ){\n\n                    return {\n\n                      id:\n                        item.libraryId ||\n                        item.id ||\n                        \"design_\" +\n                        index,\n\n                      name:\n                        item.name ||\n                        \"Design \" +\n                        (\n                          index +\n                          1\n                        ),\n\n                      category:\n                        item.category ||\n                        \"Other\",\n\n                      description:\n                        item.version ||\n                        item.category ||\n                        \"Artwork\",\n\n                      folder:\n                        item.folder ||\n                        item.sourceFolder ||\n                        \"\",\n\n                      folderPath:\n                        item.folderPath ||\n                        \"\",\n\n                      imageUrl:\n                        item.imageUrl ||\n                        \"\",\n\n                      source:\n                        \"drive\"\n\n                    };\n\n                  }\n                )\n                .filter(\n                  function(item){\n\n                    return Boolean(\n                      item.imageUrl\n                    );\n\n                  }\n                );\n\n            state.library =\n              driveItems.concat(\n                localItems\n              );\n\n            $(\"usafLibraryStatus\").className =\n              \"usaf-note good\";\n\n            $(\"usafLibraryStatus\").innerHTML =\n              \"Google Drive connected · \u003cstrong\u003e\" +\n              driveItems.length +\n              \"\u003c\/strong\u003e designs loaded.\";\n\n            $(\"usafMode\").textContent =\n              \"Drive Connected\";\n\n            renderFilters();\n            filterLibrary();\n\n            try {\n\n              delete window[\n                callback\n              ];\n\n            } catch (error) {}\n\n            script.remove();\n\n          };\n\n\n        script.src =\n          DRIVE_API +\n          \"?callback=\" +\n          encodeURIComponent(\n            callback\n          ) +\n          \"\u0026t=\" +\n          Date.now();\n\n        script.async =\n          true;\n\n        script.onerror =\n          function(){\n\n            if (finished) {\n              return;\n            }\n\n            finished =\n              true;\n\n            clearTimeout(\n              timeout\n            );\n\n            $(\"usafLibraryStatus\").className =\n              \"usaf-note error\";\n\n            $(\"usafLibraryStatus\").textContent =\n              \"Could not load Google Drive. Local artwork is still available.\";\n\n            $(\"usafMode\").textContent =\n              \"Drive Offline\";\n\n            renderFilters();\n            filterLibrary();\n\n            try {\n\n              delete window[\n                callback\n              ];\n\n            } catch (error) {}\n\n            script.remove();\n\n          };\n\n        document.body\n          .appendChild(\n            script\n          );\n\n      }\n\n\n      function renderFilters() {\n\n        const wrapper =\n          $(\"usafFilters\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        const categories =\n          [\"All\"]\n            .concat(\n              Array.from(\n                new Set(\n                  state.library.map(\n                    function(item){\n\n                      return item.category;\n\n                    }\n                  )\n                )\n              )\n            )\n            .sort(\n              function(a,b){\n\n                if (\n                  a === \"All\"\n                ) {\n                  return -1;\n                }\n\n                if (\n                  b === \"All\"\n                ) {\n                  return 1;\n                }\n\n                return a.localeCompare(b);\n\n              }\n            );\n\n        categories.forEach(\n          function(category){\n\n            const count =\n              category === \"All\"\n                ? state.library.length\n                : state.library\n                    .filter(\n                      function(item){\n\n                        return (\n                          item.category ===\n                          category\n                        );\n\n                      }\n                    )\n                    .length;\n\n            const button =\n              document.createElement(\n                \"button\"\n              );\n\n            button.type =\n              \"button\";\n\n            button.className =\n              \"usaf-chip\" +\n              (\n                state.category ===\n                  category\n                  ? \" active\"\n                  : \"\"\n              );\n\n            button.textContent =\n              category +\n              \" (\" +\n              count +\n              \")\";\n\n            button.onclick =\n              function(){\n\n                state.category =\n                  category;\n\n                state.page =\n                  1;\n\n                renderFilters();\n                filterLibrary();\n\n              };\n\n            wrapper.appendChild(\n              button\n            );\n\n          }\n        );\n\n      }\n\n\n      function filterLibrary() {\n\n        const search =\n          state.search\n            .trim()\n            .toLowerCase();\n\n        state.filtered =\n          state.library.filter(\n            function(item){\n\n              const categoryMatch =\n                state.category ===\n                  \"All\" ||\n                item.category ===\n                  state.category;\n\n              const haystack =\n                (\n                  item.name +\n                  \" \" +\n                  item.category +\n                  \" \" +\n                  item.description +\n                  \" \" +\n                  item.folder +\n                  \" \" +\n                  item.folderPath\n                )\n                  .toLowerCase();\n\n              return (\n                categoryMatch \u0026\u0026\n                (\n                  !search ||\n                  haystack.includes(\n                    search\n                  )\n                )\n              );\n\n            }\n          );\n\n        renderGallery();\n\n      }\n\n\n      function selectDesign(item) {\n\n        state.selected =\n          item;\n\n        $(\"usafSelectedDesign\").innerHTML =\n          \"\u003cstrong\u003e\" +\n          esc(item.name) +\n          \"\u003c\/strong\u003e\u003cbr\u003e\" +\n          esc(item.category);\n\n        renderGallery();\n\n      }\n\n\n      function renderGallery() {\n\n        const wrapper =\n          $(\"usafGallery\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        const perPage =\n          galleryPageSize();\n\n        const totalPages =\n          Math.max(\n            1,\n            Math.ceil(\n              state.filtered.length \/\n              perPage\n            )\n          );\n\n        state.page =\n          Math.min(\n            state.page,\n            totalPages\n          );\n\n        const start =\n          (\n            state.page -\n            1\n          ) *\n          perPage;\n\n        const visible =\n          state.filtered.slice(\n            start,\n            start +\n            perPage\n          );\n\n        $(\"usafGalleryInfo\").textContent =\n          state.filtered.length +\n          \" designs · Page \" +\n          state.page +\n          \" of \" +\n          totalPages;\n\n        visible.forEach(\n          function(item){\n\n            const card =\n              document.createElement(\n                \"article\"\n              );\n\n            card.className =\n              \"usaf-card\" +\n              (\n                state.selected \u0026\u0026\n                state.selected.id ===\n                  item.id\n                  ? \" selected\"\n                  : \"\"\n              );\n\n            card.innerHTML =\n              '\u003cdiv class=\"usaf-thumb\"\u003e' +\n\n                '\u003cimg src=\"' +\n                esc(item.imageUrl) +\n                '\" loading=\"lazy\" draggable=\"false\"\u003e' +\n\n                '\u003cspan class=\"usaf-cat\"\u003e' +\n                esc(item.category) +\n                '\u003c\/span\u003e' +\n\n              '\u003c\/div\u003e' +\n\n              '\u003cdiv class=\"usaf-card-body\"\u003e' +\n\n                '\u003cspan class=\"usaf-card-name\"\u003e' +\n                esc(item.name) +\n                '\u003c\/span\u003e' +\n\n                '\u003cdiv class=\"usaf-card-meta\"\u003e' +\n                esc(item.description) +\n                '\u003c\/div\u003e' +\n\n                '\u003cdiv class=\"usaf-card-actions\"\u003e' +\n\n                  '\u003cbutton data-select class=\"usaf-btn\" type=\"button\"\u003eSelect\u003c\/button\u003e' +\n\n                  '\u003cbutton data-mock class=\"usaf-btn primary\" type=\"button\"\u003eMockup\u003c\/button\u003e' +\n\n                  '\u003cbutton data-half class=\"usaf-btn\" type=\"button\"\u003eHalftone\u003c\/button\u003e' +\n\n                  '\u003cbutton data-gang class=\"usaf-btn\" type=\"button\"\u003e+ Gang\u003c\/button\u003e' +\n\n                '\u003c\/div\u003e' +\n\n              '\u003c\/div\u003e';\n\n            card\n              .querySelector(\"img\")\n              .oncontextmenu =\n              function(event){\n\n                event.preventDefault();\n\n              };\n\n            card\n              .querySelector(\n                \"[data-select]\"\n              )\n              .onclick =\n              function(){\n\n                selectDesign(item);\n\n              };\n\n            card\n              .querySelector(\n                \"[data-mock]\"\n              )\n              .onclick =\n              async function(){\n\n                selectDesign(item);\n\n                await addLibraryToMock(\n                  item\n                );\n\n                openTab(\"studio\");\n\n              };\n\n            card\n              .querySelector(\n                \"[data-half]\"\n              )\n              .onclick =\n              function(){\n\n                selectDesign(item);\n\n                loadLibraryToHalf(\n                  item\n                );\n\n                openTab(\"halftone\");\n\n              };\n\n            card\n              .querySelector(\n                \"[data-gang]\"\n              )\n              .onclick =\n              async function(){\n\n                selectDesign(item);\n\n                await addLibraryToGang(\n                  item\n                );\n\n                openTab(\"gang\");\n\n              };\n\n            wrapper.appendChild(\n              card\n            );\n\n          }\n        );\n\n        renderPages(\n          totalPages\n        );\n\n      }\n\n\n      function renderPages(total) {\n\n        const wrapper =\n          $(\"usafPages\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        if (\n          total \u003c= 1\n        ) {\n          return;\n        }\n\n        let first =\n          Math.max(\n            1,\n            state.page - 2\n          );\n\n        let last =\n          Math.min(\n            total,\n            first + 4\n          );\n\n        first =\n          Math.max(\n            1,\n            last - 4\n          );\n\n        for (\n          let index = first;\n          index \u003c= last;\n          index += 1\n        ) {\n\n          const button =\n            document.createElement(\n              \"button\"\n            );\n\n          button.type =\n            \"button\";\n\n          button.className =\n            \"usaf-page\" +\n            (\n              index ===\n                state.page\n                ? \" active\"\n                : \"\"\n            );\n\n          button.textContent =\n            index;\n\n          button.onclick =\n            function(){\n\n              state.page =\n                index;\n\n              renderGallery();\n\n            };\n\n          wrapper.appendChild(\n            button\n          );\n\n        }\n\n      }\n\n\n      $(\"usafSearch\").oninput =\n        function(){\n\n          state.search =\n            this.value;\n\n          state.page =\n            1;\n\n          filterLibrary();\n\n        };\n\n\n      $(\"usafRefreshLibrary\").onclick =\n        loadLibrary;\n\n\n      $(\"usafRefreshTop\").onclick =\n        loadLibrary;\n\n\n      $(\"usafLibraryUploadBtn\").onclick =\n        function(){\n\n          $(\"usafLibraryUpload\")\n            .click();\n\n        };\n\n\n      $(\"usafLibraryUpload\").onchange =\n        function(event){\n\n          Array.from(\n            event.target.files ||\n            []\n          )\n            .forEach(\n              function(file){\n\n                const reader =\n                  new FileReader();\n\n                reader.onload =\n                  function(loadEvent){\n\n                    state.library\n                      .unshift({\n\n                        id:\n                          makeId(\n                            \"local\"\n                          ),\n\n                        name:\n                          file.name,\n\n                        category:\n                          \"Local Uploads\",\n\n                        description:\n                          \"Local Artwork\",\n\n                        folder:\n                          \"\",\n\n                        folderPath:\n                          \"\",\n\n                        imageUrl:\n                          loadEvent.target.result,\n\n                        source:\n                          \"local\"\n\n                      });\n\n                    renderFilters();\n                    filterLibrary();\n\n                  };\n\n                reader.readAsDataURL(\n                  file\n                );\n\n              }\n            );\n\n          event.target.value =\n            \"\";\n\n        };\n\n\n      \/* =====================================================\n         SHOPIFY PRODUCT\n      ====================================================== *\/\n\n      function productHandle() {\n\n        const match =\n          location.pathname.match(\n            \/\\\/products\\\/([^\\\/?#]+)\/\n          );\n\n        return match\n          ? decodeURIComponent(\n              match[1]\n            )\n          : \"\";\n\n      }\n\n\n      function buildOptions(product) {\n\n        return (\n          product.options ||\n          []\n        )\n          .map(\n            function(\n              option,\n              index\n            ){\n\n              const name =\n                typeof option ===\n                  \"string\"\n                  ? option\n                  : option.name;\n\n              let values =\n                [];\n\n              if (\n                typeof option ===\n                  \"object\" \u0026\u0026\n                Array.isArray(\n                  option.values\n                )\n              ) {\n\n                values =\n                  option.values.slice();\n\n              } else {\n\n                values =\n                  Array.from(\n                    new Set(\n                      product.variants\n                        .map(\n                          function(variant){\n\n                            return variant[\n                              \"option\" +\n                              (\n                                index +\n                                1\n                              )\n                            ];\n\n                          }\n                        )\n                        .filter(Boolean)\n                    )\n                  );\n\n              }\n\n              return {\n                name:name,\n                values:values\n              };\n\n            }\n          );\n\n      }\n\n\n      function validVariantIds() {\n\n        if (!state.product) {\n          return new Set();\n        }\n\n        return new Set(\n          state.product.variants\n            .map(\n              function(variant){\n\n                return String(\n                  variant.id\n                );\n\n              }\n            )\n        );\n\n      }\n\n\n      function nativeForms() {\n\n        const valid =\n          validVariantIds();\n\n        return Array.from(\n          document.querySelectorAll(\n            'form[action*=\"\/cart\/add\"]'\n          )\n        )\n          .filter(\n            function(form){\n\n              if (\n                form.closest(\n                  \"#usaf-pro-studio\"\n                )\n              ) {\n                return false;\n              }\n\n              const input =\n                form.querySelector(\n                  '[name=\"id\"]'\n                );\n\n              if (\n                !input ||\n                !input.value\n              ) {\n                return false;\n              }\n\n              return valid.has(\n                String(\n                  input.value\n                )\n              );\n\n            }\n          );\n\n      }\n\n\n      function nativeVariantId() {\n\n        const forms =\n          nativeForms();\n\n        for (\n          let index = 0;\n          index \u003c forms.length;\n          index += 1\n        ) {\n\n          const input =\n            forms[index]\n              .querySelector(\n                '[name=\"id\"]'\n              );\n\n          if (\n            input \u0026\u0026\n            input.value\n          ) {\n\n            return String(\n              input.value\n            );\n\n          }\n\n        }\n\n        const query =\n          new URLSearchParams(\n            location.search\n          )\n            .get(\n              \"variant\"\n            );\n\n        if (\n          query \u0026\u0026\n          validVariantIds()\n            .has(\n              String(query)\n            )\n        ) {\n\n          return String(query);\n\n        }\n\n        return \"\";\n\n      }\n\n\n      function currentNativeVariant() {\n\n        if (!state.product) {\n          return null;\n        }\n\n        const id =\n          nativeVariantId();\n\n        if (!id) {\n          return null;\n        }\n\n        return (\n          state.product.variants\n            .find(\n              function(variant){\n\n                return (\n                  String(variant.id) ===\n                  String(id)\n                );\n\n              }\n            ) ||\n          null\n        );\n\n      }\n\n\n      function exactVariant(values) {\n\n        if (!state.product) {\n          return null;\n        }\n\n        return (\n          state.product.variants\n            .find(\n              function(variant){\n\n                return state.options\n                  .every(\n                    function(\n                      option,\n                      index\n                    ){\n\n                      return (\n                        variant[\n                          \"option\" +\n                          (\n                            index +\n                            1\n                          )\n                        ] ===\n                        values[index]\n                      );\n\n                    }\n                  );\n\n              }\n            ) ||\n          null\n        );\n\n      }\n\n\n      function nativeOptionControls(\n        optionName,\n        index\n      ) {\n\n        const controls =\n          [];\n\n        document\n          .querySelectorAll(\n            \"select,input\"\n          )\n          .forEach(\n            function(control){\n\n              if (\n                control.closest(\n                  \"#usaf-pro-studio\"\n                )\n              ) {\n                return;\n              }\n\n              if (\n                control.name ===\n                \"options[\" +\n                optionName +\n                \"]\"\n              ) {\n\n                controls.push(\n                  control\n                );\n\n              }\n\n            }\n          );\n\n        const selects =\n          Array.from(\n            document.querySelectorAll(\n              \"variant-selects select\"\n            )\n          );\n\n        if (\n          selects[index] \u0026\u0026\n          !controls.includes(\n            selects[index]\n          )\n        ) {\n\n          controls.push(\n            selects[index]\n          );\n\n        }\n\n        return controls;\n\n      }\n\n\n      function pushVariantToShopify(\n        variant\n      ) {\n\n        state.options\n          .forEach(\n            function(\n              option,\n              index\n            ){\n\n              const value =\n                variant[\n                  \"option\" +\n                  (\n                    index +\n                    1\n                  )\n                ];\n\n              nativeOptionControls(\n                option.name,\n                index\n              )\n                .forEach(\n                  function(control){\n\n                    if (\n                      control.tagName ===\n                        \"SELECT\" \u0026\u0026\n                      control.value !==\n                        value\n                    ) {\n\n                      control.value =\n                        value;\n\n                      control.dispatchEvent(\n                        new Event(\n                          \"input\",\n                          {\n                            bubbles:true\n                          }\n                        )\n                      );\n\n                      control.dispatchEvent(\n                        new Event(\n                          \"change\",\n                          {\n                            bubbles:true\n                          }\n                        )\n                      );\n\n                    }\n\n                    if (\n                      control.type ===\n                        \"radio\" \u0026\u0026\n                      control.value ===\n                        value \u0026\u0026\n                      !control.checked\n                    ) {\n\n                      control.checked =\n                        true;\n\n                      control.dispatchEvent(\n                        new Event(\n                          \"change\",\n                          {\n                            bubbles:true\n                          }\n                        )\n                      );\n\n                    }\n\n                  }\n                );\n\n            }\n          );\n\n        setTimeout(\n          function(){\n\n            nativeForms()\n              .forEach(\n                function(form){\n\n                  const input =\n                    form.querySelector(\n                      '[name=\"id\"]'\n                    );\n\n                  if (input) {\n\n                    input.value =\n                      variant.id;\n\n                    input.dispatchEvent(\n                      new Event(\n                        \"change\",\n                        {\n                          bubbles:true\n                        }\n                      )\n                    );\n\n                  }\n\n                }\n              );\n\n          },\n          120\n        );\n\n      }\n\n\n      function buildVariantSelectors() {\n\n        const wrapper =\n          $(\"usafVariantOptions\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        state.options.forEach(\n          function(\n            option,\n            index\n          ){\n\n            if (\n              option.values.length ===\n                1 \u0026\u0026\n              option.values[0] ===\n                \"Default Title\"\n            ) {\n              return;\n            }\n\n            const label =\n              document.createElement(\n                \"label\"\n              );\n\n            label.className =\n              \"usaf-label\";\n\n            label.textContent =\n              option.name;\n\n            const select =\n              document.createElement(\n                \"select\"\n              );\n\n            select.className =\n              \"usaf-select\";\n\n            select.id =\n              \"usafOption_\" +\n              index;\n\n            option.values.forEach(\n              function(value){\n\n                const item =\n                  document.createElement(\n                    \"option\"\n                  );\n\n                item.value =\n                  value;\n\n                item.textContent =\n                  value;\n\n                select.appendChild(\n                  item\n                );\n\n              }\n            );\n\n            select.onchange =\n              function(){\n\n                if (\n                  state.variantLock\n                ) {\n                  return;\n                }\n\n                const values =\n                  state.options.map(\n                    function(\n                      optionItem,\n                      optionIndex\n                    ){\n\n                      const control =\n                        $(\n                          \"usafOption_\" +\n                          optionIndex\n                        );\n\n                      return control\n                        ? control.value\n                        : \"\";\n\n                    }\n                  );\n\n                const variant =\n                  exactVariant(\n                    values\n                  );\n\n                if (variant) {\n\n                  applyVariant(\n                    variant,\n                    true\n                  );\n\n                }\n\n              };\n\n            wrapper.appendChild(\n              label\n            );\n\n            wrapper.appendChild(\n              select\n            );\n\n          }\n        );\n\n      }\n\n\n      async function applyVariant(\n        variant,\n        pushNative\n      ) {\n\n        if (!variant) return;\n\n        state.variantLock =\n          true;\n\n        state.variant =\n          variant;\n\n        state.options.forEach(\n          function(\n            option,\n            index\n          ){\n\n            const value =\n              variant[\n                \"option\" +\n                (\n                  index +\n                  1\n                )\n              ];\n\n            const select =\n              $(\n                \"usafOption_\" +\n                index\n              );\n\n            if (select) {\n              select.value =\n                value;\n            }\n\n          }\n        );\n\n        $(\"usafProductPrice\").textContent =\n          money(\n            Number(\n              variant.price ||\n              0\n            ) \/\n            100\n          );\n\n        $(\"usafProductStock\").textContent =\n          variant.available\n            ? \"Available\"\n            : \"Sold out\";\n\n        let imageUrl =\n          \"\";\n\n        if (\n          variant.featured_image\n        ) {\n\n          imageUrl =\n            typeof variant.featured_image ===\n              \"string\"\n              ? variant.featured_image\n              : variant.featured_image.src;\n\n        }\n\n        if (\n          !imageUrl \u0026\u0026\n          state.product \u0026\u0026\n          state.product.featured_image\n        ) {\n\n          imageUrl =\n            typeof state.product.featured_image ===\n              \"string\"\n              ? state.product.featured_image\n              : state.product.featured_image.src;\n\n        }\n\n        if (imageUrl) {\n\n          $(\"usafProductImage\").src =\n            imageUrl;\n\n          state.variantImage =\n            await loadImage(\n              imageUrl,\n              true\n            );\n\n          if (\n            !state.variantImage\n          ) {\n\n            state.variantImage =\n              await loadImage(\n                imageUrl,\n                false\n              );\n\n          }\n\n        }\n\n        if (pushNative) {\n\n          pushVariantToShopify(\n            variant\n          );\n\n        }\n\n        renderMock();\n\n        setTimeout(\n          function(){\n\n            state.variantLock =\n              false;\n\n          },\n          250\n        );\n\n      }\n\n\n      async function loadShopifyProduct() {\n\n        const handle =\n          productHandle();\n\n        if (!handle) {\n\n          $(\"usafProductStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafProductStatus\").textContent =\n            \"Open on a Shopify product page to use product mockup and checkout.\";\n\n          return;\n\n        }\n\n        try {\n\n          const response =\n            await fetch(\n              rootPath() +\n              \"products\/\" +\n              encodeURIComponent(\n                handle\n              ) +\n              \".js\",\n              {\n                cache:\"no-store\"\n              }\n            );\n\n          if (!response.ok) {\n\n            throw new Error(\n              \"Could not load Shopify product.\"\n            );\n\n          }\n\n          state.product =\n            await response.json();\n\n          state.options =\n            buildOptions(\n              state.product\n            );\n\n          if (\n            isGangText(\n              state.product.title +\n              \" \" +\n              state.product.handle\n            )\n          ) {\n\n            enableGangMode();\n\n          }\n\n          $(\"usafProductStatus\").className =\n            \"usaf-note good\";\n\n          $(\"usafProductStatus\").textContent =\n            \"Shopify product detected.\";\n\n          $(\"usafProductBox\")\n            .classList\n            .remove(\n              \"usaf-hidden\"\n            );\n\n          $(\"usafProductTitle\").textContent =\n            state.product.title;\n\n          buildVariantSelectors();\n\n          let variant =\n            currentNativeVariant();\n\n          if (!variant) {\n\n            variant =\n              state.product.variants\n                .find(\n                  function(item){\n                    return item.available;\n                  }\n                ) ||\n              state.product.variants[0];\n\n          }\n\n          await applyVariant(\n            variant,\n            false\n          );\n\n        } catch (error) {\n\n          $(\"usafProductStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafProductStatus\").textContent =\n            error.message;\n\n        }\n\n      }\n\n\n      function pullShopifyVariant() {\n\n        if (\n          state.variantLock ||\n          !state.product\n        ) {\n          return;\n        }\n\n        const variant =\n          currentNativeVariant();\n\n        if (!variant) {\n          return;\n        }\n\n        if (\n          state.variant \u0026\u0026\n          String(\n            state.variant.id\n          ) ===\n          String(\n            variant.id\n          )\n        ) {\n          return;\n        }\n\n        applyVariant(\n          variant,\n          false\n        );\n\n      }\n\n\n      document.addEventListener(\n        \"change\",\n        function(event){\n\n          if (\n            event.target.closest \u0026\u0026\n            event.target.closest(\n              \"#usaf-pro-studio\"\n            )\n          ) {\n            return;\n          }\n\n          setTimeout(\n            pullShopifyVariant,\n            80\n          );\n\n        }\n      );\n\n\n      setInterval(\n        pullShopifyVariant,\n        700\n      );\n\n\n      \/* =====================================================\n         PERSONALISATION\n      ====================================================== *\/\n\n      function currentScene() {\n\n        return state.scenes[\n          state.productType\n        ][\n          state.area\n        ];\n\n      }\n\n\n      function areaList() {\n\n        if (\n          state.productType ===\n          \"cap\"\n        ) {\n\n          return [\n            [\"front\",\"Front\"],\n            [\"leftSide\",\"Left\"],\n            [\"rightSide\",\"Right\"],\n            [\"back\",\"Back\"]\n          ];\n\n        }\n\n        return [\n          [\"front\",\"Front\"],\n          [\"back\",\"Back\"],\n          [\"leftSleeve\",\"Left Sleeve\"],\n          [\"rightSleeve\",\"Right Sleeve\"],\n          [\"neck\",\"Neck\"]\n        ];\n\n      }\n\n\n      function renderAreas() {\n\n        const wrapper =\n          $(\"usafAreas\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        areaList().forEach(\n          function(area){\n\n            const button =\n              document.createElement(\n                \"button\"\n              );\n\n            button.type =\n              \"button\";\n\n            button.className =\n              \"usaf-chip\" +\n              (\n                state.area ===\n                  area[0]\n                  ? \" active\"\n                  : \"\"\n              );\n\n            button.textContent =\n              area[1];\n\n            button.onclick =\n              function(){\n\n                state.area =\n                  area[0];\n\n                state.activeLayer =\n                  null;\n\n                renderAreas();\n                renderMock();\n                renderLayers();\n\n              };\n\n            wrapper.appendChild(\n              button\n            );\n\n          }\n        );\n\n      }\n\n\n      document\n        .querySelectorAll(\n          \"#usaf-pro-studio [data-product]\"\n        )\n        .forEach(\n          function(button){\n\n            button.onclick =\n              function(){\n\n                document\n                  .querySelectorAll(\n                    \"#usaf-pro-studio [data-product]\"\n                  )\n                  .forEach(\n                    function(element){\n\n                      element.classList\n                        .remove(\n                          \"primary\"\n                        );\n\n                    }\n                  );\n\n                button.classList\n                  .add(\n                    \"primary\"\n                  );\n\n                state.productType =\n                  button.dataset.product;\n\n                state.area =\n                  \"front\";\n\n                state.activeLayer =\n                  null;\n\n                renderAreas();\n                renderMock();\n                renderLayers();\n\n              };\n\n          }\n        );\n\n\n      document\n        .querySelectorAll(\n          \"#usaf-pro-studio [data-method]\"\n        )\n        .forEach(\n          function(button){\n\n            button.onclick =\n              function(){\n\n                document\n                  .querySelectorAll(\n                    \"#usaf-pro-studio [data-method]\"\n                  )\n                  .forEach(\n                    function(element){\n\n                      element.classList\n                        .remove(\n                          \"primary\"\n                        );\n\n                    }\n                  );\n\n                button.classList\n                  .add(\n                    \"primary\"\n                  );\n\n                state.method =\n                  button.dataset.method;\n\n                if (\n                  state.method ===\n                  \"embroidery\"\n                ) {\n\n                  $(\"usafTextStyle\").value =\n                    \"stitch\";\n\n                }\n\n                renderMock();\n\n              };\n\n          }\n        );\n\n\n      function printRect() {\n\n        if (\n          state.productType ===\n          \"cap\"\n        ) {\n\n          if (\n            state.area ===\n            \"front\"\n          ) {\n\n            return {\n              x:300,\n              y:290,\n              w:260,\n              h:120\n            };\n\n          }\n\n          return {\n            x:320,\n            y:295,\n            w:210,\n            h:110\n          };\n\n        }\n\n        if (\n          state.area ===\n          \"front\"\n        ) {\n\n          return {\n            x:280,\n            y:210,\n            w:300,\n            h:360\n          };\n\n        }\n\n        if (\n          state.area ===\n          \"back\"\n        ) {\n\n          return {\n            x:270,\n            y:210,\n            w:320,\n            h:380\n          };\n\n        }\n\n        if (\n          state.area ===\n          \"neck\"\n        ) {\n\n          return {\n            x:340,\n            y:130,\n            w:180,\n            h:90\n          };\n\n        }\n\n        return {\n          x:170,\n          y:230,\n          w:180,\n          h:180\n        };\n\n      }\n\n\n      function drawGeneric() {\n\n        mctx.save();\n\n        mctx.fillStyle =\n          state.mockColour;\n\n        mctx.strokeStyle =\n          \"rgba(0,0,0,.15)\";\n\n        if (\n          state.productType ===\n          \"cap\"\n        ) {\n\n          mctx.beginPath();\n\n          mctx.ellipse(\n            430,\n            350,\n            245,\n            135,\n            0,\n            Math.PI,\n            Math.PI * 2\n          );\n\n          mctx.lineTo(\n            675,\n            420\n          );\n\n          mctx.quadraticCurveTo(\n            560,\n            485,\n            430,\n            465\n          );\n\n          mctx.quadraticCurveTo(\n            300,\n            485,\n            185,\n            420\n          );\n\n          mctx.closePath();\n          mctx.fill();\n          mctx.stroke();\n\n        } else {\n\n          mctx.beginPath();\n\n          mctx.moveTo(280,90);\n          mctx.lineTo(210,130);\n          mctx.lineTo(150,250);\n          mctx.lineTo(225,290);\n          mctx.lineTo(245,780);\n          mctx.lineTo(615,780);\n          mctx.lineTo(635,290);\n          mctx.lineTo(710,250);\n          mctx.lineTo(650,130);\n          mctx.lineTo(580,90);\n\n          mctx.quadraticCurveTo(\n            540,\n            165,\n            430,\n            165\n          );\n\n          mctx.quadraticCurveTo(\n            320,\n            165,\n            280,\n            90\n          );\n\n          mctx.closePath();\n          mctx.fill();\n          mctx.stroke();\n\n        }\n\n        mctx.restore();\n\n      }\n\n\n      function drawTextLayerMock(layer) {\n\n        mctx.save();\n\n        mctx.font =\n          \"bold \" +\n          layer.fontSize +\n          \"px \" +\n          layer.font;\n\n        mctx.textAlign =\n          \"center\";\n\n        mctx.textBaseline =\n          \"middle\";\n\n        mctx.lineJoin =\n          \"round\";\n\n        if (\n          layer.style ===\n          \"shadow\"\n        ) {\n\n          mctx.shadowColor =\n            layer.outlineColour;\n\n          mctx.shadowBlur =\n            6;\n\n          mctx.shadowOffsetX =\n            6;\n\n          mctx.shadowOffsetY =\n            6;\n\n        }\n\n        if (\n          layer.style ===\n          \"glow\"\n        ) {\n\n          mctx.shadowColor =\n            layer.outlineColour;\n\n          mctx.shadowBlur =\n            20;\n\n        }\n\n        if (\n          layer.style ===\n          \"retro\"\n        ) {\n\n          mctx.fillStyle =\n            layer.outlineColour;\n\n          for (\n            let offset = 12;\n            offset \u003e= 3;\n            offset -= 3\n          ) {\n\n            mctx.fillText(\n              layer.text,\n              offset,\n              offset\n            );\n\n          }\n\n        }\n\n        if (\n          layer.style ===\n          \"sticker\"\n        ) {\n\n          mctx.lineWidth =\n            Math.max(\n              layer.outline *\n              2 +\n              10,\n              12\n            );\n\n          mctx.strokeStyle =\n            \"#ffffff\";\n\n          mctx.strokeText(\n            layer.text,\n            0,\n            0\n          );\n\n        }\n\n        if (\n          layer.outline \u003e 0\n        ) {\n\n          mctx.lineWidth =\n            layer.outline *\n            2;\n\n          mctx.strokeStyle =\n            layer.outlineColour;\n\n          mctx.strokeText(\n            layer.text,\n            0,\n            0\n          );\n\n        }\n\n        mctx.fillStyle =\n          layer.colour;\n\n        mctx.fillText(\n          layer.text,\n          0,\n          0\n        );\n\n        mctx.restore();\n\n      }\n\n\n      function drawMockLayer(layer) {\n\n        const rect =\n          printRect();\n\n        mctx.save();\n\n        mctx.beginPath();\n\n        mctx.rect(\n          rect.x,\n          rect.y,\n          rect.w,\n          rect.h\n        );\n\n        mctx.clip();\n\n        mctx.translate(\n          rect.x +\n          layer.x,\n          rect.y +\n          layer.y\n        );\n\n        mctx.rotate(\n          layer.rotation *\n          Math.PI \/\n          180\n        );\n\n        mctx.globalAlpha =\n          layer.opacity;\n\n        if (\n          layer.type ===\n          \"image\"\n        ) {\n\n          mctx.drawImage(\n            layer.image,\n            -layer.w \/ 2,\n            -layer.h \/ 2,\n            layer.w,\n            layer.h\n          );\n\n        } else {\n\n          drawTextLayerMock(\n            layer\n          );\n\n        }\n\n        mctx.restore();\n\n      }\n\n\n      function renderMock() {\n\n        if (\n          state.gangProduct\n        ) {\n          return;\n        }\n\n        mctx.clearRect(\n          0,\n          0,\n          860,\n          860\n        );\n\n        mctx.fillStyle =\n          \"#e5e9ed\";\n\n        mctx.fillRect(\n          0,\n          0,\n          860,\n          860\n        );\n\n        if (\n          state.variantImage \u0026\u0026\n          $(\"usafUseVariant\").checked\n        ) {\n\n          const image =\n            state.variantImage;\n\n          const scale =\n            Math.min(\n              700 \/\n              imageWidth(image),\n              760 \/\n              imageHeight(image)\n            );\n\n          const width =\n            imageWidth(image) *\n            scale;\n\n          const height =\n            imageHeight(image) *\n            scale;\n\n          mctx.drawImage(\n            image,\n            (\n              860 -\n              width\n            ) \/\n            2,\n            (\n              860 -\n              height\n            ) \/\n            2,\n            width,\n            height\n          );\n\n        } else {\n\n          drawGeneric();\n\n        }\n\n        const rect =\n          printRect();\n\n        mctx.save();\n\n        mctx.strokeStyle =\n          state.method ===\n            \"embroidery\"\n            ? \"#9f7aea\"\n            : \"#ffd400\";\n\n        mctx.fillStyle =\n          state.method ===\n            \"embroidery\"\n            ? \"rgba(159,122,234,.08)\"\n            : \"rgba(255,212,0,.08)\";\n\n        mctx.setLineDash([\n          7,\n          7\n        ]);\n\n        mctx.fillRect(\n          rect.x,\n          rect.y,\n          rect.w,\n          rect.h\n        );\n\n        mctx.strokeRect(\n          rect.x,\n          rect.y,\n          rect.w,\n          rect.h\n        );\n\n        mctx.restore();\n\n        currentScene()\n          .forEach(\n            drawMockLayer\n          );\n\n      }\n\n\n      function addImageLayer(\n        image,\n        name,\n        designId\n      ) {\n\n        const rect =\n          printRect();\n\n        const width =\n          Math.min(\n            rect.w *\n            .8,\n            260\n          );\n\n        const ratio =\n          imageWidth(image) \/\n          imageHeight(image);\n\n        const layer = {\n\n          id:\n            makeId(\n              \"layer\"\n            ),\n\n          type:\n            \"image\",\n\n          mode:\n            state.method,\n\n          image:\n            image,\n\n          name:\n            name,\n\n          designId:\n            designId ||\n            \"\",\n\n          w:\n            width,\n\n          h:\n            width \/\n            ratio,\n\n          x:\n            rect.w \/\n            2,\n\n          y:\n            rect.h \/\n            2,\n\n          rotation:\n            0,\n\n          opacity:\n            1\n\n        };\n\n        currentScene()\n          .push(\n            layer\n          );\n\n        state.activeLayer =\n          layer.id;\n\n        renderMock();\n        renderLayers();\n\n      }\n\n\n      async function addLibraryToMock(item) {\n\n        let image =\n          await loadImage(\n            item.imageUrl,\n            true\n          );\n\n        if (!image) {\n\n          image =\n            await loadImage(\n              item.imageUrl,\n              false\n            );\n\n        }\n\n        if (!image) return;\n\n        addImageLayer(\n          image,\n          item.name,\n          item.id\n        );\n\n      }\n\n\n      $(\"usafAddLibraryMock\").onclick =\n        async function(){\n\n          if (!state.selected) {\n            return;\n          }\n\n          await addLibraryToMock(\n            state.selected\n          );\n\n        };\n\n\n      $(\"usafCustomerUpload\").onchange =\n        async function(event){\n\n          const file =\n            event.target.files \u0026\u0026\n            event.target.files[0];\n\n          if (!file) {\n            return;\n          }\n\n          state.customerImage =\n            await loadImage(\n              await readFile(file),\n              false\n            );\n\n          event.target.value =\n            \"\";\n\n        };\n\n\n      $(\"usafAddCustomer\").onclick =\n        function(){\n\n          if (\n            !state.customerImage\n          ) {\n            return;\n          }\n\n          addImageLayer(\n            state.customerImage,\n            \"Customer Artwork\",\n            \"\"\n          );\n\n        };\n\n\n      function measureTextLayer(layer) {\n\n        mctx.save();\n\n        mctx.font =\n          \"bold \" +\n          layer.fontSize +\n          \"px \" +\n          layer.font;\n\n        const width =\n          Math.max(\n            90,\n            mctx.measureText(\n              layer.text\n            ).width +\n            36\n          );\n\n        mctx.restore();\n\n        layer.w =\n          width;\n\n        layer.h =\n          layer.fontSize *\n          1.45;\n\n      }\n\n\n      $(\"usafAddText\").onclick =\n        function(){\n\n          const text =\n            $(\"usafText\")\n              .value\n              .trim();\n\n          if (!text) {\n            return;\n          }\n\n          const rect =\n            printRect();\n\n          const style =\n            state.method ===\n              \"embroidery\"\n              ? \"stitch\"\n              : $(\"usafTextStyle\").value;\n\n          const layer = {\n\n            id:\n              makeId(\"layer\"),\n\n            type:\n              \"text\",\n\n            mode:\n              state.method,\n\n            text:\n              text,\n\n            font:\n              $(\"usafFont\").value,\n\n            fontSize:\n              Number(\n                $(\"usafTextSize\").value\n              ) ||\n              56,\n\n            colour:\n              $(\"usafTextColour\").value,\n\n            outlineColour:\n              $(\"usafOutlineColour\").value,\n\n            outline:\n              Number(\n                $(\"usafOutline\").value\n              ) ||\n              0,\n\n            style:\n              style,\n\n            w:\n              220,\n\n            h:\n              80,\n\n            x:\n              rect.w \/ 2,\n\n            y:\n              rect.h \/ 2,\n\n            rotation:\n              0,\n\n            opacity:\n              1\n\n          };\n\n          measureTextLayer(\n            layer\n          );\n\n          currentScene()\n            .push(\n              layer\n            );\n\n          state.activeLayer =\n            layer.id;\n\n          renderMock();\n          renderLayers();\n\n        };\n\n\n      function activeLayer() {\n\n        return (\n          currentScene()\n            .find(\n              function(layer){\n\n                return (\n                  layer.id ===\n                  state.activeLayer\n                );\n\n              }\n            ) ||\n          null\n        );\n\n      }\n\n\n      function renderLayers() {\n\n        if (\n          state.gangProduct\n        ) {\n          return;\n        }\n\n        const wrapper =\n          $(\"usafLayers\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        if (\n          !currentScene().length\n        ) {\n\n          wrapper.innerHTML =\n            '\u003cdiv class=\"usaf-note\"\u003eNo layers.\u003c\/div\u003e';\n\n          return;\n\n        }\n\n        currentScene()\n          .forEach(\n            function(\n              layer,\n              index\n            ){\n\n              const element =\n                document.createElement(\n                  \"div\"\n                );\n\n              element.className =\n                \"usaf-layer\" +\n                (\n                  state.activeLayer ===\n                    layer.id\n                    ? \" active\"\n                    : \"\"\n                );\n\n              element.textContent =\n                (\n                  index +\n                  1\n                ) +\n                \". \" +\n                (\n                  layer.type ===\n                    \"text\"\n                    ? layer.text\n                    : layer.name\n                );\n\n              element.onclick =\n                function(){\n\n                  state.activeLayer =\n                    layer.id;\n\n                  renderLayers();\n                  renderMock();\n\n                };\n\n              wrapper.appendChild(\n                element\n              );\n\n            }\n          );\n\n        const layer =\n          activeLayer();\n\n        if (layer) {\n\n          $(\"usafLayerSize\").value =\n            Math.round(layer.w);\n\n          $(\"usafLayerRotation\").value =\n            Math.round(\n              layer.rotation\n            );\n\n          $(\"usafLayerOpacity\").value =\n            layer.opacity ||\n            1;\n\n        }\n\n      }\n\n\n      $(\"usafLayerSize\").oninput =\n        function(){\n\n          const layer =\n            activeLayer();\n\n          if (!layer) {\n            return;\n          }\n\n          const oldWidth =\n            layer.w || 1;\n\n          const ratio =\n            layer.h \/\n            oldWidth;\n\n          layer.w =\n            Number(\n              this.value\n            );\n\n          layer.h =\n            layer.w *\n            ratio;\n\n          if (\n            layer.type ===\n            \"text\"\n          ) {\n\n            layer.fontSize *=\n              layer.w \/\n              oldWidth;\n\n            measureTextLayer(\n              layer\n            );\n\n          }\n\n          renderMock();\n\n        };\n\n\n      $(\"usafLayerRotation\").oninput =\n        function(){\n\n          const layer =\n            activeLayer();\n\n          if (!layer) {\n            return;\n          }\n\n          layer.rotation =\n            Number(\n              this.value\n            );\n\n          renderMock();\n\n        };\n\n\n      $(\"usafLayerOpacity\").oninput =\n        function(){\n\n          const layer =\n            activeLayer();\n\n          if (!layer) {\n            return;\n          }\n\n          layer.opacity =\n            Number(\n              this.value\n            );\n\n          renderMock();\n\n        };\n\n\n      $(\"usafCenterLayer\").onclick =\n        function(){\n\n          const layer =\n            activeLayer();\n\n          if (!layer) {\n            return;\n          }\n\n          const rect =\n            printRect();\n\n          layer.x =\n            rect.w \/ 2;\n\n          layer.y =\n            rect.h \/ 2;\n\n          renderMock();\n\n        };\n\n\n      $(\"usafDuplicateLayer\").onclick =\n        function(){\n\n          const layer =\n            activeLayer();\n\n          if (!layer) {\n            return;\n          }\n\n          const copy =\n            Object.assign(\n              {},\n              layer,\n              {\n                id:\n                  makeId(\n                    \"layer\"\n                  ),\n\n                x:\n                  layer.x +\n                  15,\n\n                y:\n                  layer.y +\n                  15\n              }\n            );\n\n          currentScene()\n            .push(\n              copy\n            );\n\n          state.activeLayer =\n            copy.id;\n\n          renderMock();\n          renderLayers();\n\n        };\n\n\n      $(\"usafDeleteLayer\").onclick =\n        function(){\n\n          const index =\n            currentScene()\n              .findIndex(\n                function(layer){\n\n                  return (\n                    layer.id ===\n                    state.activeLayer\n                  );\n\n                }\n              );\n\n          if (\n            index \u003c 0\n          ) {\n            return;\n          }\n\n          currentScene()\n            .splice(\n              index,\n              1\n            );\n\n          state.activeLayer =\n            null;\n\n          renderMock();\n          renderLayers();\n\n        };\n\n\n      $(\"usafMockColour\").oninput =\n        function(){\n\n          state.mockColour =\n            this.value;\n\n          renderMock();\n\n        };\n\n\n      $(\"usafUseVariant\").onchange =\n        renderMock;\n\n\n      \/* =====================================================\n         CHECKOUT\n      ====================================================== *\/\n\n      function mockupLayerSummary() {\n\n        const all =\n          [];\n\n        Object.keys(\n          state.scenes[\n            state.productType\n          ]\n        )\n          .forEach(\n            function(area){\n\n              state.scenes[\n                state.productType\n              ][\n                area\n              ]\n                .forEach(\n                  function(\n                    layer,\n                    index\n                  ){\n\n                    all.push(\n                      [\n                        \"Area: \" +\n                        area,\n\n                        \"Layer: \" +\n                        (\n                          index +\n                          1\n                        ),\n\n                        \"Type: \" +\n                        layer.type,\n\n                        \"Method: \" +\n                        (\n                          layer.mode ===\n                            \"embroidery\"\n                            ? \"Embroidery\"\n                            : \"DTF\"\n                        ),\n\n                        \"Name\/Text: \" +\n                        (\n                          layer.type ===\n                            \"text\"\n                            ? layer.text\n                            : layer.name\n                        ),\n\n                        \"Width: \" +\n                        Math.round(\n                          layer.w\n                        ),\n\n                        \"Height: \" +\n                        Math.round(\n                          layer.h\n                        )\n\n                      ].join(\n                        \" | \"\n                      )\n                    );\n\n                  }\n                );\n\n            }\n          );\n\n        return all.length\n          ? all.join(\"\\n\")\n          : \"No custom layers added.\";\n\n      }\n\n\n      function mockupOrderProperties() {\n\n        const summary =\n          mockupLayerSummary();\n\n        return {\n\n          \"Custom Design\":\n            \"Yes\",\n\n          \"Design Type\":\n            \"Personalised Product\",\n\n          \"Print Method\":\n            state.method ===\n              \"embroidery\"\n              ? \"Embroidery\"\n              : \"DTF\",\n\n          \"Product Type\":\n            state.productType,\n\n          \"Selected Area\":\n            state.area,\n\n          \"Variant\":\n            state.variant\n              ? state.variant.title\n              : \"\",\n\n          \"Variant ID\":\n            state.variant\n              ? String(\n                  state.variant.id\n                )\n              : \"\",\n\n          \"Customer Note\":\n            $(\"usafCustomerNote\").value ||\n            \"\",\n\n          \"Design Layers\":\n            summary.substring(\n              0,\n              900\n            )\n\n        };\n\n      }\n\n\n      async function addMockupToCart(\n        checkout\n      ) {\n\n        if (\n          !state.product ||\n          !state.variant\n        ) {\n\n          $(\"usafMockCartStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafMockCartStatus\").textContent =\n            \"Shopify product or variant was not detected.\";\n\n          return;\n\n        }\n\n        if (\n          !state.variant.available\n        ) {\n\n          $(\"usafMockCartStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafMockCartStatus\").textContent =\n            \"This variant is sold out.\";\n\n          return;\n\n        }\n\n        const quantity =\n          Math.max(\n            1,\n            Number(\n              $(\"usafMockQty\").value ||\n              1\n            )\n          );\n\n        $(\"usafMockCartStatus\").className =\n          \"usaf-note\";\n\n        $(\"usafMockCartStatus\").textContent =\n          \"Adding custom product to cart…\";\n\n        try {\n\n          const response =\n            await fetch(\n              rootPath() +\n              \"cart\/add.js\",\n              {\n                method:\"POST\",\n\n                headers:{\n                  \"Content-Type\":\n                    \"application\/json\",\n\n                  \"Accept\":\n                    \"application\/json\"\n                },\n\n                body:\n                  JSON.stringify({\n                    items:[\n                      {\n                        id:\n                          state.variant.id,\n\n                        quantity:\n                          quantity,\n\n                        properties:\n                          mockupOrderProperties()\n                      }\n                    ]\n                  })\n              }\n            );\n\n          const result =\n            await response.json();\n\n          if (\n            !response.ok\n          ) {\n\n            throw new Error(\n              result.description ||\n              result.message ||\n              \"Could not add custom product to cart.\"\n            );\n\n          }\n\n          $(\"usafMockCartStatus\").className =\n            \"usaf-note good\";\n\n          $(\"usafMockCartStatus\").textContent =\n            \"✓ Custom product added.\";\n\n          window.location.href =\n            checkout\n              ? rootPath() +\n                \"checkout\"\n              : rootPath() +\n                \"cart\";\n\n        } catch (error) {\n\n          $(\"usafMockCartStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafMockCartStatus\").textContent =\n            error.message;\n\n        }\n\n      }\n\n\n      $(\"usafMockAddCart\").onclick =\n        function(){\n\n          addMockupToCart(\n            false\n          );\n\n        };\n\n\n      $(\"usafMockBuyNow\").onclick =\n        function(){\n\n          addMockupToCart(\n            true\n          );\n\n        };\n\n\n      \/* =====================================================\n         HALFTONE\n      ====================================================== *\/\n\n      async function loadLibraryToHalf(item) {\n\n        state.halfResult =\n          null;\n\n        $(\"usafHalfStatus\").className =\n          \"usaf-note\";\n\n        $(\"usafHalfStatus\").textContent =\n          \"Loading artwork…\";\n\n        let image =\n          await loadImage(\n            item.imageUrl,\n            true\n          );\n\n        if (!image) {\n\n          image =\n            await loadImage(\n              item.imageUrl,\n              false\n            );\n\n        }\n\n        if (!image) {\n\n          $(\"usafHalfStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafHalfStatus\").textContent =\n            \"Could not load artwork.\";\n\n          return;\n\n        }\n\n        state.halfImage =\n          image;\n\n        try {\n\n          state.halfSourceCanvas =\n            imageToCanvas(\n              image\n            );\n\n          state.halfSourceCanvas\n            .getContext(\n              \"2d\",\n              {\n                willReadFrequently:true\n              }\n            )\n            .getImageData(\n              0,\n              0,\n              1,\n              1\n            );\n\n          drawHalfSourcePreview();\n\n          $(\"usafHalfStatus\").className =\n            \"usaf-note good\";\n\n          $(\"usafHalfStatus\").textContent =\n            item.name +\n            \" loaded.\";\n\n        } catch (error) {\n\n          state.halfSourceCanvas =\n            null;\n\n          drawHalfImagePreview(\n            image\n          );\n\n          $(\"usafHalfStatus\").className =\n            \"usaf-note error\";\n\n          $(\"usafHalfStatus\").textContent =\n            \"Drive image cannot be processed because of image CORS. Upload the artwork locally.\";\n\n        }\n\n      }\n\n\n      $(\"usafLibraryHalf\").onclick =\n        function(){\n\n          if (!state.selected) {\n            return;\n          }\n\n          loadLibraryToHalf(\n            state.selected\n          );\n\n          openTab(\n            \"halftone\"\n          );\n\n        };\n\n\n      $(\"usafCustomerHalf\").onclick =\n        function(){\n\n          if (\n            !state.customerImage\n          ) {\n            return;\n          }\n\n          state.halfImage =\n            state.customerImage;\n\n          state.halfSourceCanvas =\n            imageToCanvas(\n              state.customerImage\n            );\n\n          state.halfResult =\n            null;\n\n          drawHalfSourcePreview();\n\n          openTab(\n            \"halftone\"\n          );\n\n        };\n\n\n      $(\"usafHalfUpload\").onchange =\n        async function(event){\n\n          const file =\n            event.target.files \u0026\u0026\n            event.target.files[0];\n\n          if (!file) {\n            return;\n          }\n\n          const image =\n            await loadImage(\n              await readFile(file),\n              false\n            );\n\n          if (!image) {\n            return;\n          }\n\n          state.halfImage =\n            image;\n\n          state.halfSourceCanvas =\n            imageToCanvas(\n              image\n            );\n\n          state.halfResult =\n            null;\n\n          drawHalfSourcePreview();\n\n          event.target.value =\n            \"\";\n\n        };\n\n\n      function drawHalfImagePreview(image) {\n\n        halfCanvas.width =\n          Math.min(\n            1000,\n            imageWidth(image)\n          );\n\n        halfCanvas.height =\n          Math.round(\n            halfCanvas.width *\n            imageHeight(image) \/\n            imageWidth(image)\n          );\n\n        hctx.clearRect(\n          0,\n          0,\n          halfCanvas.width,\n          halfCanvas.height\n        );\n\n        hctx.drawImage(\n          image,\n          0,\n          0,\n          halfCanvas.width,\n          halfCanvas.height\n        );\n\n      }\n\n\n      function drawHalfSourcePreview() {\n\n        if (\n          !state.halfSourceCanvas\n        ) {\n          return;\n        }\n\n        const source =\n          state.halfSourceCanvas;\n\n        halfCanvas.width =\n          Math.min(\n            1000,\n            source.width\n          );\n\n        halfCanvas.height =\n          Math.round(\n            halfCanvas.width *\n            source.height \/\n            source.width\n          );\n\n        hctx.clearRect(\n          0,\n          0,\n          halfCanvas.width,\n          halfCanvas.height\n        );\n\n        hctx.drawImage(\n          source,\n          0,\n          0,\n          halfCanvas.width,\n          halfCanvas.height\n        );\n\n      }\n\n\n      function halftoneLuma(\n        r,\n        g,\n        b,\n        brightness,\n        contrast\n      ) {\n\n        let value =\n          .2126 * r +\n          .7152 * g +\n          .0722 * b;\n\n        value +=\n          brightness *\n          2.55;\n\n        const factor =\n          (\n            259 *\n            (\n              contrast +\n              255\n            )\n          ) \/\n          (\n            255 *\n            (\n              259 -\n              contrast\n            )\n          );\n\n        value =\n          factor *\n          (\n            value -\n            128\n          ) +\n          128;\n\n        return clamp(\n          value,\n          0,\n          255\n        );\n\n      }\n\n\n      function makeHalftone(\n        targetWidth\n      ) {\n\n        if (\n          !state.halfSourceCanvas\n        ) {\n          return null;\n        }\n\n        const source =\n          state.halfSourceCanvas;\n\n        const ratio =\n          source.height \/\n          source.width;\n\n        const width =\n          Math.max(\n            100,\n            Math.min(\n              6600,\n              Math.round(\n                targetWidth\n              )\n            )\n          );\n\n        const height =\n          Math.max(\n            100,\n            Math.round(\n              width *\n              ratio\n            )\n          );\n\n        const sample =\n          document.createElement(\n            \"canvas\"\n          );\n\n        sample.width =\n          width;\n\n        sample.height =\n          height;\n\n        const sctx =\n          sample.getContext(\n            \"2d\",\n            {\n              willReadFrequently:true\n            }\n          );\n\n        sctx.drawImage(\n          source,\n          0,\n          0,\n          width,\n          height\n        );\n\n        let pixels;\n\n        try {\n\n          pixels =\n            sctx\n              .getImageData(\n                0,\n                0,\n                width,\n                height\n              )\n              .data;\n\n        } catch (error) {\n\n          return null;\n\n        }\n\n        const output =\n          document.createElement(\n            \"canvas\"\n          );\n\n        output.width =\n          width;\n\n        output.height =\n          height;\n\n        const out =\n          output.getContext(\n            \"2d\"\n          );\n\n        const cell =\n          Number(\n            $(\"usafHalfCell\").value\n          ) ||\n          12;\n\n        const strength =\n          (\n            Number(\n              $(\"usafHalfStrength\").value\n            ) ||\n            100\n          ) \/\n          100;\n\n        const brightness =\n          Number(\n            $(\"usafHalfBrightness\").value\n          ) ||\n          0;\n\n        const contrast =\n          Number(\n            $(\"usafHalfContrast\").value\n          ) ||\n          0;\n\n        const shape =\n          $(\"usafHalfShape\").value;\n\n        out.clearRect(\n          0,\n          0,\n          width,\n          height\n        );\n\n        out.fillStyle =\n          $(\"usafHalfColour\").value;\n\n        for (\n          let y = cell \/ 2;\n          y \u003c height;\n          y += cell\n        ) {\n\n          for (\n            let x = cell \/ 2;\n            x \u003c width;\n            x += cell\n          ) {\n\n            const px =\n              Math.min(\n                width - 1,\n                Math.floor(x)\n              );\n\n            const py =\n              Math.min(\n                height - 1,\n                Math.floor(y)\n              );\n\n            const index =\n              (\n                py *\n                width +\n                px\n              ) *\n              4;\n\n            const alpha =\n              pixels[\n                index +\n                3\n              ] \/\n              255;\n\n            if (\n              alpha \u003c= .01\n            ) {\n              continue;\n            }\n\n            const luma =\n              halftoneLuma(\n                pixels[index],\n                pixels[index + 1],\n                pixels[index + 2],\n                brightness,\n                contrast\n              );\n\n            const darkness =\n              (\n                1 -\n                luma \/\n                255\n              ) *\n              alpha;\n\n            const dot =\n              Math.min(\n                cell *\n                1.25,\n\n                cell *\n                Math.sqrt(\n                  darkness\n                ) *\n                strength\n              );\n\n            if (\n              dot \u003c= .3\n            ) {\n              continue;\n            }\n\n            if (\n              shape ===\n              \"square\"\n            ) {\n\n              out.fillRect(\n                x -\n                dot \/ 2,\n                y -\n                dot \/ 2,\n                dot,\n                dot\n              );\n\n            } else if (\n              shape ===\n              \"diamond\"\n            ) {\n\n              out.save();\n\n              out.translate(\n                x,\n                y\n              );\n\n              out.rotate(\n                Math.PI \/ 4\n              );\n\n              out.fillRect(\n                -dot \/ 2,\n                -dot \/ 2,\n                dot,\n                dot\n              );\n\n              out.restore();\n\n            } else {\n\n              out.beginPath();\n\n              out.arc(\n                x,\n                y,\n                dot \/ 2,\n                0,\n                Math.PI * 2\n              );\n\n              out.fill();\n\n            }\n\n          }\n\n        }\n\n        return output;\n\n      }\n\n\n      $(\"usafApplyHalf\").onclick =\n        function(){\n\n          if (\n            !state.halfSourceCanvas\n          ) {\n\n            $(\"usafHalfStatus\").className =\n              \"usaf-note error\";\n\n            $(\"usafHalfStatus\").textContent =\n              \"Upload the original image locally if processing is blocked.\";\n\n            return;\n\n          }\n\n          state.halfResult =\n            makeHalftone(\n              (\n                Number(\n                  $(\"usafHalfWidth\").value\n                ) ||\n                11\n              ) *\n              DPI\n            );\n\n          if (\n            !state.halfResult\n          ) {\n            return;\n          }\n\n          halfCanvas.width =\n            state.halfResult.width;\n\n          halfCanvas.height =\n            state.halfResult.height;\n\n          hctx.clearRect(\n            0,\n            0,\n            halfCanvas.width,\n            halfCanvas.height\n          );\n\n          hctx.drawImage(\n            state.halfResult,\n            0,\n            0\n          );\n\n          $(\"usafHalfStatus\").className =\n            \"usaf-note good\";\n\n          $(\"usafHalfStatus\").textContent =\n            \"Halftone created.\";\n\n        };\n\n\n      $(\"usafResetHalf\").onclick =\n        function(){\n\n          state.halfResult =\n            null;\n\n          drawHalfSourcePreview();\n\n        };\n\n\n      \/* CAPTCHA BEFORE HALFTONE DOWNLOAD *\/\n\n      $(\"usafDownloadHalf\").onclick =\n        async function(){\n\n          if (\n            !state.halfResult\n          ) {\n\n            $(\"usafHalfStatus\").className =\n              \"usaf-note error\";\n\n            $(\"usafHalfStatus\").textContent =\n              \"Create the halftone before downloading.\";\n\n            return;\n\n          }\n\n          const verified =\n            await requireCaptcha();\n\n          if (!verified) {\n            return;\n          }\n\n          downloadCanvas(\n            state.halfResult,\n            \"USASTARFASHION-halftone.png\",\n            \"usafHalfStatus\"\n          );\n\n        };\n\n\n      $(\"usafHalfToMock\").onclick =\n        async function(){\n\n          if (\n            state.gangProduct ||\n            !state.halfResult\n          ) {\n            return;\n          }\n\n          const image =\n            await loadImage(\n              state.halfResult.toDataURL(\n                \"image\/png\"\n              ),\n              false\n            );\n\n          addImageLayer(\n            image,\n            \"Halftone Artwork\",\n            state.selected\n              ? state.selected.id\n              : \"\"\n          );\n\n          openTab(\n            \"studio\"\n          );\n\n        };\n\n\n      \/* =====================================================\n         GANG SHEET\n      ====================================================== *\/\n\n      function rotatedWidth(piece) {\n\n        const angle =\n          (\n            piece.rotation %\n              360 +\n            360\n          ) %\n          360;\n\n        return (\n          angle === 90 ||\n          angle === 270\n            ? piece.hCm\n            : piece.wCm\n        );\n\n      }\n\n\n      function rotatedHeight(piece) {\n\n        const angle =\n          (\n            piece.rotation %\n              360 +\n            360\n          ) %\n          360;\n\n        return (\n          angle === 90 ||\n          angle === 270\n            ? piece.wCm\n            : piece.hCm\n        );\n\n      }\n\n\n      function addGangPiece(\n        image,\n        name,\n        widthCm,\n        designId\n      ) {\n\n        const ratio =\n          imageWidth(image) \/\n          imageHeight(image);\n\n        const width =\n          clamp(\n            widthCm,\n            1,\n            GANG_WIDTH_CM\n          );\n\n        state.gangPieces\n          .push({\n\n            id:\n              makeId(\n                \"gang\"\n              ),\n\n            image:\n              image,\n\n            name:\n              name,\n\n            designId:\n              designId ||\n              \"\",\n\n            sourceWidth:\n              imageWidth(image),\n\n            sourceHeight:\n              imageHeight(image),\n\n            wCm:\n              width,\n\n            hCm:\n              width \/\n              ratio,\n\n            rotation:\n              0,\n\n            xCm:\n              0,\n\n            yCm:\n              0\n\n          });\n\n        renderGangPieces();\n        autoNest();\n        updateGangButtons();\n\n      }\n\n\n      async function addLibraryToGang(item) {\n\n        let image =\n          await loadImage(\n            item.imageUrl,\n            true\n          );\n\n        if (!image) {\n\n          image =\n            await loadImage(\n              item.imageUrl,\n              false\n            );\n\n        }\n\n        if (!image) {\n          return;\n        }\n\n        addGangPiece(\n          image,\n          item.name,\n          20,\n          item.id\n        );\n\n      }\n\n\n      $(\"usafGangLibrary\").onclick =\n        async function(){\n\n          if (!state.selected) {\n            return;\n          }\n\n          await addLibraryToGang(\n            state.selected\n          );\n\n        };\n\n\n      $(\"usafGangUploadBtn\").onclick =\n        function(){\n\n          $(\"usafGangUpload\")\n            .click();\n\n        };\n\n\n      $(\"usafGangUpload\").onchange =\n        async function(event){\n\n          const files =\n            Array.from(\n              event.target.files ||\n              []\n            );\n\n          for (\n            const file of files\n          ) {\n\n            const image =\n              await loadImage(\n                await readFile(file),\n                false\n              );\n\n            if (image) {\n\n              addGangPiece(\n                image,\n                file.name,\n                15,\n                \"\"\n              );\n\n            }\n\n          }\n\n          event.target.value =\n            \"\";\n\n        };\n\n\n      $(\"usafHalfToGang\").onclick =\n        function(){\n\n          if (\n            !state.halfResult\n          ) {\n            return;\n          }\n\n          addGangPiece(\n            cloneCanvas(\n              state.halfResult\n            ),\n            \"Halftone Artwork\",\n            Math.min(\n              GANG_WIDTH_CM,\n              (\n                Number(\n                  $(\"usafHalfWidth\").value\n                ) ||\n                11\n              ) *\n              2.54\n            ),\n            state.selected\n              ? state.selected.id\n              : \"\"\n          );\n\n          openTab(\n            \"gang\"\n          );\n\n        };\n\n\n      $(\"usafMockToGang\").onclick =\n        function(){\n\n          if (\n            state.gangProduct ||\n            !currentScene().length\n          ) {\n            return;\n          }\n\n          const rect =\n            printRect();\n\n          const output =\n            document.createElement(\n              \"canvas\"\n            );\n\n          output.width =\n            2200;\n\n          output.height =\n            Math.round(\n              output.width *\n              rect.h \/\n              rect.w\n            );\n\n          const out =\n            output.getContext(\n              \"2d\"\n            );\n\n          const sx =\n            output.width \/\n            rect.w;\n\n          const sy =\n            output.height \/\n            rect.h;\n\n          currentScene()\n            .forEach(\n              function(layer){\n\n                out.save();\n\n                out.translate(\n                  layer.x * sx,\n                  layer.y * sy\n                );\n\n                out.rotate(\n                  layer.rotation *\n                  Math.PI \/\n                  180\n                );\n\n                out.globalAlpha =\n                  layer.opacity ||\n                  1;\n\n                if (\n                  layer.type ===\n                  \"image\"\n                ) {\n\n                  out.drawImage(\n                    layer.image,\n                    -layer.w *\n                      sx \/\n                      2,\n                    -layer.h *\n                      sy \/\n                      2,\n                    layer.w *\n                      sx,\n                    layer.h *\n                      sy\n                  );\n\n                } else {\n\n                  out.font =\n                    \"bold \" +\n                    layer.fontSize *\n                    sx +\n                    \"px \" +\n                    layer.font;\n\n                  out.textAlign =\n                    \"center\";\n\n                  out.textBaseline =\n                    \"middle\";\n\n                  if (\n                    layer.outline \u003e\n                    0\n                  ) {\n\n                    out.lineWidth =\n                      layer.outline *\n                      sx *\n                      2;\n\n                    out.strokeStyle =\n                      layer.outlineColour;\n\n                    out.strokeText(\n                      layer.text,\n                      0,\n                      0\n                    );\n\n                  }\n\n                  out.fillStyle =\n                    layer.colour;\n\n                  out.fillText(\n                    layer.text,\n                    0,\n                    0\n                  );\n\n                }\n\n                out.restore();\n\n              }\n            );\n\n          addGangPiece(\n            output,\n            state.productType +\n            \" \" +\n            state.area,\n            20,\n            \"mockup-print-area\"\n          );\n\n          openTab(\n            \"gang\"\n          );\n\n        };\n\n\n      function renderGangPieces() {\n\n        const wrapper =\n          $(\"usafGangPieces\");\n\n        wrapper.innerHTML =\n          \"\";\n\n        if (\n          !state.gangPieces.length\n        ) {\n\n          wrapper.innerHTML =\n            '\u003cdiv class=\"usaf-note\"\u003eNo artwork added.\u003c\/div\u003e';\n\n          return;\n\n        }\n\n        state.gangPieces.forEach(\n          function(piece){\n\n            const element =\n              document.createElement(\n                \"div\"\n              );\n\n            element.className =\n              \"usaf-gang-piece\";\n\n            element.innerHTML =\n              \"\u003cstrong\u003e\" +\n              esc(piece.name) +\n              \"\u003c\/strong\u003e\" +\n\n              \"\u003csmall style='display:block;margin-top:3px;color:var(--muted);font-size:8px'\u003eArtwork\u003c\/small\u003e\" +\n\n              '\u003cdiv class=\"usaf-two\" style=\"margin-top:5px\"\u003e' +\n\n                '\u003cinput data-width class=\"usaf-input\" type=\"number\" min=\"1\" max=\"56\" step=\".1\" value=\"' +\n                piece.wCm.toFixed(1) +\n                '\"\u003e' +\n\n                '\u003cbutton data-remove class=\"usaf-btn danger\" type=\"button\"\u003eRemove\u003c\/button\u003e' +\n\n              \"\u003c\/div\u003e\";\n\n            element\n              .querySelector(\n                \"[data-width]\"\n              )\n              .onchange =\n              function(){\n\n                const ratio =\n                  piece.hCm \/\n                  piece.wCm;\n\n                piece.wCm =\n                  clamp(\n                    Number(\n                      this.value\n                    ) ||\n                    1,\n                    1,\n                    GANG_WIDTH_CM\n                  );\n\n                piece.hCm =\n                  piece.wCm *\n                  ratio;\n\n                autoNest();\n\n              };\n\n            element\n              .querySelector(\n                \"[data-remove]\"\n              )\n              .onclick =\n              function(){\n\n                state.gangPieces =\n                  state.gangPieces\n                    .filter(\n                      function(item){\n\n                        return (\n                          item.id !==\n                          piece.id\n                        );\n\n                      }\n                    );\n\n                if (\n                  state.gangSelected ===\n                  piece.id\n                ) {\n\n                  state.gangSelected =\n                    null;\n\n                }\n\n                renderGangPieces();\n                autoNest();\n                updateGangButtons();\n\n              };\n\n            wrapper.appendChild(\n              element\n            );\n\n          }\n        );\n\n      }\n\n\n      function autoNest() {\n\n        const spacing =\n          Math.max(\n            0,\n            Number(\n              $(\"usafGangSpacing\").value\n            ) ||\n            0\n          );\n\n        const pieces =\n          state.gangPieces\n            .slice()\n            .sort(\n              function(a,b){\n\n                return (\n                  b.wCm *\n                  b.hCm -\n                  a.wCm *\n                  a.hCm\n                );\n\n              }\n            );\n\n        let x =\n          0;\n\n        let y =\n          0;\n\n        let rowHeight =\n          0;\n\n        pieces.forEach(\n          function(piece){\n\n            const width =\n              rotatedWidth(\n                piece\n              );\n\n            const height =\n              rotatedHeight(\n                piece\n              );\n\n            if (\n              x \u003e 0 \u0026\u0026\n              x + width \u003e\n              GANG_WIDTH_CM\n            ) {\n\n              x =\n                0;\n\n              y +=\n                rowHeight +\n                spacing;\n\n              rowHeight =\n                0;\n\n            }\n\n            piece.xCm =\n              x;\n\n            piece.yCm =\n              y;\n\n            x +=\n              width +\n              spacing;\n\n            rowHeight =\n              Math.max(\n                rowHeight,\n                height\n              );\n\n          }\n        );\n\n        renderGang();\n\n      }\n\n\n      $(\"usafAutoNest\").onclick =\n        autoNest;\n\n\n      $(\"usafGangSpacing\").onchange =\n        autoNest;\n\n\n      $(\"usafGangBackground\").onchange =\n        function(){\n\n          state.gangBackground =\n            this.value;\n\n          renderGang();\n\n        };\n\n\n      $(\"usafGangSnap\").onchange =\n        function(){\n\n          state.gangSnap =\n            this.checked;\n\n        };\n\n\n      function gangLength() {\n\n        if (\n          !state.gangPieces.length\n        ) {\n          return 0;\n        }\n\n        return Math.max.apply(\n          null,\n          state.gangPieces\n            .map(\n              function(piece){\n\n                return (\n                  piece.yCm +\n                  rotatedHeight(\n                    piece\n                  )\n                );\n\n              }\n            )\n        );\n\n      }\n\n\n      function gangCharged() {\n\n        const actual =\n          gangLength();\n\n        if (\n          actual \u003c=\n          GANG_MIN_CM\n        ) {\n\n          return GANG_MIN_CM;\n\n        }\n\n        return (\n          Math.ceil(\n            actual \/\n            GANG_STEP_CM\n          ) *\n          GANG_STEP_CM\n        );\n\n      }\n\n\n      function gangCartQuantity() {\n\n        return Math.max(\n          10,\n          Math.round(\n            gangCharged() \/\n            GANG_STEP_CM\n          )\n        );\n\n      }\n\n\n      function gangPreviewScale() {\n\n        return Math.min(\n          12,\n          3500 \/\n          Math.max(\n            100,\n            gangLength()\n          )\n        );\n\n      }\n\n\n      function drawBackground(\n        context,\n        width,\n        height,\n        preview\n      ) {\n\n        if (\n          state.gangBackground ===\n          \"white\"\n        ) {\n\n          context.fillStyle =\n            \"#fff\";\n\n          context.fillRect(\n            0,\n            0,\n            width,\n            height\n          );\n\n          return;\n\n        }\n\n        if (\n          state.gangBackground ===\n          \"black\"\n        ) {\n\n          context.fillStyle =\n            \"#000\";\n\n          context.fillRect(\n            0,\n            0,\n            width,\n            height\n          );\n\n          return;\n\n        }\n\n        if (!preview) {\n          return;\n        }\n\n        const size =\n          16;\n\n        for (\n          let y = 0;\n          y \u003c height;\n          y += size\n        ) {\n\n          for (\n            let x = 0;\n            x \u003c width;\n            x += size\n          ) {\n\n            context.fillStyle =\n              (\n                (\n                  x \/ size +\n                  y \/ size\n                ) %\n                2\n              )\n                ? \"#e1e4e8\"\n                : \"#fff\";\n\n            context.fillRect(\n              x,\n              y,\n              size,\n              size\n            );\n\n          }\n\n        }\n\n      }\n\n\n      function drawGangPiece(\n        context,\n        piece,\n        pxPerCm,\n        selection\n      ) {\n\n        const x =\n          piece.xCm *\n          pxPerCm;\n\n        const y =\n          piece.yCm *\n          pxPerCm;\n\n        const sourceW =\n          piece.wCm *\n          pxPerCm;\n\n        const sourceH =\n          piece.hCm *\n          pxPerCm;\n\n        const displayW =\n          rotatedWidth(\n            piece\n          ) *\n          pxPerCm;\n\n        const displayH =\n          rotatedHeight(\n            piece\n          ) *\n          pxPerCm;\n\n        context.save();\n\n        context.translate(\n          x +\n          displayW \/ 2,\n          y +\n          displayH \/ 2\n        );\n\n        context.rotate(\n          piece.rotation *\n          Math.PI \/\n          180\n        );\n\n        context.drawImage(\n          piece.image,\n          -sourceW \/ 2,\n          -sourceH \/ 2,\n          sourceW,\n          sourceH\n        );\n\n        context.restore();\n\n        if (\n          selection \u0026\u0026\n          piece.id ===\n          state.gangSelected\n        ) {\n\n          context.save();\n\n          context.strokeStyle =\n            \"#ffd400\";\n\n          context.lineWidth =\n            2;\n\n          context.setLineDash([\n            7,\n            5\n          ]);\n\n          context.strokeRect(\n            x,\n            y,\n            displayW,\n            displayH\n          );\n\n          context.restore();\n\n        }\n\n      }\n\n\n      function renderGang() {\n\n        const scale =\n          gangPreviewScale();\n\n        const length =\n          Math.max(\n            100,\n            gangLength()\n          );\n\n        gangCanvas.width =\n          Math.round(\n            GANG_WIDTH_CM *\n            scale\n          );\n\n        gangCanvas.height =\n          Math.max(\n            500,\n            Math.round(\n              length *\n              scale\n            )\n          );\n\n        gctx.clearRect(\n          0,\n          0,\n          gangCanvas.width,\n          gangCanvas.height\n        );\n\n        drawBackground(\n          gctx,\n          gangCanvas.width,\n          gangCanvas.height,\n          true\n        );\n\n        state.gangPieces.forEach(\n          function(piece){\n\n            drawGangPiece(\n              gctx,\n              piece,\n              scale,\n              true\n            );\n\n          }\n        );\n\n        $(\"usafGangActual\").textContent =\n          gangLength()\n            .toFixed(1) +\n          \" cm\";\n\n        $(\"usafGangCharged\").textContent =\n          gangCharged() +\n          \" cm\";\n\n        $(\"usafGangCount\").textContent =\n          state.gangPieces.length;\n\n        $(\"usafGangUnits\").textContent =\n          gangCartQuantity();\n\n        updateSelectedGang();\n        updateGangButtons();\n\n      }\n\n\n      function selectedGang() {\n\n        return (\n          state.gangPieces\n            .find(\n              function(piece){\n\n                return (\n                  piece.id ===\n                  state.gangSelected\n                );\n\n              }\n            ) ||\n          null\n        );\n\n      }\n\n\n      function updateSelectedGang() {\n\n        const piece =\n          selectedGang();\n\n        if (!piece) {\n\n          $(\"usafGangSelected\").textContent =\n            \"Tap artwork on the sheet.\";\n\n          $(\"usafGangSelectedWidth\").disabled =\n            true;\n\n          $(\"usafGangWidthRange\").disabled =\n            true;\n\n          $(\"usafGangQuality\").className =\n            \"usaf-quality good\";\n\n          $(\"usafGangQuality\").textContent =\n            \"Resolution appears here.\";\n\n          return;\n\n        }\n\n        $(\"usafGangSelectedWidth\").disabled =\n          false;\n\n        $(\"usafGangWidthRange\").disabled =\n          false;\n\n        $(\"usafGangSelectedWidth\").value =\n          piece.wCm.toFixed(1);\n\n        $(\"usafGangWidthRange\").value =\n          piece.wCm;\n\n        $(\"usafGangSelected\").innerHTML =\n          \"\u003cstrong\u003e\" +\n          esc(piece.name) +\n          \"\u003c\/strong\u003e\u003cbr\u003e\" +\n          piece.wCm.toFixed(1) +\n          \" × \" +\n          piece.hCm.toFixed(1) +\n          \" cm\";\n\n        const effectiveDpi =\n          piece.sourceWidth \/\n          (\n            piece.wCm \/\n            2.54\n          );\n\n        if (\n          effectiveDpi \u003e=\n          250\n        ) {\n\n          $(\"usafGangQuality\").className =\n            \"usaf-quality good\";\n\n          $(\"usafGangQuality\").textContent =\n            Math.round(\n              effectiveDpi\n            ) +\n            \" DPI — good resolution\";\n\n        } else if (\n          effectiveDpi \u003e=\n          150\n        ) {\n\n          $(\"usafGangQuality\").className =\n            \"usaf-quality warn\";\n\n          $(\"usafGangQuality\").textContent =\n            Math.round(\n              effectiveDpi\n            ) +\n            \" DPI — moderate resolution\";\n\n        } else {\n\n          $(\"usafGangQuality\").className =\n            \"usaf-quality bad\";\n\n          $(\"usafGangQuality\").textContent =\n            Math.round(\n              effectiveDpi\n            ) +\n            \" DPI — low resolution\";\n\n        }\n\n      }\n\n\n      function setSelectedGangWidth(\n        value\n      ) {\n\n        const piece =\n          selectedGang();\n\n        if (!piece) {\n          return;\n        }\n\n        const ratio =\n          piece.hCm \/\n          piece.wCm;\n\n        piece.wCm =\n          clamp(\n            Number(value) ||\n            1,\n            1,\n            GANG_WIDTH_CM\n          );\n\n        piece.hCm =\n          piece.wCm *\n          ratio;\n\n        renderGang();\n\n      }\n\n\n      $(\"usafGangSelectedWidth\").oninput =\n        function(){\n\n          setSelectedGangWidth(\n            this.value\n          );\n\n        };\n\n\n      $(\"usafGangWidthRange\").oninput =\n        function(){\n\n          setSelectedGangWidth(\n            this.value\n          );\n\n        };\n\n\n      $(\"usafGangRotate\").onclick =\n        function(){\n\n          const piece =\n            selectedGang();\n\n          if (!piece) {\n            return;\n          }\n\n          piece.rotation =\n            (\n              piece.rotation +\n              90\n            ) %\n            360;\n\n          renderGang();\n\n        };\n\n\n      $(\"usafGangDuplicate\").onclick =\n        function(){\n\n          const piece =\n            selectedGang();\n\n          if (!piece) {\n            return;\n          }\n\n          const copy =\n            Object.assign(\n              {},\n              piece,\n              {\n                id:\n                  makeId(\n                    \"gang\"\n                  ),\n\n                xCm:\n                  piece.xCm +\n                  1,\n\n                yCm:\n                  piece.yCm +\n                  1\n              }\n            );\n\n          state.gangPieces.push(\n            copy\n          );\n\n          state.gangSelected =\n            copy.id;\n\n          renderGangPieces();\n          renderGang();\n\n        };\n\n\n      $(\"usafGangDelete\").onclick =\n        function(){\n\n          if (\n            !state.gangSelected\n          ) {\n            return;\n          }\n\n          state.gangPieces =\n            state.gangPieces\n              .filter(\n                function(piece){\n\n                  return (\n                    piece.id !==\n                    state.gangSelected\n                  );\n\n                }\n              );\n\n          state.gangSelected =\n            null;\n\n          renderGangPieces();\n          renderGang();\n\n        };\n\n\n      \/* =====================================================\n         GANG ACCESS \/ LOG\n      ====================================================== *\/\n\n      function validEmail(value) {\n\n        return \/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\/\n          .test(\n            String(\n              value ||\n              \"\"\n            ).trim()\n          );\n\n      }\n\n\n      function gangDownloadFormValues() {\n\n        return {\n\n          name:\n            $(\"usafGangCustomerName\")\n              .value\n              .trim(),\n\n          email:\n            $(\"usafGangCustomerEmail\")\n              .value\n              .trim(),\n\n          phone:\n            $(\"usafGangCustomerPhone\")\n              .value\n              .trim(),\n\n          message:\n            $(\"usafGangCustomerMessage\")\n              .value\n              .trim()\n\n        };\n\n      }\n\n\n      function gangDownloadAllowed() {\n\n        const form =\n          gangDownloadFormValues();\n\n        return Boolean(\n          state.customer.loggedIn \u0026\u0026\n          state.gangPieces.length \u0026\u0026\n          $(\"usafGangTerms\").checked \u0026\u0026\n          form.name.length \u003e= 2 \u0026\u0026\n          validEmail(\n            form.email\n          ) \u0026\u0026\n          form.phone.length \u003e= 7\n        );\n\n      }\n\n\n      function updateGangButtons() {\n\n        const loginStatus =\n          $(\"usafGangLoginStatus\");\n\n        const loginLink =\n          $(\"usafGangLoginLink\");\n\n        const formBox =\n          $(\"usafGangDownloadForm\");\n\n        const productionButton =\n          $(\"usafGangProduction\");\n\n        loginLink.href =\n          loginUrl();\n\n        if (\n          !state.customer.loggedIn\n        ) {\n\n          loginStatus.className =\n            \"usaf-note error\";\n\n          loginStatus.textContent =\n            \"Please login with your Shopify customer account to download.\";\n\n          loginLink.classList\n            .remove(\n              \"usaf-hidden\"\n            );\n\n          formBox.classList\n            .add(\n              \"usaf-hidden\"\n            );\n\n          productionButton.disabled =\n            true;\n\n          return;\n\n        }\n\n        loginStatus.className =\n          \"usaf-note good\";\n\n        loginStatus.textContent =\n          \"You are logged in. Complete the form to unlock download.\";\n\n        loginLink.classList\n          .add(\n            \"usaf-hidden\"\n          );\n\n        formBox.classList\n          .remove(\n            \"usaf-hidden\"\n          );\n\n        productionButton.disabled =\n          !gangDownloadAllowed();\n\n      }\n\n\n      [\n        \"usafGangCustomerName\",\n        \"usafGangCustomerEmail\",\n        \"usafGangCustomerPhone\",\n        \"usafGangCustomerMessage\"\n      ]\n        .forEach(\n          function(id){\n\n            $(id).addEventListener(\n              \"input\",\n              updateGangButtons\n            );\n\n          }\n        );\n\n\n      $(\"usafGangTerms\")\n        .addEventListener(\n          \"change\",\n          updateGangButtons\n        );\n\n\n      function requireGangDownloadAccess() {\n\n        updateGangButtons();\n\n        if (\n          !state.customer.loggedIn\n        ) {\n\n          window.location.href =\n            loginUrl();\n\n          return false;\n\n        }\n\n        if (\n          !gangDownloadAllowed()\n        ) {\n\n          alert(\n            \"Please add artwork, complete name\/email\/phone, and approve the layout before downloading.\"\n          );\n\n          return false;\n\n        }\n\n        return true;\n\n      }\n\n\n      function gangDownloadLogProperties(\n        type,\n        downloadId\n      ) {\n\n        const form =\n          gangDownloadFormValues();\n\n        return {\n\n          type:type,\n\n          downloadId:\n            downloadId,\n\n          customerId:\n            state.customer.id ||\n            \"\",\n\n          customerName:\n            form.name,\n\n          customerEmail:\n            form.email,\n\n          customerPhone:\n            form.phone,\n\n          customerMessage:\n            form.message,\n\n          filmWidth:\n            \"56 cm\",\n\n          artworkLength:\n            gangLength()\n              .toFixed(1) +\n            \" cm\",\n\n          downloadLength:\n            gangCharged() +\n            \" cm\",\n\n          artworkCount:\n            String(\n              state.gangPieces.length\n            ),\n\n          downloadedAt:\n            new Date()\n              .toISOString(),\n\n          pageUrl:\n            window.location.href,\n\n          userAgent:\n            navigator.userAgent,\n\n          background:\n            state.gangBackground,\n\n          fileName:\n            \"USASTARFASHION-\" +\n            downloadId +\n            \"-56cm-gang-300dpi.png\"\n\n        };\n\n      }\n\n\n      async function saveGangDownloadRecord(\n        type,\n        downloadId\n      ) {\n\n        const record =\n          gangDownloadLogProperties(\n            type,\n            downloadId\n          );\n\n        try {\n\n          localStorage.setItem(\n            \"USAF_LAST_GANG_DOWNLOAD\",\n            JSON.stringify(\n              record\n            )\n          );\n\n        } catch (error) {}\n\n        await fetch(\n          GOOGLE_SHEET_LOG_URL,\n          {\n            method:\n              \"POST\",\n\n            mode:\n              \"no-cors\",\n\n            headers:{\n              \"Content-Type\":\n                \"text\/plain;charset=utf-8\"\n            },\n\n            body:\n              JSON.stringify(\n                record\n              )\n          }\n        );\n\n      }\n\n\n      function createGangExport(\n        production\n      ) {\n\n        const length =\n          Math.max(\n            100,\n            gangLength()\n          );\n\n        const pxPerCm =\n          production\n            ? PRINT_PX_CM\n            : Math.min(\n                12,\n                2200 \/\n                length\n              );\n\n        const output =\n          document.createElement(\n            \"canvas\"\n          );\n\n        output.width =\n          Math.round(\n            GANG_WIDTH_CM *\n            pxPerCm\n          );\n\n        output.height =\n          Math.round(\n            length *\n            pxPerCm\n          );\n\n        const out =\n          output.getContext(\n            \"2d\"\n          );\n\n        drawBackground(\n          out,\n          output.width,\n          output.height,\n          false\n        );\n\n        state.gangPieces\n          .forEach(\n            function(piece){\n\n              drawGangPiece(\n                out,\n                piece,\n                pxPerCm,\n                false\n              );\n\n            }\n          );\n\n        return output;\n\n      }\n\n\n      \/* CAPTCHA BEFORE GANG DOWNLOAD *\/\n\n      $(\"usafGangProduction\").onclick =\n        async function(){\n\n          if (\n            !requireGangDownloadAccess()\n          ) {\n            return;\n          }\n\n          if (\n            !state.gangPieces.length\n          ) {\n            return;\n          }\n\n          const outputHeight =\n            Math.round(\n              Math.max(\n                100,\n                gangLength()\n              ) *\n              PRINT_PX_CM\n            );\n\n          const outputWidth =\n            Math.round(\n              GANG_WIDTH_CM *\n              PRINT_PX_CM\n            );\n\n          const megapixels =\n            outputWidth *\n            outputHeight;\n\n          if (\n            megapixels \u003e\n            90000000\n          ) {\n\n            $(\"usafGangExportStatus\").className =\n              \"usaf-note error\";\n\n            $(\"usafGangExportStatus\").textContent =\n              \"This 300 DPI file is too large for mobile browser export. Use desktop Chrome or split into shorter sheets.\";\n\n            return;\n\n          }\n\n          const verified =\n            await requireCaptcha();\n\n          if (!verified) {\n            return;\n          }\n\n          const downloadId =\n            createDownloadId();\n\n          const fileName =\n            \"USASTARFASHION-\" +\n            downloadId +\n            \"-56cm-gang-300dpi.png\";\n\n          $(\"usafGangCartStatus\").className =\n            \"usaf-note\";\n\n          $(\"usafGangCartStatus\").textContent =\n            \"Preparing download and saving customer record…\";\n\n          try {\n\n            await saveGangDownloadRecord(\n              \"production-300dpi\",\n              downloadId\n            );\n\n            const exportCanvas =\n              createGangExport(\n                true\n              );\n\n            downloadCanvas(\n              exportCanvas,\n              fileName,\n              \"usafGangExportStatus\"\n            );\n\n            $(\"usafGangCartStatus\").className =\n              \"usaf-note good\";\n\n            $(\"usafGangCartStatus\").textContent =\n              \"Download started. Customer download record saved.\";\n\n          } catch (error) {\n\n            console.error(error);\n\n            $(\"usafGangCartStatus\").className =\n              \"usaf-note error\";\n\n            $(\"usafGangCartStatus\").textContent =\n              \"Download record failed. Check Google Apps Script deployment.\";\n\n          }\n\n        };\n\n\n      \/* =====================================================\n         INITIALISE\n      ====================================================== *\/\n\n      window.addEventListener(\n        \"resize\",\n        function(){\n\n          if (\n            $(\"usafSection_gang\")\n              .classList\n              .contains(\n                \"active\"\n              )\n          ) {\n\n            renderGang();\n\n          }\n\n        }\n      );\n\n\n      detectGangFromPage();\n\n      renderAreas();\n\n      renderMock();\n\n      renderLayers();\n\n      renderGangPieces();\n\n      renderGang();\n\n\n      \/*\n        Google Drive connects automatically through\n        your existing Apps Script web app.\n\n        No Google OAuth button is required.\n      *\/\n\n      loadLibrary();\n\n      loadShopifyProduct();\n\n      verifyCustomerLogin();\n\n    })();\n  \u003c\/script\u003e\n\n\u003c\/div\u003e","brand":"USASTARFASHION","offers":[{"title":"Default Title","offer_id":58530480750923,"sku":null,"price":10.0,"currency_code":"GBP","in_stock":true}],"url":"https:\/\/www.usastarfashion.com\/products\/studio","provider":"USASTARFASHION","version":"1.0","type":"link"}