{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/index.cjs","../../src/components/announcement-bar.tsx","../../src/components/categories-cart.tsx","../../src/utils/url-fix.ts","../../src/utils/vendor-media-stub.ts","../../src/components/vendor-icon.tsx","../../src/components/category-card.tsx","../../src/components/auth-stub.tsx","../../src/components/user-summary-stub.tsx","../../src/components/vendor-display-button.tsx","../../src/components/comment-card.tsx","../../src/components/comment-composer.tsx","../../src/components/filter-chip.tsx","../../src/components/footer.tsx","../../src/components/unified-filter-logic.tsx","../../src/utils/format-text-stub.ts","../../src/components/footer-waitlist-button.tsx","../../src/components/hero-image-uploader.tsx","../../src/components/icons-block.tsx","../../src/components/icons-stub.tsx","../../src/components/image-cropper.tsx","../../src/components/metric-value.tsx","../../src/components/msp-display.tsx","../../src/components/open-source-features.tsx","../../src/components/pricing-display.tsx","../../src/components/results-count.tsx","../../src/components/vendor-tag.tsx","../../src/components/openframe-logo.tsx","../../src/components/selection-source-badge.tsx","../../src/components/vendor-page-skeleton.tsx","../../src/components/why-it-matters.tsx","../../src/components/yes-no-display.tsx","../../src/components/made-with-love.tsx","../../src/components/date-time-picker.tsx","../../src/components/authors/author-detail-view.tsx"],"names":["jsx","jsxs","VendorDirectoryIcon","OpenSourceIcon","CommunityHubIcon","VendorsIcon","CommunityIcon","CompareIcon","SunIcon","MoonIcon","CheckCircleIcon","GitHubIcon","PlusCircleIcon","OpenmspLogo","useMemo","OpenFrameLogo","FlamingoLogo","DatePicker"],"mappings":"AAAA,6xBAAY;AACZ,YAAY;AACZ,iCAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B;AACE;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACF,yDAA8B;AAC9B,iCAA8B;AAC9B,iCAA8B;AAC9B;AACA;ACnxCA,2CAAkB;AAClB,8BAAmE;AA6PvD,+CAAA;AA/LZ,IAAM,mBAAA,EAAqB,CAAA,EAAA,GAAM,CAAA,EAAA,GAAM;AAAC,CAAA;AAEjC,SAAS,eAAA,CAAgB;AAAA,EAC9B,mBAAA;AAAA,EACA,YAAA,EAAc,KAAA;AAAA,EACd,YAAA,EAAc,IAAA;AAAA,EACd;AACF,EAAA,EAA0B,CAAC,CAAA,EAAG;AAM5B,EAAA,MAAM,SAAA,EAAW,0CAAA,CAAW;AAG5B,EAAA,MAAM,UAAA,EAAY,mDAAA,CAAoB;AACtC,EAAA,MAAM,IAAA,EAAM,YAAA,EAA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sBAAA;AAYf,sBAAA;AAAoF,wBAAA;AACkB,QAAA;AACzE,QAAA;AAE3BA,wBAAAA;AAAC,UAAA;AAAA,UAAA;AACQ,YAAA;AACgC,YAAA;AACtB,cAAA;AACJ,cAAA;AACiC,cAAA;AAC5C,YAAA;AAAA,UAAA;AACJ,QAAA;AAEJ,MAAA;AACF,IAAA;AAEoE,IAAA;AAK7D,IAAA;AAKN,IAAA;AACH,EAAA;AAEJ;AlCouH0K;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/components/index.cjs","sourcesContent":[null,"'use client';\n\nimport { X } from 'lucide-react';\nimport { useEffect, useMemo, useState, useSyncExternalStore } from 'react';\nimport { useEndpointsRuntime } from '../contexts/endpoints-runtime-context';\nimport { useSelfFetch } from '../hooks/use-self-fetch';\nimport type { Announcement, AnnouncementBarProps, AnnouncementResponse } from '../types/announcement';\nimport { ANNOUNCEMENT_CTA_DEFAULTS } from '../types/announcement';\nimport {\n  clearLegacyAnnouncementCache,\n  dismissAnnouncement,\n  isAnnouncementDismissed,\n} from '../utils/announcement-storage';\nimport { getAppType } from '../utils/app-config';\nimport { pickReadableTextColor } from '../utils/color-analysis';\nimport { EntityIcon } from './icon-display';\nimport { AnnouncementBarView } from './ui/announcement-bar-view';\nimport { Button } from './ui/button';\n\n/**\n * ============================================================================\n * DO NOT REDESIGN THIS BAR. THE VISUAL DESIGN IS FINAL AND OWNER-APPROVED.\n * ============================================================================\n * The bar's look (44px single-line strip, text-h6 message, 20/24px icon,\n * size=\"compact\" CTA, ghost-icon dismiss) is DELIBERATE and has ALREADY been\n * regressed and reverted once (#1545 \"redesigned\" it taller/stacked; #1551\n * reverted that UI back to #1542's approved design). See the full warning in\n * `ui/announcement-bar-view.tsx`.\n *\n * A new Figma frame is NOT permission to change any of it. Do not bump the\n * height, text scale, icon size, or CTA size, and do not re-introduce a\n * stacked / full-width-CTA mobile layout. Changes here require Michael's\n * explicit written sign-off on the PR, or they get reverted. AGAIN.\n * ============================================================================\n *\n * Platform announcement bar — DUAL MODE, works with or without SSR.\n *\n * SSR mode (Next hosts, e.g. the hub): the server resolves the announcement\n * and the dismissal cookie, then passes `initialAnnouncement` (nullable).\n * The bar renders in the first HTML byte (zero layout shift, no flash for\n * dismissed users) and skips the mount fetch.\n *\n * Client-only mode (bare React apps without SSR: Vite/CRA embeds — see\n * react-embedding-example): omit `initialAnnouncement` and mount the bar\n * PROP-LESS inside an EndpointsRuntime provider; it self-fetches from the\n * provider's `announcementsUrl` (a fixed suffix under the embedder's one\n * content base). No provider → renders nothing, never fetches (silent\n * no-op). Appearance animates in (grid 0fr→1fr) so there is no hard layout\n * jump. Deliberately NO per-component URL or platform knobs: the embedder\n * is platform-agnostic — each server returns ITS OWN announcement via\n * currentPlatform(); the platform is never sent as a parameter.\n *\n * Both modes: freshness is event-driven, no polling — `useSelfFetch`'s\n * visibility revalidation re-fetches on tab refocus when the held data is\n * >60s old (matching the server cache TTL by convention); idle tabs make\n * zero requests. Dismissal cookie is the SSOT (`announcement-storage.ts`);\n * storage reads happen ONLY in effects (a render-time read would desync\n * hydration in SSR mode).\n */\n/**\n * The dismissal store has no change event of its own — it is written only by\n * this component (and by another tab, which we notice on the next revalidation\n * render). So the subscription is a no-op and `getSnapshot` is simply re-read\n * on every render. Module-level so its identity never forces a re-subscribe.\n */\nconst subscribeToNothing = () => () => {};\n\nexport function AnnouncementBar({\n  initialAnnouncement,\n  previewMode = false,\n  dismissible = true,\n  className,\n}: AnnouncementBarProps = {}) {\n  // Namespace for the dismissal cookie/legacy keys. Next hosts inline\n  // NEXT_PUBLIC_APP_TYPE (matching the server's currentPlatform(), which the\n  // hub layout uses for the SSR cookie read); platform-agnostic embeds get\n  // the fallback — fine, cookies are domain-scoped and an embed domain\n  // serves one platform's announcements.\n  const platform = getAppType();\n\n  // Optional endpoint runtime: no provider → no URL → fetching disabled.\n  const endpoints = useEndpointsRuntime();\n  const url = previewMode ? null : (endpoints?.announcementsUrl ?? null);\n\n  // MUST be memoized (the hook re-syncs on [initialData] identity — an inline\n  // literal per render would setData-loop) and strict-undefined-mapped:\n  // `null` (hub: dismissed / no active announcement) still seeds the hook and\n  // skips the mount fetch; only an ABSENT prop (embeds) enables self-fetch.\n  const initialData = useMemo<AnnouncementResponse | undefined>(\n    () => (initialAnnouncement === undefined ? undefined : { announcement: initialAnnouncement }),\n    [initialAnnouncement],\n  );\n\n  const { data } = useSelfFetch<AnnouncementResponse>(url, {\n    initialData,\n    revalidateOnVisibleAfterMs: 60_000,\n  });\n  const announcement = data?.announcement ?? null;\n\n  // Hold the last non-null announcement so DEACTIVATION (a revalidation that\n  // returns null) collapses with the same 1fr->0fr animation as dismissal\n  // instead of unmounting in place (a hard 44px jump).\n  //\n  // State adjusted during render, not a ref written in an effect: the value is\n  // read on the very next line, so a ref would make this render depend on\n  // something React cannot replay (and the effect wrote it a commit LATE).\n  // The guard makes it idempotent — React re-runs this component immediately\n  // and the second pass takes the early exit.\n  const [lastAnnouncement, setLastAnnouncement] = useState<Announcement | null>(initialAnnouncement ?? null);\n  // Expanded (height) state — initial value is a pure function of props so\n  // the SSR HTML and the hydration render agree for every cohort.\n  const [expandedState, setExpandedState] = useState<boolean>(() => initialAnnouncement != null);\n  if (announcement && announcement !== lastAnnouncement) {\n    setLastAnnouncement(announcement);\n    // A DIFFERENT announcement re-arms the bar — the `else` half of the old\n    // reconciliation effect. It is masked by `dismissed` below, so a\n    // revalidation that returns an already-dismissed announcement stays\n    // collapsed exactly as before.\n    setExpandedState(true);\n  }\n  const displayAnnouncement = announcement ?? lastAnnouncement;\n\n  // Dismissal lives in a cookie (with a legacy localStorage fallback) — an\n  // external store, and one that MUST NOT be read during a plain render or SSR\n  // and hydration desync (see the header note). `useSyncExternalStore` is the\n  // primitive for exactly that: `getServerSnapshot` reports \"not dismissed\",\n  // matching what the server already folded into `initialAnnouncement`, and the\n  // client re-reads after hydration and on every subsequent render — including\n  // the one a revalidation causes, which is what collapses a bar dismissed\n  // seconds earlier in another tab.\n  //\n  // This replaces a reconciliation effect that called `setExpandedState` on\n  // every single run: a guaranteed second render pass per mount AND per\n  // completed fetch, to publish something derivable from the store.\n  const dismissed = useSyncExternalStore(\n    subscribeToNothing,\n    () => (!previewMode && announcement ? isAnnouncementDismissed(platform, announcement.id) : false),\n    () => false,\n  );\n\n  // Preview always mirrors the draft directly (storage is not consulted there).\n  const expanded = previewMode ? announcement != null : announcement != null && expandedState && !dismissed;\n\n  // Cleanup of the pre-refactor localStorage announcement cache. Keyed on\n  // `platform` rather than `[]`: the cache is per-platform, so a platform\n  // switch has its own stale entry to clear. Idempotent either way.\n  useEffect(() => {\n    if (previewMode) return;\n    clearLegacyAnnouncementCache(platform);\n  }, [platform, previewMode]);\n\n  // Legacy migration, and now the ONLY thing this pass does: an LS-only\n  // dismissal (no cookie — the server couldn't see it) backfills the cookie so\n  // the next SSR skips the bar. The visible collapse it used to perform here is\n  // derived from `dismissed` above instead. Idempotent, and a pure side effect\n  // on the external store, so it holds no state of its own.\n  //\n  // `data` is the trigger (it fires on the seed and after every completed\n  // fetch); the rest are read inside and are stable per mount in practice, but\n  // listing them is what keeps a changed `platform` from backfilling against\n  // the previous platform's store.\n  useEffect(() => {\n    if (previewMode || !announcement) return;\n    if (isAnnouncementDismissed(platform, announcement.id)) {\n      dismissAnnouncement(platform, announcement.id); // idempotent cookie backfill\n    }\n  }, [data, announcement, platform, previewMode]);\n\n  const handleDismiss = () => {\n    if (previewMode || !announcement) return;\n    dismissAnnouncement(platform, announcement.id);\n    // Release focus BEFORE the wrapper goes aria-hidden — a focused descendant\n    // inside an aria-hidden subtree strands screen-reader focus (and Chrome\n    // refuses to apply the attribute).\n    (document.activeElement as HTMLElement | null)?.blur?.();\n    setExpandedState(false);\n  };\n\n  const handleCtaClick = () => {\n    if (previewMode || !announcement?.cta_url) return;\n    // Scheme guard: cta_url is admin-entered data — only http(s) (absolute or\n    // relative) may navigate. A `javascript:` value would otherwise execute\n    // via location.href / window.open (stored XSS).\n    let safeUrl: string;\n    try {\n      // Resolve against the CURRENT page (not the origin) so path-relative and\n      // fragment CTAs keep their pre-guard semantics.\n      const parsed = new URL(announcement.cta_url, window.location.href);\n      if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;\n      safeUrl = parsed.href;\n    } catch {\n      return;\n    }\n    if (announcement.cta_target === '_blank') {\n      window.open(safeUrl, '_blank', 'noopener,noreferrer');\n    } else {\n      window.location.href = safeUrl;\n    }\n  };\n\n  // Never had anything to show: render nothing. Dismissal AND deactivation\n  // keep the element mounted (displayAnnouncement holds the last content) at\n  // 0fr so the collapse animates.\n  if (!displayAnnouncement) return null;\n\n  // Contrast is solved with the ODS THEME system, not color literals: the\n  // admin background's tone selects which ODS theme scopes the bar's content\n  // (light-toned background needs dark-on-light = the light theme's values;\n  // dark-toned needs the dark theme's). `.theme-light` / `.theme-dark` are\n  // the design system's own scoping classes (ods-colors.css) and flip every\n  // Tier-1 `--ods-*` primitive for descendants.\n  const themeScope =\n    pickReadableTextColor(displayAnnouncement.background_color) === 'dark' ? 'theme-light' : 'theme-dark';\n\n  // Inside the scope, colors reference Tier-2 semantic aliases. Aliases\n  // declared only at :root would NOT re-resolve in a nested theme scope\n  // (they compute at :root), so ods-colors.css re-anchors these three inside\n  // `.theme-light` / `.theme-dark` specifically for theme islands like this\n  // bar. Every value comes from the active ODS theme; nothing is hardcoded.\n  const barButtonClasses =\n    'text-[color:var(--color-text-primary)] hover:bg-[var(--color-bg-hover)] active:bg-[var(--color-bg-active)]';\n\n  const hasCta = Boolean(\n    displayAnnouncement.cta_enabled && displayAnnouncement.cta_url && displayAnnouncement.cta_text,\n  );\n\n  return (\n    <div\n      role=\"region\"\n      aria-label=\"Announcement\"\n      aria-hidden={!expanded}\n      // The marker means \"I am the PAGE-CHROME bar\" — `useHeaderHeight` sums\n      // the first match with the header to offset fixed panels (e.g. the\n      // sliding admin sidebar). Admin PREVIEW instances must NOT carry it, or\n      // a preview card inflates that offset (phantom gap above the drawer).\n      {...(previewMode ? {} : { 'data-announcement-bar': true })}\n      className={`relative grid w-full transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none ${previewMode ? '' : 'z-50'} ${className ?? ''}`}\n      style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}\n    >\n      {/*\n        Markup is the shared pure view (AnnouncementBarView), which carries the\n        bar's anatomy: ONE line of text inside a 44px strip, ONE compact CTA on\n        the right (hidden below `md`, where the content row is the tap target),\n        and a ghost-icon dismiss. Surface + content treatment stay here.\n      */}\n      <div\n        className={`min-h-0 overflow-hidden ${themeScope}`}\n        style={{ backgroundColor: displayAnnouncement.background_color }}\n      >\n        <AnnouncementBarView\n          className=\"text-[color:var(--color-text-primary)]\"\n          contentClassName={hasCta ? 'cursor-pointer md:cursor-default' : undefined}\n          onContentClick={hasCta ? handleCtaClick : undefined}\n          startAdornment={\n            /* ONE unified icon path (shared with the chat): uploaded image URL\n               wins, else a library glyph by name (+ props), via <EntityIcon>. */\n            <EntityIcon\n              icon={{\n                name: displayAnnouncement.icon_name || 'openframe-logo',\n                url: displayAnnouncement.icon_url,\n                props: displayAnnouncement.icon_props,\n              }}\n              size={24}\n              // 20px below `md`, 24px from `md` up. `!` is required: logo\n              // glyphs (LogoOpenframeIcon / sizedLogo in icon-library) drive\n              // their size via an inline style, which a plain class loses to.\n              // Not `--icon-size-icon-size` — that token is 16/24 and would\n              // shrink the mobile glyph.\n              className=\"relative !size-5 shrink-0 md:!size-6\"\n            />\n          }\n          title={\n            /* Single-line message: bold title + regular description inline,\n               truncating as one unit, at the strip's caption scale (`text-h6`\n               = 13/14px — the same treatment string titles get from the view).\n               Separator is a middot (house rule: no en/em dashes in copy). */\n            <p className=\"mb-0 min-w-0 max-w-full truncate text-h6\">\n              <span className=\"font-[number:var(--font-weight-semibold)]\">{displayAnnouncement.title}</span>\n              {displayAnnouncement.description && (\n                <span className=\"hidden opacity-80 sm:inline\"> · {displayAnnouncement.description}</span>\n              )}\n            </p>\n          }\n          actionBlock={\n            /* CTA - the common Button carrying the ADMIN-CONFIGURED colors\n               (cta_button_background_color / cta_button_text_color are an\n               admin FEATURE, defaults from ANNOUNCEMENT_CTA_DEFAULTS —\n               announcement colors are data, not token surfaces). Inline\n               styles win over the variant's hover classes on every state,\n               so hover feedback is opacity (the bar's original treatment);\n               nothing can render dark-on-dark. The view CSS-hides it below\n               `md`, where the whole content row is the tap target — this\n               Button stays the keyboard/AT path (known tradeoff).\n\n               Geometry + type come from the design system's size=\"compact\"\n               (24px caption-scale pill for slim strips — rationale documented\n               on the variant in button.tsx). */\n            hasCta && displayAnnouncement.cta_text ? (\n              <Button\n                onClick={handleCtaClick}\n                variant=\"outline\"\n                size=\"compact\"\n                className=\"transition-opacity hover:opacity-90\"\n                style={{\n                  backgroundColor:\n                    displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,\n                  color: displayAnnouncement.cta_button_text_color || ANNOUNCEMENT_CTA_DEFAULTS.text,\n                  borderColor: displayAnnouncement.cta_button_background_color || ANNOUNCEMENT_CTA_DEFAULTS.background,\n                }}\n                tabIndex={expanded ? 0 : -1}\n                leftIcon={\n                  displayAnnouncement.cta_show_icon && displayAnnouncement.cta_icon_name ? (\n                    <EntityIcon\n                      icon={{ name: displayAnnouncement.cta_icon_name, props: displayAnnouncement.cta_icon_props }}\n                      size={14}\n                      className=\"h-3.5 w-3.5\"\n                    />\n                  ) : undefined\n                }\n              >\n                {displayAnnouncement.cta_text}\n              </Button>\n            ) : undefined\n          }\n          endAdornment={\n            /* Dismiss - the common Button in its ghost-icon treatment\n               (size=\"icon-sm\": 32px target, >= the 24px WCAG 2.5.8 AA floor,\n               16px glyph) with the bar's quiet tint hover. Inert in\n               previewMode; omitted entirely when the host opts out via\n               `dismissible={false}` (the bar then only disappears by\n               deactivating the announcement). */\n            dismissible ? (\n              <Button\n                onClick={handleDismiss}\n                variant=\"transparent\"\n                size=\"icon-sm\"\n                className={barButtonClasses}\n                aria-label=\"Dismiss announcement\"\n                type=\"button\"\n                tabIndex={expanded ? 0 : -1}\n              >\n                <X strokeWidth={2} />\n              </Button>\n            ) : undefined\n          }\n        />\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { ChevronRight } from 'lucide-react';\nimport Link from '../embed-shims/next-link';\nimport type { RealCategoryCardProps } from '../types/category';\nimport { VendorIcon } from './vendor-icon';\n\n// Component that receives vendor and subcategory data as props\nexport function CategoriesCart({\n  category,\n  vendors = [],\n  vendorCount = 0,\n  subcategoryCount = 0,\n  isLoading = false,\n  className = '',\n}: RealCategoryCardProps) {\n  return (\n    <Link\n      href={`/vendors?category=${category.slug}`}\n      className={`group relative block rounded-lg border border-ods-border bg-ods-card p-3 pb-4 transition-colors hover:border-ods-accent md:p-4 md:pb-6 ${className}`}\n    >\n      <div className=\"flex flex-col gap-4 md:gap-6\">\n        {/* Vendor Icons Grid */}\n        <div className=\"relative h-8 w-full overflow-hidden md:h-10\">\n          <div className=\"flex w-full gap-2 md:gap-3\">\n            {isLoading\n              ? // Skeleton loading for vendor icons\n                Array.from({ length: 20 }).map((_, index) => (\n                  <div\n                    key={index}\n                    className=\"h-8 w-8 flex-shrink-0 animate-pulse rounded bg-ods-border md:h-10 md:w-10\"\n                  />\n                ))\n              : vendors && vendors.length > 0\n                ? vendors.map(vendor => (\n                    <VendorIcon\n                      key={vendor.id}\n                      vendor={vendor}\n                      size=\"md\"\n                      className=\"overflow-hidden rounded opacity-60 grayscale filter\"\n                    />\n                  ))\n                : // No vendors found - show placeholder icons\n                  Array.from({ length: 6 }).map((_, index) => (\n                    <div\n                      key={index}\n                      className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded bg-ods-border opacity-30 md:h-10 md:w-10\"\n                    >\n                      <div className=\"h-4 w-4 rounded-sm bg-ods-skeleton md:h-6 md:w-6\" />\n                    </div>\n                  ))}\n          </div>\n\n          {/* Gradient overlays for fade effect */}\n          <div className=\"pointer-events-none absolute inset-y-0 left-0 w-4 bg-gradient-to-r from-[#212121] to-transparent md:w-6\" />\n          <div className=\"pointer-events-none absolute inset-y-0 right-0 w-4 bg-gradient-to-l from-[#212121] to-transparent md:w-6\" />\n        </div>\n\n        {/* Category Information - Updated to use real data */}\n        <div className=\"flex flex-col gap-2\">\n          <div className=\"flex flex-col gap-1\">\n            <h3 className=\"text-ods-text-primary transition-colors text-h3 group-hover:text-ods-accent\">\n              {category.name}\n            </h3>\n            <p className=\"text-ods-text-secondary text-h6\">\n              {subcategoryCount || 0} Subcategories • {vendorCount || 0} Products\n            </p>\n          </div>\n\n          <div className=\"flex items-start justify-between gap-4 md:items-end md:gap-6\">\n            <p className=\"flex-1 text-ods-text-primary text-h4\">{category.description}</p>\n\n            {/* Arrow Button */}\n            <div\n              className=\"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md border border-ods-border bg-transparent transition-colors group-hover:bg-ods-accent md:h-12 md:w-12\"\n              aria-label={`View ${category.name} category`}\n            >\n              <ChevronRight className=\"h-5 w-5 text-ods-text-primary transition-colors group-hover:text-ods-text-on-accent md:h-6 md:w-6\" />\n            </div>\n          </div>\n        </div>\n      </div>\n    </Link>\n  );\n}\n","/**\n * Utility functions for fixing URL construction issues\n */\n\n/**\n * Fix double slashes in URLs while preserving protocol slashes\n * @param url The URL to fix\n * @returns Fixed URL with single slashes in path\n */\nexport function fixUrlDoubleSlashes(url: string): string {\n  if (!url) return url;\n\n  // Split on protocol to preserve it\n  const protocolMatch = url.match(/^(https?:\\/\\/)/);\n  if (protocolMatch) {\n    const protocol = protocolMatch[1];\n    const rest = url.substring(protocol.length);\n    // Replace multiple consecutive slashes with single slash in the path part\n    const fixedRest = rest.replace(/\\/+/g, '/');\n    return protocol + fixedRest;\n  }\n\n  // For relative URLs, just fix multiple slashes\n  return url.replace(/\\/+/g, '/');\n}\n\n/**\n * Properly join URL path segments without creating double slashes\n * @param segments Path segments to join\n * @returns Properly joined path\n */\nexport function joinUrlPath(...segments: string[]): string {\n  return segments\n    .map((segment, index) => {\n      // Remove leading slash from all but first segment\n      const withoutLeading = index > 0 && segment.startsWith('/') ? segment.substring(1) : segment;\n      // Remove trailing slash from all but last segment\n      return index < segments.length - 1 && withoutLeading.endsWith('/')\n        ? withoutLeading.substring(0, withoutLeading.length - 1)\n        : withoutLeading;\n    })\n    .filter(segment => segment.length > 0)\n    .join('/');\n}\n\n/**\n * Fix Supabase storage URLs specifically\n * @param url Supabase storage URL\n * @returns Fixed URL\n */\nexport function fixSupabaseStorageUrl(url: string): string {\n  // Early return if nothing to process\n  if (!url) return url;\n\n  /**\n   * We previously restricted the fix to URLs that contained a specific Supabase\n   * domain (\"supabase.co\"). However, we now serve assets from a custom domain\n   * (e.g. app.openmsp.ai) that still uses the same `/storage/v1/object/public/`\n   * path structure. The old guard clause prevented those URLs from being\n   * cleaned which resulted in paths like:\n   *   https://app.openmsp.ai/storage/v1/object/public/logos///Ansible.png\n   * Googlebot treats the triple-slash as a distinct resource and reports 404\n   * errors which in turn causes the \"URL will be indexed only if certain\n   * conditions are met\" warning in Search Console.\n   *\n   * Solution: detect the canonical Supabase storage path segment instead of\n   * the host name. Whenever we see that path we safely collapse multiple\n   * consecutive slashes to a single slash while preserving the protocol\n   * (`https://`).\n   */\n\n  const SUPABASE_STORAGE_SEGMENT = '/storage/v1/object/public/';\n\n  if (url.includes(SUPABASE_STORAGE_SEGMENT)) {\n    return fixUrlDoubleSlashes(url);\n  }\n\n  // No known storage segment – return the original URL untouched\n  return url;\n}\n","// Vendor media utilities for UI kit\n// Real implementation copied from main project\n\nimport { isImageMedia, isVideoMedia } from './media-type';\nimport { fixSupabaseStorageUrl } from './url-fix';\nexport interface VendorMedia {\n  media_type: 'logo' | 'image' | 'video';\n  media_url: string;\n}\n\nexport interface VendorWithMedia {\n  id?: number;\n  title: string;\n  slug?: string;\n  logo?: string | null;\n  logo_url?: string; // Support direct logo_url field from lightweight API\n  vendor_media?: VendorMedia[];\n}\n\n/**\n * Get the logo URL from vendor_media array or direct logo_url field\n */\nexport function getVendorLogo(vendor: VendorWithMedia): string | null {\n  // First check for direct logo_url field (from lightweight API)\n  if (vendor.logo_url) {\n    return fixSupabaseStorageUrl(vendor.logo_url);\n  }\n\n  // Check for legacy logo field\n  if (vendor.logo) {\n    return fixSupabaseStorageUrl(vendor.logo);\n  }\n\n  // Fallback to vendor_media array (from detailed API)\n  const logoMedia = vendor.vendor_media?.find(m => m.media_type === 'logo');\n  if (logoMedia?.media_url) {\n    return fixSupabaseStorageUrl(logoMedia.media_url);\n  }\n\n  return null;\n}\n\n/**\n * Get the main image URL from vendor_media array\n */\nexport function getVendorImage(vendor: VendorWithMedia): string | null {\n  const imageMedia = vendor.vendor_media?.find(m => isImageMedia(m));\n  return imageMedia?.media_url ? fixSupabaseStorageUrl(imageMedia.media_url) : null;\n}\n\n/**\n * Get the video URL from vendor_media array\n */\nexport function getVendorVideo(vendor: VendorWithMedia): string | null {\n  const videoMedia = vendor.vendor_media?.find(m => isVideoMedia(m));\n  return videoMedia?.media_url ? fixSupabaseStorageUrl(videoMedia.media_url) : null;\n}\n\n/**\n * Get all media URLs of a specific type\n */\nexport function getVendorMediaByType(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video'): string[] {\n  return vendor.vendor_media?.filter(m => m.media_type === type).map(m => fixSupabaseStorageUrl(m.media_url)) || [];\n}\n\n/**\n * Get all media items grouped by type\n */\nexport function getVendorMediaGrouped(vendor: VendorWithMedia): {\n  logos: string[];\n  images: string[];\n  videos: string[];\n} {\n  const media = vendor.vendor_media || [];\n\n  return {\n    logos: media.filter(m => m.media_type === 'logo').map(m => fixSupabaseStorageUrl(m.media_url)),\n    images: media.filter(m => isImageMedia(m)).map(m => fixSupabaseStorageUrl(m.media_url)),\n    videos: media.filter(m => isVideoMedia(m)).map(m => fixSupabaseStorageUrl(m.media_url)),\n  };\n}\n\n/**\n * Add new media to vendor_media array (useful for admin interfaces)\n */\nexport function addVendorMedia(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video', url: string): VendorMedia[] {\n  const existingMedia = vendor.vendor_media || [];\n  const newMedia: VendorMedia = { media_type: type, media_url: url };\n  return [...existingMedia, newMedia];\n}\n\n/**\n * Remove media from vendor_media array\n */\nexport function removeVendorMedia(vendor: VendorWithMedia, url: string): VendorMedia[] {\n  return vendor.vendor_media?.filter(m => m.media_url !== url) || [];\n}\n\n/**\n * Check if vendor has media of a specific type\n */\nexport function hasVendorMedia(vendor: VendorWithMedia, type: 'logo' | 'image' | 'video'): boolean {\n  return vendor.vendor_media?.some(m => m.media_type === type) || false;\n}\n\n/**\n * Get media count by type\n */\nexport function getVendorMediaCount(vendor: VendorWithMedia): {\n  logos: number;\n  images: number;\n  videos: number;\n  total: number;\n} {\n  const media = vendor.vendor_media || [];\n\n  const logos = media.filter(m => m.media_type === 'logo').length;\n  const images = media.filter(m => isImageMedia(m)).length;\n  const videos = media.filter(m => isVideoMedia(m)).length;\n\n  return {\n    logos,\n    images,\n    videos,\n    total: media.length,\n  };\n}\n","'use client';\n\nimport Image from '../embed-shims/next-image';\nimport { cn } from '../utils/cn';\nimport { getProxiedImageUrl } from '../utils/image-proxy-stub';\nimport { getVendorLogo, type VendorWithMedia } from '../utils/vendor-media-stub';\n\ninterface VendorIconProps {\n  vendor: VendorWithMedia & {\n    id?: number;\n    title: string;\n    slug?: string;\n    logo?: string | null;\n  };\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'l' | 'xl';\n  className?: string;\n  showBackground?: boolean;\n  backgroundStyle?: 'dark' | 'light' | 'white';\n}\n\nconst sizeClasses = {\n  xs: 'w-6 h-6',\n  sm: 'w-8 h-8',\n  md: 'w-10 h-10',\n  lg: 'w-12 h-12',\n  l: 'w-14 h-14',\n  xl: 'w-16 h-16',\n};\n\nconst imageSizeMap = {\n  xs: { width: 16, height: 16 },\n  sm: { width: 20, height: 20 },\n  md: { width: 32, height: 32 },\n  lg: { width: 40, height: 40 },\n  l: { width: 38, height: 38 },\n  xl: { width: 40, height: 40 },\n};\n\nconst backgroundClasses = {\n  dark: 'bg-ods-bg border border-ods-border',\n  light: 'bg-ods-card border border-ods-border',\n  white: 'bg-white border border-[#E5E5E5]',\n};\n\n/**\n * Common VendorIcon component for displaying vendor logos consistently across the platform\n * Extracted from vendor-card.tsx for reuse in comparison tables, dropdowns, etc.\n */\nexport function VendorIcon({\n  vendor,\n  size = 'md',\n  className = '',\n  showBackground = true,\n  backgroundStyle = 'dark',\n}: VendorIconProps) {\n  const logoUrl = getVendorLogo(vendor);\n  const { width, height } = imageSizeMap[size];\n\n  const containerClasses = cn(\n    sizeClasses[size],\n    'flex flex-shrink-0 items-center justify-center rounded-lg',\n    showBackground && backgroundClasses[backgroundStyle],\n    !showBackground && 'overflow-hidden',\n    className,\n  );\n\n  return (\n    <div className={containerClasses}>\n      {logoUrl ? (\n        <Image\n          src={getProxiedImageUrl(logoUrl) || logoUrl}\n          alt={`${vendor.title} logo`}\n          width={width}\n          height={height}\n          className={cn('object-contain', showBackground ? 'p-1' : 'h-full w-full')}\n        />\n      ) : (\n        <div\n          className={cn(\n            'flex items-center justify-center uppercase text-h6',\n            backgroundStyle === 'white' ? 'text-[#333333]' : 'text-ods-text-secondary',\n          )}\n        >\n          {vendor.title?.substring(0, 2) || '??'}\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport type React from 'react';\n\ninterface CategoryCardProps {\n  name: string;\n  description: string;\n  categoryCount: number;\n  productCount: number;\n  icons?: React.ReactNode[];\n}\n\nexport function CategoryCard({ name, description, categoryCount, productCount, icons = [] }: CategoryCardProps) {\n  return (\n    <article className=\"box-border flex min-w-0 flex-col rounded-[12px] border border-ods-border bg-ods-card p-8\">\n      <div className=\"mb-8 flex items-center justify-center gap-6\">\n        {(icons.length > 0 ? icons : Array(10).fill(null)).map((icon, i) => (\n          <div key={i} className=\"flex h-10 w-10 items-center justify-center rounded bg-ods-skeleton\">\n            {icon}\n          </div>\n        ))}\n      </div>\n      <div className=\"flex flex-1 flex-col\">\n        <h2 className=\"mb-2 text-left text-ods-text-primary text-h2\">{name}</h2>\n        <div className=\"mb-4 text-left text-ods-text-secondary text-h6\">\n          {categoryCount} Categories • {productCount} Products\n        </div>\n        <div className=\"flex flex-row items-start\">\n          <p className=\"flex-1 text-left text-ods-text-primary text-h6\">{description}</p>\n          <button\n            className=\"ml-4 flex h-12 w-12 items-center justify-center rounded-[6px] border border-ods-border bg-transparent transition-colors hover:bg-ods-accent\"\n            style={{ minWidth: 48, minHeight: 48 }}\n            aria-label={`View ${name}`}\n          >\n            <svg width=\"24\" height=\"24\" fill=\"none\" stroke=\"#FAFAFA\" strokeWidth=\"2\" viewBox=\"0 0 24 24\">\n              <path d=\"M9 18l6-6-6-6\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n    </article>\n  );\n}\n","'use client';\n\n// Stub auth provider and hooks\nimport { createContext } from 'react';\nimport type { ReactNode } from 'react';\n\n/** The signed-in user, as far as this stub's consumers care. Hosts inject a\n *  richer object through `setRealAuthHook`; only `id` and `name` are read\n *  inside the lib (see `comment-card.tsx`). */\nexport interface AuthStubUser {\n  id: string;\n  name?: string;\n  [key: string]: unknown;\n}\n\nexport interface AuthContextType {\n  user: AuthStubUser | null;\n  isLoading: boolean;\n}\n\nconst AuthContext = createContext<AuthContextType>({\n  user: null,\n  isLoading: false,\n});\n\n// Global reference to real auth hook when available\nlet realUseAuth: (() => AuthContextType) | null = null;\n\nexport function setRealAuthHook(authHook: () => AuthContextType) {\n  realUseAuth = authHook;\n}\n\nexport function useAuth(): AuthContextType {\n  // Use real auth hook if available (when used in main app)\n  if (realUseAuth) {\n    try {\n      const realAuth = realUseAuth();\n      if (realAuth && realAuth.user) {\n        return realAuth;\n      }\n    } catch {\n      // Fallback if real auth fails\n    }\n  }\n\n  // Fallback mock user for UI kit context\n  return {\n    user: { id: 'mock-user-id', name: 'Mock User' },\n    isLoading: false,\n  };\n}\n\nexport function AuthProvider({ children }: { children: ReactNode }) {\n  return (\n    <AuthContext.Provider value={{ user: { id: 'mock-user-id', name: 'Mock User' }, isLoading: false }}>\n      {children}\n    </AuthContext.Provider>\n  );\n}\n","'use client';\n\nimport Image from '../embed-shims/next-image';\nimport { getProxiedImageUrl } from '../utils/image-proxy-stub';\n\n/**\n * NOTE — this is the STUB user summary (see the filename). Five props below are\n * declared and accepted so host apps can write their call sites against the\n * final shape, but the stub does not act on them yet: `editHref`, `userId`,\n * `profileData` (there is no EditProfileButton here — `showEditButton` renders\n * a plain label), and `editablePhoto` / `onPhotoChange` (no ProfilePhotoUpload\n * widget). Each is marked below. Wiring them up is a feature, not a cleanup.\n */\ninterface Props {\n  name: string;\n  email: string;\n  avatarUrl?: string | null;\n  /** Optional subtitle text (e.g., relative time) to replace email line */\n  subtitle?: string | null;\n  /** Authentication provider names (e.g. [\"google\", \"microsoft\"]) */\n  authProviders?: string[];\n  /** Show an outline Edit Profile button that routes to editHref */\n  showEditButton?: boolean;\n  /** NOT IMPLEMENTED in the stub. Path to navigate when Edit is clicked. */\n  editHref?: string;\n  /** NOT IMPLEMENTED in the stub. Passed through to EditProfileButton for analytics. */\n  userId?: string;\n  /** NOT IMPLEMENTED in the stub — accepted and ignored, so its shape is the\n   *  host's business. */\n  profileData?: unknown;\n  /** Optional MSP preview info to render below email */\n  mspPreview?: {\n    name?: string | null;\n    seatCount?: number | null;\n    technicianCount?: number | null;\n    annualRevenue?: number | null;\n    logoUrl?: string | null;\n  } | null;\n  /** Compact mode (avatar + name row, used in comment headers) */\n  compact?: boolean;\n  /** Avatar size in px for compact mode (defaults 40) */\n  avatarSize?: number;\n\n  /** NOT IMPLEMENTED in the stub. Would swap the avatar for ProfilePhotoUpload. */\n  editablePhoto?: boolean;\n  /** NOT IMPLEMENTED in the stub. Would receive the new photo URL. */\n  onPhotoChange?: (url: string | null) => void;\n}\n\nconst getAuthProviderIcon = (provider: string) => {\n  const p = provider.toLowerCase();\n  switch (p) {\n    case 'google':\n      return <Image src=\"/icons/google-logo.svg\" alt=\"Google\" width={16} height={16} className=\"h-4 w-4\" />;\n    case 'microsoft':\n    case 'azure':\n      return <Image src=\"/icons/microsoft-logo.svg\" alt=\"Microsoft\" width={16} height={16} className=\"h-4 w-4\" />;\n    case 'slack':\n    case 'slack_oidc':\n      return <div className=\"h-4 w-4 rounded-full bg-ods-text-secondary\" />;\n    default:\n      return <div className=\"h-4 w-4 rounded-full bg-ods-text-secondary\" />;\n  }\n};\n\n// Abbreviate large numbers: 1 200 → 1.2K , 15 000 → 15K , 2 000 000 → 2M\nconst formatNumber = (n: number) => {\n  if (n >= 1_000_000_000) {\n    const value = n / 1_000_000_000;\n    return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}B`;\n  }\n  if (n >= 1_000_000) {\n    const value = n / 1_000_000;\n    return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}M`;\n  }\n  if (n >= 1_000) {\n    return `${Math.round(n / 1_000)}K`;\n  }\n  return n.toLocaleString();\n};\n\nexport function UserSummary({\n  name,\n  email,\n  subtitle = null,\n  avatarUrl,\n  authProviders,\n  showEditButton = false,\n  mspPreview,\n  compact = false,\n  avatarSize = 40,\n}: Props) {\n  // Compact variant: minimal horizontal row\n  if (compact) {\n    return (\n      <div className=\"flex min-w-0 items-center gap-3\">\n        <div className=\"relative shrink-0\">\n          {avatarUrl ? (\n            <Image\n              src={getProxiedImageUrl(avatarUrl) ?? avatarUrl}\n              alt={name}\n              width={avatarSize}\n              height={avatarSize}\n              className=\"rounded-lg object-cover\"\n            />\n          ) : (\n            <div\n              className=\"flex items-center justify-center rounded-lg bg-ods-accent font-heading font-bold text-ods-text-on-accent\"\n              style={{ width: avatarSize, height: avatarSize }}\n            >\n              {name\n                .split(' ')\n                .map((n: string) => n.charAt(0))\n                .join('')\n                .slice(0, 2)}\n            </div>\n          )}\n          {mspPreview && mspPreview.logoUrl && (\n            <Image\n              src={getProxiedImageUrl(mspPreview.logoUrl) ?? mspPreview.logoUrl}\n              alt={mspPreview.name || 'MSP'}\n              width={24}\n              height={24}\n              className=\"absolute -bottom-1 -right-1 z-10 size-6 select-none rounded-full object-cover\"\n            />\n          )}\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <p\n            className=\"truncate text-ods-text-primary text-h4\"\n            title={mspPreview?.name ? `${name} • ${mspPreview.name}` : name}\n          >\n            {name}\n            {mspPreview?.name && <span className=\"text-ods-text-secondary\"> • {mspPreview.name}</span>}\n          </p>\n          <p\n            className=\"truncate text-ods-text-secondary text-h6\"\n            title={\n              subtitle && subtitle.trim().length > 0 ? subtitle : email && email.trim().length > 0 ? email : '\\u00A0'\n            }\n          >\n            {subtitle && subtitle.trim().length > 0 ? subtitle : email && email.trim().length > 0 ? email : '\\u00A0'}\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex w-full flex-col gap-4\">\n      {/* Header Row */}\n      <div className=\"flex w-full items-start gap-6\">\n        {/* Avatar with badge wrapper */}\n        <div className=\"relative h-24 w-24 shrink-0 overflow-visible\">\n          {avatarUrl ? (\n            <Image\n              src={getProxiedImageUrl(avatarUrl) ?? avatarUrl}\n              alt={name}\n              width={96}\n              height={96}\n              className=\"rounded-full object-cover\"\n            />\n          ) : (\n            <div className=\"flex h-full w-full items-center justify-center rounded-full border border-ods-border bg-ods-card font-heading text-3xl text-ods-text-secondary\">\n              {name.charAt(0).toUpperCase()}\n            </div>\n          )}\n\n          {/* MSP logo badge (show only when MSP exists) */}\n          {mspPreview && (\n            <div className=\"absolute -bottom-1 -right-1 flex size-10 select-none items-center justify-center overflow-hidden rounded-full bg-ods-bg ring-2 ring-ods-border\">\n              {mspPreview.logoUrl ? (\n                <Image\n                  src={getProxiedImageUrl(mspPreview.logoUrl) ?? mspPreview.logoUrl}\n                  alt={mspPreview.name || 'MSP Logo'}\n                  width={40}\n                  height={40}\n                  className=\"object-cover\"\n                />\n              ) : (\n                <span className=\"font-heading text-sm font-bold text-ods-text-primary\">\n                  {mspPreview.name?.charAt(0).toUpperCase() || '?'}\n                </span>\n              )}\n            </div>\n          )}\n        </div>\n\n        {/* Info + actions block */}\n        <div className=\"grid flex-1 grid-cols-[1fr_auto] gap-4\">\n          {/* LEFT : text stack */}\n          <div className=\"flex min-h-[6rem] flex-col justify-center space-y-3 truncate\">\n            <p className=\"truncate leading-none text-ods-text-primary text-h2\" title={name}>\n              {name}\n            </p>\n            <p\n              className=\"truncate break-all text-ods-text-secondary text-h4\"\n              title={\n                subtitle && subtitle.trim().length > 0 ? subtitle : email && email.trim().length > 0 ? email : '\\u00A0'\n              }\n            >\n              {subtitle && subtitle.trim().length > 0 ? subtitle : email && email.trim().length > 0 ? email : '\\u00A0'}\n            </p>\n            {mspPreview &&\n              (() => {\n                const mspSegments = [\n                  mspPreview.name ?? '—',\n                  typeof mspPreview.seatCount === 'number' ? `${formatNumber(mspPreview.seatCount)} Seats` : null,\n                  typeof mspPreview.technicianCount === 'number'\n                    ? `${formatNumber(mspPreview.technicianCount)} Technicians`\n                    : null,\n                  typeof mspPreview.annualRevenue === 'number' ? `$${formatNumber(mspPreview.annualRevenue)}` : null,\n                ].filter(Boolean) as string[];\n                const mspTitle = mspSegments.join(' • ');\n                return (\n                  <p className=\"truncate text-ods-text-primary text-h6\" title={mspTitle}>\n                    {/* Build string with separators */}\n                    {mspSegments\n                      .flatMap((txt, idx) => (idx === 0 ? [txt] : [' • ', txt]))\n                      .map((seg, idx) => (\n                        <span key={idx} className={seg === '•' ? 'text-ods-text-secondary' : ''}>\n                          {seg}\n                        </span>\n                      ))}\n                  </p>\n                );\n              })()}\n          </div>\n\n          {/* RIGHT (desktop) */}\n          {(authProviders?.length || showEditButton) && (\n            <div className=\"hidden min-h-[6rem] flex-shrink-0 flex-col items-end justify-between md:flex\">\n              {/* top part */}\n              {authProviders && authProviders.length > 0 && (\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"select-none whitespace-nowrap text-ods-text-secondary text-h6\">Authorized by</span>\n                  <div className=\"flex items-center gap-2\">\n                    {authProviders.map(p => (\n                      <div key={p} className=\"flex h-4 w-4 items-center justify-center\">\n                        {getAuthProviderIcon(p)}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n\n              {/* bottom part - Edit button would go here */}\n              {showEditButton && <div className=\"text-ods-text-secondary text-h6\">Edit Profile</div>}\n            </div>\n          )}\n        </div>\n      </div>\n\n      {/* Mobile row: Authorized by left, Edit btn right */}\n      {(authProviders?.length || showEditButton) && (\n        <div className=\"flex w-full items-center justify-between gap-4 md:hidden\">\n          {authProviders && authProviders.length > 0 && (\n            <div className=\"flex items-center gap-2\">\n              <span className=\"select-none whitespace-nowrap text-ods-text-secondary text-h6\">Authorized by</span>\n              <div className=\"flex items-center gap-2\">\n                {authProviders.map(p => (\n                  <div key={p} className=\"flex h-4 w-4 items-center justify-center\">\n                    {getAuthProviderIcon(p)}\n                  </div>\n                ))}\n              </div>\n            </div>\n          )}\n\n          {showEditButton && <div className=\"text-ods-text-secondary text-h6\">Edit Profile</div>}\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport Image from '../embed-shims/next-image';\nimport { getProxiedImageUrl } from '../utils/image-proxy-stub';\nimport { getVendorLogo, type VendorWithMedia } from '../utils/vendor-media-stub';\n\ninterface VendorDisplayButtonProps {\n  vendor: VendorWithMedia;\n  onClick?: (vendorSlug: string) => void;\n  variant?: 'default' | 'compact';\n  externalUrl?: string;\n}\n\nexport function VendorDisplayButton({ vendor, onClick, variant = 'default', externalUrl }: VendorDisplayButtonProps) {\n  const handleClick = () => {\n    if (externalUrl && vendor.slug) {\n      // `externalUrl` is the caller-resolved platform base URL (the openmsp SSOT via\n      // getPlatformProductionUrl, scheme-normalized). The old `process.env.NEXT_PUBLIC_OPENMSP_URL`\n      // override is gone — it's stored scheme-less, which made this a relative window.open().\n      window.open(`${externalUrl}/vendor/${vendor.slug}`, '_blank', 'noopener,noreferrer');\n    } else if (onClick && vendor.slug) {\n      onClick(vendor.slug);\n    }\n  };\n\n  // Resolved once for BOTH variants: the default branch used to call\n  // `getVendorLogo` three times and assert the third, which is also three\n  // chances for the three calls to disagree.\n  const logoUrl = getVendorLogo(vendor);\n\n  // Compact variant for flamingo-teaser\n  if (variant === 'compact') {\n    return (\n      <button\n        onClick={handleClick}\n        className=\"inline-flex items-center gap-2 rounded-full border border-ods-border bg-ods-card px-3 py-1.5 transition-colors hover:border-ods-accent/50\"\n      >\n        {logoUrl ? (\n          <div className=\"h-5 w-5 flex-shrink-0 overflow-hidden rounded\">\n            <Image\n              src={getProxiedImageUrl(logoUrl) || logoUrl}\n              alt={`${vendor.title} logo`}\n              width={20}\n              height={20}\n              className=\"h-full w-full object-cover\"\n            />\n          </div>\n        ) : (\n          <div className=\"flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-ods-border\">\n            <span className=\"text-[10px] font-medium text-ods-text-secondary\">\n              {vendor.title.charAt(0).toUpperCase()}\n            </span>\n          </div>\n        )}\n        <span className=\"text-ods-text-primary text-h6\">{vendor.title}</span>\n      </button>\n    );\n  }\n\n  // Default variant\n  return (\n    <button\n      onClick={handleClick}\n      className=\"flex max-w-full items-center gap-2 overflow-hidden rounded-lg border border-ods-border bg-ods-card px-3 py-2 transition-colors hover:border-ods-accent\"\n    >\n      {logoUrl ? (\n        <div className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg border border-ods-border bg-ods-card\">\n          <Image\n            src={getProxiedImageUrl(logoUrl) || logoUrl}\n            alt={`${vendor.title} logo`}\n            width={24}\n            height={24}\n            className=\"rounded object-cover\"\n          />\n        </div>\n      ) : (\n        <div className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg border border-ods-border bg-ods-border\">\n          <span className=\"text-ods-text-primary text-h6\">{vendor.title.charAt(0)}</span>\n        </div>\n      )}\n      <span className=\"min-w-0 truncate text-ods-text-primary text-h4\" title={vendor.title}>\n        {vendor.title}\n      </span>\n    </button>\n  );\n}\n","'use client';\n\nimport { formatRelativeTime } from '../utils/date-utils';\nimport { useAuth } from './auth-stub';\nimport { Button } from './ui/button';\nimport { UserSummary } from './user-summary-stub';\nimport { VendorDisplayButton } from './vendor-display-button';\n\ninterface CommentCardProps {\n  comment: {\n    id: string;\n    content: string;\n    title?: string;\n    type?: 'pro' | 'con';\n    importance?: 'Critical' | 'Important' | 'Nice-to-have';\n    createdAt: string;\n    vendor?: {\n      id: number;\n      title: string;\n      slug: string;\n      logo: string | null;\n      category?: string | null;\n    };\n    user?: {\n      id: string;\n      name: string;\n      profilePicture: string | null;\n      msp?: {\n        id: string | number;\n        name?: string | null;\n        icon_url?: string | null;\n      };\n    };\n    canDelete?: boolean;\n  };\n  onViewProduct?: (vendorSlug: string) => void;\n  onDeleteComment?: (commentId: string) => void;\n  /** @deprecated Superseded by `leadWith`; the component ignores it. */\n  showVendorInfo?: boolean;\n  compact?: boolean;\n  /**\n   * Which HEADER this tile leads with — the only thing that ever varied by\n   * surface. Two states, because there are two headers: the profile page shows\n   * a person's comments, so it leads with the VENDOR each one is about;\n   * everywhere else the vendor is the page and the commenter is the news.\n   * Every BEHAVIOUR is a prop, so a new surface never needs a new member here.\n   */\n  leadWith?: 'vendor' | 'commenter';\n  /**\n   * Read the body in FULL (thread reading) instead of the fixed-height,\n   * three-line list tile. Defaults to the list tile.\n   */\n  fullBody?: boolean;\n  /** Rendered under the body — badges + per-surface actions (design-doc threads). */\n  footer?: React.ReactNode;\n  /** Rendered after the card — nested replies + reply composer (design-doc threads). */\n  children?: React.ReactNode;\n}\n\nexport function CommentCard({\n  comment,\n  onViewProduct,\n  onDeleteComment,\n  compact = false,\n  leadWith = 'commenter',\n  fullBody = false,\n  footer,\n  children,\n}: CommentCardProps) {\n  const { user: currentUser } = useAuth();\n\n  // Use unified date formatting function\n  const formatActivityTime = (timestamp: string) => {\n    return formatRelativeTime(timestamp);\n  };\n\n  // Check if current user can delete this comment\n  /**\n   * Deletability is the CALLER's rule, never a per-surface branch in here: an\n   * explicit `comment.canDelete` always wins (design docs compute own-or-DRI\n   * with their gate module and hand it in), and the default is the only rule\n   * this component can honestly know by itself — your own comment.\n   */\n  const canUserDeleteComment = () => {\n    if (!currentUser) return false;\n    if (typeof comment.canDelete === 'boolean') return comment.canDelete;\n    return !!comment.user && currentUser.id === comment.user.id;\n  };\n\n  const showDeleteButton = canUserDeleteComment();\n\n  return (\n    <>\n      <div\n        className=\"group box-border w-full max-w-full overflow-hidden rounded-lg border border-ods-border bg-ods-card p-4 transition-all hover:border-ods-accent\"\n        style={{ maxWidth: '100%', wordBreak: 'break-word' }}\n      >\n        {/* Comment Header */}\n        <div className=\"mb-3 flex w-full flex-col gap-3 min-[420px]:flex-row min-[420px]:items-center min-[420px]:justify-between min-[420px]:gap-2\">\n          {/* Row 1: Info + Timestamp */}\n          <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n            {leadWith === 'vendor' && comment.vendor ? (\n              <>\n                {/* Vendor Button - Icon + Name */}\n                <VendorDisplayButton vendor={comment.vendor} onClick={onViewProduct || (() => {})} />\n\n                {/* Timestamp */}\n                <span className=\"flex-shrink-0 whitespace-nowrap text-ods-text-secondary text-h6\">\n                  {formatActivityTime(comment.createdAt)}\n                </span>\n              </>\n            ) : comment.user ? (\n              <UserSummary\n                name={comment.user.name}\n                email=\"\"\n                compact\n                avatarSize={48}\n                subtitle={formatActivityTime(comment.createdAt)}\n                avatarUrl={comment.user.profilePicture ?? null}\n                mspPreview={\n                  comment.user.msp\n                    ? {\n                        name: comment.user.msp.name ?? null,\n                        logoUrl: comment.user.msp.icon_url ?? null,\n                        seatCount: null,\n                        technicianCount: null,\n                        annualRevenue: null,\n                      }\n                    : null\n                }\n                showEditButton={false}\n                authProviders={[]}\n              />\n            ) : null}\n          </div>\n\n          {/* Row 2: Action Buttons */}\n          <div className=\"flex justify-start gap-2 min-[420px]:justify-end\">\n            {/* Delete Button - shown when user can delete the comment */}\n            {showDeleteButton && onDeleteComment && (\n              <Button\n                onClick={() => onDeleteComment(comment.id)}\n                variant=\"outline\"\n                size=\"icon\"\n                className=\"text-ods-text-primary\"\n              >\n                <svg\n                  width=\"22\"\n                  height=\"24\"\n                  viewBox=\"0 0 22 24\"\n                  fill=\"none\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  className=\"text-ods-text-primary\"\n                >\n                  <path\n                    d=\"M18.6025 4.37939C19.221 4.43597 19.6768 4.98399 19.6206 5.60254L18.5176 17.7358C18.4492 18.4886 18.3924 19.1183 18.3052 19.6299C18.2155 20.1552 18.0809 20.6447 17.8101 21.1021C17.4444 21.7195 16.9245 22.2292 16.3042 22.583L16.0317 22.7251C15.5519 22.9533 15.0526 23.0438 14.5215 23.0854C14.0042 23.126 13.3727 23.125 12.6172 23.125H9.38281C8.62726 23.125 7.9958 23.126 7.47852 23.0854C7.01357 23.049 6.57289 22.975 6.14844 22.8042L5.96826 22.7251C5.32037 22.4169 4.76543 21.9454 4.35693 21.3599L4.18994 21.1021C3.91912 20.6447 3.78445 20.1552 3.69482 19.6299C3.65123 19.3743 3.61556 19.0894 3.58203 18.7744L3.48242 17.7358L2.37939 5.60254L2.375 5.48682C2.38099 4.91684 2.81768 4.43244 3.39746 4.37939C3.97776 4.32664 4.496 4.72462 4.60449 5.28467L4.62061 5.39746L5.72363 17.5322C5.79528 18.3203 5.84358 18.8475 5.9126 19.252C5.97923 19.6424 6.05232 19.8298 6.12646 19.9551L6.28467 20.1836C6.45963 20.3987 6.68118 20.5732 6.93359 20.6934L7.04639 20.7388C7.17478 20.7822 7.35781 20.8195 7.6543 20.8428C8.06325 20.8749 8.59186 20.875 9.38281 20.875H12.6172C13.4081 20.875 13.9367 20.8749 14.3457 20.8428C14.7409 20.8117 14.9349 20.7559 15.0664 20.6934L15.3066 20.5557C15.5369 20.4008 15.7309 20.1958 15.8735 19.9551C15.9477 19.8298 16.0208 19.6424 16.0874 19.252C16.1564 18.8475 16.2047 18.3203 16.2764 17.5322L17.3794 5.39746C17.436 4.77903 17.984 4.32316 18.6025 4.37939Z\"\n                    fill=\"currentColor\"\n                  />\n                  <path\n                    d=\"M7.37451 15.9995V11C7.37451 10.3788 7.87842 9.87526 8.49951 9.875C9.12083 9.875 9.62451 10.3787 9.62451 11V15.9995C9.62451 16.6208 9.12083 17.1245 8.49951 17.1245C7.87842 17.1242 7.37451 16.6207 7.37451 15.9995ZM12.3755 15.9995V11C12.3755 10.3787 12.8792 9.875 13.5005 9.875C14.1216 9.87526 14.6255 10.3788 14.6255 11V15.9995C14.6255 16.6207 14.1216 17.1242 13.5005 17.1245C12.8792 17.1245 12.3755 16.6208 12.3755 15.9995ZM12.585 0.875C13.0174 0.879157 13.425 0.903301 13.8096 1.02881L14.1157 1.14746C14.4155 1.28159 14.6931 1.46221 14.9375 1.68359L15.0811 1.82568C15.4034 2.16872 15.6379 2.59872 15.8984 3.05469L16.6528 4.37451H20L20.1157 4.38037C20.6825 4.43826 21.1248 4.91747 21.125 5.49951C21.125 6.08174 20.6827 6.5607 20.1157 6.61865L20 6.62451H2C1.37868 6.62451 0.875 6.12083 0.875 5.49951C0.875264 4.87842 1.37884 4.37451 2 4.37451H5.34717L6.32129 2.6709C6.53952 2.29735 6.76267 1.95525 7.0625 1.68359L7.31738 1.47705C7.58262 1.28327 7.87687 1.1312 8.19043 1.02881L8.38525 0.974609C8.71199 0.896341 9.05467 0.878464 9.41504 0.875H12.585ZM9.85742 3.125C9.29191 3.125 9.08123 3.13201 8.97266 3.14844L8.88916 3.16748C8.83075 3.18655 8.77503 3.21301 8.72217 3.24365L8.57275 3.35205C8.53704 3.38448 8.50137 3.42629 8.42773 3.54102L8.05566 4.1709L7.93848 4.37451H14.0615L13.9443 4.1709C13.6645 3.68114 13.554 3.50141 13.4858 3.41504L13.4272 3.35205C13.3815 3.31062 13.3309 3.27443 13.2778 3.24365L13.1108 3.16748C13.019 3.13753 12.8946 3.125 12.1426 3.125H9.85742Z\"\n                    fill=\"currentColor\"\n                  />\n                </svg>\n              </Button>\n            )}\n          </div>\n        </div>\n\n        {/* Comment Content */}\n        <div\n          className=\"w-full max-w-full space-y-2 overflow-hidden\"\n          style={{ maxWidth: '100%', wordBreak: 'break-word' }}\n        >\n          {comment.title && (\n            <div className={compact ? 'flex h-[20px] items-center' : 'flex h-[24px] items-center'}>\n              <h4\n                className=\"line-clamp-1 tracking-[-0.02em] text-ods-text-primary transition-colors text-h3 group-hover:text-ods-accent\"\n                style={{\n                  wordBreak: 'break-word',\n                  overflowWrap: 'break-word',\n                  maxWidth: '100%',\n                  textOverflow: 'ellipsis',\n                  overflow: 'hidden',\n                  display: '-webkit-box',\n                  WebkitBoxOrient: 'vertical',\n                  WebkitLineClamp: 1,\n                }}\n              >\n                {comment.title}\n              </h4>\n            </div>\n          )}\n          {fullBody ? (\n            // Thread bodies are read in full — no fixed height, no clamp.\n            <p\n              className=\"whitespace-pre-wrap text-ods-text-primary text-h4\"\n              style={{ wordBreak: 'break-word', overflowWrap: 'break-word', maxWidth: '100%' }}\n            >\n              {comment.content}\n            </p>\n          ) : (\n            <div className={compact ? 'flex h-[60px] items-center' : 'flex h-[72px] items-center'}>\n              <p\n                className=\"line-clamp-3 text-ods-text-primary text-h4\"\n                style={{\n                  wordBreak: 'break-word',\n                  overflowWrap: 'break-word',\n                  maxWidth: '100%',\n                  textOverflow: 'ellipsis',\n                  overflow: 'hidden',\n                  display: '-webkit-box',\n                  WebkitBoxOrient: 'vertical',\n                  WebkitLineClamp: 3,\n                }}\n              >\n                {comment.content}\n              </p>\n            </div>\n          )}\n          {footer ? <div className=\"flex flex-wrap items-center gap-2 pt-2\">{footer}</div> : null}\n        </div>\n      </div>\n      {children}\n    </>\n  );\n}\n","'use client';\n\nimport { useState, type ReactNode, type RefObject } from 'react';\nimport { Button, Input, Label, Textarea } from './ui';\n\nexport interface CommentComposerDraft {\n  title: string;\n  body: string;\n}\n\nexport interface CommentComposerProps {\n  /**\n   * Show the title field. The vendor section's comments are titled reviews;\n   * a design-doc comment is a Slack message, so it passes `false`.\n   */\n  withTitle?: boolean;\n  titleLabel?: string;\n  titlePlaceholder?: string;\n  /** For hosts whose empty state focuses the composer (\"Be the first!\"). */\n  titleRef?: RefObject<HTMLInputElement | null>;\n  bodyLabel?: string;\n  placeholder?: string;\n  /** Extra controls under the box — design docs put the comment-type chips here. */\n  extras?: ReactNode;\n  submitLabel?: string;\n  cancelLabel?: string;\n  onCancel?: () => void;\n  pending?: boolean;\n  /** Accessible name for the body control when there is no visible label. */\n  ariaLabel?: string;\n  /**\n   * Return `false` to KEEP the draft — a failed post must never lose what the\n   * person typed. Anything else clears it.\n   */\n  onSubmit: (draft: CommentComposerDraft) => void | boolean | Promise<void | boolean>;\n}\n\n/**\n * THE comment composer: one bordered box with the field(s) inside and Send\n * beside it (stacked on mobile).\n *\n * One component for every comment surface — the vendor page and the design-doc\n * threads — so the two cannot drift in shape, spacing or submit behaviour. It\n * owns its own draft state, which is what lets both hosts stop hand-rolling\n * one (the vendor page kept title and body in a single newline-joined string).\n */\nexport function CommentComposer({\n  withTitle = false,\n  titleLabel = 'Title',\n  titlePlaceholder = 'Your comment title here',\n  titleRef,\n  bodyLabel = 'Description',\n  placeholder = 'Write your comment...',\n  extras,\n  submitLabel = 'Send',\n  cancelLabel = 'Cancel',\n  onCancel,\n  pending = false,\n  ariaLabel,\n  onSubmit,\n}: CommentComposerProps) {\n  const [title, setTitle] = useState('');\n  const [body, setBody] = useState('');\n  // A titled composer needs both halves — the host's API rejects a title-only\n  // or body-only comment, and disabling Send says so before the round trip.\n  const canSubmit = body.trim().length > 0 && (!withTitle || title.trim().length > 0) && !pending;\n\n  const submit = async () => {\n    if (!canSubmit) return;\n    const kept = await onSubmit({ title: title.trim(), body: body.trim() });\n    if (kept === false) return;\n    setTitle('');\n    setBody('');\n  };\n\n  return (\n    <div className=\"flex flex-col gap-[var(--spacing-system-xsf)]\">\n      <div className=\"flex flex-col gap-[var(--spacing-system-sf)] md:flex-row\">\n        <div className=\"flex-1 overflow-hidden rounded-lg border border-ods-border bg-ods-bg\">\n          {withTitle ? (\n            <div className=\"border-b border-ods-border\">\n              <Label className=\"px-3 pb-1 pt-2 text-ods-text-secondary text-badge\">{titleLabel}</Label>\n              <Input\n                ref={titleRef}\n                value={title}\n                onChange={e => setTitle(e.target.value)}\n                placeholder={titlePlaceholder}\n                disabled={pending}\n                className=\"rounded-none border-0 bg-transparent focus-visible:ring-0\"\n              />\n            </div>\n          ) : null}\n          {withTitle ? <Label className=\"px-3 pb-1 pt-2 text-ods-text-secondary text-badge\">{bodyLabel}</Label> : null}\n          <Textarea\n            value={body}\n            onChange={e => setBody(e.target.value)}\n            placeholder={placeholder}\n            disabled={pending}\n            aria-label={ariaLabel ?? (withTitle ? bodyLabel : submitLabel)}\n            className=\"min-h-[64px] rounded-none border-0 bg-transparent focus-visible:ring-0\"\n          />\n        </div>\n        <div className=\"flex items-start gap-[var(--spacing-system-xsf)]\">\n          {onCancel ? (\n            <Button type=\"button\" variant=\"outline\" size=\"small-legacy\" onClick={onCancel} disabled={pending}>\n              {cancelLabel}\n            </Button>\n          ) : null}\n          <Button\n            type=\"button\"\n            variant=\"accent\"\n            size=\"small-legacy\"\n            onClick={submit}\n            disabled={!canSubmit}\n            loading={pending}\n          >\n            {submitLabel}\n          </Button>\n        </div>\n      </div>\n      {extras ? <div className=\"flex flex-wrap items-center gap-[var(--spacing-system-xsf)]\">{extras}</div> : null}\n    </div>\n  );\n}\n","'use client';\n\nimport { X } from 'lucide-react';\nimport { cn } from '../utils/cn';\n\n// Unified FilterChip component for consistent styling across the application\ninterface FilterChipProps {\n  id: string;\n  label: string;\n  variant?: 'selected' | 'unselected' | 'category' | 'subcategory' | 'tag' | 'info';\n  size?: 'sm' | 'md';\n  removable?: boolean;\n  onRemove?: () => void;\n  onClick?: () => void;\n  disabled?: boolean;\n  className?: string;\n}\n\nexport function FilterChip({\n  label,\n  variant = 'unselected',\n  size = 'md',\n  removable = false,\n  onRemove,\n  onClick,\n  disabled = false,\n  className,\n}: FilterChipProps) {\n  const baseClasses = cn(\n    'group inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full font-medium transition-all duration-200',\n    'hover:scale-105 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2',\n    'font-body leading-none',\n    // Size variants - enhanced mobile sizing for better visibility and touch targets\n    size === 'sm'\n      ? 'py-1 pl-3 pr-3 text-sm md:py-1 md:pl-3 md:pr-3 md:text-sm'\n      : 'py-2 pl-3 pr-3 text-sm md:py-2 md:pl-3 md:pr-3 md:text-sm',\n    // Add gap only if removable (has X button) - placed after text\n    removable && 'gap-1 md:gap-1',\n    // Disabled state\n    disabled && 'cursor-not-allowed opacity-50 hover:scale-100 hover:shadow-none',\n  );\n\n  const variantClasses = {\n    // Legacy variants (for backward compatibility)\n    selected:\n      'bg-ods-bg-surface text-ods-text-primary border border-ods-accent hover:bg-ods-border hover:border-ods-accent-hover focus:ring-ods-accent focus:ring-offset-ods-bg',\n    unselected:\n      'bg-ods-bg-surface text-ods-text-secondary border border-ods-border hover:bg-ods-border hover:border-ods-border-hover hover:text-ods-text-primary focus:ring-ods-border focus:ring-offset-ods-bg',\n    info: 'bg-ods-border text-ods-text-secondary border border-ods-border-hover cursor-default hover:scale-100 hover:shadow-none focus:ring-ods-border-hover focus:ring-offset-ods-bg',\n\n    // New subtle selected variants - same backgrounds/text, only border colors different\n    category:\n      'bg-ods-bg-surface text-ods-text-primary border border-ods-accent/40 hover:bg-ods-border hover:border-ods-accent/60 hover:text-ods-text-primary focus:ring-ods-accent/40 focus:ring-offset-ods-bg',\n    subcategory:\n      'bg-ods-bg-surface text-ods-text-primary border border-ods-accent/60 hover:bg-ods-border hover:border-ods-accent/80 hover:text-ods-text-primary focus:ring-ods-accent/60 focus:ring-offset-ods-bg',\n    tag: 'bg-ods-bg-surface text-ods-text-primary border border-ods-accent/20 hover:bg-ods-border hover:border-ods-accent/30 hover:text-ods-text-primary focus:ring-ods-accent/20 focus:ring-offset-ods-bg',\n  };\n\n  return (\n    <div\n      className={cn(baseClasses, variantClasses[variant], className)}\n      onClick={\n        disabled\n          ? undefined\n          : e => {\n              e.preventDefault();\n              e.stopPropagation();\n              onClick?.();\n            }\n      }\n      role={onClick ? 'button' : undefined}\n      tabIndex={onClick && !disabled ? 0 : undefined}\n      aria-pressed={onClick && variant === 'selected' ? true : undefined}\n      aria-disabled={disabled}\n    >\n      <span\n        className={cn(\n          'truncate text-center font-body font-medium leading-none',\n          size === 'sm' ? 'max-w-[100px] md:max-w-[120px]' : 'max-w-[120px] md:max-w-[140px]',\n        )}\n        title={label}\n      >\n        {label}\n      </span>\n      {removable && onRemove && (\n        <button\n          type=\"button\"\n          onClick={e => {\n            e.preventDefault();\n            e.stopPropagation();\n            if (!disabled) onRemove();\n          }}\n          disabled={disabled}\n          className={cn(\n            'flex h-4 w-4 items-center justify-center rounded-full md:h-4 md:w-4',\n            'shrink-0 transition-all duration-200',\n            'focus:outline-none focus:ring-2 focus:ring-offset-1 group-hover:scale-110',\n            variant === 'category'\n              ? 'text-ods-text-primary hover:bg-ods-bg-hover focus:ring-ods-border-focus focus:ring-offset-ods-bg-surface'\n              : variant === 'subcategory'\n                ? 'text-ods-text-primary hover:bg-ods-bg-hover focus:ring-ods-border-focus focus:ring-offset-ods-bg-surface'\n                : variant === 'tag'\n                  ? 'text-ods-text-primary hover:bg-ods-bg-hover focus:ring-ods-border-focus focus:ring-offset-ods-bg-surface'\n                  : 'text-ods-text-primary hover:bg-ods-bg-hover focus:ring-ods-border-focus focus:ring-offset-ods-bg-surface',\n            disabled && 'cursor-not-allowed opacity-50 hover:scale-100',\n          )}\n          aria-label={`Remove ${label} filter`}\n          tabIndex={disabled ? -1 : 0}\n        >\n          <X className=\"h-2 w-2 md:h-2 md:w-2\" />\n        </button>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { Suspense } from 'react';\nimport type { ReactNode } from 'react';\nimport { SocialIconRow } from './social-icon-row';\nimport { Skeleton } from './ui/skeleton';\n\ninterface FooterLink {\n  href: string;\n  label: string;\n}\n\ninterface FooterSection {\n  title: string;\n  links: FooterLink[];\n}\n\ninterface FooterConfig {\n  name: string;\n  legalName: string;\n  description: string;\n  logo?: ReactNode;\n  sections: FooterSection[];\n  customComponent?: ReactNode; // Inject any custom component here\n  nameElement?: ReactNode; // Custom element for platform name with specific font\n  hideSocialRow?: boolean; // Hide the default social row\n  rightColumnContent?: ReactNode; // Custom content for right column\n  belowDescriptionContent?: ReactNode; // Custom content below description\n  moveDescriptionToRight?: boolean; // Move description and belowDescriptionContent to right column\n  keepBelowDescriptionLeft?: boolean; // Keep belowDescriptionContent on left even when moveDescriptionToRight is true\n  backgroundColor?: string; // ODS background color (e.g., 'bg-ods-card', 'bg-ods-bg')\n  social?: {\n    github?: string;\n    twitter?: string;\n    linkedin?: string;\n    reddit?: string;\n    youtube?: string;\n    instagram?: string;\n    facebook?: string;\n    discord?: string;\n    telegram?: string;\n    whatsapp?: string;\n  };\n}\n\ninterface FooterProps {\n  config?: FooterConfig;\n  renderLink?: (link: FooterLink) => ReactNode;\n}\n\nfunction NavLinkSkeleton() {\n  return <Skeleton className=\"h-5 w-20 md:h-6 md:w-24\" />;\n}\n\n/**\n * Platform-Aware Footer Component\n * Accepts configuration from app-config.ts\n */\nexport function Footer({ config, renderLink }: FooterProps) {\n  // Config is required - no hardcoded fallbacks\n  if (!config) {\n    console.warn('Footer: No config provided');\n    return null;\n  }\n\n  return <UniversalFooter config={config} renderLink={renderLink} />;\n}\n\n/**\n * Universal Footer Component\n * Renders footer based on provided config\n */\nfunction UniversalFooter({\n  config,\n  renderLink,\n}: {\n  config: FooterConfig;\n  renderLink?: (link: FooterLink) => ReactNode;\n}) {\n  const defaultRenderLink = (link: FooterLink) => (\n    <a\n      href={link.href}\n      className=\"text-md md:text-md font-body font-medium leading-[1.33] text-ods-text-primary transition-colors hover:text-ods-accent\"\n    >\n      {link.label}\n    </a>\n  );\n\n  const linkRenderer = renderLink || defaultRenderLink;\n\n  return (\n    <footer\n      className={`flex w-full flex-col items-center justify-center ${config.backgroundColor || 'bg-ods-card'} relative z-[44] min-h-[auto] gap-6 border-t border-ods-border px-6 py-10 md:min-h-[248px] md:gap-6`}\n    >\n      <div className=\"grid w-full grid-cols-2 items-start gap-6 md:gap-8 lg:grid-cols-4\">\n        {/* Column 1: Logo and optionally description */}\n        <div className=\"col-span-2 flex flex-col items-start gap-4 text-left md:col-span-1 md:gap-6 lg:col-span-1\">\n          {/* Logo and name */}\n          <div className=\"flex items-center gap-2\">\n            {config.logo && <Suspense fallback={<Skeleton className=\"h-8 w-8\" />}>{config.logo}</Suspense>}\n            {config.nameElement || (\n              <span className=\"whitespace-nowrap font-heading text-heading-5 font-bold text-ods-text-primary\">\n                {config.name}\n              </span>\n            )}\n          </div>\n\n          {/* Only show description here if NOT moving to right */}\n          {!config.moveDescriptionToRight && (\n            <>\n              <p className=\"font-body text-sm font-medium leading-[1.43] text-ods-text-primary md:text-sm\">\n                {config.description}\n              </p>\n\n              {/* Custom content below description */}\n              {config.belowDescriptionContent && (\n                <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>{config.belowDescriptionContent}</Suspense>\n              )}\n\n              {/* Conditional social row - show by default unless hideSocialRow is true */}\n              {!config.hideSocialRow && (\n                <SocialIconRow\n                  className=\"pt-2\"\n                  links={\n                    config.social\n                      ? Object.entries(config.social)\n                          .filter(([_, href]) => href)\n                          .map(([platform, href]) => ({ platform, href: href }))\n                      : undefined\n                  }\n                />\n              )}\n            </>\n          )}\n\n          {/* Show belowDescriptionContent on left even when description is moved to right */}\n          {config.moveDescriptionToRight && config.keepBelowDescriptionLeft && config.belowDescriptionContent && (\n            <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>{config.belowDescriptionContent}</Suspense>\n          )}\n        </div>\n\n        {/* Dynamic sections - 1 column each on all screens */}\n        {config.sections.map((section, index) => (\n          <div key={index} className=\"col-span-1 flex flex-col items-start gap-3 text-left\">\n            <h3 className=\"tracking-[-0.02em] text-ods-text-muted text-h5\">{section.title}</h3>\n            <div className=\"flex flex-col gap-3\">\n              {section.links.map((link, linkIndex) => (\n                <Suspense key={linkIndex} fallback={<NavLinkSkeleton />}>\n                  {linkRenderer(link)}\n                </Suspense>\n              ))}\n            </div>\n          </div>\n        ))}\n\n        {/* Custom component column - full width on mobile and medium, 1 column on large */}\n        {config.customComponent && (\n          <div className=\"col-span-2 flex flex-col justify-center md:col-span-1 lg:col-span-1\">\n            <Suspense fallback={<Skeleton className=\"h-32 w-full\" />}>{config.customComponent}</Suspense>\n          </div>\n        )}\n\n        {/* Right column content - shows if rightColumnContent is provided OR if moving description to right */}\n        {(config.rightColumnContent || config.moveDescriptionToRight) && (\n          <div className=\"col-span-2 flex flex-col justify-start gap-4 md:col-span-1 md:gap-6 lg:col-span-1\">\n            {/* Show description in right column if moveDescriptionToRight is true */}\n            {config.moveDescriptionToRight && (\n              <>\n                <p className=\"font-body text-sm font-medium leading-[1.43] text-ods-text-primary md:text-sm\">\n                  {config.description}\n                </p>\n\n                {/* Custom content below description - only if NOT keeping it on left */}\n                {config.belowDescriptionContent && !config.keepBelowDescriptionLeft && (\n                  <Suspense fallback={<Skeleton className=\"h-8 w-full\" />}>{config.belowDescriptionContent}</Suspense>\n                )}\n              </>\n            )}\n\n            {/* Regular right column content */}\n            {config.rightColumnContent && (\n              <Suspense fallback={<Skeleton className=\"h-32 w-full\" />}>{config.rightColumnContent}</Suspense>\n            )}\n          </div>\n        )}\n      </div>\n\n      {/* Copyright */}\n      <p className=\"text-md md:text-md w-full pt-4 text-center font-body font-medium leading-[1.33] text-ods-text-muted md:pt-0\">\n        © {new Date().getFullYear()} {config.legalName}. All rights reserved.\n      </p>\n    </footer>\n  );\n}\n","'use client';\n\nimport { useTransition } from 'react';\nimport { useSearchParams, useRouter } from '../embed-shims/next-navigation';\nimport { scrollElementIntoView } from '../utils/scroll-into-view';\n// Top of file, not the mid-file import block below: this one is used inside\n// the hook at the top of the file, so `no-use-before-define` is right about it.\nimport { positiveInt } from '../utils/search-params';\n\n/**\n * Unified AND Filter Logic\n *\n * This module provides common filtering logic that uses AND operations\n * instead of OR operations for all filter combinations.\n */\n\nexport interface FilterState {\n  search?: string;\n  categories?: string[];\n  subcategories?: string[];\n  tags?: string[];\n  filters?: string[];\n  pricing?: string;\n  page?: number;\n}\n\n/**\n * The facets `addFilter` / `removeFilter` / `toggleFilter` operate on.\n *\n * Deliberately EXCLUDES `page`: pagination is not a facet. It is derived state\n * that `updateFilters` resets to 1 whenever a facet changes, and it is set by\n * handing `{ page }` to `updateFilters` directly — never by name through the\n * facet helpers, whose `value` is a `string` and would have to be re-parsed.\n * Before this type existed the helpers took `keyof FilterState`, so `'page'`\n * fell straight out of all three switches: the caller got an empty update that\n * still pushed a new URL and silently reset the list back to page 1.\n *\n * When a new key is added to `FilterState`, either handle it in all three\n * switches or add it to this `Exclude` — the `never` assertions in their\n * `default` branches make it a compile error to do neither.\n */\nexport type FilterFacet = Exclude<keyof FilterState, 'page'>;\n\n/** Minimal shape `getFilterChips` needs from a tag row: slug to match the URL\n *  facet, name to render the chip. Callers pass their own richer rows. */\nexport interface FilterChipTag {\n  slug: string;\n  name: string;\n}\n\n/** Same, for a category row plus its nested subcategories. */\nexport interface FilterChipCategory extends FilterChipTag {\n  subcategories?: FilterChipTag[] | null;\n}\n\nexport interface FilterConfig {\n  basePath: string;\n  supportedFilters: {\n    categories?: boolean;\n    subcategories?: boolean;\n    tags?: boolean;\n    filters?: boolean;\n    pricing?: boolean;\n    search?: boolean;\n  };\n}\n\n/**\n * Hook for managing AND-based filter logic\n */\nexport function useUnifiedFiltering(config: FilterConfig) {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const [isPending, startTransition] = useTransition();\n\n  // Parse current filter state from URL\n  const getCurrentFilterState = (): FilterState => {\n    const search = searchParams?.get('search') || undefined;\n    const categories = searchParams?.get('category')?.split(',').filter(Boolean) || [];\n    const subcategories = searchParams?.get('subcategory')?.split(',').filter(Boolean) || [];\n    const tags = searchParams?.get('tags')?.split(',').filter(Boolean) || [];\n    const filters = searchParams?.getAll('filter') || [];\n    const pricing = searchParams?.get('pricing') || undefined;\n    const page = positiveInt(searchParams?.get('page'), 1);\n\n    return {\n      search,\n      categories,\n      subcategories,\n      tags,\n      filters,\n      pricing,\n      page,\n    };\n  };\n\n  // Update filters with AND logic\n  const updateFilters = (newFilters: Partial<FilterState>, preserveScroll = false) => {\n    const currentState = getCurrentFilterState();\n    const updatedState = { ...currentState, ...newFilters };\n\n    // Always reset to page 1 when filters change (unless explicitly preserving page)\n    if (!('page' in newFilters)) {\n      updatedState.page = 1;\n    }\n\n    const params = new URLSearchParams();\n\n    // Add search parameter\n    if (updatedState.search && config.supportedFilters.search) {\n      params.set('search', updatedState.search);\n    }\n\n    // Add category parameters (AND logic - all must match)\n    if (updatedState.categories && updatedState.categories.length > 0 && config.supportedFilters.categories) {\n      params.set('category', updatedState.categories.join(','));\n    }\n\n    // Add subcategory parameters (AND logic - all must match)\n    if (updatedState.subcategories && updatedState.subcategories.length > 0 && config.supportedFilters.subcategories) {\n      params.set('subcategory', updatedState.subcategories.join(','));\n    }\n\n    // Add tag parameters (AND logic - all must match). Unified `?tags=` param\n    // (matches blog/case-study/media/publication).\n    if (updatedState.tags && updatedState.tags.length > 0 && config.supportedFilters.tags) {\n      params.set('tags', updatedState.tags.join(','));\n    }\n\n    // Add filter parameters (AND logic - all must match)\n    if (updatedState.filters && updatedState.filters.length > 0 && config.supportedFilters.filters) {\n      updatedState.filters.forEach(filter => {\n        params.append('filter', filter);\n      });\n    }\n\n    // Add pricing parameter\n    if (updatedState.pricing && config.supportedFilters.pricing) {\n      params.set('pricing', updatedState.pricing);\n    }\n\n    // Add page parameter (only if not page 1)\n    if (updatedState.page && updatedState.page > 1) {\n      params.set('page', updatedState.page.toString());\n    }\n\n    const currentScrollY = preserveScroll ? window.scrollY : 0;\n    const newUrl = `${config.basePath}?${params.toString()}`;\n\n    startTransition(() => {\n      router.push(newUrl, { scroll: false });\n\n      if (preserveScroll) {\n        setTimeout(() => {\n          // Smooth-restore via the shared anchoring-proof tween: the filtered\n          // list re-renders mid-scroll, which cancels a NATIVE smooth scrollTo\n          // (browser scroll anchoring) — the tween re-asserts every frame.\n          scrollElementIntoView(document.body, { adjustTargetY: () => currentScrollY });\n        }, 100);\n      }\n    });\n  };\n\n  // Add a filter (AND logic)\n  const addFilter = (type: FilterFacet, value: string, preserveScroll = false) => {\n    const currentState = getCurrentFilterState();\n    const updates: Partial<FilterState> = {};\n\n    switch (type) {\n      case 'categories':\n        updates.categories = [...(currentState.categories || []), value];\n        break;\n      case 'subcategories':\n        updates.subcategories = [...(currentState.subcategories || []), value];\n        break;\n      case 'tags':\n        updates.tags = [...(currentState.tags || []), value];\n        break;\n      case 'filters':\n        updates.filters = [...(currentState.filters || []), value];\n        break;\n      case 'pricing':\n        updates.pricing = value;\n        break;\n      case 'search':\n        updates.search = value;\n        break;\n      default: {\n        const _exhaustive: never = type;\n        void _exhaustive;\n        break;\n      }\n    }\n\n    updateFilters(updates, preserveScroll);\n  };\n\n  // Remove a filter (AND logic)\n  const removeFilter = (type: FilterFacet, value?: string, preserveScroll = true) => {\n    const currentState = getCurrentFilterState();\n    const updates: Partial<FilterState> = {};\n\n    switch (type) {\n      case 'categories':\n        updates.categories = value ? (currentState.categories || []).filter(c => c !== value) : [];\n        break;\n      case 'subcategories':\n        updates.subcategories = value ? (currentState.subcategories || []).filter(s => s !== value) : [];\n        break;\n      case 'tags':\n        updates.tags = value ? (currentState.tags || []).filter(t => t !== value) : [];\n        break;\n      case 'filters':\n        updates.filters = value ? (currentState.filters || []).filter(f => f !== value) : [];\n        break;\n      case 'pricing':\n        updates.pricing = undefined;\n        break;\n      case 'search':\n        updates.search = undefined;\n        break;\n      default: {\n        const _exhaustive: never = type;\n        void _exhaustive;\n        break;\n      }\n    }\n\n    updateFilters(updates, preserveScroll);\n  };\n\n  // Toggle a filter (AND logic)\n  const toggleFilter = (type: FilterFacet, value: string, preserveScroll = false) => {\n    const currentState = getCurrentFilterState();\n    let currentValues: string[] = [];\n\n    switch (type) {\n      case 'categories':\n        currentValues = currentState.categories || [];\n        break;\n      case 'subcategories':\n        currentValues = currentState.subcategories || [];\n        break;\n      case 'tags':\n        currentValues = currentState.tags || [];\n        break;\n      case 'filters':\n        currentValues = currentState.filters || [];\n        break;\n      case 'pricing':\n        // Pricing is single-select, so toggle on/off\n        if (currentState.pricing === value) {\n          removeFilter('pricing', value, preserveScroll);\n        } else {\n          addFilter('pricing', value, preserveScroll);\n        }\n        return;\n      case 'search':\n        // Search is single-value, so just update\n        updateFilters({ search: value }, preserveScroll);\n        return;\n      default: {\n        const _exhaustive: never = type;\n        void _exhaustive;\n        return;\n      }\n    }\n\n    if (currentValues.includes(value)) {\n      removeFilter(type, value, preserveScroll);\n    } else {\n      addFilter(type, value, preserveScroll);\n    }\n  };\n\n  // Clear all filters\n  const clearAllFilters = () => {\n    startTransition(() => {\n      router.push(config.basePath, { scroll: false });\n    });\n  };\n\n  // Get filter chips for display\n  const getFilterChips = (categories?: FilterChipCategory[], tags?: FilterChipTag[]) => {\n    const currentState = getCurrentFilterState();\n    const chips: Array<{\n      id: string;\n      label: string;\n      type: 'category' | 'subcategory' | 'tag' | 'filter' | 'pricing';\n    }> = [];\n\n    // Category chips\n    if (currentState.categories && categories) {\n      currentState.categories.forEach(categorySlug => {\n        const category = categories.find(c => c.slug === categorySlug);\n        if (category) {\n          chips.push({\n            id: `category-${categorySlug}`,\n            label: category.name,\n            type: 'category',\n          });\n        }\n      });\n    }\n\n    // Subcategory chips\n    if (currentState.subcategories && categories) {\n      currentState.subcategories.forEach(subcategorySlug => {\n        // Find subcategory across all categories\n        for (const category of categories) {\n          if (category.subcategories) {\n            const subcategory = category.subcategories.find(s => s.slug === subcategorySlug);\n            if (subcategory) {\n              chips.push({\n                id: `subcategory-${subcategorySlug}`,\n                label: subcategory.name,\n                type: 'subcategory',\n              });\n              break;\n            }\n          }\n        }\n      });\n    }\n\n    // Tag chips\n    if (currentState.tags && tags) {\n      currentState.tags.forEach(tagSlug => {\n        const tag = tags.find(t => t.slug === tagSlug);\n        if (tag) {\n          chips.push({\n            id: `tag-${tagSlug}`,\n            label: tag.name,\n            type: 'tag',\n          });\n        }\n      });\n    }\n\n    // Filter chips\n    if (currentState.filters) {\n      currentState.filters.forEach(filterKey => {\n        chips.push({\n          id: `filter-${filterKey}`,\n          label: getFilterLabel(filterKey),\n          type: 'filter',\n        });\n      });\n    }\n\n    // Pricing chip\n    if (currentState.pricing) {\n      chips.push({\n        id: `pricing-${currentState.pricing}`,\n        label: getPricingLabel(currentState.pricing),\n        type: 'pricing',\n      });\n    }\n\n    return chips;\n  };\n\n  // Handle filter chip removal\n  const handleFilterChipRemove = (chipId: string) => {\n    const [type, ...idParts] = chipId.split('-');\n    const id = idParts.join('-');\n\n    switch (type) {\n      case 'category':\n        removeFilter('categories', id);\n        break;\n      case 'subcategory':\n        removeFilter('subcategories', id);\n        break;\n      case 'tag':\n        removeFilter('tags', id);\n        break;\n      case 'filter':\n        removeFilter('filters', id);\n        break;\n      case 'pricing':\n        removeFilter('pricing', id);\n        break;\n    }\n  };\n\n  return {\n    getCurrentFilterState,\n    updateFilters,\n    addFilter,\n    removeFilter,\n    toggleFilter,\n    clearAllFilters,\n    getFilterChips,\n    handleFilterChipRemove,\n    isPending,\n  };\n}\n\nimport { formatClassification, formatPricingModel } from '../utils/format-text-stub';\n\n// Helper functions for filter labels\nfunction getFilterLabel(filterKey: string): string {\n  const customLabels: Record<string, string> = {\n    recommended: 'Recommended',\n    featured: 'Featured',\n  };\n  return customLabels[filterKey] || formatClassification(filterKey);\n}\n\nfunction getPricingLabel(pricingKey: string): string {\n  return formatPricingModel(pricingKey);\n}\n\n/**\n * Vendor-specific filter configuration\n */\nexport const vendorFilterConfig: FilterConfig = {\n  basePath: '/vendors',\n  supportedFilters: {\n    categories: true,\n    subcategories: true,\n    tags: false,\n    filters: true,\n    pricing: true,\n    search: true,\n  },\n};\n\n/**\n * Blog-specific filter configuration\n */\nexport const blogFilterConfig: FilterConfig = {\n  basePath: '/blog',\n  supportedFilters: {\n    categories: true,\n    subcategories: false,\n    tags: true,\n    filters: false,\n    pricing: false,\n    search: true,\n  },\n};\n","// Stub for format-text utilities\nexport function formatText(text: string): string {\n  return text;\n}\n\nexport function truncateText(text: string, maxLength: number): string {\n  if (text.length <= maxLength) return text;\n  return text.slice(0, maxLength) + '...';\n}\n\nexport function formatClassification(classification: string): string {\n  return classification.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n}\n\nexport function formatPricingModel(pricing: string): string {\n  return pricing.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase());\n}\n","'use client';\n\nimport { useCallback } from 'react';\nimport { useChatRuntime } from '../contexts/chat-runtime-context';\nimport { useRouter } from '../embed-shims/next-navigation';\nimport { executeNavigationImperative } from './chat/utils/execute-navigation';\nimport { OpenFrameLogo } from './icons';\nimport { Button } from './ui/button';\n\nexport interface FooterWaitlistButtonProps {\n  className?: string;\n}\n\n/**\n * Small wrapper around JoinWaitlistButton for use inside the footer.\n *\n * Routes through the host's unified-navigation hook\n * (`runtime.navigation.navigate`) when a `ChatRuntimeContext` is\n * mounted — that's the same path EVERY other in-app navigation\n * surface uses (source chips, inline cards, search-autocomplete,\n * action cards). One rule, one decision tree across the whole app.\n * The hub's `HubRuntimeProvider` wires `navigate` to its `useUnifiedNav`\n * helper, so this button picks up cross-platform new-tab decisions,\n * same-URL re-scroll handling, embed-mode short-circuiting, and any\n * future host-side nav rules for free.\n *\n * Falls back to the embed-shim's `router.push` when no runtime is\n * mounted (third-party embedders who haven't set up\n * `ChatRuntimeContext` — the lib stays usable without forcing them\n * to wire the full chat-runtime).\n *\n * Target URL: `/waitlist#top`. `#top` is the canonical \"scroll to\n * page top\" anchor — the destination page has an explicit\n * `<div id=\"top\">` at the top of `<main>` so native browser anchor\n * scroll works in every browser regardless of the HTML5 magic-anchor\n * behavior.\n */\nexport function FooterWaitlistButton({ className }: FooterWaitlistButtonProps) {\n  const router = useRouter();\n  const runtime = useChatRuntime();\n\n  const handleClick = useCallback(() => {\n    // The unified nav primitive: host `navigation.navigate` if wired, else the\n    // embed-shim router; new-tab/embed decision handled internally.\n    executeNavigationImperative({ runtime, href: '/waitlist#top', fallbackNavigate: router.push });\n  }, [router, runtime]);\n\n  return (\n    <Button onClick={handleClick} className={className} leftIcon={<OpenFrameLogo />}>\n      Join Waitlist\n    </Button>\n  );\n}\n","'use client';\n\nimport { Loader2, Image as ImageIcon, Upload, X } from 'lucide-react';\nimport { useRef, useState } from 'react';\nimport type { ChangeEvent } from 'react';\nimport Image from '../embed-shims/next-image';\nimport { useToast } from '../hooks/use-toast';\nimport { errorMessage } from '../utils/common';\nimport { Button } from './ui/button';\n\ninterface HeroImageUploaderProps {\n  /** Current image URL if one already exists */\n  imageUrl?: string;\n  /** Callback fired with new image URL (or undefined if removed) */\n  onChange: (url: string | undefined) => void;\n  /** Upload endpoint (required) */\n  uploadEndpoint: string;\n  /** Height of drop-zone. Number treated as pixels, string passed directly (e.g. '100%') */\n  height?: number | string;\n  /** Image object-fit, defaults to cover */\n  objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\n  /** Show a replace/upload button overlay in addition to remove (default true for parity with blog editor) */\n  showReplaceButton?: boolean;\n  /** If true, skip the actual upload and just return a base64 data URL preview. Useful for unauthenticated flows – the caller can upload later. */\n  deferUpload?: boolean;\n  /** Optional custom upload handler for authenticated uploads. If provided, this will be used instead of the default fetch */\n  onUpload?: (file: File) => Promise<string>;\n  /** Optional custom delete handler for authenticated deletion. If provided, this will be used instead of just clearing the image */\n  onDelete?: () => Promise<void>;\n}\n\n/**\n * Pull the stored URL out of an upload response.\n *\n * `uploadEndpoint` is caller-supplied, and the three endpoints in the fleet\n * answer with three different envelopes — `{ data: { url } }` (the hub's\n * media routes), `{ url }` (the blob upload proxy) and `{ file_url }` (the\n * legacy supabase route). Returns null when none of them is a usable string,\n * which the caller reports as \"Invalid upload response\".\n */\nfunction readUploadedUrl(payload: unknown): string | null {\n  if (typeof payload !== 'object' || payload === null) return null;\n\n  if ('data' in payload) {\n    const nested = payload.data;\n    if (typeof nested === 'object' && nested !== null && 'url' in nested) {\n      if (typeof nested.url === 'string' && nested.url) return nested.url;\n    }\n  }\n  if ('url' in payload && typeof payload.url === 'string' && payload.url) return payload.url;\n  if ('file_url' in payload && typeof payload.file_url === 'string' && payload.file_url) return payload.file_url;\n\n  return null;\n}\n\n/**\n * Reusable dashed hero-style image uploader identical to Blog Editor's hero picker.\n * Handles client-side validation (JPEG/PNG/WebP/GIF up to 5 MB), upload, preview & removal.\n */\nexport function HeroImageUploader({\n  imageUrl,\n  onChange,\n  uploadEndpoint,\n  height = 300,\n  objectFit = 'cover',\n  showReplaceButton = true,\n  deferUpload = false,\n  onUpload,\n  onDelete,\n}: HeroImageUploaderProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const { toast } = useToast();\n  const [uploading, setUploading] = useState(false);\n\n  const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];\n  const MAX_SIZE = 5 * 1024 * 1024; // 5MB\n\n  const openDialog = () => inputRef.current?.click();\n\n  async function handleFile(file?: File) {\n    if (!file) return;\n    if (!ALLOWED_TYPES.includes(file.type)) {\n      toast({ title: 'Invalid file', description: 'Upload JPEG, PNG, WebP, or GIF', variant: 'destructive' });\n      return;\n    }\n    if (file.size > MAX_SIZE) {\n      toast({ title: 'File too large', description: 'Max 5MB', variant: 'destructive' });\n      return;\n    }\n\n    if (deferUpload) {\n      // Immediately convert to data URL for preview and postpone real upload\n      try {\n        setUploading(true);\n        const reader = new FileReader();\n        reader.onload = () => {\n          const dataUrl = reader.result as string;\n          onChange(dataUrl); // Return data URL so parent can preview & store locally\n          setUploading(false);\n        };\n        reader.onerror = () => {\n          toast({ title: 'File error', description: 'Failed to read image file', variant: 'destructive' });\n          setUploading(false);\n        };\n        reader.readAsDataURL(file);\n      } catch (err) {\n        toast({\n          title: 'File error',\n          description: errorMessage(err, 'Failed to process image'),\n          variant: 'destructive',\n        });\n        setUploading(false);\n      } finally {\n        if (inputRef.current) inputRef.current.value = '';\n      }\n      return;\n    }\n\n    // Upload flow - use custom handler if provided, otherwise use default fetch\n    setUploading(true);\n    try {\n      let uploadedUrl: string;\n\n      if (onUpload) {\n        // Use custom upload handler (e.g., for authenticated uploads)\n        uploadedUrl = await onUpload(file);\n      } else {\n        // Default upload flow\n        const fd = new FormData();\n        fd.append('file', file);\n        const res = await fetch(uploadEndpoint, { method: 'POST', body: fd });\n        if (!res.ok) throw new Error('Upload failed');\n        const json: unknown = await res.json();\n        const url = readUploadedUrl(json);\n        if (!url) throw new Error('Invalid upload response');\n        uploadedUrl = url;\n      }\n\n      onChange(uploadedUrl);\n    } catch (err) {\n      toast({ title: 'Upload error', description: errorMessage(err, 'Failed to upload'), variant: 'destructive' });\n    } finally {\n      setUploading(false);\n      if (inputRef.current) inputRef.current.value = '';\n    }\n  }\n\n  const handleSelect = (e: ChangeEvent<HTMLInputElement>) => {\n    // `handleFile` never rejects — it toasts failures and resets in `finally`.\n    void handleFile(e.target.files?.[0]);\n  };\n\n  const handleRemove = async () => {\n    if (onDelete) {\n      try {\n        await onDelete();\n      } catch {\n        // onDelete handler should handle its own error reporting\n        return;\n      }\n    }\n    onChange(undefined);\n  };\n\n  const heightStyle = typeof height === 'number' ? `${height}px` : height;\n\n  return (\n    <div className=\"h-full max-h-full min-h-[300px] w-full space-y-2\">\n      {imageUrl ? (\n        <div\n          className=\"group relative flex aspect-square h-auto w-full items-center justify-center overflow-hidden md:aspect-auto md:h-full\"\n          style={{ height: heightStyle }}\n        >\n          <Image src={imageUrl} className={`object-${objectFit}`} alt=\"Cover\" fill sizes=\"100vw\" unoptimized />\n          <div className=\"absolute inset-0 flex items-center justify-center gap-4 rounded-lg bg-black/40 opacity-0 transition-opacity group-hover:opacity-100\">\n            {showReplaceButton && (\n              <Button\n                variant=\"outline\"\n                size=\"icon\"\n                onClick={openDialog}\n                className=\"h-12 w-12 rounded-full bg-white text-black hover:bg-gray-100\"\n              >\n                <Upload className=\"h-5 w-5\" />\n              </Button>\n            )}\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              onClick={() => handleRemove()}\n              className=\"h-12 w-12 rounded-full bg-white text-black hover:bg-gray-100\"\n            >\n              <X className=\"h-5 w-5\" />\n            </Button>\n          </div>\n        </div>\n      ) : (\n        <div\n          className={`h-full w-full border-2 border-dashed ${uploading ? 'border-ods-accent' : 'border-ods-border hover:border-ods-accent'} flex cursor-pointer flex-col items-center justify-center rounded-lg bg-ods-bg`}\n          style={{ height: heightStyle }}\n          onClick={openDialog}\n        >\n          {uploading ? (\n            <Loader2 className=\"h-8 w-8 animate-spin text-ods-accent\" />\n          ) : (\n            <>\n              <ImageIcon className=\"h-12 w-12 text-ods-text-secondary\" />\n              <span className=\"mt-2 text-ods-text-primary text-h6\">Upload cover image</span>\n              <span className=\"mt-1 text-ods-text-secondary text-h6\">Click to upload or drag and drop</span>\n              <span className=\"text-ods-text-secondary text-h6\">PNG, JPEG, WebP, GIF up to 5MB</span>\n            </>\n          )}\n        </div>\n      )}\n\n      {/* hidden input */}\n      <input ref={inputRef} type=\"file\" accept=\"image/*\" onChange={handleSelect} className=\"hidden\" />\n    </div>\n  );\n}\n","'use client';\n\nimport { Sun, Moon, CheckCircle, Github, PlusCircle } from 'lucide-react';\nimport type React from 'react';\nimport { useState, useEffect, useMemo } from 'react';\nimport { useIsHydrated } from '../hooks/ui/use-is-hydrated';\nimport {\n  VendorDirectoryIcon,\n  OpenSourceIcon,\n  CommunityHubIcon,\n  VendorsIcon,\n  CommunityIcon,\n  CompareIcon,\n} from './icons-stub';\n\n// Map lucide icons\nconst SunIcon = Sun;\nconst MoonIcon = Moon;\nconst CheckCircleIcon = CheckCircle;\nconst GitHubIcon = Github;\nconst PlusCircleIcon = PlusCircle;\nconst OpenmspLogo = () => <div>Logo</div>;\n\ninterface IconsBlockProps {\n  /**\n   * When true, always render the loading placeholder bar regardless of whether the grid is ready.\n   * Default behaviour renders placeholder only until the grid is generated.\n   */\n  loading?: boolean;\n}\n\n// Available icons array - moved outside component to prevent recreating on each render\nconst availableIcons = [\n  VendorDirectoryIcon,\n  OpenSourceIcon,\n  CommunityHubIcon,\n  VendorsIcon,\n  CommunityIcon,\n  CompareIcon,\n  SunIcon,\n  MoonIcon,\n  CheckCircleIcon,\n  GitHubIcon,\n  PlusCircleIcon,\n  OpenmspLogo,\n];\n\n/** One cell of the decorative grid — the glyph components in `availableIcons`. */\ntype GridIcon = React.ComponentType<{ width?: number; height?: number; className?: string }>;\n\n/** Empty grid sentinel — also what \"not on the client yet\" renders as. */\nconst NO_GRID: GridIcon[][] = [];\n\n/**\n * Two rows of glyphs, deterministic in `(columns, seed)`: each cell picks from\n * the icons its left and top neighbours are NOT using, so no two touching cells\n * repeat. Pure, which is what lets it be a `useMemo` instead of state written\n * from an effect.\n */\nfunction buildIconGrid(columns: number, seed: number): GridIcon[][] {\n  const grid: GridIcon[][] = [];\n  const totalCells = columns * 2;\n\n  for (let idx = 0; idx < totalCells; idx++) {\n    const col = idx % columns;\n    const row = Math.floor(idx / columns);\n\n    // Initialize row if needed\n    if (!grid[row]) grid[row] = [];\n\n    // Get adjacent icons to avoid\n    const adjacentIcons = new Set();\n\n    // Check left neighbor (same row, previous column)\n    if (col > 0 && grid[row][col - 1]) {\n      adjacentIcons.add(grid[row][col - 1]);\n    }\n\n    // Check top neighbor (previous row, same column)\n    if (row > 0 && grid[row - 1] && grid[row - 1][col]) {\n      adjacentIcons.add(grid[row - 1][col]);\n    }\n\n    // Filter available icons to exclude adjacent ones\n    const availableOptions = availableIcons.filter(icon => !adjacentIcons.has(icon));\n\n    // Generate random selection from available options using the stable seed\n    const cellSeed = seed + idx;\n    const pseudoRandom = (cellSeed * 9301 + 49297) % 233280;\n    const normalized = pseudoRandom / 233280;\n    const iconIndex = Math.floor(normalized * availableOptions.length);\n\n    // Assign selected icon to grid\n    grid[row][col] = availableOptions[iconIndex] || availableIcons[0]; // Fallback to first icon\n  }\n\n  return grid;\n}\n\nexport function ResponsiveIconsBlock({ loading = false }: IconsBlockProps) {\n  const [columns, setColumns] = useState(24);\n  // Lazy initialiser, not a ref filled in an effect: the seed is fixed for the\n  // component's life and is read during render below.\n  const [randomSeed] = useState(() => Date.now());\n  // Client-only, and deliberately so: the seed differs between the server and\n  // the browser, so a grid rendered on the server would hydrate into a\n  // different set of glyphs. `useIsHydrated` is that gate — an empty grid keeps\n  // rendering the placeholder exactly as the old mount effect did, and past\n  // hydration the grid is DERIVED rather than published by a setState in an\n  // effect body.\n  const hydrated = useIsHydrated();\n  const iconGrid = useMemo(\n    () => (hydrated ? buildIconGrid(columns, randomSeed) : NO_GRID),\n    [hydrated, columns, randomSeed],\n  );\n\n  useEffect(() => {\n    function calculateColumns() {\n      const cols = Math.ceil(window.innerWidth / 56) + 4;\n      setColumns(cols);\n    }\n\n    calculateColumns();\n    window.addEventListener('resize', calculateColumns);\n\n    return () => window.removeEventListener('resize', calculateColumns);\n  }, []);\n\n  const displayColumns = columns;\n\n  // Get icon for specific position from pre-generated grid\n  const getIconForIndex = (index: number) => {\n    const col = index % displayColumns;\n    const row = Math.floor(index / displayColumns);\n    return iconGrid[row]?.[col] || availableIcons[0];\n  };\n\n  // When explicit loading prop true OR grid not ready → show placeholder\n  if (loading || iconGrid.length === 0) {\n    return (\n      <div\n        className=\"relative h-[80px] w-full overflow-hidden bg-[#1A1A1A] md:h-[112px]\"\n        role=\"presentation\"\n        aria-hidden=\"true\"\n      >\n        {/* subtle pulse bar */}\n        <div className=\"absolute inset-0 animate-pulse bg-[#2A2A2A]/60\" />\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className=\"relative z-10 h-[80px] w-full overflow-hidden bg-[#1A1A1A] md:h-[112px]\"\n      style={{ margin: 0, padding: 0 }}\n      role=\"presentation\"\n      aria-hidden=\"true\"\n    >\n      <style>{`\n        .icons-block svg,\n        .icons-block svg * {\n          filter: grayscale(100%) brightness(0) invert(1) brightness(0.4) !important;\n          fill: currentColor !important;\n        }\n      `}</style>\n      <div\n        className=\"icons-block grid h-full w-full\"\n        style={{\n          gridTemplateColumns: `repeat(${displayColumns}, 56px)`,\n          gridTemplateRows: 'repeat(2, 1fr)',\n        }}\n      >\n        {Array.from({ length: displayColumns * 2 }).map((_, idx) => {\n          const col = idx % displayColumns;\n          const row = Math.floor(idx / displayColumns);\n          const IconComponent = getIconForIndex(idx);\n\n          return (\n            <div\n              key={idx}\n              className=\"flex h-full w-full items-center justify-center\"\n              style={{\n                background: '#1A1A1A',\n                borderRight: col !== displayColumns - 1 ? '0.5px solid rgba(66, 66, 66, 0.5)' : undefined,\n                borderBottom: row === 0 ? '0.5px solid rgba(66, 66, 66, 0.5)' : undefined,\n                margin: 0,\n                padding: '8px',\n                boxSizing: 'border-box',\n              }}\n              role=\"presentation\"\n            >\n              <IconComponent width={16} height={16} className=\"text-[#666666] md:h-5 md:w-5\" aria-hidden=\"true\" />\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n","// Stub implementations for missing icons\nimport { DollarSign, Code, Users, Building, GitCompare, MessageSquare } from 'lucide-react';\n\nexport const OpenSourceIcon = Code;\nexport const CoinsIcon = DollarSign;\nexport const VendorDirectoryIcon = Building;\nexport const CommunityHubIcon = Users;\nexport const VendorsIcon = Building;\nexport const CommunityIcon = MessageSquare;\nexport const CompareIcon = GitCompare;\n\n// OpenmspLogo moved to real implementation (should be imported from main project)\n\n// OpenFrameLogo moved to real implementation in ./openframe-logo.tsx\n","/*\n  ImageCropper.tsx\n  ----------------\n  Reusable React component for interactive image cropping with:\n    • Drag / resize cropping via react-easy-crop\n    • Custom aspect ratio or free-form\n    • Optional circular crop overlay (avatar mode)\n    • Automatic scaling so the exported image never exceeds maxSizePx\n    • Returns PNG data URL + Blob on confirm\n    • Responsive / accessible with shadcn/ui Button & Slider components\n\n  Styling relies on Tailwind + shadcn design tokens.\n*/\n\n'use client';\n\nimport { Check, RotateCcw } from 'lucide-react';\nimport type React from 'react';\nimport { useCallback, useState } from 'react';\nimport Cropper, { type Area } from 'react-easy-crop';\nimport { cn } from '../utils/cn';\nimport { Button } from './ui/button';\nimport { Slider } from './ui/slider';\n\n/* ------------------------------------------------------------\n * Types\n * ----------------------------------------------------------*/\n\nexport interface ImageCropperResult {\n  /** Cropped PNG data URL */\n  dataUrl: string;\n  /** Corresponding PNG Blob */\n  blob: Blob;\n}\n\nexport interface ImageCropperProps {\n  /** Source image (URL or data URI) */\n  src: string;\n  /** Called when user confirms crop */\n  onComplete(result: ImageCropperResult): void;\n  /** Called when user cancels crop */\n  onCancel?(): void;\n  /** Aspect ratio (width / height).  If omitted, free-form */\n  aspectRatio?: number;\n  /** Enable circular crop overlay for avatars */\n  circular?: boolean;\n  /** Maximum width/height for the exported PNG (defaults 512) */\n  maxSizePx?: number;\n  /** Optional className for wrapper */\n  className?: string;\n}\n\n/* ------------------------------------------------------------\n * Helpers\n * ----------------------------------------------------------*/\n\n/** Util to create an HTMLImageElement that resolves when loaded */\nfunction loadImage(src: string): Promise<HTMLImageElement> {\n  return new Promise((resolve, reject) => {\n    const img = new Image();\n    img.crossOrigin = 'anonymous'; // prevent canvas tainting\n    img.onload = () => resolve(img);\n    img.onerror = () => reject(new Error('Failed to load image'));\n    img.src = src;\n  });\n}\n\n/* ------------------------------------------------------------\n * Component\n * ----------------------------------------------------------*/\n\nexport const ImageCropper: React.FC<ImageCropperProps> = ({\n  src,\n  onComplete,\n  onCancel,\n  aspectRatio,\n  circular = false,\n  maxSizePx = 512,\n  className,\n}) => {\n  const [crop, setCrop] = useState({ x: 0, y: 0 });\n  const [zoom, setZoom] = useState(1);\n  const [rotation, setRotation] = useState(0);\n  const [croppedAreaPixels, setCroppedAreaPixels] = useState<{\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null>(null);\n\n  /* ------------------ crop complete callback -----------------*/\n  const onCropComplete = useCallback((_croppedAreaPercentages: Area, areaPixels: Area) => {\n    setCroppedAreaPixels(areaPixels);\n  }, []);\n\n  /* ------------------ Build checkered background --------------*/\n  const checkerBg =\n    'bg-[length:16px_16px] bg-[linear-gradient(45deg,transparent_25%,#2a2a2a_25%,#2a2a2a_75%,transparent_75%,transparent),linear-gradient(45deg,#2a2a2a_25%,transparent_25%,transparent_75%,#2a2a2a_75%,#2a2a2a)]';\n\n  /* ------------------ Export logic ---------------------------*/\n  const exportCrop = useCallback(async () => {\n    if (!croppedAreaPixels) return undefined;\n\n    const img = await loadImage(src);\n\n    // Create canvas the size of crop\n    const canvas = document.createElement('canvas');\n\n    // Scale crop to fit maxSizePx\n    const scale = Math.min(1, maxSizePx / Math.max(croppedAreaPixels.width, croppedAreaPixels.height));\n    const outputW = Math.round(croppedAreaPixels.width * scale);\n    const outputH = Math.round(croppedAreaPixels.height * scale);\n\n    canvas.width = outputW;\n    canvas.height = outputH;\n    // `getContext` yields null when the browser refuses a 2D context (blocked\n    // canvas, exhausted GPU contexts). Name that instead of letting the next\n    // `drawImage` throw \"Cannot read properties of null\".\n    const ctx = canvas.getContext('2d');\n    if (!ctx) throw new Error('ImageCropper: could not acquire a 2D canvas context for the crop');\n\n    // Draw cropped portion\n    ctx.drawImage(\n      img,\n      croppedAreaPixels.x,\n      croppedAreaPixels.y,\n      croppedAreaPixels.width,\n      croppedAreaPixels.height,\n      0,\n      0,\n      outputW,\n      outputH,\n    );\n\n    // If circular mode, clip to circle\n    if (circular) {\n      const temp = document.createElement('canvas');\n      temp.width = outputW;\n      temp.height = outputH;\n      const tctx = temp.getContext('2d');\n      if (!tctx) throw new Error('ImageCropper: could not acquire a 2D canvas context for the circular mask');\n      tctx.beginPath();\n      tctx.arc(outputW / 2, outputH / 2, outputW / 2, 0, Math.PI * 2);\n      tctx.closePath();\n      tctx.clip();\n      tctx.drawImage(canvas, 0, 0);\n      canvas.width = outputW;\n      canvas.height = outputH;\n      ctx.clearRect(0, 0, outputW, outputH);\n      ctx.drawImage(temp, 0, 0);\n    }\n\n    return new Promise<ImageCropperResult>(resolve => {\n      canvas.toBlob(blob => {\n        if (!blob) throw new Error('Canvas export failed');\n        const reader = new FileReader();\n        reader.onloadend = () => {\n          resolve({ dataUrl: reader.result as string, blob });\n        };\n        reader.readAsDataURL(blob);\n      }, 'image/png');\n    });\n  }, [croppedAreaPixels, circular, maxSizePx, src]);\n\n  /* ------------------ Keyboard accessibility -----------------*/\n  const handleKey = (e: React.KeyboardEvent) => {\n    // Enter to confirm, Esc to cancel\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      void exportCrop().then(res => {\n        if (res) onComplete(res);\n      });\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      onCancel?.();\n    }\n  };\n\n  /* ------------------ Render ---------------------------------*/\n  return (\n    <div\n      className={cn('relative flex w-full flex-col gap-4', className)}\n      onKeyDown={handleKey}\n      tabIndex={0}\n      aria-label=\"Image cropper\"\n    >\n      {/* Cropper container */}\n      <div className={cn('relative aspect-square w-full overflow-hidden rounded-md md:aspect-video', checkerBg)}>\n        <Cropper\n          image={src}\n          crop={crop}\n          zoom={zoom}\n          rotation={rotation}\n          aspect={aspectRatio}\n          cropShape={circular ? 'round' : 'rect'}\n          showGrid={false}\n          onCropChange={setCrop}\n          onZoomChange={setZoom}\n          onRotationChange={setRotation}\n          onCropComplete={onCropComplete}\n          objectFit=\"contain\"\n        />\n      </div>\n\n      {/* Controls */}\n      <div className=\"flex flex-col gap-4\">\n        {/* Zoom */}\n        <div className=\"flex items-center gap-3\">\n          <span className=\"min-w-[60px] text-h6\">Zoom</span>\n          <Slider\n            min={1}\n            max={3}\n            step={0.01}\n            value={[zoom]}\n            onValueChange={v => setZoom(v[0])}\n            aria-label=\"Zoom\"\n            className=\"flex-1\"\n          />\n        </div>\n        {/* Rotate */}\n        <div className=\"flex items-center gap-3\">\n          <span className=\"min-w-[60px] text-h6\">Rotate</span>\n          <Slider\n            min={0}\n            max={360}\n            step={1}\n            value={[rotation]}\n            onValueChange={v => setRotation(v[0])}\n            aria-label=\"Rotation\"\n            className=\"flex-1\"\n          />\n          <Button variant=\"outline\" size=\"icon\" onClick={() => setRotation(0)} aria-label=\"Reset rotation\">\n            <RotateCcw className=\"h-4 w-4\" />\n          </Button>\n        </div>\n      </div>\n\n      {/* Action buttons */}\n      <div className=\"mt-2 flex justify-end gap-2\">\n        {onCancel && (\n          <Button variant=\"outline\" onClick={onCancel} aria-label=\"Cancel crop\">\n            Cancel\n          </Button>\n        )}\n        <Button\n          variant=\"accent\"\n          onClick={async () => {\n            const result = await exportCrop();\n            if (result) onComplete(result);\n          }}\n          leftIcon={<Check className=\"h-4 w-4\" />}\n          aria-label=\"Apply crop\"\n        >\n          Apply\n        </Button>\n      </div>\n    </div>\n  );\n};\n","'use client';\n\nimport { cn } from '../utils/cn';\n\ninterface MetricValueProps {\n  value: string | number;\n  label: string;\n  className?: string;\n}\n\n/**\n * Displays a numeric/short textual value followed by a smaller grey label.\n * Example: 30s Generation Time\n */\nexport function MetricValue({ value, label, className }: MetricValueProps) {\n  return (\n    <div className={cn('flex items-end gap-2 whitespace-nowrap text-ods-text-primary text-h4', className)}>\n      {value}\n      <span className=\"text-ods-text-secondary text-h6\">{label}</span>\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../utils/cn';\nimport { getProxiedImageUrl } from '../utils/image-proxy';\nimport { SquareAvatar } from './ui/square-avatar';\n\ninterface MSPDisplayProps {\n  name: string;\n  logoUrl?: string | null;\n  size?: number; // avatar size in px (square)\n  className?: string;\n}\n\nexport function MSPDisplay({ name, logoUrl, size = 40, className }: MSPDisplayProps) {\n  return (\n    <div className={cn('flex min-w-0 items-center gap-2', className)}>\n      <SquareAvatar\n        src={logoUrl ? getProxiedImageUrl(logoUrl) || logoUrl : undefined}\n        fallback={name}\n        alt={name}\n        sizePx={size}\n      />\n      <h2 className=\"truncate pl-2\">{name}</h2>\n    </div>\n  );\n}\n","'use client';\n\nimport { Terminal, DollarSign, Network, Users } from 'lucide-react';\nimport type React from 'react';\nimport { SECTION_HEADING_CLASS } from './layout/page-heading';\n\ninterface FeatureCardProps {\n  icon: React.ReactNode;\n  title: string;\n  description: string;\n}\n\nconst FeatureCard: React.FC<FeatureCardProps> = ({ icon, title, description }) => {\n  return (\n    <div className=\"flex h-full flex-col gap-6 rounded-3xl border border-ods-border bg-ods-card p-6 transition-colors duration-200 hover:bg-ods-bg-hover\">\n      {/* Icon Container */}\n      <div className=\"flex h-12 w-12 items-center justify-center rounded border border-ods-border bg-ods-bg\">\n        <div className=\"h-6 w-6 text-ods-text-secondary\">{icon}</div>\n      </div>\n\n      {/* Text Container */}\n      <div className=\"flex flex-col gap-2\">\n        <h3 className=\"tracking-[-0.36px] text-ods-text-primary text-h3\">{title}</h3>\n        <p className=\"text-ods-text-primary text-h4\">{description}</p>\n      </div>\n    </div>\n  );\n};\n\nconst OpenSourceFeatures: React.FC = () => {\n  const features = [\n    {\n      icon: <Terminal className=\"h-6 w-6\" />,\n      title: 'Built on FOSS',\n      description: 'No black boxes. No hidden fees. Just transparent, community-driven software you control.',\n    },\n    {\n      icon: <DollarSign className=\"h-6 w-6\" />,\n      title: 'Own Your Stack',\n      description: 'Replace overpriced, proprietary tools with open, auditable, and customizable components.',\n    },\n    {\n      icon: <Network className=\"h-6 w-6\" />,\n      title: 'Modular by Design',\n      description: 'Add, remove, or extend features with ease — OpenFrame adapts to how you work.',\n    },\n    {\n      icon: <Users className=\"h-6 w-6\" />,\n      title: 'Community-Powered',\n      description:\n        \"Developed with and for MSPs by a global open-source community. You're not just a user — you're part of the roadmap.\",\n    },\n  ];\n\n  return (\n    <section className=\"w-full bg-ods-bg py-12 md:py-16 lg:py-20\">\n      <div className=\"mx-auto w-full max-w-[1920px] px-6 md:px-20\">\n        {/* Section Title */}\n        <div className=\"flex flex-col items-center gap-10\">\n          <h2 className={`${SECTION_HEADING_CLASS} w-full text-center`}>\n            <span className=\"text-ods-accent\">100%</span>\n            <span> Open-Source. </span>\n            <span className=\"text-ods-accent\">0%</span>\n            <span> Bullsh*t.</span>\n          </h2>\n\n          {/* Features Grid */}\n          <div className=\"w-full\">\n            <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4\">\n              {features.map((feature, index) => (\n                <FeatureCard key={index} icon={feature.icon} title={feature.title} description={feature.description} />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default OpenSourceFeatures;\n","'use client';\n\nimport type { StructuredPricingSummary as StructuredPricingItem } from '../utils/compare-utils-stub';\n\n// Using StructuredPricingItem from compare-utils instead of local interface\n\ninterface PricingStyleConfig {\n  priceTextSize: string;\n  priceTextColor: string;\n  secondaryTextSize: string;\n  secondaryTextColor: string;\n  showTildePrefix: boolean;\n  fontFamily?: string;\n}\n\ninterface PricingDisplayProps {\n  pricing: StructuredPricingItem[] | string | StructuredPricingItem; // Support both new structure and legacy string\n  className?: string;\n  styleConfig?: PricingStyleConfig;\n}\n\n// Default style configurations for different contexts\nexport const PRICING_STYLES = {\n  // Comparison table style (current default)\n  comparison: {\n    priceTextSize: 'text-h6',\n    priceTextColor: 'text-ods-text-primary',\n    secondaryTextSize: 'text-h6',\n    secondaryTextColor: 'text-ods-text-secondary',\n    showTildePrefix: false,\n    fontFamily: 'font-body',\n  },\n  // Vendor dropdown compact style (Figma design)\n  compact: {\n    priceTextSize: 'text-h6',\n    priceTextColor: 'text-ods-text-secondary',\n    secondaryTextSize: 'text-h6',\n    secondaryTextColor: 'text-ods-text-secondary',\n    showTildePrefix: true,\n    fontFamily: 'font-body',\n  },\n  // Card style for vendor cards\n  card: {\n    priceTextSize: 'text-h6',\n    priceTextColor: 'text-ods-text-primary',\n    secondaryTextSize: 'text-h6',\n    secondaryTextColor: 'text-ods-text-secondary',\n    showTildePrefix: true,\n    fontFamily: 'font-body',\n  },\n} as const;\n\n/**\n * Shared component for consistent pricing display with configurable styling\n * Now accepts structured pricing data and style configuration for better control\n */\nexport function PricingDisplay({\n  pricing,\n  className = '',\n  styleConfig = PRICING_STYLES.comparison,\n}: PricingDisplayProps) {\n  // Legacy support for string input\n  if (typeof pricing === 'string') {\n    return <LegacyPricingDisplay pricing={pricing} className={className} styleConfig={styleConfig} />;\n  }\n\n  // Convert to array if single item\n  const pricingArray = Array.isArray(pricing) ? pricing : [pricing];\n\n  // Handle empty pricing\n  if (!pricingArray || pricingArray.length === 0) {\n    return (\n      <span\n        className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize} ${styleConfig.fontFamily} ${className}`}\n      >\n        No pricing data\n      </span>\n    );\n  }\n\n  // Handle single pricing item\n  if (pricingArray.length === 1) {\n    const item = pricingArray[0];\n    const price = item.ranges?.[0]?.min || 0;\n    const unit = item.ranges?.[0]?.unit;\n    return (\n      <span className={`${styleConfig.fontFamily} ${className}`}>\n        <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>\n          {formatPriceValue(price, styleConfig.showTildePrefix)}\n        </span>\n        {unit && <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>/{unit}</span>}\n      </span>\n    );\n  }\n\n  // Handle multiple pricing items\n  const priceValues = pricingArray.map(item =>\n    formatPriceValue(item.ranges?.[0]?.min || 0, styleConfig.showTildePrefix),\n  );\n\n  // Find the first item that has unit info\n  const itemWithUnit = pricingArray.find(item => item.ranges?.[0]?.unit);\n\n  return (\n    <span className={`${styleConfig.fontFamily} ${className}`}>\n      <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>{priceValues.join(' | ')}</span>\n      {itemWithUnit && itemWithUnit.ranges?.[0]?.unit && (\n        <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>\n          /{itemWithUnit.ranges[0].unit}\n        </span>\n      )}\n    </span>\n  );\n}\n\n/**\n * Format price value consistently with configurable tilde prefix\n */\nfunction formatPriceValue(price: number | 'Free' | 'Contact', showTildePrefix: boolean = false): string {\n  if (price === 'Free' || price === 'Contact') {\n    return price;\n  }\n  if (price === 0) {\n    return 'Free';\n  }\n  return showTildePrefix ? `~$${price}` : `$${price}`;\n}\n\n/**\n * Legacy component for backward compatibility with string input\n */\nfunction LegacyPricingDisplay({\n  pricing,\n  className = '',\n  styleConfig = PRICING_STYLES.comparison,\n}: {\n  pricing: string;\n  className?: string;\n  styleConfig?: PricingStyleConfig;\n}) {\n  // Handle \"Free\" case\n  if (pricing === 'Free' || pricing === 'No pricing data') {\n    return (\n      <span\n        className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize} ${styleConfig.fontFamily} ${className}`}\n      >\n        {pricing}\n      </span>\n    );\n  }\n\n  // Parse pricing string to separate main price from unit/cycle info\n  const parsePricing = (pricingStr: string) => {\n    // Handle comma-separated format like \"$10/device/month, $120/device/year\"\n    if (pricingStr.includes(', ')) {\n      // Split by comma and parse each part separately\n      const parts = pricingStr.split(', ');\n      const parsedParts = parts.map(part => {\n        const match = part.trim().match(/^(\\$\\d+(?:-\\$\\d+)?|\\d+(?:-\\d+)?|Free)(.*)$/);\n        if (match) {\n          const price = match[1].startsWith('$') ? match[1] : `$${match[1]}`;\n          return {\n            price,\n            suffix: match[2],\n          };\n        }\n        return { price: part.trim(), suffix: '' };\n      });\n\n      // Reconstruct with proper styling\n      return {\n        mainValue: parsedParts.map(p => p.price).join(', '),\n        secondaryInfo: parsedParts.length > 0 && parsedParts[0].suffix ? parsedParts[0].suffix : '',\n      };\n    }\n\n    // Handle pipe-separated format like \"$529/site/year | Free\"\n    if (pricingStr.includes(' | ')) {\n      const parts = pricingStr.split(' | ');\n      const prices = parts.map(part => {\n        const match = part.trim().match(/^(\\$\\d+(?:-\\$\\d+)?|\\d+(?:-\\d+)?|Free)(.*)$/);\n        return match ? match[1] : part.trim();\n      });\n\n      return {\n        mainValue: prices.join(' | '),\n        secondaryInfo: '',\n      };\n    }\n\n    // Handle multi-cycle format like \"$10/$120/device/month/year\"\n    if (pricingStr.includes('/$')) {\n      // Find all price patterns\n      const pricePattern = /\\$\\d+(?:-\\$\\d+)?/g;\n      const matches = [...pricingStr.matchAll(pricePattern)];\n\n      if (matches.length > 0) {\n        const lastMatch = matches[matches.length - 1];\n        const lastPriceEnd = lastMatch.index + lastMatch[0].length;\n\n        const mainValue = pricingStr.substring(0, lastPriceEnd);\n        const secondaryInfo = pricingStr.substring(lastPriceEnd);\n\n        return { mainValue, secondaryInfo };\n      }\n    }\n\n    // Handle single price format like \"$529/site/year\" or \"$0-$529/site/year\"\n    const singlePriceMatch = pricingStr.match(/^(\\$\\d+(?:-\\$\\d+)?|Free)(.*)$/);\n    if (singlePriceMatch) {\n      return {\n        mainValue: singlePriceMatch[1],\n        secondaryInfo: singlePriceMatch[2],\n      };\n    }\n\n    // Fallback - treat entire string as main value\n    return {\n      mainValue: pricingStr,\n      secondaryInfo: '',\n    };\n  };\n\n  const { mainValue, secondaryInfo } = parsePricing(pricing);\n\n  return (\n    <span className={`${styleConfig.fontFamily} ${className}`}>\n      <span className={`${styleConfig.priceTextColor} ${styleConfig.priceTextSize}`}>{mainValue}</span>\n      {secondaryInfo && (\n        <span className={`${styleConfig.secondaryTextColor} ${styleConfig.secondaryTextSize}`}>{secondaryInfo}</span>\n      )}\n    </span>\n  );\n}\n\n/**\n * Utility function to format pricing for display in the PricingDisplay component\n * This can be used to pre-process pricing strings if needed\n */\nexport function formatPricingForDisplay(pricing: string): string {\n  return pricing;\n}\n","'use client';\n\nexport interface ResultsCountProps {\n  currentPage: number;\n  pageSize: number;\n  totalResults: number;\n  resultType: 'vendors' | 'posts';\n  sortingMessage?: string;\n}\n\nexport function ResultsCount({ currentPage, pageSize, totalResults, resultType, sortingMessage }: ResultsCountProps) {\n  if (totalResults === 0) {\n    return null;\n  }\n\n  const startIndex = (currentPage - 1) * pageSize + 1;\n  const endIndex = Math.min(currentPage * pageSize, totalResults);\n  const plural = resultType === 'vendors' ? 'vendors' : 'posts';\n  const singular = resultType === 'vendors' ? 'vendor' : 'post';\n  const displayType = totalResults === 1 ? singular : plural;\n\n  return (\n    <div className=\"mb-6\">\n      <p className=\"text-ods-text-secondary text-h6\">\n        {totalResults > 0 && (\n          <>\n            Showing {startIndex}-{endIndex} of {totalResults} {displayType}\n            {sortingMessage && <span className=\"ml-2 text-ods-accent\">• {sortingMessage}</span>}\n          </>\n        )}\n      </p>\n    </div>\n  );\n}\n","'use client';\n\nimport { Boxes, Database, Hand, Plug, ShieldCheck, Sparkles } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { OpenSourceIcon, CoinsIcon } from './icons-stub';\nimport { OpenFrameLogo } from './openframe-logo';\n\nexport interface VendorTagProps {\n  type:\n    | 'open-source'\n    | 'commercial'\n    | 'free'\n    | 'freemium'\n    | 'paid'\n    | 'enterprise'\n    | 'recommended'\n    | 'classification'\n    | 'ai'\n    | 'manual'\n    | 'openframe_selected'\n    | 'placeholder'\n    | 'api'\n    | 'data'\n    | 'k8s'\n    | 'secured';\n  text?: string;\n  className?: string;\n  size?: 'sm' | 'md';\n  hidden?: boolean;\n  accentColor?: string;\n}\n\nexport function VendorTag({ type, text, className = '', hidden = false, size = 'md', accentColor }: VendorTagProps) {\n  // Base classes for the tag container\n  const baseClasses = cn(\n    'flex items-center gap-1.5 whitespace-nowrap rounded border border-ods-border bg-ods-bg',\n    size === 'sm' ? 'px-2 py-1' : 'px-2.5 py-1.5',\n  );\n\n  // Get display text and styling based on type\n  const getTagContent = () => {\n    switch (type) {\n      case 'placeholder':\n        return {\n          text: 'Placeholder',\n          textColor: 'text-ods-text-primary',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm\">\n              <Sparkles width={10} height={10} className=\"text-ods-text-primary\" />\n            </div>\n          ),\n        };\n      case 'ai':\n        return {\n          text: 'AI Selected',\n          textColor: 'text-ods-text-primary',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm\">\n              <Sparkles width={10} height={10} className=\"text-ods-text-primary\" />\n            </div>\n          ),\n        };\n      case 'manual':\n        return {\n          text: 'Manually Selected',\n          textColor: 'text-ods-text-secondary',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm\">\n              <Hand width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      case 'open-source':\n        return {\n          text: text || 'Open Source',\n          icon: (\n            <div\n              className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\"\n              style={accentColor ? { backgroundColor: accentColor } : undefined}\n            >\n              <OpenSourceIcon width={10} height={10} className=\"text-ods-text-on-accent\" />\n            </div>\n          ),\n        };\n      case 'commercial':\n        return {\n          text: text || 'Commercial Vendor',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n              <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      // platform-capability tags — same neutral icon-box chrome as 'commercial'\n      case 'api':\n        return {\n          text: text || 'API',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n              <Plug width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      case 'data':\n        return {\n          text: text || 'Data',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n              <Database width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      case 'k8s':\n        return {\n          text: text || 'K8s',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n              <Boxes width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      case 'secured':\n        return {\n          text: text || 'Secured',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n              <ShieldCheck width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      case 'free':\n        return {\n          text: text || 'Free',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\">\n              <span className=\"text-[8px] font-bold text-ods-text-on-accent\">$</span>\n            </div>\n          ),\n        };\n      case 'freemium':\n        return {\n          text: text || 'Freemium',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\">\n              <span className=\"text-[8px] font-bold text-ods-text-on-accent\">$</span>\n            </div>\n          ),\n        };\n      case 'paid':\n        return {\n          text: text || 'Paid',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n              <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n            </div>\n          ),\n        };\n      case 'enterprise':\n        return {\n          text: text || 'Enterprise',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\">\n              <span className=\"text-[8px] font-bold text-ods-text-on-accent\">E</span>\n            </div>\n          ),\n        };\n      case 'recommended':\n        return {\n          text: text || 'Recommended',\n          icon: (\n            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\">\n              <span className=\"text-[8px] font-bold text-ods-text-on-accent\">★</span>\n            </div>\n          ),\n        };\n      case 'classification': {\n        // Handle specific classification types based on the text value\n        const classificationType = text?.toLowerCase();\n\n        if (classificationType === 'open_source') {\n          return {\n            text: 'Open Source',\n            icon: (\n              <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\">\n                <OpenSourceIcon width={10} height={10} className=\"text-ods-text-on-accent\" />\n              </div>\n            ),\n          };\n        } else if (classificationType === 'commercial') {\n          return {\n            text: 'Commercial Vendor',\n            icon: (\n              <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n                <CoinsIcon width={10} height={10} className=\"text-ods-text-secondary\" />\n              </div>\n            ),\n          };\n        } else if (classificationType === 'openframe_selected') {\n          return {\n            text: 'OpenFrame Selected',\n            icon: (\n              <OpenFrameLogo\n                lowerPathColor=\"currentColor\"\n                upperPathColor=\"currentColor\"\n                className=\"h-4 w-4 text-ods-accent\"\n              />\n            ),\n          };\n        } else {\n          // Fallback for unknown classification types\n          return {\n            text: text || 'Classification',\n            icon: (\n              <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-accent\">\n                <span className=\"text-[8px] font-bold text-ods-text-on-accent\">C</span>\n              </div>\n            ),\n          };\n        }\n      }\n      case 'openframe_selected':\n        return {\n          text: text || 'OpenFrame Selected',\n          icon: (\n            <OpenFrameLogo\n              lowerPathColor=\"currentColor\"\n              upperPathColor=\"currentColor\"\n              className=\"h-4 w-4 text-ods-accent\"\n            />\n          ),\n        };\n      default:\n        return {\n          text: text || type,\n          icon: null,\n        };\n    }\n  };\n\n  const { text: displayText, icon, textColor } = getTagContent();\n\n  return (\n    <div className={cn(baseClasses, className, hidden && 'invisible')}>\n      {icon}\n      <span className={cn('font-semibold text-h5', textColor ? textColor : 'text-ods-text-primary')}>\n        {displayText}\n      </span>\n    </div>\n  );\n}\n","import type React from 'react';\n\nexport const OpenFrameLogo = ({\n  className,\n  lowerPathColor,\n  upperPathColor,\n  ...props\n}: { className?: string; lowerPathColor?: string; upperPathColor?: string } & React.SVGProps<SVGSVGElement>) => {\n  return (\n    <svg\n      {...props}\n      className={className}\n      width=\"32\"\n      height=\"32\"\n      viewBox=\"0 0 32 32\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      style={\n        {\n          '--fill-0': 'currentColor',\n          ...props.style,\n        } as React.CSSProperties\n      }\n    >\n      <g>\n        <g>\n          {/* White squares - top right, bottom left, bottom right, bottom center */}\n          <path\n            d=\"M21.3333 16.5333C21.3333 16.2388 21.5721 16 21.8667 16H26.1333C26.4279 16 26.6667 16.2388 26.6667 16.5333V20.8C26.6667 21.0946 26.4279 21.3333 26.1333 21.3333H21.8667C21.5721 21.3333 21.3333 21.0946 21.3333 20.8V16.5333Z\"\n            fill={lowerPathColor ?? '#ffffff'}\n          />\n          <path\n            d=\"M16 21.8667C16 21.5721 16.2388 21.3333 16.5333 21.3333H20.8C21.0946 21.3333 21.3333 21.5721 21.3333 21.8667V26.1333C21.3333 26.4279 21.0946 26.6667 20.8 26.6667H16.5333C16.2388 26.6667 16 26.4279 16 26.1333V21.8667Z\"\n            fill={lowerPathColor ?? '#ffffff'}\n          />\n          <path\n            d=\"M26.6667 21.8667C26.6667 21.5721 26.9054 21.3333 27.2 21.3333H31.4667C31.7612 21.3333 32 21.5721 32 21.8667V26.1333C32 26.4279 31.7612 26.6667 31.4667 26.6667H27.2C26.9054 26.6667 26.6667 26.4279 26.6667 26.1333V21.8667Z\"\n            fill={lowerPathColor ?? '#ffffff'}\n          />\n          <path\n            d=\"M21.3333 27.2C21.3333 26.9054 21.5721 26.6667 21.8667 26.6667H26.1333C26.4279 26.6667 26.6667 26.9054 26.6667 27.2V31.4667C26.6667 31.7612 26.4279 32 26.1333 32H21.8667C21.5721 32 21.3333 31.7612 21.3333 31.4667V27.2Z\"\n            fill={lowerPathColor ?? '#ffffff'}\n          />\n        </g>\n        {/* Black frame/border */}\n        <path\n          d=\"M30.9333 0H1.06667C0.477563 0 0 0.477564 0 1.06667V30.9333C0 31.5224 0.477563 32 1.06667 32H14.9333C15.5224 32 16 31.5224 16 30.9333V27.7333C16 27.1442 15.5224 26.6667 14.9333 26.6667H5.86667C5.57211 26.6667 5.33333 26.4279 5.33333 26.1333V5.86667C5.33333 5.57211 5.57211 5.33333 5.86667 5.33333H26.1333C26.4279 5.33333 26.6667 5.57212 26.6667 5.86667V14.9333C26.6667 15.5224 27.1442 16 27.7333 16H30.9333C31.5224 16 32 15.5224 32 14.9333V1.06667C32 0.477563 31.5224 0 30.9333 0Z\"\n          fill={upperPathColor ?? '#1A1A1A'}\n        />\n      </g>\n    </svg>\n  );\n};\n","'use client';\n\nimport { VendorTag } from './vendor-tag';\n\nexport type SelectionSourceType = 'ai' | 'manual' | 'placeholder';\n\ninterface SelectionSourceBadgeProps {\n  source: SelectionSourceType;\n  className?: string;\n  hidden?: boolean;\n}\n\n/**\n * Small pill badge indicating whether a vendor was selected manually or by AI.\n * Colors follow OpenMSP design tokens.\n */\nexport function SelectionSourceBadge({ source, hidden = false }: SelectionSourceBadgeProps) {\n  if (!source) {\n    return null;\n  }\n\n  return (\n    <VendorTag\n      key={`source-${source}`}\n      type={source?.toLowerCase() as 'ai' | 'manual' | 'placeholder'}\n      size=\"sm\"\n      hidden={hidden}\n    />\n  );\n}\n","'use client';\n\nimport { VendorDetailLayoutSkeleton } from './loading';\n\nexport function VendorPageSkeleton() {\n  return <VendorDetailLayoutSkeleton />;\n}\n","'use client';\n\nimport type React from 'react';\nimport { SECTION_HEADING_CLASS } from './layout/page-heading';\n\ninterface WhyItMattersItemProps {\n  number: string;\n  title: string;\n  description: string;\n  isLast?: boolean;\n}\n\nconst WhyItMattersItem: React.FC<WhyItMattersItemProps> = ({ number, title, description, isLast }) => {\n  return (\n    <li\n      className={`flex w-full flex-col items-start gap-6 p-10 transition-colors duration-200 hover:bg-ods-bg-hover md:flex-row ${!isLast ? 'border-b border-ods-border' : ''} `}\n    >\n      <span className=\"tracking-[-0.02em] text-ods-accent text-h2\">{number}</span>\n      <div className=\"flex-1\">\n        <h3 className=\"tracking-[-0.02em] text-ods-text-primary text-h2\">{title}</h3>\n        <p className=\"mt-4 text-ods-text-primary text-h4\">{description}</p>\n      </div>\n    </li>\n  );\n};\n\nconst WhyItMatters = () => {\n  const items = [\n    {\n      number: '1.',\n      title: 'Cut Costs',\n      description: 'Eliminate vendor fees with proven open-source alternatives',\n    },\n    {\n      number: '2.',\n      title: 'Stay in Control',\n      description: 'Full visibility and data ownership',\n    },\n    {\n      number: '3.',\n      title: 'Build What You Need',\n      description: 'Customize without vendor limitations',\n    },\n    {\n      number: '4.',\n      title: 'Scale Freely',\n      description: 'Designed for multi-tenant MSP environments',\n    },\n  ];\n\n  return (\n    <section className=\"bg-ods-bg\">\n      <div className=\"mx-auto w-full max-w-[1920px] px-6 md:px-20\">\n        <h2 className={`${SECTION_HEADING_CLASS} mb-6 text-center`}>Why It Matters</h2>\n        <div className=\"w-full overflow-hidden rounded-3xl border border-ods-border bg-ods-card\">\n          <ol>\n            {items.map((item, index) => (\n              <WhyItMattersItem\n                key={item.number}\n                number={item.number}\n                title={item.title}\n                description={item.description}\n                isLast={index === items.length - 1}\n              />\n            ))}\n          </ol>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default WhyItMatters;\n","'use client';\n\nimport { CheckCircleIcon as LucideCheckCircleIcon, XCircleIcon } from './ui/custom-icons';\n\ninterface YesNoDisplayProps {\n  value: boolean;\n  yesText?: string;\n  noText?: string;\n  customText?: string; // For displaying custom text instead of Yes/No\n  className?: string;\n}\n\nexport function YesNoDisplay({ value, yesText = 'Yes', noText = 'No', customText, className = '' }: YesNoDisplayProps) {\n  // If custom text is provided, display it with appropriate icon\n  if (customText) {\n    return (\n      <div className={`flex items-center gap-2 ${className}`}>\n        <LucideCheckCircleIcon className=\"h-6 w-6\" />\n        <span className=\"text-ods-text-primary text-h6\">{customText}</span>\n      </div>\n    );\n  }\n\n  // Standard Yes/No display\n  if (value) {\n    return (\n      <div className={`flex items-center gap-2 ${className}`}>\n        <LucideCheckCircleIcon className=\"h-6 w-6\" />\n        <span className=\"text-ods-text-primary text-h6\">{yesText}</span>\n      </div>\n    );\n  }\n\n  return (\n    <div className={`flex items-center gap-2 ${className}`}>\n      <XCircleIcon className=\"h-6 w-6\" />\n      <span className=\"text-ods-text-secondary text-h6\">{noText}</span>\n    </div>\n  );\n}\n\n/**\n * Unified logic to determine if a feature value should be considered \"Yes\" or \"No\"\n */\nexport function evaluateFeatureValue(\n  value: string | null | undefined,\n  dataType: 'boolean' | 'text' | 'number',\n): boolean {\n  // Handle truly empty/null values\n  if (value === null || value === undefined || value === '') {\n    return false;\n  }\n\n  const stringValue = String(value).toLowerCase().trim();\n\n  // Handle explicit null/empty string values\n  if (stringValue === 'null' || stringValue === 'n/a' || stringValue === 'none' || stringValue === '-') {\n    return false;\n  }\n\n  // Boolean data type\n  if (dataType === 'boolean') {\n    return (\n      stringValue === 'true' ||\n      stringValue === '1' ||\n      stringValue === 'yes' ||\n      stringValue === '✓' ||\n      stringValue === '✅'\n    );\n  }\n\n  // Text data type\n  if (dataType === 'text') {\n    // Handle emoji values\n    if (stringValue === '✅' || stringValue === '✓') {\n      return true;\n    }\n    if (stringValue === '❌' || stringValue === '✗') {\n      return false;\n    }\n\n    // Handle explicit no values\n    if (stringValue === 'no' || stringValue === 'false' || stringValue === '0') {\n      return false;\n    }\n\n    // Any other non-empty text is considered \"yes\"\n    return stringValue.length > 0;\n  }\n\n  // Number data type\n  if (dataType === 'number') {\n    const numValue = parseFloat(stringValue);\n    return !isNaN(numValue) && numValue > 0;\n  }\n\n  // Default fallback\n  return false;\n}\n","'use client';\n\nimport type React from 'react';\nimport { useState, useEffect } from 'react';\nimport { getBaseUrl } from '../utils';\nimport { FlamingoLogo } from './flamingo-logo';\n\ninterface MadeWithLoveProps {\n  /** Custom class name for the container */\n  className?: string;\n  /** Size variant for the component */\n  size?: 'sm' | 'md' | 'lg';\n  /** Whether to show on mobile (responsive) */\n  showOnMobile?: boolean;\n}\n\nexport function MadeWithLove({ className = '', size = 'md', showOnMobile = true }: MadeWithLoveProps) {\n  const [isMobile, setIsMobile] = useState(false);\n  const [isHovered, setIsHovered] = useState(false);\n\n  // Detect mobile/desktop for responsive behavior\n  useEffect(() => {\n    const checkMobile = () => {\n      setIsMobile(window.innerWidth < 640); // 640px = sm breakpoint\n    };\n\n    checkMobile();\n    window.addEventListener('resize', checkMobile);\n    return () => window.removeEventListener('resize', checkMobile);\n  }, []);\n\n  // Size configuration with pixel values\n  const sizeConfig = {\n    sm: {\n      logoSize: 14,\n      fontSizeMobile: '12px',\n      fontSizeDesktop: '14px',\n      gap: '4px',\n    },\n    md: {\n      logoSize: 16,\n      fontSizeMobile: '14px',\n      fontSizeDesktop: '16px',\n      gap: '4px',\n    },\n    lg: {\n      logoSize: 20,\n      fontSizeMobile: '16px',\n      fontSizeDesktop: '18px',\n      gap: '6px',\n    },\n  };\n\n  const config = sizeConfig[size];\n  const flamingoUrl = getBaseUrl('flamingo');\n\n  // Container styles using primitive CSS\n  const containerStyle: React.CSSProperties = {\n    display: !showOnMobile && isMobile ? 'none' : 'inline-flex',\n    alignItems: 'center',\n    gap: config.gap,\n    fontSize: isMobile ? config.fontSizeMobile : config.fontSizeDesktop,\n    lineHeight: 1.5,\n    color: '#ffffff',\n    fontFamily: 'inherit',\n  };\n\n  // Button/link styles using primitive CSS\n  const linkStyle: React.CSSProperties = {\n    display: 'inline-flex',\n    alignItems: 'center',\n    gap: '2px',\n    padding: 0,\n    margin: 0,\n    background: 'transparent',\n    border: 'none',\n    cursor: 'pointer',\n    textDecoration: isHovered ? 'underline' : 'none',\n    color: '#ffffff',\n    fontSize: '14px',\n    fontFamily: '\"Azeret Mono\", monospace',\n    transition: 'text-decoration 0.2s ease',\n    outline: 'none',\n  };\n\n  // Logo container styles\n  const logoStyle: React.CSSProperties = {\n    marginLeft: '2px',\n    flexShrink: 0,\n    display: 'flex',\n    alignItems: 'center',\n  };\n\n  // Text span styles\n  const textStyle: React.CSSProperties = {\n    color: '#ffffff',\n    marginLeft: '4px',\n    textDecoration: 'none',\n  };\n\n  return (\n    <div style={containerStyle} className={className}>\n      Made with love by\n      <a\n        href={flamingoUrl}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        style={linkStyle}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n      >\n        <div style={logoStyle}>\n          <FlamingoLogo\n            size={config.logoSize}\n            color=\"#f357bb\" // Direct hex instead of CSS variable for cross-framework compatibility\n          />\n        </div>\n        <span style={textStyle}>Flamingo</span>\n      </a>\n    </div>\n  );\n}\n","'use client';\n\nimport DatePicker from 'react-datepicker';\nimport { cn } from '../utils/cn';\nimport { Label } from './ui/label';\n\ninterface DateTimePickerProps {\n  value?: Date;\n  onChange: (date: Date | null) => void;\n  disabled?: boolean;\n  label?: string;\n  placeholder?: string;\n  className?: string;\n  showTimeSelect?: boolean;\n  timeFormat?: string;\n  timeIntervals?: number;\n  dateFormat?: string;\n}\n\n// Shared input styling that matches Input component pattern\nconst inputClassName = cn(\n  'flex h-10 w-full rounded-md border px-3 py-2 text-sm ring-offset-background',\n  'file:border-0 file:bg-transparent file:text-sm file:font-medium',\n  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',\n  'touch-manipulation disabled:cursor-not-allowed disabled:opacity-50',\n  // Admin theme colors\n  'border-ods-border bg-ods-card text-ods-text-primary',\n  'placeholder:text-ods-text-muted focus-visible:ring-ods-accent',\n  'text-base md:text-sm', // Mobile zoom prevention\n);\n\nexport function DateTimePicker({\n  value,\n  onChange,\n  disabled = false,\n  label,\n  placeholder = 'Select date and time',\n  className,\n  showTimeSelect = true,\n  timeFormat = 'HH:mm',\n  timeIntervals = 15,\n  dateFormat = 'MMMM d, yyyy h:mm aa',\n}: DateTimePickerProps) {\n  return (\n    <div className={cn('space-y-2', className)}>\n      {label && <Label className=\"font-semibold text-ods-text-secondary text-h5\">{label}</Label>}\n\n      <div className=\"w-full\">\n        <DatePicker\n          selected={value}\n          onChange={onChange}\n          showTimeSelect={showTimeSelect}\n          timeFormat={timeFormat}\n          timeIntervals={timeIntervals}\n          dateFormat={dateFormat}\n          placeholderText={placeholder}\n          disabled={disabled}\n          wrapperClassName=\"w-full\"\n          className={inputClassName}\n        />\n      </div>\n\n      <style\n        dangerouslySetInnerHTML={{\n          __html: `\n        .react-datepicker-wrapper {\n          width: 100% !important;\n          display: block !important;\n        }\n\n        .react-datepicker__input-container {\n          width: 100% !important;\n        }\n\n        .react-datepicker__input-container input {\n          width: 100% !important;\n        }\n\n        /* Dark theme for calendar popup */\n        .react-datepicker-popper {\n          z-index: 9999 !important;\n        }\n\n        .react-datepicker {\n          background-color: #212121 !important;\n          border: 1px solid #3A3A3A !important;\n          color: #FAFAFA !important;\n          z-index: 9999 !important;\n        }\n\n        .react-datepicker__header {\n          background-color: #2A2A2A !important;\n          border-bottom: 1px solid #3A3A3A !important;\n          color: #FAFAFA !important;\n        }\n\n        .react-datepicker__current-month {\n          color: #FAFAFA !important;\n        }\n\n        .react-datepicker__day-name {\n          color: #888888 !important;\n        }\n\n        .react-datepicker__day {\n          color: #FAFAFA !important;\n        }\n\n        .react-datepicker__day:hover {\n          background-color: #3A3A3A !important;\n        }\n\n        .react-datepicker__day--selected {\n          background-color: #FFC008 !important;\n          color: #000000 !important;\n        }\n\n        .react-datepicker__day--keyboard-selected {\n          background-color: #FFC008 !important;\n          color: #000000 !important;\n        }\n\n        .react-datepicker__time-container {\n          border-left: 1px solid #3A3A3A !important;\n        }\n\n        .react-datepicker__time {\n          background: #212121 !important;\n        }\n\n        .react-datepicker__time-box {\n          width: 85px !important;\n        }\n\n        .react-datepicker-time__header {\n          background-color: #2A2A2A !important;\n          color: #FAFAFA !important;\n        }\n\n        .react-datepicker__time-list-item {\n          color: #FAFAFA !important;\n        }\n\n        .react-datepicker__time-list-item:hover {\n          background-color: #3A3A3A !important;\n        }\n\n        .react-datepicker__time-list-item--selected {\n          background-color: #FFC008 !important;\n          color: #000000 !important;\n        }\n\n        .react-datepicker__navigation {\n          top: 1rem !important;\n        }\n\n        .react-datepicker__navigation--previous {\n          border-right-color: #888888 !important;\n        }\n\n        .react-datepicker__navigation--next {\n          border-left-color: #888888 !important;\n        }\n      `,\n        }}\n      />\n    </div>\n  );\n}\n","'use client';\n\n/**\n * Author detail-page body — identity header (avatar → name → job title →\n * socials), bio, expertise badges, and a `children` slot for the authored-\n * content rail. THE one implementation behind the hub's `/authors/[slug]`\n * page AND embedded author pages (react-embedding-example) — hosts fetch\n * the `AuthorProfile` however they like and drill it in.\n *\n * Embed-readiness contract (same rules as ArticleAuthorByline):\n *   - `Image` renders through the embed shim (plain `<img>` in non-Next\n *     hosts; the real `next/image` once registered at app init).\n *   - Avatar proxying rides the OPTIONAL ambient `ChatRuntime`\n *     (`endpoints.imageProxyUrlPrefix`) — no provider ⇒ raw URL, never\n *     throws. `proxyImageUrl` prop wins over the runtime.\n *   - The authored-content rail is a SLOT (`children`), not a baked-in\n *     fetch: the hub passes its pre-bound `RelatedContentSection` with SSR\n *     `initialItems`; embedders pass the lib section with `apiBaseUrl`.\n *     The view stays data-in, markup-out.\n *\n * Heading semantics are the HOST's: the view renders the name as a styled\n * `<p>` (like the hub profile header) so a hosting page can own its `<h1>`\n * (the hub uses an sr-only h1 for crawlers).\n */\n\nimport type React from 'react';\nimport { useChatRuntime } from '../../contexts/chat-runtime-context';\nimport Image from '../../embed-shims/next-image';\nimport type { AuthorProfile } from '../../types/entity-author';\nimport { cn } from '../../utils/cn';\nimport { formatBioText } from '../../utils/format';\nimport { getProxiedImageUrl } from '../../utils/image-proxy';\nimport { SocialIconRow } from '../social-icon-row';\nimport { StatusBadge } from '../ui/status-badge';\n\nexport interface AuthorDetailViewProps {\n  author: AuthorProfile;\n  /** Host-injected avatar-URL mapper — wins over the ambient runtime. */\n  proxyImageUrl?: (url: string) => string;\n  /** Authored-content rail (or anything else) rendered below the profile. */\n  children?: React.ReactNode;\n  className?: string;\n}\n\nexport function AuthorDetailView({ author, proxyImageUrl, children, className }: AuthorDetailViewProps) {\n  // Optional runtime — null-safe outside a provider (bare embeds render the\n  // raw avatar URL; the hub's app-wide HubRuntimeProvider supplies the proxy).\n  const runtime = useChatRuntime();\n\n  const proxiedAvatar = author.avatarUrl\n    ? proxyImageUrl\n      ? proxyImageUrl(author.avatarUrl)\n      : (getProxiedImageUrl(author.avatarUrl, {\n          proxyPrefix: runtime?.endpoints.imageProxyUrlPrefix,\n          skipDomains: runtime?.endpoints.imageProxySkipDomains,\n          directHttps: true,\n        }) ?? author.avatarUrl)\n    : null;\n\n  const subtitle = [author.jobTitle, author.company].filter(Boolean).join(' @ ');\n  const bioText = formatBioText(author.about);\n\n  return (\n    <div className={cn('flex flex-col gap-5 md:gap-6', className)}>\n      {/* Identity header: avatar left; name → subtitle → socials stacked right. */}\n      <div className=\"flex w-full items-start gap-6\">\n        <div className=\"relative h-24 w-24 shrink-0\">\n          <div className=\"relative h-full w-full overflow-hidden rounded-full border border-ods-border bg-ods-bg-surface\">\n            {proxiedAvatar ? (\n              <Image src={proxiedAvatar} alt={author.fullName} fill className=\"object-cover\" unoptimized />\n            ) : (\n              <div className=\"flex h-full w-full items-center justify-center font-heading text-3xl text-ods-text-secondary\">\n                {author.fullName.charAt(0).toUpperCase()}\n              </div>\n            )}\n          </div>\n        </div>\n\n        <div className=\"flex min-w-0 flex-col justify-center gap-2\">\n          <p className=\"truncate leading-none text-ods-text-primary text-h2\">{author.fullName}</p>\n          {subtitle && <p className=\"truncate font-body text-lg leading-none text-ods-text-secondary\">{subtitle}</p>}\n          {author.socialLinks.length > 0 && (\n            // Ghost compact row (transparent 32px) — metadata, not CTAs.\n            <SocialIconRow\n              compact\n              links={author.socialLinks.map(link => ({\n                platform: link.platform,\n                href: link.url,\n                label: `${author.fullName}'s ${link.platform}`,\n              }))}\n            />\n          )}\n        </div>\n      </div>\n\n      {bioText && <p className=\"text-ods-text-secondary text-h6\">{bioText}</p>}\n\n      {author.knowsAbout.length > 0 && (\n        <div className=\"flex flex-wrap gap-2\">\n          {author.knowsAbout.map(topic => (\n            <StatusBadge key={topic} text={topic} variant=\"button\" colorScheme=\"cyan\" singleLine />\n          ))}\n        </div>\n      )}\n\n      {children}\n    </div>\n  );\n}\n"]}