{"version":3,"file":"cart-drawer-section.min.js","mappings":"wZAcO,SAASA,IACd,OAAOC,eAAeC,KACxB,CAMO,SAASC,EAAmBC,GACjC,OAAOA,aAAiBC,OAAwB,iBAAfD,EAAME,IACzC,CAKO,SAASC,EAAuBH,GACrC,OAAOA,aAAiBC,OAAwB,sBAAfD,EAAME,IACzC,CC2BO,SAASE,EAAMC,EACpBH,GACA,QAD2B,IAA3BA,IAAAA,EAA8B,oBACF,aAAxBI,SAASC,YAAuC,qBAATL,GACjB,gBAAxBI,SAASC,WAA+B,CACxC,MAAMC,EAAQC,WAAWJ,EAAU,GAEnC,MAD8B,IAAMK,aAAaF,EAEnD,CACEG,iBAAiBT,EAAMG,GAEvB,MAD8B,IAAMO,oBAAoBV,EAAMG,EAGlE,CCzDA,MAAMQ,oBAAoBC,YAchBC,uBAAwB,EACxBC,6BAA+BC,KAAKC,wBAAwBC,KAAKF,MACjEG,8BAAgCH,KAAKI,yBAAyBF,KAAKF,MACnEK,gCAAkCL,KAAKM,2BAA2BJ,KAAKF,MACvEO,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7CS,aAAeT,KAAKU,QAAQR,KAAKF,MACjCW,gBAAkBX,KAAKY,WAAWV,KAAKF,MACvCa,qCAAuCb,KAAKc,gCAAgCZ,KAAKF,MAIlF,iBAAAe,GACLf,KAAKgB,uBAELhB,KAAKiB,aAAe9B,EAAMa,KAAKS,cAE/Bf,iBAAiB,WAAYM,KAAKW,gBACpC,CAEO,oBAAAO,GACL,MAAMC,EAAiBnB,KAAKoB,WAAWC,cAA2B,oBACpD,MAAdF,GAAAA,EAAgBxB,oBAAoB,QAASK,KAAKD,8BAElD,MAAMuB,EAAmBtB,KAAKoB,WAAWC,cAAiC,sBAC1D,MAAhBC,GAAAA,EAAkB3B,oBAAoB,QAASK,KAAKa,sCAEpDlB,oBAAoB,eAAgBK,KAAKO,oBACzCZ,oBAAoB,2BAA4BK,KAAKG,+BACrDR,oBAAoB,6BAA8BK,KAAKK,iCACvDV,oBAAoB,WAAYK,KAAKW,iBAEjCX,KAAKiB,cACPjB,KAAKiB,cAET,CAEQ,oBAAAD,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,+oCAEhC,CAEQ,OAAAf,GACN,IAAKV,KAAK0B,YACR,OAGqB1B,KAAKoB,WAAWC,cAA2B,oBACnD3B,iBAAiB,QAASM,KAAKD,8BAE9C,MAAM4B,EAAiB3B,KAAKoB,WAAWC,cAAiC,sBACpEM,IACmC,SAAjC3B,KAAK4B,QAAQC,gBACfF,EAAeG,SAEfH,EAAejC,iBAAiB,QAASM,KAAKa,uCAIlDnB,iBAAiB,2BAA4BM,KAAKG,+BAClDT,iBAAiB,6BAA8BM,KAAKK,iCAEpD,IAAK,MAAM0B,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,mBACxC,CAmCQ,uBAAAN,CAAwB8B,GAC9B,MAAMI,EAASJ,EAAMK,cASrB,KAAMD,aAAkBE,mBAEtB,YADAJ,QAAQK,IAAI,mEASd,GAAIH,EAAOI,SAET,YADAN,QAAQK,IAAI,6DAId,IACEE,eAAeC,QAAQ,gCAAiCC,KAAKC,MAAMC,WACrE,CAAE,MAAOZ,GACPC,QAAQC,KAAKF,EACf,CAEA,MAAMa,EAAa7C,KAAK8C,QAAQ,eAC1BC,EAAuB,MAAVF,OAAU,EAAVA,EAAYxB,cAAc,eAC7C,GAAI0B,EAAY,CAGd,GADsBA,EAAWC,iBAAiB,mCAChCC,OAAS,IAAMC,YAAYC,uBAAwB,CACnEN,EAAWjB,QAAQwB,KAAO,SAC1BF,YAAYC,wBAAyB,EAGrC,MAAME,EAAcrD,KAAK8C,QAAQ,gBAKjC,YAJIO,IACFA,EAAYzB,QAAQ0B,cAAgB,QAIxC,CACErB,QAAQK,IAAI,mDAEhB,MACEL,QAAQK,IAAI,0DAQdH,EAAOI,UAAW,EAGlB,MAAMgB,EAAM,IAAIC,IAAIC,QAAQC,OAAOC,KAAO,WAAY,yBACrB,SAA7B3D,KAAK4B,QAAQgC,aACfL,EAAIM,aAAaC,IAAI,gBAAiB,QAGxCC,SAASC,KAAOT,EAAIX,UACtB,CAMQ,wBAAAxC,CAAyB6D,GAChBjE,KAAKoB,WAAWC,cAAiC,oBACzDkB,UAAW,CACpB,CAEQ,0BAAAjC,CAA2B2D,GAC7BjE,KAAKF,uBACPE,KAAKkE,YAET,CAKQ,UAAAA,GACSlE,KAAKoB,WAAWC,cAAiC,oBACzDkB,UAAW,CACpB,CAEQ,aAAA/B,CAAcuB,GAMpB,GALIA,EAAMoC,OAAOC,aACfpE,KAAKF,uBAAwB,GAI3BE,KAAKF,sBAAuB,CACfE,KAAKoB,WAAWC,cAAiC,oBACzDkB,UAAW,CACpB,CACF,CAEQ,UAAA3B,CAAWmB,GACbA,EAAMsC,WACRrE,KAAKkE,YAET,CAEQ,+BAAApD,CAAgCmD,GACtC,MAAMpB,EAAa7C,KAAK8C,QAAQ,eAChC,GAAiC,YAAnB,MAAVD,OAAU,EAAVA,EAAYjB,QAAQwB,MACtBP,EAAWjB,QAAQwB,KAAO,WACrB,CAEL,MAAMrB,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNM,SAAS,EACTC,OAAQ,8BAGZ1E,KAAK2E,cAAc5C,EACrB,CACF,ECzPK,SAASY,IACd,IAAI,IAAAiC,EACF,GAAgC,mBAAV,OAAlBA,EAAOC,kBAAW,EAAXD,EAAajC,KACtB,OAAOkC,YAAYlC,KAEvB,CAAE,MAAOX,GACPC,QAAQ6C,MAAM9C,EAChB,CAEA,OAAOU,KAAKC,KACd,CAiCO,SAASoC,EAAQC,EAAcC,EAAeC,EAAaf,GAChE,IAAI,IAAAgB,EACkC,mBAAd,OAAlBA,EAAON,kBAAW,EAAXM,EAAaJ,UACtBF,YAAYE,QAAQC,EAAM,CAAEC,QAAOC,MAAKf,UAE5C,CAAE,MAAOnC,GACPC,QAAQ6C,MAAM9C,EAChB,CACF,CD+MKoD,eAAeC,IAAI,iBACtBD,eAAeE,OAAO,eAAgB1F,aErPxC,MAAM2F,mBAAmB1F,YACvB,6BAAkB2F,GAChB,MAAO,CAAC,mBAAoB,YAC9B,CAoCQC,yBAA2BzF,KAAK0F,oBAAoBxF,KAAKF,MACzDO,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7C2F,uBAAyB3F,KAAK4F,kBAAkB1F,KAAKF,MACrDK,gCAAkCL,KAAKM,2BAA2BJ,KAAKF,MACvE6F,2BAA6B7F,KAAK8F,sBAAsB5F,KAAKF,MAQ9D,iBAAAe,GACLf,KAAKgB,uBAEuB,aAAxBhB,KAAK4B,QAAQmE,QACf/F,KAAKgG,uBAOSrH,IAAgBsH,KAAKlE,GAASjD,EAAmBiD,IAC/D7C,EAAuB6C,KAEvB/B,KAAKkG,oBAGPlG,KAAKmG,aAEL,IAAK,MAAMpE,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,sBAAuBM,KAAKyF,0BAC7C/F,iBAAiB,eAAgBM,KAAKO,oBACtCb,iBAAiB,oBAAqBM,KAAK2F,wBAC3CjG,iBAAiB,6BAA8BM,KAAKK,gCACtD,CAEO,oBAAAa,GACLvB,oBAAoB,sBAAuBK,KAAKyF,0BAChD9F,oBAAoB,eAAgBK,KAAKO,oBACzCZ,oBAAoB,oBAAqBK,KAAK2F,wBAC9ChG,oBAAoB,6BAA8BK,KAAKK,gCACzD,CAEO,wBAAA+F,CAAyBpB,EAAcqB,EAAkBC,GAC9DtG,KAAKgB,uBAEDhB,KAAK0B,aAAwB,qBAATsD,GAA+BqB,IAAaC,GAClEtG,KAAKuG,SAMHvG,KAAK0B,aAAwB,cAATsD,GAAqC,OAAbqB,GAAqBA,IAAaC,GAChFtG,KAAKwG,eAET,CAEQ,oBAAAxF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,s+BAEhC,CAUQ,aAAAjB,CAAcuB,GAGpB,GAFA/B,KAAKyG,mBAEA1E,EAAMoC,OAAOC,YAAgD,UAAjCpE,KAAK4B,QAAQwB,MAAQ,QAAoB,CAC1DpD,KAAKoB,WAAWC,cAAc,UACtCqF,SAAS,EAAG,EACpB,CACF,CAMQ,iBAAAd,CAAkB3B,GACxBjE,KAAKyG,iBACP,CAMQ,mBAAAf,CAAoBzB,GAC1BjE,KAAKkG,mBACP,CAKQ,iBAAAA,GACNlG,KAAK4B,QAAQ+E,iBAAmB,OAEhC,MAAMC,EAAe5G,KAAKqB,cAA2B,0BACjDuF,IACFA,EAAahF,QAAQiF,QAAU,wDAGjC7G,KAAK4B,QAAQwB,KAAO,UACtB,CAOQ,eAAAqD,GACgC,SAAlCzG,KAAK4B,QAAQ+E,mBAIS,aAAtB3G,KAAK4B,QAAQwB,OACfpD,KAAK4B,QAAQwB,KAAO,eAGfpD,KAAK4B,QAAQ+E,iBACtB,CAWQ,0BAAArG,CAA2B2D,GACjC,GAA0B,aAAtBjE,KAAK4B,QAAQwB,KAAqB,CACpC,MAAM0D,EAAY,KAAOnE,IAAQ3C,KAAK+G,iBACtCvH,WAAWQ,KAAK6F,2BAA4BmB,KAAKC,IAAI,EAAGH,GAC1D,CACF,CAMQ,qBAAAhB,GACoB,aAAtB9F,KAAK4B,QAAQwB,OACfpD,KAAK4B,QAAQwB,KAAO,OAExB,CAEQ,cAAA8D,GACN,MAAMC,EAAQnH,KAAKoB,WAAWC,cAA2B,UACzD8F,EAAMC,OAAQ,EACdD,EAAMT,SAAS,EAAG,GAElB1G,KAAKqH,WACP,CAEQ,cAAAC,GACQtH,KAAKoB,WAAWC,cAA2B,UACnD+F,OAAQ,CAChB,CAEQ,MAAAb,GAC0B,SAA5BvG,KAAK4B,QAAQ2F,WACfvH,KAAKkH,iBAELlH,KAAKsH,gBAET,CAMQ,UAAAnB,GACN,MAAMqB,EAAe,GAAGxH,KAAK4B,QAAQwB,MAAQ,cACvCqE,EAAQzH,KAAKoB,WAAW4B,iBAAkC,uBAChE,IAAK,MAAM0E,KAAQD,EACjBC,EAAKC,OAASD,EAAK1C,OAASwC,CAEhC,CAOQ,oBAAAxB,GACN,MAAM4B,EAAQ5H,KAAKoB,WAAWC,cAA2B,UACzDuG,EAAMC,gBAAgB,QACtBD,EAAMC,gBAAgB,cACtBD,EAAMC,gBAAgB,cACtBD,EAAMR,OAAQ,EACdQ,EAAME,UAAY,CACpB,CAMQ,SAAAT,GACN,MAAMjE,EAAOpD,KAAKqB,cAA2B,UAAUrB,KAAK4B,QAAQwB,MAAQ,iBACxEA,IACFA,EAAKxB,QAAQmG,eAAiB,OAElC,CAMQ,aAAAvB,GAON,GANAxG,KAAKmG,aAEqB,aAAtBnG,KAAK4B,QAAQwB,OACfpD,KAAK+G,gBAAkBpE,KAGG,aAAxB3C,KAAK4B,QAAQmE,OAAuB,CAGtC,GAAsC,SAAlC/F,KAAK4B,QAAQ+E,iBACf,OAKFD,SAAS,EAAG,GACZ1G,KAAKoB,WAAWC,cAA2B,UAAU2G,MAAM,CAAEC,eAAe,GAC9E,KAAuC,SAA5BjI,KAAK4B,QAAQ2F,aACtBvH,KAAKoB,WAAWC,cAAc,UAAUqF,SAAS,EAAG,GACpD1G,KAAKqH,YAET,EASGjC,eAAeC,IAAI,gBACtBD,eAAeE,OAAO,cAAeC,YCzTvC,MAAM2C,oBAAoBrI,YACxB,6BAAkB2F,GAChB,MAAO,CAAC,sBAAuB,uBACjC,CA+CQrF,8BAAgCH,KAAKI,yBAAyBF,KAAKF,MACnEK,gCAAkCL,KAAKM,2BAA2BJ,KAAKF,MACvEO,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7CmI,oBAAsBnI,KAAKoI,eAAelI,KAAKF,MAC/CqI,mCAAqCrI,KAAKsI,8BAA8BpI,KAAKF,MAC7ES,aAAeT,KAAKU,QAAQR,KAAKF,MACjCuI,gCAAkCvI,KAAKwI,2BAA2BtI,KAAKF,MAEvEyI,sBAAuB,EAIxB,iBAAA1H,GACLf,KAAKgB,uBACLhB,KAAKiB,aAAe9B,EAAMa,KAAKS,aACjC,CAEO,wBAAA2F,CAAyBpB,EAAc0D,EAAmBpC,GAC1DtG,KAAK0B,aAML1B,KAAKoB,aAIG,yBAAT4D,GAAgD,SAAbsB,GACrCtG,KAAK2I,mBAGM,wBAAT3D,GAA+C,OAAbsB,GACpCtG,KAAKsD,cAAcgD,GAEvB,CAMQ,aAAAhD,CAAcvE,GACpB,MAAM6J,EAAe5I,KAAKoB,WAAWC,cAAc,iBAC/CuH,IACFA,EAAahH,QAAQiH,MAAQ9J,EAEjC,CAEO,oBAAAmC,GACL,MAAMI,EAAmBtB,KAAKoB,WAAWC,cAA2B,sBACpD,MAAhBC,GAAAA,EAAkB3B,oBAAoB,QAASK,KAAKqI,oCAEpD,MAAMS,EAAc9I,KAAKoB,WAAWC,cAAc,gBACvC,MAAXyH,GAAAA,EAAanJ,oBAAoB,QAASK,KAAKmI,qBAE/CxI,oBAAoB,2BAA4BK,KAAKG,+BACrDR,oBAAoB,6BAA8BK,KAAKK,iCACvDV,oBAAoB,eAAgBK,KAAKO,oBAGrCP,KAAKiB,cACPjB,KAAKiB,eAGP,MAAM8H,EAAoB/I,KAAKoB,WAAWC,cACxC,+BACe,MAAjB0H,GAAAA,EAAmBpJ,oBAAoB,QAASK,KAAKuI,gCACvD,CAEQ,oBAAAvH,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,u0PAEhC,CAEQ,OAAAf,GACN,IAAKV,KAAK0B,YACR,OAGuB1B,KAAKoB,WAAWC,cAA2B,sBACnD3B,iBAAiB,QAASM,KAAKqI,oCAKhD,GAHoBrI,KAAKoB,WAAWC,cAAc,gBACtC3B,iBAAiB,QAASM,KAAKmI,qBAEvCnI,KAAK4B,QAAQoH,eAAgB,CACThJ,KAAKoB,WAAWC,cAAiC,uBACzD2C,KAAOhE,KAAK4B,QAAQoH,cACpC,CAEAtJ,iBAAiB,2BAA4BM,KAAKG,+BAClDT,iBAAiB,6BAA8BM,KAAKK,iCAEpD,IAAK,MAAM0B,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBAEZP,KAAKoB,WAAWC,cACxC,+BACgB3B,iBAAiB,QAASM,KAAKuI,gCACnD,CAQQ,gBAAAI,UACC3I,KAAK4B,QAAQmG,eACpB/H,KAAKoB,WAAWC,cAAc,gBAAgB2G,MAAM,CAAEiB,cAAc,GACtE,CAEQ,cAAAb,CAAenE,GAMrB,MAAMlC,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNM,SAAS,EACTC,OAAQ,kBAGZ1E,KAAK2E,cAAc5C,EACrB,CAEQ,6BAAAuG,CAA8BrE,GAGpC,GAAkC,SAA9BjE,KAAK4B,QAAQsH,aAEf,YADAnF,SAASC,KAAOD,SAASoF,QAK3B,MAAMpH,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNM,SAAS,EACTC,OAAQ,mCAGZ1E,KAAK2E,cAAc5C,EACrB,CAYQ,oBAAAqH,CAAqBrH,GAC3B,MAAMsH,EAAOtH,EAAMoC,OAAOkF,KAE1B,GAAIA,EAAKC,MAAMrG,OAAS,EACtBjD,KAAKoB,WAAWC,cAA2B,kBAAkBsG,QAAS,EACtE3H,KAAKoB,WAAWC,cAA2B,WAAWsG,QAAS,EAC/D3H,KAAKoB,WAAWC,cAAc,eAAekI,KAAO,WAC/C,CACavJ,KAAKoB,WAAWC,cAA2B,eACnDsG,QAAS,CACrB,CAGA,MAAM6B,EAAQxJ,KAAKqB,cAA2B,6CAExCoI,EAAsBzJ,KAAKqB,cAAc,+BAEd,OAA7BrB,KAAK4B,QAAQ8H,aACXD,IACFA,EAAoB9B,QAAS,GAG3B6B,IACFA,EAAM7B,QAAS,KAGb8B,IACFA,EAAoB9B,QAAS,GAG3B0B,EAAKC,MAAMrD,KAAK0D,GACC,MAAnBF,GAAAA,EAAqBG,UAAUC,IAAI,cAEhB,MAAnBJ,GAAAA,EAAqBG,UAAU9H,OAAO,cAGpC2H,IACFA,EAAoB7H,QAAQkI,eAAiBT,EAAKU,YAAYnH,YAG5D4G,IACFA,EAAM7B,QAAS,IAOnB3H,KAAKgK,6BAA6BX,GAElC,MAAMY,EAAWjK,KAAKoB,WAAWC,cAA2B,oBAC5D4I,EAASrI,QAAQsI,SAAWb,EAAKU,YAAYnH,WAC7CqH,EAASrI,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC7CF,EAASrI,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAE/CpK,KAAKqK,cACP,CAEQ,aAAA7J,CAAcuB,GACpB,MAAMsH,EAAOtH,EAAMoC,OAAOkF,KAI1B,IACErJ,KAAKsK,gBAAgBjB,EAAKC,OAII,WAA1BvH,EAAMoC,OAAOoG,UACfvK,KAAKwK,eAAezI,EAAMoC,OAAOsG,eAErC,CAAE,MAAOzI,GACPC,QAAQD,MAAMA,EAChB,CAIAhC,KAAK0K,qBAEL,MAAMC,EAAU3K,KAAKoB,WAAWC,cAAc,MACxCuJ,EAAWvB,EAAKwB,WAStB,GAPEF,EAAQG,YADO,IAAbF,EACoB,eACA,IAAbA,EACa,wBAEA,CAAC,iBAAkBA,EAAShI,WAAY,WAAWmI,KAAK,IAG5EhJ,EAAMoC,OAAOC,WAEf,YADApE,KAAKoJ,qBAAqBrH,GAM5B,MAAMiJ,EAAehL,KAAKoB,WAAWC,cAA2B,kBAC1D4J,EAAYjL,KAAKoB,WAAWC,cAA2B,eACvD6J,EAAWlL,KAAKoB,WAAWC,cAA2B,WACtD8J,EAAYnL,KAAKoB,WAAWC,cAAc,eAE5CgI,EAAKC,MAAMrG,OAAS,GAClB+H,IACFA,EAAarD,QAAS,GAGxBsD,EAAUtD,QAAS,EACnBuD,EAASvD,QAAS,EAClBwD,EAAU5B,KAAO,SAEjB0B,EAAUtD,QAAS,EAEfqD,IACFA,EAAarD,QAAS,GAGxBuD,EAASvD,QAAS,EAClBwD,EAAU5B,KAAO,MAInB,MAAM6B,EAAcpL,KAAKqB,cAAc,yBACrCrB,KAAKqB,cAAc,uBAEfgK,EAAmCrL,KAAKqB,cAAc,+BAE5D,GAAiC,OAA7BrB,KAAK4B,QAAQ8H,YAAsB,KAAA4B,EAerC,GAdID,IACFA,EAAiC1D,QAAS,GAGxC0B,EAAKC,MAAMrD,KAAK0D,GACc,MAAhC0B,GAAAA,EAAkCzB,UAAUC,IAAI,cAEhB,MAAhCwB,GAAAA,EAAkCzB,UAAU9H,OAAO,cAGjDuJ,IACFA,EAAiCzJ,QAAQkI,eAAiBT,EAAKU,YAAYnH,YAGrC,SAArB,OAAf0I,EAAAjC,EAAKkC,iBAAU,EAAfD,EAAiBE,gBAA0B,CAC7C,MAAMC,EAAgBzL,KAAKqB,cAAc,0BACrCoK,IACFA,EAAc7J,QAAQ8J,KAAO,OAEjC,CAEIN,IACFA,EAAYzD,QAAS,EAEzB,MACM0D,IACFA,EAAiC1D,QAAS,GAGxCyD,IACFA,EAAYzD,QAAS,GAIzB,MAAMsC,EAAWjK,KAAKoB,WAAWC,cAA2B,oBAC5D4I,EAASrI,QAAQsI,SAAWb,EAAKU,YAAYnH,WAC7CqH,EAASrI,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC7CF,EAASrI,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAK/CpK,KAAKgK,6BAA6BX,EACpC,CAcQ,kBAAAqB,GACN,GAAkC,SAA9B1K,KAAK4B,QAAQsH,aACf,OAGF,GAAIlJ,KAAKyI,qBACP,OAGFzI,KAAKyI,sBAAuB,EAG5B,MAAM1G,EAAQ,IAAIuC,YAAuC,cAAe,CACtEC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNwH,QAAS,UAGb3L,KAAK2E,cAAc5C,EACrB,CAEQ,4BAAAiI,CAA6BX,GACnC,IAAIuC,EAAiB,EACrB,IAAK,MAAMC,KAAQxC,EAAKC,MAAO,KAAAwC,EAAAC,GAC4C,KAAzC,OAA5BD,EAAAD,EAAKG,0BAAqC,OAAdF,EAA5BA,EAA8BG,mBAAY,EAA1CH,EAA4CI,uBACO,eAAzB,OAA5BH,EAAAF,EAAKG,0BAAqC,OAAdD,EAA5BA,EAA8BE,mBAAY,EAA1CF,EAA4C/G,QAC5C4G,GAAkBC,EAAKG,wBAAwBG,MAK3CN,EAAKO,0BAA4B,IACnCR,GAAkBC,EAAKO,2BAG7B,CAEA,GAAIR,EAAiB,EAAG,CACtB,MAAMS,EAAarM,KAAKoB,WAAWC,cAA2B,cAC9DgL,EAAWzK,QAAQsI,UAAYb,EAAKU,YAAc6B,GAAgBhJ,WAClEyJ,EAAWzK,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC/CkC,EAAWzK,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAEvBpK,KAAKoB,WAAWC,cAA2B,mBACnDsG,QAAS,CAC7B,KAAO,CACqB3H,KAAKoB,WAAWC,cAA2B,mBACnDsG,QAAS,CAC7B,CACF,CAoBQ,eAAA2C,CAAgBgC,GAGtB,MAAMC,EAAmB,IAAIC,IAC7B,IAAK,MAAMX,KAAQS,EAAS,KAAAG,EACE,OAA5BA,EAAIZ,EAAKa,sBAALD,EAA0BE,YAC5BJ,EAAiB1C,IAAIgC,EAAKa,oBAAoBC,WAElD,CAEA,MAAMC,EAA0BN,EAAQO,KACtChB,GAAgD,SAAxCA,EAAKiB,WAAWC,qBAEpBC,EAAW3N,SAAS4N,yBAC1B,IAAK,MAAMpB,KAAQS,EAAS,KAAAY,EAC1B,IAA4B,OAA5BA,EAAIrB,EAAKa,uBAALQ,EAA0BP,cAOH,MAAvBC,OAAuB,EAAvBA,EAAyBO,OAAQtB,EAAKsB,IAAK,CAC7C,MAAMC,EAAUpN,KAAKqN,kBAAkBxB,GACvCuB,EAAQ1F,KAAO,aACX6E,EAAiBe,IAAIzB,EAAKsB,OAC5BC,EAAQxL,QAAQ2L,SAAW,OAC3BvN,KAAKwN,iBAAiBJ,EAASvB,EAAKsB,IAAKb,IAG3CU,EAASS,YAAYL,EACvB,CACF,CAEA,GAAIR,EAAyB,CAC3B,MAAMQ,EAAUpN,KAAKqN,kBAAkBT,GAMvC,GALAQ,EAAQ1F,KAAO,aAKgD,SAA3DkF,EAAwBE,WAAWC,qBACG,IAAxCH,EAAwBc,YAAmB,CAC3CN,EAAQxL,QAAQ+L,mBAAqB,OAGrC,MAAMC,EAAYvO,SAAS2D,iBAAiB,yCAAyCC,OACrFmK,EAAQxL,QAAQiM,eAAiBD,EAAY,EAAI,OAAS,QAM1D,MAAME,EAAeC,SAASnB,EAAwBE,WAAWkB,0BAC/D,IACEF,EAAe,IACjBV,EAAQxL,QAAQqM,WAAaH,EAAelB,EAAwBhC,UAAUhI,WAElF,CAEAoK,EAASS,YAAYL,EACvB,CAEA,IAAK,MAAMc,KAAOlO,KAAKgD,iBAAiB,gCACtCkL,EAAIpM,SAGN9B,KAAKyN,YAAYT,EACnB,CAOQ,cAAAxC,CAAe2D,GACrB,MAAMC,EAAUD,EAASA,EAASlL,OAAS,GAC3C,IAAKmL,EACH,OAGF,MAAMC,EAAWrO,KAAKqB,cACpB,mCAAmC+M,EAAQE,gBACzCD,IACFA,EAASzM,QAAQ2M,MAAQ,OAE7B,CAEQ,gBAAAf,CAAiBgB,EAA4BC,EAAmBnF,GACtE,IAAK,MAAMuC,KAAQvC,EAAO,KAAAoF,EACxB,IAA4B,OAAxBA,EAAA7C,EAAKa,0BAAmB,EAAxBgC,EAA0B/B,cAAe8B,EAAW,CACtD,MAAME,EAAe3O,KAAKqN,kBAAkBxB,GAC5C8C,EAAa/E,UAAUC,IAAI,cAC3B2E,EAAcf,YAAYkB,EAC5B,CACF,CACF,CAEQ,iBAAAtB,CAAkBxB,GAAY,IAAA+C,EAAAC,EAAAC,EAAAC,EACpC,MAAMC,EAAa3P,SAAS4P,cAAc,kBAoC1C,GAnCAD,EAAWzF,KAAO,WAClByF,EAAWpN,QAAQ8H,YAAc1J,KAAK4B,QAAQ8H,YAC9CsF,EAAWpN,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC/C6E,EAAWpN,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eACjD4E,EAAWpN,QAAQsN,SAAWrD,EAAKsD,MACnCH,EAAWpN,QAAQwN,SAAWvD,EAAKwD,MACnCL,EAAWpN,QAAQ0N,YAAc,OACjCN,EAAWpN,QAAQ2N,WAAa1D,EAAKsB,IACrC6B,EAAWpN,QAAQ4N,UAAY3D,EAAKyC,WAAW1L,WAC/CoM,EAAWpN,QAAQyN,MAAQxD,EAAKwD,MAChCL,EAAWpN,QAAQgJ,SAAWiB,EAAKjB,SAAShI,WAC5CoM,EAAWpN,QAAQ6N,YAAc5D,EAAK6D,aACtCV,EAAWpN,QAAQ+N,WAAa9D,EAAKtI,IACrCyL,EAAWpN,QAAQuK,MAAQN,EAAK6B,YAAY9K,WAC5CoM,EAAWpN,QAAQqM,UAAYpC,EAAK+D,oBAAoBhN,WACxDoM,EAAWpN,QAAQiO,uBAAyBhE,EAAKiE,iBAAiBlN,WAEtC,OAA5BgM,EAAI/C,EAAKa,sBAALkC,EAA0BjC,aAC5BqC,EAAWpN,QAAQmO,SAAWlE,EAAKa,oBAAoBC,YAGtB,SAA/Bd,EAAKiB,WAAWkD,aAClBhB,EAAWpN,QAAQqO,SAAW,OAC9BjB,EAAWpN,QAAQsO,YAAc,KAG/BrE,EAAKsE,SACPnB,EAAWpN,QAAQwO,cAAgBvE,EAAKsE,QAGtCtE,EAAKwE,aACPrB,EAAWpN,QAAQ0O,UAAYzE,EAAKwE,WAAWzN,aAI7CiJ,EAAKG,yBAAoD,OAAzB6C,EAAChD,EAAKa,sBAALmC,EAA0BlC,YA4BxD,GAAId,EAAKiB,WAAkC,wBAA8B,OAAzBgC,EAACjD,EAAKa,uBAALoC,EAA0BnC,YAAY,CAI5F,IAAI4D,EACJ,IAGEA,EAFsDC,KAAKC,MACzD5E,EAAKiB,WAAkC,uBACZ,GAAG4D,EAClC,CAAE,MAAO1O,GACPC,QAAQC,KAAK,gCAAiC2J,EAAKiB,WAAkC,sBACnF9K,EACJ,CAEA,GAAIuO,EAAe,CACjB,MAAMI,EAAgBtR,SAAS4P,cAAc,UAC7C0B,EAAc7F,YAAc,mBAK5B6F,EAAc/O,QAAQ2O,cAAgBA,EAAc3N,WACpD+N,EAAcjJ,KAAO,uBACrBiJ,EAAc/G,UAAUC,IAAI,2BAC5B8G,EAAc/O,QAAQgP,WAAa,4BACnC5B,EAAWvB,YAAYkD,EACzB,CACF,OAnDE,GAHA3B,EAAWpN,QAAQ2O,cAAgB1E,EAAKG,wBAAwBC,aAAayE,GAAG9N,WAChFoM,EAAWpN,QAAQiP,qBACjBhF,EAAKG,wBAAwBC,aAAajH,KAAK8L,QAAQ,kBAAmB,IACxEjF,EAAKiB,WAAkC,sBAAG,CAC5C,MAAMiE,EAAS1R,SAAS4P,cAAc,UACtC8B,EAAOnH,UAAUC,IAAI,wBACrBkH,EAAOrJ,KAAO,uBAEd,MAAMsJ,EAAUR,KAAKC,MAAM5E,EAAKiB,WAAkC,uBAIlE,GAoN2B/N,EApNGiS,EAqN7BC,MAAMC,QAAQnS,IAAUA,EAAMoS,OAAOtF,GAAQA,EAAK6E,IAAM7E,EAAK7G,MAAM/B,SAAWlE,EAAMkE,OArN7C,CACtC,IAAK,MAAMmO,KAAUJ,EAAS,CAC5B,MAAMK,EAAWhS,SAAS4P,cAAc,UACxCoC,EAAStS,MAAQqS,EAAOV,GAAG9N,WAC3ByO,EAASvG,YAAcsG,EAAOpM,KAC1BoM,EAAOV,KAAO7E,EAAKG,wBAAwBC,aAAayE,KAC1DW,EAASC,UAAW,GAGtBP,EAAOtD,YAAY4D,EACrB,CAEArC,EAAWvB,YAAYsD,EACzB,CACF,CAsMN,IAAmChS,EAxK/B,MAAMwS,EAoGV,SAA0B1F,EAAYzB,GACpC,MAAMmH,EAA8B,GAIQ,SAAxC1F,EAAKiB,WAAWC,qBAAuD,IAArBlB,EAAK6B,aACzD6D,EAASC,KAAK,CAAEvS,KAAM,OAAQ4H,QAAS,cASzC,IAAK,MAAM4K,KAAc5F,EAAK6F,gCAC5B,GAAID,EAAWE,qBAAqBtC,OAASoC,EAAWG,OAAS,EAAG,CAGlE,MAAM7S,GAAS0S,EAAWG,OAAS,KAAKC,QAAQ,GAEhDN,EAASC,KAAK,CAAEvS,KAAM,WACpB4H,QAAS,GAAG4K,EAAWE,qBAAqBtC,UAAUjF,IAAiBrL,MAC3E,MAAO,GAA0B,IAAtB0S,EAAWG,QAAgB,QAAQE,KAAKL,EAAWE,qBAAqBtC,OAAQ,CACzF,MAAM0C,EAAON,EAAWE,qBAAqBtC,MAE7C,IAAIxI,EAEFA,EADoB,IAAlBgF,EAAKjB,SACG,8CAA8CmH,IAE9C,0BAA0BlG,EAAKjB,+BAA+BmH,IAG1ER,EAASC,KAAK,CAAEvS,KAAM,cAAe4H,WACvC,CAGwC,SAAtCgF,EAAKiB,WAAWkF,mBAClBT,EAASC,KAAK,CAAEvS,KAAM,cACpB4H,QAAS,8DAG0B,WAAnCgF,EAAKiB,WAAWmF,gBAClBV,EAASC,KAAK,CAAEvS,KAAM,UACpB4H,QAAS,+DAG0B,oBAAnCgF,EAAKiB,WAAWmF,gBAClBV,EAASC,KAAK,CAAEvS,KAAM,UACpB4H,QAAS,8HAIuB,SAAhCgF,EAAKiB,WAAWoF,aAClBX,EAASC,KAAK,CAAEvS,KAAM,UAAW4H,QAAS,qDAGxCgF,EAAKiB,WAAWqF,oBAClBZ,EAASC,KAAK,CAAEvS,KAAM,cAAe4H,QAASgF,EAAKiB,WAAWqF,qBAG5DtG,EAAKiB,WAAWsF,UAClBb,EAASC,KAAK,CAAEvS,KAAM,cAAe4H,QAASgF,EAAKiB,WAAWsF,WAGhE,OAAOb,CACT,CAtKqBc,CAAiBxG,EAAM7L,KAAK4B,QAAQwI,gBAGrD,GAAImH,EAAStO,OAAS,IAA8B,OAAzB8L,EAAClD,EAAKa,uBAALqC,EAA0BpC,YAAY,CAChE,MAEM2F,EAFatS,KAAKoB,WAAWC,cAAmC,iBAC9BkR,QAAQC,WAAU,GACvCnR,cAAc,MAEjC,IAAK,MAAMwF,KAAW0K,EAAU,CAC9B,MAEMkB,EAFezS,KAAKoB,WAAWC,cAAmC,YAC5BkR,QAAQC,WAAU,GACrCnR,cAAc,MACvCoR,EAAO7I,UAAUC,IAAIhD,EAAQ5H,MAETwT,EAAOpR,cAA+B,YAC9CI,UAAYoF,EAAQA,QAEhCyL,EAAG7E,YAAYgF,EACjB,CAEAzD,EAAWvB,YAAY6E,EACzB,CAIA,OAFAtD,EAAWpN,QAAQ8Q,OAAShQ,KAAKC,MAAMC,WAEhCoM,CACT,CAMQ,wBAAA5O,CAAyB6D,GAC/BjE,KAAK2S,cACP,CAMQ,0BAAArS,CAA2B2D,GACjCjE,KAAKqK,cACP,CAUQ,YAAAA,GAIN,MAAMuI,EAAW5S,KAAKoB,WAAWC,cAA2B,kBACxDuR,IACFA,EAASjL,QAAS,EAEtB,CAEQ,YAAAgL,GAIN,MAAMC,EAAW5S,KAAKoB,WAAWC,cAA2B,kBACxDuR,IACFA,EAASjL,QAAS,EAEtB,CAEQ,0BAAAa,CAA2BvE,GACXjE,KAAKoB,WAAWC,cACpC,+BACYsG,QAAS,EAGG3H,KAAKoB,WAAWC,cAA2B,wBACnDsG,QAAS,CAC7B,EAGF,SAASgC,EAAMkC,GAAY,IAAAgH,EACzB,MAAwC,UAAlB,OAAfA,EAAAhH,EAAKiB,iBAAU,EAAf+F,EAA2B,QACpC,CCtNA,SAASC,EAAaC,GACpB,IACE,OAAOA,EAASC,QAAQ3N,IAAI,eAC9B,CAAE,MAAOrD,GACP,CAEJ,CA+BA,SAASiR,EAAWlU,GAClB,OAAc,OAAVA,GAAmC,iBAAVA,IAIL,kBAAbA,EAAMmU,KAIW,iBAAjBnU,EAAMoU,SAIS,mBAAfpU,EAAMqU,MAIS,mBAAfrU,EAAMsU,OAKnB,CAYA,SAASC,EAActR,EAAYmL,EAAapO,GAC9C,IACEiD,EAAMmL,GAAOpO,CACf,CAAE,MAAOiD,GACP,CAEJ,CAcA,SAASuR,EAAWC,GAClB,OAAO,IAAIhQ,IAAIgQ,EAAM,GAAGzP,SAAS0P,aAAa1P,SAAS2P,OACzD,CA8VOC,eAAeC,EAAWC,GAC/B,MAAMtQ,EAAMgQ,EAAW9P,QAAQC,OAAOC,KAAO,kBAEvCmQ,EAAoB,CACxBC,KAAMvD,KAAKwD,UAAUH,GACrBI,MAAO,WACPjB,QAAS,CACP,eAAgB,oBAElBkB,OAAQ,QAGV,IAAInB,EACJ,IACEA,QAAiBoB,MAAM5Q,EAAKuQ,EAC9B,CAAE,MAAO9R,GAGP,MAFAsR,EAActR,EAAO,QAAS6R,GAC9BP,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAKiR,EAAWF,GAAW,CACzB,MAAM/Q,EAAa,IAAIoS,MAAM,iDAI7B,MAHAd,EAActR,EAAO,eAAgB6R,GACrCP,EAActR,EAAO,WAAY+Q,GACjCO,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAK+Q,EAASG,GAAI,KAAAmB,EAAAC,EAChB,MAAMtS,EAAa,IAAIoS,MAAM,mBAAmBrB,EAASI,wBACzDG,EAActR,EAAO,eAAgB6R,GACrCP,EAActR,EAAO,aAAc8Q,EAAaC,IAEhD,IACEO,EAActR,EAAO,sBAAuB+Q,EAASM,OACvD,CAAE,MAAOkB,GACP,CAcF,IAAI1N,EAWJ,MAtBAyM,EAActR,EAAO,kBAAmB+Q,EAASI,QACjDG,EAActR,EAAO,eAAgB+Q,EAASxP,KAWtB,MAApBwP,EAASI,QAA8D,iBAAlB,OAA1BkB,EAAOrS,EAAMwS,oBAAa,EAAnBH,EAAqBI,aACzD5N,EAAU7E,EAAMwS,cAAcC,YACmB,iBAAd,OAA1BH,EAAOtS,EAAMwS,oBAAa,EAAnBF,EAAqBzN,WACrCA,EAAU7E,EAAMwS,cAAc3N,SAG5BA,GACFyM,EAActR,EAAO,UAAW6E,GAG5B7E,CACR,CAEA,OAAO+Q,CACT,CAUOY,eAAee,IACpB,MAAMnR,EAAMgQ,EAAW9P,QAAQC,OAAOC,KAAO,WAE7C,IAAIoP,EAuCA4B,EASAC,EA/CJ,IACE7B,QAAiBoB,MAAM5Q,EAAK,CAAE0Q,MAAO,YACvC,CAAE,MAAOjS,GAEP,MADAsR,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAKiR,EAAWF,GAAW,CACzB,MAAM/Q,EAAa,IAAIoS,MAAM,iDAG7B,MAFAd,EAActR,EAAO,WAAY+Q,GACjCO,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAK+Q,EAASG,GAAI,CAChB,MAAMlR,EAAa,IAAIoS,MAAM,mBAAmBrB,EAASI,wBACzDG,EAActR,EAAO,aAAc8Q,EAAaC,IAChDO,EAActR,EAAO,SAAU+Q,EAASI,QACxCG,EAActR,EAAO,MAAO+Q,EAASxP,KAErC,IACE+P,EAActR,EAAO,sBAAuB+Q,EAASM,OACvD,CAAE,MAAOkB,GACP,CAGF,MAAMvS,CACR,CAYA,IACE2S,QAAyB5B,EAASK,MACpC,CAAE,MAAOpR,GAGP,MAFAsR,EAActR,EAAO,SAAU+Q,EAASI,QACxCG,EAActR,EAAO,MAAO+Q,EAASxP,KAC/BvB,CACR,CAGA,IACE4S,EAAqBpE,KAAKC,MAAMkE,EAClC,CAAE,MAAO3S,GAIP,MAHAsR,EAActR,EAAO,gBAAiB2S,GACtCrB,EAActR,EAAO,SAAU+Q,EAASI,QACxCG,EAActR,EAAO,MAAO+Q,EAASxP,KAC/BvB,CACR,CAEA,IAAK6S,EAAOD,GAAqB,CAC/B,MAAM5S,EAAa,IAAIoS,MAAM,qCAI7B,MAHAd,EAActR,EAAO,gBAAiB4S,GACtCtB,EAActR,EAAO,SAAU+Q,EAASI,QACxCG,EAActR,EAAO,MAAO+Q,EAASxP,KAC/BvB,CACR,CAEA,OAAO4S,CACT,CA4GOjB,eAAemB,EAAWjB,GAC/B,MAAMtQ,EAAMgQ,EAAW9P,QAAQC,OAAOC,KAAO,kBAEvCmQ,EAAoB,CACxBC,KAAMvD,KAAKwD,UAAUH,GACrBI,MAAO,WACPjB,QAAS,CACP,eAAgB,oBAElBkB,OAAQ,QAGV,IAAInB,EACJ,IACEA,QAAiBoB,MAAM5Q,EAAKuQ,EAC9B,CAAE,MAAO9R,GAGP,MAFAsR,EAActR,EAAO,QAAS6R,GAC9BP,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAKiR,EAAWF,GAAW,CACzB,MAAM/Q,EAAa,IAAIoS,MAAM,iDAI7B,MAHAd,EAActR,EAAO,eAAgB6R,GACrCP,EAActR,EAAO,WAAY+Q,GACjCO,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAK+Q,EAASG,GAAI,KAAA6B,EAAAC,EAChB,MAAMhT,EAAa,IAAIoS,MAAM,mBAAmBrB,EAASI,wBACzDG,EAActR,EAAO,eAAgB6R,GACrCP,EAActR,EAAO,aAAc8Q,EAAaC,IAChDO,EAActR,EAAO,kBAAmB+Q,EAASI,QAEjD,IAEEG,EAActR,EAAO,sBADiB+Q,EAASM,OAEjD,CAAE,MAAOkB,GACP,CAWF,IAAI1N,EAWJ,MAVwB,MAApBkM,EAASI,QAA8D,iBAAlB,OAA1B4B,EAAO/S,EAAMwS,oBAAa,EAAnBO,EAAqBN,aACzD5N,EAAU7E,EAAMwS,cAAcC,YACmB,iBAAd,OAA1BO,EAAOhT,EAAMwS,oBAAa,EAAnBQ,EAAqBnO,WACrCA,EAAU7E,EAAMwS,cAAc3N,SAG5BA,GACFyM,EAActR,EAAO,UAAW6E,GAG5B7E,CACR,CAEA,OAAO+Q,CACT,CA2BM,SAAUkC,EAAmBjT,GACjC,GAAIA,aAAiBoS,OAAS,kBAAmBpS,GAASkT,EAASlT,EAAMwS,gBACxC,MAA/BxS,EAAMwS,cAAcrB,OAAgB,CACpC,MAAMsB,EAAczS,EAAMwS,cAAcC,YACxC,GAA2B,iBAAhBA,EAA0B,CAEnC,GAAIA,EAAYU,WAAW,UAAYV,EAAYW,SAAS,gBAC1D,OAAO,EAIT,GAAIX,EAAYW,SAAS,oBACvB,OAAO,CAEX,CACF,CAEA,OAAO,CACT,CAoGM,SAAUP,EAAO9V,GACrB,OAAOmW,EAASnW,IAAUkS,MAAMC,QAAQnS,EAAMuK,MAChD,CAEA,SAAS4L,EAASnW,GAChB,MAAwB,iBAAVA,GAAgC,OAAVA,CACtC,CC16CO,SAASsW,EAAOhM,GACrB,GAAa,OAATA,EAAe,CAEjB,MADoC,CAAEiM,gBAAiB,OAEzD,CAEA,GAAoB,iBAATjM,EAAmB,CAE5B,MADoC,CAAEiM,uBAAwBjM,EAEhE,CAEA,IAAI,IAAAiC,EAAAiK,EAAAC,EAgBF,MAfoC,CAClCC,MAAOpM,EAAKoM,MACZC,eAAgBC,OAAOC,KAAoB,OAAhBtK,EAACjC,EAAKkC,YAAUD,EAAI,CAAC,GAChDuK,eAAgBxM,EAAKwM,eACrBC,2BAAiE,OAAvCP,EAAElM,EAAK0M,uCAAgC,EAArCR,EAAuCS,IACjEC,GAA0B,MAAXA,OAAW,EAAXA,EAAa5G,OAC9B/F,MAAiB,OAAZkM,EAAEnM,EAAKC,YAAK,EAAVkM,EAAYQ,IAAInK,IAAI,IAAAqK,EAAA,MAAK,CAC9B/I,IAAS,MAAJtB,OAAI,EAAJA,EAAMsB,IACXmB,WAAgB,MAAJzC,OAAI,EAAJA,EAAMyC,WAClB1D,SAAc,MAAJiB,OAAI,EAAJA,EAAMjB,SAChBkC,WAAgB,MAAJjB,OAAI,EAAJA,EAAMiB,WAClBqJ,UAAe,MAAJtK,GAAqC,OAAjCqK,EAAJrK,EAAM6F,sCAA+B,EAArCwE,EAAuCF,IAChDvE,IAAU,IAAA2E,EAAA,OAAc,MAAV3E,GAAgC,OAAtB2E,EAAV3E,EAAYE,2BAAoB,EAAhCyE,EAAkC/G,WAIxD,CAAE,MAAOrN,GACP,MAAO,CAAEqU,cAAe,GAAGrU,IAC7B,CACF,CCdO,SAASsU,EAAgBC,EAAaC,GAM3C,IAAK3B,EAAO0B,GACV,OAAO,EAGT,IAAK1B,EAAO2B,GACV,OAAO,EAUT,GAAID,EAAMd,OAASe,EAAMf,OAASc,EAAMd,QAAUe,EAAMf,MACtD,OAAO,EAKT,GAAIc,EAAMjN,MAAMrG,SAAWuT,EAAMlN,MAAMrG,OACrC,OAAO,EAaT,IAAK,MAAMwT,KAASF,EAAMjN,MAAO,CAC/B,IAAIoN,GAAQ,EACZ,IAAK,MAAMC,KAASH,EAAMlN,MACxB,GAAImN,EAAMtJ,MAAQwJ,EAAMxJ,KAAOsJ,EAAM7L,WAAa+L,EAAM/L,SAAU,CAChE8L,GAAQ,EACR,KACF,CAGF,IAAKA,EACH,OAAO,CAEX,CAEA,OAAO,CACT,CCtDO,SAASE,EAAc5U,EAAYmL,EAAapO,GACrD,IACEiD,EAAMmL,GAAOpO,CACf,CAAE,MAAO8X,GACP,IAAIC,EAEFA,EADED,aAA2BzC,MACpByC,EAAgBhQ,QAEhB,GAAGgQ,IAGd5U,QAAQK,IAAI,kCAAmC,CAC7C6K,MACAnI,KAAW,MAALhD,OAAK,EAALA,EAAOgD,KACb+R,IAAKpB,OAAOqB,UAAUpU,SAASqU,KAAKjV,GACpC8U,UAEJ,CACF,CCuBO,SAASI,EAAOC,EAAgChT,IAkBvD,SAAiCgT,EAC/BhT,GASA,IAAIiT,EAGAA,EAFAD,aAAsBE,QACpBF,EAAWzV,YACJyV,EAEAG,OAEFH,GAGAG,OAGX,MAAMvV,EAAQ,IAAIuC,YAA2B,oBAAqB,CAChEH,SACAI,SAAS,EACTC,UAAU,IAGZ4S,EAAOzS,cAAc5C,EACvB,CA/CEwV,CAAwBJ,EAAYhT,GA2EtC,SAA2BA,GAUzB,MAAMqT,EAAe,CACnB,kBACA,gBACA,6BACA,yBACA,kBACA,eACA,mBAGF,GAAIrT,IAAWqT,EAAapC,SAASjR,EAAO4N,MAAO,KAAA0F,EAAAC,EACjD,IAAI1V,EAGFA,EADEmC,EAAOnC,iBAAiBoS,MAClBjQ,EAAOnC,MAEP,IAAIoS,MAAM,GAAGjQ,EAAOnC,SAG9B4U,EAAc5U,EAAO,OAAQmC,EAAO4N,MACpC6E,EAAc5U,EAAO,SAAUmC,EAAOwT,QACtCf,EAAc5U,EAAO,WAAYmC,EAAOoG,UACxCqM,EAAc5U,EAAO,OAAQmC,EAAOkF,MACpCuN,EAAc5U,EAAO,iBAAoC,OAApByV,EAAEtT,EAAOyT,kBAAW,EAAlBH,EAAoB/G,IAC3DkG,EAAc5U,EAAO,mBAAsC,OAApB0V,EAAEvT,EAAOyT,kBAAW,EAAlBF,EAAoB1S,MAElC,mBAAhB6S,YACTA,YAAY7V,GAEZxC,WAAWsY,EAAO,EAAG9V,EAEzB,CACF,CApHE+V,CAAkB5T,EACpB,CAqHA,SAAS2T,EAAM/Y,GACb,MAAMA,CACR,CC7LM,SAAUiZ,EAAOC,EAAqBpR,GAC1C,IAAKoR,EACH,MAAM,IAAIC,eAAerR,EAE7B,CN+0BKzB,eAAeC,IAAI,iBACtBD,eAAeE,OAAO,eAAgB4C,aM90BlC,MAAOgQ,uBAAuB9D,MAClC,WAAA+D,CAAYtR,QAAO,IAAPA,IAAAA,EAAU,mBACpBuR,MAAMvR,GACN7G,KAAKgF,KAAOhF,KAAKmY,YAAYnT,IAC/B,ECjBF,MAAMqT,EAAQ,IAAIC,WAAW,ICC7B,MAAMC,EAAY,GAClB,IAAK,IAAIC,EAAI,EAAGA,EAAI,MAAOA,EACvBD,EAAU/G,MAAMgH,EAAI,KAAO5V,SAAS,IAAI6V,MAAM,IAE3C,SAASC,EAAgBC,EAAKC,GACjC,YADuC,IAANA,IAAAA,EAAS,IAClCL,EAAUI,EAAIC,EAAS,IAC3BL,EAAUI,EAAIC,EAAS,IACvBL,EAAUI,EAAIC,EAAS,IACvBL,EAAUI,EAAIC,EAAS,IACvB,IACAL,EAAUI,EAAIC,EAAS,IACvBL,EAAUI,EAAIC,EAAS,IACvB,IACAL,EAAUI,EAAIC,EAAS,IACvBL,EAAUI,EAAIC,EAAS,IACvB,IACAL,EAAUI,EAAIC,EAAS,IACvBL,EAAUI,EAAIC,EAAS,IACvB,IACAL,EAAUI,EAAIC,EAAS,KACvBL,EAAUI,EAAIC,EAAS,KACvBL,EAAUI,EAAIC,EAAS,KACvBL,EAAUI,EAAIC,EAAS,KACvBL,EAAUI,EAAIC,EAAS,KACvBL,EAAUI,EAAIC,EAAS,MAAMC,aACrC,CCEA,IAAAC,EA1BA,SAAY9H,EAAS+H,EAAKH,GACtB,OAAKG,GAAQ/H,IAAWgI,OAAOC,WAKnC,SAAajI,EAAS+H,EAAKH,GAAQ,IAAAM,EAAAC,EAC/BnI,EAAUA,GAAW,CAAC,EACtB,MAAMoI,EAAwC,OAApCF,EAAiB,OAAjBC,EAAGnI,EAAQqI,QAAMF,EAAe,MAAXnI,EAAQsI,SAAG,EAAXtI,EAAQsI,OAAOJ,EFRvCF,OAAOO,gBAAgBlB,GES9B,GAAIe,EAAKnW,OAAS,GACd,MAAM,IAAImR,MAAM,qCAIpB,GAFAgF,EAAK,GAAgB,GAAVA,EAAK,GAAa,GAC7BA,EAAK,GAAgB,GAAVA,EAAK,GAAa,IACzBL,EAAK,CAEL,IADAH,EAASA,GAAU,GACN,GAAKA,EAAS,GAAKG,EAAI9V,OAChC,MAAM,IAAIuW,WAAW,mBAAmBZ,KAAUA,EAAS,8BAE/D,IAAK,IAAIJ,EAAI,EAAGA,EAAI,KAAMA,EACtBO,EAAIH,EAASJ,GAAKY,EAAKZ,GAE3B,OAAOO,CACX,CACA,OAAOL,EAAgBU,EAC3B,CArBWK,CAAIzI,EAAS+H,EAAKH,GAFdI,OAAOC,YAGtB,ECGO,SAASS,EAAQ3a,GACtB,GAA+B,mBAApB4a,gBAAgC,CACzC,MAAMC,EAASD,gBAAgB5a,GAS/B,IAAI8a,GAAc,EAKlB,GARqB9a,SACA6a,UAInBC,GAAc,GAGZA,EACF,OAAOD,CAEX,CAEA,OAAUpJ,KAAKC,MAAMD,KAAKwD,UAAUjV,GACtC,CCrBO,SAAS+a,EAAa/a,GAC3B,GAAqB,iBAAVA,EACT,OAAO,EAGT,GAAqB,IAAjBA,EAAMkE,OACR,OAAO,EAIT,GAAIlE,EAAMqW,SAAS,MAAQrW,EAAMqW,SAAS,MACxC,OAAO,EAGT,GAAIrW,EAAMqW,SAAS,KACjB,OAAO,EAGT,MAAM2E,EAAQhb,EAAMib,cAGpB,MAAc,cAAVD,GAAmC,SAAVA,IC1BxB,SAAgChb,GACrC,MAAwB,iBAAVA,GAAsB,cAAc+S,KAAK/S,EACzD,CD4BMkb,CAAuBF,EAK7B,CAkBO,SAASG,EAA2B7Q,GACzC,OAAKA,GAIA4H,MAAMC,QAAQ7H,EAAKwM,gBAIjBxM,EAAKwM,eACT1E,OAAOY,GAAQA,EAAKoI,YACpBnE,IAAIjE,GAAQA,EAAKA,MACjBiE,IAAIjE,GAAQA,EAAKiI,eAVX,EAWX,CAoBO,SAASI,EAAiB/Q,GAC/B,OAAKA,GAIA4H,MAAMC,QAAQ7H,EAAKwM,gBAIjBxM,EAAKwM,eAAeG,IAAIjE,GAAQA,EAAKA,MAAMiE,IAAIjE,GAAQA,EAAKiI,eAP1D,EAQX,CEiGO,SAASK,EAAMlD,EAAgC9N,EAAYuO,EAChErN,EAAmB+P,QAAO,IAAPA,IAAAA,GAAU,GAC7B,MAAMC,EAAWD,EAAU,KAAOE,IAGlCxC,EAAOnD,EAAOxL,GAAO,6BACrBzK,eAAeyK,KAAOqQ,EAAKrQ,GAG3B,MAAMoR,EAAcC,EAAeH,GAC7BI,EAAcD,EAAerR,GAK7BuR,EAAYV,EAA2BK,GACvCM,EAAYX,EAA2B7Q,GAKvCtH,EAAQ,IAAIuC,YAAoB,eAAgB,CACpDH,OAAQ,CACNkF,OACAyR,qBAAsBC,EAAWF,EAAWD,GAC5CI,uBAAwBD,EAAWH,EAAWC,GAC9CI,SAAUnC,IACV1U,WAAYkW,EACZC,WACAhQ,WACAE,eAAgByQ,EAAkBT,EAAaE,GAC/CQ,iBAAkBC,EAAoBX,EAAaE,GACnD/C,YAAa,CACXlH,GAAIkH,EAAYlH,GAChB1L,KAAM4S,EAAY5S,OAGtBT,SAAS,EACTC,UAAU,IAUZ,IAAI4S,EAGAA,EAFAD,aAAsBE,QACpBF,EAAWzV,YACJyV,EAEAG,OAEFH,GAGAG,OAGXF,EAAOzS,cAAc5C,EACvB,CAOO,SAASyY,IAEd,OADAxC,EAAOnD,EAAOjW,eAAeyK,MAAO,wCAC7BqQ,EAAK9a,eAAeyK,KAC7B,CA4COsK,eAAe0H,EAAUrK,QAAqB,IAArBA,IAAAA,EAAwB,CAAC,GAGvD,MAAMsK,EAAQtK,EAAQuK,UAAY,EAM5BC,EAASxK,EAAQwK,QAAU,CAAC,IAAK,KAEvC,IAAIC,EAEJ,IAAK,IAAIF,EAAW,EAAGA,EAAWD,EAAOC,IAAY,CACnD,GAAIA,EAAW,EAAG,CAChB,MAAMG,EAAQF,EAAOD,EAAW,SAC1BI,EAAoBD,EAC5B,CAEA,IAEE,aADmBE,EAAyB5K,EAAQ6K,QAEtD,CAAE,MAAO7Z,GACPyZ,EAAYzZ,CACd,CACF,CAEA,MAAMyZ,CACR,CAMO,MAAMK,8BAA8B1H,MAGzC,WAAA+D,CAAY0D,GACVzD,MAAM,2BACNpY,KAAKgF,KAAOhF,KAAKmY,YAAYnT,KAC7BhF,KAAK6b,QAAUA,CACjB,EAWFlI,eAAeiI,EAAyBC,GACtC,IAAKA,EACH,OAAOnH,IAGT,IAAInV,EACJ,MAAMwc,EAAiB,IAAIC,QAAe,CAACC,EAAUC,KACnD,MAAMla,EAAQ,IAAI8Z,sBAAsBD,GACxCtc,EAAQC,WAAW0c,EAAQL,EAAS7Z,KAGtC,IAEE,aADmBga,QAAQG,KAAK,CAACzH,IAAiBqH,GAEpD,CAAC,QACCtc,aAAaF,EACf,CACF,CAKA,SAASoc,EAAoBS,GAC3B,OAAO,IAAIJ,QAAQK,IACjB7c,WAAW6c,EAASD,IAExB,CA4BA,SAAS1B,EAAerR,GACtB,MAAMiT,EAA4B,CAAC,EAMnC,IAAKjT,EACH,OAAO,KAGT,IAAK,MAAMwC,KAAQxC,EAAKC,MAAO,CAC7B,MAAM8E,EAAUkO,EAAOzQ,EAAKyC,WAAW1L,YACvC,GAAIwL,EAAS,CAEX,IAAK,MAAMmO,KAAY1Q,EAAKiB,YAAc,CAAC,EAAG,CAC5CsB,EAAQoO,kBAAkBD,GAAYnO,EAAQoO,kBAAkBD,IAAa,GAC7E,MAAMxd,EAAQ8M,EAAKiB,WAAWyP,GACzBnO,EAAQoO,kBAAkBD,GAAUnH,SAASrW,IAChDqP,EAAQoO,kBAAkBD,GAAU/K,KAAKzS,EAE7C,CAEAqP,EAAQxD,UAAYiB,EAAKjB,QAC3B,KAAO,CAEL,MAAMkC,EAAuC,CAAC,EAC9C,IAAK,MAAMK,KAAOtB,EAAKiB,YAAc,CAAC,EACpCA,EAAWK,GAAO,CAACtB,EAAKiB,WAAWK,IAGrCmP,EAAOzQ,EAAKyC,WAAW1L,YAAc,CACnC6Z,UAAW5Q,EAAKsD,MAChBqN,kBAAmB1P,EAEnBX,MAAON,EAAKM,MACZkE,WAAYxE,EAAKwE,WACjBqM,cAAe7Q,EAAK6Q,cACpBhN,aAAc7D,EAAK6D,aACnB9E,SAAUiB,EAAKjB,SACf+R,IAAK9Q,EAAK8Q,IACVpZ,IAAKsI,EAAKtI,IACV+K,WAAYzC,EAAKyC,WACjBsO,cAAe/Q,EAAK+Q,cAExB,CACF,CAEA,OAAON,CACT,CAWA,SAASpB,EAAkB2B,EAA0BC,GAOnD,IAAKD,EACH,MAAO,GAQT,MAAME,EAAgC,CAAC,EACvC,IAAK,MAAM5P,KAAO2P,EACZD,EAAM1P,GAGJ2P,EAAM3P,GAAKvC,SAAWiS,EAAM1P,GAAKvC,WACnCmS,EAAW5P,GAAOuM,EAAKoD,EAAM3P,IAC7B4P,EAAW5P,GAAKvC,SAAWkS,EAAM3P,GAAKvC,SAAWiS,EAAM1P,GAAKvC,UAO9DmS,EAAW5P,GAAOuM,EAAKoD,EAAM3P,IAIjC,OAAOwI,OAAOC,KAAKmH,GAAY/G,IAAI7I,GAAO4P,EAAW5P,GACvD,CAEA,SAASiO,EAAoByB,EAA0BC,GACrD,IAAKD,EACH,MAAO,GAGT,MAAMG,EAAgC,CAAC,EACvC,IAAK,MAAM7P,KAAO0P,EACZC,EAAM3P,GACJ2P,EAAM3P,GAAKvC,SAAWiS,EAAM1P,GAAKvC,WACnCoS,EAAW7P,GAAOuM,EAAKoD,EAAM3P,IAC7B6P,EAAW7P,GAAKvC,SAAWiS,EAAM1P,GAAKvC,SAAWkS,EAAM3P,GAAKvC,UAM9DoS,EAAW7P,GAAOuM,EAAKmD,EAAM1P,IAIjC,OAAOwI,OAAOC,KAAKoH,GAAYhH,IAAI7I,GAAO6P,EAAW7P,GACvD,CAQA,SAAS4N,EAAWkC,EAAaC,GAG/B,OAFAlF,EAAO/G,MAAMC,QAAQ+L,GAAI,4CACzBjF,EAAO/G,MAAMC,QAAQgM,GAAI,4CAClBD,EAAE9L,OAAOpS,IAAUme,EAAE9H,SAASrW,GACvC,CCngBA,SAASoe,IACP7F,OAAO8F,iBAAmB9F,OAAO8F,kBAAoB,CAAEC,YAAa,EAAGC,MAAO,EAAGC,SAAU,IAE3F,MAAMC,EAASlG,OAAO8F,iBActB,QAZKK,OAAOC,UAAUF,EAAOH,cAAgBG,EAAOH,YAAc,KAChEG,EAAOH,YAAc,GAGlBI,OAAOC,UAAUF,EAAOF,SAC3BE,EAAOF,MAAQ,GAGZrM,MAAMC,QAAQsM,EAAOD,YACxBC,EAAOD,SAAW,IAGbC,CACT,CA0DO,SAASvY,EAAMD,GACpB,MAAMwY,EAASL,IAKfK,EAAOF,QAAQ,QAAAK,EAAAC,UAAA3a,OANsB4a,EAAI,IAAA5M,MAAA0M,EAAA,EAAAA,EAAA,KAAAG,EAAA,EAAAA,EAAAH,EAAAG,IAAJD,EAAIC,EAAA,GAAAF,UAAAE,GAQzC,MAAMlG,EAA2B,CAC/BiG,OACAE,MAAOrb,KAAKC,MACZ+N,GAAI8M,EAAOF,MACXtY,OACAgZ,WAAW,EACXC,YAAatb,IAOfiV,SAAuB,SAACsG,GAAkB,YAAR,IAAVA,IAAAA,GAAa,GAAUC,EAASvG,EAAasG,EAAW,GAIhFV,EAAOH,cAMHG,EAAOH,YAAc,GAkG3B,SAA2BzF,GACzB,MAUMwG,EAAW,IAVHjB,IAUaI,UAE3Btb,QAAQC,KAAK,iCAAkC,CAC7CwO,GAAIkH,EAAYlH,GAChB1L,KAAM4S,EAAY5S,KAClBuY,SAAUa,GAEd,CAnHIC,CAAkBzG,GAMpB0G,EAAO,CAAEP,MAAOnG,EAAYmG,MAAOrN,GAAIkH,EAAYlH,GAAI1L,KAAM4S,EAAY5S,KAAM/F,KAAM,UAMrF2Y,EAAYrY,MAAQC,WAAW2e,EAAU,IAAWvG,GAAa,GAEjE3V,QAAQK,IAAI,2BAA4BsV,EAAY5S,MAGpD,MAAMjD,EAAQ,IAAIuC,YAAoB,2BAA4B,CAAEH,OAAQyT,IAG5E,OAFAjT,cAAc5C,GAEP6V,CACT,CAwBA,SAASuG,EAASvG,EAA0BsG,GAK1C,QALoD,IAAVA,IAAAA,GAAa,GAKnDtG,EAAYoG,UAAW,CACzB,MAAMO,EAAWvX,KAAKwX,MAAM7b,IAAQiV,EAAYqG,aAEhD,YADAhc,QAAQK,IAAI,qCAAsCsV,EAAY5S,KAAMkZ,EAAYK,EAElF,CAEA3G,EAAYoG,WAAY,EAExBve,aAAamY,EAAYrY,OACzBqY,EAAYrY,WAAQkf,EAEpBH,EAAO,CACLP,MAAOrb,KAAKC,MACZ+N,GAAIkH,EAAYlH,GAChB1L,KAAM4S,EAAY5S,KAClB6W,QAASqC,EACTjf,KAAM,QAGR,MAAMue,EAASL,IAEXK,EAAOH,YAAc,GACvBG,EAAOH,cAGLa,GACFjc,QAAQK,IAAI,6BAA8BsV,EAAY5S,MAGxD/C,QAAQK,IAAI,6BAA8BsV,EAAY5S,MAItDD,EAAQ,QAAQ6S,EAAY5S,QAAQ4S,EAAYlH,KAAMkH,EAAYqG,YAAatb,IAAO,CACpF+N,GAAIkH,EAAYlH,GAChB1L,KAAM4S,EAAY5S,KAClB6W,QAASqC,IAIX,MAAMnc,EAAQ,IAAIuC,YAAoB,6BAA8B,CAAEH,OAAQyT,IAC9EjT,cAAc5C,EAChB,CA+BA,SAASuc,EAAOI,GACd,MACMnB,EADQJ,IACSI,SAEvBA,EAAS/L,KAAKkN,GASdnB,EAASoB,OAAO,EAAG3X,KAAKC,IAAIsW,EAASta,OAPpB,GAOuC,GAC1D,CCxQO0Q,eAAeiL,EAAOxH,EAA4BvD,EACvDtJ,GACA,MAAMqN,EAAc3S,EAAM,aAAc4O,GAExC,IAAI0G,EACAsE,EAEJ,IAQE,GALAtE,EAAWC,IAGXqE,QAAmBxD,KAEd/E,EAAgBiE,EAAUsE,GAAa,CAE1CxE,EAAMjD,EAAQyH,EAAYjH,EAAarN,GAKvC,MAAMvI,EAAa,IAAIoS,MAAM,4CAE7B,MADApS,EAAM8c,WAAa,sBACb9c,CACR,OAEM4R,EAAWC,GAEjBwG,EAAMjD,QADaiE,IACCzD,EAAarN,EACnC,CAAE,MAAOvI,GACPkV,EAAOE,EAAQ,CACbrF,KAAMgN,EAAmB/c,GACzBA,QACA2V,OAAQ,CACN9D,QACAmL,cAAe3J,EAAOkF,GACtB0E,aAAc5J,EAAOwJ,IAEvBtU,WACAqN,eAEJ,CAAC,QACCA,EAAYuG,UACd,CACF,CAEA,SAASY,EAAmB/c,GAC1B,OAAIiT,EAAmBjT,GACd,kBAGL,eAAgBA,EACXA,EAAM8c,WAGR,aACT,CCpEOnL,eAAeuL,EAAY7V,EAAY8V,GAC5C,MAAMC,EAAsB,GAC5B,IAAK,IAAI5G,EAAI,EAAGA,EAAInP,EAAKC,MAAMrG,OAAQuV,IAAK,CAC1C,MAAM3M,EAAOxC,EAAKC,MAAMkP,GACpB2G,EAAS/J,SAASvJ,EAAKsB,MACzBiS,EAAoB5N,KAAKgH,EAAI,EAEjC,CAEAR,EAAOoH,EAAoBnc,SAAWkc,EAASlc,OAAQ,mCAGvDmc,EAAoBC,UAMpB,IAAK,MAAMC,KAAQF,QACXxL,EAAW,CAAE0L,OAAM1U,SAAU,GAEvC,CCTO+I,eAAe4L,EAAelW,EAAYmW,EAC/CjV,GACA,GAAIiV,EACF,IACE,MAAMC,EAAeD,EAASnW,GAC9B,GAAIoW,EAAaxc,OAAS,EAAG,OACrBic,EAAY7V,EAAMoW,GAGxB,MAAMC,QAAmBrE,IAEzB,OADApZ,QAAQK,IAAI,mCAAoCiI,EAAUkV,GACnDC,CACT,CACF,CAAE,MAAO1d,GACPC,QAAQC,KAAK,gCAAiCqI,EAAUvI,EAC1D,CAGF,OAAOqH,CACT,CCDOsK,eAAe7R,EAAOsV,EAA4B+H,EAAoB5U,EAC3EiV,GACA,MAAM5H,EAAc3S,EAAM,cAAeka,EAAU5U,GACnD,IAEIgQ,EACAsE,EAHAc,EAAY,eAKhB,IACE3H,EAAOmH,EAASlc,OAAS,EAAG,uCAM5B+U,EAAOmH,EAASS,MAAMC,GAAW,iCA0CjC,GAtCA7H,EADqB,IAAIxL,IAAI2S,GACTW,OAASX,EAASlc,OAAQ,mCA+B9CsX,EAAWC,IAKXqE,QAAmBxD,KAEd/E,EAAgBiE,EAAUsE,GAK7B,MAHAxE,EAAMjD,EAAQyH,EAAYjH,EAAarN,GAEvCoV,EAAY,sBACN,IAAIvL,MAAM,6CAQlB4D,EAAOmH,EAASlc,QAAUsX,EAASjR,MAAMrG,OAAQ,4BAE3Cic,EAAY3E,EAAU4E,GAK5B,MAAMY,QAAoB1E,IAE1BhB,EAAMjD,QADamI,EAAeQ,EAAaP,EAAUjV,GACrCqN,EAAarN,EACnC,CAAE,MAAOvI,GACPkV,EAAOE,EAAQ,CACbrF,KAAM4N,EACN3d,QACA2V,OAAQ,CACNqI,UAAWb,EACXH,cAAe3J,EAAOkF,GACtB0E,aAAc5J,EAAOwJ,IAEvBtU,WACAqN,eAEJ,CAAC,QACCA,EAAYuG,UACd,CACF,CAEA,SAAS0B,EAAS9gB,GAChB,MAAwB,iBAAVA,CAChB,CCnHO4U,eAAejB,EAAO0E,EAA4BvD,EAAwBtJ,EAC/EiV,GACA,MAAM5H,EAAc3S,EAAM,aAAc4O,EAAOtJ,GAE/C,IAAIgQ,EACAsE,EAEJ,IAQE,GALAtE,EAAWC,IAGXqE,QAAmBxD,KAEd/E,EAAgBiE,EAAUsE,GAAa,CAE1CxE,EAAMjD,EAAQyH,EAAYjH,EAAarN,GAKvC,MAAMvI,EAAa,IAAIoS,MAAM,4CAE7B,MADApS,EAAM8c,WAAa,sBACb9c,CACR,OAEM8S,EAAWjB,GACjB,MAAMkM,QAAoB1E,IAG1BhB,EAAMjD,QADamI,EAAeQ,EAAaP,EAAUjV,GACrCqN,EAAarN,EACnC,CAAE,MAAOvI,GACPkV,EAAOE,EAAQ,CACbrF,KAAMkO,EAAmBje,GACzBA,QACA2V,OAAQ,CACN9D,QACAmL,cAAe3J,EAAOkF,GACtB0E,aAAc5J,EAAOwJ,IAEvBtU,WACAqN,eAEJ,CAAC,QACCA,EAAYuG,UACd,CACF,CAEA,SAAS8B,EAAmBje,GAC1B,OAAIiT,EAAmBjT,GACd,kBAGL,eAAgBA,EACXA,EAAM8c,WAGR,aACT,CClDO,SAASoB,EAAoB7W,EAAY8W,GAC9C,GAAsB,kBAAlBA,EAASlhB,MAA4BkhB,EAASvV,SAAU,CAG1D,OAFawV,GAAS/W,GACEA,EAAKwB,WAAa,EAAIxB,EAAKwB,aAChCkD,SAASoS,EAASvV,SAAU,GACjD,CAEA,MAAMyV,EAAgBtS,SAASoS,EAASG,MAAO,IAE/C,OADmBtZ,KAAKuZ,IAAIlX,EAAKU,YAAcsW,EAAe,IACzC,CACvB,CAUO,SAASG,EAAqBnX,EAAY8W,GAC/C,MAAMM,EAAOL,GAAS/W,GACtB,OAAIoX,IAASA,EAAK/S,YAAc,IAAMwS,EAAoB7W,EAAM8W,IACvD,CAACM,EAAKtT,KAGR,EACT,CAEA,SAASiT,GAAS/W,GAChB,OAAOA,EAAKC,MAAMuD,KAAKhB,GAAgD,SAAxCA,EAAKiB,WAAWC,oBACjD,CClDO,SAAS2T,GAA6BrX,GAC3C,MAAMsX,EAAUthB,SAASgC,cAAc,+BACvC,OAAIsf,EACKH,EAAqBnX,EAAM,CAChCiX,MAAOK,EAAQ/e,QAAQgf,eACvBhW,SAAU+V,EAAQ/e,QAAQif,kBAC1B5hB,KAAM0hB,EAAQ/e,QAAQkf,gBAInB,EACT,CCPA,MAAMC,qBAAqBlhB,YACzB,6BAAkB2F,GAChB,MAAO,CACL,oBACA,oBACA,oBACA,iCACA,kBACA,aACA,iBACA,iBACA,aACA,gBACA,kBACA,iBACA,sBACA,kBACA,oBACA,cACA,uBACA,iBAEJ,CAkKQrF,8BAAgCH,KAAKI,yBAAyBF,KAAKF,MACnEK,gCAAkCL,KAAKM,2BAA2BJ,KAAKF,MAEvEghB,0BAA4BhhB,KAAKihB,qBAAqB/gB,KAAKF,MAC3DkhB,0BAA4BlhB,KAAKmhB,qBAAqBjhB,KAAKF,MAC3DohB,2BAA6BphB,KAAKqhB,sBAAsBnhB,KAAKF,MAC7DshB,uBAAyBthB,KAAKuhB,kBAAkBrhB,KAAKF,MACrDwhB,uBAAyBxhB,KAAKyhB,kBAAkBvhB,KAAKF,MAEtD,iBAAAe,GACLf,KAAKgB,uBAEgBhB,KAAKoB,WAAWC,cAA2B,qBACnD3B,iBAAiB,QAASM,KAAKohB,4BAEnBphB,KAAKoB,WAAWC,cAA2B,gBACnD3B,iBAAiB,QAASM,KAAKshB,wBAE/BthB,KAAKoB,WAAWC,cAAiC,aACzD3B,iBAAiB,SAAUM,KAAKwhB,wBAEzC9hB,iBAAiB,2BAA4BM,KAAKG,+BAClDT,iBAAiB,6BAA8BM,KAAKK,iCAEpDL,KAAKuG,QACP,CAEO,oBAAArF,GACL,MAAMwgB,EAAe1hB,KAAKoB,WAAWC,cAA2B,qBACpD,MAAZqgB,GAAAA,EAAc/hB,oBAAoB,QAASK,KAAKohB,4BAEhD,MAAMO,EAAmB3hB,KAAKoB,WAAWC,cAA2B,gBACpD,MAAhBsgB,GAAAA,EAAkBhiB,oBAAoB,QAASK,KAAKshB,wBAEnCthB,KAAKoB,WAAWC,cAAiC,aACzD1B,oBAAoB,SAAUK,KAAKwhB,wBAK5C,MAAMI,EAAoB5hB,KAAKqB,cAAiC,yBAC/C,MAAjBugB,GAAAA,EAAmBjiB,oBAAoB,SAAUK,KAAKghB,2BAEtD,MAAMrQ,EAAgB3Q,KAAKqB,cAAiC,4BAC/C,MAAbsP,GAAAA,EAAehR,oBAAoB,QAASK,KAAKkhB,2BAEjDvhB,oBAAoB,2BAA4BK,KAAKG,+BACrDR,oBAAoB,6BAA8BK,KAAKK,gCACzD,CAEO,wBAAA+F,CAAyByb,EAAexb,EAAkBC,GAC/DtG,KAAKgB,uBAEDhB,KAAK0B,aAAe2E,IAAaC,GACnCtG,KAAKuG,QAET,CAEQ,oBAAAvF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,sxLAEhC,CAEQ,iBAAAggB,CAAkB1f,GAGxB,GAFAE,QAAQ6C,MAAM,gCAER/C,EAAMqV,kBAAkB0K,mBAC5B,OAGF,MAAMlX,EAAWmD,SAAShM,EAAMqV,OAAOrY,MAAO,IAExCgjB,EAAU,CACd,CAAC/hB,KAAK4B,QAAQ2N,YAAa3E,GAGvB2P,EAAWC,IAIjB,IAAK,MAAM3O,KAAQ0O,EAASjR,MAAO,KAAAmD,GACL,OAAxBA,EAAAZ,EAAKa,0BAAmB,EAAxBD,EAA0BE,cAAe3M,KAAK4B,QAAQ2N,aACxDwS,EAAQlW,EAAKsB,KAAOvC,EAExB,CAOA8H,EADmB1S,KAJY,CAC7B+hB,WAEe,kCAEmBrB,IAA8BsB,MAAM/f,QAAQC,KAClF,CAEQ,iBAAAqf,CAAkBtd,GAExB,MAAMlC,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN2S,OAAQ,YAGZ9W,KAAK2E,cAAc5C,EACrB,CAEQ,qBAAAsf,CAAsBtf,GAG5B,GAFAE,QAAQ6C,MAAM,2BAA4B9E,KAAK4B,QAAQ2N,YAEnDxN,EAAMqV,kBAAkB/U,oBAAsBN,EAAMqV,OAAO7U,SAAU,CAKvET,EAFmB9B,KAFF,CAACA,KAAK4B,QAAQ2N,YACd,8BAGsBmR,IAA8BsB,MAAM/f,QAAQC,KACrF,CACF,CAEQ,wBAAA9B,CAAyB6D,GAC/B,MAAMge,EAAiBjiB,KAAKoB,WAAW4B,iBACrC,iBACF,IAAK,MAAMkf,KAAiBD,EAC1BC,EAAc3f,UAAW,EAG3B,MAAMwO,EAAS/Q,KAAKqB,cAAiC,yBACjD0P,IACFA,EAAOxO,UAAW,GAGpB,MAAMoO,EAAgB3Q,KAAKqB,cAAiC,4BACxDsP,IACFA,EAAcpO,UAAW,EAE7B,CAEQ,0BAAAjC,CAA2B2D,GACjC,MAAMge,EAAiBjiB,KAAKoB,WAAW4B,iBACrC,iBACF,IAAK,MAAMkf,KAAiBD,EAC1BC,EAAc3f,UAAW,EAG3B,MAAMwO,EAAS/Q,KAAKqB,cAAiC,yBACjD0P,IACFA,EAAOxO,UAAW,GAGpB,MAAMoO,EAAgB3Q,KAAKqB,cAAiC,4BACxDsP,IACFA,EAAcpO,UAAW,EAE7B,CAEQ,MAAAgE,GACN,MAAMmb,EAAe1hB,KAAKoB,WAAWC,cAA2B,qBAE7CrB,KAAKoB,WAAWC,cAAc,cACtCqP,GAAK1Q,KAAK4B,QAAQ4N,UAE7B,MAAM2S,EACJniB,KAAKoB,WAAWC,cAAsD,UAiBxE,GAfK8gB,EAAoBvgB,QAAQwgB,MAC/BD,EAAoBvgB,QAAQygB,IAAM,GAClCF,EAAoBvgB,QAAQwgB,IAAMpiB,KAAK4B,QAAQsN,UAI7ClP,KAAK4B,QAAQmO,WACfoS,EAAoBvgB,QAAQ0gB,MAAQ,MAIjCtiB,KAAK4B,QAAQsN,UAAsC,cAA1BlP,KAAK4B,QAAQsN,UACzCjN,QAAQC,KAAK,wBAAyBlC,KAAK4B,QAAQ4N,UAAWxP,KAAK4B,QAAQyN,OAGzErP,KAAK4B,QAAQ+N,WAAY,CAC3B,MAAM4S,EAAsBviB,KAAKoB,WAAWC,cAAc,kBACtDkhB,IACFA,EAAoB3gB,QAAQ2B,IAAMvD,KAAK4B,QAAQ+N,WAEnD,CAEA,MAAM6S,EAAgBxiB,KAAKoB,WAAWC,cAAiC,UAEnErB,KAAK4B,QAAQ+N,WACf6S,EAAcxe,KAAOhE,KAAK4B,QAAQ+N,WAElC6S,EAAcnT,MAAQ,4CAKxBmT,EAAc/gB,UAAYzB,KAAK4B,QAAQyN,MAEvC,MAAMoT,EACJziB,KAAKoB,WAAWC,cAA2B,8BAC7CohB,EAAyB7gB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC7DsY,EAAyB7gB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAC/DqY,EAAyB7gB,QAAQsI,SAAWlK,KAAK4B,QAAQiO,uBAAuBjN,WAGhF,MAAM8f,EAAc1iB,KAAKoB,WAAWC,cAA2B,eAoB/D,GAnBAqhB,EAAY9gB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAChDuY,EAAY9gB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAClDsY,EAAY9gB,QAAQsI,SAAWlK,KAAK4B,QAAQqM,UAAUrL,WAElDmL,SAAS/N,KAAK4B,QAAQqM,UAAW,IACnCF,SAAS/N,KAAK4B,QAAQiO,uBAAwB,KAC9C7P,KAAKoB,WAAWC,cAAc,UAAUuI,UAAU9H,OAAO,eACzD4gB,EAAY9gB,QAAQ+gB,gBAAkB,iBACtCF,EAAyB7gB,QAAQ+gB,gBAAkB,gBAEnD3iB,KAAKoB,WAAWC,cAAc,UAAUuI,UAAUC,IAAI,eACtD6Y,EAAY9gB,QAAQ+gB,gBAAkB,GACtCF,EAAyB7gB,QAAQ+gB,gBAAkB,UAGpB,UAA7B3iB,KAAK4B,QAAQ0N,cACftP,KAAKoB,WAAWC,cAA2B,iBAAiBsG,QAAS,GAGnE3H,KAAK4B,QAAQ2O,cAAe,CAE9B,MAAMQ,EAAS/Q,KAAKqB,cAAiC,yBACjD0P,GACFA,EAAOrR,iBAAiB,SAAUM,KAAKghB,0BAE3C,KAAO,CAEL,MAAMrQ,EAAgB3Q,KAAKqB,cAAiC,4BACxDsP,GACFA,EAAcjR,iBAAiB,QAASM,KAAKkhB,0BAEjD,CAIA,GAAIlhB,KAAK4B,QAAQmO,SAAU,CAGzB,YAFoB/P,KAAKoB,WAAWC,cAA2B,qBACnDsG,QAAS,EAEvB,CAEY,MAAZ+Z,GAAAA,EAAckB,aAAa,aAAc,CAAC,SAAU5iB,KAAK4B,QAAQyN,MAAO,aAAatE,KAAK,MAE1F/K,KAAK6iB,gBACP,CAkBQ,oBAAA1B,CAAqBpf,GAC3B,GAAIA,EAAMK,yBAAyBC,kBAAmB,CASpDuc,EADmB5e,KAPY,CAC7B0Q,GAAI1Q,KAAK4B,QAAQ2N,WAEjB3E,SAAUmD,SAAS/N,KAAK4B,QAAQgJ,SAAU,IAC1CqB,aAAc8B,SAAShM,EAAMK,cAAcR,QAAQ2O,cAAe,KAEnD,gCAEmByR,MAAM/f,QAAQC,KACpD,CACF,CAMQ,oBAAA+e,CAAqBlf,GAW3B,GAAIA,EAAMK,yBAAyB0f,kBAAmB,CAWpDlD,EAJmB5e,KANY,CAC7B0Q,GAAI1Q,KAAK4B,QAAQ2N,WACjB3E,SAAUmD,SAAS/N,KAAK4B,QAAQgJ,SAAU,IAC1CqB,aAAc8B,SAAShM,EAAMK,cAAcrD,MAAO,KAEnC,uCAKmBijB,MAAM/f,QAAQC,KACpD,CACF,CAEQ,cAAA2gB,GACN,MAAMC,EAAkB9iB,KAAKoB,WAAWC,cAAiC,aAEzE,GAA8B,SAA1BrB,KAAK4B,QAAQqO,SAIf,YADA6S,EAAgBnb,QAAS,GAIM,UAA7B3H,KAAK4B,QAAQ0N,cACfwT,EAAgBvgB,UAAW,GAK7B,MACMqI,EAAWmD,SAAS/N,KAAK4B,QAAQgJ,SAAU,IAEjD,IAAIsF,EAHuB,GAY3B,GARIlQ,KAAK4B,QAAQsO,cACfA,EAAcnC,SAAS/N,KAAK4B,QAAQsO,YAAa,KAG/CA,EAActF,IAChBsF,EAActF,GATW,KAYvBsF,EAAoC,CAKtC,IAAI6S,EAAc,GAClB,IAAK,IAAIvK,EAAI,EAAGA,GAAKtI,EAAasI,IAChCuK,GAAe,kBAAkBvK,MAAMA,aAGzCsK,EAAgBrhB,UAAYshB,CAC9B,CAEAD,EAAgB/jB,MAAQiB,KAAK4B,QAAQgJ,QACvC,EASGxF,eAAeC,IAAI,mBACtBD,eAAeE,OAAO,iBAAkByb,cCxiB1C,MAAMiC,qBAAqBnjB,YACzB,6BAAkB2F,GAChB,MAAO,CAAC,aACV,CAUQjF,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7C2F,uBAAyB3F,KAAK4F,kBAAkB1F,KAAKF,MACrDijB,eAER,WAAA9K,GACEC,QACApY,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,iTAC9B,CAEO,iBAAAV,GACL,IAAK,MAAMgB,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,MACK,GAAI9C,EAAuB6C,GAChC,IACE/B,KAAK2F,uBAAuB5D,EAC9B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBACtCb,iBAAiB,oBAAqBM,KAAK2F,uBAC7C,CAEO,oBAAAzE,GACLvB,oBAAoB,eAAgBK,KAAKO,oBACzCZ,oBAAoB,oBAAqBK,KAAK2F,uBAChD,CAEO,wBAAAS,CAAyBpB,EAAc0D,EAAmBpC,GAC3DtG,KAAK0B,aAAwB,eAATsD,GAAsC,OAAbsB,GAC/CtG,KAAKkjB,kBAET,CAOQ,gBAAAA,GACN,MAAMC,EAASnjB,KAAKoB,WAAW4B,iBAAiB,iBAChD,IAAK,MAAMogB,KAASD,EACdC,IAAUpjB,KAAKijB,gBACjBG,EAAMthB,QAGZ,CAEQ,iBAAA8D,CAAkB7D,GAA6B,IAAAshB,EAErD,MAAMF,EAASnjB,KAAKoB,WAAW4B,iBAAiB,iBAChD,IAAK,MAAMogB,KAASD,EAClBC,EAAMthB,SAGR,MAAMiQ,EAAOhQ,EAAMoC,OAAO4N,KAKb,SAATA,GAA4B,iBAATA,GACrB/R,KAAKsjB,uBAGM,cAATvR,GACF/R,KAAKujB,iBAAiB,+FAIX,kBAATxR,GACF/R,KAAKujB,iBAAiB,2GAIX,+BAATxR,GACF/R,KAAKujB,iBAAiB,8IAIX,2BAATxR,GACF/R,KAAKujB,iBAAiB,yHAIX,mCAATxR,GACF/R,KAAKujB,iBAAiB,6HAIX,6CAATxR,GACF/R,KAAKujB,iBAAiB,6JAKX,wCAATxR,GACF/R,KAAKujB,iBAAiB,iKAKX,iBAATxR,GAAoC,wBAATA,GAC7B/R,KAAKujB,iBAAiB,uGAIX,gBAATxR,GAAmC,wBAATA,GAA2C,gBAATA,GACrD,wBAATA,GACA/R,KAAKujB,iBAAiB,qGAIX,oBAATxR,GAAuC,oBAATA,GAChC/R,KAAKujB,iBAAiB,kGAOX,oBAATxR,GAA8Bd,MAAMC,QAA2B,OAApBmS,EAACthB,EAAMoC,OAAOwT,aAAM,EAAnB0L,EAAqBG,eACvB,IAA5CzhB,EAAMoC,OAAOwT,OAAO6L,aAAavgB,QACP,wBAA1BlB,EAAMoC,OAAOoG,UACbvK,KAAKujB,iBAAiB,4DAGX,oBAATxR,GACF/R,KAAKujB,iBAAiB,2DAE1B,CAEQ,aAAA/iB,CAAcuB,GAAyB,IAAA0hB,EAO7C,GAAI1hB,EAAMoC,OAAOC,WAAY,CAC3B,MAAM+e,EAASnjB,KAAKoB,WAAW4B,iBAAiB,iBAChD,IAAK,MAAMogB,KAASD,EAClBC,EAAMthB,SAGR,MACF,CAOA,GADkC,CAAC,iBAAkB,qBACvBsT,SAASrT,EAAMoC,OAAOoG,UAClD,OAQF,GAAyB,OAAzBkZ,EAAI1hB,EAAMoC,OAAOoG,WAAbkZ,EAAuBtO,WAAW,sBAAuB,CAC3D,MAAMuO,EAAS1jB,KAAKoB,WAAW4B,iBAAiB,2CAChD,IAAK,MAAMhB,KAAS0hB,EAClB1hB,EAAMF,SAGR,MACF,CAOA,MAAMqhB,EAASnjB,KAAKoB,WAAW4B,iBAAiB,iBAChD,IAAK,MAAMogB,KAASD,EAClBC,EAAMthB,SASR,GALIC,EAAMoC,OAAOsG,eAAexH,OAAS,GACvCjD,KAAK2jB,sBAAsB5hB,EAAMoC,OAAOsG,gBAItC1I,EAAMoC,OAAOgX,iBAAiBlY,OAAS,EAAG,CAC5C,MACM2gB,EADS7hB,EAAMoC,OAAOgX,iBAAiBnF,IAAI5H,GAAWA,EAAQsO,eACzC3R,KAAK,MAC1BlE,EAAUxH,SAAS4P,cAAc,OAEvCpI,EAAQiE,YAAc,eAAe8Y,4BACrC,MAAMR,EAAQ/jB,SAAS4P,cAAc,iBACrCmU,EAAMxhB,QAAQiiB,UAAY,UAC1BT,EAAM3V,YAAY5G,GAClB7G,KAAKoB,WAAWqM,YAAY2V,EAC9B,CAEApjB,KAAK8jB,gCAAgC/hB,GACrC/B,KAAK+jB,4BAA4BhiB,EACnC,CAEQ,+BAAA+hB,CAAgC/hB,GACtC,IAAK,MAAMgQ,KAAQhQ,EAAMoC,OAAO2W,qBAAsB,CACpD,MAAMjU,EAAU,yBAAyBkL,KACzC/R,KAAKgkB,mBAAmBnd,EAC1B,CACF,CAEQ,2BAAAkd,CAA4BhiB,GAClC,IAAK,MAAMgQ,KAAQhQ,EAAMoC,OAAO6W,uBAAwB,CACtD,MAAMnU,EAAU,gBAAgBkL,iBAChC/R,KAAKgkB,mBAAmBnd,EAC1B,CACF,CAEQ,qBAAA8c,CAAsBra,GAC5B,MAAM2a,EAAa3a,EAAMuD,KAAKhB,GAAQA,EAAK2Q,kBAAkBxM,YAC7D,GAAIiU,EAEF,YADAjkB,KAAKkkB,sBAAsBD,GAI7B,MAAME,EAAkB7a,EAAMuD,KAAKhB,GAAQA,EAAK2Q,kBAAkBrK,oBAC9DgS,GACFnkB,KAAKokB,yBAAyBD,GAGhC,MAAME,EAAuB/a,EAAM0M,IAAInK,GAAQA,EAAK6Q,eAAe3R,KAAK,MAElElE,EAAUxH,SAAS4P,cAAc,OACvCpI,EAAQpF,UAAY,+BAA+B4iB,0BAGnD,MAAMjB,EAAQ/jB,SAAS4P,cAAc,iBACrCmU,EAAMxhB,QAAQiiB,UAAY,UAC1BT,EAAM3V,YAAY5G,GAClB7G,KAAKoB,WAAWqM,YAAY2V,EAC9B,CAEQ,wBAAAgB,CAAyBvY,GAC/B,IAAIhF,EAAUgF,EAAK2Q,kBAAkBrK,mBAAmB,GACnDtL,IACHA,EACE,uFAGJ,MAAMyd,EAAmBjlB,SAAS4P,cAAc,OAChDqV,EAAiBxZ,YAAc,yBAAyBe,EAAK6Q,kBAAkB7V,IAE/E,MAAM0d,EAAiBllB,SAAS4P,cAAc,iBAC9CsV,EAAe3iB,QAAQiiB,UAAY,UACnCU,EAAe9W,YAAY6W,GAE3BtkB,KAAKoB,WAAWqM,YAAY8W,EAC9B,CAEQ,gBAAAhB,CAAiBiB,GACvB,MAAM3d,EAAUxH,SAAS4P,cAAc,OACvCpI,EAAQiE,YAAc0Z,EAEtB,MAAMpB,EAAQ/jB,SAAS4P,cAAc,iBACrCmU,EAAMxhB,QAAQiiB,UAAY,SAC1BT,EAAM3V,YAAY5G,GAElB7G,KAAKoB,WAAWqM,YAAY2V,EAC9B,CAMQ,oBAAAE,GACN,MAAMzc,EAAUxH,SAAS4P,cAAc,OACvCpI,EAAQiE,YAAc,kDAEtB,MAAM3I,EAAS9C,SAAS4P,cAAc,UACtC9M,EAAOlD,KAAO,SACdkD,EAAO2I,YAAc,gBACrB3I,EAAOzC,iBAAiB,QAAS+kB,IAEjC,MAAMrB,EAAQ/jB,SAAS4P,cAAc,iBACrCmU,EAAMxhB,QAAQiiB,UAAY,SAC1BT,EAAM9E,OAAOzX,EAAS1E,GAEtBnC,KAAKoB,WAAWqM,YAAY2V,GAC5BpjB,KAAKijB,eAAiBG,CACxB,CAEQ,kBAAAY,CAAmBQ,GACzB,MAAM3d,EAAUxH,SAAS4P,cAAc,OACvCpI,EAAQiE,YAAc0Z,EAEtB,MAAMpB,EAAQ/jB,SAAS4P,cAAc,iBACrCmU,EAAMxhB,QAAQiiB,UAAY,UAC1BT,EAAM3V,YAAY5G,GAElB7G,KAAKoB,WAAWqM,YAAY2V,EAC9B,CAEQ,qBAAAc,CAAsBrY,GAC5B,MAAMhF,EAAUxH,SAAS4P,cAAc,OACvCpI,EAAQiE,YAAc,GAAGe,EAAK6Q,qIAI9B,MAAM0G,EAAQ/jB,SAAS4P,cAAc,iBACrCmU,EAAMxhB,QAAQiiB,UAAY,OAC1BT,EAAM3V,YAAY5G,GAElB7G,KAAKoB,WAAWqM,YAAY2V,EAC9B,EAUF,SAASqB,GAAiB1iB,GACxB,MAAMI,EAASJ,EAAMK,cACoB,IAAAsiB,EAArCviB,aAAkBE,oBACW,OAA/BqiB,EAAAviB,EAAOW,QAAQ,mBAAf4hB,EAAiC5iB,UAGnC,MAAM6iB,EAAe,IAAI3lB,MAAM,uBAC/B2F,cAAcggB,EAChB,CAQKvf,eAAeC,IAAI,kBACtBD,eAAeE,OAAO,gBAAiB0d,cCtWzC,MAAM4B,mBAAmB/kB,YACvB,6BAAkB2F,GAChB,MAAO,CACL,uBAEJ,CAsCQjF,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAE9C,iBAAAe,GACLf,KAAKgB,uBAEL,IAAK,MAAMe,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBAEtCP,KAAKuG,QACP,CAEO,oBAAArF,GACLvB,oBAAoB,eAAgBK,KAAKO,mBAC3C,CAEO,wBAAA6F,CAAyByb,EAAexb,EAAkBC,GAC/DtG,KAAKgB,uBAEDhB,KAAK0B,aAAe2E,IAAaC,GACnCtG,KAAKuG,QAET,CAEQ,oBAAAvF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,qkCAEhC,CAEQ,MAAA8E,GAEN,MAAMse,EAAc7kB,KAAKoB,WAAWC,cAA2B,iBAC3B,MAAhCrB,KAAK4B,QAAQkjB,eACfD,EAAYld,QAAS,EAErBkd,EAAYld,QAAS,EAGvB,MAAMod,EAAa/kB,KAAKoB,WAAWC,cAA2B,aAC9D0jB,EAAWnjB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC/C4a,EAAWnjB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eACjD2a,EAAWnjB,QAAQsI,SAAWlK,KAAK4B,QAAQojB,eAE3C,MAAMC,EAAkBjlB,KAAKoB,WAAWC,cAA2B,aACnE4jB,EAAgBrjB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aACpD8a,EAAgBrjB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eACtD6a,EAAgBrjB,QAAQsI,SAAWlK,KAAK4B,QAAQkjB,eAEhD9kB,KAAKoB,WAAWC,cAAc,wBAAwByJ,YACpD,IAAI9K,KAAK4B,QAAQsjB,mBACrB,CAEQ,aAAA1kB,CAAcuB,GACpB,MAAMsH,EAAOtH,EAAMoC,OAAOkF,KAC1B,GAA0B,IAAtBA,EAAKC,MAAMrG,OACb,OAGF,MAAMkiB,EA4DV,SAAyB9b,GACvB,IAAI8b,EAAW,EACf,IAAK,MAAMtZ,KAAQxC,EAAKC,MAAO,CAC7B,MAAM8b,EAAiBrX,SAASlC,EAAKiB,WAAWuY,yBAA0B,IACtE5H,OAAOC,UAAU0H,GACnBD,GAAYC,EAEZD,GAAYtZ,EAAK+D,mBAErB,CAMA,OAFAuV,EAAWne,KAAKse,MAAMH,GAEfA,CACT,CA5EqBI,CAAgBlc,GAEjCrJ,KAAK4B,QAAQojB,eAAiBG,EAASviB,WAEvC,MAAM4iB,EAAUxlB,KAAKoB,WAAWC,cAA2B,wBAC3DmkB,EAAQC,MAAMC,QAAU,OAExB,IAAIC,EAAsB,EAC1B,IAAK,MAAMC,KAAYvc,EAAK0M,iCACtB6P,EAASC,uBAAyB,IACpCF,GAAuBC,EAASC,wBAIpC,GAAIF,EAAsB,EAAG,CAG3BH,EAAQC,MAAMC,QAAU,OAExB,MAAMI,EAAmB9lB,KAAKoB,WAAWC,cAA2B,yBACpEykB,EAAiBlkB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eACvD0b,EAAiBlkB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aACrD2b,EAAiBlkB,QAAQsI,SAAWyb,EAAoB/iB,UAC1D,CAUA,MAAMkiB,EAAiB9d,KAAKwX,MAAM,KAAO2G,EAAW9b,EAAKU,cAAgB,IAEzE/J,KAAK4B,QAAQkjB,eAAiBA,EAAeliB,WAO7C,MAAMsiB,EAAkBle,KAAKse,MAAM,IAAMR,EAAiBK,GAC1DnlB,KAAK4B,QAAQsjB,gBAAkBA,EAAgBtiB,WAG/C5C,KAAK4B,QAAQkI,eAAiBT,EAAKU,YAAYnH,UACjD,EAqCGwC,eAAeC,IAAI,gBACtBD,eAAeE,OAAO,cAAesf,YCzMvC,MAAMmB,uBAAuBlmB,YAC3B,6BAAkB2F,GAChB,MAAO,CAAC,kBAAmB,oBAAqB,wBAClD,CAmGO,iBAAAzE,GACLf,KAAKgB,uBAELhB,KAAKuG,QACP,CAEO,wBAAAH,CAAyByb,EAAexb,EAAkBC,GAC/DtG,KAAKgB,uBAEDhB,KAAK0B,aAAe2E,IAAaC,GACnCtG,KAAKuG,QAET,CAEQ,oBAAAvF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,ysCAEhC,CAEQ,MAAA8E,GACN,IAAKvG,KAAK4B,QAAQ4N,UAChB,OAGF,MACMjM,EADa,IAAIC,IAAI,YAAYxD,KAAK4B,QAAQuO,SAAUpM,SAASoF,QAChDvG,WACjByM,EAAQrP,KAAKgmB,aAEnBhmB,KAAKimB,YAAY5W,EAAO9L,GACxBvD,KAAKkmB,YAAY3iB,GACjBvD,KAAKmmB,eACLnmB,KAAKomB,gBAAgB/W,GACrBrP,KAAKqmB,cAAc9iB,EACrB,CAEQ,UAAAyiB,GACN,IAAI3W,EAAQrP,KAAK4B,QAAQ0kB,aAKzB,MAJkC,kBAA9BtmB,KAAK4B,QAAQ2kB,eACflX,GAAS,IAAIrP,KAAK4B,QAAQ2kB,gBAGrBlX,CACT,CAEQ,WAAA4W,CAAY5W,EAAe9L,GACjC,MAAMijB,EAASxmB,KAAKoB,WAAWC,cAAiC,YAChEmlB,EAAOxiB,KAAOT,EACdijB,EAAO1b,YAAcuE,CACvB,CAEQ,WAAA6W,CAAY3iB,GAClB,MAAM4L,EAAQnP,KAAKoB,WAAWC,cAA2B,UAEzD8N,EAAMvN,QAAQygB,IAAM,GACpBlT,EAAMvN,QAAQwgB,IAAMpiB,KAAK4B,QAAQuN,MAEXnP,KAAKoB,WAAWC,cAAc,kBACtCO,QAAQ2B,IAAMA,CAC9B,CAOQ,aAAAkjB,GAAgB,IAAAC,EACtB,IAAIva,EAAQnM,KAAK4B,QAAQuK,MACrBiZ,EAAiBplB,KAAK4B,QAAQwjB,eAClC,MAAMuB,EAAgD,QAA9B3mB,KAAK4B,QAAQuI,aAAyB,aAAe,SAIvEyc,EAAoC,OAA3BF,EAAG1mB,KAAK4B,QAAQilB,kBAAW,EAAxBH,EAA0BI,MAAM,KAAKja,KAAKka,GAAKA,EAAE3R,SAASuR,IAE5E,GAAIC,EAAW,CACb,MAAMI,EAAmBJ,EAAUE,MAAM,KACnCG,EAAkD,QAA9BjnB,KAAK4B,QAAQuI,aACrC6c,EAAiB,GACjBA,EAAiB,GACbE,EAA8C,IAAhCC,WAAWF,GAM3BC,EAAcnZ,SAAS5B,EAAO,KAAO4B,SAAS5B,EAAO,IAAM4B,SAASqX,EAAgB,MACtFA,EAAiBjZ,GAGnBA,EAAQ+a,EAAYtkB,UACtB,CAEA,MAAO,CAAEuJ,QAAOiZ,iBAClB,CAEQ,YAAAe,GACN,MAAMiB,EAASpnB,KAAKymB,gBAEdY,EAAUrnB,KAAKoB,WAAWC,cAA2B,UAC3DgmB,EAAQzlB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC5Ckd,EAAQzlB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAC9Cid,EAAQzlB,QAAQsI,SAAWkd,EAAOjb,MAElC,MAAMmb,EAAmBtnB,KAAKoB,WAAWC,cAA2B,qBACpEimB,EAAiB1lB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aACrDmd,EAAiB1lB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAEnDgd,EAAOhC,gBACTkC,EAAiB1lB,QAAQsI,SAAWkd,EAAOhC,eAC3CkC,EAAiB1lB,QAAQ2lB,KAAO,QAChCD,EAAiB1lB,QAAQ+gB,gBAAkB,iBAC3C0E,EAAQzlB,QAAQ+gB,gBAAkB,gBAGlC2E,EAAiB1lB,QAAQ2lB,KAAO,OAChCD,EAAiB1lB,QAAQ+gB,gBAAkB,GAC3C0E,EAAQzlB,QAAQ+gB,gBAAkB,SAEtC,CAEQ,eAAAyD,CAAgB/W,GACtB,MAAMmY,EAAYxnB,KAAKoB,WAAWC,cAAc,sBAIhD,GAHAmmB,EAAU5lB,QAAQ6lB,QAAUznB,KAAK4B,QAAQ6lB,QACzCD,EAAU5lB,QAAQ2kB,aAAelX,EAE7BrP,KAAKoB,WAAWC,cAAc,4BAA4BrB,KAAK4B,QAAQ4N,oBAEpE,CACL,MAAMkY,EAAkBroB,SAAS4P,cAAc,oBAC/CyY,EAAgB9lB,QAAQuL,IAAM,aAC9Bua,EAAgB9lB,QAAQ7C,MAAQ,OAEhC,MAAMqP,EAAU/O,SAAS4P,cAAc,mBACvCb,EAAQ1G,KAAO,WACf0G,EAAQxM,QAAQ8O,GAAK1Q,KAAK4B,QAAQ4N,UAClCpB,EAAQxM,QAAQgJ,SAAW,IAC3BwD,EAAQX,YAAYia,GAEpBF,EAAU/Z,YAAYW,EACxB,CAEAoZ,EAAU5lB,QAAQ8Q,OAAShQ,KAAKC,MAAMC,UACxC,CAEQ,aAAAyjB,CAAc9iB,GACpB,MAAMokB,EAAU3nB,KAAKoB,WAAWC,cAAc,wBAC9CsmB,EAAQ/lB,QAAQgmB,QAAU5nB,KAAK4B,QAAQimB,eACvCF,EAAQ/lB,QAAQ0b,MAAQtd,KAAK4B,QAAQkmB,aACrCH,EAAQ/lB,QAAQoC,KAAO,GAAGT,mBAC5B,EASG6B,eAAeC,IAAI,qBACtBD,eAAeE,OAAO,mBAAoBygB,gBC/P5C,MAAMgC,wBAAwBloB,YACpBU,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAE9C,iBAAAe,GACLf,KAAKgB,uBAGLgnB,sBAAsB,KACpB,MAAMjmB,EAAQ,IAAI/C,MAAM,6BAA8B,CAAEuF,SAAS,EAAMC,UAAU,IACjFxE,KAAK2E,cAAc5C,KAGrB,IAAK,MAAMA,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,mBACxC,CAEO,oBAAAW,GACLvB,oBAAoB,eAAgBK,KAAKO,mBAC3C,CAEQ,oBAAAS,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,gLAEhC,CAEQ,aAAAjB,CAAcuB,GACpB,MAAMkmB,EAAalmB,EAAMoC,OAAOkF,KAAKC,MAAM0M,IAAInK,GAAQA,EAAKyC,YACtD4Z,EAAWloB,KAAKgD,iBAAiB,oBACvC,IAAImlB,EAAe,EAEnB,IAAK,MAAM/a,KAAW8a,EAAU,CAC9B,MAAM1Y,EAAYzB,SAASX,EAAQxL,QAAQ4N,UAAW,KACxCyY,EAAW7S,SAAS5F,IAAc2Y,EAAe,GAI7D/a,EAAQqY,MAAMC,QAAU,OACxByC,KAEA/a,EAAQqY,MAAMC,QAAU,MAE5B,CAIE1lB,KAAKylB,MAAMC,QADQ,IAAjByC,EACmB,OAEA,OAEzB,EAgBG/iB,eAAeC,IAAI,sBACtBD,eAAeE,OAAO,oBAAqByiB,iBCzEtC,SAASK,GAAY9H,EAAO+H,GACZ,iBAAV/H,IACTA,EAAQA,EAAMxP,QAAQ,IAAK,KAE7B,IAAI/R,EAAQ,GACZ,MAAMupB,EAAmB,sBACnBC,EAAeF,GAfH,cAiBlB,SAASG,EACPC,EACAC,EACAC,EACAC,GAEA,QAJS,IAATF,IAAAA,EAAY,QACH,IAATC,IAAAA,EAAY,UACL,IAAPC,IAAAA,EAAU,KAENC,MAAMJ,IAAqB,MAAVA,EACnB,OAAO,EAKT,MAAMK,GAFNL,GAAUA,EAAS,KAAO5W,QAAQ6W,IAEb5B,MAAM,KAO3B,OANsBgC,EAAM,GAAGhY,QAC7B,2BACA,KAAK6X,MAEaG,EAAM,GAAKF,EAAUE,EAAM,GAAK,GAGtD,CAEA,OAAQP,EAAaQ,MAAMT,GAAkB,IAC3C,IAAK,SACHvpB,EAAQypB,EAAqBlI,EAAO,GACpC,MACF,IAAK,qBACHvhB,EAAQypB,EAAqBlI,EAAO,GACpC,MACF,IAAK,8BACHvhB,EAAQypB,EAAqBlI,EAAO,EAAG,IAAK,KAC5C,MACF,IAAK,0CACHvhB,EAAQypB,EAAqBlI,EAAO,EAAG,IAAK,KAIhD,OAAOiI,EAAazX,QAAQwX,EAAkBvpB,EAChD,CClDA,MAAMiqB,2BAA2BnpB,YAavBU,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAK7C8J,eAAiB,EAMjBmf,qBAAsB,EAEvB,iBAAAloB,GACLf,KAAKgB,uBAEL,IAAK,MAAMe,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBAEtCP,KAAKuG,QACP,CAEO,oBAAArF,GACLvB,oBAAoB,eAAgBK,KAAKO,mBAC3C,CAEQ,oBAAAS,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,onBAEhC,CAEQ,aAAAjB,CAAcuB,GACpB,MAAMsH,EAAOtH,EAAMoC,OAAOkF,KAC1BrJ,KAAK8J,eAAiBT,EAAKU,YAE3B,IAAIkf,GAAsB,EAC1B,IAAK,MAAMpd,KAAQxC,EAAKC,MAAO,KAAAwC,EAC7B,GAAQ,MAAJD,GAA6B,OAAzBC,EAAJD,EAAMG,0BAAqC,OAAdF,EAA7BA,EAA+BG,eAA/BH,EAA6CI,qBAAsB,CACrE+c,GAAsB,EACtB,KACF,CACF,CAEAjpB,KAAKipB,oBAAsBA,EAE3BjpB,KAAKuG,QACP,CAEQ,MAAAA,GACN,MAAM2iB,EAAOlpB,KAAKoB,WAAWC,cAAc,QACrC8nB,EAAoBnpB,KAAKoB,WAAWC,cAAc,uBAClD+nB,EAAcppB,KAAKoB,WAAWC,cAA2B,iBAI/D,IAAIgoB,EACJ,GAHAH,EAAKpe,YAAc9K,KAAKspB,iBAGpBtpB,KAAKipB,oBACPI,EAAa,QACR,CACL,MAAME,EAAYxb,SAAS/N,KAAK4B,QAAQ2nB,UAAW,IACnDF,EAAariB,KAAKuZ,IAAI,IAAMvgB,KAAK8J,eAAiByf,EAAa,IACjE,CAEAF,EAAariB,KAAKwX,MAAM6K,GACpBR,MAAMQ,KACRA,EAAa,GAGfF,EAAkBvG,aAAa,gBAAiByG,EAAWzmB,YAC3DwmB,EAAY3D,MAAMnD,MAAQ+G,EAAa,GACzC,CAEQ,cAAAC,GACN,GAAItpB,KAAKipB,oBACP,OAAOjpB,KAAK4B,QAAQ4nB,kBAClB,iDAGJ,MACMC,EADY1b,SAAS/N,KAAK4B,QAAQ2nB,UAAW,IACpBvpB,KAAK8J,eAEpC,OAAI2f,EAAa,EAER,OAASrB,GAAYqB,EAAY,eACtC,0CAEK,+DAEX,EASGrkB,eAAeC,IAAI,yBACtBD,eAAeE,OAAO,uBAAwB0jB,oBChIhD,MAAMU,6BAA6B7pB,YACzB8pB,aAAe3pB,KAAK4pB,QAAQ1pB,KAAKF,MAElC,iBAAAe,GACAf,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,26EAG9BzB,KAAKoB,WAAWC,cAAc,UAAU3B,iBAAiB,QAASM,KAAK2pB,aACzE,CAEO,oBAAAzoB,GAAuB,IAAA2oB,EACW,OAAvCA,EAAA7pB,KAAKoB,WAAWC,cAAc,YAA9BwoB,EAAyClqB,oBAAoB,QAASK,KAAK2pB,aAC7E,CAEQ,OAAAC,CAAQ3lB,GAEd,MAAMlC,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN2S,OAAQ,YAGZ9W,KAAK2E,cAAc5C,EACrB,ECCK4R,eAAemW,GAAqBzgB,EAAY0gB,EACrDC,EAA+BC,QAAY,IAAZA,IAAAA,GAAe,GA4FhD,SAAmCF,GACjC,GAAIA,EAAS9mB,OAAS,EACpB,MAAM,IAAImR,MAAM,gDAMlB,IAAK,MAAMrC,KAAQgY,EACjB,IAAKjQ,EAAa/H,GAAO,CACvB,MAAM/P,EAAa,IAAIoS,MAAM,yBAAyBrC,KAGtD,MAFA/P,EAAMkoB,cAAgBnY,EACtB/P,EAAM6T,eAAiBkU,EACjB/nB,CACR,CAEJ,CA3GEmoB,CAA0BJ,GAE1B,MAAMK,EAuDR,SAAyB/gB,EAAYghB,GACnC,MAAMC,EAAerZ,MAAMC,QAAQ7H,EAAKwM,gBAAkBxM,EAAKwM,eAAiB,GAuBhF,GAAIyU,EAAarnB,OAASonB,EAAgB,EACxC,OAAOC,EACJnZ,OAAOY,GAAQA,EAAKoI,YAAc,QAAQrI,KAAKC,EAAKA,OACpDiE,IAAIjE,GAAQA,EAAKA,MACjBiE,IAAIjE,GAAQA,EAAKiI,eAGtB,OAAOI,EAAiB/Q,EAC1B,CAvFoBkhB,CAAgBlhB,EAAM0gB,EAAS9mB,QAC3CunB,EAAYT,EAAS/T,IAAIjE,GAAQA,EAAKiI,eAEtCyQ,EAAoBL,EAAUjZ,OAAOY,IACxCiY,EAAU/jB,KAAKykB,GACdA,EAASC,WAAa5Y,GAAQyY,EAAUpV,SAASsV,EAASE,YAKxDhF,EAFc,IADC,IAAIpZ,IAAY,IAAIie,KAAsBD,KAGlCzf,KAAK,WAC5B+J,EAAW,CAAE8Q,aACnB,MAAMiF,QAAoBxP,IAY1B,OAJI4O,GA2GN,SAAkC5gB,EAAY0gB,GAC5C,MAAMe,EAAqB5Q,EAA2B7Q,GAMtD,IAAK,MAAM0hB,KAAWhB,EAAU,CAC9B,MAAMiB,EAAeD,EAAQ/Q,cAC7B,IAAK8Q,EAAmB1V,SAAS4V,GAAe,CAC9C,MAAMhpB,EAAa,IAAIoS,MAAM,qBAAqB4W,qBAGlD,MAFAhpB,EAAMkoB,cAAgBc,EACtBhpB,EAAM6T,eAAiBkU,EACjB/nB,CACR,CACF,CACF,CA1HIipB,CAAyBJ,EAAad,GAGjCc,CACT,CCnBOlX,eAAe9J,GAAIuN,EAA4B9N,EAAkB4hB,EACtE3gB,GACA,MAAMqN,EAAc3S,EAAM,WAAYqE,EAAOiB,GAE7C,UAkBFoJ,eAAgCyD,EAA4BQ,EAC1DtO,EAAkB4hB,EAAyB3gB,GAC3CyN,EAAO1O,EAAMrG,OAAS,EAAG,8CAEzB,MAAMsX,EAAWC,IAEX2Q,EAAgB7hB,EAAM0M,IAAIoV,IAMhC,IAAIC,EAAY1oB,U9BwpBXgR,eAAuBrK,GAC5B,MAAM/F,EAAMgQ,EAAW9P,QAAQC,OAAOC,KAAO,eAEvCmQ,EAAoB,CACxBC,KAAMvD,KAAKwD,UAAU,CAAE1K,UACvB0J,QAAS,CACP,eAAgB,oBAElBiB,MAAO,WACPC,OAAQ,QAGV,IAAInB,EACJ,IACEA,QAAiBoB,MAAM5Q,EAAKuQ,EAC9B,CAAE,MAAO9R,GAGP,MAFAsR,EAActR,EAAO,QAASsH,GAC9BgK,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAKiR,EAAWF,GAAW,CACzB,MAAM/Q,EAAa,IAAIoS,MAAM,kDAI7B,MAHAd,EAActR,EAAO,QAASsH,GAC9BgK,EAActR,EAAO,WAAY+Q,GACjCO,EAActR,EAAO,MAAOuB,EAAIS,MAC1BhC,CACR,CAEA,IAAK+Q,EAASG,GAAI,KAAAoY,EAAAC,EAChB,MAAMvpB,EAAa,IAAIoS,MAAM,mBAAmBrB,EAASI,yBACzDG,EAActR,EAAO,QAASsH,GAC9BgK,EAActR,EAAO,aAAc8Q,EAAaC,IAChDO,EAActR,EAAO,kBAAmB+Q,EAASI,QAEjD,IAEEG,EAActR,EAAO,sBADM+Q,EAASM,OAEtC,CAAE,MAAOmY,GACP,CAcF,IAAI3kB,EAWJ,MAVwB,MAApBkM,EAASI,QAA8D,iBAAlB,OAA1BmY,EAAOtpB,EAAMwS,oBAAa,EAAnB8W,EAAqB7W,aACzD5N,EAAU7E,EAAMwS,cAAcC,YACmB,iBAAd,OAA1B8W,EAAOvpB,EAAMwS,oBAAa,EAAnB+W,EAAqB1kB,WACrCA,EAAU7E,EAAMwS,cAAc3N,SAG5BA,GACFyM,EAActR,EAAO,UAAW6E,GAG5B7E,CACR,CAEA,OAAO+Q,CACT,C8B3tBQ0Y,CAAQN,GACdlpB,QAAQK,IAAI,4BAA6B0E,KAAKwX,MAAM7b,IAAQ0oB,IAe5DA,EAAY1oB,IACZ,IAAI0G,QAiBNsK,eAAmC+X,EAAkBR,EAAyB3gB,GAG5E,GAAI2gB,EAAcjoB,OAAS,EACzB,OAAO,KAKT,IAAK,MAAM8O,KAAQmZ,EACjB,IAAKpR,EAAa/H,GAAO,CACvB,MAAM/P,EAAa,IAAIoS,MAAM,2BAK7B,OAJApS,EAAMuI,SAAWA,EACjBvI,EAAM6T,eAAiBqV,EACvBlpB,EAAMkoB,cAAgBnY,EACtB9P,QAAQC,KAAKF,GACN,IACT,CAcF,IAAKiP,MAAMC,QAAQwa,EAAW7V,gBAC5B,OAAO,KAMT,IAAI8V,EAAkB,KAEtB,MAAM3B,EAAYprB,eAAegtB,mBAKjC,IACED,QAAkB7B,GAAqB4B,EAAYR,EAAelB,GAAW,EAC/E,CAAE,MAAOhoB,GACPC,QAAQC,KAAKF,EAEf,CAEA,OAAO2pB,CACT,CAtEmBE,CAAoBtR,EAAU2Q,GAAiB,GAAI3gB,GACpEtI,QAAQK,IAAI,kCAAmC0E,KAAKwX,MAAM7b,IAAQ0oB,IAE7DhiB,IACHgiB,EAAY1oB,IACZ0G,QAAagS,IACbpZ,QAAQK,IAAI,0BAA2B0E,KAAKwX,MAAM7b,IAAQ0oB,KAG5DhR,EAAMjD,EAAQ/N,EAAMuO,EAAarN,EACnC,CAzDUuhB,CAAiB1U,EAAQQ,EAAatO,EAAO4hB,EAAe3gB,EACpE,CAAE,MAAOvI,GACPkV,EAAOE,EAAQ,CACbrF,KAAMga,GAAmBziB,EAAOtH,GAChCA,QACA2V,OAAQ,CACNuS,cAAegB,EACf5hB,SAEFiB,WACAqN,eAEJ,CAAC,QACCA,EAAYuG,UACd,CACF,CAkHA,SAAS4N,GAAmBziB,EAAkBtH,GAC5C,GAAIiT,EAAmBjT,GACrB,MAAO,gBAGT,G9BosCI,SAAgCA,GACpC,GAAIA,aAAiBoS,OAAS,kBAAmBpS,GAASkT,EAASlT,EAAMwS,gBACxC,MAA/BxS,EAAMwS,cAAcrB,OAAgB,CACpC,MAAMsB,EAAczS,EAAMwS,cAAcC,YACxC,GAA2B,iBAAhBA,GACqBA,EAAYoE,cAChBzD,SAAS,+CACjC,OAAO,CAGb,CAEA,OAAO,CACT,C8BjtCM4W,CAAsBhqB,GACxB,MAAO,6BAGT,G9BkqCI,SAA6BA,GACjC,GAAIA,aAAiBoS,OAAS,kBAAmBpS,GAASkT,EAASlT,EAAMwS,gBACxC,MAA/BxS,EAAMwS,cAAcrB,OAAgB,CACpC,MAAMsB,EAAczS,EAAMwS,cAAcC,YACxC,GAA2B,iBAAhBA,GACqBA,EAAYoE,cAChBzD,SAAS,yDACjC,OAAO,CAGb,CAEA,OAAO,CACT,C8B/qCM6W,CAAmBjqB,GACrB,MAAO,yBAGT,G9B4tCI,SAAoCA,GACxC,GAAIA,aAAiBoS,OAAS,kBAAmBpS,GAASkT,EAASlT,EAAMwS,gBACxC,MAA/BxS,EAAMwS,cAAcrB,OAAgB,CACpC,MAAMsB,EAAczS,EAAMwS,cAAcC,YACxC,GAA2B,iBAAhBA,GACLA,EAAYW,SAAS,wCACvB,OAAO,CAGb,CAEA,OAAO,CACT,C8BxuCM8W,CAA0BlqB,GAAQ,KAAAmqB,EAAAC,EACpC,IAAIvgB,EAKJ,OAJIoF,MAAMC,QAAQ5H,KAChBuC,EAAOvC,EAAMuD,KAAKyS,GAAY,MAAJA,OAAI,EAAJA,EAAMrT,eAG1B,OAARkgB,EAAItgB,IAAgB,OAAZsgB,EAAJA,EAAMrf,aAANqf,EAAkBha,mBACb,2CAGD,OAARia,EAAIvgB,IAAgB,OAAZugB,EAAJA,EAAMtf,aAANsf,EAAkBha,SACb,sCAGF,gCACT,CAEA,MAAO,WACT,CAEA,SAASgZ,GAAOvX,GAEd,MAAMyI,EAAS5C,EAAK7F,GAGpB,OAFAyI,EAAOxP,WAAawP,EAAOxP,YAAc,CAAC,EAC1CwP,EAAOxP,WAAWuf,UAAY/P,EAAOxP,WAAWuf,WAAatoB,SAASuoB,SAC/DhQ,CACT,CF/KKlX,eAAeC,IAAI,2BACtBD,eAAeE,OAAO,yBAA0BokB,sBGnBlD,MAAM6C,kBAAkB1sB,YACtB,6BAAkB2F,GAChB,MAAO,CAAC,uBACV,CA0CQgnB,wBAA0BxsB,KAAKysB,mBAAmBvsB,KAAKF,MACvDO,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7CmI,oBAAsBnI,KAAKoI,eAAelI,KAAKF,MAC/C0sB,sBAAwB1sB,KAAK2sB,iBAAiBzsB,KAAKF,MACnD4sB,mBAAqB5sB,KAAK6sB,cAAc3sB,KAAKF,MAC7C8sB,0BAA4B9sB,KAAK+sB,qBAAqB7sB,KAAKF,MAC3DgtB,mBAAqBhtB,KAAKitB,cAAc/sB,KAAKF,MAE9C,iBAAAe,GACLf,KAAKgB,uBAELhB,KAAKoB,WAAWC,cAAc,gBAAgB3B,iBAAiB,QAC7DM,KAAKmI,qBACPnI,KAAKoB,WAAWC,cAA2B,YAAY3B,iBAAiB,QACtEM,KAAK0sB,uBACP1sB,KAAKoB,WAAWC,cAA2B,SAAS3B,iBAAiB,QACnEM,KAAKgtB,oBAEP,IAAK,MAAMvM,KAAQzgB,KAAKgD,iBAAiB,8BACvCyd,EAAK/gB,iBAAiB,QAASM,KAAK4sB,oBAGtC,IAAK,MAAM7qB,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBACtCb,iBAAiB,qBAAsBM,KAAKwsB,yBAC5C9sB,iBAAiB,uBAAwBM,KAAK8sB,2BAE9C9sB,KAAKuG,QACP,CAEO,oBAAArF,GAAuB,IAAA2oB,EAAAqD,EAAAC,EACiB,OAA7CtD,EAAA7pB,KAAKoB,WAAWC,cAAc,kBAA9BwoB,EAA+ClqB,oBAAoB,QACjEK,KAAKmI,qBAC+C,OAAtD+kB,EAAAltB,KAAKoB,WAAWC,cAA2B,cAA3C6rB,EAAwDvtB,oBAAoB,QAC1EK,KAAK0sB,uBAC4C,OAAnDS,EAAAntB,KAAKoB,WAAWC,cAA2B,WAA3C8rB,EAAqDxtB,oBAAoB,QACvEK,KAAKgtB,oBAEP,IAAK,MAAMvM,KAAQzgB,KAAKgD,iBAAiB,8BACvCyd,EAAK9gB,oBAAoB,QAASK,KAAK4sB,oBAGzCjtB,oBAAoB,eAAgBK,KAAKO,oBACzCZ,oBAAoB,qBAAsBK,KAAKwsB,yBAC/C7sB,oBAAoB,uBAAwBK,KAAK8sB,0BACnD,CAEO,wBAAA1mB,CAAyBpB,EAAc0D,EAAmBpC,GAC3DtG,KAAK0B,aAAwB,yBAATsD,GAAgD,SAAbsB,GACzDtG,KAAK2I,kBAET,CAEQ,oBAAA3H,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,+mDAEhC,CAKQ,gBAAAkH,UACC3I,KAAK4B,QAAQmG,eACpB/H,KAAKoB,WAAWC,cAAc,gBAAgB2G,MAAM,CAAEiB,cAAc,GACtE,CAEQ,aAAAzI,CAAcuB,GACpB/B,KAAKotB,YAAcrrB,EAAMoC,OAAOkF,KAAKC,MAAMuD,KACzChB,GAAgD,SAAxCA,EAAKiB,WAAWC,oBAC5B,CAEQ,oBAAAggB,CAAqBhrB,GAA4D,IAAAsrB,EAAAC,EAAAC,EACvF,GAA4B,SAAxBvtB,KAAK4B,QAAQ4rB,OACf,OAGF,MAAMC,EAAQztB,KAAKgD,iBAAiB,8BACpC,GAAqB,IAAjByqB,EAAMxqB,OACR,OAGF,MAAMyqB,EAAS1tB,KAAK8C,QAAQ,eACtBM,EAAOsqB,EAAO9rB,QAAQwB,MAAQ,OAC9B0T,EAAqB,OAAfuW,EAAGtrB,EAAMoC,aAAM,EAAZkpB,EAAcvW,OAK7B,GAAe,aAAXA,EAAuB,CAEzB,KADwC,aAA1B4W,EAAO9rB,QAAQmE,QAAuD,SAA9B2nB,EAAO9rB,QAAQ2F,aAC7C,SAATnE,GAA4B,aAATA,EAChC,MAEJ,CAEA,GAAqB,IAAjBqqB,EAAMxqB,QAgBV,GARAjD,KAAK4B,QAAQJ,KAAkB,WAAXsV,EAAsB,SAAW,SACrD9W,KAAK4B,QAAQ+rB,WAAsB,WAATvqB,EAAoB,SAAW,OACzDpD,KAAK4tB,kBAAgD,OAA/BN,EAAmB,OAAnBC,EAAGvtB,KAAKotB,kBAAW,EAAhBG,EAAkBjf,YAAUgf,EACnDvf,SAAS0f,EAAM,GAAG7rB,QAAQ4N,UAAW,IACvCxP,KAAKuG,SAELmnB,EAAO9rB,QAAQwB,KAAO,QAEP,aAAX0T,GAAoC,WAAXA,EAAqB,CAEhD,MAAM+W,EAAe,IAAIvpB,YAAoB,6BAA8B,CACzEC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN8W,SAAUnC,IACVhC,YAGJ9W,KAAK2E,cAAckpB,EACrB,OA1BO7tB,KAAKotB,aACRptB,KAAK8tB,QAAQL,EAAM,IAAIzL,MAAM/f,QAAQC,KA0B3C,CAEQ,aAAA2qB,CAAc9qB,GACpB,GAA4B,SAAxB/B,KAAK4B,QAAQ4rB,OACf,OAGF,MAAM/M,EAAgC1e,EAAMK,cAC5CpC,KAAK4tB,kBAAoB7f,SAAS0S,EAAK7e,QAAQ4N,UAAW,IAC1DxP,KAAKuG,QACP,CAEQ,gBAAAomB,CAAiB1oB,GAAoB,IAAA8pB,EAC3C,GAA4B,SAAxB/tB,KAAK4B,QAAQ4rB,OACf,OAGF,IAAoB,OAAhBO,EAAA/tB,KAAKotB,kBAAW,EAAhBW,EAAkBzf,cAAetO,KAAK4tB,kBAExC,YADA5tB,KAAKguB,SAAShuB,KAAK4B,QAAQ+rB,YAI7B,MAAMlN,EAAOzgB,KAAKqB,cAChB,+CAA+CrB,KAAK4tB,uBACjDnN,IAILzgB,KAAK4B,QAAQ4rB,OAAS,OACtBxtB,KAAKuG,SAELvG,KAAKiuB,SAASxN,GAAMuB,MAAM/f,QAAQC,MAAMgsB,QAAQ,YACvCluB,KAAK4B,QAAQ4rB,OACpBxtB,KAAKuG,WAET,CAEQ,aAAA0mB,CAAchpB,GACQ,SAAxBjE,KAAK4B,QAAQ4rB,SAIS,WAAtBxtB,KAAK4B,QAAQJ,MACfxB,KAAKmuB,eAGPnuB,KAAKguB,SAAShuB,KAAK4B,QAAQ+rB,YAC7B,CAMQ,kBAAAlB,CAAmBxoB,GAEG,UADbjE,KAAK8C,QAAQ,eACjBlB,QAAQwB,MAA4C,SAAxBpD,KAAK4B,QAAQ4rB,QAC5B,WAAtBxtB,KAAK4B,QAAQJ,MACbxB,KAAKmuB,cAET,CAEQ,cAAA/lB,CAAenE,GAErB,MAAMlC,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNM,SAAS,EACTC,OAAQ,kBAGZ1E,KAAK2E,cAAc5C,EACrB,CAMQ,YAAAosB,GACNzb,EAAO1S,KAAM,CAAEuL,WAAY,CAAE6iB,aAAc,SAAY,2BACpDpM,MAAM/f,QAAQC,MAGjB,MAAMmsB,EAAe,IAAI/pB,YAAoB,6BAA8B,CACzEC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN8W,SAAUnC,OAGd9Y,KAAK2E,cAAc0pB,EACrB,CAOA,cAAcJ,CAASxN,GAA+B,IAAA6N,EACpD,GAAItuB,KAAKotB,oBACDtrB,EAAO9B,KAAM,CAACA,KAAKotB,YAAYjgB,KAAM,6BACvCnN,KAAKotB,aAEP,YADAptB,KAAKguB,SAAS,cAKZhuB,KAAK8tB,QAAQrN,GAEnB,MAAM8N,GAAwB,OAAhBD,EAAAtuB,KAAKotB,kBAAW,EAAhBkB,EAAkBhgB,cAAeP,SAAS0S,EAAK7e,QAAQ4N,UAAW,IAChFxP,KAAKguB,SAASO,EAAQvuB,KAAK4B,QAAQ+rB,WAAa,OAClD,CAMA,aAAcG,CAAQrN,GAA+B,IAAA+N,EACnD,MAAM3iB,EAAgB,CACpB6E,GAAI+P,EAAK7e,QAAQ4N,UACjB5E,SAAU,EACVkC,WAAY,CACVC,oBAAqB,OACrBiB,0BAA2ByS,EAAK7e,QAAQuK,QAM5C,SAFMtC,GAAI7J,KAAM,CAAC6L,GAAO,GAAI,2BAER,OAAhB2iB,EAAAxuB,KAAKotB,kBAAW,EAAhBoB,EAAkBlgB,cAAeP,SAAS0S,EAAK7e,QAAQ4N,UAAW,IACpE,OAIF,MAAMif,EAAe,IAAInqB,YAAoB,6BAA8B,CACzEC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN8W,SAAUnC,OAGd9Y,KAAK2E,cAAc8pB,EACrB,CAEQ,QAAAT,CAAS5qB,GACfpD,KAAK8C,QAAQ,eAAelB,QAAQwB,KAAOA,GAAQ,MACrD,CAEQ,MAAAmD,GACN,MAAMknB,EAAQztB,KAAKgD,iBAAiB,8BAC9B0rB,EAA+B,WAAtB1uB,KAAK4B,QAAQJ,KAE5BxB,KAAKoB,WAAWC,cAAc,UAAUyJ,YAAc4jB,EACpD,4BAA8B,wBAChC1uB,KAAKoB,WAAWC,cAAc,aAAayJ,YACzC,aAAa2iB,EAAMxqB,gCAErB,IAAK,MAAMwd,KAAQgN,EAAO,CACxB,MAAMnc,EAAWvD,SAAS0S,EAAK7e,QAAQ4N,UAAW,MAAQxP,KAAK4tB,kBAC/DnN,EAAK7e,QAAQ0P,SAAWA,EAAW,OAAS,OAC9C,CAEA,MAAMkc,EAAiC,SAAxBxtB,KAAK4B,QAAQ4rB,OAEtBmB,EAAU3uB,KAAKoB,WAAWC,cAAiC,YAC3DutB,EAAQD,EAAQttB,cAA2B,UACjDutB,EAAM9jB,YAAc4jB,EAAS,mBAAqB,iBAClDE,EAAMjnB,OAAS6lB,EACfmB,EAAQttB,cAAc,qBAAqBsG,QAAU6lB,EACrDmB,EAAQpsB,SAAWirB,EACnBmB,EAAQ/L,aAAa,YAAa4K,EAAS,OAAS,SAEpD,MAAMqB,EAAO7uB,KAAKoB,WAAWC,cAAiC,SAC9DwtB,EAAK/jB,YAAc4jB,EAAS,YAAc,uBAC1CG,EAAKtsB,SAAWirB,CAClB,EA0CGpoB,eAAeC,IAAI,eACtBD,eAAeE,OAAO,aAAcinB,WC/ZtC,MAAMuC,gCAAgCjvB,YACpC,6BAAkB2F,GAChB,MAAO,CACL,gBACA,aACA,iBACA,aACA,gBACA,kBAEJ,CAoBO,iBAAAzE,GACLf,KAAKgB,uBACLhB,KAAKuG,QACP,CAEQ,oBAAAvF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,ohCAEhC,CAEO,wBAAA2E,CAAyByb,EAAexb,EAAkBC,GAC/DtG,KAAKgB,uBAEDhB,KAAK0B,aAAe2E,IAAaC,GACnCtG,KAAKuG,QAET,CAEQ,MAAAA,GACN,MAAMwoB,EAAU/uB,KAAKoB,WAAWC,cAAc,OAC1C0tB,IACFA,EAAQ3M,IAAMpiB,KAAK4B,QAAQotB,SAC3BD,EAAQ1M,IAAMriB,KAAK4B,QAAQyN,OAG7B,MAAMgY,EAAUrnB,KAAKoB,WAAWC,cAAc,QAC9C,GAAIgmB,EAAS,CACX,MAAMlb,EAAQ4B,SAAS/N,KAAK4B,QAAQuK,MAAO,IAE3Ckb,EAAQvc,YAAcsd,GAAYjc,EAAO,0BAC3C,CAEA,MAAM8iB,EAAUjvB,KAAKoB,WAAWC,cAAc,MAC1C4tB,IACFA,EAAQnkB,YAAc9K,KAAK4B,QAAQyN,OAGrC,MAAM6f,EAAalvB,KAAKoB,WAAWC,cAAc,MAC7C6tB,IACFA,EAAWpkB,YAAc9K,KAAK4B,QAAQutB,UAGxC,MAAMC,EAAkBpvB,KAAKoB,WAAWC,cAAgC,uBAC1C,SAA1BrB,KAAK4B,QAAQ0P,SACf8d,EAAgBC,SAAU,EAE1BD,EAAgBC,SAAU,CAE9B,EASGjqB,eAAeC,IAAI,+BACtBD,eAAeE,OAAO,6BAA8BwpB,yBC5EtD,MAAMQ,iCAAiCzvB,YAiD7BQ,gCAAkCL,KAAKM,2BAA2BJ,KAAKF,MACvEO,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAO7CuvB,eAAgB,EAOhBC,kBAAmB,EAKnB1lB,eAAiB,EAKjB2lB,aAAe,EAMfxG,qBAAsB,EAKtByG,8BAA+B,EAM/BC,qBAAsB,EACtBC,mBAAoB,EAMpBC,oBAAqB,EAMrBC,wBAED,iBAAA/uB,GACLf,KAAKgB,uBAEL,IAAK,MAAMe,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKQ,cAAcuB,EACrB,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBACtCb,iBAAiB,6BAA8BM,KAAKK,iCAEpDL,KAAKuG,QACP,CAEO,oBAAArF,GACLvB,oBAAoB,eAAgBK,KAAKO,oBACzCZ,oBAAoB,6BAA8BK,KAAKK,gCACzD,CAEQ,oBAAAW,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,stWAEhC,CAEQ,aAAAjB,CAAcuB,GAAyB,IAAAuJ,EAC7C,MAAMjC,EAAOtH,EAAMoC,OAAOkF,KAC1B,IAAI4f,GAAsB,EAC1B,IAAK,MAAMpd,KAAQxC,EAAKC,MAAO,KAAAwC,EAC7B,GAAQ,MAAJD,GAA6B,OAAzBC,EAAJD,EAAMG,0BAAqC,OAAdF,EAA7BA,EAA+BG,eAA/BH,EAA6CI,qBAAsB,CACrE+c,GAAsB,EACtB,KACF,CACF,CAEAjpB,KAAKipB,oBAAsBA,EAG3BjpB,KAAK8vB,wBAA0BzmB,EAAKC,MAAMuD,KACxChB,GAAgD,SAAxCA,EAAKiB,WAAWC,qBAC1B/M,KAAK8J,eAAiBT,EAAKU,YAC3B/J,KAAKyvB,aAAezvB,KAAK8vB,wBAA0BzmB,EAAKwB,WAAa,EAAIxB,EAAKwB,WAE9E7K,KAAK+vB,oBAAoB1mB,GAEzB,MAAM2mB,EAAwC,mBAA1BjuB,EAAMoC,OAAOoG,SAC3B0lB,EAA4C,UAAnB,OAAf3kB,EAAAjC,EAAKkC,iBAAU,EAAfD,EAAiB8iB,cAEjC,GAAIpuB,KAAK8vB,wBAAyB,CAKhC,MAAMI,EAAW1P,EAAqBnX,EAAMrJ,KAAKmwB,wBACjD,GAAID,EAASjtB,OAAS,EAYpB,YALI+sB,GACFluB,EAAO9B,KAAMkwB,EAAU,6BACpBlO,MAAM/f,QAAQC,MAKvB,MAAYlC,KAAK4vB,mBAAqBK,GAAWD,EAI/ChwB,KAAKwvB,kBAAmB,EACfxvB,KAAK4vB,oBAAsBK,GAiU1C,SAAyBluB,GACvB,GAAIA,EAAMoC,OAAOC,WACf,OAAO,EAGT,MAAMmG,EAAWxI,EAAMoC,OAAOoG,UAAY,GAC1C,GAAiB,mBAAbA,GAA8C,sBAAbA,EACnC,OAAO,EAGT,OAAQA,EAAS4K,WAAW,qBAC9B,CA5UqDib,CAAgBruB,KAC/D/B,KAAKuvB,eAAgB,GAGvBvvB,KAAKuG,QACP,CAKQ,oBAAA4pB,GACN,MAAO,CACL7P,MAAOtgB,KAAK4B,QAAQgf,eACpBhW,SAAU5K,KAAK4B,QAAQif,kBACvB5hB,KAAMe,KAAK4B,QAAQkf,cAEvB,CAOQ,mBAAAiP,CAAoB1mB,GAC1B,MAAMgnB,EAAWnQ,EAAoB7W,EAAMrJ,KAAKmwB,wBAEhD,GAAInwB,KAAK6vB,oBAAsB7vB,KAAK4vB,oBAAsBS,EAAU,CAClE,MACMtuB,EAAQ,IAAIuC,YADA+rB,EAAW,8BAAgC,4BACpB,CACvC9rB,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN8W,SAAUnC,OAGd9Y,KAAK2E,cAAc5C,EACrB,CAEA/B,KAAK4vB,kBAAoBS,EACzBrwB,KAAK6vB,oBAAqB,EAC1B7vB,KAAK4B,QAAQyuB,SAAWA,EAAW,OAAS,OAC9C,CAEQ,MAAA9pB,GACNvG,KAAK4B,QAAQ0uB,QAAUtwB,KAAK8vB,wBAA0B,OAAS,QAE/D9vB,KAAKuwB,0BACLvwB,KAAKwwB,yBACLxwB,KAAKywB,wBACLzwB,KAAK0wB,yBAC8B,kBAA/B1wB,KAAK4B,QAAQkf,cACf9gB,KAAK2wB,kCAEL3wB,KAAK4wB,8BAET,CAMQ,uBAAAL,GACN,MAAMM,IAAaxxB,SAASgC,cAAc,yCACpCqK,EAAO1L,KAAK4vB,oBAAsB5vB,KAAK8vB,yBAA2Be,EACxE,IAAK,MAAMhqB,KAAWxH,SAAS2D,iBAAiB,0BAC9C6D,EAAQc,QAAU+D,CAEtB,CAMQ,0BAAApL,CAA2B2D,GASjC,GARIjE,KAAKwvB,mBACPxvB,KAAKwvB,kBAAmB,EACnBxvB,KAAK4vB,mBAAsB5vB,KAAK8vB,yBACnCpd,EAAO1S,KAAM,CAAEuL,WAAY,CAAE6iB,aAAc,KAAQ,mCAChDpM,MAAM/f,QAAQC,QAIhBlC,KAAKuvB,cACR,OAKF,GAFAvvB,KAAKuvB,eAAgB,GAEhBvvB,KAAK4vB,mBAAqB5vB,KAAK8vB,wBAClC,OAIF,MAAM/tB,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN2S,OAAQ,cAGZ9W,KAAK2E,cAAc5C,EACrB,CAEQ,+BAAA4uB,GACN,MAAMG,EAAY9wB,KAAKoB,WAAWC,cAA2B,oBAC7D,IAAKrB,KAAK4B,QAAQif,kBAGhB,YADAiQ,EAAUrL,MAAMC,QAAU,QAK5BoL,EAAUrL,MAAMC,QAAU,GAC1B,MAAM7E,EAAoB9S,SAAS/N,KAAK4B,QAAQif,kBAAmB,IAC7DkQ,EAAW/pB,KAAKse,MAAM,IAAMzE,GAE5BmQ,EAAQF,EAAUzvB,cAA2B,mBAEnD,IAAKrB,KAAK0vB,6BAA8B,CAGtCsB,EAAMvvB,UAAY,GAClB,MAAMwvB,EAAmB5xB,SAAS4P,cAAc,OAChDgiB,EAAiBrnB,UAAUC,IAAI,qBAC/BmnB,EAAMvjB,YAAYwjB,GAElB,IAAK,IAAIzY,EAAI,EAAGA,GAAKqI,EAAmBrI,IAAK,CAC3C,MAAM0Y,EAAa1Y,EAAIuY,EACjBI,EAAO9xB,SAAS4P,cAAc,OACpCkiB,EAAKvnB,UAAUC,IAAI,iBACnBsnB,EAAK1L,MAAM2L,KAAO,GAAGF,KACrBC,EAAKvvB,QAAQyvB,aAAe7Y,EAAE5V,WAC9BuuB,EAAKvO,aAAa,aAChB,OAAO/B,2DAA2ErI,KACpF2Y,EAAKrmB,YAAc0N,EAAE5V,WACrBouB,EAAMvjB,YAAY0jB,EACpB,CAEAnxB,KAAK0vB,8BAA+B,CACtC,CAEA,IAAK,IAAIlX,EAAI,EAAGA,GAAKqI,EAAmBrI,IAAK,CAC3C,MAAM8Y,EAAON,EAAM3vB,cAA2B,wBAAwBmX,OAClE8Y,EACFA,EAAK1vB,QAAQyuB,SAAW7X,GAAKxY,KAAKyvB,aAAe,OAAS,QAE1DxtB,QAAQC,KAAK,qCAAqCsW,iBAEtD,CAEA,MAAMyY,EAAmBH,EAAUzvB,cAA2B,sBAC9D,GAAIrB,KAAKyvB,cAAgB1hB,SAAS/N,KAAK4B,QAAQif,kBAAmB,IAChEoQ,EAAiBxL,MAAMnD,MAAQ,WAC1B,CACL,MAAMiP,EAAgBvxB,KAAKyvB,aAAesB,EAC1CE,EAAiBxL,MAAMnD,MAAQ,GAAGiP,IACpC,CACF,CAEQ,4BAAAX,GACN,MAAME,EAAY9wB,KAAKoB,WAAWC,cAA2B,oBAC7D,IAAKrB,KAAK4B,QAAQgf,eAGhB,YADAkQ,EAAUrL,MAAMC,QAAU,QAK5BoL,EAAUrL,MAAMC,QAAU,GAC1B,MAAM6D,EAAYxb,SAAS/N,KAAK4B,QAAQgf,eAAgB,IAClD4Q,EAAWV,EAAUzvB,cAA2B,cAElDrB,KAAK8J,gBAAkByf,GACzBvpB,KAAK4vB,mBAAoB,EACzB4B,EAAS5nB,UAAUC,IAAI,cAEvB7J,KAAK4vB,mBAAoB,EACzB4B,EAAS5nB,UAAU9H,OAAO,aAG5B,MAAMmvB,EAAmBH,EAAUzvB,cAA2B,sBACxDowB,EAAQzxB,KAAK8J,eACnB,GAAI2nB,GAAS,EAEX,YADAR,EAAiBxL,MAAMnD,MAAQ,MAIjC,MAAM+G,EAAariB,KAAKuZ,IAAIkR,EAAQlI,EAAW,GAC/C0H,EAAiBxL,MAAMnD,MAAwB,IAAb+G,EAAH,GACjC,CAOQ,sBAAAqH,GACN,GAAsC,SAAlC1wB,KAAK4B,QAAQ8vB,iBACf,OAIF,GAAmC,kBAA/B1xB,KAAK4B,QAAQkf,cACf,OAGF,MAAM6Q,EAAe3xB,KAAKoB,WAAWC,cACnC,kCAEF,IAAKrB,KAAK4B,QAAQgwB,2BAGhB,YADAD,EAAalM,MAAMC,QAAU,QAK/BiM,EAAalM,MAAMC,QAAU,OAE7B,MAAMmM,EAAoB9jB,SAAS/N,KAAK4B,QAAQgwB,2BAA4B,IACtEvR,EAAgBtS,SAAS/N,KAAK4B,QAAQgf,eAAgB,IACtDkR,EAAqB9qB,KAAKuZ,IAAIsR,EAAoBxR,EAAe,GAElErgB,KAAK2vB,sBACR3vB,KAAK2vB,qBAAsB,EAC3BgC,EAAalM,MAAM2L,KAA+B,IAArBU,EAAH,KAGxB9xB,KAAK8J,eAAiB+nB,GAAqB7xB,KAAKipB,oBAClD0I,EAAa/nB,UAAUC,IAAI,YAE3B8nB,EAAa/nB,UAAU9H,OAAO,WAElC,CAMQ,qBAAA2uB,GACN,GAAsC,SAAlCzwB,KAAK4B,QAAQ8vB,iBACf,OAGF,MAAMK,EAAW/xB,KAAKoB,WAAWC,cAA2B,sBAC5D,IAAKrB,KAAK4B,QAAQgwB,2BAGhB,YADAG,EAAStM,MAAMC,QAAU,QAK3BqM,EAAStM,MAAMC,QAAU,OACzB,MACM+D,EADoB1b,SAAS/N,KAAK4B,QAAQgwB,2BAA4B,IACrC5xB,KAAK8J,eAEtC8H,EAASwW,GAAYqB,EAAY,2BACjCuI,EAAkBvI,EAAa,EACnC,UAAU7X,4BAAmC,0BACzCqgB,EAAkBF,EAAS1wB,cAAc,SAC3C4wB,IACFA,EAAgBnnB,YAAcknB,EAElC,CAMQ,sBAAAxB,GACN,MAAM0B,EAAclyB,KAAKoB,WAAWC,cAA2B,kBAC/D,IAAIwF,EAAU,GACd,GAAmC,kBAA/B7G,KAAK4B,QAAQkf,cAAmC,CAClD,IAAK9gB,KAAK4B,QAAQif,kBAGhB,YADAqR,EAAYzM,MAAMC,QAAU,QAK9B,MAAM7E,EAAoB9S,SAAS/N,KAAK4B,QAAQif,kBAAmB,IACnE,GAAI7gB,KAAKyvB,cAAgB5O,EAEvB,YADAqR,EAAYzM,MAAMC,QAAU,QAK9BwM,EAAYzM,MAAMC,QAAU,OAC5B,MAAM+D,EAAa5I,EAAoB7gB,KAAKyvB,aAE5C5oB,GAAW,OAAO4iB,UADY,IAAfA,EAAmB,OAAS,kCAE7C,KAAO,CACL,IAAKzpB,KAAK4B,QAAQgf,eAGhB,YADAsR,EAAYzM,MAAMC,QAAU,QAK9B,MAAM6D,EAAYxb,SAAS/N,KAAK4B,QAAQgf,eAAgB,IACxD,GAAI2I,GAAavpB,KAAK8J,eAEpB,YADAooB,EAAYzM,MAAMC,QAAU,QAK9BwM,EAAYzM,MAAMC,QAAU,OAI5B7e,EAAU,GADKuhB,GAFImB,EAAYvpB,KAAK8J,eAEG,qDAEzC,CAEaooB,EAAY7wB,cAAc,SAClCyJ,YAAcjE,CACrB,EAuCGzB,eAAeC,IAAI,gCACtBD,eAAeE,OAAO,8BAA+BgqB,0BCzhBvD,MAAM6C,4BAA4BtyB,YAChC,6BAAkB2F,GAChB,MAAO,CAAC,cACV,CAwBQjF,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAK7C8J,eAAiB,EAMjBmf,qBAAsB,EAIvB,iBAAAloB,GACLf,KAAKgB,uBAEL,IAAK,MAAMe,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBAEtCP,KAAKuG,QACP,CAEO,oBAAArF,GACLvB,oBAAoB,eAAgBK,KAAKO,mBAC3C,CAEO,wBAAA6F,CAAyBpB,EAAcqB,EAAkBC,GAC9DtG,KAAKgB,uBAEDqF,IAAaC,GAIbtG,KAAK0B,cACP1B,KAAKoyB,mBACLpyB,KAAKuG,SAET,CAEQ,oBAAAvF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,0tDAEhC,CAEQ,aAAAjB,CAAcuB,GACpB,MAAMsH,EAAOtH,EAAMoC,OAAOkF,KAC1BrJ,KAAK8J,eAAiBT,EAAKU,YAE3B,IAAIkf,GAAsB,EAC1B,IAAK,MAAMpd,KAAQxC,EAAKC,MAAO,KAAAwC,EAC7B,GAAQ,MAAJD,GAA6B,OAAzBC,EAAJD,EAAMG,0BAAqC,OAAdF,EAA7BA,EAA+BG,eAA/BH,EAA6CI,qBAAsB,CACrE+c,GAAsB,EACtB,KACF,CACF,CAEAjpB,KAAKipB,oBAAsBA,EAE3BjpB,KAAKuG,QACP,CAEQ,gBAAA6rB,GACN,MAAMpB,EAAQhxB,KAAKgD,iBAAiB,iBAEpChD,KAAKqyB,WAAa,GAClB,IAAK,MAAMlB,KAAQH,EACjBhxB,KAAKqyB,WAAW7gB,KAAK,CACnBxM,KAAMmsB,EAAKvvB,QAAQ0wB,UACnB/I,UAAWxb,SAASojB,EAAKvvB,QAAQgf,eAAgB,KAGvD,CAEQ,MAAAra,GACDvG,KAAKqyB,YACRryB,KAAKoyB,mBAGP,MAAMlJ,EAAOlpB,KAAKoB,WAAWC,cAAc,QACrC8nB,EAAoBnpB,KAAKoB,WAAWC,cAAc,uBAClD+nB,EAAcppB,KAAKoB,WAAWC,cAA2B,iBAI/D,IAAIgoB,EACJ,GAHAH,EAAKpe,YAAc9K,KAAKspB,iBAGpBtpB,KAAKipB,oBACPI,EAAa,QACR,CACL,MAAME,EAAYxb,SAAS/N,KAAK4B,QAAQ2nB,UAAW,IACnDF,EAAariB,KAAKuZ,IAAI,IAAMvgB,KAAK8J,eAAiByf,EAAa,IACjE,CAEAF,EAAariB,KAAKwX,MAAM6K,GACpBR,MAAMQ,KACRA,EAAa,GAGfF,EAAkBvG,aAAa,gBAAiByG,EAAWzmB,YAC3DwmB,EAAY3D,MAAMnD,MAAQ+G,EAAa,IAEvC,MAAMkJ,EAAqBvyB,KAAKoB,WAAWC,cAAc,eACzDkxB,EAAmB9wB,UAAY,GAG/B,IAAI+wB,EAAY,EAChB,IAAK,MAAMrB,KAAQnxB,KAAKqyB,WAAY,CAClCG,IACA,MAAMC,EAAUpzB,SAAS4P,cAAc,OACvCwjB,EAAQ7oB,UAAUC,IAAI,QAEtB,MAAM6oB,EAAUrzB,SAAS4P,cAAc,QAQvC,GAPAyjB,EAAQ5nB,YAAcqmB,EAAKnsB,KAC3BytB,EAAQhlB,YAAYilB,GAEhB1yB,KAAK8J,gBAAkBqnB,EAAK5H,WAC9BkJ,EAAQ7oB,UAAUC,IAAI,YAGpB7J,KAAKqyB,WAAWpvB,SAAWuvB,EAC7BC,EAAQ7oB,UAAUC,IAAI,YACjB,CACL,MAAM0f,EAAYxb,SAAS/N,KAAK4B,QAAQ2nB,UAAW,IAC7CF,EAAariB,KAAKuZ,IAAI,IAAM4Q,EAAK5H,UAAYA,EAAa,KAChEkJ,EAAQhN,MAAM2L,KAAO,GAAG/H,IAC1B,CAEAkJ,EAAmB9kB,YAAYglB,EACjC,CAGA,MAAME,EAAsB3yB,KAAKoB,WAAWC,cAAc,oBAC1DsxB,EAAoBlxB,UAAY,GAChC+wB,EAAY,EACZ,IAAK,MAAMrB,KAAQnxB,KAAKqyB,WAAY,CAClCG,IACA,MAAMI,EAAmBvzB,SAAS4P,cAAc,OAKhD,GAJA2jB,EAAiBhpB,UAAUC,IAAI,aAE/B+oB,EAAiB9nB,YAAcsd,GAAY+I,EAAK5H,UAAW,2BAEvDvpB,KAAKqyB,WAAWpvB,SAAWuvB,EAC7BI,EAAiBhpB,UAAUC,IAAI,YAC1B,CACL,MAAM0f,EAAYxb,SAAS/N,KAAK4B,QAAQ2nB,UAAW,IAC7CF,EAAariB,KAAKuZ,IAAI,IAAM4Q,EAAK5H,UAAYA,EAAa,KAChEqJ,EAAiBnN,MAAM2L,KAAO,GAAG/H,IACnC,CAEAsJ,EAAoBllB,YAAYmlB,EAClC,CACF,CAEQ,cAAAtJ,GACN,GAAItpB,KAAKipB,oBACP,OAAOjpB,KAAK4B,QAAQ4nB,kBAClB,iDAGJ,MACMC,EADY1b,SAAS/N,KAAK4B,QAAQ2nB,UAAW,IACpBvpB,KAAK8J,eAEpC,OAAI2f,EAAa,EAER,UAAarB,GAAYqB,EAAY,eAC1C,4BAEK,4CAEX,EAcGrkB,eAAeC,IAAI,2BACtBD,eAAeE,OAAO,yBAA0B6sB,qBCrOlD,MAAMU,sBAAsBhzB,YAiBlBwJ,KAAa,KAMbypB,mBAAqB,EAKrBC,aAA8B,KAE9B5yB,8BAAgCH,KAAKI,yBAAyBF,KAAKF,MACnEgzB,4BAA8BhzB,KAAKizB,uBAAuB/yB,KAAKF,MAC/DO,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7C2F,uBAAyB3F,KAAK4F,kBAAkB1F,KAAKF,MACrDkzB,0BAA4BlzB,KAAKmzB,qBAAqBjzB,KAAKF,MAC3DozB,qBAAuBpzB,KAAKqzB,gBAAgBnzB,KAAKF,MAElD,iBAAAe,GACLf,KAAKgB,uBAEQhB,KAAKoB,WAAWC,cAA+B,kBACvD3B,iBAAiB,SAAUM,KAAKkzB,2BAEhBlzB,KAAKoB,WAAWC,cAAgC,UACxD3B,iBAAiB,QAASM,KAAKozB,sBAG5CpzB,KAAK8yB,mBAAqB,EAE1B9yB,KAAK+yB,aAAe,KAEpB,IAAK,MAAMhxB,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBACtCb,iBAAiB,6BAA8BM,KAAKgzB,6BACpDtzB,iBAAiB,2BAA4BM,KAAKG,+BAMlDH,KAAKN,iBAAiB,oBAAqBM,KAAK2F,wBAEhD3F,KAAKuG,QACP,CAEO,oBAAArF,GACL,MAAMoyB,EAAOtzB,KAAKoB,WAAWC,cAA+B,kBACxD,MAAJiyB,GAAAA,EAAM3zB,oBAAoB,SAAUK,KAAKkzB,2BAEzC,MAAMK,EAAevzB,KAAKoB,WAAWC,cAAgC,UACzD,MAAZkyB,GAAAA,EAAc5zB,oBAAoB,QAASK,KAAKozB,sBAEhDzzB,oBAAoB,eAAgBK,KAAKO,oBACzCZ,oBAAoB,6BAA8BK,KAAKgzB,6BACvDrzB,oBAAoB,2BAA4BK,KAAKG,+BAErDH,KAAKL,oBAAoB,oBAAqBK,KAAK2F,uBACrD,CAEQ,oBAAA3E,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,g2IAEhC,CAEQ,MAAA8E,GAQN,MAAMitB,EAAQtZ,EAA2Bla,KAAKqJ,MACxCoqB,EzBAH,SAA4BpqB,GAyBjC,MAAMoqB,EAAkB,IAAIC,IAE5B,IAAKrqB,EACH,OAAOoqB,EAQT,GAAIxiB,MAAMC,QAAQ7H,EAAK0M,kCACrB,IAAK,MAAME,KAAe5M,EAAK0M,iCAC7B,GAAyB,kBAArBE,EAAYhX,KAA0B,CACxC,MAAM8S,EAAOkE,EAAY5G,MAAM2K,cACzB4L,EAAW3P,EAAY4P,uBACvB8N,EAAeF,EAAgBpuB,IAAI0M,IAAS,EAClD0hB,EAAgB3vB,IAAIiO,EAAM4hB,EAAe/N,EAC3C,CAMJ,GAAI3U,MAAMC,QAAQ7H,EAAKC,OACrB,IAAK,MAAM+E,KAAYhF,EAAKC,MAC1B,GAAK2H,MAAMC,QAAQ7C,EAASqD,iCAI5B,IAAK,MAAMD,KAAcpD,EAASqD,gCAChC,GAA6C,kBAAzCD,EAAWE,qBAAqB1S,KAA0B,CAC5D,MAAM8S,EAAON,EAAWE,qBAAqBtC,MAAM2K,cAC7C4L,EAAWnU,EAAWG,OACtB+hB,EAAeF,EAAgBpuB,IAAI0M,IAAS,EAClD0hB,EAAgB3vB,IAAIiO,EAAM4hB,EAAe/N,EAC3C,CAKN,OAAO6N,CACT,CyBnE4BG,CAAmB5zB,KAAKqJ,MAc1CwqB,EAAY5iB,MAAM6iB,KAAKL,EAAgB7d,QAAQzE,OAAO9B,IAAUmkB,EAAMpe,SAAS/F,IAEjFwkB,EAAU5wB,OAAS,GACrBhB,QAAQK,IAAI,yCAA0CuxB,EAAU9oB,KAAK,OAGvE,MAAMgpB,EAAqB/zB,KAAKoB,WAAWC,cAAc,sBACzD0yB,EAAmBtyB,UAAY,GAE/B,MAAMuyB,EAA0Bh0B,KAAKoB,WAAWC,cAC9C,8BAEF,IAAK,MAAM0Q,KAAQyhB,EAAO,CACxB,MAAMS,EAA0Bj0B,KAAKk0B,sBAAsBF,EAAyBjiB,EAClF0hB,EAAgBpuB,IAAI0M,IACtBgiB,EAAmBtmB,YAAYwmB,EACjC,CAEA,MAAMpgB,EAAQ7T,KAAKoB,WAAWC,cAAgC,UAC9DwS,EAAMtR,SAAWvC,KAAK8yB,mBAAqB,EAEtB9yB,KAAKoB,WAAWC,cAAiC,kBACzDkB,SAAWsR,EAAM9U,MAAMkE,OAAS,GAAKjD,KAAK8yB,mBAAqB,EAE5E,MAAM1P,EAAQpjB,KAAKoB,WAAWC,cAAc,iBAC5C,GAAIrB,KAAK+yB,aAAc,CACrB3P,EAAMxhB,QAAQiiB,UAAY,SAC1BT,EAAM3hB,UAAY,GAClB,MAAMynB,EAAO7pB,SAAS4P,cAAc,QACpCia,EAAKpe,YAAc9K,KAAK+yB,aACxB3P,EAAM3V,YAAYyb,GAClB9F,EAAMzb,QAAS,CACjB,MACEyb,EAAMzb,QAAS,CAEnB,CAUQ,qBAAAusB,CAAsBC,EAA+BpiB,EAAcH,GACzE,MAAMqiB,EAA4CE,EAAS5hB,QAAQC,WAAU,GAEvE4hB,EAAeH,EAAwB5yB,cAA8B,kBAC3E+yB,EAAaxyB,QAAQmQ,KAAOA,EAK5B,GAHeqiB,EAAa/yB,cAAc,kBACnCyJ,YAAciH,EAEjB0L,OAAOC,UAAU9L,GAAS,CAE5B,MAAMyiB,EAAWD,EAAa/yB,cAAc,iBAC5CgzB,EAASzyB,QAAQsI,SAAW0H,EAAOhP,WACnCyxB,EAASzyB,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC7CkqB,EAASzyB,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,cACjD,KAAO,CACLnI,QAAQ6C,MAAM,8DAA+DiN,GAG7E,IAAK,MAAMuiB,KAAeF,EAAapxB,iBAA8B,gBACnEsxB,EAAY3sB,QAAS,CAEzB,CAEA,MAAM4sB,EAAMN,EAAwB5yB,cAAc,UAUlD,OATAkzB,EAAI3yB,QAAQmQ,KAAOA,EACnBwiB,EAAI3R,aAAa,aAAc,wBAA0B7Q,GACzDwiB,EAAI70B,iBAAiB,QAAS80B,IAC1Bx0B,KAAK8yB,mBAAqB,EAC5ByB,EAAIhyB,UAAW,EAEfgyB,EAAIhyB,UAAW,EAGV0xB,CACT,CAEQ,wBAAA7zB,CAAyB6D,GAC/BjE,KAAK8yB,oBAAsB,EAC3B9yB,KAAKuG,QACP,CAEQ,sBAAA0sB,CAAuBhvB,GAEzBjE,KAAK8yB,mBAAqB,IAC5B9yB,KAAK8yB,oBAAsB,GAG7B9yB,KAAKuG,QACP,CAMQ,eAAA8sB,CAAgBpvB,GACtBjE,KAAKuG,QACP,CAEQ,oBAAA4sB,CAAqBpxB,GAC3BA,EAAM0yB,iBAEN,MAAM5gB,EAAQ7T,KAAKoB,WAAWC,cAAgC,WC1N3DsS,eAAqByD,EAA4B2S,EAAoBxf,EAC1E0f,EAAqBzK,QAAT,IAAZyK,IAAAA,GAAe,GACf,MAAMrS,EAAc3S,EAAM,qBAAsB8kB,EAAUxf,GAE1D,IAAImhB,EAKJ,IACEA,EAAalR,IACb,MAAMka,QAAoB5K,GAAqB4B,EAAY3B,EACzDnrB,eAAegtB,mBAAoB3B,GAGrC5P,EAAMjD,QADkBmI,EAAemV,EAAalV,EAAUjV,GACrCqN,EAAarN,EACxC,CAAE,MAAOvI,GAOP,MAAM2yB,EAAiB1jB,MAAMC,QAAQ6Y,GAAYA,EAAS/T,IAAIjE,GAAY,MAAJA,OAAI,EAAJA,EAAMiI,eAAiB,GAE7F9C,EAAOE,EAAQ,CACbrF,KAAM,kBACN/P,QACA2V,OAAQ,CACNid,YAAavf,EAAOqW,GACpBlI,aAActJ,EAA2BwR,GACzCmJ,UAAWF,GAEbpqB,WACAqN,eAEJ,CAAC,QACCA,EAAYuG,UACd,CACF,EDkMI2W,CAJmB90B,KATF,CAAC6T,EAAM9U,OAQP,uBAKqB,EAAM2hB,IACzCqU,KAAK,IAAMlhB,EAAM9U,MAAQ,IACzBijB,MAAM/f,QAAQC,MACdgsB,QAAQ,IAAMluB,KAAKuG,SACxB,CAEQ,iBAAAX,CAAkB7D,GAAyD,IAAAizB,EAYjF,GAAIjzB,EAAMqV,SAAWpX,KACnB,OAMF,GAA0B,oBAAtB+B,EAAMoC,OAAO4N,KACf,OAKF,IAAIkjB,EACJ,IAAI,IAAA5R,EACF,GAkE6BtkB,EAlEoB,OAApBskB,EAACthB,EAAMoC,OAAOwT,aAAM,EAAnB0L,EAAqBG,aAmEhDvS,MAAMC,QAAQnS,IAAUA,EAAM6gB,MAAMxS,GAA8B,iBAAZA,IAlEvDrL,EAAMoC,OAAOwT,OAAO6L,aAAavgB,OAAS,EAC1CgyB,EAAgBlzB,EAAMoC,OAAOwT,OAAO6L,aAAaxN,IAAIkf,GAAUA,EAAOlb,mBACjE,CAILib,EAAgB7a,EAHHI,IAIf,CACF,CAAE,MAAOxY,GACPC,QAAQC,KAAKF,EACf,CAuDJ,IAAmCjD,GAlDd,OAAbi2B,EAAAC,QAAa,EAAbD,EAAe/xB,QAAS,EAC1BjD,KAAK+yB,aAAe,iHAGpB/yB,KAAK+yB,aAAe,+EAItB/yB,KAAKuG,QACP,CAEQ,aAAA/F,CAAcuB,GAGpB/B,KAAKqJ,KAAOtH,EAAMoC,OAAOkF,KAIzBrJ,KAAK+yB,aAAe,KAEpB/yB,KAAKuG,QACP,EAGF,SAASiuB,GAAwBzyB,GAM/B,MAAMqV,EAASrV,EAAMK,cAErB,GAAIgV,aAAkB/U,oBAAsB+U,EAAO7U,SAAU,CAC3D6U,EAAO7U,UAAW,GE5TfoR,eAAsByD,EAA4BrF,EAAcxH,GACrE,MAAMqN,EAAc3S,EAAM,iBAAkB8M,EAAMxH,GAElD,IAAI4qB,EAEJ,IAMEA,EAAe3a,IAKf,MAQMoL,EARgBxL,EAAiB+a,GAERhkB,OAAOikB,GAAgBA,IAAiBrjB,EAAKiI,eAMlDjP,KAAK,WACzB+J,EAAW,CAAE8Q,aAEnBvL,EAAMjD,QADaiE,IACCzD,EAAarN,EACnC,CAAE,MAAOvI,GACPkV,EAAOE,EAAQ,CACbrF,KAAM,kBACN/P,QACA2V,OAAQ,CACN5F,OACAiN,cAAe3J,EAAO8f,IAExB5qB,WACAqN,eAEJ,CAAC,QACCA,EAAYuG,UACd,CACF,EF+RIkX,CAFmBje,EATNA,EAAOxV,QAAQmQ,KACX,oCAUkBiQ,MAAM/f,QAAQC,KACnD,CACF,CAYKkD,eAAeC,IAAI,mBACtBD,eAAeE,OAAO,iBAAkButB,eGzW1C,MAAMyC,kBAAkBz1B,YACtB,6BAAkB2F,GAChB,MAAO,CACL,qBACA,uBACA,iBACA,kBACA,uBAEJ,CAiCO,iBAAAzE,GACLf,KAAKgB,uBAELhB,KAAKuG,QACP,CAEO,wBAAAH,CAAyByb,EAAexb,EAAkBC,GAC/DtG,KAAKgB,uBAEDhB,KAAK0B,aAAe4E,IAAaD,GACnCrG,KAAKuG,QAET,CAEQ,oBAAAvF,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,i0CAEhC,CAEQ,MAAA8E,GACN,KAAKvG,KAAK4B,QAAQgf,gBACf5gB,KAAK4B,QAAQwI,gBACbpK,KAAK4B,QAAQuI,cACbnK,KAAK4B,QAAQ0wB,WACd,OAGF,MAAMiD,EAAkBxnB,SAAS/N,KAAK4B,QAAQgf,eAAgB,IAExDgO,EAAQ5uB,KAAKoB,WAAWC,cAAc,UAK1CutB,EAAMntB,UADgB,IAApB8zB,EACgB,OAAOv1B,KAAK4B,QAAQ0wB,aAEpB,uDACQtyB,KAAK4B,QAAQwI,gDACfpK,KAAK4B,QAAQuI,yCAClBnK,KAAK4B,QAAQgf,mDACN5gB,KAAK4B,QAAQ0wB,YAE3C,EASGltB,eAAeC,IAAI,eACtBD,eAAeE,OAAO,aAAcgwB,WCzF/B,SAASE,GAA0BnsB,GACxC,MAAMosB,EAAqBpsB,EAAKqsB,qBAG1BC,EAcR,SAAgCtsB,GAC9B,IAAIooB,EAAQ,EACZ,IAAK,MAAMxb,KAAe5M,EAAK0M,iCAC7B0b,GAASxb,EAAY4P,uBAGvB,IAAK,MAAMha,KAAQxC,EAAKC,MACtB,IAAK,MAAMmI,KAAc5F,EAAK6F,gCAC5B+f,GAAShgB,EAAWG,OAIxB,OAAO6f,CACT,CA3B6BmE,CAAuBvsB,GAG5CwsB,EAyCR,SAAyCxsB,GACvC,IAAIooB,EAAQ,EAEZ,IAAK,MAAMxb,KAAe5M,EAAK0M,iCACzB+f,GAAqB7f,EAAY5G,SACnCoiB,GAASxb,EAAY4P,wBAIzB,IAAK,MAAMha,KAAQxC,EAAKC,MACtB,IAAK,MAAMmI,KAAc5F,EAAK6F,gCACxBokB,GAAqBrkB,EAAWE,qBAAqBtC,SACvDoiB,GAAShgB,EAAWE,qBAAqBkU,wBAK/C,OAAO4L,CACT,CA3DsCsE,CAAgC1sB,GAE9D2sB,EAwBR,SAAiD3sB,GAC/C,IAAIooB,EAAQ,EAKZ,IAAK,MAAM5lB,KAAQxC,EAAKC,MAClBuC,EAAKoqB,YACPxE,GAAS5lB,EAAKiE,kBAIlB,OAAO2hB,CACT,CArCoCyE,CAAwC7sB,GAM1E,OAJ4BosB,EAAqBE,EAE/CE,EAA8BG,CAGlC,CAoDA,SAASF,GAAqB/2B,GAC5B,GAAqB,iBAAVA,EACT,OAAO,EAGT,MAAMgb,EAAQhb,EAAMib,cAEpB,QAAID,EAAM5E,WAAW,0BAKjB4E,EAAM3E,SAAS,2BAIf,iCAAiCtD,KAAKiI,GAK5C,CC3FA,MAAMoc,0BAA0Bt2B,YAiBtBU,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAG9C,iBAAAe,GACLf,KAAKgB,uBAEL,IAAK,MAAMe,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,oBAEtCP,KAAKuG,QACP,CAEO,oBAAArF,GACLvB,oBAAoB,eAAgBK,KAAKO,mBAC3C,CAEQ,oBAAAS,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,6wBAEhC,CAEQ,aAAAjB,CAAcuB,GACpB/B,KAAKqJ,KAAOtH,EAAMoC,OAAOkF,KACzBrJ,KAAKuG,QACP,CAEQ,MAAAA,GACN,IAAKvG,KAAKqJ,KACR,OAGF,MAAM+sB,EAAqBZ,GAA0Bx1B,KAAKqJ,MAEpDgtB,EAAYr2B,KAAKoB,WAAWC,cAAc,KAE1CgxB,EAAaryB,KAAKs2B,wBAExB,GAA0B,IAAtBjE,EAAWpvB,OAEb,YADAozB,EAAU1uB,QAAS,GAIrB,IAAI4uB,EAAoB,EACxB,MAAMC,EAAqB,GAI3B,IAAK,MAAMC,KAAapE,EAGlB+D,GAAsBK,EAAU7V,eAClC6V,EAAUrpB,QAAQxL,QAAQ80B,SAAW,SAIvCD,EAAUrpB,QAAQxL,QAAQ80B,SAAW,OAGH,IAA9BF,EAAmBvzB,OAOnBszB,IAAsBE,EAAU7V,gBAClC4V,EAAmBhlB,KAAKilB,IAPxBD,EAAmBhlB,KAAKilB,GAExBF,EAAoBE,EAAU7V,iBAUA,IAA9B4V,EAAmBvzB,SACrBozB,EAAU1uB,QAAS,GAGrB,MAAM4tB,EAAkBiB,EAAmBvzB,OAAS,EAClDuzB,EAAmB,GAAG5V,eACtByR,EAAWA,EAAWpvB,OAAS,GAAG2d,eAE9B+V,EAAepB,EAAkBa,EACjC9D,EAAYkE,EAAmBxgB,IAAIwC,GAAKA,EAAE8Z,WAAWvnB,KAAK,SAEhEsrB,EAAU50B,UAAY,iDACEzB,KAAK4B,QAAQuI,8CACXnK,KAAK4B,QAAQwI,yCACpBusB,yCACYrE,KAE/B,MAAMsE,EAAqB5vB,KAAKuZ,IAAI,IAAKvZ,KAAKwX,MAC3C4X,EAAqBb,EAAmB,MACrCsB,EAAa72B,KAAKoB,WAAWC,cAA2B,aAC9Dw1B,EAAWpR,MAAMnD,MAAQ,GAAGsU,KAG5B,MAAME,EAAU92B,KAAKoB,WAAWC,cAA4B,UAC5Dy1B,EAAQl1B,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC5C2sB,EAAQl1B,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAE1CgsB,GAAsB/D,EAAWA,EAAWpvB,OAAS,GAAG2d,eAC1DkW,EAAQl1B,QAAQsI,SAAW,GAE3B4sB,EAAQl1B,QAAQsI,SAAWksB,EAAmBxzB,WAGhD,MAAMm0B,EAAS/2B,KAAKoB,WAAWC,cAA4B,SAC3D01B,EAAOn1B,QAAQuI,aAAenK,KAAK4B,QAAQuI,aAC3C4sB,EAAOn1B,QAAQwI,eAAiBpK,KAAK4B,QAAQwI,eAC7C2sB,EAAOn1B,QAAQsI,SAAWqrB,EAAgB3yB,WAEtC4zB,EAAmBvzB,OAAS,IAC9BozB,EAAU1uB,QAAS,GAGrB,MAAMyhB,EAAcyN,EAAWroB,cAC/B4a,EAAYxG,aAAa,gBAAiBgU,EAAmBh0B,YACzDg0B,EAAqB,IACvBxN,EAAYxG,aAAa,aAAc,CACrC5iB,KAAK4B,QAAQwI,gBACZgsB,EAAqB,KAAKvkB,QAAQ,GACnC,OACA7R,KAAK4B,QAAQwI,gBAEZmrB,EAAkB,KAAK3yB,WACxB,SACAmI,KAAK,KAEPqe,EAAYxG,aAAa,aAAc,CACrC5iB,KAAK4B,QAAQwI,gBACZmrB,EAAkB,KAAK3yB,WACxB,iBACAmI,KAAK,IAEX,CAEQ,qBAAAurB,GACN,MAAMpO,EAAWloB,KAAKgD,iBAAiB,cAEjCguB,EAAqB,GAC3B,IAAK,MAAM5jB,KAAW8a,EAAU,CAC9B,MAAMiJ,EAAkB,CAAC,EACzBA,EAAKhnB,aAAeiD,EAAQxL,QAAQuI,aACpCgnB,EAAK/mB,eAAiBgD,EAAQxL,QAAQwI,eACtC+mB,EAAKuF,SAAwC,SAA7BtpB,EAAQxL,QAAQ80B,SAChCvF,EAAKmB,UAAYllB,EAAQxL,QAAQ0wB,UACjCnB,EAAKvQ,eAAiB7S,SAASX,EAAQxL,QAAQgf,eAAgB,IAC/DuQ,EAAK/jB,QAAUA,EACf4jB,EAAMxf,KAAK2f,EACb,CAEA,OAAOH,EAAMgG,KAAKC,GACpB,EAGF,SAASA,GAAkBha,EAAcC,GACvC,OAAID,EAAE2D,iBAAmB1D,EAAE0D,eAClB,EAGF3D,EAAE2D,eAAiB1D,EAAE0D,gBAAkB,EAAI,CACpD,CC/FOjN,eAAeujB,GAA2CC,EAC/DC,EAAsBC,GACtB,MAAMC,EAAmBC,mBAAmBC,kBAC5C,GAAIF,EAAiBH,GACnB,OAAUG,EAAiBH,GAG7B,IAAIp4B,EAAkB,KAQtB,MAAMkf,EAActb,IACd80B,EAA8B,MAATJ,EAAAA,EAAaE,mBAAmBG,mBAE3D,IAQE,UA9FgBtb,EAuFgBqb,EAtF3B,IAAIzb,QAAiBK,IAC1B,GAAsC,YAAlCkb,mBAAmBI,WAErB,YADAtb,EAA0C,UAAlCkb,mBAAmBI,YAI7B,MAAMC,EAAcn0B,QAAQo0B,YAAcN,mBAAmBO,gBAAkB,EACzEv4B,EAAQC,WAAW6c,EAASD,EAAewb,GAAa,GAE9Dl4B,iBAAiB,0BAA2B,KAC1CD,aAAaF,GACb8c,EAA0C,UAAlCkb,mBAAmBI,aAC1B,CAAEI,MAAM,OAmFT,OADAT,EAAiBH,GAAcC,EACxBA,EAMT,MAAMY,QAAmBT,mBAAmBU,cAE5C,IAAKD,EAEH,OADAV,EAAiBH,GAAcC,EACxBA,EAkBTr4B,EAAkBi5B,EAAWd,gBAA+BC,EAAY,KAC1E,CAAE,MAAOn1B,GACPC,QAAQC,KAAK,+CAAgDF,EAC/D,CAAC,QACC+C,EAAQ,2BAA4BkZ,EAAatb,IAAO,CACtDu1B,QAASf,EACTtb,QAAS4b,GAEb,CApIF,IAAoBrb,EAgJlB,MANqB,iBAAVrd,IACTA,EAAQq4B,GAGVE,EAAiBH,GAAcp4B,EAExBA,CACT,CAiBO,SAASo5B,GAAoBp5B,EAAeq5B,GACjD,OAAOr5B,EAAM8Z,gBAAkBuf,EAASvf,aAC1C,CCpJA,SAASrY,GAAcuB,GAAyB,IAAAs2B,EAAAC,EAAAC,EAC9C,MAAMC,EAAgBn5B,SAASgC,cAC7B,uCAKGm3B,IAWgD,OAArB,OAA5BH,EAAAt2B,EAAMoC,OAAOkF,KAAKkC,iBAAU,EAA5B8sB,EAA8BI,gBAcmB,OAArB,OAA5BH,EAAAv2B,EAAMoC,OAAOkF,KAAKkC,iBAAU,EAA5B+sB,EAA8BG,gBAOmB,OAArB,OAA5BF,EAAAx2B,EAAMoC,OAAOkF,KAAKkC,iBAAU,EAA5BgtB,EAA8BE,iBAIlCvB,GAAgB,kCAAmC,KAChDnC,KAAKh2B,GA3EV,SAA2By5B,EACzBE,EAAeC,GAqBf,OAbIR,GAAoBO,EAAO,MAAQC,GAAa,OAClDH,EAAc52B,QAAQ8J,KAAO,QAI3BysB,GAAoBO,EAAO,OAC7BF,EAAc52B,QAAQ8J,KAAO,QAOxBoJ,EAAW,CAChBvJ,WAAY,CACVqtB,YAAa,KACbptB,eAAgB,KAChBitB,eAAgBC,IAGtB,CA8CmBG,CAAkBL,EAAez5B,EAAOgD,EAAMoC,OAAOkF,KAAKU,cACxEiY,MAAM/f,QAAQC,MAZfs2B,EAAc52B,QAAQ8J,KAAO,OAZzB3J,EAAMoC,OAAOkF,KAAKU,aAAe,KACnCyuB,EAAc52B,QAAQ8J,KAAO,OAE7B8sB,EAAc52B,QAAQ8J,KAAO,QAsBnC,CFsHKtG,eAAeC,IAAI,wBACtBD,eAAeE,OAAO,sBAAuB6wB,mBEvG/Ch3B,EAdA,WACE,IAAK,MAAM4C,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACEvB,GAAcuB,EAChB,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBc,GACnC,GChGA,MAAMs4B,0BAA0Bj5B,YACvB,iBAAAkB,GACLf,KAAKgB,sBACP,CAEQ,oBAAAA,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,4oCAEhC,EASG2D,eAAeC,IAAI,wBACtBD,eAAeE,OAAO,sBAAuBwzB,mBCxB/C,MAAMC,qBAAqBl5B,YACzB,6BAAkB2F,GAChB,MAAO,CAAC,uBAAwB,eAClC,CAqBQ2C,oBAAsBnI,KAAKoI,eAAelI,KAAKF,MAEhD,iBAAAe,GACLf,KAAKgB,uBACLhB,KAAKg5B,gBAEeh5B,KAAKoB,WAAWC,cAAc,gBACtC3B,iBAAiB,QAASM,KAAKmI,oBAC7C,CAEO,wBAAA/B,CAAyBpB,EAAc0D,EAAmBpC,GAC/DtG,KAAKgB,uBAEDhB,KAAK0B,aAAwB,yBAATsD,GAAgD,SAAbsB,GACzDtG,KAAK2I,mBAGH3I,KAAK0B,aAAwB,iBAATsD,GACtBhF,KAAKg5B,eAET,CAEO,oBAAA93B,GACL,MAAM4H,EAAc9I,KAAKoB,WAAWC,cAAc,gBACvC,MAAXyH,GAAAA,EAAanJ,oBAAoB,QAASK,KAAKmI,oBACjD,CAEQ,oBAAAnH,GACDhB,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,s8BAEhC,CAKQ,gBAAAkH,UACC3I,KAAK4B,QAAQmG,eACpB/H,KAAKoB,WAAWC,cAAc,gBAAgB2G,MAAM,CAAEiB,cAAc,GACtE,CAEQ,aAAA+vB,GACYh5B,KAAKoB,WAAWC,cAAc,YACtCyJ,YAAc9K,KAAK4B,QAAQiF,SAAW,EAClD,CAEQ,cAAAuB,CAAenE,GAErB,MAAMlC,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNM,SAAS,EACTC,OAAQ,kBAGZ1E,KAAK2E,cAAc5C,EACrB,EASGqD,eAAeC,IAAI,kBACtBD,eAAeE,OAAO,gBAAiByzB,cC3FzC,MAAME,mBAAmBp5B,YACvB,6BAAkB2F,GAChB,MAAO,CAAC,uBACV,CAmCQjF,mBAAqBP,KAAKQ,cAAcN,KAAKF,MAC7CmI,oBAAsBnI,KAAKoI,eAAelI,KAAKF,MAEhD,iBAAAe,GACAf,KAAKoB,aACRpB,KAAKuB,aAAa,CAAEC,KAAM,SAC1BxB,KAAKoB,WAAWK,UAAY,izCAGVzB,KAAKoB,WAAWC,cAAc,gBACtC3B,iBAAiB,QAASM,KAAKmI,qBAE3C,IAAK,MAAMpG,KAASpD,IAClB,GAAIG,EAAmBiD,GACrB,IACE/B,KAAKO,mBAAmBwB,EAC1B,CAAE,MAAOC,GACPC,QAAQC,KAAKF,EACf,CAIJtC,iBAAiB,eAAgBM,KAAKO,mBACxC,CAEO,wBAAA6F,CAAyBpB,EAAc0D,EAAmBpC,GAC3DtG,KAAK0B,aAAwB,yBAATsD,GAAgD,SAAbsB,GACzDtG,KAAK2I,kBAET,CAEO,oBAAAzH,GACL,MAAM4H,EAAc9I,KAAKoB,WAAWC,cAAc,gBACvC,MAAXyH,GAAAA,EAAanJ,oBAAoB,QAASK,KAAKmI,qBAE/CxI,oBAAoB,eAAgBK,KAAKO,mBAC3C,CAQQ,gBAAAoI,UACC3I,KAAK4B,QAAQmG,eACpB/H,KAAKoB,WAAWC,cAAc,gBAAgB2G,MAAM,CAAEiB,cAAc,GACtE,CAEQ,MAAA1C,GAAS,IAAA2yB,EACCl5B,KAAKoB,WAAWC,cAAc,cACtCyJ,YAAc9K,KAAK4B,QAAQyN,MAKnC,GAHmBrP,KAAKoB,WAAWC,cAAc,aACtCyJ,YAAc9K,KAAK4B,QAAQutB,UAEf,OAAnB+J,EAAAl5B,KAAKm5B,qBAAc,EAAnBD,EAAqBj2B,QAAS,EAChC,OAGF,MAAMm2B,EAAcp5B,KAAKgD,iBAA8B,uBACvD,IAAK,MAAMihB,KAAcmV,EAAa,CACpC,MAAM5pB,EAAYzB,SAASkW,EAAWriB,QAAQ4N,UAAW,IAIzDyU,EAAWtc,OAAS3H,KAAKm5B,eAAe/jB,SAAS5F,EACnD,CACF,CAEQ,aAAAhP,CAAcuB,GACpB/B,KAAKm5B,eAAiBp3B,EAAMoC,OAAOkF,KAAKC,MAAM0M,IAAInK,GAAQA,EAAKyC,YAE/DtO,KAAKuG,QACP,CAEQ,cAAA6B,CAAenE,GAErB,MAAMlC,EAAQ,IAAIuC,YAAoB,uBAAwB,CAC5DC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNM,SAAS,EACTC,OAAQ,kBAGZ1E,KAAK2E,cAAc5C,EACrB,ECzGF,SAASs3B,GAAKjiB,GACZ,MAAM/N,EAAOmR,IAEP8e,EAAe,IAAIh1B,YAA4C,mBAAoB,CACvFC,SAAS,EACTg1B,YAAY,EACZ/0B,UAAU,EACVL,OAAQ,CACNkF,OACA4R,SAAUnC,OAId1B,EAAOzS,cAAc20B,EACvB,CCpBA,SAASE,GAAK/0B,EAAkBC,EAAgBtB,EAAeyD,GAC7D,MAAMhE,EAAaxD,SAASgC,cAC1B,6CAKF,IAAKwB,EACH,OAGF,GAAIgE,EAAS,CACX,MAAM4yB,EAAS52B,EAAWxB,cAA2B,UAAU+B,GAAQ,iBACnEq2B,IACFA,EAAO73B,QAAQiF,QAAUA,EAE7B,CAKA,GAAsC,SAAlChE,EAAWjB,QAAQ2F,WAOrB,YANInE,EACFP,EAAWjB,QAAQwB,KAAOA,EAE1BnB,QAAQK,IAAI,kDAAmDoC,IAU/DtB,EACFP,EAAWjB,QAAQwB,KAAOA,EACuB,SAAxCP,EAAWjB,QAAQ+E,iBAC5B9D,EAAWjB,QAAQwB,KAAO,WAE1BP,EAAWjB,QAAQwB,KAAO,OAU5B,MAAMC,EAAcR,EAAWxB,cAAc,gBACzCgC,IACFA,EAAYzB,QAAQ0B,cAAgB,QAGtCT,EAAWjB,QAAQ6C,QAAUA,EAAU,OAAS,QAEhDxC,QAAQK,IAAI,cAAeoC,GAE3B,MAAMg1B,EAAY,IAAI16B,MAAM,uBAAwB,CAAEuF,SAAS,EAAMC,UAAU,IAC/E3B,EAAW8B,cAAc+0B,GAEzBh6B,iBAAiB,0BAA2Bi6B,IAG5C,MAAMC,EAAc,IAAIt1B,YAA6C,qBAAsB,CACzFC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACN8W,SAAUnC,OAGdjW,EAAW8B,cAAci1B,GAGzB,MAAMC,EAAc,IAAIv1B,YAAuC,cAAe,CAC5EC,SAAS,EACTC,UAAU,EACVL,OAAQ,CACNwH,QAAS,YAGb9I,EAAW8B,cAAck1B,EAC3B,CAmBA,SAASC,GAAMr1B,EAAkBC,GAO/B/E,oBAAoB,0BAA2Bg6B,IAE/C,MAAM92B,EAAaxD,SAASgC,cAC1B,6CAKF,IAAKwB,EACH,OAKF,GAAsC,SAAlCA,EAAWjB,QAAQ2F,WAErB,YADAtF,QAAQK,IAAI,sEAAuEoC,GAIrF7B,EAAWjB,QAAQ6C,QAAUA,EAAU,OAAS,QAEhDxC,QAAQK,IAAI,cAAeoC,GAE3B,MAAMq1B,EAAa,IAAI/6B,MAAM,wBAAyB,CAAEuF,SAAS,EAAMC,UAAU,IACjF3B,EAAW8B,cAAco1B,GAEzB,MAAMC,EAAc,IAAIh7B,MAAM,qBAAsB,CAAEuF,SAAS,EAAMC,UAAU,IAC/E3B,EAAW8B,cAAcq1B,EAC3B,CAIA,SAASC,GAAoBl4B,GAA+B,IAAAm4B,EAAA7M,EAAA8M,EAAAC,EAAAC,EAG1Db,GAFqC,OAAxBU,EAAe,OAAf7M,EAAGtrB,EAAMoC,aAAM,EAAZkpB,EAAc5oB,UAAOy1B,EACV,OAAfC,EAAGp4B,EAAMoC,aAAM,EAAZg2B,EAAcz1B,OACK,OAAd01B,EAAEr4B,EAAMoC,aAAM,EAAZi2B,EAAch3B,KAAkB,OAAdi3B,EAAEt4B,EAAMoC,aAAM,EAAZk2B,EAAcxzB,QAC1D,CAEA,SAASyzB,GAAqBv4B,GAAgC,IAAAw4B,EAAAC,EAAAC,EAG5DX,GAFqC,OAAxBS,EAAe,OAAfC,EAAGz4B,EAAMoC,aAAM,EAAZq2B,EAAc/1B,UAAO81B,EACV,OAAfE,EAAG14B,EAAMoC,aAAM,EAAZs2B,EAAc/1B,OAE/B,CAOA,SAASi1B,GAAwB11B,GAC/BhC,QAAQK,IAAI,2CACZw3B,IAAM,EAAM,gBACd,CAYA,SAASl0B,GAAkB7D,GACC,SAAtBA,EAAMoC,OAAO4N,MAAyC,iBAAtBhQ,EAAMoC,OAAO4N,MAG/CynB,IAAK,EAAM,uBAAwB,OAEvC,CF3DKp0B,eAAeC,IAAI,gBACtBD,eAAeE,OAAO,cAAe2zB,YC9EvC55B,SAASK,iBAAiB,QA9C1B,SAAiBqC,GACf,MACMqV,EADUrV,EAAM24B,eACC7tB,KAAKuK,GAAUA,aAAkB/U,mBACxC,oBAAd+U,EAAO1G,IACL0G,GACF5X,WAAW65B,GAAM,EAAGjiB,EAExB,GCiLA,WAOE,MAAMujB,EAAwBh8B,IAAgBwS,OAAOjS,GAEjDy7B,EAAsB13B,OAAS,GACjChB,QAAQK,IAAI,mDAAoDq4B,EAAsB13B,QAGxF,IAAK,MAAMlB,KAAS44B,EAClB/0B,GAAkB7D,GAGpBrC,iBAAiB,oBAAqBkG,IACtClG,iBAAiB,uBAAwB46B,IACzC56B,iBAAiB,sBAAuBu6B,GAC1C,CAEAW","sources":["webpack://lange/./lib/cart-event-queue.ts","webpack://lange/./lib/dom-ready.ts","webpack://lange/./sections/cart-drawer/cart-actions/index.ts","webpack://lange/./lib/perf.ts","webpack://lange/./sections/cart-drawer/cart-drawer/index.ts","webpack://lange/./sections/cart-drawer/cart/index.ts","webpack://lange/../node_modules/@integrabeauty/shopify-ajax-api/src/cart.ts","webpack://lange/./lib/cart/digest.ts","webpack://lange/./lib/cart/equality.ts","webpack://lange/./lib/decorate-error.ts","webpack://lange/./lib/cart/error.ts","webpack://lange/../node_modules/@integrabeauty/assert/src/index.ts","webpack://lange/../node_modules/uuid/dist/rng.js","webpack://lange/../node_modules/uuid/dist/stringify.js","webpack://lange/../node_modules/uuid/dist/v4.js","webpack://lange/./lib/object.ts","webpack://lange/./lib/cart/discount/discount.ts","webpack://lange/./lib/untrusted-value.ts","webpack://lange/./lib/cart/state.ts","webpack://lange/./lib/cart/transaction.ts","webpack://lange/./lib/cart/change.ts","webpack://lange/./lib/cart/remove-lines.ts","webpack://lange/./lib/cart/remove-follow-up.ts","webpack://lange/./lib/cart/remove.ts","webpack://lange/./lib/cart/update.ts","webpack://lange/./lib/cart/gift-with-purchase.ts","webpack://lange/./sections/cart-drawer/gift-removal.ts","webpack://lange/./sections/cart-drawer/cart-line-item/index.ts","webpack://lange/./sections/cart-drawer/cart-messages/index.ts","webpack://lange/./sections/cart-drawer/cart-totals/index.ts","webpack://lange/./sections/cart-drawer/cart-upsell-item/index.ts","webpack://lange/./sections/cart-drawer/cart-upsell-items/index.ts","webpack://lange/../node_modules/@shopify/theme-currency/currency.js","webpack://lange/./sections/cart-drawer/free-shipping-banner/index.ts","webpack://lange/./sections/cart-drawer/gift-qualified-message/index.ts","webpack://lange/./lib/cart/discount/helper.ts","webpack://lange/./lib/cart/add.ts","webpack://lange/./sections/cart-drawer/offer-view/index.ts","webpack://lange/./sections/cart-drawer/gift-with-purchase-product/index.ts","webpack://lange/./sections/cart-drawer/gift-with-purchase-progress/index.ts","webpack://lange/./sections/cart-drawer/promo-goal-progress-v2/index.ts","webpack://lange/./sections/cart-drawer/discount-input/index.ts","webpack://lange/./lib/cart/discount/apply.ts","webpack://lange/./lib/cart/discount/remove.ts","webpack://lange/./sections/cart-drawer/promo-goal/index.ts","webpack://lange/./sections/cart-drawer/promo-goal-progress/qualifying-subtotal.ts","webpack://lange/./sections/cart-drawer/promo-goal-progress/index.ts","webpack://lange/./lib/growthbook-feature.ts","webpack://lange/./sections/cart-drawer/shipping-experiment.ts","webpack://lange/./sections/cart-drawer/progress-bar-custom/index.ts","webpack://lange/./sections/cart-drawer/progress-view/index.ts","webpack://lange/./sections/cart-drawer/upsell-view/index.ts","webpack://lange/./sections/cart-drawer/checkout-started.ts","webpack://lange/./sections/cart-drawer/drawer-control.ts"],"sourcesContent":["// Accessors for the ambient cart event queue seeded and filled by the always-on cart lifecycle\n// snippet. The snippet registers a parse-time listener that buffers every cart event into the\n// queue, so a section whose bundle loads later can replay what it missed. Reaching the queue\n// through this lib, rather than the global directly, keeps the coupling to the ambient layer\n// explicit. The buffered events come off the queue untyped, so the guards here narrow each one\n// during replay.\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\ntype CartUpdateErredEvent = GlobalEventHandlersEventMap['cart-update-erred'];\n\n/**\n * Returns the ambient queue of buffered cart events. A consumer replays this as it initializes,\n * then listens live for further events.\n */\nexport function getEventQueue() {\n  return cart_lifecycle.queue;\n}\n\n/**\n * Whether the value is a cart updated event. A consumer replaying the queue narrows each buffered\n * event with this before handling it.\n */\nexport function isCartUpdatedEvent(value: any): value is CartUpdatedEvent {\n  return value instanceof Event && value.type === 'cart-updated';\n}\n\n/**\n * Whether the value is a cart update erred event.\n */\nexport function isCartUpdateErredEvent(value: any): value is CartUpdateErredEvent {\n  return value instanceof Event && value.type === 'cart-update-erred';\n}\n","/**\n * Releases the handle returned from the ready() function.\n *\n * This is idempotent. Additional calls are pointless but harmless.\n */\nexport type ReleaseHandle = () => void;\n\n/**\n * A subset of events that pertain to document ready state.\n */\ntype LoadingEventMap = Pick<DocumentEventMap, 'DOMContentLoaded' | 'load'>;\n\n/**\n * Calls the provided callback when the document is ready. This works both when the document is\n * already ready and when it is not, so callers do not have to care. This exists because it ensures\n * that each caller consistently expresses its dependency on document readiness, and to save a bit\n * of boilerplate.\n *\n * A document is considered ready when it has been parsed. This corresponds to the ready state of\n * 'interactive'. When the document has been fully loaded, the ready state is 'complete'. The\n * callback runs either way, so callers do not have to check which one they are in.\n *\n * Ready means the html is parsed and the DOM is there to query. It does not mean the page's other\n * scripts have run. Deferred scripts and module scripts run after the ready state turns\n * 'interactive' and before DOMContentLoaded fires, so a callback here can run before them. When a\n * caller needs some other script to have run already, this is the wrong tool, and it should wait on\n * whatever that script produces instead.\n *\n * That remedy has a catch of its own when the calling bundle is loaded with async. An async bundle\n * can execute after DOMContentLoaded has already fired, so a plain listener for that event never\n * runs and the work silently never happens. Nothing here can detect that, because by the time this\n * file executes the event is long gone. Catching it needs something registered at parse time,\n * before any bundle loads.\n *\n * There is a second trap right next to that one, and it applies only when waiting for\n * DOMContentLoaded. The ready state stays 'interactive' after DOMContentLoaded has fired, all the\n * way until 'complete', so the ready state cannot tell you whether DOMContentLoaded is still coming\n * or has already gone. Waiting for load is not exposed to this, because the ready state turns\n * 'complete' and load fires in the same task, so seeing 'complete' means load has fired too.\n *\n * When the document is already ready, the callback is invoked via setTimeout. This way, if the\n * callback throws, the error surfaces to the global error handler in a way that is similar to what\n * happens when an event listener throws.\n *\n * We might one day want to move this to an NPM library.\n *\n * Beware of a quirk here. setTimeout yields, and the busier the frontend, the longer it takes to\n * fire. Page load is exactly when the frontend is busiest. The timeout is a floor and not a\n * guarantee, so there is no real bound on how long that yield lasts. What this means is that using\n * this library can introduce more delay than you might expect. Compare code that runs on async load\n * and checks ready state and invokes immediately to code that runs after setTimeout. The former\n * runs in the same tick, the latter runs in \"some later tick\". That later tick can be surprisingly\n * much later. That can cause initial paint issues.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event\n * @see https://html.spec.whatwg.org/multipage/parsing.html#the-end\n */\nexport function ready(callback: (event?: Event) => void,\n  type: keyof LoadingEventMap = 'DOMContentLoaded') {\n  if (document.readyState === 'complete' || (type === 'DOMContentLoaded' &&\n    document.readyState === 'interactive')) {\n    const timer = setTimeout(callback, 0);\n    const handle: ReleaseHandle = () => clearTimeout(timer);\n    return handle;\n  } else {\n    addEventListener(type, callback);\n    const handle: ReleaseHandle = () => removeEventListener(type, callback);\n    return handle;\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport { ready, type ReleaseHandle } from '../../../lib/dom-ready.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartTransactionCompletedEvent = WindowEventMap['cart-transaction-completed'];\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * Renders a \"Continue to Checkout\" button and a \"Continue Shopping\" button that closes the side\n * cart.\n */\nclass CartActions extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Boolean represented as string to indicate if the close button should be hidden\n     */\n    hideCloseButton: string;\n\n    /**\n     * Boolean represented as string to indicate if query parameter to skip shop pay when navigating\n     * to checkout should be set to true.\n     */\n    skipShopPay: 'false' | 'true';\n  };\n\n  private checkoutInitSucceeded = false;\n  private onCheckoutButtonClickedBound = this.onCheckoutButtonClicked.bind(this);\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onReadyBound = this.onReady.bind(this);\n  private onPageShowBound = this.onPageShow.bind(this);\n  private onContinueShoppingButtonClickedBound = this.onContinueShoppingButtonClicked.bind(this);\n\n  private declare releaseReady: ReleaseHandle;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.releaseReady = ready(this.onReadyBound);\n\n    addEventListener('pageshow', this.onPageShowBound);\n  }\n\n  public disconnectedCallback() {\n    const checkoutButton = this.shadowRoot.querySelector<HTMLElement>('#checkout-button');\n    checkoutButton?.removeEventListener('click', this.onCheckoutButtonClickedBound);\n\n    const continueShopping = this.shadowRoot.querySelector<HTMLButtonElement>('#continue-shopping');\n    continueShopping?.removeEventListener('click', this.onContinueShoppingButtonClickedBound);\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n    removeEventListener('pageshow', this.onPageShowBound);\n\n    if (this.releaseReady) {\n      this.releaseReady();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onReady() {\n    if (!this.isConnected) {\n      return;\n    }\n\n    const checkoutButton = this.shadowRoot.querySelector<HTMLElement>('#checkout-button');\n    checkoutButton.addEventListener('click', this.onCheckoutButtonClickedBound);\n\n    const continueButton = this.shadowRoot.querySelector<HTMLButtonElement>('#continue-shopping');\n    if (continueButton) {\n      if (this.dataset.hideCloseButton === 'true') {\n        continueButton.remove();\n      } else {\n        continueButton.addEventListener('click', this.onContinueShoppingButtonClickedBound);\n      }\n    }\n\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  /**\n   * Handle a click on the continue to checkout button. It turns out that this is a little more\n   * complicated than it seems like it should be at first glance because we have some complicated\n   * race conditions and sad paths to handle.\n   *\n   * One issue is that the navigation sometimes takes a while. The current page remains visible\n   * sometimes right up until a large amount of the next page is loaded. Since the checkout page can\n   * take a terribly long time to load (e.g. 10+ seconds), it gives the user the impression they did\n   * not click on the button at all or that there was some error, and so they naturally click again.\n   * So another issue is that we have to carefully handle multiple clicks.\n   *\n   * Handling multiple clicks is also important because of what happens to the checkout cache during\n   * navigation. Users can edit the checkout on the checkout page. However, we do not keep the cache\n   * up to date on the checkout page. Therefore, the checkout service clears the cached checkout\n   * before redirecting. This causes an error if the user manages to click on the continue to\n   * checkout button more than once, after the cache has been cleared, but before the new page has\n   * loaded. We have to prevent those additional clicks from trying to trigger the redirect again\n   * because it leads to an error where the checkout is not found in the cache.\n   *\n   * Another subtle issue here is that disabling the button does not actually make it so that the\n   * button cannot be clicked or that its click handler is not invoked. So we have to actually check\n   * for whether the button is disabled. This is because a click that lands on a child element of a\n   * disabled button still bubbles up to and invokes this handler in Chrome and Safari (Firefox\n   * stops the bubbling). Form membership is not a factor; the button is a real button element.\n   *\n   * We only need to re-enable the button on error. On successful redirection we can leave the\n   * button state as disabled up until the browser changes the page.\n   *\n   * We do not have to log errors to Sentry here. Error handling and console logging is done by the\n   * checkout service.\n   *\n   * @see https://stackoverflow.com/questions/37109771\n   */\n  private onCheckoutButtonClicked(event: MouseEvent) {\n    const button = event.currentTarget;\n\n    // We need to narrow the type of the button variable from EventTarget to HTMLButtonElement. We\n    // want to do so without hard casting the type. The listener is bound to the element with the id\n    // #checkout-button and we are using currentTarget instead of target. This element is an actual\n    // button in the HTML. We want to later have access to the disabled property, which is not\n    // available on HTMLElement, but is on HTMLButtonElement. The correct expression here is to use\n    // instanceof HTMLButtonElement.\n\n    if (!(button instanceof HTMLButtonElement)) {\n      console.log('ignoring checkout button click because click is not on a button');\n      return;\n    }\n\n    // A disabled button does not receive clicks on itself, but a click on one of its child elements\n    // still bubbles up through the disabled button and reaches this listener in Chrome and Safari.\n    // So we manually enforce the disabled state instead of relying on the browser to prevent\n    // clicks.\n\n    if (button.disabled) {\n      console.log('ignoring checkout button click because button is disabled');\n      return;\n    }\n\n    try {\n      sessionStorage.setItem('checkout_button_clicked_epoch', Date.now().toString());\n    } catch (error) {\n      console.warn(error);\n    }\n\n    const cartDrawer = this.closest('cart-drawer');\n    const upsellView = cartDrawer?.querySelector('upsell-view');\n    if (upsellView) {\n      // query all upsells that are not hidden\n      const activeUpsells = upsellView.querySelectorAll('[data-product-id]:not([hidden])');\n      if (activeUpsells.length > 0 && !cart_drawer.upsell_modal_displayed) {\n        cartDrawer.dataset.view = 'upsell';\n        cart_drawer.upsell_modal_displayed = true;\n\n        // The messages are stale by the time the shopper comes back to the cart from the upsell.\n        const cartElement = this.closest('cart-element');\n        if (cartElement) {\n          cartElement.dataset.clearMessages = 'true';\n        }\n\n        return;\n      } else {\n        console.log('not showing upsell view on checkout button click');\n      }\n    } else {\n      console.log('element upsell-view not found on checkout button click');\n    }\n\n    // Disable the checkout button. This does not actually prevent it from being clicked because the\n    // button is located outside of a form element. However, it does cause future click events to be\n    // handled by the click listener differently. Disabling the button before the redirect is\n    // important because there is sometimes a substantial visual lag that occurs during the redirect\n    // where we have observed visitors try to click the button multiple times while waiting.\n    button.disabled = true;\n\n    // This uses a relative url so as to work on multiple domains\n    const url = new URL(Shopify.routes.root + 'checkout', 'https://langehair.com');\n    if (this.dataset.skipShopPay === 'true') {\n      url.searchParams.set('skip_shop_pay', 'true');\n    }\n\n    location.href = url.toString();\n  }\n\n  /**\n   * We only start listening for this event from within the connected callback so we know the dom is\n   * ready and the backdrop element exists.\n   */\n  private onCartTransactionStarted(_event: Event) {\n    const button = this.shadowRoot.querySelector<HTMLButtonElement>('#checkout-button');\n    button.disabled = true;\n  }\n\n  private onCartTransactionCompleted(_event: CartTransactionCompletedEvent) {\n    if (this.checkoutInitSucceeded) {\n      this.endLoading();\n    }\n  }\n\n  /**\n   * Enable the button and hide the spinner.\n   */\n  private endLoading() {\n    const button = this.shadowRoot.querySelector<HTMLButtonElement>('#checkout-button');\n    button.disabled = false;\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    if (event.detail.is_initial) {\n      this.checkoutInitSucceeded = true;\n    }\n\n    // Only enable the button if we previously observed a successful checkout-initialized event.\n    if (this.checkoutInitSucceeded) {\n      const button = this.shadowRoot.querySelector<HTMLButtonElement>('#checkout-button');\n      button.disabled = false;\n    }\n  }\n\n  private onPageShow(event: PageTransitionEvent) {\n    if (event.persisted) {\n      this.endLoading();\n    }\n  }\n\n  private onContinueShoppingButtonClicked(_event: MouseEvent) {\n    const cartDrawer = this.closest('cart-drawer');\n    if (cartDrawer?.dataset.view === 'upsell') {\n      cartDrawer.dataset.view = 'cart';\n    } else {\n      type Detail = WindowEventMap['cart-close-requested']['detail'];\n      const event = new CustomEvent<Detail>('cart-close-requested', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          animate: true,\n          source: 'continue-shopping-button'\n        }\n      });\n      this.dispatchEvent(event);\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-actions': CartActions;\n  }\n}\n\nif (!customElements.get('cart-actions')) {\n  customElements.define('cart-actions', CartActions);\n}\n","/**\n * Returns a timestamp in milliseconds for measuring durations.\n *\n * This decorates performance.now().\n *\n * This uses performance.now(), which counts from page load rather than the Unix epoch, and falls\n * back to Date.now().\n *\n * If native throws, this still returns a value, but it logs a message as a side effect.\n *\n * This happens in a try/catch, so it is safe.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/now\n */\nexport function now() {\n  try {\n    if (typeof performance?.now === 'function') {\n      return performance.now();\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n\n  return Date.now();\n}\n\n/**\n * Records a named point on the User Timing timeline in devtools.\n *\n * This decorates performance.mark within a try/catch that suppresses errors.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark\n */\nexport function mark(name: string) {\n  try {\n    if (typeof performance?.mark === 'function') {\n      performance.mark(name);\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n\n/**\n * Records a named span between two timestamps on the User Timing timeline in devtools.\n *\n * This decorates performance.measure within a try/catch that suppresses errors.\n *\n * The bounds are explicit timestamps rather than named marks, which avoids the error the marked\n * name form throws when a start mark was never recorded.\n *\n * The detail param is optional and must be structured-cloneable. It should be a plain object. If\n * the value cannot be cloned, then performance.measure throws, which is caught and logged by this\n * wrapper. If specified, the detail shows up in the tooltip in the devtools panel.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure\n */\nexport function measure(name: string, start: number, end: number, detail?: unknown) {\n  try {\n    if (typeof performance?.measure === 'function') {\n      performance.measure(name, { start, end, detail });\n    }\n  } catch (error) {\n    console.debug(error);\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent, isCartUpdateErredEvent } from '../../../lib/cart-event-queue.js';\nimport { now } from '../../../lib/perf.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartInitRequestedEvent = WindowEventMap['cart-init-requested'];\ntype CartUpdateErredEvent = GlobalEventHandlersEventMap['cart-update-erred'];\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\ntype CartTransactionCompletedEvent = WindowEventMap['cart-transaction-completed'];\n\n/**\n * This element is a higher level wrapper to implement the shopping bag drawer. It does not require\n * any knowledge of the underlying cart-element functionality and is only concerned with managing\n * the drawer state.\n *\n * The drawer shows and hides itself off `data-modal-state`, which the modal manager sets when the\n * drawer control asks it to open or close the drawer.\n *\n * The panel holds one or more views, each a named slot ending in `-view`, and shows only the one\n * matching `data-view`. The progress view is shown while a cart change is in flight, and the drawer\n * switches it back to the cart once the change completes. It is also shown while the cart\n * initializes, and the drawer switches it back to the cart once init succeeds or fails.\n *\n * On the cart page the element is rendered with `data-layout=\"embedded\"`. The panel then sits in\n * the page as an ordinary block that is always shown, rather than a dialog, and the drawer control\n * script leaves it alone.\n */\nclass CartDrawer extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-modal-state', 'data-view'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * If 'true' the opening and closing of the modal will be animated\n     */\n    animate: 'false' | 'true';\n\n    /**\n     * \"true\" while the cart is initializing. Set and cleared by this element, and read by the\n     * drawer control so a drawer opened during init opens on the progress view.\n     */\n    cartInitializing: string;\n\n    /**\n     * \"embedded\" on the cart page, where the panel sits in the page instead of floating over it.\n     * Missing means the usual drawer. Set in the liquid and fixed for the life of the page.\n     */\n    layout: string;\n\n    /**\n     * Whether the cart drawer is shown, as \"open\". Anything else, or no value at all, means closed.\n     * The modal manager sets this when the drawer control asks it to open or close the drawer.\n     *\n     * A change only takes effect while the element is on the page. A change made while it is\n     * detached is ignored, and connecting it later does not pick it up.\n     */\n    modalState: string;\n\n    /**\n     * The name of the view shown in the panel, such as \"cart\", which shows the slot named \"cart-\n     * view\". Missing means \"cart\".\n     */\n    view: string;\n  };\n\n  private onCartInitRequestedBound = this.onCartInitRequested.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCartUpdateErredBound = this.onCartUpdateErred.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private showCartAfterProgressBound = this.showCartAfterProgress.bind(this);\n\n  /**\n   * When the progress view was last switched to, so the switch back to the cart can wait out the\n   * minimum time the progress view stays up.\n   */\n  private declare progressShownAt: number;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    if (this.dataset.layout === 'embedded') {\n      this.renderEmbeddedLayout();\n    }\n\n    // Usually this element is built before the cart arrives, so it starts on the progress view. The\n    // cart can have landed already though, and then the progress view would flash up for nothing,\n    // so the queue decides. Any error in the queue counts as settled, no need to single out the\n    // init failure. Every other code comes from an operation that needs a working cart.\n    const settled = getEventQueue().some(event => isCartUpdatedEvent(event) ||\n      isCartUpdateErredEvent(event));\n    if (!settled) {\n      this.startInitProgress();\n    }\n\n    this.renderView();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-init-requested', this.onCartInitRequestedBound);\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-update-erred', this.onCartUpdateErredBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-init-requested', this.onCartInitRequestedBound);\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-update-erred', this.onCartUpdateErredBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-modal-state' && oldValue !== newValue) {\n      this.render();\n    }\n\n    // The liquid writes the first view into the markup, and this callback also runs for it when the\n    // element upgrades. That is not a switch, and connecting renders it, so we skip it here rather\n    // than scroll and move focus on page load.\n    if (this.isConnected && name === 'data-view' && oldValue !== null && oldValue !== newValue) {\n      this.onViewChanged();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * If the emitted \"cart-updated\" event is not the initialization of the cart on page load, we must\n   * scroll the cart drawer window to the top to ensure any cart related messaging is brought into\n   * view for the customer.\n   *\n   * Only the cart view shows that messaging. Adding an item from the upsell view also updates the\n   * cart, and scrolling there would jump the shopper away from the list they are browsing.\n   */\n  private onCartUpdated(event: CartUpdatedEvent) {\n    this.endInitProgress();\n\n    if (!event.detail.is_initial && (this.dataset.view || 'cart') === 'cart') {\n      const modal = this.shadowRoot.querySelector('#panel');\n      modal.scrollTo(0, 0);\n    }\n  }\n\n  /**\n   * A failed init never sends a cart, so the error ends the progress view instead. The error\n   * message renders in the cart view.\n   */\n  private onCartUpdateErred(_event: CartUpdateErredEvent) {\n    this.endInitProgress();\n  }\n\n  /**\n   * The shopper asked to load their bag again after init failed, so the progress view comes back\n   * while init runs again.\n   */\n  private onCartInitRequested(_event: CartInitRequestedEvent) {\n    this.startInitProgress();\n  }\n\n  /**\n   * The flag is set before the switch, so the switch sees it and stays quiet on the cart page.\n   */\n  private startInitProgress() {\n    this.dataset.cartInitializing = 'true';\n\n    const progressView = this.querySelector<HTMLElement>('[slot=\"progress-view\"]');\n    if (progressView) {\n      progressView.dataset.message = 'Just a moment while we load up your shopping bag ...';\n    }\n\n    this.dataset.view = 'progress';\n  }\n\n  /**\n   * Runs on every cart update, so it does nothing unless init is what the progress view is up for.\n   * The flag is cleared after the switch, so the switch still sees it and stays quiet on the cart\n   * page.\n   */\n  private endInitProgress() {\n    if (this.dataset.cartInitializing !== 'true') {\n      return;\n    }\n\n    if (this.dataset.view === 'progress') {\n      this.dataset.view = 'cart';\n    }\n\n    delete this.dataset.cartInitializing;\n  }\n\n  /**\n   * Any cart change finishing switches the progress view back to the cart, which is what keeps the\n   * progress view from being tied to one kind of change. A change that failed completes too, so its\n   * error shows on the cart view. The progress view stays up at least 250ms, so a fast change does\n   * not flash it for a split second.\n   *\n   * This runs while the drawer is closed too, such as when the shopper closed it partway through an\n   * add. Switching a closed drawer is harmless, since it picks its view again when it next opens.\n   */\n  private onCartTransactionCompleted(_event: CartTransactionCompletedEvent) {\n    if (this.dataset.view === 'progress') {\n      const remaining = 250 - (now() - this.progressShownAt);\n      setTimeout(this.showCartAfterProgressBound, Math.max(0, remaining));\n    }\n  }\n\n  /**\n   * Checks the view again because the drawer may have closed and reopened on the cart, or moved on\n   * to another view, while the minimum time ran out.\n   */\n  private showCartAfterProgress() {\n    if (this.dataset.view === 'progress') {\n      this.dataset.view = 'cart';\n    }\n  }\n\n  private showCartDrawer() {\n    const modal = this.shadowRoot.querySelector<HTMLElement>('#panel');\n    modal.inert = false;\n    modal.scrollTo(0, 0);\n\n    this.focusView();\n  }\n\n  private hideCartDrawer() {\n    const modal = this.shadowRoot.querySelector<HTMLElement>('#panel');\n    modal.inert = true;\n  }\n\n  private render() {\n    if (this.dataset.modalState === 'open') {\n      this.showCartDrawer();\n    } else {\n      this.hideCartDrawer();\n    }\n  }\n\n  /**\n   * Shows the view slot matching `data-view` and hides the rest. Hiding a slot hides whatever is\n   * assigned to it.\n   */\n  private renderView() {\n    const viewSlotName = `${this.dataset.view || 'cart'}-view`;\n    const slots = this.shadowRoot.querySelectorAll<HTMLSlotElement>('slot[name$=\"-view\"]');\n    for (const slot of slots) {\n      slot.hidden = slot.name !== viewSlotName;\n    }\n  }\n\n  /**\n   * Turns the panel from a dialog into a plain block for the cart page. The panel takes focus when\n   * the view switches, so it needs a tab index to be focusable, and -1 keeps it out of the tab\n   * order.\n   */\n  private renderEmbeddedLayout() {\n    const panel = this.shadowRoot.querySelector<HTMLElement>('#panel');\n    panel.removeAttribute('role');\n    panel.removeAttribute('aria-modal');\n    panel.removeAttribute('aria-label');\n    panel.inert = false;\n    panel.tabIndex = -1;\n  }\n\n  /**\n   * Asks the view being shown to take focus. Each view puts it on its own close button, which sits\n   * in that view's header.\n   */\n  private focusView() {\n    const view = this.querySelector<HTMLElement>(`[slot=\"${this.dataset.view || 'cart'}-view\"]`);\n    if (view) {\n      view.dataset.focusRequested = 'true';\n    }\n  }\n\n  /**\n   * A switch while the drawer is open starts the new view at its top, with focus on the new view\n   * the same as when the drawer opens.\n   */\n  private onViewChanged() {\n    this.renderView();\n\n    if (this.dataset.view === 'progress') {\n      this.progressShownAt = now();\n    }\n\n    if (this.dataset.layout === 'embedded') {\n      // Init switches happen on their own as the page loads, so they leave the page and focus where\n      // they are.\n      if (this.dataset.cartInitializing === 'true') {\n        return;\n      }\n\n      // The shopper is usually scrolled down to the Checkout button when the view switches, so the\n      // page goes back to the top to bring the start of the new view on screen.\n      scrollTo(0, 0);\n      this.shadowRoot.querySelector<HTMLElement>('#panel').focus({ preventScroll: true });\n    } else if (this.dataset.modalState === 'open') {\n      this.shadowRoot.querySelector('#panel').scrollTo(0, 0);\n      this.focusView();\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-drawer': CartDrawer;\n  }\n}\n\nif (!customElements.get('cart-drawer')) {\n  customElements.define('cart-drawer', CartDrawer);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart, Item } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport { ready, type ReleaseHandle } from '../../../lib/dom-ready.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartTransactionStartedEvent = WindowEventMap['cart-transaction-started'];\ntype CartTransactionCompletedEvent = WindowEventMap['cart-transaction-completed'];\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * Renders cart state and provides UI for adding and removing items and starting checkout\n */\nclass CartElement extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-clear-messages', 'data-focus-requested'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * URL for header \"our best sellers\" anchor link\n     */\n    bestSellersUrl: string;\n\n    /**\n     * Set from outside, such as when the drawer opens, to clear the cart messages. Setting it again\n     * clears them again, even with the same value. Removing it clears nothing.\n     */\n    clearMessages: string;\n\n    /**\n     * Country ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    countryCode: string;\n\n    /**\n     * Currency ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required.\n     *\n     * @example \"US\"\n     */\n    currencySymbol: string;\n\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * The name of the current Shopify template. Affects whether the side cart is displayed.\n     */\n    templateName: string;\n  };\n\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n  private onContinueShoppingButtonClickBound = this.onContinueShoppingButtonClick.bind(this);\n  private onReadyBound = this.onReady.bind(this);\n  private onShowDiscountInputClickedBound = this.onShowDiscountInputClicked.bind(this);\n\n  private cartViewedDispatched = false;\n\n  private declare releaseReady: ReleaseHandle;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.releaseReady = ready(this.onReadyBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (!this.isConnected) {\n      return;\n    }\n\n    // When one of these attributes is set before the element is defined, the upgrade calls this\n    // before connectedCallback, so the element is connected but has no shadow root yet.\n    if (!this.shadowRoot) {\n      return;\n    }\n\n    if (name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n\n    if (name === 'data-clear-messages' && newValue !== null) {\n      this.clearMessages(newValue);\n    }\n  }\n\n  /**\n   * Passes the clear request on to the messages, so callers outside only ever touch this element\n   * and never reach into its shadow root.\n   */\n  private clearMessages(value: string) {\n    const cartMessages = this.shadowRoot.querySelector('cart-messages');\n    if (cartMessages) {\n      cartMessages.dataset.clear = value;\n    }\n  }\n\n  public disconnectedCallback() {\n    const continueShopping = this.shadowRoot.querySelector<HTMLElement>('#continue-shopping');\n    continueShopping?.removeEventListener('click', this.onContinueShoppingButtonClickBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseClickedBound);\n\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n\n    // Extra paranoia, this should always be defined\n    if (this.releaseReady) {\n      this.releaseReady();\n    }\n\n    const showDiscountInput = this.shadowRoot.querySelector<HTMLElement>(\n      '.show-discount-input button');\n    showDiscountInput?.removeEventListener('click', this.onShowDiscountInputClickedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onReady() {\n    if (!this.isConnected) {\n      return;\n    }\n\n    const continueShopping = this.shadowRoot.querySelector<HTMLElement>('#continue-shopping');\n    continueShopping.addEventListener('click', this.onContinueShoppingButtonClickBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseClickedBound);\n\n    if (this.dataset.bestSellersUrl) {\n      const bestSellersEl = this.shadowRoot.querySelector<HTMLAnchorElement>('a.best-sellers-link');\n      bestSellersEl.href = this.dataset.bestSellersUrl;\n    }\n\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    const showDiscountInput = this.shadowRoot.querySelector<HTMLElement>(\n      '.show-discount-input button');\n    showDiscountInput.addEventListener('click', this.onShowDiscountInputClickedBound);\n  }\n\n  /**\n   * Focus moves here programmatically, so without forcing the visible-focus state the browser\n   * suppresses the focus ring after a mouse-driven open or switch, and the just-focused close\n   * button gives no visual cue of where focus landed. Older browsers that ignore the option degrade\n   * to an unadorned programmatic focus.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    // A rapid second activation of the close control can fire this again after the first close has\n    // already begun. We do not de-duplicate here and instead rely on the cart drawer's close\n    // ignoring a request when it is already closed, so the repeat stays a silent no-op rather than\n    // producing a spurious modal close.\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onContinueShoppingButtonClick(_event: MouseEvent) {\n    // if user is on the cart page there is no cart drawer and the \"Continue Shopping\" CTA should\n    // instead route the user to the home page.\n    if (this.dataset.templateName === 'cart') {\n      location.href = location.origin;\n      return;\n    }\n\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'cart-continue-shopping-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * Renders the first cart we receive.\n   *\n   * Watch out, onCartUpdated below does nearly all of this same show and hide work a second time,\n   * for every update after the first one. The two copies use different names for the same things,\n   * so promo here is promoGoalEl down there and installmentsMessage here is\n   * installmentPaymentMessageElement down there, and they look those elements up in different ways\n   * too. Nobody has collapsed the two, so a change to what shows and hides here almost certainly\n   * needs the same change down there.\n   */\n  private onCartUpdatedInitial(event: CartUpdatedEvent) {\n    const cart = event.detail.cart;\n\n    if (cart.items.length > 0) {\n      this.shadowRoot.querySelector<HTMLElement>('#cart-contents').hidden = false;\n      this.shadowRoot.querySelector<HTMLElement>('.footer').hidden = false;\n      this.shadowRoot.querySelector('#line-items').role = 'list';\n    } else {\n      const emptyCart = this.shadowRoot.querySelector<HTMLElement>('#empty-cart');\n      emptyCart.hidden = false;\n    }\n\n    // We will load only one of these\n    const promo = this.querySelector<HTMLElement>('free-shipping-banner, promo-goal-progress');\n\n    const installmentsMessage = this.querySelector('installment-payment-message');\n\n    if (this.dataset.countryCode !== 'US') {\n      if (installmentsMessage) {\n        installmentsMessage.hidden = true;\n      }\n\n      if (promo) {\n        promo.hidden = true;\n      }\n    } else {\n      if (installmentsMessage) {\n        installmentsMessage.hidden = false;\n      }\n\n      if (cart.items.some(isMBG)) {\n        installmentsMessage?.classList.add('disclaimer');\n      } else {\n        installmentsMessage?.classList.remove('disclaimer');\n      }\n\n      if (installmentsMessage) {\n        installmentsMessage.dataset.estimatedTotal = cart.total_price.toString();\n      }\n\n      if (promo) {\n        promo.hidden = false;\n      }\n    }\n\n    // If the cart has TBYB products in the cart with deferred payments, we need to show the due\n    // today messaging. We determine the total deferred payments in the cart and if this value is\n    // greater than zero we show messaging. Otherwise we hide it.\n    this.renderDeferredPaymentMessage(cart);\n\n    const estTotal = this.shadowRoot.querySelector<HTMLElement>('#estimated-total');\n    estTotal.dataset.subunits = cart.total_price.toString();\n    estTotal.dataset.currencyCode = this.dataset.currencyCode;\n    estTotal.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    this.hideBackdrop();\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    const cart = event.detail.cart;\n\n    // A row that fails to render should not also stop the totals and the show and hide work below\n    // from running.\n    try {\n      this.renderLineItems(cart.items);\n\n      // Only an add made through an add to cart button pulses. The free gift the cart adds on its\n      // own, the quantity dropdown, and an update synced from another tab do not.\n      if (event.detail.scenario === 'button') {\n        this.pulseLastAdded(event.detail.variants_added);\n      }\n    } catch (error) {\n      console.error(error);\n    }\n\n    // A cart arriving is the moment there is something to look at, so this is where the cart page\n    // announces its view. It does nothing in the drawer, which announces its own when it opens.\n    this.dispatchCartViewed();\n\n    const heading = this.shadowRoot.querySelector('h2');\n    const quantity = cart.item_count;\n    if (quantity === 0) {\n      heading.textContent = 'Shopping Bag';\n    } else if (quantity === 1) {\n      heading.textContent = 'Shopping Bag (1 item)';\n    } else {\n      heading.textContent = ['Shopping Bag (', quantity.toString(), ' items)'].join('');\n    }\n\n    if (event.detail.is_initial) {\n      this.onCartUpdatedInitial(event);\n      return;\n    }\n\n    // From here down is the second copy of the show and hide work described on onCartUpdatedInitial\n    // above.\n    const cartContents = this.shadowRoot.querySelector<HTMLElement>('#cart-contents');\n    const emptyCart = this.shadowRoot.querySelector<HTMLElement>('#empty-cart');\n    const footerEl = this.shadowRoot.querySelector<HTMLElement>('.footer');\n    const lineItems = this.shadowRoot.querySelector('#line-items');\n\n    if (cart.items.length > 0) {\n      if (cartContents) {\n        cartContents.hidden = false;\n      }\n\n      emptyCart.hidden = true;\n      footerEl.hidden = false;\n      lineItems.role = 'list';\n    } else {\n      emptyCart.hidden = false;\n\n      if (cartContents) {\n        cartContents.hidden = true;\n      }\n\n      footerEl.hidden = true;\n      lineItems.role = null;\n    }\n\n    // we will load only one of these, either the free shipping banner or promo goal progress\n    const promoGoalEl = this.querySelector('free-shipping-banner') ||\n      this.querySelector('promo-goal-progress');\n\n    const installmentPaymentMessageElement = this.querySelector('installment-payment-message');\n\n    if (this.dataset.countryCode === 'US') {\n      if (installmentPaymentMessageElement) {\n        installmentPaymentMessageElement.hidden = false;\n      }\n\n      if (cart.items.some(isMBG)) {\n        installmentPaymentMessageElement?.classList.add('disclaimer');\n      } else {\n        installmentPaymentMessageElement?.classList.remove('disclaimer');\n      }\n\n      if (installmentPaymentMessageElement) {\n        installmentPaymentMessageElement.dataset.estimatedTotal = cart.total_price.toString();\n      }\n\n      if (cart.attributes?._aux_opt_in_v2 === 'new') {\n        const promoGoalV2El = this.querySelector('promo-goal-progress-v2');\n        if (promoGoalV2El) {\n          promoGoalV2El.dataset.show = 'true';\n        }\n      }\n\n      if (promoGoalEl) {\n        promoGoalEl.hidden = false;\n      }\n    } else {\n      if (installmentPaymentMessageElement) {\n        installmentPaymentMessageElement.hidden = true;\n      }\n\n      if (promoGoalEl) {\n        promoGoalEl.hidden = true;\n      }\n    }\n\n    const estTotal = this.shadowRoot.querySelector<HTMLElement>('#estimated-total');\n    estTotal.dataset.subunits = cart.total_price.toString();\n    estTotal.dataset.currencyCode = this.dataset.currencyCode;\n    estTotal.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    // If the cart has TBYB products in the cart with deferred payments, we need to show the due\n    // today messaging. We determine the total deferred payments in the cart and if this value is\n    // greater than zero we show messaging. Otherwise we hide it.\n    this.renderDeferredPaymentMessage(cart);\n  }\n\n  /**\n   * Announces that the shopper is looking at their cart, on the cart page only. The drawer has an\n   * obvious moment for this, the point where it opens, and the cart page has nothing like it. The\n   * cart arrives after the page does, so we go with the first one we receive, which is the earliest\n   * point where there is a cart to look at.\n   *\n   * Only the first. Every update after that is the shopper changing something in a cart they are\n   * already looking at, which is not a new view.\n   *\n   * The flag holding that is per instance and not per page load, so a second instance announces a\n   * second view. Watch out for that if this element ever ends up on the page more than once.\n   */\n  private dispatchCartViewed() {\n    if (this.dataset.templateName !== 'cart') {\n      return;\n    }\n\n    if (this.cartViewedDispatched) {\n      return;\n    }\n\n    this.cartViewedDispatched = true;\n\n    type CartViewedEvent = GlobalEventHandlersEventMap['cart-viewed'];\n    const event = new CustomEvent<CartViewedEvent['detail']>('cart-viewed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        surface: 'page'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private renderDeferredPaymentMessage(cart: Cart) {\n    let deferredAmount = 0;\n    for (const item of cart.items) {\n      if (item.selling_plan_allocation?.selling_plan?.recurring_deliveries === false &&\n        item.selling_plan_allocation?.selling_plan?.name !== 'Pre-Order') {\n        deferredAmount += item.selling_plan_allocation.price;\n\n        // If a discount was applied to the item, we need to subtract that from the deferred amount\n        // because the TryNow app is not aware of any discounts and therefore the\n        // selling_plan_allocation.price property reflects the price before discount.\n        if (item.line_level_total_discount > 0) {\n          deferredAmount -= item.line_level_total_discount;\n        }\n      }\n    }\n\n    if (deferredAmount > 0) {\n      const dueTodayEl = this.shadowRoot.querySelector<HTMLElement>('#due-today');\n      dueTodayEl.dataset.subunits = (cart.total_price - deferredAmount).toString();\n      dueTodayEl.dataset.currencyCode = this.dataset.currencyCode;\n      dueTodayEl.dataset.currencySymbol = this.dataset.currencySymbol;\n\n      const dueTodayContainer = this.shadowRoot.querySelector<HTMLElement>('#tbyb-due-today');\n      dueTodayContainer.hidden = false;\n    } else {\n      const dueTodayContainer = this.shadowRoot.querySelector<HTMLElement>('#tbyb-due-today');\n      dueTodayContainer.hidden = true;\n    }\n  }\n\n  /**\n   * Renders the cart-line-item elements into the line items slot.\n   *\n   * This does not do partial renders specific to the type of cart update. This always fully re-\n   * renders after an update. Any cart update that is in response to some action (e.g. add item,\n   * remove item) can contain any kind of state change. Each action such as add to cart obtains a\n   * new cart state where multiple things, things not directly related to the items involved, may\n   * have changed. For example, removing a line item can change the key property of other items not\n   * removed. In addition, there is no pressing need to partially render because the number of items\n   * is generally small whereas the amount of engineering effort to properly partially render is\n   * huge.\n   *\n   * Prior implementations of this logic were horrifically flawed. Please never reintroduce support\n   * for, or the concept of, partial rendering.\n   *\n   * The rows share the light DOM with everything else the liquid slots into this element, such as\n   * the promo banner and the upsells, so only the rows in the line items slot are removed.\n   */\n  private renderLineItems(parents: Item[]) {\n    // Pre-compute parent keys to enable single-pass rendering. This avoids nested loops over the\n    // full cart for each item when identifying which items have children.\n    const uniqueParentKeys = new Set();\n    for (const item of parents) {\n      if (item.parent_relationship?.parent_key) {\n        uniqueParentKeys.add(item.parent_relationship.parent_key);\n      }\n    }\n\n    const giftWithPurchaseProduct = parents.find(\n      item => item.properties._gift_with_purchase === 'true');\n\n    const fragment = document.createDocumentFragment();\n    for (const item of parents) {\n      if (item.parent_relationship?.parent_key) {\n        // Child items (e.g. GWP) are rendered nested within their parent, skip in top-level loop\n        continue;\n      }\n\n      // Skip rendering the gift with purchase product in the top-level loop; it will be rendered\n      // separately.\n      if (giftWithPurchaseProduct?.key !== item.key) {\n        const element = this.createItemElement(item);\n        element.slot = 'line-items';\n        if (uniqueParentKeys.has(item.key)) {\n          element.dataset.isParent = 'true';\n          this.appendChildItems(element, item.key, parents);\n        }\n\n        fragment.appendChild(element);\n      }\n    }\n\n    if (giftWithPurchaseProduct) {\n      const element = this.createItemElement(giftWithPurchaseProduct);\n      element.slot = 'line-items';\n      // If cart updates changed the gift with purchase eligibility, a final_price value above zero\n      // is that signal. In this case, we do not mark the item as a gift with purchase in case the\n      // automated removal request fails. Otherwise a customer would have no means to remove the\n      // line item and it would reflect an incorrect price.\n      if (giftWithPurchaseProduct.properties._gift_with_purchase === 'true' &&\n        giftWithPurchaseProduct.final_price === 0) {\n        element.dataset.isGiftWithPurchase = 'true';\n\n        // The offer view holds the gifts in stock, and with only one there is nothing to change to.\n        const giftCount = document.querySelectorAll('offer-view gift-with-purchase-product').length;\n        element.dataset.giftChangeable = giftCount > 1 ? 'true' : 'false';\n\n        // The promos app's cart transform zeroes the line's price outright rather than discounting\n        // it, so every price on the line reads 0. The gift's regular price only survives in the\n        // property saved when the gift was added. Using it as the line price makes the line show\n        // $0 with that price struck through, the same way a discounted line does.\n        const regularPrice = parseInt(giftWithPurchaseProduct.properties._gift_with_purchase_price,\n          10);\n        if (regularPrice > 0) {\n          element.dataset.linePrice = (regularPrice * giftWithPurchaseProduct.quantity).toString();\n        }\n      }\n\n      fragment.appendChild(element);\n    }\n\n    for (const row of this.querySelectorAll(':scope > [slot=\"line-items\"]')) {\n      row.remove();\n    }\n\n    this.appendChild(fragment);\n  }\n\n  /**\n   * Plays the pulse on the row of the last variant added, whether the add made a new row or raised\n   * the quantity of an existing one. Every render replaces all the rows, so the next render drops\n   * the attribute and the pulse never replays.\n   */\n  private pulseLastAdded(variants: CartUpdatedEvent['detail']['variants_added']) {\n    const variant = variants[variants.length - 1];\n    if (!variant) {\n      return;\n    }\n\n    const lineItem = this.querySelector<HTMLElement>(\n      `cart-line-item[data-variant-id=\"${variant.variant_id}\"]`);\n    if (lineItem) {\n      lineItem.dataset.pulse = 'true';\n    }\n  }\n\n  private appendChildItems(parentElement: HTMLElement, parentKey: string, items: Item[]) {\n    for (const item of items) {\n      if (item.parent_relationship?.parent_key === parentKey) {\n        const childElement = this.createItemElement(item);\n        childElement.classList.add('child-item');\n        parentElement.appendChild(childElement);\n      }\n    }\n  }\n\n  private createItemElement(item: Item) {\n    const lineItemEl = document.createElement('cart-line-item');\n    lineItemEl.role = 'listitem';\n    lineItemEl.dataset.countryCode = this.dataset.countryCode;\n    lineItemEl.dataset.currencyCode = this.dataset.currencyCode;\n    lineItemEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    lineItemEl.dataset.imageSrc = item.image;\n    lineItemEl.dataset.imageAlt = item.title;\n    lineItemEl.dataset.isAvailable = 'true';\n    lineItemEl.dataset.lineItemId = item.key;\n    lineItemEl.dataset.variantId = item.variant_id.toString();\n    lineItemEl.dataset.title = item.title;\n    lineItemEl.dataset.quantity = item.quantity.toString();\n    lineItemEl.dataset.productType = item.product_type;\n    lineItemEl.dataset.productUrl = item.url;\n    lineItemEl.dataset.price = item.final_price.toString();\n    lineItemEl.dataset.linePrice = item.original_line_price.toString();\n    lineItemEl.dataset.linePriceAfterDiscount = item.final_line_price.toString();\n\n    if (item.parent_relationship?.parent_key) {\n      lineItemEl.dataset.parentId = item.parent_relationship.parent_key;\n    }\n\n    if (item.properties._is_upsell === 'true') {\n      lineItemEl.dataset.isUpsell = 'true';\n      lineItemEl.dataset.maxQuantity = '1';\n    }\n\n    if (item.handle) {\n      lineItemEl.dataset.productHandle = item.handle;\n    }\n\n    if (item.product_id) {\n      lineItemEl.dataset.productId = item.product_id.toString();\n    }\n\n    // Child items (e.g. GWP) are free promotional products, so subscription options don't apply\n    if (item.selling_plan_allocation && !item.parent_relationship?.parent_key) {\n      lineItemEl.dataset.sellingPlanId = item.selling_plan_allocation.selling_plan.id.toString();\n      lineItemEl.dataset.sellingPlanFrequency =\n        item.selling_plan_allocation.selling_plan.name.replace('Delivery every ', '');\n      if (item.properties['_selling_plan_options']) {\n        const select = document.createElement('select');\n        select.classList.add('selling-plan-options');\n        select.slot = 'selling-plan-options';\n\n        const options = JSON.parse(item.properties['_selling_plan_options']);\n\n        // If the parsed JSON does not conform to what is expected, we simply do not render and\n        // inject the selling plan options input\n        if (isSellingPlanOptionsArray(options)) {\n          for (const option of options) {\n            const optionEl = document.createElement('option');\n            optionEl.value = option.id.toString();\n            optionEl.textContent = option.name;\n            if (option.id === item.selling_plan_allocation.selling_plan.id) {\n              optionEl.selected = true;\n            }\n\n            select.appendChild(optionEl);\n          }\n\n          lineItemEl.appendChild(select);\n        }\n      }\n    } else if (item.properties['_selling_plan_options'] && !item.parent_relationship?.parent_key) {\n      // If not a subscription but selling plans are available - allow upgrading to a subscription\n      // using the first available selling plan. Child items are excluded since these kind of\n      // interactions are bound to their parent line item.\n      let sellingPlanId: number;\n      try {\n        const sellingPlans = <{ id: number; name: string; }[]>JSON.parse(\n          item.properties['_selling_plan_options']);\n        sellingPlanId = sellingPlans[0].id;\n      } catch (error) {\n        console.warn('Invalid _selling_plan_options', item.properties['_selling_plan_options'],\n          error);\n      }\n\n      if (sellingPlanId) {\n        const upgradeButton = document.createElement('button');\n        upgradeButton.textContent = 'Subscribe & Save';\n        // This is the plan to upgrade to, so it means something different than the line item\n        // element's own data-selling-plan-id, which is the plan the item is already on. It lives on\n        // the button rather than on the element so that offering a choice of plans later is just\n        // more buttons. The line item element reads it back off the button on click.\n        upgradeButton.dataset.sellingPlanId = sellingPlanId.toString();\n        upgradeButton.slot = 'selling-plan-options';\n        upgradeButton.classList.add('upgrade-to-subscription');\n        upgradeButton.dataset.clickTrack = 'Cart subscription upgrade';\n        lineItemEl.appendChild(upgradeButton);\n      }\n    }\n\n    const messages = generateMessages(item, this.dataset.currencySymbol);\n\n    // Child items inherit messaging context from parent, suppress duplicates to avoid UI clutter\n    if (messages.length > 0 && !item.parent_relationship?.parent_key) {\n      const ulTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>('#message-list');\n      const ulClone = <HTMLElement>ulTemplate.content.cloneNode(true);\n      const ul = ulClone.querySelector('ul');\n\n      for (const message of messages) {\n        const itemTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>('#message');\n        const itemClone = <HTMLElement>itemTemplate.content.cloneNode(true);\n        const itemEl = itemClone.querySelector('li');\n        itemEl.classList.add(message.type);\n\n        const itemMessage = itemEl.querySelector<HTMLSpanElement>('.message');\n        itemMessage.innerHTML = message.message;\n\n        ul.appendChild(itemEl);\n      }\n\n      lineItemEl.appendChild(ul);\n    }\n\n    lineItemEl.dataset.update = Date.now().toString();\n\n    return lineItemEl;\n  }\n\n  /**\n   * We only start listening for this event from within the connected callback so we know the DOM is\n   * ready and the backdrop element exists.\n   */\n  private onCartTransactionStarted(_event: CartTransactionStartedEvent) {\n    this.showBackdrop();\n  }\n\n  /**\n   * We only start listening for this event from within the connected callback so we know the DOM is\n   * ready and the backdrop element exists.\n   */\n  private onCartTransactionCompleted(_event: CartTransactionCompletedEvent) {\n    this.hideBackdrop();\n  }\n\n  /**\n   * The cart has its own loading veil: a translucent overlay with a spinner that covers the cart's\n   * contents while a cart transaction (add, remove, quantity change, discount) is in flight. Its\n   * job is to signal that work is underway and to lightly block the contents while the request is\n   * pending, alongside the disabling of individual controls. This is a different kind of backdrop\n   * than the page layers that dim everything behind a modal or drawer: its scope is only this\n   * element's own content area, and clicking it does not dismiss anything.\n   */\n  private hideBackdrop() {\n    // The loading veil is rendered as part of this element's template, so it lives in the shadow\n    // tree and must be looked up through the shadow root. A light-DOM lookup silently finds nothing\n    // and the toggle below quietly does nothing.\n    const backdrop = this.shadowRoot.querySelector<HTMLElement>('#cart-backdrop');\n    if (backdrop) {\n      backdrop.hidden = true;\n    }\n  }\n\n  private showBackdrop() {\n    // The loading veil is rendered as part of this element's template, so it lives in the shadow\n    // tree and must be looked up through the shadow root. A light-DOM lookup silently finds nothing\n    // and the toggle below quietly does nothing.\n    const backdrop = this.shadowRoot.querySelector<HTMLElement>('#cart-backdrop');\n    if (backdrop) {\n      backdrop.hidden = false;\n    }\n  }\n\n  private onShowDiscountInputClicked(_event: MouseEvent) {\n    const discountInput = this.shadowRoot.querySelector<HTMLSlotElement>(\n      'slot[name=\"discount-input\"]');\n    discountInput.hidden = false;\n\n    // hide the button after clicking it\n    const showDiscountInput = this.shadowRoot.querySelector<HTMLElement>('.show-discount-input');\n    showDiscountInput.hidden = true;\n  }\n}\n\nfunction isMBG(item: Item) {\n  return item.properties?.['_is_mbg'] === 'true';\n}\n\ninterface SellingPlanOptions {\n  id: number;\n  name: string;\n}\n\ninterface LineItemMessage {\n  message: string;\n  type: 'discount' | 'gift' | 'information' | 'warning';\n}\n\n/**\n * Builds the messages to show for a line item.\n */\nfunction generateMessages(item: Item, currencySymbol: string) {\n  const messages: LineItemMessage[] = [];\n\n  // A gift line only counts as free once the cart transform has zeroed its price. That is the same\n  // test the cart uses to mark the line item a gift with purchase.\n  if (item.properties._gift_with_purchase === 'true' && item.final_price === 0) {\n    messages.push({ type: 'gift', message: 'Free gift' });\n  }\n\n  // Render the discounts allocated to the line. It is possible for allocations to allocate 0\n  // dollars of a discount application to a line. This is pretty common with discounts such as BXGY\n  // discounts that do not reduce the price of the items used in the BX part of the discount, only\n  // the GY part. The discounts are allocated to the line as $0 off, as the lines participate in the\n  // discount but are not discounted themselves. We only want to render reductions.\n\n  for (const allocation of item.line_level_discount_allocations) {\n    if (allocation.discount_application.title && allocation.amount > 0) {\n      // The allocation amount is in subunits (e.g. cents in USD), so we divide by 100. We use\n      // toFixed to ensure we show 2 significant digits\n      const value = (allocation.amount / 100).toFixed(2);\n\n      messages.push({ type: 'discount',\n        message: `${allocation.discount_application.title} (${currencySymbol}${value})` });\n    } else if (allocation.amount === 0 && /bogo/i.test(allocation.discount_application.title)) {\n      const code = allocation.discount_application.title;\n\n      let message: string;\n      if (item.quantity === 1) {\n        message = `This product qualifies another product for ${code}`;\n      } else {\n        message = `This product qualifies ${item.quantity} other products for ${code}`;\n      }\n\n      messages.push({ type: 'information', message });\n    }\n  }\n\n  if (item.properties._contains_alcohol === 'true') {\n    messages.push({ type: 'information',\n      message: 'This item may only ship to the contiguous U.S and Canada.' });\n  }\n\n  if (item.properties._compatibility === 'soleil') {\n    messages.push({ type: 'warning',\n      message: 'Only compatible with <a href=\"/products/soleil\">Soleil</a>' });\n  }\n\n  if (item.properties._compatibility === 'max-high-volume') {\n    messages.push({ type: 'warning',\n      message: 'TOOL SOLD SEPARATELY. Requires <a href=\"/products/multi-volume-series\">' +\n        'Multi-Volume Series</a> brush dryer handle to work' });\n  }\n\n  if (item.properties._final_sale === 'true') {\n    messages.push({ type: 'warning', message: 'This item is final sale, not eligible for return' });\n  }\n\n  if (item.properties._backorder_message) {\n    messages.push({ type: 'information', message: item.properties._backorder_message });\n  }\n\n  if (item.properties._is_tbyb) {\n    messages.push({ type: 'information', message: item.properties._is_tbyb });\n  }\n\n  return messages;\n}\n\nfunction isSellingPlanOptionsArray(value: any): value is SellingPlanOptions[] {\n  return Array.isArray(value) && value.filter(item => item.id && item.name).length === value.length;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-element': CartElement;\n  }\n}\n\nif (!customElements.get('cart-element')) {\n  customElements.define('cart-element', CartElement);\n}\n","import type {} from '@integrabeauty/shopify-browser-types';\n\n/**\n * Represents the value of a cart attribute. Ordinarily values are strings. However, Shopify also\n * supports storing nested objects as values.\n */\nexport type AttributeValue = Record<string, string> | string;\n\n/**\n * The type of the value of `Cart['attributes']`\n */\nexport type Attributes = Record<string, AttributeValue>;\n\n/**\n * This is the stored cart, which is what a get cart answers with. A mutation answers with a\n * superset, so see the response body types alongside each mutation for the extra properties those\n * carry.\n *\n * @todo selling plan properties\n * @todo endpoint request body response body types\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart\n */\nexport interface Cart {\n  /**\n   * An object where the key is the attribute name and the value is the attribute value.\n   */\n  attributes: Attributes;\n\n  /**\n   * A discount application represents a type of discount.\n   *\n   * When a discount is applied to one or more line items, it is said to be _allocated_ to the line\n   * items. That is why line level discount allocation objects have a nested application object that\n   * will correlate with one of the applications in this array.\n   *\n   * When a discount is applied to the whole cart, it will only be present in this array because it\n   * is not allocated to particular lines.\n   */\n  cart_level_discount_applications: DiscountApplication[];\n\n  /**\n   * The ISO currency code for the cart.\n   *\n   * @example \"USD\"\n   */\n  currency: string;\n\n  /**\n   * Discount codes that have been applied to the cart.\n   *\n   * Not all codes are applicable. It is possible for a code to be applied but inapplicable.\n   *\n   * This does not provide information about the amount of each discount. Refer to the cart level\n   * discount applications or line level discount allocations for that information.\n   *\n   * This array will include shipping discounts. A shipping discount will be present in the array if\n   * applied but in the inapplicable state while in the cart. The discount will appear as applicable\n   * at checkout automatically.\n   */\n  discount_codes: DiscountCode[];\n\n  /**\n   * Total quantity of items in a cart\n   */\n  item_count: number;\n\n  /**\n   * The cart's line items.\n   *\n   * A cart will always have a defined items array, even if the cart is empty.\n   */\n  items: Item[];\n\n  items_subtotal_price: number;\n\n  /**\n   * An optional textual note that may be applied to the cart. If a cart becomes a checkout, and\n   * then the checkout becomes an order, the cart note will carry over to the order note.\n   *\n   * An order's note, and an order's note attributes, are two completely separate concepts.\n   */\n  note: string;\n\n  original_total_price: number;\n\n  /**\n   * This is true when at least one item in the cart requires shipping.\n   *\n   * This is false when no items in the cart require shipping.\n   *\n   * Shopify supports digital-only carts. These are carts that when purchased do not require the\n   * creation of a fulfillment. For example, a cart that consists only of a digital gift card.\n   */\n  requires_shipping: boolean;\n\n  /**\n   * This property is defined when there are cart mutations that involve the use of liquid-rendered\n   * sections.\n   *\n   * @see https://shopify.dev/docs/api/ajax/reference/cart#request-sections\n   */\n  sections: Record<string, string>;\n\n  /**\n   * This is the publicly visible cart id.\n   *\n   * If a checkout is created from this cart, the checkout's cart token will be equal to this value.\n   *\n   * Never write logic that parses this value. The format can change at any time, and has changed\n   * several times in the past.\n   *\n   * The value may contain url-like values, e.g. \"abc?key=123\"\n   */\n  token: string;\n\n  /**\n   * The total reduction in price in currency subunits (e.g. in cents for USD).\n   *\n   * @example 5010\n   */\n  total_discount: number;\n\n  /**\n   * Total price in currency subunits. For example, if the currency is USD, this is in cents.\n   *\n   * @example 1050\n   */\n  total_price: number;\n\n  /**\n   * The total weight of the cart (in grams?). This is a simple sum of the weight of the items of\n   * the cart (unit weight * quantity).\n   *\n   * @example 123.456\n   */\n  total_weight: number;\n}\n\n/**\n * Represents a discount that has been applied to an item.\n */\nexport interface Discount {\n  amount: number;\n  title: string;\n}\n\n/**\n * Represents a discount application that has been allocated to a line item.\n */\nexport interface DiscountAllocation {\n  /**\n   * The amount of the application that was allocated to a particular line item.\n   *\n   * When a discount application is allocated to several line items, this is a partial value of the\n   * full discount application amount that is allocated to this particular line. When the discount\n   * application is only allocated to a single line, this value will be equal to the total value of\n   * the discount application.\n   *\n   * For example, let's say we have cart with 2 line items and 10 dollar off discount application.\n   * The total value of the discount application is 10 dollars. Let's say the discount is allocated\n   * to both lines. The value of each allocation is 5 dollars. For each line this amount will\n   * contain the value 500. Within the nested application object, the application's total amount\n   * will be 1000. This is because each nested application object is a copy of the one application\n   * that happens to have multiple allocations (the discount is applied to multiple lines).\n   *\n   * The amount is in currency subunits. For example, for USD, this is the value of cents. For\n   * example, a discount of $2.50 is 250.\n   *\n   * @example 1050\n   */\n  amount: number;\n\n  /**\n   * The corresponding discount application that was allocated to a particular line item.\n   */\n  discount_application: DiscountApplication;\n}\n\n/**\n * A discount application represents a discount of some kind. There are automatically applied\n * discount applications, discount code applications, and script applied discount applications.\n */\nexport interface DiscountApplication {\n  allocation_method: string;\n  created_at: string;\n  description: string;\n  key: string;\n  target_selection: string;\n  target_type: string;\n  title: string;\n  total_allocated_amount: number;\n  type: string;\n  value: string;\n  value_type: string;\n}\n\n/**\n * The details of an image resource, such as the hero image for a product.\n */\nexport interface Image {\n  alt: string;\n  aspect_ratio: number;\n  height: number;\n  url: string;\n  width: number;\n}\n\n/**\n * Represents a line item in a cart.\n */\nexport interface Item {\n  /**\n   * The price of the line item after discounts\n   */\n  discounted_price: number;\n\n  /**\n   * Discount codes applied to the line item.\n   *\n   * @deprecated\n   */\n  discounts: Discount[];\n\n  /**\n   * The main image for the product\n   */\n  featured_image: Image;\n\n  /**\n   * The combined price, in the currency's subunit, of all of the items in the line item. This\n   * includes any line-level discounts.\n   *\n   * The value is equal to line_item.final_price multiplied by line_item.quantity. It's output in\n   * the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  final_line_price: number;\n\n  /**\n   * The price of the line item in the currency's subunit. This includes any line-level discounts.\n   *\n   * The value is output in the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  final_price: number;\n\n  /**\n   * Returns true if the product associated with the line item is a gift card. Returns false if not.\n   */\n  gift_card: boolean;\n\n  /**\n   * The weight of the line item in the store's default weight unit.\n   */\n  grams: number;\n\n  handle: string;\n\n  has_components: boolean;\n\n  /**\n   * The ID of the line item. The ID differs depending on the context. The following table outlines\n   * the possible contexts and their associated values:\n   *\n   * Context Value\n   *\n   * * cart.items The ID of the line item's variant. This ID isn't unique, and can be shared by\n   *   multiple items with the same variant.\n   * * checkout.line_items A temporary unique hash generated for the checkout.\n   * * order.line_items A unique integer ID.\n   */\n  id: number;\n\n  image: string;\n\n  /**\n   * Added in conjunction with parent_relationship to support determining whether a line item can be\n   * removed or have its quantity updated when the line item is a child of another line item, e.g.\n   * part of a bundle or value set or promotional gift.\n   */\n  instructions: {\n    can_remove: boolean;\n    can_update_quantity: boolean;\n  };\n\n  key: string;\n\n  /**\n   * The discount allocations that apply to the line item.\n   */\n  line_level_discount_allocations: DiscountAllocation[];\n\n  /**\n   * The total amount of any discounts applied to the line item in the currency's subunit.\n   *\n   * The value is output in the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  line_level_total_discount: number;\n\n  /**\n   * @deprecated https://shopify.dev/docs/api/liquid/objects/line_item\n   */\n  line_price: number;\n\n  /**\n   * The name and value pairs for each option of the variant associated with the line item.\n   *\n   * The array is never empty because variants with no options still have a default option. Because\n   * of this, you should use line_item.product.has_only_default_variant to check whether there's any\n   * information to output.\n   */\n  options_with_values: Option[];\n\n  /**\n   * The combined price of all of the items in a line item in the currency's subunit, before any\n   * discounts have been applied.\n   *\n   * The value is equal to line_item.original_price multiplied by line_item.quantity. It's output in\n   * the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  original_line_price: number;\n\n  /**\n   * The price of the line item in the currency's subunit, before discounts have been applied.\n   *\n   * The value is output in the customer's local (presentment) currency.\n   *\n   * For currencies without subunits, such as JPY and KRW, tenths and hundredths of a unit are\n   * appended. For example, 1000 Japanese yen is output as 100000.\n   */\n  original_price: number;\n\n  /**\n   * The parent relationship of the line item. This is only present if the line item is a child of\n   * another line item. For example, when the line item is part of a bundle or value set or\n   * promotional gift.\n   *\n   * The parent relationship must be established by adding the \"parent_id\" property to the add to\n   * bag request.\n   *\n   * @see https://shopify.dev/docs/apps/build/product-merchandising/nested-cart-lines/create-nested-cart-lines#cart-ajax-api\n   */\n  parent_relationship: {\n    parent_key: string;\n  };\n\n  presentment_price: number;\n\n  /**\n   * @deprecated https://shopify.dev/docs/api/liquid/objects/line_item\n   */\n  price: number;\n\n  product_description: string;\n\n  product_has_only_default_variant: boolean;\n\n  /**\n   * The ID of the line item's product.\n   */\n  product_id: number;\n\n  product_title: string;\n\n  product_type: string;\n\n  properties: Record<string, string>;\n  quantity: number;\n\n  quantity_rule: QuantityRule;\n\n  requires_shipping: boolean;\n\n  /**\n   * The selling plan allocation of the line item.\n   */\n  selling_plan_allocation: SellingPlanAllocation;\n\n  /**\n   * The SKU of the variant in the line item.\n   *\n   * Stock Keeping Unit.\n   */\n  sku: string;\n\n  successfully_fulfilled_quantity: number;\n\n  taxable: boolean;\n\n  /**\n   * The title of the line item. The title is a combination of line_item.product.title and\n   * line_item.variant.title, separated by a hyphen.\n   *\n   * In most contexts, the line item title appears in the customer's preferred language. However, in\n   * the context of an order, the line item title appears in the language that the customer checked\n   * out in. The title can receive an override value from the Cart Transform API. Overrides take\n   * precedence over translations.\n   *\n   * #### Line item title history\n   *\n   * When referencing line item, product, and variant titles in the context of an order, the\n   * following changes might result in a different output than you expect:\n   * * A product or variant being deleted\n   * * A product or variant title being edited\n   *\n   * When line_item.title is referenced for an order, the line item title at the time of the order\n   * is returned. However, when line_item.product.title and line_item.variant.title are referenced,\n   * the current value for each title is returned.\n   */\n  title: string;\n\n  /**\n   * @deprecated https://shopify.dev/docs/api/liquid/objects/line_item\n   */\n  total_discount: number;\n\n  unit_price_measurement: {\n    measured_type: string;\n    quantity_unit: string;\n    quantity_value: string;\n    reference_unit: string;\n    reference_value: number;\n  };\n\n  untranslated_product_title: string;\n\n  untranslated_variant_title: string;\n\n  /**\n   * The relative URL of the variant associated with the line item.\n   */\n  url: string;\n\n  /**\n   * The ID of the line item's variant.\n   */\n  variant_id: number;\n\n  variant_options: string[];\n\n  variant_title: string;\n\n  /**\n   * The vendor of the variant associated with the line item.\n   */\n  vendor: string;\n}\n\nexport interface SellingPlanAllocation {\n  compare_at_price: number;\n\n  per_delivery_price: number;\n\n  /**\n   * @example 900\n   */\n  price: number;\n\n  price_adjustments: SellingPlanAllocationPriceAdjustment[];\n  selling_plan: SellingPlan;\n}\n\nexport interface SellingPlanAllocationPriceAdjustment {\n  /**\n   * @example 1\n   */\n  position: number;\n\n  /**\n   * @example 900\n   */\n  price: number;\n}\n\nexport interface SellingPlan {\n  description: unknown;\n\n  fixed_selling_plan: boolean;\n\n  id: number;\n  name: string;\n\n  options: SellingPlanOption[];\n\n  price_adjustments: SellingPlanPriceAdjustment[];\n\n  recurring_deliveries: boolean;\n}\n\nexport interface SellingPlanOption {\n  name: string;\n  position: number;\n  value: string;\n}\n\nexport interface SellingPlanPriceAdjustment {\n  order_count: unknown;\n  position: number;\n  value: number;\n  value_type: string;\n}\n\nexport interface Option {\n  name: string;\n  value: string;\n}\n\nexport interface QuantityRule {\n  increment: number;\n  max: number;\n  min: number;\n}\n\nexport interface DiscountCode {\n  /**\n   * Whether the discount is currently applicable. It is possible to apply a discount code to a cart\n   * but have it not yet be applicable. When not applicable, there will not be any associated\n   * discount applications or allocations. The moment someone performs an action that makes the\n   * discount code applicable, Shopify will update the property to true.\n   */\n  applicable: boolean;\n\n  /**\n   * The discount code string.\n   *\n   * This is NOT guaranteed to be uppercase. Discount codes entered at checkout are captured in\n   * whatever case was used during input.\n   *\n   * @example \"MONEY1234\"\n   * @example \"Abc123def\"\n   */\n  code: string;\n}\n\n/**\n * Returns the request id header from the ajax cart api response. Capturing this value is important\n * when helping Shopify support investigate issues.\n *\n * This does not error. If there is a problem, this returns null or undefined.\n */\nfunction getRequestId(response: Response) {\n  try {\n    return response.headers.get('X-Request-Id');\n  } catch (error) {\n    // ignore\n  }\n}\n\n/**\n * Returns whether the given value is shaped like a fetch Response.\n *\n * The return type of fetch is a contract, not a verified fact. A third party that overrides the\n * global fetch can resolve it to null, undefined, or some unrelated value, in which case the very\n * next property access throws an opaque error with no indication that a bad override was the cause.\n * This checks structurally for the members this module reads rather than with an instanceof, so a\n * conforming fetch polyfill that returns its own Response class still passes while a missing or\n * malformed value is trapped at the point it is received.\n *\n * This type check does not technically prove that the value is a genuine Response object, only that\n * it has the expected shape. We are intentionally lying with types here, so that we can leverage\n * the type inference to support the subset of use cases this module actually relies on. More over,\n * this lying is ok, as the call to `response = await fetch` is also lying because it overly trusts\n * that fetch returns value of type Response. We are going to keep trying to \"make fetch happen\"\n * regardless.\n *\n * ## War Story\n *\n * As much as it seems like this is an insane and irrationally paranoid check, you would be\n * surprised. For example, in the wild, we observed a third party vendor that hooked global fetch,\n * had an error in their hook, and simultaneously, another vendor's third party polyfill that only\n * defined fetch without also defining Response, leading to even more obscure cases when older\n * browsers were involved. Then, to make matters worse, a fix was implemented leveraging the clean\n * global fetch using an iframe content window hack to work around the inability to cleanly capture\n * the native fetch before it was erroneously hooked, and this broke certain expressions that use\n * instanceof because instanceof has unintuitive behavior when checking instanceof of a value from a\n * different global context, e.g. from an iframe. A web of lies!\n */\nfunction isResponse(value: any): value is Response {\n  if (value === null || typeof value !== 'object') {\n    return false;\n  }\n\n  if (typeof value.ok !== 'boolean') {\n    return false;\n  }\n\n  if (typeof value.status !== 'number') {\n    return false;\n  }\n\n  if (typeof value.text !== 'function') {\n    return false;\n  }\n\n  if (typeof value.json !== 'function') {\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * Attaches a single piece of context onto an error before it is thrown.\n *\n * The target may be a value caught upstream rather than one constructed here, and such a value can\n * carry a non-writable property, such as a DOMException whose code is a read-only accessor, or be\n * frozen outright, in which case assigning to it throws. Decorating an error must never itself\n * fail, so a rejected assignment is swallowed. This context is best effort metadata, so the\n * property is skipped when it cannot be set, leaving the error otherwise intact to be thrown and\n * handled by the caller.\n */\nfunction decorateError(error: any, key: string, value: unknown) {\n  try {\n    error[key] = value;\n  } catch (error) {\n    // The target rejects this property. Skip it rather than letting decoration fail.\n  }\n}\n\n/**\n * Returns an absolute url for the given path.\n *\n * It would be nice to just use a relative url in fetch. However, we see real world errors about\n * requests including credentials. Visitors might have buggy extensions inserting document.baseURI\n * or they are browsing the site in some weird way. To work around this, we build up a full url that\n * omits the credentials.\n *\n * Unfortunately, specifying the base url means we have to also solve the problem of determining it.\n * There is currently a desire to avoid location.origin due to browser support. we do not want to\n * touch the document base uri. Use host over hostname since that includes port.\n */\nfunction resolveUrl(path: string) {\n  return new URL(path, `${location.protocol}//${location.host}`);\n}\n\n/**\n * An item to add to the cart\n */\nexport interface AddItem {\n  /**\n   * Variant id\n   *\n   * Shopify says that it supports variant ids as numbers, but we have witnessed strange errors when\n   * using numbers so we impose an artificial limitation on the type here and require strings.\n   */\n  id: string;\n\n  /**\n   * The id of the line item to add the item as a child of. This is used for adding items as\n   * children of value sets and bundles. If not specified, the item is added as a top level line\n   * item.\n   *\n   * @see https://shopify.dev/docs/apps/build/product-merchandising/nested-cart-lines/create-nested-cart-lines#cart-ajax-api\n   */\n  parent_id?: number | string;\n\n  /**\n   * Line item properties to include.\n   *\n   * If the variant does not already exist in the cart, then the logic is straightforward. This will\n   * result in a new line item with these properties.\n   *\n   * If the variant exists in the cart, then the logic is convoluted. If the specified properties\n   * are identical to the existing properties, then the quantity of the existing line item\n   * increments. If the specified properties are different, then a new line item is created with\n   * these properties, resulting in the same variant being present in multiple line items.\n   */\n  properties?: Attributes;\n\n  /**\n   * The amount of the variant to add.\n   *\n   * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-add-js\n   */\n  quantity: number;\n\n  selling_plan?: number | string;\n}\n\n/**\n * The response body of a cart add.\n *\n * This one is not a cart at all, unlike the response body of a change or an update. It carries only\n * the items the request asked for, so there is no token, no totals, and no view of the rest of the\n * cart. See the notes on the add function for why the body is not worth reading.\n *\n * None of the functions here parse this body, because a caller that wants the resulting state\n * performs its own get cart afterward. So this type exists to describe what the endpoint answers\n * with rather than to annotate a return value.\n */\nexport interface CartAddResponseBody {\n  /**\n   * The line items the add produced, one per item in the request.\n   *\n   * These look like ordinary cart line items and are typed as such, though a few of the rarer line\n   * item properties have not been seen on one, so treat the shape as the usual line item rather\n   * than a guaranteed match.\n   */\n  items: Item[];\n}\n\n/**\n * Adds items to the cart. At least one item should be specified.\n *\n * This does not fetch the response body. The raw response body does not contain the full cart\n * state. The raw response body may not be the latest state of cart items. The raw response body may\n * contain zero-quantity items still present after prior removal.\n *\n * This validates the response status. If the status is not ok this throws an error.\n *\n * This specifies fetch mode no-store so that the call is not unintentionally cached.\n *\n * Adding an item the cart does not necessarily mean a new line item is created. For each item in\n * the input, Shopify makes a server side determination of whether to update the quantity of an\n * existing line item or add a new line item. The factors in that decision are related to how a line\n * item key is determined, which is poorly documented. Basically, if an item has some different\n * properties, e.g. line item properties, or pricing, then the item will always be added as a new\n * line item, even though the variant id is the same as an existing line item.\n *\n * The line item scripts run on every mutation. The scripts will remove any orphaned value set line\n * items implicitly. Keep this in mind when debugging.\n *\n * The content type header must be specified in the request or Shopify interprets the request as a\n * form submission, which we are not doing here, which leads to an error.\n *\n * Shopify's injected front end code (e.g. Trekkie, Monorail) triggers a console warning when it\n * detects that the number of items requested to be added is less than the number that was added.\n * This is not necessarily an error. It can indicate that the line item scripts removed one or more\n * line items during the add mutation.\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-add-js\n * @see https://help.shopify.com/en/manual/checkout-settings/script-editor/shopify-scripts\n */\nexport async function cartAdd(items: AddItem[]) {\n  const url = resolveUrl(Shopify.routes.root + 'cart/add.js');\n\n  const init: RequestInit = {\n    body: JSON.stringify({ items }),\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    cache: 'no-store',\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'items', items);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetch did not return a response adding to cart');\n    decorateError(error, 'items', items);\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} adding to cart`);\n    decorateError(error, 'items', items);\n    decorateError(error, 'request_id', getRequestId(response));\n    decorateError(error, 'response_status', response.status);\n\n    try {\n      const responseBody = await response.json();\n      decorateError(error, 'response_body', responseBody);\n    } catch (responseConsumeAndParseBodyError) {\n      // ignore\n    }\n\n    // For certain error responses, Shopify provides a clearer error message in the response body.\n    // On some 422 responses a more specific description is present alongside the generic message,\n    // so prefer the description, fall back to the generic message, and otherwise leave the default\n    // received-status message in place. For example, adding a bundle whose component is unavailable\n    // yields a body like the following, where description is meaningful and message is generic:\n    //\n    // { \"description\": \"The bundle product 'Product Title' cannot be added to the cart.\",\n    // \"message\": \"Cart Error\", \"status\": 422 }\n    //\n    // The status property of the response body mirrors the HTTP status code (empirical).\n\n    let message;\n    if (response.status === 422 && typeof error.response_body?.description === 'string') {\n      message = error.response_body.description;\n    } else if (typeof error.response_body?.message === 'string') {\n      message = error.response_body.message;\n    }\n\n    if (message) {\n      decorateError(error, 'message', message);\n    }\n\n    throw error;\n  }\n\n  return response;\n}\n\n/**\n * Either id or line must be specified.\n */\nexport interface CartChangeInput {\n  /**\n   * Can be either the variant id or the line item key.\n   */\n  id?: number | string;\n\n  /**\n   * A cart can have multiple line items that share the same variant_id. For example, when variants\n   * have different line item properties, or automatic discounts create variants at different\n   * prices. To account for this, you can use the line property when updating the cart.\n   *\n   * The value of line is the 1-based index position of the item in the cart.\n   */\n  line?: number;\n\n  /**\n   * The properties property sets the line item properties. Its value must be an object of key-value\n   * pairs.\n   *\n   * Whenever a POST request includes properties, it overwrites the entire properties object. Any\n   * key-value pairs that were already in the properties object are erased.\n   *\n   * It's not possible to set a line item's properties property to an empty object once a value is\n   * set. If you need to remove all line item properties, then you need to remove the entire line\n   * item.\n   *\n   * You can visually hide line item properties at checkout by creating private properties. This\n   * technique might be an alternative to removing a line item when managing properties.\n   */\n  properties?: Record<string, any>;\n\n  /**\n   * The value of the quantity property represents the new quantity you want for the line item. It\n   * must be an integer.\n   *\n   * You can remove a line item by setting the quantity to 0.\n   *\n   * It is not clear from the documentation, but based on the description of the selling plan\n   * property, it looks like quantity is actually optional and defaults to 1 when not specified.\n   */\n  quantity?: number;\n\n  /**\n   * The selling_plan property sets the line item selling plan. Its value must be one of the\n   * following:\n   *\n   * * The selling plan ID: To set a specific selling plan for the line item.\n   * * null or an empty string: To remove any selling plan from the line item.\n   *\n   * When specifying the selling_plan property, consider the following:\n   * * You can use only the line property for identifying the line item.\n   * * You should always specify the quantity property. If the quantity isn't specified, then it's\n   *   assumed to be 1.\n   */\n  selling_plan?: number;\n}\n\n/**\n * The response body of a cart change.\n *\n * The two extra arrays are not part of the stored cart. Neither one is present in a get cart\n * response, and a cart update answers with a different extra property instead, so these describe\n * what the change did rather than what the cart holds.\n *\n * None of the functions here parse this body, because a caller that wants the resulting state\n * performs its own get cart afterward. So this type exists to describe what the endpoint answers\n * with rather than to annotate a return value.\n */\nexport interface CartChangeResponseBody extends Cart {\n  /**\n   * The line items the change brought into the cart.\n   *\n   * Nothing has been observed to put an entry in here, including a change that raised a quantity,\n   * so whether an entry looks like a removed one is unconfirmed.\n   */\n  items_added: CartChangeItem[];\n\n  /**\n   * The line items the change took out of the cart.\n   *\n   * A removal driven from a cart drawer, which identifies the line by its position rather than by\n   * variant, does report the removed line here. A removal that identifies the line by variant id\n   * has come back empty, so what qualifies is not settled.\n   */\n  items_removed: CartChangeItem[];\n}\n\n/**\n * An entry in one of the two arrays a cart change response carries.\n *\n * This is undocumented and the shape here is what has been observed on a removal. It is close\n * enough to a line item to be mistaken for one and it is not one, so a few differences are worth\n * knowing. The id is not the variant id, the line key arrives under a different property name, and\n * the price is a decimal string rather than an integer in subunits. It also carries no line item\n * properties, no weight, no discount details, and no parent relationship.\n */\nexport interface CartChangeItem {\n  /**\n   * An opaque identifier for the entry, formatted as a uuid.\n   *\n   * This is not the variant id, unlike the id of a cart line item.\n   */\n  id: string;\n\n  image: string;\n\n  presentment_price: number;\n\n  /**\n   * The price as a decimal string, in the presentment currency.\n   *\n   * Note that this is not an integer in currency subunits, which is how a price is expressed\n   * everywhere else in the cart.\n   *\n   * @example \"289.00\"\n   */\n  price: string;\n\n  product_id: number;\n\n  /**\n   * The title of the product, which here includes the variant.\n   *\n   * Note that a cart line item excludes the variant from this property and only combines the two in\n   * its title.\n   *\n   * @example \"TurboStyle - Blush\"\n   */\n  product_title: string;\n\n  product_type: string;\n  quantity: number;\n  sku: string;\n  title: string;\n  untranslated_product_title: string;\n  untranslated_variant_title: string;\n  url: string;\n  variant_id: number;\n  variant_title: string;\n  vendor: string;\n\n  /**\n   * The line item key, which a cart line item exposes as its key instead.\n   *\n   * @example \"47267055599716:fa232aa1a0b5b250164c9843d7a2ee6c\"\n   */\n  view_key: string;\n}\n\n/**\n * Use the /{locale}/cart/change.js endpoint to change the quantity, properties, and selling_plan\n * properties of a cart line item. Only items already in your cart can be changed, and you can\n * change only one line item at a time.\n *\n * The POST data requires either an id or line property to identify the line item to be changed.\n *\n * The value of the id property can be one of the following:\n *\n * * The line item's variant_id\n * * The line item's key\n *\n * A cart can have multiple line items that share the same variant_id. For example, when variants\n * have different line item properties, or automatic discounts create variants at different prices.\n * Because of this, it's recommended to use the line item key to ensure that you're only changing\n * the intended line item.\n *\n * If the item that you're attempting to change isn't already in the cart, then\n * /{locale}/cart/change.js doesn't add it. Instead, it returns a 404 error.\n *\n * The documentation states that the response body is the JSON of the cart. This exposes the\n * response object but does not fetch the response body.\n *\n * ### Removing out of stock items\n * It is unclear but based on a conversation with Shopify support, prefer to use cart/change instead\n * of cart/update to avoid issues with removing an out of stock item from the cart. Using update\n * results in a 422 error but supposedly cart/change does not.\n */\nexport async function cartChange(input: CartChangeInput) {\n  const url = resolveUrl(Shopify.routes.root + 'cart/change.js');\n\n  const init: RequestInit = {\n    body: JSON.stringify(input),\n    cache: 'no-store',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'input', input);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetch did not return a response changing cart');\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} changing cart`);\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'request_id', getRequestId(response));\n\n    try {\n      decorateError(error, 'response_body', await response.json());\n    } catch (consumeError) {\n      // ignore\n    }\n\n    decorateError(error, 'response_status', response.status);\n    decorateError(error, 'response_url', response.url);\n\n    // For certain error responses, Shopify provides a clearer error message in the response body.\n    // On some 422 responses a more specific description is present alongside the generic message,\n    // so prefer the description, fall back to the generic message, and otherwise leave the default\n    // received-status message in place. Shopify's generic message on these is literally \"Cart\n    // Error\", so promoting it over the description would discard the only meaningful text.\n    //\n    // The status property of the response body mirrors the HTTP status code (empirical).\n\n    let message;\n    if (response.status === 422 && typeof error.response_body?.description === 'string') {\n      message = error.response_body.description;\n    } else if (typeof error.response_body?.message === 'string') {\n      message = error.response_body.message;\n    }\n\n    if (message) {\n      decorateError(error, 'message', message);\n    }\n\n    throw error;\n  }\n\n  return response;\n}\n\n/**\n * Use the GET /{locale}/cart.js endpoint to get the cart as JSON.\n *\n * All monetary properties are returned in the customer's presentment currency. To check the\n * customer's presentment currency, you can use the currency field in the response.\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#get-locale-cart-js\n */\nexport async function cartFetch() {\n  const url = resolveUrl(Shopify.routes.root + 'cart.js');\n\n  let response;\n  try {\n    response = await fetch(url, { cache: 'no-store' });\n  } catch (error) {\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetching cart did not return a valid response');\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} fetching cart`);\n    decorateError(error, 'request_id', getRequestId(response));\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n\n    try {\n      decorateError(error, 'response_body', await response.json());\n    } catch (consumeError) {\n      // ignore\n    }\n\n    throw error;\n  }\n\n  // Using response.json() obscures errors and prevents us from being able to see what the raw\n  // response body looks like. The usual cases of errant response bodies include:\n  //\n  // - default response bodies for errors, which are using plain text or html\n  // - firewall interceptions that produce html as a response, usually html\n  // - wifi connect interstitial page load, usually html\n  //\n  // So we are intentionally using response.text() and JSON.parse.\n\n  let responseBodyText;\n  try {\n    responseBodyText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let responseBodyObject;\n  try {\n    responseBodyObject = JSON.parse(responseBodyText);\n  } catch (error) {\n    decorateError(error, 'response_body', responseBodyText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isCart(responseBodyObject)) {\n    const error: any = new Error('Unexpected get cart response body');\n    decorateError(error, 'response_body', responseBodyObject);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  return responseBodyObject;\n}\n\nexport interface CartUpdateInput {\n  /**\n   * New cart attributes. Existing cart attributes are overwritten.\n   */\n  attributes?: Attributes;\n\n  /**\n   * Beta shopify feature. Essentially works like Storefront discount apply mutation.\n   *\n   * Pass an empty string to remove discount(s).\n   */\n  discount?: string;\n\n  /**\n   * Cart note.\n   */\n  note?: string;\n\n  /**\n   * Even though Shopify supports specifying variant ids as numbers, we impose the restriction that\n   * the property names of the updates object must be strings due to some strange looking errors\n   * when specifying numbers.\n   */\n  updates?: Record<string, any>;\n}\n\n/**\n * The response body of a cart update.\n *\n * The changelog is not part of the stored cart. It is absent from a get cart response, and a cart\n * change answers with two differently named arrays instead, so it describes what the update did\n * rather than what the cart holds.\n *\n * None of the functions here parse this body, because a caller that wants the resulting state\n * performs its own get cart afterward. So this type exists to describe what the endpoint answers\n * with rather than to annotate a return value.\n */\nexport interface CartUpdateResponseBody extends Cart {\n  /**\n   * A summary of what the update brought into the cart.\n   *\n   * This is present on every successful update, including one that only writes attributes or a\n   * note, in which case the arrays inside it are empty.\n   */\n  items_changelog: ItemsChangelog;\n}\n\n/**\n * The summary of what an update brought into the cart.\n *\n * This is undocumented, so the shape here is only what has been observed. The added array is the\n * only property seen so far. A removed counterpart would be the obvious sibling but no update\n * response has produced one, not even one that set a quantity to zero, so it is left out rather\n * than guessed at.\n */\nexport interface ItemsChangelog {\n  /**\n   * The variants the update brought into the cart, and the quantity each one landed at.\n   *\n   * Only a variant that was not in the cart beforehand shows up here. Raising or lowering the\n   * quantity of a line that already exists leaves this empty, as does removing one, so this reports\n   * newly created lines rather than every quantity change.\n   */\n  added: ItemsChangelogAddition[];\n}\n\n/**\n * An entry in the added array of an update's changelog.\n *\n * This is not a line item. It carries only the variant and the quantity, so it cannot stand in for\n * the corresponding entry in the cart's items array.\n */\nexport interface ItemsChangelogAddition {\n  /**\n   * The quantity the line item landed at, not the amount by which it changed.\n   */\n  quantity: number;\n\n  /**\n   * Note that this is a number here, unlike elsewhere in this module where a variant id is\n   * restricted to a string on the request side.\n   */\n  variant_id: number;\n}\n\n/**\n * Performs a cart update.\n *\n * This does not return the cart updated state. You will need to do a followup call to fetch the\n * updated state of the cart. One reason this does also return the updated state is that we must\n * perform a second call to fetch the updated state but not all callers are interested in doing so.\n *\n * This does not return the response body of the call. There is no need to fetch the body because\n * the body does not contain useful data. The response body can contain 0 quantity items and other\n * kinds of strange things.\n *\n * This must specify the content type header. Shopify supports both a JSON request body and a form\n * data request body. Shopify defaults to interpreting requests as form submissions. This uses the\n * JSON format so the header is required.\n *\n * Backend scripts may affect the output.\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-update-js\n * @see https://shopify.dev/docs/api/ajax/reference/cart#update-line-item-quantities\n * @see https://shopify.dev/docs/api/ajax/reference/cart#private-cart-attributes\n */\nexport async function cartUpdate(input: CartUpdateInput) {\n  const url = resolveUrl(Shopify.routes.root + 'cart/update.js');\n\n  const init: RequestInit = {\n    body: JSON.stringify(input),\n    cache: 'no-store',\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'input', input);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!isResponse(response)) {\n    const error: any = new Error('fetch did not return a response updating cart');\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'response', response);\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} updating cart`);\n    decorateError(error, 'request_body', input);\n    decorateError(error, 'request_id', getRequestId(response));\n    decorateError(error, 'response_status', response.status);\n\n    try {\n      const parsedErrorResponseBody = await response.json();\n      decorateError(error, 'response_body', parsedErrorResponseBody);\n    } catch (consumeError) {\n      // ignore\n    }\n\n    // For certain error responses, Shopify provides a clearer error message in the response body.\n    // On some 422 responses a more specific description is present alongside the generic message,\n    // so prefer the description, fall back to the generic message, and otherwise leave the default\n    // received-status message in place. Shopify's generic message on these is literally \"Cart\n    // Error\", so promoting it over the description would discard the only meaningful text.\n    //\n    // The status property of the response body mirrors the HTTP status code (empirical).\n\n    let message;\n    if (response.status === 422 && typeof error.response_body?.description === 'string') {\n      message = error.response_body.description;\n    } else if (typeof error.response_body?.message === 'string') {\n      message = error.response_body.message;\n    }\n\n    if (message) {\n      decorateError(error, 'message', message);\n    }\n\n    throw error;\n  }\n\n  return response;\n}\n\n/**\n * Returns whether the error indicates the add failed because one of the products is out of stock\n * (not available for sale). In this case Shopify provides an error response body that looks like\n * this:\n *\n * ```json\n * {\n *   \"description\": \"All 0 <product title> are in your cart\",\n *   \"message\": \"Cart Error\",\n *   \"status\": 422\n * }\n * ```\n *\n * ```json\n * {\n *   \"description\": \"The product '<title>' is already sold out.\",\n *   \"message\": \"Cart Error\",\n *   \"status\": 422\n * }\n * ```\n *\n * This error can happen both when adding an item or updating the quantity of an item.\n *\n * This assumes error messages are in English.\n */\nexport function isUnavailableError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      // this one is only present in the add error response\n      if (description.startsWith('All 0') && description.includes('in your cart')) {\n        return true;\n      }\n\n      // this string is sometimes present in the update error response, not just the add response\n      if (description.includes('already sold out')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the error indicates the add failed because the shopper already holds the item's\n * per-order purchase limit in their cart. Shopify provides an error response body that looks like\n * this:\n *\n * ```json\n * {\n *   \"description\": \"The maximum quantity of this item is already in your cart.\",\n *   \"message\": \"The maximum quantity of this item is already in your cart.\",\n *   \"status\": 422\n * }\n * ```\n *\n * This assumes error messages are in English.\n */\nexport function isMaxQuantityError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      const normalizedDescription = description.toLowerCase();\n      if (normalizedDescription.includes('maximum quantity of this item is already in your cart')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the error indicates the add failed because the request mixed a bundle product and\n * an add-on in the same `cart/add.js` call, which Shopify does not allow. Shopify provides an error\n * response body that looks like this:\n *\n * ```json\n * {\n *   \"description\": \"Bundle products and add-ons cannot be mixed\",\n *   \"message\": \"Bundle products and add-ons cannot be mixed\",\n *   \"status\": 422\n * }\n * ```\n *\n * This assumes error messages are in English.\n */\nexport function isBundleAddonMixError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      const normalizedDescription = description.toLowerCase();\n      if (normalizedDescription.includes('bundle products and add-ons cannot be mixed')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the error indicates the add failed because the selling plan sent with the item is\n * not one Shopify will accept for that variant. This happens when a rendered page carries a selling\n * plan ID (for example a pre-order deferred-payment plan) that has since become stale: the variant\n * came back in stock, or the plan was edited or removed, after the page was rendered but before the\n * shopper clicked add. Shopify responds with a body that looks like this:\n *\n * ```json\n * {\n *   \"description\": \"Cannot apply selling plan to variant\",\n *   \"message\": \"Cart Error\",\n *   \"status\": 422\n * }\n * ```\n *\n * This assumes error messages are in English.\n */\nexport function isInvalidSellingPlanError(error: any) {\n  if (error instanceof Error && 'response_body' in error && isObject(error.response_body) &&\n    error.response_body.status === 422) {\n    const description = error.response_body.description;\n    if (typeof description === 'string') {\n      if (description.includes('Cannot apply selling plan to variant')) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n\n/**\n * Returns whether the given value looks like a cart object.\n *\n * This is not a complete validation. This is a shallow check that examines whether the value is an\n * object containing an items property that is an array. Any value that meets that criteria\n * satisfies the meaning of a cart.\n */\nexport function isCart(value: any): value is Cart {\n  return isObject(value) && Array.isArray(value.items);\n}\n\nfunction isObject(value: any): value is Record<string, any> {\n  return typeof value === 'object' && value !== null;\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Returns a general object consisting of only those properties from the cart that might be useful\n * during triage. This avoids the lazy capture of the full cart, which is often full of lots of data\n * irrelevant to analysis, and often results in truncation that obscures the values that are very\n * important to analysis.\n *\n * The output type here is intentionally vaguely typed so as to not bake in any future guarantees of\n * property names. This output is not meant for library usage. It is only meant for relaying to\n * other downstream systems that operate on unknown bags of data.\n *\n * This is written defensively to never throw so that it is safe to call from critical paths.\n */\nexport function digest(cart: Partial<Cart>) {\n  if (cart === null) {\n    const output: Record<string, any> = { cart_input_type: 'null' };\n    return output;\n  }\n\n  if (typeof cart !== 'object') {\n    const output: Record<string, any> = { cart_input_type: typeof cart };\n    return output;\n  }\n\n  try {\n    const output: Record<string, any> = {\n      token: cart.token,\n      attribute_keys: Object.keys(cart.attributes ?? {}),\n      discount_codes: cart.discount_codes,\n      cart_level_discount_titles: cart.cart_level_discount_applications?.map(\n        application => application?.title),\n      items: cart.items?.map(item => ({\n        key: item?.key,\n        variant_id: item?.variant_id,\n        quantity: item?.quantity,\n        properties: item?.properties,\n        discounts: item?.line_level_discount_allocations?.map(\n          allocation => allocation?.discount_application?.title)\n      }))\n    };\n    return output;\n  } catch (error) {\n    return { digest_failed: `${error}` };\n  }\n}\n","import { type Cart, isCart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Returns whether two carts are equal. This is an approximate check that only considers line items.\n *\n * This does not consider line item properties.\n *\n * This does not consider cart attributes.\n *\n * This does not consider discount applications and allocations.\n *\n * This function exists separate from a full cart equality check because we need a way to consider\n * only certain state changes as meaningful. One of the reasons this is needed is because we have\n * little to no control over third party changes to cart state. There are apps that inject code that\n * does its own cart modifications, e.g. adjusting the cart's attributes on page load, and short of\n * uninstalling them we cannot prevent this. There are even mobile apps that inject content scripts\n * that assume control over the cart state. The decision making to install apps does not always\n * appreciate the complexity of the state comparison case. Given that, we need a way to treat only\n * certain state changes as meaningful, which means being deliberate about which parts of the api\n * contract we honor. This function therefore only considers line items and ignores other state\n * changes because it is intentionally only adhering to one clause of the Shopify cart contract.\n *\n * This is complicated because Shopify's cart mutations generally do not return the updated cart\n * state. They often return junk. The only way to reliably determine the updated cart state is to\n * fetch it separately, which always involves fetching the entire state. Which unfortunately is\n * often the first time that we learn that something else has modified the cart. We have to\n * reconcile the difference between the edits we issued and the actual updated cart state,\n * accounting for unexpected changes. In other words, we are comparing projections of the state\n * rather than the actual state. Since the endpoints do not support projection (e.g. give me only\n * these fields), we do this by selectively comparing only certain cart properties.\n */\nexport function hasSimilarItems(cart1: Cart, cart2: Cart) {\n  // Sanity check the inputs. This should never happen. However, it does allow the caller to be less\n  // careful with guaranteeing null values. Also, we have seen runtime state diverge from statically\n  // typed state in rare cases of bugs and unexpected third party interference, so even though this\n  // is redundant type checking at compile time, it is not redundant at runtime.\n\n  if (!isCart(cart1)) {\n    return false;\n  }\n\n  if (!isCart(cart2)) {\n    return false;\n  }\n\n  // Seeing a new token is unlikely to ever happen and is more of a sanity check. One plausible\n  // situation is cart expiration resulting in cart replacement.\n\n  // We have seen cases where we have partial cart state and the token is missing, so this only does\n  // token comparison when both tokens are known, to still allow for other kinds of cart state\n  // comparison when either is unknown.\n\n  if (cart1.token && cart2.token && cart1.token !== cart2.token) {\n    return false;\n  }\n\n  // The same number of line items should be present in both carts.\n\n  if (cart1.items.length !== cart2.items.length) {\n    return false;\n  }\n\n  // The lines should have the same keys with the same quantities. If any one line does not, break.\n  // Because there are the same number of line items in both carts, finding that every item from\n  // cart1 exists in cart2 implies that every item from cart2 exists in cart1, so we only need to\n  // examine state in one direction.\n\n  // Watch out for careless type inconsistencies, e.g. key as a number on input but key as a string\n  // on output. It is best to only compare output types because those are consistent. The cart api\n  // unfortunately has some Postel's Law like behavior. Avoid comparing a manually constructed cart\n  // to a fetched cart.\n\n  for (const item1 of cart1.items) {\n    let found = false;\n    for (const item2 of cart2.items) {\n      if (item1.key === item2.key && item1.quantity === item2.quantity) {\n        found = true;\n        break;\n      }\n    }\n\n    if (!found) {\n      return false;\n    }\n  }\n\n  return true;\n}\n","/**\n * Copies a single piece of supplementary context onto an error before it is thrown or reported.\n *\n * Use this when decorating a value the calling code did not construct, such as a caught value or an\n * error handed in by another module. An error the caller just built with new is always writable, so\n * plain assignment is fine there. A value from elsewhere may be a foreign object this code does not\n * own. Such a value can carry a non-writable property, such as a DOMException whose code is a read-\n * only accessor, or be frozen outright, and assigning to it then throws. Attaching context must\n * never itself fail, so a rejected assignment is swallowed and the remaining context is still\n * attempted. Guarding each property in isolation means a single read-only slot, the common case,\n * costs only that one property rather than all of them.\n *\n * In practice the value that triggers this is almost always a DOMException that reached the caller\n * from a fetch the browser cut short, such as an abort when an in-flight request is interrupted.\n * The platform defines a DOMException's code, name, and message as read-only attributes, so\n * assigning a code onto one throws under the strict mode that all bundled module code runs in. It\n * surfaces from Safari far more than elsewhere for two reasons. The reported wording, attempted to\n * assign to readonly property, is JavaScriptCore's phrasing for this failure, so the same bug in\n * another engine is worded differently and groups as a separate report, which makes this one look\n * Safari specific even though the read-only rule is not. And iOS Safari abandons in-flight fetches\n * aggressively when a tab is backgrounded or a connection drops, which is when such a DOMException\n * tends to be produced and handed to code that assumed an ordinary writable error.\n *\n * A swallowed failure is logged at a level low enough that it is recorded as a breadcrumb for\n * monitoring rather than escalated into its own captured event. That keeps the lost context visible\n * alongside the eventual report without adding fresh noise of its own. The breadcrumb names the\n * property that could not be attached, the kind of value that rejected it, and why, which is enough\n * to recognize a read-only or frozen caught value after the fact.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Read-only\n * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMException\n * @see https://webidl.spec.whatwg.org/#dfn-read-only\n */\nexport function decorateError(error: any, key: string, value: unknown) {\n  try {\n    error[key] = value;\n  } catch (assignmentError) {\n    let reason: string;\n    if (assignmentError instanceof Error) {\n      reason = assignmentError.message;\n    } else {\n      reason = `${assignmentError}`;\n    }\n\n    console.log('Failed to assign error property', {\n      key,\n      name: error?.name,\n      tag: Object.prototype.toString.call(error),\n      reason\n    });\n  }\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { decorateError } from '../decorate-error.js';\nimport type { Transaction } from './transaction.js';\n\ntype CartUpdateErredEvent = CustomEvent<{\n  /**\n   * Cart state.\n   *\n   * This may be undefined or null or invalid because almost nothing is guaranteed.\n   */\n  cart?: Cart;\n\n  /**\n   * An error code that describes the context of the error or the type of the error.\n   */\n  code: string;\n\n  /**\n   * The error that occurred. This will almost always be of type `Error`. However, this is set to\n   * the value of whatever error was caught. Values not of type `Error` can be thrown. We are not\n   * checking that the value is an instance of an `Error` first. It is not safe to assume this is of\n   * type `Error` particularly because of code minimization and third party modifications.\n   */\n  error: any;\n\n  /**\n   * A mix of misc properties that various cart transactions will include that help provide more\n   * information about the inputs to the code that led to the error.\n   */\n  inputs?: Record<string, any>;\n\n  /**\n   * If true, this means the event was emitted by a multi-fetch operation where some of the fetches\n   * succeeded. For example, consider the addItems call, which has to do two fetches in its virtual\n   * transaction. This will be true when the fetch to add the item succeeded but the fetch to get\n   * the new cart state after the add failed. This flag exists so that callers can choose to react\n   * to this situation differently, e.g. by retrying just the cart state fetch, or changing what\n   * error message is displayed (a failure to add versus a partially successful add).\n   */\n  partial?: boolean;\n\n  /**\n   * An optional programmatic code or human-readable description of the context in which some cart\n   * update was taken. This is primarily for analytics, but can also be used for ui logic.\n   *\n   * For example:\n   * * distinctly identify two different add to cart buttons\n   * * distinctly identify a particular ui element used to apply a discount\n   */\n  scenario?: string;\n\n  /**\n   * The transaction in which the error occurred.\n   */\n  transaction: Transaction;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'cart-update-erred': CartUpdateErredEvent;\n  }\n}\n\n/**\n * Broadcasts a cart failure and, when it is a genuine programmer error, surfaces it to the global\n * error handler so that monitoring records it.\n *\n * The cart helpers deliberately catch their own failures so that the code awaiting them is not\n * forced to handle a promise rejection. Every failure is broadcast as an event for the interface to\n * react to. That catch keeps the promise from rejecting, but for an unexpected failure it does not\n * mean the problem was actually dealt with. It is still a real error that would have reached the\n * top level had it never been caught. The escalation step re-throws those so that they do, leaving\n * error monitoring to record whatever uncaught errors arrive with no knowledge of the cart.\n */\nexport function report(dispatcher: EventTarget | null, detail: CartUpdateErredEvent['detail']) {\n  dispatchCartUpdateErred(dispatcher, detail);\n  escalateCartError(detail);\n}\n\n/**\n * Dispatches the failure as an event for the interface to react to.\n *\n * The dispatcher is the target the event is dispatched from. It is required but may be null, and it\n * may be any event target, not necessarily an element or window. The target is used as given, with\n * two exceptions that exist only to keep the event reaching window level listeners: a null target\n * dispatches from window, and so does a detached element.\n *\n * A detached element is swapped for window because of how bubbling works. We want to guarantee the\n * event bubbles up to window so that window level listeners are notified. A detached element only\n * bubbles up to its detached subtree root, which is not window, so dispatching from it directly\n * would not reach those listeners.\n */\nfunction dispatchCartUpdateErred(dispatcher: EventTarget | null,\n  detail: CartUpdateErredEvent['detail']) {\n  // Dispatch the failure from whatever target the caller designates; this forms no opinion about\n  // what kind of target it is. The only special handling exists to keep the event reaching the\n  // global listeners: a detached element has no live ancestor chain to bubble through, so it is\n  // swapped for the global scope, and a null target means the global scope outright. Any other\n  // target, including a connected element, is used as given. Callers should still prefer the\n  // element most directly responsible for the failure, so a connected element carries truthful\n  // provenance and bubbles, crossing shadow boundaries, up to the global listeners.\n\n  let target: EventTarget;\n  if (dispatcher instanceof Element) {\n    if (dispatcher.isConnected) {\n      target = dispatcher;\n    } else {\n      target = window;\n    }\n  } else if (dispatcher) {\n    target = dispatcher;\n  } else {\n    target = window;\n  }\n\n  const event = new CustomEvent<typeof detail>('cart-update-erred', {\n    detail,\n    bubbles: true,\n    composed: true\n  });\n\n  target.dispatchEvent(event);\n}\n\n/**\n * Surfaces a genuine programmer error to the global error handler so that monitoring records it.\n *\n * Some failures are expected outcomes with their own user facing messaging rather than programmer\n * errors: a shopper adding an out of stock item, or mistyping a discount code. Those are never\n * escalated to monitoring. Each such failure is identified by its code, the same classification the\n * cart helpers attach for the interface, so the list here must keep pace with the codes those\n * helpers produce.\n *\n * The throw is deferred to a later task so that it escapes the surrounding promise chain instead of\n * rejecting it, preserving the rejection-free contract of the cart helpers. The error keeps the\n * stack captured when it was first created, so the report still points at the origin of the failure\n * rather than at this deferred throw. However, beware that this does mean that the error event\n * properties will reflect this throw context, not the original error context, which means that the\n * instrumenting code must prioritize the original error properties over the error event properties.\n *\n * When available, the throw is routed through the reportError api, which surfaces the value to the\n * global error handler without the extra task hop. Older engines that lack it fall back to a\n * deferred throw, which reaches the same handler.\n *\n * The context describing the failure is attached to the error so that monitoring which serializes\n * the non-standard properties of an error can include it. A value that is not an error is wrapped\n * so that the context still has somewhere to live.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/reportError\n */\nfunction escalateCartError(detail: CartUpdateErredEvent['detail']) {\n  // These are expected outcomes with their own user facing messaging rather than programmer errors.\n  //\n  // This list of codes to ignore deliberately excludes invalid selling plan codes such as\n  // ADD_ITEMS_INVALID_SELLING_PLAN, ADD_ITEMS_INVALID_BACKORDER_SELLING_PLAN, and\n  // ADD_ITEMS_INVALID_TBYB_SELLING_PLAN. Unlike the ephemeral shopper conditions, a rejected\n  // selling plan is a plan we attached to the item, so it signals that something is misconfigured\n  // rather than a transient outcome. We want these to continue reaching error diagnostics so that\n  // misconfiguration is visible.\n\n  const ignoredCodes = [\n    'APPLY_DISCOUNTS',\n    'ADD_ITEMS_OOS',\n    'ADD_ITEMS_BUNDLE_ADDON_MIX',\n    'ADD_ITEMS_MAX_QUANTITY',\n    'CHANGE_CART_OOS',\n    'INIT_TIMEOUT',\n    'UPDATE_CART_OOS'\n  ];\n\n  if (detail && !ignoredCodes.includes(detail.code)) {\n    let error: any;\n\n    if (detail.error instanceof Error) {\n      error = detail.error;\n    } else {\n      error = new Error(`${detail.error}`);\n    }\n\n    decorateError(error, 'code', detail.code);\n    decorateError(error, 'inputs', detail.inputs);\n    decorateError(error, 'scenario', detail.scenario);\n    decorateError(error, 'cart', detail.cart);\n    decorateError(error, 'transaction_id', detail.transaction?.id);\n    decorateError(error, 'transaction_name', detail.transaction?.name);\n\n    if (typeof reportError === 'function') {\n      reportError(error);\n    } else {\n      setTimeout(raise, 0, error);\n    }\n  }\n}\n\nfunction raise(value: unknown) {\n  throw value;\n}\n","/**\n * Throws an assertion error if the given condition is falsy.\n *\n * @param expression any expression, although one that evaluates to true or false is preferred by\n * convention\n * @param message optional error text\n */\nexport function assert(expression: unknown, message?: string) {\n  if (!expression) {\n    throw new AssertionError(message);\n  }\n}\n\nexport class AssertionError extends Error {\n  constructor(message = 'Assertion error') {\n    super(message);\n    this.name = this.constructor.name;\n  }\n}\n","const rnds8 = new Uint8Array(16);\nexport default function rng() {\n    return crypto.getRandomValues(rnds8);\n}\n","import validate from './validate.js';\nconst byteToHex = [];\nfor (let i = 0; i < 256; ++i) {\n    byteToHex.push((i + 0x100).toString(16).slice(1));\n}\nexport function unsafeStringify(arr, offset = 0) {\n    return (byteToHex[arr[offset + 0]] +\n        byteToHex[arr[offset + 1]] +\n        byteToHex[arr[offset + 2]] +\n        byteToHex[arr[offset + 3]] +\n        '-' +\n        byteToHex[arr[offset + 4]] +\n        byteToHex[arr[offset + 5]] +\n        '-' +\n        byteToHex[arr[offset + 6]] +\n        byteToHex[arr[offset + 7]] +\n        '-' +\n        byteToHex[arr[offset + 8]] +\n        byteToHex[arr[offset + 9]] +\n        '-' +\n        byteToHex[arr[offset + 10]] +\n        byteToHex[arr[offset + 11]] +\n        byteToHex[arr[offset + 12]] +\n        byteToHex[arr[offset + 13]] +\n        byteToHex[arr[offset + 14]] +\n        byteToHex[arr[offset + 15]]).toLowerCase();\n}\nfunction stringify(arr, offset = 0) {\n    const uuid = unsafeStringify(arr, offset);\n    if (!validate(uuid)) {\n        throw TypeError('Stringified UUID is invalid');\n    }\n    return uuid;\n}\nexport default stringify;\n","import rng from './rng.js';\nimport { unsafeStringify } from './stringify.js';\nfunction v4(options, buf, offset) {\n    if (!buf && !options && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return _v4(options, buf, offset);\n}\nfunction _v4(options, buf, offset) {\n    options = options || {};\n    const rnds = options.random ?? options.rng?.() ?? rng();\n    if (rnds.length < 16) {\n        throw new Error('Random bytes length must be >= 16');\n    }\n    rnds[6] = (rnds[6] & 0x0f) | 0x40;\n    rnds[8] = (rnds[8] & 0x3f) | 0x80;\n    if (buf) {\n        offset = offset || 0;\n        if (offset < 0 || offset + 16 > buf.length) {\n            throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);\n        }\n        for (let i = 0; i < 16; ++i) {\n            buf[offset + i] = rnds[i];\n        }\n        return buf;\n    }\n    return unsafeStringify(rnds);\n}\nexport default v4;\n","/**\n * Returns a deep clone of the given value.\n *\n * Prefers structuredClone and falls back to a JSON round trip. The fallback covers both an\n * environment without structuredClone and one whose structuredClone cannot be trusted. On the\n * storefront a compromised browser can leave a monkey-patched structuredClone that returns\n * undefined for a real object rather than cloning it, which otherwise surfaces as confusing\n * property-access failures far from the cause. The values cloned through here are plain data that\n * round trips faithfully through JSON, so the fallback is equivalent rather than a degradation.\n */\nexport function copy<T>(value: T) {\n  if (typeof structuredClone === 'function') {\n    const cloned = structuredClone(value);\n\n    // A healthy structuredClone returns a nullish result only for a nullish input, so a nullish\n    // clone of a non-nullish value means the primitive cannot be trusted. Fall through to the\n    // serialization clone in that case.\n\n    const inputPresent = value !== null && value !== undefined;\n    const cloneMissing = cloned === null || cloned === undefined;\n\n    let trustworthy = true;\n    if (inputPresent && cloneMissing) {\n      trustworthy = false;\n    }\n\n    if (trustworthy) {\n      return cloned;\n    }\n  }\n\n  return <T>JSON.parse(JSON.stringify(value));\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { containsJavascriptWord } from '../../untrusted-value.js';\n\n/**\n * Returns whether a discount code string is well-formed. This does not guarantee the discount code\n * is valid and in the applicable state. That is why this function is not named isValid.\n *\n * This tolerates any kind of input so as to provide a convenient API. The caller is not obligated\n * to check that the input is a string beforehand. This function exists to avoid that boilerplate.\n * However, callers should be providing values that are strings.\n */\nexport function isWellFormed(value: any) {\n  if (typeof value !== 'string') {\n    return false;\n  }\n\n  if (value.length === 0) {\n    return false;\n  }\n\n  // This catches abuse of path parameters\n  if (value.includes('/') || value.includes('\\\\')) {\n    return false;\n  }\n\n  if (value.includes(' ')) {\n    return false;\n  }\n\n  const upper = value.toUpperCase();\n\n  // This catches common js bugs in the calling code\n  if (upper === 'UNDEFINED' || upper === 'NULL') {\n    return false;\n  }\n\n  if (containsJavascriptWord(upper)) {\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * Returns normalized array of discount code strings currently applied to the cart.\n *\n * This filters out codes from the cart's discount_codes array property where the applicable value\n * is not true. That is what distinguishes this function from {@link getDiscountCodes}.\n *\n * This normalizes the codes. Shopify does not guarantee the code strings are uppercase. This will\n * uppercase.\n *\n * Tolerates null/undefined cart. Returns an empty array. This is safe to use without checking\n * whether cart is defined.\n *\n * Tolerates invalid discount_codes property. This supports a rare case where the cart was SSRed\n * correctly, but then not fetched. The SSR state is missing the property. In this case an empty\n * array is returned.\n */\nexport function getApplicableDiscountCodes(cart: Cart) {\n  if (!cart) {\n    return [];\n  }\n\n  if (!Array.isArray(cart.discount_codes)) {\n    return [];\n  }\n\n  return cart.discount_codes\n    .filter(code => code.applicable)\n    .map(code => code.code)\n    .map(code => code.toUpperCase());\n}\n\n/**\n * Returns all codes currently in the cart's discount codes array.\n *\n * This includes applicable as well as inapplicable codes. This inclusion of inapplicable codes is\n * what distinguishes this from the function {@link getApplicableDiscountCodes}.\n *\n * The returned codes are always uppercase. This must uppercase the codes returned because Shopify\n * does not guarantee the codes are upper case. Codes applied at checkout are saved in whatever\n * input case was used. The logic that applies codes while shopping can guarantee the input codes\n * are in uppercase but the logic at checkout cannot because the checkout logic is managed by\n * Shopify.\n *\n * It is possible that the discount_codes array property in the input cart is undefined. Shopify\n * does not support rendering the discount_codes array in liquid. There is a situation where the\n * cart state was server side rendered without discount_codes and then we failed to update the state\n * and therefore failed to set the property dynamically during init. This tolerates that so as to\n * guarantee that an array is always returned.\n */\nexport function getDiscountCodes(cart: Cart) {\n  if (!cart) {\n    return [];\n  }\n\n  if (!Array.isArray(cart.discount_codes)) {\n    return [];\n  }\n\n  return cart.discount_codes.map(code => code.code).map(code => code.toUpperCase());\n}\n\n/**\n * Totals up the discount amounts the cart has allocated and returns them as a map. Values are in\n * cents.\n *\n * The keys are discount application titles in uppercase rather than discount codes. For an ordinary\n * code those are the same string, which is what lets a caller match this map against the cart's\n * discount codes. They are not guaranteed to agree though, because Shopify treats the title as the\n * customer facing name of the discount rather than as the code. So a caller can end up holding a\n * title here that matches none of the codes on the cart.\n *\n * A code missing from this map has not necessarily done nothing. It means the cart allocated no\n * amount to it. A free shipping code behaves that way. It sits among the cart's discount codes as\n * applicable while appearing in neither array read here, because shipping is not priced until\n * checkout.\n *\n * Tolerates a null or undefined cart, and a cart missing any of the arrays this reads. Returns an\n * empty map. This is safe to use without checking whether the cart is defined.\n */\nexport function getDiscountAmounts(cart: Cart) {\n  // Shopify keeps discounts in two separate places, split by scope, so we read both to find all of\n  // them. A discount covering the cart as a whole lands in cart_level_discount_applications. A\n  // discount covering particular products lands on each line item holding one of those products, in\n  // that item's line_level_discount_allocations.\n  //\n  // The two do not overlap, which is what makes it safe to add both into one map without counting a\n  // discount twice. We checked this in real carts from both directions. A cart wide code showed up\n  // only in the cart level array with the line allocations empty, and a product code showed up only\n  // on the lines with the cart level array empty.\n  //\n  // A product discount is allocated separately to every line it touches, so its total is the sum of\n  // those per line amounts. That is what the running total is for, and it is also why the same code\n  // can appear many times while we walk the lines.\n  //\n  // Note which field each loop reads, because the two arrays do not name their amounts the same\n  // way. The line loop reads the allocation's amount, which is the share allocated to that one\n  // line. The application sitting inside that allocation carries a total_allocated_amount too, and\n  // in the carts we looked at it happened to equal the same share, but it describes the application\n  // rather than the line, so we do not read it there.\n  //\n  // We skip anything that is not a discount code. Automatically applied discounts and script\n  // applied discounts land in these same arrays, but they carry no code for a caller to match\n  // against, so they do not belong in a map keyed this way.\n\n  const discountAmounts = new Map<string, number>();\n\n  if (!cart) {\n    return discountAmounts;\n  }\n\n  // The isArray checks in the following code are considered acceptable paranoia. We want this to\n  // work in bad environments with partial carts, which we have surprisingly encountered.\n\n  // Discounts covering the cart as a whole\n\n  if (Array.isArray(cart.cart_level_discount_applications)) {\n    for (const application of cart.cart_level_discount_applications) {\n      if (application.type === 'discount_code') {\n        const code = application.title.toUpperCase();\n        const discount = application.total_allocated_amount;\n        const currentTotal = discountAmounts.get(code) || 0;\n        discountAmounts.set(code, currentTotal + discount);\n      }\n    }\n  }\n\n  // Discounts covering particular products, spread over the lines holding those products\n\n  if (Array.isArray(cart.items)) {\n    for (const lineItem of cart.items) {\n      if (!Array.isArray(lineItem.line_level_discount_allocations)) {\n        continue;\n      }\n\n      for (const allocation of lineItem.line_level_discount_allocations) {\n        if (allocation.discount_application.type === 'discount_code') {\n          const code = allocation.discount_application.title.toUpperCase();\n          const discount = allocation.amount;\n          const currentTotal = discountAmounts.get(code) || 0;\n          discountAmounts.set(code, currentTotal + discount);\n        }\n      }\n    }\n  }\n\n  return discountAmounts;\n}\n","/**\n * Returns whether a string contains the word \"javascript\" anywhere. This is a lightweight heuristic\n * for rejecting obviously-junk externally-sourced values before they enter state, not real XSS\n * defense, which happens more strictly at the actual sinks. The broad match suits values from a\n * constrained alphabet, such as discount codes, where the word never legitimately appears.\n */\nexport function containsJavascriptWord(value: any) {\n  return typeof value === 'string' && /javascript/i.test(value);\n}\n\n/**\n * Returns whether a string contains the \"javascript:\" URI scheme. This is a lightweight heuristic\n * for rejecting the classic scheme-injection vector before a value enters state, not real XSS\n * defense, which happens more strictly at the actual sinks. Unlike {@link containsJavascriptWord}\n * this tolerates the bare word, so a value that is itself an arbitrary external URL, such as a\n * referrer, is not falsely rejected merely for mentioning javascript in a path.\n */\nexport function containsJavascriptScheme(value: any) {\n  return typeof value === 'string' && /javascript:/i.test(value);\n}\n","import { assert } from '@integrabeauty/assert';\nimport { type Cart, cartFetch as cartFetchOnce, isCart } from '@integrabeauty/shopify-ajax-api';\nimport { v4 as uuidv4 } from 'uuid';\nimport { copy } from '../object.js';\nimport { getApplicableDiscountCodes } from './discount/discount.js';\nimport type { Transaction } from './transaction.js';\n\n/**\n * This event is generated when the state of the cart changes. The event includes the old state and\n * the new state and some deltas.\n *\n * Various cart api calls can result in unexpected behavior. For example, adding an item to cart\n * could cause the line key of unrelated cart lines to change. Therefore, this does not generate\n * deltas based on line key. In addition, each time cart state is obtained, there is no guarantee\n * that the state only changed because of the specified change. For example, a visitor interacting\n * with the same cart in multiple tabs may update the state in one tab, but because the deltas are\n * based on the prior state per tab, the next update in one of the tabs would diff against older\n * state. Therefore, listeners should refrain from operating on deltas and always treat each cart\n * update holistically.\n */\ntype CartUpdatedEvent = CustomEvent<{\n  /**\n   * The new cart state.\n   */\n  cart: Cart;\n\n  /**\n   * Discount codes present in the new cart state that are not present in the old cart state.\n   *\n   * This only includes discount codes. This does not include automatically applied discounts or\n   * script applied discounts.\n   */\n  discount_codes_added: string[];\n\n  /**\n   * Discount codes present in the old cart state that are not present in the new cart state.\n   *\n   * This only includes discount codes. This does not include automatically applied discounts or\n   * script applied discounts.\n   */\n  discount_codes_removed: string[];\n\n  /**\n   * A unique event id. This is useful if tracking this event client side and server side and there\n   * is a need to correlate front end and backend events.\n   */\n  event_id: string;\n\n  /**\n   * Whether this event represents a cart initialization.\n   *\n   * The diffs for an initial update are not useful. In this case the transition is from the empty\n   * state. Do not treat an initial update as a non-initial update.\n   */\n  is_initial?: boolean;\n\n  /**\n   * The previous state of the cart.\n   *\n   * This may be null. For example, when initializing the cart.\n   */\n  previous: Cart;\n\n  /**\n   * Certain transactions provide a description of the context in which the transaction occurred.\n   * For example, two add to cart elements might specify two scenarios when adding to cart, which\n   * then generates an update event with different scenarios, which can then be relayed downstream\n   * to analytics to differentiate in reporting.\n   *\n   * This is primarily an analytics label, but be aware that some consumers also branch on specific\n   * values as behavior, for example suppressing UI reactions to updates the shopper did not\n   * initiate in this tab. Certain values are therefore effectively part of behavior, not only\n   * reporting, so renaming one can change how the UI reacts, not just how a change is reported.\n   *\n   * This is intentionally a broad type. This is not meant to be a type safe enumeration. There are\n   * no heavy safeguards here.\n   */\n  scenario?: string;\n\n  /**\n   * Details of the cart service transaction that triggered this update event.\n   */\n  transaction: {\n    /**\n     * Transaction id\n     */\n    id: number;\n\n    /**\n     * Transaction name\n     */\n    name: string;\n  };\n\n  /**\n   * An array of variant units added to cart. This includes both variants added as new line items\n   * and quantity increases of existing line items.\n   *\n   * A product variant in this array does not contain all the properties of a cart item because we\n   * cannot trivially discern which lines changed when a cart updates.\n   */\n  variants_added: ProductVariant[];\n\n  /**\n   * Variants removed from cart. This can be removal of lines or quantity decreases.\n   *\n   * A product variant does not contain all the properties of a cart item because we cannot\n   * trivially discern which lines changed as a result of a state change.\n   */\n  variants_removed: ProductVariant[];\n}>;\n\n/**\n * Represents a product variant. A Shopify product has one or more variants. A Shopify product with\n * no explicitly configured variant still has a default variant.\n *\n * This should not be confused with a canonical product variant that exists in a single line item in\n * a cart object. This is an amalgamation, a composition of the variant as it exists in one or more\n * line items in a cart. This assumes that price and other properties are the same, apart from the\n * special merged_properties property, which combines the line item properties of the lines\n * containing the variant together.\n */\ninterface ProductVariant {\n  /**\n   * Path to variant image\n   *\n   * @example \"//langehair.com/cdn/shop/products/filename.jpg?v=1680740790\"\n   */\n  image_url: string;\n\n  /**\n   * The combined line item properties of the line items containing this variant\n   *\n   * The key of this object is the property name.\n   *\n   * The values for each key in this object are the distinct values that were merged together.\n   */\n  merged_properties: Record<string, string[]>;\n\n  price: number;\n  product_id: number;\n  product_title: string;\n  product_type: string;\n  quantity: number;\n  sku: string;\n\n  /**\n   * Product variant url (relative)\n   *\n   * @example \"/products/salt-sea-texturizing-spray?variant=41345560477796\"\n   */\n  url: string;\n\n  variant_id: number;\n  variant_title: string;\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'cart-updated': CartUpdatedEvent;\n  }\n}\n\n/**\n * Sets the current state of the cart. This is the in memory state that is stored as the value of a\n * global variable, not the server side state.\n *\n * In memory state is PER TAB PER WINDOW/APP. If someone is browsing in multiple tabs concurrently\n * the state may conflict. The cart token corresponds to a server side cart id and is stored in a\n * cookie that is shared among all tabs, so it is possible for multiple tabs to refer to the same\n * cart object.\n *\n * Always use this method to update the state of the global cart variable. Do not update the state\n * of the global cart variable directly.\n *\n * Throws an error if the given cart is not a cart. This also indicates a more insidious problem\n * where there is no longer a guarantee that client side state reflects server side state, so\n * consider some error handling.\n *\n * This saves a copy of the cart so that future updates by reference to the cart object do not\n * modify the global cart state.\n *\n * Emit a cart updated event that contains some of the differences. This emits the updated event\n * even when there are no changes detected.\n *\n * Any update can result in unrelated cart changes. Changing one line's properties could cause line\n * merging. Also, prior state comes from page state, which is per tab, so interactions in multiple\n * tabs can produce strange behavior.\n *\n * The dispatcher is the target the updated event is dispatched from. It is required but may be\n * null, and it may be any event target, not necessarily an element or window. The target is used as\n * given, with two exceptions that exist only to keep the event reaching window level listeners: a\n * null target dispatches from window, and so does a detached element. A detached element only\n * bubbles up to its detached subtree root, which is not window, so dispatching from it directly\n * would not reach those listeners. Callers should prefer the element most directly responsible for\n * the change, so a connected element carries truthful provenance and bubbles, crossing shadow\n * boundaries, up to the global listeners. Genuinely ambient updates with no originating element,\n * such as init or a cross tab sync, pass window (or null).\n */\nexport function write(dispatcher: EventTarget | null, cart: Cart, transaction: Transaction,\n  scenario?: string, initial = false) {\n  const previous = initial ? null : read();\n\n  // Update global state.\n  assert(isCart(cart), 'Cannot write invalid cart');\n  cart_lifecycle.cart = copy(cart);\n\n  // Prepare for diffing. We group by variant because line keys change in unexpected ways.\n  const oldVariants = groupByVariant(previous);\n  const newVariants = groupByVariant(cart);\n\n  // For discount code deltas, we only examine deltas in codes that both exist and are in the\n  // applicable state in the cart's discount_codes array.\n\n  const prevCodes = getApplicableDiscountCodes(previous);\n  const currCodes = getApplicableDiscountCodes(cart);\n\n  // Emit an event with diffs\n\n  type Detail = CartUpdatedEvent['detail'];\n  const event = new CustomEvent<Detail>('cart-updated', {\n    detail: {\n      cart,\n      discount_codes_added: complement(currCodes, prevCodes),\n      discount_codes_removed: complement(prevCodes, currCodes),\n      event_id: uuidv4(),\n      is_initial: initial,\n      previous,\n      scenario,\n      variants_added: findVariantsAdded(oldVariants, newVariants),\n      variants_removed: findVariantsRemoved(oldVariants, newVariants),\n      transaction: {\n        id: transaction.id,\n        name: transaction.name\n      }\n    },\n    bubbles: true,\n    composed: true\n  });\n\n  // Dispatch the update from whatever target the caller designates; this forms no opinion about\n  // what kind of target it is. The only special handling exists to keep the event reaching the\n  // global listeners: a detached element has no live ancestor chain to bubble through, so it is\n  // swapped for the global scope, and a null target means the global scope outright. Any other\n  // target, including a connected element, is used as given, so it carries truthful provenance and\n  // bubbles, crossing shadow boundaries, up to the global listeners.\n\n  let target: EventTarget;\n  if (dispatcher instanceof Element) {\n    if (dispatcher.isConnected) {\n      target = dispatcher;\n    } else {\n      target = window;\n    }\n  } else if (dispatcher) {\n    target = dispatcher;\n  } else {\n    target = window;\n  }\n\n  target.dispatchEvent(event);\n}\n\n/**\n * Returns a copy of the global cart state.\n *\n * Throws an error if the global cart object is not a cart object.\n */\nexport function read() {\n  assert(isCart(cart_lifecycle.cart), 'Global cart state is invalid on read');\n  return copy(cart_lifecycle.cart);\n}\n\ninterface FetchOptions {\n  /**\n   * The total number of requests to make, counting the first one.\n   */\n  attempts?: number;\n\n  /**\n   * How long to wait in milliseconds before each retry, one entry per retry.\n   */\n  delays?: number[];\n\n  /**\n   * How long in milliseconds to wait on a single attempt before counting it as failed. Leaving it\n   * out means each attempt waits as long as the request takes.\n   */\n  timeout?: number;\n}\n\n/**\n * Reads the server side cart state.\n *\n * This wraps the ajax api call of the same name and is what cart code here should reach for\n * instead. The only things it adds are a few attempts and an optional time limit on each one, so it\n * behaves the same otherwise.\n *\n * A cart read is idempotent and it sits in the middle of things the shopper is waiting on, like\n * adding to cart, so a single failed request is worth another go rather than turning into an error\n * the shopper sees. We make up to a few attempts, waiting a little longer before each retry. If\n * every attempt fails then the error from the last one is thrown and the caller decides what that\n * means. Some callers treat it as fatal, some stay quiet about it, so this forms no opinion.\n *\n * Do not use this to react to a mutation you just made unless you are fine with a repeat request,\n * because each attempt sends a fresh one.\n *\n * Callers can pass options to change how many attempts are made, how long to wait between them, and\n * how long a single attempt gets before it counts as failed. Leaving them out gets the defaults\n * below, and no time limit.\n *\n * There is no limit on the total time across all attempts. The caller is responsible for picking\n * options that keep the worst case acceptable, which is every attempt using its full time limit\n * plus every wait between them.\n */\nexport async function cartFetch(options: FetchOptions = {}) {\n  // How many times we are willing to ask the server before giving up. The first attempt counts, so\n  // this is the total number of requests and not the number of retries on top of one.\n  const limit = options.attempts || 3;\n\n  // How long we wait before each retry. Shopify throttles cart reads, so going right back at it\n  // just piles more requests onto something that already told us to slow down. These stay short as\n  // a balance between waiting long enough to avoid another 429 and waiting so long the user\n  // perceives the UI as broken.\n  const delays = options.delays || [100, 250];\n\n  let lastError: Error;\n\n  for (let attempts = 0; attempts < limit; attempts++) {\n    if (attempts > 0) {\n      const delay = delays[attempts - 1];\n      await resolveAfterTimeout(delay);\n    }\n\n    try {\n      const cart = await cartFetchOnceWithTimeout(options.timeout);\n      return cart;\n    } catch (error) {\n      lastError = error;\n    }\n  }\n\n  throw lastError;\n}\n\n/**\n * Thrown when a single cart read takes longer than its time limit. Callers can check for it to tell\n * a hung read apart from one that failed outright.\n */\nexport class CartFetchTimeoutError extends Error {\n  public declare timeout: number;\n\n  constructor(timeout: number) {\n    super('Timed out fetching cart');\n    this.name = this.constructor.name;\n    this.timeout = timeout;\n  }\n}\n\n/**\n * Reads the cart once, giving up after the given number of milliseconds. Leaving out the timeout\n * means there is no limit.\n *\n * Giving up does not stop the request. It keeps going in the background and whatever it eventually\n * returns is ignored. That is fine because reading the cart changes nothing, but it means this must\n * never be used for a cart write, since a write that times out here can still land later.\n */\nasync function cartFetchOnceWithTimeout(timeout: number) {\n  if (!timeout) {\n    return cartFetchOnce();\n  }\n\n  let timer: ReturnType<typeof setTimeout>;\n  const timeoutPromise = new Promise<never>((_resolve, reject) => {\n    const error = new CartFetchTimeoutError(timeout);\n    timer = setTimeout(reject, timeout, error);\n  });\n\n  try {\n    const cart = await Promise.race([cartFetchOnce(), timeoutPromise]);\n    return cart;\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\n/**\n * Returns a promise which resolves after the given timeout\n */\nfunction resolveAfterTimeout(milliseconds: number) {\n  return new Promise(resolve => {\n    setTimeout(resolve, milliseconds);\n  });\n}\n\ntype ProductVariantMap = Record<string, ProductVariant>;\n\n/**\n * Aggregates cart line items by variant. Returns the aggregated variants as an object where each\n * key of the object is a variant id and each value is the corresponding variant object.\n *\n * This does some complex stuff. One key thing to constantly point out is that a variant can appear\n * in more than one line item in a cart. There are several reasons why. Shopify generally refers to\n * this as a variant split. Variant splits happen for reasons such as having different line item\n * properties, or different prices as a result of allocated discount applications, or because of\n * more rare reasons like product subscriptions. Because we are grouping by variant id, we have to\n * invent some novel ways of combining the different values of the line items containing the same\n * variant.\n *\n * To combine the line item properties, we invent a new property, merged_properties, that contains\n * the properties of each line item. In each merged properties value, there is an array of values.\n * When appending to the values array, we only append unique values (using exact value comparison).\n *\n * There are many downstream concerns. Here is one example. When relaying an add to cart event to a\n * third party analytics system, we sometimes want the product's image url. This function's output\n * is the input to the variants_added array of the cart updated event emitted as a result of a cart\n * write. So we have to embed the image url here in the variant object so that it can be accessed\n * without having to do some kind of later lookup.\n *\n * @param cart the input cart to aggregate\n */\nfunction groupByVariant(cart: Cart) {\n  const output: ProductVariantMap = {};\n\n  // It is important to return null here, and not the empty object, so that later functions that use\n  // this output can trivially distinguish between transitions from null state and transitions from\n  // empty cart state. The transition from null state happens on init.\n\n  if (!cart) {\n    return null;\n  }\n\n  for (const item of cart.items) {\n    const variant = output[item.variant_id.toString()];\n    if (variant) {\n      // Merge the item's properties into the variant's properties.\n      for (const property in item.properties || {}) {\n        variant.merged_properties[property] = variant.merged_properties[property] || [];\n        const value = item.properties[property];\n        if (!variant.merged_properties[property].includes(value)) {\n          variant.merged_properties[property].push(value);\n        }\n      }\n\n      variant.quantity += item.quantity;\n    } else {\n      // Merge in the initial properties, where each value is a one value array.\n      const properties: Record<string, string[]> = {};\n      for (const key in item.properties || {}) {\n        properties[key] = [item.properties[key]];\n      }\n\n      output[item.variant_id.toString()] = {\n        image_url: item.image,\n        merged_properties: properties,\n        // eslint-disable-next-line @typescript-eslint/no-deprecated\n        price: item.price,\n        product_id: item.product_id,\n        product_title: item.product_title,\n        product_type: item.product_type,\n        quantity: item.quantity,\n        sku: item.sku,\n        url: item.url,\n        variant_id: item.variant_id,\n        variant_title: item.variant_title\n      };\n    }\n  }\n\n  return output;\n}\n\n/**\n * Returns an array of variant data objects that were added as a result of the write mutation.\n *\n * This tolerates nulls\n *\n * @param c0map variants in old cart state\n * @param c1map variants in new cart state\n * @returns variants added array\n */\nfunction findVariantsAdded(c0map: ProductVariantMap, c1map: ProductVariantMap) {\n  // When the cart transitions from the null state, there are no previous variants. This means that\n  // all variants in the new state have been added. Transitioning from null can mean multiple\n  // things. It can mean either that cart state was initialized on page load, or that cart state was\n  // recovered from error. Because of the ambiguity we are not emitting adds for transitions from\n  // the null state.\n\n  if (!c0map) {\n    return [];\n  }\n\n  // Figure out which variants had quantity increases in any of the line items containing those\n  // variants. Any variant that had a quantity increase is referred to here as an increment. In\n  // addition, look for any variants that are present in the new state but not in the old state, in\n  // which case their full quantity is the quantity added.\n\n  const increments: ProductVariantMap = {};\n  for (const key in c1map) {\n    if (c0map[key]) {\n      // In this branch, the variant was present in the old map and is present also in the new map.\n\n      if (c1map[key].quantity > c0map[key].quantity) {\n        increments[key] = copy(c1map[key]);\n        increments[key].quantity = c1map[key].quantity - c0map[key].quantity;\n      } else {\n        // ignore 0 quantity change and negative quantity change (decrements).\n      }\n    } else {\n      // In this branch, the variant is present in the new map but not in the old map.\n\n      increments[key] = copy(c1map[key]);\n    }\n  }\n\n  return Object.keys(increments).map(key => increments[key]);\n}\n\nfunction findVariantsRemoved(c0map: ProductVariantMap, c1map: ProductVariantMap) {\n  if (!c0map) {\n    return [];\n  }\n\n  const decrements: ProductVariantMap = {};\n  for (const key in c0map) {\n    if (c1map[key]) {\n      if (c1map[key].quantity < c0map[key].quantity) {\n        decrements[key] = copy(c1map[key]);\n        decrements[key].quantity = c0map[key].quantity - c1map[key].quantity;\n      } else {\n        // ignore 0 quantity change\n      }\n    } else {\n      // if the old variant is not found in the updated cart, all quantity was removed\n      decrements[key] = copy(c0map[key]);\n    }\n  }\n\n  return Object.keys(decrements).map(key => decrements[key]);\n}\n\n/**\n * Return all values present in array a that are not present in array b.\n *\n * The complement of a set is the set that includes all the elements of the universal set that are\n * not present in the given set. Here, a is the universal set and b is the given set.\n */\nfunction complement(a: string[], b: string[]) {\n  assert(Array.isArray(a), 'complement parameter \"a\" is not an array');\n  assert(Array.isArray(b), 'complement parameter \"b\" is not an array');\n  return a.filter(value => !b.includes(value));\n}\n","import { measure, now } from '../perf.js';\n\ntype CartTransactionStartedEvent = CustomEvent<Transaction>;\ntype CartTransactionCompletedEvent = CustomEvent<Transaction>;\n\n/**\n * The bookkeeping shared by every copy of this module. Each entry bundles its own copy, so the\n * numbers and the timeline have to sit on the window for the copies to agree on them.\n */\ninterface State {\n  /** The number of currently active cart service transactions. */\n  activeCount: number;\n\n  /** Number of cart operations started so far, and the source of transaction ids. */\n  count: number;\n\n  /** A rolling log of recent transaction start and end entries. */\n  timeline: Record<string, any>[];\n}\n\ndeclare global {\n  var cart_transaction: State;\n\n  interface WindowEventMap {\n    'cart-transaction-completed': CartTransactionCompletedEvent;\n    'cart-transaction-started': CartTransactionStartedEvent;\n  }\n}\n\n/**\n * Obtain the shared state, initializing it as needed.\n *\n * We do not assume the global state is reliably present and instead access state via this function\n * every time because we have seen very surprising errors in the wild to which we wish to remain\n * resilient, even at the cost of generating artificial and inaccurate state.\n */\nfunction getSharedState() {\n  window.cart_transaction = window.cart_transaction || { activeCount: 0, count: 0, timeline: [] };\n\n  const shared = window.cart_transaction;\n\n  if (!Number.isInteger(shared.activeCount) || shared.activeCount < 0) {\n    shared.activeCount = 0;\n  }\n\n  if (!Number.isInteger(shared.count)) {\n    shared.count = 0;\n  }\n\n  if (!Array.isArray(shared.timeline)) {\n    shared.timeline = [];\n  }\n\n  return shared;\n}\n\n/**\n * A transaction represents an action carried out by the cart service. For example, an add to cart\n * transaction or a remove from cart transaction.\n *\n * A transaction is a plain record minted by the start function and completed through the handle it\n * carries. The bookkeeping fields live on the record because completion is a module function that\n * operates on the record rather than a method that closes over it.\n */\nexport interface Transaction {\n  /** The arguments to the start of the transaction */\n  args: any[];\n\n  /** Complete this transaction. Idempotent, and reachable only through the handle. */\n  complete?(didTimeOut?: boolean): void;\n\n  /** Whether completion has already run, so a second call is a no-op */\n  completed: boolean;\n\n  /** The transaction start time in milliseconds */\n  epoch: number;\n\n  /** Transaction unique global id */\n  id: number;\n\n  /** Transaction name, e.g. function name */\n  name: string;\n\n  /**\n   * The transaction start time on the monotonic clock, used only to compute duration.\n   *\n   * This is separate from epoch, which stays wall-clock so it still answers when the transaction\n   * happened. The two clocks have different origins, so never mix them in one subtraction.\n   */\n  startMillis: number;\n\n  /** Timeout timer id */\n  timer?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Whether any cart transaction is currently in flight. A transaction stops counting once it\n * completes or times out, so this cannot stay true for longer than the transaction timeout.\n *\n * Interface elements that are created partway through a transaction never saw its started event, so\n * they have to ask this when they connect. The add to cart button relies on it to stay disabled\n * when it is rebuilt during an add. Changing what this reports, for example counting only some\n * kinds of transactions, changes when shoppers can tap add to cart. See issue 7022.\n */\nexport function isActive() {\n  return getSharedState().activeCount > 0;\n}\n\n/**\n * Start a cart service transaction and return its handle. Use the handle's complete method to end\n * the transaction.\n */\nexport function start(name: string, ...args: any[]) {\n  const shared = getSharedState();\n\n  // Increment the shared transaction id. A simple monotonic counter ensures that no two\n  // transactions have the same id (but see the caveats mentioned in getting shared state).\n\n  shared.count++;\n\n  const transaction: Transaction = {\n    args,\n    epoch: Date.now(),\n    id: shared.count,\n    name,\n    completed: false,\n    startMillis: now()\n  };\n\n  // Expose complete as a \"method\" of the returned object. Callers are expected to complete the\n  // transaction via its own method. This is better than exporting the top level complete function\n  // as a free function where callers could mistakenly complete the wrong transaction.\n\n  transaction.complete = (didTimeOut = false) => complete(transaction, didTimeOut);\n\n  // Increment the number of active transactions. This should be a transition from 0 to 1.\n\n  shared.activeCount++;\n\n  // If as a result of the increment there is now more than one active transaction, report the\n  // problem but continue. The goal is not to error. Many transactions are able to reconcile from\n  // overlapping flights.\n\n  if (shared.activeCount > 1) {\n    reportConcurrency(transaction);\n  }\n\n  // Record the transaction in the active transactions timeline. This is a fresh object instead of\n  // the original transaction so that it snapshots.\n\n  append({ epoch: transaction.epoch, id: transaction.id, name: transaction.name, type: 'start' });\n\n  // Start a timer that ensures the transaction settles eventually. Ensuring this is important\n  // because the upper layers typically lock down the UI during a transaction, and we want to\n  // prevent locking the UI indefinitely.\n\n  transaction.timer = setTimeout(complete, 30 * 1000, transaction, true);\n\n  console.log('Cart transaction started', transaction.name);\n\n  type Detail = CartTransactionStartedEvent['detail'];\n  const event = new CustomEvent<Detail>('cart-transaction-started', { detail: transaction });\n  dispatchEvent(event);\n\n  return transaction;\n}\n\n/**\n * Complete a transaction.\n *\n * Appends the transaction to the transaction history log.\n *\n * Emits a transaction completed event. Various transactions should complete when done to signal the\n * completion to listeners.\n *\n * This runs either manually whenever the transaction completes, or automatically, if the\n * transaction did not complete quickly enough (e.g. because of an error, omission, or a genuinely\n * long operation that timed out).\n *\n * Transactions should be short-lived. Transactions are automatically considered complete after a\n * certain amount of time so as to guarantee completion. Higher layer interfaces rely on transaction\n * state to lock interfaces for periods of time. Locking must be definite.\n *\n * Completion is idempotent. The timeout safety net and the caller's own completion can both fire\n * for one transaction when a tab is suspended long enough for the operation to outlive the timeout,\n * as happens on a backgrounded mobile tab that resumes. Whichever fires first records the outcome\n * and any later call is a no-op, so the active transaction count decrements exactly once per start\n * and cannot drift negative and defeat the interface locks that read it.\n */\nfunction complete(transaction: Transaction, didTimeOut = false) {\n  // A transaction that already timed out lands here when its operation finally finishes, so we log\n  // how long it really took. That tells a stalled request that eventually came back apart from one\n  // that never did.\n\n  if (transaction.completed) {\n    const duration = Math.round(now() - transaction.startMillis);\n    console.log('Cart transaction already completed', transaction.name, didTimeOut, duration);\n    return;\n  }\n\n  transaction.completed = true;\n\n  clearTimeout(transaction.timer);\n  transaction.timer = undefined;\n\n  append({\n    epoch: Date.now(),\n    id: transaction.id,\n    name: transaction.name,\n    timeout: didTimeOut,\n    type: 'end'\n  });\n\n  const shared = getSharedState();\n\n  if (shared.activeCount > 0) {\n    shared.activeCount--;\n  }\n\n  if (didTimeOut) {\n    console.log('Cart transaction timed out', transaction.name);\n  }\n\n  console.log('Cart transaction completed', transaction.name);\n\n  // Capture performance data representing each transaction's duration.\n\n  measure(`cart-${transaction.name}-${transaction.id}`, transaction.startMillis, now(), {\n    id: transaction.id,\n    name: transaction.name,\n    timeout: didTimeOut\n  });\n\n  type Detail = CartTransactionCompletedEvent['detail'];\n  const event = new CustomEvent<Detail>('cart-transaction-completed', { detail: transaction });\n  dispatchEvent(event);\n}\n\n/**\n * Capture a concurrent transaction in a way that observers can see it\n */\nfunction reportConcurrency(transaction: Transaction) {\n  const state = getSharedState();\n\n  // Snapshot the state. The reason for this is because the state that is rendered in the devtools\n  // console is the state of the live reference when the value is viewed in the log, which is after\n  // the time it was recorded. Snapshotting saves the value at the time of recording it.\n  //\n  // This is a shallow snapshot. Since the timeline is append only, we are basically just recording\n  // the timeline up to the time of the snapshot, e.g. basically this is just a partial view of an\n  // array that may have more entries by the time it is expanded but those entries are hidden.\n\n  const snapshot = [...state.timeline];\n\n  console.warn('Concurrent transaction started', {\n    id: transaction.id,\n    name: transaction.name,\n    timeline: snapshot\n  });\n}\n\n/**\n * Add a timeline entry.\n *\n * The type of entry here is intentionally general because this has a relaxed opinion on what gets\n * stored.\n */\nfunction append(entry: Record<string, any>) {\n  const state = getSharedState();\n  const timeline = state.timeline;\n\n  timeline.push(entry);\n\n  const capacity = 50;\n\n  // Keep only the most recent entries so the timeline does not grow without bound over a long\n  // session. Removing from the front drops the oldest; the clamp guards against a negative removal\n  // count while the timeline is still under the cap.\n  // TODO: the clamp may be unnecessary, since splice already treats a negative delete count as\n  // zero, so it is likely defensive rather than required. Verify and drop it if so.\n  timeline.splice(0, Math.max(timeline.length - capacity, 0));\n}\n","import { type CartChangeInput, type Cart, cartChange, isUnavailableError } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from './digest.js';\nimport { hasSimilarItems } from './equality.js';\nimport { report } from './error.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start } from './transaction.js';\n\n/**\n * Change the cart's line item quantities, note, selling plan allocation or attributes.\n *\n * @param target the element most directly responsible for the change, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param input cart change input\n * @param scenario optional description or code describing the context in which this takes place\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-change-js\n */\nexport async function change(target: EventTarget | null, input: CartChangeInput,\n  scenario?: string) {\n  const transaction = start('changeCart', input);\n\n  let previous: Cart;\n  let remoteCart: Cart;\n\n  try {\n    // It is possible that the client side cart state is stale, so we need to grab it in order to\n    // compare it to the remote state.\n    previous = read();\n\n    // Grab the remote state\n    remoteCart = await cartFetch();\n\n    if (!hasSimilarItems(previous, remoteCart)) {\n      // Update local state to reflect the latest remote state.\n      write(target, remoteCart, transaction, scenario);\n\n      // Define a custom error code when throwing this error so that calling code can extract a more\n      // specific error code.\n\n      const error: any = new Error('Cart state out of sync during changeCart');\n      error.error_code = 'CHANGE_ITEMS_DESYNC';\n      throw error;\n    }\n\n    await cartChange(input);\n    const cart = await cartFetch();\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: determineErrorCode(error),\n      error,\n      inputs: {\n        input,\n        previous_cart: digest(previous),\n        fetched_cart: digest(remoteCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nfunction determineErrorCode(error: any) {\n  if (isUnavailableError(error)) {\n    return 'CHANGE_CART_OOS';\n  }\n\n  if ('error_code' in error) {\n    return error.error_code;\n  }\n\n  return 'CHANGE_CART';\n}\n","import { assert } from '@integrabeauty/assert';\nimport { type Cart, cartChange } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Remove the given lines, finding each line's position in the given cart.\n */\nexport async function removeLines(cart: Cart, lineKeys: string[]) {\n  const lineIndicesToRemove = [];\n  for (let i = 0; i < cart.items.length; i++) {\n    const item = cart.items[i];\n    if (lineKeys.includes(item.key)) {\n      lineIndicesToRemove.push(i + 1);\n    }\n  }\n\n  assert(lineIndicesToRemove.length === lineKeys.length, 'One or more line keys not found');\n\n  // Reorder from last to first, as each removal changes the position of later ones\n  lineIndicesToRemove.reverse();\n\n  // Then perform the removals. We cannot perform the removals concurrently. We have to perform the\n  // removals sequentially as racing them concurrently carries the risk that the removals occur in\n  // the order we did not intend.\n\n  for (const line of lineIndicesToRemove) {\n    await cartChange({ line, quantity: 0 });\n  }\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { removeLines } from './remove-lines.js';\nimport { cartFetch } from './state.js';\n\n/**\n * Run a follow-up removal from inside a transaction that already changed the cart, and return the\n * cart to write. The follow-up is given the changed cart and returns the keys of any further lines\n * to remove, such as a free gift the cart no longer qualifies for.\n *\n * Running this inside the caller's transaction keeps the follow-up from starting a second,\n * overlapping transaction once the caller writes the cart. The caller's change already went through\n * by now, so a failed follow-up is only a warning, and the caller still writes whatever cart this\n * returns. See issue 9131.\n *\n * @param cart the cart fetched after the caller's change\n * @param followUp optional function returning the keys of further lines to remove\n * @param scenario the caller's scenario, for the warning\n */\nexport async function removeFollowUp(cart: Cart, followUp?: (cart: Cart) => string[],\n  scenario?: string) {\n  if (followUp) {\n    try {\n      const followUpKeys = followUp(cart);\n      if (followUpKeys.length > 0) {\n        await removeLines(cart, followUpKeys);\n\n        // Awaited inside the try so that a failed fetch stays a warning too.\n        const latestCart = await cartFetch();\n        console.log('Cart follow-up removal completed', scenario, followUpKeys);\n        return latestCart;\n      }\n    } catch (error) {\n      console.warn('Cart follow-up removal failed', scenario, error);\n    }\n  }\n\n  return cart;\n}\n","import { assert } from '@integrabeauty/assert';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from './digest.js';\nimport { hasSimilarItems } from './equality.js';\nimport { report } from './error.js';\nimport { removeFollowUp } from './remove-follow-up.js';\nimport { removeLines } from './remove-lines.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start } from './transaction.js';\n\n/**\n * Removes items from the cart.\n *\n * Each id in the array can be either a line key or a variant id. A cart can have multiple line\n * items that share the same variant id. For example, when variants have different line item\n * properties, or automatic discounts create variants at different prices. Because of this, it is\n * recommended to use the line item key to ensure that you're only changing the intended line item.\n *\n * If you want to also change other cart properties in the same call, use the more general\n * updateCart call instead while specifying lines with quantity 0 to effectively remove. This\n * removeItems call is just a convenient but limited way of doing a cart update.\n *\n * Even though Shopify permits specifying a variant id as a number, we require that it is a string\n * because we have seen strange, undocumented errors when specifying variant ids as numbers, such as\n * some kind of server side Ruby error with parsing numbers.\n *\n * @param target the element most directly responsible for the removal, used to originate any\n * failure event with truthful provenance instead of at global scope\n * @param lineKeys the line keys or variant ids of the items to remove\n * @param scenario optional description or code describing the context in which this takes place\n * @param followUp optional function given the cart after the removal, returning the keys of any\n * further lines to remove before the transaction ends, such as a free gift the cart no longer\n * qualifies for\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#update-line-item-quantities\n */\nexport async function remove(target: EventTarget | null, lineKeys: string[], scenario?: string,\n  followUp?: (cart: Cart) => string[]) {\n  const transaction = start('removeItems', lineKeys, scenario);\n  let errorCode = 'REMOVE_ITEMS';\n\n  let previous: Cart;\n  let remoteCart: Cart;\n\n  try {\n    assert(lineKeys.length > 0, 'At least one item must be specified');\n\n    // Even though shopify supports variant ids as change item ids as numbers, this function does\n    // not support using variant id. This function only operates on line keys, which must always be\n    // strings. Even though we have type safety checks, we still want a runtime check, as the\n    // calling code is typically less diligent.\n    assert(lineKeys.every(isString), 'One or more invalid line keys');\n\n    // Ensure the caller did not provide duplicates\n    const uniqueKeySet = new Set(lineKeys);\n    assert(uniqueKeySet.size === lineKeys.length, 'One or more duplicate line keys');\n\n    // cart/update supports removing multiple items at once, but cart/change only supports changing\n    // a single item at once. While it would be ideal to use cart/update, cart/update has an\n    // undocumented bug where attempting to set the quantity of an out of stock line item to 0\n    // results in a 422 error. We reached out to Shopify support, and they stated that we should use\n    // cart/change instead. So now removing multiple items at once gets complicated, because we have\n    // no real transactional safety. In fact, we should probably drop support for removing multiple\n    // items at once and fully migrate the use cases for removing multiple items to other solutions,\n    // e.g. use the Shopify cart transforms for bulk add/remove/update, such as what the Shopify\n    // bundles app does. In the short term we are going to attempt to support bulk removal by\n    // sequentially removing items.\n\n    // Removing multiple items at once, or at least attempting to support the invented abstraction\n    // of an atomic mutation on the cart that is actually a compound mutation, is further\n    // complicated by how cart/change behaves. Another undocumented feature of cart/change is that\n    // changing any one item's quantity to 0 can cause other line items to change their keys. The\n    // keys of other lines change in an undocumented manner. It is not clear if affecting a single\n    // line can change any other line, or only lines that have a later position. I think what is\n    // happening is that the keys of later lines shift when the quantity of an earlier line is\n    // changed to 0. So, the idea is that if we remove lines in reverse order, we avoid the issue\n    // with shifting keys per change.\n\n    // Clarified, removing any one line can impact the keys of all other lines. So we cannot remove\n    // by key in a batch, because each single removal affects the rest.\n\n    // The next problem then is that the calling code is not obligated to provide us line keys in\n    // natural cart order. So we have to find a way to get the natural order. In which case the next\n    // problem is that in order to figure that out we need the current cart state, which is not\n    // provided as input. So we have to get the current cart state and assume it is authoritative.\n\n    previous = read();\n\n    // Verify that the cart state is not stale by fetching the latest server side cart state prior\n    // to mutation.\n\n    remoteCart = await cartFetch();\n\n    if (!hasSimilarItems(previous, remoteCart)) {\n      // Update local state to reflect the latest remote state.\n      write(target, remoteCart, transaction, scenario);\n\n      errorCode = 'REMOVE_ITEMS_DESYNC';\n      throw new Error('Cart state out of sync during removeItems');\n    }\n\n    // Verify that the number of lines to remove is less than or equal to the number of items in the\n    // cart. It is possible that the visitor is looking at a stale cart state and then trying to\n    // remove one or more items from a stale cart state. One thing we can detect is that when the\n    // cart state is stale, there should still always be enough items in the cart to remove. This is\n    // somewhat redundant with a later assertion but it still gives us a more exact error.\n    assert(lineKeys.length <= previous.items.length, 'Too many line keys');\n\n    await removeLines(previous, lineKeys);\n\n    // Because we intentionally ignore the output of each cart/change, obtain the updated cart state\n    // through yet another request.\n\n    const fetchedCart = await cartFetch();\n    const cart = await removeFollowUp(fetchedCart, followUp, scenario);\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: errorCode,\n      error,\n      inputs: {\n        line_keys: lineKeys,\n        previous_cart: digest(previous),\n        fetched_cart: digest(remoteCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nfunction isString(value: any): value is string {\n  return typeof value === 'string';\n}\n","import { type CartUpdateInput, type Cart, cartUpdate, isUnavailableError } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from './digest.js';\nimport { hasSimilarItems } from './equality.js';\nimport { report } from './error.js';\nimport { removeFollowUp } from './remove-follow-up.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start } from './transaction.js';\n\n/**\n * Update the cart's line item quantities, note, or attributes.\n *\n * @param target the element most directly responsible for the update, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param input cart update input\n * @param scenario optional description or code describing the context in which this takes place\n * @param followUp optional function given the cart after the update, returning the keys of any\n * lines to remove before the transaction ends, such as a free gift the cart no longer qualifies for\n *\n * @see https://shopify.dev/docs/api/ajax/reference/cart#post-locale-cart-update-js\n */\nexport async function update(target: EventTarget | null, input: CartUpdateInput, scenario?: string,\n  followUp?: (cart: Cart) => string[]) {\n  const transaction = start('updateCart', input, scenario);\n\n  let previous: Cart;\n  let remoteCart: Cart;\n\n  try {\n    // It is possible that the client side cart state is stale, so we need to grab it in order to\n    // compare it to the remote state.\n    previous = read();\n\n    // Grab the remote state\n    remoteCart = await cartFetch();\n\n    if (!hasSimilarItems(previous, remoteCart)) {\n      // Update local state to reflect the latest remote state.\n      write(target, remoteCart, transaction, scenario);\n\n      // Define a custom error code when throwing this error so that calling code can extract a more\n      // specific error code.\n\n      const error: any = new Error('Cart state out of sync during updateCart');\n      error.error_code = 'UPDATE_ITEMS_DESYNC';\n      throw error;\n    }\n\n    await cartUpdate(input);\n    const fetchedCart = await cartFetch();\n\n    const cart = await removeFollowUp(fetchedCart, followUp, scenario);\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: determineErrorCode(error),\n      error,\n      inputs: {\n        input,\n        previous_cart: digest(previous),\n        fetched_cart: digest(remoteCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nfunction determineErrorCode(error: any) {\n  if (isUnavailableError(error)) {\n    return 'UPDATE_CART_OOS';\n  }\n\n  if ('error_code' in error) {\n    return error.error_code;\n  }\n\n  return 'UPDATE_CART';\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * The gift with purchase threshold settings.\n */\ninterface ThresholdSettings {\n  /**\n   * The cart total the shopper has to reach for the gift, in cents. Used unless the threshold type\n   * is a quantity and a quantity is set.\n   */\n  cents?: string;\n\n  /**\n   * How many items the shopper has to have in the cart for the gift, not counting the gift. Used\n   * when the threshold type is a quantity.\n   */\n  quantity?: string;\n\n  /**\n   * Whether the threshold is a cart total or an item count, either `cart-total` or `cart-quantity`.\n   * Anything other than `cart-quantity` is treated as a cart total.\n   */\n  type?: string;\n}\n\n/**\n * Whether the cart meets the gift with purchase threshold. A quantity threshold does not count the\n * gift itself, and a cart total threshold measures the total after discount codes.\n */\nexport function isThresholdAchieved(cart: Cart, settings: ThresholdSettings) {\n  if (settings.type === 'cart-quantity' && settings.quantity) {\n    const gift = findGift(cart);\n    const quantity = gift ? cart.item_count - 1 : cart.item_count;\n    return quantity >= parseInt(settings.quantity, 10);\n  }\n\n  const giftThreshold = parseInt(settings.cents, 10);\n  const percentage = Math.min(cart.total_price / giftThreshold, 1);\n  return percentage >= 1;\n}\n\n/**\n * The keys of the claimed gift lines the cart should drop, or an empty array.\n *\n * A Shopify cart transform makes the gift free while the cart qualifies, so a gift line with a\n * price on it means the cart no longer qualifies. The transform measures the cart before discount\n * codes and our threshold check measures it after, so they can disagree, and the gift goes when\n * either says so. The gift removal doc beside the progress bar has the details.\n */\nexport function findGiftKeysToRemove(cart: Cart, settings: ThresholdSettings) {\n  const gift = findGift(cart);\n  if (gift && (gift.final_price > 0 || !isThresholdAchieved(cart, settings))) {\n    return [gift.key];\n  }\n\n  return [];\n}\n\nfunction findGift(cart: Cart) {\n  return cart.items.find(item => item.properties._gift_with_purchase === 'true');\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { findGiftKeysToRemove } from '../../lib/cart/gift-with-purchase.js';\n\n/**\n * Like finding the gift keys to remove, reading the threshold settings off the cart drawer's gift\n * with purchase progress bar. Returns an empty array when the bar is not on the page.\n *\n * Cart drawer actions that can drop the cart under the threshold pass this as the follow-up to the\n * cart lib, so the gift goes in the same transaction as the shopper's own change. See issue 9131.\n */\nexport function findGiftKeysToRemoveFromPage(cart: Cart) {\n  const giftBar = document.querySelector('gift-with-purchase-progress');\n  if (giftBar) {\n    return findGiftKeysToRemove(cart, {\n      cents: giftBar.dataset.thresholdCents,\n      quantity: giftBar.dataset.thresholdQuantity,\n      type: giftBar.dataset.thresholdType\n    });\n  }\n\n  return [];\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { CartChangeInput, CartUpdateInput } from '@integrabeauty/shopify-ajax-api';\nimport { change } from '../../../lib/cart/change.js';\nimport { remove } from '../../../lib/cart/remove.js';\nimport { read } from '../../../lib/cart/state.js';\nimport { update } from '../../../lib/cart/update.js';\nimport { findGiftKeysToRemoveFromPage } from '../gift-removal.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a cart line item, with the ability for the user to adjust quantities, and remove the\n * item.\n */\nclass CartLineItem extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-is-available',\n      'data-max-quantity',\n      'data-line-item-id',\n      'data-line-price-after-discount',\n      'data-line-price',\n      'data-price',\n      'data-image-src',\n      'data-image-alt',\n      'data-title',\n      'data-quantity',\n      'data-variant-id',\n      'data-parent-id',\n      'data-product-handle',\n      'data-product-id',\n      'data-product-type',\n      'data-update',\n      'data-selling-plan-id',\n      'data-is-upsell'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Country ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    countryCode: string;\n\n    /**\n     * Currency ISO Code. Required\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required\n     */\n    currencySymbol: string;\n\n    /**\n     * \"true\" on the gift with purchase line when there is more than one gift to pick from, which\n     * shows a Change link. Set by the cart when it builds the line.\n     */\n    giftChangeable: string;\n\n    /**\n     * Image alternate text.\n     */\n    imageAlt: string;\n\n    /**\n     * Image url.\n     */\n    imageSrc: string;\n\n    /**\n     * Whether the item has inventory that is sellable.\n     */\n    isAvailable: string;\n\n    /**\n     * Whether the item is a gift with purchase.\n     */\n    isGiftWithPurchase: string;\n\n    /**\n     * Boolean string indicating this is a parent line item with child items (e.g. GWP). Parent line\n     * items have controls for quantity and deletion, and child line items are visually indented\n     * with no quantity or delete controls because they are bound to the parent line item via the\n     * Shopify API.\n     */\n    isParent: 'false' | 'true';\n\n    /**\n     * Whether the item is an upsell item that qualifies for special pricing or logic.\n     */\n    isUpsell: string;\n\n    /**\n     * The id of the item in the cart\n     */\n    lineItemId: string;\n\n    /**\n     * The price before discounts. This could be either the MSRP (the compare at price) or it could\n     * be the marked down price (the sales price).\n     *\n     * This is for the entire line, not per unit, so this takes into account quantity.\n     */\n    linePrice: string;\n\n    /**\n     * This is the price after discounts. This is for the entire line, not per unit, so this also\n     * takes into account quantity.\n     */\n    linePriceAfterDiscount: string;\n\n    /**\n     * The maximum quantity of the item that can be added to cart. This controls whether certain\n     * features, such as a UI element for incrementing quantity, are enabled. Some items have a\n     * limitation that they can only be included once in a cart because they are offered at a\n     * reduced price under specific conditions.\n     */\n    maxQuantity: string;\n\n    /**\n     * If present, this line item is a child of another line item (e.g. GWP). The parentId is the\n     * Shopify line item key of the parent (format: \"variantId:propertiesHash\"). Child items have\n     * controls hidden because Shopify API automatically replicates parent actions (quantity\n     * changes, deletions) to all children.\n     */\n    parentId: string;\n\n    /**\n     * The price of the item (per unit)\n     */\n    price: string;\n\n    /**\n     * The Shopify url slug of the product in the line item.\n     */\n    productHandle: string;\n\n    /**\n     * The Shopify product id\n     */\n    productId: string;\n\n    /**\n     * The Shopify product type\n     *\n     * @example \"hair-care\"\n     */\n    productType: string;\n\n    /**\n     * A url for the PDP of the product.\n     */\n    productUrl: string;\n\n    /**\n     * Boolean string. Setting it to \"true\" pulses a faint outline on the product image once. The\n     * stylesheet does all of it, so removing the attribute and setting it again plays it again.\n     */\n    pulse: 'false' | 'true';\n\n    /**\n     * The number of items\n     */\n    quantity: string;\n\n    /**\n     * String representation of the selling plan. This is used to display the selling plan delivery\n     * frequency in the UI.\n     */\n    sellingPlanFrequency: string;\n\n    /**\n     * If the item has an associated selling plan, e.g. it is a subscription item, then this is the\n     * id of the related selling plan that has all of the specifics of the subscription.\n     */\n    sellingPlanId: string;\n\n    /**\n     * The line item display title, which is typically the product title.\n     */\n    title: string;\n\n    /**\n     * Setting this attribute to a new value triggers a render. This exists so that we can trivially\n     * change several attributes without triggering a render per attribute change, then set this one\n     * attribute to trigger a single render.\n     */\n    update: string;\n\n    /**\n     * The id of the variant represented by the cart line item\n     */\n    variantId: string;\n  };\n\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n\n  private onSellingPlanChangedBound = this.onSellingPlanChanged.bind(this);\n  private onUpgradeButtonClickBound = this.onUpgradeButtonClick.bind(this);\n  private onRemoveLineItemClickBound = this.onRemoveLineItemClick.bind(this);\n  private onChangeGiftClickBound = this.onChangeGiftClick.bind(this);\n  private onQuantityChangedBound = this.onQuantityChanged.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const removeButton = this.shadowRoot.querySelector<HTMLElement>('.remove-line-item');\n    removeButton.addEventListener('click', this.onRemoveLineItemClickBound);\n\n    const changeGiftButton = this.shadowRoot.querySelector<HTMLElement>('.change-gift');\n    changeGiftButton.addEventListener('click', this.onChangeGiftClickBound);\n\n    const quantity = this.shadowRoot.querySelector<HTMLSelectElement>('.quantity');\n    quantity.addEventListener('change', this.onQuantityChangedBound);\n\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const removeButton = this.shadowRoot.querySelector<HTMLElement>('.remove-line-item');\n    removeButton?.removeEventListener('click', this.onRemoveLineItemClickBound);\n\n    const changeGiftButton = this.shadowRoot.querySelector<HTMLElement>('.change-gift');\n    changeGiftButton?.removeEventListener('click', this.onChangeGiftClickBound);\n\n    const quantity = this.shadowRoot.querySelector<HTMLSelectElement>('.quantity');\n    quantity.removeEventListener('change', this.onQuantityChangedBound);\n\n    // These two live in our light DOM and only one of them is ever there, depending on whether the\n    // item is already a subscription. render() puts the listener on whichever one it finds, so we\n    // take it back off here to match.\n    const sellingPlanSelect = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n    sellingPlanSelect?.removeEventListener('change', this.onSellingPlanChangedBound);\n\n    const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n    upgradeButton?.removeEventListener('click', this.onUpgradeButtonClickBound);\n\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onQuantityChanged(event: Event) {\n    console.debug('Cart line quantity changed');\n\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    const quantity = parseInt(event.target.value, 10);\n\n    const updates = {\n      [this.dataset.lineItemId]: quantity\n    };\n\n    const previous = read();\n\n    // Child quantities are bound to parent via Shopify API, but we explicitly include them here for\n    // optimistic UI updates before the API response completes\n    for (const item of previous.items) {\n      if (item.parent_relationship?.parent_key === this.dataset.lineItemId) {\n        updates[item.key] = quantity;\n      }\n    }\n\n    const input: CartUpdateInput = {\n      updates\n    };\n    const scenario = 'cart-line-item-quantity-changed';\n    const dispatcher = this;\n    update(dispatcher, input, scenario, findGiftKeysToRemoveFromPage).catch(console.warn);\n  }\n\n  private onChangeGiftClick(_event: MouseEvent) {\n    type Detail = GlobalEventHandlersEventMap['offer-view-requested']['detail'];\n    const event = new CustomEvent<Detail>('offer-view-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        reason: 'change'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onRemoveLineItemClick(event: MouseEvent) {\n    console.debug('Cart line remove clicked', this.dataset.lineItemId);\n\n    if (event.target instanceof HTMLButtonElement && !event.target.disabled) {\n      const lineKeys = [this.dataset.lineItemId];\n      const scenario = 'cart-line-item-remove-click';\n      const dispatcher = this;\n\n      remove(dispatcher, lineKeys, scenario, findGiftKeysToRemoveFromPage).catch(console.warn);\n    }\n  }\n\n  private onCartTransactionStarted(_event: WindowEventMap['cart-transaction-started']) {\n    const actionElements = this.shadowRoot.querySelectorAll<HTMLButtonElement | HTMLSelectElement>(\n      '.cart-actions');\n    for (const actionElement of actionElements) {\n      actionElement.disabled = true;\n    }\n\n    const select = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n    if (select) {\n      select.disabled = true;\n    }\n\n    const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n    if (upgradeButton) {\n      upgradeButton.disabled = true;\n    }\n  }\n\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    const actionElements = this.shadowRoot.querySelectorAll<HTMLButtonElement | HTMLSelectElement>(\n      '.cart-actions');\n    for (const actionElement of actionElements) {\n      actionElement.disabled = false;\n    }\n\n    const select = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n    if (select) {\n      select.disabled = false;\n    }\n\n    const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n    if (upgradeButton) {\n      upgradeButton.disabled = false;\n    }\n  }\n\n  private render() {\n    const removeButton = this.shadowRoot.querySelector<HTMLElement>('.remove-line-item');\n\n    const lineItemEl = this.shadowRoot.querySelector('.line-item');\n    lineItemEl.id = this.dataset.variantId;\n\n    const primaryImageElement =\n      this.shadowRoot.querySelector<HTMLElementTagNameMap['image-element']>('#image');\n\n    if (!primaryImageElement.dataset.src) {\n      primaryImageElement.dataset.alt = '';\n      primaryImageElement.dataset.src = this.dataset.imageSrc;\n    }\n\n    // Child items (e.g. GWP) use smaller images to create visual hierarchy\n    if (this.dataset.parentId) {\n      primaryImageElement.dataset.width = '90';\n    }\n\n    // Possibly 'undefined' as a string type when passed an undefined value via data attribute\n    if (!this.dataset.imageSrc || this.dataset.imageSrc === 'undefined') {\n      console.warn('Product missing image', this.dataset.variantId, this.dataset.title);\n    }\n\n    if (this.dataset.productUrl) {\n      const featuredImageAnchor = this.shadowRoot.querySelector('clickable-area');\n      if (featuredImageAnchor) {\n        featuredImageAnchor.dataset.url = this.dataset.productUrl;\n      }\n    }\n\n    const lineItemTitle = this.shadowRoot.querySelector<HTMLAnchorElement>('#title');\n\n    if (this.dataset.productUrl) {\n      lineItemTitle.href = this.dataset.productUrl;\n    } else {\n      lineItemTitle.title = 'This product does not have a page to view';\n    }\n\n    // Although we can use textContent to assign the title text to the anchor element, we\n    // intentionally use innerHTML to ensure any HTML entities (i.e. &amp;) are decoded.\n    lineItemTitle.innerHTML = this.dataset.title;\n\n    const linePriceAfterDiscountEl =\n      this.shadowRoot.querySelector<HTMLElement>('#line-price-after-discount');\n    linePriceAfterDiscountEl.dataset.currencyCode = this.dataset.currencyCode;\n    linePriceAfterDiscountEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    linePriceAfterDiscountEl.dataset.subunits = this.dataset.linePriceAfterDiscount.toString();\n\n    // element with id line-price is the \"compare at price\"\n    const linePriceEl = this.shadowRoot.querySelector<HTMLElement>('#line-price');\n    linePriceEl.dataset.currencyCode = this.dataset.currencyCode;\n    linePriceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    linePriceEl.dataset.subunits = this.dataset.linePrice.toString();\n\n    if (parseInt(this.dataset.linePrice, 10) >\n      parseInt(this.dataset.linePriceAfterDiscount, 10)) {\n      this.shadowRoot.querySelector('.price').classList.remove('no-discount');\n      linePriceEl.dataset.ariaLabelPrefix = 'Regular price:';\n      linePriceAfterDiscountEl.dataset.ariaLabelPrefix = 'Sale price:';\n    } else {\n      this.shadowRoot.querySelector('.price').classList.add('no-discount');\n      linePriceEl.dataset.ariaLabelPrefix = '';\n      linePriceAfterDiscountEl.dataset.ariaLabelPrefix = 'Price:';\n    }\n\n    if (this.dataset.isAvailable === 'false') {\n      this.shadowRoot.querySelector<HTMLElement>('#out-of-stock').hidden = false;\n    }\n\n    if (this.dataset.sellingPlanId) {\n      // if this is a subscription item handle changing the selling plan\n      const select = this.querySelector<HTMLSelectElement>('.selling-plan-options');\n      if (select) {\n        select.addEventListener('change', this.onSellingPlanChangedBound);\n      }\n    } else {\n      // if not a subscription but selling plans are available - allow upgrading to a subscription\n      const upgradeButton = this.querySelector<HTMLButtonElement>('.upgrade-to-subscription');\n      if (upgradeButton) {\n        upgradeButton.addEventListener('click', this.onUpgradeButtonClickBound);\n      }\n    }\n\n    // Child items have quantity/delete controls hidden - Shopify API replicates all parent actions\n    // (quantity changes, deletions) to children automatically\n    if (this.dataset.parentId) {\n      const controlsRow = this.shadowRoot.querySelector<HTMLElement>('.line-item-bottom');\n      controlsRow.hidden = true;\n      return;\n    }\n\n    removeButton?.setAttribute('aria-label', ['remove', this.dataset.title, 'from cart'].join(' '));\n\n    this.renderQuantity();\n  }\n\n  /**\n   * When a user clicks the upgrade button, this sets the selling plan of the line item to the one\n   * specified in the button's selling plan id attribute.\n   *\n   * The upgrade button is not part of this element's template. The cart element creates it, sets\n   * data-selling-plan-id (the plan to upgrade to, which is not the same thing as our own data-\n   * selling-plan-id), and slots it into our light DOM. We read that attribute back here, hence the\n   * untyped dataset access. The comment where the parent sets it has the why.\n   *\n   * The parent creates the button as an HTMLButtonElement, and we read the current target rather\n   * than the target, so the instanceof check should always pass.\n   *\n   * Event listeners cannot be async, and promises must have explicit error handling. The underlying\n   * changeCart call emits errors via a side-channel error event instead of throwing. Therefore we\n   * basically ignore the error in the promise chain since it only happens in extreme edge cases.\n   */\n  private onUpgradeButtonClick(event: MouseEvent) {\n    if (event.currentTarget instanceof HTMLButtonElement) {\n      const input: CartChangeInput = {\n        id: this.dataset.lineItemId,\n        // Shopify resets the line to a quantity of 1 when the change leaves quantity out\n        quantity: parseInt(this.dataset.quantity, 10),\n        selling_plan: parseInt(event.currentTarget.dataset.sellingPlanId, 10)\n      };\n      const scenario = 'cart-line-item-upgrade-click';\n      const dispatcher = this;\n      change(dispatcher, input, scenario).catch(console.warn);\n    }\n  }\n\n  /**\n   * There is no more specific type of event for change events, so Event is the correct type. React\n   * has a virtual ChangeEvent type but that is not applicable here.\n   */\n  private onSellingPlanChanged(event: Event) {\n    // Documenting a previous bug for posterity. This previously was hard casting event.target to\n    // HTMLOptionElement. That is incorrect because change events are always fired on the select\n    // element, not its options. It was obscured because both HTMLSelectElement and\n    // HTMLOptionElement have a value property. Second, this now uses event.currentTarget instead of\n    // event.target. Both happened to be the same select here (the listener is on the select, and\n    // change does not bubble from the options), so this was not a live bug, but currentTarget is\n    // the more robust expression of intent: it is by definition the element the listener is bound\n    // to. Third, we prefer a runtime check over a hard cast precisely because it avoids this class\n    // of bugs.\n\n    if (event.currentTarget instanceof HTMLSelectElement) {\n      const input: CartChangeInput = {\n        id: this.dataset.lineItemId,\n        quantity: parseInt(this.dataset.quantity, 10),\n        selling_plan: parseInt(event.currentTarget.value, 10)\n      };\n      const scenario = 'cart-line-item-selling-plan-changed';\n      const dispatcher = this;\n\n      // We intentionally omit error handling here because the underlying cart module will handle\n      // it.\n      change(dispatcher, input, scenario).catch(console.warn);\n    }\n  }\n\n  private renderQuantity() {\n    const quantityElement = this.shadowRoot.querySelector<HTMLSelectElement>('.quantity');\n\n    if (this.dataset.isUpsell === 'true') {\n      // Only one item of an upsell product can be added, so no need to render the quantity selector\n\n      quantityElement.hidden = true;\n      return;\n    }\n\n    if (this.dataset.isAvailable === 'false') {\n      quantityElement.disabled = true;\n    }\n\n    // Since switching the quantity control to a select element, we should have predefined options\n    // in the HTML. For now, we've agreed to let users add up to 5 items, but this is negotiable.\n    const defaultMaxQuantity = 10;\n    const quantity = parseInt(this.dataset.quantity, 10);\n\n    let maxQuantity = defaultMaxQuantity;\n    if (this.dataset.maxQuantity) {\n      maxQuantity = parseInt(this.dataset.maxQuantity, 10);\n    }\n\n    if (maxQuantity < quantity) {\n      maxQuantity = quantity;\n    }\n\n    if (maxQuantity !== defaultMaxQuantity) {\n      // Currently, we set data-max-quantity to a value of 1 for the cart line item only for upsell\n      // products. That means in most cases, we will be rendering the default options, so this check\n      // prevents updating the DOM if not needed.\n\n      let optionsHtml = '';\n      for (let i = 1; i <= maxQuantity; i++) {\n        optionsHtml += `<option value=\"${i}\">${i}</option>`;\n      }\n\n      quantityElement.innerHTML = optionsHtml;\n    }\n\n    quantityElement.value = this.dataset.quantity;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-line-item': CartLineItem;\n  }\n}\n\nif (!customElements.get('cart-line-item')) {\n  customElements.define('cart-line-item', CartLineItem);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent, isCartUpdateErredEvent } from '../../../lib/cart-event-queue.js';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\ntype CartUpdateErredEvent = GlobalEventHandlersEventMap['cart-update-erred'];\ntype ProductVariant = CartUpdatedEvent['detail']['variants_added'][number];\n\n/**\n * Renders cart messages by building and appending alert-element elements.\n */\nclass CartMessages extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-clear'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Set from outside, such as when the drawer opens, to clear the messages. Setting it again\n     * clears them again, even with the same value. Removing it clears nothing.\n     */\n    clear: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCartUpdateErredBound = this.onCartUpdateErred.bind(this);\n  private initErrorAlert: HTMLElement;\n\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.shadowRoot.innerHTML = `<style>${styles}</style>`;\n  }\n\n  public connectedCallback() {\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      } else if (isCartUpdateErredEvent(event)) {\n        try {\n          this.onCartUpdateErredBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-update-erred', this.onCartUpdateErredBound);\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-update-erred', this.onCartUpdateErredBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-clear' && newValue !== null) {\n      this.onClearRequested();\n    }\n  }\n\n  /**\n   * The messages describe what just happened, so they are stale once the shopper has moved on. The\n   * init error stays, because the cart is still not loaded and its button is the only way to retry\n   * without reloading the page.\n   */\n  private onClearRequested() {\n    const alerts = this.shadowRoot.querySelectorAll('alert-element');\n    for (const alert of alerts) {\n      if (alert !== this.initErrorAlert) {\n        alert.remove();\n      }\n    }\n  }\n\n  private onCartUpdateErred(event: CartUpdateErredEvent) {\n    // Clear any previous messages\n    const alerts = this.shadowRoot.querySelectorAll('alert-element');\n    for (const alert of alerts) {\n      alert.remove();\n    }\n\n    const code = event.detail.code;\n\n    // We don't check whether the shopper has cookies turned off, since we don't trust the browser's\n    // answer enough to show an error over it. A shopper with cookies blocked or unsupported\n    // probably ends up here with the generic init error instead.\n    if (code === 'INIT' || code === 'INIT_TIMEOUT') {\n      this.showInitErrorMessage();\n    }\n\n    if (code === 'ADD_ITEMS') {\n      this.showErrorMessage('Oh no! There was a problem adding the items to your bag. ' +\n        'Please try again in a few moments.');\n    }\n\n    if (code === 'ADD_ITEMS_OOS') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'One or more of the products is out of stock.');\n    }\n\n    if (code === 'ADD_ITEMS_BUNDLE_ADDON_MIX') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'A bundle and an add-on cannot be added together. Try adding them one at a time.');\n    }\n\n    if (code === 'ADD_ITEMS_MAX_QUANTITY') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'The maximum quantity of this item is already in your cart.');\n    }\n\n    if (code === 'ADD_ITEMS_INVALID_SELLING_PLAN') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'One or more of the products is not available for subscription.');\n    }\n\n    if (code === 'ADD_ITEMS_INVALID_BACKORDER_SELLING_PLAN') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'The pre-order for one or more of the products is no longer available. ' +\n        'Try refreshing the page.');\n    }\n\n    if (code === 'ADD_ITEMS_INVALID_TBYB_SELLING_PLAN') {\n      this.showErrorMessage('Oh no! There was a problem adding the item(s) to your bag. ' +\n        'One or more of the products is no longer available to try before you buy. ' +\n        'Try refreshing the page.');\n    }\n\n    if (code === 'REMOVE_ITEMS' || code === 'REMOVE_ITEMS_DESYNC') {\n      this.showErrorMessage('Oh no! Something went wrong removing items from your bag! ' +\n        'Try refreshing the page and trying again.');\n    }\n\n    if (code === 'UPDATE_CART' || code === 'UPDATE_ITEMS_DESYNC' || code === 'CHANGE_CART' ||\n      code === 'CHANGE_ITEMS_DESYNC') {\n      this.showErrorMessage('Oh no! Something went wrong updating items in your bag! ' +\n        'Try refreshing the page and trying again.');\n    }\n\n    if (code === 'UPDATE_CART_OOS' || code === 'CHANGE_CART_OOS') {\n      this.showErrorMessage('Oh no! Something went wrong updating items in your bag! ' +\n        'One or more products are out of stock.');\n    }\n\n    // We only show a cart level error when the cart had no discount codes before and the code was\n    // not entered in the discount input. Otherwise the <discount-input> custom element shows its\n    // own message.\n    if (code === 'APPLY_DISCOUNTS' && Array.isArray(event.detail.inputs?.before_codes) &&\n      event.detail.inputs.before_codes.length === 0 &&\n      event.detail.scenario !== 'cart-discount-input') {\n      this.showErrorMessage('Oh no! Something went wrong applying your discount code.');\n    }\n\n    if (code === 'REMOVE_DISCOUNT') {\n      this.showErrorMessage('Oh no! Something went wrong removing your discount code.');\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    // We do not show change messages for the transition from a null cart to an initial cart state.\n    // The deltas are not meaningful.\n    //\n    // An init that failed before this one may have left its error showing, such as when this\n    // element replays the queue and finds the failure ahead of the retry that worked. The cart is\n    // here now, so we clear it.\n    if (event.detail.is_initial) {\n      const alerts = this.shadowRoot.querySelectorAll('alert-element');\n      for (const alert of alerts) {\n        alert.remove();\n      }\n\n      return;\n    }\n\n    // Skip announcements for updates the shopper did not initiate in this tab (state synced from\n    // another tab, or a bfcache restore catching up to the server). Announcing a delta they did not\n    // cause here would be wrong. This couples to specific scenario values by design.\n\n    const nonUserInitiatedScenarios = ['cross-tab-sync', 'bfcache-reconcile'];\n    if (nonUserInitiatedScenarios.includes(event.detail.scenario)) {\n      return;\n    }\n\n    // The free gift's own changes, such as adding the gift the shopper picked or removing it once\n    // the cart stops qualifying, happen on the way between views and speak for themselves. Skipping\n    // them also keeps up the message about the change the shopper made just before. An error is\n    // different, since a gift change that went through means an earlier failure, such as a gift\n    // that would not add, no longer describes the cart.\n    if (event.detail.scenario?.startsWith('gift-with-purchase')) {\n      const errors = this.shadowRoot.querySelectorAll('alert-element[data-alert-type=\"danger\"]');\n      for (const error of errors) {\n        error.remove();\n      }\n\n      return;\n    }\n\n    // We generally should not be mutating the dom one node at a time and should use some batch\n    // removal technique such as setting the parent element's innerHTML to an empty string. However\n    // there are extremely few messages so this is not a concern.\n\n    // Clear any previous messages\n    const alerts = this.shadowRoot.querySelectorAll('alert-element');\n    for (const alert of alerts) {\n      alert.remove();\n    }\n\n    // Show added items messages\n    if (event.detail.variants_added.length > 0) {\n      this.showItemsAddedMessage(event.detail.variants_added);\n    }\n\n    // Show removed items messages\n    if (event.detail.variants_removed.length > 0) {\n      const titles = event.detail.variants_removed.map(variant => variant.product_title);\n      const itemsString = titles.join(', ');\n      const message = document.createElement('div');\n\n      message.textContent = `You removed ${itemsString} from your shopping bag.`;\n      const alert = document.createElement('alert-element');\n      alert.dataset.alertType = 'success';\n      alert.appendChild(message);\n      this.shadowRoot.appendChild(alert);\n    }\n\n    this.showDiscountCodeAppliedMessages(event);\n    this.showDiscountRemovedMessages(event);\n  }\n\n  private showDiscountCodeAppliedMessages(event: CartUpdatedEvent) {\n    for (const code of event.detail.discount_codes_added) {\n      const message = `Applied discount code ${code}.`;\n      this.showSuccessMessage(message);\n    }\n  }\n\n  private showDiscountRemovedMessages(event: CartUpdatedEvent) {\n    for (const code of event.detail.discount_codes_removed) {\n      const message = `The discount ${code} was removed.`;\n      this.showSuccessMessage(message);\n    }\n  }\n\n  private showItemsAddedMessage(items: ProductVariant[]) {\n    const upsellItem = items.find(item => item.merged_properties._is_upsell);\n    if (upsellItem) {\n      this.showUpsellItemMessage(upsellItem);\n      return;\n    }\n\n    const backorderedItem = items.find(item => item.merged_properties._backorder_message);\n    if (backorderedItem) {\n      this.showBackorderItemMessage(backorderedItem);\n    }\n\n    const addedLineItemsString = items.map(item => item.product_title).join(', ');\n\n    const message = document.createElement('div');\n    message.innerHTML = `This is so great! You added ${addedLineItemsString} to your shopping` +\n      ' bag!';\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'success';\n    alert.appendChild(message);\n    this.shadowRoot.appendChild(alert);\n  }\n\n  private showBackorderItemMessage(item: ProductVariant) {\n    let message = item.merged_properties._backorder_message[0];\n    if (!message) {\n      message =\n        'This item is currently on backorder, estimated ship date is visible on product page';\n    }\n\n    const backorderMessage = document.createElement('div');\n    backorderMessage.textContent = `Important note about: ${item.product_title}. ${message}`;\n\n    const backorderAlert = document.createElement('alert-element');\n    backorderAlert.dataset.alertType = 'warning';\n    backorderAlert.appendChild(backorderMessage);\n\n    this.shadowRoot.appendChild(backorderAlert);\n  }\n\n  private showErrorMessage(messageHtml: string) {\n    const message = document.createElement('div');\n    message.textContent = messageHtml;\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'danger';\n    alert.appendChild(message);\n\n    this.shadowRoot.appendChild(alert);\n  }\n\n  /**\n   * Shows that the cart failed to load, with a button that asks cart init to run again so the\n   * shopper can recover without reloading the page.\n   */\n  private showInitErrorMessage() {\n    const message = document.createElement('div');\n    message.textContent = 'Something went wrong loading your shopping bag.';\n\n    const button = document.createElement('button');\n    button.type = 'button';\n    button.textContent = 'Reload my bag';\n    button.addEventListener('click', onReloadBagClick);\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'danger';\n    alert.append(message, button);\n\n    this.shadowRoot.appendChild(alert);\n    this.initErrorAlert = alert;\n  }\n\n  private showSuccessMessage(messageHtml: string) {\n    const message = document.createElement('div');\n    message.textContent = messageHtml;\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'success';\n    alert.appendChild(message);\n\n    this.shadowRoot.appendChild(alert);\n  }\n\n  private showUpsellItemMessage(item: ProductVariant) {\n    const message = document.createElement('div');\n    message.textContent = `${item.product_title} is a special ` +\n      'additional savings item. It can only be purchased together with a regular item and only ' +\n      'once per order.';\n\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertType = 'info';\n    alert.appendChild(message);\n\n    this.shadowRoot.appendChild(alert);\n  }\n}\n\n/**\n * Asks cart init to run again. Init lives in a different bundle, so we ask through an event on\n * window rather than calling it.\n *\n * We take the error away first, and the cart shows its loading message while init runs. If init\n * fails again, the error comes back with a new button, so the shopper can tell it failed.\n */\nfunction onReloadBagClick(event: MouseEvent) {\n  const button = event.currentTarget;\n  if (button instanceof HTMLButtonElement) {\n    button.closest('alert-element')?.remove();\n  }\n\n  const requestEvent = new Event('cart-init-requested');\n  dispatchEvent(requestEvent);\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-messages': CartMessages;\n  }\n}\n\nif (!customElements.get('cart-messages')) {\n  customElements.define('cart-messages', CartMessages);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * Renders the cart's totals. Displays the subtotal of all line items, the total discount amount,\n * and the discount percentage (if any).\n */\nclass CartTotals extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-estimated-total'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * ISO currency code.\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * Locale specific currency characters.\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n\n    /**\n     * The price before discounts less the price after discounts.\n     */\n    discountAmount: string;\n\n    /**\n     * The price after discounts divided by the price before discounts, as a percentage.\n     */\n    discountPercent: string;\n\n    /**\n     * The price after discounts but before taxes and shipping, which are unknown until checkout.\n     */\n    estimatedTotal: string;\n\n    /**\n     * The price before discounts.\n     */\n    subtotalAmount: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    // Hide discount row if discount is $0 (which is the same as subtotal = estimated total)\n    const discountRow = this.shadowRoot.querySelector<HTMLElement>('#discount-row');\n    if (this.dataset.discountAmount === '0') {\n      discountRow.hidden = true;\n    } else {\n      discountRow.hidden = false;\n    }\n\n    const subtotalEl = this.shadowRoot.querySelector<HTMLElement>('#subtotal');\n    subtotalEl.dataset.currencyCode = this.dataset.currencyCode;\n    subtotalEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    subtotalEl.dataset.subunits = this.dataset.subtotalAmount;\n\n    const discountTotalEl = this.shadowRoot.querySelector<HTMLElement>('#discount');\n    discountTotalEl.dataset.currencyCode = this.dataset.currencyCode;\n    discountTotalEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    discountTotalEl.dataset.subunits = this.dataset.discountAmount;\n\n    this.shadowRoot.querySelector('#discount-percentage').textContent =\n      `(${this.dataset.discountPercent}%)`;\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    const cart = event.detail.cart;\n    if (cart.items.length === 0) {\n      return;\n    }\n\n    const subtotal = computeSubtotal(cart);\n\n    this.dataset.subtotalAmount = subtotal.toString();\n\n    const applied = this.shadowRoot.querySelector<HTMLElement>('#order-discounts-row');\n    applied.style.display = 'none';\n\n    let orderDiscountsValue = 0;\n    for (const discount of cart.cart_level_discount_applications) {\n      if (discount.total_allocated_amount > 0) {\n        orderDiscountsValue += discount.total_allocated_amount;\n      }\n    }\n\n    if (orderDiscountsValue > 0) {\n      // If a cart level discount is applied, we unset display none to ensure \"Order Discounts\"\n      // heading is visible.\n      applied.style.display = 'flex';\n\n      const orderDiscountsEl = this.shadowRoot.querySelector<HTMLElement>('#order-discount-total');\n      orderDiscountsEl.dataset.currencySymbol = this.dataset.currencySymbol;\n      orderDiscountsEl.dataset.currencyCode = this.dataset.currencyCode;\n      orderDiscountsEl.dataset.subunits = orderDiscountsValue.toString();\n    }\n\n    // We cannot use the original discount amount because that is the difference between the sales\n    // price and the discounted price. The sales price is the implicitly discounted MSRP. We want\n    // the difference from the MSRP.\n\n    // The total after discounts, e.g. the estimated total less taxes and shipping that someone will\n    // pay, is the total_price property. Because total_price is floating point, we want to round to\n    // two significant digits.\n\n    const discountAmount = Math.round(100 * (subtotal - cart.total_price)) / 100;\n\n    this.dataset.discountAmount = discountAmount.toString();\n\n    // The discount percent must be manipulated to be based upon our custom subtotal so that it\n    // represents the reduction of the MSRP, not the sales price. We multiply by 100 so that we get\n    // a nice visual percentage (e.g. a 10% discount). We use floor instead of round so that we\n    // never over-represent the percentage. (e.g. 5.5% off is 5% off, not 6%).\n\n    const discountPercent = Math.floor(100 * discountAmount / subtotal);\n    this.dataset.discountPercent = discountPercent.toString();\n\n    // Set this attribute last because this triggers a render\n    this.dataset.estimatedTotal = cart.total_price.toString();\n  }\n}\n\n/**\n * The subtotal should be the total of the compare at price elements. Because we are working with\n * compare at prices, we cannot use the cart's price before discounts fields. We have to calculate\n * this on our own.\n *\n * There was an implementation choice to compute this in the server side rendered cart state in\n * liquid or to compute this here. It seems better to do it here because JavaScript is much more\n * expressive and the cost of performing the calculation here is not significant and because this\n * clearly documents the logic and is easier to maintain.\n */\nfunction computeSubtotal(cart: Cart) {\n  let subtotal = 0;\n  for (const item of cart.items) {\n    const compareAtPrice = parseInt(item.properties._value_set_compare_price, 10);\n    if (Number.isInteger(compareAtPrice)) {\n      subtotal += compareAtPrice;\n    } else {\n      subtotal += item.original_line_price;\n    }\n  }\n\n  // If doing some math somehow resulted in floating point junk, remove it. This generally does not\n  // happen in browsers that we tested but you never know.\n  subtotal = Math.floor(subtotal);\n\n  return subtotal;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-totals': CartTotals;\n  }\n}\n\nif (!customElements.get('cart-totals')) {\n  customElements.define('cart-totals', CartTotals);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * This element renders an upsell item with the ability for the user to add an upsell to cart.\n */\nclass CartUpsellItem extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-variant-id', 'data-product-tags', 'data-compare-at-price'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Compare at price\n     */\n    compareAtPrice: string;\n\n    /**\n     * Country ISO code\n     */\n    country: string;\n\n    /**\n     * Currency ISO code\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol character\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n\n    /**\n     * Product handle\n     */\n    handle: string;\n\n    /**\n     * Product id\n     */\n    id: string;\n\n    /**\n     * Product image url\n     */\n    image: string;\n\n    /**\n     * Product price\n     */\n    price: string;\n\n    /**\n     * CSV of product tags\n     */\n    productTags: string;\n\n    /**\n     * Product title, used together with the variant title to build the displayed upsell name.\n     */\n    productTitle: string;\n\n    /**\n     * Average product rating\n     */\n    reviewsAverage: string;\n\n    /**\n     * Number of product reviews\n     */\n    reviewsCount: string;\n\n    /**\n     * Variant SKU\n     */\n    sku: string;\n\n    /**\n     * Name of the Shopify template the upsell is being rendered within.\n     */\n    templateName: string;\n\n    /**\n     * Product title.\n     */\n    title: string;\n\n    /**\n     * Product type\n     */\n    type: string;\n\n    /**\n     * Id of the variant added to the cart when the upsell is accepted. Rendering is skipped until\n     * this is set.\n     */\n    variantId: string;\n\n    /**\n     * Variant title, appended to the product title unless it is the default variant title.\n     */\n    variantTitle: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.render();\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    if (!this.dataset.variantId) {\n      return;\n    }\n\n    const upsellLink = new URL(`products/${this.dataset.handle}`, location.origin);\n    const url = upsellLink.toString();\n    const title = this.buildTitle();\n\n    this.renderTitle(title, url);\n    this.renderImage(url);\n    this.renderPrices();\n    this.renderAddButton(title);\n    this.renderReviews(url);\n  }\n\n  private buildTitle() {\n    let title = this.dataset.productTitle;\n    if (this.dataset.variantTitle !== 'Default Title') {\n      title += ` ${this.dataset.variantTitle}`;\n    }\n\n    return title;\n  }\n\n  private renderTitle(title: string, url: string) {\n    const anchor = this.shadowRoot.querySelector<HTMLAnchorElement>('.title a');\n    anchor.href = url;\n    anchor.textContent = title;\n  }\n\n  private renderImage(url: string) {\n    const image = this.shadowRoot.querySelector<HTMLElement>('#image');\n    // the image is decorative (described by the adjacent text)\n    image.dataset.alt = '';\n    image.dataset.src = this.dataset.image;\n\n    const clickableArea = this.shadowRoot.querySelector('clickable-area');\n    clickableArea.dataset.url = url;\n  }\n\n  /**\n   * Some upsells carry their price in a product tag instead of on the variant, so we have to work\n   * out which price to actually show. The compare at price comes back empty when there is nothing\n   * to strike through.\n   */\n  private resolvePrices() {\n    let price = this.dataset.price;\n    let compareAtPrice = this.dataset.compareAtPrice;\n    const upsellTagSearch = this.dataset.currencyCode === 'CAD' ? 'UPSELL:CAD' : 'UPSELL';\n\n    // It is important to use optional chaining here because product tags is not guaranteed to be\n    // defined\n    const upsellTag = this.dataset.productTags?.split(' ').find(t => t.includes(upsellTagSearch));\n\n    if (upsellTag) {\n      const upsellPriceArray = upsellTag.split(':');\n      const upsellPriceString = this.dataset.currencyCode === 'CAD' ?\n        upsellPriceArray[2] :\n        upsellPriceArray[1];\n      const upsellPrice = parseFloat(upsellPriceString) * 100;\n\n      // Not all upsell items require their price and compareAtPrice values to be updated before\n      // being presented to the user. To prevent overwriting already properly priced products, we\n      // must confirm the extracted upsell price from the product tags is a lower value. If so we\n      // proceed with updating the compareAtPrice.\n      if (upsellPrice < parseInt(price, 10) && parseInt(price, 10) > parseInt(compareAtPrice, 10)) {\n        compareAtPrice = price;\n      }\n\n      price = upsellPrice.toString();\n    }\n\n    return { price, compareAtPrice };\n  }\n\n  private renderPrices() {\n    const prices = this.resolvePrices();\n\n    const priceEl = this.shadowRoot.querySelector<HTMLElement>('#price');\n    priceEl.dataset.currencyCode = this.dataset.currencyCode;\n    priceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    priceEl.dataset.subunits = prices.price;\n\n    const compareAtPriceEl = this.shadowRoot.querySelector<HTMLElement>('#compare-at-price');\n    compareAtPriceEl.dataset.currencyCode = this.dataset.currencyCode;\n    compareAtPriceEl.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    if (prices.compareAtPrice) {\n      compareAtPriceEl.dataset.subunits = prices.compareAtPrice;\n      compareAtPriceEl.dataset.hide = 'false';\n      compareAtPriceEl.dataset.ariaLabelPrefix = 'Regular price:';\n      priceEl.dataset.ariaLabelPrefix = 'Sale price:';\n    } else {\n      // hide the compare\n      compareAtPriceEl.dataset.hide = 'true';\n      compareAtPriceEl.dataset.ariaLabelPrefix = '';\n      priceEl.dataset.ariaLabelPrefix = 'Price:';\n    }\n  }\n\n  private renderAddButton(title: string) {\n    const addButton = this.shadowRoot.querySelector('add-to-cart-button');\n    addButton.dataset.country = this.dataset.country;\n    addButton.dataset.variantTitle = title;\n\n    if (this.shadowRoot.querySelector(`variant-element[data-id=\"${this.dataset.variantId}\"]`)) {\n      // do not add\n    } else {\n      const customAttribute = document.createElement('custom-attribute');\n      customAttribute.dataset.key = '_is_upsell';\n      customAttribute.dataset.value = 'true';\n\n      const variant = document.createElement('variant-element');\n      variant.slot = 'variants';\n      variant.dataset.id = this.dataset.variantId;\n      variant.dataset.quantity = '1';\n      variant.appendChild(customAttribute);\n\n      addButton.appendChild(variant);\n    }\n\n    addButton.dataset.update = Date.now().toString();\n  }\n\n  private renderReviews(url: string) {\n    const reviews = this.shadowRoot.querySelector('product-rating-stars');\n    reviews.dataset.average = this.dataset.reviewsAverage;\n    reviews.dataset.count = this.dataset.reviewsCount;\n    reviews.dataset.href = `${url}#reviews-section`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'cart-upsell-item': CartUpsellItem;\n  }\n}\n\nif (!customElements.get('cart-upsell-item')) {\n  customElements.define('cart-upsell-item', CartUpsellItem);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * The container for the upsell suggestions shown in the cart drawer. The individual suggestions are\n * cart upsell item children.\n *\n * This exists to keep the suggestions relevant as the cart changes. On every cart update it hides\n * any item already in the cart, shows at most four, and collapses itself when none are left to\n * suggest.\n */\nclass CartUpsellItems extends HTMLElement implements CustomElement {\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    // The animation frame ensures the element has actually rendered.\n    requestAnimationFrame(() => {\n      const event = new Event('cart-upsell-items-rendered', { bubbles: true, composed: true });\n      this.dispatchEvent(event);\n    });\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    const variantIds = event.detail.cart.items.map(item => item.variant_id);\n    const elements = this.querySelectorAll('cart-upsell-item');\n    let visibleCount = 0;\n\n    for (const element of elements) {\n      const variantId = parseInt(element.dataset.variantId, 10);\n      const show = !variantIds.includes(variantId) && visibleCount < 4;\n\n      if (show) {\n        // if upsellItemElement is not included in the cart unhide it\n        element.style.display = 'flex';\n        visibleCount++;\n      } else {\n        element.style.display = 'none';\n      }\n    }\n\n    // Hide upsell items section if it is empty\n    if (visibleCount === 0) {\n      this.style.display = 'none';\n    } else {\n      this.style.display = 'block';\n    }\n  }\n}\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    /**\n     * Dispatched by the upsell items element whenever the upsell items element is rendered.\n     */\n    'cart-upsell-items-rendered': Event;\n  }\n\n  interface HTMLElementTagNameMap {\n    'cart-upsell-items': CartUpsellItems;\n  }\n}\n\nif (!customElements.get('cart-upsell-items')) {\n  customElements.define('cart-upsell-items', CartUpsellItems);\n}\n","/**\n * Currency Helpers\n * -----------------------------------------------------------------------------\n * A collection of useful functions that help with currency formatting\n *\n * Current contents\n * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value.\n *\n */\n\nconst moneyFormat = '${{amount}}';\n\n/**\n * Format money values based on your shop currency settings\n * @param  {Number|string} cents - value in cents or dollar amount e.g. 300 cents\n * or 3.00 dollars\n * @param  {String} format - shop money_format setting\n * @return {String} value - formatted value\n */\nexport function formatMoney(cents, format) {\n  if (typeof cents === 'string') {\n    cents = cents.replace('.', '');\n  }\n  let value = '';\n  const placeholderRegex = /\\{\\{\\s*(\\w+)\\s*\\}\\}/;\n  const formatString = format || moneyFormat;\n\n  function formatWithDelimiters(\n    number,\n    precision = 2,\n    thousands = ',',\n    decimal = '.'\n  ) {\n    if (isNaN(number) || number == null) {\n      return 0;\n    }\n\n    number = (number / 100.0).toFixed(precision);\n\n    const parts = number.split('.');\n    const dollarsAmount = parts[0].replace(\n      /(\\d)(?=(\\d\\d\\d)+(?!\\d))/g,\n      `$1${thousands}`\n    );\n    const centsAmount = parts[1] ? decimal + parts[1] : '';\n\n    return dollarsAmount + centsAmount;\n  }\n\n  switch (formatString.match(placeholderRegex)[1]) {\n    case 'amount':\n      value = formatWithDelimiters(cents, 2);\n      break;\n    case 'amount_no_decimals':\n      value = formatWithDelimiters(cents, 0);\n      break;\n    case 'amount_with_comma_separator':\n      value = formatWithDelimiters(cents, 2, '.', ',');\n      break;\n    case 'amount_no_decimals_with_comma_separator':\n      value = formatWithDelimiters(cents, 0, '.', ',');\n      break;\n  }\n\n  return formatString.replace(placeholderRegex, value);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { formatMoney } from '@shopify/theme-currency';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * This element is responsible for showing a free shipping message. It reacts to a cart updated\n * event to calculate if the cart qualifies for free shipping.\n *\n * The free shipping is applicable when the estimated cart total reaches a certain threshold or when\n * there is a subscription item in the cart.\n */\nclass FreeShippingBanner extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Text to show when free shipping is unlocked because of a subscription item in the cart\n     */\n    subscriptionText: string;\n\n    /**\n     * Minimum cart value for free shipping (in cents).\n     */\n    threshold: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Estimated customer cart value, exact value is calculated on checkout shipping step (in cents).\n   */\n  private estimatedTotal = 0;\n\n  /**\n   * Whether there is a subscription item in the cart, which unlocks free shipping regardless of the\n   * cart value.\n   */\n  private subscriptionsInCart = false;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    const cart = event.detail.cart;\n    this.estimatedTotal = cart.total_price;\n\n    let subscriptionsInCart = false;\n    for (const item of cart.items) {\n      if (item?.selling_plan_allocation?.selling_plan?.recurring_deliveries) {\n        subscriptionsInCart = true;\n        break;\n      }\n    }\n\n    this.subscriptionsInCart = subscriptionsInCart;\n\n    this.render();\n  }\n\n  private render() {\n    const span = this.shadowRoot.querySelector('span');\n    const progressContainer = this.shadowRoot.querySelector('.progress-container');\n    const progressBar = this.shadowRoot.querySelector<HTMLElement>('.progress-bar');\n\n    span.textContent = this.getMessageText();\n\n    let percentage: number;\n    if (this.subscriptionsInCart) {\n      percentage = 100;\n    } else {\n      const threshold = parseInt(this.dataset.threshold, 10);\n      percentage = Math.min(100, (this.estimatedTotal / threshold) * 100);\n    }\n\n    percentage = Math.round(percentage);\n    if (isNaN(percentage)) {\n      percentage = 0;\n    }\n\n    progressContainer.setAttribute('aria-valuenow', percentage.toString());\n    progressBar.style.width = percentage + '%';\n  }\n\n  private getMessageText() {\n    if (this.subscriptionsInCart) {\n      return this.dataset.subscriptionText ||\n        'Free shipping unlocked with subscription item!';\n    }\n\n    const threshold = parseInt(this.dataset.threshold, 10);\n    const difference = threshold - this.estimatedTotal;\n\n    if (difference > 0) {\n      // eslint-disable-next-line no-template-curly-in-string\n      return 'Add ' + formatMoney(difference, '${{amount}}') +\n        ' to unlock Free Contiguous US Shipping!';\n    } else {\n      return 'Congratulations! You\\'ve unlocked Free Contiguous US Shipping!';\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'free-shipping-banner': FreeShippingBanner;\n  }\n}\n\nif (!customElements.get('free-shipping-banner')) {\n  customElements.define('free-shipping-banner', FreeShippingBanner);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Tells a shopper whose cart qualifies for a free gift, and has none yet, that they can still pick\n * one. Clicking it asks for the offer view. It renders hidden, and the gift with purchase progress\n * bar shows and hides every copy of it on the page as the cart changes.\n */\nclass GiftQualifiedMessage extends HTMLElement implements CustomElement {\n  private onClickBound = this.onClick.bind(this);\n\n  public connectedCallback() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n\n    this.shadowRoot.querySelector('button').addEventListener('click', this.onClickBound);\n  }\n\n  public disconnectedCallback() {\n    this.shadowRoot.querySelector('button')?.removeEventListener('click', this.onClickBound);\n  }\n\n  private onClick(_event: MouseEvent) {\n    type Detail = GlobalEventHandlersEventMap['offer-view-requested']['detail'];\n    const event = new CustomEvent<Detail>('offer-view-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        reason: 'chosen'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'gift-qualified-message': GiftQualifiedMessage;\n  }\n}\n\nif (!customElements.get('gift-qualified-message')) {\n  customElements.define('gift-qualified-message', GiftQualifiedMessage);\n}\n","import { type Cart, cartUpdate } from '@integrabeauty/shopify-ajax-api';\nimport { cartFetch } from '../state.js';\nimport { getApplicableDiscountCodes, getDiscountCodes, isWellFormed } from './discount.js';\n\ntype DiscountOverride = typeof cart_lifecycle.discount_overrides[number];\n\n/**\n * Updates a cart's discount codes. This uses a specially enabled feature on the lange Shopify shop\n * that enables the use of the cart update ajax api to modify the cart's discounts. The special\n * feature enables the use of a discount property in the update call.\n *\n * New codes are partially validated before the http request is sent. If any new code is invalid,\n * this throws an error immediately.\n *\n * This will send an update request even if the discount codes have not changed. This does not check\n * whether sending the request is pointless. One of the reasons why this check is not performed is\n * because sometimes callers want to call this function even when it seems pointless because of the\n * risk that client side state is out of sync with server side state. That kind of thing can happen\n * for example when browsing the site in multiple tabs, because client side state is largely managed\n * per tab, so one tab may naively update discounts without awareness of changes made in other tabs.\n * While we attempt to keep state synchronized across tabs, there is no guarantee.\n *\n * The special discount property of the update request body has no documentation. We figured out\n * that it works similar to the checkout links discount url parameter. Multiple codes can be\n * specified as a comma separated value. This also means that codes should not contain commas. We\n * also figured out that it overwrites all discounts. This does not just add new discounts. That is\n * why this requires both the set of existing codes and the new codes.\n *\n * @param cart the state of the cart prior to update\n * @param newCodes the codes that should be applied to the cart in addition to the existing codes\n * @param overrides discount overrides, this is expected to always be defined even if there are no\n * overrides\n * @param postValidate whether to check if the new codes were actually applied after the update\n *\n * @see https://help.shopify.com/en/manual/discounts/managing-discount-codes\n */\nexport async function applyDiscountsHelper(cart: Cart, newCodes: string[],\n  overrides: DiscountOverride[], postValidate = true) {\n  validateCodesBeforeUpdate(newCodes);\n\n  const oldUppers = getCurrentCodes(cart, newCodes.length);\n  const newUppers = newCodes.map(code => code.toUpperCase());\n\n  const retainedOldUppers = oldUppers.filter(code =>\n    !overrides.some(override =>\n      override.old_code === code && newUppers.includes(override.new_code)));\n\n  const mergedUppers = new Set<string>([...retainedOldUppers, ...newUppers]);\n  const mergedCodes = [...mergedUppers];\n\n  const discount = mergedCodes.join(',');\n  await cartUpdate({ discount });\n  const updatedCart = await cartFetch();\n\n  // Only validate if post validation requested. Validation is performed by default but there are\n  // some use cases where discounts are applied even in inapplicable situations, such as when the\n  // cart is empty (the discount is applied but if you look at the discount object in the\n  // discount_codes array the applicable property is false). By only validating if requested we\n  // avoid generating routine validation errors.\n\n  if (postValidate) {\n    validateCodesAfterUpdate(updatedCart, newCodes);\n  }\n\n  return updatedCart;\n}\n\n/**\n * Obtain the existing discount codes. Generally, we want to retain all codes, including currently\n * inapplicable codes, as those codes may become applicable later and have been deemed applied even\n * though they have no effect. However, retaining all codes leads to the possible accumulation of\n * several inapplicable codes. This leads into a poorly documented issue. Once there are about 5 or\n * 6 codes applied, in either the applicable or inapplicable state, future applications fail without\n * regard to code applicability.\n *\n * Therefore, this needs to take into account the total number of discount codes that will be sent\n * as a part of a cart update. This is why the new codes array is also an input to this function\n * that has only to do with old codes.\n *\n * This is a hacky rushed fix that needs to be revisited. Expired codes stick around. Typo codes\n * stick around. Once we reach more than 5 codes (in any state), all future applications, even of\n * valid codes, incorrectly fail to apply. So in this case, we have to filter down the list. The\n * quickest thing we can do is just drop codes that are applied but in the inapplicable state. We\n * prefer not to do this because we want to support visitors who apply a code, it becomes applied\n * but inapplicable, then add a product, and it becomes applicable.\n *\n * This hacky fix is not perfect because it is not deduplicating. This is an inaccurate and quick\n * fix for \"several\" issues, not all of the issues stemming from bad codes sticking around.\n *\n * There is a limitation on the number of discount codes that can be applied to the same order,\n * including codes in the inapplicable state. According to Shopify, \"Customers can use a maximum of\n * five product or order discount codes and one shipping discount code on the same order.\"\n *\n * @see https://help.shopify.com/en/manual/discounts/combining-discounts/discount-combinations#limitations\n */\nfunction getCurrentCodes(cart: Cart, newCodesCount: number) {\n  const currentCodes = Array.isArray(cart.discount_codes) ? cart.discount_codes : [];\n\n  // We test against what would be the total number of applied codes in determining which codes\n  // should be included in the output. If adding the new codes would exceed the threshold, then we\n  // try our best to trim the number of currently applied codes down to below the threshold. Since\n  // we cannot tell which discount codes are shipping discount codes, we use the threshold of 5\n  // instead of 6.\n\n  // There is currently no reliable additional way of choosing which discount codes to remove. There\n  // is no clear documentation on the order of the codes in the array. So we have to choose an\n  // arbitrary method of ejecting some codes. For now, this basically just gets rid of any codes\n  // that are inapplicable. That will most likely remove typos and expired codes. Most visitors will\n  // not discern the missing codes.\n\n  // This does not guarantee that the output is reduced to below the threshold. If there are 5\n  // applicable codes, then no codes will be removed, and the 6th application can still fail.\n\n  // This is not trying to get down to just below the threshold. This does a whole sweep. We might\n  // want to change this to remove inapplicable codes up until the threshold is no longer exceeded.\n\n  // Drop all inapplicable codes by keeping around only those older codes that are applicable. Try\n  // to retain inapplicable shipping codes.\n\n  if (currentCodes.length + newCodesCount > 5) {\n    return currentCodes\n      .filter(code => code.applicable || /ship/i.test(code.code))\n      .map(code => code.code)\n      .map(code => code.toUpperCase());\n  }\n\n  return getDiscountCodes(cart);\n}\n\nfunction validateCodesBeforeUpdate(newCodes: string[]) {\n  if (newCodes.length < 1) {\n    throw new Error('At least one discount code must be specified');\n  }\n\n  // If any one of the input codes is invalid, reject with a specific error message that mentions\n  // the code. It is important to be explicit here because this error may be visible to users.\n\n  for (const code of newCodes) {\n    if (!isWellFormed(code)) {\n      const error: any = new Error(`Invalid discount code ${code}`);\n      error.discount_code = code;\n      error.discount_codes = newCodes;\n      throw error;\n    }\n  }\n}\n\n/**\n * When validating whether the codes applied, we only validate whether new codes applied. We do not\n * validate whether existing codes applied, because it is possible that applying a new code caused a\n * previous code to be removed, because the new code replaces the existing code, for example,\n * because the new code offers a better discount.\n *\n * If a code was applied but in the inapplicable state, then we treat the add as a failure, even\n * though the code persists in the discount_codes array. The rationale is complex. Basically, we\n * must address the UX concern that we must provide feedback that an invalid code was applied, so we\n * must trigger an error. However, some codes, such as shipping codes, can be early applied. Some\n * codes, such as shipping codes, can apply but have no cart level applications or line level\n * allocations, but still be applied, and still be valid, even though the code is not actually\n * affecting the estimated total. We do not want to treat that as an error.\n *\n * For a shipping discount that is applicable, even though it has no effect, it will appear in the\n * discount_codes array with applicable true.\n *\n * For a fictional discount that is inapplicable, it will appear in the discount_codes array with\n * applicable false.\n */\nfunction validateCodesAfterUpdate(cart: Cart, newCodes: string[]) {\n  const updatedNormalCodes = getApplicableDiscountCodes(cart);\n\n  // Downstream analytics sometimes does a breakdown on the error message. Input new codes are in\n  // mixed cases, leading to nearly duplicate messages. To make it easier on analysis, we report on\n  // the normalized discount code, not the actual characters entered by visitors.\n\n  for (const newCode of newCodes) {\n    const upperNewCode = newCode.toUpperCase();\n    if (!updatedNormalCodes.includes(upperNewCode)) {\n      const error: any = new Error(`The discount code ${upperNewCode} was not applied`);\n      error.discount_code = upperNewCode;\n      error.discount_codes = newCodes;\n      throw error;\n    }\n  }\n}\n","import { assert } from '@integrabeauty/assert';\nimport { type AddItem, isUnavailableError, cartAdd, type Cart, isInvalidSellingPlanError, isMaxQuantityError, isBundleAddonMixError } from '@integrabeauty/shopify-ajax-api';\nimport { copy } from '../object.js';\nimport { now } from '../perf.js';\nimport { isWellFormed } from './discount/discount.js';\nimport { applyDiscountsHelper } from './discount/helper.js';\nimport { report } from './error.js';\nimport { cartFetch, read, write } from './state.js';\nimport { start, type Transaction } from './transaction.js';\n\n/**\n * Adds items to the cart. Emits an event when completed.\n *\n * This fails if the request to add fails, or if the request to get the updated state fails.\n *\n * This must perform multiple http requests due to limitations in Shopify's ajax cart api. The first\n * fetch mutates, and the second fetch gets the updated state. Unfortunately, this means that it is\n * possible for the mutation to succeed but for the subsequent fetch to fail. Callers should be wary\n * of this situation. On some future successful retrieval of server side state, the added items will\n * appear, despite this function emitting an error event. This means, for example, that callers\n * should not assume that failure to add is necessarily a real failure.\n *\n * If a discount is specified to be applied on add, it is applied as a part of the same transaction.\n * However, this also introduces some unwanted complexity. This function does not emit an error if\n * applying the discount fails, because this must preserve client side to server side state\n * fidelity, and the second fetch call happens after the first, and Shopify does not support multi-\n * request transactions (e.g. batched conditional requests). There is no simple way to rollback a\n * partial commit because the rollback itself could also fail as it would be an additional request.\n *\n * The new discount is not applied if it appears as existing. This avoids an unnecessary request.\n * The cart's discount_codes array automatically updates the applicable state of applied discount\n * codes as a result of adding new items. There is no need to reapply to trigger the transition from\n * inapplicable to applicable.\n *\n * At the moment, it is unclear whether this should be fetching cart state prior to applying new\n * discounts. Local cart state is assumed to be accurate, so the existing discount codes should be\n * preserved, so there should not be a need, so this does not perform the additional fetch.\n *\n * @param target the element most directly responsible for the add, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param items the items to add\n * @param discountCodes optional discount codes to attempt to apply to the cart after adding the\n * items\n * @param scenario optional description of the context of the add to cart, e.g. the type of ui\n * element or ux experience, that is used by downstream analytics\n */\nexport async function add(target: EventTarget | null, items: AddItem[], discountCodes?: string[],\n  scenario?: string) {\n  const transaction = start('addItems', items, scenario);\n\n  try {\n    await addItemsInternal(target, transaction, items, discountCodes, scenario);\n  } catch (error) {\n    report(target, {\n      code: determineErrorCode(items, error),\n      error,\n      inputs: {\n        discount_code: discountCodes,\n        items\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n\nasync function addItemsInternal(target: EventTarget | null, transaction: Transaction,\n  items: AddItem[], discountCodes: string[], scenario: string) {\n  assert(items.length > 0, 'At least one item must be specified to add');\n\n  const previous = read();\n\n  const enrichedItems = items.map(enrich);\n\n  // An add can outlast the transaction timeout, and then nothing says which of its requests\n  // stalled. So we log each step with how long it took. When a later cart error is reported, the\n  // steps missing from its breadcrumbs point at the request that never came back.\n\n  let stepStart = now();\n  await cartAdd(enrichedItems);\n  console.log('Cart add request finished', Math.round(now() - stepStart));\n\n  // We ignore the output of cart/add.js for several reasons. One reason is that we need the full\n  // cart state, but add.js output only contains an items property. We cannot safely merge the new\n  // items with the local state because multiple changes can happen to a cart in multiple tabs, and\n  // because it is not possible to compare new line items to old line items by line item key because\n  // we have observed line item key shifting, line item splitting, and other surprises. Another\n  // reason is that the response body includes zombie items (0 quantity items) from failed adds.\n\n  // If applying a discount code on add, then we will use the output of that call as the new cart\n  // state. If applying the discount fails, we still treat the add to cart as a success, and\n  // fallback to fetching the new cart state. We treat failure to apply the discount in this case as\n  // non-fatal to add to cart because we have effectively already updated cart state and must\n  // reflect that state change even though the discount failed to apply.\n\n  stepStart = now();\n  let cart = await applyDiscountsOnAdd(previous, discountCodes || [], scenario);\n  console.log('Cart add discount step finished', Math.round(now() - stepStart));\n\n  if (!cart) {\n    stepStart = now();\n    cart = await cartFetch();\n    console.log('Cart add fetch finished', Math.round(now() - stepStart));\n  }\n\n  write(target, cart, transaction, scenario);\n}\n\n/**\n * Try to apply some discount codes to the cart.\n *\n * Returns null when there was no mutation.\n */\nasync function applyDiscountsOnAdd(beforeCart: Cart, discountCodes: string[], scenario?: string) {\n  // We tolerate pointless calls so as to reduce caller boilerplate\n\n  if (discountCodes.length < 1) {\n    return null;\n  }\n\n  // If any input discount code is malformed, do nothing. This does not throw.\n\n  for (const code of discountCodes) {\n    if (!isWellFormed(code)) {\n      const error: any = new Error('Malformed discount code');\n      error.scenario = scenario;\n      error.discount_codes = discountCodes;\n      error.discount_code = code;\n      console.warn(error);\n      return null;\n    }\n  }\n\n  // This is currently using local state. There are two modes for local state. There is the initial\n  // server side render, and there is the state after init. The states are subtly different. On\n  // initial server side render the cart is missing the discount_codes array because that is not\n  // accessible to liquid. The cart init logic was changed to do a fetch of cart state on every page\n  // load just to get the discount_codes array. Sometimes that fetch fails, leaving the server side\n  // rendered state as is, in which case the discount_codes property is undefined. This must account\n  // for that. In doing so, opt out of apply on add entirely, as any apply discounts call must\n  // specify all codes to apply, including existing codes. As we do not know what the previous codes\n  // are when init partially failed, it is better to just skip applying than to unintentionally\n  // cause existing codes to drop.\n\n  if (!Array.isArray(beforeCart.discount_codes)) {\n    return null;\n  }\n\n  // As a part of this function's contract, it returns null to signal that a new cart state was not\n  // obtained. Therefore, as tempting as it might be, do not init afterCart to beforeCart.\n\n  let afterCart: Cart = null;\n\n  const overrides = cart_lifecycle.discount_overrides;\n\n  // Try to apply. Post validation is disabled because this is a form of best effort automatic\n  // application where errors are routine and not fatal.\n\n  try {\n    afterCart = await applyDiscountsHelper(beforeCart, discountCodes, overrides, false);\n  } catch (error) {\n    console.warn(error);\n    // continue\n  }\n\n  return afterCart;\n}\n\n/**\n * Classifies a caught add-to-cart failure into a cart error code.\n *\n * Shopify rejects an invalid selling plan with the same generic description regardless of what the\n * plan was for, so the specific code is disambiguated from the items we sent rather than from the\n * error itself. At render time each line item is tagged with a type-specific property: a backorder\n * message for pre-order plans and a marker for try-before-you-buy plans. A selling-plan rejection\n * carrying neither marker keeps the generic code, because nothing on the item positively identifies\n * it as anything more specific.\n */\nfunction determineErrorCode(items: AddItem[], error: any) {\n  if (isUnavailableError(error)) {\n    return 'ADD_ITEMS_OOS';\n  }\n\n  if (isBundleAddonMixError(error)) {\n    return 'ADD_ITEMS_BUNDLE_ADDON_MIX';\n  }\n\n  if (isMaxQuantityError(error)) {\n    return 'ADD_ITEMS_MAX_QUANTITY';\n  }\n\n  if (isInvalidSellingPlanError(error)) {\n    let item: AddItem;\n    if (Array.isArray(items)) {\n      item = items.find(line => line?.selling_plan);\n    }\n\n    if (item?.properties?._backorder_message) {\n      return 'ADD_ITEMS_INVALID_BACKORDER_SELLING_PLAN';\n    }\n\n    if (item?.properties?._is_tbyb) {\n      return 'ADD_ITEMS_INVALID_TBYB_SELLING_PLAN';\n    }\n\n    return 'ADD_ITEMS_INVALID_SELLING_PLAN';\n  }\n\n  return 'ADD_ITEMS';\n}\n\nfunction enrich(input: AddItem) {\n  // Clone so that the enrichment below does not mutate the caller's input.\n  const output = copy(input);\n  output.properties = output.properties || {};\n  output.properties._url_path = output.properties._url_path || location.pathname;\n  return output;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { AddItem, Item } from '@integrabeauty/shopify-ajax-api';\nimport { v4 as uuidv4 } from 'uuid';\nimport { add } from '../../../lib/cart/add.js';\nimport { remove } from '../../../lib/cart/remove.js';\nimport { update } from '../../../lib/cart/update.js';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype GiftWithPurchaseProduct = HTMLElementTagNameMap['gift-with-purchase-product'];\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * A view of the cart drawer offering the shopper something they just qualified for. Today the only\n * offer is the gift with purchase, so the view is a free gift picker. The gift with purchase\n * progress bar asks for it right after a cart change the shopper made unlocks the gift. The message\n * saying the shopper qualified asks for it when clicked, and so does the Change link on the gift\n * line.\n *\n * The shopper picks one of the gifts, or skips. Skipping a fresh offer, or closing the drawer on\n * it, is remembered in a cart attribute the progress bar reads, so the offer does not come back on\n * every cart change. Skipping a change keeps the gift the shopper already has.\n *\n * When there is only one gift there is nothing to pick, so this adds it without showing the view.\n */\nclass OfferView extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-focus-requested'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * \"embedded\" on the cart page, where there is no drawer to close. Set in the liquid.\n     */\n    layout: string;\n\n    /**\n     * \"change\" when the shopper is swapping the gift they already have, or \"unlock\" when they are\n     * being offered one. Set each time the view is asked for.\n     */\n    mode: 'change' | 'unlock';\n\n    /**\n     * The view to go back to when the shopper is done here, such as \"upsell\". Set each time the\n     * view is asked for.\n     */\n    returnView: string;\n\n    /**\n     * \"true\" while the shopper's pick is being saved to the cart.\n     */\n    saving: string;\n  };\n\n  /**\n   * The gift line in the cart, if there is one.\n   */\n  private declare claimedGift: Item;\n\n  /**\n   * The variant of the gift selected in the picker.\n   */\n  private declare selectedVariantId: number;\n\n  private onCartDrawerClosedBound = this.onCartDrawerClosed.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n  private onConfirmClickedBound = this.onConfirmClicked.bind(this);\n  private onGiftClickedBound = this.onGiftClicked.bind(this);\n  private onOfferViewRequestedBound = this.onOfferViewRequested.bind(this);\n  private onSkipClickedBound = this.onSkipClicked.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.shadowRoot.querySelector('close-button').addEventListener('click',\n      this.onCloseClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#confirm').addEventListener('click',\n      this.onConfirmClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#skip').addEventListener('click',\n      this.onSkipClickedBound);\n\n    for (const gift of this.querySelectorAll('gift-with-purchase-product')) {\n      gift.addEventListener('click', this.onGiftClickedBound);\n    }\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-drawer-closed', this.onCartDrawerClosedBound);\n    addEventListener('offer-view-requested', this.onOfferViewRequestedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    this.shadowRoot.querySelector('close-button')?.removeEventListener('click',\n      this.onCloseClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#confirm')?.removeEventListener('click',\n      this.onConfirmClickedBound);\n    this.shadowRoot.querySelector<HTMLElement>('#skip')?.removeEventListener('click',\n      this.onSkipClickedBound);\n\n    for (const gift of this.querySelectorAll('gift-with-purchase-product')) {\n      gift.removeEventListener('click', this.onGiftClickedBound);\n    }\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-drawer-closed', this.onCartDrawerClosedBound);\n    removeEventListener('offer-view-requested', this.onOfferViewRequestedBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * See the upsell view for why focus is forced visible here.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    this.claimedGift = event.detail.cart.items.find(\n      item => item.properties._gift_with_purchase === 'true');\n  }\n\n  private onOfferViewRequested(event: GlobalEventHandlersEventMap['offer-view-requested']) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    const gifts = this.querySelectorAll('gift-with-purchase-product');\n    if (gifts.length === 0) {\n      return;\n    }\n\n    const drawer = this.closest('cart-drawer');\n    const view = drawer.dataset.view || 'cart';\n    const reason = event.detail?.reason;\n\n    // An unlock only interrupts a shopper who is looking at their bag. Over the upsell view it\n    // would get in the way of checkout, so the message saying they qualified waits there instead.\n    // The progress view counts as the bag, since it is up while an add from a product page runs.\n    if (reason === 'unlocked') {\n      const shown = drawer.dataset.layout === 'embedded' || drawer.dataset.modalState === 'open';\n      if (!shown || (view !== 'cart' && view !== 'progress')) {\n        return;\n      }\n    }\n\n    if (gifts.length === 1) {\n      if (!this.claimedGift) {\n        this.addGift(gifts[0]).catch(console.warn);\n      }\n\n      return;\n    }\n\n    this.dataset.mode = reason === 'change' ? 'change' : 'unlock';\n    this.dataset.returnView = view === 'upsell' ? 'upsell' : 'cart';\n    this.selectedVariantId = this.claimedGift?.variant_id ??\n      parseInt(gifts[0].dataset.variantId, 10);\n    this.render();\n\n    drawer.dataset.view = 'offer';\n\n    if (reason === 'unlocked' || reason === 'chosen') {\n      type Detail = GiftWithPurchaseOfferedEvent['detail'];\n      const offeredEvent = new CustomEvent<Detail>('gift-with-purchase-offered', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          event_id: uuidv4(),\n          reason\n        }\n      });\n      this.dispatchEvent(offeredEvent);\n    }\n  }\n\n  private onGiftClicked(event: MouseEvent) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    const gift = <GiftWithPurchaseProduct>event.currentTarget;\n    this.selectedVariantId = parseInt(gift.dataset.variantId, 10);\n    this.render();\n  }\n\n  private onConfirmClicked(_event: MouseEvent) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    if (this.claimedGift?.variant_id === this.selectedVariantId) {\n      this.showView(this.dataset.returnView);\n      return;\n    }\n\n    const gift = this.querySelector<GiftWithPurchaseProduct>(\n      `gift-with-purchase-product[data-variant-id=\"${this.selectedVariantId}\"]`);\n    if (!gift) {\n      return;\n    }\n\n    this.dataset.saving = 'true';\n    this.render();\n\n    this.saveGift(gift).catch(console.warn).finally(() => {\n      delete this.dataset.saving;\n      this.render();\n    });\n  }\n\n  private onSkipClicked(_event: MouseEvent) {\n    if (this.dataset.saving === 'true') {\n      return;\n    }\n\n    if (this.dataset.mode !== 'change') {\n      this.rememberSkip();\n    }\n\n    this.showView(this.dataset.returnView);\n  }\n\n  /**\n   * Closing the drawer on a fresh offer counts as skipping it. The drawer opens on another view\n   * next time, so the offer is only shown once.\n   */\n  private onCartDrawerClosed(_event: GlobalEventHandlersEventMap['cart-drawer-closed']) {\n    const drawer = this.closest('cart-drawer');\n    if (drawer.dataset.view === 'offer' && this.dataset.saving !== 'true' &&\n      this.dataset.mode !== 'change') {\n      this.rememberSkip();\n    }\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * The progress bar reads this attribute to stop offering the gift. It clears it once the cart\n   * drops below the threshold, so crossing it again is a fresh offer.\n   */\n  private rememberSkip() {\n    update(this, { attributes: { _gwp_skipped: 'true' } }, 'gift-with-purchase-skip')\n      .catch(console.warn);\n\n    type Detail = GiftWithPurchaseSkippedEvent['detail'];\n    const skippedEvent = new CustomEvent<Detail>('gift-with-purchase-skipped', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        event_id: uuidv4()\n      }\n    });\n    this.dispatchEvent(skippedEvent);\n  }\n\n  /**\n   * Swapping a gift removes the old one and then adds the new one. The cart helpers report a\n   * failure as an error the cart view shows, rather than throwing, so we check the cart they leave\n   * behind instead. On a failure we land on the cart so the shopper sees the error.\n   */\n  private async saveGift(gift: GiftWithPurchaseProduct) {\n    if (this.claimedGift) {\n      await remove(this, [this.claimedGift.key], 'gift-with-purchase-change');\n      if (this.claimedGift) {\n        this.showView('cart');\n        return;\n      }\n    }\n\n    await this.addGift(gift);\n\n    const added = this.claimedGift?.variant_id === parseInt(gift.dataset.variantId, 10);\n    this.showView(added ? this.dataset.returnView : 'cart');\n  }\n\n  /**\n   * We mark the line as a gift so the cart can find it and style it. We also store the gift's\n   * regular price on the line so the cart can show what it's worth, since the line itself is free.\n   */\n  private async addGift(gift: GiftWithPurchaseProduct) {\n    const item: AddItem = {\n      id: gift.dataset.variantId,\n      quantity: 1,\n      properties: {\n        _gift_with_purchase: 'true',\n        _gift_with_purchase_price: gift.dataset.price\n      }\n    };\n\n    await add(this, [item], [], 'gift-with-purchase-add');\n\n    if (this.claimedGift?.variant_id !== parseInt(gift.dataset.variantId, 10)) {\n      return;\n    }\n\n    type Detail = GlobalEventHandlersEventMap['gift-with-purchase-claimed']['detail'];\n    const claimedEvent = new CustomEvent<Detail>('gift-with-purchase-claimed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        event_id: uuidv4()\n      }\n    });\n    this.dispatchEvent(claimedEvent);\n  }\n\n  private showView(view: string) {\n    this.closest('cart-drawer').dataset.view = view || 'cart';\n  }\n\n  private render() {\n    const gifts = this.querySelectorAll('gift-with-purchase-product');\n    const unlock = this.dataset.mode !== 'change';\n\n    this.shadowRoot.querySelector('#title').textContent = unlock ?\n      'You unlocked a free gift!' : 'Change your free gift';\n    this.shadowRoot.querySelector('#subtitle').textContent =\n      `Pick 1 of ${gifts.length}. Free with your order.`;\n\n    for (const gift of gifts) {\n      const selected = parseInt(gift.dataset.variantId, 10) === this.selectedVariantId;\n      gift.dataset.selected = selected ? 'true' : 'false';\n    }\n\n    const saving = this.dataset.saving === 'true';\n\n    const confirm = this.shadowRoot.querySelector<HTMLButtonElement>('#confirm');\n    const label = confirm.querySelector<HTMLElement>('.label');\n    label.textContent = unlock ? 'Add my free gift' : 'Save my choice';\n    label.hidden = saving;\n    confirm.querySelector('loading-indicator').hidden = !saving;\n    confirm.disabled = saving;\n    confirm.setAttribute('aria-busy', saving ? 'true' : 'false');\n\n    const skip = this.shadowRoot.querySelector<HTMLButtonElement>('#skip');\n    skip.textContent = unlock ? 'No thanks' : 'Keep my current gift';\n    skip.disabled = saving;\n  }\n}\n\n/**\n * Sent when the offer view shows a fresh gift offer, so analytics can count shoppers who saw it.\n */\ntype GiftWithPurchaseOfferedEvent = CustomEvent<{\n  /**\n   * UUID representing this one instance of the event.\n   */\n  event_id: string;\n\n  /**\n   * How the offer came up. \"unlocked\" means the view opened on its own because a cart change the\n   * shopper made unlocked the gift. \"chosen\" means the shopper clicked the message saying they\n   * qualified.\n   */\n  reason: 'chosen' | 'unlocked';\n}>;\n\n/**\n * Sent when the shopper turns down a fresh gift offer, either with the No thanks button or by\n * closing the drawer while the offer is up.\n */\ntype GiftWithPurchaseSkippedEvent = CustomEvent<{\n  /**\n   * UUID representing this one instance of the event.\n   */\n  event_id: string;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'gift-with-purchase-offered': GiftWithPurchaseOfferedEvent;\n    'gift-with-purchase-skipped': GiftWithPurchaseSkippedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'offer-view': OfferView;\n  }\n}\n\nif (!customElements.get('offer-view')) {\n  customElements.define('offer-view', OfferView);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { formatMoney } from '@shopify/theme-currency';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * One gift the shopper can pick from inside the offer view. Renders a gift with purchase product.\n */\nclass GiftWithPurchaseProduct extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-selected',\n      'data-price',\n      'data-image-url',\n      'data-title',\n      'data-subtitle',\n      'data-variant-id'\n    ];\n  }\n\n  declare readonly dataset: {\n    imageUrl: string;\n\n    /**\n     * The gift's regular price, in cents.\n     */\n    price: string;\n\n    /**\n     * Whether this is the gift selected in the picker. The offer view sets this.\n     */\n    selected: string;\n\n    subtitle: string;\n    title: string;\n    variantId: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.render();\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  private render() {\n    const imageEl = this.shadowRoot.querySelector('img');\n    if (imageEl) {\n      imageEl.src = this.dataset.imageUrl;\n      imageEl.alt = this.dataset.title;\n    }\n\n    const priceEl = this.shadowRoot.querySelector('span');\n    if (priceEl) {\n      const price = parseInt(this.dataset.price, 10);\n      // eslint-disable-next-line no-template-curly-in-string\n      priceEl.textContent = formatMoney(price, '${{amount_no_decimals}}');\n    }\n\n    const titleEl = this.shadowRoot.querySelector('h3');\n    if (titleEl) {\n      titleEl.textContent = this.dataset.title;\n    }\n\n    const subtitleEl = this.shadowRoot.querySelector('h4');\n    if (subtitleEl) {\n      subtitleEl.textContent = this.dataset.subtitle;\n    }\n\n    const selectedCheckEl = this.shadowRoot.querySelector<HTMLInputElement>('input[type=\"radio\"]');\n    if (this.dataset.selected === 'true') {\n      selectedCheckEl.checked = true;\n    } else {\n      selectedCheckEl.checked = false;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'gift-with-purchase-product': GiftWithPurchaseProduct;\n  }\n}\n\nif (!customElements.get('gift-with-purchase-product')) {\n  customElements.define('gift-with-purchase-product', GiftWithPurchaseProduct);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart, Item } from '@integrabeauty/shopify-ajax-api';\nimport { formatMoney } from '@shopify/theme-currency';\nimport { v4 as uuidv4 } from 'uuid';\nimport { findGiftKeysToRemove, isThresholdAchieved } from '../../../lib/cart/gift-with-purchase.js';\nimport { remove } from '../../../lib/cart/remove.js';\nimport { update } from '../../../lib/cart/update.js';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * Custom element for displaying the progress of gift with purchase promotions in the cart. The\n * element also tracks free shipping if both required attributes are present.\n *\n * The bar only shows progress while the cart is under the threshold. Picking the gift happens in\n * the offer view, which this element asks for right after a cart change the shopper made unlocks\n * the gift. Once the cart qualifies and has no gift, such as after the shopper skipped the offer,\n * this element shows the message saying they qualified, on the cart view and on the upsell view.\n */\nclass GiftWithPurchaseProgress extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Boolean flag indicating whether the gift with purchase threshold has been achieved. Used to\n     * update the visual state of the progress bar.\n     */\n    achieved: 'false' | 'true';\n\n    /**\n     * \"true\" while the cart holds a free gift. Used by the styles to hide the bar once there is\n     * nothing left on it to show.\n     */\n    claimed: 'false' | 'true';\n\n    /**\n     * Value representing the free shipping threshold (in cents). If left blank we can assume free\n     * shipping is not available currently. Only when this attribute is set can the free shipping\n     * progress be shown.\n     */\n    freeShippingThresholdCents: string;\n\n    /**\n     * Boolean flag to determine whether the free shipping progress should be shown. Requires the\n     * `data-free-shipping-threshold-cents` attribute to be set.\n     */\n    showFreeShipping: 'false' | 'true';\n\n    /**\n     * Text to show when free shipping is unlocked because of a subscription item in the cart\n     */\n    subscriptionText: string;\n\n    /**\n     * Minimum cart value for achieving the gift with purchase (in cents).\n     */\n    thresholdCents: string;\n\n    /**\n     * Minimum cart quantity for achieving the gift with purchase.\n     */\n    thresholdQuantity: string;\n\n    /**\n     * Type of goal for the promo progress bar. Can be either based on the cart quantity or the cart\n     * total. Used by loaded styles to determine the visual representation of the progress bar.\n     */\n    thresholdType: 'cart-quantity' | 'cart-total';\n  };\n\n  private onCartTransactionCompletedBound = this.onCartTransactionCompleted.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Set when a cart change the shopper made in this tab unlocked the gift. The offer view is asked\n   * for once that change's transaction completes, so the offer comes after the change rather than\n   * in the middle of it.\n   */\n  private unlockPending = false;\n\n  /**\n   * Set when the cart dropped below the threshold with a skip saved on it. The skip is cleared once\n   * the change that dropped it completes, for the same reason as the unlock above. Starting another\n   * cart change in the middle of one logs a concurrent transaction warning.\n   */\n  private skipClearPending = false;\n\n  /**\n   * Estimated customer cart value, exact value is calculated on checkout shipping step (in cents).\n   */\n  private estimatedTotal = 0;\n\n  /**\n   * Current quantity of items in the cart excluding any already added gift with purchase items.\n   */\n  private cartQuantity = 0;\n\n  /**\n   * Whether there is a subscription item in the cart, which unlocks free shipping regardless of the\n   * cart value.\n   */\n  private subscriptionsInCart = false;\n\n  /**\n   * The quantity steps are built once, since the threshold doesn't change after the page loads.\n   */\n  private quantityThresholdInitialized = false;\n\n  /**\n   * The free shipping marker is placed once, since the threshold doesn't change after the page\n   * loads.\n   */\n  private shippingInitialized = false;\n  private thresholdAchieved = false;\n\n  /**\n   * Set once the first cart has been checked against the threshold, so the cart the page loads with\n   * does not count as a change.\n   */\n  private achievedStateKnown = false;\n\n  /**\n   * The gift line in the cart, if there is one. It can still be set for a gift that is about to be\n   * removed because the cart no longer qualifies.\n   */\n  private claimedGiftWithPurchase?: Item;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdated(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionCompletedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    const cart = event.detail.cart;\n    let subscriptionsInCart = false;\n    for (const item of cart.items) {\n      if (item?.selling_plan_allocation?.selling_plan?.recurring_deliveries) {\n        subscriptionsInCart = true;\n        break;\n      }\n    }\n\n    this.subscriptionsInCart = subscriptionsInCart;\n\n    // determine if a gift with purchase has already been claimed in the cart\n    this.claimedGiftWithPurchase = cart.items.find(\n      item => item.properties._gift_with_purchase === 'true');\n    this.estimatedTotal = cart.total_price;\n    this.cartQuantity = this.claimedGiftWithPurchase ? cart.item_count - 1 : cart.item_count;\n\n    this.updateAchievedState(cart);\n\n    const fromThisTab = event.detail.scenario !== 'cross-tab-sync';\n    const skipped = cart.attributes?._gwp_skipped === 'true';\n\n    if (this.claimedGiftWithPurchase) {\n      // Removing a line, changing a quantity, and applying a code in the drawer already remove a\n      // gift the cart no longer qualifies for in the same transaction, so this is a backup for\n      // everything else, such as a failed follow-up or a code applied outside the drawer. The gift\n      // removal doc alongside this file has the details.\n      const giftKeys = findGiftKeysToRemove(cart, this.getThresholdSettings());\n      if (giftKeys.length > 0) {\n        // Leave the removal to the tab where the cart changed. That tab broadcasts the cart without\n        // the gift once its removal finishes, and this tab catches up then. Removing here too would\n        // race that tab, and because removal works by line position, the losing request removes\n        // whichever product moved into the gift's old line. The cost is that if that tab never\n        // finishes its removal, such as when it is closed or the removal fails, no other tab steps\n        // in, and the priced gift stays in the cart until the next cart change in any tab.\n        if (fromThisTab) {\n          remove(this, giftKeys, 'gift-with-purchase-locked')\n            .catch(console.warn);\n        }\n\n        return;\n      }\n    } else if (!this.thresholdAchieved && skipped && fromThisTab) {\n      // A skip only lasts while the cart qualifies, so crossing the threshold again is a fresh\n      // offer. When the cart still holds a gift on its way out, this waits for the cart the removal\n      // leaves behind, rather than run two cart changes at once.\n      this.skipClearPending = true;\n    } else if (this.thresholdAchieved && !skipped && isShopperChange(event)) {\n      this.unlockPending = true;\n    }\n\n    this.render();\n  }\n\n  /**\n   * The gift threshold settings, read from this element's data attributes.\n   */\n  private getThresholdSettings() {\n    return {\n      cents: this.dataset.thresholdCents,\n      quantity: this.dataset.thresholdQuantity,\n      type: this.dataset.thresholdType\n    };\n  }\n\n  /**\n   * Works out whether the cart meets the gift threshold and sends an unlocked or locked event when\n   * that changes. A cart that already qualifies when the page loads sends no unlocked event, only\n   * later changes do.\n   */\n  private updateAchievedState(cart: Cart) {\n    const achieved = isThresholdAchieved(cart, this.getThresholdSettings());\n\n    if (this.achievedStateKnown && this.thresholdAchieved !== achieved) {\n      const eventType = achieved ? 'gift-with-purchase-unlocked' : 'gift-with-purchase-locked';\n      const event = new CustomEvent(eventType, {\n        bubbles: true,\n        composed: true,\n        detail: {\n          event_id: uuidv4()\n        }\n      });\n      this.dispatchEvent(event);\n    }\n\n    this.thresholdAchieved = achieved;\n    this.achievedStateKnown = true;\n    this.dataset.achieved = achieved ? 'true' : 'false';\n  }\n\n  private render() {\n    this.dataset.claimed = this.claimedGiftWithPurchase ? 'true' : 'false';\n\n    this.renderQualifiedMessages();\n    this.renderThresholdMessage();\n    this.renderShippingMessage();\n    this.renderShippingProgress();\n    if (this.dataset.thresholdType === 'cart-quantity') {\n      this.renderQuantityThresholdProgress();\n    } else {\n      this.renderCentsThresholdProgress();\n    }\n  }\n\n  /**\n   * The message lives on the cart view and on the upsell view, so we show or hide every copy on the\n   * page. It needs at least one gift in stock to offer.\n   */\n  private renderQualifiedMessages() {\n    const hasGifts = !!document.querySelector('offer-view gift-with-purchase-product');\n    const show = this.thresholdAchieved && !this.claimedGiftWithPurchase && hasGifts;\n    for (const message of document.querySelectorAll('gift-qualified-message')) {\n      message.hidden = !show;\n    }\n  }\n\n  /**\n   * Runs once the change that unlocked the gift is done. The cart can have moved on by then, such\n   * as when a gift was added in the meantime, so we check again.\n   */\n  private onCartTransactionCompleted(_event: WindowEventMap['cart-transaction-completed']) {\n    if (this.skipClearPending) {\n      this.skipClearPending = false;\n      if (!this.thresholdAchieved && !this.claimedGiftWithPurchase) {\n        update(this, { attributes: { _gwp_skipped: '' } }, 'gift-with-purchase-skip-cleared')\n          .catch(console.warn);\n      }\n    }\n\n    if (!this.unlockPending) {\n      return;\n    }\n\n    this.unlockPending = false;\n\n    if (!this.thresholdAchieved || this.claimedGiftWithPurchase) {\n      return;\n    }\n\n    type Detail = GlobalEventHandlersEventMap['offer-view-requested']['detail'];\n    const event = new CustomEvent<Detail>('offer-view-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        reason: 'unlocked'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private renderQuantityThresholdProgress() {\n    const container = this.shadowRoot.querySelector<HTMLElement>('.goals-container');\n    if (!this.dataset.thresholdQuantity) {\n      // current state has invalid configuration, fail gracefully\n      container.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    container.style.display = '';\n    const thresholdQuantity = parseInt(this.dataset.thresholdQuantity, 10);\n    const stepSize = Math.floor(100 / thresholdQuantity);\n\n    const goals = container.querySelector<HTMLElement>('.goals-progress');\n    // Initialize the quantity threshold progress steps if not already done\n    if (!this.quantityThresholdInitialized) {\n      // Clear existing shipping and cart total progress steps if current gift with purchase\n      // progress type is set to cart-quantity\n      goals.innerHTML = '';\n      const progressAchieved = document.createElement('div');\n      progressAchieved.classList.add('progress-achieved');\n      goals.appendChild(progressAchieved);\n\n      for (let i = 1; i <= thresholdQuantity; i++) {\n        const stepOffset = i * stepSize;\n        const goal = document.createElement('div');\n        goal.classList.add('quantity-goal');\n        goal.style.left = `${stepOffset}%`;\n        goal.dataset.quantityStep = i.toString();\n        goal.setAttribute('aria-label',\n          `Add ${thresholdQuantity} items to your cart to unlock the gift, currently have ${i}`);\n        goal.textContent = i.toString();\n        goals.appendChild(goal);\n      }\n\n      this.quantityThresholdInitialized = true;\n    }\n\n    for (let i = 1; i <= thresholdQuantity; i++) {\n      const step = goals.querySelector<HTMLElement>(`[data-quantity-step=\"${i}\"]`);\n      if (step) {\n        step.dataset.achieved = i <= this.cartQuantity ? 'true' : 'false';\n      } else {\n        console.warn(`Step element [data-quantity-step=\"${i}\"] not found.`);\n      }\n    }\n\n    const progressAchieved = container.querySelector<HTMLElement>('.progress-achieved');\n    if (this.cartQuantity >= parseInt(this.dataset.thresholdQuantity, 10)) {\n      progressAchieved.style.width = '100%';\n    } else {\n      const progressWidth = this.cartQuantity * stepSize;\n      progressAchieved.style.width = `${progressWidth}%`;\n    }\n  }\n\n  private renderCentsThresholdProgress() {\n    const container = this.shadowRoot.querySelector<HTMLElement>('.goals-container');\n    if (!this.dataset.thresholdCents) {\n      // current state has invalid configuration, fail gracefully\n      container.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    container.style.display = '';\n    const threshold = parseInt(this.dataset.thresholdCents, 10);\n    const giftGoal = container.querySelector<HTMLElement>('.gift-goal');\n\n    if (this.estimatedTotal >= threshold) {\n      this.thresholdAchieved = true;\n      giftGoal.classList.add('achieved');\n    } else {\n      this.thresholdAchieved = false;\n      giftGoal.classList.remove('achieved');\n    }\n\n    const progressAchieved = container.querySelector<HTMLElement>('.progress-achieved');\n    const total = this.estimatedTotal;\n    if (total <= 0) {\n      progressAchieved.style.width = '0%';\n      return;\n    }\n\n    const percentage = Math.min(total / threshold, 1);\n    progressAchieved.style.width = `${percentage * 100}%`;\n  }\n\n  /**\n   * Updates the free shipping icon on the progress bar. Responsible for updating whether to show\n   * the goal as achieved based on the current cart state or hide it if the threshold has not been\n   * configured.\n   */\n  private renderShippingProgress() {\n    if (this.dataset.showFreeShipping !== 'true') {\n      return;\n    }\n\n    // No shipping progress for cart-quantity threshold type as not incorporated into the design\n    if (this.dataset.thresholdType === 'cart-quantity') {\n      return;\n    }\n\n    const shippingGoal = this.shadowRoot.querySelector<HTMLElement>(\n      '.goals-progress .shipping-goal');\n\n    if (!this.dataset.freeShippingThresholdCents) {\n      // current state has invalid configuration, fail gracefully\n      shippingGoal.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    shippingGoal.style.display = 'flex';\n\n    const shippingThreshold = parseInt(this.dataset.freeShippingThresholdCents, 10);\n    const giftThreshold = parseInt(this.dataset.thresholdCents, 10);\n    const shippingPercentage = Math.min(shippingThreshold / giftThreshold, 1);\n\n    if (!this.shippingInitialized) {\n      this.shippingInitialized = true;\n      shippingGoal.style.left = `${shippingPercentage * 100}%`;\n    }\n\n    if (this.estimatedTotal > shippingThreshold || this.subscriptionsInCart) {\n      shippingGoal.classList.add('achieved');\n    } else {\n      shippingGoal.classList.remove('achieved');\n    }\n  }\n\n  /**\n   * Responsible for rendering the threshold messaging for free shipping progress in the div.heading\n   * element.\n   */\n  private renderShippingMessage() {\n    if (this.dataset.showFreeShipping !== 'true') {\n      return;\n    }\n\n    const shipping = this.shadowRoot.querySelector<HTMLElement>('.heading .shipping');\n    if (!this.dataset.freeShippingThresholdCents) {\n      // current state has invalid configuration, fail gracefully\n      shipping.style.display = 'none';\n      return;\n    }\n\n    // restore visibility in case it was previously hidden\n    shipping.style.display = 'flex';\n    const shippingThreshold = parseInt(this.dataset.freeShippingThresholdCents, 10);\n    const difference = shippingThreshold - this.estimatedTotal;\n    // eslint-disable-next-line no-template-curly-in-string\n    const amount = formatMoney(difference, '${{amount_no_decimals}}');\n    const shippingMessage = difference > 0 ?\n      `You're ${amount} away from free shipping` : 'You have free shipping!';\n    const shippingElement = shipping.querySelector('.copy');\n    if (shippingElement) {\n      shippingElement.textContent = shippingMessage;\n    }\n  }\n\n  /**\n   * Responsible for rendering the threshold messaging for gift with purchase progress in the\n   * div.heading element.\n   */\n  private renderThresholdMessage() {\n    const giftMessage = this.shadowRoot.querySelector<HTMLElement>('.heading .gift');\n    let message = '';\n    if (this.dataset.thresholdType === 'cart-quantity') {\n      if (!this.dataset.thresholdQuantity) {\n        // current state has invalid configuration, fail gracefully\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // Once the cart qualifies, the qualified message or the gift line says so instead.\n      const thresholdQuantity = parseInt(this.dataset.thresholdQuantity, 10);\n      if (this.cartQuantity >= thresholdQuantity) {\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // restore visibility in case it was previously hidden\n      giftMessage.style.display = 'flex';\n      const difference = thresholdQuantity - this.cartQuantity;\n      const plural = difference === 1 ? 'item' : 'items';\n      message += `Add ${difference} more ${plural} to unlock your free gift`;\n    } else {\n      if (!this.dataset.thresholdCents) {\n        // current state has invalid configuration, fail gracefully\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // Once the cart qualifies, the qualified message or the gift line says so instead.\n      const threshold = parseInt(this.dataset.thresholdCents, 10);\n      if (threshold <= this.estimatedTotal) {\n        giftMessage.style.display = 'none';\n        return;\n      }\n\n      // restore visibility in case it was previously hidden\n      giftMessage.style.display = 'flex';\n      const difference = threshold - this.estimatedTotal;\n      // eslint-disable-next-line no-template-curly-in-string\n      const amount = formatMoney(difference, '${{amount_no_decimals}}');\n      message = `${amount} until free gift unlocked`;\n    }\n\n    const copy = giftMessage.querySelector('.copy');\n    copy.textContent = message;\n  }\n}\n\n/**\n * Whether the cart change came from something the shopper did in this tab. The cart loading with\n * the page, a cart synced from another tab, and the gift's own changes do not count.\n */\nfunction isShopperChange(event: CartUpdatedEvent) {\n  if (event.detail.is_initial) {\n    return false;\n  }\n\n  const scenario = event.detail.scenario || '';\n  if (scenario === 'cross-tab-sync' || scenario === 'bfcache-reconcile') {\n    return false;\n  }\n\n  return !scenario.startsWith('gift-with-purchase');\n}\n\ntype GiftWithPurchaseEvent = CustomEvent<{\n  /**\n   * UUID representing a unique instance of gift with purchase event by a customer.\n   */\n  event_id: string;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'gift-with-purchase-claimed': GiftWithPurchaseEvent;\n    'gift-with-purchase-locked': GiftWithPurchaseEvent;\n    'gift-with-purchase-unlocked': GiftWithPurchaseEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'gift-with-purchase-progress': GiftWithPurchaseProgress;\n  }\n}\n\nif (!customElements.get('gift-with-purchase-progress')) {\n  customElements.define('gift-with-purchase-progress', GiftWithPurchaseProgress);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { formatMoney } from '@shopify/theme-currency';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * This element is responsible for showing multi-tiered promotion progress in the users shopping\n * cart. It reacts to a cart updated event to calculate if the cart qualifies for any of the\n * promotional goals.\n *\n * Currently, the element is being used exclusively to test free shipping, which is both applicable\n * when the estimated cart total reaches a certain threshold or when there is a subscription item in\n * the cart.\n *\n * This is the second version of the promo goals progress, which is a replacement for the original.\n * The original promo goals progress element is still available as a separate custom element, but it\n * is not currently in use.\n *\n * @todo This element currently assumes that the only promo-goal is free shipping. If we want to\n * support multiple promo-goals, we will need to update the logic to not make this assumption.\n */\nclass PromoGoalProgressV2 extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-update'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Boolean flag to determine whether the promo goals progress bar should be shown.\n     */\n    show: 'false' | 'true';\n\n    /**\n     * Text to show when free shipping is unlocked because of a subscription item in the cart\n     */\n    subscriptionText: string;\n\n    /**\n     * Minimum cart value for free shipping (in cents).\n     */\n    threshold: string;\n\n    /**\n     * Mechanism to update the promo goals progress bar after threshold updates are processed.\n     */\n    update: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n\n  /**\n   * Estimated customer cart value, exact value is calculated on checkout shipping step (in cents).\n   */\n  private estimatedTotal = 0;\n\n  /**\n   * Whether there is a subscription item in the cart, which unlocks free shipping regardless of the\n   * cart value.\n   */\n  private subscriptionsInCart = false;\n\n  private declare promoGoals: PromoGoal[];\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (oldValue === newValue) {\n      return;\n    }\n\n    if (this.isConnected) {\n      this.ingestPromoGoals();\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    const cart = event.detail.cart;\n    this.estimatedTotal = cart.total_price;\n\n    let subscriptionsInCart = false;\n    for (const item of cart.items) {\n      if (item?.selling_plan_allocation?.selling_plan?.recurring_deliveries) {\n        subscriptionsInCart = true;\n        break;\n      }\n    }\n\n    this.subscriptionsInCart = subscriptionsInCart;\n\n    this.render();\n  }\n\n  private ingestPromoGoals() {\n    const goals = this.querySelectorAll('promo-goal-v2');\n\n    this.promoGoals = [];\n    for (const goal of goals) {\n      this.promoGoals.push({\n        name: goal.dataset.offerText,\n        threshold: parseInt(goal.dataset.thresholdCents, 10)\n      });\n    }\n  }\n\n  private render() {\n    if (!this.promoGoals) {\n      this.ingestPromoGoals();\n    }\n\n    const span = this.shadowRoot.querySelector('span');\n    const progressContainer = this.shadowRoot.querySelector('.progress-container');\n    const progressBar = this.shadowRoot.querySelector<HTMLElement>('.progress-bar');\n\n    span.textContent = this.getMessageText();\n\n    let percentage: number;\n    if (this.subscriptionsInCart) {\n      percentage = 100;\n    } else {\n      const threshold = parseInt(this.dataset.threshold, 10);\n      percentage = Math.min(100, (this.estimatedTotal / threshold) * 100);\n    }\n\n    percentage = Math.round(percentage);\n    if (isNaN(percentage)) {\n      percentage = 0;\n    }\n\n    progressContainer.setAttribute('aria-valuenow', percentage.toString());\n    progressBar.style.width = percentage + '%';\n\n    const goalNamesContainer = this.shadowRoot.querySelector('.goal-names');\n    goalNamesContainer.innerHTML = '';\n\n    // Loop through promo goals to add goal names to the progress bar\n    let goalIndex = 0;\n    for (const goal of this.promoGoals) {\n      goalIndex++;\n      const nameDiv = document.createElement('div');\n      nameDiv.classList.add('name');\n\n      const spanDiv = document.createElement('span');\n      spanDiv.textContent = goal.name;\n      nameDiv.appendChild(spanDiv);\n\n      if (this.estimatedTotal >= goal.threshold) {\n        nameDiv.classList.add('achieved');\n      }\n\n      if (this.promoGoals.length === goalIndex) {\n        nameDiv.classList.add('last');\n      } else {\n        const threshold = parseInt(this.dataset.threshold, 10);\n        const percentage = Math.min(100, (goal.threshold / threshold) * 100);\n        nameDiv.style.left = `${percentage}%`;\n      }\n\n      goalNamesContainer.appendChild(nameDiv);\n    }\n\n    // Loop through promo goals to add threshold markers to the progress bar\n    const thresholdsContainer = this.shadowRoot.querySelector('.goal-thresholds');\n    thresholdsContainer.innerHTML = '';\n    goalIndex = 0;\n    for (const goal of this.promoGoals) {\n      goalIndex++;\n      const thresholdElement = document.createElement('div');\n      thresholdElement.classList.add('threshold');\n      // eslint-disable-next-line no-template-curly-in-string\n      thresholdElement.textContent = formatMoney(goal.threshold, '${{amount_no_decimals}}');\n\n      if (this.promoGoals.length === goalIndex) {\n        thresholdElement.classList.add('last');\n      } else {\n        const threshold = parseInt(this.dataset.threshold, 10);\n        const percentage = Math.min(100, (goal.threshold / threshold) * 100);\n        thresholdElement.style.left = `${percentage}%`;\n      }\n\n      thresholdsContainer.appendChild(thresholdElement);\n    }\n  }\n\n  private getMessageText() {\n    if (this.subscriptionsInCart) {\n      return this.dataset.subscriptionText ||\n        'Free shipping unlocked with subscription item!';\n    }\n\n    const threshold = parseInt(this.dataset.threshold, 10);\n    const difference = threshold - this.estimatedTotal;\n\n    if (difference > 0) {\n      // eslint-disable-next-line no-template-curly-in-string\n      return 'You\\'re ' + formatMoney(difference, '${{amount}}') +\n        ' away from Free Shipping!';\n    } else {\n      return 'You\\'ve Earned Free Contiguous US Shipping!';\n    }\n  }\n}\n\ninterface PromoGoal {\n  name: string;\n  threshold: number;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'promo-goal-progress-v2': PromoGoalProgressV2;\n  }\n}\n\nif (!customElements.get('promo-goal-progress-v2')) {\n  customElements.define('promo-goal-progress-v2', PromoGoalProgressV2);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { apply } from '../../../lib/cart/discount/apply.js';\nimport { getApplicableDiscountCodes, getDiscountAmounts, getDiscountCodes } from '../../../lib/cart/discount/discount.js';\nimport { remove } from '../../../lib/cart/discount/remove.js';\nimport { read } from '../../../lib/cart/state.js';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport { findGiftKeysToRemoveFromPage } from '../gift-removal.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * Renders a text field for someone to type in a discount code and apply it to the cart. This also\n * renders the apply button, some error messages, the currently applied code(s), and a way of\n * removing discounts.\n */\nclass DiscountInput extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Currency code\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol (e.g., $)\n     */\n    currencySymbol: string;\n  };\n\n  /**\n   * The most recent cart state. Both the codes we show and the amounts we show beside them are\n   * derived from this when rendering, so the two always describe the same cart.\n   */\n  private cart: Cart = null;\n\n  /**\n   * Number of active transactions. This helps guard against applying / removing multiple codes at\n   * the same time\n   */\n  private activeTransactions = 0;\n\n  /**\n   * Last error message. If not set, the error box won't be displayed\n   */\n  private errorMessage: string | null = null;\n\n  private onCartTransactionStartedBound = this.onCartTransactionStarted.bind(this);\n  private onCartTransactionEndedBound = this.onCartTransactionEnded.bind(this);\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCartUpdateErredBound = this.onCartUpdateErred.bind(this);\n  private onDiscountFormSubmitBound = this.onDiscountFormSubmit.bind(this);\n  private onDiscountInputBound = this.onDiscountInput.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const form = this.shadowRoot.querySelector<HTMLFormElement>('#discount-form');\n    form.addEventListener('submit', this.onDiscountFormSubmitBound);\n\n    const inputElement = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n    inputElement.addEventListener('input', this.onDiscountInputBound);\n\n    // we might have missed some events while disconnected, so we reset the transactions lock\n    this.activeTransactions = 0;\n    // and the error message\n    this.errorMessage = null;\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n    addEventListener('cart-transaction-completed', this.onCartTransactionEndedBound);\n    addEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n\n    // This element applies discounts and dispatches its own failures from itself, so we listen here\n    // rather than at global scope. That way it only receives the failures it caused, without having\n    // to match on a scenario token to recognize them.\n\n    this.addEventListener('cart-update-erred', this.onCartUpdateErredBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const form = this.shadowRoot.querySelector<HTMLFormElement>('#discount-form');\n    form?.removeEventListener('submit', this.onDiscountFormSubmitBound);\n\n    const inputElement = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n    inputElement?.removeEventListener('input', this.onDiscountInputBound);\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n    removeEventListener('cart-transaction-completed', this.onCartTransactionEndedBound);\n    removeEventListener('cart-transaction-started', this.onCartTransactionStartedBound);\n\n    this.removeEventListener('cart-update-erred', this.onCartUpdateErredBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    // The cart describes its discounts in two separate places and we need both of them here. The\n    // discount_codes array is the list of codes sitting on the cart, along with whether each one\n    // actually took, and it carries no money. The discount applications carry the money, but they\n    // are labeled with a title rather than a code. So we build the list from the codes, because a\n    // code is also what the remove button has to send back to Shopify, and then we look up each\n    // amount by matching a title against a code.\n\n    const codes = getApplicableDiscountCodes(this.cart);\n    const discountAmounts = getDiscountAmounts(this.cart);\n\n    // An unmatched amount is one whose title matches none of the codes we are about to display.\n    // That happens when the application's title is not a code string, or when the cart holds an\n    // allocation for a code it does not mark applicable, since we only display applicable ones.\n    // Nothing downstream needs this, so we say it and carry on.\n    //\n    // We log rather than warn because it is routine rather than a fault, and we log rather than\n    // debug because log is the tier that becomes a Sentry breadcrumb. It is rare and it describes\n    // real cart state, so it is worth carrying along if some unrelated error is reported later. The\n    // mismatch in the other direction, a code we show with no amount to put beside it, stays at\n    // debug below because it fires for every applicable code with no amount, which is routine for a\n    // free shipping code.\n\n    const unmatched = Array.from(discountAmounts.keys()).filter(title => !codes.includes(title));\n\n    if (unmatched.length > 0) {\n      console.log('Found unmatched discount applications:', unmatched.join(', '));\n    }\n\n    const appliedDiscountsEl = this.shadowRoot.querySelector('.applied-discounts');\n    appliedDiscountsEl.innerHTML = '';\n\n    const appliedDiscountTemplate = this.shadowRoot.querySelector<HTMLTemplateElement>(\n      '#applied-discount-template');\n\n    for (const code of codes) {\n      const appliedDiscountFragment = this.createAppliedDiscount(appliedDiscountTemplate, code,\n        discountAmounts.get(code));\n      appliedDiscountsEl.appendChild(appliedDiscountFragment);\n    }\n\n    const input = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n    input.disabled = this.activeTransactions > 0;\n\n    const submitButton = this.shadowRoot.querySelector<HTMLButtonElement>('#submit-button');\n    submitButton.disabled = input.value.length < 3 || this.activeTransactions > 0;\n\n    const alert = this.shadowRoot.querySelector('alert-element');\n    if (this.errorMessage) {\n      alert.dataset.alertType = 'danger';\n      alert.innerHTML = '';\n      const span = document.createElement('span');\n      span.textContent = this.errorMessage;\n      alert.appendChild(span);\n      alert.hidden = false;\n    } else {\n      alert.hidden = true;\n    }\n  }\n\n  /**\n   * Builds one row for the list of applied discounts, showing the code, the amount coming off the\n   * cart for it, and a button to remove it. The amount is optional because a code can be applied\n   * without the cart allocating anything to it, and the row hides its amount in that case.\n   *\n   * The caller passes the template in because a row is built per code and the template is the same\n   * one every time, so looking it up here would repeat that work for every row.\n   */\n  private createAppliedDiscount(template: HTMLTemplateElement, code: string, amount: number) {\n    const appliedDiscountFragment = <DocumentFragment>template.content.cloneNode(true);\n\n    const discountInfo = appliedDiscountFragment.querySelector<HTMLDivElement>('.discount-info');\n    discountInfo.dataset.code = code;\n\n    const codeEl = discountInfo.querySelector('.discount-code');\n    codeEl.textContent = code;\n\n    if (Number.isInteger(amount)) {\n      // if the amount is found, we display it\n      const amountEl = discountInfo.querySelector('price-element');\n      amountEl.dataset.subunits = amount.toString();\n      amountEl.dataset.currencyCode = this.dataset.currencyCode;\n      amountEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    } else {\n      console.debug('Trying to display discount application, but no amount found', code);\n\n      // if the amount is not found, we still display the code, but we hide the amount\n      for (const infoElement of discountInfo.querySelectorAll<HTMLElement>('.amount-info')) {\n        infoElement.hidden = true;\n      }\n    }\n\n    const cta = appliedDiscountFragment.querySelector('button');\n    cta.dataset.code = code;\n    cta.setAttribute('aria-label', 'Remove discount code ' + code);\n    cta.addEventListener('click', onRemoveDiscountClicked);\n    if (this.activeTransactions > 0) {\n      cta.disabled = true;\n    } else {\n      cta.disabled = false;\n    }\n\n    return appliedDiscountFragment;\n  }\n\n  private onCartTransactionStarted(_event: WindowEventMap['cart-transaction-started']) {\n    this.activeTransactions += 1;\n    this.render();\n  }\n\n  private onCartTransactionEnded(_event: WindowEventMap['cart-transaction-completed']) {\n    // there is a completed event without a matching started event on page load - ignore it\n    if (this.activeTransactions > 0) {\n      this.activeTransactions -= 1;\n    }\n\n    this.render();\n  }\n\n  /**\n   * Whenever someone is typing in the input, we have to consider enabling or disabling the apply\n   * button.\n   */\n  private onDiscountInput(_event: Event) {\n    this.render();\n  }\n\n  private onDiscountFormSubmit(event: SubmitEvent) {\n    event.preventDefault();\n\n    const input = this.shadowRoot.querySelector<HTMLInputElement>('#input');\n\n    const newCodes = [input.value];\n\n    // Both of these carry logic here, not just analytics, unlike most other cart mutations.\n    // Dispatching the failure from this element is what lets this element's own listener catch it\n    // and render the message beside the input. The scenario lets the global listener that renders\n    // errors at the top of the cart drawer recognize this case and suppress its own message,\n    // deferring to the messaging shown beside the input.\n\n    const scenario = 'cart-discount-input';\n    const dispatcher = this;\n\n    // The wrapper in finally is important because it keeps render bound to this instance.\n\n    apply(dispatcher, newCodes, scenario, true, findGiftKeysToRemoveFromPage)\n      .then(() => input.value = '')\n      .catch(console.warn)\n      .finally(() => this.render());\n  }\n\n  private onCartUpdateErred(event: GlobalEventHandlersEventMap['cart-update-erred']) {\n    // Because this listener is attached to the element itself rather than to global scope, it only\n    // receives failures that reach this element. Discount failures from other sources, such as\n    // application on page load or as part of adding to the cart, originate elsewhere and never\n    // arrive here; those are surfaced by a separate global listener that renders errors at the top\n    // of the cart drawer.\n\n    // Handle only failures dispatched at this element, the ones it caused, and not any that merely\n    // bubble up through it from a descendant. By convention this element dispatches its own\n    // discount failures from itself, so this holds, but asserting it makes that a local guarantee\n    // rather than something inherited from how errors happen to be dispatched elsewhere.\n\n    if (event.target !== this) {\n      return;\n    }\n\n    // Narrow to discount application failures so that any other kind of failure this element might\n    // dispatch is not mistaken for one.\n\n    if (event.detail.code !== 'APPLY_DISCOUNTS') {\n      return;\n    }\n\n    // Try to get the existing codes\n\n    let existingCodes: string[];\n    try {\n      if (isFullyDefinedStringArray(event.detail.inputs?.before_codes) &&\n        event.detail.inputs.before_codes.length > 0) {\n        existingCodes = event.detail.inputs.before_codes.map(before => before.toUpperCase());\n      } else {\n        const cart = read();\n        // this ignores whether a discount code is currently applicable as we are only using this to\n        // decide what error message to show\n        existingCodes = getDiscountCodes(cart);\n      }\n    } catch (error) {\n      console.warn(error);\n    }\n\n    // If there are already some codes, then one of the reasons a discount does not apply is because\n    // the existing codes are combinable with the new codes and the existing codes provide a better\n    // discount. We try to provide a better error message in this case.\n    if (existingCodes?.length > 0) {\n      this.errorMessage = 'Oops. It looks like this discount could not be applied. But don\\'t ' +\n        'worry, you\\'re already getting the best deal!';\n    } else {\n      this.errorMessage = 'There was a problem applying the discount.' +\n        ' Please try again in a few moments';\n    }\n\n    this.render();\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    // update the cart we render from, both on initial page load and when the cart state changes\n\n    this.cart = event.detail.cart;\n\n    // On any update, we have a successful update. If there was a prior update error, there may be\n    // still an error message. Hide that error message.\n    this.errorMessage = null;\n    // trigger a render\n    this.render();\n  }\n}\n\nfunction onRemoveDiscountClicked(event: MouseEvent) {\n  // Use the current target rather than the target or the bound receiver. The current target is by\n  // definition the element this listener is attached to, the remove button, regardless of which\n  // inner element actually received the click and without depending on how this function is\n  // invoked. That makes it the reliable choice both for reading the code and as the failure origin.\n\n  const target = event.currentTarget;\n\n  if (target instanceof HTMLButtonElement && !target.disabled) {\n    target.disabled = true;\n\n    const code = target.dataset.code;\n    const scenario = 'cart-discount-input-remove-click';\n\n    // The dispatcher is the clicked button, the element most directly responsible for the removal.\n    // This is a standalone listener bound to the button, so the button is what is in scope; the\n    // surrounding input element would have to be reached by climbing out of its shadow tree. It\n    // also makes no observable difference to listeners outside that tree, since the failure event\n    // composes out of it and is seen as originating from the surrounding element either way.\n\n    const dispatcher = target;\n\n    remove(dispatcher, code, scenario).catch(console.warn);\n  }\n}\n\nfunction isFullyDefinedStringArray(value: any): value is string[] {\n  return Array.isArray(value) && value.every(element => typeof element === 'string');\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'discount-input': DiscountInput;\n  }\n}\n\nif (!customElements.get('discount-input')) {\n  customElements.define('discount-input', DiscountInput);\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from '../digest.js';\nimport { report } from '../error.js';\nimport { removeFollowUp } from '../remove-follow-up.js';\nimport { read, write } from '../state.js';\nimport { start } from '../transaction.js';\nimport { getApplicableDiscountCodes } from './discount.js';\nimport { applyDiscountsHelper } from './helper.js';\n\n/**\n * Applies discount codes to the cart.\n *\n * Do not include existing codes in new codes. Only include new codes in new codes.\n *\n * This does not check whether client state is desynced because it does not matter.\n *\n * @param target the element most directly responsible for the apply, used to originate any failure\n * event with truthful provenance instead of at global scope\n * @param newCodes the new discount codes to apply, excluding existing codes\n * @param scenario optional description or code describing the context in which this takes place\n * @param postValidate whether a new code that ends up applied but not applicable counts as a\n * failure, which it always does for an empty cart\n * @param followUp optional function given the cart after the codes apply, returning the keys of any\n * lines to remove before the transaction ends, such as a free gift the cart no longer qualifies for\n */\nexport async function apply(target: EventTarget | null, newCodes: string[], scenario?: string,\n  postValidate = true, followUp?: (cart: Cart) => string[]) {\n  const transaction = start('applyDiscountCodes', newCodes, scenario);\n\n  let beforeCart: Cart;\n\n  // TODO: this is cheating by relying on a global declared in the cart lifecycle section, which is\n  // a different section. we need a different way to load overrides, such as in their own section.\n\n  try {\n    beforeCart = read();\n    const appliedCart = await applyDiscountsHelper(beforeCart, newCodes,\n      cart_lifecycle.discount_overrides, postValidate);\n\n    const afterCart = await removeFollowUp(appliedCart, followUp, scenario);\n    write(target, afterCart, transaction, scenario);\n  } catch (error) {\n    // In this catch clause we must be extra paranoid not to introduce additional errors so we can\n    // make very few assumptions about state.\n\n    // Downstream analytics looks at codes that failed to apply. Downstream tools do simple string\n    // aggregation. If we use raw codes, which can be in mixed case, we end up with one bucket for\n    // each variation of character case. To make things easier we emit normalized values.\n    const normalNewCodes = Array.isArray(newCodes) ? newCodes.map(code => code?.toUpperCase()) : [];\n\n    report(target, {\n      code: 'APPLY_DISCOUNTS',\n      error,\n      inputs: {\n        before_cart: digest(beforeCart),\n        before_codes: getApplicableDiscountCodes(beforeCart),\n        new_codes: normalNewCodes\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n","import { cartUpdate, type Cart } from '@integrabeauty/shopify-ajax-api';\nimport { digest } from '../digest.js';\nimport { report } from '../error.js';\nimport { cartFetch, read, write } from '../state.js';\nimport { start } from '../transaction.js';\nimport { getDiscountCodes } from './discount.js';\n\n/**\n * Remove a specific discount code from the cart.\n *\n * This leverages a special flag configured in the lange store that enables the discount property to\n * be specified in the cart update request body. This special feature is not documented.\n *\n * Shopify does not provide an API for removing a specific code. We figured out that the discount\n * property accepts a comma separated values list of discounts to apply. Specifying the empty string\n * means remove all. In order to remove a specific discount, we specify a csv of all codes other\n * than the specific code to remove, which effectively removes that discount code. If omitting the\n * discount code results in an empty csv string, then effectively all codes are removed.\n *\n * @param target the element most directly responsible for the removal, used to originate any\n * failure event with truthful provenance instead of at global scope\n * @param code the discount code to remove\n * @param scenario optional description or code describing the context in which this takes place\n */\nexport async function remove(target: EventTarget | null, code: string, scenario?: string) {\n  const transaction = start('removeDiscount', code, scenario);\n\n  let previousCart: Cart;\n\n  try {\n    // In order to remove the discount codes, we need to determine the new set of codes to apply. We\n    // have to preserve any existing codes. The trick to removal is that we basically just filter\n    // out the code to be removed from the list of existing codes, then apply the resulting set of\n    // codes.\n\n    previousCart = read();\n\n    // When obtaining previous codes, we ignore whether the code is applicable. We want to retain\n    // inapplicable codes in the update.\n\n    const previousCodes = getDiscountCodes(previousCart);\n\n    const newCodes = previousCodes.filter(previousCode => previousCode !== code.toUpperCase());\n\n    // The code being removed may be the only applied code, in which case newCodes is an empty\n    // array. When newCodes is an empty array, the join returns an empty string. When discount is an\n    // empty string, cartUpdate is effectively removing all discount codes.\n\n    const discount = newCodes.join(',');\n    await cartUpdate({ discount });\n    const cart = await cartFetch();\n    write(target, cart, transaction, scenario);\n  } catch (error) {\n    report(target, {\n      code: 'REMOVE_DISCOUNT',\n      error,\n      inputs: {\n        code,\n        previous_cart: digest(previousCart)\n      },\n      scenario,\n      transaction\n    });\n  } finally {\n    transaction.complete();\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a promo goal. Used in slot of promo-goal-progress.\n */\nclass PromoGoal extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return [\n      'data-currency-code',\n      'data-currency-symbol',\n      'data-is-locked',\n      'data-offer-text',\n      'data-threshold-cents'\n    ];\n  }\n\n  declare readonly dataset: {\n    /**\n     * The ISO code of the currency\n     *\n     * @example USD\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol\n     *\n     * @example $\n     */\n    currencySymbol: string;\n\n    /**\n     * The state of the goal\n     */\n    isLocked: 'false' | 'true';\n\n    /**\n     * Text that describes the promotional goal\n     */\n    offerText: string;\n\n    /**\n     * The amount of money the customer needs to spend to unlock the promotional goal in cents\n     */\n    thresholdCents: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    this.render();\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && newValue !== oldValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    if (!this.dataset.thresholdCents ||\n      !this.dataset.currencySymbol ||\n      !this.dataset.currencyCode ||\n      !this.dataset.offerText) {\n      return;\n    }\n\n    const thresholdAmount = parseInt(this.dataset.thresholdCents, 10);\n\n    const label = this.shadowRoot.querySelector('.label');\n\n    // if the promo goal does not have a spend threshold, then it is instantly applicable. in this\n    // case we just need to render 'get y' not 'spend x, get y'\n    if (thresholdAmount === 0) {\n      label.innerHTML = `Get ${this.dataset.offerText}!`;\n    } else {\n      label.innerHTML = `Spend <price-element\n        data-currency-symbol=\"${this.dataset.currencySymbol}\"\n        data-currency-code=\"${this.dataset.currencyCode}\"\n        data-subunits=\"${this.dataset.thresholdCents}\">\n        </price-element>, get ${this.dataset.offerText}!`;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'promo-goal': PromoGoal;\n  }\n}\n\nif (!customElements.get('promo-goal')) {\n  customElements.define('promo-goal', PromoGoal);\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\n\n/**\n * Computes how much of the cart counts toward the next promo goal threshold.\n *\n * We want the total after discounts, except that we want to exclude the promotional discount\n * itself. As a result, we cannot work with the original total price or the price after discounts.\n * So we have to create a custom total of discounts and subtract this from the original total price\n * to get a custom price after discounts.\n *\n * This is a front end mirror of the qualifying amount computed server side by the backend tiered\n * discount functions. If those functions change what counts toward a tier, this must change with\n * them or the progress bar will mislead.\n */\nexport function computeQualifyingSubtotal(cart: Cart) {\n  const originalTotalPrice = cart.original_total_price;\n\n  // The total amount of discounts, regardless of the type of discount\n  const totalDiscountPrice = calculateTotalDiscount(cart);\n\n  // This is the total amount discounted by promo goal discounts (e.g. BMSM)\n  const totalPromoGoalDiscountPrice = calculateTotalPromoGoalDiscount(cart);\n\n  const totalPriceOfExcludedItems = calculateTotalPriceOfPromoExcludedItems(cart);\n\n  const priceAfterDiscounts = originalTotalPrice - totalDiscountPrice;\n  const priceAfterDiscountsExceptPromoGoalDiscounts = priceAfterDiscounts +\n    totalPromoGoalDiscountPrice - totalPriceOfExcludedItems;\n\n  return priceAfterDiscountsExceptPromoGoalDiscounts;\n}\n\nfunction calculateTotalDiscount(cart: Cart) {\n  let total = 0;\n  for (const application of cart.cart_level_discount_applications) {\n    total += application.total_allocated_amount;\n  }\n\n  for (const item of cart.items) {\n    for (const allocation of item.line_level_discount_allocations) {\n      total += allocation.amount;\n    }\n  }\n\n  return total;\n}\n\nfunction calculateTotalPriceOfPromoExcludedItems(cart: Cart) {\n  let total = 0;\n\n  // There are also $0 items to exclude, but since they are 0, we just ignore them since they do not\n  // change the total.\n\n  for (const item of cart.items) {\n    if (item.gift_card) {\n      total += item.final_line_price;\n    }\n  }\n\n  return total;\n}\n\nfunction calculateTotalPromoGoalDiscount(cart: Cart) {\n  let total = 0;\n\n  for (const application of cart.cart_level_discount_applications) {\n    if (isPromoNamedDiscount(application.title)) {\n      total += application.total_allocated_amount;\n    }\n  }\n\n  for (const item of cart.items) {\n    for (const allocation of item.line_level_discount_allocations) {\n      if (isPromoNamedDiscount(allocation.discount_application.title)) {\n        total += allocation.discount_application.total_allocated_amount;\n      }\n    }\n  }\n\n  return total;\n}\n\nfunction isPromoNamedDiscount(value: string) {\n  if (typeof value !== 'string') {\n    return false;\n  }\n\n  const upper = value.toUpperCase();\n\n  if (upper.startsWith('BUY MORE SAVE MORE')) {\n    return true;\n  }\n\n  // This is a special $20 off that is allocated over line items. This is also a type of promo goal.\n  if (upper.includes('$20 DISCOUNT APPLIED')) {\n    return true;\n  }\n\n  if (/SPEND \\$[0-9]+ GET [0-9]+% OFF/.test(upper)) {\n    return true;\n  }\n\n  return false;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\nimport { computeQualifyingSubtotal } from './qualifying-subtotal.js';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * Takes promo-goal web elements as inputs in its slot and renders a progress bar.\n */\nclass PromoGoalProgress extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Active currency ISO code, used when rendering the amounts in the progress message.\n     *\n     * @example \"USD\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency symbol character, used when rendering the amounts in the progress message.\n     *\n     * @example \"$\"\n     */\n    currencySymbol: string;\n  };\n\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private declare cart: Cart;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    this.cart = event.detail.cart;\n    this.render();\n  }\n\n  private render() {\n    if (!this.cart) {\n      return;\n    }\n\n    const qualifyingSubtotal = computeQualifyingSubtotal(this.cart);\n\n    const paragraph = this.shadowRoot.querySelector('p');\n\n    const promoGoals = this.collectPromoGoalsData();\n\n    if (promoGoals.length === 0) {\n      paragraph.hidden = true;\n      return;\n    }\n\n    let eligibleThreshold = 0;\n    const eligiblePromoGoals = [];\n\n    // The goals array is sorted from lowest threshold to highest\n\n    for (const promoGoal of promoGoals) {\n      // When the cart subtotal (estimated total) is greater than or equal to the threshold, then\n      // the promo goal has already been unlocked and can be skipped.\n      if (qualifyingSubtotal >= promoGoal.thresholdCents) {\n        promoGoal.element.dataset.isLocked = 'false';\n        continue;\n      }\n\n      promoGoal.element.dataset.isLocked = 'true';\n\n      // If there are no eligible promo goals, add them now\n      if (eligiblePromoGoals.length === 0) {\n        eligiblePromoGoals.push(promoGoal);\n        // also capture any other promo goals with identical threshold\n        eligibleThreshold = promoGoal.thresholdCents;\n        continue;\n      }\n\n      if (eligibleThreshold === promoGoal.thresholdCents) {\n        eligiblePromoGoals.push(promoGoal);\n        continue;\n      }\n    }\n\n    if (eligiblePromoGoals.length === 0) {\n      paragraph.hidden = true;\n    }\n\n    const thresholdAmount = eligiblePromoGoals.length > 0 ?\n      eligiblePromoGoals[0].thresholdCents :\n      promoGoals[promoGoals.length - 1].thresholdCents;\n\n    const amountToGoal = thresholdAmount - qualifyingSubtotal;\n    const offerText = eligiblePromoGoals.map(i => i.offerText).join(' and ');\n\n    paragraph.innerHTML = `Add <price-element\n      data-currency-code=\"${this.dataset.currencyCode}\"\n      data-currency-symbol=\"${this.dataset.currencySymbol}\"\n      data-subunits=\"${amountToGoal}\">\n      </price-element> to unlock ${offerText}!`;\n\n    const percentSpentToGoal = Math.min(100, Math.round(\n      (qualifyingSubtotal / thresholdAmount) * 100));\n    const progressEl = this.shadowRoot.querySelector<HTMLElement>('#progress');\n    progressEl.style.width = `${percentSpentToGoal}%`;\n\n    type PriceElement = HTMLElementTagNameMap['price-element'];\n    const spentEl = this.shadowRoot.querySelector<PriceElement>('#spent');\n    spentEl.dataset.currencyCode = this.dataset.currencyCode;\n    spentEl.dataset.currencySymbol = this.dataset.currencySymbol;\n\n    if (qualifyingSubtotal >= promoGoals[promoGoals.length - 1].thresholdCents) {\n      spentEl.dataset.subunits = '';\n    } else {\n      spentEl.dataset.subunits = qualifyingSubtotal.toString();\n    }\n\n    const goalEl = this.shadowRoot.querySelector<PriceElement>('#goal');\n    goalEl.dataset.currencyCode = this.dataset.currencyCode;\n    goalEl.dataset.currencySymbol = this.dataset.currencySymbol;\n    goalEl.dataset.subunits = thresholdAmount.toString();\n\n    if (eligiblePromoGoals.length > 0) {\n      paragraph.hidden = false;\n    }\n\n    const progressBar = progressEl.parentElement;\n    progressBar.setAttribute('aria-valuenow', percentSpentToGoal.toString());\n    if (percentSpentToGoal < 100) {\n      progressBar.setAttribute('aria-label', [\n        this.dataset.currencySymbol,\n        (qualifyingSubtotal / 100).toFixed(2),\n        ' of ',\n        this.dataset.currencySymbol,\n        // threshold amounts will usually be rounded to a full dollar\n        (thresholdAmount / 100).toString(),\n        ' goal'\n      ].join(''));\n    } else {\n      progressBar.setAttribute('aria-label', [\n        this.dataset.currencySymbol,\n        (thresholdAmount / 100).toString(),\n        ' goal reached'\n      ].join(''));\n    }\n  }\n\n  private collectPromoGoalsData() {\n    const elements = this.querySelectorAll('promo-goal');\n\n    const goals: PromoGoal[] = [];\n    for (const element of elements) {\n      const goal = <PromoGoal>{};\n      goal.currencyCode = element.dataset.currencyCode;\n      goal.currencySymbol = element.dataset.currencySymbol;\n      goal.isLocked = element.dataset.isLocked === 'true';\n      goal.offerText = element.dataset.offerText;\n      goal.thresholdCents = parseInt(element.dataset.thresholdCents, 10);\n      goal.element = element;\n      goals.push(goal);\n    }\n\n    return goals.sort(compareThresholds);\n  }\n}\n\nfunction compareThresholds(a: PromoGoal, b: PromoGoal) {\n  if (a.thresholdCents === b.thresholdCents) {\n    return 0;\n  }\n\n  return a.thresholdCents < b.thresholdCents ? -1 : 1;\n}\n\ninterface PromoGoal {\n  currencyCode: string;\n  currencySymbol: string;\n  element: HTMLElement;\n  isLocked: boolean;\n  offerText: string;\n  thresholdCents: number;\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'promo-goal-progress': PromoGoalProgress;\n  }\n}\n\nif (!customElements.get('promo-goal-progress')) {\n  customElements.define('promo-goal-progress', PromoGoalProgress);\n}\n","import { measure, now } from './perf.js';\n\n/**\n * Waits for the runtime's load to reach a terminal state. Resolves true once a client is ready to\n * read, and false once the runtime has failed or the given number of milliseconds passes.\n *\n * The caller does not have to care whether the runtime loaded before or after it, which is the\n * whole point. Arriving late is covered by reading the load state, because the broadcast has\n * already been and gone by then, and arriving early is covered by listening for the broadcast.\n * Between them there is no ordering a caller can lose to.\n *\n * A failure resolves as soon as it is known rather than waiting out the timeout. That matters\n * because two of the failures are decided long before any caller's deadline. The theme editor is\n * known before a single bundle runs, and a missing anonymous id is known the moment the runtime\n * looks for one. Sitting out two or three seconds for an answer that already exists would delay\n * everything downstream of the value for nothing.\n *\n * Anything other than pending is terminal, which is what the check tests for, so a failure reason\n * added later needs no change here.\n *\n * On a preview theme the wait is extended, because the runtime deliberately holds the broadcast\n * open there to give the GrowthBook DevTools extension time to push forced variant overrides in. A\n * caller that gave up partway through that hold would silently drop the forced variant and make the\n * extension unreliable to debug with. How long the hold lasts comes off the runtime global, so the\n * two sides cannot drift apart into a hold that outlives the wait covering it.\n *\n * Neither the preview check nor the hold length can be read from anything the runtime's boot script\n * writes, because that script loads async and can execute after a caller has already asked. Both\n * the Shopify globals and the runtime's inline seed are set in the head instead, so they are always\n * available.\n *\n * The timeout stays as a floor for the one case no broadcast can cover, the boot script never\n * running at all. On that path the listener is left registered rather than torn down, since a\n * broadcast that does eventually arrive resolves a promise that has already settled, which changes\n * nothing.\n */\nfunction whenLoaded(milliseconds: number) {\n  return new Promise<boolean>(resolve => {\n    if (growthbook_runtime.load_state !== 'pending') {\n      resolve(growthbook_runtime.load_state === 'ready');\n      return;\n    }\n\n    const extraMillis = Shopify.previewMode ? growthbook_runtime.preview_hold_ms : 0;\n    const timer = setTimeout(resolve, milliseconds + extraMillis, false);\n\n    addEventListener('growthbook-load-settled', () => {\n      clearTimeout(timer);\n      resolve(growthbook_runtime.load_state === 'ready');\n    }, { once: true });\n  });\n}\n\n/**\n * Fault-tolerant wrapper around GrowthBook's own getFeatureValue SDK method.\n *\n * In GrowthBook the unit is a feature (a feature flag) identified by a feature key. The feature's\n * value for the current user is decided by evaluating its rules. One kind of rule is an experiment\n * rule (a \"feature flag experiment\"): it randomly buckets the user into a variation and returns\n * that variation's value. So the same feature key can resolve to a static flag value or to an A/B\n * test assignment, and this call returns whatever it resolves to. The caller cannot tell which, and\n * does not need to. When the feature is backed by an experiment rule, the returned value is the\n * user's assigned variation.\n *\n * Calling this is not a passive read, and that is the thing to know before adding a call. Bucketing\n * happens during the read, and the SDK reports the exposure to the runtime's tracking callback on\n * the way back out, which is what puts the visitor in the experiment's analytics. So the moment a\n * section calls this, that visitor counts as having seen whatever variation came back.\n *\n * That makes where a call sits matter as much as what it does with the answer. A read the visitor\n * could not possibly have seen still counts, and every such visitor lands in both groups and\n * behaves the same in both, which drags the measured difference between them toward zero. So gate\n * the call on the thing being on the page and reachable, rather than reading up front and deciding\n * later.\n *\n * On top of the raw SDK call this wrapper adds:\n *\n * - Fault tolerance. On a timeout, an error, or an undefined feature it returns the caller's\n *   fallback instead of throwing. It does assume the runtime global is present, which the always-on\n *   layer guarantees, so a page that somehow lacks it rejects rather than falling back.\n * - A shared, pre-initialized client. It reads the single client the cross-section coordination\n *   global exposes, so the SDK is initialized once per page rather than once per call.\n * - Indifference to load order. It waits on the runtime's ready signal rather than on the client\n *   promise existing yet, so a caller whose bundle runs before the runtime's is not penalized for\n *   it.\n * - A bounded wait. It always resolves within the given timeout, or within a global default when no\n *   per-call timeout is passed.\n * - Per-page memoization. The first resolved value for a feature key is cached and returned for\n *   every later call in the same page load. Once the payload has loaded this buys nothing on its\n *   own, because the SDK evaluates from attributes that do not change during a page, so repeated\n *   reads would agree with each other anyway. Where it earns its place is the timeout boundary. A\n *   caller that gave up waiting holds the fallback and the page has already acted on it, the client\n *   can become ready a moment later, and without it the next caller would get the real value and\n *   disagree with what the page is showing.\n *\n * @param featureKey The GrowthBook feature key.\n * @param fallback The value returned when the feature is undefined or when the lookup times out or\n * errors.\n * @param timeoutMs Optional per-call override for the lookup timeout, in milliseconds.\n *\n * @see https://docs.growthbook.io/lib/js#quick-usage GrowthBook JS SDK getFeatureValue\n * @see https://docs.growthbook.io/feature-flag-experiments How A/B tests are delivered via features\n * @see https://docs.growthbook.io/features/basics Feature flag fundamentals (keys, values, rules)\n */\nexport async function getFeatureValue<T extends string = string>(featureKey: string,\n  fallback: NoInfer<T>, timeoutMs?: number) {\n  const resolvedFeatures = growthbook_runtime.resolved_features;\n  if (resolvedFeatures[featureKey]) {\n    return <T>resolvedFeatures[featureKey];\n  }\n\n  let value: T | null = null;\n\n  // The span starts here rather than at function entry, so that it only ever covers a real wait.\n  // The memoized return above answers without waiting on anything, and timing it would put a span\n  // on the timeline for every read in the page. Leaving it outside means a span showing up at all\n  // is a caller that actually waited. Closing the span in a finally is what covers the timeout and\n  // the failure paths, which are the ones most worth a number.\n\n  const startMillis = now();\n  const effectiveTimeoutMs = timeoutMs ?? growthbook_runtime.default_timeout_ms;\n\n  try {\n    const loaded = await whenLoaded(effectiveTimeoutMs);\n\n    // The runtime either failed or ran out of time, and neither is reported here so as to avoid\n    // Sentry noise, so we return instead of throwing. The fallback is cached so that later calls\n    // for the same feature key in the same page load get the same answer, since a feature read in\n    // several places has to agree with itself.\n\n    if (!loaded) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // The signal says the client is ready, so this await is already settled and only defends\n    // against the impossible case of the two disagreeing.\n\n    const growthbook = await growthbook_runtime.client_loader;\n\n    if (!growthbook) {\n      resolvedFeatures[featureKey] = fallback;\n      return fallback;\n    }\n\n    // Passing null here rather than the caller's fallback is deliberate. The SDK applies its own\n    // default only on a strict null check against the evaluated value, so handing it null makes\n    // this a straight pass-through of whatever the feature evaluated to, and every \"nothing usable\"\n    // case then lands on the single type check below.\n    //\n    // The evaluated value is null for an unknown feature key, for a feature with no default value\n    // configured, and for an unmet prerequisite. Passing the fallback here would cover exactly\n    // those and nothing else.\n    //\n    // What it would not cover is a value that is present but not a string. A feature configured\n    // with a number, a boolean, or a JSON value evaluates to that value, the SDK's null check does\n    // not fire, and the value comes back untouched. Undefined slips through for the same reason. So\n    // the type check below is load-bearing either way, and routing everything through it keeps the\n    // decision about what counts as usable in one place.\n\n    value = <T | null>growthbook.getFeatureValue<string | null>(featureKey, null);\n  } catch (error) {\n    console.warn('Error getting feature value from GrowthBook:', error);\n  } finally {\n    measure('growthbook-feature-value', startMillis, now(), {\n      feature: featureKey,\n      timeout: effectiveTimeoutMs\n    });\n  }\n\n  // The value comes from GrowthBook, so its type is an unverified boundary assertion. Coalesce\n  // anything that is not a string (null, or a missing or unexpected value) to the caller's default,\n  // so the resolved value is always a defined string.\n\n  if (typeof value !== 'string') {\n    value = fallback;\n  }\n\n  resolvedFeatures[featureKey] = value;\n\n  return value;\n}\n\n/**\n * Compares a value read through {@link getFeatureValue} against an expected value, ignoring case.\n * Every comparison against a feature value goes through this, even where a plain equality check\n * would do today, so that the casing decision lives in one place and new code copied from an\n * existing call site picks it up.\n *\n * The value is GrowthBook boundary data whose casing is not guaranteed, and a feature has been seen\n * returning an uppercase value. The expected side can be typed by hand too, such as an experiment\n * id typed into a section setting in the theme editor. Neither side's casing is enforced anywhere,\n * so we lowercase both. We leave the value itself untouched in the wrapper, so a caller reading it\n * directly still sees exactly what GrowthBook served.\n *\n * @param value The feature value\n * @param expected The value to compare against\n */\nexport function featureValueMatches(value: string, expected: string) {\n  return value.toLowerCase() === expected.toLowerCase();\n}\n","import { cartUpdate } from '@integrabeauty/shopify-ajax-api';\nimport { getEventQueue, isCartUpdatedEvent } from '../../lib/cart-event-queue.js';\nimport { ready } from '../../lib/dom-ready.js';\nimport { getFeatureValue, featureValueMatches } from '../../lib/growthbook-feature.js';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * We are intentionally not using a transaction to lock down the UI here or rewrite the global page\n * cart state. We do not think it is needed here. Beware that this relies on other code that checks\n * for this attribute to not rely on the cart state from events, unless it so happens to be based on\n * updated state after a later fetch. So this is a point in time where local cart state in the cart\n * lifecycle goes stale.\n *\n * Shopify's update cart endpoint only mutates the specified attributes. We do not need to gather\n * all attributes first in order to ensure they are preserved. We are just writing the attributes we\n * own and leaving all of the other attributes unaffected. This function is written with the\n * assumption that there is a point to the update.\n *\n * This should not be called when the attribute already exists. The caller should be checking first.\n */\nfunction onGetFeatureValue(promoProgress: HTMLElementTagNameMap['promo-goal-progress-v2'],\n  group: string, cartTotal: number) {\n  // We have just found out which group this shopper is in, and this is the only point in this page\n  // view where we know it. The attribute write below does not come back around to us as a cart\n  // updated event, so nothing else is going to show the bar. If we leave it to the attribute, the\n  // shopper sees no bar at all until their next page load fetches the cart from Shopify and tells\n  // us the group all over again.\n\n  // Only group \"2\" sees the free shipping progress bar when the cart total is at least $75\n  if (featureValueMatches(group, '2') && cartTotal >= 7500) {\n    promoProgress.dataset.show = 'true';\n  }\n\n  // Only group \"1\" sees the free shipping progress bar unconditionally\n  if (featureValueMatches(group, '1')) {\n    promoProgress.dataset.show = 'true';\n  }\n\n  // We clear the attribute from the first shipping experiment at the same time. Shoppers who were\n  // in that experiment still carry it on their cart, and Shopify copies every cart attribute onto\n  // the order's note attributes at checkout, so it keeps turning up in reporting long after that\n  // experiment ended. Setting a value to null is how we remove a key.\n  return cartUpdate({\n    attributes: {\n      _aux_opt_in: null,\n      _aux_opt_in_v2: null,\n      _aux_opt_in_v3: group\n    }\n  });\n}\n\nfunction onCartUpdated(event: CartUpdatedEvent) {\n  const promoProgress = document.querySelector<HTMLElementTagNameMap['promo-goal-progress-v2']>(\n    'cart-element promo-goal-progress-v2');\n\n  // Since the feature gate determining whether to render the promo goals progress bar is server\n  // side, we need to check if the promo goals progress bar exists to determine if the experiment is\n  // active. If the promo goals progress bar does not exist, we can exit early.\n  if (!promoProgress) {\n    return;\n  }\n\n  // Once a shopper's group is written on their cart, every later cart update comes back through\n  // here, so this is what shows the bar on all of their page loads after the one where we picked\n  // the group. We stop instead of falling through, because reading the feature again would report\n  // another exposure to GrowthBook for a shopper we already bucketed.\n\n  // If the shopper is in the \"2\" group they are shown the $89 Free Shipping progress bar when the\n  // cart total price is at least $75\n  if (event.detail.cart.attributes?._aux_opt_in_v3 === '2') {\n    // Since cart total price changes in both directions, we must also hide the progress bar when\n    // the total falls below the threshold.\n    if (event.detail.cart.total_price >= 7500) {\n      promoProgress.dataset.show = 'true';\n    } else {\n      promoProgress.dataset.show = 'false';\n    }\n\n    return;\n  }\n\n  // If the shopper is in the \"1\" group they are shown the $89 Free Shipping progress bar\n  // unconditionally\n  if (event.detail.cart.attributes?._aux_opt_in_v3 === '1') {\n    promoProgress.dataset.show = 'true';\n    return;\n  }\n\n  // Shoppers bucketed into \"old\" group get no progress bar, so there is nothing to render. We still\n  // have to stop here so that we do not fall through and read the feature a second time.\n  if (event.detail.cart.attributes?._aux_opt_in_v3 === '0') {\n    return;\n  }\n\n  getFeatureValue('shipping-suppression-experiment', '0')\n    .then(value => onGetFeatureValue(promoProgress, value, event.detail.cart.total_price))\n    .catch(console.warn);\n}\n\nfunction onReady() {\n  for (const event of getEventQueue()) {\n    if (isCartUpdatedEvent(event)) {\n      try {\n        onCartUpdated(event);\n      } catch (error) {\n        console.warn(error);\n      }\n    }\n  }\n\n  addEventListener('cart-updated', onCartUpdated);\n}\n\nready(onReady);\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A thin progress bar for a wait whose real progress we cannot measure, where all we know is the\n * longest the wait can last. A small ball moves along a line and gets all the way to the end right\n * when that longest time is up. It moves quickly at first and slows down as it goes, so a short\n * wait still looks like it got somewhere and a long one never looks stuck. The bar fades in slowly,\n * so a wait that ends quickly barely shows it.\n *\n * The bar is timed entirely by CSS animations, so there is no state to keep and nothing for a\n * caller to start or stop. It starts over each time it goes from hidden to shown, for example when\n * the cart drawer switches to the progress view. Whoever places it has to set its duration through\n * the `--progress-bar-custom-duration` custom property, and without one the ball never moves.\n */\nclass ProgressBarCustom extends HTMLElement implements CustomElement {\n  public connectedCallback() {\n    this.initializeShadowRoot();\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'progress-bar-custom': ProgressBarCustom;\n  }\n}\n\nif (!customElements.get('progress-bar-custom')) {\n  customElements.define('progress-bar-custom', ProgressBarCustom);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * A view of the cart drawer saying that something is in progress, shown while a cart change is in\n * flight. Whoever asks the drawer for this view passes the message along with the request, so the\n * view itself knows nothing about which change it is waiting on. Add to cart is one user, and the\n * drawer itself is another while the cart initializes. The drawer handles showing and hiding it,\n * and switching back to the cart, so this element only lays out the view, shows the message, and\n * asks the drawer to close when its close button is clicked.\n */\nclass ProgressView extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-focus-requested', 'data-message'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * \"embedded\" on the cart page, where there is no drawer to close. Set in the liquid.\n     */\n    layout: string;\n\n    /**\n     * The text to show, such as \"Just a moment ...\". Set by the drawer control from the open\n     * request, or by the drawer while the cart initializes.\n     */\n    message: string;\n  };\n\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.renderMessage();\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseClickedBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n\n    if (this.isConnected && name === 'data-message') {\n      this.renderMessage();\n    }\n  }\n\n  public disconnectedCallback() {\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseClickedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * See the upsell view for why focus is forced visible here.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private renderMessage() {\n    const messageEl = this.shadowRoot.querySelector('#message');\n    messageEl.textContent = this.dataset.message || '';\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'progress-view': ProgressView;\n  }\n}\n\nif (!customElements.get('progress-view')) {\n  customElements.define('progress-view', ProgressView);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { getEventQueue, isCartUpdatedEvent } from '../../../lib/cart-event-queue.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype CartUpdatedEvent = GlobalEventHandlersEventMap['cart-updated'];\n\n/**\n * The pre-checkout upsell, shown as a view of the cart drawer. The drawer switches to it when a\n * shopper clicks Checkout, and \"Back to Bag\" switches back. The drawer handles showing and hiding,\n * so this element only lays out the view, keeps its product list in sync with the cart, and asks\n * the drawer to close when its close button is clicked.\n */\nclass UpsellView extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-focus-requested'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Currency ISO code. Required.\n     *\n     * @example \"US\"\n     */\n    currencyCode: string;\n\n    /**\n     * Currency Symbol. Required.\n     *\n     * @example \"US\"\n     */\n    currencySymbol: string;\n\n    /**\n     * Set to \"true\" by the cart drawer when it shows this view, asking the view to put focus on its\n     * close button. The view removes it again once it has, so the next request fires too.\n     */\n    focusRequested: string;\n\n    /**\n     * Subtitle copy to display in the pre-checkout upsell\n     */\n    subtitle: string;\n\n    /**\n     * Title copy to display in the pre-checkout upsell\n     */\n    title: string;\n  };\n\n  private declare cartVariantIds: number[];\n  private onCartUpdatedBound = this.onCartUpdated.bind(this);\n  private onCloseClickedBound = this.onCloseClicked.bind(this);\n\n  public connectedCallback() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseClickedBound);\n\n    for (const event of getEventQueue()) {\n      if (isCartUpdatedEvent(event)) {\n        try {\n          this.onCartUpdatedBound(event);\n        } catch (error) {\n          console.warn(error);\n        }\n      }\n    }\n\n    addEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  public attributeChangedCallback(name: string, _oldValue: string, newValue: string) {\n    if (this.isConnected && name === 'data-focus-requested' && newValue === 'true') {\n      this.focusCloseButton();\n    }\n  }\n\n  public disconnectedCallback() {\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseClickedBound);\n\n    removeEventListener('cart-updated', this.onCartUpdatedBound);\n  }\n\n  /**\n   * Focus moves here programmatically, so without forcing the visible-focus state the browser\n   * suppresses the focus ring after a mouse-driven open or switch, and the just-focused close\n   * button gives no visual cue of where focus landed. Older browsers that ignore the option degrade\n   * to an unadorned programmatic focus.\n   */\n  private focusCloseButton() {\n    delete this.dataset.focusRequested;\n    this.shadowRoot.querySelector('close-button').focus({ focusVisible: true });\n  }\n\n  private render() {\n    const titleEl = this.shadowRoot.querySelector('.header h2');\n    titleEl.textContent = this.dataset.title;\n\n    const subtitleEl = this.shadowRoot.querySelector('.header p');\n    subtitleEl.textContent = this.dataset.subtitle;\n\n    if (this.cartVariantIds?.length < 1) {\n      return;\n    }\n\n    const upsellItems = this.querySelectorAll<HTMLElement>('li[data-product-id]');\n    for (const upsellItem of upsellItems) {\n      const variantId = parseInt(upsellItem.dataset.variantId, 10);\n      // When the variant is already in the cart, hidden becomes true and the item is not shown.\n      // When it is not in the cart, hidden becomes false and the item is shown. We only upsell\n      // something the shopper does not already have.\n      upsellItem.hidden = this.cartVariantIds.includes(variantId);\n    }\n  }\n\n  private onCartUpdated(event: CartUpdatedEvent) {\n    this.cartVariantIds = event.detail.cart.items.map(item => item.variant_id);\n\n    this.render();\n  }\n\n  private onCloseClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['cart-close-requested']['detail'];\n    const event = new CustomEvent<Detail>('cart-close-requested', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        animate: true,\n        source: 'close-button'\n      }\n    });\n    this.dispatchEvent(event);\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'upsell-view': UpsellView;\n  }\n}\n\nif (!customElements.get('upsell-view')) {\n  customElements.define('upsell-view', UpsellView);\n}\n","import type { Cart } from '@integrabeauty/shopify-ajax-api';\nimport { v4 as uuidv4 } from 'uuid';\nimport { read } from '../../lib/cart/state.js';\n\n/**\n * Reports the start of checkout when the visitor clicks the checkout button.\n *\n * Checkout can occur on other websites such as Shopify's Shop Pay checkout. We do not have a\n * trivial way of recording the start of checkout when a checkout page loads. Therefore we have to\n * try to fire the event before navigating to checkout.\n *\n * This watches for the click from out here rather than being called by the cart actions element, so\n * that the element's own click handling stays concerned only with navigating to checkout. It is a\n * best effort report, and observing in parallel means it cannot delay or prevent the navigation.\n * The button lives inside a shadow root, so the click is identified from the composed path rather\n * than from the target, which is retargeted to the host by the time it reaches us. The report is\n * then dispatched from the button itself, since that is the element responsible for it. Bubbling\n * out of the shadow root is what carries it to the window level consumers that read it.\n *\n * The event is dispatched via setTimeout. Non-native events are evaluated synchronously by\n * listeners. Dispatching via setTimeout ensures that no long-running listener introduces extra\n * delay before the redirect occurs. It is vital that the redirect occurs quickly. There is already\n * a concerning lag that happens when navigating to checkout that we do not want to increase.\n */\nfunction onClick(event: MouseEvent) {\n  const targets = event.composedPath();\n  const target = targets.find(target => target instanceof HTMLButtonElement &&\n    target.id === 'checkout-button');\n  if (target) {\n    setTimeout(emit, 0, target);\n  }\n}\n\nfunction emit(target: EventTarget) {\n  const cart = read();\n\n  const startedEvent = new CustomEvent<CheckoutStartedEvent['detail']>('checkout-started', {\n    bubbles: true,\n    cancelable: false,\n    composed: true,\n    detail: {\n      cart,\n      event_id: uuidv4()\n    }\n  });\n\n  target.dispatchEvent(startedEvent);\n}\n\n/**\n * Announces that the visitor is on their way to checkout.\n */\ntype CheckoutStartedEvent = CustomEvent<{\n  /**\n   * Snapshot of the cart taken at the moment of the click.\n   */\n  cart: Cart;\n\n  /**\n   * Identifies this occurrence so downstream pixels can deduplicate it.\n   */\n  event_id: string;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'checkout-started': CheckoutStartedEvent;\n  }\n}\n\ndocument.addEventListener('click', onClick);\n","import { v4 as uuidv4 } from 'uuid';\nimport { getEventQueue, isCartUpdateErredEvent } from '../../lib/cart-event-queue.js';\n\n// Drives showing and hiding the cart drawer. It owns the open and close requests the rest of the\n// page dispatches, the light dismiss gesture the modal manager broadcasts, the decision to open on\n// a failed cart init, and the state attributes on the drawer element that everything else reads.\n// This lives beside the section index rather than inside it because the index is only there to pull\n// the section's pieces together, and it lives outside the drawer element because the requests come\n// from all over the page rather than from the element itself.\n\ntype CartCloseRequestedEvent = WindowEventMap['cart-close-requested'];\ntype ModalDismissRequestedEvent = WindowEventMap['modal-dismiss-requested'];\ntype CartUpdateErredEvent = GlobalEventHandlersEventMap['cart-update-erred'];\n\n/**\n * Opens the cart drawer. Emits a cart-drawer-opened event and a cart-viewed event.\n *\n * The cart-drawer-opened event is primarily only intended for downstream analytics, not managing UI\n * state. Avoid implementing any ui management logic that uses this event.\n *\n * Callers ask to open without knowing whether the drawer is already open, the same way they do when\n * closing, so this mirrors close and owns that check itself. The source is only for diagnostics.\n *\n * The view is the one to show, such as \"progress\", and missing means the cart. A request naming a\n * view while the drawer is already open switches to that view. The message is handed to the view as\n * its `data-message`, for views that show one.\n */\nfunction open(animate: boolean, source: string, view?: string, message?: string) {\n  const cartDrawer = document.querySelector<HTMLElement>(\n    'cart-drawer:not([data-layout=\"embedded\"])'\n  );\n\n  // Not finding the drawer is normal and not a problem. The cart page renders the drawer element in\n  // its embedded layout, which is always shown, so there is no drawer to open there.\n  if (!cartDrawer) {\n    return;\n  }\n\n  if (message) {\n    const viewEl = cartDrawer.querySelector<HTMLElement>(`[slot=\"${view || 'cart'}-view\"]`);\n    if (viewEl) {\n      viewEl.dataset.message = message;\n    }\n  }\n\n  // The modal manager marks the drawer open while it is shown, so an open request that arrives\n  // while it is already open is redundant and the rest of this would re-open an open drawer. Only a\n  // request naming a view still has something to do, which is switching to it.\n  if (cartDrawer.dataset.modalState === 'open') {\n    if (view) {\n      cartDrawer.dataset.view = view;\n    } else {\n      console.log('Drawer already open, ignoring open request from', source);\n    }\n\n    return;\n  }\n\n  // The drawer opens on the cart unless the request names another view, whichever view it was\n  // showing when it last closed. Resetting here rather than on close keeps the old view in place\n  // while the panel slides out. While the cart is still initializing there is no cart to show yet,\n  // so it opens on the progress view instead.\n  if (view) {\n    cartDrawer.dataset.view = view;\n  } else if (cartDrawer.dataset.cartInitializing === 'true') {\n    cartDrawer.dataset.view = 'progress';\n  } else {\n    cartDrawer.dataset.view = 'cart';\n  }\n\n  // Messages left from the last time the drawer was open are stale now. Clearing them here rather\n  // than on close keeps them in place while the panel slides out, the same as the view.\n  //\n  // This also clears a message the shopper never saw. If they close the drawer while an add is\n  // still running, the add finishes while the drawer is closed and its confirmation gets cleared\n  // the next time they open it. We are fine with that, because a shopper who closed the drawer\n  // before the add finished probably does not care about seeing the confirmation later.\n  const cartElement = cartDrawer.querySelector('cart-element');\n  if (cartElement) {\n    cartElement.dataset.clearMessages = 'true';\n  }\n\n  cartDrawer.dataset.animate = animate ? 'true' : 'false';\n\n  console.log('Cart opened', source);\n\n  const showEvent = new Event('modal-open-requested', { bubbles: true, composed: true });\n  cartDrawer.dispatchEvent(showEvent);\n\n  addEventListener('modal-dismiss-requested', onModalDismissRequested);\n\n  type CartDrawerOpenedEvent = GlobalEventHandlersEventMap['cart-drawer-opened'];\n  const openedEvent = new CustomEvent<CartDrawerOpenedEvent['detail']>('cart-drawer-opened', {\n    bubbles: true,\n    composed: true,\n    detail: {\n      event_id: uuidv4()\n    }\n  });\n  cartDrawer.dispatchEvent(openedEvent);\n\n  type CartViewedEvent = GlobalEventHandlersEventMap['cart-viewed'];\n  const viewedEvent = new CustomEvent<CartViewedEvent['detail']>('cart-viewed', {\n    bubbles: true,\n    composed: true,\n    detail: {\n      surface: 'drawer'\n    }\n  });\n  cartDrawer.dispatchEvent(viewedEvent);\n}\n\n/**\n * Closes the cart drawer.\n *\n * This bails out unless the drawer is actually open, which makes closing idempotent at the source.\n * Callers ask to close without knowing whether the drawer is open. The installment message closes\n * the cart preemptively before opening its own modal even though on a product page the cart is\n * almost never open, and the drawer's own close control can be activated twice in quick succession\n * before the first close finishes. Without this guard each of those still announces a close to the\n * modal manager, which then sees a close for a modal it does not have recorded as active and logs a\n * mismatch warning. Mirroring the early return that the open path already uses keeps a redundant\n * close from reaching the manager at all, and it also avoids broadcasting a closed event for a\n * close that never happened. Production diagnostics confirmed this unguarded close was the source\n * of those warnings and that the page was already fully torn down in every case, so returning here\n * strands nothing.\n *\n * The source is only for diagnostics, the same as on open.\n */\nfunction close(animate: boolean, source: string) {\n  // Drop the dismiss listener unconditionally, ahead of the early returns below. It is registered\n  // on the window while the drawer is open, so it outlives the drawer element: if the section is\n  // rebuilt and replaced while open, the replacement comes back without the open flag, and the\n  // guards below would then skip cleanup and leak the listener. Removal is idempotent and has no\n  // observable effect when nothing is registered, unlike the event broadcasts further down, which\n  // must stay behind the guard so a redundant close stays silent.\n  removeEventListener('modal-dismiss-requested', onModalDismissRequested);\n\n  const cartDrawer = document.querySelector<HTMLElement>(\n    'cart-drawer:not([data-layout=\"embedded\"])'\n  );\n\n  // Not finding the drawer is normal and not a problem. The cart page renders the drawer element in\n  // its embedded layout, which is always shown, so there is no drawer to close there.\n  if (!cartDrawer) {\n    return;\n  }\n\n  // The modal manager marks the drawer open while it is shown, so anything other than open means\n  // there is nothing to close and the rest of this would be a redundant close.\n  if (cartDrawer.dataset.modalState !== 'open') {\n    console.log('Not closing cart because the drawer is not open, close request from', source);\n    return;\n  }\n\n  cartDrawer.dataset.animate = animate ? 'true' : 'false';\n\n  console.log('Cart closed', source);\n\n  const closeEvent = new Event('modal-close-requested', { bubbles: true, composed: true });\n  cartDrawer.dispatchEvent(closeEvent);\n\n  const closedEvent = new Event('cart-drawer-closed', { bubbles: true, composed: true });\n  cartDrawer.dispatchEvent(closedEvent);\n}\n\ntype CartOpenRequestedEvent = WindowEventMap['cart-open-requested'];\n\nfunction onCartOpenRequested(event: CartOpenRequestedEvent) {\n  const animate = event.detail?.animate ?? true;\n  const source = event.detail?.source;\n  open(animate, source, event.detail?.view, event.detail?.message);\n}\n\nfunction onCartCloseRequested(event: CartCloseRequestedEvent) {\n  const animate = event.detail?.animate ?? true;\n  const source = event.detail?.source;\n  close(animate, source);\n}\n\n/**\n * Closes the cart drawer when the user performs a light-dismiss gesture (a click on the backdrop or\n * an escape key press) while it is the open modal. The modal manager broadcasts the request; we\n * close with animation since this is a user-initiated dismissal.\n */\nfunction onModalDismissRequested(_event: ModalDismissRequestedEvent) {\n  console.log('Closing cart on modal-dismiss-requested');\n  close(true, 'modal-dismiss');\n}\n\n/**\n * Opens the drawer when cart init fails, so the shopper sees that it failed. The message itself is\n * rendered separately by cart-messages in response to the same error, so all we do here is make it\n * visible. We have no other way to show the shopper that message. Only an init failure opens the\n * drawer. The other cart errors happen while the drawer is already in use, so they are left to\n * their own handling.\n *\n * We keep listening after the first failure rather than unregistering, because cart init can happen\n * more than once during the life of the page.\n */\nfunction onCartUpdateErred(event: CartUpdateErredEvent) {\n  if (event.detail.code === 'INIT' || event.detail.code === 'INIT_TIMEOUT') {\n    // The error message renders in the cart view. Naming it keeps the drawer off the progress view\n    // in case the drawer hears about the failure after this does.\n    open(true, 'cart-lifecycle-error', 'cart');\n  }\n}\n\nfunction main() {\n  // Init can fail before this bundle runs, because this bundle is deferred and the lifecycle\n  // snippet starts init while the page is still parsing. A listener we register now would never\n  // hear about that failure, and the drawer would stay shut over an error message already rendered\n  // inside it. So we replay the buffered cart events for a failure we missed, then listen for the\n  // ones still to come.\n\n  const cartUpdateErrorEvents = getEventQueue().filter(isCartUpdateErredEvent);\n\n  if (cartUpdateErrorEvents.length > 0) {\n    console.log('Replaying %d cart error events in drawer-control', cartUpdateErrorEvents.length);\n  }\n\n  for (const event of cartUpdateErrorEvents) {\n    onCartUpdateErred(event);\n  }\n\n  addEventListener('cart-update-erred', onCartUpdateErred);\n  addEventListener('cart-close-requested', onCartCloseRequested);\n  addEventListener('cart-open-requested', onCartOpenRequested);\n}\n\nmain();\n"],"names":["getEventQueue","cart_lifecycle","queue","isCartUpdatedEvent","value","Event","type","isCartUpdateErredEvent","ready","callback","document","readyState","timer","setTimeout","clearTimeout","addEventListener","removeEventListener","CartActions","HTMLElement","checkoutInitSucceeded","onCheckoutButtonClickedBound","this","onCheckoutButtonClicked","bind","onCartTransactionStartedBound","onCartTransactionStarted","onCartTransactionCompletedBound","onCartTransactionCompleted","onCartUpdatedBound","onCartUpdated","onReadyBound","onReady","onPageShowBound","onPageShow","onContinueShoppingButtonClickedBound","onContinueShoppingButtonClicked","connectedCallback","initializeShadowRoot","releaseReady","disconnectedCallback","checkoutButton","shadowRoot","querySelector","continueShopping","attachShadow","mode","innerHTML","isConnected","continueButton","dataset","hideCloseButton","remove","event","error","console","warn","button","currentTarget","HTMLButtonElement","log","disabled","sessionStorage","setItem","Date","now","toString","cartDrawer","closest","upsellView","querySelectorAll","length","cart_drawer","upsell_modal_displayed","view","cartElement","clearMessages","url","URL","Shopify","routes","root","skipShopPay","searchParams","set","location","href","_event","endLoading","detail","is_initial","persisted","CustomEvent","bubbles","composed","animate","source","dispatchEvent","_performance","performance","debug","measure","name","start","end","_performance3","customElements","get","define","CartDrawer","observedAttributes","onCartInitRequestedBound","onCartInitRequested","onCartUpdateErredBound","onCartUpdateErred","showCartAfterProgressBound","showCartAfterProgress","layout","renderEmbeddedLayout","some","startInitProgress","renderView","attributeChangedCallback","oldValue","newValue","render","onViewChanged","endInitProgress","scrollTo","cartInitializing","progressView","message","remaining","progressShownAt","Math","max","showCartDrawer","modal","inert","focusView","hideCartDrawer","modalState","viewSlotName","slots","slot","hidden","panel","removeAttribute","tabIndex","focusRequested","focus","preventScroll","CartElement","onCloseClickedBound","onCloseClicked","onContinueShoppingButtonClickBound","onContinueShoppingButtonClick","onShowDiscountInputClickedBound","onShowDiscountInputClicked","cartViewedDispatched","_oldValue","focusCloseButton","cartMessages","clear","closeButton","showDiscountInput","bestSellersUrl","focusVisible","templateName","origin","onCartUpdatedInitial","cart","items","role","promo","installmentsMessage","countryCode","isMBG","classList","add","estimatedTotal","total_price","renderDeferredPaymentMessage","estTotal","subunits","currencyCode","currencySymbol","hideBackdrop","renderLineItems","scenario","pulseLastAdded","variants_added","dispatchCartViewed","heading","quantity","item_count","textContent","join","cartContents","emptyCart","footerEl","lineItems","promoGoalEl","installmentPaymentMessageElement","_cart$attributes","attributes","_aux_opt_in_v2","promoGoalV2El","show","surface","deferredAmount","item","_item$selling_plan_al","_item$selling_plan_al2","selling_plan_allocation","selling_plan","recurring_deliveries","price","line_level_total_discount","dueTodayEl","parents","uniqueParentKeys","Set","_item$parent_relation","parent_relationship","parent_key","giftWithPurchaseProduct","find","properties","_gift_with_purchase","fragment","createDocumentFragment","_item$parent_relation2","key","element","createItemElement","has","isParent","appendChildItems","appendChild","final_price","isGiftWithPurchase","giftCount","giftChangeable","regularPrice","parseInt","_gift_with_purchase_price","linePrice","row","variants","variant","lineItem","variant_id","pulse","parentElement","parentKey","_item$parent_relation3","childElement","_item$parent_relation4","_item$parent_relation5","_item$parent_relation6","_item$parent_relation7","lineItemEl","createElement","imageSrc","image","imageAlt","title","isAvailable","lineItemId","variantId","productType","product_type","productUrl","original_line_price","linePriceAfterDiscount","final_line_price","parentId","_is_upsell","isUpsell","maxQuantity","handle","productHandle","product_id","productId","sellingPlanId","JSON","parse","id","upgradeButton","clickTrack","sellingPlanFrequency","replace","select","options","Array","isArray","filter","option","optionEl","selected","messages","push","allocation","line_level_discount_allocations","discount_application","amount","toFixed","test","code","_contains_alcohol","_compatibility","_final_sale","_backorder_message","_is_tbyb","generateMessages","ul","content","cloneNode","itemEl","update","showBackdrop","backdrop","_item$properties","getRequestId","response","headers","isResponse","ok","status","text","json","decorateError","resolveUrl","path","protocol","host","async","cartChange","input","init","body","stringify","cache","method","fetch","Error","_error$response_body3","_error$response_body4","consumeError","response_body","description","cartFetch","responseBodyText","responseBodyObject","isCart","cartUpdate","_error$response_body5","_error$response_body6","isUnavailableError","isObject","startsWith","includes","digest","cart_input_type","_cart$cart_level_disc","_cart$items","token","attribute_keys","Object","keys","discount_codes","cart_level_discount_titles","cart_level_discount_applications","map","application","_item$line_level_disc","discounts","_allocation$discount_","digest_failed","hasSimilarItems","cart1","cart2","item1","found","item2","decorate_error_decorateError","assignmentError","reason","tag","prototype","call","report","dispatcher","target","Element","window","dispatchCartUpdateErred","ignoredCodes","_detail$transaction","_detail$transaction2","inputs","transaction","reportError","raise","escalateCartError","assert","expression","AssertionError","constructor","super","rnds8","Uint8Array","byteToHex","i","slice","unsafeStringify","arr","offset","toLowerCase","dist_v4","buf","crypto","randomUUID","_ref","_options$random","rnds","random","rng","getRandomValues","RangeError","_v4","copy","structuredClone","cloned","trustworthy","isWellFormed","upper","toUpperCase","containsJavascriptWord","getApplicableDiscountCodes","applicable","getDiscountCodes","write","initial","previous","read","oldVariants","groupByVariant","newVariants","prevCodes","currCodes","discount_codes_added","complement","discount_codes_removed","event_id","findVariantsAdded","variants_removed","findVariantsRemoved","state_cartFetch","limit","attempts","delays","lastError","delay","resolveAfterTimeout","cartFetchOnceWithTimeout","timeout","CartFetchTimeoutError","timeoutPromise","Promise","_resolve","reject","race","milliseconds","resolve","output","property","merged_properties","image_url","product_title","sku","variant_title","c0map","c1map","increments","decrements","a","b","getSharedState","cart_transaction","activeCount","count","timeline","shared","Number","isInteger","_len","arguments","args","_key","epoch","completed","startMillis","didTimeOut","complete","snapshot","reportConcurrency","append","duration","round","undefined","entry","splice","change","remoteCart","error_code","determineErrorCode","previous_cart","fetched_cart","removeLines","lineKeys","lineIndicesToRemove","reverse","line","removeFollowUp","followUp","followUpKeys","latestCart","errorCode","every","isString","size","fetchedCart","line_keys","update_determineErrorCode","isThresholdAchieved","settings","findGift","giftThreshold","cents","min","findGiftKeysToRemove","gift","findGiftKeysToRemoveFromPage","giftBar","thresholdCents","thresholdQuantity","thresholdType","CartLineItem","onSellingPlanChangedBound","onSellingPlanChanged","onUpgradeButtonClickBound","onUpgradeButtonClick","onRemoveLineItemClickBound","onRemoveLineItemClick","onChangeGiftClickBound","onChangeGiftClick","onQuantityChangedBound","onQuantityChanged","removeButton","changeGiftButton","sellingPlanSelect","_name","HTMLSelectElement","updates","catch","actionElements","actionElement","primaryImageElement","src","alt","width","featuredImageAnchor","lineItemTitle","linePriceAfterDiscountEl","linePriceEl","ariaLabelPrefix","setAttribute","renderQuantity","quantityElement","optionsHtml","CartMessages","initErrorAlert","onClearRequested","alerts","alert","_event$detail$inputs","showInitErrorMessage","showErrorMessage","before_codes","_event$detail$scenari","errors","showItemsAddedMessage","itemsString","alertType","showDiscountCodeAppliedMessages","showDiscountRemovedMessages","showSuccessMessage","upsellItem","showUpsellItemMessage","backorderedItem","showBackorderItemMessage","addedLineItemsString","backorderMessage","backorderAlert","messageHtml","onReloadBagClick","_button$closest","requestEvent","CartTotals","discountRow","discountAmount","subtotalEl","subtotalAmount","discountTotalEl","discountPercent","subtotal","compareAtPrice","_value_set_compare_price","floor","computeSubtotal","applied","style","display","orderDiscountsValue","discount","total_allocated_amount","orderDiscountsEl","CartUpsellItem","buildTitle","renderTitle","renderImage","renderPrices","renderAddButton","renderReviews","productTitle","variantTitle","anchor","resolvePrices","_this$dataset$product","upsellTagSearch","upsellTag","productTags","split","t","upsellPriceArray","upsellPriceString","upsellPrice","parseFloat","prices","priceEl","compareAtPriceEl","hide","addButton","country","customAttribute","reviews","average","reviewsAverage","reviewsCount","CartUpsellItems","requestAnimationFrame","variantIds","elements","visibleCount","formatMoney","format","placeholderRegex","formatString","formatWithDelimiters","number","precision","thousands","decimal","isNaN","parts","match","FreeShippingBanner","subscriptionsInCart","span","progressContainer","progressBar","percentage","getMessageText","threshold","subscriptionText","difference","GiftQualifiedMessage","onClickBound","onClick","_this$shadowRoot$quer","applyDiscountsHelper","newCodes","overrides","postValidate","discount_code","validateCodesBeforeUpdate","oldUppers","newCodesCount","currentCodes","getCurrentCodes","newUppers","retainedOldUppers","override","old_code","new_code","updatedCart","updatedNormalCodes","newCode","upperNewCode","validateCodesAfterUpdate","discountCodes","enrichedItems","enrich","stepStart","_error$response_body","_error$response_body2","responseConsumeAndParseBodyError","cartAdd","beforeCart","afterCart","discount_overrides","applyDiscountsOnAdd","addItemsInternal","add_determineErrorCode","isBundleAddonMixError","isMaxQuantityError","isInvalidSellingPlanError","_item","_item2","_url_path","pathname","OfferView","onCartDrawerClosedBound","onCartDrawerClosed","onConfirmClickedBound","onConfirmClicked","onGiftClickedBound","onGiftClicked","onOfferViewRequestedBound","onOfferViewRequested","onSkipClickedBound","onSkipClicked","_this$shadowRoot$quer2","_this$shadowRoot$quer3","claimedGift","_event$detail","_this$claimedGift$var","_this$claimedGift","saving","gifts","drawer","returnView","selectedVariantId","offeredEvent","addGift","_this$claimedGift2","showView","saveGift","finally","rememberSkip","_gwp_skipped","skippedEvent","_this$claimedGift3","added","_this$claimedGift4","claimedEvent","unlock","confirm","label","skip","GiftWithPurchaseProduct","imageEl","imageUrl","titleEl","subtitleEl","subtitle","selectedCheckEl","checked","GiftWithPurchaseProgress","unlockPending","skipClearPending","cartQuantity","quantityThresholdInitialized","shippingInitialized","thresholdAchieved","achievedStateKnown","claimedGiftWithPurchase","updateAchievedState","fromThisTab","skipped","giftKeys","getThresholdSettings","isShopperChange","achieved","claimed","renderQualifiedMessages","renderThresholdMessage","renderShippingMessage","renderShippingProgress","renderQuantityThresholdProgress","renderCentsThresholdProgress","hasGifts","container","stepSize","goals","progressAchieved","stepOffset","goal","left","quantityStep","step","progressWidth","giftGoal","total","showFreeShipping","shippingGoal","freeShippingThresholdCents","shippingThreshold","shippingPercentage","shipping","shippingMessage","shippingElement","giftMessage","PromoGoalProgressV2","ingestPromoGoals","promoGoals","offerText","goalNamesContainer","goalIndex","nameDiv","spanDiv","thresholdsContainer","thresholdElement","DiscountInput","activeTransactions","errorMessage","onCartTransactionEndedBound","onCartTransactionEnded","onDiscountFormSubmitBound","onDiscountFormSubmit","onDiscountInputBound","onDiscountInput","form","inputElement","codes","discountAmounts","Map","currentTotal","getDiscountAmounts","unmatched","from","appliedDiscountsEl","appliedDiscountTemplate","appliedDiscountFragment","createAppliedDiscount","template","discountInfo","amountEl","infoElement","cta","onRemoveDiscountClicked","preventDefault","appliedCart","normalNewCodes","before_cart","new_codes","apply","then","_existingCodes","existingCodes","before","previousCart","previousCode","remove_remove","PromoGoal","thresholdAmount","computeQualifyingSubtotal","originalTotalPrice","original_total_price","totalDiscountPrice","calculateTotalDiscount","totalPromoGoalDiscountPrice","isPromoNamedDiscount","calculateTotalPromoGoalDiscount","totalPriceOfExcludedItems","gift_card","calculateTotalPriceOfPromoExcludedItems","PromoGoalProgress","qualifyingSubtotal","paragraph","collectPromoGoalsData","eligibleThreshold","eligiblePromoGoals","promoGoal","isLocked","amountToGoal","percentSpentToGoal","progressEl","spentEl","goalEl","sort","compareThresholds","getFeatureValue","featureKey","fallback","timeoutMs","resolvedFeatures","growthbook_runtime","resolved_features","effectiveTimeoutMs","default_timeout_ms","load_state","extraMillis","previewMode","preview_hold_ms","once","growthbook","client_loader","feature","featureValueMatches","expected","_event$detail$cart$at","_event$detail$cart$at2","_event$detail$cart$at3","promoProgress","_aux_opt_in_v3","group","cartTotal","_aux_opt_in","onGetFeatureValue","ProgressBarCustom","ProgressView","renderMessage","UpsellView","_this$cartVariantIds","cartVariantIds","upsellItems","emit","startedEvent","cancelable","drawer_control_open","viewEl","showEvent","onModalDismissRequested","openedEvent","viewedEvent","drawer_control_close","closeEvent","closedEvent","onCartOpenRequested","_event$detail$animate","_event$detail2","_event$detail3","_event$detail4","onCartCloseRequested","_event$detail$animate2","_event$detail5","_event$detail6","composedPath","cartUpdateErrorEvents","main"],"sourceRoot":""}