{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-N3G3YG3V.cjs","../src/components/providers/dynamic-theme-provider.tsx","../src/hooks/use-dynamic-theming.ts","../src/components/providers/theme-provider.tsx","../src/components/features/array-entry-manager.tsx","../src/components/ui/allowed-domains-input.tsx","../src/components/ui/announcement-bar-view.tsx","../src/components/ui/autocomplete.tsx","../src/components/ui/hidden-tags-popup.tsx","../src/components/ui/truncate-text.tsx","../src/components/ui/checkbox-block.tsx","../src/components/ui/checkbox-with-description.tsx","../src/components/ui/date-picker.tsx","../src/components/ui/select.tsx","../src/components/ui/duration-input.tsx","../src/components/ui/field.tsx","../src/components/ui/info-hint.tsx","../src/components/ui/honeypot-field.tsx","../src/components/ui/json-detail-list.tsx","../src/components/ui/info-card.tsx","../src/components/ui/progress-bar.tsx","../src/components/ui/info-row.tsx","../src/components/ui/assignee-dropdown.tsx","../src/components/ui/deleted-user-avatar.tsx","../src/components/ui/searchable-select.tsx","../src/components/ui/info-section.tsx","../src/components/ui/input-trigger.tsx","../src/components/ui/media-type-selector.tsx","../src/components/ui/no-data/no-data.tsx","../src/components/ui/no-data/no-data-actions.tsx","../src/components/ui/no-data/no-data-styles.ts","../src/components/ui/no-data/no-data-message.tsx","../src/components/ui/page-loader.tsx","../src/components/ui/radio-group.tsx","../src/components/ui/switch.tsx","../src/components/ui/tag-select-dropdown.tsx","../src/components/ui/tags-input.tsx","../src/components/ui/badge.tsx","../src/components/ui/tags-manager.tsx","../src/components/ui/textarea.tsx","../src/components/ui/alert-dialog.tsx","../src/components/ui/aspect-ratio.tsx","../src/components/ui/dialog.tsx","../src/components/ui/modal-guarded-close.tsx","../src/components/ui/admin-form-modal.tsx","../src/utils/hubspot-collected-forms.ts","../src/components/ui/separator.tsx","../src/components/ui/sheet.tsx","../src/components/ui/accordion.tsx","../src/components/ui/breadcrumb.tsx","../src/components/ui/menubar.tsx","../src/components/ui/navigation-menu.tsx","../src/components/ui/crossfade-panels.tsx","../src/components/ui/tab-content.tsx","../src/components/ui/tab-navigation.tsx","../src/components/ui/alert.tsx","../src/components/ui/ai-generated-badge.tsx","../src/components/ui/fade-preview.tsx","../src/components/ui/progress.tsx","../src/components/ui/release-changelog-section.tsx","../src/components/ui/markdown/rich/rich-markdown-renderer.tsx","../src/components/embeds/rich-markdown-runtime.tsx","../src/components/embeds/claude-embed.tsx","../src/components/embeds/embed-viewer-frame.tsx","../src/utils/fullscreen-switch.ts","../src/components/embeds/embed-iframe.tsx","../src/components/embeds/figma-embed.tsx","../src/components/embeds/linkedin-embed-client.tsx","../src/components/embeds/embed-container.tsx","../src/components/embeds/markdown-image.tsx","../src/components/embeds/og-link-preview.tsx","../src/components/embeds/reddit-embed-client.tsx","../src/utils/social-embed-cache.ts","../src/components/media-carousel.tsx","../src/components/embeds/twitter-embed-client.tsx","../src/components/ui/markdown/rich/embed-overrides.tsx","../src/utils/social-platforms.ts","../src/components/ui/markdown/rich/shortcodes.ts","../src/components/ui/stacked-rows-panel.tsx","../src/components/ui/slack-channel-chip.tsx","../src/components/ui/status-indicator.tsx","../src/components/layout/back-button.tsx","../src/components/layout/list-page-layout.tsx","../src/components/ui/error-state.tsx","../src/components/ui/filter-modal.tsx","../src/components/ui/filter-checkbox-item.tsx","../src/components/ui/sort-column-item.tsx","../src/components/ui/tag-key-value-filter.tsx","../src/components/ui/page-actions.tsx","../src/components/layout/page-container.tsx","../src/components/ui/entity-image.tsx","../src/components/layout/title-block.tsx","../src/components/layout/page-layout.tsx","../src/components/layout/article-detail-layout.tsx","../src/components/toggle-group.tsx","../src/components/ui/toggle.tsx","../src/components/ui/benefit-card.tsx","../src/utils/seo-title.ts","../src/utils/seo-description.ts","../src/utils/brand-marks.ts","../src/utils/os-platforms.ts","../src/utils/validation-utils.ts","../src/utils/confidence-helpers.ts","../src/utils/dev-sections/dev-section-param-keys.ts","../src/utils/tool-utils.ts","../src/types/shell.types.ts","../src/utils/shell-utils.ts","../src/utils/video-bite-id.ts","../src/types/os.types.ts","../src/utils/os-utils.ts","../src/utils/country-phone-utils.ts","../src/utils/generic-domain-utils.ts","../src/utils/dismiss-cookie.ts","../src/utils/announcement-storage.ts","../src/utils/dismissal-storage.ts","../src/utils/walkthrough-deep-link.ts","../src/utils/sse-subscription.ts","../src/utils/sse-decision-frame.ts","../src/utils/dev-sections/openframe-dev-sections.ts","../src/types/announcement.ts","../src/types/product-release.ts","../src/types/delivery.ts","../src/types/tmcg.ts","../src/utils/content-href.ts","../src/utils/doc-tree-nav.ts","../src/utils/doc-path-utils.ts","../src/utils/tree-builder.ts","../src/utils/markdown-section-extractor.ts","../src/utils/hubspot-meetings-convention.ts","../src/utils/page-header-constants.ts","../src/utils/first-touch-attribution.ts","../src/utils/ods-spacing.ts","../src/utils/csv.ts","../src/components/ui/brand-association-card.tsx","../src/components/ui/brand-association-grid.tsx","../src/components/ui/bullet-list.tsx","../src/components/ui/chevron-button.tsx","../src/components/ui/circular-progress.tsx","../src/components/ui/custom-icons.tsx","../src/components/ui/dashboard-info-card.tsx","../src/components/ui/info-card-row.tsx","../src/components/features/os-type-badge.tsx","../src/components/ui/device-card.tsx","../src/components/ui/device-card-compact.tsx","../src/components/ui/feature-card.tsx","../src/components/ui/feature-list.tsx","../src/components/ui/source-tooltip.tsx","../src/components/ui/highlight-card.tsx","../src/components/ui/icons-block.tsx","../src/components/ui/dropdown-button.tsx","../src/components/ui/more-actions-menu.tsx","../src/components/ui/organization-card.tsx","../src/components/ui/service-card.tsx","../src/components/ui/slider.tsx","../src/components/ui/title-content-block.tsx","../src/components/ui/touch-friendly-tooltip.tsx","../src/components/ui/content-loader.tsx","../src/components/ui/table/table.tsx","../src/components/ui/cursor-pagination.tsx","../src/components/ui/data-table/data-table.tsx","../src/components/ui/data-table/utils.ts","../src/components/ui/data-table/data-table-skeleton.tsx","../src/components/ui/table/table-empty-state.tsx","../src/components/ui/table/table-column-filter-dropdown.tsx","../src/components/ui/table/utils.ts","../src/components/ui/table/table-header.tsx","../src/components/ui/table/table-cell.tsx","../src/components/ui/table/table-skeleton.tsx","../src/components/ui/table/table-row.tsx","../src/components/ui/table/use-table-motion.ts","../src/components/ui/table/table-description-cell.tsx","../src/components/ui/table/table-timestamp-cell.tsx","../src/components/ui/query-report-table/query-report-table.tsx","../src/components/ui/query-report-table/query-report-table-header.tsx","../src/components/ui/query-report-table/query-report-table-row.tsx","../src/components/ui/query-report-table/query-report-table-skeleton.tsx","../src/components/ui/query-report-table/utils.ts","../src/components/ui/data-table/data-table-empty.tsx","../src/components/ui/data-table/data-table-row.tsx","../src/components/ui/data-table/data-table-body.tsx","../src/components/ui/data-table/data-table-cursor-footer.tsx","../src/components/ui/data-table/data-table-header.tsx","../src/components/ui/data-table/data-table-column-filter.tsx","../src/components/ui/data-table/data-table-infinite-footer.tsx","../src/components/ui/data-table/data-table-row-count.tsx","../src/components/ui/data-table/use-data-table.ts","../src/components/ui/data-table/index.ts","../src/components/ui/password-input.tsx","../src/components/ui/phone-input.tsx","../src/components/ui/search-input.tsx","../src/components/ui/filter-list.tsx","../src/components/ui/tag-search-input.tsx","../src/components/ui/markdown-editor.tsx","../src/components/ui/file-upload.tsx","../src/components/ui/image-uploader.tsx","../src/components/ui/ticket-status-config-row.tsx","../src/components/ui/color-preset-select.tsx","../src/components/popover.tsx","../src/components/ui/test-run.tsx","../src/components/ui/ticket-attachments-list.tsx","../src/components/ui/ticket-detail-section.tsx","../src/components/ui/ticket-notes-section.tsx","../src/components/ui/ticket-note-card.tsx","../src/components/ui/ticket-info-section.tsx","../src/components/ui/take-over-ticket-modal.tsx","../src/components/ui/reopen-ticket-modal.tsx","../src/components/ui/reopen-ticket-request-modal.tsx","../src/components/ui/filter-pill-row.tsx","../src/components/ui/lazy-mount.tsx","../src/components/ui/icon-tile.tsx","../src/components/ui/policy-link.tsx","../src/components/features/auth/account-details-fields.tsx","../src/components/features/auth/auth-benefits-panel.tsx","../src/components/features/auth/auth-branding.tsx","../src/components/features/auth/auth-shell.tsx","../src/components/features/auth/back-to-login-link.tsx","../src/components/features/auth/labeled-divider.tsx","../src/components/features/auth/sso-providers.tsx","../src/components/features/auth/complete-account-form.tsx","../src/components/features/auth/terms-agreement-label.tsx","../src/components/features/auth/create-organization-form.tsx","../src/components/features/auth/invite-link-invalid-modal.tsx","../src/components/features/auth/legal-links.tsx","../src/components/features/auth/login-form.tsx","../src/components/features/auth/openframe-sso-login-form.tsx","../src/components/features/auth/openframe-sso-signup-form.tsx","../src/components/features/auth/password-reset-form.tsx","../src/components/features/auth/sso-auth-shell.tsx","../src/components/features/auth/sso-join-form.tsx","../src/components/features/provider-button.tsx","../src/components/features/auth-providers-list.tsx","../src/components/features/changelog-manager.tsx","../src/components/features/changelog-sections-manager.tsx","../src/components/features/clickup-tasks-manager.tsx","../src/components/features/command-box.tsx","../src/components/features/entity-tag-badges.tsx","../src/components/features/error-boundary.tsx","../src/components/features/figma-prototype-viewer.tsx","../src/components/features/section-selector.tsx","../src/components/features/filters-dropdown.tsx","../src/components/features/github-releases-manager.tsx","../src/components/features/knowledge-base-links-manager.tsx","../src/components/features/loading-provider.tsx","../src/components/features/media-gallery-manager.tsx","../src/components/features/more-about-button.tsx","../src/components/features/os-type-badge-group.tsx","../src/components/features/parallax-image-showcase.tsx","../src/components/features/paths-display.tsx","../src/components/features/platform-filter.tsx","../src/components/features/push-button-selector.tsx","../src/components/loading/unified-skeleton.tsx","../src/components/loading/card-skeleton.tsx","../src/components/loading/page-layout-skeleton.tsx","../src/components/content-loading-container.tsx","../src/components/loading/content-skeleton.tsx","../src/components/profile/ProfileLoadingSkeleton.tsx","../src/components/loading/msp-profile-form-skeleton.tsx","../src/components/loading/category-card-skeleton.tsx","../src/components/loading/category-vendor-selector-skeleton.tsx","../src/components/loading/wizard-layout-skeleton.tsx","../src/components/loading/margin-report-skeleton.tsx","../src/components/loading/users-grid-skeleton.tsx","../src/components/dynamic-skeleton.tsx","../src/hooks/use-theme-aware.ts","../src/components/loading/organization-icon-skeleton.tsx","../src/components/loading/organization-card-skeleton.tsx","../src/components/loading/device-card-skeleton.tsx","../src/components/features/release-media-manager.tsx","../src/components/features/select-button.tsx","../src/components/features/seo-editor-preview.tsx","../src/components/features/ai-enrich/ConfidenceBadge.tsx","../src/components/features/social-links-manager.tsx","../src/components/social-icon-row.tsx","../src/components/features/start-with-openframe-button.tsx","../src/components/features/status-filter-component.tsx","../src/components/features/tags-selector.tsx","../src/components/features/warning-block.tsx","../src/components/features/video-bites-display.tsx","../src/components/features/walkthrough-video.tsx","../src/components/features/use-walkthrough-video.ts","../src/components/features/video-source-selector.tsx","../src/components/features/subtitles-editor.tsx","../src/components/features/transcript-summary-editor.tsx","../src/components/features/highlight-video-section.tsx","../src/components/features/ai-enrich/AIEnrichSection.tsx","../src/components/features/ai-enrich/AIEnrichButton.tsx","../src/components/features/ai-enrich/AIWarningsSection.tsx","../src/components/features/highlight-config-section.tsx","../src/components/features/entity-summary-editor.tsx","../src/components/features/transcribe-summarize-section.tsx","../src/components/features/ai-enrich/AIStatusIndicator.tsx","../src/components/features/ai-enrich/AIRequiredBadge.tsx","../src/components/features/video-clips-section.tsx","../src/components/features/highlight-generation-section.tsx","../src/components/features/highlight-video-preview.tsx","../src/components/features/transcribe-and-summarize-combined-section.tsx","../src/components/features/highlight-video-combined-section.tsx","../src/components/features/view-toggle.tsx","../src/components/features/policy-configuration-panel.tsx","../src/components/features/waitlist-form.tsx","../src/components/features/notifications/notifications-context.tsx","../src/components/features/notifications/notification-drawer.tsx","../src/components/navigation/app-layout-context.ts","../src/components/navigation/app-layout-drawer.tsx","../src/components/features/notifications/notification-tile.tsx","../src/components/features/notifications/notification-popups.tsx","../src/components/features/notifications/approval-request-notification-tile.tsx","../src/components/features/notifications/types.ts","../src/components/features/time-tracker/time-tracker-context.tsx","../src/components/features/time-tracker/time-tracker-panel.tsx","../src/components/features/time-tracker/use-tracker-clock.ts","../src/components/features/time-tracker/time-tracker-header-button.tsx","../src/components/navigation/unread-dot.tsx","../src/components/navigation/header-button.tsx","../src/components/features/board/board.tsx","../src/utils/auto-scroll-ancestors.ts","../src/utils/scroll-parent.ts","../src/components/features/board/aim.ts","../src/components/features/board/board-column.tsx","../src/components/features/board/board-column-header.tsx","../src/components/features/board/color-utils.ts","../src/components/features/board/drop-aim.ts","../src/components/features/board/use-lane-scroll-anchor.ts","../src/components/features/board/lane-geometry.ts","../src/components/features/board/ticket-card.tsx","../src/utils/drag-effect.ts","../src/components/features/board/board-ticket-approval.tsx","../src/components/features/board/pending-move.ts","../src/components/features/board/ticket-card-skeleton.tsx","../src/components/features/board/resolve-drop.ts","../src/components/features/board/use-board-collapse.ts","../src/components/features/board/touch-board.tsx","../src/components/features/board/types.ts","../src/components/features/sortable-list/slot-geometry.ts","../src/components/features/sortable-list/sortable-list.tsx","../src/components/features/sortable-list/sortable-move-buttons.tsx","../src/components/features/sortable-list/use-sortable-item.ts","../src/components/features/ticket-status-config-list/ticket-status-config-list.tsx"],"names":["jsx","useState","jsxs","forwardRef","useMemo","useEffect","cva","useCallback","useId","ChevronDown","useRef","useContext","ExternalLink","formatTime","Fragment","backgroundClasses","createContext","size","Info","spacingMap","ChevronRight","getHideClasses","flexRender","getCoreRowModel","getFilteredRowModel","getSortedRowModel","EyeIcon","innerInputStyles","REASON_MAX_LENGTH","Loader2","ImageIcon","Upload","VideoIcon","dropTargetForElements","memo","combine","attachClosestEdge"],"mappings":"AAAA,+iCAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;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;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACF,wDAA6B;AAC7B;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;AACF,wDAA6B;AAC7B;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;AACF,wDAA6B;AAC7B;AACE;AACA;AACF,wDAA6B;AAC7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AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/chunk-N3G3YG3V.cjs","sourcesContent":[null,"'use client';\n\nimport type { ReactNode } from 'react';\nimport { createContext, useContext } from 'react';\nimport { useDynamicTheming, type ThemeConfig } from '../../hooks/use-dynamic-theming';\n\ninterface DynamicThemeContextType {\n  theme: ThemeConfig;\n  isDark: boolean;\n  updateTheme: (theme: Partial<ThemeConfig>) => void;\n  toggleDark: () => void;\n}\n\nconst DynamicThemeContext = createContext<DynamicThemeContextType | null>(null);\n\nexport function DynamicThemeProvider({ children }: { children: ReactNode }) {\n  const themeState = useDynamicTheming();\n\n  return <DynamicThemeContext.Provider value={themeState}>{children}</DynamicThemeContext.Provider>;\n}\n\nexport function useDynamicTheme() {\n  const context = useContext(DynamicThemeContext);\n  if (!context) {\n    throw new Error('useDynamicTheme must be used within a DynamicThemeProvider');\n  }\n  return context;\n}\n","import { useState, useEffect } from 'react';\n\nexport interface ThemeConfig {\n  primaryColor: string;\n  secondaryColor: string;\n  accentColor: string;\n  backgroundColor: string;\n  textColor: string;\n}\n\nconst defaultTheme: ThemeConfig = {\n  primaryColor: '#FFC008',\n  secondaryColor: '#161616',\n  accentColor: '#E5E5E5',\n  backgroundColor: '#FAFAFA',\n  textColor: '#161616',\n};\n\nexport function useDynamicTheming() {\n  const [theme, setTheme] = useState<ThemeConfig>(defaultTheme);\n  const [isDark, setIsDark] = useState(false);\n\n  useEffect(() => {\n    // Check for system preference. The initial read and the change listener are\n    // the same operation against the same external source, so they share one\n    // callback — `matchMedia` is not readable during SSR, which is why this\n    // cannot be a lazy `useState` initialiser.\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n    const syncFromMediaQuery = () => setIsDark(mediaQuery.matches);\n    syncFromMediaQuery();\n\n    mediaQuery.addEventListener('change', syncFromMediaQuery);\n    return () => mediaQuery.removeEventListener('change', syncFromMediaQuery);\n  }, []);\n\n  const updateTheme = (newTheme: Partial<ThemeConfig>) => {\n    setTheme(prev => ({ ...prev, ...newTheme }));\n  };\n\n  const toggleDark = () => {\n    setIsDark(prev => !prev);\n  };\n\n  return {\n    theme,\n    isDark,\n    updateTheme,\n    toggleDark,\n  };\n}\n","'use client';\n\nimport { ThemeProvider as NextThemesProvider, useTheme as useNextTheme } from 'next-themes';\nimport type { ThemeProviderProps as NextThemesProviderProps } from 'next-themes/dist/types';\nimport { useCallback } from 'react';\nimport { useIsHydrated } from '../../hooks/ui/use-is-hydrated';\n\n/**\n * ODS theme system — thin wrapper over `next-themes`.\n *\n * We deliberately do NOT hand-roll a provider/no-flash script: `next-themes`\n * is already a dependency, is battle-tested in Next.js (App & Pages router),\n * injects its own pre-paint anti-flash script, handles SSR + localStorage +\n * cross-tab sync, and also works in plain React (Vite/Tauri).\n *\n * Product model (locked): a MANUAL light/dark switch, default DARK,\n * persisted to localStorage. No \"system\" mode (`enableSystem={false}`).\n *\n * Drives styling by setting `data-theme=\"light|dark\"` on <html>;\n * `src/styles/ods-colors.css` swaps the `--ods-*` primitives accordingly.\n *\n * Public API exposed by this module:\n *   • `<ThemeProvider>` — preconfigured next-themes provider.\n *   • `useTheme()`      — raw next-themes hook (advanced cases).\n *   • `useThemeToggle()`— headless convenience hook for building toggle UI\n *                         in consumer apps (no styled component on purpose;\n *                         apps own their button visuals via the lib's\n *                         existing `<Button>`).\n */\n\nexport type Theme = 'light' | 'dark';\n\nexport const THEME_STORAGE_KEY = 'ods-theme';\nexport const THEME_ATTRIBUTE = 'data-theme';\nexport const DEFAULT_THEME: Theme = 'dark';\n\nexport type ThemeProviderProps = Partial<NextThemesProviderProps>;\n\n/**\n * Pre-configured provider. Wrap the app once (Next.js: in the root layout;\n * apps must keep `suppressHydrationWarning` on <html> — already the case).\n * No `<ThemeScript>` needed — next-themes handles the no-flash script.\n *\n * All next-themes props are overridable, but the ODS defaults below encode\n * the product decision.\n */\nexport function ThemeProvider({ children, ...overrides }: ThemeProviderProps) {\n  return (\n    <NextThemesProvider\n      attribute={THEME_ATTRIBUTE}\n      defaultTheme={DEFAULT_THEME}\n      enableSystem={false}\n      themes={['light', 'dark']}\n      storageKey={THEME_STORAGE_KEY}\n      disableTransitionOnChange={false}\n      {...overrides}\n    >\n      {children}\n    </NextThemesProvider>\n  );\n}\n\n/**\n * Re-export of next-themes' `useTheme` (no custom logic on top).\n * Returns `{ theme, setTheme, resolvedTheme, themes, ... }`.\n */\nexport const useTheme = useNextTheme;\n\n/* ------------------------------------------------------------------ */\n/* useThemeToggle — headless convenience for building toggle UIs      */\n/* ------------------------------------------------------------------ */\n\nexport interface UseThemeToggleResult {\n  /** Becomes `true` after the client has hydrated and resolved the stored\n   *  preference. Until then, `theme`/`isDark`/`isLight` reflect the SSR\n   *  default (`DEFAULT_THEME`) — handy for rendering a stable placeholder. */\n  mounted: boolean;\n  /** Resolved active theme (`\"dark\"` until mounted, then real value). */\n  theme: Theme;\n  isDark: boolean;\n  isLight: boolean;\n  /** Flip dark↔light and persist. */\n  toggle: () => void;\n  /** Set explicitly to `\"light\"` or `\"dark\"` and persist. */\n  setTheme: (theme: Theme) => void;\n}\n\n/**\n * Headless toggle helper. Build any button you like:\n *\n *   const { isDark, toggle, mounted } = useThemeToggle()\n *   <Button size=\"icon\" variant=\"transparent\" onClick={toggle} aria-label=\"…\">\n *     {mounted && (isDark ? <Sun01Icon /> : <MoonIcon />)}\n *   </Button>\n *\n * The mount gate avoids hydration mismatch (next-themes only knows the\n * persisted preference on the client after mount).\n */\nexport function useThemeToggle(): UseThemeToggleResult {\n  const { resolvedTheme, theme, setTheme } = useTheme();\n  const mounted = useIsHydrated();\n\n  const active: Theme = mounted ? (resolvedTheme === 'light' || theme === 'light' ? 'light' : 'dark') : DEFAULT_THEME;\n\n  const setOdsTheme = useCallback((next: Theme) => setTheme(next), [setTheme]);\n\n  const toggle = useCallback(() => setTheme(active === 'dark' ? 'light' : 'dark'), [active, setTheme]);\n\n  return {\n    mounted,\n    theme: active,\n    isDark: active === 'dark',\n    isLight: active === 'light',\n    toggle,\n    setTheme: setOdsTheme,\n  };\n}\n","'use client';\n\nimport { Trash2, Plus, Save, Loader2 } from 'lucide-react';\nimport { useState, useEffect, type ReactNode, type ClipboardEvent } from 'react';\nimport { Button, Input, Label } from '../ui';\n\ninterface ArrayEntryManagerProps<T extends object> {\n  title: ReactNode; // Support string or ReactNode for badge integration\n  items: T[];\n  /**\n   * Called with the committed items. May be async: in `requireSave` mode the\n   * save button awaits it before clearing the dirty flag, so a consumer that\n   * persists over the network keeps the unsaved-changes indicator lit until the\n   * write settles. Declaring plain `void` here made that `await` a no-op on\n   * paper while real callers were already passing async handlers.\n   */\n  onChange: (items: T[]) => void | Promise<void>;\n  fieldKey: keyof T; // The key to edit (e.g., 'github_release_url', 'kb_article_path')\n  placeholder: string;\n  emptyMessage: string;\n  addButtonText: string;\n  saveButtonText?: string; // Text for save button when requireSave=true\n  icon?: ReactNode;\n  className?: string;\n  requireSave?: boolean; // If true, show \"Save\" button and only call onChange when clicked\n  onDirtyChange?: (isDirty: boolean) => void; // Callback when dirty state changes\n  renderLabel?: (item: T, index: number) => ReactNode; // Custom label/badge renderer for each entry\n  /**\n   * An optional SECOND field on each entry, edited above the main one — a\n   * human name for the thing the main field addresses.\n   *\n   * Design docs need it because a link's own title is the only name available:\n   * claude.ai serves the same `og:title` (\"Claude Artifact\") for every artifact\n   * and its per-artifact metadata endpoint is unreachable server-side, so an\n   * unnamed link can only ever render as its type. Omitted by every other\n   * caller, which keeps their single-field row exactly as it was.\n   */\n  titleFieldKey?: keyof T;\n  titlePlaceholder?: string;\n  isSaving?: boolean; // Loading state for save button\n}\n\nexport function ArrayEntryManager<T extends object>({\n  title,\n  items,\n  onChange,\n  fieldKey,\n  placeholder,\n  emptyMessage,\n  addButtonText,\n  saveButtonText = 'Save Changes',\n  icon,\n  className = '',\n  requireSave = false,\n  onDirtyChange,\n  renderLabel,\n  titleFieldKey,\n  titlePlaceholder,\n  isSaving = false,\n}: ArrayEntryManagerProps<T>) {\n  // Local state for draft changes (when requireSave=true)\n  const [draftItems, setDraftItems] = useState<T[]>(items);\n  const [isDirty, setIsDirty] = useState(false);\n\n  // Sync draft with props when items change from parent (but NOT when editing or\n  // saving). Adjusted while rendering — React's documented prop-sync pattern —\n  // rather than from an effect: the rows are drawn from `draftItems`, so a\n  // parent refresh committed one frame of the OLD list before replacing it.\n  const [syncedFrom, setSyncedFrom] = useState({ items, isDirty, isSaving });\n  if (syncedFrom.items !== items || syncedFrom.isDirty !== isDirty || syncedFrom.isSaving !== isSaving) {\n    setSyncedFrom({ items, isDirty, isSaving });\n    if (!isDirty && !isSaving) {\n      setDraftItems(items);\n    }\n  }\n\n  // Notify parent when dirty state changes\n  useEffect(() => {\n    if (onDirtyChange) {\n      onDirtyChange(isDirty);\n    }\n  }, [isDirty, onDirtyChange]);\n\n  // `onChange` may be async (see the prop docs). Only the save button awaits it;\n  // every other call site commits optimistically, so route them through here to\n  // surface a rejection rather than leave it unhandled.\n  const commit = (next: T[]) => {\n    void Promise.resolve(onChange(next)).catch((err: unknown) => {\n      console.error('[ArrayEntryManager] onChange failed:', err);\n    });\n  };\n\n  const workingItems = requireSave ? draftItems : items;\n  const setWorkingItems = requireSave\n    ? (newItems: T[]) => {\n        setDraftItems(newItems);\n        setIsDirty(true);\n      }\n    : commit;\n\n  const addItem = () => {\n    const newItem = { [fieldKey]: '' } as T;\n    setWorkingItems([newItem, ...workingItems]); // Add at top for better UX\n  };\n\n  const removeItem = (index: number) => {\n    setWorkingItems(workingItems.filter((_, i) => i !== index));\n  };\n\n  const updateItem = (index: number, value: string, key: keyof T = fieldKey) => {\n    const updated = [...workingItems];\n    updated[index] = { ...updated[index], [key]: value };\n    setWorkingItems(updated);\n  };\n\n  const handleSave = async () => {\n    await onChange(draftItems);\n    setIsDirty(false); // Reset after save completes\n  };\n\n  // Handle paste of multiple IDs separated by newlines\n  const handlePaste = (index: number, e: ClipboardEvent<HTMLInputElement>) => {\n    const pastedText = e.clipboardData.getData('text');\n\n    // Split by newlines (handles \\n, \\r\\n, \\r)\n    const lines = pastedText\n      .split(/[\\r\\n]+/)\n      .map(line => line.trim())\n      .filter(line => line.length > 0);\n\n    // If only one line, let default paste behavior handle it\n    if (lines.length <= 1) {\n      return;\n    }\n\n    // Prevent default paste for multi-line\n    e.preventDefault();\n\n    const currentItem = items[index];\n    const currentValue = (currentItem[fieldKey] as string) || '';\n\n    // Build new items array\n    const newItems = [...items];\n\n    if (currentValue.trim() === '') {\n      // If current field is empty, use first pasted value for it\n      newItems[index] = { ...newItems[index], [fieldKey]: lines[0] };\n\n      // Add remaining lines as new items after current index\n      const additionalItems = lines.slice(1).map(line => ({ [fieldKey]: line }) as T);\n      newItems.splice(index + 1, 0, ...additionalItems);\n    } else {\n      // If current field has value, add all pasted lines as new items after current\n      const additionalItems = lines.map(line => ({ [fieldKey]: line }) as T);\n      newItems.splice(index + 1, 0, ...additionalItems);\n    }\n\n    commit(newItems);\n  };\n\n  return (\n    <div className={`space-y-3 ${className}`}>\n      <div className=\"flex items-center justify-between\">\n        <Label>{title}</Label>\n        <div className=\"flex items-center gap-2\">\n          {requireSave && isDirty && (\n            <Button\n              type=\"button\"\n              size=\"small-legacy\"\n              onClick={handleSave}\n              disabled={isSaving}\n              leftIcon={isSaving ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Save className=\"h-4 w-4\" />}\n              className=\"bg-ods-accent text-ods-text-on-accent text-h6 hover:bg-ods-accent-hover\"\n            >\n              {isSaving ? 'Saving...' : saveButtonText}\n            </Button>\n          )}\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"small-legacy\"\n            onClick={addItem}\n            leftIcon={<Plus className=\"h-4 w-4\" />}\n            className=\"text-h6\"\n          >\n            {addButtonText}\n          </Button>\n        </div>\n      </div>\n\n      {workingItems.map((item, index) => (\n        <div key={index} className=\"flex items-center gap-3 rounded-lg border border-ods-border bg-ods-bg-surface p-3\">\n          {icon && <div className=\"flex h-8 w-8 items-center justify-center\">{icon}</div>}\n\n          {/* min-w-0: a wide renderLabel (e.g. a DeliveryRow) must truncate, not push the row past its host. */}\n          <div className=\"min-w-0 flex-1 space-y-2\">\n            {renderLabel && renderLabel(item, index)}\n            {titleFieldKey ? (\n              <Input\n                placeholder={titlePlaceholder ?? 'Name'}\n                value={(item[titleFieldKey] as string) ?? ''}\n                onChange={e => updateItem(index, e.target.value, titleFieldKey)}\n                onKeyDown={e => e.key === 'Enter' && e.preventDefault()}\n                className=\"border-ods-border bg-ods-bg text-ods-text-primary\"\n              />\n            ) : null}\n            <Input\n              placeholder={placeholder}\n              value={item[fieldKey] as string}\n              onChange={e => updateItem(index, e.target.value)}\n              onKeyDown={e => e.key === 'Enter' && e.preventDefault()}\n              onPaste={e => handlePaste(index, e)}\n              className=\"border-ods-border bg-ods-bg text-ods-text-primary\"\n            />\n          </div>\n\n          <Button\n            type=\"button\"\n            variant=\"transparent\"\n            size=\"icon\"\n            onClick={() => removeItem(index)}\n            className=\"text-ods-error hover:bg-ods-error-secondary\"\n          >\n            <Trash2 className=\"h-4 w-4\" />\n          </Button>\n        </div>\n      ))}\n\n      {workingItems.length === 0 && (\n        <div className=\"rounded-lg border border-ods-border bg-ods-bg-surface px-4 py-4 text-center\">\n          <p className=\"text-ods-text-secondary text-h6\">{emptyMessage}</p>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { PlusCircle, X } from 'lucide-react';\nimport { type KeyboardEvent, forwardRef, useRef, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Button } from './button';\nimport { Input } from './input';\nimport { Label } from './label';\n\ninterface AllowedDomainsInputProps {\n  value: string[];\n  onChange: (domains: string[]) => void;\n  onValidate?: (domain: string) => { valid: boolean; error?: string; cleanedDomain?: string };\n  label?: string;\n  placeholder?: string;\n  disabled?: boolean;\n  error?: string | null;\n  helperText?: string;\n  className?: string;\n}\n\nconst AllowedDomainsInput = forwardRef<HTMLDivElement, AllowedDomainsInputProps>(\n  (\n    {\n      value,\n      onChange,\n      onValidate,\n      label = 'Allowed Domains',\n      placeholder = 'example.com',\n      disabled = false,\n      error,\n      helperText,\n      className,\n    },\n    ref,\n  ) => {\n    const [inputValue, setInputValue] = useState('');\n    const [localError, setLocalError] = useState<string | null>(null);\n    const inputRef = useRef<HTMLInputElement>(null);\n\n    const displayError = error || localError;\n\n    const addDomain = () => {\n      const trimmedValue = inputValue.trim();\n      if (!trimmedValue) return;\n\n      // Validate if validator provided\n      if (onValidate) {\n        const validation = onValidate(trimmedValue);\n        if (!validation.valid) {\n          setLocalError(validation.error || 'Invalid domain');\n          return;\n        }\n        // Use cleaned domain if provided\n        const domainToAdd = validation.cleanedDomain || trimmedValue;\n        if (!value.includes(domainToAdd)) {\n          onChange([...value, domainToAdd]);\n          setLocalError(null);\n        }\n      } else {\n        // No validation, just add if not duplicate\n        if (!value.includes(trimmedValue)) {\n          onChange([...value, trimmedValue]);\n        }\n      }\n      setInputValue('');\n    };\n\n    const removeDomain = (index: number) => {\n      const newDomains = value.filter((_, i) => i !== index);\n      onChange(newDomains);\n    };\n\n    const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === 'Enter') {\n        e.preventDefault();\n        addDomain();\n      }\n    };\n\n    return (\n      <div ref={ref} className={cn('space-y-3', className)}>\n        {label && <Label>{label}</Label>}\n\n        {/* Existing domains */}\n        {value.map((domain, index) => (\n          <div key={index} className=\"flex items-center gap-2\">\n            <Input value={domain} disabled className=\"flex-1 rounded-[6px] border-ods-border bg-ods-card\" />\n            <Button\n              type=\"button\"\n              variant=\"transparent\"\n              size=\"small-legacy\"\n              leftIcon={<X className=\"h-4 w-4\" />}\n              onClick={() => removeDomain(index)}\n              disabled={disabled}\n              className=\"shrink-0 text-ods-text-secondary hover:text-ods-text-primary\"\n            />\n          </div>\n        ))}\n\n        {/* Add new domain input */}\n        <div className=\"flex items-center gap-2\">\n          <Input\n            ref={inputRef}\n            value={inputValue}\n            onChange={e => {\n              setInputValue(e.target.value);\n              setLocalError(null);\n            }}\n            onKeyDown={handleKeyDown}\n            placeholder={placeholder}\n            disabled={disabled}\n            className=\"flex-1 rounded-[6px] border-ods-border bg-ods-card\"\n          />\n        </div>\n\n        {/* Add Domain button */}\n        <button\n          type=\"button\"\n          className={cn(\n            'flex items-center gap-2 py-2 text-ods-text-primary transition-colors hover:text-ods-accent',\n            disabled && 'cursor-not-allowed opacity-50',\n          )}\n          onClick={addDomain}\n          disabled={disabled}\n        >\n          <PlusCircle className=\"h-5 w-5 text-ods-text-secondary\" />\n          <span className=\"font-bold text-h6\">Add Domain</span>\n        </button>\n\n        {/* Error message */}\n        {displayError && <p className=\"text-ods-error text-h6\">{displayError}</p>}\n\n        {/* Helper text */}\n        {helperText && !displayError && <p className=\"text-ods-text-secondary text-h6\">{helperText}</p>}\n      </div>\n    );\n  },\n);\n\nAllowedDomainsInput.displayName = 'AllowedDomainsInput';\n\nexport { AllowedDomainsInput };\nexport type { AllowedDomainsInputProps };\n","'use client';\n\nimport type { CSSProperties, ReactElement, ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * Mirrors `screens.md` in tailwind.config.ts (800px), which is also where the\n * ODS responsive tokens step up (ods-responsive-tokens.css). ONE breakpoint\n * for the whole bar: the `md:` variants below and the `onContentClick` guard\n * must never disagree (they did before — the guard read a stale 768px while\n * the CSS switched at 800px).\n */\nconst MD_QUERY = '(min-width: 800px)';\n\nexport interface AnnouncementBarViewProps {\n  /**\n   * Leading slot — typically an icon, at content width before the title. Size\n   * it in the slot: the bar's icon runs 20px below `md` and 24px from `md` up,\n   * and no ODS icon token carries that pair (`--icon-size-icon-size` is 16/24).\n   */\n  startAdornment?: ReactNode;\n  /**\n   * Bar message, always ONE line. A plain string gets the strip's text\n   * treatment (`text-h6`, truncating); pass a ReactElement to own the markup\n   * (e.g. bold title + inline description truncating as one unit).\n   */\n  title?: string | ReactElement;\n  /**\n   * Primary CTA, at content width after the title. Hidden below `md`, where\n   * the content row becomes the tap target instead (`onContentClick`) — the\n   * banner pattern's touch-first tradeoff.\n   */\n  actionBlock?: ReactElement;\n  /** Trailing slot (e.g. a dismiss button). Outside the tap target, every breakpoint. */\n  endAdornment?: ReactElement;\n  /**\n   * Below `md` only: fires when the content row is tapped, standing in for the\n   * `actionBlock` that is CSS-hidden at that width. No-op from `md` up, where\n   * the CTA is visible and owns the interaction.\n   */\n  onContentClick?: () => void;\n  /** Extra classes for the padded content row (e.g. a cursor affordance). */\n  contentClassName?: string;\n  /** Surface styling (background, text color, theme scope) is the consumer's — pass it here. */\n  className?: string;\n  style?: CSSProperties;\n}\n\n/**\n * ============================================================================\n * DO NOT REDESIGN THIS BAR. THIS IS THE FINAL, OWNER-APPROVED DESIGN.\n * ============================================================================\n * The 44px single-line strip below is DELIBERATE and has already been\n * regressed and reverted. Read this before you touch a single class:\n *\n *   - #1542 (Michael Assraf) RESTORED this exact 44px / text-h6 / compact-CTA\n *     design after it drifted.\n *   - #1545 \"redesigned\" it into a 56px, text-h4, stacked-mobile bar.\n *   - #1551 REVERTED #1545's UI wholesale, back to THIS.\n *\n * Do NOT:\n *   - bump the height (min-h-11 / 44px), the text scale (text-h6), the icon\n *     size (20/24px), or the CTA size (compact),\n *   - re-introduce a stacked / multi-row / full-width-CTA mobile layout,\n *   - \"match a new mockup\", \"improve\", \"modernize\", or \"clean up\" any of it.\n *\n * This design is the source of truth. A Figma frame is NOT authorization to\n * change it. If you genuinely believe it must change, get Michael's explicit\n * written sign-off on the PR FIRST — otherwise your change WILL be reverted\n * again, exactly like #1545 was. Touch it and you own the revert.\n * ============================================================================\n *\n * Pure presentational announcement/notification bar. No data fetching,\n * storage, or navigation — consumers own state and pass content through slots.\n *\n * Anatomy follows the announcement-bar industry standard: ONE line of text at\n * 13-14px inside a 44px-tall strip (guides converge on 40-60px with a single\n * sentence; two stacked 18px rows blow past that), ONE compact CTA on the\n * right, and a trailing dismiss slot. Spacing is the ODS responsive tokens,\n * which step at the same 800px as `md`: `l` = 16/24px edge padding, `s` =\n * 8/12px gap, `m` = CTA offset, `xs` = 4/8px.\n */\nexport function AnnouncementBarView({\n  startAdornment,\n  title,\n  actionBlock,\n  endAdornment,\n  onContentClick,\n  contentClassName,\n  className,\n  style,\n}: AnnouncementBarViewProps) {\n  const handleContentClick = onContentClick\n    ? () => {\n        if (!window.matchMedia(MD_QUERY).matches) onContentClick();\n      }\n    : undefined;\n\n  return (\n    <div className={cn('flex min-h-11 w-full max-w-full items-center', className)} style={style}>\n      {/* Content row — the tap target below `md`, where the CTA is hidden.\n          Its vertical padding never drives the height; the 44px strip does. */}\n      <div\n        className={cn(\n          'flex min-w-0 flex-1 flex-row items-center gap-[var(--spacing-system-s)] py-[var(--spacing-system-xs)] pl-[var(--spacing-system-l)]',\n          // The strip's right inset normally comes from the endAdornment\n          // wrapper's `mr`; without a trailing slot nothing padded the right\n          // edge and the CTA/text sat flush against it. Mirror the `l` edge\n          // padding in that case.\n          !endAdornment && 'pr-[var(--spacing-system-l)]',\n          contentClassName,\n        )}\n        onClick={handleContentClick}\n      >\n        {startAdornment && <div className=\"flex shrink-0 items-center\">{startAdornment}</div>}\n        {typeof title === 'string' ? (\n          <p className=\"mb-0 min-w-0 max-w-full flex-1 truncate text-h6\">{title}</p>\n        ) : (\n          title != null && <div className=\"min-w-0 max-w-full flex-1\">{title}</div>\n        )}\n        {/* Below `md` the CTA is visually replaced by the row-wide tap target,\n            but a `display:none` button is unreachable by keyboard and AT. So\n            it is only visually hidden there (`sr-only`) and reveals itself on\n            focus — the row stays the touch affordance while Tab still reaches\n            a real, labelled control. `md:` restores the normal inline CTA.\n\n            The wrapper stops click propagation: below `md` the CTA lives INSIDE\n            the content row (which owns `onContentClick`), so a keyboard/AT\n            activation of the revealed button would bubble up and fire the row\n            handler too — and both resolve to the same CTA action, opening two\n            tabs for `target=\"_blank\"`. Stopping here keeps the row tap target\n            for empty space while making the button the sole handler when hit. */}\n        {actionBlock && (\n          <div\n            className=\"sr-only ml-[var(--spacing-system-m)] shrink-0 focus-within:not-sr-only md:not-sr-only md:flex\"\n            onClick={e => e.stopPropagation()}\n          >\n            {actionBlock}\n          </div>\n        )}\n      </div>\n      {/* Trailing slot. Its own 32px hit box optically re-centers the glyph\n          against the 16/24px left edge. */}\n      {endAdornment && (\n        <div className=\"ml-[var(--spacing-system-xs)] mr-[var(--spacing-system-m)] shrink-0\">{endAdornment}</div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';\nimport { Loader2 } from 'lucide-react';\nimport {\n  type ChangeEvent,\n  type ForwardedRef,\n  type KeyboardEvent,\n  type MouseEvent as ReactMouseEvent,\n  type ReactElement,\n  type ReactNode,\n  forwardRef,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { useAutoLimitTags } from '../../hooks/ui/use-auto-limit-tags';\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';\nimport { cn } from '../../utils/cn';\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';\nimport { TrashIcon } from '../icons-v2-generated/interface/trash-icon';\nimport { CheckIcon } from '../icons-v2-generated/signs-and-symbols/check-icon';\nimport { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon';\nimport { FieldWrapper } from './field-wrapper';\nimport { HiddenTagsPopup } from './hidden-tags-popup';\nimport { Tag } from './tag';\nimport { TruncateText } from './truncate-text';\n\nexport interface AutocompleteOption<T = string> {\n  label: string;\n  value: T;\n  /**\n   * Leading glyph for this option — a flag, a crest, a logo, an avatar.\n   *\n   * Rendered by the DEFAULT option row and, for the selected option, in the\n   * input's start adornment. Entity pickers (a state, a club, a city) read as\n   * a list of names without it, so it belongs on the shared control rather\n   * than in a `renderOption` each caller re-invents.\n   */\n  icon?: ReactNode;\n  /**\n   * Second, quieter line under the label — a club's home city, the states a\n   * region covers, a person's email.\n   *\n   * A plain string is truncated with a tooltip; a node is rendered as-is, so an\n   * option can carry richer detail (a row of state flags, say). The row grows\n   * to fit either.\n   */\n  description?: ReactNode;\n}\n\nexport type AutocompleteInputChangeReason = 'input' | 'reset' | 'clear';\n\ninterface AutocompleteBaseProps<T = string> {\n  /** Available options to select from */\n  options: AutocompleteOption<T>[];\n  /** Placeholder text */\n  placeholder?: string;\n  /** Whether the component is disabled */\n  disabled?: boolean;\n  /**\n   * Element displayed at the start of the input.\n   *\n   * OPTIONAL for an icon-bearing option list: when omitted, the SELECTED\n   * option's own `icon` is used, so the trigger and the list agree without the\n   * caller wiring it twice. Pass this to override (a static search glyph, say).\n   */\n  startAdornment?: ReactNode;\n  /** Whether to show clear button */\n  showClearAll?: boolean;\n  /** Custom className for the container */\n  className?: string;\n  /** Custom className for the dropdown */\n  dropdownClassName?: string;\n  /** When true, allows creating new options by typing */\n  freeSolo?: boolean;\n  /** Label for the input */\n  label?: string;\n  /** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles) */\n  labelVariant?: 'default' | 'large';\n  /** Error message displayed below the field */\n  error?: string;\n  /** Custom filter function */\n  filterOptions?: (options: AutocompleteOption<T>[], inputValue: string) => AutocompleteOption<T>[];\n  /** Render custom option content */\n  renderOption?: (option: AutocompleteOption<T>, isSelected: boolean) => ReactNode;\n  /** When true, shows validation error styling */\n  invalid?: boolean;\n  /** No options text */\n  noOptionsText?: string;\n  /** Controlled input value. When provided, the component won't manage input state internally. */\n  inputValue?: string;\n  /** Callback when input value changes (typing, selection, clearing). Fires in both controlled and uncontrolled modes. */\n  onInputChange?: (value: string, reason: AutocompleteInputChangeReason) => void;\n  /** Loading state */\n  loading?: boolean;\n  /** Loading text */\n  loadingText?: string;\n  /** When true, shows a clickable \"+ Create\" option when no results match the input */\n  creatable?: boolean;\n  /** Callback fired after a new option is created via creatable. Use it to persist the new option server-side, etc. */\n  onCreateOption?: (inputValue: string) => void;\n  /** Max length for a created option. When exceeded, creation is blocked and a hint is shown. Omit for no limit. */\n  maxCreateLength?: number;\n  /** When set, each unselected option shows a hover trash button that calls this. Omit to hide delete. */\n  onDeleteOption?: (value: T) => void;\n  isDeletingOption?: boolean;\n  /** When true, disables built-in client-side filtering (useful when options are filtered server-side via onInputChange) */\n  disableClientFilter?: boolean;\n  /** Whether to show the chevron icon. Default true */\n  showChevron?: boolean;\n  /** Whether to clear the input when the dropdown opens (single mode only). Default true */\n  clearOnOpen?: boolean;\n}\n\nexport interface AutocompleteSingleProps<T = string> extends AutocompleteBaseProps<T> {\n  /** Single-select mode (default) */\n  multiple?: false;\n  /** Currently selected value */\n  value: T | null;\n  /** Callback when selection changes */\n  onChange: (value: T | null) => void;\n}\n\nexport interface AutocompleteMultipleProps<T = string> extends AutocompleteBaseProps<T> {\n  /** Enable multi-select mode */\n  multiple: true;\n  /** Currently selected values */\n  value: T[];\n  /** Callback when selection changes */\n  onChange: (value: T[]) => void;\n  /** Maximum number of items that can be selected */\n  maxItems?: number;\n  /** Render custom tag content */\n  renderTag?: (option: AutocompleteOption<T>) => ReactNode;\n  /** Maximum number of visible tags. Set to \"auto\" for automatic calculation based on available width. Default \"auto\" */\n  limitTags?: number | 'auto';\n  /** Custom render function for the \"+N\" overflow chip */\n  getLimitTagsText?: (more: number) => ReactNode;\n}\n\nexport type AutocompleteProps<T = string> = AutocompleteSingleProps<T> | AutocompleteMultipleProps<T>;\n\n// Inner input styles matching Input component\nconst innerInputStyles = cn(\n  'min-w-[60px] flex-1 border-none bg-transparent outline-none',\n  'text-h4',\n  'text-ods-text-primary placeholder:text-ods-text-secondary',\n  // Disabled - match Input exactly (value greys out, placeholder dims further)\n  'disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border',\n);\n\nfunction AutocompleteInner<T = string>(props: AutocompleteProps<T>, ref: ForwardedRef<HTMLDivElement>) {\n  const {\n    options,\n    disabled = false,\n    startAdornment,\n    showClearAll = true,\n    className,\n    dropdownClassName,\n    freeSolo = false,\n    label,\n    labelVariant,\n    error,\n    filterOptions,\n    renderOption,\n    invalid = false,\n    noOptionsText = 'No options',\n    inputValue: inputValueProp,\n    onInputChange,\n    loading = false,\n    loadingText = 'Loading...',\n    creatable = false,\n    onCreateOption,\n    maxCreateLength,\n    onDeleteOption,\n    isDeletingOption = false,\n    disableClientFilter = false,\n    showChevron = true,\n    clearOnOpen = true,\n  } = props;\n\n  const multiple = props.multiple ?? false;\n  const placeholder = props.placeholder ?? (multiple ? 'Add More...' : 'Select...');\n\n  // Multiple-only props\n  const maxItems = multiple ? (props as AutocompleteMultipleProps<T>).maxItems : undefined;\n  const renderTag = multiple ? (props as AutocompleteMultipleProps<T>).renderTag : undefined;\n  const limitTagsProp = multiple ? ((props as AutocompleteMultipleProps<T>).limitTags ?? 'auto') : 'auto';\n  const getLimitTagsText = multiple\n    ? ((props as AutocompleteMultipleProps<T>).getLimitTagsText ?? ((more: number) => `+${more}`))\n    : (more: number) => `+${more}`;\n\n  // Normalize value to array for internal use. Memoised: the single-value and\n  // empty branches both minted a new array on every render, so the memo keyed\n  // on this never hit.\n  const valueArray: T[] = useMemo(\n    () => (multiple ? (props.value as T[]) : props.value != null ? [props.value as T] : []),\n    [multiple, props.value],\n  );\n\n  const [internalInputValue, setInternalInputValue] = useState('');\n  const isInputControlled = inputValueProp !== undefined;\n  const inputValue = isInputControlled ? inputValueProp : internalInputValue;\n\n  const updateInputValue = (value: string, reason: AutocompleteInputChangeReason) => {\n    if (!isInputControlled) {\n      setInternalInputValue(value);\n    }\n    onInputChange?.(value, reason);\n  };\n\n  const [isOpen, setIsOpen] = useState(false);\n  const [highlightedIndex, setHighlightedIndex] = useState(-1);\n  const keyboardPadding = useKeyboardCollisionPadding();\n  const containerRef = useRef<HTMLDivElement>(null);\n  const hiddenTagsPopupRef = useRef<HTMLDivElement>(null);\n\n  const isInvalid = invalid || !!error;\n\n  // Combine refs\n  useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n  // Get selected options\n  const selectedOptions = useMemo(() => {\n    return valueArray.map(v => options.find(opt => opt.value === v) ?? { label: String(v), value: v });\n  }, [valueArray, options]);\n\n  // Single mode: the currently selected option\n  const selectedOption = !multiple && selectedOptions.length > 0 ? selectedOptions[0] : null;\n  // A single-select trigger shows the selection's glyph unless the caller\n  // supplied its own adornment.\n  const resolvedStartAdornment = startAdornment ?? selectedOption?.icon ?? null;\n\n  // Placeholder logic\n  const inputPlaceholder = multiple ? (valueArray.length === 0 ? placeholder : 'Add More...') : placeholder;\n\n  // ---- Auto limit tags via shared hook ----\n  // Destructured rather than kept as `autoLimitTags.<x>Ref`: reaching through\n  // the object in JSX reads the property on every render, which is a ref\n  // access in render even though the ref object itself is only being handed to\n  // React. Pulling them out once is what the other consumers of this hook do.\n  const {\n    visibleCount: autoVisibleCount,\n    middleRef,\n    measureRef,\n    textMeasureRef,\n    badgeRef,\n    inputRef,\n  } = useAutoLimitTags({\n    count: multiple ? selectedOptions.length : 0,\n    limitTags: multiple ? limitTagsProp : 0,\n    placeholder: inputPlaceholder,\n  });\n\n  const visibleCount = multiple ? autoVisibleCount : 0;\n  const visibleTags = multiple ? selectedOptions.slice(0, visibleCount) : [];\n  const hiddenTags = multiple ? selectedOptions.slice(visibleCount) : [];\n  const hiddenTagsCount = multiple ? selectedOptions.length - visibleCount : 0;\n\n  const [showHiddenTags, setShowHiddenTags] = useState(false);\n  const hiddenTagsRef = useRef<HTMLDivElement>(null);\n\n  // Close hidden tags list on outside click\n  useEffect(() => {\n    if (!showHiddenTags) return undefined;\n    const handleClick = (e: MouseEvent) => {\n      const target = e.target as Node;\n      const inButton = hiddenTagsRef.current?.contains(target);\n      const inPopup = hiddenTagsPopupRef.current?.contains(target);\n      if (!inButton && !inPopup) {\n        setShowHiddenTags(false);\n      }\n    };\n    document.addEventListener('mousedown', handleClick);\n    return () => document.removeEventListener('mousedown', handleClick);\n  }, [showHiddenTags]);\n\n  // Synchronously open dropdown, pre-filling input when clearOnOpen is false\n  const openDropdown = () => {\n    if (!multiple && !clearOnOpen && selectedOption) {\n      updateInputValue(selectedOption.label, 'reset');\n    }\n    setShowHiddenTags(false);\n    setIsOpen(true);\n  };\n\n  // Input display value:\n  // - Single mode, closed, has selection → show selected label\n  // - Single mode, open, clearOnOpen=false → show inputValue (pre-filled with label)\n  // - Otherwise → show inputValue (what user is typing)\n  const inputDisplayValue = !multiple && !isOpen && selectedOption ? selectedOption.label : inputValue;\n\n  // Filter options based on inputValue\n  const filteredOptions = useMemo(() => {\n    if (disableClientFilter) {\n      return options;\n    }\n\n    if (filterOptions) {\n      return filterOptions(options, inputValue);\n    }\n\n    if (!inputValue.trim()) {\n      return options;\n    }\n\n    const lowerInput = inputValue.toLowerCase();\n    return options.filter(opt => opt.label.toLowerCase().includes(lowerInput));\n  }, [options, inputValue, filterOptions, disableClientFilter]);\n\n  // Show \"+ Create\" option when creatable is on, user typed something, and nothing matched\n  const showCreateOption = creatable && inputValue.trim().length > 0 && filteredOptions.length === 0;\n  const isCreateTooLong = maxCreateLength != null && inputValue.trim().length > maxCreateLength;\n\n  // Handle creating a new option\n  const handleCreate = () => {\n    const trimmed = inputValue.trim();\n    if (!trimmed || (maxCreateLength != null && trimmed.length > maxCreateLength)) return;\n\n    const newValue = trimmed as T;\n\n    if (multiple) {\n      if (maxItems && valueArray.length >= maxItems) return;\n      (props as AutocompleteMultipleProps<T>).onChange([...valueArray, newValue]);\n    } else {\n      (props as AutocompleteSingleProps<T>).onChange(newValue);\n    }\n\n    updateInputValue('', 'reset');\n    setIsOpen(false);\n    onCreateOption?.(trimmed);\n  };\n\n  // Reset highlighted index when options change. Adjusted while rendering, not\n  // from an effect: the highlight is drawn from this value in THIS render, so an\n  // effect commits one frame highlighting whatever row now happens to sit at the\n  // old index — and Enter pressed in that frame selects it.\n  const [highlightedFor, setHighlightedFor] = useState(filteredOptions.length);\n  if (highlightedFor !== filteredOptions.length) {\n    setHighlightedFor(filteredOptions.length);\n    setHighlightedIndex(-1);\n  }\n\n  // Handle input change\n  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n    updateInputValue(e.target.value, 'input');\n    if (!isOpen) {\n      setIsOpen(true);\n    }\n    setHighlightedIndex(-1);\n  };\n\n  // Handle option selection\n  const handleSelect = (option: AutocompleteOption<T>) => {\n    if (multiple) {\n      // Multiple mode: toggle selection\n      const isSelected = valueArray.includes(option.value);\n\n      if (isSelected) {\n        (props as AutocompleteMultipleProps<T>).onChange(valueArray.filter(v => v !== option.value));\n      } else {\n        if (maxItems && valueArray.length >= maxItems) {\n          return;\n        }\n        (props as AutocompleteMultipleProps<T>).onChange([...valueArray, option.value]);\n      }\n\n      updateInputValue('', 'reset');\n      inputRef.current?.focus();\n    } else {\n      // Single mode: select and close\n      (props as AutocompleteSingleProps<T>).onChange(option.value);\n      // When clearOnOpen is false, keep the label as inputValue so\n      // filteredOptions is pre-computed before the next open (prevents flicker)\n      updateInputValue(clearOnOpen ? '' : option.label, 'reset');\n      setIsOpen(false);\n    }\n  };\n\n  // Handle clear\n  const handleClearAll = (e: ReactMouseEvent) => {\n    e.stopPropagation();\n    e.preventDefault();\n    if (multiple) {\n      (props as AutocompleteMultipleProps<T>).onChange([]);\n    } else {\n      (props as AutocompleteSingleProps<T>).onChange(null);\n    }\n    if (!isInputControlled) {\n      setInternalInputValue('');\n    }\n    onInputChange?.('', 'clear');\n    setIsOpen(false);\n  };\n\n  // Handle keyboard navigation\n  const handleKeyDown = (e: KeyboardEvent) => {\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault();\n        if (!isOpen) {\n          openDropdown();\n        }\n        setHighlightedIndex(prev => (prev < filteredOptions.length - 1 ? prev + 1 : 0));\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        setHighlightedIndex(prev => (prev > 0 ? prev - 1 : filteredOptions.length - 1));\n        break;\n      case 'Enter':\n        e.preventDefault();\n        if (showCreateOption) {\n          handleCreate();\n        } else if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {\n          handleSelect(filteredOptions[highlightedIndex]);\n        } else if (freeSolo && inputValue.trim()) {\n          const newOption: AutocompleteOption<T> = {\n            label: inputValue.trim(),\n            value: inputValue.trim() as T,\n          };\n          handleSelect(newOption);\n        }\n        break;\n      case 'Escape': {\n        // Restore label when clearOnOpen is false, same as handleOpenChange close\n        const resetValue = !multiple && !clearOnOpen && selectedOption ? selectedOption.label : '';\n        updateInputValue(resetValue, 'reset');\n        setIsOpen(false);\n        setHighlightedIndex(-1);\n        break;\n      }\n      case 'Backspace':\n        break;\n    }\n  };\n\n  // Handle popover open/close\n  const handleOpenChange = (open: boolean) => {\n    if (open) {\n      openDropdown();\n    } else {\n      // When clearOnOpen is false and there's a selection, restore the label\n      // so filteredOptions stays pre-computed for the next open\n      const resetValue = !multiple && !clearOnOpen && selectedOption ? selectedOption.label : '';\n      updateInputValue(resetValue, 'reset');\n      setIsOpen(false);\n    }\n  };\n\n  const canAddMore = multiple ? !maxItems || valueArray.length < maxItems : true;\n  const hasValue = valueArray.length > 0;\n\n  const popover = (\n    <PopoverPrimitive.Root open={isOpen} onOpenChange={handleOpenChange} modal={false}>\n      <PopoverPrimitive.Anchor asChild>\n        <div\n          // Same marker Input/Textarea/Select expose — it is how a form finds\n          // (and scrolls to) the first field that failed validation.\n          data-invalid={isInvalid || undefined}\n          className={cn(\n            // Layout — single line, no wrapping\n            'flex min-h-11 cursor-text items-center rounded-[6px] border md:min-h-12',\n            'focus-within:outline-none',\n            'transition-colors duration-200',\n            'border-ods-border bg-ods-card',\n            'group',\n            !disabled &&\n              'hover:border-ods-border-hover hover:bg-ods-bg-hover active:border-ods-border-active active:bg-ods-bg-active',\n            disabled && '!cursor-not-allowed bg-ods-bg',\n            // Adornments / chevron carry their own colour — grey them with the\n            // value so a disabled field reads as one flat colour. Scoped to the\n            // DIRECT span child (the start adornment) on purpose: as a descendant\n            // rule it also hit the selected `Tag` chips in the middle zone, whose\n            // label is a span, and they went invisible against their own fill in\n            // the light theme. Anything nested inside the adornment that sets its\n            // own colour keeps it; anything that doesn't inherits the grey.\n            'has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled',\n            isOpen && !isInvalid && 'border-ods-accent hover:border-ods-accent',\n            isInvalid && 'border-ods-error hover:border-ods-error',\n          )}\n          onClick={() => {\n            if (!disabled) {\n              inputRef.current?.focus();\n              openDropdown();\n            }\n          }}\n        >\n          {/* Start Adornment — the caller's, else the selected option's own icon. */}\n          {resolvedStartAdornment && (\n            <span\n              className={cn(\n                // Images are sized alongside svgs: an entity adornment is\n                // usually a flag or a crest, and an unconstrained <img> lays\n                // out at 0x0 inside this flex row.\n                'flex-shrink-0 pl-3 text-ods-text-secondary transition-colors duration-200',\n                '[&_img]:size-4 [&_img]:object-contain md:[&_img]:size-6 [&_svg]:size-4 md:[&_svg]:size-6',\n                isOpen && !isInvalid && 'text-ods-accent',\n                isInvalid && 'text-ods-error',\n              )}\n            >\n              {resolvedStartAdornment}\n            </span>\n          )}\n\n          {/* Middle zone: tags + input — single line with overflow */}\n          <div ref={middleRef} className=\"flex min-w-0 flex-1 items-center gap-2 overflow-hidden px-2\">\n            {/* Tags (multiple mode only) */}\n            {multiple &&\n              visibleTags.map(option => (\n                <Tag\n                  key={String(option.value)}\n                  variant=\"outline\"\n                  labelClassName=\"max-w-[90px] truncate\"\n                  label={renderTag ? renderTag(option) : option.label}\n                  onClose={\n                    !disabled\n                      ? () => {\n                          (props as AutocompleteMultipleProps<T>).onChange(valueArray.filter(v => v !== option.value));\n                        }\n                      : undefined\n                  }\n                />\n              ))}\n\n            {/* Overflow indicator button (multiple mode only) */}\n            {multiple && hiddenTagsCount > 0 && (\n              <div ref={hiddenTagsRef} className=\"shrink-0\">\n                <button\n                  ref={badgeRef}\n                  type=\"button\"\n                  onClick={e => {\n                    e.stopPropagation();\n                    setShowHiddenTags(prev => {\n                      if (!prev) setIsOpen(false);\n                      return !prev;\n                    });\n                  }}\n                  className={cn(\n                    'flex h-8 items-center px-2',\n                    'rounded-[6px] border border-ods-border bg-ods-card',\n                    'text-ods-text-secondary text-h5',\n                    'cursor-pointer transition-colors hover:bg-ods-bg-hover',\n                  )}\n                >\n                  {getLimitTagsText(hiddenTagsCount)}\n                </button>\n              </div>\n            )}\n\n            {/* Input */}\n            {canAddMore && (\n              <input\n                ref={inputRef}\n                type=\"text\"\n                value={inputDisplayValue}\n                onChange={handleInputChange}\n                onKeyDown={handleKeyDown}\n                onFocus={() => openDropdown()}\n                placeholder={inputPlaceholder}\n                disabled={disabled}\n                className={innerInputStyles}\n              />\n            )}\n          </div>\n\n          {/* Clear / Chevron — pinned right */}\n          <div className=\"flex shrink-0 items-center gap-1 pr-3\">\n            {showClearAll && (hasValue || inputValue.length > 0) && !disabled && isOpen && (\n              <button\n                type=\"button\"\n                onClick={handleClearAll}\n                className=\"flex items-center justify-center transition-opacity hover:opacity-70\"\n                aria-label=\"Clear all\"\n              >\n                <XmarkCircleIcon className=\"size-4 text-ods-text-secondary md:size-6\" />\n              </button>\n            )}\n            {loading ? (\n              <Loader2\n                className={cn(\n                  'size-4 animate-spin md:size-6',\n                  isInvalid ? 'text-ods-error' : isOpen ? 'text-ods-accent' : 'text-ods-text-secondary',\n                )}\n              />\n            ) : (\n              showChevron && (\n                <Chevron02DownIcon\n                  className={cn(\n                    'size-4 transition-all duration-200 md:size-6',\n                    'text-ods-text-secondary',\n                    isOpen && 'rotate-180',\n                    isOpen && !isInvalid && 'text-ods-accent',\n                    isInvalid && 'text-ods-error',\n                  )}\n                />\n              )\n            )}\n          </div>\n        </div>\n      </PopoverPrimitive.Anchor>\n\n      <PopoverPrimitive.Content\n        className={cn(\n          'z-50 mt-1 w-[var(--radix-popover-trigger-width)]',\n          'rounded-[4px] border border-ods-border bg-ods-card',\n          'flex max-h-[var(--radix-popper-available-height)] flex-col overflow-hidden',\n          'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',\n          'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',\n          dropdownClassName,\n        )}\n        sideOffset={4}\n        align=\"start\"\n        // The anchor is the text field that raises the software keyboard, so\n        // this list is always positioned with the keyboard up — see\n        // useKeyboardCollisionPadding.\n        collisionPadding={{ bottom: keyboardPadding }}\n        onOpenAutoFocus={e => {\n          e.preventDefault();\n          inputRef.current?.focus();\n        }}\n        onInteractOutside={e => {\n          // Don't close if clicking inside the anchor/input container\n          if (containerRef.current?.contains(e.target as Node)) {\n            e.preventDefault();\n          }\n        }}\n      >\n        <ScrollAreaPrimitive.Root className=\"flex min-h-0 flex-col overflow-hidden\">\n          <ScrollAreaPrimitive.Viewport className=\"max-h-[240px] min-h-0 w-full [&>div]:!block\">\n            <div role=\"listbox\">\n              {loading ? (\n                <div className=\"px-3 py-2 text-ods-text-secondary text-h6\">{loadingText}</div>\n              ) : filteredOptions.length === 0 ? (\n                showCreateOption ? (\n                  isCreateTooLong ? (\n                    <div className=\"flex h-11 min-w-0 items-center px-[var(--spacing-system-mf)] text-ods-error text-h5 md:h-12\">\n                      Maximum {maxCreateLength} characters\n                    </div>\n                  ) : (\n                    <div\n                      role=\"option\"\n                      aria-selected={false}\n                      className={cn(\n                        'flex h-11 min-w-0 cursor-pointer items-center px-[var(--spacing-system-mf)] transition-colors md:h-12',\n                        'text-ods-accent text-h4',\n                        'hover:bg-ods-bg-hover',\n                      )}\n                      onClick={handleCreate}\n                    >\n                      {/* text-current: the row owns the typography/color (accent), not the TruncateText defaults. */}\n                      <TruncateText className=\"text-current\">{`+ Create \"${inputValue.trim()}\"`}</TruncateText>\n                    </div>\n                  )\n                ) : (\n                  <div className=\"px-3 py-2 text-ods-text-secondary text-h6\">\n                    {freeSolo && inputValue.trim() ? (\n                      <span>Press Enter to add &quot;{inputValue}&quot;</span>\n                    ) : (\n                      noOptionsText\n                    )}\n                  </div>\n                )\n              ) : (\n                filteredOptions.map((option, index) => {\n                  const isSelected = valueArray.includes(option.value);\n                  const isHighlighted = index === highlightedIndex;\n\n                  return (\n                    <div\n                      key={String(option.value)}\n                      role=\"option\"\n                      aria-selected={isSelected}\n                      className={cn(\n                        // `min-h` rather than a fixed height: an option may carry\n                        // a second line, and a clipped one is worse than a taller row.\n                        'group/item flex min-h-11 cursor-pointer items-center border-b border-ods-border px-[var(--spacing-system-mf)] py-[var(--spacing-system-xsf)] transition-colors last:border-b-0 md:min-h-12',\n                        'text-h4',\n                        isHighlighted && 'bg-ods-bg-surface',\n                        isSelected ? 'text-ods-accent' : 'text-ods-text-primary',\n                        !isHighlighted && 'hover:bg-ods-bg-hover',\n                      )}\n                      onClick={() => handleSelect(option)}\n                      onMouseEnter={() => setHighlightedIndex(index)}\n                    >\n                      {renderOption ? (\n                        renderOption(option, isSelected)\n                      ) : (\n                        <div className=\"flex w-full min-w-0 items-center justify-between gap-[var(--spacing-system-xsf)]\">\n                          <div className=\"flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xsf)]\">\n                            {option.icon && (\n                              <span className=\"flex size-5 shrink-0 items-center justify-center overflow-hidden [&_img]:size-full [&_img]:object-contain [&_svg]:size-full\">\n                                {option.icon}\n                              </span>\n                            )}\n                            <div className=\"flex min-w-0 flex-1 flex-col\">\n                              {/* text-current: selection state colors the row (accent vs primary); inherit it. */}\n                              <TruncateText className=\"text-current\">{option.label}</TruncateText>\n                              {typeof option.description === 'string' ? (\n                                <TruncateText tone=\"secondary\" variant=\"h6\">\n                                  {option.description}\n                                </TruncateText>\n                              ) : (\n                                option.description\n                              )}\n                            </div>\n                          </div>\n                          <div className=\"flex shrink-0 items-center gap-[var(--spacing-system-xsf)]\">\n                            {isSelected && <CheckIcon className=\"text-ods-accent\" size={20} />}\n                            {onDeleteOption && !isSelected && (\n                              <button\n                                type=\"button\"\n                                aria-label={`Delete ${option.label}`}\n                                onClick={e => {\n                                  e.stopPropagation();\n                                  onDeleteOption(option.value);\n                                }}\n                                disabled={isDeletingOption}\n                                className=\"opacity-0 transition-opacity focus-visible:opacity-100 disabled:pointer-events-none disabled:opacity-50 group-hover/item:opacity-100\"\n                              >\n                                <TrashIcon className=\"size-4 text-ods-error\" />\n                              </button>\n                            )}\n                          </div>\n                        </div>\n                      )}\n                    </div>\n                  );\n                })\n              )}\n            </div>\n          </ScrollAreaPrimitive.Viewport>\n          <ScrollAreaPrimitive.Scrollbar className=\"hidden\" orientation=\"vertical\">\n            <ScrollAreaPrimitive.Thumb />\n          </ScrollAreaPrimitive.Scrollbar>\n        </ScrollAreaPrimitive.Root>\n      </PopoverPrimitive.Content>\n    </PopoverPrimitive.Root>\n  );\n\n  return (\n    <FieldWrapper label={label} labelVariant={labelVariant} error={error} className={className}>\n      <div className=\"relative\" ref={containerRef}>\n        {popover}\n\n        {/* Hidden tags popup — outside overflow-hidden; right-anchored to the field */}\n        {multiple && showHiddenTags && hiddenTagsCount > 0 && (\n          <HiddenTagsPopup\n            ref={hiddenTagsPopupRef}\n            items={hiddenTags}\n            disabled={disabled}\n            className=\"left-auto right-0 max-w-full\"\n            onRemove={value => {\n              const newValue = valueArray.filter(v => v !== value);\n              (props as AutocompleteMultipleProps<T>).onChange(newValue);\n              if (typeof limitTagsProp === 'number' && newValue.length <= limitTagsProp) setShowHiddenTags(false);\n            }}\n          />\n        )}\n\n        {/* Off-screen measurement containers for auto-limit */}\n        {multiple && (\n          <>\n            <span\n              ref={textMeasureRef}\n              aria-hidden=\"true\"\n              className=\"pointer-events-none invisible absolute left-0 top-0 -z-10 whitespace-nowrap text-h4\"\n            >\n              {inputPlaceholder}\n            </span>\n            <div\n              ref={measureRef}\n              aria-hidden=\"true\"\n              className=\"pointer-events-none invisible absolute left-0 top-0 -z-10 flex gap-2\"\n            >\n              {selectedOptions.map(option => (\n                <Tag\n                  key={`m-${String(option.value)}`}\n                  variant=\"outline\"\n                  labelClassName=\"max-w-[90px] truncate\"\n                  label={renderTag ? renderTag(option) : option.label}\n                  onClose={() => {}}\n                />\n              ))}\n            </div>\n          </>\n        )}\n      </div>\n    </FieldWrapper>\n  );\n}\n\n// Use overloaded signatures so TS can narrow single vs multiple based on the `multiple` prop\ntype AutocompleteComponent = {\n  <T = string>(props: AutocompleteMultipleProps<T> & { ref?: ForwardedRef<HTMLDivElement> }): ReactElement;\n  <T = string>(props: AutocompleteSingleProps<T> & { ref?: ForwardedRef<HTMLDivElement> }): ReactElement;\n};\n\nexport const Autocomplete = forwardRef(AutocompleteInner) as AutocompleteComponent;\n","'use client';\n\nimport { type CSSProperties, type ForwardedRef, type ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\nimport { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon';\n\nexport interface HiddenTagItem {\n  label: ReactNode;\n  value: unknown;\n}\n\nexport interface HiddenTagsPopupProps {\n  items: HiddenTagItem[];\n  onRemove?: (value: unknown) => void;\n  disabled?: boolean;\n  className?: string;\n  style?: CSSProperties;\n}\n\nexport const HiddenTagsPopup = forwardRef(function HiddenTagsPopupImpl(\n  { items, onRemove, disabled, className, style }: HiddenTagsPopupProps,\n  ref: ForwardedRef<HTMLDivElement>,\n) {\n  return (\n    <div\n      ref={ref}\n      style={style}\n      className={cn(\n        // Base positioning is neutral (left-anchored); consumers override via\n        // `style.left` (search-input, tag-search-input) or `className` (autocomplete).\n        'absolute left-0 top-full z-50 mt-1 min-w-[200px]',\n        'rounded-[6px] border border-ods-border bg-ods-card shadow-lg',\n        'duration-150 animate-in fade-in-0 zoom-in-95',\n        className,\n      )}\n    >\n      {items.map(item => (\n        <div\n          key={String(item.value)}\n          className={cn(\n            'flex h-11 items-center justify-between gap-3 px-3 md:h-12',\n            'border-b border-ods-border last:border-b-0',\n          )}\n        >\n          <span\n            className=\"min-w-0 flex-1 truncate uppercase text-ods-text-primary text-h5\"\n            title={typeof item.label === 'string' ? item.label : undefined}\n          >\n            {item.label}\n          </span>\n          {!disabled && onRemove && (\n            <button\n              type=\"button\"\n              onClick={e => {\n                e.stopPropagation();\n                onRemove(item.value);\n              }}\n              className=\"shrink-0 text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n              aria-label={`Remove ${String(item.label)}`}\n            >\n              <XmarkCircleIcon size={20} />\n            </button>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n});\nHiddenTagsPopup.displayName = 'HiddenTagsPopup';\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { useIsTruncated } from '../../hooks/ui/use-is-truncated';\nimport { cn } from '../../utils/cn';\nimport { FloatingTooltip } from './floating-tooltip';\n\n/** ODS typography variants. Maps to the `.text-h1`…`.text-h6` utilities. */\nexport type TruncateTextVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n\n/** ODS text tone. Maps to the `text-ods-text-*` colour utilities. */\nexport type TruncateTextTone = 'primary' | 'secondary';\n\nexport interface TruncateTextProps {\n  children: string;\n  /** Tooltip content; defaults to `children`. */\n  tooltip?: ReactNode;\n  /** Extra classes merged after the variant/tone defaults. */\n  className?: string;\n  side?: 'top' | 'right' | 'bottom' | 'left';\n  /** Max visible lines. `1` uses `truncate` (single-line ellipsis); higher values use `line-clamp-N`. */\n  lines?: 1 | 2 | 3 | 4 | 5 | 6;\n  /** ODS typography token. Default: `'h4'` (body). */\n  variant?: TruncateTextVariant;\n  /** ODS text tone. Default: `'primary'`. */\n  tone?: TruncateTextTone;\n  /** Force the monospace (heading) font family — preserves the variant's size while swapping family. */\n  mono?: boolean;\n  /**\n   * Extra classes for the tooltip trigger wrapper — the element that becomes the\n   * flex/grid item in the caller's layout (e.g. `flex-1`). Merged after the\n   * built-in `min-w-0 max-w-full`, which is what lets the trigger shrink inside\n   * flex rows so the text ellipsizes instead of clipping.\n   */\n  triggerClassName?: string;\n  /**\n   * Trigger wrapper element. Default `'div'`. Use `'span'` where a block element\n   * is invalid HTML — inside a `<p>`, a heading, or another `<span>`; the span\n   * trigger is `inline-block` so the inner truncation still measures.\n   */\n  as?: 'div' | 'span';\n}\n\nconst VARIANT_CLASS: Record<TruncateTextVariant, string> = {\n  h1: 'text-h1',\n  h2: 'text-h2',\n  h3: 'text-h3',\n  h4: 'text-h4',\n  h5: 'text-h5',\n  h6: 'text-h6',\n};\n\nconst TONE_CLASS: Record<TruncateTextTone, string> = {\n  primary: 'text-ods-text-primary',\n  secondary: 'text-ods-text-secondary',\n};\n\nconst LINE_CLAMP_CLASS: Record<2 | 3 | 4 | 5 | 6, string> = {\n  2: 'line-clamp-2',\n  3: 'line-clamp-3',\n  4: 'line-clamp-4',\n  5: 'line-clamp-5',\n  6: 'line-clamp-6',\n};\n\n/**\n * Truncated text bound to the ODS typography system. Shows a `FloatingTooltip`\n * with the full value when (and only when) the content overflows.\n *\n * ```tsx\n * <TruncateText>{name}</TruncateText>                                // h4 / primary\n * <TruncateText variant=\"h6\" tone=\"secondary\">{email}</TruncateText> // caption\n * <TruncateText lines={3}>{description}</TruncateText>               // 3-line clamp\n * ```\n */\nexport function TruncateText({\n  children,\n  tooltip,\n  className,\n  side = 'top',\n  lines = 1,\n  variant = 'h4',\n  tone = 'primary',\n  mono = false,\n  triggerClassName,\n  as = 'div',\n}: TruncateTextProps) {\n  const isMultiLine = lines > 1;\n  const { ref, isTruncated } = useIsTruncated<HTMLSpanElement>(children, { multiline: isMultiLine });\n\n  const clampClass = isMultiLine ? LINE_CLAMP_CLASS[lines as Exclude<typeof lines, 1>] : 'truncate block';\n\n  return (\n    <FloatingTooltip\n      content={tooltip ?? children}\n      side={side}\n      disabled={!isTruncated}\n      className=\"max-w-xs whitespace-pre-line [overflow-wrap:anywhere]\"\n      as={as}\n      // min-w-0: a flex/grid item's automatic minimum size is content-based when\n      // overflow is visible, so without this the trigger refuses to shrink and the\n      // text clips with no ellipsis and no tooltip.\n      triggerClassName={cn('min-w-0 max-w-full', as === 'span' && 'inline-block align-bottom', triggerClassName)}\n    >\n      <span\n        ref={ref}\n        className={cn(\n          VARIANT_CLASS[variant],\n          TONE_CLASS[tone],\n          mono && '[font-family:var(--font-family-heading)]',\n          clampClass,\n          className,\n        )}\n      >\n        {children}\n      </span>\n    </FloatingTooltip>\n  );\n}\n","'use client';\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { type ComponentRef, type ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\nimport { CheckboxCheckmarkIcon } from '../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon';\nimport { RequiredMark } from './required-mark';\n\ninterface CheckboxBlockProps {\n  id?: string;\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onCheckedChange?: (checked: boolean) => void;\n  /** Primary label (supports rich text / ReactNode for inline links) */\n  label: ReactNode;\n  /** Optional secondary description below the label (supports rich text / ReactNode) */\n  description?: ReactNode;\n  /** Keep the label on one line and ellipsize on overflow instead of wrapping */\n  truncateLabel?: boolean;\n  /**\n   * Optional trailing slot rendered at the end of the row (e.g. an action\n   * button). Interactive content must call `preventDefault` on click to stop\n   * the wrapping label from toggling the checkbox.\n   */\n  trailing?: ReactNode;\n  disabled?: boolean;\n  /** Marks the box `aria-required` AND draws the `RequiredMark` after the label — the\n   *  same pairing every required field carries, so neither signal stands alone. */\n  required?: boolean;\n  /** Error message displayed below the block (also triggers red border) */\n  error?: string;\n  className?: string;\n}\n\nconst CheckboxBlock = forwardRef<ComponentRef<typeof CheckboxPrimitive.Root>, CheckboxBlockProps>(\n  (\n    {\n      id,\n      checked,\n      defaultChecked,\n      onCheckedChange,\n      label,\n      description,\n      truncateLabel,\n      trailing,\n      disabled,\n      required,\n      error,\n      className,\n    },\n    ref,\n  ) => (\n    <div className={cn('relative flex w-full flex-col', className)}>\n      <label\n        htmlFor={id}\n        // Same marker every field exposes — it is how a form finds (and scrolls\n        // to) the first control that failed validation.\n        data-invalid={error ? true : undefined}\n        className={cn(\n          'flex w-full items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset',\n          // Trailing content stacks full-width below the text on mobile.\n          trailing && 'flex-wrap md:flex-nowrap',\n          'p-[var(--spacing-system-sf)]',\n          !description && 'min-h-[44px] md:min-h-[48px]',\n          description && 'min-h-[60px] md:min-h-[64px]',\n          'bg-ods-card ring-ods-border',\n          'cursor-pointer transition-colors duration-200',\n          // States mirror Input: hover / active move the BACKGROUND, the ring\n          // stays put. Disabled swaps to the flat `ods-bg` fill and greys the\n          // text + the box instead of fading the whole block, so a disabled block\n          // reads the same as a disabled field. The greying itself lives on the\n          // TEXT COLUMN below, not here: as a descendant rule on the label it also\n          // repainted the checked INDICATOR (Radix renders it as a `span`, and the\n          // checkmark svg inherits `currentColor`) and whatever `trailing` renders\n          // — a `Tag`'s label is a span too, and it went invisible against its own\n          // fill in the light theme.\n          !disabled && 'hover:bg-ods-bg-hover active:bg-ods-bg-active',\n          disabled && 'cursor-not-allowed bg-ods-bg',\n          error && 'ring-ods-error',\n        )}\n      >\n        <CheckboxPrimitive.Root\n          ref={ref}\n          id={id}\n          checked={checked}\n          defaultChecked={defaultChecked}\n          onCheckedChange={onCheckedChange}\n          disabled={disabled}\n          aria-required={required || undefined}\n          aria-invalid={error ? true : undefined}\n          className={cn(\n            'h-4 w-4 shrink-0 md:h-6 md:w-6',\n            'rounded-[6px] border-2',\n            error ? 'border-ods-error' : 'border-[var(--color-border-strong)]',\n            'bg-ods-card',\n            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-card',\n            // Disabled: the EMPTY box dims with the text instead of the whole\n            // block fading. A checked box keeps its accent fill — greying it too\n            // would bury the checkmark, and \"on but locked\" has to stay readable.\n            'disabled:cursor-not-allowed',\n            'disabled:data-[state=unchecked]:border-ods-border disabled:data-[state=unchecked]:bg-ods-bg',\n            'data-[state=checked]:border-[var(--color-accent-primary)] data-[state=checked]:bg-[var(--color-accent-primary)]',\n          )}\n        >\n          <CheckboxPrimitive.Indicator className=\"flex items-center justify-center text-ods-text-on-accent\">\n            <CheckboxCheckmarkIcon className=\"h-2 w-2 md:h-2.5 md:w-2.5\" />\n          </CheckboxPrimitive.Indicator>\n        </CheckboxPrimitive.Root>\n        <div\n          className={cn(\n            'flex min-w-0 flex-1 flex-col justify-center',\n            // Direct children only — these are the two component-owned wrappers.\n            // `label` and `description` are ReactNode (rich text, inline links),\n            // so a descendant rule would repaint content the caller styled itself.\n            disabled && '[&>span]:text-ods-text-disabled',\n          )}\n        >\n          <span\n            className={cn(\n              '!leading-5 text-h4 md:!leading-6',\n              'select-none text-ods-text-primary',\n              truncateLabel ? 'block truncate' : 'break-words',\n            )}\n          >\n            {label}\n            {required && <RequiredMark />}\n          </span>\n          {description && (\n            <span className={cn('!leading-4 text-h6', 'select-none break-words text-ods-text-secondary')}>\n              {description}\n            </span>\n          )}\n        </div>\n        {trailing && <div className=\"flex w-full shrink-0 items-center md:ml-auto md:w-auto\">{trailing}</div>}\n      </label>\n      {error && (\n        <p className=\"absolute bottom-0 left-0 right-0 translate-y-full truncate text-ods-error text-h6\" title={error}>\n          {error}\n        </p>\n      )}\n    </div>\n  ),\n);\nCheckboxBlock.displayName = 'CheckboxBlock';\n\nexport { CheckboxBlock };\nexport type { CheckboxBlockProps };\n","'use client';\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\nimport { CheckboxCheckmarkIcon } from '../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon';\nimport { Label } from './label';\n\ninterface CheckboxWithDescriptionProps {\n  id: string;\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  title: string;\n  description: string;\n  disabled?: boolean;\n  className?: string;\n}\n\nconst CheckboxWithDescription = forwardRef<HTMLDivElement, CheckboxWithDescriptionProps>(\n  ({ id, checked, onCheckedChange, title, description, disabled, className }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\n        'flex items-start gap-3 rounded-lg border border-ods-border bg-ods-card p-4',\n        disabled && 'cursor-not-allowed opacity-50',\n        className,\n      )}\n    >\n      <CheckboxPrimitive.Root\n        id={id}\n        checked={checked}\n        onCheckedChange={onCheckedChange}\n        disabled={disabled}\n        className={cn(\n          'peer mt-0.5 h-5 w-5 shrink-0 rounded-sm border border-ods-border bg-ods-bg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-ods-accent data-[state=checked]:bg-ods-accent',\n        )}\n      >\n        <CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-ods-text-on-accent')}>\n          <CheckboxCheckmarkIcon size={10} />\n        </CheckboxPrimitive.Indicator>\n      </CheckboxPrimitive.Root>\n      <div className=\"flex flex-col gap-1\">\n        <Label\n          htmlFor={id}\n          spacing=\"tight\"\n          className={cn('cursor-pointer leading-none', disabled && 'cursor-not-allowed')}\n        >\n          {title}\n        </Label>\n        <span className=\"text-ods-text-secondary text-h6\">{description}</span>\n      </div>\n    </div>\n  ),\n);\nCheckboxWithDescription.displayName = 'CheckboxWithDescription';\n\nexport { CheckboxWithDescription };\nexport type { CheckboxWithDescriptionProps };\n","'use client';\n\nimport * as Popover from '@radix-ui/react-popover';\nimport { ArrowUpDown, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';\nimport { type ReactNode, useCallback, useMemo, useState } from 'react';\nimport { DayPicker, type DateRange, type DayPickerProps, type Matcher } from 'react-day-picker';\nimport { useMdUp } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { Button } from './button';\nimport { FieldWrapper } from './field-wrapper';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';\nimport type { SortDirection } from './sort-column-item';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type DatePickerMode = 'single' | 'range';\n\nexport interface DatePickerBaseProps {\n  /** Placeholder text when no date is selected */\n  placeholder?: string;\n  /** Format function for displaying the date */\n  formatDate?: (date: Date) => string;\n  /** Whether the picker is disabled */\n  disabled?: boolean;\n  /** Additional class name for the trigger button */\n  className?: string;\n  /** Number of months to display */\n  numberOfMonths?: 1 | 2;\n  /** Minimum selectable date */\n  fromDate?: Date;\n  /** Maximum selectable date */\n  toDate?: Date;\n  /** Locale for formatting */\n  locale?: DayPickerProps['locale'];\n  /** Label text displayed above the picker */\n  label?: string;\n  /** Error message displayed below the picker */\n  error?: string;\n  /** When true, renders red error border */\n  invalid?: boolean;\n}\n\nexport interface SingleDatePickerProps extends DatePickerBaseProps {\n  mode: 'single';\n  value?: Date;\n  onChange?: (date: Date | undefined) => void;\n}\n\nexport interface RangeDatePickerProps extends DatePickerBaseProps {\n  mode: 'range';\n  value?: DateRange;\n  onChange?: (range: DateRange | undefined) => void;\n}\n\nexport type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nconst defaultFormatDate = (date: Date): string => {\n  return date.toLocaleDateString('en-US', {\n    month: '2-digit',\n    day: '2-digit',\n    year: 'numeric',\n  });\n};\n\nconst formatDateRange = (range: DateRange | undefined, formatFn: (date: Date) => string): string => {\n  if (!range?.from) return '';\n  if (!range.to) return formatFn(range.from);\n  return `${formatFn(range.from)} - ${formatFn(range.to)}`;\n};\n\n// ============================================================================\n// Calendar Components\n// ============================================================================\n\ninterface CalendarNavButtonProps {\n  direction: 'left' | 'right';\n  onClick?: () => void;\n  /** Set at a `fromDate` / `toDate` bound — there is nothing selectable past it. */\n  disabled?: boolean;\n  'aria-label'?: string;\n}\n\n/** The DS icon button, at its standard size in every placement — month\n *  navigation is a primary control, not chrome to be shrunk to fit. What has\n *  to give instead is the calendar's WIDTH: a `bare` host states one wide\n *  enough for the caption to sit between the two buttons (see the meeting\n *  scheduler's `CALENDAR_W`). */\nfunction CalendarNavButton({ direction, onClick, disabled, 'aria-label': ariaLabel }: CalendarNavButtonProps) {\n  return (\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      size=\"icon\"\n      className=\"shrink-0\"\n      onClick={onClick}\n      disabled={disabled}\n      aria-label={ariaLabel}\n      leftIcon={direction === 'left' ? <ChevronLeft className=\"size-6\" /> : <ChevronRight className=\"size-6\" />}\n    />\n  );\n}\n\n// ============================================================================\n// DatePickerCalendar Component\n// ============================================================================\n\nexport interface DatePickerCalendarProps {\n  mode: DatePickerMode;\n  selected: Date | DateRange | undefined;\n  onSelect: (value: Date | DateRange | undefined) => void;\n  numberOfMonths?: 1 | 2;\n  fromDate?: Date;\n  toDate?: Date;\n  locale?: DayPickerProps['locale'];\n  /**\n   * When true the calendar fills its container width and day cells flex to\n   * fit (instead of the fixed 40px cells). Used by DateFilterMenu so the grid\n   * lines up with the surrounding controls per the Figma filter-menu.\n   */\n  fluid?: boolean;\n  /**\n   * The calendar ALONE — the Figma `Date Picker` component as drawn, with no\n   * card surface and no outer inset: the nav header flush to both edges, 8px,\n   * then a full-bleed grid. Seven columns divide the container, so the\n   * cells are as wide as the host makes them.\n   *\n   * FIXED 272px tall at every one of those widths (8 + the 48px icon-button\n   * header + a 24px weekday strip + six 32px rows; 268 on a phone, where the\n   * DS icon button is 44), which is the design's own contract — the\n   * component is drawn 240 tall at 340, 282 and 240 wide. Height that ignored\n   * the width is what lets a host place it in a card that states a height, and\n   * keeps a phone-width month from becoming a 356px band.\n   *\n   * The DAY inside each cell stays square, so the selection and today's tint\n   * read as rounded squares rather than as pills stretched across a wide\n   * cell.\n   *\n   * For hosts that place the calendar inside a panel they already own — the\n   * meeting scheduler — where the standalone card chrome reads as a card\n   * inside a card and its 16px inset fights the panel's own 24px one. The\n   * popover and filter-menu keep the surface; nothing about the type, colours\n   * or day states changes between the two.\n   */\n  bare?: boolean;\n  /**\n   * Days to disable ON TOP of the `fromDate`/`toDate` bounds — a matcher, so a\n   * host can disable by predicate. \"Inside the allowed window\" and \"actually\n   * selectable\" are different questions, and only the host can answer the\n   * second (the meeting scheduler greys out days with no bookable slot).\n   */\n  disabledDays?: Matcher | Matcher[];\n  /**\n   * Controlled visible month; pair with `onMonthChange`. Omit both and the\n   * calendar owns its month, which is the right default — a host needs these\n   * only when paging is ALSO a data event, e.g. the scheduler refetches\n   * availability per month and would otherwise show an unpopulated month.\n   */\n  month?: Date;\n  onMonthChange?: (month: Date) => void;\n}\n\n/**\n * The calendar surface behind every date control in the design system —\n * `DatePicker`'s popover and `DateFilterMenu` — exported so a host that needs\n * an always-visible day grid renders THIS one. A second hand-styled day grid\n * is how the two drift apart.\n */\nexport function DatePickerCalendar({\n  mode,\n  selected,\n  onSelect,\n  numberOfMonths = 1,\n  fromDate,\n  toDate,\n  locale,\n  fluid = false,\n  bare = false,\n  disabledDays: extraDisabledDays,\n  month: monthProp,\n  onMonthChange,\n}: DatePickerCalendarProps) {\n  const today = new Date();\n\n  const isMdUp = useMdUp() ?? true;\n  const monthsToShow = isMdUp ? numberOfMonths : 1;\n\n  const [draftRange, setDraftRange] = useState<DateRange | undefined>(\n    mode === 'range' ? (selected as DateRange | undefined) : undefined,\n  );\n  const [hoveredDate, setHoveredDate] = useState<Date | undefined>(undefined);\n\n  // Keep the internal range in sync when the consumer changes `selected`\n  // externally (e.g. DateFilterMenu Reset while the popover stays open).\n  // Adjusted while rendering — React's documented prop-sync pattern — rather\n  // than from an effect: the calendar highlights `draftRange` in THIS render,\n  // so a Reset done from an effect painted the old selection one more time\n  // before clearing it.\n  const [syncedSelection, setSyncedSelection] = useState({ mode, selected });\n  if (syncedSelection.mode !== mode || syncedSelection.selected !== selected) {\n    setSyncedSelection({ mode, selected });\n    if (mode === 'range') {\n      setDraftRange(selected as DateRange | undefined);\n    }\n  }\n\n  const rangeSelected = draftRange;\n  const hasCompleteRange =\n    mode === 'range' &&\n    !!rangeSelected?.from &&\n    !!rangeSelected?.to &&\n    rangeSelected.from.getTime() !== rangeSelected.to.getTime();\n\n  const isPreviewDate = (date: Date): boolean => {\n    if (!draftRange?.from || draftRange.to || !hoveredDate) return false;\n    const start = Math.min(draftRange.from.getTime(), hoveredDate.getTime());\n    const end = Math.max(draftRange.from.getTime(), hoveredDate.getTime());\n    return date.getTime() >= start && date.getTime() <= end;\n  };\n\n  const handleRangeSelect = (triggerDate: Date | undefined): void => {\n    if (!triggerDate) return;\n\n    if (!draftRange?.from || draftRange.to) {\n      // First click starts the range — propagate it so consumers can already\n      // act on a single-day selection (e.g. DateFilterMenu Apply/Reset).\n      const started: DateRange = { from: triggerDate, to: undefined };\n      setDraftRange(started);\n      onSelect(started);\n      return;\n    }\n    // Second click closes the range, ordering the two ends.\n    const start = draftRange.from;\n    const completed: DateRange =\n      triggerDate.getTime() < start.getTime() ? { from: triggerDate, to: start } : { from: start, to: triggerDate };\n    setDraftRange(completed);\n    setHoveredDate(undefined);\n    onSelect(completed);\n  };\n\n  // Fixed 40px cells by default; in fluid mode cells flex to fill the width.\n  // `bare` is a fluid layout by definition — its cell size IS container/7.\n  const isFluid = fluid || bare;\n  // BARE: fixed row height, elastic width — the Figma component is 240px tall\n  // at every width it is drawn at (340 on a phone, 282 on a tablet, 240 on the\n  // desktop card), so a month occupies the same band whatever column it lands\n  // in. 8 + the 48px header + a 24px weekday strip + six 32px rows = 272.\n  //\n  // Height that does NOT follow the width is the whole point: seven square\n  // cells across a phone's 340px would be a 356px-tall month, half again what\n  // the design budgets, and the same rule handed a wide column a month tall\n  // enough to push a card that states its height.\n  const cellOuter = bare ? 'flex-1 min-w-0 h-[32px]' : isFluid ? 'flex-1 aspect-square min-w-0' : 'size-10';\n  // ...but the DAY ITSELF stays square inside that wider cell: `h-full` gives\n  // the button the row's height and `aspect-square` turns it into a 32x32\n  // rounded square, centred by the cell. `w-auto` is load-bearing — it drops\n  // the width the button would otherwise take, leaving `aspect-square` inert.\n  //\n  // This is why the day STATES below are painted on the button in bare mode\n  // rather than on the cell: react-day-picker puts `selected`/`today` on the\n  // cell, and a cell 48px wide by 30 tall renders the selection as a stretched\n  // pill. Same colours, same radius, smaller box.\n  const cellInner = bare ? 'h-full w-auto aspect-square rounded-[6px]' : isFluid ? 'size-full' : 'size-10';\n  // The weekday strip is a LABEL row, not a day: `bare` gives it the height of\n  // its own text rather than a full row.\n  const weekdayOuter = bare ? 'flex-1 min-w-0 h-6' : cellOuter;\n\n  // Day states, per paint target (see `cellInner`). Range mode is never bare —\n  // a range MUST fill its cells or the middle would break into islands — so\n  // only these three single-day states fork.\n  const hoverClass = bare ? '[&>button]:hover:bg-ods-bg-surface' : 'hover:bg-ods-bg-surface hover:rounded-[6px]';\n  const todayClass = bare\n    ? '[&>button]:bg-ods-bg-surface [&>button]:hover:!bg-ods-bg-surface'\n    : 'bg-ods-bg-surface rounded-[6px] hover:!bg-ods-bg-surface';\n  /**\n   * Today's tint applies only while today is NOT part of the selection.\n   *\n   * Both states paint the same box, and both have to use `!important` to beat\n   * react-day-picker's concatenated base classes — so on hover the two rules\n   * had equal specificity and the winner came down to their order in the\n   * generated stylesheet. Today's tint won, and a selected today turned GREY\n   * under the pointer, as if it had been deselected.\n   *\n   * Deciding it here rather than in CSS: \"today\" is a hint about where you\n   * are in the month, and once that day is chosen the selection is the\n   * stronger statement — there is nothing left for the tint to say.\n   */\n  const dayNumber = (date: Date): number => date.getFullYear() * 10000 + date.getMonth() * 100 + date.getDate();\n  const isSelectedDay = (date: Date): boolean => {\n    const n = dayNumber(date);\n    if (mode === 'single') {\n      const picked = selected as Date | undefined;\n      return !!picked && dayNumber(picked) === n;\n    }\n    const range = rangeSelected;\n    if (!range?.from) return false;\n    if (!range.to) return dayNumber(range.from) === n;\n    return n >= dayNumber(range.from) && n <= dayNumber(range.to);\n  };\n  const isUnclaimedToday = (date: Date): boolean => dayNumber(date) === dayNumber(today) && !isSelectedDay(date);\n\n  const selectedClass = bare\n    ? '[&>button]:!bg-ods-accent [&>button]:!text-ods-card [&>button]:!font-bold [&>button]:hover:!bg-ods-accent'\n    : '!bg-ods-accent !text-ods-card !font-bold !rounded-[6px] hover:!bg-ods-accent';\n\n  // Surface + inset, per placement. Bare drops the card and the 16px inset\n  // (the host's panel provides both) and replaces the inset with the 8px the\n  // design puts between the header and the grid — the ONLY spacing it keeps.\n  const surfaceClass = bare\n    ? 'flex w-full flex-col'\n    : cn('overflow-hidden rounded-[6px] border border-ods-border bg-ods-card', isFluid && 'w-full');\n  // Bare: an unpadded header row over the grid, 8px apart — the calendar's\n  // whole height is the two of them added up, never the container's.\n  const captionRowClass = cn('flex items-center justify-between gap-1', bare ? 'shrink-0' : 'px-4 pt-4');\n  // The caption is the only elastic thing in the header: it takes the space\n  // the two fixed buttons leave and truncates rather than pushing into them.\n  // \"September 2026\" is ~131px at text-h4, and the two 48px buttons plus their\n  // gaps take 104 — so a `bare` host has to state a width of ~256 or more, or\n  // the label and the next-month button collide (worse in a locale with longer\n  // month names).\n  const captionClass = 'min-w-0 flex-1 truncate text-center text-h4 text-ods-text-primary';\n\n  const classNames: DayPickerProps['classNames'] = {\n    root: cn(\n      'date-picker-calendar',\n      bare ? 'flex w-full flex-col pt-[var(--spacing-system-xsf)]' : cn('p-4', isFluid && 'w-full'),\n    ),\n    months: cn('flex gap-8', bare && 'w-full'),\n    month: cn('flex flex-col', bare ? 'w-full' : cn('gap-2', isFluid && 'w-full')),\n    month_caption: 'hidden',\n    nav: 'hidden',\n    month_grid: cn('border-collapse', bare ? 'flex w-full flex-col' : isFluid && 'w-full'),\n    weekdays: 'flex',\n    ...(bare ? { weeks: 'flex flex-col' } : {}),\n    weekday: cn(weekdayOuter, 'flex items-center justify-center', 'text-ods-text-secondary text-h6'),\n    week: 'flex',\n    day: cn(\n      cellOuter,\n      'flex items-center justify-center',\n      'text-ods-text-primary text-h4',\n      'cursor-pointer',\n      'transition-colors duration-150',\n      hoverClass,\n    ),\n    day_button: cn(\n      cellInner,\n      'flex items-center justify-center',\n      'cursor-pointer border-none bg-transparent outline-none',\n      'font-inherit text-inherit',\n    ),\n    // Painted through the `todayTint` modifier instead — see `isUnclaimedToday`.\n    today: '',\n    selected: cn(\n      selectedClass,\n      // In range mode, selected class should not override range_start/range_end/range_middle\n      mode === 'range' && 'range-selected',\n    ),\n    outside: 'text-ods-border opacity-50 hover:!bg-transparent',\n    // Out of bounds: greyed, un-hoverable, and not-allowed under the cursor.\n    //\n    // Every rule here is `!`, and the inner button is targeted explicitly —\n    // neither is decoration. react-day-picker CONCATENATES the modifier's classes\n    // onto the base `day` ones (no tailwind-merge), so `text-ods-text-disabled`\n    // and `text-ods-text-primary` both land on the cell and the winner comes down\n    // to their order in the generated stylesheet, not in the attribute; without\n    // `!` the disabled day rendered in full white, indistinguishable from a\n    // selectable one. The cursor needs the child selector on top of that: it is\n    // the `day_button` INSIDE the cell that carries `cursor-pointer`, and a\n    // `disabled` <button> does not inherit the cell's cursor on its own.\n    disabled: cn(\n      '!cursor-not-allowed !text-ods-text-disabled hover:!bg-transparent',\n      '[&>button]:!cursor-not-allowed [&>button]:!text-ods-text-disabled',\n      // Also drop the `today` tint, or an unselectable today renders BLANK:\n      // `--color-text-disabled` and `--color-bg-surface` are the SAME token\n      // (`--ods-system-greys-soft-grey`), so the number disappears into the\n      // highlight — grey on identical grey. Reachable by any picker whose\n      // `fromDate` is in the future, and permanently by the meeting scheduler,\n      // where today usually has no bookable slots left.\n      //\n      // Dropping the tint rather than repainting the text: a disabled day is\n      // not actionable, so a highlight claiming otherwise is the wrong signal.\n      // The day still reads as today's position in the month by being the one\n      // dimmed cell between yesterday and tomorrow.\n      '!bg-transparent [&>button]:!bg-transparent',\n    ),\n    hidden: 'invisible',\n    // Range styles matching Figma design:\n    // - range_start: bright yellow #ffc008, bold, left radius (full radius if single selection)\n    // - range_end: bright yellow #ffc008, bold, right radius\n    // - range_middle: dark yellow var(--ods-open-yellow-light), medium weight text\n    // Border radius on row edges is handled via CSS in the style tag below\n    range_start: cn(\n      'range-start !bg-ods-accent !font-bold !text-ods-card hover:!bg-ods-accent',\n      hasCompleteRange ? '!rounded-l-[6px] !rounded-r-none' : '!rounded-[6px]',\n    ),\n    range_end: cn(\n      'range-end !bg-ods-accent !font-bold !text-ods-card hover:!bg-ods-accent',\n      hasCompleteRange ? '!rounded-l-none !rounded-r-[6px]' : '!rounded-[6px]',\n    ),\n    range_middle:\n      'range-middle !bg-ods-open-yellow-light !text-ods-card !font-medium !rounded-none hover:!bg-ods-open-yellow-light',\n  };\n\n  // Controlled when the host passes `month`, self-owned otherwise. The\n  // internal state is kept either way so an uncontrolled calendar still works;\n  // controlled it is simply never read, which keeps `changeMonth` one path.\n  const [uncontrolledMonth, setUncontrolledMonth] = useState<Date>(\n    mode === 'single' ? (selected as Date) || today : (selected as DateRange)?.from || today,\n  );\n  const month = monthProp ?? uncontrolledMonth;\n  const changeMonth = (next: Date): void => {\n    setUncontrolledMonth(next);\n    onMonthChange?.(next);\n  };\n\n  /**\n   * `fromDate` / `toDate` → what react-day-picker **v9** actually understands.\n   *\n   * v8 took the bounds as props of those names and both disabled the days\n   * outside them and stopped the navigation there. v9 removed them: they are\n   * typed `@deprecated … has been removed` and the runtime never reads them —\n   * so forwarding them, as this calendar did after the v8 → v9 upgrade, silently\n   * dropped every bound. A \"next 30 days\" picker happily accepted last year, and\n   * the `disabled` class already defined in `classNames` below never applied to\n   * anything. Same shape of miss as the v8 `classNames` keys called out in\n   * `calendar.tsx` — the other half of that migration.\n   *\n   * The public API is unchanged: consumers keep passing `fromDate`/`toDate`, and\n   * this is the single place either is translated.\n   */\n  const disabledDays = useMemo<Matcher[]>(() => {\n    const matchers: Matcher[] = [];\n    if (fromDate) matchers.push({ before: fromDate });\n    if (toDate) matchers.push({ after: toDate });\n    if (extraDisabledDays) {\n      matchers.push(...(Array.isArray(extraDisabledDays) ? extraDisabledDays : [extraDisabledDays]));\n    }\n    return matchers;\n  }, [fromDate, toDate, extraDisabledDays]);\n\n  // Out-of-bounds days are disabled, not hidden — a month keeps its shape, and\n  // the greyed-out day is what tells the user the bound exists. Navigation stops\n  // at the bound too (v8 did the same): past it there is nothing selectable, so\n  // paging further only walks through dead months. The nav is this component's\n  // own — `hideNavigation` turns off DayPicker's — so the limit is applied here\n  // rather than through `startMonth`/`endMonth`.\n  // Compared as a month ORDINAL, not as dates: the bound falls on some day of\n  // its month, and that month must stay reachable — a `fromDate` of the 20th\n  // still has to let the user page back to that month to pick the 25th.\n  const monthIndex = (date: Date): number => date.getFullYear() * 12 + date.getMonth();\n  const lastVisibleMonth = monthIndex(month) + (monthsToShow === 2 ? 1 : 0);\n  const canGoPrevious = !fromDate || monthIndex(month) > monthIndex(fromDate);\n  const canGoNext = !toDate || lastVisibleMonth < monthIndex(toDate);\n\n  const shiftMonth = (by: number) => {\n    const next = new Date(month);\n    next.setMonth(next.getMonth() + by);\n    changeMonth(next);\n  };\n  const handlePreviousMonth = () => shiftMonth(-1);\n  const handleNextMonth = () => shiftMonth(1);\n\n  const formatMonthYear = (date: Date): string => {\n    return date.toLocaleDateString('en-US', {\n      month: 'long',\n      year: 'numeric',\n    });\n  };\n\n  const getSecondMonth = (date: Date): Date => {\n    const next = new Date(date);\n    next.setMonth(next.getMonth() + 1);\n    return next;\n  };\n\n  // CSS for range styles:\n  // - Ensure range_middle overrides selected styles (darker yellow for middle dates)\n  // - Add row-edge border radius on range middle dates\n  const rangeStyles = `\n    .date-picker-calendar .range-middle {\n      background-color: var(--ods-open-yellow-light) !important;\n      font-weight: 500 !important;\n    }\n    .date-picker-calendar .range-middle:hover {\n      background-color: var(--ods-open-yellow-light) !important;\n    }\n    .date-picker-calendar .range-start,\n    .date-picker-calendar .range-end {\n      background-color: var(--color-accent-primary) !important;\n      font-weight: 700 !important;\n    }\n    .date-picker-calendar .range-start:hover,\n    .date-picker-calendar .range-end:hover {\n      background-color: var(--color-accent-primary) !important;\n    }\n    .date-picker-calendar .range-middle:first-child {\n      border-top-left-radius: 6px !important;\n      border-bottom-left-radius: 6px !important;\n    }\n    .date-picker-calendar .range-middle:last-child {\n      border-top-right-radius: 6px !important;\n      border-bottom-right-radius: 6px !important;\n    }\n  `;\n\n  if (mode === 'single') {\n    return (\n      <div className={surfaceClass}>\n        <div className={captionRowClass}>\n          <CalendarNavButton\n            direction=\"left\"\n            onClick={handlePreviousMonth}\n            disabled={!canGoPrevious}\n            aria-label=\"Previous month\"\n          />\n          <span className={captionClass}>{formatMonthYear(month)}</span>\n          <CalendarNavButton\n            direction=\"right\"\n            onClick={handleNextMonth}\n            disabled={!canGoNext}\n            aria-label=\"Next month\"\n          />\n        </div>\n        <DayPicker\n          mode=\"single\"\n          selected={selected as Date | undefined}\n          onSelect={date => onSelect(date)}\n          month={month}\n          onMonthChange={changeMonth}\n          classNames={classNames}\n          modifiers={{ todayTint: isUnclaimedToday }}\n          modifiersClassNames={{ todayTint: todayClass }}\n          showOutsideDays\n          fixedWeeks\n          disabled={disabledDays}\n          locale={locale}\n          hideNavigation\n        />\n      </div>\n    );\n  }\n\n  // Range mode\n  return (\n    <div\n      className={cn('overflow-hidden rounded-md border border-ods-border bg-ods-card', isFluid && 'w-full')}\n      onMouseLeave={() => setHoveredDate(undefined)}\n    >\n      <style>{rangeStyles}</style>\n      <div className=\"flex\">\n        {/* First month */}\n        <div className=\"flex-1\">\n          <div className=\"flex items-center justify-between px-4 pt-4\">\n            <CalendarNavButton\n              direction=\"left\"\n              onClick={handlePreviousMonth}\n              disabled={!canGoPrevious}\n              aria-label=\"Previous month\"\n            />\n            <span className={captionClass}>{formatMonthYear(month)}</span>\n            {monthsToShow === 1 && (\n              <CalendarNavButton\n                direction=\"right\"\n                onClick={handleNextMonth}\n                disabled={!canGoNext}\n                aria-label=\"Next month\"\n              />\n            )}\n            {monthsToShow === 2 && <div className=\"size-10 md:size-12\" />}\n          </div>\n          <DayPicker\n            mode=\"range\"\n            selected={draftRange}\n            onSelect={(_range, triggerDate) => handleRangeSelect(triggerDate)}\n            onDayMouseEnter={day => setHoveredDate(day)}\n            modifiers={{ preview: isPreviewDate, todayTint: isUnclaimedToday }}\n            modifiersClassNames={{ preview: 'bg-ods-bg-surface', todayTint: todayClass }}\n            month={month}\n            onMonthChange={changeMonth}\n            classNames={classNames}\n            showOutsideDays\n            fixedWeeks\n            disabled={disabledDays}\n            locale={locale}\n            hideNavigation\n          />\n        </div>\n\n        {/* Second month (if monthsToShow === 2) */}\n        {monthsToShow === 2 && (\n          <div className=\"flex-1 border-l border-ods-border\">\n            <div className=\"flex items-center justify-between px-4 pt-4\">\n              <div className=\"size-10 md:size-12\" />\n              <span className={captionClass}>{formatMonthYear(getSecondMonth(month))}</span>\n              <CalendarNavButton\n                direction=\"right\"\n                onClick={handleNextMonth}\n                disabled={!canGoNext}\n                aria-label=\"Next month\"\n              />\n            </div>\n            <DayPicker\n              mode=\"range\"\n              selected={draftRange}\n              onSelect={(_range, triggerDate) => handleRangeSelect(triggerDate)}\n              onDayMouseEnter={day => setHoveredDate(day)}\n              modifiers={{ preview: isPreviewDate, todayTint: isUnclaimedToday }}\n              modifiersClassNames={{ preview: 'bg-ods-bg-surface', todayTint: todayClass }}\n              month={getSecondMonth(month)}\n              classNames={classNames}\n              showOutsideDays\n              fixedWeeks\n              disabled={disabledDays}\n              locale={locale}\n              hideNavigation\n            />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n// ============================================================================\n// Trigger button styles (shared)\n// ============================================================================\n\nconst triggerButtonStyles = cn(\n  // Layout\n  'flex w-full items-center gap-2',\n  'h-11 rounded-[6px] border px-3 md:h-12',\n  // Colors\n  'border-ods-border bg-ods-card',\n  // Typography\n  'text-h4',\n  // Hover & active (not disabled)\n  'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',\n  'focus:outline-none',\n  // Disabled - match Input exactly. The value/placeholder span inside sets its\n  // own colour, so the child rule (higher specificity) greys it too. Scoped to\n  // DIRECT children like every other field, so nested content that owns its\n  // colour keeps it.\n  'disabled:!cursor-not-allowed disabled:bg-ods-bg',\n  'disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled',\n  // Animation\n  'transition-colors duration-200',\n);\n\nconst timeSelectTriggerStyles = cn(\n  'flex items-center justify-between gap-1',\n  'h-11 min-h-0 rounded-[6px] border px-3 md:h-12',\n  'border-ods-border bg-ods-card',\n  'text-h4',\n  'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',\n  'focus:outline-none',\n  'disabled:!cursor-not-allowed disabled:bg-ods-bg',\n  'disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled',\n  'cursor-pointer transition-colors duration-200',\n  'text-ods-text-primary',\n);\n\n// ============================================================================\n// DatePicker Component\n// ============================================================================\n\nexport function DatePicker(props: DatePickerProps) {\n  const {\n    placeholder = 'Select date',\n    formatDate = defaultFormatDate,\n    disabled = false,\n    className,\n    numberOfMonths = 1,\n    fromDate,\n    toDate,\n    locale,\n    label,\n    error,\n    invalid = false,\n  } = props;\n\n  const [open, setOpen] = useState(false);\n  const isInvalid = invalid || !!error;\n\n  const displayValue = useMemo(() => {\n    if (props.mode === 'single') {\n      return props.value ? formatDate(props.value) : '';\n    }\n    return formatDateRange(props.value, formatDate);\n  }, [props.mode, props.value, formatDate]);\n\n  const handleSelect = (value: Date | DateRange | undefined) => {\n    if (props.mode === 'single') {\n      props.onChange?.(value as Date | undefined);\n      if (value) {\n        setOpen(false);\n      }\n    } else {\n      const range = value as DateRange | undefined;\n      props.onChange?.(range);\n      if (range?.from && range?.to) {\n        setOpen(false);\n      }\n    }\n  };\n\n  const picker = (\n    <Popover.Root open={open} onOpenChange={setOpen}>\n      <Popover.Trigger asChild>\n        <button\n          type=\"button\"\n          disabled={disabled}\n          // Same marker Input/Textarea/Select expose — it is how a form finds\n          // (and scrolls to) the first field that failed validation.\n          data-invalid={isInvalid || undefined}\n          className={cn(\n            triggerButtonStyles,\n            'group',\n            open && !isInvalid && 'border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card',\n            isInvalid && 'border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card',\n            className,\n          )}\n        >\n          <Calendar className=\"size-6 shrink-0 text-ods-text-secondary\" />\n          <span\n            className={cn(\n              'flex-1 truncate text-left',\n              displayValue ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n            )}\n          >\n            {displayValue || placeholder}\n          </span>\n        </button>\n      </Popover.Trigger>\n\n      <Popover.Portal>\n        <Popover.Content\n          className={cn(\n            'z-[9999]',\n            'animate-in fade-in-0 zoom-in-95',\n            'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n            'data-[side=bottom]:slide-in-from-top-2',\n            'data-[side=top]:slide-in-from-bottom-2',\n          )}\n          sideOffset={8}\n          align=\"start\"\n        >\n          <DatePickerCalendar\n            mode={props.mode}\n            selected={props.value}\n            onSelect={handleSelect}\n            numberOfMonths={numberOfMonths}\n            fromDate={fromDate}\n            toDate={toDate}\n            locale={locale}\n          />\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n\n  return (\n    <FieldWrapper label={label} error={error}>\n      {picker}\n    </FieldWrapper>\n  );\n}\n\n// ============================================================================\n// DatePickerInput Component (with time selector styling from Figma)\n// ============================================================================\n\nexport interface DatePickerInputProps extends DatePickerBaseProps {\n  mode?: 'single';\n  value?: Date;\n  onChange?: (date: Date | undefined) => void;\n  /** Show time selector next to date */\n  showTime?: boolean;\n  /** Use 24-hour format instead of 12-hour */\n  use24HourFormat?: boolean;\n}\n\n// Generate hour options\nconst generateHourOptions = (use24Hour: boolean): string[] => {\n  if (use24Hour) {\n    return Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));\n  }\n  return Array.from({ length: 12 }, (_, i) => (i + 1).toString().padStart(2, '0'));\n};\n\n// Generate minute options (00, 01, 02, ... 59)\nconst generateMinuteOptions = (): string[] => {\n  return Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));\n};\n\nexport function DatePickerInput({\n  placeholder = 'Select date',\n  formatDate = defaultFormatDate,\n  disabled = false,\n  className,\n  numberOfMonths = 1,\n  fromDate,\n  toDate,\n  locale,\n  value,\n  onChange,\n  showTime = false,\n  use24HourFormat = false,\n  label,\n  error,\n  invalid = false,\n}: DatePickerInputProps) {\n  const [open, setOpen] = useState(false);\n  const isInvalid = invalid || !!error;\n\n  const displayValue = value ? formatDate(value) : '';\n\n  // Extract time from value\n  const hour = useMemo(() => {\n    if (!value) return '';\n    const hours = value.getHours();\n    if (use24HourFormat) {\n      return hours.toString().padStart(2, '0');\n    }\n    const hour12 = hours % 12 || 12;\n    return hour12.toString().padStart(2, '0');\n  }, [value, use24HourFormat]);\n\n  const minute = useMemo(() => {\n    if (!value) return '';\n    return value.getMinutes().toString().padStart(2, '0');\n  }, [value]);\n\n  const period = useMemo((): 'AM' | 'PM' => {\n    if (!value) return 'AM';\n    return value.getHours() >= 12 ? 'PM' : 'AM';\n  }, [value]);\n\n  const handleSelect = (date: Date | DateRange | undefined) => {\n    const newDate = date as Date | undefined;\n    if (newDate && value) {\n      // Preserve time when selecting a new date\n      newDate.setHours(value.getHours(), value.getMinutes(), 0, 0);\n    }\n    onChange?.(newDate);\n    if (newDate) {\n      setOpen(false);\n    }\n  };\n\n  const handleHourChange = (newHour: string) => {\n    const date = value ? new Date(value) : new Date();\n    let hours = parseInt(newHour, 10);\n    if (!use24HourFormat) {\n      const isPM = period === 'PM';\n      if (hours === 12) {\n        hours = isPM ? 12 : 0;\n      } else {\n        hours = isPM ? hours + 12 : hours;\n      }\n    }\n    date.setHours(hours);\n    onChange?.(date);\n  };\n\n  const handleMinuteChange = (newMinute: string) => {\n    const date = value ? new Date(value) : new Date();\n    date.setMinutes(parseInt(newMinute, 10));\n    onChange?.(date);\n  };\n\n  const handlePeriodChange = (newPeriod: 'AM' | 'PM') => {\n    const date = value ? new Date(value) : new Date();\n    let hours = date.getHours();\n    if (newPeriod === 'AM' && hours >= 12) {\n      hours -= 12;\n    } else if (newPeriod === 'PM' && hours < 12) {\n      hours += 12;\n    }\n    date.setHours(hours);\n    onChange?.(date);\n  };\n\n  const hourOptions = useMemo(() => generateHourOptions(use24HourFormat), [use24HourFormat]);\n  const minuteOptions = useMemo(() => generateMinuteOptions(), []);\n\n  const content = (\n    <div className={cn('flex items-center gap-2', !label && !error && className)}>\n      {/* Date Picker */}\n      <Popover.Root open={open} onOpenChange={setOpen}>\n        <Popover.Trigger asChild>\n          <button\n            type=\"button\"\n            disabled={disabled}\n            // Same marker Input/Textarea/Select expose — it is how a form finds\n            // (and scrolls to) the first field that failed validation.\n            data-invalid={isInvalid || undefined}\n            className={cn(\n              triggerButtonStyles,\n              'group',\n              open && !isInvalid && 'border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card',\n              isInvalid && 'border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card',\n              'flex-1',\n            )}\n          >\n            <Calendar className=\"size-6 shrink-0 text-ods-text-secondary\" />\n            <span\n              className={cn(\n                'flex-1 truncate text-left',\n                displayValue ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n              )}\n            >\n              {displayValue || placeholder}\n            </span>\n          </button>\n        </Popover.Trigger>\n\n        <Popover.Portal>\n          <Popover.Content\n            className={cn(\n              'z-[9999]',\n              'animate-in fade-in-0 zoom-in-95',\n              'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n              'data-[side=bottom]:slide-in-from-top-2',\n              'data-[side=top]:slide-in-from-bottom-2',\n            )}\n            sideOffset={8}\n            align=\"start\"\n          >\n            <DatePickerCalendar\n              mode=\"single\"\n              selected={value}\n              onSelect={handleSelect}\n              numberOfMonths={numberOfMonths}\n              fromDate={fromDate}\n              toDate={toDate}\n              locale={locale}\n            />\n          </Popover.Content>\n        </Popover.Portal>\n      </Popover.Root>\n\n      {/* Time Selects (optional) */}\n      {showTime && (\n        <div className=\"flex items-center gap-1\">\n          {/* Hour Select */}\n          <Select value={hour} onValueChange={handleHourChange} disabled={disabled}>\n            <SelectTrigger className={cn(timeSelectTriggerStyles, 'w-[80px]')}>\n              <SelectValue placeholder=\"HH\" />\n            </SelectTrigger>\n            <SelectContent>\n              {hourOptions.map(h => (\n                <SelectItem key={h} value={h}>\n                  {h}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n\n          <span className=\"text-ods-text-secondary text-h4\">:</span>\n\n          {/* Minute Select */}\n          <Select value={minute} onValueChange={handleMinuteChange} disabled={disabled}>\n            <SelectTrigger className={cn(timeSelectTriggerStyles, 'w-[80px]')}>\n              <SelectValue placeholder=\"MM\" />\n            </SelectTrigger>\n            <SelectContent>\n              {minuteOptions.map(m => (\n                <SelectItem key={m} value={m}>\n                  {m}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n\n          {/* AM/PM Select (only for 12-hour format) */}\n          {!use24HourFormat && (\n            <Select value={period} onValueChange={val => handlePeriodChange(val as 'AM' | 'PM')} disabled={disabled}>\n              <SelectTrigger className={cn(timeSelectTriggerStyles, 'w-[80px]')}>\n                <SelectValue placeholder=\"AM\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"AM\">AM</SelectItem>\n                <SelectItem value=\"PM\">PM</SelectItem>\n              </SelectContent>\n            </Select>\n          )}\n        </div>\n      )}\n    </div>\n  );\n\n  return (\n    <FieldWrapper label={label} error={error} className={className}>\n      {content}\n    </FieldWrapper>\n  );\n}\n\n// ============================================================================\n// DatePickerInputSimple Component (with single time selector)\n// ============================================================================\n\nexport interface DatePickerInputSimpleProps extends DatePickerBaseProps {\n  mode?: 'single';\n  value?: Date;\n  onChange?: (date: Date | undefined) => void;\n  /** Show time selector next to date */\n  showTime?: boolean;\n  /** Time interval in minutes (default: 30) */\n  timeInterval?: number;\n  /** Use 24-hour format instead of 12-hour */\n  use24HourFormat?: boolean;\n}\n\n// Generate time options with specified interval\nconst generateTimeOptions = (intervalMinutes: number, use24Hour: boolean): { value: string; label: string }[] => {\n  const options: { value: string; label: string }[] = [];\n  const totalMinutesInDay = 24 * 60;\n\n  for (let minutes = 0; minutes < totalMinutesInDay; minutes += intervalMinutes) {\n    const hours = Math.floor(minutes / 60);\n    const mins = minutes % 60;\n\n    const value = `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;\n\n    let label: string;\n    if (use24Hour) {\n      label = value;\n    } else {\n      const hour12 = hours % 12 || 12;\n      const period = hours >= 12 ? 'PM' : 'AM';\n      label = `${hour12.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')} ${period}`;\n    }\n\n    options.push({ value, label });\n  }\n\n  return options;\n};\n\nexport function DatePickerInputSimple({\n  placeholder = 'Select date',\n  formatDate = defaultFormatDate,\n  disabled = false,\n  className,\n  numberOfMonths = 1,\n  fromDate,\n  toDate,\n  locale,\n  value,\n  onChange,\n  showTime = false,\n  timeInterval = 30,\n  use24HourFormat = false,\n  label,\n  error,\n  invalid = false,\n}: DatePickerInputSimpleProps) {\n  const [open, setOpen] = useState(false);\n  const isInvalid = invalid || !!error;\n\n  const displayValue = value ? formatDate(value) : '';\n\n  // Get current time value as HH:MM string\n  const timeValue = useMemo(() => {\n    if (!value) return '';\n    const hours = value.getHours();\n    const minutes = value.getMinutes();\n    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;\n  }, [value]);\n\n  // Get display label for current time\n  const timeDisplayLabel = useMemo(() => {\n    if (!value) return '';\n    const hours = value.getHours();\n    const minutes = value.getMinutes();\n\n    if (use24HourFormat) {\n      return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;\n    }\n\n    const hour12 = hours % 12 || 12;\n    const period = hours >= 12 ? 'PM' : 'AM';\n    return `${hour12.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')} ${period}`;\n  }, [value, use24HourFormat]);\n\n  const handleSelect = (date: Date | DateRange | undefined) => {\n    const newDate = date as Date | undefined;\n    if (newDate && value) {\n      // Preserve time when selecting a new date\n      newDate.setHours(value.getHours(), value.getMinutes(), 0, 0);\n    }\n    onChange?.(newDate);\n    if (newDate) {\n      setOpen(false);\n    }\n  };\n\n  const handleTimeChange = (newTime: string) => {\n    const [hours, minutes] = newTime.split(':').map(Number);\n    const date = value ? new Date(value) : new Date();\n    date.setHours(hours, minutes, 0, 0);\n    onChange?.(date);\n  };\n\n  const timeOptions = useMemo(\n    () => generateTimeOptions(timeInterval, use24HourFormat),\n    [timeInterval, use24HourFormat],\n  );\n\n  const content = (\n    <div className={cn('flex items-center gap-2', !label && !error && className)}>\n      {/* Date Picker */}\n      <Popover.Root open={open} onOpenChange={setOpen}>\n        <Popover.Trigger asChild>\n          <button\n            type=\"button\"\n            disabled={disabled}\n            // Same marker Input/Textarea/Select expose — it is how a form finds\n            // (and scrolls to) the first field that failed validation.\n            data-invalid={isInvalid || undefined}\n            className={cn(\n              triggerButtonStyles,\n              'group',\n              open && !isInvalid && 'border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card',\n              isInvalid && 'border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card',\n              'flex-1',\n            )}\n          >\n            <Calendar className=\"size-6 shrink-0 text-ods-text-secondary\" />\n            <span\n              className={cn(\n                'flex-1 truncate text-left',\n                displayValue ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n              )}\n            >\n              {displayValue || placeholder}\n            </span>\n          </button>\n        </Popover.Trigger>\n\n        <Popover.Portal>\n          <Popover.Content\n            className={cn(\n              'z-[9999]',\n              'animate-in fade-in-0 zoom-in-95',\n              'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n              'data-[side=bottom]:slide-in-from-top-2',\n              'data-[side=top]:slide-in-from-bottom-2',\n            )}\n            sideOffset={8}\n            align=\"start\"\n          >\n            <DatePickerCalendar\n              mode=\"single\"\n              selected={value}\n              onSelect={handleSelect}\n              numberOfMonths={numberOfMonths}\n              fromDate={fromDate}\n              toDate={toDate}\n              locale={locale}\n            />\n          </Popover.Content>\n        </Popover.Portal>\n      </Popover.Root>\n\n      {/* Single Time Select (optional) */}\n      {showTime && (\n        <Select value={timeValue} onValueChange={handleTimeChange} disabled={disabled}>\n          <SelectTrigger className={cn(timeSelectTriggerStyles, 'flex-1')}>\n            <SelectValue placeholder=\"Select time\">{timeDisplayLabel || 'Select time'}</SelectValue>\n          </SelectTrigger>\n          <SelectContent>\n            {timeOptions.map(option => (\n              <SelectItem key={option.value} value={option.value}>\n                {option.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      )}\n    </div>\n  );\n\n  return (\n    <FieldWrapper label={label} error={error} className={className}>\n      {content}\n    </FieldWrapper>\n  );\n}\n\n// ============================================================================\n// DateFilterPanel Component (sort select + fluid calendar)\n// ============================================================================\n\nexport interface DateFilterPanelProps {\n  /** Selection mode for the calendar. Defaults to \"range\". */\n  mode?: DatePickerMode;\n  /** Current sort direction shown in the select. */\n  sort: SortDirection;\n  onSortChange: (sort: SortDirection) => void;\n  /** Current calendar selection. */\n  selected: Date | DateRange | undefined;\n  onSelect: (value: Date | DateRange | undefined) => void;\n  /** Minimum selectable date. */\n  fromDate?: Date;\n  /** Maximum selectable date. */\n  toDate?: Date;\n  /** Locale for the calendar. */\n  locale?: DayPickerProps['locale'];\n  /** Label for the ascending sort option. */\n  ascLabel?: string;\n  /** Label for the descending sort option. */\n  descLabel?: string;\n  className?: string;\n}\n\n/**\n * DateFilterPanel — the controlled sort-direction select + fluid calendar\n * block shared by DateFilterMenu (popover) and FilterModal (mobile \"Sort and\n * Filter\"). Owns no state: the consumer drafts and commits the values.\n */\nexport function DateFilterPanel({\n  mode = 'range',\n  sort,\n  onSortChange,\n  selected,\n  onSelect,\n  fromDate,\n  toDate,\n  locale,\n  ascLabel = 'Sort by Ascending',\n  descLabel = 'Sort by Descending',\n  className,\n}: DateFilterPanelProps) {\n  return (\n    <div className={cn('flex flex-col gap-4', className)}>\n      {/* Sort direction selector */}\n      <Select value={sort} onValueChange={value => onSortChange(value as SortDirection)}>\n        <SelectTrigger className=\"gap-2\" aria-label=\"Sort direction\">\n          {/* Wrapper is a <div> (not <span>) so SelectTrigger's\n              `[&>span]:line-clamp-1` rule doesn't force it to a vertical\n              -webkit-box and stack the icon/label into a column. */}\n          <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n            <ArrowUpDown className=\"size-6 shrink-0 text-ods-text-secondary\" />\n            <span className=\"truncate\">\n              <SelectValue />\n            </span>\n          </div>\n        </SelectTrigger>\n        <SelectContent>\n          <SelectItem value=\"asc\">{ascLabel}</SelectItem>\n          <SelectItem value=\"desc\">{descLabel}</SelectItem>\n        </SelectContent>\n      </Select>\n\n      {/* Calendar */}\n      <DatePickerCalendar\n        mode={mode}\n        selected={selected}\n        onSelect={onSelect}\n        numberOfMonths={1}\n        fromDate={fromDate}\n        toDate={toDate}\n        locale={locale}\n        fluid\n      />\n    </div>\n  );\n}\n\n// ============================================================================\n// DateFilterMenu Component (sort + calendar filter popover from Figma)\n// ============================================================================\n\nexport interface DateFilterResult {\n  /** Selected sort direction */\n  sort: SortDirection;\n  /** Selected single date (mode === \"single\") */\n  date?: Date;\n  /** Selected date range (mode === \"range\") */\n  range?: DateRange;\n}\n\nexport interface DateFilterMenuProps {\n  /** Selection mode for the calendar. Defaults to \"range\". */\n  mode?: DatePickerMode;\n  /** Current (applied) sort direction. Defaults to \"desc\". */\n  sort?: SortDirection;\n  /** Baseline sort direction — Reset restores it, and a draft that differs\n   *  from it counts as an active change (shows Reset). Defaults to \"desc\". */\n  defaultSort?: SortDirection;\n  /** Current (applied) single date — used when mode === \"single\". */\n  date?: Date;\n  /** Current (applied) range — used when mode === \"range\". */\n  range?: DateRange;\n  /** Fired when the user presses Apply with the drafted selection. Also fired\n   *  by Reset with a cleared selection so the consumer refetches unfiltered data. */\n  onApply?: (result: DateFilterResult) => void;\n  /** Fired when the menu closes (Close button, outside click, Esc). */\n  onClose?: () => void;\n  /** Custom trigger element (rendered via Radix `asChild` — must accept a ref,\n   *  e.g. a native button). Defaults to the outline calendar icon Button. */\n  trigger?: ReactNode;\n  /** Disable the trigger. */\n  disabled?: boolean;\n  /** Minimum selectable date. */\n  fromDate?: Date;\n  /** Maximum selectable date. */\n  toDate?: Date;\n  /** Locale for the calendar. */\n  locale?: DayPickerProps['locale'];\n  /** Popover alignment relative to the trigger. */\n  align?: 'start' | 'center' | 'end';\n  /** Label for the ascending sort option. */\n  ascLabel?: string;\n  /** Label for the descending sort option. */\n  descLabel?: string;\n  /** Additional class name for the trigger button. */\n  className?: string;\n  /** Accessible label for the trigger. */\n  'aria-label'?: string;\n}\n\n/**\n * DateFilterMenu — a calendar-icon-triggered popover combining a sort-direction\n * selector and a date / date-range calendar, with Close/Reset and Apply actions.\n * The sort and date selection are drafted internally and committed via\n * `onApply`. While a date is selected, Close is replaced by Reset, which\n * clears and commits the empty selection (fires `onApply`) so the consumer\n * drops the filter.\n */\nexport function DateFilterMenu({\n  mode = 'range',\n  sort = 'desc',\n  defaultSort = 'desc',\n  date,\n  range,\n  onApply,\n  onClose,\n  trigger,\n  disabled = false,\n  fromDate,\n  toDate,\n  locale,\n  align = 'start',\n  ascLabel = 'Sort by Ascending',\n  descLabel = 'Sort by Descending',\n  className,\n  'aria-label': ariaLabel = 'Open date filter',\n}: DateFilterMenuProps) {\n  const [open, setOpen] = useState(false);\n\n  // Drafted selection — initialized from props each time the menu opens.\n  const [draftSort, setDraftSort] = useState<SortDirection>(sort);\n  const [draftSelected, setDraftSelected] = useState<Date | DateRange | undefined>(mode === 'single' ? date : range);\n\n  const resetDraft = useCallback(() => {\n    setDraftSort(sort);\n    setDraftSelected(mode === 'single' ? date : range);\n  }, [sort, date, range, mode]);\n\n  const handleOpenChange = (next: boolean) => {\n    if (next) {\n      resetDraft();\n    } else {\n      onClose?.();\n    }\n    setOpen(next);\n  };\n\n  const handleApply = () => {\n    const result: DateFilterResult =\n      mode === 'single'\n        ? { sort: draftSort, date: draftSelected as Date | undefined }\n        : { sort: draftSort, range: draftSelected as DateRange | undefined };\n    onApply?.(result);\n    setOpen(false);\n  };\n\n  const handleClose = () => {\n    onClose?.();\n    setOpen(false);\n  };\n\n  // Anything to reset? A calendar selection or a non-default sort — drives Close vs Reset.\n  const hasSelection =\n    mode === 'single' ? Boolean(draftSelected) : Boolean((draftSelected as DateRange | undefined)?.from);\n  const hasChanges = hasSelection || draftSort !== defaultSort;\n\n  // Reset restores the defaults and commits them so the consumer drops the\n  // filter and refetches; the menu stays open with the button back to Close.\n  const handleReset = () => {\n    setDraftSort(defaultSort);\n    setDraftSelected(undefined);\n    onApply?.(mode === 'single' ? { sort: defaultSort, date: undefined } : { sort: defaultSort, range: undefined });\n  };\n\n  return (\n    <Popover.Root open={open} onOpenChange={handleOpenChange}>\n      <Popover.Trigger asChild>\n        {trigger ?? (\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"icon\"\n            disabled={disabled}\n            aria-label={ariaLabel}\n            className={className}\n            leftIcon={<Calendar className=\"size-6\" />}\n          />\n        )}\n      </Popover.Trigger>\n\n      <Popover.Portal>\n        <Popover.Content\n          className={cn(\n            'z-[9999] w-80 max-w-[calc(100vw-2rem)]',\n            'flex flex-col gap-4 rounded-[6px] border border-ods-border bg-ods-bg p-4',\n            'animate-in fade-in-0 zoom-in-95',\n            'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',\n            'data-[side=bottom]:slide-in-from-top-2',\n            'data-[side=top]:slide-in-from-bottom-2',\n          )}\n          sideOffset={8}\n          align={align}\n        >\n          {/* Sort + calendar (shared panel) */}\n          <DateFilterPanel\n            mode={mode}\n            sort={draftSort}\n            onSortChange={setDraftSort}\n            selected={draftSelected}\n            onSelect={setDraftSelected}\n            fromDate={fromDate}\n            toDate={toDate}\n            locale={locale}\n            ascLabel={ascLabel}\n            descLabel={descLabel}\n          />\n\n          {/* Actions */}\n          <div className=\"flex w-full items-stretch gap-4\">\n            <Button type=\"button\" variant=\"outline\" fullWidth onClick={hasChanges ? handleReset : handleClose}>\n              {hasChanges ? 'Reset' : 'Close'}\n            </Button>\n            <Button type=\"button\" variant=\"accent\" fullWidth onClick={handleApply}>\n              Apply\n            </Button>\n          </div>\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n}\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { type DateRange };\n","'use client';\n\nimport * as SelectPrimitive from '@radix-ui/react-select';\nimport { ChevronDown, ChevronUp } from 'lucide-react';\nimport { type ComponentPropsWithoutRef, type ComponentRef, type ElementRef, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';\nimport { CheckIcon } from '../icons-v2-generated/signs-and-symbols/check-icon';\nimport { FieldWrapper } from './field-wrapper';\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = forwardRef<\n  ComponentRef<typeof SelectPrimitive.Trigger>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {\n    invalid?: boolean;\n    label?: string;\n    /** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles) */\n    labelVariant?: 'default' | 'large';\n    error?: string;\n  }\n>(({ className, children, invalid, label, labelVariant, error, ...props }, ref) => {\n  const isInvalid = invalid || !!error;\n\n  const trigger = (\n    <SelectPrimitive.Trigger\n      ref={ref}\n      data-invalid={isInvalid || undefined}\n      className={cn(\n        // Layout & spacing - match Input\n        // text-left resets the <button> UA default text-align:center, which the\n        // SelectValue span inherits and would otherwise center short values,\n        // reading as spurious left indentation on the selected value.\n        'flex h-11 w-full items-center justify-between gap-2 rounded-[6px] border px-3 text-left outline-none md:h-12',\n        // Typography - match Input exactly\n        'text-h4',\n        // Theme palette - match Input exactly\n        'border-ods-border bg-ods-card text-ods-text-primary data-[placeholder]:text-ods-text-secondary',\n        'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',\n        !isInvalid && 'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',\n        'group',\n        // Disabled - match Input exactly: value greys out, placeholder dims\n        // further. The `data-[placeholder]` rule above is a class+attribute\n        // selector, so the disabled placeholder rule stacks `:disabled` on top\n        // of it to win on specificity rather than on source order.\n        'disabled:!cursor-not-allowed disabled:bg-ods-bg',\n        'disabled:text-ods-text-disabled disabled:data-[placeholder]:text-ods-border',\n        // The chevron sets its own colour — grey it with the value.\n        'disabled:[&_svg]:text-ods-text-disabled',\n        'cursor-pointer transition-colors duration-200',\n        '[&>span]:line-clamp-1',\n        isInvalid &&\n          'border-ods-error enabled:hover:border-ods-error enabled:active:border-ods-error data-[state=open]:border-ods-error',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <SelectPrimitive.Icon asChild>\n        <Chevron02DownIcon\n          className={cn(\n            'shrink-0 text-ods-text-secondary transition-all duration-200 group-data-[state=open]:rotate-180',\n            isInvalid ? 'text-ods-error' : 'group-data-[state=open]:text-ods-accent',\n          )}\n          size={24}\n        />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n\n  return (\n    <FieldWrapper label={label} labelVariant={labelVariant} error={error}>\n      {trigger}\n    </FieldWrapper>\n  );\n});\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = forwardRef<\n  ComponentRef<typeof SelectPrimitive.ScrollUpButton>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn('flex cursor-default items-center justify-center py-1', className)}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = forwardRef<\n  ComponentRef<typeof SelectPrimitive.ScrollDownButton>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn('flex cursor-default items-center justify-center py-1', className)}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = forwardRef<\n  ComponentRef<typeof SelectPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = 'popper', ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        'relative z-[9999] max-h-96 overflow-hidden rounded-[4px] border border-ods-border bg-ods-card text-ods-text-primary',\n        'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        position === 'popper' &&\n          'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',\n        className,\n      )}\n      position={position}\n      style={{\n        width: 'var(--radix-select-trigger-width)',\n        minWidth: 'var(--radix-select-trigger-width)',\n      }}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport className=\"w-full\">{children}</SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = forwardRef<\n  ElementRef<typeof SelectPrimitive.Label>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label ref={ref} className={cn('py-1.5 pl-8 pr-2 font-semibold text-h6', className)} {...props} />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = forwardRef<\n  ComponentRef<typeof SelectPrimitive.Item>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      'flex h-11 w-full cursor-pointer select-none items-center justify-between border-b border-ods-border px-4 last:border-b-0 md:h-12',\n      // Typography - match trigger\n      'whitespace-nowrap text-h4',\n      // Hover state with visible background change\n      'outline-none hover:bg-ods-bg-hover data-[highlighted]:bg-ods-bg-surface',\n      'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      'transition-colors duration-150',\n      className,\n    )}\n    {...props}\n  >\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n    <SelectPrimitive.ItemIndicator>\n      <CheckIcon className=\"text-ods-accent\" size={20} />\n    </SelectPrimitive.ItemIndicator>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = forwardRef<\n  ComponentRef<typeof SelectPrimitive.Separator>,\n  ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-ods-border', className)} {...props} />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  SelectTrigger,\n  SelectValue,\n};\n","'use client';\n\nimport { Fragment, type KeyboardEvent, useRef, useState, type ChangeEvent } from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface DurationInputProps {\n  /** Duration as `HH:MM:SS`. */\n  value: string;\n  onChange: (value: string) => void;\n  invalid?: boolean;\n  className?: string;\n}\n\ntype Segments = [string, string, string];\n\nconst SEGMENT_META = [\n  { label: 'Hours', placeholder: 'HH', max: 99 },\n  { label: 'Minutes', placeholder: 'MM', max: 59 },\n  { label: 'Seconds', placeholder: 'SS', max: 59 },\n] as const;\n\nconst pad2 = (s: string): string => s.padStart(2, '0').slice(-2);\nconst compose = (s: Segments): string => `${pad2(s[0])}:${pad2(s[1])}:${pad2(s[2])}`;\nconst split = (value: string): Segments => {\n  const parts = value.split(':');\n  return [parts[0] || '00', parts[1] || '00', parts[2] || '00'];\n};\n\n/**\n * Segmented `HH:MM:SS` entry: each unit is its own field, typing fills the focused\n * segment and auto-advances, arrows move/increment. Segments clamp to their max\n * (MM/SS ≤ 59), so an invalid duration can't be produced. Value in/out is `HH:MM:SS`.\n */\nexport function DurationInput({ value, onChange, invalid, className }: DurationInputProps) {\n  const [segments, setSegments] = useState<Segments>(() => split(value));\n  const refs = [useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null)];\n\n  // Adopt an externally-set value (edit pre-fill / form reset) without clobbering in-progress typing.\n  // Adjusted while rendering (React's documented prop-sync pattern), not from an\n  // effect: the segments ARE what this render draws, so an effect painted the\n  // old duration once and replaced it on the next pass. Edge-triggered on the\n  // prop rather than comparing `compose(segments)` to `value` on every render —\n  // an uncontrolled caller that ignores `onChange` would never converge, and\n  // that comparison would then re-set state forever.\n  const [syncedValue, setSyncedValue] = useState(value);\n  if (syncedValue !== value) {\n    setSyncedValue(value);\n    setSegments(prev => (compose(prev) === value ? prev : split(value)));\n  }\n\n  const focusSegment = (i: number) => {\n    const el = refs[i]?.current;\n    if (!el) return;\n    el.focus();\n    el.select();\n  };\n\n  const commit = (next: Segments, advanceFrom?: number) => {\n    setSegments(next);\n    onChange(compose(next));\n    if (advanceFrom !== undefined && advanceFrom < 2) focusSegment(advanceFrom + 1);\n  };\n\n  const handleChange = (i: number) => (event: ChangeEvent<HTMLInputElement>) => {\n    const digits = event.target.value.replace(/\\D/g, '').slice(-2);\n    const { max } = SEGMENT_META[i];\n    const next = [...segments] as Segments;\n\n    if (digits.length === 2) {\n      next[i] = String(Math.min(Number(digits), max));\n      commit(next, i);\n    } else if (digits.length === 1 && max === 59 && Number(digits) > 5) {\n      // 6–9 can't start a valid two-digit minute/second, so commit and advance now.\n      next[i] = digits;\n      commit(next, i);\n    } else {\n      next[i] = digits;\n      commit(next);\n    }\n  };\n\n  const handleKeyDown = (i: number) => (event: KeyboardEvent<HTMLInputElement>) => {\n    const el = event.currentTarget;\n    const { max } = SEGMENT_META[i];\n\n    if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {\n      event.preventDefault();\n      const delta = event.key === 'ArrowUp' ? 1 : -1;\n      const nextValue = (Number(segments[i] || '0') + delta + (max + 1)) % (max + 1);\n      const next = [...segments] as Segments;\n      next[i] = String(nextValue);\n      commit(next);\n      requestAnimationFrame(() => el.select());\n    } else if (event.key === 'ArrowLeft' && el.selectionStart === 0 && i > 0) {\n      event.preventDefault();\n      focusSegment(i - 1);\n    } else if (event.key === 'ArrowRight' && el.selectionEnd === el.value.length && i < 2) {\n      event.preventDefault();\n      focusSegment(i + 1);\n    } else if (event.key === 'Backspace' && el.value === '' && i > 0) {\n      event.preventDefault();\n      focusSegment(i - 1);\n    } else if (event.key === ':' && i < 2) {\n      event.preventDefault();\n      focusSegment(i + 1);\n    }\n  };\n\n  return (\n    <div\n      onMouseDown={event => {\n        if (event.target === event.currentTarget) {\n          event.preventDefault();\n          focusSegment(0);\n        }\n      }}\n      className={cn(\n        'flex h-11 w-full items-center gap-[var(--spacing-system-xxs)] rounded-md border border-ods-border bg-ods-card px-[var(--spacing-system-sf)] transition-colors duration-200 md:h-12',\n        'cursor-text focus-within:border-ods-accent',\n        invalid && 'border-ods-error focus-within:border-ods-error',\n        className,\n      )}\n    >\n      {SEGMENT_META.map((meta, i) => (\n        <Fragment key={meta.label}>\n          {i > 0 && <span className=\"text-ods-text-secondary\">:</span>}\n          <input\n            ref={refs[i]}\n            inputMode=\"numeric\"\n            size={2}\n            value={segments[i]}\n            onChange={handleChange(i)}\n            onKeyDown={handleKeyDown(i)}\n            onFocus={event => event.currentTarget.select()}\n            onBlur={() =>\n              setSegments(prev => {\n                const padded = [...prev] as Segments;\n                padded[i] = pad2(prev[i]);\n                return padded;\n              })\n            }\n            placeholder={meta.placeholder}\n            aria-label={meta.label}\n            className=\"rounded-sm bg-transparent text-center !font-mono tabular-nums text-ods-text-primary outline-none text-h4 placeholder:text-ods-text-secondary focus:bg-ods-bg-hover\"\n          />\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport { type ReactNode, useId } from 'react';\nimport { InfoHint } from './info-hint';\nimport { Label } from './label';\nimport { RequiredMark } from './required-mark';\n\n/**\n * Accessibility props handed to the control by the `Field` render-prop.\n * Spread them onto the input/select/textarea so the label, required flag and\n * error message are all announced.\n */\nexport interface FieldRenderProps {\n  id: string;\n  'aria-required'?: boolean;\n  'aria-invalid'?: boolean;\n  'aria-describedby'?: string;\n}\n\nexport interface FieldProps {\n  label: string;\n  /** Definition shown in a hover `InfoHint` next to the label. */\n  hint?: ReactNode;\n  required?: boolean;\n  children: (props: FieldRenderProps) => ReactNode;\n  error?: string | null;\n  /**\n   * Inline content after the label text — AI badges, confidence chips. Lives in\n   * the LABEL ROW so fields that carry badges keep the same geometry as fields\n   * that don't; a hand-rolled label row next to a `Field` is what makes two\n   * columns' controls land at different heights.\n   */\n  labelExtras?: ReactNode;\n  /**\n   * Right-aligned content at the end of the label row — character counters,\n   * shortcuts. Same rationale: a counter rendered as its own row under the\n   * control gives that column an extra line and misaligns every sibling.\n   */\n  labelEnd?: ReactNode;\n}\n\n/**\n * One form field: label, optional hint, control, optional error.\n *\n * Labels are a single line (hints live in the hover InfoHint icon, never in\n * flow), so cells top-align naturally and controls land on the same baseline.\n * Do NOT reintroduce `h-full` + `mt-auto` here: in a grid row it stretches the\n * cell to the tallest sibling and shoves this cell's control down to align with\n * the bottom of the sibling's ERROR text, detaching it from its own label.\n *\n * The label is tied to its control with a generated id, so clicking the label\n * focuses the input and assistive tech announces the field by name. `children`\n * is a REQUIRED render function — `{(f) => <Input {...f} />}` — spread what it\n * gives you onto the control. A plain-node escape hatch would leave `htmlFor`\n * pointing at an id that exists on no element, which is worse than no\n * association at all (the label looks wired up and announces nothing), so the\n * type doesn't offer one.\n */\nexport function Field({ label, hint, required, children, error, labelExtras, labelEnd }: FieldProps) {\n  const controlId = useId();\n  const errorId = `${controlId}-error`;\n  const renderProps: FieldRenderProps = {\n    id: controlId,\n    // A bare `*` draws a star and tells assistive tech nothing.\n    ...(required ? { 'aria-required': true } : {}),\n    ...(error ? { 'aria-invalid': true, 'aria-describedby': errorId } : {}),\n  };\n  return (\n    <div className=\"flex flex-col gap-[var(--spacing-system-xxs)]\">\n      <div className=\"flex items-center gap-1.5\">\n        <Label htmlFor={controlId}>\n          {label}\n          {required && <RequiredMark />}\n        </Label>\n        {hint && <InfoHint label={label}>{hint}</InfoHint>}\n        {labelExtras}\n        {labelEnd && <span className=\"ml-auto shrink-0\">{labelEnd}</span>}\n      </div>\n      <div className=\"pt-[var(--spacing-system-xxs)]\">{children(renderProps)}</div>\n      {/* `role=\"alert\"` + the id the control points at: a validation message\n          rendered as plain text is never announced. */}\n      {error && (\n        <p id={errorId} role=\"alert\" className=\"text-ods-error text-h6\">\n          {error}\n        </p>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { Info } from 'lucide-react';\nimport { type ReactNode, useId } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Button } from './button';\nimport { FloatingTooltip } from './floating-tooltip';\n\nexport interface InfoHintProps {\n  children: ReactNode;\n  /** What this hint is ABOUT — 15 buttons all named \"More information\" name nothing. */\n  label?: string;\n  side?: 'top' | 'right' | 'bottom' | 'left';\n  className?: string;\n}\n\n/**\n * The \"what does this mean?\" affordance — an ⓘ icon that reveals a definition\n * on hover.\n *\n * Config-heavy screens carry vocabulary that is obvious to whoever built the\n * feature and opaque to everyone else — `judged`, `deterministic`, `observe`,\n * `error`. A term nobody can define is a term nobody configures correctly, so\n * every one of them gets a hint rather than a wiki page somebody has to go find.\n *\n * This is a thin cap over `FloatingTooltip`, not a second tooltip: it fixes the\n * icon, the trigger element and the sizing so every hint looks and behaves the\n * same. All the hover/portal/placement behaviour is the shared component's.\n */\nexport function InfoHint({ children, label, side = 'top', className }: InfoHintProps) {\n  const describedById = useId();\n  return (\n    <span className={cn('inline-flex shrink-0 items-center', className)}>\n      <FloatingTooltip\n        content={children}\n        side={side}\n        as=\"span\"\n        triggerClassName=\"inline-flex shrink-0 items-center\"\n        // Width comes from FloatingTooltip's shared max-w-xs default — no\n        // per-cap override, so every hint wraps like every other tooltip.\n        className=\"text-h6\"\n      >\n        {/*\n          `icon-inline` is a Button variant, not a className stack. The hint sits\n          inside a line of badges, so `icon-sm`'s 32px target would swamp a 10px\n          badge — and shrinking it at the call site is exactly the override the\n          house rule forbids.\n        */}\n        <Button\n          type=\"button\"\n          variant=\"transparent\"\n          size=\"icon-inline\"\n          // A hint lives inside interactive rows (e.g. a role=\"checkbox\" rule\n          // row): asking \"what does this mean?\" must never toggle the row it\n          // sits in.\n          onClick={e => e.stopPropagation()}\n          aria-label={label ? `About ${label}` : 'More information'}\n          aria-describedby={describedById}\n          className=\"text-ods-text-secondary hover:text-ods-text-primary\"\n        >\n          <Info className=\"h-3.5 w-3.5 shrink-0\" />\n        </Button>\n      </FloatingTooltip>\n      {/*\n        The tooltip opens on HOVER only — `FloatingTooltip` composes\n        `useHover`/`useDismiss`/`useRole` and no `useFocus`, so a keyboard user\n        tabbing here would otherwise reach a button that reveals nothing. This\n        visually-hidden copy is the same text, exposed via `aria-describedby`,\n        so assistive tech reads the definition without depending on hover.\n      */}\n      <span id={describedById} className=\"sr-only\">\n        {children}\n      </span>\n    </span>\n  );\n}\n","'use client';\n\nimport { forwardRef } from 'react';\n\n/**\n * HoneypotField — an invisible decoy input for bot detection. Real users never\n * see or fill it; naive bots that fill every field trip it (the server blocks\n * any submission where this field is non-empty).\n *\n * Made invisible WITHOUT `type=\"hidden\"` / `display:none` (sophisticated bots\n * skip those): an off-screen, zero-size, aria-hidden, non-focusable,\n * non-pointer wrapper. Layout utilities only — no colours/hex (ODS-clean). No\n * <label> (avoids id coupling); `aria-hidden` + `tabIndex={-1}` keep it out of\n * the accessibility tree and tab order.\n *\n * AUTOFILL-PROOFING (load-bearing — real users were 403'd when autofill filled\n * the decoy; see humanity-signals.ts module doc): browsers ignore\n * `autocomplete=\"off\"` for address-like fields, so the input is `readOnly`\n * until focused — autofill and password managers skip read-only inputs, while\n * a DOM-driving bot that focuses the field to type into it lifts the guard and\n * still trips the trap. The `data-*` attributes opt out of the major password\n * managers (1Password, LastPass, Bitwarden, Dashlane) by their documented\n * ignore contracts. React never re-applies the prop after the focus handler\n * flips the DOM property (same-value props aren't re-committed), so the\n * lift-on-focus sticks.\n *\n * forwardRef so `<HoneypotField {...honeypotInputProps} />` (which carries the\n * ref from useHumanitySignals) works on both React 18 and 19.\n */\nexport const HoneypotField = forwardRef<HTMLInputElement, { name: string }>(function HoneypotFieldImpl({ name }, ref) {\n  return (\n    <div aria-hidden=\"true\" className=\"pointer-events-none absolute -m-px h-px w-px overflow-hidden p-0 opacity-0\">\n      <input\n        ref={ref}\n        type=\"text\"\n        name={name}\n        tabIndex={-1}\n        autoComplete=\"off\"\n        readOnly\n        onFocus={e => {\n          const input = e.currentTarget;\n          input.readOnly = false;\n        }}\n        data-1p-ignore=\"true\"\n        data-lpignore=\"true\"\n        data-bwignore=\"true\"\n        data-form-type=\"other\"\n      />\n    </div>\n  );\n});\nHoneypotField.displayName = 'HoneypotField';\n","'use client';\n\nimport { cn } from '../../utils/cn';\nimport { formatRelativeTime } from '../../utils/date-utils';\n\n/**\n * THE generic JSON detail renderer — readable key/value prose for machine\n * payloads whose field names are NOT guaranteed (evidence blobs, audit\n * payloads, webhook bodies). No schema knowledge: keys are humanised,\n * ISO-dates become relative times, nested objects indent, and anything\n * unparseable falls back to raw. Use this instead of `<pre>{JSON.stringify}`\n * or a per-shape bespoke renderer.\n */\n\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n/**\n * `Array.isArray` narrows an `unknown` to `any[]`, which then leaks `any` into\n * every element read. This renderer never assumes an element type — `unknown[]`\n * is the honest narrowing, and `<Value>` already takes `unknown`.\n */\nfunction isUnknownArray(value: unknown): value is unknown[] {\n  return Array.isArray(value);\n}\n\nfunction humanizeKey(key: string): string {\n  const spaced = key\n    .replace(/[_-]+/g, ' ')\n    .replace(/([a-z])([A-Z])/g, '$1 $2')\n    .trim();\n  return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\nfunction Value({ value, depth }: { value: unknown; depth: number }) {\n  if (value === null || value === undefined) {\n    return <span className=\"text-ods-text-secondary\">—</span>;\n  }\n  if (typeof value === 'string') {\n    if (ISO_DATE.test(value)) {\n      return (\n        <span title={value} className=\"text-ods-text-secondary\">\n          {formatRelativeTime(value)}\n        </span>\n      );\n    }\n    return <span className=\"whitespace-pre-wrap text-ods-text-primary\">{value}</span>;\n  }\n  if (typeof value === 'number' || typeof value === 'boolean') {\n    return <span className=\"text-ods-text-primary text-code\">{String(value)}</span>;\n  }\n  if (isUnknownArray(value)) {\n    if (value.every(v => typeof v === 'string' || typeof v === 'number')) {\n      return <span className=\"text-ods-text-primary\">{value.join(', ')}</span>;\n    }\n    return (\n      <div className=\"space-y-[var(--spacing-system-xxs)]\">\n        {value.map((v, i) => (\n          <Value key={i} value={v} depth={depth + 1} />\n        ))}\n      </div>\n    );\n  }\n  if (typeof value === 'object' && depth < 3) {\n    return <JsonDetailList data={value} depth={depth + 1} />;\n  }\n  return <pre className=\"whitespace-pre-wrap text-ods-text-secondary text-code\">{JSON.stringify(value, null, 2)}</pre>;\n}\n\nexport interface JsonDetailListProps {\n  /** Object, array, JSON string, or any scalar — rendered faithfully either way. */\n  data: unknown;\n  /** Internal nesting level — leave at the default from call sites. */\n  depth?: number;\n}\n\nexport function JsonDetailList({ data, depth = 0 }: JsonDetailListProps) {\n  let parseFailed = false;\n  const obj =\n    typeof data === 'string'\n      ? (() => {\n          try {\n            return JSON.parse(data) as unknown;\n          } catch {\n            parseFailed = true;\n            return null;\n          }\n        })()\n      : data;\n\n  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n    // A string that ISN'T JSON is prose — show it as-is.\n    if (typeof data === 'string' && parseFailed) {\n      return <p className=\"whitespace-pre-wrap text-ods-text-primary text-h6\">{data}</p>;\n    }\n    // Everything else is a real (parsed or given) non-object VALUE — an array,\n    // scalar, or null. Render the value through `Value`, never the source\n    // string: `'[\"a\",\"b\"]'` must render as the array, not as raw JSON text.\n    return (\n      <div className=\"text-h6\">\n        <Value value={obj} depth={depth} />\n      </div>\n    );\n  }\n\n  return (\n    <dl\n      className={cn(\n        'space-y-[var(--spacing-system-xsf)]',\n        depth > 0 && 'border-l border-ods-border pl-[var(--spacing-system-sf)]',\n      )}\n    >\n      {Object.entries(obj as Record<string, unknown>).map(([key, value]) => (\n        <div key={key}>\n          <dt className=\"text-ods-text-secondary text-h6\">{humanizeKey(key)}</dt>\n          <dd className=\"text-h6\">\n            <Value value={value} depth={depth} />\n          </dd>\n        </div>\n      ))}\n    </dl>\n  );\n}\n","'use client';\n\nimport React from 'react';\nimport { useCopyToClipboard } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { CheckIcon } from '../icons-v2-generated';\nimport { Copy01Icon } from '../icons-v2-generated/documents';\nimport { ProgressBar } from './progress-bar';\n\nexport interface InfoCardFooterData {\n  /** Leading icon next to the text, expected at 24x24 (e.g. <ShieldCheckIcon size={24} />) */\n  icon?: React.ReactNode;\n  text: string;\n  /** Trailing icon/logo aligned to the right edge, expected at 24x24 */\n  logo?: React.ReactNode;\n  /** External resource link rendered below the text row */\n  link?: {\n    href: string;\n    /** Defaults to href without the protocol */\n    label?: string;\n  };\n}\n\nexport interface InfoCardData {\n  title?: string;\n  subtitle?: string;\n  icon?: React.ReactNode;\n  items: Array<{\n    label?: string;\n    value: string | string[];\n    copyable?: boolean;\n    /** Optional trailing icon/logo (e.g. an SVG) rendered next to the value */\n    icon?: React.ReactNode;\n  }>;\n  progress?: {\n    value: number;\n    warningThreshold?: number;\n    criticalThreshold?: number;\n    inverted?: boolean; // if true, high values are good (green), low values are bad (red)\n  };\n  /** Optional footer rendered below the content, separated by a divider line */\n  footer?: InfoCardFooterData;\n}\n\ninterface InfoCardProps {\n  data: InfoCardData;\n  className?: string;\n}\n\nexport function InfoCard({ data, className = '' }: InfoCardProps) {\n  return (\n    <div\n      className={cn('flex w-full flex-col overflow-hidden rounded-md border border-ods-border bg-ods-card', className)}\n    >\n      {/* Header (title + subtitle) and body (rows + progress) are two groups separated by\n          `gap-l`; the title/subtitle stack tightly and the rows use `gap-xs` — matching the ODS\n          \"Info-card\" design. With no title/subtitle, the body is the only group. */}\n      <div className=\"flex w-full flex-col items-start gap-[var(--spacing-system-l)] p-[var(--spacing-system-m)]\">\n        {(data.title || data.subtitle) && (\n          <div className=\"flex w-full flex-col items-start self-stretch\">\n            {data.title && (\n              <div className=\"flex items-center gap-[var(--spacing-system-xsf)] self-stretch\">\n                <span className=\"min-w-0 truncate text-ods-text-primary text-h4\" title={data.title}>\n                  {data.title}\n                </span>\n                {data.icon}\n              </div>\n            )}\n            {data.subtitle && (\n              <div className=\"flex items-center gap-[var(--spacing-system-xsf)] self-stretch\">\n                <span className=\"min-w-0 truncate text-ods-text-secondary text-h4\" title={data.subtitle}>\n                  {data.subtitle}\n                </span>\n                {/* Icon lives with the title when present; otherwise it falls to the subtitle so\n                    subtitle-only cards still render it. */}\n                {!data.title && data.icon}\n              </div>\n            )}\n          </div>\n        )}\n\n        {(data.items.length > 0 || data.progress) && (\n          <div className=\"flex w-full flex-col items-start gap-[var(--spacing-system-xs)] self-stretch\">\n            {/* Info items */}\n            {data.items.map((item, index) => {\n              const values = Array.isArray(item.value) ? item.value : [item.value];\n\n              return (\n                <React.Fragment key={index}>\n                  {values.map((val, valIndex) => (\n                    <InfoCardValueRow\n                      key={`${index}-${valIndex}`}\n                      label={item.label}\n                      value={val}\n                      showLabel={valIndex === 0}\n                      copyable={item.copyable}\n                      icon={valIndex === 0 ? item.icon : undefined}\n                      copyAriaLabel={`Copy ${item.label ?? 'value'} ${valIndex + 1}`}\n                    />\n                  ))}\n                </React.Fragment>\n              );\n            })}\n\n            {/* Progress bar */}\n            {data.progress && (\n              <ProgressBar\n                progress={data.progress.value}\n                warningThreshold={data.progress.warningThreshold}\n                criticalThreshold={data.progress.criticalThreshold}\n                inverted={data.progress.inverted}\n              />\n            )}\n          </div>\n        )}\n      </div>\n\n      {/* Footer */}\n      {data.footer && <InfoCardFooter footer={data.footer} />}\n    </div>\n  );\n}\n\nfunction InfoCardFooter({ footer }: { footer: InfoCardFooterData }) {\n  return (\n    <div className=\"mt-auto flex w-full flex-col border-t border-ods-border p-[var(--spacing-system-m)]\">\n      <div className=\"flex w-full items-center justify-between gap-1\">\n        <div className=\"flex items-center gap-1\">\n          {footer.icon}\n          <span className=\"text-ods-text-primary text-h4\">{footer.text}</span>\n        </div>\n        {footer.logo}\n      </div>\n      {footer.link && (\n        <a\n          href={footer.link.href}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"truncate text-ods-text-secondary underline text-h6 hover:text-ods-text-primary\"\n        >\n          {footer.link.label ?? footer.link.href.replace(/^https?:\\/\\//, '')}\n        </a>\n      )}\n    </div>\n  );\n}\n\ninterface InfoCardValueRowProps {\n  label?: string;\n  value: string;\n  showLabel: boolean;\n  copyable?: boolean;\n  icon?: React.ReactNode;\n  copyAriaLabel: string;\n}\n\nfunction InfoCardValueRow({ label, value, showLabel, copyable, icon, copyAriaLabel }: InfoCardValueRowProps) {\n  const { copy, copied } = useCopyToClipboard();\n\n  return (\n    <div className=\"flex h-6 w-full items-center gap-[var(--spacing-system-xs)] self-stretch\">\n      <span className=\"whitespace-nowrap text-ods-text-primary text-h4\">{showLabel ? label : ''}</span>\n      <div className=\"h-px flex-1 bg-ods-divider\" />\n      <div className=\"flex max-w-[60%] items-center gap-[var(--spacing-system-xsf)]\">\n        <span className=\"select-text truncate text-ods-text-primary text-h4\" title={value}>\n          {value}\n        </span>\n        {icon}\n        {copyable && (\n          <button\n            type=\"button\"\n            onClick={() => copy(value)}\n            className={cn(\n              'transition-colors',\n              copied ? 'text-ods-success' : 'text-ods-text-secondary hover:text-ods-text-primary',\n            )}\n            aria-label={copyAriaLabel}\n          >\n            {copied ? <CheckIcon size={16} /> : <Copy01Icon size={16} />}\n          </button>\n        )}\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { useMdUp } from '../../hooks';\n\ntype ProgressBarProps = {\n  progress: number; // 0–100\n  warningThreshold?: number; // default 70\n  criticalThreshold?: number; // default 90\n  segmentWidth?: number; // desktop segment width (px)\n  mobileSegmentWidth?: number; // mobile segment width (px)\n  segmentGap?: number; // px, default 2\n  height?: number; // desktop height (px)\n  mobileHeight?: number; // mobile height (px)\n  inverted?: boolean; // if true, high values are good (green), low values are bad (red)\n};\n\nexport const ProgressBar: React.FC<ProgressBarProps> = ({\n  progress,\n  warningThreshold = 75,\n  criticalThreshold = 90,\n  segmentWidth = 3.43,\n  mobileSegmentWidth = 5,\n  segmentGap = 2,\n  height = 24,\n  mobileHeight = 8,\n  inverted = false,\n}) => {\n  const isMdUp = useMdUp() ?? true;\n  const effectiveSegmentWidth = isMdUp ? segmentWidth : mobileSegmentWidth;\n  const effectiveHeight = isMdUp ? height : mobileHeight;\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [segmentCount, setSegmentCount] = useState(0);\n\n  useEffect(() => {\n    if (!containerRef.current) return undefined;\n\n    const resizeObserver = new ResizeObserver(() => {\n      if (containerRef.current) {\n        const width = containerRef.current.offsetWidth;\n        // N segments have only (N-1) gaps, so add one gap back before dividing —\n        // otherwise the last segment is dropped due to a phantom trailing gap.\n        const count = Math.floor((width + segmentGap) / (effectiveSegmentWidth + segmentGap));\n        setSegmentCount(count);\n      }\n    });\n\n    resizeObserver.observe(containerRef.current);\n\n    return () => resizeObserver.disconnect();\n  }, [effectiveSegmentWidth, segmentGap]);\n\n  // Pick color based on thresholds using ODS design tokens\n  const getColor = () => {\n    if (inverted) {\n      // Inverted: high values = good (green), low values = bad (red)\n      // For battery health: 100% = green, <30% = red\n      if (progress >= criticalThreshold) return 'var(--color-success)'; // high = green\n      if (progress >= warningThreshold) return 'var(--color-warning)'; // medium = warning\n      return 'var(--color-error)'; // low = red\n    } else {\n      // Normal: high values = bad (red), low values = good (green)\n      // For disk usage: 100% = red, <70% = green\n      if (progress >= criticalThreshold) return 'var(--color-error)'; // critical red\n      if (progress >= warningThreshold) return 'var(--color-warning)'; // warning yellow\n      return 'var(--color-success)'; // base green\n    }\n  };\n\n  return (\n    <div ref={containerRef} className=\"flex w-full\" style={{ gap: `${segmentGap}px` }}>\n      {Array.from({ length: segmentCount }).map((_, i) => (\n        <div\n          key={i}\n          className=\"rounded\"\n          style={{\n            width: `${effectiveSegmentWidth}px`,\n            height: `${effectiveHeight}px`,\n            backgroundColor:\n              i < Math.round((progress / 100) * segmentCount) ? getColor() : 'var(--color-bg-surface-active)', // unfilled segments\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n","'use client';\n\nimport type { ReactNode } from 'react';\n\ninterface InfoRowProps {\n  label: string;\n  value: string;\n  icon?: ReactNode;\n}\n\nexport function InfoRow({ label, value, icon }: InfoRowProps) {\n  return (\n    <div className=\"flex w-full items-center gap-2\">\n      <div className=\"overflow-hidden text-ellipsis whitespace-nowrap text-ods-text-primary text-h4\">{label}</div>\n      <div className=\"h-px min-h-px min-w-px flex-1 bg-ods-border\" />\n      <div className=\"flex items-center gap-1 overflow-hidden text-ellipsis whitespace-nowrap text-ods-text-primary text-h4\">\n        {value}\n        {icon}\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { type MouseEvent, useCallback, useMemo, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { PenEditIcon, UserIcon, UserPlusIcon } from '../icons-v2-generated';\nimport { Autocomplete, type AutocompleteOption } from './autocomplete';\nimport { DeletedUserAvatar } from './deleted-user-avatar';\nimport { SearchableSelect, type SearchableSelectOption } from './searchable-select';\nimport { SquareAvatar } from './square-avatar';\n\nexport interface TicketAssigneeOption {\n  value: string;\n  label: string;\n  imageUrl?: string;\n}\n\nexport interface AssigneeDropdownProps {\n  currentAssignee?: {\n    id: string;\n    name: string;\n    avatarSrc?: string;\n    /** Deleted account (DELETED / SELF_DELETED) — renders the red user-x placeholder + red name. */\n    deleted?: boolean;\n  };\n  options: TicketAssigneeOption[];\n  isLoading?: boolean;\n  isPending?: boolean;\n  onAssign: (userId: string | null) => void;\n  variant?: 'default' | 'compact';\n  className?: string;\n  /**\n   * Intercepts the trigger: when set (compact variant), clicking the\n   * avatar/assign button calls this instead of opening the dropdown — e.g. to\n   * open a Take Over confirmation for tickets still worked by the AI.\n   */\n  onTriggerClick?: () => void;\n}\n\nexport function AssigneeDropdown(props: AssigneeDropdownProps) {\n  if (props.variant === 'compact') {\n    return <CompactAssigneeDropdown {...props} />;\n  }\n  return <DefaultAssigneeDropdown {...props} />;\n}\n\nfunction CompactAssigneeDropdown({\n  currentAssignee,\n  options,\n  isLoading,\n  onAssign,\n  className,\n  onTriggerClick,\n}: AssigneeDropdownProps) {\n  const hasAssignee = !!currentAssignee;\n\n  // Current assignee first; SearchableSelect filters but never reorders.\n  const orderedOptions = useMemo<SearchableSelectOption[]>(() => {\n    const withAvatars = options.map(o => ({\n      value: o.value,\n      label: o.label,\n      icon: (\n        <SquareAvatar\n          src={o.imageUrl}\n          alt={o.label}\n          fallback={o.label}\n          size=\"sm\"\n          variant=\"round\"\n          className=\"h-6 w-6 shrink-0\"\n        />\n      ),\n    }));\n    if (!currentAssignee) return withAvatars;\n    const current = withAvatars.find(o => o.value === currentAssignee.id);\n    if (!current) return withAvatars;\n    return [current, ...withAvatars.filter(o => o.value !== currentAssignee.id)];\n  }, [options, currentAssignee]);\n\n  const handleSelect = (userId: string) => {\n    // Selecting the current assignee again unassigns.\n    onAssign(currentAssignee?.id === userId ? null : userId);\n  };\n\n  // Board cards are links: an intercepted trigger must not navigate or bubble.\n  const handleTriggerClick = onTriggerClick\n    ? (event: MouseEvent) => {\n        event.preventDefault();\n        event.stopPropagation();\n        onTriggerClick();\n      }\n    : undefined;\n\n  const trigger = hasAssignee ? (\n    <button\n      type=\"button\"\n      onClick={handleTriggerClick}\n      aria-label=\"Change assignee\"\n      className={cn(\n        'shrink-0 rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',\n        className,\n      )}\n    >\n      {currentAssignee.deleted ? (\n        <DeletedUserAvatar size=\"sm\" accessibleLabel={`Deleted user: ${currentAssignee.name || currentAssignee.id}`} />\n      ) : (\n        <SquareAvatar\n          src={currentAssignee.avatarSrc}\n          alt={currentAssignee.name}\n          fallback={currentAssignee.name || 'User'}\n          size=\"sm\"\n          variant=\"round\"\n        />\n      )}\n    </button>\n  ) : (\n    <button\n      type=\"button\"\n      onClick={handleTriggerClick}\n      aria-label=\"Assign user\"\n      className={cn(\n        'flex size-8 shrink-0 items-center justify-center rounded-full border border-ods-border',\n        'text-ods-text-secondary transition-colors hover:border-ods-accent hover:text-ods-accent',\n        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',\n        className,\n      )}\n    >\n      <UserPlusIcon className=\"size-4\" />\n    </button>\n  );\n\n  // Intercepted trigger: render the plain button — no dropdown to open.\n  if (onTriggerClick) return trigger;\n\n  return (\n    <SearchableSelect\n      options={orderedOptions}\n      value={currentAssignee?.id ?? null}\n      onValueChange={handleSelect}\n      searchPlaceholder=\"Search users...\"\n      emptyText=\"No users found\"\n      isLoading={isLoading}\n      trigger={trigger}\n      align=\"end\"\n      contentClassName=\"w-72\"\n    />\n  );\n}\n\nfunction DefaultAssigneeDropdown({ currentAssignee, options, isLoading, onAssign, className }: AssigneeDropdownProps) {\n  const [isEditing, setIsEditing] = useState(false);\n  const hasAssignee = !!currentAssignee;\n\n  const renderOption = useCallback((option: AutocompleteOption) => {\n    const opt = option as TicketAssigneeOption;\n    return (\n      <div className=\"flex w-full min-w-0 items-center gap-[var(--spacing-system-sf)]\">\n        <SquareAvatar\n          src={opt.imageUrl}\n          alt={opt.label}\n          fallback={opt.label}\n          size=\"sm\"\n          variant=\"round\"\n          className=\"h-6 w-6 shrink-0\"\n        />\n        <span className=\"truncate\" title={opt.label}>\n          {opt.label}\n        </span>\n      </div>\n    );\n  }, []);\n\n  if (isEditing) {\n    return (\n      <div className={cn('min-w-0', className)}>\n        <Autocomplete\n          options={options}\n          value={currentAssignee?.id ?? null}\n          onChange={val => {\n            onAssign(val);\n            setIsEditing(false);\n          }}\n          placeholder=\"Search users...\"\n          loading={isLoading}\n          showChevron={false}\n          startAdornment={\n            hasAssignee ? (\n              currentAssignee.deleted ? (\n                <DeletedUserAvatar\n                  size=\"sm\"\n                  className=\"h-6 w-6\"\n                  accessibleLabel={`Deleted user: ${currentAssignee.name || currentAssignee.id}`}\n                />\n              ) : (\n                <SquareAvatar\n                  src={currentAssignee.avatarSrc}\n                  alt={currentAssignee.name}\n                  fallback={currentAssignee.name || 'User'}\n                  size=\"sm\"\n                  variant=\"round\"\n                  className=\"h-6 w-6\"\n                />\n              )\n            ) : (\n              <UserIcon className=\"size-5 text-ods-text-secondary\" />\n            )\n          }\n          renderOption={renderOption}\n        />\n        <span className=\"mt-0.5 block truncate text-ods-text-secondary text-h6\">Assigned</span>\n      </div>\n    );\n  }\n\n  if (hasAssignee) {\n    return (\n      <div className={cn('flex min-w-0 items-center gap-[var(--spacing-system-xs)]', className)}>\n        {currentAssignee.deleted ? (\n          <DeletedUserAvatar\n            size=\"md\"\n            accessibleLabel={`Deleted user: ${currentAssignee.name || currentAssignee.id}`}\n          />\n        ) : (\n          <SquareAvatar\n            src={currentAssignee.avatarSrc}\n            alt={currentAssignee.name}\n            fallback={currentAssignee.name || 'User'}\n            size=\"md\"\n            variant=\"round\"\n            className=\"shrink-0\"\n          />\n        )}\n        <div className=\"min-w-0 flex-1 overflow-hidden\">\n          <div className=\"flex flex-col justify-center\">\n            <div className=\"flex w-full min-w-0 items-center gap-[var(--spacing-system-xxs)]\">\n              <button\n                type=\"button\"\n                onClick={() => setIsEditing(true)}\n                className=\"group flex cursor-pointer items-center gap-[var(--spacing-system-xxs)] text-left\"\n              >\n                <PenEditIcon className=\"size-4 shrink-0 text-ods-text-secondary transition-colors group-hover:text-ods-accent\" />\n                <span\n                  className={cn(\n                    'truncate text-h4',\n                    currentAssignee.deleted ? 'text-ods-error' : 'text-ods-text-primary',\n                  )}\n                  title={currentAssignee.name}\n                >\n                  {currentAssignee.name}\n                </span>\n              </button>\n            </div>\n            <span className=\"truncate text-ods-text-secondary text-h6\">Assigned</span>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn('min-w-0', className)}>\n      <button\n        type=\"button\"\n        onClick={() => setIsEditing(true)}\n        className=\"flex cursor-pointer items-center gap-[var(--spacing-system-xxs)] truncate text-left text-ods-accent underline transition-opacity text-h4 hover:opacity-80\"\n      >\n        <UserIcon className=\"size-4 shrink-0\" />\n        <span>Assign User</span>\n      </button>\n      <span className=\"block truncate text-ods-text-secondary text-h6\">Assigned</span>\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../utils/cn';\nimport { UserXmarkIcon } from '../icons-v2-generated';\n\n/** Mirrors SquareAvatar's size buckets (sm 32 / md 40 / lg 48). */\nconst SIZE_CLASSES = {\n  sm: 'h-8 w-8',\n  md: 'h-10 w-10',\n  lg: 'h-12 w-12',\n} as const;\n\nconst ICON_CLASSES = {\n  sm: 'w-4 h-4',\n  md: 'w-5 h-5',\n  lg: 'w-6 h-6',\n} as const;\n\nexport interface DeletedUserAvatarProps {\n  size?: keyof typeof SIZE_CLASSES;\n  className?: string;\n  /**\n   * Accessible name for the placeholder. Pass the user's identity when known\n   * (e.g. \"Deleted user: Jane Doe\") so assistive technology doesn't lose who\n   * the row refers to. Falls back to the generic \"Deleted user\".\n   */\n  accessibleLabel?: string;\n}\n\n/**\n * Red round user-x placeholder shown instead of the avatar for deleted users\n * (accounts with status DELETED / SELF_DELETED). A dedicated component — not a\n * `SquareAvatar` prop — because `SquareAvatar`'s only fallback is text\n * initials, and initials for a deleted account is exactly what the design\n * replaces.\n */\nexport function DeletedUserAvatar({ size = 'md', className, accessibleLabel }: DeletedUserAvatarProps) {\n  return (\n    <span\n      role=\"img\"\n      aria-label={accessibleLabel || 'Deleted user'}\n      className={cn(\n        'flex shrink-0 items-center justify-center rounded-full bg-ods-error/20',\n        SIZE_CLASSES[size],\n        className,\n      )}\n    >\n      <UserXmarkIcon className={cn('text-ods-error', ICON_CLASSES[size])} />\n    </span>\n  );\n}\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { type ReactNode, useMemo, useState } from 'react';\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';\nimport { cn } from '../../utils/cn';\nimport { CheckIcon, SearchIcon } from '../icons-v2-generated';\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';\nimport { Input } from './input';\n\nexport interface SearchableSelectOption {\n  value: string;\n  label: string;\n  /** Optional leading visual for the option row (avatar, icon, badge). */\n  icon?: ReactNode;\n}\n\nexport interface SearchableSelectProps {\n  options: SearchableSelectOption[];\n  /** Currently selected option value; null/undefined when nothing is selected. */\n  value?: string | null;\n  onValueChange: (value: string) => void;\n  /** Default trigger text while nothing is selected. */\n  placeholder?: string;\n  /** Placeholder of the search input rendered as the first dropdown item. */\n  searchPlaceholder?: string;\n  emptyText?: string;\n  loadingText?: string;\n  isLoading?: boolean;\n  disabled?: boolean;\n  /**\n   * Custom trigger node (rendered via Popover.Trigger asChild). When omitted,\n   * a select-like button (Input/SelectTrigger styling, chevron, placeholder)\n   * is rendered.\n   */\n  trigger?: ReactNode;\n  /** Custom option-row renderer; default is icon + label + selected check. */\n  renderOption?: (option: SearchableSelectOption, isSelected: boolean) => ReactNode;\n  align?: 'start' | 'center' | 'end';\n  /** Classes for the default trigger button. */\n  className?: string;\n  /** Classes for the popover content (e.g. a fixed width for icon triggers). */\n  contentClassName?: string;\n}\n\n/**\n * Select-style dropdown whose first item is a search field filtering the\n * option list — the pattern used by the ticket assignee picker (\"Search\n * users...\") generalized for any entity (devices, users, ...). Provide a\n * custom `trigger` for icon-button use cases; the default trigger looks and\n * behaves like `SelectTrigger`.\n */\nexport function SearchableSelect({\n  options,\n  value = null,\n  onValueChange,\n  placeholder = 'Select...',\n  searchPlaceholder = 'Search...',\n  emptyText = 'No results found',\n  loadingText = 'Loading…',\n  isLoading = false,\n  disabled = false,\n  trigger,\n  renderOption,\n  align = 'start',\n  className,\n  contentClassName,\n}: SearchableSelectProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [search, setSearch] = useState('');\n  const keyboardPadding = useKeyboardCollisionPadding();\n\n  // Clear the query on close, while rendering rather than from an effect: the\n  // popover closes and reopens without unmounting, so an effect left the\n  // previous query in the field for the frame the popover reopened in. Guarded\n  // on `search`, so the extra render pass this schedules takes the early exit.\n  if (!isOpen && search !== '') setSearch('');\n\n  const filtered = useMemo(() => {\n    const q = search.trim().toLowerCase();\n    if (!q) return options;\n    return options.filter(o => o.label.toLowerCase().includes(q));\n  }, [options, search]);\n\n  const selectedOption = useMemo(\n    () => (value != null ? options.find(o => o.value === value) : undefined),\n    [options, value],\n  );\n\n  const handleSelect = (optionValue: string) => {\n    onValueChange(optionValue);\n    setIsOpen(false);\n  };\n\n  const defaultTrigger = (\n    <button\n      type=\"button\"\n      disabled={disabled}\n      className={cn(\n        // Mirrors SelectTrigger so the closed state is indistinguishable from\n        // the regular Select.\n        'flex h-11 w-full items-center justify-between gap-2 rounded-md border px-3 text-left outline-none md:h-12',\n        'border-ods-border bg-ods-card text-h4',\n        selectedOption ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n        'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',\n        'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',\n        'group cursor-pointer transition-colors duration-200 disabled:!cursor-not-allowed disabled:bg-ods-bg',\n        className,\n      )}\n    >\n      <span className=\"line-clamp-1\">{selectedOption ? selectedOption.label : placeholder}</span>\n      <Chevron02DownIcon\n        className=\"shrink-0 text-ods-text-secondary transition-all duration-200 group-data-[state=open]:rotate-180 group-data-[state=open]:text-ods-accent\"\n        size={24}\n      />\n    </button>\n  );\n\n  return (\n    <PopoverPrimitive.Root open={isOpen} onOpenChange={disabled ? undefined : setIsOpen} modal={false}>\n      <PopoverPrimitive.Trigger asChild>{trigger ?? defaultTrigger}</PopoverPrimitive.Trigger>\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Content\n          align={align}\n          sideOffset={6}\n          // The search field below raises the software keyboard the moment this\n          // opens — see useKeyboardCollisionPadding for why neither the collision\n          // viewport nor the available height knows about it otherwise.\n          collisionPadding={{ bottom: keyboardPadding }}\n          className={cn(\n            'z-50 min-w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-md border border-ods-border bg-ods-card shadow-lg',\n            'flex max-h-[var(--radix-popper-available-height)] flex-col',\n            'data-[state=open]:animate-in data-[state=closed]:animate-out',\n            'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n            'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n            'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',\n            contentClassName,\n          )}\n        >\n          <div className=\"shrink-0 border-b border-ods-border\">\n            <Input\n              autoFocus\n              value={search}\n              onChange={e => setSearch(e.target.value)}\n              placeholder={searchPlaceholder}\n              startAdornment={<SearchIcon className=\"size-4 text-ods-text-secondary\" />}\n              className=\"rounded-none border-0\"\n            />\n          </div>\n          <div className=\"max-h-80 min-h-0 overflow-y-auto py-[var(--spacing-system-xs)]\" role=\"listbox\">\n            {isLoading ? (\n              <div className=\"px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-ods-text-secondary text-h5\">\n                {loadingText}\n              </div>\n            ) : filtered.length === 0 ? (\n              <div className=\"px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-ods-text-secondary text-h5\">\n                {emptyText}\n              </div>\n            ) : (\n              filtered.map(opt => {\n                const isSelected = value === opt.value;\n                return (\n                  <button\n                    key={opt.value}\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    onClick={() => handleSelect(opt.value)}\n                    className={cn(\n                      'flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',\n                      'transition-colors hover:bg-ods-bg-hover',\n                      isSelected && 'bg-ods-bg-hover',\n                    )}\n                  >\n                    {renderOption ? (\n                      renderOption(opt, isSelected)\n                    ) : (\n                      <>\n                        {opt.icon}\n                        <span className=\"flex-1 truncate text-ods-text-primary text-h4\" title={opt.label}>\n                          {opt.label}\n                        </span>\n                        {isSelected && <CheckIcon className=\"size-4 shrink-0 text-ods-accent\" />}\n                      </>\n                    )}\n                  </button>\n                );\n              })\n            )}\n          </div>\n        </PopoverPrimitive.Content>\n      </PopoverPrimitive.Portal>\n    </PopoverPrimitive.Root>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport Link from '../../embed-shims/next-link';\nimport { cn } from '../../utils/cn';\nimport { AssigneeDropdown, type AssigneeDropdownProps } from './assignee-dropdown';\nimport { SquareAvatar } from './square-avatar';\nimport { TicketStatusTag, type TicketStatusTagProps } from './ticket-status-tag';\n\n/** A plain-text value, optionally with a trailing image (avatar), leading icon, or click action. */\nexport interface InfoSectionTextValue {\n  type?: 'text';\n  text: string;\n  /** Image shown after the text (e.g. a user or organization avatar). */\n  imageSrc?: string;\n  /** Initials shown when the image is missing or fails to load. Defaults to `text`. */\n  imageFallback?: string;\n  /** Avatar shape. Defaults to `round`. */\n  imageVariant?: 'square' | 'round';\n  /** Icon shown before the text. */\n  icon?: ReactNode;\n  /** Renders the text as a button; on hover it takes the accent color. */\n  onClick?: () => void;\n  /**\n   * Renders the text as a link (Next.js `Link`). Takes precedence over `onClick`.\n   * A regular click navigates in the same tab; the browser's native new-tab\n   * affordances (cmd/ctrl-click, \"open in new tab\") work as usual.\n   */\n  href?: string;\n  /** Force the link to always open in a new tab (sets `target=\"_blank\"`). */\n  openInNewTab?: boolean;\n}\n\n/** An inline assignee picker (autocomplete with search). Defaults to the `compact` variant. */\nexport type InfoSectionAssigneeValue = { type: 'assignee' } & AssigneeDropdownProps;\n\n/** An inline status changer (dropdown). Pass `options` + `onSelect` to make it interactive. */\nexport type InfoSectionStatusValue = { type: 'status' } & TicketStatusTagProps;\n\n/** Arbitrary value content. */\nexport interface InfoSectionCustomValue {\n  type: 'custom';\n  content: ReactNode;\n}\n\nexport type InfoSectionValue =\n  InfoSectionTextValue | InfoSectionAssigneeValue | InfoSectionStatusValue | InfoSectionCustomValue;\n\nexport interface InfoSectionRow {\n  /** Stable key for the row. */\n  id: string;\n  /** Left-side label. */\n  label: string;\n  /** Right-side value: text (with optional image), assignee picker, status dropdown, or custom node. */\n  value: InfoSectionValue;\n}\n\nexport interface InfoSectionProps {\n  /** Optional caption rendered above the card (e.g. \"Ticket Details\"). */\n  title?: string;\n  rows: InfoSectionRow[];\n  className?: string;\n}\n\nfunction TextValue({ value }: { value: InfoSectionTextValue }) {\n  const { text, imageSrc, imageFallback, imageVariant = 'round', icon, onClick, href, openInNewTab } = value;\n  const hasImage = imageSrc !== undefined || imageFallback !== undefined;\n  const interactiveClass = 'truncate text-left text-h4 text-ods-text-primary transition-colors hover:text-ods-accent';\n  return (\n    <>\n      {icon && <span className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary\">{icon}</span>}\n      {href ? (\n        <Link\n          href={href}\n          target={openInNewTab ? '_blank' : undefined}\n          rel={openInNewTab ? 'noopener noreferrer' : undefined}\n          title={text}\n          className={interactiveClass}\n        >\n          {text}\n        </Link>\n      ) : onClick ? (\n        <button type=\"button\" onClick={onClick} title={text} className={interactiveClass}>\n          {text}\n        </button>\n      ) : (\n        <span className=\"truncate text-ods-text-primary text-h4\" title={text}>\n          {text}\n        </span>\n      )}\n      {hasImage && (\n        <SquareAvatar\n          src={imageSrc}\n          alt={text}\n          fallback={imageFallback ?? text}\n          size=\"sm\"\n          variant={imageVariant}\n          className=\"h-4 w-4\"\n        />\n      )}\n    </>\n  );\n}\n\nfunction RowValue({ value }: { value: InfoSectionValue }) {\n  switch (value.type) {\n    case 'assignee': {\n      const { type: _type, ...props } = value;\n      return <AssigneeDropdown {...props} variant={props.variant ?? 'compact'} />;\n    }\n    case 'status': {\n      const { type: _type, ...props } = value;\n      return <TicketStatusTag {...props} />;\n    }\n    case 'custom':\n      return <>{value.content}</>;\n    default:\n      return <TextValue value={value} />;\n  }\n}\n\n/**\n * A labelled info card: a vertical stack of `label —— value` rows inside a\n * bordered card, with an optional uppercase caption above it. Each row's value\n * is configurable — plain text (with an optional trailing image), an inline\n * assignee picker (autocomplete with search), an inline status dropdown, or an\n * arbitrary node.\n */\nexport function InfoSection({ title, rows, className }: InfoSectionProps) {\n  return (\n    <div className={cn('flex flex-col gap-[var(--spacing-system-xxs)]', className)}>\n      {title && <p className=\"text-ods-text-secondary text-h5\">{title}</p>}\n      <div className=\"flex flex-col gap-[var(--spacing-system-xsf)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-mf)]\">\n        {rows.map(row => (\n          <div key={row.id} className=\"flex w-full min-w-0 items-center gap-[var(--spacing-system-xsf)]\">\n            <span className=\"shrink-0 text-ods-text-secondary text-h4\">{row.label}</span>\n            <div className=\"h-px min-w-4 flex-1 bg-ods-border\" />\n            <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xsf)]\">\n              <RowValue value={row.value} />\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { type ButtonHTMLAttributes, type ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface InputTriggerProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {\n  selectedLabel?: ReactNode;\n  placeholder?: string;\n  startIcon?: ReactNode;\n  endIcon?: ReactNode;\n  invalid?: boolean;\n}\n\n/**\n * Input-styled button used to open a non-`<Select>` popup — `ActionsMenuDropdown`,\n * `Popover`, custom calendars, etc.\n *\n * Use `InputTrigger` when:\n *   - You need a select-shaped trigger that opens something other than `<Select>`.\n *   - You want consistent Input-field visuals next to other form fields.\n *\n * Don't use `InputTrigger` for:\n *   - Single-value selection from a flat list — use `<Select>` / `<Autocomplete>`.\n *   - Page-level CTAs / action buttons — use `<Button>` (bold, centered, hierarchical).\n *\n * The component is `forwardRef`'d so it works as the `asChild` child of any Radix\n * trigger (DropdownMenu, Popover, etc.). For form labels & error messages, compose\n * with `<FieldWrapper>`.\n */\nexport const InputTrigger = forwardRef<HTMLButtonElement, InputTriggerProps>(\n  ({ selectedLabel, placeholder, startIcon, endIcon, invalid, className, disabled, ...props }, ref) => {\n    const isPlaceholder = selectedLabel === undefined || selectedLabel === null || selectedLabel === '';\n\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        disabled={disabled}\n        data-invalid={invalid || undefined}\n        {...props}\n        className={cn(\n          'flex h-11 w-full items-center gap-2 rounded-[6px] border px-3 outline-none md:h-12',\n          'text-h4',\n          'border-ods-border bg-ods-card text-ods-text-primary',\n          'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',\n          !invalid && 'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',\n          invalid && 'border-ods-error enabled:hover:border-ods-error data-[state=open]:border-ods-error',\n          // Disabled - match Input / SelectTrigger: grey the TEXT, don't fade the\n          // whole control (a blanket opacity also washes out the border and the\n          // adornment icons, which no other field does). The child rule catches\n          // the label span, which sets its own placeholder colour. Scoped to\n          // DIRECT children: `selectedLabel` is a ReactNode the caller owns, so\n          // anything inside it that sets its own colour keeps it.\n          'disabled:cursor-not-allowed disabled:bg-ods-bg',\n          'disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled',\n          'transition-colors duration-200',\n          className,\n        )}\n      >\n        {startIcon && <span className=\"flex shrink-0 items-center text-ods-text-secondary\">{startIcon}</span>}\n        <span\n          className={cn('min-w-0 flex-1 truncate text-left', isPlaceholder && 'text-ods-text-secondary')}\n          title={isPlaceholder ? placeholder : typeof selectedLabel === 'string' ? selectedLabel : undefined}\n        >\n          {isPlaceholder ? placeholder : selectedLabel}\n        </span>\n        {endIcon && <span className=\"flex shrink-0 items-center text-ods-text-secondary\">{endIcon}</span>}\n      </button>\n    );\n  },\n);\nInputTrigger.displayName = 'InputTrigger';\n","'use client';\n\nimport { Video, Image as ImageIcon, FileText, Archive, CheckSquare, BookOpen, FileType } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select';\n\nexport type ResourceMediaType = 'video' | 'png' | 'jpg' | 'svg' | 'document' | 'pdf' | 'zip' | 'guide' | 'checklist';\n\ninterface MediaTypeOption {\n  value: ResourceMediaType;\n  label: string;\n  icon: ReactNode;\n}\n\nconst mediaTypeOptions: MediaTypeOption[] = [\n  { value: 'video', label: 'Video', icon: <Video className=\"h-4 w-4\" /> },\n  { value: 'png', label: 'PNG Image', icon: <ImageIcon className=\"h-4 w-4\" /> },\n  { value: 'jpg', label: 'JPG Image', icon: <ImageIcon className=\"h-4 w-4\" /> },\n  { value: 'svg', label: 'SVG Image', icon: <ImageIcon className=\"h-4 w-4\" /> },\n  { value: 'pdf', label: 'PDF', icon: <FileType className=\"h-4 w-4\" /> },\n  { value: 'document', label: 'Document', icon: <FileText className=\"h-4 w-4\" /> },\n  { value: 'zip', label: 'ZIP Archive', icon: <Archive className=\"h-4 w-4\" /> },\n  { value: 'guide', label: 'Guide', icon: <BookOpen className=\"h-4 w-4\" /> },\n  { value: 'checklist', label: 'Checklist', icon: <CheckSquare className=\"h-4 w-4\" /> },\n];\n\ninterface MediaTypeSelectorProps {\n  value?: ResourceMediaType;\n  onValueChange?: (value: ResourceMediaType) => void;\n  placeholder?: string;\n  className?: string;\n  disabled?: boolean;\n}\n\nexport function MediaTypeSelector({\n  value,\n  onValueChange,\n  placeholder = 'Select media type',\n  className,\n  disabled,\n}: MediaTypeSelectorProps) {\n  return (\n    // Remount the Select whenever `value` changes — it reads `defaultValue` at\n    // mount, so without a fresh instance an externally-set value never shows.\n    // The key is the value itself: the old counter state was bumped from an\n    // effect, which remounted the Select on MOUNT too (0 → 1 with nothing\n    // changed) and cost an extra render pass on every change to say what the\n    // prop already said.\n    <Select\n      key={value ?? '__unset__'}\n      value={value || undefined}\n      onValueChange={onValueChange}\n      disabled={disabled}\n      defaultValue={value}\n    >\n      <SelectTrigger className={className}>\n        <SelectValue placeholder={placeholder} />\n      </SelectTrigger>\n      <SelectContent>\n        {mediaTypeOptions.map(option => (\n          <SelectItem key={option.value} value={option.value}>\n            <div className=\"flex items-center gap-2\">\n              {option.icon}\n              <span>{option.label}</span>\n            </div>\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n","import React from 'react';\n\nimport { cn } from '../../../utils/cn';\nimport { Button, type ButtonProps } from '../button';\nimport { NoDataActions, type NoDataActionProps } from './no-data-actions';\nimport { NoDataMessage } from './no-data-message';\n\ninterface NoDataProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n  // --- Message ---\n  /** Leading glyph, sized to 16px (mobile) / 24px (md+). */\n  icon?: React.ReactNode;\n  /** Primary line. */\n  title?: React.ReactNode;\n  /** Secondary line. */\n  description?: React.ReactNode;\n\n  // --- Clickable blocks ---\n  /** Clickable blocks. Dynamic count. Hidden when empty/omitted. */\n  actions?: NoDataActionProps[];\n\n  // --- Footer button ---\n  /** Footer button label. When set (and `button` is not), renders the button. */\n  buttonLabel?: React.ReactNode;\n  /** Leading icon for the footer button. */\n  buttonIcon?: React.ReactNode;\n  /** Click handler for the footer button. */\n  onButtonClick?: ButtonProps['onClick'];\n  /** Extra props forwarded to the footer button (variant, href, loading, …). */\n  buttonProps?: Partial<ButtonProps>;\n  /** Fully custom footer node; overrides the `buttonLabel`/`buttonIcon` path. */\n  button?: React.ReactNode;\n}\n\n/**\n * A centered empty-state panel: a muted icon/title/description message, an\n * optional group of clickable blocks, and an optional footer button, stacked\n * vertically with breakpoint-aware spacing. Each region renders only when its\n * inputs are present, so the same component covers a bare message, a message\n * with a button, the full layout, and everything in between.\n */\nconst NoData = React.forwardRef<HTMLDivElement, NoDataProps>(function NoDataImpl(\n  {\n    icon,\n    title,\n    description,\n    actions,\n    buttonLabel,\n    buttonIcon,\n    onButtonClick,\n    buttonProps,\n    button,\n    className,\n    ...props\n  },\n  ref,\n) {\n  const hasMessage = !!(icon || title || description);\n  const hasActions = !!actions?.length;\n\n  const footerButton =\n    button ??\n    (buttonLabel != null ? (\n      <Button\n        variant=\"outline\"\n        leftIcon={buttonIcon}\n        onClick={onButtonClick}\n        {...buttonProps}\n        className={cn('w-full md:w-auto', buttonProps?.className)}\n      >\n        {buttonLabel}\n      </Button>\n    ) : null);\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        'flex w-full flex-col items-center justify-center gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)]',\n        className,\n      )}\n      {...props}\n    >\n      {hasMessage && <NoDataMessage icon={icon} title={title} description={description} />}\n      {hasActions && <NoDataActions actions={actions} />}\n      {footerButton}\n    </div>\n  );\n});\nNoData.displayName = 'NoData';\n\nexport { NoData, type NoDataProps };\n","import React from 'react';\n\nimport { cn } from '../../../utils/cn';\nimport { noDataActionsVariants, noDataIconClasses, type NoDataActionsVariant } from './no-data-styles';\n\ninterface NoDataActionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n  /** Leading glyph, sized to 16px (mobile) / 24px (md+). Optional. */\n  icon?: React.ReactNode;\n  /** Short label describing the block. */\n  label?: React.ReactNode;\n}\n\n/**\n * A single static info block: a leading icon with a short label, laid out\n * icon-beside-label on mobile and icon-above-label on larger screens. Purely\n * presentational — no hover or click. The outer border, rounded corners and\n * dividers come from the surrounding group. Capped at 244px wide on md+.\n */\nconst NoDataAction = React.forwardRef<HTMLDivElement, NoDataActionProps>(function NoDataActionImpl(\n  { icon, label, children, className, ...props },\n  ref,\n) {\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        'flex flex-1 items-center gap-[var(--spacing-system-s)] p-[var(--spacing-system-m)] text-ods-text-secondary',\n        'md:w-[224px] md:flex-none md:flex-col md:items-start md:gap-[var(--spacing-system-xs)]',\n        className,\n      )}\n      {...props}\n    >\n      {icon && <span className={cn(noDataIconClasses, 'text-ods-text-primary')}>{icon}</span>}\n      <span className=\"min-w-0 flex-1 break-words text-h6 md:w-full md:flex-none\">{label ?? children}</span>\n    </div>\n  );\n});\nNoDataAction.displayName = 'NoDataAction';\n\ninterface NoDataActionsProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** The blocks to render. When omitted, `children` are rendered instead. */\n  actions?: NoDataActionProps[];\n  /** Surface treatment of the group panel. */\n  variant?: NoDataActionsVariant;\n}\n\n/**\n * A group of static info blocks sharing one transparent, bordered panel. Renders\n * as a horizontal row with vertical dividers on larger screens and a vertical\n * stack with horizontal dividers on mobile. Only the outermost blocks have\n * rounded corners.\n */\nconst NoDataActions = React.forwardRef<HTMLDivElement, NoDataActionsProps>(function NoDataActionsImpl(\n  { actions, variant = 'outline', className, children, ...props },\n  ref,\n) {\n  const blocks = actions\n    ? actions.map((action, index) => <NoDataAction key={index} {...action} />)\n    : React.Children.toArray(children);\n\n  return (\n    <div\n      ref={ref}\n      className={cn(noDataActionsVariants({ variant }), 'flex w-full flex-col md:w-auto md:flex-row', className)}\n      {...props}\n    >\n      {blocks.map((block, index) => (\n        <React.Fragment key={index}>\n          {index > 0 && (\n            <div\n              aria-hidden=\"true\"\n              className=\"shrink-0 self-stretch border-t border-ods-border md:border-l md:border-t-0\"\n            />\n          )}\n          {block}\n        </React.Fragment>\n      ))}\n    </div>\n  );\n});\nNoDataActions.displayName = 'NoDataActions';\n\nexport { NoDataAction, NoDataActions, type NoDataActionProps, type NoDataActionsProps };\n","// Shared style atoms for the NoData (empty-state) family. A single source of\n// truth for the icon scale and the info-block group surface, expressed as CVA\n// variants so new looks can be added without touching the markup.\n\nimport { cva, type VariantProps } from 'class-variance-authority';\n\n// Leading-icon glyph scale: 16px on mobile, 24px on md+. Color is intentionally\n// omitted so the glyph inherits the current text color.\nexport const noDataIconClasses =\n  'inline-flex shrink-0 items-center justify-center [&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6';\n\n// Surface for the info-block group container: a transparent, bordered panel with\n// rounded corners. `overflow-hidden` keeps only the outer blocks round — inner\n// blocks stay square.\nexport const noDataActionsVariants = cva('overflow-hidden rounded-md', {\n  variants: {\n    variant: {\n      outline: 'border border-ods-border',\n    },\n  },\n  defaultVariants: { variant: 'outline' },\n});\n\nexport type NoDataActionsVariant = NonNullable<VariantProps<typeof noDataActionsVariants>['variant']>;\n","import React from 'react';\n\nimport { cn } from '../../../utils/cn';\nimport { noDataIconClasses } from './no-data-styles';\n\ninterface NoDataMessageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {\n  /** Leading glyph, sized to 16px (mobile) / 24px (md+). Optional. */\n  icon?: React.ReactNode;\n  /** Primary line (body size). */\n  title?: React.ReactNode;\n  /** Secondary line (caption size). */\n  description?: React.ReactNode;\n}\n\n/**\n * A transparent, centered stack of an icon, a title and a description, in muted\n * gray. Any piece is optional, so it also covers \"icon + title\", \"title only\",\n * and similar reduced forms.\n */\nconst NoDataMessage = React.forwardRef<HTMLDivElement, NoDataMessageProps>(function NoDataMessageImpl(\n  { icon, title, description, className, ...props },\n  ref,\n) {\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        'flex flex-col items-center gap-[var(--spacing-system-l)] text-center text-ods-text-secondary',\n        className,\n      )}\n      {...props}\n    >\n      {icon && <span className={noDataIconClasses}>{icon}</span>}\n      {(title || description) && (\n        <div className=\"flex flex-col\">\n          {title && <p className=\"break-words text-h4\">{title}</p>}\n          {description && <p className=\"break-words text-h6\">{description}</p>}\n        </div>\n      )}\n    </div>\n  );\n});\nNoDataMessage.displayName = 'NoDataMessage';\n\nexport { NoDataMessage, type NoDataMessageProps };\n","'use client';\n\nimport { cn } from '../../utils/cn';\nimport { OpenFrameLogo } from '../icons/openframe-logo';\n\nexport interface PageLoaderProps {\n  /**\n   * Main loading text\n   */\n  title?: string;\n  /**\n   * Secondary description text\n   */\n  description?: string;\n  /**\n   * Additional CSS classes for the container\n   */\n  className?: string;\n  /**\n   * Show the loader icon\n   */\n  showIcon?: boolean;\n}\n\n/**\n * Full page loader component\n * Used for loading states that take up the full viewport\n */\nexport function PageLoader({\n  title = 'Loading',\n  description = 'Getting your data ready',\n  className,\n  showIcon = true,\n}: PageLoaderProps) {\n  return (\n    <div className={cn('flex min-h-screen w-full flex-col items-center justify-center bg-ods-bg', className)}>\n      <div className=\"flex flex-col items-center gap-6 p-6\">\n        {showIcon && (\n          <div className=\"relative h-6 w-6\">\n            <OpenFrameLogo\n              className=\"h-6 w-6 animate-pulse text-ods-accent\"\n              upperPathColor=\"currentColor\"\n              lowerPathColor=\"var(--color-text-tertiary)\"\n            />\n          </div>\n        )}\n        <div className=\"flex flex-col items-center text-center\">\n          <p className=\"text-ods-text-tertiary text-h4\">{title}</p>\n          <p className=\"mt-1 text-ods-text-tertiary opacity-70 text-h6\">{description}</p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * Compact version of the page loader for smaller containers\n */\nexport function CompactPageLoader({ title = 'Loading', description, className, showIcon = true }: PageLoaderProps) {\n  return (\n    <div className={cn('flex flex-col items-center justify-center p-12', className)}>\n      <div className=\"flex flex-col items-center gap-4\">\n        {showIcon && (\n          <div className=\"relative h-6 w-6\">\n            <OpenFrameLogo\n              className=\"h-6 w-6 animate-pulse text-ods-accent\"\n              upperPathColor=\"currentColor\"\n              lowerPathColor=\"var(--color-text-tertiary)\"\n            />\n          </div>\n        )}\n        <div className=\"flex flex-col items-center text-center\">\n          <p className=\"text-ods-text-tertiary text-h6\">{title}</p>\n          {description && <p className=\"mt-1 text-ods-text-tertiary opacity-70 text-h6\">{description}</p>}\n        </div>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport * as RadioGroupPrimitive from '@radix-ui/react-radio-group';\nimport { type ComponentPropsWithoutRef, type ComponentRef, type ReactNode, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst RadioGroup = forwardRef<\n  ComponentRef<typeof RadioGroupPrimitive.Root>,\n  ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />;\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = forwardRef<\n  ComponentRef<typeof RadioGroupPrimitive.Item>,\n  ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        'group relative h-6 w-6 shrink-0 rounded-full border-2 bg-ods-card',\n        'border-[var(--color-border-strong)]',\n        'transition-colors duration-150',\n        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-card',\n        'disabled:cursor-not-allowed disabled:opacity-50',\n        'data-[state=checked]:border-ods-accent',\n        'hover:enabled:border-ods-accent/60',\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex h-full w-full items-center justify-center\">\n        <span className=\"block h-1/2 w-1/2 rounded-full bg-ods-accent\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\ninterface RadioGroupBlockOption {\n  value: string;\n  label: ReactNode;\n  description?: ReactNode;\n  disabled?: boolean;\n  /** Optional trailing slot rendered at the end of the row (e.g. a discount tag) */\n  trailing?: ReactNode;\n}\n\ninterface RadioGroupBlockProps extends Omit<ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>, 'children'> {\n  options: RadioGroupBlockOption[];\n  /**\n   * - `separated` (default): each option is its own bordered card stacked with gaps.\n   * - `grouped`: options share a single outer border with dividers between rows.\n   */\n  variant?: 'separated' | 'grouped';\n  /** Error message displayed below the group (also triggers red borders) */\n  error?: string;\n  itemClassName?: string;\n}\n\nconst RadioGroupBlock = forwardRef<ComponentRef<typeof RadioGroupPrimitive.Root>, RadioGroupBlockProps>(\n  ({ className, options, variant = 'separated', error, itemClassName, disabled, ...props }, ref) => {\n    const isGrouped = variant === 'grouped';\n    return (\n      <div className={cn('relative flex w-full flex-col', className)}>\n        <RadioGroupPrimitive.Root\n          ref={ref}\n          // Same marker every field exposes — it is how a form finds (and\n          // scrolls to) the first control that failed validation. The GROUP is\n          // the field here; the individual options are not separately invalid.\n          data-invalid={error ? true : undefined}\n          className={cn(\n            'w-full',\n            isGrouped\n              ? cn(\n                  'flex flex-col overflow-hidden rounded-[6px] border bg-ods-card',\n                  error ? 'border-ods-error' : 'border-ods-border',\n                )\n              : 'grid gap-2',\n          )}\n          disabled={disabled}\n          {...props}\n        >\n          {options.map((option, index) => {\n            const itemId = `${props.name ?? 'radio-block'}-${option.value}`;\n            const isDisabled = disabled || option.disabled;\n            const isLast = index === options.length - 1;\n            return (\n              <label\n                key={option.value}\n                htmlFor={itemId}\n                aria-disabled={isDisabled || undefined}\n                className={cn(\n                  'flex w-full items-center gap-3',\n                  'transition-colors duration-200',\n                  isDisabled ? 'cursor-not-allowed' : 'cursor-pointer',\n                  // Hover / active move the BACKGROUND only, like Input and\n                  // CheckboxBlock — the border stays put so an unselected row\n                  // never flashes \"selected\" under the cursor.\n                  !isDisabled && 'hover:bg-ods-bg-hover active:bg-ods-bg-active',\n                  isGrouped\n                    ? cn(\n                        'bg-ods-card px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs,8px)]',\n                        !isLast && 'border-b',\n                        error ? 'border-ods-error' : 'border-ods-border',\n                      )\n                    : cn(\n                        'rounded-[6px] border bg-ods-card p-[var(--spacing-system-sf)]',\n                        !isDisabled &&\n                          'has-[[data-state=checked]]:bg-[var(--ods-system-greys-soft-grey)]/30 has-[[data-state=checked]]:border-ods-accent/40',\n                        error ? 'border-ods-error' : 'border-ods-border',\n                      ),\n                  // Disabled matches a disabled field: flat `ods-bg` fill and\n                  // greyed text, not a fade of the whole row. The greying lives\n                  // on the TEXT COLUMN below, not here: as a descendant rule on\n                  // the row it also repainted whatever `trailing` renders — a\n                  // `Tag`'s label is a span too, and it went invisible against\n                  // its own fill.\n                  isDisabled && 'bg-ods-bg',\n                  itemClassName,\n                )}\n              >\n                <RadioGroupPrimitive.Item\n                  id={itemId}\n                  value={option.value}\n                  disabled={isDisabled}\n                  className={cn(\n                    'h-6 w-6 shrink-0 rounded-full border-2 bg-ods-card',\n                    error ? 'border-ods-error' : 'border-[var(--color-border-strong)]',\n                    'transition-colors duration-150',\n                    'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-card',\n                    // Disabled: the EMPTY dot dims with the text; a selected one\n                    // keeps its accent so \"on but locked\" stays readable.\n                    'disabled:cursor-not-allowed',\n                    'disabled:data-[state=unchecked]:border-ods-border disabled:data-[state=unchecked]:bg-ods-bg',\n                    'data-[state=checked]:border-ods-accent',\n                  )}\n                >\n                  <RadioGroupPrimitive.Indicator className=\"flex h-full w-full items-center justify-center\">\n                    <span className=\"block h-1/2 w-1/2 rounded-full bg-ods-accent\" />\n                  </RadioGroupPrimitive.Indicator>\n                </RadioGroupPrimitive.Item>\n                <div\n                  className={cn(\n                    'flex min-w-0 flex-1 flex-col justify-center',\n                    // Direct children only — these are the two component-owned\n                    // wrappers. `option.label` / `option.description` are\n                    // ReactNode, so a descendant rule would repaint content the\n                    // caller styled itself.\n                    isDisabled && '[&>span]:text-ods-text-disabled',\n                  )}\n                >\n                  <span\n                    className={cn(\n                      'font-[family-name:var(--font-h4-family)] text-[length:var(--font-size-h4-body)] font-[number:var(--font-h4-weight)] leading-[24px]',\n                      'select-none truncate text-ods-text-primary',\n                    )}\n                    title={typeof option.label === 'string' ? option.label : undefined}\n                  >\n                    {option.label}\n                  </span>\n                  {option.description && (\n                    <span\n                      className={cn(\n                        'font-[family-name:var(--font-h6-family)] text-[length:var(--font-size-h6-caption)] font-[number:var(--font-h6-weight)] leading-[20px]',\n                        'select-none text-ods-text-secondary',\n                      )}\n                    >\n                      {option.description}\n                    </span>\n                  )}\n                </div>\n                {option.trailing && <div className=\"ml-auto flex shrink-0 items-center\">{option.trailing}</div>}\n              </label>\n            );\n          })}\n        </RadioGroupPrimitive.Root>\n        {error && (\n          <p\n            className=\"absolute bottom-0 left-0 right-0 translate-y-full truncate text-ods-error text-h6\"\n            title={error}\n          >\n            {error}\n          </p>\n        )}\n      </div>\n    );\n  },\n);\nRadioGroupBlock.displayName = 'RadioGroupBlock';\n\nexport { RadioGroup, RadioGroupItem, RadioGroupBlock };\nexport type { RadioGroupBlockProps, RadioGroupBlockOption };\n","'use client';\n\nimport * as SwitchPrimitives from '@radix-ui/react-switch';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef, useEffect, useState } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Switch = forwardRef<\n  ElementRef<typeof SwitchPrimitives.Root>,\n  ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {\n    checked?: boolean;\n    onCheckedChange?: (checked: boolean) => void;\n  }\n>(({ className, checked, onCheckedChange, ...props }, ref) => {\n  // Use local state if not controlled\n  const [isChecked, setIsChecked] = useState(checked || false);\n\n  // Update local state when controlled prop changes\n  useEffect(() => {\n    if (checked !== undefined) {\n      setIsChecked(checked);\n    }\n  }, [checked]);\n\n  const handleCheckedChange = (newChecked: boolean) => {\n    setIsChecked(newChecked);\n    onCheckedChange?.(newChecked);\n  };\n\n  return (\n    <SwitchPrimitives.Root\n      className={cn(\n        'peer inline-flex h-4 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 bg-ods-card px-0.5 transition-colors hover:bg-ods-bg-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus focus-visible:ring-offset-2 focus-visible:ring-offset-background active:bg-ods-bg-active disabled:cursor-not-allowed disabled:opacity-50',\n        isChecked ? 'border-ods-accent hover:border-ods-accent-hover' : 'border-ods-text-secondary',\n        className,\n      )}\n      checked={checked}\n      onCheckedChange={handleCheckedChange}\n      {...props}\n      ref={ref}\n    >\n      <SwitchPrimitives.Thumb\n        className={cn(\n          'pointer-events-none block size-2 rounded-full transition-transform duration-200',\n          isChecked ? 'bg-ods-accent' : 'bg-ods-text-secondary',\n        )}\n        style={{\n          transform: isChecked ? 'translateX(8px)' : 'translateX(0px)',\n        }}\n      />\n    </SwitchPrimitives.Root>\n  );\n});\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { useState, useMemo } from 'react';\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';\nimport { cn } from '../../utils/cn';\nimport { CheckIcon, PlusIcon, SearchIcon, TrashIcon } from '../icons-v2-generated';\nimport { Button } from './button';\nimport { Input } from './input';\n\nexport interface TagSelectOption {\n  id: string;\n  label: string;\n}\n\nexport interface TagSelectDropdownProps {\n  /** All selectable tags. */\n  options: TagSelectOption[];\n  /** IDs of the currently selected tags. */\n  selectedIds: string[];\n  /** Called with the next full selection whenever a tag is toggled. */\n  onChange: (ids: string[]) => void;\n  /** Called when the inline \"Create\" row is chosen. Omit to disable creation. */\n  onCreate?: (name: string) => void;\n  /** Max length for a newly created tag name. Omit for no limit. */\n  maxCreateLength?: number;\n  /** Called when a row's hover trash button is clicked. Omit to hide delete. */\n  onDelete?: (id: string) => void;\n  isCreating?: boolean;\n  isDeleting?: boolean;\n  /** Trigger button label. */\n  triggerLabel?: string;\n  searchPlaceholder?: string;\n  disabled?: boolean;\n  align?: 'start' | 'center' | 'end';\n  /** Forwarded to the trigger button. */\n  className?: string;\n}\n\n/**\n * Multi-select tag picker rendered as a button-triggered dropdown (the\n * multi-select sibling of the compact `AssigneeDropdown`): a trigger opens a\n * popover with an internal search field, a checkable option list, and an\n * inline \"Create\" row. Selected chips are rendered by the caller.\n */\nexport function TagSelectDropdown({\n  options,\n  selectedIds,\n  onChange,\n  onCreate,\n  maxCreateLength,\n  onDelete,\n  isCreating = false,\n  isDeleting = false,\n  triggerLabel = 'Add Tags',\n  searchPlaceholder = 'Search or create tags...',\n  disabled = false,\n  align = 'start',\n  className,\n}: TagSelectDropdownProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [search, setSearch] = useState('');\n  const keyboardPadding = useKeyboardCollisionPadding();\n\n  // Clear the query on close, while rendering rather than from an effect: the\n  // popover closes and reopens without unmounting, so an effect left the\n  // previous query in the field for the frame the popover reopened in. Guarded\n  // on `search`, so the extra render pass this schedules takes the early exit.\n  if (!isOpen && search !== '') setSearch('');\n\n  const query = search.trim();\n  const filtered = useMemo(() => {\n    const q = query.toLowerCase();\n    if (!q) return options;\n    return options.filter(o => o.label.toLowerCase().includes(q));\n  }, [options, query]);\n\n  // Selected tags float to the top of the list.\n  const ordered = useMemo(() => {\n    const selected = filtered.filter(o => selectedIds.includes(o.id));\n    const rest = filtered.filter(o => !selectedIds.includes(o.id));\n    return [...selected, ...rest];\n  }, [filtered, selectedIds]);\n\n  const showCreate = !!onCreate && !!query && !options.some(o => o.label.toLowerCase() === query.toLowerCase());\n  const isCreateTooLong = maxCreateLength != null && query.length > maxCreateLength;\n  const canCreate = showCreate && !isCreateTooLong;\n\n  const toggle = (id: string) => {\n    onChange(selectedIds.includes(id) ? selectedIds.filter(i => i !== id) : [...selectedIds, id]);\n  };\n\n  const handleCreate = () => {\n    if (!onCreate || !canCreate) return;\n    onCreate(query);\n    setSearch('');\n  };\n\n  return (\n    <PopoverPrimitive.Root open={isOpen} onOpenChange={setIsOpen} modal={false}>\n      <PopoverPrimitive.Trigger asChild>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"small\"\n          leftIcon={<PlusIcon />}\n          disabled={disabled}\n          className={cn('w-fit focus-visible:ring-0', className)}\n        >\n          {triggerLabel}\n        </Button>\n      </PopoverPrimitive.Trigger>\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Content\n          align={align}\n          sideOffset={6}\n          // The search field below raises the software keyboard the moment this\n          // opens — see useKeyboardCollisionPadding.\n          collisionPadding={{ bottom: keyboardPadding }}\n          className={cn(\n            'z-50 w-72 overflow-hidden rounded-[6px] border border-ods-border bg-ods-card shadow-lg',\n            'flex max-h-[var(--radix-popper-available-height)] flex-col',\n            'data-[state=open]:animate-in data-[state=closed]:animate-out',\n            'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n            'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n            'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',\n          )}\n        >\n          <div className=\"shrink-0 border-b border-ods-border\">\n            <Input\n              autoFocus\n              value={search}\n              onChange={e => setSearch(e.target.value)}\n              onKeyDown={e => {\n                if (e.key === 'Enter' && canCreate) {\n                  e.preventDefault();\n                  handleCreate();\n                }\n              }}\n              placeholder={searchPlaceholder}\n              startAdornment={<SearchIcon className=\"size-4 text-ods-text-secondary\" />}\n              className=\"rounded-none border-0\"\n            />\n          </div>\n          <div className=\"max-h-72 min-h-0 overflow-y-auto py-[var(--spacing-system-xs)]\" role=\"listbox\">\n            {ordered.map(opt => {\n              const isSelected = selectedIds.includes(opt.id);\n              return (\n                <div\n                  key={opt.id}\n                  role=\"option\"\n                  aria-selected={isSelected}\n                  tabIndex={0}\n                  onClick={() => toggle(opt.id)}\n                  onKeyDown={e => {\n                    if (e.key === 'Enter' || e.key === ' ') {\n                      e.preventDefault();\n                      toggle(opt.id);\n                    }\n                  }}\n                  className={cn(\n                    'group/item flex w-full cursor-pointer items-center gap-[var(--spacing-system-xs)] text-left',\n                    'px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)]',\n                    'transition-colors hover:bg-ods-bg-hover',\n                    isSelected && 'bg-ods-bg-hover',\n                  )}\n                >\n                  <span\n                    className={cn('flex-1 truncate text-h4', isSelected ? 'text-ods-accent' : 'text-ods-text-primary')}\n                    title={opt.label}\n                  >\n                    {opt.label}\n                  </span>\n                  {isSelected && <CheckIcon className=\"size-4 shrink-0 text-ods-accent\" />}\n                  {onDelete && !isSelected && (\n                    <button\n                      type=\"button\"\n                      aria-label={`Delete tag ${opt.label}`}\n                      onClick={e => {\n                        e.stopPropagation();\n                        onDelete(opt.id);\n                      }}\n                      disabled={isDeleting}\n                      className={cn(\n                        'shrink-0 opacity-0 transition-opacity focus-visible:opacity-100 group-hover/item:opacity-100',\n                        'disabled:pointer-events-none disabled:opacity-50',\n                      )}\n                    >\n                      <TrashIcon className=\"size-4 text-ods-error\" />\n                    </button>\n                  )}\n                </div>\n              );\n            })}\n\n            {showCreate &&\n              (isCreateTooLong ? (\n                <div className=\"flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-ods-error text-h5\">\n                  Maximum {maxCreateLength} characters\n                </div>\n              ) : (\n                <button\n                  type=\"button\"\n                  onClick={handleCreate}\n                  disabled={isCreating}\n                  className={cn(\n                    'flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',\n                    'transition-colors hover:bg-ods-bg-hover disabled:pointer-events-none disabled:opacity-50',\n                  )}\n                >\n                  <PlusIcon className=\"size-4 shrink-0 text-ods-accent\" />\n                  <span className=\"flex-1 truncate text-ods-accent text-h4\" title={`Create \"${query}\"`}>\n                    Create &ldquo;{query}&rdquo;\n                  </span>\n                </button>\n              ))}\n\n            {filtered.length === 0 && !showCreate && (\n              <div className=\"px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-ods-text-secondary text-h5\">\n                No tags found\n              </div>\n            )}\n          </div>\n        </PopoverPrimitive.Content>\n      </PopoverPrimitive.Portal>\n    </PopoverPrimitive.Root>\n  );\n}\n","'use client';\n\nimport { X, Plus } from 'lucide-react';\nimport { type KeyboardEvent, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Badge } from './badge';\nimport { Button } from './button';\nimport { Input } from './input';\nimport { Label } from './label';\n\ninterface TagsInputProps {\n  value: string[];\n  onChange: (tags: string[]) => void;\n  placeholder?: string;\n  className?: string;\n  disabled?: boolean;\n  maxTags?: number;\n  inputClassName?: string;\n  badgeClassName?: string;\n  label?: string;\n}\n\nexport function TagsInput({\n  value = [],\n  onChange,\n  placeholder = 'Add a tag...',\n  className,\n  disabled = false,\n  maxTags,\n  inputClassName,\n  badgeClassName,\n  label,\n}: TagsInputProps) {\n  const [inputValue, setInputValue] = useState('');\n\n  const handleAddTag = () => {\n    const trimmedValue = inputValue.trim();\n\n    if (!trimmedValue) return;\n\n    // Check if tag already exists\n    if (value.includes(trimmedValue)) {\n      setInputValue('');\n      return;\n    }\n\n    // Check max tags limit\n    if (maxTags && value.length >= maxTags) {\n      return;\n    }\n\n    onChange([...value, trimmedValue]);\n    setInputValue('');\n  };\n\n  const handleRemoveTag = (tagToRemove: string) => {\n    onChange(value.filter(tag => tag !== tagToRemove));\n  };\n\n  const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleAddTag();\n    }\n  };\n\n  return (\n    <div className={cn('space-y-2', className)}>\n      {label && <Label>{label}</Label>}\n\n      <div className=\"flex items-center gap-2\">\n        <Input\n          value={inputValue}\n          onChange={e => setInputValue(e.target.value)}\n          onKeyPress={handleKeyPress}\n          placeholder={placeholder}\n          disabled={disabled || (maxTags ? value.length >= maxTags : false)}\n          className={cn('flex-1 border-ods-border bg-ods-bg text-ods-text-primary', inputClassName)}\n        />\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          onClick={handleAddTag}\n          disabled={disabled || !inputValue.trim() || (maxTags ? value.length >= maxTags : false)}\n          leftIcon={<Plus className=\"h-4 w-4\" />}\n        >\n          Add\n        </Button>\n      </div>\n\n      {value.length > 0 && (\n        <div className=\"flex flex-wrap gap-2\">\n          {value.map(tag => (\n            <Badge key={tag} className={cn('flex items-center gap-1 py-1 pl-2 pr-1', badgeClassName)}>\n              {tag}\n              {!disabled && (\n                <button\n                  type=\"button\"\n                  onClick={() => handleRemoveTag(tag)}\n                  className=\"rounded p-0.5 transition-colors hover:bg-white/20\"\n                  aria-label={`Remove ${tag} tag`}\n                >\n                  <X className=\"h-3 w-3\" />\n                </button>\n              )}\n            </Badge>\n          ))}\n        </div>\n      )}\n\n      {maxTags && (\n        <p className=\"text-ods-text-secondary text-h6\">\n          {value.length}/{maxTags} tags\n        </p>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport type { HTMLAttributes } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst badgeVariants = cva(\n  // Weight from the ODS token. Scale stays at 12px rather than the `text-h6`\n  // composite: a badge is a stamp, and that composite is 14/20 on desktop —\n  // moving badges onto it grew every one of them.\n  'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-[number:var(--font-weight-semibold)] transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',\n  {\n    variants: {\n      variant: {\n        default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',\n        secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',\n        destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',\n        outline: 'text-foreground',\n        success: 'border-transparent bg-ods-success text-ods-text-on-accent hover:bg-ods-success-hover',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n);\n\nexport interface BadgeProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return <div className={cn(badgeVariants({ variant }), className)} {...props} />;\n}\n\nexport { Badge, badgeVariants };\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';\nimport { type MouseEvent, useCallback, useRef, useState } from 'react';\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';\nimport { cn } from '../../utils/cn';\nimport {\n  CheckIcon,\n  PencilIcon,\n  PlusIcon,\n  SearchIcon,\n  TrashIcon,\n  XmarkIcon,\n  XmarkCircleIcon,\n} from '../icons-v2-generated';\nimport { Button } from './button';\nimport { FieldWrapper } from './field-wrapper';\nimport { Input } from './input';\nimport { Tag } from './tag';\n\nexport interface TagItem {\n  id: string;\n  name: string;\n  color?: string;\n}\n\nexport interface TagsManagerProps {\n  /** All available tags */\n  tags: TagItem[];\n  /** IDs of currently selected tags */\n  selectedIds: string[];\n  /** Called when selection changes */\n  onChange: (ids: string[]) => void;\n  /** Called to create a new tag; should return the created tag */\n  onCreateTag?: (name: string) => Promise<TagItem | null | undefined>;\n  /** Called to update a tag name */\n  onUpdateTag?: (id: string, name: string) => Promise<void>;\n  /** Called to delete a tag */\n  onDeleteTag?: (id: string) => Promise<void>;\n  /** Whether create is in progress */\n  isCreating?: boolean;\n  /** Whether update is in progress */\n  isUpdating?: boolean;\n  /** Whether delete is in progress */\n  isDeleting?: boolean;\n  /** Label displayed above the input */\n  label?: string;\n  /** Placeholder for the search input */\n  searchPlaceholder?: string;\n  /** Whether the component is disabled */\n  disabled?: boolean;\n  /** Additional className for the root container */\n  className?: string;\n}\n\nexport function TagsManager({\n  tags,\n  selectedIds,\n  onChange,\n  onCreateTag,\n  onUpdateTag,\n  onDeleteTag,\n  isCreating = false,\n  isUpdating = false,\n  isDeleting = false,\n  label = 'Tags',\n  searchPlaceholder = 'Search and add Tags',\n  disabled = false,\n  className,\n}: TagsManagerProps) {\n  const [open, setOpen] = useState(false);\n  const [search, setSearch] = useState('');\n  const keyboardPadding = useKeyboardCollisionPadding();\n  const [editingId, setEditingId] = useState<string | null>(null);\n  const [editingName, setEditingName] = useState('');\n  const editInputRef = useRef<HTMLInputElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const selectedTags = tags.filter(t => selectedIds.includes(t.id));\n  const filtered = tags.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));\n  const showCreateOption =\n    onCreateTag && search.trim() && !tags.some(t => t.name.toLowerCase() === search.trim().toLowerCase());\n\n  const toggleTag = useCallback(\n    (id: string) => {\n      if (selectedIds.includes(id)) {\n        onChange(selectedIds.filter(i => i !== id));\n      } else {\n        onChange([...selectedIds, id]);\n      }\n    },\n    [selectedIds, onChange],\n  );\n\n  const handleCreate = useCallback(async () => {\n    if (!onCreateTag) return;\n    const name = search.trim();\n    if (!name) return;\n    try {\n      const result = await onCreateTag(name);\n      if (result?.id) {\n        onChange([...selectedIds, result.id]);\n        setSearch('');\n      }\n    } catch (error) {\n      // Leave the typed name in the input so the user can retry; the caller\n      // owns user-facing error reporting via its own mutation state.\n      console.error('Failed to create tag:', error);\n    }\n  }, [search, onCreateTag, selectedIds, onChange]);\n\n  const startEdit = useCallback((id: string, name: string) => {\n    setEditingId(id);\n    setEditingName(name);\n    setTimeout(() => editInputRef.current?.focus(), 0);\n  }, []);\n\n  const confirmEdit = useCallback(async () => {\n    if (!onUpdateTag || !editingId || !editingName.trim()) return;\n    try {\n      await onUpdateTag(editingId, editingName.trim());\n      // Only leave edit mode once the rename actually landed.\n      setEditingId(null);\n      setEditingName('');\n    } catch (error) {\n      console.error('Failed to rename tag:', error);\n    }\n  }, [editingId, editingName, onUpdateTag]);\n\n  const cancelEdit = useCallback(() => {\n    setEditingId(null);\n    setEditingName('');\n  }, []);\n\n  const handleDelete = useCallback(\n    async (id: string) => {\n      if (!onDeleteTag) return;\n      try {\n        await onDeleteTag(id);\n        if (selectedIds.includes(id)) {\n          onChange(selectedIds.filter(i => i !== id));\n        }\n      } catch (error) {\n        // Keep the tag selected if the delete failed — dropping it from the\n        // selection would silently desync the form from the server.\n        console.error('Failed to delete tag:', error);\n      }\n    },\n    [onDeleteTag, selectedIds, onChange],\n  );\n\n  const handleClearAll = useCallback(\n    (e: MouseEvent) => {\n      e.stopPropagation();\n      onChange([]);\n      setSearch('');\n      inputRef.current?.focus();\n    },\n    [onChange],\n  );\n\n  return (\n    <FieldWrapper label={label} className={className}>\n      <div className=\"relative\" ref={containerRef}>\n        <PopoverPrimitive.Root open={open} onOpenChange={setOpen}>\n          {/* Anchor container */}\n          <PopoverPrimitive.Anchor asChild>\n            <label\n              className={cn(\n                'flex min-h-11 w-full cursor-text flex-wrap items-center gap-2 rounded-[6px] border px-3 py-1 md:min-h-12',\n                'transition-colors duration-200',\n                'border-ods-border bg-ods-card',\n                'group',\n                !disabled &&\n                  'hover:border-ods-border-hover hover:bg-ods-bg-hover active:border-ods-border-active active:bg-ods-bg-active',\n                disabled && '!cursor-not-allowed bg-ods-bg',\n                open && 'border-ods-accent hover:border-ods-accent',\n              )}\n              onClickCapture={() => {\n                if (!disabled) setOpen(true);\n              }}\n            >\n              {/* Search adornment */}\n              <span\n                className={cn(\n                  'flex-shrink-0 text-ods-text-secondary transition-colors duration-200 [&_svg]:size-4 md:[&_svg]:size-6',\n                  open && 'text-ods-accent',\n                )}\n              >\n                <SearchIcon />\n              </span>\n\n              {/* Selected tags rendered as Tag components */}\n              {selectedTags.map(tag => (\n                <Tag\n                  key={tag.id}\n                  label={tag.name}\n                  variant=\"outline\"\n                  onClose={disabled ? undefined : () => toggleTag(tag.id)}\n                />\n              ))}\n\n              {/* Inline search input */}\n              <input\n                ref={inputRef}\n                type=\"text\"\n                value={search}\n                onChange={e => {\n                  setSearch(e.target.value);\n                  if (!open) setOpen(true);\n                }}\n                onFocus={() => setOpen(true)}\n                onKeyDown={e => {\n                  if (e.key === 'Escape') setOpen(false);\n                  if (e.key === 'Enter' && showCreateOption) {\n                    e.preventDefault();\n                    void handleCreate();\n                  }\n                  if (e.key === 'Backspace' && !search && selectedIds.length > 0) {\n                    onChange(selectedIds.slice(0, -1));\n                  }\n                }}\n                placeholder={selectedTags.length === 0 ? searchPlaceholder : 'Add More...'}\n                disabled={disabled}\n                className={cn(\n                  'min-w-0 flex-1 border-none bg-transparent outline-none',\n                  'text-h4',\n                  'text-ods-text-primary placeholder:text-ods-text-secondary',\n                  'disabled:cursor-not-allowed',\n                )}\n              />\n\n              {/* Clear all button — shown when tags are selected */}\n              {selectedTags.length > 0 && !disabled && (\n                <button\n                  type=\"button\"\n                  onClick={handleClearAll}\n                  className=\"flex flex-shrink-0 items-center justify-center transition-opacity hover:opacity-70 [&_svg]:size-4 md:[&_svg]:size-6\"\n                  aria-label=\"Clear all tags\"\n                >\n                  <XmarkCircleIcon className=\"text-ods-text-secondary\" />\n                </button>\n              )}\n            </label>\n          </PopoverPrimitive.Anchor>\n\n          {/* Dropdown */}\n          <PopoverPrimitive.Content\n            className={cn(\n              'z-50 mt-1 w-[var(--radix-popover-trigger-width)]',\n              'rounded border border-ods-border bg-ods-card',\n              'flex max-h-[var(--radix-popper-available-height)] flex-col overflow-hidden',\n              'data-[state=open]:animate-in data-[state=closed]:animate-out',\n              'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n              'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n              'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',\n            )}\n            sideOffset={4}\n            align=\"start\"\n            // The anchor is the tag field that raises the software keyboard, so\n            // this list is always positioned with the keyboard up — see\n            // useKeyboardCollisionPadding.\n            collisionPadding={{ bottom: keyboardPadding }}\n            onOpenAutoFocus={e => {\n              e.preventDefault();\n              inputRef.current?.focus();\n            }}\n            onInteractOutside={e => {\n              if (containerRef.current?.contains(e.target as Node)) {\n                e.preventDefault();\n              }\n            }}\n          >\n            <ScrollAreaPrimitive.Root className=\"flex min-h-0 flex-col overflow-hidden\">\n              <ScrollAreaPrimitive.Viewport className=\"max-h-60 min-h-0 w-full\">\n                <div role=\"listbox\">\n                  {filtered.map(tag => {\n                    const isSelected = selectedIds.includes(tag.id);\n                    const isEditing = editingId === tag.id;\n\n                    if (isEditing) {\n                      return (\n                        <div\n                          key={tag.id}\n                          className=\"flex items-center gap-1 border-b border-ods-border px-2 py-1 last:border-b-0\"\n                        >\n                          <Input\n                            ref={editInputRef}\n                            value={editingName}\n                            onChange={e => setEditingName(e.target.value)}\n                            onKeyDown={e => {\n                              if (e.key === 'Enter') void confirmEdit();\n                              if (e.key === 'Escape') cancelEdit();\n                            }}\n                            className=\"flex-1\"\n                          />\n                          <Button\n                            type=\"button\"\n                            variant=\"transparent\"\n                            size=\"icon\"\n                            onClick={confirmEdit}\n                            disabled={isUpdating}\n                          >\n                            <CheckIcon size={14} className=\"text-ods-success\" />\n                          </Button>\n                          <Button type=\"button\" variant=\"transparent\" size=\"icon\" onClick={cancelEdit}>\n                            <XmarkIcon size={14} className=\"text-ods-text-secondary\" />\n                          </Button>\n                        </div>\n                      );\n                    }\n\n                    return (\n                      <div\n                        key={tag.id}\n                        role=\"option\"\n                        aria-selected={isSelected}\n                        tabIndex={0}\n                        className={cn(\n                          'flex h-11 cursor-pointer items-center border-b border-ods-border px-4 transition-colors last:border-b-0 md:h-12',\n                          'text-h4',\n                          isSelected ? 'text-ods-accent' : 'text-ods-text-primary',\n                          'group/item hover:bg-ods-bg-hover',\n                        )}\n                        onClick={() => toggleTag(tag.id)}\n                        onKeyDown={e => {\n                          if (e.key === 'Enter' || e.key === ' ') {\n                            e.preventDefault();\n                            toggleTag(tag.id);\n                          }\n                        }}\n                      >\n                        <div className=\"flex w-full items-center justify-between\">\n                          <span className=\"truncate\" title={tag.name}>\n                            {tag.name}\n                          </span>\n                          <div className=\"flex shrink-0 items-center gap-1\">\n                            {isSelected && <CheckIcon className=\"text-ods-accent\" size={20} />}\n                            {(onUpdateTag || onDeleteTag) && (\n                              <div className=\"flex items-center gap-0.5 opacity-0 transition-opacity group-hover/item:opacity-100\">\n                                {onUpdateTag && (\n                                  <Button\n                                    type=\"button\"\n                                    variant=\"transparent\"\n                                    size=\"icon\"\n                                    onClick={e => {\n                                      e.stopPropagation();\n                                      startEdit(tag.id, tag.name);\n                                    }}\n                                  >\n                                    <PencilIcon size={14} className=\"text-ods-text-secondary\" />\n                                  </Button>\n                                )}\n                                {onDeleteTag && (\n                                  <Button\n                                    type=\"button\"\n                                    variant=\"transparent\"\n                                    size=\"icon\"\n                                    onClick={e => {\n                                      e.stopPropagation();\n                                      void handleDelete(tag.id);\n                                    }}\n                                    disabled={isDeleting}\n                                  >\n                                    <TrashIcon size={14} className=\"text-ods-error\" />\n                                  </Button>\n                                )}\n                              </div>\n                            )}\n                          </div>\n                        </div>\n                      </div>\n                    );\n                  })}\n\n                  {showCreateOption && (\n                    <div\n                      role=\"option\"\n                      // A create-new action, never a selectable value — but the\n                      // role still requires the state, and `false` is the truth.\n                      aria-selected={false}\n                      tabIndex={0}\n                      className={cn(\n                        'flex h-11 cursor-pointer items-center gap-2 px-4 transition-colors text-h4 md:h-12',\n                        'hover:bg-ods-bg-hover',\n                        isCreating && 'pointer-events-none opacity-50',\n                      )}\n                      onClick={handleCreate}\n                      onKeyDown={e => {\n                        if (e.key === 'Enter' || e.key === ' ') {\n                          e.preventDefault();\n                          void handleCreate();\n                        }\n                      }}\n                    >\n                      <PlusIcon size={16} className=\"shrink-0 text-ods-accent\" />\n                      <span className=\"truncate text-ods-accent\" title={`Create \"${search.trim()}\"`}>\n                        Create &ldquo;{search.trim()}&rdquo;\n                      </span>\n                    </div>\n                  )}\n\n                  {filtered.length === 0 && !showCreateOption && (\n                    <div className=\"px-4 py-2 text-ods-text-secondary text-h6\">No tags found</div>\n                  )}\n                </div>\n              </ScrollAreaPrimitive.Viewport>\n              <ScrollAreaPrimitive.Scrollbar className=\"hidden\" orientation=\"vertical\">\n                <ScrollAreaPrimitive.Thumb />\n              </ScrollAreaPrimitive.Scrollbar>\n            </ScrollAreaPrimitive.Root>\n          </PopoverPrimitive.Content>\n        </PopoverPrimitive.Root>\n      </div>\n    </FieldWrapper>\n  );\n}\n","'use client';\n\nimport { type ButtonHTMLAttributes, type ReactNode, type TextareaHTMLAttributes, forwardRef, useId } from 'react';\n\nimport { cn } from '../../utils/cn';\nimport { FieldWrapper } from './field-wrapper';\n\nexport interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n  /** When true, renders red error border & ring */\n  invalid?: boolean;\n  /** Label text displayed above the textarea */\n  label?: string;\n  /** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles) */\n  labelVariant?: 'default' | 'large';\n  /** Error message displayed below the textarea */\n  error?: string;\n  /** Element rendered at the right edge of the field (e.g. send icon). */\n  endIcon?: ReactNode;\n  /**\n   * When true, `endIcon` is wrapped in a real `<button>` with hover / active\n   * / focus-visible styling matching the rest of the design system. Pass any\n   * extra button attributes (`onClick`, `disabled`, `aria-label`, …) via\n   * `endIconButtonProps`.\n   */\n  endIconAsButton?: boolean;\n  /** Extra attributes for the end-icon button. */\n  endIconButtonProps?: Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'>;\n  /** Element rendered at the LEFT edge of the field (e.g. a `+` add button).\n   *  Like `endIcon`, presence of this lifts the field into the wrapped layout\n   *  so the adornment sits visually inside the same border. Pass an\n   *  already-interactive node (button / menu trigger) — it renders verbatim. */\n  startIcon?: ReactNode;\n  /** When true (adorned layout only), the field draws NO border / background /\n   *  radius of its own — only its inner padding & layout. Use when an outer\n   *  container provides the card chrome (e.g. composer with a context-chip\n   *  header above the input, Figma 1:6073). */\n  hideBorder?: boolean;\n}\n\nconst Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(\n  (\n    {\n      className,\n      invalid = false,\n      label,\n      labelVariant,\n      error,\n      endIcon,\n      endIconAsButton = false,\n      endIconButtonProps,\n      startIcon,\n      hideBorder = false,\n      ...props\n    },\n    ref,\n  ) => {\n    const isInvalid = invalid || !!error;\n    const hasAdornment = !!endIcon || !!startIcon;\n\n    // The adorned layout wraps the field in a `<label>`. Without an explicit\n    // `htmlFor`, the label's control defaults to its FIRST labelable descendant\n    // — which is `startIcon` (e.g. the composer `+` button) when present, so a\n    // click anywhere on the field synthesises a click on that button. Bind the\n    // label to the textarea explicitly to keep clicks (and focus) on the input.\n    const reactId = useId();\n    const fieldId = props.id ?? reactId;\n\n    // Without any adornment we keep the historical layout: bare textarea, the\n    // border/bg/hover all live on the textarea itself.\n    if (!hasAdornment) {\n      return (\n        <FieldWrapper label={label} labelVariant={labelVariant} error={error}>\n          <textarea\n            className={cn(\n              'flex min-h-[96px] w-full rounded-[6px] border p-3',\n              'text-h4',\n              'focus:border-ods-accent focus-visible:outline-none',\n              'touch-manipulation transition-colors duration-200',\n              'border-ods-border bg-ods-card text-ods-text-primary placeholder:text-ods-text-secondary',\n              !props.disabled &&\n                'hover:border-ods-border-hover hover:bg-ods-bg-hover active:border-ods-border-active active:bg-ods-bg-active',\n              props.disabled && '!cursor-not-allowed bg-ods-bg disabled:placeholder:text-ods-border',\n              'relative z-10 cursor-text',\n              'resize-y',\n              isInvalid && 'border-ods-error hover:border-ods-error focus:border-ods-error',\n              className,\n            )}\n            ref={ref}\n            {...props}\n          />\n        </FieldWrapper>\n      );\n    }\n\n    // With an end icon we lift border/bg onto a `<label>` wrapper so the icon\n    // sits visually inside the same field. The textarea becomes a transparent\n    // child. Wrapper classes mirror `Input` 1:1 (same items-center, same\n    // `has-[:focus]` selectors, same active state, same outline reset) so\n    // hover / focus / cursor behaviour matches the standard input exactly.\n    //\n    // Focus selector: historically `has-[:focus]` (any focusable descendant —\n    // textarea OR the end-icon submit button — lights the accent). We narrow it\n    // to `textarea:focus` ONLY when a `startIcon` is present, because that's the\n    // composer-context layout where the start adornment hosts the context\n    // picker's own `<input>` (search field) — which must NOT light the field\n    // chrome. End-icon-only consumers (ticket reply, contact form, …) keep the\n    // original any-focus behaviour so the submit-button focus ring is unchanged.\n    const accentBorder = startIcon ? 'has-[textarea:focus]:border-ods-accent' : 'has-[:focus]:border-ods-accent';\n    const accentBorderError = startIcon ? 'has-[textarea:focus]:border-ods-error' : 'has-[:focus]:border-ods-error';\n    const adornmentAccent = startIcon\n      ? 'group-has-[textarea:focus]:text-ods-accent'\n      : 'group-has-[:focus]:text-ods-accent';\n    const content = (\n      <label\n        htmlFor={fieldId}\n        data-invalid={isInvalid || undefined}\n        className={cn(\n          // Wrapper mirrors `Input`, but uses `items-end` so the icon sticks\n          // to the bottom-right when the textarea grows multi-line. Vertical\n          // padding lives here (not on the textarea) so both children share\n          // the same bottom baseline at any height.\n          //\n          // Padding is sized so the single-line height matches `Input` exactly\n          // (44px mobile / 48px desktop): line `leading-6` (24px) + 2× padding\n          // + 1px border top/bottom. `min-h-11 md:min-h-12` is the matching\n          // floor; with symmetric padding the single line stays centered.\n          'flex min-h-11 w-full cursor-text items-end gap-2 px-3 py-[9px] md:min-h-12 md:py-[11px]',\n          'has-[:focus-visible]:outline-none',\n          'group',\n          'transition-colors duration-200',\n          // Card chrome (border / bg / radius / hover / focus). Suppressed when\n          // `hideBorder` — an outer container owns it instead.\n          !hideBorder && cn('rounded-[6px] border border-ods-border bg-ods-card', accentBorder),\n          !hideBorder &&\n            !props.disabled &&\n            'hover:border-ods-border-hover hover:bg-ods-bg-hover active:border-ods-border-active active:bg-ods-bg-active',\n          !hideBorder && props.disabled && '!cursor-not-allowed bg-ods-bg',\n          // Start / end icons carry their own colour — grey them with the value\n          // so a disabled field reads as one flat colour. Scoped to the DIRECT\n          // span children (the two icon wrappers): the nodes inside them are\n          // rendered verbatim so callers can pass an interactive trigger and own\n          // its styling, and a descendant rule would overwrite exactly that.\n          'has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled',\n          !hideBorder && isInvalid && cn('border-ods-error hover:border-ods-error', accentBorderError),\n          hideBorder && 'bg-transparent',\n        )}\n      >\n        {startIcon && (\n          // Start adornment (e.g. the composer `+`). `items-end` + `h-6`\n          // co-centers it with the first text line, matching `endIcon`. The\n          // node is rendered verbatim so callers can pass an interactive\n          // button / menu trigger and own its styling + active color.\n          <span className=\"flex h-6 shrink-0 items-center\">{startIcon}</span>\n        )}\n        <textarea\n          ref={ref}\n          rows={1}\n          id={fieldId}\n          className={cn(\n            'm-0 box-border min-w-0 flex-1 resize-none border-none bg-transparent p-0 outline-none',\n            // Native CSS auto-grow (Chrome 123+, FF 124+, Safari 16.4+) — no\n            // JS needed to size to content. Falls back to `rows={1}` natural\n            // height on older browsers.\n            '[field-sizing:content]',\n            // `!leading-6` (not plain `leading-6`): on mobile `text-h4` sets a\n            // 20px line-height of its own that otherwise overrides the leading,\n            // leaving the 20px text box short of the 24px icon box and dropping\n            // the text below center under `items-end`. Forcing 24px keeps text\n            // and icon co-centered and the single-line height at 44px.\n            '!leading-6 text-ods-text-primary text-h4 placeholder:text-ods-text-secondary',\n            // Hard cap on growth: beyond `max-h` the textarea scrolls\n            // internally instead of pushing the icon out of frame.\n            'max-h-[160px] overflow-y-auto',\n            'scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30',\n            // Disabled - match Input exactly (value greys out, placeholder dims further)\n            'disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border',\n            className,\n          )}\n          {...props}\n        />\n\n        {endIconAsButton ? (\n          <button\n            type=\"button\"\n            aria-label={endIconButtonProps?.['aria-label'] ?? 'Submit'}\n            {...endIconButtonProps}\n            className={cn(\n              // `h-6` matches the textarea's `leading-6` line box so the icon's\n              // visual center lines up with the text on the first line. Without\n              // it, `items-end` bottom-aligns the smaller mobile icon (size-4)\n              // against the 24px text line and drops it below the text center.\n              'flex h-6 shrink-0 items-center text-ods-text-secondary transition-colors duration-200',\n              adornmentAccent,\n              'group-data-[invalid]:text-ods-error',\n              '[&_svg]:size-4 md:[&_svg]:size-6',\n              'cursor-pointer hover:text-ods-text-primary',\n              'focus-visible:text-ods-accent focus-visible:outline-none',\n              'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ods-text-secondary',\n              endIconButtonProps?.className,\n            )}\n          >\n            {endIcon}\n          </button>\n        ) : (\n          <span\n            className={cn(\n              'flex h-6 shrink-0 items-center text-ods-text-secondary transition-colors duration-200',\n              adornmentAccent,\n              'group-data-[invalid]:text-ods-error',\n              '[&_svg]:size-4 md:[&_svg]:size-6',\n            )}\n          >\n            {endIcon}\n          </span>\n        )}\n      </label>\n    );\n\n    return (\n      <FieldWrapper label={label} labelVariant={labelVariant} error={error}>\n        {content}\n      </FieldWrapper>\n    );\n  },\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n","'use client';\n\nimport * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';\nimport { type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\nimport { buttonVariants } from './button';\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = forwardRef<\n  ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      'fixed inset-0 z-50 bg-ods-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = forwardRef<\n  ElementRef<typeof AlertDialogPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        // Centered on the KEYBOARD-FREE area, not the layout viewport: nothing\n        // resizes the viewport when a mobile software keyboard opens, so a\n        // plain top-[50%] leaves the lower half of the dialog behind it. The\n        // max-height keeps that upward shift from pushing the top off-screen.\n        // See modal-v2.tsx for the full reasoning; --of-keyboard-inset is\n        // published by the app (keyboard-inset.ts) and 0 everywhere else.\n        // Content scrolls directly here, where DialogContent has to hand that\n        // job to a wrapper: this Content has no absolutely-positioned close\n        // button for a scroll container to carry off the top.\n        'fixed left-[50%] top-[calc(50%_-_var(--of-keyboard-inset,0px)/2)] z-50 grid max-h-[calc(100dvh_-_var(--of-keyboard-inset,0px)_-_2rem)] w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-[var(--spacing-system-mf)] overflow-y-auto border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-1/2 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-1/2 md:rounded-lg',\n        className,\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col space-y-2 text-center md:text-left', className)} {...props} />\n);\nAlertDialogHeader.displayName = 'AlertDialogHeader';\n\nconst AlertDialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col-reverse md:flex-row md:justify-end md:space-x-2', className)} {...props} />\n);\nAlertDialogFooter.displayName = 'AlertDialogFooter';\n\nconst AlertDialogTitle = forwardRef<\n  ElementRef<typeof AlertDialogPrimitive.Title>,\n  ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title ref={ref} className={cn('text-h3', className)} {...props} />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = forwardRef<\n  ElementRef<typeof AlertDialogPrimitive.Description>,\n  ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description ref={ref} className={cn('text-muted-foreground text-h6', className)} {...props} />\n));\nAlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = forwardRef<\n  ElementRef<typeof AlertDialogPrimitive.Action>,\n  ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = forwardRef<\n  ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 md:mt-0', className)}\n    {...props}\n  />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};\n","'use client';\n\nimport * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';\n\nconst AspectRatio = AspectRatioPrimitive.Root;\n\nexport { AspectRatio };\n","'use client';\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { X } from 'lucide-react';\nimport { type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = forwardRef<\n  ElementRef<typeof DialogPrimitive.Overlay>,\n  ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      'fixed inset-0 z-[9998] bg-ods-overlay backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = forwardRef<\n  ElementRef<typeof DialogPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        // Centered on the KEYBOARD-FREE area, not the layout viewport: nothing\n        // resizes the viewport when a mobile software keyboard opens, so a\n        // plain top-[50%] leaves the lower half of the dialog behind it. See\n        // modal-v2.tsx for the full reasoning; --of-keyboard-inset is published\n        // by the app (keyboard-inset.ts) and 0 everywhere else.\n        //\n        // The cap is on THIS box but the scrolling is not: the close button\n        // below is `absolute` against it, so a scroll container here would\n        // carry the X off the top. The wrapper around `children` scrolls\n        // instead, and carries the section gap that used to sit here so\n        // header/body/footer keep their spacing.\n        //\n        // `grid-rows-[minmax(0,1fr)]` is what makes the cap bite. An implicit\n        // row track is `auto`-sized, so it grows to its content and overflows\n        // the max-height no matter what min-height the wrapper carries —\n        // measured at 1136px of rows inside an 843px box. Constraining the\n        // track is what hands the wrapper a scrollbar; `1fr` still collapses to\n        // max-content when the dialog is shorter than the cap, so short dialogs\n        // keep hugging their content.\n        'fixed left-[50%] top-[calc(50%_-_var(--of-keyboard-inset,0px)/2)] z-[9999] grid max-h-[calc(100dvh_-_var(--of-keyboard-inset,0px)_-_2rem)] w-full max-w-lg translate-x-[-50%] translate-y-[-50%] grid-rows-[minmax(0,1fr)] border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-1/2 data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-1/2 md:rounded-lg',\n        className,\n      )}\n      {...props}\n    >\n      {/* min-h-0 is load-bearing: a grid item defaults to min-height:auto and\n          would refuse to shrink below its content, so the cap above would push\n          past the viewport instead of handing this box a scrollbar. */}\n      <div className=\"grid min-h-0 gap-[var(--spacing-system-mf)] overflow-y-auto\">{children}</div>\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col space-y-1.5 text-center md:text-left', className)} {...props} />\n);\nDialogHeader.displayName = 'DialogHeader';\n\nconst DialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col-reverse md:flex-row md:justify-end md:space-x-2', className)} {...props} />\n);\nDialogFooter.displayName = 'DialogFooter';\n\nconst DialogTitle = forwardRef<\n  ElementRef<typeof DialogPrimitive.Title>,\n  ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title ref={ref} className={cn('text-h3', className)} {...props} />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = forwardRef<\n  ElementRef<typeof DialogPrimitive.Description>,\n  ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description ref={ref} className={cn('text-muted-foreground text-h6', className)} {...props} />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n","'use client';\n\nimport { useState, useRef, useCallback, useEffect } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Button } from './button';\nimport { ModalV2, ModalV2Content, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2';\n\n/**\n * The unified dirty-state contract for modals: a promise-based house confirm\n * (`useConfirm`), a close guard that asks before discarding unsaved edits\n * (`useGuardedClose`), and the standing \"Unsaved changes\" footer chip\n * (`UnsavedChangesChip`). Form-modal shells compose these three instead of\n * reimplementing the affordance — closing a dirty form must never silently\n * discard the edits, and the chip is the standing signal that edits exist.\n */\n\n/**\n * State-driven confirm dialog using `ModalV2` — the house replacement for\n * `window.confirm`. `ask(title, body)` returns a `Promise<boolean>`; the\n * consumer awaits the user's choice and then proceeds. The returned `dialog`\n * element MUST be rendered in the host's JSX (inside the host modal is fine —\n * `ModalV2` stacks).\n *\n * Concurrency: a second `ask` while a dialog is already open resolves `false`\n * immediately — without the guard the second `setPending` would overwrite the\n * first entry and the first Promise would hang forever. The ref-based\n * in-flight flag also covers two synchronous `ask()` calls in the same render\n * tick, which `useState` alone races.\n */\nexport function useConfirm() {\n  const [open, setOpen] = useState(false);\n  const [pending, setPending] = useState<{\n    title: string;\n    body: string;\n    resolve: (ok: boolean) => void;\n  } | null>(null);\n  const askingRef = useRef(false);\n  // Mirror of the active request, so unmount cleanup can settle it without\n  // depending on (already-frozen) state.\n  const pendingRef = useRef<{ resolve: (ok: boolean) => void } | null>(null);\n  const ask = useCallback((title: string, body: string) => {\n    return new Promise<boolean>(resolve => {\n      if (askingRef.current) {\n        resolve(false);\n        return;\n      }\n      askingRef.current = true;\n      const entry = { title, body, resolve };\n      pendingRef.current = entry;\n      setPending(entry);\n      setOpen(true);\n    });\n  }, []);\n  const decide = useCallback(\n    (ok: boolean) => {\n      askingRef.current = false;\n      // Clear the ref FIRST so a later unmount cleanup can't re-resolve a\n      // request the user already decided.\n      pendingRef.current = null;\n      setOpen(false);\n      pending?.resolve(ok);\n      setPending(null);\n    },\n    [pending],\n  );\n  // Host unmounted while the confirmation was open: the decision UI is gone,\n  // so settle the promise as \"no\" — otherwise the caller awaits forever.\n  useEffect(\n    () => () => {\n      pendingRef.current?.resolve(false);\n      pendingRef.current = null;\n      askingRef.current = false;\n    },\n    [],\n  );\n\n  const dialog = pending ? (\n    <ModalV2 isOpen={open} onClose={() => decide(false)}>\n      <ModalV2Header>\n        <ModalV2Title>{pending.title}</ModalV2Title>\n      </ModalV2Header>\n      <ModalV2Content>\n        <p className=\"text-ods-text-secondary text-h6\">{pending.body}</p>\n      </ModalV2Content>\n      <ModalV2Footer>\n        <Button variant=\"outline\" onClick={() => decide(false)}>\n          Cancel\n        </Button>\n        <Button variant=\"destructive\" onClick={() => decide(true)}>\n          Confirm\n        </Button>\n      </ModalV2Footer>\n    </ModalV2>\n  ) : null;\n\n  return { ask, dialog };\n}\n\nexport interface UseGuardedCloseOptions {\n  /** Confirm dialog title. */\n  title?: string;\n  /** Confirm dialog body. */\n  body?: string;\n}\n\n/**\n * Close guard for dirty forms: `guardedClose` runs `onClose` directly while\n * clean, and asks the house confirm first once `dirty` — wire it to the\n * modal's `onClose` AND the Cancel button so every dismissal path is guarded.\n * Render the returned `dialog` in the host's JSX.\n */\nexport function useGuardedClose(\n  dirty: boolean,\n  onClose: () => void,\n  {\n    title = 'Discard unsaved changes?',\n    body = 'Your edits have not been saved. Close and discard them?',\n  }: UseGuardedCloseOptions = {},\n) {\n  const { ask, dialog } = useConfirm();\n  const guardedClose = useCallback(async () => {\n    if (dirty && !(await ask(title, body))) return;\n    onClose();\n  }, [dirty, ask, title, body, onClose]);\n  return { guardedClose, dialog };\n}\n\nexport interface UnsavedChangesChipProps {\n  /** Names WHAT is dirty (hover tooltip) — turns \"why is this dirty?!\" into a hover. */\n  detail?: string;\n  className?: string;\n}\n\n/** The standing \"Unsaved changes\" signal for a dirty modal's footer. */\nexport function UnsavedChangesChip({ detail, className }: UnsavedChangesChipProps) {\n  return (\n    <span className={cn('text-ods-warning text-h6', className)} role=\"status\" title={detail}>\n      Unsaved changes\n    </span>\n  );\n}\n","'use client';\n\nimport { useId, type ReactNode } from 'react';\nimport { HUBSPOT_DO_NOT_COLLECT_FORM_PROPS } from '../../utils/hubspot-collected-forms';\nimport { Button } from './button';\nimport { UnsavedChangesChip, useGuardedClose } from './modal-guarded-close';\nimport {\n  ModalV2,\n  ModalV2Content,\n  ModalV2Footer,\n  ModalV2Header,\n  ModalV2Title,\n  ModalV2TwoColumn,\n  type ModalV2Size,\n} from './modal-v2';\nimport { Skeleton } from './skeleton';\n\n/**\n * Label widths for the loading placeholder. Uneven on purpose — a column of\n * identical bars reads as a rendering artifact rather than a form arriving.\n */\nconst SKELETON_FIELD_WIDTHS = ['w-24', 'w-32', 'w-20', 'w-40', 'w-28', 'w-36'];\n\n/** Layout: pass `children` for one column, or `leftColumn`+`rightColumn` for two. */\ntype AdminFormModalLayout =\n  | { children: ReactNode; leftColumn?: never; rightColumn?: never }\n  | { leftColumn: ReactNode; rightColumn: ReactNode; children?: never };\n\ntype AdminFormModalBaseProps = {\n  title: ReactNode;\n  /** Secondary line under the title (what this record is, why it matters). */\n  subtitle?: ReactNode;\n  isOpen: boolean;\n  onClose: () => void;\n  onSave: () => void;\n  saveLabel: string;\n  canSave?: boolean;\n  saving?: boolean;\n  /**\n   * The entity is still being fetched — the modal was opened from a URL on a\n   * cold load, so there is no list row to borrow from. Content is hidden behind\n   * a placeholder and Save is held until the form has real values to submit.\n   */\n  loading?: boolean;\n  error?: string | null;\n  /**\n   * Placeholder to show while `loading`, for modals whose body is NOT a field\n   * grid. The default bars imply a form; a modal that resolves into a list or a\n   * table would visibly change shape when the data lands.\n   */\n  loadingContent?: ReactNode;\n  /** Secondary actions pinned to the footer's LEFT edge (Delete, Preview…). */\n  footerExtras?: ReactNode;\n  /**\n   * Additional PRIMARY actions, rendered next to Save (Publish, Mark\n   * Complete…). A second primary action put in `footerExtras` lands at the far\n   * left, a whole modal-width away from the button it belongs beside.\n   */\n  footerActions?: ReactNode;\n  /** Status text between the extras and the buttons (e.g. \"3 rules selected\"). */\n  footerStatus?: ReactNode;\n  /** Unsaved edits exist: shows the footer chip and guards close with a confirm. */\n  dirty?: boolean;\n  /** Names WHAT is dirty (chip tooltip) — turns \"why is this dirty?!\" into a hover. */\n  dirtyDetail?: string;\n  /**\n   * Panel width. Two-column layouts are always `wide` — passing\n   * `leftColumn`/`rightColumn` upgrades this automatically.\n   */\n  size?: ModalV2Size;\n  contentClassName?: string;\n};\n\n/**\n * THE admin form-modal shell: header, scrolling content (one column or two), an\n * error line, and a Cancel/Save footer with optional extra actions.\n *\n * Every admin form modal renders this rather than reassembling `ModalV2` +\n * header + footer by hand. Three copies of the same shell had already been\n * written in one feature (repo form, rule form, bindings), which is how the\n * Save button ends up saying \"Saving…\" in one place and spinning silently in\n * another.\n *\n * Width is a `size` variant, never a call-site `max-w-*` — the widths had\n * drifted across seven values (`max-w-md` through `max-w-[1400px]`) for modals\n * doing the same job.\n */\nexport function AdminFormModal({\n  title,\n  subtitle,\n  isOpen,\n  onClose,\n  onSave,\n  saveLabel,\n  canSave = true,\n  saving = false,\n  loading = false,\n  error = null,\n  loadingContent,\n  footerExtras,\n  footerActions,\n  footerStatus,\n  dirty = false,\n  dirtyDetail,\n  size,\n  contentClassName = 'space-y-[var(--spacing-system-lf)]',\n  children,\n  leftColumn,\n  rightColumn,\n}: AdminFormModalBaseProps & AdminFormModalLayout) {\n  const twoColumn = leftColumn !== undefined && rightColumn !== undefined;\n  const effectiveSize: ModalV2Size = twoColumn ? 'wide' : (size ?? 'medium');\n  /** Placeholder column count follows the panel width so the skeleton occupies\n   *  the same shape the real content will. */\n  const skeletonColumns = effectiveSize === 'wide' ? 2 : 1;\n  const formId = useId();\n  // The lib's unified dirty-state contract: closing a dirty settings form must\n  // never silently discard the edits — `useGuardedClose` asks the house\n  // confirm (ModalV2, never window.confirm) before running `onClose`.\n  const { guardedClose, dialog } = useGuardedClose(dirty, onClose, {\n    title: 'Discard unsaved changes?',\n    body: 'Your edits have not been saved. Close and discard them?',\n  });\n  // NO field autofocus here. `ModalV2` owns focus now (focus-in on open, focus\n  // containment while topmost, Tab trap, restore to the opener), so the shell\n  // reaching in to focus \"the first field\" only fought it — and picked widgets\n  // that ACT on focus: the author selector's combobox opened its popover on\n  // every `?edit=<id>` load, and on a cold URL load the programmatic focus had\n  // no preceding pointer event, so the UA painted a focus ring too. Focus lands\n  // on the dialog itself, which is the correct dialog behavior; the first Tab\n  // reaches the first field.\n  if (!isOpen) return null;\n\n  return (\n    <ModalV2 isOpen onClose={guardedClose} size={effectiveSize}>\n      <ModalV2Header>\n        <ModalV2Title>{title}</ModalV2Title>\n        {/* A <div>, not a <p>. `subtitle` is a ReactNode and callers legitimately\n            pass badges and chips — `Badge` renders a <div>, which is invalid\n            inside <p>. The browser then re-parents it during hydration and React\n            throws a mismatch (seen on role-baselines, whose subtitle carries a\n            weight Badge). Typography is unchanged. */}\n        {subtitle && <div className=\"text-ods-text-secondary text-h6\">{subtitle}</div>}\n      </ModalV2Header>\n\n      {/* A real <form>: Enter in a text input previously did nothing in every\n          admin modal routed through this shell. */}\n      <form\n        id={formId}\n        {...HUBSPOT_DO_NOT_COLLECT_FORM_PROPS}\n        // Inherit the ModalV2 flex chain — an unstyled form between the shell\n        // and ModalV2Content made flex-1/min-h-0 inert, so tall modals pushed\n        // the Cancel/Save footer past 90vh.\n        className=\"flex min-h-0 flex-1 flex-col\"\n\n        onSubmit={e => {\n          e.preventDefault();\n          if (canSave && !saving) onSave();\n        }}\n        // Some modals contain a FILTER input, not a form field. Enter there must\n        // not ship the save — in the bindings modal that would commit an\n        // absolute-state rule-set replacement mid-search.\n        onKeyDown={e => {\n          if (e.key === 'Enter' && (e.target as HTMLElement)?.dataset?.noSubmitOnEnter === 'true') {\n            e.preventDefault();\n          }\n        }}\n      >\n        {loading ? (\n          // NOT `contentClassName` — that describes the real content's layout\n          // (master-detail modals pass `flex`, which turns the placeholder into\n          // a shrink-to-fit flex item pinned to the left of a 1400px panel).\n          // The placeholder owns its own layout and mirrors the column count so\n          // the modal doesn't visibly re-flow when the entity lands.\n          <ModalV2Content>\n            {loadingContent ? (\n              <div aria-busy=\"true\">{loadingContent}</div>\n            ) : (\n              <div\n                aria-busy=\"true\"\n                className={\n                  // Mirrors the PANEL WIDTH, not the slot API. `wide` covers the\n                  // master-detail modals too (seo-redirect, managed-repo,\n                  // exclusions) — they pass `children`, so keying on `twoColumn`\n                  // drew one column of full-width bars across 1400px, which reads\n                  // as a broken form rather than a loading one.\n                  skeletonColumns === 2\n                    ? 'grid grid-cols-1 gap-[var(--spacing-system-xl)] lg:grid-cols-2'\n                    : 'grid grid-cols-1 gap-[var(--spacing-system-xl)]'\n                }\n              >\n                <span className=\"sr-only\">Loading…</span>\n                {Array.from({ length: skeletonColumns }, (_, col) => (\n                  <div key={col} className=\"space-y-[var(--spacing-system-lf)]\" aria-hidden=\"true\">\n                    {SKELETON_FIELD_WIDTHS.map((labelWidth, i) => (\n                      <div key={i} className=\"space-y-[var(--spacing-system-xsf)]\">\n                        <Skeleton className={`h-4 ${labelWidth}`} />\n                        <Skeleton className=\"h-10 w-full\" />\n                      </div>\n                    ))}\n                  </div>\n                ))}\n              </div>\n            )}\n          </ModalV2Content>\n        ) : twoColumn ? (\n          <ModalV2TwoColumn left={leftColumn} right={rightColumn} />\n        ) : (\n          <ModalV2Content className={contentClassName}>{children}</ModalV2Content>\n        )}\n        {/* Outside the scrolling region(s): in a two-column modal an error\n            belongs to the save, not to either column, and must stay visible\n            wherever each column happens to be scrolled to. */}\n        {error && (\n          <p role=\"alert\" className=\"shrink-0 text-ods-error text-h6\">\n            {error}\n          </p>\n        )}\n      </form>\n\n      <ModalV2Footer>\n        <div className=\"flex w-full items-center justify-between gap-[var(--spacing-system-xsf)]\">\n          <div className=\"flex items-center gap-[var(--spacing-system-xsf)]\">{footerExtras}</div>\n          <div className=\"flex items-center gap-[var(--spacing-system-xsf)]\">\n            {footerStatus}\n            {dirty && <UnsavedChangesChip detail={dirtyDetail} />}\n            {footerActions}\n            <Button variant=\"outline\" size=\"small-legacy\" onClick={guardedClose} disabled={saving}>\n              Cancel\n            </Button>\n            <Button\n              size=\"small-legacy\"\n              type=\"submit\"\n              form={formId}\n              // NO `onClick` — the `form` association is the single activation\n              // path. Carrying both fired `onSave()` twice per click (the click\n              // handler, then the form's default submit), and the pending-state\n              // guard cannot catch it because React Query's re-render happens a\n              // microtask later. On the create path that meant two POSTs and two\n              // registry rows with the same name.\n              loading={saving}\n              disabled={saving || loading || !canSave}\n            >\n              {saveLabel}\n            </Button>\n          </div>\n        </div>\n      </ModalV2Footer>\n      {dialog}\n    </ModalV2>\n  );\n}\n","/**\n * HubSpot's collected-forms opt-out — ONE owner for every `<form>` in the system.\n *\n * Hosts that run the HubSpot tracking tag (the hub's root layout does; so does\n * the OpenFrame dashboard, via GTM) also load HubSpot's \"collected forms\"\n * script, which binds to EVERY `<form>` on the page and re-posts its fields to\n * HubSpot as a \"non-HubSpot form\" submission. For a form that already reaches\n * HubSpot server-side (booking, contact) that double-counts the contact's\n * conversions and, with no id/name on the element, names the phantom form after\n * its class list (\"Dashboard | OpenFrame: .flex, .flex-col, …\"). For every\n * other form (search boxes, admin modals, sign-up) it ships fields HubSpot has\n * no business seeing.\n *\n * `data-hs-do-not-collect` is the script's own opt-out, checked at bind time,\n * so an opted-out form is never observed. Spread these props onto EVERY\n * `<form>` that is not itself a HubSpot form — the attribute must never be\n * spelled by hand at a call site.\n */\nexport const HUBSPOT_DO_NOT_COLLECT_FORM_PROPS = { 'data-hs-do-not-collect': 'true' } as const;\n","'use client';\n\nimport * as SeparatorPrimitive from '@radix-ui/react-separator';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Separator = forwardRef<\n  ElementRef<typeof SeparatorPrimitive.Root>,\n  ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (\n  <SeparatorPrimitive.Root\n    ref={ref}\n    decorative={decorative}\n    orientation={orientation}\n    className={cn('shrink-0 bg-border', orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]', className)}\n    {...props}\n  />\n));\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n","'use client';\n\nimport * as SheetPrimitive from '@radix-ui/react-dialog';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { X } from 'lucide-react';\nimport { type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Sheet = SheetPrimitive.Root;\n\nconst SheetTrigger = SheetPrimitive.Trigger;\n\nconst SheetClose = SheetPrimitive.Close;\n\nconst SheetPortal = SheetPrimitive.Portal;\n\nconst SheetOverlay = forwardRef<\n  ElementRef<typeof SheetPrimitive.Overlay>,\n  ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Overlay\n    className={cn(\n      'fixed inset-0 z-[9997] bg-ods-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n      className,\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nSheetOverlay.displayName = SheetPrimitive.Overlay.displayName;\n\nconst sheetVariants = cva(\n  'fixed z-[9998] gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',\n  {\n    variants: {\n      side: {\n        top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n        bottom:\n          'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n        left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left md:max-w-sm',\n        right:\n          'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right md:max-w-sm',\n      },\n    },\n    defaultVariants: {\n      side: 'right',\n    },\n  },\n);\n\ninterface SheetContentProps\n  extends ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = forwardRef<ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(\n  ({ side = 'right', className, children, ...props }, ref) => (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>\n        {children}\n        <SheetPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary\">\n          <X className=\"h-4 w-4\" />\n          <span className=\"sr-only\">Close</span>\n        </SheetPrimitive.Close>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  ),\n);\nSheetContent.displayName = SheetPrimitive.Content.displayName;\n\nconst SheetHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col space-y-2 text-center md:text-left', className)} {...props} />\n);\nSheetHeader.displayName = 'SheetHeader';\n\nconst SheetFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn('flex flex-col-reverse md:flex-row md:justify-end md:space-x-2', className)} {...props} />\n);\nSheetFooter.displayName = 'SheetFooter';\n\nconst SheetTitle = forwardRef<\n  ElementRef<typeof SheetPrimitive.Title>,\n  ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title ref={ref} className={cn('text-foreground text-h3', className)} {...props} />\n));\nSheetTitle.displayName = SheetPrimitive.Title.displayName;\n\nconst SheetDescription = forwardRef<\n  ElementRef<typeof SheetPrimitive.Description>,\n  ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description ref={ref} className={cn('text-muted-foreground text-h6', className)} {...props} />\n));\nSheetDescription.displayName = SheetPrimitive.Description.displayName;\n\nexport {\n  Sheet,\n  SheetPortal,\n  SheetOverlay,\n  SheetTrigger,\n  SheetClose,\n  SheetContent,\n  SheetHeader,\n  SheetFooter,\n  SheetTitle,\n  SheetDescription,\n};\n","'use client';\n\nimport * as AccordionPrimitive from '@radix-ui/react-accordion';\nimport { ChevronDown } from 'lucide-react';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Accordion = AccordionPrimitive.Root;\n\nconst AccordionItem = forwardRef<\n  ElementRef<typeof AccordionPrimitive.Item>,\n  ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />\n));\nAccordionItem.displayName = 'AccordionItem';\n\nconst AccordionTrigger = forwardRef<\n  ElementRef<typeof AccordionPrimitive.Trigger>,\n  ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Header className=\"flex\">\n    <AccordionPrimitive.Trigger\n      ref={ref}\n      className={cn(\n        'flex flex-1 items-center justify-between py-4 transition-all text-h6 hover:underline [&[data-state=open]>svg]:rotate-180',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\n    </AccordionPrimitive.Trigger>\n  </AccordionPrimitive.Header>\n));\nAccordionTrigger.displayName = 'AccordionTrigger';\n\nconst AccordionContent = forwardRef<\n  ElementRef<typeof AccordionPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className={cn(\n      'overflow-hidden transition-all text-h6 data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',\n      className,\n    )}\n    {...props}\n  >\n    <div className=\"pb-4 pt-0\">{children}</div>\n  </AccordionPrimitive.Content>\n));\nAccordionContent.displayName = 'AccordionContent';\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent };\n","'use client';\n\nimport { Slot } from '@radix-ui/react-slot';\nimport { ChevronRight, MoreHorizontal } from 'lucide-react';\nimport { type ComponentProps, type ComponentPropsWithoutRef, type ReactNode, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Breadcrumb = forwardRef<\n  HTMLElement,\n  ComponentPropsWithoutRef<'nav'> & {\n    separator?: ReactNode;\n  }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />);\nBreadcrumb.displayName = 'Breadcrumb';\n\nconst BreadcrumbList = forwardRef<HTMLOListElement, ComponentPropsWithoutRef<'ol'>>(({ className, ...props }, ref) => (\n  <ol\n    ref={ref}\n    className={cn('flex flex-wrap items-center gap-1.5 break-words text-ods-text-muted text-h6 md:gap-2.5', className)}\n    {...props}\n  />\n));\nBreadcrumbList.displayName = 'BreadcrumbList';\n\nconst BreadcrumbItem = forwardRef<HTMLLIElement, ComponentPropsWithoutRef<'li'>>(({ className, ...props }, ref) => (\n  <li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />\n));\nBreadcrumbItem.displayName = 'BreadcrumbItem';\n\nconst BreadcrumbLink = forwardRef<\n  HTMLAnchorElement,\n  ComponentPropsWithoutRef<'a'> & {\n    asChild?: boolean;\n  }\n>(({ asChild, className, ...props }, ref) => {\n  const Comp = asChild ? Slot : 'a';\n\n  return <Comp ref={ref} className={cn('transition-colors hover:text-ods-text-primary', className)} {...props} />;\n});\nBreadcrumbLink.displayName = 'BreadcrumbLink';\n\nconst BreadcrumbPage = forwardRef<HTMLSpanElement, ComponentPropsWithoutRef<'span'>>(({ className, ...props }, ref) => (\n  <span\n    ref={ref}\n    role=\"link\"\n    aria-disabled=\"true\"\n    aria-current=\"page\"\n    className={cn('font-normal text-ods-text-primary', className)}\n    {...props}\n  />\n));\nBreadcrumbPage.displayName = 'BreadcrumbPage';\n\nconst BreadcrumbSeparator = ({ children, className, ...props }: ComponentProps<'li'>) => (\n  <li role=\"presentation\" aria-hidden=\"true\" className={cn('[&>svg]:h-3.5 [&>svg]:w-3.5', className)} {...props}>\n    {children ?? <ChevronRight />}\n  </li>\n);\nBreadcrumbSeparator.displayName = 'BreadcrumbSeparator';\n\nconst BreadcrumbEllipsis = ({ className, ...props }: ComponentProps<'span'>) => (\n  <span\n    role=\"presentation\"\n    aria-hidden=\"true\"\n    className={cn('flex h-9 w-9 items-center justify-center', className)}\n    {...props}\n  >\n    <MoreHorizontal className=\"h-4 w-4\" />\n    <span className=\"sr-only\">More</span>\n  </span>\n);\nBreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';\n\nexport {\n  Breadcrumb,\n  BreadcrumbList,\n  BreadcrumbItem,\n  BreadcrumbLink,\n  BreadcrumbPage,\n  BreadcrumbSeparator,\n  BreadcrumbEllipsis,\n};\n","'use client';\n\nimport * as MenubarPrimitive from '@radix-ui/react-menubar';\nimport { Check, ChevronRight, Circle } from 'lucide-react';\nimport { type ComponentPropsWithoutRef, type ElementRef, type HTMLAttributes, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst MenubarMenu = MenubarPrimitive.Menu;\n\nconst MenubarGroup = MenubarPrimitive.Group;\n\nconst MenubarPortal = MenubarPrimitive.Portal;\n\nconst MenubarSub = MenubarPrimitive.Sub;\n\nconst MenubarRadioGroup = MenubarPrimitive.RadioGroup;\n\nconst Menubar = forwardRef<\n  ElementRef<typeof MenubarPrimitive.Root>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Root\n    ref={ref}\n    className={cn('flex h-10 items-center space-x-1 rounded-md border bg-background p-1', className)}\n    {...props}\n  />\n));\nMenubar.displayName = MenubarPrimitive.Root.displayName;\n\nconst MenubarTrigger = forwardRef<\n  ElementRef<typeof MenubarPrimitive.Trigger>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-3 py-1.5 outline-none text-h6 focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',\n      className,\n    )}\n    {...props}\n  />\n));\nMenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;\n\nconst MenubarSubTrigger = forwardRef<\n  ElementRef<typeof MenubarPrimitive.SubTrigger>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <MenubarPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 outline-none text-h6 focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto h-4 w-4\" />\n  </MenubarPrimitive.SubTrigger>\n));\nMenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;\n\nconst MenubarSubContent = forwardRef<\n  ElementRef<typeof MenubarPrimitive.SubContent>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n      className,\n    )}\n    {...props}\n  />\n));\nMenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;\n\nconst MenubarContent = forwardRef<\n  ElementRef<typeof MenubarPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>\n>(({ className, align = 'start', alignOffset = -4, sideOffset = 8, ...props }, ref) => (\n  <MenubarPrimitive.Portal>\n    <MenubarPrimitive.Content\n      ref={ref}\n      align={align}\n      alignOffset={alignOffset}\n      sideOffset={sideOffset}\n      className={cn(\n        'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n        className,\n      )}\n      {...props}\n    />\n  </MenubarPrimitive.Portal>\n));\nMenubarContent.displayName = MenubarPrimitive.Content.displayName;\n\nconst MenubarItem = forwardRef<\n  ElementRef<typeof MenubarPrimitive.Item>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Item\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 outline-none text-h6 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      inset && 'pl-8',\n      className,\n    )}\n    {...props}\n  />\n));\nMenubarItem.displayName = MenubarPrimitive.Item.displayName;\n\nconst MenubarCheckboxItem = forwardRef<\n  ElementRef<typeof MenubarPrimitive.CheckboxItem>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <MenubarPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 outline-none text-h6 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <MenubarPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </MenubarPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </MenubarPrimitive.CheckboxItem>\n));\nMenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;\n\nconst MenubarRadioItem = forwardRef<\n  ElementRef<typeof MenubarPrimitive.RadioItem>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <MenubarPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 outline-none text-h6 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <MenubarPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </MenubarPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </MenubarPrimitive.RadioItem>\n));\nMenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;\n\nconst MenubarLabel = forwardRef<\n  ElementRef<typeof MenubarPrimitive.Label>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Label\n    ref={ref}\n    className={cn('px-2 py-1.5 font-semibold text-h6', inset && 'pl-8', className)}\n    {...props}\n  />\n));\nMenubarLabel.displayName = MenubarPrimitive.Label.displayName;\n\nconst MenubarSeparator = forwardRef<\n  ElementRef<typeof MenubarPrimitive.Separator>,\n  ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />\n));\nMenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;\n\nconst MenubarShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {\n  return <span className={cn('ml-auto text-muted-foreground text-h6', className)} {...props} />;\n};\nMenubarShortcut.displayname = 'MenubarShortcut';\n\nexport {\n  Menubar,\n  MenubarMenu,\n  MenubarTrigger,\n  MenubarContent,\n  MenubarItem,\n  MenubarSeparator,\n  MenubarLabel,\n  MenubarCheckboxItem,\n  MenubarRadioGroup,\n  MenubarRadioItem,\n  MenubarPortal,\n  MenubarSubContent,\n  MenubarSubTrigger,\n  MenubarGroup,\n  MenubarSub,\n  MenubarShortcut,\n};\n","'use client';\n\nimport * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';\nimport { cva } from 'class-variance-authority';\nimport { ChevronDown } from 'lucide-react';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst NavigationMenuViewport = forwardRef<\n  ElementRef<typeof NavigationMenuPrimitive.Viewport>,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>\n>(({ className, ...props }, ref) => (\n  <div className={cn('absolute left-0 top-full flex justify-center')}>\n    <NavigationMenuPrimitive.Viewport\n      className={cn(\n        'origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]',\n        className,\n      )}\n      ref={ref}\n      {...props}\n    />\n  </div>\n));\n\nconst NavigationMenu = forwardRef<\n  ElementRef<typeof NavigationMenuPrimitive.Root>,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Root\n    ref={ref}\n    className={cn('relative z-10 flex max-w-max flex-1 items-center justify-center', className)}\n    {...props}\n  >\n    {children}\n    <NavigationMenuViewport />\n  </NavigationMenuPrimitive.Root>\n));\nNavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;\n\nconst NavigationMenuList = forwardRef<\n  ElementRef<typeof NavigationMenuPrimitive.List>,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.List\n    ref={ref}\n    className={cn('group flex flex-1 list-none items-center justify-center space-x-1', className)}\n    {...props}\n  />\n));\nNavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;\n\nconst NavigationMenuItem = NavigationMenuPrimitive.Item;\n\nconst navigationMenuTriggerStyle = cva(\n  'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 transition-colors text-h6 hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50',\n);\n\nconst NavigationMenuTrigger = forwardRef<\n  ElementRef<typeof NavigationMenuPrimitive.Trigger>,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Trigger\n    ref={ref}\n    className={cn(navigationMenuTriggerStyle(), 'group', className)}\n    {...props}\n  >\n    {children}{' '}\n    <ChevronDown\n      className=\"relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180\"\n      aria-hidden=\"true\"\n    />\n  </NavigationMenuPrimitive.Trigger>\n));\nNavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;\n\nconst NavigationMenuContent = forwardRef<\n  ElementRef<typeof NavigationMenuPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Content\n    ref={ref}\n    className={cn(\n      'left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto',\n      className,\n    )}\n    {...props}\n  />\n));\nNavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;\n\nconst NavigationMenuLink = NavigationMenuPrimitive.Link;\n\nNavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;\n\nconst NavigationMenuIndicator = forwardRef<\n  ElementRef<typeof NavigationMenuPrimitive.Indicator>,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.Indicator\n    ref={ref}\n    className={cn(\n      'top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in',\n      className,\n    )}\n    {...props}\n  >\n    <div className=\"relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md\" />\n  </NavigationMenuPrimitive.Indicator>\n));\nNavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;\n\nexport {\n  navigationMenuTriggerStyle,\n  NavigationMenu,\n  NavigationMenuList,\n  NavigationMenuItem,\n  NavigationMenuContent,\n  NavigationMenuTrigger,\n  NavigationMenuLink,\n  NavigationMenuIndicator,\n  NavigationMenuViewport,\n};\n","'use client';\n\n/**\n * <CrossfadePanels> — THE keep-mounted tab-panel stack: every panel stays in\n * the DOM, stacked in the SAME grid cell, and switching is an opacity\n * crossfade. Use it under any tab control (TabNavigation, TabSelector, …)\n * whenever panels are content surfaces where height stability and a soft\n * switch matter (strips, media rails, dashboards-in-view).\n *\n * Invariants encoded HERE exactly once (so no consumer re-derives them):\n * - HEIGHT LOCK: grid items in one cell size the row to max(panel heights) —\n *   switching never shifts the page (no CLS), and surrounding layout treats\n *   the stack as one constant-height block.\n * - FADE-THEN-HIDE: `visibility` transitions alongside `opacity`; per the CSS\n *   transitions spec's special visibility interpolation the outgoing panel\n *   stays paintable until the fade completes, THEN becomes `hidden` — no\n *   pop-out, and the settled hidden panel is fully removed from paint.\n * - DEAD WHILE HIDDEN: `inert` + `aria-hidden` + `pointer-events-none` on\n *   inactive panels — no tab-stops, no screen-reader content, no hover/click\n *   leaks from the invisible layer (WCAG aria-hidden-focus).\n * - MOTION RESPECT: `prefers-reduced-motion` collapses the fade to an\n *   instant swap.\n * - The ACTIVE panel takes `z-[1]` so the incoming layer composites on top\n *   during the crossfade regardless of DOM order.\n *\n * This deliberately complements (not replaces) <TabContent>, which MOUNTS one\n * panel at a time — right for heavy admin surfaces that should unmount when\n * hidden. CrossfadePanels is for surfaces whose panels are cheap enough to\n * keep alive and whose switch must be seamless.\n *\n * NOTE for marquee/animated panel content: hidden panels stay mounted and\n * their animations keep running unless the content self-pauses. CardsStrip\n * panels are fine (its rAF gates on near-viewport + visibility), but a\n * consumer embedding heavier always-on content should pause it via its own\n * visibility signal.\n */\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface CrossfadePanel {\n  id: string;\n  content: React.ReactNode;\n}\n\nexport interface CrossfadePanelsProps {\n  /** Which panel is shown. Unknown id → all panels hidden (renders the\n   *  stack's reserved height, nothing visible). */\n  activeId: string | undefined;\n  panels: ReadonlyArray<CrossfadePanel>;\n  /** Crossfade duration in ms. Default 300. */\n  durationMs?: number;\n  className?: string;\n  /** Extra classes for every panel wrapper (e.g. `min-w-0` is built in). */\n  panelClassName?: string;\n}\n\nexport function CrossfadePanels({\n  activeId,\n  panels,\n  durationMs = 300,\n  className,\n  panelClassName,\n}: CrossfadePanelsProps) {\n  if (panels.length === 0) return null;\n  return (\n    <div className={cn('grid w-full', className)}>\n      {panels.map(p => {\n        const active = p.id === activeId;\n        return (\n          <div\n            key={p.id}\n            className={cn(\n              'col-start-1 row-start-1 min-w-0 transition-[opacity,visibility] ease-out motion-reduce:transition-none',\n              active ? 'visible z-[1] opacity-100' : 'pointer-events-none invisible opacity-0',\n              panelClassName,\n            )}\n            style={{ transitionDuration: `${durationMs}ms` }}\n            aria-hidden={!active || undefined}\n            inert={!active || undefined}\n          >\n            {p.content}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * `TabComponent` and `componentProps` are ONE unit: the panel's props type is\n * inferred from the bag the caller passes, so the two can no longer drift.\n */\ninterface TabContentProps<P extends object = Record<string, never>> {\n  activeTab: string;\n  TabComponent: React.ComponentType<P> | null;\n  componentProps?: P;\n  className?: string;\n  emptyStateTitle?: string;\n  emptyStateDescription?: string;\n  minHeight?: string;\n}\n\nexport function TabContent<P extends object = Record<string, never>>({\n  activeTab,\n  TabComponent,\n  componentProps,\n  className,\n  emptyStateTitle = 'Tab Not Found',\n  emptyStateDescription,\n  minHeight = 'min-h-[400px]',\n}: TabContentProps<P>) {\n  const defaultDescription = `The selected tab \"${activeTab}\" could not be found.`;\n\n  return (\n    <div className={cn(minHeight, className)}>\n      {TabComponent ? (\n        // `createElement` (not `<TabComponent {...componentProps} />`) so the\n        // optional bag can be omitted entirely without asserting an empty `P`.\n        React.createElement(TabComponent, componentProps ?? null)\n      ) : (\n        <div className={cn(minHeight, 'flex items-center justify-center')}>\n          <div className=\"text-center\">\n            <h3 className=\"mb-2 text-ods-text-primary text-h3\">{emptyStateTitle}</h3>\n            <p className=\"text-ods-text-secondary\">{emptyStateDescription || defaultDescription}</p>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport {\n  useState,\n  useEffect,\n  useLayoutEffect,\n  useDeferredValue,\n  useMemo,\n  useRef,\n  useCallback,\n  memo,\n  startTransition,\n} from 'react';\nimport { useSearchParams, useRouter, usePathname } from '../../embed-shims/next-navigation';\nimport { cn } from '../../utils/cn';\n\nexport interface TabItem {\n  id: string;\n  label: string;\n  /** Optional — text-only tabs (e.g. the homepage content-strip switcher) omit it. */\n  icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n  /**\n   * The tab's panel. Its props come from the caller's shared `componentProps`\n   * bag (see `<TabContent>`, which IS generic over that bag) — but a\n   * heterogeneous tab LIST cannot name it, and the two live usage patterns\n   * demand OPPOSITE defaults, so no single non-`any` type serves both:\n   *\n   *   a) panels WITH required props — `TabItem[]` holding\n   *      `ComponentType<{ scheduleId: string }>` (openframe-oss-frontend's\n   *      schedule / script detail tabs). Needs a default assignable INTO every\n   *      concrete panel, i.e. `never`.\n   *   b) panels rendered straight from `getTabComponent(...)` as\n   *      `<TabComponent />` with no props (openframe-oss-frontend's customers /\n   *      settings / scripts pages). Needs a default the empty prop bag\n   *      satisfies, i.e. `Record<string, never>`.\n   *\n   * Verified with a generic `TabItem<P extends object = …>` against both\n   * patterns: `Record<string, never>` rejects (a) (TS2322 — \"Property\n   * 'scheduleId' is missing\"), `never` rejects (b) (TS2769/TS2786 — \"'C' cannot\n   * be used as a JSX component\"). Typing this therefore means migrating the\n   * consumers to `TabItem<TheirProps>`, not changing this line.\n   */\n  component?: React.ComponentType<any>;\n  indicator?: 'success' | 'warning' | 'error';\n}\n\nexport interface TabNavigationUrlSyncOptions {\n  paramName?: string; // Default: 'tab'\n  replaceState?: boolean; // Default: true (use replace instead of push)\n}\n\ninterface TabNavigationProps {\n  // Legacy controlled mode (when urlSync is disabled)\n  activeTab?: string;\n  onTabChange?: (tabId: string) => void;\n\n  tabs: TabItem[];\n  className?: string;\n  shadowClassName?: string; // Tailwind class for shadow gradient color, e.g. \"from-black\" or \"from-red-500\"\n  /** Force the right-edge gradient to always render, independent of scroll state. */\n  showRightGradient?: boolean;\n  /** Force the left-edge gradient to always render, independent of scroll state. */\n  showLeftGradient?: boolean;\n  /** Tabs grow to share the bar's full width equally (Figma segmented-underline\n   *  look, e.g. the 480px homepage strip switcher). Default: natural width. */\n  stretchTabs?: boolean;\n\n  // URL sync mode\n  urlSync?: boolean | TabNavigationUrlSyncOptions;\n  defaultTab?: string; // Fallback when no valid tab in URL or initial value\n\n  /**\n   * Render prop for the tab BODY. Receives the tab to render and, additively,\n   * a `isStale` flag — true while the bar has already moved to a newly clicked\n   * tab but the body is still showing the PREVIOUS one (see `deferredActiveTab`\n   * below). Use it to dim the body or show a pending hint; without it a slow\n   * tab looks like a tab that simply did nothing.\n   */\n  children?: (activeTab: string, state: { isStale: boolean }) => React.ReactNode;\n}\n\n/** Where the underline sits, in the strip's own coordinates. */\ninterface IndicatorRect {\n  left: number;\n  width: number;\n}\n\nconst HIDDEN_INDICATOR: IndicatorRect = { left: 0, width: 0 };\n\nconst StatusDot = ({ indicator, className }: { indicator: NonNullable<TabItem['indicator']>; className?: string }) => (\n  <div\n    className={cn(\n      'h-3 w-3 rounded-full border-2 border-ods-bg',\n      indicator === 'error' && 'bg-ods-error',\n      indicator === 'warning' && 'bg-ods-accent',\n      indicator === 'success' && 'bg-ods-success',\n      className,\n    )}\n  />\n);\n\ninterface TabBarProps {\n  tabs: TabItem[];\n  activeTab: string;\n  onTabChange: (tabId: string) => void;\n  className?: string;\n  shadowClassName?: string;\n  showLeftGradient: boolean;\n  showRightGradient: boolean;\n  stretchTabs: boolean;\n}\n\n/**\n * The strip itself — buttons, overflow scrolling, edge fades, underline.\n *\n * Split out of `TabNavigation` for one reason, and it is a performance one: the\n * strip keeps state that changes for reasons having nothing to do with which tab\n * is open (scroll position → edge fades, ResizeObserver → re-measure). While\n * that state lived on `TabNavigation`, every one of those updates re-rendered\n * the component that CALLS the body render prop, so merely scrolling the tab\n * strip on a narrow screen re-rendered whatever heavy table was open underneath.\n * Down here it re-renders only the bar; `TabNavigation` — and the body — never\n * hear about it.\n */\nconst TabBar = memo(function TabBarImpl({\n  tabs,\n  activeTab,\n  onTabChange,\n  className,\n  shadowClassName,\n  showLeftGradient,\n  showRightGradient,\n  stretchTabs,\n}: TabBarProps) {\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const activeTabRef = useRef<HTMLButtonElement>(null);\n  const isFirstActiveScrollRef = useRef(true);\n  const [canScrollLeft, setCanScrollLeft] = useState(false);\n  const [canScrollRight, setCanScrollRight] = useState(false);\n\n  // The underline is ONE element that slides, not a div mounted under whichever\n  // tab is active — that version could only ever pop from tab to tab. Position\n  // is measured rather than derived from CSS because the tabs are natural-width\n  // (label-dependent) and live in a scroll container, so nothing but layout\n  // knows where the active one actually is.\n  const [indicator, setIndicator] = useState<IndicatorRect>(HIDDEN_INDICATOR);\n  // Suppresses the slide for the FIRST placement: a deep link landing on the\n  // third tab should not open with the underline gliding in from the left edge.\n  //\n  // It must only become true one render AFTER the underline first gets a real\n  // position. Flipping it in the SAME render would put the transition class on\n  // the element for the very paint where width and opacity jump off zero, and\n  // the browser would animate exactly the entrance this exists to suppress.\n  //\n  // Tracked as state adjusted while rendering rather than a ref written in a\n  // layout effect: it drives a className, i.e. rendered output, and output must\n  // not depend on a value React is not tracking. `measureIndicator` keeps\n  // `indicator`'s identity stable when the measurement is unchanged, so\n  // comparing by identity is what \"the position moved\" means. Once\n  // `placedOnce` is true nothing here writes again.\n  const [placement, setPlacement] = useState<{ indicator: IndicatorRect; placedOnce: boolean }>({\n    indicator: HIDDEN_INDICATOR,\n    placedOnce: false,\n  });\n\n  const measureIndicator = useCallback(() => {\n    const active = activeTabRef.current;\n    // No active tab (unknown id, or an empty tabs array) → collapse it away\n    // rather than leaving it under whichever tab it last sat on.\n    const next = active ? { left: active.offsetLeft, width: active.offsetWidth } : HIDDEN_INDICATOR;\n    setIndicator(prev => (prev.left === next.left && prev.width === next.width ? prev : next));\n  }, []);\n\n  const updateScrollShadows = useCallback(() => {\n    const el = scrollRef.current;\n    if (!el) return;\n    setCanScrollLeft(el.scrollLeft > 0);\n    setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);\n  }, []);\n\n  // Layout effect: the underline's first frame must already be under the right\n  // tab. In a passive effect it would paint at the previous position first.\n  useLayoutEffect(() => {\n    measureIndicator();\n  }, [measureIndicator, activeTab, tabs]);\n\n  useEffect(() => {\n    const el = scrollRef.current;\n    if (!el) return undefined;\n\n    updateScrollShadows();\n\n    // Translate a vertical mouse wheel into horizontal scroll. macOS mice emit\n    // only deltaY, which the browser won't apply to a horizontal-only overflow\n    // container, so the strip is otherwise un-scrollable with a plain mouse.\n    // Non-passive so preventDefault() can suppress the page from scrolling.\n    const onWheel = (e: WheelEvent) => {\n      // A trackpad's horizontal gesture (deltaX) already scrolls natively; a\n      // pinch-zoom arrives as wheel+ctrlKey — leave both untouched.\n      if (e.deltaX !== 0 || e.ctrlKey) return;\n      if (el.scrollWidth <= el.clientWidth) return;\n      // deltaY isn't always pixels: Firefox mice report lines (deltaMode 1),\n      // some devices pages (mode 2). Normalize so the scroll isn't a no-op.\n      const delta = e.deltaMode === 1 ? e.deltaY * 40 : e.deltaMode === 2 ? e.deltaY * el.clientWidth : e.deltaY;\n      // Only consume the event while there's room to scroll in that direction;\n      // at either edge, let it bubble so the page scrolls (native chaining).\n      const canGoRight = el.scrollLeft + el.clientWidth < el.scrollWidth - 1;\n      const canGoLeft = el.scrollLeft > 0;\n      if ((delta > 0 && !canGoRight) || (delta < 0 && !canGoLeft)) return;\n      el.scrollLeft += delta;\n      e.preventDefault();\n    };\n\n    // Scrolling moves the viewport, not the content — `offsetLeft` is unchanged,\n    // so only the fades need recomputing. A resize moves both.\n    el.addEventListener('scroll', updateScrollShadows, { passive: true });\n    el.addEventListener('wheel', onWheel, { passive: false });\n    const ro = new ResizeObserver(() => {\n      updateScrollShadows();\n      measureIndicator();\n    });\n    ro.observe(el);\n    // Web fonts land after first paint and change every label's width, which\n    // moves every tab but fires no resize on the strip itself.\n    let cancelled = false;\n    document.fonts?.ready\n      .then(() => {\n        if (!cancelled) measureIndicator();\n      })\n      .catch(() => {});\n\n    return () => {\n      cancelled = true;\n      el.removeEventListener('scroll', updateScrollShadows);\n      el.removeEventListener('wheel', onWheel);\n      ro.disconnect();\n    };\n  }, [updateScrollShadows, measureIndicator]);\n\n  // Bring the active tab into view when it changes (e.g. clicking a partly\n  // off-screen tab, or a URL-driven change). Uses the browser's own smooth\n  // scroll and moves ONLY the strip — never the page — by nudging just enough\n  // to reveal the tab on whichever edge it's clipped. Complements the native\n  // trackpad / Shift+wheel / mouse-wheel scrolling rather than fighting it.\n  useEffect(() => {\n    const el = scrollRef.current;\n    const active = activeTabRef.current;\n    if (!el || !active) return;\n    // On mount (e.g. a deep link landing on an off-screen tab, such as\n    // `?tab=software` as the last of many), snap instantly instead of visibly\n    // sliding right after first paint — smooth is reserved for later tab changes.\n    const behavior = isFirstActiveScrollRef.current ? 'auto' : 'smooth';\n    isFirstActiveScrollRef.current = false;\n    const elRect = el.getBoundingClientRect();\n    const aRect = active.getBoundingClientRect();\n    if (aRect.left < elRect.left) {\n      el.scrollBy({ left: aRect.left - elRect.left, behavior });\n    } else if (aRect.right > elRect.right) {\n      el.scrollBy({ left: aRect.right - elRect.right, behavior });\n    }\n  }, [activeTab]);\n\n  const leftFade = canScrollLeft || showLeftGradient;\n  const rightFade = canScrollRight || showRightGradient;\n\n  const borderStyle = useMemo<React.CSSProperties>(() => {\n    const c = 'var(--color-border-default)';\n    if (leftFade && rightFade) {\n      return {\n        background: `linear-gradient(to right, transparent 0, ${c} 40px, ${c} calc(100% - 40px), transparent 100%)`,\n      };\n    }\n    if (leftFade) {\n      return { background: `linear-gradient(to right, transparent 0, ${c} 40px, ${c} 100%)` };\n    }\n    if (rightFade) {\n      return { background: `linear-gradient(to right, ${c} 0, ${c} calc(100% - 40px), transparent 100%)` };\n    }\n    return { background: c };\n  }, [leftFade, rightFade]);\n\n  // Real width, moved by `translateX`. This used to be a 1px bar stretched with\n  // `scaleX(width)` — transform and opacity being the only properties the\n  // compositor can animate without laying out again — but scaling from a ONE\n  // PIXEL base turns that pixel into an error multiplier.\n  //\n  // A CSS pixel only lands on a whole device pixel at integer scale factors. At\n  // 125% / 150% OS scaling, a scaled Mac resolution, or any browser zoom, the\n  // base is rasterized at the nearest whole device pixel — call it 0.8 or 1.33\n  // CSS px — and `scaleX(150)` multiplies that rounding by 150. The underline\n  // then renders a fifth short or a third long, per tab, with `measureIndicator`\n  // having been perfectly correct all along. It shows up from the SECOND tab\n  // change on, because that is when the transition promotes the bar into its own\n  // composited layer, where it is rasterized in its own 1×4 coordinate space and\n  // only then stretched.\n  //\n  // Animating `width` costs a layout per frame for this one element — it is\n  // `absolute`, so nothing else in the strip is laid out with it, and 200ms of\n  // that for a 4px-tall bar is not measurable. Correctness at every zoom level\n  // is worth more than keeping one property on the compositor.\n  const isPlaced = indicator.width > 0;\n  const indicatorStyle: React.CSSProperties = {\n    width: `${indicator.width}px`,\n    transform: `translateX(${indicator.left}px)`,\n    opacity: isPlaced ? 1 : 0,\n  };\n  // True from the render AFTER the one that first committed a real position.\n  const shouldAnimateIndicator =\n    placement.placedOnce || (placement.indicator !== indicator && placement.indicator.width > 0);\n  if (!placement.placedOnce && placement.indicator !== indicator) {\n    setPlacement({ indicator, placedOnce: shouldAnimateIndicator });\n  }\n\n  return (\n    <div className={cn('relative w-full', className)}>\n      {/* scrollbar-hide: tabs stay swipe/wheel-scrollable, bar never shows.\n          `relative` makes this the offsetParent the underline is measured against. */}\n      <div\n        ref={scrollRef}\n        className=\"scrollbar-hide relative flex h-full items-center justify-start gap-[var(--spacing-system-xxs)] overflow-x-auto overflow-y-hidden\"\n      >\n        {tabs.map(tab => {\n          const isActive = activeTab === tab.id;\n\n          return (\n            <button\n              key={tab.id}\n              ref={isActive ? activeTabRef : undefined}\n              type=\"button\"\n              onClick={() => onTabChange(tab.id)}\n              className={cn(\n                'relative flex shrink-0 cursor-pointer items-center justify-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',\n                // Named rather than `transition-all`: the only thing that moves\n                // here is colour. `all` also made the browser re-check every\n                // property each frame, including the background GRADIENT — which\n                // never animated anyway, since the inactive state has no\n                // background-image to interpolate from.\n                'border-none bg-transparent outline-none transition-colors duration-200',\n                // `outline-none` above drops the browser's own focus ring, so\n                // put one back or the strip is un-navigable by keyboard — you\n                // can tab through it with nothing to show where you are. Inset,\n                // so it stays within the button instead of overlapping the tab\n                // sitting 4px away.\n                'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus',\n                stretchTabs && 'flex-1',\n                // Known limitation: ODS color vars hold hex values, so Tailwind\n                // alpha modifiers (to-ods-accent/10) silently produce no CSS.\n                // color-mix() accepts hex, so we derive the 10%-alpha stop from the token.\n                isActive\n                  ? 'bg-gradient-to-b from-transparent to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'\n                  : 'hover:bg-gradient-to-b hover:from-transparent hover:to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]',\n              )}\n            >\n              {tab.icon ? (\n                <div className=\"relative flex items-center justify-center\">\n                  <tab.icon\n                    className={cn(\n                      'h-4 w-4 transition-colors md:h-6 md:w-6',\n                      isActive ? 'text-ods-accent' : 'text-ods-text-secondary',\n                    )}\n                  />\n                  {tab.indicator && <StatusDot indicator={tab.indicator} className=\"absolute right-0 top-[-3px]\" />}\n                </div>\n              ) : tab.indicator ? (\n                // Text-only tabs keep their status badge — inline dot before\n                // the label (there's no icon corner to anchor to).\n                <StatusDot indicator={tab.indicator} className=\"shrink-0\" />\n              ) : null}\n\n              <span\n                className={cn(\n                  'whitespace-nowrap transition-colors text-h4',\n                  isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n                )}\n              >\n                {tab.label}\n              </span>\n            </button>\n          );\n        })}\n\n        {/* One underline for the whole strip, sliding between tabs. Inside the\n            scroll container so it travels with the content. Width comes from the\n            style below in real pixels — see why it is not a scaled 1px bar. */}\n        <div\n          aria-hidden\n          // Deliberately reachable by test id: the bar is aria-hidden and\n          // carries no text, so there is no accessible query for it, and its\n          // computed width/transform is a real contract (see tab-navigation\n          // tests) rather than incidental styling.\n          data-testid=\"tab-navigation-underline\"\n          className={cn(\n            'pointer-events-none absolute bottom-0 left-0 h-1 bg-ods-accent',\n            shouldAnimateIndicator &&\n              'transition-[transform,width,opacity] duration-200 ease-out motion-reduce:transition-none',\n          )}\n          style={indicatorStyle}\n        />\n      </div>\n\n      {/* Fade shadows — visible when content overflows or when forced via props */}\n      {leftFade && (\n        <div\n          className={cn(\n            'pointer-events-none absolute bottom-0 left-0 top-0 w-10 bg-gradient-to-r to-transparent',\n            shadowClassName || 'from-ods-bg',\n          )}\n        />\n      )}\n      {rightFade && (\n        <div\n          className={cn(\n            'pointer-events-none absolute bottom-0 right-0 top-0 w-10 bg-gradient-to-l to-transparent',\n            shadowClassName || 'from-ods-bg',\n          )}\n        />\n      )}\n\n      {/* Bottom border — a gradient-capable 1px line that fades to transparent on any edge that has an active fade shadow. */}\n      <div className=\"pointer-events-none absolute bottom-0 left-0 right-0 h-px\" style={borderStyle} />\n    </div>\n  );\n});\n\nexport function TabNavigation({\n  activeTab: controlledActiveTab,\n  onTabChange: controlledOnTabChange,\n  tabs,\n  className,\n  shadowClassName,\n  showRightGradient = false,\n  showLeftGradient = false,\n  stretchTabs = false,\n  urlSync = false,\n  defaultTab,\n  children,\n}: TabNavigationProps) {\n  const router = useRouter();\n  const pathname = usePathname();\n  const searchParams = useSearchParams();\n\n  // Determine URL sync settings\n  const isUrlSyncEnabled = !!urlSync;\n  const paramName = typeof urlSync === 'object' ? urlSync.paramName || 'tab' : 'tab';\n  const replaceState = typeof urlSync === 'object' ? urlSync.replaceState !== false : true;\n\n  // Valid tab IDs set\n  const validTabIds = useMemo(() => new Set(tabs.map(t => t.id)), [tabs]);\n  // A string, so the sync effect below depends on a VALUE rather than on the\n  // `tabs` array identity — a consumer passing an inline array literal would\n  // otherwise re-run that effect on every one of its renders.\n  const fallbackTab = defaultTab || tabs[0]?.id || '';\n\n  // What the URL currently says. Read during render (not in the effect) so the\n  // effect can compare it against the last value it ACTED on.\n  const urlTab = isUrlSyncEnabled ? searchParams?.get(paramName) || '' : '';\n\n  // Get initial tab value\n  const getInitialTab = () => {\n    if (isUrlSyncEnabled && validTabIds.has(urlTab)) return urlTab;\n    return fallbackTab;\n  };\n\n  // Internal state for URL sync mode\n  const [internalActiveTab, setInternalActiveTab] = useState(getInitialTab);\n\n  // Use internal state if URL sync is enabled, otherwise use controlled prop\n  const activeTab = isUrlSyncEnabled ? internalActiveTab : controlledActiveTab || '';\n\n  // The last URL value this component reconciled against.\n  const lastSyncedUrlTabRef = useRef(urlTab);\n  // Tabs written to the URL whose navigation has not landed yet, oldest first.\n  // Until a write lands, `searchParams` still reports the PREVIOUS tab — and\n  // reading that as somebody else editing the URL is exactly what used to push\n  // the tab back to where the click started.\n  const inFlightUrlTabsRef = useRef<string[]>([]);\n\n  // Sync with URL changes (back/forward navigation, a link into a tab).\n  //\n  // Not guarded on `nextTab !== internalActiveTab`, which is what this used to\n  // compare. That re-ran whenever our own click moved the state, and at that\n  // moment `router.replace` has not landed — so it read the tab we just left,\n  // decided the state was wrong, and pushed the tab BACK, only for the arriving\n  // URL to move it forward again. Whether the flicker was visible came down to\n  // whether the navigation beat the passive-effect flush.\n  //\n  // The three cases below are the three things that can actually have happened.\n  useEffect(() => {\n    if (!isUrlSyncEnabled) return;\n\n    // 1. One of our own writes arriving. The state moved on the click, so there\n    //    is nothing to set — just drop it, along with anything written before it\n    //    that a faster second click has already superseded.\n    const landed = inFlightUrlTabsRef.current.indexOf(urlTab);\n    if (landed !== -1) {\n      inFlightUrlTabsRef.current.splice(0, landed + 1);\n      lastSyncedUrlTabRef.current = urlTab;\n      return;\n    }\n\n    // 2. The URL has not moved. Either a write is still in flight and this run\n    //    is an unrelated re-render landing in the gap (a `tabs` array with a\n    //    fresh identity, say — which a guard on the URL alone would misread as\n    //    an external change and use to undo the click), or the TAB LIST itself\n    //    changed. The latter is why a URL guard is not enough on its own: a\n    //    consumer whose tabs load asynchronously mounts with an empty list,\n    //    resolves to no tab, and would then wait for a URL change that never\n    //    comes — deep link dropped, nothing ever active. So re-resolve, but only\n    //    when what we are showing is no longer in the list.\n    if (urlTab === lastSyncedUrlTabRef.current) {\n      setInternalActiveTab(prev => (validTabIds.has(prev) ? prev : validTabIds.has(urlTab) ? urlTab : fallbackTab));\n      return;\n    }\n\n    // 3. The URL moved somewhere we did not write it: back/forward, a redirect,\n    //    a link elsewhere on the page. That wins, and anything still in flight\n    //    is stale by definition.\n    inFlightUrlTabsRef.current.length = 0;\n    lastSyncedUrlTabRef.current = urlTab;\n    setInternalActiveTab(validTabIds.has(urlTab) ? urlTab : fallbackTab);\n  }, [isUrlSyncEnabled, urlTab, validTabIds, fallbackTab]);\n\n  // Everything the click handler reads but must not be re-created for. Held in\n  // a ref so `handleTabChange` is reference-stable: it is the one prop that\n  // would otherwise break `TabBar`'s memo on every navigation, since\n  // `searchParams` gets a new identity each time ANY query param moves.\n  const navRef = useRef({ isUrlSyncEnabled, controlledOnTabChange, searchParams, pathname, paramName, replaceState });\n  // Filled in an effect rather than in the render body. With `useDeferredValue`\n  // this component renders more than once per commit, and React is free to\n  // discard a render outright — a ref written during one of those would hand the\n  // click handler a `pathname`/`searchParams` pair that never committed, and the\n  // click would write its query string against the wrong URL. A click can only\n  // arrive after a commit, so the handler still reads current values.\n  useEffect(() => {\n    navRef.current = { isUrlSyncEnabled, controlledOnTabChange, searchParams, pathname, paramName, replaceState };\n  });\n\n  const handleTabChange = useCallback(\n    (tabId: string) => {\n      const nav = navRef.current;\n\n      if (!nav.isUrlSyncEnabled) {\n        // Legacy controlled mode\n        nav.controlledOnTabChange?.(tabId);\n        return;\n      }\n\n      // The bar follows the click immediately — this update stays urgent.\n      setInternalActiveTab(tabId);\n      // ...and is what the URL is about to say. Recorded so the sync effect knows\n      // the param arriving later is ours, and knows that until it does arrive the\n      // URL is merely stale rather than disagreeing with us. Skipped when it is\n      // already the newest thing in flight, so a double-click on one tab does not\n      // queue the same id twice.\n      const inFlight = inFlightUrlTabsRef.current;\n      if (inFlight[inFlight.length - 1] !== tabId) inFlight.push(tabId);\n\n      const params = new URLSearchParams(nav.searchParams?.toString());\n      params.set(nav.paramName, tabId);\n      const method = nav.replaceState ? 'replace' : 'push';\n      // Non-urgent: the navigation re-renders every `useSearchParams()` subscriber\n      // on the page, which is a lot of work for a query param nobody is waiting\n      // on. In a transition it yields to the click feedback instead of competing\n      // with it.\n      startTransition(() => {\n        // `scroll: false` because only a query param moves here. The App Router\n        // otherwise scrolls to the top on every navigation, which throws away the\n        // reading position of anyone using a tab strip partway down a page.\n        router[method](`${nav.pathname}?${params.toString()}`, { scroll: false });\n      });\n\n      nav.controlledOnTabChange?.(tabId);\n    },\n    [router],\n  );\n\n  // The tab BAR follows the click immediately; the tab BODY is deferred, which\n  // makes React treat swapping it as a transition. That is the whole fix for the\n  // flash: a tab whose data is not in the client cache yet SUSPENDS the moment it\n  // mounts, and outside a transition React has to show that Suspense fallback —\n  // so the skeleton appears for a frame before the real content lands. Inside a\n  // transition React keeps the previous tab on screen and swaps only once the new\n  // one is ready. Deferring the body rather than the whole state keeps the\n  // underline responsive, so the click still feels instant.\n  //\n  // Two things this asks of the consumer, both worth knowing:\n  //  - The render prop runs on BOTH passes (once with the old id, once with the\n  //    new). Put anything expensive behind `memo` so the first pass is free.\n  //  - A SLOW tab leaves the previous tab's content under the new tab's\n  //    underline with nothing saying so — worse than a skeleton, because stale\n  //    data reads as fresh data. `isStale` is handed to the render prop so the\n  //    body can mark itself while it waits.\n  const deferredActiveTab = useDeferredValue(activeTab);\n  const isStale = activeTab !== deferredActiveTab;\n\n  return (\n    <>\n      <TabBar\n        tabs={tabs}\n        activeTab={activeTab}\n        onTabChange={handleTabChange}\n        className={className}\n        shadowClassName={shadowClassName}\n        showLeftGradient={showLeftGradient}\n        showRightGradient={showRightGradient}\n        stretchTabs={stretchTabs}\n      />\n\n      {/* Render children with active tab if provided */}\n      {children && children(deferredActiveTab, { isStale })}\n    </>\n  );\n}\n\n// Utility function to get tab by id\nexport const getTabById = (tabs: TabItem[], tabId: string): TabItem | undefined => tabs.find(tab => tab.id === tabId);\n\n// Utility function to get tab component\nexport const getTabComponent = (tabs: TabItem[], tabId: string): TabItem['component'] | null => {\n  const tab = getTabById(tabs, tabId);\n  return tab?.component || null;\n};\n","'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { type HTMLAttributes, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst alertVariants = cva(\n  'relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',\n  {\n    variants: {\n      variant: {\n        default: 'bg-background text-foreground',\n        destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',\n        warning: 'border-transparent bg-ods-warning-secondary text-ods-warning [&>svg]:text-ods-warning',\n        error: 'border-transparent bg-ods-error-secondary text-ods-error [&>svg]:text-ods-error',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n);\n\nconst Alert = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>>(\n  ({ className, variant, ...props }, ref) => (\n    <div ref={ref} role=\"alert\" className={cn(alertVariants({ variant }), className)} {...props} />\n  ),\n);\nAlert.displayName = 'Alert';\n\nconst AlertTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(\n  ({ className, ...props }, ref) => (\n    <h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} />\n  ),\n);\nAlertTitle.displayName = 'AlertTitle';\n\nconst AlertDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(\n  ({ className, ...props }, ref) => (\n    <div ref={ref} className={cn('text-h6 [&_p]:leading-relaxed', className)} {...props} />\n  ),\n);\nAlertDescription.displayName = 'AlertDescription';\n\nexport { Alert, AlertTitle, AlertDescription };\n","import { Sparkles } from 'lucide-react';\nimport { Badge } from './badge';\n\n/**\n * Canonical \"AI Generated\" chip (Sparkles + label). Single source of truth for\n * every AI-generated indicator across the app — replaces the byte-identical\n * inline `<Badge variant=\"secondary\"><Sparkles/> AI Generated</Badge>` that was\n * copy-pasted across the summary / video / SEO editors.\n */\nexport function AIGeneratedBadge({ className }: { className?: string }) {\n  return (\n    <Badge variant=\"secondary\" className={`flex items-center gap-1${className ? ` ${className}` : ''}`}>\n      <Sparkles className=\"h-3 w-3\" />\n      AI Generated\n    </Badge>\n  );\n}\n","'use client';\n\nimport { ChevronDown } from 'lucide-react';\nimport type React from 'react';\nimport { useState, useRef } from 'react';\nimport { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';\n\n/**\n * FadePreview — the single shared progressive-disclosure primitive for\n * long lists/sections: renders children inside a height-clamped wrapper\n * whose bottom fades out via a CSS mask, with a \"Show N more / Show less\"\n * toggle below.\n *\n * Extracted from `ReleaseChangelogSection`'s `previewFirst` mode (which\n * itself unified the investor-update detail page's duplicated\n * `FadedHighlightSection`). Every fade-preview surface must compose THIS\n * component — do not re-inline the mask/clamp/toggle trio.\n *\n * When `hiddenCount <= 0` the children render unclamped and no toggle\n * shows (single-entry sections need no disclosure).\n */\nexport interface FadePreviewProps {\n  /** How many items are visually hidden while collapsed — drives the\n   *  \"Show N more\" label. Pass `total - visible`; `<= 0` disables the\n   *  clamp + fade + toggle entirely. */\n  hiddenCount: number;\n  /** Collapsed height in px. ~120 shows one changelog entry's title +\n   *  the start of its description before the mask kicks in. Callers with\n   *  taller rows (e.g. delivery tables) pass a larger value. */\n  collapsedHeight?: number;\n  /** Reset the expanded state when this value changes — otherwise a\n   *  parent that refetches and shrinks the list would leave a stale\n   *  \"expanded\" state and a momentarily-wrong \"Show 0 more\" button.\n   *  Callers typically pass the item count. */\n  resetKey?: unknown;\n  children: React.ReactNode;\n}\n\nexport function FadePreview({ hiddenCount, collapsedHeight = 120, resetKey, children }: FadePreviewProps) {\n  const [expanded, setExpanded] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  // Collapse when the caller's `resetKey` changes. Adjusted while rendering —\n  // React's documented pattern for a prop-driven reset — rather than from an\n  // effect: the layout effect below sets `max-height` from `expanded` on the\n  // same commit, so resetting a commit late made the panel snap open at the new\n  // list's full height and then collapse again.\n  const [collapsedFor, setCollapsedFor] = useState(resetKey);\n  if (collapsedFor !== resetKey) {\n    setCollapsedFor(resetKey);\n    setExpanded(false);\n  }\n\n  // The expanded height is a DOM MEASUREMENT, so it is applied to the node in\n  // a layout effect rather than read during render. Reading `scrollHeight` in\n  // the render body measured whatever the previous commit had laid out and\n  // then never looked again: children that grew after expanding (an image\n  // finishing load, a lazily rendered row) stayed clipped at the old height\n  // with no re-render able to correct it, and the `?? 2000` fallback silently\n  // capped anything taller on the very first expand. Unconditional, so every\n  // commit re-measures, and it runs before paint so the height transition\n  // still animates from the collapsed value.\n  useIsomorphicLayoutEffect(() => {\n    const el = contentRef.current;\n    if (!el) return;\n    el.style.maxHeight = expanded ? `${el.scrollHeight}px` : `${collapsedHeight}px`;\n  });\n\n  const needsFade = hiddenCount > 0;\n\n  // No disclosure needed → no clamp wrapper at all. (Keeping the wrapper\n  // with a `scrollHeight ?? 2000` max-height would clip tall content on\n  // the first render, before the ref measures.)\n  if (!needsFade) return <>{children}</>;\n\n  return (\n    <div className=\"relative\">\n      <div\n        ref={contentRef}\n        className=\"overflow-hidden transition-[max-height] duration-500\"\n        style={{\n          transitionTimingFunction: 'cubic-bezier(0.33, 1, 0.68, 1)',\n          // `maxHeight` is intentionally absent — the layout effect above owns\n          // it, because the expanded value is a live DOM measurement.\n          ...(!expanded\n            ? {\n                maskImage: 'linear-gradient(to bottom, black 30%, transparent 100%)',\n                WebkitMaskImage: 'linear-gradient(to bottom, black 30%, transparent 100%)',\n              }\n            : {}),\n        }}\n      >\n        {children}\n      </div>\n      <button\n        type=\"button\"\n        onClick={() => setExpanded(!expanded)}\n        className=\"mt-4 flex items-center gap-1.5 text-ods-text-secondary transition-colors duration-200 text-h6 hover:text-ods-accent\"\n      >\n        <span>{expanded ? 'Show less' : `Show ${hiddenCount} more`}</span>\n        <ChevronDown className={`h-3.5 w-3.5 transition-transform duration-300 ${expanded ? 'rotate-180' : ''}`} />\n      </button>\n    </div>\n  );\n}\n","'use client';\n\nimport * as ProgressPrimitive from '@radix-ui/react-progress';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';\n\nimport { cn } from '../../utils/cn';\n\nconst Progress = forwardRef<\n  ElementRef<typeof ProgressPrimitive.Root>,\n  ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {\n    indicatorClassName?: string;\n  }\n>(({ className, value, indicatorClassName, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className={cn('h-full w-full flex-1 bg-primary transition-all', indicatorClassName)}\n      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nexport { Progress };\n","'use client';\n\nimport { ChevronDown } from 'lucide-react';\nimport type React from 'react';\nimport { useState } from 'react';\nimport type { ChangelogEntry } from '../../types/product-release';\nimport { Badge } from './badge';\nimport { FadePreview } from './fade-preview';\nimport { RichMarkdownRenderer } from './markdown';\n\ninterface ReleaseChangelogSectionProps {\n  title: string;\n  entries: ChangelogEntry[];\n  isBreaking?: boolean;\n  hideTitle?: boolean;\n  /** When true, section starts collapsed and can be toggled open/closed\n   *  via a button on the title row. Mutually exclusive with `previewFirst`. */\n  collapsible?: boolean;\n  /** Initial collapsed state (only used when collapsible=true). Defaults to true (collapsed). */\n  defaultCollapsed?: boolean;\n  /** When true, render the first entry in full and fade-mask the rest\n   *  with a \"Show N more / Show less\" toggle below the list. Hides the\n   *  fade + toggle when there's only one entry. Mutually exclusive with\n   *  `collapsible` — when both are passed, `collapsible` wins.\n   *\n   *  This is the same progressive-disclosure pattern used on the\n   *  investor-update detail page's Key Highlights / Financial Notes\n   *  sections (formerly a duplicated `FadedHighlightSection` component\n   *  in the hub — unified here). */\n  previewFirst?: boolean;\n  /** Optional lucide icon rendered inline before the title text. Matches the\n   *  catalog card's changelog-strip icons (Sparkles for Features, Wrench for\n   *  Fixes, TrendingUp for Improvements, AlertTriangle for Breaking) — same\n   *  visual taxonomy across catalog and detail. Inherits the title's color\n   *  (secondary for normal sections, red for breaking). */\n  icon?: React.ReactNode;\n  /** Markdown renderer for each entry's description. Optional — defaults to\n   *  the lib's `RichMarkdownRenderer` so changelog rich-link previews\n   *  (YouTube, OG cards, etc.) work out of the box. Hosts that already\n   *  wrap with a Supabase-aware preset can keep passing their own. */\n  MarkdownRenderer?: React.ComponentType<{ content: string }>;\n}\n\n// Collapsed height for the preview-first mode. ~120px shows the first\n// entry's title + the start of its description before the mask kicks in.\nconst PREVIEW_COLLAPSED_HEIGHT = 120;\n\nexport function ReleaseChangelogSection({\n  title,\n  entries,\n  isBreaking = false,\n  hideTitle = false,\n  collapsible = false,\n  defaultCollapsed = true,\n  previewFirst = false,\n  icon,\n  MarkdownRenderer = RichMarkdownRenderer,\n}: ReleaseChangelogSectionProps) {\n  const [collapsed, setCollapsed] = useState(collapsible ? defaultCollapsed : false);\n\n  if (!entries || entries.length === 0) return null;\n\n  // collapsible wins when both flags are passed (documented in JSDoc).\n  const inPreviewMode = previewFirst && !collapsible;\n  const showEntries = !collapsible || !collapsed;\n\n  return (\n    <div className=\"space-y-4\">\n      {!hideTitle &&\n        (collapsible ? (\n          <button\n            type=\"button\"\n            onClick={() => setCollapsed(!collapsed)}\n            className=\"flex w-full cursor-pointer items-center justify-between\"\n          >\n            <h2\n              className={`flex items-center gap-2 text-h2 ${isBreaking ? 'text-ods-error' : 'text-ods-text-primary'}`}\n            >\n              {icon}\n              {title}\n              <Badge variant=\"secondary\" className=\"ml-2\">\n                {entries.length}\n              </Badge>\n            </h2>\n            <ChevronDown\n              className={`h-4 w-4 shrink-0 text-ods-text-secondary transition-transform duration-200 ${\n                !collapsed ? 'rotate-180' : ''\n              }`}\n            />\n          </button>\n        ) : (\n          <h2 className={`flex items-center gap-2 text-h2 ${isBreaking ? 'text-ods-error' : 'text-ods-text-primary'}`}>\n            {icon}\n            {title}\n            <Badge variant=\"secondary\" className=\"ml-2\">\n              {entries.length}\n            </Badge>\n          </h2>\n        ))}\n      {showEntries &&\n        (inPreviewMode ? (\n          /* Preview-first mode: the shared `FadePreview` primitive clamps\n             the list to ~one full entry, fade-masks the rest, and renders\n             the \"Show N more / Show less\" toggle. */\n          <FadePreview\n            hiddenCount={entries.length - 1}\n            collapsedHeight={PREVIEW_COLLAPSED_HEIGHT}\n            resetKey={entries.length}\n          >\n            <ChangelogEntryList entries={entries} MarkdownRenderer={MarkdownRenderer} />\n          </FadePreview>\n        ) : (\n          <ChangelogEntryList entries={entries} MarkdownRenderer={MarkdownRenderer} />\n        ))}\n    </div>\n  );\n}\n\n/**\n * Internal list renderer — shared by the default and preview-first\n * branches. Each entry is a bordered-left list item with bold title\n * + markdown-rendered description body.\n */\nfunction ChangelogEntryList({\n  entries,\n  MarkdownRenderer,\n}: {\n  entries: ChangelogEntry[];\n  MarkdownRenderer: React.ComponentType<{ content: string }>;\n}) {\n  return (\n    <ul className=\"space-y-6\">\n      {entries.map((entry, index) => (\n        <li key={index} className=\"ml-0 border-l-2 border-ods-border pl-4\">\n          {/* Entry title — run through the SAME markdown renderer as the\n              description so inline markdown (links like `[label](url)`,\n              emphasis) renders instead of showing as raw text, then pinned\n              back to the `text-h3` body+BOLD scale (per ODS tokens:\n              `--font-h3-weight: var(--font-weight-bold)`, 14/18px responsive)\n              so plain-text titles look exactly as before. `[&_p]:!my-0`\n              strips the renderer's paragraph margins; `mb-2` keeps the gap to\n              the description. */}\n          <div className=\"mb-2 text-ods-text-primary text-h3 [&_p+p]:!mt-2 [&_p]:!my-0 [&_p]:!text-[length:var(--font-size-h3-body)] [&_p]:!font-bold [&_p]:!leading-[var(--font-line-space-h3-body)]\">\n            <MarkdownRenderer content={entry.title} />\n          </div>\n          {entry.description && (\n            /* Entry description — body text matches the main release\n               summary at the SAME 14/18px responsive `text-h4` scale.\n               The `MarkdownRenderer` forces its own `<p>` typography\n               which would override `text-h4` on `lg+` viewports and\n               inflate the changelog body to 20px. The `[&_p]:!` overrides\n               pin every descendant `<p>` back to the h4 responsive tokens\n               so the breakpoints stay aligned with the rest of the page. */\n            <div className=\"text-ods-text-primary text-h4 [&_p]:!text-[length:var(--font-size-h4-body)] [&_p]:!font-medium [&_p]:!leading-[var(--font-line-space-h4-body)]\">\n              <MarkdownRenderer content={entry.description} />\n            </div>\n          )}\n        </li>\n      ))}\n    </ul>\n  );\n}\n","'use client';\n\n/**\n * RichMarkdownRenderer — the content composition over the unified\n * `MarkdownEngine`: RuntimeProvider + shortcodes + embed overrides +\n * fetch-based internal-link resolver + `article` typography.\n *\n * This IS the real component (the old 1223-line standalone implementation\n * was deleted in the unification). Public prop surface is unchanged, so\n * every hub/app call site compiles untouched. Deltas vs the old\n * implementation are the ones reviewed in the unification plan: the\n * sanitize stack + tag-escape pre-pass now apply to content surfaces\n * (`extraAllowedHtmlTags={['video','source']}` keeps authored video\n * working), the engine is memoized, and the dead light-mode branches are\n * gone.\n */\nimport type React from 'react';\nimport { memo, useCallback, useMemo } from 'react';\nimport type { ResolveLinkResult } from '../../../../types/doc-source';\nimport {\n  RichMarkdownRuntimeProvider,\n  useRichMarkdownRuntime,\n  type RichMarkdownRuntime,\n} from '../../../embeds/rich-markdown-runtime';\nimport { MarkdownEngine, NO_BROKEN_LINKS, type MarkdownEngineProps } from '../engine';\nimport { resolveTextSizeConfig } from '../text-size';\nimport { buildRichEmbedOverrides } from './embed-overrides';\nimport { processShortcodes } from './shortcodes';\n\n/** Raw-HTML tags authored content needs beyond the chat-safe baseline. */\nconst RICH_EXTRA_TAGS = ['video', 'source'];\n\n/**\n * `/api/docs/resolve-link` is an untrusted wire boundary: `Response.json()`\n * hands back `any`, and returning it straight through as a `ResolveLinkResult`\n * meant the renderer's link handler trusted fields the endpoint may never have\n * sent. Narrow instead — anything that is not an object, or whose fields are\n * the wrong primitive, degrades to `{ success: false }`, which is the\n * \"leave the link alone\" outcome `base-components` already handles.\n */\nconst toResolveLinkResult = (body: unknown): ResolveLinkResult => {\n  if (typeof body !== 'object' || body === null) {\n    return { success: false };\n  }\n  const fields: Record<string, unknown> = { ...body };\n  const readString = (key: string): string | undefined => (typeof fields[key] === 'string' ? fields[key] : undefined);\n\n  return {\n    success: fields.success === true,\n    resolvedPath: readString('resolvedPath'),\n    type: readString('type'),\n    action: readString('action'),\n    error: readString('error'),\n    message: readString('message'),\n  };\n};\n\n/**\n * The engine props this composition OWNS and therefore does not forward:\n * shortcode preprocessing, the embed component map, the `article` text\n * preset, the fetch link resolver, the rich tag allowlist, and streaming\n * (authored content is never streamed).\n */\ntype RichOwnedEngineProps =\n  | 'extraAllowedHtmlTags'\n  | 'preprocessContent'\n  | 'componentOverrides'\n  | 'textSize'\n  | 'onResolveLink'\n  | 'additionalRemarkPlugins'\n  | 'streaming';\n\n/**\n * Derived from `MarkdownEngineProps` rather than hand-copied. The previous\n * shape restated 7 engine props here and a THIRD time in `InnerProps`, so\n * every engine prop doc/type change had to be mirrored in three places (and\n * a missed mirror type-checks fine while silently dropping the prop).\n */\nexport interface RichMarkdownRendererProps\n  extends Partial<RichMarkdownRuntime>, Omit<MarkdownEngineProps, RichOwnedEngineProps> {\n  /** Source for resolving internal links (default: 'openframe-docs'). Registry id from DOC_SOURCES. */\n  resolveSource?: string;\n  /** Path of the internal link-resolver endpoint. Default '/api/docs/resolve-link'. */\n  resolveLinkEndpointUrl?: string;\n}\n\nconst RichMarkdownInner: React.FC<InnerProps> = ({\n  content,\n  className = '',\n  sectionIds,\n  onInternalLinkClick,\n  brokenLinks,\n  currentPath,\n  resolveSource,\n  resolveLinkEndpointUrl,\n  demoteMarkdownH1ToH2,\n}) => {\n  const { ogScraperUrl } = useRichMarkdownRuntime();\n\n  // The OG link-preview endpoint is `${apiBaseUrl}${ogEndpointPath}` —\n  // split the runtime URL once. Full URLs route through the cross-origin\n  // proxy; path-only values are used as the path with an empty base.\n  const { ogApiBaseUrl, ogEndpointPath } = useMemo(() => {\n    try {\n      const u = new URL(ogScraperUrl);\n      return {\n        ogApiBaseUrl: `${u.protocol}//${u.host}`,\n        ogEndpointPath: u.pathname,\n      };\n    } catch {\n      return { ogApiBaseUrl: '', ogEndpointPath: ogScraperUrl };\n    }\n  }, [ogScraperUrl]);\n\n  // ONE link-resolution code path: the engine's callback seam. This is the\n  // fetch implementation the old Rich renderer had inline in its <a>\n  // handler — the engine's base `a` component now drives it.\n  const onResolveLink = useCallback(\n    async (href: string, path: string): Promise<ResolveLinkResult> => {\n      const response = await fetch(resolveLinkEndpointUrl, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ link: href, currentPath: path, source: resolveSource }),\n      });\n      return toResolveLinkResult(await response.json());\n    },\n    [resolveLinkEndpointUrl, resolveSource],\n  );\n\n  const textSizes = useMemo(() => resolveTextSizeConfig('article'), []);\n\n  const componentOverrides = useMemo(\n    () => buildRichEmbedOverrides({ ogApiBaseUrl, ogEndpointPath, textSizes }),\n    [ogApiBaseUrl, ogEndpointPath, textSizes],\n  );\n\n  return (\n    <MarkdownEngine\n      content={content}\n      className={className}\n      sectionIds={sectionIds}\n      demoteMarkdownH1ToH2={demoteMarkdownH1ToH2}\n      brokenLinks={brokenLinks}\n      currentPath={currentPath}\n      onInternalLinkClick={onInternalLinkClick}\n      onResolveLink={onResolveLink}\n      preprocessContent={processShortcodes}\n      componentOverrides={componentOverrides}\n      textSize=\"article\"\n      extraAllowedHtmlTags={RICH_EXTRA_TAGS}\n    />\n  );\n};\n\nconst RichMarkdownRendererImpl: React.FC<RichMarkdownRendererProps> = ({\n  content,\n  className = '',\n  sectionIds,\n  onInternalLinkClick,\n  brokenLinks = NO_BROKEN_LINKS,\n  currentPath,\n  resolveSource = 'openframe-docs',\n  resolveLinkEndpointUrl = '/api/docs/resolve-link',\n  demoteMarkdownH1ToH2 = false,\n  // Runtime overrides; provider fills the defaults\n  redditProxyUrl,\n  twitterProxyUrl,\n  ogScraperUrl,\n  transformImageSrc,\n}) => {\n  return (\n    <RichMarkdownRuntimeProvider\n      redditProxyUrl={redditProxyUrl}\n      twitterProxyUrl={twitterProxyUrl}\n      ogScraperUrl={ogScraperUrl}\n      transformImageSrc={transformImageSrc}\n    >\n      <RichMarkdownInner\n        content={content}\n        className={className}\n        sectionIds={sectionIds}\n        onInternalLinkClick={onInternalLinkClick}\n        brokenLinks={brokenLinks}\n        currentPath={currentPath}\n        resolveSource={resolveSource}\n        resolveLinkEndpointUrl={resolveLinkEndpointUrl}\n        demoteMarkdownH1ToH2={demoteMarkdownH1ToH2}\n      />\n    </RichMarkdownRuntimeProvider>\n  );\n};\n\n/**\n * \"The outer component already applied these defaults\", ENCODED IN THE TYPE:\n * the runtime props are consumed by the provider (so they are gone here),\n * and the three defaulted props are `Required` — a missed default upstream\n * is a type error rather than a silent `undefined`.\n */\ntype InnerProps = Omit<RichMarkdownRendererProps, keyof RichMarkdownRuntime> &\n  Required<Pick<RichMarkdownRendererProps, 'resolveSource' | 'resolveLinkEndpointUrl' | 'demoteMarkdownH1ToH2'>>;\n\n/** Memoized — see the engine's memo rationale. */\nexport const RichMarkdownRenderer = memo(RichMarkdownRendererImpl);\n","'use client';\n\nimport { createContext, useContext, useMemo, type ReactNode } from 'react';\n\n/**\n * Runtime knobs threaded from `<RichMarkdownRenderer>` down to its\n * satellite embed clients (reddit / twitter / og link preview / markdown\n * image). Lives in its own micro-context — distinct from the\n * `ChatRuntimeContext` used by the lib's chat module — because the\n * markdown renderer is mounted from documentation pages (blog, legal,\n * data room, knowledge base) that do NOT have a chat runtime in scope,\n * and the satellites need just three knobs:\n *\n *   - WHERE to fetch the reddit / twitter / OG-scrape proxy\n *   - HOW to transform a markdown image URL (the hub injects its\n *     Supabase image transformer; embedders pass null / identity)\n *\n * Defaults match the hub's existing endpoints so passing\n * `<RichMarkdownRenderer>` no props at all still works end-to-end on\n * the hub. Embedders override per-prop as needed.\n */\nexport interface RichMarkdownRuntime {\n  redditProxyUrl: string;\n  twitterProxyUrl: string;\n  ogScraperUrl: string;\n  /** Hub-only Supabase image transformer. Returning null means \"don't\n   *  rewrite — use the src as-is\". Defaults to identity (no rewrite). */\n  transformImageSrc: (\n    src: string,\n    opts?: { width?: number; quality?: number; resize?: 'cover' | 'contain' | 'fill' },\n  ) => string | null;\n}\n\nconst DEFAULT_RUNTIME: RichMarkdownRuntime = {\n  redditProxyUrl: '/api/blog/reddit-proxy',\n  twitterProxyUrl: '/api/blog/twitter-proxy',\n  ogScraperUrl: '/api/og-scraper',\n  transformImageSrc: () => null,\n};\n\nconst RichMarkdownRuntimeContext = createContext<RichMarkdownRuntime>(DEFAULT_RUNTIME);\n\n/**\n * Provider that fills in defaults for any prop the caller didn't pass.\n * Memoizes the resolved runtime so satellites don't re-render when an\n * unrelated parent state ticks.\n */\nexport function RichMarkdownRuntimeProvider({\n  redditProxyUrl,\n  twitterProxyUrl,\n  ogScraperUrl,\n  transformImageSrc,\n  children,\n}: Partial<RichMarkdownRuntime> & { children: ReactNode }) {\n  const value = useMemo<RichMarkdownRuntime>(\n    () => ({\n      redditProxyUrl: redditProxyUrl ?? DEFAULT_RUNTIME.redditProxyUrl,\n      twitterProxyUrl: twitterProxyUrl ?? DEFAULT_RUNTIME.twitterProxyUrl,\n      ogScraperUrl: ogScraperUrl ?? DEFAULT_RUNTIME.ogScraperUrl,\n      transformImageSrc: transformImageSrc ?? DEFAULT_RUNTIME.transformImageSrc,\n    }),\n    [redditProxyUrl, twitterProxyUrl, ogScraperUrl, transformImageSrc],\n  );\n  return <RichMarkdownRuntimeContext.Provider value={value}>{children}</RichMarkdownRuntimeContext.Provider>;\n}\n\n/**\n * Read the ambient runtime. Returns the defaults when called outside any\n * `RichMarkdownRuntimeProvider` — so embedders that drop a satellite into\n * a non-renderer context (e.g. a release page calling\n * `<RedditEmbedClient>` directly) get the hub-matching defaults for free.\n */\nexport function useRichMarkdownRuntime(): RichMarkdownRuntime {\n  return useContext(RichMarkdownRuntimeContext);\n}\n","'use client';\n\nimport { ExternalLink } from 'lucide-react';\nimport { useClaudeMirrorSrc } from '../../hooks/use-claude-mirror-src';\nimport { toClaudeEmbedUrl } from '../../utils/embed-url-converters';\nimport { ClaudeIcon } from '../icons/claude-icon';\nimport { Button } from '../ui/button/button';\nimport { EmbedViewerFrame } from './embed-viewer-frame';\n\nexport type ClaudeEmbedKind = 'artifact' | 'design';\n\nexport interface ClaudeEmbedProps {\n  /** Any claude.ai / claude.site url. */\n  url: string;\n  /** Which surface the link points at — only the heading differs. */\n  kind?: ClaudeEmbedKind;\n  /** The author's name for this link, when they gave one. */\n  title?: string;\n  /** iframe height (CSS value), matching `FigmaEmbed`'s prop. */\n  height?: string;\n  /** iframe loading strategy. Defaults to `lazy`, as `FigmaEmbed` does. */\n  loading?: 'eager' | 'lazy';\n}\n\nconst KIND_HEADING: Record<ClaudeEmbedKind, string> = {\n  artifact: 'Claude Artifact',\n  design: 'Claude Design',\n};\n\n/**\n * A Claude artifact, in the SAME chrome as every other embed viewer\n * (`EmbedViewerFrame` — what `FigmaEmbed` renders): icon, heading, an\n * \"Open in Claude\" action, and the frame below. Reached through the\n * `{{claude-artifact:URL}}` / `{{claude-design:URL}}` shortcodes, so it looks\n * identical wherever markdown renders — a spec body, a comment, a links rail.\n *\n * FRAME SOURCE — fully under the hood, in priority order:\n *   1. The host's SELF-HOSTED MIRROR: the url's artifact id derives the\n *      storage-view proxy path (`toClaudeMirrorPath`), probed with a\n *      1-byte ranged fetch. When the host has ingested a copy, the frame\n *      shows it — claude.ai frame-locks CODE artifacts entirely, so this\n *      is the only way those ever render inline.\n *   2. claude.ai's own `/embed` route for a PUBLISHED artifact\n *      (`toClaudeEmbedUrl`).\n *   3. Neither → the shared frame's empty state.\n * The consumer passes only the claude URL; \"Open in Claude\" always\n * targets it, so commenting stays on claude.ai.\n *\n * What defeats the claude.ai leg: a Claude CODE url (no Embed settings\n * exist for it, and its `/embed` path answers `frame-ancestors 'self'`),\n * a Claude Design url (no embed route), an artifact whose author has not\n * allow-listed this host, or an http host (mixed content).\n */\nexport function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = 'lazy' }: ClaudeEmbedProps) {\n  // Mirror detection lives in `useClaudeMirrorSrc` (derive from\n  // EndpointsRuntime + 1-byte ranged probe + background revalidation) — this\n  // component stays presentational.\n  const { src: mirrorSrc, status } = useClaudeMirrorSrc(url);\n  // While the probe is in flight (on a cold view that IS the ~1-2s\n  // self-heal), show the shell's loading state rather than the claude.ai\n  // fallback — a code artifact's fallback is the empty \"no embeddable view\"\n  // box, and flashing it before the mirror resolves reads as broken.\n  const isProbing = status === 'probing';\n  // Found → the mirror; otherwise the claude.ai embed (or its empty state).\n  // Suppressed while probing so the loading skeleton owns the body.\n  const embedUrl = isProbing ? null : (mirrorSrc ?? toClaudeEmbedUrl(url));\n  return (\n    <EmbedViewerFrame\n      className=\"my-6 space-y-3\"\n      icon={<ClaudeIcon className=\"h-5 w-5 shrink-0\" />}\n      title={title?.trim() || KIND_HEADING[kind]}\n      titleVariant=\"h6\"\n      isLoading={isProbing}\n      actions={\n        <Button\n          variant=\"outline\"\n          size=\"small-legacy\"\n          href={url}\n          openInNewTab\n          leftIcon={<ClaudeIcon className=\"h-4 w-4\" />}\n          rightIcon={<ExternalLink className=\"h-4 w-4\" />}\n          className=\"w-full sm:w-auto\"\n        >\n          Open in Claude\n        </Button>\n      }\n      src={embedUrl}\n      // The framed artifact has no internal fullscreen button of its own\n      // (unlike Figma's player) — the shell provides the toggle for both\n      // the claude.ai `/embed` route and a mirror.\n      fullscreenControl\n      loading={loading}\n      height={height}\n      // Same frame contract Figma gets — an artifact is a real app: it copies\n      // to the clipboard and can go fullscreen.\n      allow=\"clipboard-write; clipboard-read; fullscreen\"\n      // …but UNLIKE Figma, an artifact is USER-AUTHORED HTML and JS, so it is\n      // sandboxed. Omitting `allow-top-navigation` is the point: an artifact\n      // cannot navigate the page it is embedded in. The `allow-same-origin`\n      // token is CONDITIONAL on where the frame points:\n      //   - claude.ai `/embed` (no mirror): Anthropic's own embed snippet\n      //     uses it, and on a CROSS-ORIGIN frame it grants the frame\n      //     claude.ai's origin — never ours. The artifact runtime needs\n      //     it (storage, postMessage handshake).\n      //   - the derived MIRROR is served from the HOST's OWN origin —\n      //     `allow-same-origin` + `allow-scripts` there would be a no-op\n      //     sandbox handing the artifact first-party cookies and\n      //     `window.parent`. A static mirror renders fine from an opaque\n      //     origin, so the token is dropped.\n      sandbox={\n        mirrorSrc\n          ? 'allow-scripts allow-popups allow-forms'\n          : 'allow-scripts allow-same-origin allow-popups allow-forms'\n      }\n      allowFullScreen\n      emptyIcon={<ClaudeIcon className=\"mb-4 h-16 w-16\" />}\n      emptyMessage=\"Open this one in Claude · it has no embeddable view\"\n    />\n  );\n}\n","'use client';\n\nimport { Maximize2, Minimize2 } from 'lucide-react';\nimport type React from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { FullscreenSwitchController } from '../../utils/fullscreen-switch';\nimport { Button } from '../ui/button/button';\nimport { EmbedIframe, EmbedLoadingSkeleton } from './embed-iframe';\n\n/**\n * EmbedViewerFrame — the ONE viewer shell shared by every embed viewer\n * (Google Sheets / PDF / Figma): header row (icon + title + caller-supplied\n * actions) over an `EmbedIframe`, with an inline empty state when there is no\n * `src`.\n *\n * Extraction rules (output parity with the three pre-existing viewers is the\n * merge gate — every knob below exists because the shells were NOT byte-\n * identical):\n *   - `titleVariant`: sheets/pdf render `h2.text-h3`; figma renders\n *     `span.text-h6 font-semibold`.\n *   - `actions` is rendered AS-IS in the header row — each viewer owns its\n *     action container (pdf wraps two buttons, figma wraps a stateful\n *     ToggleGroup + button). State lives in the CALLER (figma's present/\n *     browse toggle keeps its `useState` in `FigmaEmbed`), so the slot is a\n *     plain ReactNode and re-renders flow through normally.\n *   - `className` overrides the wrapper (figma uses `my-6 space-y-3`).\n *   - Empty body (`src` falsy) keeps the header visible (figma's behavior).\n *     Viewers that historically rendered a standalone empty state WITHOUT a\n *     header (sheets/pdf with no URL at all) early-return before mounting\n *     the frame.\n */\nexport interface EmbedViewerFrameProps {\n  /** Header icon, already sized by the caller (the viewers pass `w-5 h-5`). */\n  icon: React.ReactNode;\n  title: string;\n  /** `h3` = `h2.text-h3` (sheets/pdf, default); `h6` = `span.text-h6 font-semibold` (figma). */\n  titleVariant?: 'h3' | 'h6';\n  /** Rendered verbatim in the header row — the caller owns its own container/state. */\n  actions?: React.ReactNode;\n  /** Iframe source; falsy renders the inline empty state (or the loading\n   *  state when `isLoading`) under the header. */\n  src: string | null | undefined;\n  /** While true AND there is no `src` yet, the body shows the shared embed\n   *  loading skeleton (`EmbedLoadingSkeleton`) instead of the empty state —\n   *  for a viewer that is still resolving where its frame points (e.g.\n   *  ClaudeEmbed probing for a self-hosted mirror), so a cold first view\n   *  reads as \"loading\" (identical to every other embed), not \"nothing here\". */\n  isLoading?: boolean;\n  /** Large icon for the inline empty state (the viewers pass `w-16 h-16`). */\n  emptyIcon?: React.ReactNode;\n  emptyMessage?: string;\n  height?: string;\n  allow?: string;\n  allowFullScreen?: boolean;\n  loading?: 'eager' | 'lazy';\n  /** iframe `sandbox` — see `EmbedIframe`. Set it for user-authored contents. */\n  sandbox?: string;\n  /** Wrapper classes; defaults to the sheets/pdf `space-y-4`. */\n  className?: string;\n  /** Opt-in fullscreen toggle in the header row (rendered after `actions`,\n   *  only when there is a `src`). Fullscreens the WHOLE frame — header\n   *  included — so the exit control stays reachable (Esc works too).\n   *  Exists because some embedded contents ship their own fullscreen\n   *  button (the Figma player) and some cannot (a self-hosted Claude\n   *  artifact mirror) — the shell provides the affordance uniformly. */\n  fullscreenControl?: boolean;\n}\n\nexport function EmbedViewerFrame({\n  icon,\n  title,\n  titleVariant = 'h3',\n  actions,\n  src,\n  isLoading,\n  emptyIcon,\n  emptyMessage = 'Content not available',\n  height,\n  allow,\n  allowFullScreen,\n  loading,\n  sandbox,\n  className,\n  fullscreenControl,\n}: EmbedViewerFrameProps) {\n  const frameRef = useRef<HTMLDivElement>(null);\n  const [isFullscreen, setIsFullscreen] = useState(false);\n\n  // ONE fullscreen implementation across the library and its hosts:\n  // `FullscreenSwitchController` (shared with the hub's deck) owns the\n  // Fullscreen API calls, webkit fallbacks, rejection safety, and the\n  // change-event-driven state — Esc, F11, buttons and browser chrome all\n  // converge on its listener, so the icon can never desync. Here it runs\n  // element-level (`target` = this frame; no mask classes — the frame\n  // styles off its own state).\n  const controllerRef = useRef<FullscreenSwitchController | null>(null);\n  useEffect(() => {\n    if (!fullscreenControl) return undefined;\n    const controller = new FullscreenSwitchController({\n      target: () => frameRef.current,\n      onFullscreenChange: setIsFullscreen,\n    });\n    controllerRef.current = controller;\n    controller.attach();\n    return () => {\n      controllerRef.current = null;\n      controller.detach();\n    };\n  }, [fullscreenControl]);\n  const toggleFullscreen = () => controllerRef.current?.toggle();\n\n  return (\n    <div\n      ref={frameRef}\n      className={cn(\n        'space-y-[var(--spacing-system-mf)]',\n        className,\n        // Fullscreen paints its own ground (the fullscreened element\n        // otherwise sits on the UA's black backdrop) and scrolls itself.\n        isFullscreen && 'overflow-auto bg-ods-bg p-[var(--spacing-system-mf)]',\n      )}\n    >\n      <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          {icon}\n          {titleVariant === 'h3' ? (\n            <h2 className=\"truncate text-ods-text-primary text-h3\">{title}</h2>\n          ) : (\n            <span className=\"truncate font-semibold text-ods-text-primary text-h6\">{title}</span>\n          )}\n        </div>\n        {/* Same responsive action-row idiom as the PDF viewer's button pair\n            and every viewer's own action: full-width stacked on mobile,\n            inline row from `sm:` up, SAME Button idiom (outline /\n            small-legacy / w-4 icon / w-full sm:w-auto). Without the\n            fullscreen toggle, `actions` renders EXACTLY as before — the\n            wrapper exists only when the toggle joins it, so pre-existing\n            viewers (figma/pdf/sheets) are byte-identical. */}\n        {fullscreenControl && src ? (\n          <div className=\"flex flex-col gap-[var(--spacing-system-xsf)] sm:flex-row sm:items-center\">\n            {actions}\n            <Button\n              variant=\"outline\"\n              size=\"small-legacy\"\n              onClick={toggleFullscreen}\n              aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n              leftIcon={isFullscreen ? <Minimize2 className=\"h-4 w-4\" /> : <Maximize2 className=\"h-4 w-4\" />}\n              className=\"w-full sm:w-auto\"\n            >\n              {isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}\n            </Button>\n          </div>\n        ) : (\n          actions\n        )}\n      </div>\n      {src ? (\n        <EmbedIframe\n          src={src}\n          title={title}\n          // 96px = the fullscreen wrapper's own chrome above the iframe:\n          // header row (~40px incl. its gap) + the wrapper's mf padding\n          // (16px × 2) + the mf space-y gap (16px), rounded up so the\n          // frame never forces a scrollbar. Recheck if that chrome changes.\n          height={isFullscreen ? 'calc(100vh - 96px)' : height}\n          allow={allow}\n          allowFullScreen={allowFullScreen}\n          loading={loading}\n          sandbox={sandbox}\n        />\n      ) : isLoading ? (\n        // The EXACT skeleton every embed shows while its iframe loads\n        // (`EmbedIframe`'s `EmbedLoadingSkeleton`) — so a viewer still\n        // RESOLVING where its frame points (ClaudeEmbed probing for a mirror,\n        // before it has a `src`) reads 1:1 with the Figma/Sheets/PDF loading\n        // state, then settles into the iframe in place.\n        <EmbedLoadingSkeleton height={height} />\n      ) : (\n        // The SAME box the iframe would have filled: same height, same rounded\n        // border. A viewer whose content is missing must not collapse the page\n        // around it — two embeds side by side (a figma frame and a claude link\n        // that has no embeddable view) have to read as two equal blocks.\n        // Without a `height` the caller never asked for a fixed box, so the\n        // intrinsic `py-16` stands.\n        <div\n          className=\"flex flex-col items-center justify-center rounded-lg border border-ods-border px-4 py-16 text-center\"\n          style={height ? { height } : undefined}\n        >\n          {emptyIcon}\n          <p className=\"text-ods-text-secondary\">{emptyMessage}</p>\n        </div>\n      )}\n    </div>\n  );\n}\n","/**\n * FullscreenSwitchController — the ONE place fullscreen enter/exit lives.\n *\n * Framework-agnostic (no React): the consumer passes callbacks (and, for\n * page-level surfaces, the <html> mask classes); the controller owns the\n * Fullscreen API calls, webkit fallbacks, and the flicker-free switch\n * choreography. Originally extracted from the hub's company-hub deck (PR\n * #737); moved into the lib so EVERY fullscreen surface — the deck, the\n * embed viewers — shares one implementation instead of re-deriving the\n * event-ordering lessons below. The hub's `lib/utils/fullscreen-switch.ts`\n * re-exports from here (extractItems shim precedent).\n *\n * Two shapes of consumer:\n *   - PAGE-LEVEL (the deck): no `target` — fullscreens\n *     `document.documentElement`, uses the mask classes + geometry pulses.\n *   - ELEMENT-LEVEL (embed viewers): pass `target` — fullscreens one\n *     element; classes/pulses are optional and simply skipped when absent.\n *\n * Invariants (each guards a bug found while building the deck's fullscreen):\n * - Fullscreen STATE follows the fullscreenchange event exclusively (never\n *   set optimistically): Esc, keyboard shortcuts, buttons and browser\n *   chrome all converge on one listener.\n * - The switching mask (when configured) goes up PRE-EMPTIVELY:\n *   Chrome/Safari PAINT resized frames BEFORE fullscreenchange fires\n *   (whatwg/fullscreen#74), so masking inside the change event is already\n *   too late. `toggle()` masks before requesting; an Escape keydown\n *   listener (capture) masks the one exit path that skips toggle(); the\n *   change event remains the fallback for browser-chrome-initiated\n *   transitions.\n * - The swap is a multi-frame viewport animation (macOS especially): one\n *   rAF re-anchor is NOT enough. While masked, every resize fires\n *   onGeometryPulse (re-measure + re-anchor under a constant scrollY);\n *   quiet resizes for `quietMs` (or the `capMs` hard cap — a rejected\n *   fullscreen request after a pre-emptive mask must never wedge the mask\n *   up) end the switch.\n */\n\n/** Webkit-prefixed Fullscreen API fallbacks (Safari < 16.4 desktop, older\n *  iPadOS; iOS 26+ ships the standard API — isSupported() gates elsewhere). */\ntype FsDocument = Document & {\n  webkitFullscreenElement?: Element | null;\n  webkitFullscreenEnabled?: boolean;\n  webkitExitFullscreen?: () => void;\n};\ntype FsElement = HTMLElement & { webkitRequestFullscreen?: () => void };\n\nconst fullscreenElementOf = (doc: FsDocument) => doc.fullscreenElement ?? doc.webkitFullscreenElement ?? null;\n\nexport interface FullscreenSwitchOptions {\n  /** The element to fullscreen. Omit for page-level surfaces (the deck):\n   *  `document.documentElement` — window scrolling survives, which\n   *  element-level fullscreen would freeze. A callback so refs resolve at\n   *  toggle time, not construction time. */\n  target?: () => HTMLElement | null;\n  /** Held on <html> while fullscreen is ACTIVE (steady-state styling).\n   *  Optional — element-level consumers style off their own state. */\n  activeClass?: string;\n  /** Held on <html> for the DURATION of an enter/exit swap (the\n   *  mask/curtain). Optional — see `activeClass`. */\n  switchingClass?: string;\n  /** New fullscreen state, straight from the change event. For a `target`\n   *  consumer this is \"MY element is the fullscreen element\". */\n  onFullscreenChange: (isFullscreen: boolean) => void;\n  /** Geometry moved under a constant scrollY — re-measure + re-anchor.\n   *  Optional; fired once per resize during the swap and once more when\n   *  the switch ends. */\n  onGeometryPulse?: () => void;\n  /** Switch ends after resizes have been quiet this long (default 200ms). */\n  quietMs?: number;\n  /** Hard cap on a switch, mask-wedge failsafe (default 1000ms). */\n  capMs?: number;\n}\n\nconst DEFAULT_QUIET_MS = 200;\nconst DEFAULT_CAP_MS = 1000;\n\nexport class FullscreenSwitchController {\n  private quietTimer = 0;\n  private capTimer = 0;\n  private attached = false;\n\n  constructor(private readonly opts: FullscreenSwitchOptions) {}\n\n  /** SSR-safe feature detection — call post-mount to gate the affordance. */\n  static isSupported(): boolean {\n    if (typeof document === 'undefined') return false;\n    const doc = document as FsDocument;\n    const el = document.documentElement as FsElement;\n    return (\n      Boolean(el.requestFullscreen || el.webkitRequestFullscreen) &&\n      (doc.fullscreenEnabled ?? doc.webkitFullscreenEnabled ?? true) !== false\n    );\n  }\n\n  get isFullscreen(): boolean {\n    const fs = fullscreenElementOf(document);\n    const target = this.opts.target?.();\n    return target ? fs === target : Boolean(fs);\n  }\n\n  attach(): void {\n    if (this.attached) return;\n    this.attached = true;\n    const doc = document as FsDocument;\n    doc.addEventListener('fullscreenchange', this.onChange);\n    doc.addEventListener('webkitfullscreenchange', this.onChange);\n    window.addEventListener('keydown', this.onEscape, true);\n  }\n\n  detach(): void {\n    if (!this.attached) return;\n    this.attached = false;\n    const doc = document as FsDocument;\n    doc.removeEventListener('fullscreenchange', this.onChange);\n    doc.removeEventListener('webkitfullscreenchange', this.onChange);\n    window.removeEventListener('keydown', this.onEscape, true);\n    window.removeEventListener('resize', this.onResize);\n    window.clearTimeout(this.quietTimer);\n    window.clearTimeout(this.capTimer);\n    if (this.opts.activeClass) doc.documentElement.classList.remove(this.opts.activeClass);\n    if (this.opts.switchingClass) doc.documentElement.classList.remove(this.opts.switchingClass);\n  }\n\n  /** Mask first (pre-emptive — see header), then request/exit. */\n  toggle = (): void => {\n    const doc = document as FsDocument;\n    this.beginSwitch();\n    if (fullscreenElementOf(doc)) {\n      if (doc.exitFullscreen) doc.exitFullscreen().catch(() => {});\n      else doc.webkitExitFullscreen?.();\n    } else {\n      const el = (this.opts.target?.() ?? document.documentElement) as FsElement;\n      // navigationUI:'hide' = prefer screen space over browser nav chrome.\n      if (el.requestFullscreen) el.requestFullscreen({ navigationUI: 'hide' }).catch(() => {});\n      else el.webkitRequestFullscreen?.();\n    }\n  };\n\n  /** Raise the mask + start tracking. Idempotent — safe from the pre-emptive\n   *  paths (toggle, Escape) AND again from the change event. */\n  private beginSwitch = (): void => {\n    if (this.opts.switchingClass) document.documentElement.classList.add(this.opts.switchingClass);\n    window.addEventListener('resize', this.onResize);\n    window.clearTimeout(this.capTimer);\n    this.capTimer = window.setTimeout(this.endSwitch, this.opts.capMs ?? DEFAULT_CAP_MS);\n  };\n\n  private endSwitch = (): void => {\n    window.clearTimeout(this.quietTimer);\n    window.clearTimeout(this.capTimer);\n    window.removeEventListener('resize', this.onResize);\n    this.opts.onGeometryPulse?.();\n    if (this.opts.switchingClass) document.documentElement.classList.remove(this.opts.switchingClass);\n  };\n\n  private armQuiet = (): void => {\n    window.clearTimeout(this.quietTimer);\n    this.quietTimer = window.setTimeout(this.endSwitch, this.opts.quietMs ?? DEFAULT_QUIET_MS);\n  };\n\n  private onResize = (): void => {\n    this.opts.onGeometryPulse?.();\n    this.armQuiet();\n  };\n\n  /** Escape is the one exit path that skips toggle() — pre-mask on the\n   *  keydown (capture phase), before the browser begins the swap. */\n  private onEscape = (e: KeyboardEvent): void => {\n    if (e.key === 'Escape' && this.isFullscreen) this.beginSwitch();\n  };\n\n  private onChange = (): void => {\n    const fs = this.isFullscreen;\n    if (this.opts.activeClass) document.documentElement.classList.toggle(this.opts.activeClass, fs);\n    this.beginSwitch();\n    requestAnimationFrame(() => this.opts.onGeometryPulse?.());\n    this.armQuiet();\n    this.opts.onFullscreenChange(fs);\n  };\n}\n","'use client';\n\nimport type React from 'react';\nimport { useState, useCallback, useRef, useEffect } from 'react';\n\n/** Loading skeleton for iframe embeds — clean iframe-sized rectangle.\n *  Uses `bg-ods-card` (visible token) rather than `bg-ods-skeleton`\n *  which resolves to transparent in this build (the same gotcha\n *  documented in `chat-message-row.tsx`'s skeleton). No fake inner\n *  placeholder cruft — a real loading iframe shows a blank rectangle.\n *\n *  Exported so a viewer that is still RESOLVING where its frame points\n *  (ClaudeEmbed probing for a self-hosted mirror, before it has a `src`)\n *  can render the IDENTICAL skeleton through `EmbedViewerFrame`, keeping\n *  every embed's loading state 1:1 with the Figma/Sheets/PDF viewers. */\nexport function EmbedLoadingSkeleton({ height }: { height?: string }) {\n  return (\n    <div\n      className=\"w-full animate-pulse overflow-hidden rounded-lg border border-ods-border bg-ods-card\"\n      style={{ height: height || 'calc(100vh - 250px)' }}\n    />\n  );\n}\n\nexport interface EmbedIframeProps {\n  /** The URL to embed */\n  src: string;\n  /** Accessible title for the iframe */\n  title: string;\n  /** Additional class names for the outer container */\n  className?: string;\n  /** Container height (CSS value). Defaults to `calc(100vh - 250px)` */\n  height?: string;\n  /** iframe `allow` attribute */\n  allow?: string;\n  /** iframe `referrerPolicy` attribute */\n  referrerPolicy?: React.IframeHTMLAttributes<HTMLIFrameElement>['referrerPolicy'];\n  /** iframe `loading` attribute */\n  loading?: 'eager' | 'lazy';\n  /** iframe `allowFullScreen` attribute */\n  allowFullScreen?: boolean;\n  /**\n   * iframe `sandbox` attribute. Omitted by default — a first-party vendor\n   * (Figma, Google Sheets) is trusted with the full frame contract.\n   *\n   * Pass it for a frame whose contents are AUTHORED BY USERS: without the\n   * attribute an embedded document may navigate the top window, and a sandbox\n   * that omits `allow-top-navigation` takes that away. On a CROSS-ORIGIN frame\n   * `allow-same-origin` grants the frame its own origin, never the host's.\n   */\n  sandbox?: string;\n}\n\n/**\n * Base iframe wrapper with loading skeleton and proper memory cleanup.\n *\n * Prevents memory leaks by:\n * - Using `key={src}` to force full unmount/remount when src changes\n * - Setting iframe src to about:blank on unmount to release the embedded document\n * - Resetting loaded state when src changes\n */\nexport function EmbedIframe({\n  src,\n  title,\n  className,\n  height,\n  allow,\n  referrerPolicy,\n  loading,\n  allowFullScreen,\n  sandbox,\n}: EmbedIframeProps) {\n  // Which src finished loading, rather than a bare \"loaded\" flag reset from an\n  // effect on every src change. Same information, but the reset is implicit:\n  // a new `src` simply stops matching, so the skeleton is back on the SAME\n  // render that changed the src instead of one commit later — which is exactly\n  // the frame that used to show the previous embed under the new src.\n  const [loadedSrc, setLoadedSrc] = useState<string | null>(null);\n  const isLoaded = loadedSrc === src;\n  const iframeRef = useRef<HTMLIFrameElement>(null);\n  const handleLoad = useCallback(() => setLoadedSrc(src), [src]);\n\n  useEffect(() => {\n    const iframe = iframeRef.current;\n    return () => {\n      if (iframe) {\n        try {\n          iframe.src = 'about:blank';\n        } catch {\n          // Cross-origin iframes may throw — safe to ignore\n        }\n      }\n    };\n  }, [src]);\n\n  const resolvedHeight = height || 'calc(100vh - 250px)';\n\n  return (\n    <>\n      {!isLoaded && <EmbedLoadingSkeleton height={resolvedHeight} />}\n      <div\n        className={`w-full overflow-hidden rounded-lg border border-ods-border ${!isLoaded ? 'h-0 overflow-hidden' : ''} ${className || ''}`}\n        style={isLoaded ? { height: resolvedHeight } : undefined}\n      >\n        <iframe\n          key={src}\n          ref={iframeRef}\n          src={src}\n          className=\"h-full w-full border-0\"\n          title={title}\n          onLoad={handleLoad}\n          allow={allow}\n          referrerPolicy={referrerPolicy}\n          loading={loading}\n          allowFullScreen={allow?.includes('fullscreen') ? undefined : allowFullScreen}\n          sandbox={sandbox}\n        />\n      </div>\n    </>\n  );\n}\n","'use client';\n\nimport { ExternalLink, Play, LayoutGrid } from 'lucide-react';\nimport { useState } from 'react';\nimport { toFigmaEmbedUrl, toFigmaOriginalUrl, isFigmaSlidesUrl } from '../../utils/embed-url-converters';\nimport { FigmaIcon } from '../icons-v2-generated';\nimport { Button, ToggleGroup, ToggleGroupItem } from '../ui';\nimport { EmbedViewerFrame } from './embed-viewer-frame';\n\nexport interface FigmaEmbedProps {\n  /** Any Figma URL (design/file/proto/board/slides/deck) or an already-resolved embed URL. */\n  url: string;\n  /** Heading shown above the embed. Defaults to \"Figma Design\". */\n  title?: string;\n  /**\n   * iframe height (CSS value). The data-room document viewer omits it (full\n   * height, `calc(100vh - 250px)`); inline markdown passes e.g. `\"70vh\"` so the\n   * embed sits naturally inside article content.\n   */\n  height?: string;\n  /** iframe loading strategy. Defaults to `\"lazy\"`; the data-room viewer passes `\"eager\"`. */\n  loading?: 'eager' | 'lazy';\n}\n\ntype SlidesView = 'present' | 'browse';\n\n/**\n * Two-state present/browse toggle for Figma Slides. `present` (default) uses\n * Figma's deck viewer (full-bleed slide + `‹ n/N ›` nav bar + keyboard nav);\n * `browse` uses the thumbnail-rail + zoom viewer.\n */\nfunction SlidesViewToggle({ view, onChange }: { view: SlidesView; onChange: (v: SlidesView) => void }) {\n  const options: { key: SlidesView; label: string; Icon: typeof Play }[] = [\n    { key: 'present', label: 'Present', Icon: Play },\n    { key: 'browse', label: 'Browse', Icon: LayoutGrid },\n  ];\n  return (\n    <ToggleGroup\n      type=\"single\"\n      value={view}\n      onValueChange={(v: string) => {\n        if (v && v !== view) onChange(v as SlidesView);\n      }}\n      aria-label=\"Figma slides view mode\"\n      className=\"flex shrink-0 items-center gap-0.5 rounded-lg border border-ods-border bg-ods-card p-0.5\"\n    >\n      {options.map(({ key, label, Icon }) => {\n        const active = view === key;\n        return (\n          <ToggleGroupItem\n            key={key}\n            value={key}\n            aria-label={label}\n            className={`flex items-center gap-1.5 rounded-md px-2.5 py-1 transition-colors text-h6 ${\n              active\n                ? 'bg-ods-accent text-ods-text-on-accent'\n                : 'text-ods-text-secondary hover:bg-ods-bg-hover hover:text-ods-text-primary'\n            }`}\n          >\n            <Icon className=\"h-4 w-4 shrink-0\" />\n            {label}\n          </ToggleGroupItem>\n        );\n      })}\n    </ToggleGroup>\n  );\n}\n\n/**\n * Single source of truth for every Figma surface — the data-room document viewer\n * and in-article markdown both render this. A header (icon + title + \"Open in\n * Figma\") over an interactive Figma iframe, built from the canonical\n * `toFigmaEmbedUrl` / `toFigmaOriginalUrl` converters + the shared\n * `<EmbedViewerFrame>` (which owns the header/empty-state/iframe shell).\n * Only height/loading differ per surface.\n *\n * For Slides decks, a present/browse toggle (default = present) lets viewers flip\n * slides with Figma's native nav bar + keyboard, or switch to the thumbnail-rail\n * browse view. The toggle's state lives HERE (the frame's `actions` slot is a\n * plain ReactNode) — flipping it recomputes `embedSrc`, and the frame's\n * `EmbedIframe` remounts on the new src exactly as before the extraction.\n */\nexport function FigmaEmbed({ url, title, height, loading = 'lazy' }: FigmaEmbedProps) {\n  const [view, setView] = useState<SlidesView>('present');\n  const isSlides = url ? isFigmaSlidesUrl(url) : false;\n  const embedSrc = url ? toFigmaEmbedUrl(url, { slidesView: view }) : null;\n  const originalUrl = (() => {\n    if (!url) return null;\n    try {\n      const parsed = new URL(toFigmaOriginalUrl(url));\n      const host = parsed.hostname.toLowerCase();\n      const okHost = host === 'figma.com' || host.endsWith('.figma.com');\n      const okProtocol = parsed.protocol === 'https:' || parsed.protocol === 'http:';\n      return okHost && okProtocol ? parsed.toString() : null;\n    } catch {\n      return null;\n    }\n  })();\n  const heading = title || 'Figma Design';\n\n  return (\n    <EmbedViewerFrame\n      className=\"my-6 space-y-3\"\n      icon={<FigmaIcon className=\"h-5 w-5 shrink-0\" />}\n      title={heading}\n      titleVariant=\"h6\"\n      actions={\n        <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n          {isSlides && embedSrc && <SlidesViewToggle view={view} onChange={setView} />}\n          {originalUrl && (\n            <Button\n              variant=\"outline\"\n              size=\"small-legacy\"\n              href={originalUrl}\n              openInNewTab\n              leftIcon={<FigmaIcon className=\"h-4 w-4\" />}\n              rightIcon={<ExternalLink className=\"h-4 w-4\" />}\n              className=\"w-full sm:w-auto\"\n            >\n              Open in Figma\n            </Button>\n          )}\n        </div>\n      }\n      src={embedSrc}\n      allow=\"clipboard-write; clipboard-read; fullscreen\"\n      loading={loading}\n      height={height}\n      allowFullScreen\n      emptyIcon={<FigmaIcon className=\"mb-4 h-16 w-16 text-ods-text-secondary\" />}\n      emptyMessage=\"Figma URL not configured\"\n    />\n  );\n}\n","'use client';\n\nimport { ExternalLink } from 'lucide-react';\nimport { useMemo, useState } from 'react';\nimport { LinkedinIcon } from '../icons-v2-generated/brand-logos/linkedin-icon';\nimport { LinkedInContainer } from './embed-container';\n\n/**\n * Derive LinkedIn's official embed URL from any post URL or URN.\n * LinkedIn renders public posts at /embed/feed/update/<urn>. Returns '' when no\n * URN can be derived, so the component falls back to a link instead of a broken\n * (X-Frame-blocked) iframe.\n */\nfunction toLinkedInEmbedUrl(url: string): string {\n  if (!url) return '';\n  if (url.includes('linkedin.com/embed/')) return url.split('?')[0];\n  let m = url.match(/urn:li:(activity|share|ugcPost):(\\d+)/i);\n  if (m) return `https://www.linkedin.com/embed/feed/update/urn:li:${m[1]}:${m[2]}`;\n  m = url.match(/activity[-:](\\d{15,25})/i);\n  if (m) return `https://www.linkedin.com/embed/feed/update/urn:li:activity:${m[1]}`;\n  m = url.match(/-(\\d{15,25})(?:-[A-Za-z0-9_-]+)?\\/?(?:\\?.*)?$/);\n  if (m) return `https://www.linkedin.com/embed/feed/update/urn:li:activity:${m[1]}`;\n  return '';\n}\n\ninterface LinkedInEmbedProps {\n  url: string;\n  /** Fixed iframe height — LinkedIn embeds don't auto-resize. */\n  height?: number;\n}\n\nexport function LinkedInEmbedClient({ url, height = 600 }: LinkedInEmbedProps) {\n  const embedUrl = useMemo(() => toLinkedInEmbedUrl(url), [url]);\n  const [loaded, setLoaded] = useState(false);\n\n  // No derivable URN → graceful fallback card with a link (mirrors reddit's error state)\n  if (!embedUrl) {\n    return (\n      <LinkedInContainer>\n        <div className=\"p-6\">\n          <div className=\"mb-4 flex items-center space-x-3 text-ods-text-secondary\">\n            <LinkedinIcon className=\"h-5 w-5 shrink-0\" />\n            <span>LinkedIn post</span>\n          </div>\n          <a\n            href={url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center space-x-2 rounded-md border border-ods-border bg-ods-card px-4 py-2 text-ods-text-primary transition-colors text-h6 hover:bg-ods-bg-hover\"\n          >\n            <LinkedinIcon className=\"h-4 w-4\" />\n            <span>View on LinkedIn</span>\n          </a>\n        </div>\n      </LinkedInContainer>\n    );\n  }\n\n  return (\n    <LinkedInContainer>\n      <div className=\"relative w-full\" style={{ height }}>\n        {!loaded && (\n          <div className=\"absolute inset-0 animate-pulse p-6\">\n            <div className=\"mb-4 flex items-center space-x-3\">\n              <div className=\"h-12 w-12 rounded-full bg-ods-border\" />\n              <div>\n                <div className=\"mb-2 h-4 w-32 rounded bg-ods-border\" />\n                <div className=\"h-3 w-24 rounded bg-ods-border\" />\n              </div>\n            </div>\n            <div className=\"space-y-2\">\n              <div className=\"h-4 w-full rounded bg-ods-border\" />\n              <div className=\"h-4 w-3/4 rounded bg-ods-border\" />\n            </div>\n          </div>\n        )}\n        <iframe\n          src={embedUrl}\n          title=\"Embedded LinkedIn post\"\n          className=\"h-full w-full\"\n          style={{ border: 0 }}\n          loading=\"lazy\"\n          allowFullScreen\n          onLoad={() => setLoaded(true)}\n        />\n      </div>\n      <div className=\"border-t border-ods-border bg-ods-bg-surface px-4 py-3\">\n        <a\n          href={url}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"inline-flex items-center space-x-2 text-ods-accent transition-colors text-h6 hover:text-ods-accent/80\"\n        >\n          <ExternalLink className=\"h-4 w-4\" />\n          <span>View on LinkedIn</span>\n        </a>\n      </div>\n    </LinkedInContainer>\n  );\n}\n","import type React from 'react';\nimport { cn } from '../../utils/cn';\n\n// Base container sizes for different embed types\nexport const EMBED_SIZES = {\n  youtube: 'max-w-3xl', // 768px - Video content needs more space\n  twitter: 'max-w-md', // 448px - Narrow tweets, mobile-first\n  reddit: 'max-w-xl', // 576px - Medium width for discussion threads\n  linkedin: 'max-w-lg', // 512px - LinkedIn post embed, mobile-first\n  linkPreview: 'max-w-lg', // 512px - Balanced width for cards\n} as const;\n\nexport type EmbedSize = keyof typeof EMBED_SIZES;\n\ninterface EmbedContainerProps {\n  size: EmbedSize;\n  children: React.ReactNode;\n  className?: string;\n}\n\n// Base container for all embeds\nexport function EmbedContainer({ size, children, className = '' }: EmbedContainerProps) {\n  return (\n    <div\n      className={cn(\n        'mx-auto overflow-hidden rounded-lg',\n        'border border-ods-border bg-ods-card',\n        'transition-all duration-200 ease-in-out',\n        'hover:border-ods-accent/30 hover:shadow-lg hover:shadow-ods-accent/10',\n        EMBED_SIZES[size],\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\n// Specific containers for each platform\nexport function YouTubeContainer({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <EmbedContainer size=\"youtube\" className={cn('my-6', className)}>\n      {children}\n    </EmbedContainer>\n  );\n}\n\nexport function TwitterContainer({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <EmbedContainer size=\"twitter\" className={cn('my-6', className)}>\n      {children}\n    </EmbedContainer>\n  );\n}\n\nexport function RedditContainer({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <EmbedContainer size=\"reddit\" className={cn('my-6', className)}>\n      {children}\n    </EmbedContainer>\n  );\n}\n\nexport function LinkPreviewContainer({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <EmbedContainer size=\"linkPreview\" className={cn('my-6', className)}>\n      {children}\n    </EmbedContainer>\n  );\n}\n\nexport function LinkedInContainer({ children, className }: { children: React.ReactNode; className?: string }) {\n  return (\n    <EmbedContainer size=\"linkedin\" className={cn('my-6', className)}>\n      {children}\n    </EmbedContainer>\n  );\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { useRichMarkdownRuntime } from './rich-markdown-runtime';\n\n/**\n * In-article markdown image.\n *\n * Markdown images have unknown intrinsic dimensions — which is what broke the old\n * approach: a fixed width/height made the Supabase loader `resize=cover` CROP tall\n * screenshots, and `w-full` then blew them up to the column width. This renders\n * through the lib's `next/image` shim (full Next.js Image Optimization on the hub,\n * plain `<img>` everywhere else) and fixes both problems at the source:\n *\n *   - `object-contain` flips the hub's Supabase loader (injected via\n *     `transformImageSrc` from the {@link RichMarkdownRuntimeProvider}) to\n *     `resize=contain` — aspect preserved, never cropped.\n *   - We learn the real aspect ratio from a tiny probe (the same\n *     `transformImageSrc` at 48px when available) and pass matching width/height\n *     so `next/image`'s aspect-ratio box is correct.\n *   - CSS (`max-w-full` + `max-h` + auto) caps the on-page size so a tall\n *     screenshot can't dominate the article, while the browser keeps the true\n *     aspect ratio.\n *\n * Embedders that don't pass `transformImageSrc` get an identity fallback — the\n * raw `src` is used both for the probe and the display copy.\n */\nconst MAX_H_REM = 32; // ~512px on-page cap\nconst DISPLAY_W = 768; // logical width hint; the optimizer handles srcset + retina\n\nexport function MarkdownImage({ src, alt }: { src: string; alt?: string }) {\n  const { transformImageSrc } = useRichMarkdownRuntime();\n  // Tagged with the src it was measured from. That tag IS the reset: a reused\n  // instance handed a new `src` stops matching immediately, so it can never\n  // size the new image with the previous image's ratio — where the old explicit\n  // `setRatio(null)` in the effect did the same thing one commit late (and cost\n  // a second render pass every time).\n  const [probed, setProbed] = useState<{ src: string; ratio: number } | null>(null);\n  const ratio = probed?.src === src ? probed.ratio : null; // width / height\n\n  useEffect(() => {\n    let cancelled = false;\n    // Probe a tiny aspect-preserving variant so we learn the real ratio without\n    // downloading the full image; the display copy is then fetched once, at the right size.\n    // When no transformer is wired (embedders), fall back to the raw src.\n    const probeSrc = transformImageSrc(src, { width: 48, resize: 'contain', quality: 20 }) ?? src;\n    const probe = new window.Image();\n    probe.onload = () => {\n      if (!cancelled && probe.naturalWidth && probe.naturalHeight) {\n        setProbed({ src, ratio: probe.naturalWidth / probe.naturalHeight });\n      }\n    };\n    probe.onerror = () => {\n      if (!cancelled) setProbed({ src, ratio: 1.5 }); // neutral fallback so we still render something\n    };\n    probe.src = probeSrc;\n    return () => {\n      cancelled = true;\n    };\n  }, [src, transformImageSrc]);\n\n  // Reserve a neutral box while probing so the layout doesn't jump when the image appears.\n  if (!ratio) {\n    return (\n      <span\n        className=\"mx-auto my-2 block w-full max-w-full animate-pulse rounded-lg bg-ods-card\"\n        style={{ aspectRatio: '3 / 2', maxHeight: `${MAX_H_REM}rem` }}\n        aria-hidden\n      />\n    );\n  }\n\n  return (\n    <Image\n      src={src}\n      alt={alt ?? 'No image available'}\n      width={DISPLAY_W}\n      height={Math.round(DISPLAY_W / ratio)}\n      sizes=\"(max-width: 768px) 100vw, 768px\"\n      loading=\"lazy\"\n      // `object-contain` → SupabaseOptimizedImage uses `resize=contain` (no crop).\n      // `w-full` (not `w-auto`) gives the img a definite width so it lays out + lazy-loads\n      // even before decode. Height follows via the aspect-ratio box; `maxHeight` caps tall\n      // images and `maxWidth = maxHeight × ratio` shrinks the width in lockstep so the box\n      // stays snug to the image (no letterbox bars).\n      className=\"mx-auto my-2 block h-auto w-full rounded-lg object-contain\"\n      style={{ maxWidth: `calc(${MAX_H_REM}rem * ${ratio})`, maxHeight: `${MAX_H_REM}rem` }}\n    />\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { useState, useEffect, Component, type ReactNode } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { useImageEdgeColor } from '../../hooks';\n\n/**\n * Open-Graph metadata returned by the consumer's scrape endpoint.\n *\n * The shape MUST match the JSON the OG endpoint serves at `ogEndpointPath`.\n * The hub's `/api/og-scraper` returns exactly these fields — embedders\n * with a different endpoint must return the same shape (or adapt at the\n * route boundary). Keeps the consumer surface trivial: one URL → one card.\n */\nexport interface OGData {\n  title: string;\n  description: string;\n  image: string;\n  originalImage?: string;\n  url: string;\n  siteName: string;\n  type: string;\n  favicon: string;\n}\n\ninterface ErrorBoundaryProps {\n  children: ReactNode;\n  fallback: ReactNode;\n}\n\ninterface ErrorBoundaryState {\n  hasError: boolean;\n}\n\n/**\n * Tiny error boundary tailored for OG link previews — caught errors quietly\n * fall back to the `fallback` prop (typically a plain hyperlink) so a single\n * broken third-party preview can't crash a whole article view.\n *\n * Named `OGLinkErrorBoundary` (not the generic `ErrorBoundary`) because the\n * lib already exports a separate `ErrorBoundary` from\n * `components/features/error-boundary.tsx`. The top-level `components/index.ts`\n * barrel re-exports both `./embeds` and `./features` via `export *`, so a\n * second `ErrorBoundary` here collides as TS2308.\n */\nexport class OGLinkErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\n  constructor(props: ErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(): ErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n    console.warn('Link preview error caught by boundary:', error, errorInfo);\n  }\n\n  render() {\n    if (this.state.hasError) return this.props.fallback;\n    return this.props.children;\n  }\n}\n\n/**\n * Builds a placeholder image URL when the scrape returns no image. Hub passes\n * its own `buildOgPlaceholderUrl` (which hits `/api/og-placeholder?…&platform=`;\n * the route resolves the platform's brand colors server-side); other embedders\n * can omit the prop to disable the placeholder entirely.\n *\n * Receives the post-scrape `title` and `siteName` so the placeholder can echo\n * the actual card content, not a generic graphic.\n */\nexport type BuildPlaceholderUrl = (title: string, siteName: string) => string | null;\n\nexport interface OGLinkPreviewProps {\n  /** The external URL to preview. */\n  url: string;\n  /** Origin / base URL the OG endpoint is served from. Empty / undefined\n   *  means same-origin (hub-direct use). Embed contexts pass the hub's\n   *  origin here (e.g. `'https://hub.example.com'`) so the fetch hits\n   *  the hub instead of the embedder origin.\n   *\n   *  Pattern matches lib's `useNatsDialogSubscription({apiBaseUrl})` +\n   *  `buildSuggestionUrl({apiBaseUrl})` so all embed-ready surfaces share\n   *  one configuration knob. */\n  apiBaseUrl?: string;\n  /** Path of the OG endpoint on the configured base. Default\n   *  `'/api/og-scraper'` matches the hub's route. Override if the\n   *  embedder serves the same `OGData` shape from a different path. */\n  ogEndpointPath?: string;\n  /** Optional placeholder-builder. Omit to disable the placeholder image\n   *  (the card then degrades to a favicon+title chip when no scraped image\n   *  is available). The hub injects its `buildOgPlaceholderUrl` here. */\n  buildPlaceholderUrl?: BuildPlaceholderUrl;\n  /** CURATED title — our own copy for this link (e.g. a CMS-managed press\n   *  row). Wins over the scrape; see the precedence rule on the component. */\n  curatedTitle?: string;\n  /** CURATED description. Wins over the scrape. */\n  curatedDescription?: string;\n  /** CURATED cover image. Wins over the scrape — an outlet's og:image is a\n   *  fallback for links we have no cover for, never an override of one we\n   *  deliberately chose. */\n  curatedImage?: string;\n  /** Publication / source name shown alongside the favicon (e.g. \"TechCrunch\"). */\n  publicationName?: string;\n  /** Publication logo URL shown alongside the title (defaults to favicon). */\n  publicationLogo?: string;\n  /** Card variant. `compact` = horizontal layout (~120px tall) suited for\n   *  in-doc placements; `default` = larger vertical layout for press / hero\n   *  positions. */\n  variant?: 'default' | 'compact';\n  /** Disable the synthesized placeholder image even when `buildPlaceholderUrl`\n   *  is provided — used by the markdown renderer to keep doc cards lighter. */\n  enablePlaceholder?: boolean;\n}\n\nfunction getDomain(urlStr: string): string {\n  try {\n    return new URL(urlStr).hostname.replace('www.', '');\n  } catch {\n    return 'External Link';\n  }\n}\n\nfunction domainToTitle(domain: string): string {\n  return domain\n    .split('.')[0]\n    .replace(/-/g, ' ')\n    .replace(/\\b\\w/g, c => c.toUpperCase());\n}\n\nconst ExternalLinkIcon = ({ size = 16 }: { size?: number }) => (\n  <svg\n    width={size}\n    height={size}\n    fill=\"none\"\n    stroke=\"currentColor\"\n    viewBox=\"0 0 24 24\"\n    className=\"flex-shrink-0 text-ods-text-secondary transition-colors group-hover:text-ods-accent\"\n  >\n    <path\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth={2}\n      d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\"\n    />\n  </svg>\n);\n\nconst Favicon = ({ src, size = 'w-6 h-6' }: { src: string; size?: string }) => (\n  <Image\n    src={src}\n    alt=\"\"\n    className={size}\n    width={24}\n    height={24}\n    unoptimized\n    onError={e => {\n      const img = e.target as HTMLImageElement;\n      img.style.display = 'none';\n    }}\n  />\n);\n\n/**\n * Rich Open-Graph link preview card with skeleton, fallback, and image-edge\n * background detection.\n *\n * Flow:\n *  1. Validate URL early (no network for malformed input, localhost, or\n *     RFC1918 ranges — those render as plain `<a>` tags).\n *  2. `GET ogEndpointPath?url=<encoded>` — embedder serves the shape declared\n *     in `OGData`.\n *  3. Resolve the image by the PRECEDENCE rule below.\n *  4. Extract a letterbox background color from the resolved image via\n *     `useImageEdgeColor`. Same-origin proxy is REQUIRED for cross-origin\n *     images so the `<canvas>` extraction doesn't taint.\n *  5. Render compact (h-[120px] horizontal) or default (vertical w/ aspect-video\n *     hero) variant, with image-less degraded variants for each.\n *\n * PRECEDENCE — curated over scraped, for EVERY field alike. A `curated*` prop\n * is a value we chose ourselves; the scrape only fills what curation left\n * empty:\n *\n *     curatedImage → scraped og:image → scraped originalImage → placeholder\n *\n * The image chain used to invert this — any outlet og:image beat an uploaded\n * cover — so a deliberate editorial choice was silently overridden wherever\n * the outlet happened to publish one, while title and description read\n * curated-first 200 lines down. The chain is now expressed ONCE, as\n * `imageCandidates`, and `handleImageError` retires the candidate currently on\n * screen instead of restating the order: a positional error handler is how the\n * two halves drift apart.\n */\nexport const OGLinkPreview: React.FC<OGLinkPreviewProps> = ({\n  url,\n  apiBaseUrl,\n  ogEndpointPath = '/api/og-scraper',\n  buildPlaceholderUrl,\n  curatedTitle,\n  curatedDescription,\n  curatedImage,\n  publicationName,\n  publicationLogo,\n  variant = 'default',\n  enablePlaceholder = true,\n}) => {\n  const [ogData, setOgData] = useState<OGData | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState(false);\n  /** URLs that failed to load. Keyed by url, so the candidate order below is\n   *  the single owner of precedence. */\n  const [failedImages, setFailedImages] = useState<ReadonlySet<string>>(new Set());\n\n  let isValidUrl = true;\n  let isLocalhost = false;\n  try {\n    if (url && typeof url === 'string') {\n      const urlObj = new URL(url);\n      if (\n        ['localhost', '127.0.0.1', '0.0.0.0'].includes(urlObj.hostname) ||\n        urlObj.hostname.startsWith('192.168.') ||\n        urlObj.hostname.startsWith('10.') ||\n        urlObj.hostname.startsWith('172.')\n      ) {\n        isLocalhost = true;\n      }\n    } else {\n      isValidUrl = false;\n    }\n  } catch {\n    isValidUrl = false;\n  }\n\n  useEffect(() => {\n    if (!isValidUrl || isLocalhost) return undefined;\n\n    // A `url` change starts a second fetch while the first is still in flight.\n    // Without this guard the SUPERSEDED response can land last and paint the\n    // PREVIOUS link's title/image/favicon into this card — or flip it to the\n    // fallback error card for a link that actually resolved fine.\n    let cancelled = false;\n\n    const fetchOGData = async () => {\n      try {\n        new URL(url);\n        setLoading(true);\n        // Compose `${base}${path}?url=…`. Empty base → relative path\n        // (same-origin); absolute base → cross-origin embed against the hub.\n        // Plain string concat is safer than `new URL(path, base)` because\n        // the latter resolves `path` against the BASE's pathname when\n        // `path` is relative, producing surprising URLs when the embedder\n        // serves the lib from a subpath.\n        const endpoint = `${apiBaseUrl ?? ''}${ogEndpointPath}?url=${encodeURIComponent(url)}`;\n        const response = await fetch(endpoint);\n        if (cancelled) return;\n        if (response.ok) {\n          // The endpoint is embedder-supplied, so treat every field as\n          // optional and fill the gaps the way the error card does (empty\n          // string = \"nothing to render\"). `OGData` promises non-optional\n          // strings; without this a sparse payload broke that promise and the\n          // image/favicon chain got `undefined` where it tests for falsy.\n          const data = (await response.json()) as Partial<OGData> | null;\n          if (cancelled) return;\n          if (data?.title && data.title !== 'Link Preview Unavailable') {\n            setOgData({\n              title: data.title,\n              description: data.description ?? '',\n              image: data.image ?? '',\n              originalImage: data.originalImage,\n              url: data.url ?? url,\n              siteName: data.siteName ?? '',\n              type: data.type ?? 'website',\n              favicon: data.favicon ?? '',\n            });\n          } else {\n            setError(true);\n          }\n        } else {\n          setError(true);\n        }\n      } catch {\n        if (!cancelled) setError(true);\n      } finally {\n        if (!cancelled) setLoading(false);\n      }\n    };\n\n    // Never rejects — try/catch/finally, every write gated on `cancelled`.\n    void fetchOGData();\n\n    return () => {\n      cancelled = true;\n    };\n  }, [url, isValidUrl, isLocalhost, apiBaseUrl, ogEndpointPath]);\n\n  const isCompact = variant === 'compact';\n  const domain = getDomain(url);\n\n  const effectiveData: OGData | null =\n    ogData ??\n    (error\n      ? {\n          title: curatedTitle || domainToTitle(domain),\n          description: curatedDescription || domain,\n          image: '',\n          url,\n          siteName: publicationName || domain,\n          type: 'website',\n          favicon: `https://www.google.com/s2/favicons?domain=${domain}&sz=32`,\n        }\n      : null);\n\n  // Hub-injected placeholder builder — fires only when the post-scrape image\n  // chain is empty AND `enablePlaceholder` is true. `null` when unprovided.\n  const placeholderImageUrl =\n    enablePlaceholder && buildPlaceholderUrl && effectiveData?.title\n      ? buildPlaceholderUrl(effectiveData.title, effectiveData.siteName || domain)\n      : null;\n\n  // THE precedence rule, in one place (see the component doc). Each candidate\n  // carries its SLOT, so the winner is identified by where it came from and\n  // never by string-comparing the resolved url back against the inputs: two\n  // slots can legitimately hold the same url (an embedder passing the outlet's\n  // own image as `curatedImage`), and `===` would then pick the render branch\n  // by coincidence.\n  const imageCandidates: { source: 'curated' | 'og' | 'og-original' | 'placeholder'; url?: string | null }[] = [\n    { source: 'curated', url: curatedImage },\n    { source: 'og', url: effectiveData?.image },\n    { source: 'og-original', url: effectiveData?.originalImage },\n    { source: 'placeholder', url: placeholderImageUrl },\n  ];\n  const resolvedImage = imageCandidates.find(c => !!c.url && !failedImages.has(c.url)) ?? null;\n  const resolvedImageUrl = resolvedImage?.url ?? null;\n\n  const hasImage = !!resolvedImageUrl;\n  const isCuratedImage = resolvedImage?.source === 'curated';\n  const isPlaceholder = resolvedImage?.source === 'placeholder';\n  const bgColor = useImageEdgeColor(resolvedImageUrl ?? null, 'var(--color-bg-surface)');\n\n  const renderSkeleton = () =>\n    isCompact ? (\n      <div className=\"my-4\">\n        <div className=\"flex h-[120px] flex-row overflow-hidden rounded-lg border border-ods-border bg-ods-card\">\n          <div className=\"h-full w-[200px] flex-shrink-0 animate-pulse bg-ods-border\" />\n          <div className=\"flex flex-1 flex-col justify-center p-3\">\n            <div className=\"mb-2 h-4 w-3/4 animate-pulse rounded bg-ods-border\" />\n            <div className=\"mb-1 h-3 w-full animate-pulse rounded bg-ods-border\" />\n            <div className=\"mb-2 h-3 w-2/3 animate-pulse rounded bg-ods-border\" />\n            <div className=\"h-3 w-1/3 animate-pulse rounded bg-ods-border\" />\n          </div>\n        </div>\n      </div>\n    ) : (\n      <div className=\"my-6\">\n        <div className=\"block overflow-hidden rounded-lg border border-ods-border bg-ods-card\">\n          <div className=\"relative aspect-video w-full animate-pulse overflow-hidden bg-ods-border\" />\n          <div className=\"p-4\">\n            <div className=\"flex items-start gap-3\">\n              <div className=\"mt-0.5 h-6 w-6 flex-shrink-0 animate-pulse rounded bg-ods-border\" />\n              <div className=\"min-w-0 flex-1\">\n                <div className=\"mb-2 h-[2.5rem] overflow-hidden leading-[1.25rem]\">\n                  <div\n                    className=\"animate-pulse rounded bg-ods-border\"\n                    style={{ height: '1.25rem', marginBottom: '0.25rem' }}\n                  />\n                  <div className=\"w-3/4 animate-pulse rounded bg-ods-border\" style={{ height: '1.25rem' }} />\n                </div>\n                <div className=\"mb-2 h-[2.5rem] overflow-hidden leading-[1.25rem]\">\n                  <div\n                    className=\"animate-pulse rounded bg-ods-border\"\n                    style={{ height: '1.25rem', marginBottom: '0.25rem' }}\n                  />\n                  <div className=\"w-5/6 animate-pulse rounded bg-ods-border\" style={{ height: '1.25rem' }} />\n                </div>\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"animate-pulse rounded bg-ods-border\" style={{ height: '0.75rem', width: '6rem' }} />\n                  <div className=\"animate-pulse rounded bg-ods-border\" style={{ height: '0.75rem', width: '5rem' }} />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n\n  if (!url || typeof url !== 'string' || !isValidUrl) return renderSkeleton();\n\n  if (isLocalhost) {\n    return (\n      <div className=\"my-6\">\n        <a\n          href={url}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"inline-flex items-center gap-2 text-ods-accent transition-colors hover:text-ods-accent-hover\"\n        >\n          <span className=\"underline\">{url}</span>\n          <ExternalLinkIcon size={14} />\n        </a>\n      </div>\n    );\n  }\n\n  if (loading) return renderSkeleton();\n  if (!effectiveData) return renderSkeleton();\n\n  const title = curatedTitle || effectiveData.title;\n  // Empty string when the scrape returned nothing — descriptions render\n  // conditionally below. Avoids the legacy `'No description available'` filler\n  // that signaled \"broken card\" to users.\n  const description = curatedDescription || effectiveData.description || '';\n  const ogDomain = getDomain(effectiveData.url);\n  const faviconSrc = effectiveData.favicon || `https://www.google.com/s2/favicons?domain=${ogDomain}&sz=32`;\n  const logoSrc = publicationLogo || faviconSrc;\n\n  // Retire whichever candidate is on screen; the next one down the chain\n  // takes over on the re-render. Never restate the order here.\n  const handleImageError = () => {\n    const failed = resolvedImageUrl;\n    if (!failed) return;\n    setFailedImages(prev => (prev.has(failed) ? prev : new Set(prev).add(failed)));\n  };\n\n  const renderImage = () => {\n    if (!resolvedImageUrl) return null;\n    if (isPlaceholder) {\n      return (\n        <Image src={resolvedImageUrl} alt={title} className=\"rounded-md object-cover\" fill sizes=\"100vw\" unoptimized />\n      );\n    }\n    if (isCuratedImage) {\n      return (\n        <Image\n          src={resolvedImageUrl}\n          alt={title}\n          fill\n          className=\"rounded-md object-contain transition-transform duration-300 group-hover:scale-105\"\n          onError={handleImageError}\n          unoptimized={resolvedImageUrl.includes('/render/image/')}\n        />\n      );\n    }\n    return (\n      <Image\n        src={resolvedImageUrl}\n        alt={title}\n        className=\"rounded-md object-contain transition-transform duration-300 group-hover:scale-105\"\n        fill\n        sizes=\"100vw\"\n        unoptimized\n        onError={handleImageError}\n      />\n    );\n  };\n\n  if (isCompact) {\n    if (!hasImage) {\n      return (\n        <div className=\"my-4\">\n          <a\n            href={effectiveData.url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"group flex flex-row items-center gap-3 overflow-hidden rounded-lg border border-ods-border bg-ods-card px-4 py-3 transition-all duration-200 hover:border-ods-accent\"\n          >\n            <div className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-ods-bg-surface\">\n              <Favicon src={faviconSrc} size=\"w-5 h-5\" />\n            </div>\n            <div className=\"min-w-0 flex-1\">\n              <h3 className=\"truncate font-semibold text-ods-text-primary transition-colors text-h6 group-hover:text-ods-accent\">\n                {title}\n              </h3>\n              {description && <p className=\"truncate text-ods-text-secondary text-h6\">{description}</p>}\n            </div>\n            <ExternalLinkIcon size={14} />\n          </a>\n        </div>\n      );\n    }\n    return (\n      <div className=\"my-4\">\n        <a\n          href={effectiveData.url}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"group flex h-[120px] flex-row overflow-hidden rounded-lg border border-ods-border bg-ods-card transition-colors hover:border-ods-accent\"\n        >\n          <div\n            className=\"relative flex h-full w-[200px] flex-shrink-0 items-center justify-center overflow-hidden rounded-lg transition-colors duration-300\"\n            style={{ backgroundColor: bgColor }}\n          >\n            {renderImage()}\n          </div>\n          <div className=\"flex min-w-0 flex-1 flex-col justify-center p-3\">\n            <h3\n              className=\"overflow-hidden font-semibold text-ods-text-primary transition-colors text-h6 group-hover:text-ods-accent\"\n              style={{ display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}\n            >\n              {title}\n            </h3>\n            {description && (\n              <p\n                className=\"mt-1 overflow-hidden text-ods-text-secondary text-h6\"\n                style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}\n              >\n                {description}\n              </p>\n            )}\n            <div className=\"mt-1 truncate text-ods-text-secondary text-h6\">{effectiveData.siteName || ogDomain}</div>\n          </div>\n        </a>\n      </div>\n    );\n  }\n\n  if (!hasImage) {\n    return (\n      <div className=\"my-6\">\n        <a\n          href={effectiveData.url}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"group flex items-center gap-3 overflow-hidden rounded-lg border border-ods-border bg-ods-card px-4 py-3 transition-all duration-200 hover:border-ods-accent\"\n        >\n          <div className=\"flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-ods-bg-surface\">\n            <Favicon src={faviconSrc} />\n          </div>\n          <div className=\"min-w-0 flex-1\">\n            <h3 className=\"truncate font-semibold text-ods-text-primary transition-colors text-h6 group-hover:text-ods-accent\">\n              {title}\n            </h3>\n            {description && <p className=\"truncate text-ods-text-secondary text-h6\">{description}</p>}\n          </div>\n          <ExternalLinkIcon />\n        </a>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"my-6\">\n      <a\n        href={effectiveData.url}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"group block overflow-hidden rounded-lg border border-ods-border bg-ods-card transition-colors hover:border-ods-accent\"\n      >\n        <div\n          className=\"relative flex aspect-video w-full items-center justify-center overflow-hidden rounded-lg transition-colors duration-300\"\n          style={{ backgroundColor: bgColor }}\n        >\n          {renderImage()}\n        </div>\n        <div className=\"p-4\">\n          <div className=\"flex items-start gap-3\">\n            <Image\n              src={logoSrc}\n              alt={publicationName || ''}\n              className=\"mt-0.5 h-6 w-6 flex-shrink-0 rounded object-contain\"\n              width={24}\n              height={24}\n              unoptimized\n              onError={e => {\n                const img = e.target as HTMLImageElement;\n                img.style.display = 'none';\n              }}\n            />\n            <div className=\"min-w-0 flex-1\">\n              <h3\n                className=\"mb-2 h-[2.5rem] overflow-hidden font-semibold text-ods-text-primary transition-colors text-h6 group-hover:text-ods-accent\"\n                style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}\n              >\n                {title}\n              </h3>\n              {description && (\n                <p\n                  className=\"mb-2 h-[2.5rem] overflow-hidden text-ods-text-secondary text-h6\"\n                  style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}\n                >\n                  {description}\n                </p>\n              )}\n              <div className=\"flex items-center gap-2 text-ods-text-secondary text-h6\">\n                <span>{effectiveData.siteName}</span>\n                <span>•</span>\n                <span className=\"truncate\">{ogDomain}</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </a>\n    </div>\n  );\n};\n","'use client';\n\nimport { useState, useEffect, useRef } from 'react';\nimport { formatLargeNumber } from '../../utils/format';\nimport type { MediaItem as CarouselMediaItem } from '../../utils/media-carousel-utils-stub';\nimport { socialCache } from '../../utils/social-embed-cache';\nimport { MediaCarousel } from '../media-carousel';\nimport { RedditContainer } from './embed-container';\nimport { useRichMarkdownRuntime } from './rich-markdown-runtime';\n\n// Using inline SVG icons to avoid dependency issues\nconst MessageCircleIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z\" />\n  </svg>\n);\n\nconst ExternalLinkIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6\" />\n    <polyline points=\"15,3 21,3 21,9\" />\n    <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n  </svg>\n);\n\nconst ArrowUpIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <polyline points=\"18,15 12,9 6,15\" />\n  </svg>\n);\n\nconst ClockIcon = () => (\n  <svg width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <polyline points=\"12,6 12,12 16,14\" />\n  </svg>\n);\n\nconst UserIcon = () => (\n  <svg width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\" />\n    <circle cx=\"12\" cy=\"7\" r=\"4\" />\n  </svg>\n);\n\nconst RedditIcon = () => (\n  <svg width=\"20\" height=\"20\" fill=\"#FF4500\" viewBox=\"0 0 24 24\">\n    <circle cx=\"9\" cy=\"12\" r=\"1\" />\n    <circle cx=\"15\" cy=\"12\" r=\"1\" />\n    <path d=\"M22 12a2 2 0 1 0-4 0c0 5.5-4.5 10-10 10S-2 17.5-2 12a2 2 0 1 0-4 0c0 7.7 6.3 14 14 14s14-6.3 14-14z\" />\n    <path d=\"M8 10c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2\" />\n  </svg>\n);\n\n// Simplified Reddit profile picture component\nconst RedditProfilePic = ({ username }: { username: string }) => {\n  return (\n    <div\n      className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-[#FF4500]\"\n      title={`u/${username}`}\n    >\n      <span className=\"text-xs font-bold text-white\">u/</span>\n    </div>\n  );\n};\n\n/** Coarse \"3h ago\" label. Module scope on purpose: it closes over nothing,\n *  and `Date.now()` inside a component's render body is an impurity React\n *  Compiler rejects (the label is day/hour granularity, so a per-render read\n *  is harmless — it just must not happen during render). */\nconst formatTimeAgo = (timestamp: number) => {\n  const now = Math.floor(Date.now() / 1000);\n  const diffSeconds = now - timestamp;\n\n  if (diffSeconds < 60) return 'just now';\n  if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`;\n  if (diffSeconds < 86400) return `${Math.floor(diffSeconds / 3600)}h ago`;\n  return `${Math.floor(diffSeconds / 86400)}d ago`;\n};\n\ninterface RedditPost {\n  title: string;\n  selftext: string;\n  author: string;\n  subreddit: string;\n  created_utc: number;\n  ups: number;\n  num_comments: number;\n  url: string;\n  permalink: string;\n  preview?: {\n    images: Array<{\n      source: {\n        url: string;\n        width: number;\n        height: number;\n      };\n      resolutions: Array<{\n        url: string;\n        width: number;\n        height: number;\n      }>;\n    }>;\n  };\n  media?: {\n    reddit_video?: {\n      fallback_url: string;\n      height: number;\n      width: number;\n      is_gif: boolean;\n    };\n  };\n  secure_media?: {\n    reddit_video?: {\n      fallback_url: string;\n      height: number;\n      width: number;\n      is_gif: boolean;\n    };\n  };\n  post_hint?: string;\n  is_video?: boolean;\n  domain?: string;\n  gallery_data?: {\n    items: Array<{\n      media_id: string;\n    }>;\n  };\n  media_metadata?: Record<\n    string,\n    {\n      s: {\n        u: string;\n        x: number;\n        y: number;\n      };\n    }\n  >;\n}\n\n// Internal media-item shape; we cast to the carousel's expected MediaItem at\n// the render boundary so we don't have to fabricate `id`s/`alt` for every push.\ninterface MediaItem {\n  type: 'image' | 'video';\n  src: string;\n  width: number;\n  height: number;\n  alt?: string;\n  isGif?: boolean;\n  poster?: string;\n}\n\n/**\n * `<permalink>.json` response: `[postListing, commentListing]`. Only the post\n * listing's first child is read.\n */\ntype RedditListingResponse = Array<{ data?: { children?: Array<{ data?: RedditPost }> } }>;\n\ninterface RedditEmbedProps {\n  url: string;\n  maxWidth?: number;\n}\n\nexport function RedditEmbedClient({ url, maxWidth = 700 }: RedditEmbedProps) {\n  const { redditProxyUrl } = useRichMarkdownRuntime();\n  const [redditData, setRedditData] = useState<RedditPost | null>(null);\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const fetchedForRef = useRef<string | null>(null);\n\n  useEffect(() => {\n    // Normalize the Reddit URL to JSON format\n    const jsonUrl = url.endsWith('.json') ? url : `${url.replace(/\\/$/, '')}.json`;\n\n    // Fetch once PER URL: StrictMode's double mount is deduped, and a genuinely\n    // different `url` prop now refetches instead of showing the first embed\n    // forever.\n    if (fetchedForRef.current === jsonUrl) return;\n    fetchedForRef.current = jsonUrl;\n\n    // Reddit-specific data validator\n    const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n    const validateRedditData = (data: unknown): data is RedditListingResponse => {\n      if (!Array.isArray(data)) return false;\n      const postListing: unknown = data[0];\n      if (!isRecord(postListing) || !isRecord(postListing.data)) return false;\n      const children = postListing.data.children;\n      return Array.isArray(children) && children.length > 0;\n    };\n\n    // Stale-response guard. Deliberately keyed on `fetchedForRef` rather than a\n    // per-run `cancelled` boolean: the ref above makes StrictMode's second\n    // effect run a no-op, so a per-run flag would cancel the ONLY in-flight\n    // request and leave the embed spinning forever. Comparing against the ref\n    // suppresses only genuinely superseded responses — without it, a `url`\n    // change lets the previous post's payload land last and render the WRONG\n    // Reddit post (or drop the spinner while the new fetch is still running).\n    const isCurrent = () => fetchedForRef.current === jsonUrl;\n\n    // Use centralized cache hierarchy\n    // `fetchWithHierarchy` never rejects — failures arrive through `onError`.\n    void socialCache.fetchWithHierarchy({\n      platform: 'reddit',\n      url: jsonUrl,\n      apiEndpoint: redditProxyUrl,\n      dataValidator: validateRedditData,\n      onDataUpdate: data => {\n        if (isCurrent() && data[0]?.data?.children?.[0]?.data) {\n          setRedditData(data[0].data.children[0].data);\n        }\n      },\n      onError: errorMsg => {\n        if (isCurrent()) setError(errorMsg);\n      },\n      onLoading: isLoading => {\n        if (isCurrent()) setLoading(isLoading);\n      },\n    });\n  }, [url, redditProxyUrl]);\n\n  if (loading) {\n    return (\n      <RedditContainer>\n        <div className=\"animate-pulse rounded-lg border border-ods-border bg-ods-card p-6\">\n          <div className=\"mb-4 flex items-center space-x-3\">\n            <div className=\"h-12 w-12 rounded-full bg-ods-border\"></div>\n            <div>\n              <div className=\"mb-2 h-4 w-32 rounded bg-ods-border\"></div>\n              <div className=\"h-3 w-24 rounded bg-ods-border\"></div>\n            </div>\n          </div>\n          <div className=\"mb-4 space-y-2\">\n            <div className=\"h-4 w-full rounded bg-ods-border\"></div>\n            <div className=\"h-4 w-3/4 rounded bg-ods-border\"></div>\n          </div>\n          <div className=\"flex items-center space-x-4\">\n            <div className=\"h-4 w-16 rounded bg-ods-border\"></div>\n            <div className=\"h-4 w-16 rounded bg-ods-border\"></div>\n            <div className=\"h-4 w-16 rounded bg-ods-border\"></div>\n          </div>\n        </div>\n      </RedditContainer>\n    );\n  }\n\n  if (error || !redditData) {\n    return (\n      <RedditContainer>\n        <div className=\"rounded-lg border border-ods-border bg-ods-card p-6\">\n          <div className=\"mb-4 flex items-center space-x-3 text-ods-text-secondary\">\n            <RedditIcon />\n            <span>Reddit post unavailable</span>\n          </div>\n\n          <div className=\"text-center\">\n            <p className=\"mb-4 text-ods-text-secondary text-h6\">\n              This Reddit post could not be loaded. It may have been deleted, made private, or the subreddit may be\n              restricted.\n            </p>\n            <a\n              href={url}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex items-center space-x-2 rounded-md bg-[#FF4500] px-4 py-2 text-white transition-colors text-h6 hover:bg-[#E03D00]\"\n            >\n              <RedditIcon />\n              <span>View on Reddit</span>\n            </a>\n          </div>\n        </div>\n      </RedditContainer>\n    );\n  }\n\n  // Enhanced media extraction from Reddit post data\n  const getMediaContent = (): MediaItem[] => {\n    // FIRST: Check if the post has been removed or deleted - if so, don't extract media\n    // Reddit API exact-match indicators only. Previously this also did\n    // `title.toLowerCase().includes('removed' | 'deleted')` which suppressed\n    // legitimate posts whose titles mention those words (e.g. \"Comment was\n    // removed by mods\", \"Deleted scenes from my favorite movie\").\n    const isRemovedOrDeleted =\n      redditData.selftext === '[removed]' ||\n      redditData.selftext === '[deleted]' ||\n      redditData.author === '[deleted]' ||\n      (redditData.title && redditData.title.includes('[removed]'));\n\n    if (isRemovedOrDeleted) {\n      console.log('🚫 Post content removed - skipping all media extraction for:', redditData.title);\n      return []; // Return empty media array for removed posts\n    }\n\n    const media: MediaItem[] = [];\n\n    console.log('🔍 Reddit media extraction for:', redditData.title);\n    console.log('📊 Full Reddit data structure:', {\n      url: redditData.url,\n      domain: redditData.domain,\n      post_hint: redditData.post_hint,\n      is_video: redditData.is_video,\n      media: redditData.media,\n      secure_media: redditData.secure_media,\n      preview: redditData.preview,\n      gallery_data: redditData.gallery_data,\n      media_metadata: redditData.media_metadata,\n    });\n\n    // 1. Check for Reddit hosted video (v.redd.it) - PRIORITY\n    const video = redditData.media?.reddit_video || redditData.secure_media?.reddit_video;\n    if (video && video.fallback_url) {\n      console.log('📹 Found Reddit video:', video);\n\n      // Generate poster URL from video URL and preview data\n      let posterUrl = '';\n\n      // Try to get poster from preview images first\n      if (redditData.preview?.images?.[0]?.source?.url) {\n        posterUrl = redditData.preview.images[0].source.url.replace(/&amp;/g, '&');\n        console.log('✅ Using preview image as video poster:', posterUrl);\n      } else {\n        // Fallback: try to generate from video URL\n        try {\n          const baseUrl = video.fallback_url.replace(/DASH_\\d+\\.mp4.*$/, '');\n          posterUrl = `${baseUrl}DASH_720.jpg`;\n          console.log('🎯 Generated poster URL:', posterUrl);\n        } catch {\n          console.log('Could not generate poster URL');\n        }\n      }\n\n      // Try to get a better video URL by replacing DASH format\n      let videoUrl = video.fallback_url;\n\n      // If it's a DASH URL, try to get a direct MP4 format\n      if (videoUrl.includes('DASH_')) {\n        // Try different quality levels for Reddit videos\n        const baseUrl = videoUrl.replace(/DASH_\\d+\\.mp4.*$/, '');\n\n        // 480p as default: the widest-compatibility rung of Reddit's DASH\n        // ladder (the others are 240/360/720).\n        videoUrl = `${baseUrl}DASH_480.mp4`;\n        console.log('🎯 Optimized Reddit video URL for compatibility:', videoUrl);\n      }\n\n      media.push({\n        type: 'video',\n        src: videoUrl,\n        width: video.width || 640,\n        height: video.height || 480,\n        isGif: video.is_gif || false,\n        poster: posterUrl,\n      });\n\n      // Return early for videos to avoid showing preview images as well\n      console.log('📋 Final Reddit media (video):', media);\n      return media;\n    }\n\n    // 2. Check for Reddit gallery (multiple images)\n    if (redditData.media_metadata && redditData.gallery_data) {\n      console.log('🖼️ Found Reddit gallery');\n      const galleryItems = redditData.gallery_data.items || [];\n\n      for (const item of galleryItems) {\n        const mediaId = item.media_id;\n        const mediaInfo = redditData.media_metadata[mediaId];\n\n        if (mediaInfo && mediaInfo.s && mediaInfo.s.u) {\n          // Reddit encodes URLs, need to decode\n          const imageUrl = mediaInfo.s.u.replace(/&amp;/g, '&');\n          console.log('✅ Adding gallery image:', imageUrl);\n\n          media.push({\n            type: 'image',\n            src: imageUrl,\n            width: mediaInfo.s.x || 0,\n            height: mediaInfo.s.y || 0,\n            alt: redditData.title,\n          });\n        }\n      }\n\n      if (media.length > 0) {\n        console.log('📋 Final Reddit media (gallery):', media);\n        return media;\n      }\n    }\n\n    // 3. Check for single image preview (but not if it's actually a video)\n    if (redditData.preview?.images?.[0] && !redditData.is_video) {\n      const imageData = redditData.preview.images[0];\n      console.log('🖼️ Found preview image data:', imageData);\n\n      // Use best resolution that fits our constraints\n      let source = imageData.source;\n      if (imageData.resolutions && imageData.resolutions.length > 0) {\n        // Find best resolution under 1200px width, or use source\n        const bestResolution = imageData.resolutions.filter(r => r.width <= 1200).sort((a, b) => b.width - a.width)[0];\n        source = bestResolution || imageData.source;\n      }\n\n      if (source && source.url) {\n        const cleanUrl = source.url.replace(/&amp;/g, '&');\n        console.log('✅ Adding preview image:', cleanUrl);\n        media.push({\n          type: 'image',\n          src: cleanUrl,\n          width: source.width,\n          height: source.height,\n          alt: redditData.title,\n        });\n      }\n    }\n\n    // 4. Check for direct media URLs (imgur, i.redd.it, etc.) - only if no other media found\n    if (media.length === 0 && redditData.url) {\n      const directUrl = redditData.url.toLowerCase();\n      console.log('🔗 Checking direct URL:', directUrl);\n\n      // Image formats\n      if (directUrl.match(/\\.(jpg|jpeg|png|gif|webp)(\\?.*)?$/i)) {\n        console.log('📸 Found direct image URL');\n        media.push({\n          type: 'image',\n          src: redditData.url,\n          width: 0,\n          height: 0,\n          alt: redditData.title,\n        });\n      }\n      // Video formats\n      else if (directUrl.match(/\\.(mp4|webm|mov|avi)(\\?.*)?$/i)) {\n        console.log('🎬 Found direct video URL');\n\n        // Try to generate poster from preview if available\n        let posterUrl = '';\n        if (redditData.preview?.images?.[0]?.source?.url) {\n          posterUrl = redditData.preview.images[0].source.url.replace(/&amp;/g, '&');\n        }\n\n        media.push({\n          type: 'video',\n          src: redditData.url,\n          width: 0,\n          height: 0,\n          isGif: false,\n          poster: posterUrl,\n        });\n      }\n      // Special handling for imgur\n      else if (directUrl.includes('imgur.com') && !directUrl.includes('.gifv')) {\n        console.log('🌐 Found Imgur link');\n        // Convert imgur links to direct image links\n        const imgurId = directUrl.match(/imgur\\.com\\/([a-zA-Z0-9]+)/)?.[1];\n        if (imgurId && !directUrl.includes('/a/') && !directUrl.includes('/gallery/')) {\n          // Try both jpg and png\n          media.push({\n            type: 'image',\n            src: `https://i.imgur.com/${imgurId}.jpg`,\n            width: 0,\n            height: 0,\n            alt: redditData.title,\n          });\n        }\n      }\n      // i.redd.it images\n      else if (directUrl.includes('i.redd.it')) {\n        console.log('🖼️ Found i.redd.it image');\n        media.push({\n          type: 'image',\n          src: redditData.url,\n          width: 0,\n          height: 0,\n          alt: redditData.title,\n        });\n      }\n    }\n\n    console.log('📋 Final Reddit media array:', media);\n    return media;\n  };\n\n  const mediaContent = getMediaContent();\n  // Lib's `MediaCarousel` expects items shaped like the carousel-utils-stub\n  // `MediaItem` (which has a required `id`). Reddit constructs items without an\n  // `id`, so we synthesize one at the boundary. Keep the runtime cast — lib\n  // carousel keys by index and only reads `.type`/`.src`/`.poster`/`.alt`.\n  const carouselItems: CarouselMediaItem[] = mediaContent.map((m, i) => ({\n    id: `reddit-${i}`,\n    type: m.type,\n    src: m.src,\n    poster: m.poster,\n    alt: m.alt,\n    width: m.width,\n    height: m.height,\n  }));\n\n  // Format numbers using utility function\n  const formatNumber = formatLargeNumber;\n\n  const truncateText = (text: string, maxLength: number = 600) => {\n    if (text.length <= maxLength) return text;\n    return text.slice(0, maxLength) + '...';\n  };\n\n  return (\n    <RedditContainer>\n      <div className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-card\">\n        {/* Header with Profile Picture */}\n        <div className=\"border-b border-ods-border p-4\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center space-x-3\">\n              {/* Lazy-loaded User Profile Picture */}\n              <div className=\"h-8 w-8 flex-shrink-0 overflow-hidden rounded-full\">\n                <RedditProfilePic username={redditData.author} />\n              </div>\n              <div>\n                <p className=\"text-ods-text-primary text-h6\">r/{redditData.subreddit}</p>\n                <div className=\"flex items-center space-x-2 text-ods-text-secondary text-h6\">\n                  <UserIcon />\n                  <span>u/{redditData.author}</span>\n                  <ClockIcon />\n                  <span>{formatTimeAgo(redditData.created_utc)}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Content - Matching Twitter Style */}\n        <div className=\"p-4\">\n          <h3 className=\"mb-3 text-ods-text-primary text-h3\">{redditData.title}</h3>\n\n          {redditData.selftext && (\n            <div\n              className=\"mb-4 overflow-hidden text-ods-text-secondary text-h6\"\n              style={{ maxHeight: `${maxWidth - 200}px` }}\n            >\n              <p className=\"whitespace-pre-wrap\">{truncateText(redditData.selftext)}</p>\n            </div>\n          )}\n\n          {/* Enhanced Media Section with Carousel */}\n          {carouselItems.length > 0 && <MediaCarousel media={carouselItems} aspectRatio=\"16/9\" />}\n\n          {/* Stats - Matching Twitter Style */}\n          <div className=\"flex items-center space-x-6 text-ods-text-secondary text-h6\">\n            <div className=\"flex items-center space-x-1\">\n              <ArrowUpIcon />\n              <span>{formatNumber(redditData.ups)} upvotes</span>\n            </div>\n            <div className=\"flex items-center space-x-1\">\n              <MessageCircleIcon />\n              <span>{formatNumber(redditData.num_comments)} comments</span>\n            </div>\n          </div>\n        </div>\n\n        {/* Footer - Matching Twitter Style */}\n        <div className=\"border-t border-ods-border bg-ods-bg-surface px-4 py-3\">\n          <a\n            href={url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center space-x-2 text-ods-accent transition-colors text-h6 hover:opacity-80\"\n          >\n            <ExternalLinkIcon />\n            <span>View on Reddit</span>\n          </a>\n        </div>\n      </div>\n    </RedditContainer>\n  );\n}\n","// Unified Social Media Embed fetch layer.\n// Shared between Reddit and Twitter embed clients for consistent behavior.\n//\n// SIMPLIFIED (2026-08, \"all 58 Reddit embeds broken\" incident): the server\n// proxy is now the single authority — it owns a DURABLE database cache and\n// the authenticated upstream fetch (Reddit's `.json` API 403s all\n// unauthenticated clients, so the old browser direct-fetch could never work\n// again). The old client-side hierarchy (cache-only preflight round-trip,\n// direct browser fetch, client→server POST cache pushes, background\n// refreshes) was flaky machinery layered over an unreliable server cache and\n// is DELETED — do not reintroduce it. Client flow is exactly:\n//\n//   memory (per-pageload dedupe) → GET proxy (server cache + upstream)\n//\n// Endpoint paths are passed in via `apiEndpoint` on each call so embedders\n// can route through their own reverse proxy (e.g. `/content/api/blog/reddit-proxy`).\n\n// Per-pageload memory cache shared between component instances. The cache is\n// payload-agnostic — each caller's `dataValidator` is what turns an entry back\n// into its own concrete shape.\nconst dataCache = new Map<string, unknown>();\n\n// In-flight GET dedupe: several embeds of the same URL mount concurrently\n// (before any of them has populated the memory cache).\nconst inflightRequests = new Map<string, Promise<unknown>>();\n\nexport interface FetchOptions<TData> {\n  platform: 'reddit' | 'twitter';\n  url: string;\n  apiEndpoint: string;\n  /** Type guard for the proxy payload. This module never knows the shape, so\n   *  the caller's guard is the single place `unknown` becomes `TData`. */\n  dataValidator: (data: unknown) => data is TData;\n  onDataUpdate: (data: TData) => void;\n  onError: (error: string) => void;\n  onLoading: (loading: boolean) => void;\n}\n\nexport class SocialEmbedCache {\n  private static instance: SocialEmbedCache;\n\n  static getInstance(): SocialEmbedCache {\n    if (!SocialEmbedCache.instance) {\n      SocialEmbedCache.instance = new SocialEmbedCache();\n    }\n    return SocialEmbedCache.instance;\n  }\n\n  getFromMemory(url: string): unknown {\n    return dataCache.get(url) ?? null;\n  }\n\n  setInMemory(url: string, data: unknown): void {\n    dataCache.set(url, data);\n  }\n\n  /**\n   * Memory → proxy. Kept the historical name so the embed clients'\n   * call sites are unchanged.\n   */\n  async fetchWithHierarchy<TData>(options: FetchOptions<TData>): Promise<void> {\n    const { url, platform, apiEndpoint, dataValidator, onDataUpdate, onError, onLoading } = options;\n\n    const memoryData = this.getFromMemory(url);\n    if (dataValidator(memoryData)) {\n      onDataUpdate(memoryData);\n      onLoading(false);\n      return;\n    }\n\n    try {\n      const data = await this.fetchFromProxy(url, apiEndpoint, dataValidator);\n      if (data) {\n        this.setInMemory(url, data);\n        onDataUpdate(data);\n        onLoading(false);\n        return;\n      }\n      // Structured 404 from the proxy — the content itself is unavailable.\n      onError(`This ${platform === 'reddit' ? 'Reddit post' : 'tweet'} could not be loaded.`);\n    } catch (error) {\n      console.log(`❌ [${platform} embed] Proxy fetch failed for: ${url}`, error);\n      onError(`Unable to load ${platform} content`);\n    }\n    onLoading(false);\n  }\n\n  /**\n   * Returns the validated payload, `null` for a structured \"content\n   * unavailable\" 404, and throws for everything else. Concurrent calls for\n   * the same URL share one request.\n   */\n  private fetchFromProxy<TData>(\n    url: string,\n    apiEndpoint: string,\n    dataValidator: (data: unknown) => data is TData,\n  ): Promise<TData | null> {\n    const inflight = inflightRequests.get(url);\n    // A shared in-flight request resolves to `unknown` (the map is\n    // payload-agnostic) — re-run THIS caller's guard on the result rather\n    // than asserting it. Rejections still propagate to every sharer.\n    if (inflight) return inflight.then(data => (dataValidator(data) ? data : null));\n\n    const request = (async (): Promise<TData | null> => {\n      const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`, {\n        signal: AbortSignal.timeout(15_000),\n      });\n      if (response.ok) {\n        const data: unknown = await response.json();\n        if (dataValidator(data)) return data;\n        throw new Error('Server proxy returned invalid data shape');\n      }\n      if (response.status === 404) return null;\n      throw new Error(`Server proxy failed: ${response.status}`);\n    })();\n\n    // Hold the tracked promise instead of storing it and reading it back out\n    // of the map: it is the same object, and the caller now gets it directly\n    // rather than through a lookup whose key the `finally` above will delete.\n    const tracked = request.finally(() => inflightRequests.delete(url));\n    inflightRequests.set(url, tracked);\n    return tracked;\n  }\n}\n\nexport const socialCache = SocialEmbedCache.getInstance();\n","'use client';\n\nimport { useState, useRef, memo, useCallback, type KeyboardEvent, type TouchEvent } from 'react';\nimport { Image, Link } from '../embed-shims';\nimport { cn } from '../utils/cn';\nimport type { MediaItem } from '../utils/media-carousel-utils-stub';\nimport { Video } from './features/video';\nimport { ImageOffIcon } from './icons-v2-generated/audio-and-visual/image-off-icon';\nimport { Button } from './ui/button';\n\n// Navigation icons\nconst ChevronLeftIcon = () => (\n  <svg width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <polyline points=\"15,18 9,12 15,6\" />\n  </svg>\n);\n\nconst ChevronRightIcon = () => (\n  <svg width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <polyline points=\"9,18 15,12 9,6\" />\n  </svg>\n);\n\n/**\n * THE media carousel (Figma 4033:90570) — the single slide viewer for the\n * whole codebase: OpenFrame homepage categories, openmsp vendor screenshots,\n * who-am-i profiles, reddit embeds. One viewport + dot pagination; no\n * thumbnail rail, no slide counter (both removed with the 2026-07 redesign).\n *\n * - Slides render through the SSoT `<Video>` (MuxPlayer / lite-youtube\n *   facade) for `video`/`youtube` items and a broken-image-resilient\n *   `<Image>` shim for `image` items.\n * - Dots: 24px hit target, 8px dot, active = platform accent (per design).\n * - Hover-revealed prev/next arrows (`showArrows`), keyboard arrows, and\n *   touch swipe all preserved from the previous generation.\n */\nexport interface MediaCarouselProps {\n  media: MediaItem[];\n  className?: string;\n  /** Use aspect ratio instead of height classes for stable dimensions */\n  aspectRatio?: '16/9' | '4/3' | '3/2' | '1/1';\n  /** Hover-revealed prev/next arrows (default true; dots always render). */\n  showArrows?: boolean;\n  /** How content should fit within the container */\n  objectFit?: 'contain' | 'cover';\n  /** When true, the first slide's image or YouTube poster uses `priority` / eager load (e.g. vendor detail LCP). */\n  posterPriority?: boolean;\n  /** Optional per-host rewrite for image URLs (e.g. the hub proxies plain\n   *  `http://` sources to avoid mixed content). Applied to image slide srcs\n   *  and derived thumbnails only — video/youtube URLs go to `<Video>` raw. */\n  transformImageSrc?: (url: string) => string;\n  /** Always reserve the dot-row height (24px) even when there is nothing to\n   *  paginate — for surfaces that swap media sets in place (the OpenFrame\n   *  categories showcase), so 1-slide vs N-slide sets never shift layout.\n   *  Default false: standalone carousels (vendor page) collapse the row. */\n  reserveDotRow?: boolean;\n}\n\nexport const MediaCarousel = memo(function MediaCarouselImpl({\n  media,\n  className,\n  aspectRatio = '16/9',\n  showArrows = true,\n  objectFit = 'contain',\n  posterPriority = false,\n  transformImageSrc,\n  reserveDotRow = false,\n}: MediaCarouselProps) {\n  const [requestedIndex, setCurrentIndex] = useState(0);\n  const [touchStart, setTouchStart] = useState<number | null>(null);\n  const [touchEnd, setTouchEnd] = useState<number | null>(null);\n  /** Resource URLs that failed to load (e.g. 404, blocked CDN) */\n  const [brokenResourceUrls, setBrokenResourceUrls] = useState<Record<string, true>>({});\n  /** The src whose <Image> finished loading — keyed by URL (not a shared\n   *  boolean) so a late onLoad from a previous slide can never reveal the\n   *  current image prematurely; slide changes need no reset effect. */\n  const [loadedImageSrc, setLoadedImageSrc] = useState<string | null>(null);\n  const carouselRef = useRef<HTMLDivElement>(null);\n\n  const onImageError = useCallback((originalUrl: string) => {\n    const key = originalUrl.trim() || '__missing_src__';\n    setBrokenResourceUrls(b => (b[key] ? b : { ...b, [key]: true }));\n  }, []);\n\n  // Clamp `currentIndex` whenever `media` shrinks (e.g. an admin removes a\n  // slide while the user is on the last one) so the dot active state +\n  // `currentItem` lookup stay in sync. Derived, not written back into state\n  // from an effect: `media.length` is known in the very render that shrank, so\n  // clamping here means the carousel never paints the out-of-range frame the\n  // effect used to correct on the pass after.\n  const currentIndex = media.length > 0 && requestedIndex >= media.length ? media.length - 1 : requestedIndex;\n\n  const currentItem = media[currentIndex] || media[0];\n\n  // Navigation — `<Video>` owns its own play/pause lifecycle. Both step from\n  // the CLAMPED index, so an out-of-range request left over from a shrunk\n  // `media` advances from the slide actually on screen rather than from a\n  // position nobody can see.\n  const nextSlide = useCallback(() => {\n    setCurrentIndex(prev => (Math.min(prev, media.length - 1) + 1) % media.length);\n  }, [media.length]);\n\n  const prevSlide = useCallback(() => {\n    setCurrentIndex(prev => (Math.min(prev, media.length - 1) - 1 + media.length) % media.length);\n  }, [media.length]);\n\n  const selectSlide = useCallback((index: number) => {\n    setCurrentIndex(prev => (index === prev ? prev : index));\n  }, []);\n\n  // Keyboard navigation - only when carousel is focused\n  const handleKeyDown = useCallback(\n    (e: KeyboardEvent) => {\n      if (media.length <= 1) return;\n\n      if (e.key === 'ArrowLeft') {\n        e.preventDefault();\n        prevSlide();\n      } else if (e.key === 'ArrowRight') {\n        e.preventDefault();\n        nextSlide();\n      }\n    },\n    [nextSlide, prevSlide, media.length],\n  );\n\n  // Early return if no media provided\n  if (!media || media.length === 0) {\n    return null;\n  }\n\n  // Additional safety check\n  if (!currentItem) {\n    return null;\n  }\n\n  // Touch/swipe handling\n  const minSwipeDistance = 50;\n\n  const onTouchStart = (e: TouchEvent) => {\n    setTouchEnd(null);\n    setTouchStart(e.targetTouches[0].clientX);\n  };\n\n  const onTouchMove = (e: TouchEvent) => {\n    setTouchEnd(e.targetTouches[0].clientX);\n  };\n\n  const onTouchEnd = () => {\n    // Explicit null checks — a swipe starting/ending at the viewport edge has\n    // clientX === 0, which a truthiness check would wrongly discard.\n    if (touchStart === null || touchEnd === null) return;\n\n    const distance = touchStart - touchEnd;\n    const isLeftSwipe = distance > minSwipeDistance;\n    const isRightSwipe = distance < -minSwipeDistance;\n\n    if (isLeftSwipe && media.length > 1) {\n      nextSlide();\n    }\n    if (isRightSwipe && media.length > 1) {\n      prevSlide();\n    }\n  };\n\n  const displayImageSrc = (url: string): string => (transformImageSrc ? transformImageSrc(url) : url);\n\n  // Render any playable slide via the SSoT `<Video>` — its default `auto`\n  // routing owns the youtube-vs-file decision (lite-youtube facade vs\n  // MuxPlayer), so the carousel keeps NO url-detection fork of its own.\n  const renderVideo = (item: MediaItem, index: number) => (\n    <Video\n      key={`video-${index}-${item.src}`}\n      url={item.src}\n      poster={item.poster ? displayImageSrc(item.poster) : undefined}\n      title={item.alt || `Video ${index + 1}`}\n      muted\n      layout=\"fill\"\n      priority={Boolean(posterPriority && index === 0)}\n      className=\"h-full w-full bg-black\"\n    />\n  );\n\n  const renderImageUnavailable = (failedUrl: string) => (\n    <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-3 bg-ods-card px-6 text-center\">\n      <ImageOffIcon className=\"size-8 text-ods-text-secondary\" />\n      <p className=\"max-w-md text-ods-text-secondary text-h6\">This image is no longer available at the original URL.</p>\n      <Link\n        href={failedUrl}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"break-all text-ods-accent underline text-h6\"\n      >\n        Open link\n      </Link>\n    </div>\n  );\n\n  // Render image — skeleton until onLoad; keep Image opacity-0 until then to\n  // avoid the browser broken-image icon flash.\n  const renderImage = (item: MediaItem, index: number) => {\n    const rawSrc = typeof item.src === 'string' ? item.src.trim() : '';\n    if (!rawSrc) {\n      return (\n        <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-3 bg-ods-card px-6 text-center\">\n          <p className=\"max-w-md text-ods-text-secondary text-h6\">No image URL for this slide.</p>\n        </div>\n      );\n    }\n    if (brokenResourceUrls[rawSrc]) {\n      return renderImageUnavailable(rawSrc);\n    }\n    const mainImagePriority = Boolean(posterPriority && index === 0);\n    const imageLoaded = loadedImageSrc === rawSrc;\n    return (\n      <div className=\"absolute inset-0 bg-ods-bg\">\n        {!imageLoaded && <div className=\"absolute inset-0 z-[1] animate-pulse bg-ods-card/90\" aria-hidden />}\n        <Image\n          src={displayImageSrc(rawSrc)}\n          alt={item.alt || `Media ${index + 1}`}\n          className={cn(\n            `h-full w-full object-${objectFit} relative z-[2]`,\n            !imageLoaded && 'opacity-0',\n            imageLoaded && 'opacity-100 transition-opacity duration-200',\n          )}\n          priority={mainImagePriority}\n          loading={mainImagePriority ? 'eager' : 'lazy'}\n          sizes=\"(max-width: 1279px) 100vw, 1428px\"\n          width={1428}\n          height={802}\n          unoptimized\n          onLoad={() => setLoadedImageSrc(rawSrc)}\n          onError={() => onImageError(rawSrc)}\n        />\n      </div>\n    );\n  };\n\n  // Render main media item — video AND youtube both route through the SSoT\n  // `<Video>` (its `auto` kind owns the detection).\n  const renderMainMedia = (item: MediaItem, index: number) => {\n    switch (item.type) {\n      case 'youtube':\n      case 'video':\n        return renderVideo(item, index);\n      case 'image':\n      default:\n        return renderImage(item, index);\n    }\n  };\n\n  // Get CSS for aspect ratio\n  const getAspectRatioClass = () => {\n    switch (aspectRatio) {\n      case '4/3':\n        return 'aspect-[4/3]';\n      case '3/2':\n        return 'aspect-[3/2]';\n      case '1/1':\n        return 'aspect-square';\n      case '16/9':\n      default:\n        return 'aspect-video';\n    }\n  };\n\n  return (\n    <div className={cn('flex flex-col items-center gap-[var(--spacing-system-l)]', className)}>\n      {/* Main Display Area with Fixed Aspect Ratio — Figma media frame:\n          rounded-md, ods border, semantic elevation token. */}\n      <div\n        ref={carouselRef}\n        className={cn(\n          'group relative w-full overflow-hidden rounded-md border border-ods-border bg-ods-bg',\n          '[box-shadow:var(--shadow-media-frame)]',\n          getAspectRatioClass(),\n        )}\n        onTouchStart={onTouchStart}\n        onTouchMove={onTouchMove}\n        onTouchEnd={onTouchEnd}\n        onKeyDown={media.length > 1 ? handleKeyDown : undefined}\n        tabIndex={media.length > 1 ? 0 : undefined}\n        role={media.length > 1 ? 'region' : undefined}\n        aria-label={media.length > 1 ? 'Media carousel, use arrow keys to navigate' : undefined}\n      >\n        {/* Media content */}\n        {renderMainMedia(currentItem, currentIndex)}\n\n        {/* Navigation Arrows — hover-revealed, only with multiple items.\n            The common <Button> (transparent) with overlay positioning. */}\n        {showArrows && media.length > 1 && (\n          <>\n            <Button\n              variant=\"transparent\"\n              onClick={prevSlide}\n              className=\"absolute left-3 top-1/2 z-10 h-auto -translate-y-1/2 rounded-full bg-black/50 p-2 text-ods-text-on-dark opacity-0 transition-opacity duration-200 hover:bg-black/70 group-hover:opacity-100 md:h-auto\"\n              aria-label=\"Previous media\"\n            >\n              <ChevronLeftIcon />\n            </Button>\n\n            <Button\n              variant=\"transparent\"\n              onClick={nextSlide}\n              className=\"absolute right-3 top-1/2 z-10 h-auto -translate-y-1/2 rounded-full bg-black/50 p-2 text-ods-text-on-dark opacity-0 transition-opacity duration-200 hover:bg-black/70 group-hover:opacity-100 md:h-auto\"\n              aria-label=\"Next media\"\n            >\n              <ChevronRightIcon />\n            </Button>\n          </>\n        )}\n      </div>\n\n      {/* Dot pagination (Figma) — 24px hit target, 8px dot, active accent.\n          Plain grouped buttons (NOT tablist: no roving tabIndex/arrow-key\n          model here — aria-current marks the active slide instead). */}\n      {(media.length > 1 || reserveDotRow) && (\n        <div className=\"flex h-6 items-center gap-[var(--spacing-system-xs)]\" role=\"group\" aria-label=\"Choose slide\">\n          {media.length > 1 &&\n            media.map((item, index) => {\n              const isActive = index === currentIndex;\n              return (\n                <Button\n                  key={index}\n                  variant=\"transparent\"\n                  aria-current={isActive ? 'true' : undefined}\n                  aria-label={`Go to slide ${index + 1}`}\n                  onClick={() => selectSlide(index)}\n                  className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full p-0 md:h-6\"\n                >\n                  <span\n                    className={cn(\n                      'size-2 rounded-full transition-colors duration-150',\n                      isActive ? 'bg-ods-accent' : 'bg-ods-bg-surface hover:bg-ods-bg-surface-hover',\n                    )}\n                  />\n                </Button>\n              );\n            })}\n        </div>\n      )}\n    </div>\n  );\n});\n\n// The carousel's item shape is the lib-wide SSOT — re-exported here so hub\n// code can `import type { MediaItem } from '.../components'`.\nexport type { MediaItem };\n","'use client';\n\nimport { useState, useEffect, useRef } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { socialCache } from '../../utils/social-embed-cache';\nimport { XLogo } from '../icons/x-logo';\nimport { TwitterContainer } from './embed-container';\nimport { useRichMarkdownRuntime } from './rich-markdown-runtime';\n\n// Using inline SVG icons to avoid dependency issues\nconst MessageCircleIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z\" />\n  </svg>\n);\n\nconst ExternalLinkIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6\" />\n    <polyline points=\"15,3 21,3 21,9\" />\n    <line x1=\"10\" y1=\"14\" x2=\"21\" y2=\"3\" />\n  </svg>\n);\n\nconst HeartIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z\" />\n  </svg>\n);\n\nconst RepeatIcon = () => (\n  <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <polyline points=\"17,1 21,5 17,9\" />\n    <path d=\"M3 11V9a4 4 0 0 1 4-4h14\" />\n    <polyline points=\"7,23 3,19 7,15\" />\n    <path d=\"M21 13v2a4 4 0 0 1-4 4H3\" />\n  </svg>\n);\n\nconst ClockIcon = () => (\n  <svg width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <polyline points=\"12,6 12,12 16,14\" />\n  </svg>\n);\n\nconst UserIcon = () => (\n  <svg width=\"12\" height=\"12\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n    <path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\" />\n    <circle cx=\"12\" cy=\"7\" r=\"4\" />\n  </svg>\n);\n\n// X glyph: the lib's standard XLogo (color follows the text context).\nconst XIcon = () => <XLogo className=\"h-5 w-5\" color=\"currentColor\" />;\n\ninterface TwitterOEmbedResponse {\n  url: string;\n  author_name: string;\n  author_url: string;\n  html: string;\n  width: number;\n  height: number;\n  type: string;\n  cache_age: string;\n  provider_name: string;\n  provider_url: string;\n  version: string;\n}\n\ninterface TwitterEmbedProps {\n  url: string;\n  tweetId?: string;\n  maxWidth?: number;\n}\n\nexport function TwitterEmbedClient({ url, tweetId, maxWidth = 700 }: TwitterEmbedProps) {\n  const { twitterProxyUrl } = useRichMarkdownRuntime();\n  const [tweetData, setTweetData] = useState<TwitterOEmbedResponse | null>(null);\n  const [fetchLoading, setLoading] = useState(true);\n  const [fetchError, setError] = useState<string | null>(null);\n  const fetchedForRef = useRef<string | null>(null);\n\n  // Extract tweet ID from URL if not provided\n  const extractedTweetId = tweetId || url.match(/status\\/(\\d+)/)?.[1];\n\n  // A url we cannot pull an id out of is knowable from the props alone: there\n  // is nothing to request and nothing to wait for. Derived here instead of\n  // written into state from the effect, which cost a spinner frame plus a\n  // second render pass to say something the FIRST render already knew.\n  const error = extractedTweetId ? fetchError : 'Invalid tweet URL or ID';\n  const loading = extractedTweetId ? fetchLoading : false;\n\n  // Normalize the Twitter URL\n  const tweetUrl =\n    url.includes('twitter.com') || url.includes('x.com')\n      ? url\n      : `https://twitter.com/twitter/status/${extractedTweetId}`;\n\n  useEffect(() => {\n    // Only run once\n    // Fetch once PER tweet: StrictMode's double mount is deduped, and a\n    // genuinely different `tweetUrl` now refetches instead of showing the\n    // first embed forever.\n    if (fetchedForRef.current === tweetUrl) return;\n    fetchedForRef.current = tweetUrl;\n\n    // Reported from the derived `error` / `loading` above.\n    if (!extractedTweetId) return;\n\n    // Stale-response guard. Deliberately keyed on `fetchedForRef` rather than a\n    // per-run `cancelled` boolean: the ref above makes StrictMode's second\n    // effect run a no-op, so a per-run flag would cancel the ONLY in-flight\n    // request and leave the embed spinning forever. Comparing against the ref\n    // suppresses only genuinely superseded responses — without it, a `tweetUrl`\n    // change lets the previous tweet's payload land last and render the WRONG\n    // tweet (or drop the spinner while the new fetch is still running).\n    const isCurrent = () => fetchedForRef.current === tweetUrl;\n\n    // Use centralized cache hierarchy\n    // `fetchWithHierarchy` never rejects — failures arrive through `onError`.\n    void socialCache.fetchWithHierarchy({\n      platform: 'twitter',\n      url: tweetUrl,\n      apiEndpoint: twitterProxyUrl,\n      dataValidator: (data): data is TwitterOEmbedResponse =>\n        typeof data === 'object' && data !== null && typeof (data as TwitterOEmbedResponse).html === 'string',\n      onDataUpdate: data => {\n        if (isCurrent()) setTweetData(data);\n      },\n      onError: errorMsg => {\n        if (isCurrent()) setError(errorMsg);\n      },\n      onLoading: isLoading => {\n        if (isCurrent()) setLoading(isLoading);\n      },\n    });\n  }, [tweetUrl, twitterProxyUrl, extractedTweetId]);\n\n  if (loading) {\n    return (\n      <TwitterContainer>\n        <div className=\"animate-pulse rounded-lg border border-ods-border bg-ods-card p-6\">\n          <div className=\"mb-4 flex items-center space-x-3\">\n            <div className=\"h-12 w-12 rounded-full bg-ods-border\"></div>\n            <div>\n              <div className=\"mb-2 h-4 w-32 rounded bg-ods-border\"></div>\n              <div className=\"h-3 w-24 rounded bg-ods-border\"></div>\n            </div>\n          </div>\n          <div className=\"mb-4 space-y-2\">\n            <div className=\"h-4 w-full rounded bg-ods-border\"></div>\n            <div className=\"h-4 w-3/4 rounded bg-ods-border\"></div>\n          </div>\n          <div className=\"flex items-center space-x-4\">\n            <div className=\"h-4 w-16 rounded bg-ods-border\"></div>\n            <div className=\"h-4 w-16 rounded bg-ods-border\"></div>\n            <div className=\"h-4 w-16 rounded bg-ods-border\"></div>\n          </div>\n        </div>\n      </TwitterContainer>\n    );\n  }\n\n  if (error || !tweetData) {\n    return (\n      <TwitterContainer>\n        <div className=\"rounded-lg border border-ods-border bg-ods-card p-6\">\n          <div className=\"mb-4 flex items-center space-x-3 text-ods-text-secondary\">\n            <XIcon />\n            <span>Tweet unavailable</span>\n          </div>\n\n          <div className=\"text-center\">\n            <p className=\"mb-4 text-ods-text-secondary text-h6\">\n              This tweet could not be loaded. It may have been deleted, made private, or the account may be suspended.\n            </p>\n            <a\n              href={url}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex items-center space-x-2 rounded-md bg-ods-bg-surface px-4 py-2 text-ods-text-primary transition-colors text-h6 hover:bg-ods-bg-active\"\n            >\n              <XIcon />\n              <span>View on X</span>\n            </a>\n          </div>\n        </div>\n      </TwitterContainer>\n    );\n  }\n\n  // Parse the HTML to extract detailed tweet information and media\n  const parser = new DOMParser();\n  const doc = parser.parseFromString(tweetData.html, 'text/html');\n  const blockquote = doc.querySelector('blockquote');\n\n  // Extract tweet text (remove attribution line)\n  const fullText = blockquote?.textContent || '';\n  const tweetText = fullText.replace(/- .* \\(@.*\\).*$/, '').trim();\n\n  // Extract username from author_url (e.g., https://twitter.com/username)\n  const username = tweetData.author_url ? tweetData.author_url.split('/').pop() : '';\n\n  // Extract any links from the tweet\n  const links = Array.from(blockquote?.querySelectorAll('a') || [])\n    .map(link => ({\n      url: link.href,\n      text: link.textContent || link.href,\n    }))\n    .filter(link => !link.url.includes('twitter.com') && !link.url.includes('x.com'));\n\n  // Format time (simulated - we don't have real timestamp from oEmbed)\n  const formatTime = () => {\n    return 'on X'; // Simplified since we don't have actual timestamp\n  };\n\n  const truncateText = (text: string, maxLength: number = 600) => {\n    if (text.length <= maxLength) return text;\n    return text.slice(0, maxLength) + '...';\n  };\n\n  // Profile picture URL using Unavatar service.\n  // The hub used to proxy this via `useProxiedImageUrl` (chat runtime), but\n  // docs / blog pages don't mount a chat runtime, so we fetch unavatar\n  // directly. Embedders that need a proxy can register a runtime later.\n  const getProfilePicUrl = (handle: string | undefined) => {\n    if (!handle) return '';\n    return `https://unavatar.io/twitter/${handle}`;\n  };\n\n  return (\n    <TwitterContainer>\n      <div className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-card\">\n        {/* Header with Profile Picture */}\n        <div className=\"border-b border-ods-border p-4\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center space-x-3\">\n              {/* User Profile Picture */}\n              <div className=\"h-8 w-8 flex-shrink-0 overflow-hidden rounded-full\">\n                <Image\n                  src={getProfilePicUrl(username)}\n                  alt={`${tweetData.author_name} profile picture`}\n                  className=\"h-full w-full object-cover\"\n                  width={32}\n                  height={32}\n                  unoptimized\n                  onError={e => {\n                    // Simple fallback without state updates\n                    const target = e.target as HTMLImageElement;\n                    target.src =\n                      'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0yMCAyMXYtMmE0IDQgMCAwIDAtNC00SDhhNCA0IDAgMCAwLTQgNHYyIi8+PGNpcmNsZSBjeD0iMTIiIGN5PSI3IiByPSI0Ii8+PC9zdmc+';\n                  }}\n                />\n              </div>\n              <div>\n                <p className=\"text-ods-text-primary text-h6\">@{username}</p>\n                <div className=\"flex items-center space-x-2 text-ods-text-secondary text-h6\">\n                  <UserIcon />\n                  <span>{tweetData.author_name}</span>\n                  <ClockIcon />\n                  <span>{formatTime()}</span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        {/* Content */}\n        <div className=\"p-4\">\n          {tweetText && (\n            <div\n              className=\"mb-4 overflow-hidden text-ods-text-secondary text-h6\"\n              style={{ maxHeight: `${maxWidth - 200}px` }}\n            >\n              <p className=\"whitespace-pre-wrap\">{truncateText(tweetText)}</p>\n            </div>\n          )}\n\n          {/* Links Section */}\n          {links.length > 0 && (\n            <div className=\"mb-4 space-y-2\">\n              {links.map((link, index) => (\n                <a\n                  key={index}\n                  href={link.url}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"inline-flex items-center space-x-2 text-[#1DA1F2] transition-colors text-h6 hover:text-ods-accent\"\n                >\n                  <ExternalLinkIcon />\n                  <span className=\"underline\">{link.text}</span>\n                </a>\n              ))}\n            </div>\n          )}\n\n          {/* Stats */}\n          <div className=\"flex items-center space-x-6 text-ods-text-secondary text-h6\">\n            <div className=\"flex items-center space-x-1\">\n              <HeartIcon />\n              <span>Likes</span>\n            </div>\n            <div className=\"flex items-center space-x-1\">\n              <RepeatIcon />\n              <span>Retweets</span>\n            </div>\n            <div className=\"flex items-center space-x-1\">\n              <MessageCircleIcon />\n              <span>Replies</span>\n            </div>\n          </div>\n        </div>\n\n        {/* Footer */}\n        <div className=\"border-t border-ods-border bg-ods-bg-surface px-4 py-3\">\n          <a\n            href={url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center space-x-2 text-ods-accent transition-colors text-h6 hover:opacity-80\"\n          >\n            <ExternalLinkIcon />\n            <span>View on X</span>\n          </a>\n        </div>\n      </div>\n    </TwitterContainer>\n  );\n}\n","'use client';\n\n/**\n * Rich-composition component overrides: the embed layer (Video SSOT,\n * Reddit/Twitter/LinkedIn embeds, OG link previews, Figma, MarkdownImage).\n *\n * These imports are deliberately isolated in this file — keeping them out\n * of the engine and the simple composition keeps the CHAT bundle free of\n * embed code (a runtime `variant` prop could not be tree-shaken; a\n * separate composition module can).\n */\nimport type { ElementContent } from 'hast';\nimport type { Components } from 'react-markdown';\nimport { ClaudeEmbed } from '../../../embeds/claude-embed';\nimport { FigmaEmbed } from '../../../embeds/figma-embed';\nimport { LinkedInEmbedClient } from '../../../embeds/linkedin-embed-client';\nimport { MarkdownImage } from '../../../embeds/markdown-image';\nimport { OGLinkPreview, OGLinkErrorBoundary } from '../../../embeds/og-link-preview';\nimport { RedditEmbedClient } from '../../../embeds/reddit-embed-client';\nimport { TwitterEmbedClient } from '../../../embeds/twitter-embed-client';\nimport { Video } from '../../../features/video';\nimport { buildStandardLeafRenderers, hasRenderableSrc, type MdRenderProps } from '../base-components';\nimport type { TextSizeElement } from '../text-size';\n\n/** Depth-first search of a hast node for the first `<a href>` matching `hostRe`. */\nfunction findFirstHref(node: ElementContent | undefined, hostRe: RegExp): string | null {\n  if (!node || node.type !== 'element') return null;\n  if (node.tagName === 'a') {\n    const href = node.properties.href;\n    if (typeof href === 'string' && hostRe.test(href)) return href;\n  }\n  for (const child of node.children) {\n    const found = findFirstHref(child, hostRe);\n    if (found) return found;\n  }\n  return null;\n}\n\n/**\n * Shortcode-expanded embed `<div>`s carry their payload on `data-*`\n * attributes (`processShortcodes` emits them). React's `div` prop type does\n * not model `data-*`, so the ones this renderer reads are declared here.\n */\ntype EmbedDivProps = MdRenderProps<'div'> & {\n  'data-video-id'?: string;\n  'data-post-url'?: string;\n  'data-tweet-url'?: string;\n  'data-url'?: string;\n  'data-figma-url'?: string;\n  'data-kind'?: string;\n  'data-title'?: string;\n};\n\nexport interface BuildRichEmbedOverridesOptions {\n  ogApiBaseUrl: string;\n  ogEndpointPath: string;\n  textSizes: Record<TextSizeElement, string>;\n}\n\n/**\n * Overrides spread LAST onto the engine's base map: `code` (fence-language\n * embeds), `div` (shortcode-expanded embeds), `img` (MarkdownImage with\n * runtime transformImageSrc), `video` (raw tags → Video SSOT).\n */\nexport function buildRichEmbedOverrides({\n  ogApiBaseUrl,\n  ogEndpointPath,\n  textSizes,\n}: BuildRichEmbedOverridesOptions): Partial<Components> {\n  // Standard `code` / `blockquote` / `div` come from the base SSOT — this\n  // module owns ONLY the embed special cases and delegates every\n  // fall-through, so the shared markup can never drift between the two\n  // compositions.\n  //\n  // The delegations below CALL those renderers as plain functions rather\n  // than rendering them as components (that is what the `as any` casts are\n  // for). That is only sound because `buildStandardLeafRenderers` is\n  // documented HOOK-FREE — if a hook ever appears in one of them, these\n  // call sites must become real elements (`<Standard.code {...props} />`)\n  // first.\n  const standard = buildStandardLeafRenderers({ textSizes });\n\n  return {\n    // Fence-language embeds (```youtube-embed etc.); everything else\n    // (mermaid, highlighted blocks, inline code) falls through to `standard`.\n    code: (codeProps: MdRenderProps<'code'>) => {\n      const { className: codeClassName, children } = codeProps;\n      const match = /language-(\\w+)/.exec(codeClassName || '');\n      const language = match ? match[1] : '';\n      const fenceText = () => String(children).replace(/\\n$/, '').trim();\n\n      if (language === 'youtube-embed') {\n        return <Video kind=\"youtube\" url={fenceText()} />;\n      }\n      if (language === 'reddit-embed') {\n        return <RedditEmbedClient url={fenceText()} />;\n      }\n      if (language === 'tweet-embed') {\n        return <TwitterEmbedClient url={fenceText()} />;\n      }\n      if (language === 'link-preview') {\n        return (\n          <OGLinkPreview\n            url={fenceText()}\n            variant=\"compact\"\n            enablePlaceholder={false}\n            apiBaseUrl={ogApiBaseUrl}\n            ogEndpointPath={ogEndpointPath}\n          />\n        );\n      }\n      if (language === 'figma-embed') {\n        return <FigmaEmbed url={fenceText()} height=\"70vh\" />;\n      }\n      if (language === 'linkedin-embed') {\n        return <LinkedInEmbedClient url={fenceText()} />;\n      }\n\n      return standard.code(codeProps);\n    },\n\n    // Shortcode-expanded embeds: <div class=\"youtube-embed\" data-video-id>…\n    div: (divProps: EmbedDivProps) => {\n      const {\n        className,\n        'data-video-id': videoId,\n        'data-post-url': postUrl,\n        'data-tweet-url': tweetUrl,\n        'data-url': dataUrl,\n        'data-kind': claudeKind,\n        'data-title': claudeTitle,\n        'data-figma-url': figmaUrl,\n      } = divProps;\n      // Each branch requires its payload attribute: a shortcode div that\n      // lost it used to hand the embed component `url={undefined}` and\n      // render a permanently broken player. Falling through to the plain\n      // `div` keeps the surrounding content intact instead.\n      if (className === 'youtube-embed' && videoId) {\n        return <Video kind=\"youtube\" url={videoId} />;\n      }\n      if (className === 'reddit-embed' && postUrl) {\n        return <RedditEmbedClient url={postUrl} />;\n      }\n      if (className === 'tweet-embed' && tweetUrl) {\n        return <TwitterEmbedClient url={tweetUrl} />;\n      }\n      if (className === 'link-preview') {\n        const url = dataUrl;\n        if (!url) {\n          console.warn('Invalid URL for link preview:', url);\n          return <div className=\"text-sm text-ods-text-secondary\">Invalid link</div>;\n        }\n        try {\n          new URL(url);\n          return (\n            <OGLinkErrorBoundary\n              fallback={<div className=\"text-sm text-ods-text-secondary\">Link preview unavailable</div>}\n            >\n              <OGLinkPreview\n                url={url}\n                variant=\"compact\"\n                enablePlaceholder={false}\n                apiBaseUrl={ogApiBaseUrl}\n                ogEndpointPath={ogEndpointPath}\n              />\n            </OGLinkErrorBoundary>\n          );\n        } catch (e) {\n          console.warn('Malformed URL for link preview:', url, e);\n          return <div className=\"text-sm text-ods-text-secondary\">Malformed URL: {url}</div>;\n        }\n      }\n      if (className === 'figma-embed' && figmaUrl) {\n        return <FigmaEmbed url={figmaUrl} height=\"70vh\" />;\n      }\n      if (className === 'claude-embed' && dataUrl) {\n        return (\n          <ClaudeEmbed\n            url={dataUrl}\n            kind={claudeKind === 'design' ? 'design' : 'artifact'}\n            title={claudeTitle}\n            height=\"70vh\"\n          />\n        );\n      }\n      if (className === 'linkedin-embed' && postUrl) {\n        return <LinkedInEmbedClient url={postUrl} />;\n      }\n      return standard.div(divProps);\n    },\n\n    // Pasted-from-the-platform embed markup lives in published blog posts as\n    // `<blockquote class=\"reddit-embed-bq\">…<a href=\"post-url\">` (58 blocks\n    // across 9 posts) — and Twitter's equivalent is\n    // `<blockquote class=\"twitter-tweet\">…<a href=\"status-url\">`. Each pair\n    // ships a `widgets.js` loader script, which `processShortcodes` strips\n    // from the source text (Step 1.5). The composition rehydrates the\n    // blockquote itself: extract the post URL from the first matching link\n    // and render the platform's embed-client SSOT. Other blockquotes fall\n    // through to the engine's base blockquote.\n    blockquote: (bqProps: MdRenderProps<'blockquote'>) => {\n      const { node, className } = bqProps;\n      const classNames: string = Array.isArray(className) ? className.join(' ') : (className ?? '');\n      if (classNames.includes('reddit-embed-bq')) {\n        const postUrl = findFirstHref(node, /reddit\\.com/);\n        if (postUrl) return <RedditEmbedClient url={postUrl} />;\n      }\n      if (classNames.includes('twitter-tweet')) {\n        const tweetUrl = findFirstHref(node, /(?:twitter\\.com|x\\.com)\\/[^/]+\\/status\\//);\n        if (tweetUrl) return <TwitterEmbedClient url={tweetUrl} />;\n      }\n      return standard.blockquote(bqProps);\n    },\n\n    // In-article images: MarkdownImage reads `transformImageSrc` from the\n    // rich-markdown runtime (Supabase optimization on the hub, identity\n    // elsewhere). Guard against empty `![]()`.\n    img: ({ src, alt }: MdRenderProps<'img'>) => {\n      // Empty-`![]()` guard shared with the base `img` renderer.\n      if (!hasRenderableSrc(src)) return null;\n      return <MarkdownImage src={src.trim()} alt={alt} />;\n    },\n\n    // Raw <video> tags in stored content (blog publisher injection) route\n    // to the <Video> SSOT (MuxPlayer) so HLS manifests play everywhere.\n    video: ({ src, poster, className }: MdRenderProps<'video'>) => {\n      if (!hasRenderableSrc(src)) return null;\n      return (\n        <div className={`overflow-hidden ${className || 'my-8 w-full rounded-lg'}`}>\n          <div className=\"aspect-video w-full\">\n            <Video kind=\"file\" url={src.trim()} poster={typeof poster === 'string' ? poster : undefined} />\n          </div>\n        </div>\n      );\n    },\n  };\n}\n","/**\n * Social platform helpers — normalization, host classification, the shared link\n * type, and the \"find the link for a platform\" picker.\n *\n * **This module does NOT own the list of platforms.** That list lives in the\n * `social_platforms` table and only there: it is admin-editable, it carries\n * `display_name`, `icon_name`, `base_url`, `url_pattern`, `category` and\n * `sort_order`, and the hub already reads it everywhere\n * (`lib/data/social-platforms-utils.ts`, `useSocialPlatforms`). A hardcoded\n * tuple here would be a second vocabulary that drifts the first time somebody\n * adds a row — and, being a compile-time union, would reject that row outright.\n *\n * So a platform is a `string` at every boundary. What code owns is the two\n * things a table cannot hold:\n *\n *   - the ICON COMPONENTS (`SOCIAL_ICON_COMPONENTS` in `social-icon-row`),\n *     which the DB names through `icon_name`;\n *   - the incoming SPELLINGS below, which are not platforms but ways external\n *     data refers to them (`x` for twitter, `fb` for facebook).\n *\n * JSX-free leaf with its own `exports` subpath, so `server-only` DALs and hub\n * scripts can import the type and the classifier without pulling in React.\n */\n\n/**\n * One row of `social_platforms`, in the shape callers pass around.\n *\n * Deliberately a narrow READ view: the columns that decide presentation and\n * URL-building, not the OAuth or media-config columns, which have their own\n * consumers and no business here.\n */\nexport interface SocialPlatformDefinition {\n  /** `social_platforms.name` — the canonical key, lowercase. */\n  name: string;\n  /** `social_platforms.base_url`, e.g. `https://twitter.com/`. */\n  baseUrl?: string | null;\n  /** `social_platforms.url_pattern`, e.g. `https://twitter.com/{username}`. */\n  urlPattern?: string | null;\n}\n\n/**\n * Accepted SPELLINGS that resolve onto a platform name.\n *\n * Not a vocabulary — every value here must be a `social_platforms.name`. These\n * exist because external data says `x`, `fb` or `generic` where the table says\n * `twitter`, `facebook` and `website`.\n */\nexport const SOCIAL_PLATFORM_ALIASES: Record<string, string> = {\n  x: 'twitter',\n  mail: 'email',\n  web: 'website',\n  url: 'website',\n  yt: 'youtube',\n  ig: 'instagram',\n  fb: 'facebook',\n  generic: 'website',\n};\n\n/**\n * Lower-case, trim, and resolve a known alias. Returns `null` only for a blank\n * input — an unrecognised name is passed THROUGH, because this module is not\n * the authority on which platforms exist.\n */\nexport function normalizeSocialPlatform(platform: string | null | undefined): string | null {\n  if (!platform) return null;\n  const key = platform.toLowerCase().trim();\n  if (!key) return null;\n  return SOCIAL_PLATFORM_ALIASES[key] ?? key;\n}\n\n/**\n * THE hostname test: an exact host match or a subdomain of it.\n * (`hostMatches('www.github.com', 'github.com')` is true; `notgithub.com` is not.)\n */\nexport function hostMatches(hostname: string | null | undefined, domain: string): boolean {\n  if (!hostname) return false;\n  const host = hostname.toLowerCase().replace(/^www\\./, '');\n  const bare = domain.toLowerCase().replace(/^www\\./, '');\n  return host === bare || host.endsWith(`.${bare}`);\n}\n\n/**\n * Alternate domains a platform's own `url_pattern` cannot express — a rename\n * (`x.com`), a share domain (`youtu.be`), an invite domain (`discord.gg`).\n *\n * The ONLY hardcoded host knowledge left, and it is additive: every primary\n * host is derived from the row. Keyed by `social_platforms.name`, so a row that\n * does not exist contributes nothing. If this grows, the answer is an\n * `alt_hosts` column on the table, not a longer map here.\n */\nexport const SOCIAL_PLATFORM_ALT_HOSTS: Record<string, readonly string[]> = {\n  twitter: ['x.com'],\n  youtube: ['youtu.be'],\n  discord: ['discord.gg'],\n  telegram: ['telegram.org'],\n  facebook: ['fb.com'],\n};\n\n/** The host a `base_url`/`url_pattern` points at, or null when it names none. */\nfunction hostOf(value: string | null | undefined): string | null {\n  if (!value) return null;\n  try {\n    return new URL(value.includes('://') ? value : `https://${value}`).hostname;\n  } catch {\n    return null;\n  }\n}\n\n/** Every domain that identifies a platform: its row's host plus any alternates. */\nexport function socialPlatformHosts(platform: SocialPlatformDefinition): string[] {\n  const primary = hostOf(platform.urlPattern) ?? hostOf(platform.baseUrl);\n  return [...(primary ? [primary] : []), ...(SOCIAL_PLATFORM_ALT_HOSTS[platform.name] ?? [])];\n}\n\n/**\n * Classify a URL by its host against the platforms the DATABASE defines.\n *\n * `platforms` is the caller's loaded `social_platforms` rows — passing them in\n * is what keeps this function pure and this module free of a platform list.\n * Returns `'website'` for a URL that parses but matches no row (the table's own\n * catch-all row), and `null` for something that is not a URL at all.\n */\nexport function classifySocialHost(\n  url: string | null | undefined,\n  platforms: readonly SocialPlatformDefinition[],\n): string | null {\n  const hostname = hostOf(url);\n  if (!hostname) return null;\n  for (const platform of platforms) {\n    if (socialPlatformHosts(platform).some(domain => hostMatches(hostname, domain))) {\n      return platform.name;\n    }\n  }\n  return 'website';\n}\n\n/**\n * A rendered social link. Exactly ONE of `href` (anchor, target _blank) or\n * `onClick` (share popup / copy-to-clipboard) — the discriminated union makes a\n * dead no-action entry unrepresentable. `SocialIconRow` re-exports this type;\n * it lives here so `server-only` and script graphs can name it without React.\n */\nexport type SocialIconLink = {\n  platform: string;\n  label?: string;\n  /** `internal` links only render on surfaces that opt in (and must be gated server-side). */\n  visibility?: 'external' | 'internal';\n} & ({ href: string; onClick?: never } | { onClick: () => void; href?: never });\n\n/**\n * THE \"find the link for a platform\" predicate, case-insensitive and\n * alias-aware, over ANY shape that carries a `platform` field (the mapped\n * profile rows and the rendered icon links both qualify).\n */\nexport function pickSocialLink<T extends { platform: string }>(\n  links: readonly T[] | null | undefined,\n  platform: string,\n): T | undefined {\n  const target = normalizeSocialPlatform(platform);\n  if (!target || !links) return undefined;\n  return links.find(link => normalizeSocialPlatform(link.platform) === target);\n}\n","/**\n * Shortcode + auto-URL-embed preprocessing for the rich (content)\n * composition. Moved verbatim from the old RichMarkdownRenderer — the\n * `{{youtube:...}}` / `{% youtube %}` / thumbnail-link / auto-URL grammar\n * is authored-content SSOT; chat surfaces never run this.\n */\n\nimport { hostMatches } from '../../../../utils/social-platforms';\n\nexport const processShortcodes = (content: string): string => {\n  let processedContent = content;\n\n  // Escape values interpolated into the raw HTML `data-*` attributes generated below.\n  // With rehypeRaw enabled, an unescaped `\"`/`<`/`>` in a URL or id could break out of\n  // the attribute and inject markup, so every interpolated embed value goes through this.\n  const escapeAttr = (value: string) =>\n    value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n\n  /**\n   * Emit one shortcode-expanded embed div. Every `embed-overrides.tsx` branch\n   * REQUIRES its payload attribute — a div that lost it renders as an empty\n   * div (and, before that guard existed, a permanently broken player). So the\n   * callers below must never hand a blank `value` in here; they keep the\n   * literal shortcode text instead, which shows the author their markup is wrong.\n   */\n  const embedDiv = (className: string, attr: string, value: string) =>\n    `\\n\\n<div class=\"${className}\" ${attr}=\"${escapeAttr(value)}\"></div>\\n\\n`;\n\n  /**\n   * Replace the FIRST literal occurrence of `needle` with `replacement`.\n   * The function-form replacer is deliberate: with a plain string replacement\n   * JS re-interprets `$&`, `` $` ``, `$'` and `$$` **in the replacement text**,\n   * so a URL or a restored code fence containing them was silently corrupted\n   * (a fenced `mktemp /tmp/x.$$` became `/tmp/x.$`; a bash `$'\\n'` spliced the\n   * rest of the document into the fence).\n   */\n  const replaceLiteral = (haystack: string, needle: string, replacement: string) =>\n    haystack.replace(needle, () => replacement);\n\n  // First, process explicit shortcodes\n  processedContent = processedContent\n    // YouTube embeds: {{youtube:VIDEO_ID}}\n    .replace(/\\{\\{youtube:([^}]+)\\}\\}/g, (match: string, videoId: string) => {\n      const id = videoId.trim();\n      return id ? embedDiv('youtube-embed', 'data-video-id', id) : match;\n    })\n    // Markdoc-style YouTube: {% youtube id=\"VIDEO_ID\" /%} or {% youtube id=\"VIDEO_ID\" title=\"...\" /%}\n    .replace(/\\{%\\s*youtube\\s+id=\"([^\"]+)\"(?:\\s+title=\"[^\"]*\")?\\s*\\/?%\\}/g, (match: string, videoId: string) => {\n      const id = videoId.trim();\n      return id ? embedDiv('youtube-embed', 'data-video-id', id) : match;\n    })\n    /**\n     * SHORTCODE: YouTube Thumbnail Link (RECOMMENDED - GitHub + Flamingo Compatible)\n     *\n     * SYNTAX: [![Title](https://img.youtube.com/vi/VIDEO_ID/QUALITY.jpg)](https://www.youtube.com/watch?v=VIDEO_ID)\n     *   - On GitHub: renders as a clickable thumbnail image linking to YouTube\n     *   - On Flamingo: converts to a full embedded YouTube player\n     * Use this format for docs that must work on BOTH GitHub and Flamingo;\n     * {{youtube:ID}} / {% youtube id=\"ID\" /%} are Flamingo-only.\n     */\n    .replace(\n      /\\[!\\[([^\\]]*)\\]\\(https?:\\/\\/img\\.youtube\\.com\\/vi\\/([a-zA-Z0-9_-]+)\\/[^)]+\\)\\]\\(https?:\\/\\/(?:www\\.)?(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([a-zA-Z0-9_-]+)[^)]*\\)/g,\n      (match: string, _altText: string, _thumbId: string, videoId: string) => {\n        return embedDiv('youtube-embed', 'data-video-id', videoId);\n      },\n    )\n    // Reddit embeds: {{reddit:POST_URL}}\n    .replace(/\\{\\{reddit:([^}]+)\\}\\}/g, (match: string, urlOrId: string) => {\n      const postUrl = urlOrId.trim();\n      // Without this guard a blank payload built the subreddit-less\n      // `https://reddit.com/r/`, i.e. a guaranteed-broken embed.\n      if (!postUrl) return match;\n      const fullUrl = postUrl.startsWith('http') ? postUrl : `https://reddit.com/r/${postUrl}`;\n      return embedDiv('reddit-embed', 'data-post-url', fullUrl);\n    })\n    // Twitter/X embeds: {{tweet:TWEET_URL}} or {{twitter:TWEET_URL}}\n    .replace(/\\{\\{(?:tweet|twitter):([^}]+)\\}\\}/g, (match: string, urlOrId: string) => {\n      const tweetInput = urlOrId.trim();\n      // Same as reddit: a blank payload built `…/status/` with no id.\n      if (!tweetInput) return match;\n      const tweetUrl = tweetInput.startsWith('http') ? tweetInput : `https://twitter.com/twitter/status/${tweetInput}`;\n      return embedDiv('tweet-embed', 'data-tweet-url', tweetUrl);\n    })\n    // Figma by file key (spec grammar): {{figma:FILE_KEY[:NODE_ID]}} → rewritten to the\n    // canonical URL form so the {{figma:URL}} rule below owns the rendering.\n    .replace(\n      /\\{\\{figma:(?!https?:)([A-Za-z0-9]+)(?::([0-9]+[-:][0-9]+))?\\}\\}/g,\n      (match: string, key: string, node: string | undefined) => {\n        const nodeParam = node ? `?node-id=${node.replace(':', '-')}` : '';\n        return `{{figma:https://www.figma.com/design/${key}${nodeParam}}}`;\n      },\n    )\n    // Figma embeds: {{figma:URL}}\n    .replace(/\\{\\{figma:([^}]+)\\}\\}/g, (match: string, url: string) => {\n      const figmaUrl = url.trim();\n      return figmaUrl ? embedDiv('figma-embed', 'data-figma-url', figmaUrl) : match;\n    })\n    // Claude artifact / Claude Design: {{claude-artifact:URL}} / {{claude-design:URL}}\n    // — the same shape as figma, so a Claude link is a markdown BLOCK wherever\n    // markdown renders, never a bespoke card one surface hand-assembles.\n    // `{{claude-artifact:URL}}` or `{{claude-artifact:URL|Name}}` — the optional\n    // name after the pipe is the ONLY source of a real title (claude.ai serves\n    // one og:title for every artifact), so the block carries it through.\n    .replace(\n      /\\{\\{claude-(artifact|design):([^|}]+)(?:\\|([^}]*))?\\}\\}/g,\n      (match: string, kind: string, url: string, title: string | undefined) => {\n        const name = title?.trim();\n        const titleAttr = name ? ` data-title=\"${escapeAttr(name)}\"` : '';\n        return `\\n\\n<div class=\"claude-embed\" data-url=\"${escapeAttr(url.trim())}\" data-kind=\"${kind}\"${titleAttr}></div>\\n\\n`;\n      },\n    )\n    // LinkedIn embeds: {{linkedin:POST_URL}}\n    .replace(/\\{\\{linkedin:([^}]+)\\}\\}/g, (match: string, url: string) => {\n      const postUrl = url.trim();\n      return postUrl ? embedDiv('linkedin-embed', 'data-post-url', postUrl) : match;\n    })\n    // Link previews: {{link:URL}}\n    .replace(/\\{\\{link:([^}]+)\\}\\}/g, (match: string, url: string) => {\n      const previewUrl = url.trim();\n      return previewUrl ? embedDiv('link-preview', 'data-url', previewUrl) : match;\n    });\n\n  // Next, auto-detect standalone URLs (but NOT those already in markdown links or code blocks)\n\n  // Step 1: Temporarily replace code blocks to protect them\n  const codeBlocks: string[] = [];\n  processedContent = processedContent.replace(/```[\\s\\S]*?```|`[^`]+`/g, match => {\n    const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;\n    codeBlocks.push(match);\n    return placeholder;\n  });\n\n  // Step 1.5: Strip well-formed `<script …>…</script>` elements from prose.\n  // Pasted social embed markup ships a loader script alongside its markup\n  // (Reddit: `<blockquote class=\"reddit-embed-bq\">…` + embed.reddit.com\n  // widgets.js; Twitter: platform.twitter.com widgets.js). Scripts NEVER\n  // execute on this surface — the sanitize stack strips real ones — but the\n  // engine's text pre-pass escapes the tag first, so it used to render as\n  // VISIBLE source text under every embed. Runs after Step 1 so script tags\n  // inside code fences / inline code stay literal sample code; an UNCLOSED\n  // opener is left alone (falls through to today's escape-to-text behavior,\n  // which is the safe degrade). Quantifiers are hard-bounded (ReDoS).\n  processedContent = processedContent.replace(/<script\\b[^>]{0,2000}>[\\s\\S]{0,20000}?<\\/script\\s{0,10}>/gi, '');\n\n  // Step 2: Temporarily replace markdown links to protect them\n  const markdownLinks: string[] = [];\n  processedContent = processedContent.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, match => {\n    const placeholder = `__MARKDOWN_LINK_${markdownLinks.length}__`;\n    markdownLinks.push(match);\n    return placeholder;\n  });\n\n  // Step 2.5: Temporarily replace table rows to protect URLs inside tables\n  const tableRows: string[] = [];\n  processedContent = processedContent.replace(/^\\|.+\\|$/gm, match => {\n    const placeholder = `__TABLE_ROW_${tableRows.length}__`;\n    tableRows.push(match);\n    return placeholder;\n  });\n\n  // Step 3: Auto-detect standalone URLs and convert to appropriate embeds\n  processedContent = processedContent\n    // YouTube URLs (standalone only)\n    .replace(\n      /(?:^|\\s)(https?:\\/\\/(?:www\\.)?(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([a-zA-Z0-9_-]+))(?:\\s|$)/g,\n      (match: string, fullUrl: string, videoId: string) => {\n        return replaceLiteral(match, fullUrl, embedDiv('youtube-embed', 'data-video-id', videoId));\n      },\n    )\n    // Reddit URLs (standalone only)\n    .replace(/(?:^|\\s)(https?:\\/\\/(?:www\\.)?reddit\\.com\\/[^\\s]+)(?:\\s|$)/g, (match: string, redditUrl: string) => {\n      return replaceLiteral(match, redditUrl, embedDiv('reddit-embed', 'data-post-url', redditUrl));\n    })\n    // Twitter/X URLs (standalone only)\n    .replace(\n      /(?:^|\\s)(https?:\\/\\/(?:www\\.)?(?:twitter\\.com|x\\.com)\\/[^/\\s]+\\/status\\/\\d+)(?:\\s|$)/g,\n      (match: string, tweetUrl: string) => {\n        return replaceLiteral(match, tweetUrl, embedDiv('tweet-embed', 'data-tweet-url', tweetUrl));\n      },\n    )\n    // Figma URLs (standalone only) - design/file/proto/board/deck/slides → interactive embed\n    .replace(\n      /(?:^|\\s)(https?:\\/\\/(?:www\\.|embed\\.)?figma\\.com\\/(?:design|file|proto|board|deck|slides)\\/[^\\s]+)(?:\\s|$)/g,\n      (match: string, figmaUrl: string) => {\n        return replaceLiteral(match, figmaUrl, embedDiv('figma-embed', 'data-figma-url', figmaUrl));\n      },\n    )\n    // LinkedIn post URLs (standalone only) → native post embed\n    .replace(\n      /(?:^|\\s)(https?:\\/\\/(?:www\\.)?linkedin\\.com\\/(?:posts|feed\\/update|embed\\/feed\\/update)\\/[^\\s]+)(?:\\s|$)/g,\n      (match: string, liUrl: string) => {\n        return replaceLiteral(match, liUrl, embedDiv('linkedin-embed', 'data-post-url', liUrl));\n      },\n    )\n    // Other external URLs (standalone only) - convert to link previews\n    .replace(/(?:^|\\s)(https?:\\/\\/[^\\s]+)(?:\\s|$)/g, (match: string, url: string) => {\n      try {\n        const urlObj = new URL(url);\n        const hostname = urlObj.hostname.toLowerCase();\n\n        // Exact host match (or a subdomain of it) — substring checks like\n        // `hostname.includes('x.com')` false-positive on \"zabbix.com\". ONE\n        // owner: `hostMatches` in the JSX-free social-platforms leaf, so a\n        // future refinement (`m.` hosts, punycode) cannot land in only one of\n        // the two allowlists that need it.\n        const hostIs = (domain: string) => hostMatches(hostname, domain);\n        // Non-video YouTube URLs (channels, playlists, `@handle`) fall\n        // through to the og-scraper; only video URLs become inline players.\n        const isYouTubeVideo = (hostIs('youtube.com') && urlObj.searchParams.has('v')) || hostIs('youtu.be');\n        // LinkedIn non-post URLs (profiles, companies) fall through to the\n        // og-scraper too; only interactive post embeds have handlers above.\n        if (isYouTubeVideo || hostIs('reddit.com') || hostIs('twitter.com') || hostIs('x.com') || hostIs('figma.com')) {\n          return match;\n        }\n\n        return replaceLiteral(match, url, embedDiv('link-preview', 'data-url', url));\n      } catch (e) {\n        console.warn('Failed to parse URL for link preview:', url, e);\n        return match;\n      }\n    });\n\n  // Step 3.5: Restore table rows\n  tableRows.forEach((row, index) => {\n    processedContent = replaceLiteral(processedContent, `__TABLE_ROW_${index}__`, row);\n  });\n\n  // Step 4: Restore markdown links\n  markdownLinks.forEach((link, index) => {\n    processedContent = replaceLiteral(processedContent, `__MARKDOWN_LINK_${index}__`, link);\n  });\n\n  // Step 5: Restore code blocks (MUST be last to prevent link preview in code)\n  codeBlocks.forEach((block, index) => {\n    processedContent = replaceLiteral(processedContent, `__CODE_BLOCK_${index}__`, block);\n  });\n\n  return processedContent;\n};\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * StackedRowsPanel — a single bordered card that stacks rows inside it.\n *\n * Each row can be:\n * - a single full-width element (e.g. title + subtitle) — pass one column,\n * - N columns (each a value + label, possibly a tag/avatar) — pass several columns,\n * - an arbitrary node (e.g. a nested table) — pass `content`.\n *\n * Border-radius behaviour is handled entirely by the container: `rounded-md` +\n * `overflow-clip` clip the first/last rows to the rounded corners. So a single row\n * rounds all four corners, while many rows visually round only the top corners of\n * the first row and the bottom corners of the last — with no per-row corner logic.\n *\n * Cells render the ODS `value` (`text-h4`) over `label` (`text-h6`) pattern with an\n * optional leading icon, matching the app's detail layouts. Pass a node as `value`\n * (e.g. a `Tag`) to render a status chip with a caption beneath it.\n */\n\nexport type PanelColumnAlign = 'left' | 'center' | 'right';\nexport type PanelHideAt = 'md' | 'lg' | 'xl';\n\nexport interface PanelColumn {\n  /** Stable key for the column. */\n  key: string;\n  /** Custom cell content (tag, avatar + text, button…). Takes precedence over `value`/`label`. */\n  content?: ReactNode;\n  /** Primary value rendered in the ODS h4 style when `content` is not set. */\n  value?: ReactNode;\n  /** Secondary label rendered under the value (ODS h6, secondary colour). */\n  label?: string;\n  /** Icon shown before the value. */\n  icon?: ReactNode;\n  /** Icon shown before the whole value/label stack, vertically centered against\n   *  both lines (the info-banner pattern: 24px glyph + title + caption). */\n  leadingIcon?: ReactNode;\n  /** When set, the cell becomes an external link. */\n  href?: string;\n  /** Tailwind width class. Defaults to `flex-[1_0_0] min-w-0`. */\n  width?: string;\n  /** Horizontal alignment of the column content. Defaults to `left`. */\n  align?: PanelColumnAlign;\n  /** Hide the column below this breakpoint (mirrors the DataTable `hideAt` concept). */\n  hideAt?: PanelHideAt;\n}\n\nexport interface PanelRow {\n  /** Stable key for the row. */\n  id: string;\n  /** One column → full-width row; several columns → a columns row. */\n  columns?: PanelColumn[];\n  /** Arbitrary content (e.g. a nested table). Takes precedence over `columns`. */\n  content?: ReactNode;\n  /** Per-row overrides (e.g. height, or `p-0` for a nested table that should reach the edges). */\n  className?: string;\n}\n\nexport interface StackedRowsPanelProps {\n  rows: PanelRow[];\n  /** Optional uppercase caption rendered above the card. */\n  title?: string;\n  className?: string;\n}\n\nconst ALIGN_CLASS: Record<PanelColumnAlign, string> = {\n  left: 'justify-start items-start',\n  center: 'justify-center items-center',\n  right: 'justify-end items-end',\n};\n\n// Horizontal alignment for value/label cells. `left` keeps the default stretch so\n// the value/label can truncate; center/right shrink to content and align the cell.\nconst CELL_ITEMS_ALIGN: Record<PanelColumnAlign, string> = {\n  left: '',\n  center: 'items-center',\n  right: 'items-end',\n};\n\nconst HIDE_CLASS: Record<PanelHideAt, string> = {\n  md: 'hidden md:flex',\n  lg: 'hidden lg:flex',\n  xl: 'hidden xl:flex',\n};\n\n// Base column-row class. Row height is content-driven via the min-heights, matching\n// the joined-row pattern used across the detail layouts (56px mobile, 80px tablet+).\nconst ROW_CLASS =\n  'flex items-center gap-[var(--spacing-system-m)] px-[var(--spacing-system-m)] min-h-14 md:min-h-20 border-b border-ods-border last:border-b-0';\n\nfunction CellValue({ column }: { column: PanelColumn }) {\n  const valueNode = (\n    <div\n      className=\"truncate text-ods-text-primary text-h4\"\n      title={typeof column.value === 'string' ? column.value : undefined}\n    >\n      {column.value}\n    </div>\n  );\n\n  return (\n    <>\n      <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xxs)]\">\n        {column.icon && <span className=\"shrink-0\">{column.icon}</span>}\n        {column.href ? (\n          <a\n            href={column.href}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"min-w-0 transition-opacity hover:opacity-80\"\n          >\n            {valueNode}\n          </a>\n        ) : (\n          valueNode\n        )}\n      </div>\n      {column.label ? <p className=\"truncate text-ods-text-secondary text-h6\">{column.label}</p> : null}\n    </>\n  );\n}\n\nfunction PanelCell({ column }: { column: PanelColumn }) {\n  const widthClass = column.width ?? 'flex-[1_0_0] min-w-0';\n  const hideClass = column.hideAt ? HIDE_CLASS[column.hideAt] : 'flex';\n\n  if (column.content !== undefined) {\n    return (\n      <div className={cn(hideClass, 'flex-col', ALIGN_CLASS[column.align ?? 'left'], widthClass)}>{column.content}</div>\n    );\n  }\n\n  if (column.leadingIcon) {\n    return (\n      <div className={cn(hideClass, 'items-center gap-[var(--spacing-system-s)]', widthClass)}>\n        <span className=\"shrink-0\">{column.leadingIcon}</span>\n        <div className={cn('flex min-w-0 flex-1 flex-col justify-center', CELL_ITEMS_ALIGN[column.align ?? 'left'])}>\n          <CellValue column={column} />\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={cn(hideClass, 'flex-col justify-center', CELL_ITEMS_ALIGN[column.align ?? 'left'], widthClass)}>\n      <CellValue column={column} />\n    </div>\n  );\n}\n\nfunction PanelRowView({ row }: { row: PanelRow }) {\n  // Arbitrary content (e.g. a nested table): keep it edge-to-edge unless the caller\n  // overrides via `className`. No fixed height — let the content size itself.\n  if (row.content !== undefined) {\n    return <div className={cn('border-b border-ods-border last:border-b-0', row.className)}>{row.content}</div>;\n  }\n\n  const columns = row.columns ?? [];\n  const isFullWidth = columns.length === 1;\n\n  // A single-column row is a full-width title/subtitle row: pad symmetrically\n  // (ODS `m`) instead of using a fixed column-row height.\n  const rowClass = isFullWidth\n    ? cn(\n        'flex items-center gap-[var(--spacing-system-m)] border-b border-ods-border p-[var(--spacing-system-m)] last:border-b-0',\n        row.className,\n      )\n    : cn(ROW_CLASS, row.className);\n\n  return (\n    <div className={rowClass}>\n      {columns.map(column => (\n        <PanelCell key={column.key} column={column} />\n      ))}\n    </div>\n  );\n}\n\nexport function StackedRowsPanel({ rows, title, className }: StackedRowsPanelProps) {\n  const card = (\n    <div className=\"flex flex-col overflow-clip rounded-md border border-ods-border bg-ods-card\">\n      {rows.map(row => (\n        <PanelRowView key={row.id} row={row} />\n      ))}\n    </div>\n  );\n\n  if (!title) {\n    return <div className={className}>{card}</div>;\n  }\n\n  return (\n    <div className={cn('flex flex-col gap-[var(--spacing-system-xxs)]', className)}>\n      <p className=\"uppercase text-ods-text-secondary text-h5\">{title}</p>\n      {card}\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { SlackIcon } from '../icons/slack-icon';\n\nexport interface SlackChannelChipProps {\n  /** Channel display name, no '#' prefix. Falls back to plain \"Slack\" when null. */\n  name?: string | null;\n  /** Web deep link to the channel (e.g. https://app.slack.com/client/T…/C…). Renders unlinked when null. */\n  href?: string | null;\n  /** Extra classes merged onto the chip span. */\n  className?: string;\n}\n\n/**\n * Inline Slack-channel reference: the Slack icon + \"#name\", optionally linked\n * to the channel's web deep link. Server-component safe (no hooks/state).\n * Degrades gracefully: no name renders plain \"Slack\", no href renders unlinked.\n */\nexport function SlackChannelChip({ name, href, className }: SlackChannelChipProps) {\n  const inner = (\n    <span className={cn('inline-flex items-center gap-1 align-baseline text-ods-text-primary', className)}>\n      <SlackIcon className=\"h-4 w-4 flex-shrink-0\" />\n      {name ? `#${name}` : 'Slack'}\n    </span>\n  );\n  if (!href) return inner;\n  return (\n    <a href={href} target=\"_blank\" rel=\"noopener noreferrer\" className=\"hover:underline\">\n      {inner}\n    </a>\n  );\n}\n","'use client';\n\ninterface StatusIndicatorProps {\n  status: 'success' | 'pending' | 'error' | 'missing';\n  label: string;\n  href?: string;\n}\n\nexport function StatusIndicator({ status, label, href }: StatusIndicatorProps) {\n  const getStatusColor = (kind: string) => {\n    switch (kind) {\n      case 'success':\n        return 'bg-ods-success text-ods-success';\n      case 'pending':\n        return 'bg-ods-warning text-ods-warning';\n      case 'error':\n        return 'bg-ods-error text-ods-error';\n      case 'missing':\n        return 'bg-ods-warning text-ods-warning';\n      default:\n        return 'bg-ods-text-muted text-ods-text-muted';\n    }\n  };\n\n  const colorClasses = getStatusColor(status);\n  const [dotColor, textColor] = colorClasses.split(' ');\n\n  const content = (\n    <div className=\"flex items-center gap-1.5\">\n      <div className={`h-3 w-3 rounded-full ${dotColor}`} />\n      <span className={`text-h6 ${textColor}`}>{label}</span>\n    </div>\n  );\n\n  if (href) {\n    return (\n      <a href={href} target=\"_blank\" rel=\"noopener noreferrer\" className=\"cursor-pointer hover:underline\">\n        {content}\n      </a>\n    );\n  }\n\n  return content;\n}\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon';\n\nexport interface BackButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {\n  label?: string;\n  onClick?: React.MouseEventHandler<HTMLButtonElement>;\n}\n\nexport function BackButton({ label = 'Back', className, type = 'button', ...props }: BackButtonProps) {\n  return (\n    <button\n      type={type}\n      className={cn(\n        'group inline-flex items-center justify-center self-start rounded-md',\n        'gap-[var(--spacing-system-xsf)] py-[var(--spacing-system-sf)]',\n        'text-ods-text-secondary hover:text-ods-text-primary',\n        'transition-colors duration-200',\n        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',\n        className,\n      )}\n      {...props}\n    >\n      <Chevron02LeftIcon className=\"size-6 shrink-0\" />\n      <span className=\"text-h4\">{label}</span>\n    </button>\n  );\n}\n\nexport default BackButton;\n","'use client';\n\nimport type React from 'react';\nimport { useEffect, useState } from 'react';\nimport { useDebounce } from '../../hooks/ui/use-debounce';\nimport { cn } from '../../utils/cn';\nimport { Filter02Icon, SearchIcon } from '../icons-v2-generated';\nimport { Button } from '../ui/button';\nimport { PageError } from '../ui/error-state';\nimport { FilterModal, type FilterGroup, type SortConfig, type SortDirection } from '../ui/filter-modal';\nimport { Input } from '../ui/input';\nimport type { TableFilters } from '../ui/table/types';\nimport { ListPageContainer, type PageActionButton } from './page-container';\n\nexport interface ListPageLayoutProps {\n  title: string;\n  headerActions?: React.ReactNode;\n  actions?: PageActionButton[];\n  searchPlaceholder: string;\n  searchValue: string;\n  onSearch: (term: string) => void;\n  children: React.ReactNode;\n  error?: string | null;\n  className?: string;\n  padding?: 'none' | 'sm' | 'md' | 'lg';\n  background?: 'default' | 'card' | 'transparent';\n  mobileFilterGroups?: FilterGroup[];\n  onMobileFilterChange?: (filters: TableFilters) => void;\n  currentMobileFilters?: TableFilters;\n  mobileSortConfig?: SortConfig;\n  onMobileSort?: (column: string, direction: SortDirection) => void;\n  mobileFilterTitle?: string;\n  stickyHeader?: boolean;\n}\n\n/**\n * @deprecated Use `PageLayout` from `'../layout/page-layout'` instead.\n *\n * Standardized Layout for List Pages\n *\n * A comprehensive layout component that provides 100% consistent structure\n * for all list-based pages throughout the OpenFrame application.\n *\n * ## Layout Structure:\n * ```\n * ┌─────────────────────────────────────────────────────────────┐\n * │ Title (left aligned)    │    Header Actions (right aligned) │\n * ├─────────────────────────────────────────────────────────────┤\n * │                 Search Bar (full width)                    │\n * ├─────────────────────────────────────────────────────────────┤\n * │                Table/Grid with Filters                     │\n * │                    (main content)                          │\n * └─────────────────────────────────────────────────────────────┘\n * ```\n *\n * ## Key Features:\n * - **Consistent Spacing**: All pages use identical padding and gaps\n * - **Responsive Design**: Works seamlessly across all screen sizes\n * - **Accessibility**: Proper semantic HTML and ARIA support\n * - **Error Handling**: Built-in error state display\n * - **Flexible Actions**: Supports any combination of buttons/controls\n * - **Search Integration**: Standardized search bar positioning\n *\n * ## Currently Used By:\n * - `/devices` - Device management with table/grid toggle\n * - `/logs-page` - Log analysis with refresh functionality\n * - `/scripts` - Script management with new/refresh actions\n * - `/mingo` tabs - Archive/current chats with filtering\n * - `/policies-and-queries` tabs - Policy management with refresh/new actions\n *\n * ## Design Tokens:\n * - Uses ODS design system tokens for consistent theming\n * - Maintains proper contrast ratios and accessibility standards\n * - Supports both light and dark mode themes\n *\n * ## Performance:\n * - Minimal re-renders through proper prop drilling\n * - Optimized for large datasets with virtualization support\n * - Built-in debouncing for search operations\n *\n * @example\n * ```tsx\n * <ListPageLayout\n *   title=\"My Data\"\n *   headerActions={<Button>Refresh</Button>}\n *   searchPlaceholder=\"Search items...\"\n *   searchValue={searchTerm}\n *   onSearch={setSearchTerm}\n *   error={error}\n * >\n *   <Table data={items} columns={columns} />\n * </ListPageLayout>\n * ```\n */\nexport function ListPageLayout({\n  title,\n  headerActions,\n  actions,\n  searchPlaceholder,\n  searchValue,\n  onSearch,\n  children,\n  error,\n  className,\n  padding = 'sm',\n  background = 'default',\n  mobileFilterGroups,\n  onMobileFilterChange,\n  currentMobileFilters,\n  mobileSortConfig,\n  onMobileSort,\n  mobileFilterTitle,\n  stickyHeader = false,\n}: ListPageLayoutProps) {\n  const [mobileFilterOpen, setMobileFilterOpen] = useState(false);\n  const [localSearchValue, setLocalSearchValue] = useState(searchValue);\n  const debouncedSearchValue = useDebounce(localSearchValue, 500);\n\n  // Sync local value when controlled value changes externally. Adjusted while\n  // rendering (React's documented prop-sync pattern) instead of from an effect:\n  // the input is rendered from `localSearchValue`, so an effect committed one\n  // frame still showing the pre-sync text and then re-rendered to replace it.\n  const [syncedSearchValue, setSyncedSearchValue] = useState(searchValue);\n  if (syncedSearchValue !== searchValue) {\n    setSyncedSearchValue(searchValue);\n    setLocalSearchValue(searchValue);\n  }\n\n  // Call onSearch when debounced value changes\n  useEffect(() => {\n    if (debouncedSearchValue !== searchValue) {\n      onSearch(debouncedSearchValue);\n    }\n  }, [debouncedSearchValue, onSearch, searchValue]);\n\n  // Check if mobile filter is enabled (has filter groups or sort config)\n  const hasMobileFilter =\n    (mobileFilterGroups && mobileFilterGroups.length > 0) || (mobileSortConfig && mobileSortConfig.columns.length > 0);\n\n  if (error) {\n    return <PageError message={error} />;\n  }\n\n  return (\n    <ListPageContainer\n      title={title}\n      headerActions={headerActions}\n      actions={actions}\n      padding={padding}\n      background={background}\n      className={className}\n    >\n      <div>\n        {/* Search Bar with Mobile Filter Button */}\n        <div\n          className={cn(\n            'flex items-center gap-4',\n            !stickyHeader && 'w-full',\n            stickyHeader &&\n              'sticky top-0 z-20 -mx-[var(--spacing-system-l)] -mt-[var(--spacing-system-l)] flex items-center gap-[var(--spacing-system-m)] bg-ods-bg p-[var(--spacing-system-l)]',\n          )}\n        >\n          <Input\n            placeholder={searchPlaceholder}\n            onChange={e => setLocalSearchValue(e.target.value)}\n            value={localSearchValue}\n            className=\"flex-1\"\n            startAdornment={<SearchIcon className=\"h-4 w-4 md:h-6 md:w-6\" />}\n          />\n\n          {/* Mobile Filter Button - only visible on mobile when filter is enabled */}\n          {hasMobileFilter && (\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              className=\"md:hidden\"\n              onClick={() => setMobileFilterOpen(true)}\n              aria-label=\"Open filters\"\n            >\n              <Filter02Icon />\n            </Button>\n          )}\n        </div>\n\n        {/* Mobile Filter Modal */}\n        {hasMobileFilter && (\n          <FilterModal\n            isOpen={mobileFilterOpen}\n            onClose={() => setMobileFilterOpen(false)}\n            title={mobileFilterTitle}\n            filterGroups={mobileFilterGroups || []}\n            onFilterChange={onMobileFilterChange || (() => {})}\n            currentFilters={currentMobileFilters || {}}\n            sortConfig={mobileSortConfig}\n            onSort={onMobileSort}\n          />\n        )}\n\n        {/* Main Content - Table/Grid with filters */}\n        {children}\n      </div>\n    </ListPageContainer>\n  );\n}\n\n// Re-export PageActionButton type for convenience\nexport type { PageActionButton } from './page-container';\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport default ListPageLayout;\n","'use client';\n\nimport { AlertTriangle, RefreshCw, Home } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { Button } from './button';\n\ninterface ErrorStateProps {\n  title?: string;\n  message: string;\n  variant?: 'error' | 'warning' | 'info';\n  showIcon?: boolean;\n  showRetry?: boolean;\n  showHome?: boolean;\n  onRetry?: () => void;\n  onHome?: () => void;\n  className?: string;\n  containerClassName?: string;\n}\n\nexport function ErrorState({\n  title = 'Error',\n  message,\n  variant = 'error',\n  showIcon = true,\n  showRetry = false,\n  showHome = false,\n  onRetry,\n  onHome,\n  className,\n  containerClassName,\n}: ErrorStateProps) {\n  const getVariantStyles = () => {\n    switch (variant) {\n      case 'error':\n        return {\n          // -secondary tokens, NOT `/20` alpha modifiers: the ods colors are\n          // hex CSS vars, and Tailwind 3.x can't inject alpha into those —\n          // the modifier silently produced no background at all.\n          bg: 'bg-ods-error-secondary',\n          border: 'border-ods-error',\n          text: 'text-ods-error',\n          icon: 'text-ods-error',\n        };\n      case 'warning':\n        return {\n          bg: 'bg-ods-warning-secondary',\n          border: 'border-ods-warning',\n          text: 'text-ods-warning',\n          icon: 'text-ods-warning',\n        };\n      // `default`, not `case 'info'`: the switch has to be TOTAL. Falling off\n      // the end returned `undefined`, and every read below (`styles.bg`) would\n      // then throw — a blank screen instead of the error the component exists\n      // to show. Reachable from any untyped host passing an unknown variant.\n      case 'info':\n      default:\n        return {\n          bg: 'bg-ods-bg-surface',\n          border: 'border-ods-border',\n          text: 'text-ods-text-secondary',\n          icon: 'text-ods-text-secondary',\n        };\n    }\n  };\n\n  const styles = getVariantStyles();\n\n  return (\n    <div className={cn('p-6', containerClassName)}>\n      <div className={cn('rounded-lg border p-4', styles.bg, styles.border, className)}>\n        <div className=\"flex items-start gap-3\">\n          {showIcon && <AlertTriangle className={cn('mt-0.5 h-5 w-5 flex-shrink-0', styles.icon)} />}\n          <div className=\"flex-1\">\n            <h3 className={cn('mb-1 font-semibold', styles.text)}>{title}</h3>\n            <p className={cn('text-h6', styles.text)}>{message}</p>\n            {(showRetry || showHome) && (\n              <div className=\"mt-3 flex gap-2\">\n                {showRetry && onRetry && (\n                  <Button\n                    onClick={onRetry}\n                    variant=\"outline\"\n                    size=\"small-legacy\"\n                    className=\"h-8\"\n                    leftIcon={<RefreshCw className=\"h-4 w-4\" />}\n                  >\n                    Try Again\n                  </Button>\n                )}\n                {showHome && onHome && (\n                  <Button\n                    onClick={onHome}\n                    variant=\"outline\"\n                    size=\"small-legacy\"\n                    className=\"h-8\"\n                    leftIcon={<Home className=\"h-4 w-4\" />}\n                  >\n                    Go Home\n                  </Button>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n// Convenience components for common error scenarios\nexport function PageError({\n  message,\n  onRetry,\n  onHome,\n}: {\n  message: string;\n  onRetry?: () => void;\n  onHome?: () => void;\n}) {\n  return (\n    <ErrorState\n      title=\"Page Error\"\n      message={message}\n      variant=\"error\"\n      showRetry={!!onRetry}\n      showHome={!!onHome}\n      onRetry={onRetry}\n      onHome={onHome}\n    />\n  );\n}\n\nexport function LoadError({ message, onRetry }: { message: string; onRetry?: () => void }) {\n  return <ErrorState title=\"Loading Error\" message={message} variant=\"error\" showRetry={!!onRetry} onRetry={onRetry} />;\n}\n\nexport function NotFoundError({\n  message = 'The requested item was not found',\n  onHome,\n}: {\n  message?: string;\n  onHome?: () => void;\n}) {\n  return <ErrorState title=\"Not Found\" message={message} variant=\"warning\" showHome={!!onHome} onHome={onHome} />;\n}\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { cn } from '../../utils/cn';\nimport { Filter02Icon } from '../icons-v2-generated/sort-and-filter/filter-02-icon';\nimport { Button } from './button';\nimport { DateFilterPanel, type DateFilterResult, type DateRange } from './date-picker';\nimport { FilterCheckboxItem } from './filter-checkbox-item';\nimport { ModalV2, ModalV2Content, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2';\nimport { ScrollFadeOverlay, useScrollFade } from './scroll-fade';\nimport { Skeleton } from './skeleton';\nimport { SortColumnItem, type SortConfig, type SortDirection } from './sort-column-item';\nimport type { TableFilters } from './table/types';\nimport { TagKeyValueFilter, type TagKeyConfig } from './tag-key-value-filter';\n\n// Re-export sub-component types for consumers\nexport type { SortableColumn, SortConfig, SortDirection } from './sort-column-item';\nexport type { TagKeyConfig, TagValueOption } from './tag-key-value-filter';\n\nexport interface FilterModalOption {\n  id: string;\n  label: string;\n  count?: number;\n}\n\nexport interface FilterGroup {\n  id: string;\n  title: string;\n  options: FilterModalOption[];\n}\n\nexport interface FilterModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  title?: string;\n  filterGroups: FilterGroup[];\n  onFilterChange: (filters: TableFilters) => void;\n  currentFilters?: TableFilters;\n  resetButtonText?: string;\n  applyButtonText?: string;\n  className?: string;\n  sortConfig?: SortConfig;\n  onSort?: (column: string, direction: SortDirection) => void;\n  onSortClear?: (column: string) => void;\n  /** Tag key:value filter config. When provided, renders a key→value filter section */\n  tagFilterKeys?: TagKeyConfig[];\n  /** Currently selected tags in \"key:value\" format */\n  selectedTags?: string[];\n  /** Called when tag selection changes */\n  onTagsChange?: (tags: string[]) => void;\n  /** Title for the tag keys section */\n  tagFilterTitle?: string;\n  /** Show skeleton loading state */\n  isLoading?: boolean;\n  /** Date sort + range filter rendered as the last section. Drafted with the\n   *  other filters and committed via `onChange` on Apply (defaults on Reset). */\n  dateFilter?: {\n    /** Section title. Defaults to 'Date'. */\n    title?: string;\n    /** Applied sort direction. Defaults to `defaultSort`. */\n    sort?: SortDirection;\n    /** Baseline sort direction restored by Reset. Defaults to 'desc'. */\n    defaultSort?: SortDirection;\n    /** Applied date range. */\n    range?: DateRange;\n    /** Minimum selectable date. */\n    fromDate?: Date;\n    /** Maximum selectable date (e.g. today, to block future dates). */\n    toDate?: Date;\n    /** Fired on Apply with the drafted values, and on Reset with the defaults. */\n    onChange: (result: DateFilterResult) => void;\n  };\n  /** Text shown in empty state title */\n  emptyStateTitle?: string;\n  /** Text shown in empty state description */\n  emptyStateDescription?: string;\n}\n\nexport function FilterModal({\n  isOpen,\n  onClose,\n  title = 'Sort and Filter',\n  filterGroups,\n  onFilterChange,\n  currentFilters = {},\n  resetButtonText = 'Reset Filters',\n  applyButtonText = 'Apply Filters',\n  className,\n  sortConfig,\n  onSort,\n  onSortClear,\n  tagFilterKeys,\n  selectedTags,\n  onTagsChange,\n  tagFilterTitle,\n  dateFilter,\n  isLoading = false,\n  emptyStateTitle = 'No filters available',\n  emptyStateDescription = 'There are no filter options to display at the moment',\n}: FilterModalProps) {\n  const [selectedFilters, setSelectedFilters] = useState<TableFilters>(() => {\n    return { ...currentFilters };\n  });\n  const [pendingTags, setPendingTags] = useState<string[]>(selectedTags ?? []);\n\n  const dateDefaultSort: SortDirection = dateFilter?.defaultSort ?? 'desc';\n  const [draftDateSort, setDraftDateSort] = useState<SortDirection>(dateFilter?.sort ?? dateDefaultSort);\n  const [draftDateRange, setDraftDateRange] = useState<DateRange | undefined>(dateFilter?.range);\n\n  // Scroll shadows: bottom fade by default while content continues below,\n  // top fade once scrolled (same pattern as the chat lists).\n  const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>();\n\n  // Re-seeding the drafts is keyed on the modal opening and on the CONTENT of\n  // the incoming filters — a parent that rebuilds the same objects every render\n  // must not stomp what the user has picked. The keys are extracted to named\n  // variables (a `JSON.stringify(...)` inline in the array is opaque to static\n  // checking), and the values themselves are read at fire time through a ref so\n  // the effect has no stale-closure gap and no identity dependency.\n  const seedKey = `${JSON.stringify(currentFilters)}|${JSON.stringify(selectedTags ?? [])}`;\n  const seedSourceRef = useRef({ currentFilters, selectedTags, dateFilter, dateDefaultSort });\n  useEffect(() => {\n    seedSourceRef.current = { currentFilters, selectedTags, dateFilter, dateDefaultSort };\n  });\n\n  useEffect(() => {\n    if (!isOpen) return;\n    const seed = seedSourceRef.current;\n    setSelectedFilters({ ...seed.currentFilters });\n    setPendingTags(seed.selectedTags ?? []);\n    setDraftDateSort(seed.dateFilter?.sort ?? seed.dateDefaultSort);\n    setDraftDateRange(seed.dateFilter?.range);\n  }, [isOpen, seedKey]);\n\n  const handleFilterToggle = (groupId: string, optionId: string, checked: boolean) => {\n    setSelectedFilters((prev: TableFilters) => {\n      const current = prev[groupId] || [];\n      const updated = checked\n        ? current.includes(optionId)\n          ? current\n          : [...current, optionId]\n        : current.filter((id: string) => id !== optionId);\n\n      if (updated.length > 0) {\n        return { ...prev, [groupId]: updated };\n      }\n      const next = { ...prev };\n      delete next[groupId];\n      return next;\n    });\n  };\n\n  const handleReset = () => {\n    onFilterChange({});\n    onTagsChange?.([]);\n    dateFilter?.onChange({ sort: dateDefaultSort, range: undefined });\n    onClose();\n  };\n\n  const handleApply = () => {\n    onFilterChange(selectedFilters);\n    onTagsChange?.(pendingTags);\n    dateFilter?.onChange({ sort: draftDateSort, range: draftDateRange });\n    onClose();\n  };\n\n  const getColumnDirection = (columnKey: string): SortDirection | undefined => {\n    return sortConfig?.sortBy === columnKey ? sortConfig.sortDirection : undefined;\n  };\n\n  const hasSort = !!sortConfig && sortConfig.columns.length > 0;\n  const hasFilterGroups = filterGroups.length > 0;\n  const hasTagFilter = !!tagFilterKeys && tagFilterKeys.length > 0 && !!onTagsChange;\n  const isEmpty = !isLoading && !hasSort && !hasFilterGroups && !hasTagFilter && !dateFilter;\n\n  return (\n    <ModalV2 isOpen={isOpen} onClose={onClose} className={cn('max-h-[90vh] max-w-none', className)}>\n      <ModalV2Header>\n        <ModalV2Title>{title}</ModalV2Title>\n      </ModalV2Header>\n\n      <ModalV2Content\n        className={cn(\n          'flex min-h-0 flex-1 flex-col',\n          !isEmpty && 'md:rounded-md md:border md:border-ods-border md:bg-ods-bg md:px-3',\n        )}\n      >\n        <div className=\"relative flex min-h-0 flex-1 flex-col\">\n          <div\n            ref={scrollRef}\n            onScroll={updateFade}\n            className=\"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto md:py-3\"\n          >\n            {isLoading ? (\n              <>\n                {[0, 1].map(group => (\n                  <div key={group} className=\"flex flex-col gap-1\">\n                    <Skeleton className=\"h-5 w-24\" />\n                    <div className=\"overflow-hidden rounded-md border border-ods-border\">\n                      {[0, 1, 2].map(row => (\n                        <div\n                          key={row}\n                          className=\"flex items-center gap-3 border-b border-ods-border bg-ods-card px-4 py-3 last:border-b-0\"\n                        >\n                          <Skeleton className=\"h-6 w-6 shrink-0 rounded-[6px]\" />\n                          <Skeleton className=\"h-4 max-w-[60%] flex-1\" />\n                          <Skeleton className=\"h-4 w-10 shrink-0\" />\n                        </div>\n                      ))}\n                    </div>\n                  </div>\n                ))}\n              </>\n            ) : isEmpty ? (\n              <div className=\"flex h-[240px] flex-col items-center justify-center gap-6 p-6\">\n                <Filter02Icon className=\"text-ods-text-secondary\" size={24} />\n                <div className=\"flex flex-col items-center text-center text-ods-text-secondary\">\n                  <p className=\"text-h4\">{emptyStateTitle}</p>\n                  <p className=\"text-h6\">{emptyStateDescription}</p>\n                </div>\n              </div>\n            ) : (\n              <>\n                {/* Sort columns */}\n                {sortConfig && sortConfig.columns.length > 0 && (\n                  <div className=\"flex flex-col gap-1\">\n                    <span className=\"text-ods-text-secondary text-h5\">{sortConfig.title || 'Sort By'}</span>\n                    <div className=\"overflow-hidden rounded-md border border-ods-border\">\n                      {sortConfig.columns.map(column => (\n                        <SortColumnItem\n                          key={column.key}\n                          column={column}\n                          currentDirection={getColumnDirection(column.key)}\n                          onSort={direction => onSort?.(column.key, direction)}\n                          onClear={onSortClear ? () => onSortClear(column.key) : undefined}\n                        />\n                      ))}\n                    </div>\n                  </div>\n                )}\n\n                {/* Filter groups. A group with no options is skipped, not drawn as an\n            empty box: same rule the column funnels follow — a control that\n            cannot be used is worse than no control. */}\n                {filterGroups.map(group => {\n                  if (group.options.length === 0) return null;\n                  const groupSelection = selectedFilters[group.id] || [];\n                  return (\n                    <div key={group.id} className=\"flex flex-col gap-1\">\n                      <span className=\"text-ods-text-secondary text-h5\">{group.title}</span>\n                      <div className=\"overflow-hidden rounded-md border border-ods-border\">\n                        {group.options.map(option => (\n                          <FilterCheckboxItem\n                            key={option.id}\n                            label={option.label}\n                            count={option.count}\n                            checked={groupSelection.includes(option.id)}\n                            onChange={checked => handleFilterToggle(group.id, option.id, checked)}\n                          />\n                        ))}\n                      </div>\n                    </div>\n                  );\n                })}\n\n                {/* Tag key:value filter */}\n                {tagFilterKeys && tagFilterKeys.length > 0 && onTagsChange && (\n                  <TagKeyValueFilter\n                    keys={tagFilterKeys}\n                    selectedTags={pendingTags}\n                    onTagsChange={setPendingTags}\n                    keysTitle={tagFilterTitle}\n                  />\n                )}\n                {/* Date sort + range filter */}\n                {dateFilter && (\n                  <div className=\"flex flex-col gap-1\">\n                    <span className=\"text-ods-text-secondary text-h5\">{dateFilter.title ?? 'Date'}</span>\n                    <DateFilterPanel\n                      mode=\"range\"\n                      sort={draftDateSort}\n                      onSortChange={setDraftDateSort}\n                      selected={draftDateRange}\n                      onSelect={value => setDraftDateRange(value as DateRange | undefined)}\n                      fromDate={dateFilter.fromDate}\n                      toDate={dateFilter.toDate}\n                      className=\"gap-[var(--spacing-system-xxs)]\"\n                    />\n                  </div>\n                )}\n              </>\n            )}\n          </div>\n\n          {/* Scroll shadows over the filter list edges */}\n          <ScrollFadeOverlay edge=\"top\" visible={fadeTop} />\n          <ScrollFadeOverlay edge=\"bottom\" visible={fadeBottom} />\n        </div>\n      </ModalV2Content>\n\n      <ModalV2Footer>\n        <Button variant=\"outline\" className=\"h-11 flex-1\" onClick={handleReset}>\n          {resetButtonText}\n        </Button>\n        <Button variant=\"accent\" className=\"h-11 flex-1\" onClick={handleApply}>\n          {applyButtonText}\n        </Button>\n      </ModalV2Footer>\n    </ModalV2>\n  );\n}\n","'use client';\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { cn } from '../../utils/cn';\nimport { CheckboxCheckmarkIcon } from '../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon';\n\nexport interface FilterCheckboxItemProps {\n  label: string;\n  checked: boolean;\n  onChange: (checked: boolean) => void;\n  count?: number;\n  className?: string;\n}\n\nexport function FilterCheckboxItem({ label, checked, onChange, count, className }: FilterCheckboxItemProps) {\n  return (\n    <div\n      className={cn(\n        'flex items-center gap-3 border-b border-ods-border bg-ods-card last:border-b-0',\n        'cursor-pointer transition-colors hover:bg-ods-bg-hover',\n        count !== undefined ? 'px-4 py-3' : 'p-3',\n        className,\n      )}\n      onClick={() => onChange(!checked)}\n    >\n      <CheckboxPrimitive.Root\n        checked={checked}\n        onCheckedChange={c => onChange(c === true)}\n        className={cn(\n          'h-6 w-6 shrink-0 rounded-[6px] border-2',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',\n          checked ? 'border-ods-accent bg-ods-accent' : 'border-ods-text-secondary bg-ods-card',\n        )}\n        onClick={e => e.stopPropagation()}\n      >\n        <CheckboxPrimitive.Indicator className=\"flex items-center justify-center text-ods-text-on-accent\">\n          <CheckboxCheckmarkIcon size={10} />\n        </CheckboxPrimitive.Indicator>\n      </CheckboxPrimitive.Root>\n      <span className=\"flex-1 text-ods-text-primary text-h4\">{label}</span>\n      {count !== undefined && (\n        <span className=\"shrink-0 text-ods-text-secondary text-h5\">{count.toLocaleString()}</span>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../utils/cn';\nimport { Arrow01DownIcon, Arrow01UpIcon, SwitchVrIcon } from '../icons-v2-generated';\n\nexport interface SortableColumn {\n  key: string;\n  label: string;\n  sortKey?: string;\n}\n\nexport type SortDirection = 'asc' | 'desc';\n\nexport interface SortConfig {\n  columns: SortableColumn[];\n  sortBy?: string;\n  sortDirection?: SortDirection;\n  title?: string;\n}\n\nexport interface SortColumnItemProps {\n  column: SortableColumn;\n  currentDirection?: SortDirection;\n  onSort: (direction: SortDirection) => void;\n  onClear?: () => void;\n}\n\nexport function SortColumnItem({ column, currentDirection, onSort, onClear }: SortColumnItemProps) {\n  const handleClick = () => {\n    if (!currentDirection) {\n      onSort('asc');\n    } else if (currentDirection === 'asc') {\n      onSort('desc');\n    } else {\n      if (onClear) {\n        onClear();\n      } else {\n        onSort('asc');\n      }\n    }\n  };\n\n  const getSortIcon = () => {\n    if (currentDirection === 'asc') {\n      return <Arrow01UpIcon className=\"h-4 w-4 text-ods-accent\" />;\n    }\n    if (currentDirection === 'desc') {\n      return <Arrow01DownIcon className=\"h-4 w-4 text-ods-accent\" />;\n    }\n    return <SwitchVrIcon className=\"h-4 w-4 text-ods-text-secondary\" />;\n  };\n\n  return (\n    <div\n      onClick={handleClick}\n      className={cn(\n        'flex w-full items-center justify-between gap-2 bg-ods-card p-3',\n        'cursor-pointer text-left transition-colors hover:bg-ods-bg-hover',\n        'border-b border-ods-border last:border-b-0',\n      )}\n    >\n      <span className={cn('flex-1 text-h4', currentDirection ? 'text-ods-text-primary' : 'text-ods-text-secondary')}>\n        {column.label}\n      </span>\n      {getSortIcon()}\n    </div>\n  );\n}\n","'use client';\n\nimport { useMemo } from 'react';\n\nimport { cn } from '../../utils/cn';\nimport { FilterCheckboxItem } from './filter-checkbox-item';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TagValueOption {\n  id: string;\n  label: string;\n  count?: number;\n}\n\nexport interface TagKeyConfig {\n  /** Unique key identifier, used as prefix in tags (e.g. \"site\" → \"site:chicago\") */\n  key: string;\n  /** Display label for the key */\n  label: string;\n  /** Available values for this key */\n  values: TagValueOption[];\n}\n\nexport interface TagKeyValueFilterProps {\n  /** Available tag keys with their values */\n  keys: TagKeyConfig[];\n  /** Currently selected tags in \"key:value\" format */\n  selectedTags: string[];\n  /** Called when tags change */\n  onTagsChange: (tags: string[]) => void;\n  /** Title for the keys block. Default \"Tag Keys\" */\n  keysTitle?: string;\n  /** Custom className */\n  className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function TagKeyValueFilter({\n  keys,\n  selectedTags,\n  onTagsChange,\n  keysTitle = 'Tag Keys',\n  className,\n}: TagKeyValueFilterProps) {\n  // Parse selected tags → { \"site\": Set([\"chicago\"]), \"env\": Set([\"production\"]) }\n  const selectedMap = useMemo(() => {\n    const map = new Map<string, Set<string>>();\n    for (const tag of selectedTags) {\n      const colonIdx = tag.indexOf(':');\n      if (colonIdx === -1) continue;\n      const key = tag.slice(0, colonIdx);\n      const value = tag.slice(colonIdx + 1);\n      // Hold the bucket rather than `has`-then-`get`: one lookup, and the\n      // value the compiler can see is the one we insert.\n      let values = map.get(key);\n      if (!values) {\n        values = new Set();\n        map.set(key, values);\n      }\n      values.add(value);\n    }\n    return map;\n  }, [selectedTags]);\n\n  // Which keys are \"active\" (checked in the keys list)\n  const activeKeys = useMemo(() => {\n    return new Set(selectedMap.keys());\n  }, [selectedMap]);\n\n  // Toggle a key on/off — when unchecked, remove ALL values for that key\n  const handleKeyToggle = (key: string, checked: boolean) => {\n    if (checked) {\n      // Add a marker so the section appears even with no values checked\n      onTagsChange([...selectedTags, `${key}:`]);\n    } else {\n      // Remove all tags that start with this key\n      onTagsChange(selectedTags.filter(t => !t.startsWith(`${key}:`)));\n    }\n  };\n\n  // Toggle a specific value\n  const handleValueToggle = (key: string, valueId: string, checked: boolean) => {\n    const tag = `${key}:${valueId}`;\n    if (checked) {\n      // Remove the empty marker if it exists, add the real value\n      const cleaned = selectedTags.filter(t => t !== `${key}:`);\n      onTagsChange([...cleaned, tag]);\n    } else {\n      const newTags = selectedTags.filter(t => t !== tag);\n      // If no values left for this key, keep the empty marker so section stays visible\n      const hasOtherValues = newTags.some(t => t.startsWith(`${key}:`) && t !== `${key}:`);\n      if (!hasOtherValues) {\n        onTagsChange([...newTags, `${key}:`]);\n      } else {\n        onTagsChange(newTags);\n      }\n    }\n  };\n\n  // Keys that are checked → their value sections appear below\n  const activeKeyConfigs = keys.filter(k => activeKeys.has(k.key));\n\n  return (\n    <div className={cn('flex flex-col gap-4', className)}>\n      {/* ── Tag Keys checkbox list ── */}\n      <div className=\"flex flex-col gap-2\">\n        <span className=\"uppercase tracking-[-0.28px] text-ods-text-secondary text-h5\">{keysTitle}</span>\n        <div className=\"overflow-hidden rounded-[6px] border border-ods-border\">\n          {keys.map(keyConfig => (\n            <FilterCheckboxItem\n              key={keyConfig.key}\n              label={keyConfig.label}\n              checked={activeKeys.has(keyConfig.key)}\n              onChange={checked => handleKeyToggle(keyConfig.key, checked)}\n            />\n          ))}\n        </div>\n      </div>\n\n      {/* ── Value sections for each checked key ── */}\n      {activeKeyConfigs.map(keyConfig => {\n        const selectedValues = selectedMap.get(keyConfig.key) ?? new Set();\n        return (\n          <div key={keyConfig.key} className=\"flex flex-col gap-2\">\n            <span className=\"uppercase tracking-[-0.28px] text-ods-text-secondary text-h5\">{keyConfig.label}</span>\n            <div className=\"overflow-hidden rounded-[6px] border border-ods-border\">\n              {keyConfig.values.map(option => (\n                <FilterCheckboxItem\n                  key={option.id}\n                  label={option.label}\n                  count={option.count}\n                  checked={selectedValues.has(option.id)}\n                  onChange={checked => handleValueToggle(keyConfig.key, option.id, checked)}\n                />\n              ))}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport { Chevron02DownIcon } from '../icons-v2-generated';\nimport { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from './actions-menu';\nimport type { ButtonProps, SplitButtonIconAction } from './button';\nimport { Button, SplitButton } from './button';\nimport { Skeleton } from './skeleton';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';\n\nexport type PageActionButton = {\n  /** Button label. Omit to render an icon-only button. */\n  label?: string;\n  /** Accessible name. Required for icon-only buttons (when `label` is omitted). */\n  ariaLabel?: string;\n  /** Click handler. Optional when `href` or `submenu` is provided. */\n  onClick?: () => void;\n  icon?: React.ReactNode;\n  /** `overlay` is media chrome only — SplitButton has no divider colour for it. */\n  variant?: Exclude<ButtonProps['variant'], 'overlay' | 'glyph'>;\n  disabled?: boolean;\n  /**\n   * For SplitButton actions (when `iconAction` is set): disables only the main\n   * half. Combine with `iconAction.disabled` for icon-only disable. Ignored\n   * for non-SplitButton actions.\n   */\n  mainDisabled?: boolean;\n  loading?: boolean;\n  /** Show action only on mobile (below md). Default: visible on all screens. */\n  showOnlyMobile?: boolean;\n  /**\n   * Render the desktop button as icon-only (label hidden, icon centered). The full\n   * label still appears in the mobile \"...\" dropdown. The desktop icon is forced to\n   * `text-ods-text-primary`; the mobile row keeps the caller-provided icon color.\n   */\n  iconOnlyOnDesktop?: boolean;\n  /** Render as a link (next/link). Mutually exclusive with `submenu`. */\n  href?: string;\n  /** Forwarded to next/link's prefetch. Only applies when `href` is set. */\n  prefetch?: boolean;\n  /** Open link in a new tab. Only applies when `href` is set. */\n  openInNewTab?: boolean;\n  /**\n   * Render the action as a `SplitButton` (two independent click targets).\n   * The main half runs `onClick`/`href`; the icon half runs its own action.\n   * Mutually exclusive with `submenu`.\n   */\n  iconAction?: SplitButtonIconAction;\n  /**\n   * Render a button with a chevron that opens a dropdown. The whole button is\n   * a single click target — clicking anywhere opens the menu.\n   * Mutually exclusive with `iconAction` and `href`/`onClick`.\n   */\n  submenu?: ActionsMenuItem[];\n  /** When set, the rendered desktop button is wrapped in a hover tooltip. */\n  tooltip?: React.ReactNode;\n};\n\nfunction actionKey(action: PageActionButton, idx: number) {\n  return `${action.label ?? action.ariaLabel ?? 'action'}-${idx}`;\n}\n\nfunction actionToMenuItems(action: PageActionButton, idx: number): ActionsMenuItem[] {\n  if (action.submenu && action.submenu.length > 0) {\n    // When a split-button action collapses into the merged mobile \"...\" menu,\n    // its chevron disappears and its children become sibling rows. Prefix\n    // each child with the parent label.\n    if (!action.label) return action.submenu;\n    return action.submenu.map(item => ({\n      ...item,\n      label: `${action.label} (${item.label})`,\n    }));\n  }\n\n  if (!action.label) return [];\n  return [\n    {\n      id: `action-${idx}`,\n      label: action.label,\n      icon: action.icon,\n      onClick: action.onClick,\n      disabled: action.disabled,\n      href: action.href,\n      iconAction: action.iconAction\n        ? {\n            icon: action.iconAction.icon,\n            'aria-label': action.iconAction['aria-label'],\n            onClick: action.iconAction.onClick as (() => void) | undefined,\n            href: action.iconAction.href,\n            openInNewTab: action.iconAction.openInNewTab,\n            disabled: action.iconAction.disabled,\n          }\n        : undefined,\n    },\n  ];\n}\n\ninterface RenderOptions {\n  /** Force the rendered button to be icon-only (label hidden). */\n  iconOnly?: boolean;\n  /** Stretch the button to fill flex parent (used in mobile bottom bar). */\n  fullWidth?: boolean;\n}\n\nfunction renderActionButton(action: PageActionButton, opts: RenderOptions = {}): React.ReactNode {\n  const button = renderRawActionButton(action, opts);\n  if (!action.tooltip) return button;\n  return (\n    <TooltipProvider delayDuration={0}>\n      <Tooltip>\n        <TooltipTrigger asChild>{button}</TooltipTrigger>\n        <TooltipContent>{action.tooltip}</TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n\nfunction renderRawActionButton(action: PageActionButton, opts: RenderOptions = {}): React.ReactNode {\n  // Two-target SplitButton — primary action + secondary icon action.\n  if (action.iconAction) {\n    return (\n      <SplitButton\n        variant={action.variant ?? undefined}\n        href={action.href}\n        prefetch={action.prefetch}\n        openInNewTab={action.openInNewTab}\n        onClick={action.onClick}\n        disabled={action.disabled}\n        mainDisabled={action.mainDisabled}\n        leftIcon={action.icon}\n        fullWidth={opts.fullWidth}\n        iconAction={action.iconAction}\n      >\n        {action.label}\n      </SplitButton>\n    );\n  }\n\n  // Submenu — single click target with a trailing chevron divider.\n  if (action.submenu && action.submenu.length > 0) {\n    return (\n      <ActionsMenuDropdown\n        groups={[{ items: action.submenu }]}\n        customTrigger={\n          <Button\n            variant=\"outline\"\n            disabled={action.disabled}\n            loading={action.loading}\n            leftIcon={action.icon}\n            splitIcon={<Chevron02DownIcon className=\"h-4 w-4\" />}\n            className={opts.fullWidth ? 'flex-1' : undefined}\n          >\n            {action.label}\n          </Button>\n        }\n      />\n    );\n  }\n\n  // Icon-only button (no label, or explicitly icon-only on desktop).\n  const isIconOnly = opts.iconOnly || !action.label || action.iconOnlyOnDesktop;\n  if (isIconOnly) {\n    const iconNode = action.iconOnlyOnDesktop ? (\n      <span className=\"inline-flex [&_svg]:!text-ods-text-primary\">{action.icon}</span>\n    ) : (\n      action.icon\n    );\n    return (\n      <Button\n        variant={action.variant}\n        size=\"icon\"\n        href={action.href}\n        prefetch={action.prefetch}\n        openInNewTab={action.openInNewTab}\n        onClick={action.onClick}\n        disabled={action.disabled}\n        loading={action.loading}\n        leftIcon={iconNode}\n        aria-label={action.label ?? action.ariaLabel}\n      />\n    );\n  }\n\n  // Default labeled button.\n  return (\n    <Button\n      variant={action.variant}\n      href={action.href}\n      prefetch={action.prefetch}\n      openInNewTab={action.openInNewTab}\n      onClick={action.onClick}\n      disabled={action.disabled}\n      loading={action.loading}\n      leftIcon={action.icon}\n      className={opts.fullWidth ? 'flex-1' : undefined}\n    >\n      {action.label}\n    </Button>\n  );\n}\n\nexport interface PageActionsProps {\n  variant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary';\n  actions: PageActionButton[];\n  menuActions?: ActionsMenuGroup[];\n  /**\n   * Slot rendered before the action buttons (e.g. a `TabSelector` for\n   * view-mode toggles). Honored by the `icon-buttons` and `menu-primary`\n   * variants. On mobile, `icon-buttons` hides it; `menu-primary` surfaces it\n   * as the header row INSIDE the \"…\" menu (the tickets design — the view\n   * switcher must stay reachable where the header has no room for it).\n   */\n  selector?: React.ReactNode;\n  className?: string;\n  /**\n   * Render placeholders instead of the actions — for pages whose action SET\n   * depends on data still in flight. Opt-in: a page whose actions are already\n   * final while its title loads keeps rendering them.\n   */\n  loading?: boolean;\n}\n\nconst ACTIONS_GAP = 'gap-[var(--spacing-system-xs)]';\n\n/** Matches `Button` size=\"icon\" / size=\"default\" exactly, so a placeholder and the\n *  button that replaces it occupy the same box at both breakpoints. `rounded-md`\n *  is the same token `Button` itself uses — a raw `6px` would agree today and\n *  drift the moment a platform overrides `--radius`. */\nconst SKELETON_HEIGHT = 'h-11 md:h-12';\nconst SKELETON_ICON_WIDTH = 'w-11 md:w-12';\nconst SKELETON_LABEL_WIDTH = 'w-[120px] md:w-[140px]';\nconst SKELETON_RADIUS = 'rounded-md';\n\n/**\n * Placeholders for actions whose SHAPE isn't known yet — the page is still\n * loading the record that decides which actions exist (e.g. an archived entity\n * offers \"Unarchive\" where an active one offers \"Edit\").\n *\n * Why not render the actions anyway: a button is a promise. Painting the\n * optimistic set means a flash of the wrong header AND a live click target that\n * can act on the wrong assumption before the data lands. A grey box promises\n * nothing and can't be clicked.\n *\n * The placeholders are derived from the actions the caller passed — one per\n * action, each sized like the button that action WILL render — so the common\n * case (the data confirms that set) settles with no layout shift at all. Sizing\n * per action matters: `renderRawActionButton` collapses a label-less action (or\n * one flagged `iconOnlyOnDesktop`) to a square `size=\"icon\"` button, and a\n * 140px-wide placeholder snapping to 48px is exactly the shift this is meant to\n * avoid.\n *\n * An empty list still yields one placeholder: `loading` means the action set is\n * unknown, not known-empty. Its shape comes from `emptyFallback`, because what\n * an empty header settles into differs per variant — `icon-buttons` and\n * `menu-primary` fall back to the square \"…\" overflow trigger, `primary-buttons`\n * has no overflow trigger at all and settles into a labelled button.\n */\nfunction ActionSkeletons({\n  actions,\n  fullWidth,\n  emptyFallback = 'icon',\n}: {\n  actions: PageActionButton[];\n  fullWidth?: boolean;\n  emptyFallback?: 'icon' | 'label';\n}) {\n  const items = actions.length > 0 ? actions : [null];\n  return (\n    <>\n      {items.map((action, idx) => {\n        // Mirrors `renderRawActionButton`'s `isIconOnly`. The mobile bottom bar\n        // stretches only labelled buttons (`fullWidth: !!action.label`), so an\n        // icon action keeps its square box there too.\n        const isIconOnly = action ? !action.label || !!action.iconOnlyOnDesktop : emptyFallback === 'icon';\n        return (\n          <Skeleton\n            // Static placeholder list — index IS the identity here.\n            key={`action-skeleton-${idx}`}\n            className={cn(\n              SKELETON_HEIGHT,\n              SKELETON_RADIUS,\n              isIconOnly ? SKELETON_ICON_WIDTH : fullWidth ? 'flex-1' : SKELETON_LABEL_WIDTH,\n            )}\n          />\n        );\n      })}\n    </>\n  );\n}\n\n/** Mobile placeholder for the variants that collapse to ONE trigger (icon button or \"…\"). */\nfunction MobileTriggerSkeleton() {\n  return <Skeleton className={cn(SKELETON_HEIGHT, SKELETON_ICON_WIDTH, SKELETON_RADIUS)} />;\n}\n\nexport function PageActions({\n  variant = 'icon-buttons',\n  actions,\n  menuActions,\n  selector,\n  className,\n  loading,\n}: PageActionsProps) {\n  if (variant === 'icon-buttons') {\n    return (\n      <IconButtonsVariant\n        actions={actions}\n        menuActions={menuActions}\n        selector={selector}\n        className={className}\n        loading={loading}\n      />\n    );\n  }\n\n  if (variant === 'menu-primary') {\n    return (\n      <MenuPrimaryVariant\n        actions={actions}\n        menuActions={menuActions || []}\n        selector={selector}\n        className={className}\n        loading={loading}\n      />\n    );\n  }\n\n  return <PrimaryButtonsVariant actions={actions} className={className} loading={loading} />;\n}\n\nfunction IconButtonsVariant({\n  actions,\n  menuActions,\n  selector,\n  className,\n  loading,\n}: {\n  actions: PageActionButton[];\n  menuActions?: ActionsMenuGroup[];\n  selector?: React.ReactNode;\n  className?: string;\n  loading?: boolean;\n}) {\n  const desktopActions = actions.filter(a => !a.showOnlyMobile);\n  const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0);\n\n  const isSingleAction = actions.length === 1 && !actions[0].submenu?.length;\n  const singleAction = isSingleAction ? actions[0] : null;\n  const useSingleActionMobile = isSingleAction && !hasMenuActions;\n\n  const mobileMenuGroups = [{ items: actions.flatMap(actionToMenuItems) }, ...(menuActions ?? [])];\n  const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0);\n\n  return (\n    <>\n      {/* Desktop: every action as an icon button + optional overflow menu */}\n      <div className={cn('hidden items-center md:flex', ACTIONS_GAP, className)}>\n        {selector}\n        {loading ? (\n          <ActionSkeletons actions={desktopActions} />\n        ) : (\n          <>\n            {desktopActions.map((action, idx) => (\n              <React.Fragment key={actionKey(action, idx)}>{renderActionButton(action)}</React.Fragment>\n            ))}\n            {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}\n          </>\n        )}\n      </div>\n\n      {/* Mobile: single icon button OR all actions merged into one \"...\" menu */}\n      <div className={cn('flex md:hidden', className)}>\n        {loading ? (\n          <MobileTriggerSkeleton />\n        ) : useSingleActionMobile && singleAction ? (\n          renderActionButton(singleAction, { iconOnly: true })\n        ) : hasMobileMenuItems ? (\n          <ActionsMenuDropdown groups={mobileMenuGroups} />\n        ) : null}\n      </div>\n    </>\n  );\n}\n\n/**\n * Primary buttons variant — primary + outline buttons on desktop,\n * fixed bottom bar on mobile.\n */\nfunction PrimaryButtonsVariant({\n  actions,\n  className,\n  loading,\n}: {\n  actions: PageActionButton[];\n  className?: string;\n  loading?: boolean;\n}) {\n  // Sort: outline first, accent last (rightmost on desktop).\n  const sortedActions = [...actions].sort((a, b) => {\n    if (a.variant === 'accent' && b.variant !== 'accent') return 1;\n    if (a.variant !== 'accent' && b.variant === 'accent') return -1;\n    return 0;\n  });\n\n  const desktopActions = sortedActions.filter(a => !a.showOnlyMobile);\n\n  return (\n    <>\n      <div className={cn('hidden items-center md:flex', ACTIONS_GAP, className)}>\n        {loading ? (\n          // No overflow trigger in this variant — an unknown action set settles\n          // into a labelled button, not a square one.\n          <ActionSkeletons actions={desktopActions} emptyFallback=\"label\" />\n        ) : (\n          desktopActions.map((action, idx) => (\n            <React.Fragment key={`desktop-${actionKey(action, idx)}`}>{renderActionButton(action)}</React.Fragment>\n          ))\n        )}\n      </div>\n\n      <MobileBottomActions actions={sortedActions} loading={loading} />\n    </>\n  );\n}\n\n/**\n * Menu + primary variant — \"...\" menu + primary buttons on desktop,\n * all actions merged into a single \"...\" menu on mobile.\n */\nfunction MenuPrimaryVariant({\n  actions,\n  menuActions,\n  selector,\n  className,\n  loading,\n}: {\n  actions: PageActionButton[];\n  menuActions: ActionsMenuGroup[];\n  selector?: React.ReactNode;\n  className?: string;\n  loading?: boolean;\n}) {\n  const desktopActions = actions.filter(a => !a.showOnlyMobile);\n  const hasMenuActions = menuActions.some(g => g.items.length > 0);\n\n  const mobileMenuGroups = [{ items: actions.flatMap(actionToMenuItems) }, ...menuActions];\n  const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0);\n\n  return (\n    <>\n      <div className={cn('hidden items-center md:flex', ACTIONS_GAP, className)}>\n        {selector}\n        {loading ? (\n          <ActionSkeletons actions={desktopActions} />\n        ) : (\n          <>\n            {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}\n            {desktopActions.map((action, idx) => (\n              <React.Fragment key={`desktop-${actionKey(action, idx)}`}>\n                {renderActionButton({ ...action, variant: action.variant || 'accent' })}\n              </React.Fragment>\n            ))}\n          </>\n        )}\n      </div>\n\n      <div className={cn('flex md:hidden', className)}>\n        {loading ? (\n          <MobileTriggerSkeleton />\n        ) : hasMobileMenuItems || selector ? (\n          // The selector rides into the menu as its header row: on mobile the\n          // title row has no room for it, and hiding it entirely (what\n          // `icon-buttons` does) would leave no way to switch views at all.\n          <ActionsMenuDropdown groups={mobileMenuGroups} header={selector} />\n        ) : null}\n      </div>\n    </>\n  );\n}\n\nfunction MobileBottomActions({ actions, loading }: { actions: PageActionButton[]; loading?: boolean }) {\n  return (\n    <div\n      className={cn(\n        'fixed bottom-0 left-0 right-0 z-50 md:hidden',\n        'border-t border-ods-border bg-ods-card',\n        // `lf` (24px at every breakpoint), NOT `l` — this bar is `md:hidden`, so the\n        // only value it ever uses is the MOBILE one, and `l` is 16px there. The\n        // fixed variant is what keeps the 24px the bar has always had.\n        'flex items-start p-[var(--spacing-system-lf)]',\n        ACTIONS_GAP,\n      )}\n    >\n      {loading ? (\n        <ActionSkeletons actions={actions} fullWidth emptyFallback=\"label\" />\n      ) : (\n        actions.map((action, idx) => (\n          <React.Fragment key={`mobile-${actionKey(action, idx)}`}>\n            {renderActionButton(action, { fullWidth: !!action.label })}\n          </React.Fragment>\n        ))\n      )}\n    </div>\n  );\n}\n\nexport function usePageActionsBottomPadding(variant: PageActionsProps['variant']) {\n  return variant === 'primary-buttons' || variant === 'menu-primary' ? 'pb-40 md:pb-0' : '';\n}\n\nexport default PageActions;\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport type { ActionsMenuGroup } from '../ui/actions-menu';\nimport { PageActions, type PageActionButton } from '../ui/page-actions';\nimport { BackButton } from './back-button';\n\n// Legacy interface for backward compatibility (layout version)\ninterface LegacyPageContainerProps {\n  children: React.ReactNode;\n  className?: string;\n  /** Whether to apply full-width background to the entire section */\n  fullWidthBackground?: boolean;\n  /** Custom background style/className for the section wrapper */\n  backgroundClassName?: string;\n  /** Custom background style object */\n  backgroundStyle?: React.CSSProperties;\n  /** Custom padding for the content container (overrides default responsive padding) */\n  contentPadding?: string;\n  /** Custom max-width for the content container (default: max-w-[1920px]) */\n  maxWidth?: string;\n  /** HTML element type for the container */\n  as?: 'section' | 'div' | 'main' | 'article';\n  /** HTML id for the container */\n  id?: string;\n}\n\n// New advanced interface (UI version)\ninterface AdvancedPageContainerProps {\n  /**\n   * Page content\n   */\n  children: React.ReactNode;\n  /**\n   * Page variant determines layout structure\n   */\n  variant?: 'list' | 'detail' | 'form' | 'content';\n  /**\n   * Page title (displayed as h1)\n   */\n  title?: string;\n  /**\n   * Subtitle or description (supports both string and ReactNode)\n   */\n  subtitle?: string | React.ReactNode;\n  /**\n   * Back button configuration\n   */\n  backButton?: {\n    label?: string;\n    onClick: () => void;\n  };\n  /**\n   * Header actions (buttons, controls, etc.)\n   * Can be used together with or instead of `actions` prop\n   */\n  headerActions?: React.ReactNode;\n  /**\n   * Page action buttons configuration\n   * Automatically renders PageActions component with appropriate variant:\n   * - 'list' variant → 'icon-buttons' (collapses to menu on mobile)\n   * - 'detail'/'form' variants → 'primary-buttons' (fixed bottom on mobile)\n   */\n  actions?: PageActionButton[];\n  /**\n   * Override the automatically determined PageActions variant\n   */\n  actionsVariant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary';\n  /**\n   * Page action menu items configuration\n   */\n  menuActions?: ActionsMenuGroup[];\n  /**\n   * Custom header content (overrides title/subtitle)\n   */\n  headerContent?: React.ReactNode;\n  /**\n   * Container padding\n   */\n  padding?: 'none' | 'sm' | 'md' | 'lg';\n  /**\n   * Container background\n   */\n  background?: 'default' | 'card' | 'transparent';\n  /**\n   * Additional CSS classes for container\n   */\n  className?: string;\n  /**\n   * Additional CSS classes for content area\n   */\n  contentClassName?: string;\n  /**\n   * Whether to show the standard header section\n   */\n  showHeader?: boolean;\n}\n\n// Union type that supports both interfaces\nexport type PageContainerProps = LegacyPageContainerProps | AdvancedPageContainerProps;\n\n// Type guard to determine which interface is being used\nfunction isAdvancedProps(props: PageContainerProps): props is AdvancedPageContainerProps {\n  return (\n    'variant' in props ||\n    'title' in props ||\n    'subtitle' in props ||\n    'backButton' in props ||\n    'headerActions' in props ||\n    'headerContent' in props ||\n    'showHeader' in props ||\n    'contentClassName' in props ||\n    'actions' in props ||\n    'actionsVariant' in props\n  );\n}\n\n/**\n * @deprecated Use `PageLayout` from `'../layout/page-layout'` instead.\n *\n * Unified Page Container Component\n *\n * Supports both legacy layout patterns and advanced UI patterns:\n *\n * LEGACY USAGE (backward compatible):\n * <PageContainer backgroundClassName=\"bg-gray-100\">\n *   <h1>Your Content</h1>\n * </PageContainer>\n *\n * ADVANCED USAGE (new features):\n * <PageContainer variant=\"detail\" title=\"Page Title\" backButton={{onClick: () => {}}}>\n *   <div>Your Content</div>\n * </PageContainer>\n */\nexport function PageContainer(props: PageContainerProps) {\n  if (isAdvancedProps(props)) {\n    return renderAdvancedPageContainer(props);\n  } else {\n    return renderLegacyPageContainer(props);\n  }\n}\n\n// Legacy implementation (preserves original behavior exactly)\nfunction renderLegacyPageContainer({\n  children,\n  className = '',\n  fullWidthBackground = true,\n  backgroundClassName = 'bg-ods-bg',\n  backgroundStyle,\n  contentPadding,\n  maxWidth = 'max-w-[1920px]',\n  as: Component = 'section',\n  id,\n}: LegacyPageContainerProps) {\n  const content = <div className={cn(maxWidth, contentPadding, 'mx-auto', className)}>{children}</div>;\n\n  if (fullWidthBackground) {\n    return (\n      <Component className={cn('w-full', backgroundClassName)} style={backgroundStyle} id={id}>\n        {content}\n      </Component>\n    );\n  }\n\n  // If fullWidthBackground is false, apply background to content container only\n  return (\n    <Component className=\"w-full\" id={id}>\n      <div className={cn(maxWidth, contentPadding, 'mx-auto', backgroundClassName, className)} style={backgroundStyle}>\n        {children}\n      </div>\n    </Component>\n  );\n}\n\n// Advanced implementation (from UI component)\nfunction renderAdvancedPageContainer({\n  children,\n  variant = 'content',\n  title,\n  subtitle,\n  backButton,\n  headerActions,\n  headerContent,\n  actions,\n  actionsVariant,\n  menuActions,\n  padding = 'none',\n  background = 'transparent',\n  className,\n  contentClassName,\n  showHeader = true,\n}: AdvancedPageContainerProps) {\n  // Determine PageActions variant based on page variant\n  const getActionsVariant = () => {\n    if (actionsVariant) return actionsVariant;\n    // List pages use icon-buttons (collapses to menu on mobile)\n    if (variant === 'list') return 'icon-buttons';\n    // Detail/form pages use primary-buttons (fixed bottom on mobile)\n    return 'primary-buttons';\n  };\n\n  // Render actions component\n  const renderActions = () => {\n    if (!actions || actions.length === 0) return null;\n    return <PageActions variant={getActionsVariant()} actions={actions} menuActions={menuActions} />;\n  };\n\n  // Check if we need bottom padding for mobile fixed actions\n  const needsBottomPadding = actions && actions.length > 0 && getActionsVariant() === 'primary-buttons';\n\n  const paddingClasses = {\n    none: '',\n    sm: 'p-4',\n    md: 'p-6',\n    lg: 'p-8',\n  };\n\n  const backgroundClasses = {\n    default: 'bg-ods-bg',\n    card: 'bg-ods-card',\n    transparent: '',\n  };\n\n  const renderHeader = () => {\n    if (!showHeader) return null;\n\n    if (headerContent) {\n      return <div className={cn('flex items-end justify-between gap-4')}>{headerContent}</div>;\n    }\n\n    if (variant === 'detail') {\n      return (\n        <div className=\"flex items-end justify-between gap-4 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between\">\n          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n            {/* Back Button */}\n            {backButton && (\n              <BackButton onClick={backButton.onClick} label={backButton.label} className=\"hidden md:inline-flex\" />\n            )}\n\n            {/* Title */}\n            {title && <h1 className=\"text-ods-text-primary text-h2\">{title}</h1>}\n\n            {/* Subtitle */}\n            {subtitle && <div className=\"text-ods-text-secondary text-h6\">{subtitle}</div>}\n          </div>\n\n          {/* Header Actions */}\n          {(headerActions || actions) && (\n            <div className=\"flex shrink-0 items-center gap-2\">\n              {headerActions}\n              {renderActions()}\n            </div>\n          )}\n        </div>\n      );\n    }\n\n    if (variant === 'list') {\n      return (\n        <div className=\"flex items-center justify-between gap-4 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-center lg:justify-between\">\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n            {/* Back Button */}\n            {backButton && (\n              <BackButton onClick={backButton.onClick} label={backButton.label} className=\"hidden md:inline-flex\" />\n            )}\n\n            {title && <h1 className=\"text-ods-text-primary text-h2\">{title}</h1>}\n            {subtitle && <div className=\"text-ods-text-secondary text-h6\">{subtitle}</div>}\n          </div>\n\n          {/* Header Actions */}\n          {(headerActions || actions) && (\n            <div className=\"flex shrink-0 items-center gap-3\">\n              {headerActions}\n              {renderActions()}\n            </div>\n          )}\n        </div>\n      );\n    }\n\n    if (variant === 'form') {\n      return (\n        <div className=\"flex items-end justify-between gap-4 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between\">\n          <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n            {/* Back Button */}\n            {backButton && (\n              <BackButton onClick={backButton.onClick} label={backButton.label} className=\"hidden md:inline-flex\" />\n            )}\n\n            {title && <h1 className=\"text-ods-text-primary text-h2\">{title}</h1>}\n          </div>\n\n          {/* Header Actions */}\n          {(headerActions || actions) && (\n            <div className=\"flex shrink-0 items-center gap-4\">\n              {headerActions}\n              {renderActions()}\n            </div>\n          )}\n        </div>\n      );\n    }\n\n    // Default content header\n    return (\n      <div className=\"flex items-center justify-between gap-4 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-center lg:justify-between\">\n        {(title || subtitle) && (\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n            {title && <h1 className=\"text-ods-text-primary text-h2\">{title}</h1>}\n            {subtitle && <div className=\"text-ods-text-secondary text-h6\">{subtitle}</div>}\n          </div>\n        )}\n\n        {(headerActions || actions) && (\n          <div className=\"flex shrink-0 items-center gap-3\">\n            {headerActions}\n            {renderActions()}\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  const getContainerClasses = () => {\n    const baseClasses = ['flex flex-col w-full', backgroundClasses[background], paddingClasses[padding]];\n\n    switch (variant) {\n      case 'list':\n        return cn(baseClasses, 'gap-4 md:gap-6', className);\n      case 'detail':\n        return cn(baseClasses, 'gap-4 md:gap-6', className);\n      case 'form':\n        return cn(baseClasses, 'gap-6 md:gap-10', className);\n      case 'content':\n      default:\n        return cn(baseClasses, 'gap-4 md:gap-6', className);\n    }\n  };\n\n  const getContentClasses = () => {\n    // Add bottom padding on mobile when using primary-buttons variant (fixed bottom bar)\n    const mobilePadding = needsBottomPadding ? 'pb-28 md:pb-0' : '';\n\n    switch (variant) {\n      case 'detail':\n        return cn('flex-1 overflow-auto', mobilePadding, contentClassName);\n      case 'list':\n        return cn('flex flex-col gap-4 md:gap-6', mobilePadding, contentClassName);\n      case 'form':\n        return cn('flex flex-col gap-4 md:gap-10', mobilePadding, contentClassName);\n      case 'content':\n      default:\n        return cn('flex-1', mobilePadding, contentClassName);\n    }\n  };\n\n  return (\n    <div className={getContainerClasses()}>\n      {renderHeader()}\n\n      <div className={getContentClasses()}>{children}</div>\n    </div>\n  );\n}\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const ListPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) => (\n  <PageContainer {...props} variant=\"list\" />\n);\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const DetailPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) => (\n  <PageContainer {...props} variant=\"detail\" />\n);\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const FormPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) => (\n  <PageContainer {...props} variant=\"form\" />\n);\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const ContentPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) => (\n  <PageContainer {...props} variant=\"content\" />\n);\n\n// Re-export PageActionButton type for convenience\nexport type { PageActionButton } from '../ui/page-actions';\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport default PageContainer;\n","'use client';\n\nimport React from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { useAuthedImageSrc } from '../../hooks/use-authed-image-src';\nimport { cn } from '../../utils/cn';\nimport { personInitials } from '../../utils/format';\n\n/** The design size, when a caller does not state one. */\nconst ENTITY_IMAGE_SIZE = 'size-[52px] md:size-[60px]';\n\nexport interface EntityImageProps {\n  src?: string | null;\n  alt?: string;\n  /** Overrides the initials source. Defaults to `alt`. */\n  fallbackText?: string;\n  /**\n   * REPLACES the default size, for both the image and its initials fallback.\n   *\n   * Sizing through `className` does NOT work here and cannot be made to:\n   * the default is responsive (`size-[52px] md:size-[60px]`), and\n   * tailwind-merge lets `size-*` override `w`/`h` but never the reverse — so a\n   * caller passing `h-4 w-6` dropped the base `size-[52px]` and kept\n   * `md:size-[60px]`, rendering 16px on a phone and 60px on a desktop. State\n   * the size here instead; it is substituted, not merged.\n   */\n  sizeClassName?: string;\n  className?: string;\n}\n\nexport function EntityImage({ src, alt, fallbackText, sizeClassName, className }: EntityImageProps) {\n  const resolvedSrc = useAuthedImageSrc(src);\n  // Which src failed, rather than a bare \"failed\" flag reset from an effect on\n  // every src change. The reset is then implicit — a new src simply is not the\n  // one that failed — so the fallback initials never survive into the render\n  // that swapped the image, and a new src costs no second render pass.\n  const [failedSrc, setFailedSrc] = React.useState<string | null>(null);\n  const imageFailed = resolvedSrc !== undefined && failedSrc === resolvedSrc;\n\n  const showFallback = imageFailed || !resolvedSrc;\n  const initials = personInitials(fallbackText ?? alt);\n\n  if (showFallback) {\n    return (\n      <div\n        aria-label={alt}\n        className={cn(\n          'flex shrink-0 select-none items-center justify-center rounded-md border border-ods-border bg-ods-bg text-ods-text-secondary text-h4',\n          sizeClassName ?? ENTITY_IMAGE_SIZE,\n          className,\n        )}\n      >\n        {initials || '?'}\n      </div>\n    );\n  }\n\n  return (\n    <Image\n      src={resolvedSrc}\n      alt={alt ?? ''}\n      onError={() => setFailedSrc(resolvedSrc)}\n      className={cn(\n        'shrink-0 rounded-md border border-ods-border object-contain',\n        sizeClassName ?? ENTITY_IMAGE_SIZE,\n        className,\n      )}\n      // Intrinsic hint only — the responsive `size-*` classes above own the\n      // rendered box. `unoptimized` because `useAuthedImageSrc` hands back a\n      // `blob:` URL under bearer-token hosts, which no image optimizer can fetch.\n      width={60}\n      height={60}\n      unoptimized\n    />\n  );\n}\n","'use client';\n\n/* ============================================================================\n * ⛔️ FROZEN — DO NOT MODIFY (AI agents & contributors, read this first)\n * ----------------------------------------------------------------------------\n * `TitleBlock` is the FINALIZED title/subtitle/back-button/actions chrome used\n * by `PageLayout`. It is a locked, complete component — treat it as read-only.\n *\n * Do NOT: change the markup/CSS, alter the title typography (`text-h2`) or\n * subtitle (`text-h6`), change the image/title 2-column layout, re-route this\n * through a `PageHeader`/`PageWithHeader` primitive, or add/rename props. Do\n * NOT \"unify\"/\"refactor\"/\"simplify\" it or restyle it to match another surface.\n *\n * Why this rule exists (the incident it prevents): a refactor once rewrote\n * this to delegate to a new `PageHeader` (title bumped to `text-h1`, new\n * subtitle styling) to \"unify\" page chrome — it silently changed every page\n * using `PageLayout` and had to be fully reverted. This code IS that reverted,\n * correct baseline.\n *\n * Downstream consumers (OpenFrame pages, `DevSectionPage`, `DocViewer`, and the\n * multi-platform hub via its local `PageWithHeader`) depend on the EXACT\n * current output. If a new design needs different chrome, build a SEPARATE new\n * component — never mutate this one. If an edit here seems unavoidable, STOP\n * and get explicit human sign-off first.\n *\n * SANCTIONED EXCEPTION (2026-06, explicit human sign-off): the OPTIONAL\n * `titleSize` prop. It defaults to `'h2'` — i.e. the frozen baseline above is\n * unchanged for EVERY existing caller. A caller may pass `titleSize=\"h1\"` to\n * opt the title typography up to `text-h1` (used by the unified Help Center\n * pages). This is additive and default-preserving; do NOT change the default or\n * touch anything else here.\n *\n * SANCTIONED EXCEPTION (2026-06, explicit human sign-off): the OPTIONAL\n * `titleAdornment` and `loading` props. `titleAdornment` renders a node inline\n * after the title (e.g. a status `Tag`). `loading` swaps ONLY the title/subtitle\n * TEXT for line-box-accurate skeleton bars (the surrounding `h1`/`p` and their\n * typography are untouched, so a loading header is pixel-identical in height to\n * the loaded one — used by page skeletons that render through `PageLayout`).\n * Both are additive + default-preserving: omit them and every existing caller is\n * byte-identical. Do NOT change defaults or touch anything else here.\n *\n * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL\n * `subtitleRow` prop. Defaults to `'when-set'` — the subtitle row keeps\n * rendering only when `subtitle` is truthy, so every existing caller is\n * byte-identical. The other two values decide how a page trades a header that\n * never moves against a header that never shows an empty line; see the prop's\n * own doc for which to pick. Additive + default-preserving; do NOT change the\n * default.\n *\n * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL\n * `titleWrap` prop. Defaults to `false` — the frozen single-line `truncate`\n * baseline is unchanged for every existing caller. Content DETAIL pages (whose\n * h1 is CMS data of arbitrary length — releases, legal docs, FAQ docs, dev\n * sections) pass `titleWrap` to let the title wrap onto multiple lines instead\n * of being ellipsis-clipped. Additive + default-preserving; do NOT change the\n * default or touch anything else here.\n *\n * SANCTIONED EXCEPTION (2026-07, explicit human sign-off — ClickUp 86ahd6uy5):\n * responsive action layout on md+. This one intentionally CHANGES the baseline\n * for every caller, per the approved design (Figma open-design-system node\n * 2200-7452): instead of tablet always stacking actions below the title\n * (`md:flex-col`) and desktop always keeping one row (long titles clipped),\n * md+ is a single `flex-wrap` row — actions stay inline with a short title on\n * BOTH tablet and desktop, and wrap to a second row only when the title is\n * long enough to overflow. The title column is `md:flex-none md:max-w-full`\n * so its natural width drives the wrap while `truncate` still clamps a title\n * that alone exceeds the container. The mobile (base) layout is untouched.\n * This is the new frozen baseline; do NOT re-introduce breakpoint stacking.\n *\n * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): overflow tooltips on\n * the title and subtitle. This one CHANGES the baseline for every caller: the\n * native `title=\"\"` attribute on both is replaced by the ODS `FloatingTooltip`,\n * armed only when the text is genuinely clipped (`useIsTruncated`). Both stay\n * single-line — that is the design, and the tooltip is what makes a clipped\n * title readable instead of merely pretty. Nothing is lost for assistive tech:\n * `text-overflow: ellipsis` never hid the text from the accessibility tree, so\n * the full string was always exposed; the attribute only ever served a mouse.\n * The wrapper `div` the tooltip needs goes OUTSIDE the `h1`/`p` — never inside:\n * a `div` inside a `p` is auto-closed by the HTML parser, which would split the\n * subtitle paragraph and desync hydration. The `titleWrap` branch is untouched\n * (wrapped text never clips, so it never needs a tooltip).\n * ========================================================================== */\n\nimport type React from 'react';\nimport { useIsTruncated } from '../../hooks/ui/use-is-truncated';\nimport { cn } from '../../utils/cn';\nimport type { ActionsMenuGroup } from '../ui/actions-menu';\nimport { EntityImage } from '../ui/entity-image';\nimport { FloatingTooltip } from '../ui/floating-tooltip';\nimport { PageActions, type PageActionButton } from '../ui/page-actions';\nimport { BackButton } from './back-button';\n\n/**\n * Minimum height of the title block's content column, matched to the action\n * button height: the icon button on mobile (`h-11` → 44px) and the default\n * button on desktop (`md:h-12` → 48px). Applied to the inner title column (which\n * has no padding) rather than the root — the root's `pt`/`mb` are box-sizing\n * border-box and would otherwise absorb the floor. Keeps the header a consistent\n * height across pages whether or not they render action buttons, so the content\n * below starts at the same baseline. Exported so other page chrome can reuse it.\n */\nexport const TITLE_BLOCK_MIN_HEIGHT = 'min-h-11 md:min-h-12';\n\nexport interface TitleBlockProps {\n  title?: string;\n  subtitle?: string;\n  image?: { src: string; alt?: string };\n  backButton?: { label?: string; onClick: () => void };\n  actions?: PageActionButton[];\n  actionsVariant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary';\n  menuActions?: ActionsMenuGroup[];\n  /** Desktop-only slot (e.g. a `TabSelector`) rendered with the actions. Hidden on mobile. */\n  selector?: React.ReactNode;\n  /**\n   * Visual variant.\n   * - `plain` (default): transparent background, no border.\n   * - `card`: card background, border, and padding on mobile only — collapses to plain on md+.\n   */\n  variant?: 'plain' | 'card';\n  className?: string;\n  /** Title typography size. Default `'h2'` (the frozen baseline). Pass `'h1'` to\n   *  opt the title up to `text-h1` (the unified Help Center pages). Subtitle stays\n   *  `text-h6` either way. */\n  titleSize?: 'h1' | 'h2';\n  /** Optional node rendered inline, immediately after the title (e.g. a status `Tag`).\n   *  Additive + default-preserving: omit it and every existing caller is byte-identical. */\n  titleAdornment?: React.ReactNode;\n  /** When true, the title/subtitle TEXT is replaced by line-box-accurate skeleton bars\n   *  (typography + surrounding markup unchanged, so header height is identical to loaded).\n   *  Additive + default-preserving: omit it and every existing caller is byte-identical. */\n  loading?: boolean;\n  /**\n   * Render the ACTIONS as placeholders too. Separate from `loading` on purpose:\n   * a page whose action set is already final (it depends on the route, not on\n   * the record) keeps showing its real buttons while the title loads.\n   */\n  loadingActions?: boolean;\n  /**\n   * When the subtitle row occupies the layout. The subtitle is often optional\n   * RECORD data — unknown while loading, sometimes absent afterwards — and there\n   * is no setting that both fills the loading header and never leaves a blank\n   * line, so the caller picks which one matters.\n   *\n   * - `'when-set'` (default) — only when `subtitle` has text.\n   * - `'while-loading'` — also during `loading`, as a skeleton bar. The header\n   *   looks complete while it loads, and the row collapses if the record turns\n   *   out to have no subtitle (so that case settles one line shorter).\n   * - `'always'` — the row never collapses; an empty subtitle holds its line\n   *   with an invisible spacer. The header cannot change height. Use it when the\n   *   loaded page ALWAYS has a subtitle — typically a standalone skeleton\n   *   component matching a page whose subtitle is guaranteed.\n   */\n  subtitleRow?: 'when-set' | 'while-loading' | 'always';\n  /** When true, a long title WRAPS onto multiple lines instead of the frozen single-line\n   *  ellipsis clamp. For content detail pages whose h1 is CMS data of arbitrary length.\n   *  Additive + default-preserving: omit it and every existing caller is byte-identical. */\n  titleWrap?: boolean;\n}\n\n/**\n * Inline text skeleton — used only in `loading` mode, placed directly inside the real\n * `h1`/`p`. It is a single `inline-block` bar whose height is intentionally SHORTER than the\n * typography line-height, and it uses the default baseline alignment. That way the element's\n * own line-box STRUT (text-h2 → 40px, text-h6 → 20px) sets the height exactly as it would for\n * real text — the bar fits within the ascent and never inflates the line. So a loading header\n * is pixel-identical in height to the loaded one. Phrasing-valid (`span` only).\n */\nfunction TitleTextSkeleton({ widthClass, heightClass }: { widthClass: string; heightClass: string }) {\n  return (\n    <span\n      aria-hidden\n      className={cn('inline-block max-w-full animate-pulse rounded-md bg-ods-border', widthClass, heightClass)}\n    />\n  );\n}\n\n/**\n * Bar geometry per title size, following the ODS type scale across breakpoints\n * (`md` = 800px, `lg` = 1280px here — the SAME widths the typography tokens\n * switch at, so bar and text step together).\n *\n * `h2` needs no `lg` step because `text-h2` has none: it is 24/32 on mobile and\n * 32/40 from tablet up. `text-h1` scales at both breakpoints (40 → 48 → 56), so\n * its bar does too — reusing the h2 bar there would leave a stamp-sized smudge\n * inside a 64px line. Heights stay well under the line box's ascent on purpose;\n * see `TitleTextSkeleton` for why that is what keeps the header height honest.\n */\nconst TITLE_SKELETON_SIZE = {\n  h1: { width: 'w-56 md:w-80 lg:w-96', height: 'h-7 md:h-8 lg:h-9' },\n  h2: { width: 'w-48 md:w-72', height: 'h-4 md:h-6' },\n} as const;\n\nexport function TitleBlock({\n  title,\n  subtitle,\n  image,\n  backButton,\n  actions,\n  actionsVariant = 'icon-buttons',\n  menuActions,\n  selector,\n  variant = 'plain',\n  className,\n  titleSize = 'h2',\n  titleAdornment,\n  loading,\n  loadingActions,\n  subtitleRow = 'when-set',\n  titleWrap = false,\n}: TitleBlockProps) {\n  const hasSubtitleRow = !!subtitle || subtitleRow === 'always' || (subtitleRow === 'while-loading' && !!loading);\n  const hasActions = actions && actions.length > 0;\n  const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0);\n  const titleClass = titleSize === 'h1' ? 'text-h1' : 'text-h2';\n  // Frozen baseline is the single-line `truncate`; `titleWrap` swaps it for\n  // multi-line wrapping (break-words guards pathological unbroken tokens).\n  const titleOverflowClass = titleWrap ? 'break-words' : 'truncate';\n  const skeletonSize = TITLE_SKELETON_SIZE[titleSize];\n\n  // Exactly one title element renders per pass, so one ref covers every branch.\n  // The tooltip arms itself only on real clipping — repeating a fully visible\n  // title back to the user is noise, and `titleWrap`/loading never clip at all.\n  const { ref: titleRef, isTruncated: titleTruncated } = useIsTruncated<HTMLHeadingElement>(loading ? null : title);\n  const { ref: subtitleRef, isTruncated: subtitleTruncated } = useIsTruncated<HTMLParagraphElement>(\n    loading ? null : subtitle,\n  );\n  const titleNode = loading ? (\n    <TitleTextSkeleton widthClass={skeletonSize.width} heightClass={skeletonSize.height} />\n  ) : (\n    title\n  );\n\n  return (\n    <div\n      className={cn(\n        'flex items-end justify-between gap-[var(--spacing-system-m)]',\n        // md+: one wrapping row — actions stay inline with a short title and\n        // wrap to a second row only when the title overflows (ClickUp 86ahd6uy5).\n        'md:flex-wrap md:content-end',\n        'pt-[var(--spacing-system-l)]',\n        variant === 'card'\n          ? cn(\n              'border-b border-ods-border bg-ods-card',\n              'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',\n              'md:border-b-0 md:bg-transparent',\n              'md:px-0 md:pb-0',\n              'md:mb-[var(--spacing-system-l)]',\n            )\n          : 'mb-[var(--spacing-system-l)]',\n        className,\n      )}\n    >\n      {/* md+: `flex-none` sizes the column to its content so a long title (capped\n          at `max-w-full`, still truncating) pushes the actions onto the next\n          wrap line; a short title leaves them inline. Base (mobile) keeps flex-1. */}\n      <div\n        className={cn(\n          'flex min-w-0 flex-1 flex-col justify-center gap-[var(--spacing-system-xs)] md:max-w-full md:flex-none',\n          TITLE_BLOCK_MIN_HEIGHT,\n        )}\n      >\n        {backButton && (\n          <BackButton onClick={backButton.onClick} label={backButton.label} className=\"hidden md:inline-flex\" />\n        )}\n        {image || hasSubtitleRow || loading ? (\n          <div className=\"flex w-full min-w-0 items-center gap-[var(--spacing-system-m)]\">\n            {image && <EntityImage src={image.src} alt={image.alt} fallbackText={image.alt || title} />}\n            <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n              {(loading || title) &&\n                (titleAdornment ? (\n                  <div className=\"flex w-full min-w-0 items-center gap-[var(--spacing-system-m)]\">\n                    {/* `min-w-0` moves onto the tooltip's wrapper: the wrapper is\n                        now the flex item, and without it the row refuses to\n                        shrink and pushes the adornment out. */}\n                    <FloatingTooltip\n                      content={title}\n                      side=\"bottom\"\n                      disabled={!titleTruncated}\n                      triggerClassName=\"min-w-0\"\n                    >\n                      <h1\n                        ref={titleRef}\n                        className={cn(titleClass, 'min-w-0 text-ods-text-primary', titleOverflowClass)}\n                      >\n                        {titleNode}\n                      </h1>\n                    </FloatingTooltip>\n                    <span className=\"shrink-0\">{titleAdornment}</span>\n                  </div>\n                ) : (\n                  <FloatingTooltip content={title} side=\"bottom\" disabled={!titleTruncated}>\n                    <h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary', titleOverflowClass)}>\n                      {titleNode}\n                    </h1>\n                  </FloatingTooltip>\n                ))}\n              {hasSubtitleRow && (\n                /* The NBSP is a pure spacer, reached only in `'always'` mode: an\n                   empty `p` collapses to zero height (its only content would be\n                   collapsible whitespace), which is exactly the shift that mode\n                   exists to prevent. Hidden from AT — it carries no meaning. */\n                <FloatingTooltip content={subtitle} side=\"bottom\" disabled={!subtitleTruncated}>\n                  <p\n                    ref={subtitleRef}\n                    className=\"truncate text-ods-text-secondary text-h6\"\n                    aria-hidden={!loading && !subtitle ? true : undefined}\n                  >\n                    {loading ? (\n                      <TitleTextSkeleton widthClass=\"w-28 md:w-36\" heightClass=\"h-2.5 md:h-3\" />\n                    ) : (\n                      subtitle || '\\u00A0'\n                    )}\n                  </p>\n                </FloatingTooltip>\n              )}\n            </div>\n          </div>\n        ) : (\n          title &&\n          (titleAdornment ? (\n            <div className=\"flex w-full min-w-0 items-center gap-[var(--spacing-system-m)]\">\n              <FloatingTooltip content={title} side=\"bottom\" disabled={!titleTruncated} triggerClassName=\"min-w-0\">\n                <h1 ref={titleRef} className={cn(titleClass, 'min-w-0 text-ods-text-primary', titleOverflowClass)}>\n                  {title}\n                </h1>\n              </FloatingTooltip>\n              <span className=\"shrink-0\">{titleAdornment}</span>\n            </div>\n          ) : (\n            /* This branch never had the single-line clamp, so text already\n                 wraps; `titleWrap` only adds break-words for pathological\n                 unbroken tokens. No class change when the prop is unset —\n                 the frozen baseline stays byte-identical. */\n            <h1 className={cn(titleClass, 'text-ods-text-primary', titleWrap && 'break-words')}>{title}</h1>\n          ))\n        )}\n      </div>\n\n      {/* `loadingActions` opens this gate on its own: the case the flag exists for\n          is a page that does not YET know its action set, which is exactly the\n          page that passes `actions={[]}` — gating on `hasActions` alone would\n          render nothing and then pop the real buttons in. */}\n      {(hasActions || hasMenuActions || selector || loadingActions) && (\n        <div className=\"flex shrink-0 items-center gap-2\">\n          <PageActions\n            variant={actionsVariant}\n            actions={actions ?? []}\n            menuActions={menuActions}\n            selector={selector}\n            loading={loadingActions}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default TitleBlock;\n","'use client';\n\n/* ============================================================================\n * ⛔️ FROZEN — DO NOT MODIFY (AI agents & contributors, read this first)\n * ----------------------------------------------------------------------------\n * `PageLayout` and its `TitleBlock` are a FINALIZED, locked component. They are\n * the canonical, stable page chrome for OpenFrame surfaces and their visual +\n * behavioral contract is intentionally complete. Treat this file as read-only.\n *\n * Do NOT: change the markup/CSS, swap the title typography (`text-h2`) or\n * subtitle (`text-h6`), re-architect this to delegate to another primitive,\n * add/rename props, or \"unify\"/\"refactor\"/\"simplify\" it. Do NOT restyle to\n * match some other surface.\n *\n * Why this rule exists (the incident it prevents): a refactor once re-styled\n * this layout (bumped the title to `text-h1`, rerouted it through a new\n * `PageHeader`/`PageWithHeader` chain) to \"unify\" page chrome. That silently\n * changed the look of every page rendered through `PageLayout` and had to be\n * fully reverted. The current code IS the reverted, correct baseline.\n *\n * MANY consumers depend on the EXACT current output — not only OpenFrame pages\n * but also `DevSectionPage`, `DocViewer`, and the multi-platform hub (through\n * its own local `PageWithHeader`). A change here ripples across all of them.\n *\n * If a new design genuinely needs different chrome: build a SEPARATE new\n * component for it. Never mutate this one. If you believe an edit here is\n * unavoidable, STOP and get explicit human sign-off first.\n *\n * SANCTIONED EXCEPTION (2026-06, explicit human sign-off): the OPTIONAL\n * `titleSize` prop, forwarded to `TitleBlock`. Defaults to `'h2'` — the frozen\n * baseline is unchanged for every existing caller. `titleSize=\"h1\"` opts the\n * title up to `text-h1` (the unified Help Center pages). Additive + default-\n * preserving; do NOT change the default or anything else here.\n *\n * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL\n * `titleWrap` prop, forwarded to `TitleBlock`. Defaults to `false` (frozen\n * single-line truncate). Content detail pages with CMS titles pass it to let\n * long titles wrap instead of ellipsis-clip. Additive + default-preserving.\n *\n * SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL\n * `subtitleRow` prop, forwarded to `TitleBlock`. Defaults to `'when-set'` (the\n * subtitle row renders only when there is text for it). Pages whose subtitle is\n * optional record data use it to choose how the row behaves while loading.\n * Additive + default-preserving.\n * ========================================================================== */\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport type { ActionsMenuGroup } from '../ui/actions-menu';\nimport type { PageActionButton } from '../ui/page-actions';\nimport { TitleBlock } from './title-block';\n\nexport interface PageLayoutProps {\n  children: React.ReactNode;\n  title?: string;\n  subtitle?: string;\n  image?: { src: string; alt?: string };\n  backButton?: { label?: string; onClick: () => void };\n  actions?: PageActionButton[];\n  actionsVariant?: 'icon-buttons' | 'primary-buttons' | 'menu-primary';\n  menuActions?: ActionsMenuGroup[];\n  /** Desktop-only slot (e.g. a `TabSelector`) rendered with the actions. Hidden on mobile. */\n  selector?: React.ReactNode;\n  /** Header visual variant. `card` adds a card background, border, and padding on mobile. */\n  headerVariant?: 'plain' | 'card';\n  className?: string;\n  contentClassName?: string;\n  showHeader?: boolean;\n  /** Title typography size, forwarded to `TitleBlock`. Default `'h2'` (frozen\n   *  baseline). Pass `'h1'` for the unified Help Center pages. */\n  titleSize?: 'h1' | 'h2';\n  /** Optional node rendered inline next to the title (e.g. a status `Tag`), forwarded to `TitleBlock`. */\n  titleAdornment?: React.ReactNode;\n  /** When true, the title/subtitle render as line-box-accurate skeleton bars (forwarded to\n   *  `TitleBlock`). Header height stays identical to the loaded state — for page skeletons. */\n  loading?: boolean;\n  /** Render the header ACTIONS as placeholders (their set depends on data still loading). */\n  loadingActions?: boolean;\n  /** When the subtitle row occupies the layout (forwarded to `TitleBlock`):\n   *  `'when-set'` (default), `'while-loading'` (skeleton bar while loading, collapses\n   *  if the record has no subtitle), or `'always'` (never collapses). */\n  subtitleRow?: 'when-set' | 'while-loading' | 'always';\n  /** When true, a long title wraps onto multiple lines instead of the frozen single-line\n   *  ellipsis clamp (forwarded to `TitleBlock`). For content detail pages whose h1 is CMS\n   *  data of arbitrary length. Additive + default-preserving. */\n  titleWrap?: boolean;\n}\n\n/**\n * Page layout container with consistent spacing, header, and actions.\n *\n * Uses `--spacing-system-l` as the gap between sections.\n */\nexport function PageLayout({\n  children,\n  title,\n  subtitle,\n  image,\n  backButton,\n  actions,\n  actionsVariant = 'icon-buttons',\n  menuActions,\n  selector,\n  headerVariant,\n  className,\n  contentClassName,\n  showHeader = true,\n  titleSize,\n  titleAdornment,\n  loading,\n  loadingActions,\n  subtitleRow,\n  titleWrap,\n}: PageLayoutProps) {\n  const hasActions = actions && actions.length > 0;\n  const needsBottomPadding = hasActions && actionsVariant === 'primary-buttons';\n  const hasHeader = showHeader && (title || subtitle || image || backButton || hasActions || selector || loading);\n\n  return (\n    <div className={cn('flex w-full flex-col', className)}>\n      {hasHeader && (\n        <TitleBlock\n          title={title}\n          subtitle={subtitle}\n          image={image}\n          backButton={backButton}\n          actions={actions}\n          actionsVariant={actionsVariant}\n          menuActions={menuActions}\n          selector={selector}\n          variant={headerVariant}\n          titleSize={titleSize}\n          titleAdornment={titleAdornment}\n          loading={loading}\n          loadingActions={loadingActions}\n          subtitleRow={subtitleRow}\n          titleWrap={titleWrap}\n        />\n      )}\n\n      <div\n        className={cn(\n          'flex flex-1 flex-col gap-[var(--spacing-system-l)]',\n          needsBottomPadding && 'pb-28 md:pb-0',\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nexport type { PageActionButton } from '../ui/page-actions';\nexport { TitleBlock, TITLE_BLOCK_MIN_HEIGHT } from './title-block';\nexport type { TitleBlockProps } from './title-block';\nexport default PageLayout;\n","import type React from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * Unified page layout components.\n *\n * PageShell: full-width <main> for list pages, dashboards, and other wide\n * layouts (max-w-[1920px]) — bg + min-height + centered, max-width content box.\n * ArticleDetailLayout: the same shell constrained to a readable article width\n * (max-w-[1280px]) — blog posts, release notes, case studies, etc.\n * Child components render content only; no layout wrappers needed.\n *\n * Outer padding is driven by CSS custom properties (see\n * `styles/ods-page-shell.css`), so an embedder matches its host grid by setting\n * the `--page-shell-*` vars on ANY ancestor — no prop-threading through the lib\n * wrappers (DevSectionPage / LegalDocumentPage / ReleaseDetailPage / …), no\n * imperative global, no context. The cascade scopes each override to its own\n * subtree. Per-instance escape hatch: `contentClassName` (Tailwind padding\n * utilities there win over the class defaults).\n */\n\ninterface LayoutProps {\n  children: React.ReactNode;\n  /** JSON-LD schema script elements (breadcrumbs, article schema, etc.) */\n  schemas?: React.ReactNode;\n  /** Per-instance class override on the content box (wins over the var defaults). */\n  contentClassName?: string;\n}\n\n/**\n * Full-width page shell for list pages, dashboards, and other wide layouts.\n * Max width: 1920px.\n *\n * No `min-h-screen` here on purpose: the root layout already wraps page\n * content in a `flex min-h-screen flex-col` → `flex-1` chain (app/layout.tsx),\n * so the footer is pushed to the bottom of the viewport on short pages without\n * this shell forcing its own 100vh. Adding `min-h-screen` here stacks a second\n * full viewport INSIDE that growing `flex-1`, which only manifests as dead\n * space when the shell is a hero section above sibling content\n * (roadmap-and-releases). There is no case where the shell itself should fill\n * the viewport.\n */\nexport function PageShell({ children, schemas, contentClassName }: LayoutProps) {\n  return (\n    <main className=\"bg-ods-bg\">\n      {schemas}\n      <div className={cn('page-shell-content mx-auto max-w-[1920px]', contentClassName)}>{children}</div>\n    </main>\n  );\n}\n\n/**\n * Constrained layout for article/detail pages (max-w-[1280px]) — readable\n * content width for blog posts, release notes, case studies, investor updates.\n * Fixed hub padding (`px-6 md:px-20 py-6 md:py-10`); pass `contentClassName` to\n * override per instance. (PageShell's `--page-shell-*` var system does NOT apply\n * here — this layout keeps its own fixed spacing.)\n */\nexport function ArticleDetailLayout({ children, schemas, contentClassName }: LayoutProps) {\n  return (\n    <main className=\"bg-ods-bg\">\n      {schemas}\n      <div className={cn('mx-auto max-w-[1280px] px-6 py-6 md:px-20 md:py-10', contentClassName)}>{children}</div>\n    </main>\n  );\n}\n","'use client';\n\nimport * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';\nimport type { VariantProps } from 'class-variance-authority';\nimport {\n  type ComponentPropsWithoutRef,\n  type ElementRef,\n  type ReactNode,\n  createContext,\n  forwardRef,\n  useContext,\n} from 'react';\n\nimport { cn } from '../utils/cn';\nimport { toggleVariants } from './ui/toggle';\n\nconst ToggleGroupContext = createContext<VariantProps<typeof toggleVariants>>({\n  size: 'default',\n  variant: 'default',\n});\n\nconst ToggleGroup = forwardRef<\n  ElementRef<typeof ToggleGroupPrimitive.Root>,\n  VariantProps<typeof toggleVariants> & {\n    className?: string;\n    children?: ReactNode;\n  } & ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>\n>(({ className, variant, size, children, ...props }, ref) => (\n  <ToggleGroupPrimitive.Root ref={ref} className={cn('flex items-center justify-center gap-1', className)} {...props}>\n    <ToggleGroupContext.Provider value={{ variant, size }}>{children}</ToggleGroupContext.Provider>\n  </ToggleGroupPrimitive.Root>\n));\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;\n\nconst ToggleGroupItem = forwardRef<\n  ElementRef<typeof ToggleGroupPrimitive.Item>,\n  ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, value, ...props }, ref) => {\n  const context = useContext(ToggleGroupContext);\n\n  return (\n    <ToggleGroupPrimitive.Item\n      ref={ref}\n      value={value}\n      className={cn(\n        toggleVariants({\n          variant: context.variant || variant,\n          size: context.size || size,\n        }),\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </ToggleGroupPrimitive.Item>\n  );\n});\n\nToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;\n\nexport { ToggleGroup, ToggleGroupItem };\n","'use client';\n\nimport * as TogglePrimitive from '@radix-ui/react-toggle';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\n\nconst toggleVariants = cva(\n  'inline-flex items-center justify-center rounded-md transition-colors text-h6 hover:bg-ods-bg-hover hover:text-ods-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-ods-accent data-[state=on]:text-ods-text-on-accent',\n  {\n    variants: {\n      variant: {\n        default: 'bg-transparent',\n        outline: 'border border-ods-border bg-transparent hover:bg-ods-accent hover:text-ods-text-on-accent',\n      },\n      size: {\n        default: 'h-10 px-3',\n        sm: 'h-9 px-2.5',\n        lg: 'h-11 px-5',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\nconst Toggle = forwardRef<\n  ElementRef<typeof TogglePrimitive.Root>,\n  ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root ref={ref} className={cn(toggleVariants({ variant, size, className }))} {...props} />\n));\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils';\n\ninterface BenefitCardProps {\n  icon?: React.ReactNode;\n  title: string;\n  description: string;\n  variant?: 'default' | 'dark' | 'auth-figma';\n  className?: string;\n}\n\nexport const BenefitCard: React.FC<BenefitCardProps> = ({\n  icon,\n  title,\n  description,\n  variant = 'default',\n  className = '',\n}) => {\n  // Direction and cross-axis alignment only — every other per-variant value\n  // (including `gap`, which used to hide in a ternary here) lives in\n  // `variantStyles` so one variant is one place.\n  const baseStyles = 'flex items-start justify-start relative';\n\n  const variantStyles = {\n    default: {\n      container:\n        'gap-[var(--spacing-system-xsf)] bg-ods-bg p-[var(--spacing-system-xsf)] md:p-[var(--spacing-system-mf)]',\n      title: 'font-body font-bold text-body-md md:text-body-lg text-ods-text-primary',\n      description: 'font-body font-medium text-body-sm md:text-body-md text-ods-text-secondary',\n    },\n    dark: {\n      container: 'gap-[var(--spacing-system-xsf)] bg-transparent p-0 shadow-[0px_48px_80px_0px_rgba(0,0,0,0.24)]',\n      title: 'font-mono font-semibold text-heading-4 leading-[40px] text-ods-text-primary tracking-[-0.64px]',\n      description: 'font-body font-medium text-body-lg leading-6 text-ods-text-tertiary',\n    },\n    'auth-figma': {\n      container: 'gap-[var(--spacing-system-mf)] bg-transparent p-[var(--spacing-system-lf)]',\n      title: 'text-h3 text-ods-text-primary tracking-[-0.36px]',\n      description: 'text-h4 text-ods-text-secondary',\n    },\n  };\n\n  const styles = variantStyles[variant];\n\n  return (\n    <div className={cn(baseStyles, styles.container, className)}>\n      {icon && <div>{icon}</div>}\n      <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n        <h3 className={cn(styles.title)}>{title}</h3>\n        <p className={cn(styles.description)}>{description}</p>\n      </div>\n    </div>\n  );\n};\n\ninterface BenefitCardGridProps {\n  children: React.ReactNode;\n  className?: string;\n  columns?: 2 | 3 | 4; // Support 2, 3, or 4 columns\n}\n\n/**\n * Which dividers card `index` of `totalItems` draws in a `columns` grid. Pure:\n * the per-breakpoint row math (a 3-column grid lays out 1-up / 2-up / 3-up, so a\n * card's row differs per breakpoint) is what needs testing, and the grid itself\n * is a `cloneElement` over children.\n */\nexport function benefitCardBorderClass(\n  index: number,\n  totalItems: number,\n  columns: NonNullable<BenefitCardGridProps['columns']>,\n): string {\n  const isLastItem = index === totalItems - 1;\n  if (columns === 4) {\n    // 4-column grid borders\n    const isLastInRow = (index + 1) % 4 === 0;\n    const isInLastRow = index >= totalItems - 4;\n    return cn(\n      !isLastInRow && 'border-ods-border md:border-r',\n      !isInLastRow && 'border-b md:border-b lg:border-b-0',\n      index < 2 && 'lg:border-b-0',\n    );\n  } else if (columns === 3) {\n    // 3 columns lay out 1-up / 2-up / 3-up, so the row a card sits in\n    // differs per breakpoint and a single rule cannot describe it. The\n    // old shared 2-3 branch assumed one row and, at `md` (grid-cols-2),\n    // gave card 1 a trailing `md:border-r` while suppressing the divider\n    // between the two rows.\n    const isLastInMdRow = (index + 1) % 2 === 0;\n    const isInLastMdRow = index >= totalItems - (totalItems % 2 === 0 ? 2 : 1);\n    const isLastInLgRow = (index + 1) % 3 === 0;\n    const isInLastLgRow = index >= totalItems - (totalItems % 3 === 0 ? 3 : totalItems % 3);\n    return cn(\n      'border-ods-border',\n      !isLastItem && 'border-b',\n      isInLastMdRow && 'md:border-b-0',\n      !isLastInMdRow && !isLastItem && 'md:border-r',\n      isLastInMdRow && 'md:border-r-0',\n      isInLastLgRow && 'lg:border-b-0',\n      !isInLastMdRow && !isInLastLgRow && 'lg:border-b',\n      !isLastInLgRow && !isLastItem && 'lg:border-r',\n      isLastInLgRow && 'lg:border-r-0',\n    );\n  } else {\n    // 2 columns: a flex row, so one rule genuinely does describe it.\n    return isLastItem ? 'border-b-0' : 'border-b md:border-b-0 md:border-r border-ods-border';\n  }\n}\n\nexport const BenefitCardGrid: React.FC<BenefitCardGridProps> = ({ children, className = '', columns = 2 }) => {\n  const childrenArray = React.Children.toArray(children);\n\n  const gridClass =\n    columns === 4\n      ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4'\n      : columns === 3\n        ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3'\n        : 'flex flex-col md:flex-row';\n\n  return (\n    <div\n      className={cn(\n        'overflow-hidden rounded-lg border border-ods-border bg-ods-card shadow-ods-card',\n        gridClass,\n        className,\n      )}\n    >\n      {childrenArray.map((child, index) => {\n        const borderClass = benefitCardBorderClass(index, childrenArray.length, columns);\n\n        return React.cloneElement(child as React.ReactElement<{ className?: string }>, {\n          key: index,\n          className: borderClass,\n        });\n      })}\n    </div>\n  );\n};\n","/**\n * Single source of truth for the `seo_title` length budget.\n *\n * The `seo_title` is rendered as the page `<title>` VERBATIM — no brand suffix is\n * appended to it (the `| <Brand>` suffix is kept only on the default/actual-title\n * fallback). So this is the full rendered-title budget; search engines truncate\n * page titles around 60 chars.\n *\n * This number lives in exactly ONE place. Consumers:\n *   - `SEOEditorPreview` — input `maxLength` + the too-long alert.\n *   - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)\n *     for the AI generation prompt guidance and the DB CHECK value.\n */\nexport const SEO_TITLE_MAX_LENGTH = 60;\n","/**\n * Single source of truth for the `seo_description` length budget.\n *\n * Unlike `seo_title` (soft search-engine guidance), this is a HARD cap: the\n * `seo_description` DB columns are `varchar(160)` and Postgres rejects longer\n * values outright (\"value too long for type character varying(160)\").\n *\n * This number lives in exactly ONE place. Consumers:\n *   - `SEOEditorPreview` — input `maxLength` + the too-long alert.\n *   - the hub — imports it (via `@flamingo-stack/openframe-frontend-core/utils`)\n *     for the AI generation prompt guidance and the rewrite-to-fit helper.\n */\nexport const SEO_DESCRIPTION_MAX_LENGTH = 160;\n","/** Monochrome brand silhouette SVGs (for CSS mask-image tinting), centralized so\n *  consumers don't ship their own copies. Keyed by lowercase brand name. */\nexport const BRAND_MARK_SVGS: Record<string, string> = {\n  amazonwebservices: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Amazon Web Services</title><path d=\"M6.763 10.036c0 .296.032.535.088.71.064.176.144.368.256.576.04.063.056.127.056.183 0 .08-.048.16-.152.24l-.503.335a.383.383 0 0 1-.208.072c-.08 0-.16-.04-.239-.112a2.47 2.47 0 0 1-.287-.375 6.18 6.18 0 0 1-.248-.471c-.622.734-1.405 1.101-2.347 1.101-.67 0-1.205-.191-1.596-.574-.391-.384-.59-.894-.59-1.533 0-.678.239-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583c0-.607-.127-1.03-.375-1.277-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103-.295.072-.583.16-.862.272a2.287 2.287 0 0 1-.28.104.488.488 0 0 1-.127.023c-.112 0-.168-.08-.168-.247v-.391c0-.128.016-.224.056-.28a.597.597 0 0 1 .224-.167c.279-.144.614-.264 1.005-.36a4.84 4.84 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647.439.43.662 1.085.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144.287-.096.543-.271.758-.51.128-.152.224-.32.272-.512.047-.191.08-.423.08-.694v-.335a6.66 6.66 0 0 0-.735-.136 6.02 6.02 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296zm6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 5.55a1.398 1.398 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783c.151 0 .255.025.31.08.065.048.113.16.16.312l1.342 5.284 1.245-5.284c.04-.16.088-.264.151-.312a.549.549 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348c.048-.16.104-.264.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1.137 1.137 0 0 1-.056.2l-1.923 6.17c-.048.16-.104.263-.168.311a.51.51 0 0 1-.303.08h-.687c-.151 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32l-1.238-5.148-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.563.563 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247.048 0 .096.008.144.024.048.016.12.048.2.08.271.12.566.215.878.279.319.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.777.777 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.902 1.902 0 0 1-.4-1.158c0-.335.073-.63.216-.886.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .359.008.535.032.183.024.35.056.518.088.16.04.312.08.455.127.144.048.256.096.336.144a.69.69 0 0 1 .24.2.43.43 0 0 1 .071.263v.375c0 .168-.064.256-.184.256a.83.83 0 0 1-.303-.096 3.652 3.652 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223-.248.152-.375.383-.375.71 0 .224.08.416.24.567.159.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767.247.327.367.702.367 1.117 0 .343-.072.655-.207.926-.144.272-.336.511-.583.703-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167zM21.698 16.207c-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351 3.384 1.963 7.559 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.439-.2.814.287.383.607zM22.792 14.961c-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151.32-.79 1.03-2.57.695-2.994z\"/></svg>`,\n  datadog: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Datadog</title><path d=\"M19.57 17.04l-1.997-1.316-1.665 2.782-1.937-.567-1.706 2.604.087.82 9.274-1.71-.538-5.794zm-8.649-2.498l1.488-.204c.241.108.409.15.697.223.45.117.97.23 1.741-.16.18-.088.553-.43.704-.625l6.096-1.106.622 7.527-10.444 1.882zm11.325-2.712l-.602.115L20.488 0 .789 2.285l2.427 19.693 2.306-.334c-.184-.263-.471-.581-.96-.989-.68-.564-.44-1.522-.039-2.127.53-1.022 3.26-2.322 3.106-3.956-.056-.594-.15-1.368-.702-1.898-.02.22.017.432.017.432s-.227-.289-.34-.683c-.112-.15-.2-.199-.319-.4-.085.233-.073.503-.073.503s-.186-.437-.216-.807c-.11.166-.137.48-.137.48s-.241-.69-.186-1.062c-.11-.323-.436-.965-.343-2.424.6.421 1.924.321 2.44-.439.171-.251.288-.939-.086-2.293-.24-.868-.835-2.16-1.066-2.651l-.028.02c.122.395.374 1.223.47 1.625.293 1.218.372 1.642.234 2.204-.116.488-.397.808-1.107 1.165-.71.358-1.653-.514-1.713-.562-.69-.55-1.224-1.447-1.284-1.883-.062-.477.275-.763.445-1.153-.243.07-.514.192-.514.192s.323-.334.722-.624c.165-.109.262-.178.436-.323a9.762 9.762 0 0 0-.456.003s.42-.227.855-.392c-.318-.014-.623-.003-.623-.003s.937-.419 1.678-.727c.509-.208 1.006-.147 1.286.257.367.53.752.817 1.569.996.501-.223.653-.337 1.284-.509.554-.61.99-.688.99-.688s-.216.198-.274.51c.314-.249.66-.455.66-.455s-.134.164-.259.426l.03.043c.366-.22.797-.394.797-.394s-.123.156-.268.358c.277-.002.838.012 1.056.037 1.285.028 1.552-1.374 2.045-1.55.618-.22.894-.353 1.947.68.903.888 1.609 2.477 1.259 2.833-.294.295-.874-.115-1.516-.916a3.466 3.466 0 0 1-.716-1.562 1.533 1.533 0 0 0-.497-.85s.23.51.23.96c0 .246.03 1.165.424 1.68-.039.076-.057.374-.1.43-.458-.554-1.443-.95-1.604-1.067.544.445 1.793 1.468 2.273 2.449.453.927.186 1.777.416 1.997.065.063.976 1.197 1.15 1.767.306.994.019 2.038-.381 2.685l-1.117.174c-.163-.045-.273-.068-.42-.153.08-.143.241-.5.243-.572l-.063-.111c-.348.492-.93.97-1.414 1.245-.633.359-1.363.304-1.838.156-1.348-.415-2.623-1.327-2.93-1.566 0 0-.01.191.048.234.34.383 1.119 1.077 1.872 1.56l-1.605.177.759 5.908c-.337.048-.39.071-.757.124-.325-1.147-.946-1.895-1.624-2.332-.599-.384-1.424-.47-2.214-.314l-.05.059a2.851 2.851 0 0 1 1.863.444c.654.413 1.181 1.481 1.375 2.124.248.822.42 1.7-.248 2.632-.476.662-1.864 1.028-2.986.237.3.481.705.876 1.25.95.809.11 1.577-.03 2.106-.574.452-.464.69-1.434.628-2.456l.714-.104.258 1.834 11.827-1.424zM15.05 6.848c-.034.075-.085.125-.007.37l.004.014.013.032.032.073c.14.287.295.558.552.696.067-.011.136-.019.207-.023.242-.01.395.028.492.08.009-.048.01-.119.005-.222-.018-.364.072-.982-.626-1.308-.264-.122-.634-.084-.757.068a.302.302 0 0 1 .058.013c.186.066.06.13.027.207m1.958 3.392c-.092-.05-.52-.03-.821.005-.574.068-1.193.267-1.328.372-.247.191-.135.523.047.66.511.382.96.638 1.432.575.29-.038.546-.497.728-.914.124-.288.124-.598-.058-.698m-5.077-2.942c.162-.154-.805-.355-1.556.156-.554.378-.571 1.187-.041 1.646.053.046.096.078.137.104a4.77 4.77 0 0 1 1.396-.412c.113-.125.243-.345.21-.745-.044-.542-.455-.456-.146-.749\"/></svg>`,\n  git: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Git</title><path d=\"M13.09 23.549a1.54 1.54 0 0 1-2.18 0L.451 13.089a1.54 1.54 0 0 1 0-2.179l7.191-7.19 2.733 2.733a1.85 1.85 0 0 0 .964 2.326v6.66a1.849 1.849 0 1 0 1.54 0V8.957l2.508 2.508a1.85 1.85 0 1 0 1.09-1.09l-2.634-2.634a1.85 1.85 0 0 0-2.378-2.377L8.73 2.63 10.91.451a1.54 1.54 0 0 1 2.179 0l10.459 10.46a1.54 1.54 0 0 1 0 2.179z\"/></svg>`,\n  github: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>GitHub</title><path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"/></svg>`,\n  gnubash: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>GNU Bash</title><path d=\"M21.038,4.9l-7.577-4.498C13.009,0.134,12.505,0,12,0c-0.505,0-1.009,0.134-1.462,0.403L2.961,4.9 C2.057,5.437,1.5,6.429,1.5,7.503v8.995c0,1.073,0.557,2.066,1.462,2.603l7.577,4.497C10.991,23.866,11.495,24,12,24 c0.505,0,1.009-0.134,1.461-0.402l7.577-4.497c0.904-0.537,1.462-1.529,1.462-2.603V7.503C22.5,6.429,21.943,5.437,21.038,4.9z M15.17,18.946l0.013,0.646c0.001,0.078-0.05,0.167-0.111,0.198l-0.383,0.22c-0.061,0.031-0.111-0.007-0.112-0.085L14.57,19.29 c-0.328,0.136-0.66,0.169-0.872,0.084c-0.04-0.016-0.057-0.075-0.041-0.142l0.139-0.584c0.011-0.046,0.036-0.092,0.069-0.121 c0.012-0.011,0.024-0.02,0.036-0.026c0.022-0.011,0.043-0.014,0.062-0.006c0.229,0.077,0.521,0.041,0.802-0.101 c0.357-0.181,0.596-0.545,0.592-0.907c-0.003-0.328-0.181-0.465-0.613-0.468c-0.55,0.001-1.064-0.107-1.072-0.917 c-0.007-0.667,0.34-1.361,0.889-1.8l-0.007-0.652c-0.001-0.08,0.048-0.168,0.111-0.2l0.37-0.236 c0.061-0.031,0.111,0.007,0.112,0.087l0.006,0.653c0.273-0.109,0.511-0.138,0.726-0.088c0.047,0.012,0.067,0.076,0.048,0.151 l-0.144,0.578c-0.011,0.044-0.036,0.088-0.065,0.116c-0.012,0.012-0.025,0.021-0.038,0.028c-0.019,0.01-0.038,0.013-0.057,0.009 c-0.098-0.022-0.332-0.073-0.699,0.113c-0.385,0.195-0.52,0.53-0.517,0.778c0.003,0.297,0.155,0.387,0.681,0.396 c0.7,0.012,1.003,0.318,1.01,1.023C16.105,17.747,15.736,18.491,15.17,18.946z M19.143,17.859c0,0.06-0.008,0.116-0.058,0.145 l-1.916,1.164c-0.05,0.029-0.09,0.004-0.09-0.056v-0.494c0-0.06,0.037-0.093,0.087-0.122l1.887-1.129 c0.05-0.029,0.09-0.004,0.09,0.056V17.859z M20.459,6.797l-7.168,4.427c-0.894,0.523-1.553,1.109-1.553,2.187v8.833 c0,0.645,0.26,1.063,0.66,1.184c-0.131,0.023-0.264,0.039-0.398,0.039c-0.42,0-0.833-0.114-1.197-0.33L3.226,18.64 c-0.741-0.44-1.201-1.261-1.201-2.142V7.503c0-0.881,0.46-1.702,1.201-2.142l7.577-4.498c0.363-0.216,0.777-0.33,1.197-0.33 c0.419,0,0.833,0.114,1.197,0.33l7.577,4.498c0.624,0.371,1.046,1.013,1.164,1.732C21.686,6.557,21.12,6.411,20.459,6.797z\"/></svg>`,\n  googlecloud: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Google Cloud</title><path d=\"M12.19 2.38a9.344 9.344 0 0 0-9.234 6.893c.053-.02-.055.013 0 0-3.875 2.551-3.922 8.11-.247 10.941l.006-.007-.007.03a6.717 6.717 0 0 0 4.077 1.356h5.173l.03.03h5.192c6.687.053 9.376-8.605 3.835-12.35a9.365 9.365 0 0 0-2.821-4.552l-.043.043.006-.05A9.344 9.344 0 0 0 12.19 2.38zm-.358 4.146c1.244-.04 2.518.368 3.486 1.15a5.186 5.186 0 0 1 1.862 4.078v.518c3.53-.07 3.53 5.262 0 5.193h-5.193l-.008.009v-.04H6.785a2.59 2.59 0 0 1-1.067-.23h.001a2.597 2.597 0 1 1 3.437-3.437l3.013-3.012A6.747 6.747 0 0 0 8.11 8.24c.018-.01.04-.026.054-.023a5.186 5.186 0 0 1 3.67-1.69z\"/></svg>`,\n  grafana: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Grafana</title><path d=\"M23.02 10.59a8.578 8.578 0 0 0-.862-3.034 8.911 8.911 0 0 0-1.789-2.445c.337-1.342-.413-2.505-.413-2.505-1.292-.08-2.113.4-2.416.62-.052-.02-.102-.044-.154-.064-.22-.089-.446-.172-.677-.247-.231-.073-.47-.14-.711-.197a9.867 9.867 0 0 0-.875-.161C14.557.753 12.94 0 12.94 0c-1.804 1.145-2.147 2.744-2.147 2.744l-.018.093c-.098.029-.2.057-.298.088-.138.042-.275.094-.413.143-.138.055-.275.107-.41.166a8.869 8.869 0 0 0-1.557.87l-.063-.029c-2.497-.955-4.716.195-4.716.195-.203 2.658.996 4.33 1.235 4.636a11.608 11.608 0 0 0-.607 2.635C1.636 12.677.953 15.014.953 15.014c1.926 2.214 4.171 2.351 4.171 2.351.003-.002.006-.002.006-.005.285.509.615.994.986 1.446.156.19.32.371.488.548-.704 2.009.099 3.68.099 3.68 2.144.08 3.553-.937 3.849-1.173a9.784 9.784 0 0 0 3.164.501h.08l.055-.003.107-.002.103-.005.003.002c1.01 1.44 2.788 1.646 2.788 1.646 1.264-1.332 1.337-2.653 1.337-2.94v-.058c0-.02-.003-.039-.003-.06.265-.187.52-.387.758-.6a7.875 7.875 0 0 0 1.415-1.7c1.43.083 2.437-.885 2.437-.885-.236-1.49-1.085-2.216-1.264-2.354l-.018-.013-.016-.013a.217.217 0 0 1-.031-.02c.008-.092.016-.18.02-.27.011-.162.016-.323.016-.48v-.253l-.005-.098-.008-.135a1.891 1.891 0 0 0-.01-.13c-.003-.042-.008-.083-.013-.125l-.016-.124-.018-.122a6.215 6.215 0 0 0-2.032-3.73 6.015 6.015 0 0 0-3.222-1.46 6.292 6.292 0 0 0-.85-.048l-.107.002h-.063l-.044.003-.104.008a4.777 4.777 0 0 0-3.335 1.695c-.332.4-.592.84-.768 1.297a4.594 4.594 0 0 0-.312 1.817l.003.091c.005.055.007.11.013.164a3.615 3.615 0 0 0 .698 1.82 3.53 3.53 0 0 0 1.827 1.282c.33.098.66.14.971.137.039 0 .078 0 .114-.002l.063-.003c.02 0 .041-.003.062-.003.034-.002.065-.007.099-.01.007 0 .018-.003.028-.003l.031-.005.06-.008a1.18 1.18 0 0 0 .112-.02c.036-.008.072-.013.109-.024a2.634 2.634 0 0 0 .914-.415c.028-.02.056-.041.085-.065a.248.248 0 0 0 .039-.35.244.244 0 0 0-.309-.06l-.078.042c-.09.044-.184.083-.283.116a2.476 2.476 0 0 1-.475.096c-.028.003-.054.006-.083.006l-.083.002c-.026 0-.054 0-.08-.002l-.102-.006h-.012l-.024.006c-.016-.003-.031-.003-.044-.006-.031-.002-.06-.007-.091-.01a2.59 2.59 0 0 1-.724-.213 2.557 2.557 0 0 1-.667-.438 2.52 2.52 0 0 1-.805-1.475 2.306 2.306 0 0 1-.029-.444l.006-.122v-.023l.002-.031c.003-.021.003-.04.005-.06a3.163 3.163 0 0 1 1.352-2.29 3.12 3.12 0 0 1 .937-.43 2.946 2.946 0 0 1 .776-.101h.06l.07.002.045.003h.026l.07.005a4.041 4.041 0 0 1 1.635.49 3.94 3.94 0 0 1 1.602 1.662 3.77 3.77 0 0 1 .397 1.414l.005.076.003.075c.002.026.002.05.002.075 0 .024.003.052 0 .07v.065l-.002.073-.008.174a6.195 6.195 0 0 1-.08.639 5.1 5.1 0 0 1-.267.927 5.31 5.31 0 0 1-.624 1.13 5.052 5.052 0 0 1-3.237 2.014 4.82 4.82 0 0 1-.649.066l-.039.003h-.287a6.607 6.607 0 0 1-1.716-.265 6.776 6.776 0 0 1-3.4-2.274 6.75 6.75 0 0 1-.746-1.15 6.616 6.616 0 0 1-.714-2.596l-.005-.083-.002-.02v-.056l-.003-.073v-.096l-.003-.104v-.07l.003-.163c.008-.22.026-.45.054-.678a8.707 8.707 0 0 1 .28-1.355c.128-.444.286-.872.473-1.277a7.04 7.04 0 0 1 1.456-2.1 5.925 5.925 0 0 1 .953-.763c.169-.111.343-.213.524-.306.089-.05.182-.091.273-.135.047-.02.093-.042.138-.062a7.177 7.177 0 0 1 .714-.267l.145-.045c.049-.015.098-.026.148-.041.098-.029.197-.052.296-.076.049-.013.1-.02.15-.033l.15-.032.151-.028.076-.013.075-.01.153-.024c.057-.01.114-.013.171-.023l.169-.021c.036-.003.073-.008.106-.01l.073-.008.036-.003.042-.002c.057-.003.114-.008.171-.01l.086-.006h.023l.037-.003.145-.007a7.999 7.999 0 0 1 1.708.125 7.917 7.917 0 0 1 2.048.68 8.253 8.253 0 0 1 1.672 1.09l.09.077.089.078c.06.052.114.107.171.159.057.052.112.106.166.16.052.055.107.107.159.164a8.671 8.671 0 0 1 1.41 1.978c.012.026.028.052.04.078l.04.078.075.156c.023.051.05.1.07.153l.065.15a8.848 8.848 0 0 1 .45 1.34.19.19 0 0 0 .201.142.186.186 0 0 0 .172-.184c.01-.246.002-.532-.024-.856z\"/></svg>`,\n  ibm: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>IBM</title><path d=\"M23.544 15.993c.038 0 .06-.017.06-.053v-.036c0-.035-.022-.052-.06-.052h-.09v.14zm-.09.262h-.121v-.498h.225c.112 0 .169.066.169.157 0 .079-.036.129-.09.15l.111.19h-.133l-.092-.17h-.07zm.434-.222v-.062c0-.2-.157-.357-.363-.357a.355.355 0 00-.363.357v.062c0 .2.156.358.363.358a.355.355 0 00.363-.358zm-.838-.03c0-.28.212-.492.475-.492.264 0 .475.213.475.491 0 .279-.211.491-.475.491a.477.477 0 01-.475-.49zM16.21 8.13l-.216-.624h-3.56v.624zm.413 1.19l-.216-.623h-3.973v.624zm2.65 7.147h3.107v-.624h-3.108zm0-1.192h3.107v-.623h-3.108zm0-1.19h1.864v-.624h-1.865zm0-1.191h1.864v-.624h-1.865zm0-1.191h1.864v-.624h-3.555l-.175.504-.175-.504h-3.555v.624h1.865v-.574l.2.574h3.33l.2-.574zm1.864-1.815h-3.142l-.217.624h3.359zm-7.46 3.006h1.865v-.624h-1.865zm0 1.19h1.865v-.623h-1.865zm-1.243 1.191h3.108v-.623h-3.108zm0 1.192h3.108v-.624h-3.108zm6.386-8.961l-.216.624h3.776v-.624zm-.629 1.815h4.19v-.624h-3.974zm-4.514 1.19h3.359l-.216-.623h-3.143zm2.482 2.383h2.496l.218-.624h-2.932zm.417 1.19h1.662l.218-.623h-2.098zm.416 1.191h.83l.218-.623h-1.266zm.414 1.192l.217-.624h-.432zm-12.433-.006l4.578.006c.622 0 1.18-.237 1.602-.624h-6.18zm4.86-3v.624h2.092c0-.216-.03-.425-.083-.624zm-3.616.624h1.865v-.624H6.217zm3.617-3.573h2.008c.053-.199.083-.408.083-.624H9.834zm-3.617 0h1.865v-.624H6.217zM9.55 7.507H4.973v.624h6.18a2.36 2.36 0 00-1.602-.624zm2.056 1.191H4.973v.624h6.884a2.382 2.382 0 00-.25-.624zm-5.39 2.382v.624h4.87c.207-.176.382-.387.519-.624zm4.87 1.191h-4.87v.624h5.389a2.39 2.39 0 00-.519-.624zm-6.114 3.006h6.634c.11-.193.196-.402.25-.624H4.973zM0 8.13h4.352v-.624H0zm0 1.191h4.352v-.624H0zm1.243 1.191h1.865v-.624H1.243zm0 1.191h1.865v-.624H1.243zm0 1.19h1.865v-.623H1.243zm0 1.192h1.865v-.624H1.243zM0 15.276h4.352v-.623H0zm0 1.192h4.352v-.624H0Z\"/></svg>`,\n  kubernetes: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Kubernetes</title><path d=\"M10.204 14.35l.007.01-.999 2.413a5.171 5.171 0 0 1-2.075-2.597l2.578-.437.004.005a.44.44 0 0 1 .484.606zm-.833-2.129a.44.44 0 0 0 .173-.756l.002-.011L7.585 9.7a5.143 5.143 0 0 0-.73 3.255l2.514-.725.002-.009zm1.145-1.98a.44.44 0 0 0 .699-.337l.01-.005.15-2.62a5.144 5.144 0 0 0-3.01 1.442l2.147 1.523.004-.002zm.76 2.75l.723.349.722-.347.18-.78-.5-.623h-.804l-.5.623.179.779zm1.5-3.095a.44.44 0 0 0 .7.336l.008.003 2.134-1.513a5.188 5.188 0 0 0-2.992-1.442l.148 2.615.002.001zm10.876 5.97l-5.773 7.181a1.6 1.6 0 0 1-1.248.594l-9.261.003a1.6 1.6 0 0 1-1.247-.596l-5.776-7.18a1.583 1.583 0 0 1-.307-1.34L2.1 5.573c.108-.47.425-.864.863-1.073L11.305.513a1.606 1.606 0 0 1 1.385 0l8.345 3.985c.438.209.755.604.863 1.073l2.062 8.955c.108.47-.005.963-.308 1.34zm-3.289-2.057c-.042-.01-.103-.026-.145-.034-.174-.033-.315-.025-.479-.038-.35-.037-.638-.067-.895-.148-.105-.04-.18-.165-.216-.216l-.201-.059a6.45 6.45 0 0 0-.105-2.332 6.465 6.465 0 0 0-.936-2.163c.052-.047.15-.133.177-.159.008-.09.001-.183.094-.282.197-.185.444-.338.743-.522.142-.084.273-.137.415-.242.032-.024.076-.062.11-.089.24-.191.295-.52.123-.736-.172-.216-.506-.236-.745-.045-.034.027-.08.062-.111.088-.134.116-.217.23-.33.35-.246.25-.45.458-.673.609-.097.056-.239.037-.303.033l-.19.135a6.545 6.545 0 0 0-4.146-2.003l-.012-.223c-.065-.062-.143-.115-.163-.25-.022-.268.015-.557.057-.905.023-.163.061-.298.068-.475.001-.04-.001-.099-.001-.142 0-.306-.224-.555-.5-.555-.275 0-.499.249-.499.555l.001.014c0 .041-.002.092 0 .128.006.177.044.312.067.475.042.348.078.637.056.906a.545.545 0 0 1-.162.258l-.012.211a6.424 6.424 0 0 0-4.166 2.003 8.373 8.373 0 0 1-.18-.128c-.09.012-.18.04-.297-.029-.223-.15-.427-.358-.673-.608-.113-.12-.195-.234-.329-.349-.03-.026-.077-.062-.111-.088a.594.594 0 0 0-.348-.132.481.481 0 0 0-.398.176c-.172.216-.117.546.123.737l.007.005.104.083c.142.105.272.159.414.242.299.185.546.338.743.522.076.082.09.226.1.288l.16.143a6.462 6.462 0 0 0-1.02 4.506l-.208.06c-.055.072-.133.184-.215.217-.257.081-.546.11-.895.147-.164.014-.305.006-.48.039-.037.007-.09.02-.133.03l-.004.002-.007.002c-.295.071-.484.342-.423.608.061.267.349.429.645.365l.007-.001.01-.003.129-.029c.17-.046.294-.113.448-.172.33-.118.604-.217.87-.256.112-.009.23.069.288.101l.217-.037a6.5 6.5 0 0 0 2.88 3.596l-.09.218c.033.084.069.199.044.282-.097.252-.263.517-.452.813-.091.136-.185.242-.268.399-.02.037-.045.095-.064.134-.128.275-.034.591.213.71.248.12.556-.007.69-.282v-.002c.02-.039.046-.09.062-.127.07-.162.094-.301.144-.458.132-.332.205-.68.387-.897.05-.06.13-.082.215-.105l.113-.205a6.453 6.453 0 0 0 4.609.012l.106.192c.086.028.18.042.256.155.136.232.229.507.342.84.05.156.074.295.145.457.016.037.043.09.062.129.133.276.442.402.69.282.247-.118.341-.435.213-.71-.02-.039-.045-.096-.065-.134-.083-.156-.177-.261-.268-.398-.19-.296-.346-.541-.443-.793-.04-.13.007-.21.038-.294-.018-.022-.059-.144-.083-.202a6.499 6.499 0 0 0 2.88-3.622c.064.01.176.03.213.038.075-.05.144-.114.28-.104.266.039.54.138.87.256.154.06.277.128.448.173.036.01.088.019.13.028l.009.003.007.001c.297.064.584-.098.645-.365.06-.266-.128-.537-.423-.608zM16.4 9.701l-1.95 1.746v.005a.44.44 0 0 0 .173.757l.003.01 2.526.728a5.199 5.199 0 0 0-.108-1.674A5.208 5.208 0 0 0 16.4 9.7zm-4.013 5.325a.437.437 0 0 0-.404-.232.44.44 0 0 0-.372.233h-.002l-1.268 2.292a5.164 5.164 0 0 0 3.326.003l-1.27-2.296h-.01zm1.888-1.293a.44.44 0 0 0-.27.036.44.44 0 0 0-.214.572l-.003.004 1.01 2.438a5.15 5.15 0 0 0 2.081-2.615l-2.6-.44-.004.005z\"/></svg>`,\n  linux: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Linux</title><path d=\"M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z\"/></svg>`,\n  microsoft: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Microsoft</title><path d=\"M0 0v11.408h11.408V0zm12.594 0v11.408H24V0zM0 12.594V24h11.408V12.594zm12.594 0V24H24V12.594z\"/></svg>`,\n  openstack: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>OpenStack</title><path d=\"M18.575 9.29h5.418v5.42h-5.418zM0 9.29h5.419v5.42H0zm18.575 7.827a1.207 1.207 0 0 1-1.206 1.206H6.623a1.207 1.207 0 0 1-1.205-1.206v-.858H0v5.252a2.236 2.236 0 0 0 2.229 2.23h19.53A2.237 2.237 0 0 0 24 21.512V16.26h-5.425zM21.763.258H2.233a2.236 2.236 0 0 0-2.23 2.23V7.74h5.419v-.858a1.206 1.206 0 0 1 1.205-1.206h10.746a1.206 1.206 0 0 1 1.205 1.206v.858H24V2.487A2.237 2.237 0 0 0 21.763.258Z\"/></svg>`,\n  oracle: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Oracle</title><path d=\"M16.412 4.412h-8.82a7.588 7.588 0 0 0-.008 15.176h8.828a7.588 7.588 0 0 0 0-15.176zm-.193 12.502H7.786a4.915 4.915 0 0 1 0-9.828h8.433a4.914 4.914 0 1 1 0 9.828z\"/></svg>`,\n  perforce: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Perforce</title><path d=\"m7.386 14.957 2.279-1.316-.576-.333A1.48 1.48 0 0 1 8.334 12c0-.262.073-.915.755-1.308l10.31-5.953a1.49 1.49 0 0 1 1.51 0c.228.13.755.52.755 1.308v11.906c0 .788-.527 1.178-.754 1.308s-.828.393-1.51 0l-2.732-1.577-2.334 1.348 3.899 2.251a3.81 3.81 0 0 0 3.845 0A3.81 3.81 0 0 0 24 17.953V6.047a3.81 3.81 0 0 0-1.922-3.33 3.83 3.83 0 0 0-1.923-.52c-.66 0-1.32.173-1.922.52L7.923 8.67A3.81 3.81 0 0 0 6 12c0 1.17.51 2.234 1.386 2.956zm9.228-5.913-2.279 1.316.576.333c.682.393.755 1.046.755 1.308 0 .263-.073.915-.755 1.308l-10.31 5.954a1.49 1.49 0 0 1-1.51 0 1.48 1.48 0 0 1-.755-1.308V6.047c0-.788.527-1.178.754-1.308s.828-.393 1.51 0l2.732 1.577 2.334-1.348-3.899-2.251a3.81 3.81 0 0 0-3.845 0A3.81 3.81 0 0 0 0 6.047v11.906c0 1.39.72 2.635 1.922 3.33a3.83 3.83 0 0 0 1.923.52c.66 0 1.32-.173 1.922-.52l10.31-5.953A3.81 3.81 0 0 0 18 12c0-1.17-.51-2.234-1.386-2.956\"/></svg>`,\n  postgresql: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>PostgreSQL</title><path d=\"M23.5594 14.7228a.5269.5269 0 0 0-.0563-.1191c-.139-.2632-.4768-.3418-1.0074-.2321-1.6533.3411-2.2935.1312-2.5256-.0191 1.342-2.0482 2.445-4.522 3.0411-6.8297.2714-1.0507.7982-3.5237.1222-4.7316a1.5641 1.5641 0 0 0-.1509-.235C21.6931.9086 19.8007.0248 17.5099.0005c-1.4947-.0158-2.7705.3461-3.1161.4794a9.449 9.449 0 0 0-.5159-.0816 8.044 8.044 0 0 0-1.3114-.1278c-1.1822-.0184-2.2038.2642-3.0498.8406-.8573-.3211-4.7888-1.645-7.2219.0788C.9359 2.1526.3086 3.8733.4302 6.3043c.0409.818.5069 3.334 1.2423 5.7436.4598 1.5065.9387 2.7019 1.4334 3.582.553.9942 1.1259 1.5933 1.7143 1.7895.4474.1491 1.1327.1441 1.8581-.7279.8012-.9635 1.5903-1.8258 1.9446-2.2069.4351.2355.9064.3625 1.39.3772a.0569.0569 0 0 0 .0004.0041 11.0312 11.0312 0 0 0-.2472.3054c-.3389.4302-.4094.5197-1.5002.7443-.3102.064-1.1344.2339-1.1464.8115-.0025.1224.0329.2309.0919.3268.2269.4231.9216.6097 1.015.6331 1.3345.3335 2.5044.092 3.3714-.6787-.017 2.231.0775 4.4174.3454 5.0874.2212.5529.7618 1.9045 2.4692 1.9043.2505 0 .5263-.0291.8296-.0941 1.7819-.3821 2.5557-1.1696 2.855-2.9059.1503-.8707.4016-2.8753.5388-4.1012.0169-.0703.0357-.1207.057-.1362.0007-.0005.0697-.0471.4272.0307a.3673.3673 0 0 0 .0443.0068l.2539.0223.0149.001c.8468.0384 1.9114-.1426 2.5312-.4308.6438-.2988 1.8057-1.0323 1.5951-1.6698zM2.371 11.8765c-.7435-2.4358-1.1779-4.8851-1.2123-5.5719-.1086-2.1714.4171-3.6829 1.5623-4.4927 1.8367-1.2986 4.8398-.5408 6.108-.13-.0032.0032-.0066.0061-.0098.0094-2.0238 2.044-1.9758 5.536-1.9708 5.7495-.0002.0823.0066.1989.0162.3593.0348.5873.0996 1.6804-.0735 2.9184-.1609 1.1504.1937 2.2764.9728 3.0892.0806.0841.1648.1631.2518.2374-.3468.3714-1.1004 1.1926-1.9025 2.1576-.5677.6825-.9597.5517-1.0886.5087-.3919-.1307-.813-.5871-1.2381-1.3223-.4796-.839-.9635-2.0317-1.4155-3.5126zm6.0072 5.0871c-.1711-.0428-.3271-.1132-.4322-.1772.0889-.0394.2374-.0902.4833-.1409 1.2833-.2641 1.4815-.4506 1.9143-1.0002.0992-.126.2116-.2687.3673-.4426a.3549.3549 0 0 0 .0737-.1298c.1708-.1513.2724-.1099.4369-.0417.156.0646.3078.26.3695.4752.0291.1016.0619.2945-.0452.4444-.9043 1.2658-2.2216 1.2494-3.1676 1.0128zm2.094-3.988-.0525.141c-.133.3566-.2567.6881-.3334 1.003-.6674-.0021-1.3168-.2872-1.8105-.8024-.6279-.6551-.9131-1.5664-.7825-2.5004.1828-1.3079.1153-2.4468.079-3.0586-.005-.0857-.0095-.1607-.0122-.2199.2957-.2621 1.6659-.9962 2.6429-.7724.4459.1022.7176.4057.8305.928.5846 2.7038.0774 3.8307-.3302 4.7363-.084.1866-.1633.3629-.2311.5454zm7.3637 4.5725c-.0169.1768-.0358.376-.0618.5959l-.146.4383a.3547.3547 0 0 0-.0182.1077c-.0059.4747-.054.6489-.115.8693-.0634.2292-.1353.4891-.1794 1.0575-.11 1.4143-.8782 2.2267-2.4172 2.5565-1.5155.3251-1.7843-.4968-2.0212-1.2217a6.5824 6.5824 0 0 0-.0769-.2266c-.2154-.5858-.1911-1.4119-.1574-2.5551.0165-.5612-.0249-1.9013-.3302-2.6462.0044-.2932.0106-.5909.019-.8918a.3529.3529 0 0 0-.0153-.1126 1.4927 1.4927 0 0 0-.0439-.208c-.1226-.4283-.4213-.7866-.7797-.9351-.1424-.059-.4038-.1672-.7178-.0869.067-.276.1831-.5875.309-.9249l.0529-.142c.0595-.16.134-.3257.213-.5012.4265-.9476 1.0106-2.2453.3766-5.1772-.2374-1.0981-1.0304-1.6343-2.2324-1.5098-.7207.0746-1.3799.3654-1.7088.5321a5.6716 5.6716 0 0 0-.1958.1041c.0918-1.1064.4386-3.1741 1.7357-4.4823a4.0306 4.0306 0 0 1 .3033-.276.3532.3532 0 0 0 .1447-.0644c.7524-.5706 1.6945-.8506 2.802-.8325.4091.0067.8017.0339 1.1742.081 1.939.3544 3.2439 1.4468 4.0359 2.3827.8143.9623 1.2552 1.9315 1.4312 2.4543-1.3232-.1346-2.2234.1268-2.6797.779-.9926 1.4189.543 4.1729 1.2811 5.4964.1353.2426.2522.4522.2889.5413.2403.5825.5515.9713.7787 1.2552.0696.087.1372.1714.1885.245-.4008.1155-1.1208.3825-1.0552 1.717-.0123.1563-.0423.4469-.0834.8148-.0461.2077-.0702.4603-.0994.7662zm.8905-1.6211c-.0405-.8316.2691-.9185.5967-1.0105a2.8566 2.8566 0 0 0 .135-.0406 1.202 1.202 0 0 0 .1342.103c.5703.3765 1.5823.4213 3.0068.1344-.2016.1769-.5189.3994-.9533.6011-.4098.1903-1.0957.333-1.7473.3636-.7197.0336-1.0859-.0807-1.1721-.151zm.5695-9.2712c-.0059.3508-.0542.6692-.1054 1.0017-.055.3576-.112.7274-.1264 1.1762-.0142.4368.0404.8909.0932 1.3301.1066.887.216 1.8003-.2075 2.7014a3.5272 3.5272 0 0 1-.1876-.3856c-.0527-.1276-.1669-.3326-.3251-.6162-.6156-1.1041-2.0574-3.6896-1.3193-4.7446.3795-.5427 1.3408-.5661 2.1781-.463zm.2284 7.0137a12.3762 12.3762 0 0 0-.0853-.1074l-.0355-.0444c.7262-1.1995.5842-2.3862.4578-3.4385-.0519-.4318-.1009-.8396-.0885-1.2226.0129-.4061.0666-.7543.1185-1.0911.0639-.415.1288-.8443.1109-1.3505.0134-.0531.0188-.1158.0118-.1902-.0457-.4855-.5999-1.938-1.7294-3.253-.6076-.7073-1.4896-1.4972-2.6889-2.0395.5251-.1066 1.2328-.2035 2.0244-.1859 2.0515.0456 3.6746.8135 4.8242 2.2824a.908.908 0 0 1 .0667.1002c.7231 1.3556-.2762 6.2751-2.9867 10.5405zm-8.8166-6.1162c-.025.1794-.3089.4225-.6211.4225a.5821.5821 0 0 1-.0809-.0056c-.1873-.026-.3765-.144-.5059-.3156-.0458-.0605-.1203-.178-.1055-.2844.0055-.0401.0261-.0985.0925-.1488.1182-.0894.3518-.1226.6096-.0867.3163.0441.6426.1938.6113.4186zm7.9305-.4114c.0111.0792-.049.201-.1531.3102-.0683.0717-.212.1961-.4079.2232a.5456.5456 0 0 1-.075.0052c-.2935 0-.5414-.2344-.5607-.3717-.024-.1765.2641-.3106.5611-.352.297-.0414.6111.0088.6356.1851z\"/></svg>`,\n  redhat: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Red Hat</title><path d=\"M16.009 13.386c1.577 0 3.86-.326 3.86-2.202a1.765 1.765 0 0 0-.04-.431l-.94-4.08c-.216-.898-.406-1.305-1.982-2.093-1.223-.625-3.888-1.658-4.676-1.658-.733 0-.947.946-1.822.946-.842 0-1.467-.706-2.255-.706-.757 0-1.25.515-1.63 1.576 0 0-1.06 2.99-1.197 3.424a.81.81 0 0 0-.028.245c0 1.162 4.577 4.974 10.71 4.974m4.101-1.435c.218 1.032.218 1.14.218 1.277 0 1.765-1.984 2.745-4.593 2.745-5.895.004-11.06-3.451-11.06-5.734a2.326 2.326 0 0 1 .19-.925C2.746 9.415 0 9.794 0 12.217c0 3.969 9.405 8.861 16.851 8.861 5.71 0 7.149-2.582 7.149-4.62 0-1.605-1.387-3.425-3.887-4.512\"/></svg>`,\n  slack: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Slack</title><path d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z\"/></svg>`,\n  splunk: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Splunk</title><path d=\"M23.348 11.911l-2.241-1.091v-.65L24 11.621v.593l-2.893 1.438v-.636zm-5.397 1.841h-.961v-5.31h.961v3.116h.102l1.28-1.481.723.31-1.23 1.316 1.453 1.809-.888.311-1.44-1.996zm-2.577-.002v-2.068a2.685 2.685 0 0 0-.026-.42.791.791 0 0 0-.09-.26c-.113-.202-.308-.304-.59-.304a.888.888 0 0 0-.461.113.673.673 0 0 0-.286.33 1.012 1.012 0 0 0-.07.263c-.012.13-.019.262-.017.395v1.95h-.961v-3.614h.961l.002.485c.185-.2.373-.348.566-.437.192-.089.418-.134.673-.134.286 0 .527.058.721.177a1.016 1.016 0 0 1 .475.665 1.972 1.972 0 0 1 .054.448c.002.1.004.22.004.358v2.053zm-4.115.002l-.002-.485a1.783 1.783 0 0 1-.565.437 1.597 1.597 0 0 1-.674.135c-.285 0-.524-.057-.72-.17a.972.972 0 0 1-.425-.504.75.75 0 0 1-.054-.167 1.918 1.918 0 0 1-.033-.199 2.033 2.033 0 0 1-.017-.258 15.516 15.516 0 0 1-.005-.355V10.13h.956v2.07c-.003.141.006.282.026.42.015.092.045.18.09.26.113.204.308.306.59.306.36 0 .606-.15.74-.449.035-.082.06-.168.074-.257.017-.134.024-.269.022-.403v-1.95h.955v3.624zM7.184 8.44h.955v5.31h-.955zM5.759 11.9c0-.396-.08-.708-.24-.937a.759.759 0 0 0-.657-.345.804.804 0 0 0-.693.366c-.171.245-.256.574-.253.99 0 .405.084.723.25.957a.796.796 0 0 0 .69.347.685.685 0 0 0 .433-.135.985.985 0 0 0 .277-.34c.071-.14.121-.292.147-.448.03-.151.043-.3.046-.455m1.01-.036c.003.266-.04.532-.129.786-.082.23-.204.441-.364.626-.31.361-.764.567-1.24.563a1.67 1.67 0 0 1-.313-.028 1.041 1.041 0 0 1-.275-.098 1.33 1.33 0 0 1-.257-.178 2.379 2.379 0 0 1-.265-.268v2.293h-.929v-5.425h.93l.004.529c.169-.212.353-.368.55-.468.197-.1.426-.15.688-.147a1.509 1.509 0 0 1 1.156.507c.148.166.259.361.33.571.08.236.12.485.115.737m-4.21.89a.946.946 0 0 1-.102.441 1.007 1.007 0 0 1-.282.345c-.13.1-.275.173-.43.22a1.8 1.8 0 0 1-.546.08 1.985 1.985 0 0 1-.637-.097 1.964 1.964 0 0 1-.563-.32l.312-.505c.15.126.284.217.405.275.115.057.24.087.368.087a.557.557 0 0 0 .373-.12.396.396 0 0 0 .14-.322.475.475 0 0 0-.12-.318 1.306 1.306 0 0 0-.187-.173 9.231 9.231 0 0 0-.308-.232 6.787 6.787 0 0 1-.281-.21 2.11 2.11 0 0 1-.252-.232 1.039 1.039 0 0 1-.18-.275.826.826 0 0 1-.069-.347.893.893 0 0 1 .094-.409.935.935 0 0 1 .255-.314 1.22 1.22 0 0 1 .39-.203c.16-.05.327-.074.494-.072.184 0 .368.026.545.076.174.05.338.123.488.219l-.282.454a1.05 1.05 0 0 0-.608-.201.504.504 0 0 0-.323.102.307.307 0 0 0-.126.253c0 .098.041.193.113.26.074.078.203.186.385.325.185.136.336.253.457.355.104.085.202.182.286.286.065.08.115.173.145.273a.808.808 0 0 1 .046.299Z\"/></svg>`,\n  supabase: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>Supabase</title><path d=\"M11.9 1.036c-.015-.986-1.26-1.41-1.874-.637L.764 12.05C-.33 13.427.65 15.455 2.409 15.455h9.579l.113 7.51c.014.985 1.259 1.408 1.873.636l9.262-11.653c1.093-1.375.113-3.403-1.645-3.403h-9.642z\"/></svg>`,\n  vicarius: `<svg width=\"32\" height=\"32\" viewBox=\"0 0 32 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n<g clip-path=\"url(#clip0_3987_3816)\">\n<path d=\"M20.867 18.9738C20.6247 18.2663 20.2007 17.7357 19.2921 17.2051C19.8978 17.0282 20.3218 16.9103 20.5641 16.7924C20.3824 16.3207 20.2007 16.0849 19.7767 15.7901C20.867 13.7266 22.1996 12.7833 22.0785 11.9579C21.4121 12.3116 20.7459 13.5497 19.5344 14.0803C19.7767 12.0758 20.6853 10.8967 20.6853 10.1892C20.6853 9.83543 20.2613 9.12793 19.8978 8.65625C17.8989 11.5452 16.6875 15.0237 16.6875 18.7969C16.6875 21.2731 17.2327 23.6903 18.1413 25.8718C18.747 26.2845 19.2315 26.6383 19.7161 27.3457C19.6556 25.7539 20.3218 25.1643 19.7767 21.9806C20.019 22.0985 20.2613 22.2754 20.5035 22.2754C20.5642 21.3911 20.5035 20.2708 19.8978 18.62C20.2007 18.62 20.3824 18.738 20.867 18.9738Z\" fill=\"url(#paint0_linear_3987_3816)\"/>\n<path d=\"M6.73533 22.0471C5.57407 22.4427 4.64506 23.0078 3.89024 23.7425C2.38059 21.5385 1.45158 18.8258 1.45158 16C1.45158 8.25743 7.8966 1.98429 15.8513 1.98429C23.8059 1.98429 30.2509 8.25743 30.2509 16C30.2509 22.3296 25.8962 27.6985 19.9737 29.4506V28.2072C20.0899 28.2072 20.2641 28.3203 20.4963 28.6028C20.7867 26.9074 20.8447 25.1554 20.6125 23.3469C21.135 23.573 21.5415 23.9686 21.8898 24.4207C22.006 23.0644 21.7157 21.5949 20.9608 20.1256C21.4253 20.2951 21.7737 20.6342 22.064 21.1428C22.1802 19.8995 22.006 18.8258 20.9608 17.8085C21.3092 17.865 21.5415 18.091 22.064 18.5432C21.7737 17.0738 20.8447 16.7912 20.8447 15.7739C20.8447 14.1915 23.9801 13.0047 22.3543 11.0832C21.8318 11.6483 21.3092 12.157 20.7286 12.5526C20.9608 11.6483 21.4834 10.5746 21.4834 10.0659C21.4834 9.50079 19.9737 7.57925 19.7415 7.29666C19.6835 7.2402 19.3931 7.18366 19.1609 7.2402C18.4641 7.29666 18.6964 7.74878 18.4061 8.03137C17.9996 8.54002 17.419 9.16167 17.0125 9.78332C16.7222 10.1224 16.548 10.6311 16.2577 10.6876C11.903 11.3657 11.4965 15.2087 7.316 14.8132C7.49017 16.0565 8.53527 16.8477 9.87078 17.4129C9.23203 19.1083 7.72242 20.5777 5.74826 21.9341C6.09665 21.9341 6.38698 21.9906 6.73533 22.0471ZM0 16C0 24.5337 7.08374 31.4286 15.8513 31.4286C24.6188 31.4286 31.7025 24.5337 31.7025 16C31.7025 7.46625 24.6188 0.571411 15.8513 0.571411C7.08374 0.571411 0 7.46625 0 16ZM15.1545 15.5479C14.6319 15.6044 14.5158 15.6609 13.9932 15.8305C14.0513 14.5871 15.6771 13.0047 17.2448 11.2527C17.9996 10.405 18.9867 8.59655 19.3351 8.59655C19.5673 8.59655 20.5544 9.95291 20.5544 10.4615C20.5544 11.1397 19.6835 12.27 19.4512 14.1915C20.6125 13.6829 21.1931 12.496 21.8898 12.157C22.064 13.0047 20.7286 13.9089 19.6835 15.887C20.0899 16.226 20.2641 16.4521 20.4382 16.8477C20.206 16.9608 19.7996 17.0738 19.219 17.2433C20.148 17.7519 20.5544 18.3171 20.7286 18.9388C20.3222 18.7127 20.0899 18.6562 19.7415 18.5997C20.3222 20.1256 20.3802 21.2559 20.3222 22.1036C20.0899 22.1036 19.7996 21.9906 19.6254 21.821C20.148 24.8728 19.5092 25.4379 19.5673 26.9639C19.1028 26.2857 18.6383 25.8901 18.0577 25.551C17.6512 27.0769 17.0125 27.7551 16.6061 28.1507C16.9545 26.0031 16.8964 24.0816 15.9674 22.1601C15.6771 22.4427 15.619 22.7818 15.4448 23.1774C14.2836 22.3862 13.8191 21.1993 14.5158 19.0518C13.9932 19.4474 13.8191 19.7865 13.5287 20.1256C13.1803 17.5259 13.9932 16.5651 15.1545 15.5479Z\" fill=\"#10121F\"/>\n</g>\n<defs>\n<linearGradient id=\"paint0_linear_3987_3816\" x1=\"18.5841\" y1=\"27.6523\" x2=\"21.025\" y2=\"9.03623\" gradientUnits=\"userSpaceOnUse\">\n<stop stop-color=\"#4C4EF0\"/>\n<stop offset=\"1\" stop-color=\"#4C4EF0\" stop-opacity=\"0\"/>\n</linearGradient>\n<clipPath id=\"clip0_3987_3816\">\n<rect width=\"32\" height=\"32\" fill=\"white\"/>\n</clipPath>\n</defs>\n</svg>`,\n  vmware: `<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><title>VMware</title><path d=\"M.5 10.1a.505.505 0 00-.197.048.497.497 0 00-.25.68l1.138 2.475c.179.38.38.592.721.592.342 0 .542-.22.72-.592l1.003-2.186a.144.144 0 01.144-.092.16.16 0 01.157.16v2.118a.535.535 0 101.066 0v-1.73a.531.531 0 01.566-.552.52.52 0 01.541.551v1.73a.531.531 0 00.53.593.539.539 0 00.535-.592v-1.73a.531.531 0 01.564-.552.52.52 0 01.543.551v1.73a.531.531 0 00.528.593.535.535 0 00.535-.592v-1.969a1.234 1.234 0 00-1.283-1.23 1.647 1.647 0 00-1.14.486 1.26 1.26 0 00-1.095-.483 1.807 1.807 0 00-1.074.483 1.287 1.287 0 00-.961-.483 1.177 1.177 0 00-1.158.786l-.729 1.716-.933-2.203.011-.004A.505.505 0 00.5 10.1zm18.43.06a.27.27 0 00-.266.274h.002v3.142a.27.27 0 10.535 0v-1.222c0-1.037.571-1.56 1.27-1.643a.266.266 0 00.238-.274.258.258 0 00-.266-.269 1.465 1.465 0 00-1.242.88v-.614a.266.266 0 00-.271-.274zm-6.735.008a.273.273 0 00-.25.217l-.912 2.627-.902-2.62a.28.28 0 00-.274-.22.266.266 0 00-.27.258.493.493 0 00.034.144l1.09 3.037.02-.007a.319.319 0 00.298.242.3.3 0 00.293-.242l.903-2.583.896 2.583a.3.3 0 00.293.242h.018a.319.319 0 00.293-.242l1.097-3.038a.512.512 0 00.033-.144.258.258 0 00-.265-.25.262.262 0 00-.258.209l-.918 2.63-.904-2.626a.285.285 0 00-.278-.217h-.025a.273.273 0 00-.012 0zm10.168.008a1.75 1.75 0 00-1.691 1.851 1.765 1.765 0 001.76 1.858l-.008.013a1.784 1.784 0 001.33-.539.228.228 0 00.082-.17.228.228 0 00-.379-.168 1.435 1.435 0 01-1.018.415 1.237 1.237 0 01-1.24-1.207h2.555a.247.247 0 00.246-.247c0-.945-.593-1.806-1.637-1.806zm-5.744.002a1.571 1.571 0 00-.158.006 2.384 2.384 0 00-1.078.205.22.22 0 00-.143.222.24.24 0 00.235.229.266.266 0 00.095-.024 1.822 1.822 0 01.834-.162c.691 0 1.07.334 1.07.979v.125a3.796 3.796 0 00-1.103-.15c-.892 0-1.52.4-1.52 1.16l-.003-.004c0 .736.671 1.117 1.34 1.117a1.575 1.575 0 001.298-.62v.343a.247.247 0 00.254.25.254.254 0 00.258-.262v-1.983a1.416 1.416 0 00-.379-1.046 1.571 1.571 0 00-1-.385zm5.719.43c.714 0 1.085.565 1.139 1.214h-2.278a1.222 1.222 0 011.139-1.215zm-5.885 1.382a3.75 3.75 0 011.057.153V12.49c0 .57-.539.973-1.2.973-.485 0-.904-.261-.904-.713 0-.467.375-.76 1.047-.76Z\"/></svg>`,\n};\n\nexport type BrandMarkName = keyof typeof BRAND_MARK_SVGS;\n\n/** A data: URI usable directly in CSS `mask-image: url(...)` / `-webkit-mask-image`. */\nexport function brandMarkDataUri(name: string): string {\n  const svg = BRAND_MARK_SVGS[name.toLowerCase()];\n  if (!svg) return '';\n  return `data:image/svg+xml,${encodeURIComponent(svg)}`;\n}\n","import type { ComponentType, SVGProps } from 'react';\nimport { WindowsIcon, LinuxIcon, MacOSIcon } from '../components/icons';\n\n// OS-level platforms (Windows, Linux, MacOS)\n\nexport type OSPlatformId = 'windows' | 'linux' | 'darwin';\n\nexport interface OSPlatformOption {\n  id: OSPlatformId;\n  name: string;\n  icon: ComponentType<SVGProps<SVGSVGElement>>;\n}\n\nexport const OS_PLATFORMS: OSPlatformOption[] = [\n  { id: 'windows', name: 'Windows', icon: WindowsIcon },\n  { id: 'linux', name: 'Linux', icon: LinuxIcon },\n  { id: 'darwin', name: 'MacOS', icon: MacOSIcon },\n];\n\nexport const DEFAULT_OS_PLATFORM: OSPlatformId = 'windows';\n","/**\n * Email Domain Validation Utilities\n *\n * Validates email domain formats (the part after @ in email addresses)\n * Used for SSO domain allowlist feature to restrict auto-provisioning to specific domains.\n */\n\n/**\n * Validates an email domain format (the part after @ in email addresses)\n * Valid: example.com, sub.domain.org, my-company.co.uk\n * Invalid: http://example.com, @example.com, example, .com\n *\n * @param domain - The domain string to validate\n * @returns true if the domain is a valid email domain format\n */\nexport function isValidEmailDomain(domain: string): boolean {\n  if (!domain || typeof domain !== 'string') {\n    return false;\n  }\n\n  const trimmed = domain.trim();\n  if (!trimmed) {\n    return false;\n  }\n\n  // Domain regex: must have at least one dot, valid characters, and proper TLD\n  // Allows subdomains, hyphens (not at start/end of labels), alphanumeric characters\n  const domainRegex = /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\\.[A-Za-z0-9-]{1,63})*\\.[A-Za-z]{2,}$/;\n  return domainRegex.test(trimmed);\n}\n\n/**\n * Validation result type\n */\nexport interface EmailDomainValidationResult {\n  valid: boolean;\n  error?: string;\n  cleanedDomain?: string;\n}\n\n/**\n * Validates an email domain with detailed error messages\n * Also cleans the input (removes leading @, trims whitespace)\n *\n * @param domain - The domain string to validate\n * @returns Validation result with error message if invalid\n */\nexport function validateEmailDomain(domain: string): EmailDomainValidationResult {\n  if (!domain || typeof domain !== 'string') {\n    return { valid: false, error: 'Email domain is required' };\n  }\n\n  // Clean the input: trim and remove leading @ if user enters it\n  let trimmed = domain.trim();\n  if (trimmed.startsWith('@')) {\n    trimmed = trimmed.substring(1);\n  }\n\n  // Check for empty after cleaning\n  if (!trimmed) {\n    return { valid: false, error: 'Email domain is required' };\n  }\n\n  // Check for protocol (common mistake: entering full URL)\n  if (trimmed.includes('://')) {\n    return { valid: false, error: 'Enter email domain only (e.g., openframe.ai), not a URL' };\n  }\n\n  // Check for @ symbol (common mistake: entering full email address)\n  if (trimmed.includes('@')) {\n    return { valid: false, error: 'Enter domain only, not a full email address' };\n  }\n\n  // Check for spaces\n  if (trimmed.includes(' ')) {\n    return { valid: false, error: 'Domain cannot contain spaces' };\n  }\n\n  // Check for trailing slash\n  if (trimmed.includes('/')) {\n    return { valid: false, error: 'Enter domain only, without paths' };\n  }\n\n  // Validate the domain format\n  if (!isValidEmailDomain(trimmed)) {\n    return { valid: false, error: 'Invalid domain format (e.g., openframe.ai)' };\n  }\n\n  // Return success with the cleaned domain\n  return { valid: true, cleanedDomain: trimmed.toLowerCase() };\n}\n\n/**\n * Validates a list of email domains\n * Returns the first error encountered or success if all domains are valid\n *\n * @param domains - Array of domain strings to validate\n * @returns Validation result with array of cleaned domains if valid\n */\nexport function validateEmailDomainList(domains: string[]): {\n  valid: boolean;\n  error?: string;\n  cleanedDomains?: string[];\n} {\n  if (!Array.isArray(domains)) {\n    return { valid: false, error: 'Domains must be an array' };\n  }\n\n  const cleanedDomains: string[] = [];\n  const seenDomains = new Set<string>();\n\n  for (const domain of domains) {\n    const result = validateEmailDomain(domain);\n\n    if (!result.valid) {\n      return { valid: false, error: `Invalid domain \"${domain}\": ${result.error}` };\n    }\n\n    // `EmailDomainValidationResult` is a flat interface, not a discriminated\n    // union, so `valid: true` does not prove `cleanedDomain` is set. Treat a\n    // missing one as a validation failure rather than pushing `undefined` into\n    // the cleaned list (which is what the old assertion allowed downstream).\n    const { cleanedDomain } = result;\n    if (!cleanedDomain) {\n      return { valid: false, error: `Invalid domain \"${domain}\": could not be normalized` };\n    }\n\n    // Check for duplicates (case-insensitive)\n    if (seenDomains.has(cleanedDomain)) {\n      return { valid: false, error: `Duplicate domain: ${cleanedDomain}` };\n    }\n\n    seenDomains.add(cleanedDomain);\n    cleanedDomains.push(cleanedDomain);\n  }\n\n  return { valid: true, cleanedDomains };\n}\n\n/**\n * Cleans a domain string by removing common user input mistakes\n * Does NOT validate - use validateEmailDomain for validation\n *\n * @param domain - The domain string to clean\n * @returns Cleaned domain string\n */\nexport function cleanEmailDomain(domain: string): string {\n  if (!domain || typeof domain !== 'string') {\n    return '';\n  }\n\n  let cleaned = domain.trim().toLowerCase();\n\n  // Remove leading @\n  if (cleaned.startsWith('@')) {\n    cleaned = cleaned.substring(1);\n  }\n\n  // Remove protocol if present\n  cleaned = cleaned.replace(/^https?:\\/\\//, '');\n\n  // Remove trailing slash and path\n  cleaned = cleaned.split('/')[0];\n\n  // Remove www. prefix\n  cleaned = cleaned.replace(/^www\\./, '');\n\n  return cleaned;\n}\n","/**\n * Confidence helpers for AI enrichment\n * Shared utilities for displaying confidence levels from AI operations\n */\n\n/**\n * Get Tailwind CSS classes for confidence badge display (border and text color)\n * @param confidence - Confidence score (0-100)\n * @returns CSS classes for border and text color\n */\nexport function getConfidenceColorClass(confidence?: number): string {\n  if (!confidence && confidence !== 0) return '';\n  if (confidence >= 80) return 'border-green-500 text-green-500';\n  if (confidence >= 50) return 'border-yellow-500 text-yellow-500';\n  return 'border-red-500 text-red-500';\n}\n\n/**\n * Get confidence level category\n * @param confidence - Confidence score (0-100)\n * @returns Confidence level category\n */\nexport function getConfidenceLevel(confidence?: number): 'high' | 'medium' | 'low' | 'none' {\n  if (!confidence && confidence !== 0) return 'none';\n  if (confidence >= 80) return 'high';\n  if (confidence >= 50) return 'medium';\n  return 'low';\n}\n\n/**\n * Get Tailwind CSS class for border color only\n * @param confidence - Confidence score (0-100)\n * @returns CSS class for border color\n */\nexport function getConfidenceBorderClass(confidence?: number): string {\n  if (!confidence && confidence !== 0) return '';\n  if (confidence >= 80) return 'border-green-500';\n  if (confidence >= 50) return 'border-yellow-500';\n  return 'border-red-500';\n}\n\n/**\n * Get Tailwind CSS class for text color only\n * @param confidence - Confidence score (0-100)\n * @returns CSS class for text color\n */\nexport function getConfidenceTextClass(confidence?: number): string {\n  if (!confidence && confidence !== 0) return '';\n  if (confidence >= 80) return 'text-green-500';\n  if (confidence >= 50) return 'text-yellow-500';\n  return 'text-red-500';\n}\n\n/**\n * Get Tailwind CSS class for background color (subtle)\n * @param confidence - Confidence score (0-100)\n * @returns CSS class for subtle background color\n */\nexport function getConfidenceBgClass(confidence?: number): string {\n  if (!confidence && confidence !== 0) return '';\n  if (confidence >= 80) return 'bg-green-500/10';\n  if (confidence >= 50) return 'bg-yellow-500/10';\n  return 'bg-red-500/10';\n}\n\n/**\n * Get descriptive label for confidence level\n * @param confidence - Confidence score (0-100)\n * @returns Human-readable confidence label\n */\nexport function getConfidenceLabel(confidence?: number): string {\n  if (!confidence && confidence !== 0) return 'Unknown';\n  if (confidence >= 80) return 'High';\n  if (confidence >= 50) return 'Medium';\n  return 'Low';\n}\n","/**\n * Canonical URL param keys for the dev-center sections — the ONE source for both:\n *   - the chrome registry (`OPENFRAME_DEV_SECTIONS`), which WRITES `?<key>=…` to the URL, and\n *   - the list views (`RoadmapView` / `ProductReleasesView` / `DeliveryLists`), which READ\n *     `?<key>=…` to fetch the filtered list.\n *\n * The chrome and the view MUST agree on the key or filtering silently breaks. Importing\n * from here (instead of re-declaring the literal in each place) makes that impossible to\n * get wrong. Pure string constants — no React, no heavy deps — so both server-bundled\n * utils and client views can import it freely.\n */\nexport const DEV_SECTION_PARAM_KEYS = {\n  /** Free-text search box — shared by every dev-center section. */\n  search: 'search',\n  /** Roadmap (and Help Center tickets) status filter. */\n  status: 'status',\n  /** Product-releases stability-tier filter. */\n  releaseStatus: 'release_status',\n  /** Delivery (bug-fix / enhancement) task-type filter. */\n  deliveryTaskType: 'task_type',\n} as const;\n\n/** Section keys that participate in `<section>-<id>` anchor IDs.\n *  The URL composer (`appendSearchAndHash` in hub `dev-section-url.ts`)\n *  and the row components (`DeliveryRow`, `RoadmapCard`, `HelpCenterCard`)\n *  both call `devSectionAnchorId` so the DOM `id` and the URL hash stay\n *  in lockstep — adding a new section means adding the literal here\n *  ONCE, not at every render site. */\nexport type DevSectionAnchorKind = 'roadmap' | 'delivery' | 'ticket';\n\n/** Compose the canonical `<section>-<id>` anchor id used by the dev-center\n *  rows + URL composer. */\nexport function devSectionAnchorId(section: DevSectionAnchorKind, id: string): string {\n  return `${section}-${id}`;\n}\n\n/**\n * THE single source of truth for \"which ticket is open\" — the URL query\n * param `HelpCenterList` derives its drawer state from. Every producer\n * of a ticket deep link goes through `buildTicketOpenHref`; every\n * consumer reads THIS param name. Lives HERE (pure, server-safe, beside\n * its sibling param keys) because producers span both worlds: the hub's\n * SERVER-side chat-ref builder and the client header cell.\n */\nexport const TICKET_OPEN_PARAM = 'ticket';\n\n/**\n * Build THE canonical ticket deep link — the ONE producer for every\n * surface that links to a ticket (chat inline cards via the hub's\n * `buildChatRefFromTicketRow`, the header `TicketAlertsButton`, any\n * future caller). Byte-identical output everywhere:\n *\n *   `<base>?ticket=<id>&search=<id>#ticket-<id>`\n *\n * All three parts are load-bearing:\n *   - `?ticket=`  — drawer auto-open (`HelpCenterList` derives its open\n *                   state from this param);\n *   - `&search=`  — filters the list to ONLY that ticket so the\n *                   drawer-open lookup hits regardless of which PAGE the\n *                   ticket would normally land on (without it, tickets\n *                   on page 2+ silently fail to auto-open);\n *   - `#ticket-…` — scroll anchor via the dev-section deep-link\n *                   dispatch (`useScrollToHash` + `devSectionAnchorId`).\n *\n * `base` is the host's tickets surface path and may carry ANY nesting\n * prefix (hub `/tickets`, console `/help-center/tickets`, an embedder's\n * `/support/portal/tickets`). Bases already carrying a query string are\n * composed with `&`.\n */\nexport function buildTicketOpenHref(base: string, ticketExternalId: string): string {\n  const enc = encodeURIComponent(ticketExternalId);\n  const sep = base.includes('?') ? '&' : '?';\n  return `${base}${sep}${TICKET_OPEN_PARAM}=${enc}&${DEV_SECTION_PARAM_KEYS.search}=${enc}#${devSectionAnchorId('ticket', enc)}`;\n}\n","/**\n * Tool Utilities\n *\n * Provides normalization and conversion utilities for tool types.\n * Handles various input formats (aliases, case variations) and converts\n * them to the canonical ToolType used throughout the platform.\n */\n\nimport { type ToolType, toolLabels } from '../types/tool.types';\n\n/**\n * Map of common tool name variants to canonical ToolType\n */\nconst toolAliasMap: Record<string, ToolType> = {\n  // Fleet MDM\n  FLEET: 'FLEET_MDM',\n  FLEET_MDM: 'FLEET_MDM',\n  'FLEET-MDM': 'FLEET_MDM',\n  FLEETMDM: 'FLEET_MDM',\n  fleet: 'FLEET_MDM',\n  fleet_mdm: 'FLEET_MDM',\n  'fleet-mdm': 'FLEET_MDM',\n  fleetmdm: 'FLEET_MDM',\n  'fleetmdm-agent': 'FLEET_MDM',\n\n  // MeshCentral\n  MESHCENTRAL: 'MESHCENTRAL',\n  MESH: 'MESHCENTRAL',\n  MESH_CENTRAL: 'MESHCENTRAL',\n  'MESH-CENTRAL': 'MESHCENTRAL',\n  mesh: 'MESHCENTRAL',\n  meshcentral: 'MESHCENTRAL',\n  mesh_central: 'MESHCENTRAL',\n  'mesh-central': 'MESHCENTRAL',\n  'meshcentral-agent': 'MESHCENTRAL',\n\n  // Authentik\n  AUTHENTIK: 'AUTHENTIK',\n  authentik: 'AUTHENTIK',\n\n  // OpenFrame\n  OPENFRAME: 'OPENFRAME',\n  openframe: 'OPENFRAME',\n  OPEN_FRAME: 'OPENFRAME',\n  'OPEN-FRAME': 'OPENFRAME',\n  open_frame: 'OPENFRAME',\n  'open-frame': 'OPENFRAME',\n\n  // OpenFrame Chat\n  OPENFRAME_CHAT: 'OPENFRAME_CHAT',\n  'OPENFRAME-CHAT': 'OPENFRAME_CHAT',\n  OPENFRAMECHAT: 'OPENFRAME_CHAT',\n  openframe_chat: 'OPENFRAME_CHAT',\n  'openframe-chat': 'OPENFRAME_CHAT',\n  openframechat: 'OPENFRAME_CHAT',\n\n  // OpenFrame Client\n  OPENFRAME_CLIENT: 'OPENFRAME_CLIENT',\n  'OPENFRAME-CLIENT': 'OPENFRAME_CLIENT',\n  OPENFRAMECLIENT: 'OPENFRAME_CLIENT',\n  openframe_client: 'OPENFRAME_CLIENT',\n  'openframe-client': 'OPENFRAME_CLIENT',\n  openframeclient: 'OPENFRAME_CLIENT',\n\n  // OpenFrame RMM\n  RMM: 'OPENFRAME_RMM',\n  rmm: 'OPENFRAME_RMM',\n  OPENFRAME_RMM: 'OPENFRAME_RMM',\n  'OPENFRAME-RMM': 'OPENFRAME_RMM',\n  OPENFRAMERMM: 'OPENFRAME_RMM',\n  openframe_rmm: 'OPENFRAME_RMM',\n  'openframe-rmm': 'OPENFRAME_RMM',\n  openframermm: 'OPENFRAME_RMM',\n  'openframe-rmm-agent': 'OPENFRAME_RMM',\n\n  // OSQUERY\n  OSQUERY: 'OSQUERY',\n\n  // System\n  SYSTEM: 'SYSTEM',\n  system: 'SYSTEM',\n};\n\n/**\n * Normalizes a tool name string to the canonical ToolType.\n * Handles various formats like 'rmm', 'OPENFRAME_RMM', 'openframe-rmm', etc.\n *\n * @param input - The tool name string to normalize\n * @returns The canonical ToolType, or undefined if no match found\n *\n * @example\n * normalizeToolType('rmm') // => 'OPENFRAME_RMM'\n * normalizeToolType('FLEET-MDM') // => 'FLEET_MDM'\n * normalizeToolType('unknown') // => undefined\n */\nexport function normalizeToolType(input?: string): ToolType | undefined {\n  if (!input) return undefined;\n\n  // Try exact match first\n  const exact = toolAliasMap[input];\n  if (exact) return exact;\n\n  // Try uppercase\n  const upper = input.toUpperCase();\n  if (toolAliasMap[upper]) return toolAliasMap[upper];\n\n  // Try lowercase\n  const lower = input.toLowerCase();\n  if (toolAliasMap[lower]) return toolAliasMap[lower];\n\n  return undefined;\n}\n\n/**\n * Normalizes a tool name string to the canonical ToolType with a fallback.\n * Returns 'SYSTEM' as the default if no match is found.\n *\n * @param input - The tool name string to normalize\n * @returns The canonical ToolType, defaults to 'SYSTEM' if no match\n *\n * @example\n * normalizeToolTypeWithFallback('rmm') // => 'OPENFRAME_RMM'\n * normalizeToolTypeWithFallback('unknown') // => 'SYSTEM'\n */\nexport function normalizeToolTypeWithFallback(input?: string): ToolType {\n  return normalizeToolType(input) ?? 'SYSTEM';\n}\n\n/**\n * Converts any tool name variant to its display label.\n * Handles normalization internally.\n *\n * @param input - The tool name string (any format)\n * @returns The display label, or the original input if no match\n *\n * @example\n * toToolLabel('rmm') // => 'RMM'\n * toToolLabel('FLEET-MDM') // => 'Fleet'\n * toToolLabel('unknown') // => 'unknown'\n */\nexport function toToolLabel(input?: string): string {\n  if (!input) return '';\n\n  const toolType = normalizeToolType(input);\n  if (toolType) {\n    return toolLabels[toolType];\n  }\n\n  return input;\n}\n\n/**\n * Checks if a string is a valid tool type (or can be normalized to one).\n *\n * @param input - The string to check\n * @returns True if the input can be normalized to a valid ToolType\n *\n * @example\n * isValidToolType('rmm') // => true\n * isValidToolType('FLEET_MDM') // => true\n * isValidToolType('unknown') // => false\n */\nexport function isValidToolType(input?: string): boolean {\n  return normalizeToolType(input) !== undefined;\n}\n\n/**\n * Gets all valid tool type aliases for a given canonical ToolType.\n * Useful for documentation or validation purposes.\n *\n * @param toolType - The canonical ToolType\n * @returns Array of all aliases that map to this tool type\n */\nexport function getToolTypeAliases(toolType: ToolType): string[] {\n  return Object.entries(toolAliasMap)\n    .filter(([_, value]) => value === toolType)\n    .map(([key]) => key);\n}\n\n/**\n * Get display label for a tool type\n */\nexport function getToolLabel(toolType: ToolType): string {\n  return toolLabels[toolType] || toolType;\n}\n","import type { ComponentType } from 'react';\nimport { BashIcon, CmdIcon, DenoIcon, NushellIcon, ShellIcon } from '../components/icons';\n/**\n * Centralized Shell Types\n *\n * Single source of truth for all shell/script type information across the platform.\n * Based on the RMM-supported shell types.\n */\nimport { PowershellLogoIcon } from '../components/icons-v2-generated/brand-logos/powershell-logo-icon';\nimport { PythonLogoIcon } from '../components/icons-v2-generated/brand-logos/python-logo-icon';\nimport type { IconProps } from './icons';\n\nexport const ShellTypeValues = {\n  POWERSHELL: 'POWERSHELL',\n  CMD: 'CMD',\n  BASH: 'BASH',\n  PYTHON: 'PYTHON',\n  NUSHELL: 'NUSHELL',\n  DENO: 'DENO',\n  SHELL: 'SHELL',\n} as const;\n\nexport type ShellType = (typeof ShellTypeValues)[keyof typeof ShellTypeValues];\n\n/**\n * Shell type definition with all metadata\n */\nexport interface ShellTypeDefinition {\n  id: ShellType;\n  label: string;\n  value: string;\n  icon: ComponentType<IconProps>;\n}\n\n/**\n * Complete list of all shell types with icons and labels\n * SINGLE SOURCE OF TRUTH - Use this everywhere\n */\nexport const SHELL_TYPES: ShellTypeDefinition[] = [\n  { id: ShellTypeValues.POWERSHELL, label: 'PowerShell', value: 'powershell', icon: PowershellLogoIcon },\n  { id: ShellTypeValues.CMD, label: 'Batch', value: 'cmd', icon: CmdIcon },\n  { id: ShellTypeValues.BASH, label: 'Bash', value: 'bash', icon: BashIcon },\n  { id: ShellTypeValues.PYTHON, label: 'Python', value: 'python', icon: PythonLogoIcon },\n  { id: ShellTypeValues.NUSHELL, label: 'Nu', value: 'nushell', icon: NushellIcon },\n  { id: ShellTypeValues.DENO, label: 'Deno', value: 'deno', icon: DenoIcon },\n  { id: ShellTypeValues.SHELL, label: 'Shell', value: 'shell', icon: ShellIcon },\n];\n\n/**\n * Maps shell types to display labels\n */\nexport const shellLabels: Record<ShellType, string> = {\n  [ShellTypeValues.POWERSHELL]: 'PowerShell',\n  [ShellTypeValues.CMD]: 'Batch',\n  [ShellTypeValues.BASH]: 'Bash',\n  [ShellTypeValues.PYTHON]: 'Python',\n  [ShellTypeValues.NUSHELL]: 'Nu',\n  [ShellTypeValues.DENO]: 'Deno',\n  [ShellTypeValues.SHELL]: 'Shell',\n};\n","/**\n * Shell Type Utilities\n *\n * Helper functions for working with shell types.\n */\n\nimport type React from 'react';\nimport { type ShellType, SHELL_TYPES, shellLabels } from '../types/shell.types';\n\n/**\n * Get display label for a shell type\n */\nexport function getShellLabel(shellType?: string): string {\n  if (!shellType) return 'Unknown';\n  const normalized = shellType.toUpperCase() as ShellType;\n  return shellLabels[normalized] || shellType;\n}\n\n/**\n * Get icon component for a shell type\n */\nexport function getShellIcon(shellType?: string): React.ComponentType<{ className?: string }> | undefined {\n  if (!shellType) return undefined;\n  const normalized = shellType.toUpperCase() as ShellType;\n  const shellDef = SHELL_TYPES.find(s => s.id === normalized);\n  return shellDef?.icon;\n}\n","// Video-bite element identity — SSOT for the stable per-element `id` on\n// `video_bites` / `teasers` jsonb elements (see types/video-processing.ts\n// `VideoTeaser.id`) and for the ONE element-matching predicate shared by the\n// hub's bite writers (`saveEntityVideoBitesResult` merge + the\n// `reconcileIncomingBites` clobber guard).\n//\n// Isomorphic by design: runs in the admin editor (browser) and in the hub's\n// Mux reconciliation sweep (Node). Zero dependencies.\n\n/** Shape every matcher/stamper here operates on — the identity-relevant subset of VideoTeaser. */\nexport interface BiteIdentity {\n  id?: string;\n  url: string;\n  source_url?: string;\n}\n\n/** 8 lowercase hex chars. Collision-safe within one array (n≤~100 → p≈1e-6); identity is always scoped to (table, row, column). */\nexport const BITE_ID_RE = /^[0-9a-f]{8}$/;\n\n/** Generate a new 8-hex bite element id. */\nexport function generateBiteId(): string {\n  return crypto.randomUUID().replace(/-/g, '').slice(0, 8);\n}\n\n/**\n * Stamp missing ids and re-stamp in-array duplicates (copy/pasted bites).\n * Returns the SAME array reference when nothing changed so callers can cheaply\n * skip persistence (`changed === false` ⇒ no write needed).\n */\nexport function ensureBiteIds<T extends { id?: string }>(bites: readonly T[]): { bites: T[]; changed: boolean } {\n  const seen = new Set<string>();\n  let changed = false;\n  const out = bites.map(bite => {\n    const valid = typeof bite.id === 'string' && BITE_ID_RE.test(bite.id) && !seen.has(bite.id);\n    if (valid) {\n      seen.add(bite.id as string);\n      return bite;\n    }\n    changed = true;\n    let id = generateBiteId();\n    while (seen.has(id)) id = generateBiteId();\n    seen.add(id);\n    return { ...bite, id };\n  });\n  return changed ? { bites: out, changed } : { bites: bites as T[], changed };\n}\n\n/**\n * The ONE element-identity predicate. Matching legs (any-of, NOT\n * short-circuit-on-id-inequality — two sides can carry DIFFERENT ids for\n * the same clip, e.g. a redelivered Vizard ingest regenerates ids):\n *   1. `id` equality — when both sides carry one and they match.\n *   2. `incoming.url === existing.url` — plain url (pre-flip stale sessions,\n *      plain re-saves).\n *   3. `incoming.url === existing.source_url` — the incoming side still holds\n *      the original storage URL of an element the Mux pipeline already flipped\n *      to HLS (redelivered ingests, stale editor arrays).\n *\n * Callers that match a whole incoming array against an existing array MUST\n * consume each existing element at most once (multiset matching) — duplicate\n * incoming urls must not all bind to one element and clone its id.\n */\nexport function matchBiteElement(existing: BiteIdentity, incoming: BiteIdentity): boolean {\n  if (existing.id && incoming.id && existing.id === incoming.id) return true;\n  if (incoming.url === existing.url) return true;\n  if (existing.source_url && incoming.url === existing.source_url) return true;\n  return false;\n}\n","/**\n * Centralized OS Types\n *\n * Single source of truth for all operating system type information across the platform.\n * Handles normalization of OS values from various sources (Fleet MDM, OpenFrame RMM, GraphQL).\n */\n\nimport type React from 'react';\nimport { AppleLogoIcon } from '../components/icons-v2-generated/brand-logos/apple-logo-icon';\nimport { LinuxLogoIcon } from '../components/icons-v2-generated/brand-logos/linux-logo-icon';\nimport { WindowsLogoGreyIcon } from '../components/icons-v2-generated/brand-logos/windows-logo-grey-icon';\nimport type { OSPlatformId } from '../utils/os-platforms';\n\nexport const OSTypeValues = {\n  WINDOWS: 'WINDOWS',\n  MACOS: 'MACOS',\n  LINUX: 'LINUX',\n} as const;\n\nexport type OSType = (typeof OSTypeValues)[keyof typeof OSTypeValues];\n\n/**\n * OS type definition with all metadata\n */\nexport interface OSTypeDefinition {\n  id: OSType;\n  label: string;\n  value: string;\n  icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\n  platformId: OSPlatformId;\n  aliases: string[]; // Alternative names/values that map to this OS\n}\n\n/**\n * Complete list of all OS types with icons and labels\n * SINGLE SOURCE OF TRUTH - Use this everywhere\n */\nexport const OS_TYPES: OSTypeDefinition[] = [\n  {\n    id: OSTypeValues.MACOS,\n    label: 'macOS',\n    value: OSTypeValues.MACOS,\n    icon: AppleLogoIcon,\n    platformId: 'darwin',\n    aliases: ['darwin', 'macos', 'mac os', 'osx', 'os x', 'mac'], // Put more specific ones first, 'mac' last to avoid false matches\n  },\n  {\n    id: OSTypeValues.WINDOWS,\n    label: 'Windows',\n    value: OSTypeValues.WINDOWS,\n    icon: WindowsLogoGreyIcon,\n    platformId: 'windows',\n    aliases: ['windows', 'win32', 'win64', 'win'], // 'win' last since it's shortest\n  },\n  {\n    id: OSTypeValues.LINUX,\n    label: 'Linux',\n    value: OSTypeValues.LINUX,\n    icon: LinuxLogoIcon,\n    platformId: 'linux',\n    aliases: ['linux', 'ubuntu', 'debian', 'centos', 'redhat', 'fedora', 'pop', 'pop!_os', 'arch', 'manjaro'],\n  },\n];\n\n/**\n * Maps OS types to display labels\n */\nexport const osLabels: Record<OSType, string> = {\n  [OSTypeValues.WINDOWS]: 'Windows',\n  [OSTypeValues.MACOS]: 'macOS',\n  [OSTypeValues.LINUX]: 'Linux',\n};\n","/**\n * OS Type Utilities\n *\n * Helper functions for working with OS types.\n */\n\nimport type React from 'react';\nimport { type OSType, type OSTypeDefinition, OS_TYPES, osLabels } from '../types/os.types';\nimport type { OSPlatformId } from './os-platforms';\n\n/**\n * Normalize OS type string to standard OSType enum\n * Handles case-insensitive matching and various OS name variations\n *\n * @param osType - Raw OS type string from API or device data\n * @returns Normalized OSType or undefined if not recognized\n *\n * @example\n * normalizeOSType('windows') // 'WINDOWS'\n * normalizeOSType('Darwin') // 'MACOS'\n * normalizeOSType('Ubuntu') // 'LINUX'\n */\nexport function normalizeOSType(osType?: string): OSType | undefined {\n  if (!osType) return undefined;\n\n  const normalized = osType.toLowerCase().trim();\n\n  // Check for exact matches first, then partial matches\n  // This prevents \"win\" from matching \"darwin\"\n  for (const osTypeDef of OS_TYPES) {\n    // Check for exact word match first\n    if (\n      osTypeDef.aliases.some(alias => {\n        // Exact match\n        if (normalized === alias) return true;\n        // Word boundary match (e.g., \"mac\" in \"mac os\" but not in \"vmac\")\n        const wordBoundaryRegex = new RegExp(`\\\\b${alias}\\\\b`, 'i');\n        return wordBoundaryRegex.test(osType);\n      })\n    ) {\n      return osTypeDef.id;\n    }\n  }\n\n  // Fallback to partial matching for version strings like \"macOS 26.0.1\"\n  for (const osTypeDef of OS_TYPES) {\n    if (osTypeDef.aliases.some(alias => normalized.includes(alias))) {\n      return osTypeDef.id;\n    }\n  }\n\n  return undefined;\n}\n\n/**\n * Get display label for an OS type\n *\n * @param osType - OS type string (can be raw or normalized)\n * @returns Display label\n */\nexport function getOSLabel(osType?: string): string {\n  if (!osType) return 'Unknown';\n\n  const normalized = normalizeOSType(osType);\n  return normalized ? osLabels[normalized] : osType;\n}\n\n/**\n * Get icon component for an OS type\n *\n * @param osType - OS type string (can be raw or normalized)\n * @returns Icon component or undefined\n */\nexport function getOSIcon(osType?: string): React.ComponentType<React.SVGProps<SVGSVGElement>> | undefined {\n  if (!osType) return undefined;\n\n  const normalized = normalizeOSType(osType);\n  if (!normalized) return undefined;\n\n  const osTypeDef = OS_TYPES.find(t => t.id === normalized);\n  return osTypeDef?.icon;\n}\n\n/**\n * Get OS type definition by OS type string\n *\n * @param osType - OS type string (can be raw or normalized)\n * @returns Complete OS type definition or undefined\n */\nexport function getOSTypeDefinition(osType?: string): OSTypeDefinition | undefined {\n  if (!osType) return undefined;\n\n  const normalized = normalizeOSType(osType);\n  if (!normalized) return undefined;\n\n  return OS_TYPES.find(t => t.id === normalized);\n}\n\n/**\n * Get platform ID for an OS type (for cross-referencing with OS_PLATFORMS)\n *\n * @param osType - OS type string (can be raw or normalized)\n * @returns Platform ID or undefined\n */\nexport function getOSPlatformId(osType?: string): OSPlatformId | undefined {\n  const osTypeDef = getOSTypeDefinition(osType);\n  return osTypeDef?.platformId;\n}\n\n/**\n * Check if device OS matches a specific platform\n *\n * @param deviceOS - Device OS string\n * @param targetPlatform - Target platform to check against\n * @returns True if OS matches platform\n *\n * @example\n * isOSPlatform('Darwin', 'darwin') // true\n * isOSPlatform('Windows 10', 'windows') // true\n * isOSPlatform('Ubuntu', 'linux') // true\n */\nexport function isOSPlatform(deviceOS?: string, targetPlatform?: OSPlatformId): boolean {\n  if (!deviceOS || !targetPlatform) return false;\n\n  const platformId = getOSPlatformId(deviceOS);\n  return platformId === targetPlatform;\n}\n","import { getCountries, getCountryCallingCode, isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';\nimport type { CountryCode } from 'libphonenumber-js';\n\nexport interface CountryPhoneData {\n  code: CountryCode;\n  name: string;\n  dialCode: string;\n  flag: string;\n}\n\n/**\n * Priority country codes shown at the top of country selectors\n */\nconst PRIORITY_CODES: CountryCode[] = ['US', 'CA', 'GB', 'AU'];\n\n/**\n * Convert ISO 3166-1 alpha-2 country code to flag emoji\n */\nfunction countryCodeToFlag(code: string): string {\n  return code\n    .toUpperCase()\n    .split('')\n    .map(char => String.fromCodePoint(0x1f1e6 + char.charCodeAt(0) - 65))\n    .join('');\n}\n\n/**\n * Build country phone data from libphonenumber-js metadata + Intl.DisplayNames\n */\nfunction buildCountryData(): { priority: CountryPhoneData[]; others: CountryPhoneData[] } {\n  const displayNames = new Intl.DisplayNames(['en'], { type: 'region' });\n  const allCodes = getCountries();\n\n  const toData = (code: CountryCode): CountryPhoneData => ({\n    code,\n    name: displayNames.of(code) || code,\n    dialCode: `+${getCountryCallingCode(code)}`,\n    flag: countryCodeToFlag(code),\n  });\n\n  const priority = PRIORITY_CODES.map(toData);\n\n  const prioritySet = new Set(PRIORITY_CODES);\n  const others = allCodes\n    .filter(c => !prioritySet.has(c))\n    .map(toData)\n    .sort((a, b) => a.name.localeCompare(b.name));\n\n  return { priority, others };\n}\n\nlet _cache: { priority: CountryPhoneData[]; others: CountryPhoneData[] } | null = null;\n\n/**\n * Get all countries split into priority (US, CA, GB, AU) and the rest alphabetically\n */\nexport function getCountryPhoneData() {\n  if (!_cache) {\n    _cache = buildCountryData();\n  }\n  return _cache;\n}\n\n/**\n * Find country data by ISO code\n */\nexport function getCountryByCode(code: CountryCode): CountryPhoneData | undefined {\n  const { priority, others } = getCountryPhoneData();\n  return priority.find(c => c.code === code) || others.find(c => c.code === code);\n}\n\n/**\n * Validate a phone number for a specific country using libphonenumber\n */\nexport function validatePhoneNumber(phoneNumber: string, countryCode: CountryCode): boolean {\n  if (!phoneNumber.trim()) return true; // optional field\n  return isValidPhoneNumber(phoneNumber, countryCode);\n}\n\n/**\n * Format a phone number to E.164 format (e.g., +14155552671)\n */\nexport function formatPhoneE164(phoneNumber: string, countryCode: CountryCode): string {\n  try {\n    const parsed = parsePhoneNumber(phoneNumber, countryCode);\n    if (parsed && parsed.isValid()) {\n      return parsed.format('E.164');\n    }\n  } catch {\n    // fall through to manual formatting\n  }\n  const dialCode = `+${getCountryCallingCode(countryCode)}`;\n  const digits = phoneNumber.replace(/\\D/g, '');\n  return `${dialCode}${digits}`;\n}\n","/**\n * Generic Domain Detection\n * Detects personal/generic email providers (gmail, yahoo, etc.)\n */\n\nexport const GENERIC_EMAIL_DOMAINS = [\n  // Major providers\n  'gmail.com',\n  'yahoo.com',\n  'hotmail.com',\n  'outlook.com',\n  'live.com',\n  'msn.com',\n  // Apple\n  'icloud.com',\n  'me.com',\n  'mac.com',\n  // Other providers\n  'aol.com',\n  'protonmail.com',\n  'mail.com',\n  'yandex.com',\n  'zoho.com',\n  'gmx.com',\n  'fastmail.com',\n  // ISP-based\n  'comcast.net',\n  'verizon.net',\n  'att.net',\n  'sbcglobal.net',\n  'bellsouth.net',\n  'cox.net',\n  // International\n  'qq.com',\n  '163.com',\n  '126.com',\n  'web.de',\n  't-online.de',\n] as const;\n\nexport type GenericEmailDomain = (typeof GENERIC_EMAIL_DOMAINS)[number];\n\nexport function extractDomainFromEmail(email: string): string | null {\n  if (!email || !email.includes('@')) return null;\n  return email.split('@')[1]?.toLowerCase() || null;\n}\n\nexport function normalizeDomain(domain: string): string {\n  return (\n    domain\n      ?.toLowerCase()\n      .replace(/^https?:\\/\\//, '')\n      .replace(/^www\\./, '')\n      .split('/')[0]\n      .trim() || ''\n  );\n}\n\nexport function isGenericDomain(domain: string): boolean {\n  const normalized = normalizeDomain(domain);\n  return GENERIC_EMAIL_DOMAINS.includes(normalized as GenericEmailDomain);\n}\n\nexport function hasGenericEmailDomain(email: string): boolean {\n  const domain = extractDomainFromEmail(email);\n  return domain ? isGenericDomain(domain) : false;\n}\n\nexport function isGenericWebsiteDomain(website: string): boolean {\n  return isGenericDomain(normalizeDomain(website));\n}\n","/**\n * Dismissal-cookie primitives — THE one implementation of \"this surface was\n * dismissed\", shared by every dismissible surface (announcement bar, floating\n * walkthrough video, and anything added later).\n *\n * The dismissal rule is an ID MATCH, never mere cookie presence: the store\n * holds the id of the LAST dismissed item, so publishing a NEW item re-shows\n * the surface while the old dismissal still suppresses the old item.\n *\n * TWO TIERS. The cookie is primary wherever cookies work; on a cookie-averse\n * document `localStorage` takes over under the same key. See `canUseCookies`\n * for when that is, and `fallbackStore` for why it is a fallback and not a\n * mirror.\n *\n * No `\"use client\"` on purpose — `isDismissedCookieValue` is pure and is used by\n * server code (the hub's root layout reads the cookie during SSR to avoid a\n * flash); the DOM-touching helpers guard on `typeof document`.\n */\n\nimport { createLocalStorageAdapter } from './local-storage-adapter';\n\n/** One year — the COOKIE tier's horizon. The fallback tier has no expiry, so a\n *  dismissal in a native shell is permanent; adding an expiry envelope there\n *  would be inventing a second encoding to answer a question nobody has asked. */\nconst DISMISS_COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/**\n * Whether `document.cookie` actually stores anything here.\n *\n * A Document whose URL scheme is not http(s) is COOKIE-AVERSE — the HTML spec's\n * term for one where the `document.cookie` setter is a silent no-op and the\n * getter returns ''. That is where the custom-scheme shells run: Tauri serves\n * the bundle from `tauri://localhost` on macOS/Linux, Capacitor from\n * `capacitor://localhost` on iOS. Every dismissal there was dropped on write\n * and unreadable on the next mount, so the surface came back in every new\n * window and on every launch. Tauri on Windows and Capacitor on Android serve\n * http(s), were never affected, and stay on the cookie tier.\n *\n * The scheme is read directly rather than probed with a throwaway write: a lib\n * whose consumers are consent-regulated marketing sites should not set a\n * cookie, however briefly, to answer a question the URL already answers.\n * Deliberately NOT covered: cookies blocked by user or enterprise policy on an\n * http(s) origin — the surface reappears there exactly as it always has, which\n * is what someone who blocked cookies asked for.\n *\n * Read off `document.URL`, not `window.location`: averseness is a property of\n * the DOCUMENT's URL, and this file must stay usable wherever `document` is the\n * only thing the other helpers here already require. `about:blank` and\n * `about:srcdoc` are the one miss: they carry their creator's origin and are\n * served its cookies, yet land on the fallback tier here. Accepted — nothing\n * hosts this lib in one, and the cost is a re-show, not a broken surface.\n */\nfunction canUseCookies(): boolean {\n  if (typeof document === 'undefined') return false;\n  return document.URL.startsWith('http:') || document.URL.startsWith('https:');\n}\n\n/**\n * The fallback tier for cookie-averse documents, keyed identically to the\n * cookie so a host needs no second key.\n *\n * A FALLBACK, never a mirror: cookies stay primary wherever they work, because\n * they are the only tier a server can read — which is what lets the hub\n * suppress a dismissed surface during SSR instead of flashing it — and writing\n * both would mean clearing cookies no longer clears a dismissal.\n */\nfunction fallbackStore(name: string) {\n  return createLocalStorageAdapter<string>({\n    key: name,\n    logTag: '[dismiss-cookie]',\n    validate: (parsed): parsed is string => typeof parsed === 'string',\n  });\n}\n\n/** THE dismissal match rule. `undefined` id (nothing active) → not dismissed. */\nexport function isDismissedCookieValue(cookieValue: string | undefined, id: string | undefined): boolean {\n  return !!id && cookieValue === id;\n}\n\n/**\n * Read a dismissal. Returns `undefined` when absent OR unreadable — one\n * guarantee with two causes: a cookie with bad percent-encoding, where\n * `decodeURIComponent` throws `URIError`, and a fallback value that is not the\n * JSON string it should be, where the adapter swallows the parse failure.\n * Callers run this inside reveal paths where an uncaught throw would hide the\n * surface permanently, so failing open — treated as not-dismissed — is always\n * the safe side.\n */\nexport function readDismissCookie(name: string): string | undefined {\n  if (typeof document === 'undefined') return undefined;\n  if (!canUseCookies()) return fallbackStore(name).load() ?? undefined;\n  const match = document.cookie.split('; ').find(row => row.startsWith(`${name}=`));\n  if (!match) return undefined;\n  try {\n    return decodeURIComponent(match.slice(name.length + 1));\n  } catch {\n    return undefined;\n  }\n}\n\n/** Persist a dismissal. */\nexport function writeDismissCookie(name: string, id: string): void {\n  if (typeof document === 'undefined') return;\n  if (!canUseCookies()) {\n    fallbackStore(name).save(id);\n    return;\n  }\n  document.cookie = `${name}=${encodeURIComponent(id)}; path=/; max-age=${DISMISS_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\n/**\n * Clear a dismissal (test/story helper).\n *\n * BOTH tiers, unconditionally — the tier check governs writes, not removal.\n * Clearing writes nothing and cannot resurrect a dismissal, so it carries none\n * of the reasons the write path commits to one tier, and a clear that leaves\n * the other tier holding a value is a lie.\n *\n * NOT for a shell that changes the scheme it serves under: that is an origin\n * change, so the old tier moves to a storage bucket no code in the new origin\n * can reach — `clear` included.\n */\nexport function clearDismissCookie(name: string): void {\n  if (typeof document === 'undefined') return;\n  fallbackStore(name).clear();\n  document.cookie = `${name}=; path=/; max-age=0`;\n}\n","/**\n * Announcement dismissal store.\n *\n * The dismissal COOKIE is the source of truth wherever cookies work: the hub's\n * root layout reads it server-side to decide whether to SSR-seed the bar (no\n * flash for dismissed users), and the client reads it via\n * `isAnnouncementDismissed`. One cookie per platform holding the LAST dismissed\n * announcement id — dismissal is an id-match, never mere cookie presence, so a\n * new announcement always shows after an old one was dismissed, and\n * re-activating an old announcement intentionally re-shows it.\n *\n * On a cookie-averse document (see `utils/dismiss-cookie.ts`) the primitive\n * writes `<platform>-announcement-dismissed` to localStorage instead. Do not\n * mistake it for the legacy key below and delete the write: it is the ONLY\n * thing making a dismissal persist in a shell. `clearAnnouncementDismissals`' prefix sweep does match it, which\n * is right — a full reset should take both.\n *\n * The legacy per-id localStorage key (`<platform>-announcement-<id>-dismissed`)\n * is READ-ONLY legacy: consulted only when no cookie exists for the platform,\n * purely as the migration trigger (the bar's mount effect backfills the\n * cookie). Nothing writes it any more, so that store dies out.\n *\n * No `\"use client\"` directive on purpose — `announcementDismissCookieName` and\n * `isDismissedCookieValue` are pure/isomorphic and imported by the hub's\n * server layout; the DOM-touching helpers guard on `typeof document`.\n */\n\nimport { isDismissedCookieValue, readDismissCookie, writeDismissCookie, clearDismissCookie } from './dismiss-cookie';\n\n/** Cookie name for a platform's dismissed-announcement id — the ONE home for\n *  the encoding, shared by the client writer and the hub's SSR reader. */\nexport function announcementDismissCookieName(platform: string): string {\n  return `${platform}-announcement-dismissed`;\n}\n\n/** THE dismissal match rule (id-match, not presence), shared across the\n *  server/client boundary. `undefined` id (no active announcement) → false. */\n// The match rule + cookie IO live in `utils/dismiss-cookie.ts` — shared with\n// every other dismissible surface. Re-exported so existing importers (incl. the\n// hub's SSR layout) keep their import path.\nexport { isDismissedCookieValue } from './dismiss-cookie';\n\nconst legacyDismissKey = (platform: string, id: string) => `${platform}-announcement-${id}-dismissed`;\n\n/** Persist a dismissal (1 year). The SSR layout sees it on the next request —\n *  except in a native shell, where the primitive falls back to localStorage and\n *  there is no SSR layout to see it anyway. The LEGACY per-id key is still never\n *  written. */\nexport function dismissAnnouncement(platform: string, id: string): void {\n  if (typeof document === 'undefined') return;\n  writeDismissCookie(announcementDismissCookieName(platform), id);\n}\n\n/**\n * Client-side dismissal check — primary store first, with the legacy\n * localStorage key consulted ONLY when that comes back empty. Reads browser\n * storage, so callers must invoke it\n * from effects, never during render (a render-time read would desync hydration\n * from the SSR HTML).\n */\nexport function isAnnouncementDismissed(platform: string, id: string): boolean {\n  if (typeof document === 'undefined') return false;\n  const cookieValue = readDismissCookie(announcementDismissCookieName(platform));\n  if (cookieValue !== undefined) return isDismissedCookieValue(cookieValue, id);\n  try {\n    return localStorage.getItem(legacyDismissKey(platform, id)) !== null;\n  } catch {\n    return false;\n  }\n}\n\n/** Remove ALL dismissal state for a platform — test/story helper so callers\n *  never restate the key encoding. The header owns the tier inventory. */\nexport function clearAnnouncementDismissals(platform: string): void {\n  if (typeof document === 'undefined') return;\n  clearDismissCookie(announcementDismissCookieName(platform));\n  try {\n    const prefix = `${platform}-announcement-`;\n    Object.keys(localStorage).forEach(key => {\n      if (key.startsWith(prefix) && key.endsWith('-dismissed')) {\n        localStorage.removeItem(key);\n      }\n    });\n  } catch {\n    // ignore storage errors\n  }\n}\n\n/** One-time cleanup of pre-refactor storage: the orphaned announcement cache\n *  (the old \"instant paint\" blob) — called from the bar's mount effect. */\nexport function clearLegacyAnnouncementCache(platform: string): void {\n  if (typeof window === 'undefined') return;\n  try {\n    localStorage.removeItem(`${platform}-announcement-cache`);\n  } catch {\n    // ignore storage errors\n  }\n}\n","/**\n * Walkthrough-video dismissal — a thin key-naming layer over the shared\n * dismissal-cookie primitives (`utils/dismiss-cookie.ts`). Same id-match\n * semantics as the announcement bar, from the same implementation.\n */\n\nimport { isDismissedCookieValue, readDismissCookie, writeDismissCookie, clearDismissCookie } from './dismiss-cookie';\n\n/** Cookie-name stem. */\nexport const WALKTHROUGH_VIDEO_DISMISS_KEY = 'walkthrough-video-dismissed';\n\n/** THE per-platform cookie name — the ONE home for the encoding. Shaped exactly\n *  like `announcementDismissCookieName` (`<platform>-<name>-dismissed`): `:` is\n *  not a `token` character under RFC 6265, so the old `…-dismissed:<platform>`\n *  form was only working on browser leniency. Hosts must not rebuild this\n *  inline, or an SSR reader added later will restate the separator.\n *\n *  Renaming orphans existing cookies, so anyone who had dismissed the card sees\n *  it once more. Deliberate and one-time: the alternative is reading both names\n *  forever to save a single re-dismissal. */\nexport function walkthroughDismissCookieName(platform: string): string {\n  return `${platform}-${WALKTHROUGH_VIDEO_DISMISS_KEY}`;\n}\n\n/** Client-side dismissal check (id-match). Reads browser storage, so call it\n *  from an effect, never during render. */\nexport function isWalkthroughDismissed(key: string, id: string | undefined): boolean {\n  return isDismissedCookieValue(readDismissCookie(key), id);\n}\n\n/** Persist a dismissal for this video id. */\nexport function dismissWalkthrough(key: string, id: string): void {\n  writeDismissCookie(key, id);\n}\n\n/** Clear a platform key's dismissal (test/story helper). */\nexport function clearWalkthroughDismissal(key: string): void {\n  clearDismissCookie(key);\n}\n","/**\n * Walkthrough-video deep link — the ONE home for the query param that opens\n * the floating walkthrough widget's theater full-screen (paused) on page\n * load. Consumed by the widget itself (`FloatingWalkthroughVideo` defaults\n * its `deepLinkParam` prop to this) and by hosts that MINT deep-link URLs\n * (invitation emails) or carry the param across redirects (the hub's\n * auth-wall proxy). Lives beside `dismissal-storage.ts` — the same\n * widget-adjacent naming-layer altitude.\n */\n\n/** `?walkthrough=1` — presence-based on the read side (any value counts);\n *  `=1` is just the canonical value URL minters write. */\nexport const WALKTHROUGH_OPEN_QUERY_PARAM = 'walkthrough';\n\n/** Append the theater deep-link param to a URL (absolute or relative). */\nexport function withWalkthroughOpenParam(url: string): string {\n  return `${url}${url.includes('?') ? '&' : '?'}${WALKTHROUGH_OPEN_QUERY_PARAM}=1`;\n}\n","/**\n * Fetch-based SSE subscription primitive.\n *\n * Why not `EventSource`: cross-origin embedders authenticate via the\n * `embedAuthedFetch` adapter's HEADERS (OpenFrame bearer), which\n * `EventSource` cannot carry. This reads `response.body` from a normal\n * authed fetch and parses standard `event:`/`data:` frames.\n *\n * Lifecycle (never-terminating by design):\n *   - Infinite reconnects with capped exponential backoff (~30s max +\n *     jitter), reset on a successful open. Do NOT copy the terminating\n *     `maxRetries → exhausted` model of the hub's server-side\n *     `RealtimeRetryManager` — a long outage must self-heal when the\n *     backend returns, because there is NO polling fallback behind this.\n *   - Liveness by silence: the server keepalives every ~15s; any bytes\n *     (INCLUDING `: keepalive` comment lines — they reset the timer\n *     before frame parsing drops them) count as life. Silence beyond\n *     `silenceTimeoutMs` (default 45s = 3× keepalive) aborts + reconnects.\n *   - Terminal responses (NO retry): any 4xx except 408/429 — the\n *     `x-block-layer` header, when readable, is logged for attribution\n *     only, never used as a retry gate (proxies may strip it); and\n *     204 → a distinct `no-stream` status (the caller decides when to\n *     try again). Backoff-retry is for transport errors, 408/429, and 5xx.\n *\n * THE common SSE client for lib + hosts — exported from the `utils`\n * barrel. Consumers: `TicketLiveProvider` (lib) and the hub's\n * workflow/invocation stream hooks (which replaced the hub's old\n * EventSource-based `sse-client.ts`). Finite streams (workflows,\n * invocations) end with a terminal event and a server-side close —\n * their handlers MUST call `close()` on the terminal event, or the\n * never-terminating reconnect loop re-opens the finished stream.\n */\n\nimport { embedAuthedFetch } from './embed-authed-fetch';\n\n/** Transport-level status. `suspended` = paused by `pauseWhenHidden`\n *  after the hidden grace elapsed (resumes automatically on visible). */\nexport type SseTransportStatus =\n  'connecting' | 'open' | 'reconnecting' | 'no-stream' | 'terminal' | 'suspended' | 'closed';\n\nexport interface SseSubscriptionOptions {\n  url: string;\n  /** Defaults to `embedAuthedFetch` (adapter headers + credentials). */\n  fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;\n  /** Called per parsed frame: `eventName` from `event:` (default\n   *  'message'), `data` JSON-parsed when possible (raw string otherwise).\n   *  Server `status` frames are ALSO forwarded here (after the client's\n   *  own lifecycle handling) for consumers that render health detail. */\n  onEvent: (eventName: string, data: unknown) => void;\n  onStatusChange?: (status: SseTransportStatus) => void;\n  /**\n   * Consolidated liveness signal — true only when the SERVER confirmed\n   * its Realtime subscription (`status: subscribed` frame), never merely\n   * \"HTTP open\". THE CLIENT owns the whole lifecycle policy:\n   *   - transport open without `subscribed` within 15s → hard reconnect;\n   *   - server `retrying` → connected=false, give the server 90s to\n   *     recover before a client hard reconnect;\n   *   - server `reconnect_failed` → connected=false, hard reconnect at\n   *     the capped 30s delay (no stampedes);\n   *   - any transport drop → connected=false (reconnect via backoff).\n   * Fired only on transitions.\n   */\n  onConnectedChange?: (connected: boolean) => void;\n  /**\n   * Pause the subscription while the tab is hidden (45s grace so\n   * Cmd-Tab thrash doesn't churn connections), resume + reconnect on\n   * visible, and reconnect on `online`. OPT-IN — short-lived streams\n   * (workflow/invocation monitors) must keep running in background\n   * tabs. Long-lived per-user streams should enable it (scale relief:\n   * a hidden tab holds no server invocation).\n   */\n  pauseWhenHidden?: boolean;\n  /** Silence threshold before the connection is presumed dead. MUST be\n   *  ≥2.5× the server keepalive cadence or jitter causes false-disconnect\n   *  churn (each one costs a full server invocation). */\n  silenceTimeoutMs?: number;\n  maxBackoffMs?: number;\n  initialBackoffMs?: number;\n}\n\nexport interface SseSubscription {\n  /** Permanently stop — no further reconnects, no callbacks. */\n  close: () => void;\n  /** Drop the current connection (if any) and reconnect immediately,\n   *  resetting backoff. No-op after `close()`. */\n  reconnectNow: () => void;\n}\n\nconst DEFAULT_SILENCE_TIMEOUT_MS = 45_000;\nconst DEFAULT_MAX_BACKOFF_MS = 30_000;\nconst DEFAULT_INITIAL_BACKOFF_MS = 1_000;\n/** No server `subscribed` frame within this window after transport-open\n *  → the subscription is presumed dead behind a live HTTP stream. */\nconst SUBSCRIBE_CONFIRM_TIMEOUT_MS = 15_000;\n/** Server `retrying` = it is recovering its own Realtime channel\n *  (exponential backoff, ~5 attempts). Grace before the client gives up\n *  waiting and hard-reconnects. */\nconst SERVER_RETRY_GRACE_MS = 90_000;\n/** After a server `reconnect_failed`, the next client attempt starts at\n *  this capped delay — each reconnect costs a full server invocation +\n *  the server's own Realtime retries; an outage must not stampede. */\nconst FAILED_RECONNECT_DELAY_MS = 30_000;\n/** Grace before a hidden tab's subscription is suspended. */\nconst HIDDEN_GRACE_MS = 45_000;\n\nexport function createSseSubscription(options: SseSubscriptionOptions): SseSubscription {\n  const {\n    url,\n    fetchImpl = embedAuthedFetch,\n    onEvent,\n    onStatusChange,\n    onConnectedChange,\n    pauseWhenHidden = false,\n    silenceTimeoutMs = DEFAULT_SILENCE_TIMEOUT_MS,\n    maxBackoffMs = DEFAULT_MAX_BACKOFF_MS,\n    initialBackoffMs = DEFAULT_INITIAL_BACKOFF_MS,\n  } = options;\n\n  let closed = false;\n  let suspended = false;\n  let attempt = 0;\n  /** Connection generation — bumped by each connect() and by\n   *  reconnectNow(). A stale loop (aborted, still unwinding) compares\n   *  its captured generation before scheduling a reconnect, so an\n   *  explicit reconnect can never race a zombie loop into two\n   *  concurrent connections. */\n  let generation = 0;\n  let abortController: AbortController | null = null;\n  let retryTimerId: ReturnType<typeof setTimeout> | null = null;\n  let silenceTimerId: ReturnType<typeof setTimeout> | null = null;\n  // Lifecycle timers — the client owns the WHOLE connected-state policy.\n  let confirmTimerId: ReturnType<typeof setTimeout> | null = null;\n  let serverGraceTimerId: ReturnType<typeof setTimeout> | null = null;\n  let failedDelayTimerId: ReturnType<typeof setTimeout> | null = null;\n  let hiddenGraceTimerId: ReturnType<typeof setTimeout> | null = null;\n\n  let connected = false;\n  const setConnected = (next: boolean) => {\n    if (connected === next) return;\n    connected = next;\n    try {\n      onConnectedChange?.(next);\n    } catch (err) {\n      console.error('[sse-subscription] onConnectedChange threw:', err);\n    }\n  };\n\n  const clearLifecycleTimer = (id: ReturnType<typeof setTimeout> | null) => {\n    if (id) clearTimeout(id);\n    return null;\n  };\n\n  const setStatus = (status: SseTransportStatus) => {\n    if (closed && status !== 'closed') return;\n    // ANY transport transition means the server subscription is not\n    // (or not yet) confirmed — `subscribed` frames re-assert true.\n    setConnected(false);\n    serverGraceTimerId = clearLifecycleTimer(serverGraceTimerId);\n    if (status !== 'open') {\n      confirmTimerId = clearLifecycleTimer(confirmTimerId);\n    }\n    onStatusChange?.(status);\n  };\n\n  /** Hard reconnect, optionally delayed — used by the lifecycle policy\n   *  (confirm timeout / server give-up / failed-reconnect pacing). */\n  const internalReconnect = (delayMs: number) => {\n    failedDelayTimerId = clearLifecycleTimer(failedDelayTimerId);\n    if (delayMs <= 0) {\n      doReconnect();\n      return;\n    }\n    failedDelayTimerId = setTimeout(() => {\n      failedDelayTimerId = null;\n      doReconnect();\n    }, delayMs);\n  };\n\n  /** Interpret server `status` frames — the server-side Realtime\n   *  subscription health channel emitted by the hub's SSE engine. */\n  const handleServerStatus = (data: unknown) => {\n    const status = (data as { status?: string } | null)?.status;\n    if (status === 'subscribed') {\n      confirmTimerId = clearLifecycleTimer(confirmTimerId);\n      serverGraceTimerId = clearLifecycleTimer(serverGraceTimerId);\n      setConnected(true);\n      return;\n    }\n    if (status === 'retrying') {\n      // Server is recovering its own channel — wait (bounded) before a\n      // client hard reconnect stampedes it. Disarm the subscribe-confirm\n      // timer: a pre-`subscribed` retry would otherwise fire the 15s hard\n      // reconnect and the grace window would never apply.\n      confirmTimerId = clearLifecycleTimer(confirmTimerId);\n      setConnected(false);\n      if (!serverGraceTimerId) {\n        serverGraceTimerId = setTimeout(() => {\n          serverGraceTimerId = null;\n          doReconnect();\n        }, SERVER_RETRY_GRACE_MS);\n      }\n      return;\n    }\n    if (status === 'reconnect_failed') {\n      // Same disarm as `retrying` — a still-armed confirm timer would\n      // preempt the deliberate 30s failed-reconnect delay.\n      confirmTimerId = clearLifecycleTimer(confirmTimerId);\n      setConnected(false);\n      serverGraceTimerId = clearLifecycleTimer(serverGraceTimerId);\n      internalReconnect(FAILED_RECONNECT_DELAY_MS);\n    }\n  };\n\n  const clearTimers = () => {\n    if (retryTimerId) {\n      clearTimeout(retryTimerId);\n      retryTimerId = null;\n    }\n    if (silenceTimerId) {\n      clearTimeout(silenceTimerId);\n      silenceTimerId = null;\n    }\n    confirmTimerId = clearLifecycleTimer(confirmTimerId);\n    serverGraceTimerId = clearLifecycleTimer(serverGraceTimerId);\n    failedDelayTimerId = clearLifecycleTimer(failedDelayTimerId);\n    hiddenGraceTimerId = clearLifecycleTimer(hiddenGraceTimerId);\n  };\n\n  const armSilenceTimer = () => {\n    if (silenceTimerId) clearTimeout(silenceTimerId);\n    silenceTimerId = setTimeout(() => {\n      // Dead-air: the read loop is stuck on a connection that will never\n      // produce again. Abort → the loop's catch schedules a reconnect.\n      abortController?.abort();\n    }, silenceTimeoutMs);\n  };\n\n  const scheduleReconnect = () => {\n    if (closed || suspended || retryTimerId) return;\n    setStatus('reconnecting');\n    const backoff = Math.min(maxBackoffMs, initialBackoffMs * 2 ** attempt);\n    const jitter = backoff * 0.25 * Math.random();\n    attempt += 1;\n    retryTimerId = setTimeout(() => {\n      retryTimerId = null;\n      void connect();\n    }, backoff + jitter);\n  };\n\n  const parseFrame = (rawFrame: string) => {\n    let eventName = 'message';\n    const dataLines: string[] = [];\n    for (const line of rawFrame.split('\\n')) {\n      if (line.startsWith(':')) continue; // comment (keepalive) — liveness only\n      if (line.startsWith('event:')) {\n        eventName = line.slice(6).trim() || 'message';\n      } else if (line.startsWith('data:')) {\n        dataLines.push(line.slice(5).trimStart());\n      }\n    }\n    if (dataLines.length === 0) return;\n    const rawData = dataLines.join('\\n');\n    let data: unknown = rawData;\n    try {\n      data = JSON.parse(rawData);\n    } catch {\n      // Non-JSON data frame — deliver the raw string.\n    }\n    // Lifecycle FIRST (server `status` frames drive `connected`), then\n    // forward every frame — status included — to the domain handler.\n    if (eventName === 'status') {\n      handleServerStatus(data);\n    }\n    try {\n      onEvent(eventName, data);\n    } catch (err) {\n      console.error('[sse-subscription] onEvent handler threw:', err);\n    }\n  };\n\n  // A function DECLARATION, not a `const` arrow: `connect` and\n  // `scheduleReconnect` call each other, so whichever one is written second\n  // is a forward reference no reordering can remove. Declarations hoist, so\n  // the cycle is expressible without one.\n  async function connect(): Promise<void> {\n    if (closed || suspended) return;\n    const gen = ++generation;\n    setStatus('connecting');\n    abortController = new AbortController();\n\n    let response: Response;\n    try {\n      response = await fetchImpl(url, {\n        method: 'GET',\n        // Explicit Accept — embedAuthedFetch otherwise injects\n        // `Content-Type: application/json` defaults meant for POSTs.\n        headers: { Accept: 'text/event-stream' },\n        signal: abortController.signal,\n      });\n    } catch {\n      // Network error / abort — transport-level, retryable.\n      if (!closed && gen === generation) scheduleReconnect();\n      return;\n    }\n    if (closed || gen !== generation) return;\n\n    if (response.status === 204) {\n      // Contract: nothing to stream for this identity. Caller decides\n      // when to try again — no retry loop here.\n      setStatus('no-stream');\n      return;\n    }\n    if (!response.ok) {\n      const retryable = response.status === 408 || response.status === 429 || response.status >= 500;\n      if (retryable) {\n        scheduleReconnect();\n        return;\n      }\n      // Terminal 4xx. x-block-layer (proxy surface block) is logged for\n      // attribution when readable — never gates the decision.\n      const blockLayer = response.headers.get('x-block-layer');\n      console.warn(\n        `[sse-subscription] terminal ${response.status} for ${url}${blockLayer ? ` (x-block-layer: ${blockLayer})` : ''} — not retrying`,\n      );\n      setStatus('terminal');\n      return;\n    }\n    if (!response.body) {\n      scheduleReconnect();\n      return;\n    }\n\n    // Successful open — reset backoff, start liveness accounting.\n    attempt = 0;\n    setStatus('open');\n    armSilenceTimer();\n    // Transport open ≠ connected: require the server's `subscribed`\n    // status frame within the confirm window, else the subscription is\n    // presumed dead behind a live HTTP stream → hard reconnect.\n    confirmTimerId = clearLifecycleTimer(confirmTimerId);\n    confirmTimerId = setTimeout(() => {\n      confirmTimerId = null;\n      if (!connected) doReconnect();\n    }, SUBSCRIBE_CONFIRM_TIMEOUT_MS);\n\n    const reader = response.body.getReader();\n    const decoder = new TextDecoder();\n    let buffer = '';\n    try {\n      for (;;) {\n        const { done, value } = await reader.read();\n        if (closed || gen !== generation) return;\n        if (done) break;\n        // ANY bytes are liveness — including comment keepalives, which\n        // the frame parser below will drop.\n        armSilenceTimer();\n        buffer += decoder.decode(value, { stream: true });\n        // SSE line terminators may be CRLF, LF, or bare CR per spec.\n        // Normalize to LF before boundary scanning — a CRLF stream would\n        // otherwise never match '\\n\\n' and frames would pile up unparsed.\n        // A trailing CR is held back one iteration: it may be the first\n        // half of a CRLF split across reads (normalization is idempotent\n        // on the already-normalized remainder).\n        let pendingCr = '';\n        if (buffer.endsWith('\\r')) {\n          pendingCr = '\\r';\n          buffer = buffer.slice(0, -1);\n        }\n        buffer = buffer.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n        // Frames are separated by a blank line (\\n\\n).\n        for (;;) {\n          const boundary = buffer.indexOf('\\n\\n');\n          if (boundary === -1) break;\n          const rawFrame = buffer.slice(0, boundary);\n          buffer = buffer.slice(boundary + 2);\n          parseFrame(rawFrame);\n        }\n        buffer += pendingCr;\n      }\n    } catch {\n      // Aborted (silence timer / reconnectNow) or stream error — fall\n      // through to reconnect.\n    } finally {\n      if (silenceTimerId) {\n        clearTimeout(silenceTimerId);\n        silenceTimerId = null;\n      }\n      try {\n        reader.releaseLock();\n      } catch {\n        // Already released.\n      }\n    }\n\n    // Server closed the stream (e.g. Vercel maxDuration) — reconnect.\n    if (!closed && gen === generation) scheduleReconnect();\n  }\n\n  /** Hard reconnect core — invalidate the current loop BEFORE aborting\n   *  so its unwinding never schedules a competing (backoff) reconnect. */\n  function doReconnect() {\n    if (closed || suspended) return;\n    generation += 1;\n    clearTimers();\n    attempt = 0;\n    abortController?.abort();\n    void connect();\n  }\n\n  // ---- pauseWhenHidden: suspend/resume with the tab, reconnect on\n  // network return. Registered here (not in callers) — this is client\n  // lifecycle policy, not domain logic.\n  const suspend = () => {\n    if (closed || suspended) return;\n    suspended = true;\n    generation += 1;\n    clearTimers();\n    abortController?.abort();\n    setStatus('suspended');\n  };\n  const onVisibilityChange = () => {\n    if (document.visibilityState === 'hidden') {\n      if (!hiddenGraceTimerId) {\n        hiddenGraceTimerId = setTimeout(() => {\n          hiddenGraceTimerId = null;\n          // Scale relief: a hidden tab holds no server invocation.\n          suspend();\n        }, HIDDEN_GRACE_MS);\n      }\n      return;\n    }\n    // Visible again — cancel the grace, or resume if already suspended\n    // (the fresh connect re-delivers server-pushed state).\n    hiddenGraceTimerId = clearLifecycleTimer(hiddenGraceTimerId);\n    if (suspended) {\n      suspended = false;\n      void connect();\n    }\n  };\n  const onOnline = () => {\n    if (!suspended) doReconnect();\n  };\n  const listenersActive = pauseWhenHidden && typeof document !== 'undefined' && typeof window !== 'undefined';\n  if (listenersActive) {\n    document.addEventListener('visibilitychange', onVisibilityChange);\n    window.addEventListener('online', onOnline);\n  }\n\n  void connect();\n\n  return {\n    close: () => {\n      if (closed) return;\n      closed = true;\n      generation += 1;\n      clearTimers();\n      abortController?.abort();\n      if (listenersActive) {\n        document.removeEventListener('visibilitychange', onVisibilityChange);\n        window.removeEventListener('online', onOnline);\n      }\n      setConnected(false);\n      onStatusChange?.('closed');\n    },\n    reconnectNow: () => {\n      if (closed) return;\n      suspended = false;\n      doReconnect();\n    },\n  };\n}\n","'use client';\n\n/**\n * Shared decoder for the `decision_resolved` leading frame the\n * `confirm-tool` route emits as the first chunk of its SSE response.\n *\n * Wire format (matches `use-embedded-chat.ts:270-394`):\n *   - Frames are JSON objects separated by `\\0` (NUL).\n *   - `\\x1E` (record separator) marks the transition from leading\n *     metadata frames into the text body (chat path).\n *   - For the tickets-UI path, the request body carries `messages: []`\n *     which makes phase-2 (text body) structurally unreachable —\n *     `confirm-tool/route.ts:569` skips the `runDocsChat` call. So the\n *     stream contains exactly ONE frame (`decision_resolved`) followed\n *     by EOF.\n *\n * This helper drains the stream defensively (reads all remaining bytes\n * after the leading frame so the server-side `composed.start()` doesn't\n * hang on a half-closed connection) and returns the first decoded\n * `decision_resolved` frame. Throws on:\n *   - Empty body\n *   - First frame is not `decision_resolved`\n *   - Malformed JSON before the first `\\0`\n *\n * Migration note: `use-embedded-chat.ts` still inlines the full decoder\n * including phase-2 text + trailing usage parsing. A future refactor\n * could move the leading-frame loop here and have the chat shell\n * compose with that — out of scope for this PR.\n */\n\nexport interface DecisionResolvedFrame {\n  kind: 'decision_resolved';\n  ok: boolean;\n  action: 'approved' | 'rejected';\n  toolName?: string;\n  willAutoContinue?: boolean;\n  proposalId?: string;\n  /** Approve-path result envelope. `mirror_synced=false` means the\n   *  HubSpot REST call succeeded but the local mirror upsert lagged —\n   *  callers should optimistic-render and schedule a delayed refetch. */\n  result?: {\n    ticket_id?: string;\n    status?: string | null;\n    mirror_synced?: boolean;\n  } | null;\n  receiptText?: string;\n}\n\n/**\n * Read the leading `decision_resolved` frame from a confirm-tool SSE\n * response. Drains the stream to end-of-file before resolving so the\n * server doesn't sit on a half-closed socket.\n *\n * @throws {Error} when the body is empty, the first frame is not\n *   `decision_resolved`, or the leading JSON is malformed.\n */\nexport async function readLeadingDecisionFrame(response: Response): Promise<DecisionResolvedFrame> {\n  const reader = response.body?.getReader();\n  if (!reader) throw new Error('readLeadingDecisionFrame: response has no body');\n\n  const decoder = new TextDecoder();\n  let buffer = '';\n  let frame: DecisionResolvedFrame | null = null;\n\n  try {\n    while (true) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      buffer += decoder.decode(value, { stream: true });\n\n      // Try to extract the leading frame ASAP — we only need the first\n      // chunk that contains a `\\0`. Everything after is either phase-2\n      // text (unexpected for tickets-UI but tolerated) or end-of-stream.\n      if (frame === null) {\n        const nullIdx = buffer.indexOf('\\0');\n        const recIdx = buffer.indexOf('\\x1E');\n        // If `\\x1E` (text-body marker) shows up before `\\0`, the server\n        // emitted phase-2 without a leading frame — protocol violation.\n        if (recIdx !== -1 && (nullIdx === -1 || recIdx < nullIdx)) {\n          throw new Error('readLeadingDecisionFrame: text-body sentinel arrived before leading frame');\n        }\n        if (nullIdx !== -1) {\n          const raw = buffer.slice(0, nullIdx);\n          let parsed: unknown;\n          try {\n            parsed = JSON.parse(raw);\n          } catch (err) {\n            throw new Error(`readLeadingDecisionFrame: leading JSON parse failed: ${(err as Error).message}`);\n          }\n          const obj = parsed as Record<string, unknown>;\n          if (obj?.kind !== 'decision_resolved') {\n            throw new Error(`readLeadingDecisionFrame: expected decision_resolved, got kind=${String(obj?.kind)}`);\n          }\n          frame = normalizeDecisionFrame(obj);\n          // Continue draining so the server-side stream closes cleanly.\n          buffer = buffer.slice(nullIdx + 1);\n        }\n      }\n      // Past the leading frame: just consume + discard the remainder.\n      // Don't allocate a parser for phase-2 text; the tickets-UI path\n      // is supposed to be a single frame.\n    }\n  } finally {\n    // Release the lock even if we threw mid-loop.\n    try {\n      reader.releaseLock();\n    } catch {\n      // No-op: reader may already be released if the stream ended cleanly.\n    }\n  }\n\n  if (frame === null) {\n    throw new Error('readLeadingDecisionFrame: stream closed before leading frame arrived');\n  }\n  return frame;\n}\n\nfunction normalizeDecisionFrame(obj: Record<string, unknown>): DecisionResolvedFrame {\n  const action = obj.action === 'rejected' ? 'rejected' : 'approved';\n  const result = (obj.result ?? null) as DecisionResolvedFrame['result'];\n  return {\n    kind: 'decision_resolved',\n    ok: obj.ok === true,\n    action,\n    ...(typeof obj.toolName === 'string' ? { toolName: obj.toolName } : {}),\n    ...(obj.willAutoContinue === true ? { willAutoContinue: true } : {}),\n    ...(typeof obj.proposalId === 'string' ? { proposalId: obj.proposalId } : {}),\n    ...(result ? { result } : {}),\n    ...(typeof obj.receiptText === 'string' ? { receiptText: obj.receiptText } : {}),\n  };\n}\n","/**\n * Canonical per-section metadata for the OpenFrame dev-center surfaces\n * (Roadmap / Bug-fixes & Enhancements / Releases / Onboarding Guides).\n *\n * One file co-locates every per-section value the openframe homepage\n * navigator card AND the destination full-page hero both consume —\n * icon, brief navigator description, longer hero paragraph, search\n * config (placeholder + paramKey), filter config (paramKey + options).\n *\n * Adding a 5th section is one edit here and zero edits across the\n * page files / navigator grid.\n *\n * EMBEDDER NOTE — `onboarding`:\n *   The `onboarding` entry is INERT on `hero`/`search`/`filter` because\n *   `/onboarding-guides` is owned by `OnboardingGuidesCatalogView` (a\n *   hub-side full-page view), not the shared `DevSectionView` chrome.\n *   This entry exists ONLY to drive the homepage navigator card. Any\n *   embedder iterating the registry must SKIP `onboarding` if they\n *   don't host the `/onboarding-guides` route — e.g.\n *   `Object.values(OPENFRAME_DEV_SECTIONS).filter(s => s.href !== '/onboarding-guides')`.\n *\n * SERVER-BUNDLE SAFETY:\n *   This module imports lucide-react icon components as JSX values but\n *   never RENDERS them at module load. Lib's tsup config places `utils/`\n *   in the server bundle (no `\"use client\"` banner), which is safe\n *   because component-function references don't execute on import.\n *   Precedent: `src/utils/platform-config.tsx` (also lucide imports,\n *   also server-bundle, also imported by route-page `metadata` exports).\n */\n\nimport { Map as MapIcon, Wrench, Rocket, GraduationCap, LifeBuoy, type LucideIcon } from 'lucide-react';\nimport { releaseStatusOptions } from '../../types';\nimport { DEV_SECTION_PARAM_KEYS, TICKET_OPEN_PARAM } from './dev-section-param-keys';\n\n// Roadmap status options — `as const` preserves readonly tuple typing\n// across the registry boundary.\nexport const ROADMAP_STATUS_OPTIONS = [\n  { value: 'all', label: 'All' },\n  { value: 'completed', label: 'Completed' },\n  { value: 'in_progress', label: 'In Progress' },\n] as const;\n\n// Delivery (ClickUp custom item type) filter options. `Bug` and `Request`\n// are the ClickUp `custom_item_id` labels — 1008 / 1009.\nexport const DELIVERY_TASK_TYPE_OPTIONS = [\n  { value: 'all', label: 'All' },\n  { value: 'Bug', label: 'Bug-fix' },\n  { value: 'Request', label: 'Enhancement' },\n] as const;\n\n// Ticket status filter for the Help Center. Lowercase wire values\n// match what `/api/chat/agent/find-ticket` accepts in `body.status`\n// (the route normalizes to lowercase + allowlists 'open' | 'closed'\n// before threading into `findTicketExecutor`'s `selfStatus` scope).\nexport const TICKET_STATUS_OPTIONS = [\n  { value: 'all', label: 'All' },\n  { value: 'open', label: 'Open' },\n  { value: 'closed', label: 'Closed' },\n] as const;\n\nexport interface OpenframeDevSection {\n  /** Route href the navigator card and any internal cross-link composes. */\n  href: string;\n  icon: LucideIcon;\n  /** Brief copy for the homepage navigator card (≈50 char teaser). */\n  navigator: {\n    title: string;\n    description: string;\n  };\n  /** Longer copy for the destination page hero (≈120-150 char marketing paragraph). */\n  hero: {\n    title: string;\n    description: string;\n  };\n  /** Inline search bar configuration. */\n  search: {\n    /** Placeholder text shown in the search input. */\n    placeholder: string;\n    /** URL search param the input writes on submit and the list reads on fetch. */\n    paramKey: string;\n    /** Companion URL params torn down whenever the search commits EMPTY.\n     *  For params that deep-links set TOGETHER with the search (e.g. the\n     *  tickets `?ticket=<id>&search=<id>` pair from chat cards) — clearing\n     *  the search must clear the whole linked context, not leave a stale\n     *  companion param filtering the view. */\n    clearParamKeys?: readonly string[];\n  } | null;\n  /** Filter pill row configuration. `null` when the section has no filter (e.g. onboarding-guides). */\n  filter: {\n    label: string;\n    /** URL search param the pills write and the list reads. */\n    paramKey: string;\n    /** The option value that maps to \"no filter applied\". When selected,\n     *  the URL param is removed instead of being set. Defaults to the\n     *  first option's value if omitted — explicit field guards against\n     *  brittleness if `options` is later reordered. */\n    defaultValue: string;\n    options: readonly { value: string; label: string }[];\n  } | null;\n}\n\nexport const OPENFRAME_DEV_SECTIONS = {\n  roadmap: {\n    href: '/roadmap',\n    icon: MapIcon,\n    navigator: {\n      title: 'Development Roadmap',\n      description: \"What we're building next — vote on upcoming features.\",\n    },\n    hero: {\n      title: 'Development Roadmap',\n      description:\n        \"See what's in flight, what's planned, and what's up for community vote. The entire OpenFrame roadmap is public.\",\n    },\n    search: { placeholder: 'Search roadmap items...', paramKey: DEV_SECTION_PARAM_KEYS.search },\n    filter: {\n      label: 'Status',\n      paramKey: DEV_SECTION_PARAM_KEYS.status,\n      defaultValue: 'all',\n      options: ROADMAP_STATUS_OPTIONS,\n    },\n  },\n  delivery: {\n    href: '/bug-fixes-and-enhancements',\n    icon: Wrench,\n    navigator: {\n      title: 'Bug-fixes & Enhancements',\n      description: 'Recently shipped fixes and improvements.',\n    },\n    hero: {\n      title: 'Bug-fixes & Enhancements',\n      description:\n        'A running log of fixes and improvements shipping into OpenFrame — recently completed and actively in progress.',\n    },\n    search: { placeholder: 'Search tasks...', paramKey: DEV_SECTION_PARAM_KEYS.search },\n    filter: {\n      label: 'Type',\n      paramKey: DEV_SECTION_PARAM_KEYS.deliveryTaskType,\n      defaultValue: 'all',\n      options: DELIVERY_TASK_TYPE_OPTIONS,\n    },\n  },\n  releases: {\n    href: '/releases',\n    icon: Rocket,\n    navigator: {\n      title: 'Product Releases',\n      description: 'Version history and release notes.',\n    },\n    hero: {\n      title: 'Product Releases',\n      description:\n        'Version notes, change summaries, and stability tier (alpha / beta / stable) for every OpenFrame release.',\n    },\n    search: { placeholder: 'Search releases...', paramKey: DEV_SECTION_PARAM_KEYS.search },\n    filter: {\n      label: 'Status',\n      paramKey: DEV_SECTION_PARAM_KEYS.releaseStatus,\n      defaultValue: 'all',\n      options: releaseStatusOptions,\n    },\n  },\n  onboarding: {\n    href: '/onboarding-guides',\n    icon: GraduationCap,\n    navigator: {\n      title: 'Onboarding Guides',\n      description: 'Step-by-step product walkthroughs.',\n    },\n    hero: {\n      title: 'Getting Started',\n      description:\n        'Step-by-step walkthroughs for getting up and running with OpenFrame — from your first device deployment to advanced integrations.',\n    },\n    // `search` / `filter` are intentionally null — onboarding-guides\n    // uses a custom RAG-backed search dropdown (`useDocSearch`, not the\n    // local text filter `DevSectionView` ships) and dynamic section\n    // pills (counts vary with content, vs the static status options\n    // every other dev-center surface uses). Both controls are injected\n    // by the host via `DevSectionPage`'s `preControls` slot — same\n    // mechanism tickets uses for its \"Open a new ticket\" form.\n    search: null,\n    filter: null,\n  },\n  tickets: {\n    href: '/tickets',\n    icon: LifeBuoy,\n    navigator: {\n      title: 'Help Center',\n      description: 'Open and manage your support tickets.',\n    },\n    hero: {\n      title: 'Help Center',\n      description:\n        'Open new tickets, follow up on existing ones, and track responses from the team — all in one place.',\n    },\n    search: {\n      placeholder: 'Search your tickets...',\n      paramKey: DEV_SECTION_PARAM_KEYS.search,\n      // Chat-card deep links arrive as `?ticket=<id>&search=<id>`\n      // (buildTicketOpenHref) — clearing the search resets the whole\n      // linked context, drawer param included.\n      clearParamKeys: [TICKET_OPEN_PARAM],\n    },\n    filter: {\n      label: 'Status',\n      paramKey: DEV_SECTION_PARAM_KEYS.status,\n      defaultValue: 'all',\n      options: TICKET_STATUS_OPTIONS,\n    },\n  },\n} as const satisfies Record<string, OpenframeDevSection>;\n\nexport type OpenframeDevSectionKey = keyof typeof OPENFRAME_DEV_SECTIONS;\n","import type { EntityPlatformAssoc } from './entity-platform';\n// Announcement system TypeScript types\nimport type { LegacyPlatform, PlatformFilter } from './platform';\n\n// Database Models\nexport interface Announcement {\n  id: string;\n  title: string;\n  description: string;\n  background_color: string;\n  icon_name?: string;\n  icon_url?: string;\n  /** Extra props for the main bar/icon */\n  icon_props?: Record<string, unknown>;\n  /** Hydrated entity_platforms associations (featured-first) — what the hub\n   *  DAL's attachPlatformsToRow(s) emits. No singular platform/platform_id. */\n  announcement_platforms?: EntityPlatformAssoc[];\n  is_active: boolean;\n  // CTA (Call-To-Action) fields\n  cta_enabled?: boolean;\n  cta_text?: string;\n  cta_icon_name?: string;\n  cta_show_icon?: boolean;\n  cta_url?: string;\n  cta_target?: '_self' | '_blank';\n  // Custom CTA button colors (hex codes like #FFFFFF). Optional.\n  cta_button_background_color?: string | null;\n  cta_button_text_color?: string | null;\n  /** Additional props to spread onto the CTA icon component */\n  cta_icon_props?: Record<string, unknown>;\n  created_at: string;\n  updated_at: string;\n  created_by?: string;\n  created_by_user?: {\n    id: string;\n    email: string;\n    full_name?: string;\n  };\n  announcement_assets?: AnnouncementAsset[];\n}\n\nexport interface AnnouncementAsset {\n  id: string;\n  announcement_id: string;\n  file_name: string;\n  file_url: string;\n  file_size: number;\n  mime_type: string;\n  upload_path: string;\n  created_at: string;\n}\n\n// API Request/Response Types\nexport interface CreateAnnouncementData {\n  title: string;\n  description: string;\n  background_color: string;\n  icon_name?: string;\n  icon_url?: string;\n  /** Extra props for the main bar/icon */\n  icon_props?: Record<string, unknown>;\n  platforms?: string[]; // entity_platforms set (multi-select)\n  is_active?: boolean;\n  // CTA (Call-To-Action) fields\n  cta_enabled?: boolean;\n  cta_text?: string;\n  cta_icon_name?: string;\n  cta_show_icon?: boolean;\n  cta_url?: string;\n  cta_target?: '_self' | '_blank';\n  cta_button_background_color?: string;\n  cta_button_text_color?: string;\n  cta_icon_props?: Record<string, unknown>;\n  created_by: string;\n}\n\nexport interface UpdateAnnouncementData {\n  title?: string;\n  description?: string;\n  background_color?: string;\n  icon_name?: string;\n  icon_url?: string;\n  /** Extra props for the main bar/icon */\n  icon_props?: Record<string, unknown>;\n  platforms?: string[]; // entity_platforms set (multi-select)\n  is_active?: boolean;\n  // CTA (Call-To-Action) fields\n  cta_enabled?: boolean;\n  cta_text?: string;\n  cta_icon_name?: string;\n  cta_show_icon?: boolean;\n  cta_url?: string;\n  cta_target?: '_self' | '_blank';\n  cta_button_background_color?: string;\n  cta_button_text_color?: string;\n  cta_icon_props?: Record<string, unknown>;\n}\n\nexport interface AnnouncementListResponse {\n  announcements: Announcement[];\n  pagination: {\n    page: number;\n    page_size: number;\n    total_count: number;\n    total_pages: number;\n  };\n  filters?: {\n    platform?: LegacyPlatform;\n    is_active?: boolean;\n  };\n}\n\n/**\n * Wire shape of GET /api/announcements/active — the single typed home for the\n * `{ announcement }` envelope (hub route return + AnnouncementBar's\n * useSelfFetch generic). `null` when no active announcement exists.\n */\nexport interface AnnouncementResponse {\n  announcement: Announcement | null;\n}\n\n/**\n * Default CTA button colors — one home for the pair the DAL create/update\n * defaults and the admin form both reference. Announcement colors are\n * admin-chosen data (not ODS-token surfaces) by design.\n */\nexport const ANNOUNCEMENT_CTA_DEFAULTS = {\n  background: '#1A1A1A',\n  text: '#FAFAFA',\n} as const;\n\n// Form Data Types\nexport interface AnnouncementFormData {\n  title: string;\n  description: string;\n  background_color: string;\n  icon_name: string;\n  icon_url?: string;\n  platforms: string[]; // entity_platforms set (multi-select; no single platform_id)\n  is_active: boolean;\n  // CTA (Call-To-Action) fields\n  cta_enabled: boolean;\n  cta_text: string;\n  cta_icon_name: string;\n  cta_show_icon: boolean;\n  cta_url: string;\n  cta_target: '_self' | '_blank';\n  // New CTA button custom colors\n  cta_button_background_color?: string;\n  cta_button_text_color?: string;\n  cta_icon_props?: string; // JSON string in form\n  /** Extra props for the main bar/icon */\n  icon_props?: string;\n}\n\n// API Query Parameters\nexport interface GetAnnouncementsOptions {\n  platform?: PlatformFilter; // Changed from Platform to PlatformFilter to include 'all'\n  is_active?: boolean;\n  page?: number;\n  pageSize?: number;\n  sortBy?: 'created_at' | 'updated_at' | 'title';\n  sortOrder?: 'asc' | 'desc';\n}\n\n// File Upload Types\nexport interface IconUploadResponse {\n  success: boolean;\n  file_url?: string;\n  file_name?: string;\n  file_size?: number;\n  error?: string;\n}\n\nexport interface IconUploadData {\n  file: File;\n  announcement_id?: string;\n}\n\n// Available SVG Icons (extend as needed)\nexport interface AvailableSvgIcon {\n  name: string;\n  display_name: string;\n  component_name: string;\n}\n\n// Error Types\nexport interface AnnouncementError {\n  code: string;\n  message: string;\n  details?: string;\n}\n\n// Validation Types\nexport interface AnnouncementValidation {\n  title: {\n    required: boolean;\n    minLength: number;\n    maxLength: number;\n  };\n  description: {\n    required: boolean;\n    minLength: number;\n    maxLength: number;\n  };\n  background_color: {\n    required: boolean;\n    pattern: RegExp;\n  };\n  icon_name: {\n    required: boolean;\n    validOptions: string[];\n  };\n  platform: {\n    required: boolean;\n    validOptions: LegacyPlatform[];\n  };\n}\n\n// Import unified platform configuration (removed duplicate definition)\nexport type { PlatformConfig } from './platform';\n\n// Component Props Types\nexport interface AnnouncementBarProps {\n  /**\n   * SSR mode seed. `null` = the server resolved \"nothing to show\" (no active\n   * announcement, or dismissed via cookie) — seeds the fetch hook and skips\n   * the mount fetch. Absent (`undefined`) = client-only mode: the bar\n   * self-fetches on mount from the EndpointsRuntime provider's\n   * `announcementsUrl` (see react-embedding-example — no per-component URL\n   * or platform knobs; the server resolves its own platform via\n   * currentPlatform()).\n   */\n  initialAnnouncement?: Announcement | null;\n  /**\n   * Render-only mode for the admin live preview: no fetch/revalidation, inert\n   * dismiss button, and no storage side effects (never writes dismissal\n   * cookies or touches localStorage).\n   */\n  previewMode?: boolean;\n  /**\n   * When false, the dismiss (X) button is not rendered — the bar can only\n   * disappear by deactivating the announcement. Default true.\n   */\n  dismissible?: boolean;\n  className?: string;\n}\n\nexport interface AnnouncementFormProps {\n  announcement?: Announcement;\n  onSubmit: (data: AnnouncementFormData) => Promise<void>;\n  onCancel: () => void;\n  isLoading?: boolean;\n  availableIcons: AvailableSvgIcon[];\n}\n\nexport interface AnnouncementListProps {\n  announcements: Announcement[];\n  onEdit: (announcement: Announcement) => void;\n  onDelete: (announcementId: string) => void;\n  onToggleActive: (announcementId: string, isActive: boolean) => void;\n  isLoading?: boolean;\n}\n\n// Database Utility Types\nexport interface AnnouncementFilters {\n  platform?: LegacyPlatform;\n  is_active?: boolean;\n  created_by?: string;\n  search?: string;\n}\n\nexport interface AnnouncementSortOptions {\n  field: 'created_at' | 'updated_at' | 'title' | 'platform';\n  direction: 'asc' | 'desc';\n}\n\n// SVG Icon Type\nexport interface SvgIcon {\n  name: string;\n  label: string;\n  /** Optional pre-resolved glyph component. Every registry below (and every\n   *  known consumer) passes `null` and renders by `name` through the shared\n   *  icon resolver — the slot is kept for callers that carry a component. */\n  component: React.ComponentType<React.SVGProps<SVGSVGElement>> | null;\n}\n\n// Platform display configuration\n// PLATFORM_CONFIGS removed - all platform data now comes from database via getPlatformsConfig()\n\n// Available SVG icons configuration\nexport const AVAILABLE_SVG_ICONS: SvgIcon[] = [\n  // OpenFrame Logo Options\n  { name: 'openframe-logo', label: 'OpenFrame Logo', component: null },\n\n  // Platform Logos\n  { name: 'openmsp-logo', label: 'OpenMSP Logo', component: null },\n  { name: 'flamingo-logo', label: 'Flamingo Logo', component: null },\n\n  // Lucide Icons\n  { name: 'megaphone', label: 'Megaphone', component: null },\n  { name: 'bell', label: 'Bell', component: null },\n  { name: 'info', label: 'Information', component: null },\n  { name: 'star', label: 'Star', component: null },\n  { name: 'rocket', label: 'Rocket', component: null },\n  { name: 'package', label: 'Package', component: null },\n];\n","// Product Release Types\n// Following the blog.ts pattern for consistency\n\nimport type { BlogTag, TagAssoc } from './blog';\nimport type { EntityAuthor } from './entity-author';\nimport type { PlatformRecord } from './platform';\nimport type { VideoTeaser, TranscriptWord, SpeakerMapping } from './video-processing';\n\nexport interface ChangelogEntry {\n  title: string;\n  description?: string;\n  /**\n   * Per-entry visibility flag. Optional so product releases (which always render\n   * publicly) remain backwards compatible. Investor updates require this field\n   * because each entry can be marked internal — see investor-update-utils.ts.\n   */\n  visibility?: 'public' | 'internal';\n}\n\nexport interface ReleaseMedia {\n  id: number;\n  release_id: number;\n  media_type: 'image' | 'video' | 'screenshot' | 'demo';\n  media_url: string;\n  display_order: number | null;\n  title: string | null;\n  description: string | null;\n  created_at: string;\n  updated_at: string;\n  created_by: string;\n}\n\nexport interface ProductReleaseGitHubLink {\n  id: number;\n  release_id: number;\n  github_release_url: string; // Full URL (e.g., https://github.com/org/repo/releases/tag/v2.0.0)\n  display_order: number | null;\n  created_at: string;\n}\n\nexport interface KnowledgeBaseLink {\n  id: number;\n  release_id: number;\n  kb_article_path: string; // Relative path (e.g., /api/authentication/api-keys.md)\n  display_order: number | null;\n  created_at: string;\n}\n\nexport interface ClickUpRoadmapTask {\n  id: number;\n  release_id: number;\n  clickup_task_id: string; // Just the task ID - details from roadmap folder\n  display_order: number | null;\n  created_at: string;\n}\n\nexport interface ClickUpDeliveryTask {\n  id: number;\n  release_id: number;\n  clickup_task_id: string; // Just the task ID - details from delivery list\n  display_order: number | null;\n  created_at: string;\n}\n\nexport interface ProductRelease {\n  id: number;\n  title: string;\n  slug: string;\n  version: string;\n  summary: string | null;\n  content: string | null;\n\n  // Release metadata\n  release_type: 'major' | 'minor' | 'patch' | 'beta' | 'alpha';\n  release_status: 'alpha' | 'beta' | 'stable' | 'deprecated';\n  release_date: string;\n\n  // Changelog (JSONB arrays)\n  features_added: ChangelogEntry[];\n  bugs_fixed: ChangelogEntry[];\n  improvements: ChangelogEntry[];\n  breaking_changes: ChangelogEntry[];\n\n  // External relationships (one-to-many)\n  github_releases?: ProductReleaseGitHubLink[];\n  knowledge_base_links?: KnowledgeBaseLink[];\n  clickup_roadmap_tasks?: ClickUpRoadmapTask[];\n  clickup_delivery_tasks?: ClickUpDeliveryTask[];\n  release_media?: ReleaseMedia[];\n\n  // Media (keeping featured_image for hero)\n  featured_image: string | null;\n\n  // Documentation\n  migration_guide_url: string | null;\n  documentation_url: string | null;\n\n  // Video (youtube_url for embeds, main_video_url for AI processing)\n  youtube_url: string | null;\n  main_video_url: string | null;\n  transcript: string | null;\n  transcript_words_data?: TranscriptWord[];\n  srt_content?: string | null;\n  highlight_srt_content?: string | null;\n  video_summary: string | null; // AI-generated summary from video transcription\n  video_bites: VideoTeaser[]; // JSONB array of extracted video clips\n  highlight_video_url: string | null;\n  highlight_video_thumbnail: string | null;\n  highlight_video_duration_ms: number | null;\n  highlight_video_source: 'manual' | 'ai_generated' | null;\n  main_video_thumbnail: string | null;\n  ai_transcript_formatted?: string;\n  speaker_mapping?: SpeakerMapping;\n  ai_confidence_transcript: number | null;\n  ai_confidence_video_summary: number | null; // Confidence for video-generated summary\n  ai_confidence_summary: number | null; // Confidence for entity summary (Release Content Generation)\n\n  // SEO\n  seo_title: string | null;\n  seo_description: string | null;\n  seo_keywords: string | null;\n  og_image_url: string | null;\n\n  // Publishing\n  status: 'draft' | 'published' | 'archived';\n  published_at: string | null;\n  author_id: string | null;\n\n  // HubSpot marketing email (mirrors InvestorUpdate). `hubspot_email_id` is the\n  // stored HubSpot email id; `hubspot_email_url` is computed by the admin GET\n  // route from the production portal id (never persisted).\n  hubspot_email_id?: string | null;\n  hubspot_email_url?: string | null;\n  /**\n   * Hydrated by `getRelease` (detail) and `getReleases` (list) via\n   * `hydrateAuthor` in the hub DAL. Optional so admin form + every existing\n   * consumer that doesn't read author stay backward-compatible.\n   */\n  author?: EntityAuthor;\n\n  // Timestamps\n  created_at: string;\n  updated_at: string;\n\n  // Editor-provided AI generation instructions (steers Generate Content prompt)\n  custom_instructions: string | null;\n\n  // Analytics\n  view_count: number;\n  download_count: number;\n\n  // Relations (populated by joins)\n  platforms?: PlatformRecord[];\n  tags?: BlogTag[];\n  product_release_platforms?: Array<{\n    platform_id: string;\n    is_featured: boolean;\n    featured_order: number | null;\n    // The release DAL flattens the joined `platforms` row onto each junction\n    // entry (`...platform.platforms`), so `name`/`display_name` are present at\n    // runtime — `composeContentUrlFromPlatforms` reads `name` to resolve the\n    // target platform. Declared here so callers can pass this array to the\n    // `composeContentUrl` seam without a cast.\n    name?: string;\n    display_name?: string;\n  }>;\n  // Flat unified tag-association shape (hydrated by entity-tag-utils).\n  product_release_tags?: TagAssoc[];\n}\n\nexport interface CreateProductReleaseData {\n  title: string;\n  slug: string;\n  version: string;\n  summary?: string;\n  content?: string;\n  release_type: 'major' | 'minor' | 'patch' | 'beta' | 'alpha';\n  release_status: 'alpha' | 'beta' | 'stable' | 'deprecated';\n  release_date: string;\n  features_added?: ChangelogEntry[];\n  bugs_fixed?: ChangelogEntry[];\n  improvements?: ChangelogEntry[];\n  breaking_changes?: ChangelogEntry[];\n  github_releases?: Array<{ github_release_url: string; display_order?: number }>;\n  knowledge_base_links?: Array<{ kb_article_path: string; display_order?: number }>;\n  clickup_roadmap_tasks?: Array<{ clickup_task_id: string; display_order?: number }>;\n  clickup_delivery_tasks?: Array<{ clickup_task_id: string; display_order?: number }>;\n  release_media?: Array<{\n    media_type: 'image' | 'video' | 'screenshot' | 'demo';\n    media_url: string;\n    title?: string;\n    description?: string;\n    display_order?: number;\n  }>;\n  featured_image?: string;\n  migration_guide_url?: string;\n  documentation_url?: string;\n  youtube_url?: string;\n  main_video_url?: string;\n  transcript?: string;\n  srt_content?: string | null;\n  highlight_srt_content?: string | null;\n  video_summary?: string; // AI-generated summary from video transcription\n  video_bites?: VideoTeaser[];\n  highlight_video_url?: string | null;\n  highlight_video_thumbnail?: string | null;\n  highlight_video_duration_ms?: number | null;\n  highlight_video_source?: 'manual' | 'ai_generated' | null;\n  main_video_thumbnail?: string | null;\n  seo_title?: string;\n  seo_description?: string;\n  seo_keywords?: string;\n  og_image_url?: string;\n  status: 'draft' | 'published' | 'archived';\n  published_at?: string | null;\n  author_id: string;\n  platforms: string[]; // Array of platform IDs (UUIDs)\n  featured_platform?: string; // Platform ID for featured\n  tags: number[]; // Array of tag IDs\n  custom_instructions?: string | null;\n}\n\nexport type UpdateProductReleaseData = Partial<CreateProductReleaseData>;\n\nexport interface ProductReleaseFilters {\n  platform?: string | 'all';\n  tags?: string[];\n  version?: string;\n  release_type?: ('major' | 'minor' | 'patch' | 'beta' | 'alpha')[];\n  release_status?: ('alpha' | 'beta' | 'stable' | 'deprecated')[];\n  has_breaking_changes?: boolean;\n  search?: string;\n  featured?: boolean;\n  status?: string;\n  limit?: number;\n  offset?: number;\n  ids?: (number | string)[];\n}\n\nexport interface ProductReleaseListResponse {\n  data: ProductRelease[];\n  count: number;\n}\n\n// GitHub Release interface (for wizard)\nexport interface GitHubRelease {\n  id: string;\n  tag_name: string;\n  name: string;\n  body: string;\n  published_at: string;\n  html_url: string;\n  prerelease: boolean;\n  draft: boolean;\n}\n\n// Release type options for dropdowns\nexport const releaseTypeOptions = [\n  { value: 'major', label: 'Major Release', description: 'Breaking changes, new architecture', color: 'red' },\n  { value: 'minor', label: 'Minor Release', description: 'New features, backward compatible', color: 'blue' },\n  { value: 'patch', label: 'Patch Release', description: 'Bug fixes only', color: 'green' },\n  { value: 'beta', label: 'Beta Release', description: 'Pre-release testing version', color: 'yellow' },\n  { value: 'alpha', label: 'Alpha Release', description: 'Early testing version', color: 'orange' },\n] as const;\n\n// Release status options\nexport const releaseStatusOptions = [\n  { value: 'alpha', label: 'Alpha', description: 'Early development, unstable', color: 'orange' },\n  { value: 'beta', label: 'Beta', description: 'Feature complete, testing', color: 'yellow' },\n  { value: 'stable', label: 'Stable', description: 'Production ready', color: 'green' },\n  { value: 'deprecated', label: 'Deprecated', description: 'No longer supported', color: 'gray' },\n] as const;\n\n// Changelog category labels\nexport const changelogLabels = {\n  features_added: 'Features Added',\n  bugs_fixed: 'Bugs Fixed',\n  improvements: 'Improvements',\n  breaking_changes: 'Breaking Changes',\n} as const;\n\n// Semantic versioning validation regex\nexport const SEMVER_REGEX = /^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$/;\n","/**\n * Delivery (ClickUp Bug-fixes & Enhancements) wire types — shared between the\n * hub's `/api/delivery*` route shapes and the lib's `DeliveryTable`/`DeliveryLists`/\n * `DeliverySection` components.\n *\n * Lifted from hub `types/delivery.ts` so embedders consuming the lib's\n * delivery surfaces and the lib's own `ReleaseDetailPage` (which renders\n * related delivery items) share one canonical shape.\n */\n\n/**\n * Display identity for a task assignee — WIRE-SAFE shape: name + avatar\n * only, resolved server-side (ClickUp assignee × profiles). Emails are\n * deliberately NOT on the wire; the roadmap/delivery boards are public\n * surfaces (same policy as the RAG mappers). Feeds `AvatarStack`\n * directly.\n */\nexport interface TaskAssigneeDisplay {\n  /** ClickUp member id — stable render key. */\n  id: number;\n  name: string | null;\n  avatarUrl: string | null;\n}\n\nexport interface DeliveryItem {\n  id: string;\n  title: string;\n  description: string;\n  status: string;\n  statusColor: string; // ClickUp status color\n  taskType: 'Request' | 'Bug' | string; // ClickUp task type\n  /**\n   * Canonical ClickUp custom_item_id (1008 = Bug, 1009 = Request, …).\n   * Surfaced here so the chat's compact card can render a type-specific\n   * lucide icon via `TaskTypeIcon` instead of the two-letter initials\n   * fallback. Single source of truth lives in\n   * `lib/utils/clickup-task-type-utils.ts` (hub-side).\n   */\n  customItemId: number | null;\n  /** Assignee display identities (name + avatar, never email) — powers\n   *  the AvatarStack on cards. Optional: producers that don't enrich\n   *  (e.g. activity-cache recent_tasks) simply omit it. */\n  assignees?: TaskAssigneeDisplay[];\n  /**\n   * Every ClickUp list the task is associated with (home list + ClickUp's\n   * \"Tasks in Multiple Lists\" locations). UI joins these for display.\n   * Falls back to a single-element array containing the home list when\n   * there are no additional locations.\n   */\n  listNames: string[];\n  dateOpened: number; // Unix timestamp\n  dateUpdated: number; // Unix timestamp\n  dateClosed: number | null; // Unix timestamp or null if not closed\n  clickupUrl: string;\n}\n\nexport interface DeliveryResponse {\n  completed: DeliveryItem[];\n  inProgress: DeliveryItem[];\n}\n\n// Task type to badge label mapping\nexport const TASK_TYPE_LABELS = {\n  Request: 'ENHANCEMENT',\n  Bug: 'BUG-FIX',\n} as const;\n\n// Task type to badge text-color mapping (ODS attention-red for Bug; default for others)\nexport const TASK_TYPE_TEXT_COLORS = {\n  Request: '', // Default white/grey\n  Bug: 'text-[var(--ods-attention-red-error)]',\n} as const;\n","/**\n * TMCG (The Miami Cyber Gang) related types and interfaces\n */\n\nexport interface TMCGMemberSocialLink {\n  platform: string;\n  url: string;\n  username?: string;\n}\n\nexport interface TMCGMember {\n  id: string;\n  full_name: string;\n  avatar_url?: string | null;\n  company?: string | null;\n  about?: string | null;\n  job_title?: string | null;\n  tmcg_roles: string[];\n  social_links: TMCGMemberSocialLink[];\n  created_at: string;\n  updated_at: string;\n}\n\nexport interface TMCGMembersResponse {\n  success: boolean;\n  data: {\n    members: TMCGMember[];\n    count: number;\n  };\n  error?: string;\n}\n\nexport interface TMCGMemberResponse {\n  success: boolean;\n  data: TMCGMember;\n  error?: string;\n}\n\n/**\n * TMCG Member Card Props\n */\nexport interface TMCGMemberCardProps {\n  member: TMCGMember;\n  className?: string;\n  compact?: boolean;\n}\n\n/**\n * TMCG Members Grid Props\n */\nexport interface TMCGMembersGridProps {\n  members: TMCGMember[];\n  isLoading?: boolean;\n  error?: string | null;\n  className?: string;\n}\n\n/**\n * TMCG Social Links Props\n */\nexport interface TMCGSocialLinksProps {\n  socialLinks: TMCGMemberSocialLink[];\n  className?: string;\n  size?: 'sm' | 'md' | 'lg';\n}\n\n/**\n * Hook return types\n */\nexport interface UseTMCGMembersResult {\n  members: TMCGMember[];\n  isLoading: boolean;\n  error: string | null;\n  refetch: () => void;\n}\n\nexport interface UseTMCGMemberResult {\n  member: TMCGMember | null;\n  isLoading: boolean;\n  error: string | null;\n  refetch: () => void;\n}\n\n/**\n * TMCG specific constants\n */\nexport const TMCG_ROLES = {\n  FOUNDER: 'founder',\n  ORGANIZER: 'organizer',\n  MEMBER: 'member',\n  CONTRIBUTOR: 'contributor',\n  SPEAKER: 'speaker',\n  MENTOR: 'mentor',\n} as const;\n\nexport type TMCGRole = (typeof TMCG_ROLES)[keyof typeof TMCG_ROLES];\n\n/**\n * TMCG role display names\n */\nexport const TMCG_ROLE_DISPLAY_NAMES: Record<string, string> = {\n  [TMCG_ROLES.FOUNDER]: 'Founder',\n  [TMCG_ROLES.ORGANIZER]: 'Organizer',\n  [TMCG_ROLES.MEMBER]: 'Member',\n  [TMCG_ROLES.CONTRIBUTOR]: 'Contributor',\n  [TMCG_ROLES.SPEAKER]: 'Speaker',\n  [TMCG_ROLES.MENTOR]: 'Mentor',\n};\n","/**\n * The SINGLE content-href authority for every embeddable surface — the\n * `ChatRuntime.composeContentUrl` seam. Page views (onboarding catalog/detail,\n * product releases) AND chat surfaces (entity cards, source chips, search\n * results) all resolve a content link through this one function, so a given\n * content type lands in the SAME place no matter where it's rendered.\n *\n * ## Why this exists\n *\n * Before unification there were two link builders: `composeContentUrl` (pages)\n * and `resolveSourceRowCTA` (chat), and only the former honored the embedder's\n * in-app routing config — so the same `product_release` soft-navigated in-app on\n * the releases page but opened OUT to the hub as a chat card. Now `resolveSource-\n * RowCTA` delegates its href to this seam, so one config (`hostedTypes` /\n * `overrides`) governs internal-vs-external for pages and chat alike.\n *\n * `makeComposeContentUrl` builds the embedder default from a small config: the\n * set of types THIS host serves in-app (→ relative href, soft-navigates) vs.\n * everything else (→ the canonical hub URL, opens out). The hub wires its own\n * composer (`composeContentUrlFromPlatforms` — cross-platform topology) to the\n * same seam. Pure + server-safe (no React, no browser APIs).\n *\n *   composeContentUrl: makeComposeContentUrl({\n *     hostedTypes: new Set(['onboarding_guide', 'product_release']),\n *     contentOrigin: VITE_HUB_ORIGIN,\n *   })\n */\n\nimport { byKey, getPlatformProductionUrl } from '../platform-domains';\nimport { canonicalContentRefType } from './list-url';\n\n/**\n * Type → in-app route suffix. The public-hostable subset of the hub's\n * `PUBLIC_URL_PATHS` (`lib/utils/content-url-builder.ts`); the hub keeps\n * its own copy, this is the embedder default and must stay in sync. (The\n * cross-repo boundary makes a shared import impossible; the lib test pins this\n * constant against a literal copy of itself, so it does NOT detect hub-side\n * drift — values match today and are kept aligned by hand.)\n */\nexport const DEFAULT_CONTENT_SUFFIXES: Record<string, string> = {\n  onboarding_guide: 'onboarding-guides',\n  product_release: 'releases',\n  blog_post: 'blog',\n  case_study: 'case-studies',\n  customer_interview: 'interviews',\n  investor_update: 'investor-updates',\n  webinar: 'webinars',\n  podcast: 'podcasts',\n  event: 'events',\n  // Author profile pages (`/authors/<slug>`) — identified by the profile\n  // slug. Mirrors the hub's `PUBLIC_URL_PATHS.author`, which derives from\n  // `AUTHORS_PATH` in the hub's `lib/utils/breadcrumbs.ts` (the hub-side\n  // SSOT for the segment).\n  author: 'authors',\n};\n\n/** Input to the unified content-href seam. ONE object covers both callers:\n *  page views pass `type` + `identifier` (the slug); chat rows pass `type` +\n *  `identifier` (the primary-key id) + `externalUrl` (the canonical hub URL,\n *  from which the slug is recovered for in-app routing). */\nexport interface ComposeContentUrlInput {\n  /** The content's CANONICAL documentType (e.g. `'product_release'`,\n   *  `'blog_post'`).\n   *\n   *  Rail-vocab aliases (`'blog_post_existing'`) must be canonicalized by the\n   *  caller via `canonicalContentRefType` BEFORE they reach the seam: a host\n   *  writes its `hostedTypes` / `suffixes` / `overrides` against the canonical\n   *  vocabulary, so an alias silently misses every one of them and falls\n   *  through to the default `<contentOrigin>/<type>/<id>` branch — which is how\n   *  the same blog post reached `/blog/<slug>` from one path and\n   *  `/blog_post_existing/<slug>` from another. */\n  type: string;\n  /** Content identifier. Page views pass the slug; chat rows pass the\n   *  primary-key id (the slug is recovered from `externalUrl` when hosted). */\n  identifier: string;\n  /** Preferred path segment for HOSTED types when there is no `externalUrl`\n   *  to recover the slug from — the Mingo/NATS transport ships bare\n   *  `[card://type:id]` markers with no ref metadata, so a fetch-mode card\n   *  knows the row's slug only AFTER it loads the row. `identifier` must stay\n   *  the primary key (that is what `overrides` deep-link on), hence a separate\n   *  field rather than overloading it. Ignored for non-hosted types. */\n  slug?: string | null;\n  /** Hydrated platform junction from the list APIs. `hostedTypes` membership\n   *  still decides in-app vs out; this decides WHICH origin an out-link points\n   *  at — an OpenMSP-owned row resolves to openmsp.ai, not to the embedder's\n   *  `contentOrigin`. Read only when `targetPlatform` is absent. Accepts the\n   *  three junction shapes the DALs produce (see `primaryPlatformOf`). */\n  platforms?: Array<{ name?: string }>;\n  /** The canonical hub URL when the caller already has it (chat entity rows\n   *  carry it from the RAG mapper). Hosted types relativize it to an in-app\n   *  path; non-hosted types use it verbatim (authoritative). Absent for pages. */\n  externalUrl?: string | null;\n  /** Platform that owns `externalUrl` (chat rows). Passed through on the\n   *  non-hosted branch. */\n  targetPlatform?: string | null;\n}\n\nexport interface ContentHrefOptions {\n  /** Types THIS host serves in-app → relative href (soft-nav). Everything\n   *  else resolves to the row's `externalUrl` / `contentOrigin` (opens out). */\n  hostedTypes: ReadonlySet<string>;\n  /** Fallback origin for non-hosted types with no `externalUrl` AND no\n   *  resolvable owning platform (e.g. `https://openframe.app`). When the row\n   *  DOES name its platform, that platform's canonical origin wins — otherwise\n   *  cross-platform content (an OpenMSP blog post) would be linked to the\n   *  wrong site. */\n  contentOrigin: string;\n  /** Per-type route suffix. Defaults to {@link DEFAULT_CONTENT_SUFFIXES}. */\n  suffixes?: Record<string, string>;\n  /** Per-type full override — wins over the suffix logic. Receives the same\n   *  `identifier` the seam was called with. */\n  overrides?: Record<string, (identifier: string) => { href: string; targetPlatform: string | null }>;\n}\n\n/** What the seam resolves a content reference to. */\nexport interface ComposedContentUrl {\n  href: string;\n  targetPlatform: string | null;\n  /**\n   * Set ONLY when the href came from the host's EXPLICIT per-type `overrides`\n   * entry — i.e. the host stated where this type lives in ITS app, rather than\n   * the composer falling through to a hosted-suffix or `<origin>/<type>/<id>`\n   * synthesis.\n   *\n   * The distinction is load-bearing for fetch-mode chat cards whose fetched row\n   * already carries a destination (`refHydratedEntry`'s `item.url`, minted by\n   * the CONTENT HOST for its OWN surface). That url must beat a synthesized\n   * guess, but must NOT beat a host that has explicitly re-homed the type:\n   * a HubSpot ticket's `item.url` is the hub's `/tickets?ticket=…`, which in an\n   * embedder that serves the same ticket at `/help-center/tickets` silently\n   * resolves against the embedder's own unrelated `/tickets` route.\n   *\n   * Optional and absent by default, so a hand-written composer (the hub's\n   * cross-platform one) keeps the pre-existing \"row url wins\" behavior.\n   */\n  hostOverride?: boolean;\n}\n\n/** The unified `composeContentUrl` seam shape on `ChatRuntime`. ALWAYS returns\n *  a tuple (never null) — the seam type is non-nullable and callers read\n *  `.href` unconditionally. */\nexport type ComposeContentUrl = (input: ComposeContentUrlInput) => ComposedContentUrl;\n\n/**\n * Owning platform of a content row, from the hydrated junction the list APIs\n * return. Mirrors the hub's `extractPrimaryPlatform` (`lib/utils/content-url-\n * builder.ts`): the same junction arrives in three shapes depending on which\n * DAL produced it, so all three are read.\n *\n *   1. `{ platforms: { name } }`  — raw Supabase row (rag-mappers)\n *   2. `{ platform_name }`        — legacy pre-flattened admin endpoints\n *   3. `{ name }`                 — modern flattened shape (blog / case-study utils)\n *\n * Returns the FIRST resolvable name; `null` when the junction is absent or\n * carries no usable platform (caller then falls back to `contentOrigin`).\n */\nfunction primaryPlatformOf(platforms: ComposeContentUrlInput['platforms']): string | null {\n  if (!Array.isArray(platforms)) return null;\n  for (const entry of platforms) {\n    const row = entry as {\n      name?: unknown;\n      platform_name?: unknown;\n      platforms?: { name?: unknown };\n    };\n    const name = row?.platforms?.name ?? row?.platform_name ?? row?.name;\n    if (typeof name === 'string' && name.trim()) return name.trim();\n  }\n  return null;\n}\n\n/** Last non-empty path segment of a URL (relative or absolute) — the content\n *  slug of a canonical detail URL like `https://hub/releases/my-release`.\n *  Returns `null` on a malformed URL or empty path. */\nfunction lastPathSegment(url: string): string | null {\n  try {\n    // Resolve against a dummy base so relative inputs parse too.\n    const pathname = new URL(url, 'https://_.local').pathname;\n    const segs = pathname.split('/').filter(Boolean);\n    return segs.length > 0 ? segs[segs.length - 1] : null;\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Build the embedder's `composeContentUrl` for the unified seam.\n *\n * Resolution order (the merged rule used by BOTH page views and chat cards):\n *   1. `overrides[type]` — explicit per-type href, returned with\n *      `hostOverride: true` (the only branch that sets it).\n *   2. `hostedTypes.has(type)` → relative `/<suffix>/<slug>` (in-app, soft-nav).\n *      Chat rows carry the hub URL not the slug, so the slug is recovered from\n *      `externalUrl`; page views pass the slug as `identifier`.\n *   3. `externalUrl` present → use it verbatim (RAG-authoritative hub URL; the\n *      chat non-hosted case).\n *   4. else → `${contentOrigin}/<suffix>/<identifier>` (page-view non-hosted).\n *\n * In-vs-out is decided by `hostedTypes` membership — NOT platform equality,\n * since an embedder has a free-form `source`. The `platforms` arg is part of the\n * seam signature but unused here.\n */\nexport function makeComposeContentUrl(opts: ContentHrefOptions): ComposeContentUrl {\n  const suffixes = opts.suffixes ?? DEFAULT_CONTENT_SUFFIXES;\n  return ({ type: rawType, identifier, slug: slugHint, externalUrl, targetPlatform, platforms }) => {\n    // Defence in depth: callers are expected to hand over a canonical\n    // documentType (see `ComposeContentUrlInput.type`), but a stray alias must\n    // not silently produce a `<origin>/blog_post_existing/<slug>` URL. Same\n    // canonicalizer `buildListUrl` uses — one alias table, both paths.\n    const type = canonicalContentRefType(rawType);\n    const override =\n      opts.overrides && Object.prototype.hasOwnProperty.call(opts.overrides, type) ? opts.overrides[type] : undefined;\n    // Flagged as an explicit host decision — see `ComposedContentUrl.hostOverride`\n    // for why a fetch-mode card has to tell this apart from the branches below.\n    if (override) return { ...override(identifier), hostOverride: true };\n\n    const seg = (Object.prototype.hasOwnProperty.call(suffixes, type) ? suffixes[type] : undefined) ?? type;\n\n    if (opts.hostedTypes.has(type)) {\n      // In-app (soft-nav). Recover the slug from the hub URL for chat rows;\n      // page views already pass the slug as `identifier`. Guard: if the recovered\n      // segment is the type's OWN suffix (e.g. a malformed/list externalUrl like\n      // `https://hub/releases/` → `'releases'`), fall back to `identifier` instead\n      // of emitting a nonsensical `/releases/releases`.\n      //\n      // `slug` (the explicit hint) sits between the two: a Mingo fetch-mode card\n      // has NO externalUrl to recover from but DOES know the row's slug once it\n      // loads, and its `identifier` must stay the primary key so `overrides`\n      // still deep-link correctly.\n      const recovered = externalUrl ? lastPathSegment(externalUrl) : null;\n      const slug = recovered && recovered !== seg ? recovered : slugHint?.trim() || identifier;\n      return { href: `/${seg}/${slug}`, targetPlatform: null };\n    }\n    // Not hosted → opens out. Prefer the RAG-authoritative `externalUrl` (chat).\n    if (externalUrl) return { href: externalUrl, targetPlatform: targetPlatform ?? null };\n\n    // No externalUrl → compose it. The destination is the origin of the\n    // platform that OWNS the row, not a fixed hub: an OpenMSP blog post lives\n    // on openmsp.ai, and pinning every non-hosted type to `contentOrigin` sent\n    // it to flamingo.run (a 404 / wrong site). Owner comes from the explicit\n    // `targetPlatform`, else the hydrated platforms junction — mirroring the\n    // hub's `composeContentUrlFromPlatforms` + `extractPrimaryPlatform`.\n    // Origins resolve through the platform-domain SSOT (env override → default\n    // production URL), so a per-deploy `NEXT_PUBLIC_*_URL` is honoured here too.\n    //\n    // `contentOrigin` stays the fallback for rows with no resolvable owner and\n    // for platform names the registry doesn't know — an embedder's explicit\n    // configuration must win over the registry's flamingo default.\n    // Only a REGISTRY-RESOLVED owner counts. An unknown name (a platform the\n    // registry doesn't carry) falls back to `contentOrigin` for the href, and\n    // reporting it as the `targetPlatform` anyway made the two disagree:\n    // `decideNewTab` prefers `targetPlatform` over its origin check, so a link\n    // pointing at the CURRENT host opened in a new tab.\n    const claimedOwner = targetPlatform ?? primaryPlatformOf(platforms);\n    const owner = claimedOwner && byKey(claimedOwner) ? claimedOwner : null;\n    const origin = owner ? getPlatformProductionUrl(owner) : opts.contentOrigin;\n    // The hub's public detail routes are SLUG-based, so the `slug` hint wins\n    // over `identifier` here too — a Mingo fetch-mode card knows the row's slug\n    // but its `identifier` is the marker's primary key, which would 404.\n    return {\n      href: `${origin.replace(/\\/+$/, '')}/${seg}/${slugHint?.trim() || identifier}`,\n      // Report the owner so `decideNewTab` can compare it against the current\n      // app instead of falling back to an origin guess (which is unreliable in\n      // dev, where every platform shares localhost). `null` whenever the href\n      // came from `contentOrigin`, so the two never disagree.\n      targetPlatform: owner,\n    };\n  };\n}\n\n/**\n * Default href shape when `runtime.composeContentUrl` is NOT wired\n * (single-platform embedders without cross-platform topology). Shared by\n * every catalog/detail view that composes a content link (onboarding guides,\n * product releases, …) so the no-composer fallback has ONE source — both\n * views pass their `basePath`-derived shape through here.\n */\nexport function buildDefaultHref(basePath: string, slug: string): { href: string; targetPlatform: string | null } {\n  return { href: `${basePath}/${slug}`, targetPlatform: null };\n}\n\n/**\n * Resolve a content link via the host's `composeContentUrl` when wired, else the\n * same-origin `buildDefaultHref` fallback — the exact ternary every catalog/detail page\n * VIEW repeated. Centralizes the `composeContentUrl` input shape + the fallback in one\n * place so a future input-shape change lands once, not per view.\n */\nexport function resolveContentHref(\n  composeContentUrl: ComposeContentUrl | undefined,\n  args: { type: string; slug: string; basePath: string; platforms?: ComposeContentUrlInput['platforms'] },\n): { href: string; targetPlatform: string | null } {\n  return composeContentUrl\n    ? composeContentUrl({ type: args.type, identifier: args.slug, platforms: args.platforms })\n    : buildDefaultHref(args.basePath, args.slug);\n}\n","/**\n * Pure helpers for doc-source tree navigation — shared by client hook and RSC SSR.\n * README convention (folder-index file) is configurable but defaults to 'README.md'.\n */\n\nimport type { DocNode } from '../types/doc-source';\n\n/**\n * Canonical folder-index filename. Single SSOT consumed by `doc-tree-nav`\n * pure helpers, `useDocumentTree` (default config), `DocSeoContent` (fallback\n * fetch path), and `multi-level-navigation`'s visual-selection logic. Stays\n * in canonical case here; consumers that do case-insensitive comparison\n * (the navigation) lowercase locally.\n */\nexport const DEFAULT_FOLDER_INDEX_FILE = 'README.md';\n\n/** @deprecated Re-exported under the new name; remove after consumers migrate. */\nconst DEFAULT_FOLDER_INDEX = DEFAULT_FOLDER_INDEX_FILE;\n\n/**\n * Strip the folder-index filename from a path, returning the folder path.\n * For default 'README.md': 'folder/README.md' → 'folder', 'README.md' → '', 'folder/file.md' → 'folder/file.md'\n */\nexport function stripFolderIndexFromPath(path: string, folderIndexFile: string = DEFAULT_FOLDER_INDEX): string {\n  const suffix = `/${folderIndexFile}`;\n  if (path.endsWith(suffix)) return path.slice(0, -suffix.length);\n  if (path === folderIndexFile) return '';\n  return path;\n}\n\nexport function findDocNodeByPath(path: string, nodes: DocNode[]): DocNode | null {\n  for (const node of nodes) {\n    if (node.path === path) return node;\n    if (node.children) {\n      const found = findDocNodeByPath(path, node.children);\n      if (found) return found;\n    }\n  }\n  return null;\n}\n\n/**\n * Given a node path like \"folder1/subfolder2\", return DOM-style node IDs for each segment\n * (accordion sidebar — matches use-document-tree).\n */\nexport function getDocAncestorNodeIds(nodePath: string): string[] {\n  const parts = nodePath.split('/');\n  const ids: string[] = [];\n  let current = '';\n  for (const part of parts) {\n    current = current ? `${current}-${part}` : part;\n    ids.push(current.toLowerCase());\n  }\n  return ids;\n}\n\n/**\n * Resolve which storage path to load (matches use-document-tree content effect).\n */\nexport function resolveContentFetchPath(\n  selectedPath: string,\n  structure: DocNode[],\n  folderIndexFile: string = DEFAULT_FOLDER_INDEX,\n): string | null {\n  if (selectedPath === '') {\n    return folderIndexFile;\n  }\n  const node = findDocNodeByPath(selectedPath, structure);\n  if (node && node.type === 'folder' && !node.hasReadme) {\n    return null;\n  }\n  let pathToFetch = selectedPath;\n  if (node && node.type === 'folder' && node.hasReadme) {\n    pathToFetch = `${selectedPath}/${folderIndexFile}`;\n  }\n  return pathToFetch;\n}\n\nexport function getInitialExpandedNodeIds(cleanInitialPath: string): string[] {\n  if (!cleanInitialPath) return [];\n  const pathForExpansion = cleanInitialPath.includes('.')\n    ? cleanInitialPath.substring(0, cleanInitialPath.lastIndexOf('/'))\n    : cleanInitialPath;\n  if (!pathForExpansion) return [];\n  return getDocAncestorNodeIds(pathForExpansion);\n}\n\n/**\n * When a doc source's root has no top-level folder-index file, navigate to the first\n * folder that does. Matches the historical knowledge-base default.\n */\nexport function getDocSourceDefaultPath(\n  structure: DocNode[],\n  folderIndexFile: string = DEFAULT_FOLDER_INDEX,\n): string | null {\n  if (!structure.length) return null;\n  const hasRootIndex = structure.some(node => node.type === 'file' && node.path === folderIndexFile);\n  if (hasRootIndex) return null;\n  // Scan ALL root nodes for the first folder with a README — not just\n  // structure[0]. Otherwise a leading non-folder root (file / folder without\n  // README) silently returns null even when a later root folder has one.\n  const firstFolderWithReadme = structure.find(node => node.type === 'folder' && node.hasReadme && !!node.path);\n  return firstFolderWithReadme?.path ?? null;\n}\n","/**\n * Shared document path utilities.\n * Single source of truth for path/slug operations used by all doc-source viewers\n * (knowledge base, data room, future).\n */\n\nimport { DEFAULT_FOLDER_INDEX_FILE } from './doc-tree-nav';\n\n/**\n * Convert a document name to a URL-safe path slug.\n * Used when creating, moving, or renaming documents.\n * e.g., \"Investor Thesis\" → \"investor-thesis\"\n */\nexport function toDocSlug(name: string): string {\n  return name.trim().toLowerCase().replace(/\\s+/g, '-');\n}\n\n/**\n * Convert a full document path to a DOM-safe node ID.\n * Used for tree node IDs and sidebar expand/collapse state.\n * e.g., \"legal/cap-table/safe.md\" → \"legal-cap-table-safe.md\"\n */\nexport function pathToNodeId(path: string): string {\n  return path.replace(/[/\\s]/g, '-').toLowerCase();\n}\n\n/**\n * Normalize a raw URL path from a route handler.\n * Strips folder-index suffixes (default README.md) and lowercases segments.\n * Used by both data-room and knowledge-base [...path] route handlers.\n */\nexport function normalizeDocPath(segments: string[], folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE): string {\n  // Lowercase BOTH sides BEFORE comparison so paths from URL segments\n  // (`readme.md`) and the canonical folder-index constant (`README.md`)\n  // strip uniformly. Lowercasing only at the end (the previous shape) left\n  // lowercase URL inputs unstripped → bogus content lookups.\n  const lowerPath = (segments?.join('/') || '').toLowerCase();\n  const lowerIndex = folderIndexFile.toLowerCase();\n\n  const suffix = `/${lowerIndex}`;\n  if (lowerPath.endsWith(suffix)) return lowerPath.slice(0, -suffix.length);\n  if (lowerPath === lowerIndex) return '';\n  return lowerPath;\n}\n","// =============================================================================\n// Shared Document Tree Builder\n// =============================================================================\n// Generic 3-pass algorithm for building hierarchical trees from flat records.\n// Used by all doc-source DALs (openframe-docs, data-room-docs, future).\n\n/**\n * Base interface for tree nodes. Consumers extend this with additional fields.\n */\nexport interface TreeNodeBase {\n  id: string;\n  name: string;\n  path: string;\n  type: 'file' | 'folder';\n  sortOrder?: number;\n  children?: TreeNodeBase[];\n}\n\n/**\n * Format a document name for display: remove .md extension and capitalize.\n */\nexport function formatDocName(name: string): string {\n  let displayName = name;\n  if (displayName.endsWith('.md')) {\n    displayName = displayName.slice(0, -3);\n  }\n  if (displayName.toLowerCase() === 'readme') {\n    return 'README';\n  }\n  return displayName.charAt(0).toUpperCase() + displayName.slice(1).replace(/-/g, ' ');\n}\n\n/**\n * Sort children: README first, then folders, then files — each group by sort_order then alphabetical.\n * Recurses into folder children to ensure consistent ordering at all levels.\n */\nexport function sortTreeChildren<TNode extends TreeNodeBase>(nodes: TNode[]): TNode[] {\n  nodes.sort((a, b) => {\n    const aIsReadme = a.name.toLowerCase() === 'readme' || a.name.toLowerCase() === 'readme.md';\n    const bIsReadme = b.name.toLowerCase() === 'readme' || b.name.toLowerCase() === 'readme.md';\n    if (aIsReadme && !bIsReadme) return -1;\n    if (!aIsReadme && bIsReadme) return 1;\n\n    if (a.type === 'folder' && b.type !== 'folder') return -1;\n    if (a.type !== 'folder' && b.type === 'folder') return 1;\n\n    const aOrder = a.sortOrder ?? Infinity;\n    const bOrder = b.sortOrder ?? Infinity;\n    if (aOrder !== bOrder) return aOrder - bOrder;\n\n    return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });\n  });\n\n  for (const node of nodes) {\n    if (node.type === 'folder' && node.children) {\n      node.children = sortTreeChildren(node.children as TNode[]);\n    }\n  }\n\n  return nodes;\n}\n\n/**\n * Build a hierarchical document tree from a flat list of records.\n *\n * 3-pass algorithm:\n *   1. Create node map from flat records using the provided `mapFn`\n *   2. Build parent-child hierarchy using `getParentPath`\n *   3. Sort children (folders first, README, then others)\n */\nexport function buildDocumentTree<TNode extends TreeNodeBase, TDoc>(\n  docs: TDoc[],\n  mapFn: (doc: TDoc) => TNode,\n  getParentPath: (doc: TDoc) => string | null,\n  getPath: (doc: TDoc) => string,\n): TNode[] {\n  const nodeMap = new Map<string, TNode>();\n  const rootNodes: TNode[] = [];\n\n  for (const doc of docs) {\n    const node = mapFn(doc);\n    nodeMap.set(getPath(doc), node);\n  }\n\n  for (const doc of docs) {\n    // Pass 1 inserted every doc under this exact key, so a miss is\n    // unreachable — but if a `getPath` implementation ever became\n    // non-deterministic, dropping one node beats throwing away the tree.\n    const node = nodeMap.get(getPath(doc));\n    if (!node) continue;\n    let resolvedParentPath = getParentPath(doc);\n\n    if (!resolvedParentPath) {\n      const nodePath = getPath(doc);\n      const lastSlash = nodePath.lastIndexOf('/');\n      if (lastSlash > 0) {\n        resolvedParentPath = nodePath.substring(0, lastSlash);\n      }\n    }\n\n    if (resolvedParentPath) {\n      const parent = nodeMap.get(resolvedParentPath);\n      if (parent) {\n        // Lazily initialize `children` so callers whose `mapFn` doesn't\n        // pre-seed it still get a proper tree (previously a missing\n        // `parent.children` silently flattened the child to the root).\n        if (!parent.children) parent.children = [];\n        (parent.children as TNode[]).push(node);\n      } else {\n        rootNodes.push(node);\n      }\n    } else {\n      rootNodes.push(node);\n    }\n  }\n\n  return sortTreeChildren(rootNodes);\n}\n","/**\n * Shared markdown section extractor\n * Extracts heading sections from markdown content for navigation.\n */\n\nimport { createHeadingIdDeduper, scanHeadings, slugifyHeadingBase, stripHeadingEmojis } from './markdown-heading-id';\nimport { stripInlineMarkdown } from './markdown-to-plain';\n\nexport interface MarkdownSection {\n  id: string;\n  title: string;\n  level: number;\n}\n\nexport interface ExtractSectionsOptions {\n  /** Maximum heading level to include (default: 2 = H1-H2) */\n  maxLevel?: number;\n  /** Remove emoji characters from IDs (default: true) */\n  removeEmojis?: boolean;\n  /** Handle duplicate IDs by appending a counter suffix (default: true) */\n  handleDuplicateIds?: boolean;\n  /** Strip bold, italic, and inline code markers from titles (default: true) */\n  stripFormattingMarkers?: boolean;\n  /** Skip code blocks and YAML frontmatter (default: true) */\n  skipCodeAndYamlBlocks?: boolean;\n}\n\nconst DEFAULT_OPTIONS: Required<ExtractSectionsOptions> = {\n  maxLevel: 2,\n  removeEmojis: true,\n  handleDuplicateIds: true,\n  stripFormattingMarkers: true,\n  skipCodeAndYamlBlocks: true,\n};\n\nexport function extractSections(markdown: string, options: ExtractSectionsOptions = {}): MarkdownSection[] {\n  const opts = { ...DEFAULT_OPTIONS, ...options };\n  const sections: MarkdownSection[] = [];\n  const dedupe = createHeadingIdDeduper();\n\n  // ONE scanner, shared with the renderer's heading-id map\n  // (utils/markdown-heading-id.ts). This file used to carry its own\n  // line loop — `line.startsWith('```')` for fences (blind to `~~~`, to\n  // longer backtick runs and to the CommonMark 0..3-space indent) and a\n  // bare `line === '---'` YAML toggle (which a mid-document thematic break\n  // or setext underline tripped, swallowing every heading after it). Both\n  // produced sections the renderer emitted no id for.\n  const scanned = scanHeadings(markdown, {\n    skipFrontmatter: opts.skipCodeAndYamlBlocks,\n    skipFences: opts.skipCodeAndYamlBlocks,\n  });\n\n  for (const heading of scanned) {\n    if (heading.level > opts.maxLevel) continue;\n\n    let title = heading.text;\n    if (opts.stripFormattingMarkers) {\n      title = stripInlineMarkdown(title).trim();\n    }\n\n    // Slug chain SSOT — shared with the renderers' heading-id generator\n    // (see utils/markdown-heading-id.ts). Extractor and renderers MUST\n    // agree or deep-link anchors silently diverge.\n    const baseId = slugifyHeadingBase(opts.removeEmojis ? stripHeadingEmojis(title) : title);\n\n    // Symbol-only heading fallback: the 1-based ordinal among the headings\n    // this extractor EMITS (levels ≤ maxLevel). The renderer's heading-id\n    // generator mirrors this exact counter for levels ≤ 2\n    // (components/ui/markdown/heading-ids.ts) — the two must agree or\n    // deep-link anchors diverge for symbol-only headings.\n    const cleanId = baseId || `section-${sections.length + 1}`;\n\n    sections.push({\n      id: opts.handleDuplicateIds ? dedupe(cleanId) : cleanId,\n      title,\n      level: heading.level,\n    });\n  }\n\n  return sections;\n}\n","/**\n * HubSpot meeting-link naming convention — the single EXECUTABLE home of the\n * rule that decides which scheduling links surface on a host's booking page.\n *\n * Server-safe, React-free, zero HubSpot-API knowledge: pure string rules over\n * link NAMES. The team-process documentation lives in\n * `docs/EMBEDDING_HUBSPOT_MEETINGS.md` — this module is the parser that doc\n * describes; this file's tests pin the behavior.\n *\n * THE CONVENTION IS NAME-ONLY. HubSpot link slugs are IMMUTABLE after\n * creation (verified in-portal), so nothing semantic may ever live in the\n * slug — it is an opaque URL identity. The link NAME (freely editable in\n * HubSpot at any time) carries everything:\n *\n *   \"Title | Short description | Audience Label\"\n *\n *  - A link is LISTED on the booking page iff its name declares the third\n *    (Audience) segment — that segment is the opt-in marker, the displayed\n *    intended-audience entity (\"For Prospect Investors\") AND the grouping\n *    key (via `schedulingAudienceKey`). Links without it stay unlisted but\n *    remain natively bookable through path deep-links.\n *  - Title/description feed the directory rows and booking pages (an\n *    enabled per-link welcome screen wins over the name for those two).\n */\n\n/**\n * Slug SHAPE guard (any number of `/`-separated kebab segments) — used only\n * to validate path-deep-link input before lookups. Carries NO semantics.\n */\nexport const SCHEDULING_SLUG_SHAPE = /^[a-z0-9-]+(\\/[a-z0-9-]+)*$/;\n\n/** Upper bound for availability month paging (widget nav, route 400, DAL clamp). */\nexport const MAX_MONTH_OFFSET = 11;\n\n/** The one spelling of slug normalisation — every map key and lookup goes through it. */\nexport function normalizeSchedulingSlug(slug: string): string {\n  return slug.trim().toLowerCase();\n}\n\n/** Full-slug shape validity (path-resolution safety only — never semantics). */\nexport function isValidSchedulingSlug(slug: string): boolean {\n  return SCHEDULING_SLUG_SHAPE.test(normalizeSchedulingSlug(slug));\n}\n\nexport interface ParsedSchedulingLinkName {\n  title: string;\n  description: string | null;\n  /**\n   * The third `|` segment: the intended-audience entity (\"Prospect\n   * Investors\", \"OpenFrame Users\"). Non-null ⇒ the link is LISTED; it is\n   * both the display label and (slugified) the grouping key.\n   */\n  audienceLabel: string | null;\n}\n\n/**\n * Parse a link NAME against the `\"Title | Description | Audience\"`\n * convention. Splits on `|` (first two pipes); no `|` → the whole name is\n * the title. Empty segments normalize to null.\n */\nexport function parseSchedulingLinkName(name: string): ParsedSchedulingLinkName {\n  const parts = name.split('|');\n  if (parts.length === 1) return { title: name.trim(), description: null, audienceLabel: null };\n  const title = (parts[0] ?? '').trim();\n  const description = (parts[1] ?? '').trim();\n  const audienceLabel = parts.length > 2 ? parts.slice(2).join('|').trim() : '';\n  return {\n    title: title || name.trim(),\n    description: description || null,\n    audienceLabel: audienceLabel || null,\n  };\n}\n\n/** Whether a link name opts into the directory: its Audience segment slugifies\n *  to a non-empty key — the SAME rule the directory groups by, so \"listed\" and\n *  \"has a group\" can never disagree. */\nexport function isListedSchedulingName(name: string): boolean {\n  return schedulingAudienceKey(parseSchedulingLinkName(name).audienceLabel ?? '') !== null;\n}\n\n/**\n * Grouping key for an audience label: `\"Prospect Investors\"` →\n * `\"prospect-investors\"`. Links sharing a key group together regardless of\n * their (opaque) slugs. Empty after slugification → null (caller treats the\n * link as unlisted).\n */\nexport function schedulingAudienceKey(label: string): string | null {\n  const key = label\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, '-')\n    .replace(/^-+|-+$/g, '');\n  return key || null;\n}\n","/**\n * String constants shared by page-header consumers (`<PageHeader>`,\n * `<DevSectionPage>`, `<DocsHubPage>` callers). Lives in `utils/` —\n * NOT `'use client'` — so server modules (e.g. the hub's\n * `lib/docs/hub-docs-presets.tsx` that builds preset JSX with\n * `<Icon className={SECTION_HERO_ICON_CLASS} />`) can import the raw\n * string. Importing the constant from a `'use client'` module turns it\n * into a Next.js client-reference proxy in server contexts, which\n * lucide's internal `mergeClasses` then calls `.trim()` on and crashes.\n */\n\n/** Tailwind class applied uniformly to every section-hero / page-header\n *  icon across the lib (Roadmap Map, Releases Rocket, Knowledge Hub\n *  BookOpen, Data Room Building2, …). Yellow accent color, 40x40. */\nexport const SECTION_HERO_ICON_CLASS = 'h-10 w-10 text-ods-accent';\n","/**\n * First-touch attribution capture.\n *\n * THE PROBLEM. Server-side UTM extraction reads the request body and, failing that, UTM\n * params on the `referer` header. But `referer` is the page the form was submitted FROM, and\n * a visitor who lands on `/?utm_source=reddit` and then navigates before converting arrives\n * with a clean referer. Measured on the live DB: waitlist rows from the last 90 days have\n * `ip_address` on 350/350 and `utm_source` on **0/350**. The parameters are not being lost in\n * transit; they are gone by submit time.\n *\n * THE FIX. Capture the landing URL's attribution ONCE, on first page view, and replay it into\n * every submit body from then on. The server already prefers body-supplied UTMs over the\n * referer, so nothing changes server-side.\n *\n * FIRST touch, not last: `capture()` never overwrites an existing record, so a visitor who\n * lands on an ad and later arrives via a bookmark keeps the ad attribution.\n *\n * Built on `createLocalStorageAdapter` so there is no hand-rolled Web Storage access and no\n * module-scope `window` — `./utils` is imported by server-safe consumers, so touching\n * `window` at module scope would break SSR.\n *\n * SESSION-SCOPED, deliberately and without an opt-out. Three reasons:\n *  - first-touch attribution for a single visit is the useful signal;\n *  - a session record does not persist an identifier across visits, which keeps this out of\n *    consent-banner territory;\n *  - `sessionStorage` is per-tab, so `load()`-then-`save()` cannot race another tab.\n *    `localStorage` is shared, so two tabs opening simultaneously could both observe no\n *    record and the second write would silently replace the first — breaking the one\n *    invariant this module exists to hold. Web Storage has no compare-and-swap, so that race\n *    cannot be closed reliably; the option is therefore not offered rather than offered with\n *    a caveat. If cross-session persistence is ever needed it belongs in a server-side\n *    cookie, where it can be written atomically.\n *\n * NEVER STORES A RAW URL. See `sanitizeLandingUrl`.\n */\n\nimport { createLocalStorageAdapter } from './local-storage-adapter';\n\n/** Attribution parameters worth carrying from the landing URL to the submit body. */\nexport interface FirstTouchAttribution {\n  utm_source?: string;\n  utm_medium?: string;\n  utm_campaign?: string;\n  utm_content?: string;\n  utm_term?: string;\n  /** Reddit Ads click id. */\n  rdt_cid?: string;\n  /**\n   * The landing PAGE — `origin + pathname` only. Never the query string or fragment.\n   * See `sanitizeLandingUrl` for why.\n   */\n  landing_url?: string;\n  /** ISO timestamp of first capture, for debugging stale records. */\n  captured_at?: string;\n}\n\nexport const FIRST_TOUCH_ATTRIBUTION_KEY = 'of.first_touch_attribution';\n\nconst TRACKED_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'rdt_cid'] as const;\n\n/** Every key this module is allowed to store or replay. */\nconst ALLOWED_KEYS: ReadonlySet<string> = new Set<string>([...TRACKED_PARAMS, 'landing_url', 'captured_at']);\n\n/**\n * Reduce a URL to `origin + pathname`.\n *\n * The raw `window.location.href` must NEVER be stored or replayed. It routinely carries\n * things that have nothing to do with attribution and everything to do with security: OAuth\n * `code`/`state`, magic-link and password-reset tokens, `access_token` in the fragment,\n * email addresses and other PII in query params. This value is spread into every submit body,\n * so anything kept here would be POSTed to our API and forwarded on to HubSpot — a token in\n * a CRM record is a token in every integration downstream of it.\n *\n * The path alone answers the only question worth asking (\"which page did they land on\"), so\n * the query and fragment are dropped wholesale rather than filtered. An allowlist of \"safe\"\n * params would need updating every time a new auth flow adds one; dropping everything cannot\n * go stale.\n */\nexport function sanitizeLandingUrl(url: string): string | undefined {\n  try {\n    const parsed = new URL(url);\n    // Non-http(s) schemes have no meaningful landing page.\n    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return undefined;\n    return `${parsed.origin}${parsed.pathname}`;\n  } catch {\n    return undefined;\n  }\n}\n\nfunction makeAdapter() {\n  return createLocalStorageAdapter<FirstTouchAttribution>({\n    key: FIRST_TOUCH_ATTRIBUTION_KEY,\n    backend: 'session',\n    logTag: '[first-touch-attribution]',\n    // Anything running on this origin can write to sessionStorage, and whatever `load()`\n    // returns is spread into submit bodies. So the stored shape is validated, not assumed:\n    // allowlisted keys only, string values only. Without this a malformed or planted record\n    // would inject arbitrary keys into an API payload.\n    validate: (parsed): parsed is FirstTouchAttribution => {\n      if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return false;\n      return Object.entries(parsed as Record<string, unknown>).every(\n        ([key, value]) => ALLOWED_KEYS.has(key) && typeof value === 'string',\n      );\n    },\n  });\n}\n\n/** Parse the tracked parameters out of a URL. Returns `{}` when none are present. */\nexport function parseAttributionFromUrl(url: string): FirstTouchAttribution {\n  const out: FirstTouchAttribution = {};\n  try {\n    const parsed = new URL(url);\n    for (const param of TRACKED_PARAMS) {\n      const value = parsed.searchParams.get(param);\n      if (value && value.trim()) out[param] = value.trim();\n    }\n  } catch {\n    return {};\n  }\n  return out;\n}\n\n/**\n * Record the landing URL's attribution if nothing is stored yet.\n *\n * Idempotent and first-touch-preserving: a later call with different parameters is ignored.\n * Safe to call on every page view, and a no-op during SSR.\n *\n * @returns what is VERIFIABLY stored after the call — a pre-existing record, the new record\n *   read back from storage, or `{}` when nothing was captured or the write did not stick.\n */\nexport function captureFirstTouchAttribution(options: { url?: string } = {}): FirstTouchAttribution {\n  if (typeof window === 'undefined') return {};\n\n  const adapter = makeAdapter();\n  const existing = adapter.load();\n  if (existing && Object.keys(existing).length > 0) return existing;\n\n  const href = options.url ?? window.location.href;\n  const parsed = parseAttributionFromUrl(href);\n  if (Object.keys(parsed).length === 0) return {};\n\n  const landingUrl = sanitizeLandingUrl(href);\n  const record: FirstTouchAttribution = {\n    ...parsed,\n    ...(landingUrl ? { landing_url: landingUrl } : {}),\n    captured_at: new Date().toISOString(),\n  };\n  adapter.save(record);\n\n  // Confirm it PERSISTED before reporting success. `save()` returns void and swallows its\n  // errors, so in blocked or quota-exceeded storage (Safari private mode, a full quota) the\n  // write silently no-ops. Returning `record` there would hand the caller data that no\n  // subsequent `getFirstTouchAttribution()` can recover — the caller would replay\n  // attribution on this page view and none on the next, which is worse than replaying none\n  // at all because it looks like it worked.\n  return adapter.load() ?? {};\n}\n\n/** Read the stored attribution. `{}` when nothing was captured or during SSR. */\nexport function getFirstTouchAttribution(): FirstTouchAttribution {\n  if (typeof window === 'undefined') return {};\n  return makeAdapter().load() ?? {};\n}\n\n/**\n * Merge stored attribution into a submit body.\n *\n * Values already on the body WIN — a form that collected a real value explicitly should not\n * be overwritten by a stored one. Spread this into every submit payload:\n *\n *   body: JSON.stringify(withFirstTouchAttribution({ email, name }))\n */\nexport function withFirstTouchAttribution<T extends Record<string, unknown>>(body: T): T & FirstTouchAttribution {\n  const stored = getFirstTouchAttribution();\n  const merged: Record<string, unknown> = { ...stored, ...body };\n  for (const [k, v] of Object.entries(merged)) {\n    if (v === undefined || v === null || v === '') delete merged[k];\n  }\n  return merged as T & FirstTouchAttribution;\n}\n\n/** Clear the stored record. Exposed for tests and for a consent-withdrawal path. */\nexport function clearFirstTouchAttribution(): void {\n  if (typeof window === 'undefined') return;\n  makeAdapter().clear();\n}\n","/**\n * ODS spacing tokens + the ONE Tailwind-step → token conversion table.\n *\n * The ODS preset deliberately ships NO Tailwind `spacing` scale, so `gap-lf` is\n * not a class — spacing is written in the ARBITRARY-VALUE form\n * `gap-[var(--spacing-system-lf)]`.\n *\n * THAT FORM MUST BE WRITTEN AS A LITERAL IN JSX. Tailwind generates CSS only\n * for class strings its scanner can find verbatim in source, so a class NAME\n * assembled at runtime (`` `gap-[var(--spacing-system-${token})]` ``) produces\n * no rule at all and the spacing silently disappears — the variable resolves\n * fine, but no declaration ever references it. This module therefore exports\n * the token DATA (for tooling and tests) and deliberately NO class builder.\n *\n * JSX-free leaf with its own `exports` subpath so the hub's ODS allowlist script\n * (a plain node/tsx script) can import the token pattern without React.\n */\n\n/** Every `--spacing-system-*` token, in ascending order. */\nexport const ODS_SPACING_TOKENS = [\n  'zero',\n  'xxs',\n  'xs',\n  'xsf',\n  's',\n  'sf',\n  'm',\n  'mf',\n  'l',\n  'lf',\n  'xl',\n  'xlf',\n  'xxl',\n] as const;\n\nexport type OdsSpacingToken = (typeof ODS_SPACING_TOKENS)[number];\n\n/** Matches a `--spacing-system-<token>` reference (used by the hub's allowlist script). */\nexport const ODS_SPACING_TOKEN_PATTERN = /--spacing-system-([a-z]+)/g;\n\n/** Spacing properties the conversion covers (gap + every padding/margin edge). */\n/**\n * Every spacing property ODS tokenizes. A runtime TUPLE, not just a union, so\n * a consumer that has to scan source for untokenized spacing reads the list\n * from here instead of re-spelling it.\n */\nexport const ODS_SPACING_PROPS = [\n  'gap',\n  'p',\n  'px',\n  'py',\n  'pt',\n  'pb',\n  'pl',\n  'pr',\n  'm',\n  'mx',\n  'my',\n  'mt',\n  'mb',\n  'ml',\n  'mr',\n  'space-x',\n  'space-y',\n] as const;\n\nexport type OdsSpacingProp = (typeof ODS_SPACING_PROPS)[number];\n\n/**\n * THE conversion table: a Tailwind step whose pixel value has an EXACT ODS token.\n * Every entry is asserted against `ods-responsive-tokens.css` (4 x step px, and\n * fixed at every breakpoint) by this leaf's vitest. A step that is absent here\n * has no exact token and must be carried verbatim on a component's exported\n * \"untokenized spacing\" allowlist.\n */\nexport const TAILWIND_STEP_TO_ODS_TOKEN = {\n  1: 'xxs',\n  2: 'xsf',\n  3: 'sf',\n  4: 'mf',\n  6: 'lf',\n  10: 'xlf',\n} as const satisfies Record<number, OdsSpacingToken>;\n\nexport type TailwindSpacingStep = keyof typeof TAILWIND_STEP_TO_ODS_TOKEN;\n","/**\n * CSV serialization — ONE quoting rule for every export in the lib and the hub.\n *\n * Beyond RFC-4180 quoting, cells are FORMULA-NEUTRALIZED: a spreadsheet treats a\n * leading `=`, `+`, `-` or `@` as a formula, so an exported value like\n * `=HYPERLINK(\"http://evil\",\"click\")` executes on open (CSV injection). A cell\n * that starts with one of those AND is not a plain numeric literal gets a leading\n * apostrophe, which Excel/Sheets strip on display.\n */\n\nexport const CSV_CONTENT_TYPE = 'text/csv;charset=utf-8;';\n\nconst NUMERIC_LITERAL = /^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$/;\nconst FORMULA_PREFIX = /^[=+\\-@]/;\n\n/** Quote + neutralize a single cell. */\nexport function csvEscape(value: unknown): string {\n  if (value === null || value === undefined) return '';\n  let cell = String(value);\n  if (FORMULA_PREFIX.test(cell) && !NUMERIC_LITERAL.test(cell)) cell = `'${cell}`;\n  if (cell.includes(',') || cell.includes('\"') || cell.includes('\\n') || cell.includes('\\r')) {\n    return `\"${cell.replace(/\"/g, '\"\"')}\"`;\n  }\n  return cell;\n}\n\nexport interface CsvColumn<TRow> {\n  /** Header text (escaped like any other cell). */\n  header: string;\n  /** Cell value for a row. */\n  value: (row: TRow) => unknown;\n}\n\n/** Serialize rows to a CSV document (header + `\\n`-joined rows). */\nexport function toCsv<TRow>(rows: readonly TRow[], columns: readonly CsvColumn<TRow>[]): string {\n  const header = columns.map(c => csvEscape(c.header)).join(',');\n  const body = rows.map(row => columns.map(c => csvEscape(c.value(row))).join(','));\n  return [header, ...body].join('\\n');\n}\n","'use client';\n\nimport { ExternalLink } from 'lucide-react';\nimport React from 'react';\nimport { Button } from './button';\n\nexport interface BrandAssociationItem {\n  icon: React.ComponentType<{ size?: number; color?: string; className?: string }> | React.ReactElement;\n  title: string;\n  description: string;\n  buttonText: string;\n  link: string;\n}\n\nexport interface BrandAssociationCardProps {\n  item: BrandAssociationItem;\n  className?: string;\n}\n\nexport function BrandAssociationCard({ item, className = '' }: BrandAssociationCardProps) {\n  // Helper function to render icon - handle both React elements and components\n  const renderIcon = () => {\n    // If it's already a React element, just return it\n    if (React.isValidElement(item.icon)) {\n      return item.icon;\n    }\n\n    // If it's a component type, render it with props\n    const IconComponent = item.icon as React.ComponentType<{ size?: number; color?: string; className?: string }>;\n    return <IconComponent size={120} color=\"currentColor\" className=\"h-20 w-20\" />;\n  };\n\n  return (\n    <div className={`relative bg-ods-bg p-10 ${className}`}>\n      <div className=\"space-y-6\">\n        {/* Icon */}\n        <div className=\"flex h-20 w-20 items-center justify-center\">{renderIcon()}</div>\n\n        {/* Title */}\n        <h3 className=\"text-ods-text-primary text-h2\">{item.title}</h3>\n\n        {/* Description */}\n        <p className=\"text-ods-text-secondary text-h6\">{item.description}</p>\n\n        {/* Browse Button */}\n        <Button variant=\"outline\" href={item.link} openInNewTab rightIcon={<ExternalLink className=\"h-4 w-4\" />}>\n          Browse {item.buttonText}\n        </Button>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { BrandAssociationCard, type BrandAssociationItem } from './brand-association-card';\n\nexport interface BrandAssociationGridProps {\n  items: BrandAssociationItem[];\n  columns?: 2 | 3;\n  className?: string;\n  cardClassName?: string;\n}\n\nexport function BrandAssociationGrid({\n  items,\n  columns = 2,\n  className = '',\n  cardClassName = 'bg-ods-card border border-ods-border rounded-lg p-0 overflow-hidden',\n}: BrandAssociationGridProps) {\n  const gridCols = columns === 2 ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1 md:grid-cols-3';\n  const itemsPerRow = columns;\n  const rows = Math.ceil(items.length / itemsPerRow);\n\n  const getBorderClasses = (isLastRow: boolean, isLastInRow: boolean, globalIndex: number) => {\n    let classes = '';\n\n    // Right border - responsive logic\n    if (!isLastInRow) {\n      // On mobile (1 column), never show right border\n      // On desktop (2/3 columns), show right border except for last in row\n      classes += ' md:border-r border-ods-border';\n    }\n\n    // Bottom border logic\n    const isLastItem = globalIndex === items.length - 1;\n\n    // Mobile: bottom border for all except last item\n    if (!isLastItem) {\n      classes += ' border-b border-ods-border';\n    }\n\n    // Desktop: override mobile border, show bottom border for all rows except last\n    if (!isLastRow) {\n      classes += ' md:border-b border-ods-border';\n    } else {\n      // Last row on desktop - remove bottom border\n      classes += ' md:border-b-0';\n    }\n\n    return classes;\n  };\n\n  return (\n    <div className={`${cardClassName} ${className}`}>\n      {Array.from({ length: rows }, (_, rowIndex) => {\n        const startIndex = rowIndex * itemsPerRow;\n        const rowItems = items.slice(startIndex, startIndex + itemsPerRow);\n        const isLastRow = rowIndex === rows - 1;\n\n        return (\n          <div key={rowIndex} className={`grid ${gridCols}`}>\n            {rowItems.map((item, itemIndex) => {\n              const globalIndex = startIndex + itemIndex;\n              const isLastInRow = itemIndex === rowItems.length - 1;\n              const borderClasses = getBorderClasses(isLastRow, isLastInRow, globalIndex);\n\n              return <BrandAssociationCard key={globalIndex} item={item} className={borderClasses} />;\n            })}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { ShapeCircleDashIcon } from '../icons';\n\nexport interface BulletListItem {\n  text: string;\n  id?: string;\n}\n\nexport interface BulletListProps {\n  items: string[] | BulletListItem[];\n  bulletColor?: string;\n  bulletIcon?: React.ComponentType<{ size?: number; color?: string; className?: string }>;\n  bulletSize?: number;\n  textClassName?: string;\n  itemClassName?: string;\n  containerClassName?: string;\n  spacing?: 'sm' | 'md' | 'lg';\n}\n\nconst spacingMap = {\n  sm: 'space-y-2',\n  md: 'space-y-3',\n  lg: 'space-y-4',\n};\n\nexport function BulletList({\n  items,\n  bulletColor = 'var(--color-text-primary)',\n  bulletIcon: BulletIcon = ShapeCircleDashIcon,\n  bulletSize = 16,\n  textClassName = 'text-h4 text-ods-text-primary',\n  itemClassName = 'flex items-start gap-3',\n  containerClassName = '',\n  spacing = 'md',\n}: BulletListProps) {\n  const normalizedItems = items.map(item => (typeof item === 'string' ? { text: item, id: undefined } : item));\n\n  return (\n    <div className={`${spacingMap[spacing]} ${containerClassName}`}>\n      {normalizedItems.map((item, index) => (\n        <div key={item.id || index} className={itemClassName}>\n          {/* <div className=\"flex-shrink-0 mt-[0.75rem]\"> */}\n          <div className=\"mt-1 flex-shrink-0\">\n            <BulletIcon size={bulletSize} color={bulletColor} />\n          </div>\n          {/* </div> */}\n          <p className={textClassName}>{item.text}</p>\n        </div>\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport { ChevronUp, ChevronDown } from 'lucide-react';\nimport { type ButtonHTMLAttributes, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\n\ninterface ChevronButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  direction?: 'up' | 'down';\n  size?: 'sm' | 'md' | 'lg';\n  isExpanded?: boolean;\n  backgroundColor?: string;\n  borderColor?: string;\n}\n\nconst ChevronButton = forwardRef<HTMLButtonElement, ChevronButtonProps>(\n  ({ className, direction = 'down', size = 'md', isExpanded, backgroundColor, borderColor, ...props }, ref) => {\n    const Icon = (isExpanded ? ChevronUp : ChevronDown) || (direction === 'up' ? ChevronUp : ChevronDown);\n    const sizeClasses = {\n      sm: 'h-4 w-4',\n      md: 'h-5 w-5',\n      lg: 'h-6 w-6',\n    };\n\n    return (\n      <button\n        className={cn(\n          'inline-flex items-center justify-center rounded-md ring-offset-background transition-colors text-h6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',\n          'h-10 px-4 py-2',\n          className,\n        )}\n        style={{\n          backgroundColor: backgroundColor,\n          borderColor: borderColor,\n        }}\n        ref={ref}\n        {...props}\n      >\n        <Icon className={sizeClasses[size]} />\n      </button>\n    );\n  },\n);\nChevronButton.displayName = 'ChevronButton';\n\nexport { ChevronButton };\n","'use client';\n\nimport { cn } from '../../utils/cn';\n\nexport type CircularProgressVariant = 'success' | 'warning' | 'error' | 'info' | 'accent';\n\n/**\n * How to treat a `percentage` over 100.\n * - `clamp` (default): value is clamped to 0–100 for both the ring and the label.\n * - `wrap`: the ring wraps around once per 100. The label shows the actual\n *   (unclamped) percentage, while the ring fills to `percentage % 100` — e.g.\n *   130 → 30%, 230 → 30%, 200 → 100% (exact laps render as full). Use together\n *   with `variant=\"warning\"` / `\"error\"` to communicate overage semantically.\n */\nexport type CircularProgressOverflow = 'clamp' | 'wrap';\n\ninterface CircularProgressProps {\n  percentage: number; // 0-100 in clamp mode; may exceed 100 in wrap mode\n  variant?: CircularProgressVariant; // Default: 'success'\n  size?: number; // Default: 56\n  strokeWidth?: number; // Default: 10\n  showLabel?: boolean; // Show percentage text in center (default: true)\n  labelFormat?: 'percent' | 'value'; // 'percent' = \"70%\", 'value' = \"70\" (default: 'percent')\n  overflow?: CircularProgressOverflow; // Default: 'clamp'\n  className?: string;\n}\n\n// Fallback track for variants that don't have a designed secondary color\n// (info, accent). success/warning/error use their ODS `*-secondary` token.\nconst SUBTLE_TRACK = 'rgba(255, 255, 255, 0.06)';\n\nconst variantColors: Record<CircularProgressVariant, { progress: string; track: string }> = {\n  success: {\n    progress: 'var(--color-success)',\n    track: 'var(--color-success-secondary)',\n  },\n  warning: {\n    progress: 'var(--color-warning)',\n    track: 'var(--color-warning-secondary)',\n  },\n  error: {\n    progress: 'var(--color-error)',\n    track: 'var(--color-error-secondary)',\n  },\n  info: {\n    progress: 'var(--ods-system-greys-white)',\n    track: SUBTLE_TRACK,\n  },\n  accent: {\n    progress: 'var(--ods-accent)',\n    track: SUBTLE_TRACK,\n  },\n};\n\nexport function CircularProgress({\n  percentage,\n  variant = 'success',\n  size = 56,\n  strokeWidth = 10,\n  showLabel = true,\n  labelFormat = 'percent',\n  overflow = 'clamp',\n  className,\n}: CircularProgressProps) {\n  const safePercentage = Math.max(0, percentage);\n  const isWrap = overflow === 'wrap';\n\n  // In wrap mode the ring shows `percentage % 100`. Exact laps (200, 300, …)\n  // would otherwise render empty, so collapse them to a full 100% ring.\n  let ringPercentage: number;\n  if (isWrap && safePercentage > 100) {\n    const mod = safePercentage % 100;\n    ringPercentage = mod === 0 ? 100 : mod;\n  } else {\n    ringPercentage = Math.min(100, safePercentage);\n  }\n\n  const labelPercentage = isWrap ? safePercentage : Math.min(100, safePercentage);\n\n  const radius = (size - strokeWidth) / 2;\n  const circumference = radius * 2 * Math.PI;\n  const ringOffset = circumference - (ringPercentage / 100) * circumference;\n\n  const { progress, track } = variantColors[variant];\n\n  return (\n    <div className={cn('relative', className)} style={{ width: size, height: size }}>\n      <svg width={size} height={size} className=\"-rotate-90 transform\">\n        <circle cx={size / 2} cy={size / 2} r={radius} stroke={track} strokeWidth={strokeWidth} fill=\"none\" />\n        <circle\n          cx={size / 2}\n          cy={size / 2}\n          r={radius}\n          stroke={progress}\n          strokeWidth={strokeWidth}\n          fill=\"none\"\n          strokeDasharray={circumference}\n          strokeDashoffset={ringOffset}\n          strokeLinecap=\"butt\"\n          className=\"transition-all duration-300 ease-out\"\n        />\n      </svg>\n      {showLabel && (\n        <div className=\"absolute inset-0 flex items-center justify-center\">\n          <span className=\"font-medium text-ods-text-primary\" style={{ fontSize: size <= 40 ? 10 : 12 }}>\n            {Math.round(labelPercentage)}\n            {labelFormat === 'percent' ? '%' : ''}\n          </span>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nexport const CheckIcon = ({ className }: { className?: string }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M20 6L9 17l-5-5\" />\n  </svg>\n);\n\nexport const XIcon = ({ className }: { className?: string }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M18 6L6 18\" />\n    <path d=\"M6 6l12 12\" />\n  </svg>\n);\n\nexport const MinusIcon = ({ className }: { className?: string }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M5 12h14\" />\n  </svg>\n);\n\nexport const CheckCircleIcon = ({ className }: { className?: string }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14\" />\n    <path d=\"M9 11l3 3L22 4\" />\n  </svg>\n);\n\nexport const XCircleIcon = ({ className }: { className?: string }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <path d=\"M15 9l-6 6\" />\n    <path d=\"M9 9l6 6\" />\n  </svg>\n);\n","import type { ReactNode } from 'react';\nimport Link from '../../embed-shims/next-link';\nimport { cn } from '../../utils/cn';\nimport { QuestionCircleIcon } from '../icons-v2-generated/signs-and-symbols/question-circle-icon';\nimport { CircularProgress, type CircularProgressOverflow, type CircularProgressVariant } from './circular-progress';\nimport { FloatingTooltip } from './floating-tooltip';\nimport { Tag } from './tag';\n\n/**\n * How the percentage is rendered next to the value:\n * - `'auto'` (default): a colored `Tag` for `warning`/`error` variants, plain\n *   \"(NN%)\" text otherwise. Preserves the original coupling to `progressVariant`.\n * - `'plain'`: always plain \"(NN%)\" text, regardless of `progressVariant`. Use\n *   when you want a colored progress ring but a neutral percentage (Figma).\n * - `'tag'`: always a colored `Tag`.\n */\nexport type DashboardInfoCardPercentageDisplay = 'auto' | 'plain' | 'tag';\n\n/**\n * Size of the progress ring in px. A single number sets the ring size from the\n * Tailwind `md` (800px) breakpoint up — on mobile the ring always shrinks to\n * the 24px Figma spec. The responsive form sets `base` (mobile) explicitly,\n * plus optional `md` / `lg` (1280px) overrides — e.g. `{ base: 24, lg: 56 }`\n * keeps the small ring through tablet, and `{ base: 40 }` pins 40px at every\n * breakpoint.\n */\nexport type DashboardInfoCardProgressSize = number | { base: number; md?: number; lg?: number };\n\nexport interface DashboardInfoCardProps {\n  title?: string;\n  /** Node rendered in place of the title text. Takes precedence over `title`. */\n  titleSlot?: ReactNode;\n  /**\n   * Tag rendered in the title row, after the title (Figma \"status\" variant) —\n   * e.g. `<Tag variant=\"outline\" label=\"AI Handling\" />` or a TicketStatusTag.\n   * Works with or without a `title`.\n   */\n  titleTag?: ReactNode;\n  /**\n   * Icon or image shown in a bordered square slot on the left (Figma \"icon\"\n   * variant): 32px box / 16px content on mobile, 56px box / 24px content from\n   * `md` up. The passed node is stretched to fill the content area, so plain\n   * icon components and `<img>` both work.\n   */\n  icon?: ReactNode;\n  value: string | number;\n  percentage?: number;\n  showProgress?: boolean;\n  progressVariant?: CircularProgressVariant;\n  /**\n   * Controls how `percentage` is rendered (Tag vs plain text) independently of\n   * the progress-ring color. Default `'auto'`. See {@link DashboardInfoCardPercentageDisplay}.\n   */\n  percentageDisplay?: DashboardInfoCardPercentageDisplay;\n  /**\n   * How the progress ring treats values over 100. Forwarded to `CircularProgress`.\n   * Default: `'clamp'` — existing behavior (clamped to 0–100).\n   * Use `'wrap'` for overage/overflow semantics (excess rendered as a red arc).\n   */\n  progressOverflow?: CircularProgressOverflow;\n  /**\n   * Size of the circular progress ring. Defaults to the Figma spec\n   * `{ base: 24, md: 56 }` — 24px on mobile, 56px on tablet and desktop. A\n   * number changes only the tablet/desktop size (mobile stays 24px); use\n   * `{ base, md?, lg? }` for full control. Stroke width scales proportionally\n   * with the size.\n   */\n  progressSize?: DashboardInfoCardProgressSize;\n  className?: string;\n  /**\n   * Navigation URL — renders the card as a Next.js Link\n   * When provided, cursor becomes pointer and hover accent styles are applied\n   */\n  href?: string;\n  /** Tooltip content shown on a question-mark icon next to the value */\n  tooltip?: ReactNode;\n  /** Override the value text className (default: `text-h3 md:text-h2` per Figma) */\n  valueClassName?: string;\n  /** Secondary text rendered beside the value (e.g. an entry/item count). */\n  subValue?: ReactNode;\n}\n\nexport function DashboardInfoCard({\n  title,\n  titleSlot,\n  titleTag,\n  icon,\n  value,\n  percentage,\n  showProgress = false,\n  progressVariant,\n  percentageDisplay = 'auto',\n  progressOverflow,\n  progressSize,\n  className,\n  href,\n  tooltip,\n  valueClassName,\n  subValue,\n}: DashboardInfoCardProps) {\n  const formattedValue = typeof value === 'number' ? value.toLocaleString() : value;\n\n  const renderPercentage = () => {\n    if (percentage === undefined) return null;\n\n    const asTag =\n      percentageDisplay === 'tag' ||\n      (percentageDisplay === 'auto' && (progressVariant === 'warning' || progressVariant === 'error'));\n\n    if (asTag) {\n      const tagVariant =\n        progressVariant === 'warning' || progressVariant === 'error'\n          ? progressVariant\n          : progressVariant === 'success'\n            ? 'success'\n            : 'grey';\n      return <Tag variant={tagVariant} label={`${percentage}%`} />;\n    }\n\n    return <p className=\"text-ods-text-secondary text-h4\">({percentage}%)</p>;\n  };\n\n  const renderProgress = () => {\n    if (!showProgress || percentage === undefined) return null;\n\n    // Keep the ring proportionally weighted (10px stroke at the 56px default).\n    const strokeFor = (size: number) => Math.max(2, Math.round((size * 10) / 56));\n\n    const common = {\n      percentage,\n      variant: progressVariant,\n      overflow: progressOverflow,\n      showLabel: false,\n    };\n\n    // Figma spec: the mobile ring is always 24px. The default is 56px from\n    // `md` (800px) up; a numeric `progressSize` overrides only that md+ size.\n    const size =\n      typeof progressSize === 'number' ? { base: 24, md: progressSize } : (progressSize ?? { base: 24, md: 56 });\n\n    // Responsive: one ring per provided breakpoint, toggled with Tailwind\n    // display utilities so only one shows at a time. The class names must stay\n    // static string literals — Tailwind's JIT scanner can't see\n    // dynamically-built names (e.g. `${bp}:hidden`).\n    const { base, md, lg } = size;\n\n    const rings: Array<{ key: string; size: number; className: string }> = [\n      { key: 'base', size: base, className: md !== undefined ? 'md:hidden' : lg !== undefined ? 'lg:hidden' : '' },\n    ];\n    if (md !== undefined) {\n      rings.push({\n        key: 'md',\n        size: md,\n        className: lg !== undefined ? 'hidden md:block lg:hidden' : 'hidden md:block',\n      });\n    }\n    if (lg !== undefined) {\n      rings.push({ key: 'lg', size: lg, className: 'hidden lg:block' });\n    }\n\n    return (\n      <>\n        {rings.map(ring => (\n          <CircularProgress\n            key={ring.key}\n            {...common}\n            size={ring.size}\n            strokeWidth={strokeFor(ring.size)}\n            className={ring.className}\n          />\n        ))}\n      </>\n    );\n  };\n\n  const cardContent = (\n    <>\n      {/* Icon slot (Figma \"icon\" variant): 32px box / 16px content on mobile,\n          56px box / 24px content from `md` up. */}\n      {icon && (\n        <div className=\"flex size-8 shrink-0 items-center justify-center rounded-sm border border-ods-border bg-ods-bg p-[var(--spacing-system-xxs)] text-ods-text-primary md:size-14\">\n          <span className=\"flex size-4 items-center justify-center md:size-6 [&>*]:size-full\">{icon}</span>\n        </div>\n      )}\n\n      {/* Content section */}\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        {/* Title row: caption and/or tag (Figma \"status\" variant) */}\n        <div className=\"flex items-center gap-[var(--spacing-system-xxs)]\">\n          {titleSlot ?? (title !== undefined && <p className=\"truncate text-ods-text-secondary text-h5\">{title}</p>)}\n          {titleTag}\n        </div>\n\n        {/* Value and percentage */}\n        <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n          <p className={cn('truncate text-ods-text-primary text-h3 md:text-h2', valueClassName)}>{formattedValue}</p>\n          {subValue && <p className=\"text-ods-text-secondary text-h6\">{subValue}</p>}\n          {renderPercentage()}\n          {tooltip && (\n            <FloatingTooltip content={tooltip} side=\"top\">\n              <span className=\"cursor-help text-ods-text-secondary\">\n                <QuestionCircleIcon size={20} />\n              </span>\n            </FloatingTooltip>\n          )}\n        </div>\n      </div>\n\n      {/* Progress indicator */}\n      {renderProgress()}\n    </>\n  );\n\n  // Figma spec: mobile 8px padding / 8px gap / exactly 64px tall; tablet+ 16px\n  // padding / 16px gap / exactly 104px tall; 6px radius. Fixed height (not\n  // min-h) — with a 32px titleTag the natural content height slightly exceeds\n  // the spec, and Figma's frame is fixed; items-center absorbs the difference.\n  const baseClassName =\n    'bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-xsf)] md:p-[var(--spacing-system-m)] h-16 md:h-[104px] flex gap-[var(--spacing-system-s)] md:gap-[var(--spacing-system-m)] items-center transition-all';\n\n  if (href) {\n    return (\n      <Link\n        href={href}\n        className={cn(\n          baseClassName,\n          'group cursor-pointer',\n          'hover:border-ods-border-hover hover:bg-ods-card-hover',\n          className,\n        )}\n      >\n        {cardContent}\n      </Link>\n    );\n  }\n\n  return <div className={cn(baseClassName, className)}>{cardContent}</div>;\n}\n","import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface InfoCardRowSection {\n  title: string;\n  caption: string;\n  icon?: ReactNode;\n}\n\nexport interface InfoCardRowProps {\n  lead: InfoCardRowSection;\n  stats: [InfoCardRowSection, InfoCardRowSection];\n  className?: string;\n}\n\nconst CELL =\n  'flex items-center gap-[var(--spacing-system-s)] min-w-0 p-[var(--spacing-system-xsf)] md:p-[var(--spacing-system-m)] min-h-[60px] md:min-h-[76px]';\n\nfunction StatCell({ section, className }: { section: InfoCardRowSection; className?: string }) {\n  return (\n    <div className={cn(CELL, 'flex-1', className)}>\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <p className=\"truncate uppercase text-ods-text-secondary text-h5\">{section.title}</p>\n        <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-s)]\">\n          <p className=\"min-w-0 flex-1 truncate text-ods-text-primary text-h3\">{section.caption}</p>\n          {section.icon && (\n            <div className=\"flex size-6 shrink-0 items-center justify-center md:hidden\">{section.icon}</div>\n          )}\n        </div>\n      </div>\n      {section.icon && <div className=\"hidden size-6 shrink-0 items-center justify-center md:flex\">{section.icon}</div>}\n    </div>\n  );\n}\n\nexport function InfoCardRow({ lead, stats, className }: InfoCardRowProps) {\n  return (\n    <div\n      className={cn(\n        'flex shrink-0 flex-col overflow-hidden rounded-md border border-ods-border bg-ods-card md:flex-row',\n        className,\n      )}\n    >\n      <div className={cn(CELL, 'border-b border-ods-border md:flex-1 md:border-b-0 md:border-r')}>\n        {lead.icon && (\n          <div className=\"flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-sm border border-ods-border [&>*]:size-full [&_img]:object-cover\">\n            {lead.icon}\n          </div>\n        )}\n        <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n          <p className=\"truncate text-ods-text-primary text-h3\">{lead.title}</p>\n          <p className=\"truncate text-ods-text-secondary text-h6\">{lead.caption}</p>\n        </div>\n      </div>\n\n      <div className=\"flex md:flex-1\">\n        <StatCell section={stats[0]} className=\"border-r border-ods-border\" />\n        <StatCell section={stats[1]} />\n      </div>\n    </div>\n  );\n}\n","'use client';\n\n/**\n * OSTypeBadge Component\n *\n * Displays operating system type with icon and label.\n * Automatically normalizes OS type strings from various sources.\n */\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { getOSIcon, getOSLabel, normalizeOSType } from '../../utils/os-utils';\n\nexport interface OSTypeBadgeProps {\n  /** OS type string (case-insensitive, handles aliases) */\n  osType?: string;\n  /** Additional CSS classes */\n  className?: string;\n  /** Show only icon (no label) */\n  iconOnly?: boolean;\n  /** Icon size class (default: w-4 h-4) */\n  iconSize?: string;\n  /** Show label only (no icon) */\n  labelOnly?: boolean;\n  /** Icon color */\n  iconColor?: string;\n  /** Right icon */\n  rightIcon?: React.ReactNode;\n}\n\n/**\n * OSTypeBadge - Displays OS type with icon and label\n *\n * @example\n * ```tsx\n * <OSTypeBadge osType=\"windows\" />\n * <OSTypeBadge osType=\"Darwin\" />\n * <OSTypeBadge osType=\"Ubuntu\" iconOnly />\n * ```\n */\nexport const OSTypeBadge: React.FC<OSTypeBadgeProps> = ({\n  osType,\n  className = '',\n  iconOnly = false,\n  iconSize = 'w-4 h-4',\n  iconColor = 'var(--color-text-secondary)',\n  labelOnly = false,\n  rightIcon = null,\n}) => {\n  if (!osType) {\n    return labelOnly ? <span className={cn('text-ods-text-secondary', className)}>Unknown</span> : null;\n  }\n\n  const normalized = normalizeOSType(osType);\n  if (!normalized && !labelOnly) return null;\n\n  const label = getOSLabel(osType);\n  const IconComponent = getOSIcon(osType);\n\n  if (iconOnly && IconComponent) {\n    return <IconComponent className={cn(iconSize, 'text-ods-text-secondary', className)} />;\n  }\n\n  if (labelOnly) {\n    return <span className={cn('text-ods-text-primary', className)}>{label}</span>;\n  }\n\n  return (\n    <div className={cn('flex items-center gap-1 text-ods-text-primary text-h4', className)}>\n      {IconComponent && <IconComponent className={iconSize} color={iconColor} />}\n      {label}\n      {rightIcon && rightIcon}\n    </div>\n  );\n};\n\nOSTypeBadge.displayName = 'OSTypeBadge';\n\n/**\n * OSTypeIcon - Displays only the OS icon\n *\n * @example\n * ```tsx\n * <OSTypeIcon osType=\"windows\" />\n * <OSTypeIcon osType=\"Darwin\" size=\"w-5 h-5\" />\n * ```\n */\nexport const OSTypeIcon: React.FC<{\n  osType?: string;\n  size?: string;\n  className?: string;\n}> = ({ osType, size = 'w-4 h-4', className = '' }) => {\n  return <OSTypeBadge osType={osType} iconOnly iconSize={size} className={className} />;\n};\n\nOSTypeIcon.displayName = 'OSTypeIcon';\n\n/**\n * OSTypeLabel - Displays only the OS label\n *\n * @example\n * ```tsx\n * <OSTypeLabel osType=\"windows\" />\n * <OSTypeLabel osType=\"Darwin\" />\n * ```\n */\nexport const OSTypeLabel: React.FC<{\n  osType?: string;\n  className?: string;\n}> = ({ osType, className = '' }) => {\n  return <OSTypeBadge osType={osType} labelOnly className={className} />;\n};\n\nOSTypeLabel.displayName = 'OSTypeLabel';\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport type { OSPlatformId } from '../../utils/os-platforms';\nimport { OSTypeIcon } from '../features/os-type-badge';\nimport { MonitorIcon } from '../icons-v2-generated/devices/monitor-icon';\nimport { Ellipsis01Icon } from '../icons-v2-generated/interface';\nimport { Tag, type TagProps } from './tag';\n\nexport interface Device {\n  id?: string;\n  machineId?: string;\n  name: string;\n  type?: 'desktop' | 'laptop' | 'mobile' | 'tablet' | 'server';\n  operatingSystem?: OSPlatformId | 'macos' | 'ios' | 'android'; // Support both OSPlatformId and legacy values\n  organization?: string;\n  status?: 'active' | 'inactive' | 'offline' | 'warning' | 'error';\n  lastSeen?: string | Date;\n  tags?: string[];\n  // Additional device properties\n  ipAddress?: string;\n  macAddress?: string;\n  version?: string;\n  location?: string;\n}\n\n// Action button configuration\nexport interface ActionButton {\n  label: string;\n  onClick?: () => void;\n  variant?: 'default' | 'outline' | 'secondary';\n  visible?: boolean;\n}\n\nexport interface DeviceCardProps extends React.HTMLAttributes<HTMLDivElement> {\n  device: Device;\n  actions?: {\n    moreButton?: {\n      visible?: boolean;\n      onClick?: () => void;\n    };\n    detailsButton?: {\n      visible?: boolean;\n      component?: React.ReactNode;\n    };\n    customActions?: ActionButton[];\n  };\n  statusTag?: TagProps;\n  onDeviceClick?: (device: Device) => void;\n}\n\nexport function DeviceCard({\n  device,\n  actions = {\n    moreButton: { visible: true },\n  },\n  statusTag,\n  onDeviceClick,\n  className,\n  ...props\n}: DeviceCardProps) {\n  // Format date for last seen\n  const formatLastSeen = (lastSeen?: string | Date) => {\n    if (!lastSeen) return null;\n\n    const date = typeof lastSeen === 'string' ? new Date(lastSeen) : lastSeen;\n    // UTC getters so \"last seen\" is identical on server (UTC) and client\n    // (local) — otherwise React #418 hydration mismatch.\n    const year = date.getUTCFullYear();\n    const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n    const day = String(date.getUTCDate()).padStart(2, '0');\n    const hours = String(date.getUTCHours()).padStart(2, '0');\n    const minutes = String(date.getUTCMinutes()).padStart(2, '0');\n\n    return `${year}/${month}/${day}, ${hours}:${minutes}`;\n  };\n\n  return (\n    <div\n      onClick={onDeviceClick ? () => onDeviceClick(device) : undefined}\n      className={cn(\n        'overflow-clip rounded-[6px] border border-ods-border bg-ods-card',\n        'flex flex-col gap-4 p-4',\n        onDeviceClick && 'cursor-pointer',\n        className,\n      )}\n      {...props}\n    >\n      {/* Row 1: Device icon | OS icon + Name + Organization | More button | Details button | Custom actions */}\n      <div className=\"flex w-full items-center gap-4\">\n        {/* Device type icon */}\n        <div className=\"flex shrink-0 items-center justify-center rounded-[6px] border border-ods-border p-2\">\n          <MonitorIcon className=\"text-ods-text-secondary\" size={16} />\n        </div>\n\n        {/* OS icon + Device name + Organization (stacked) */}\n        <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n          <div className=\"flex items-center gap-1\">\n            {device.operatingSystem && (\n              <OSTypeIcon\n                osType={device.operatingSystem === 'macos' ? 'darwin' : device.operatingSystem}\n                size=\"w-4 h-4\"\n                className=\"shrink-0\"\n              />\n            )}\n            <span className=\"truncate text-ods-text-primary text-h4\" title={device.name}>\n              {device.name}\n            </span>\n          </div>\n          {device.organization && (\n            <span className=\"truncate text-ods-text-secondary text-h6\" title={device.organization}>\n              {device.organization}\n            </span>\n          )}\n        </div>\n\n        {/* More button */}\n        {actions.moreButton?.visible !== false && (\n          <div\n            className=\"flex shrink-0 cursor-pointer items-center justify-center rounded-[6px] border border-ods-border p-3 transition-colors hover:bg-ods-bg-hover\"\n            onClick={e => {\n              e.stopPropagation();\n              actions.moreButton?.onClick?.();\n            }}\n          >\n            <Ellipsis01Icon className=\"text-ods-text-primary\" />\n          </div>\n        )}\n\n        {/* Details button */}\n        {actions.detailsButton?.visible !== false && actions.detailsButton?.component && (\n          <div className=\"shrink-0\" onClick={e => e.stopPropagation()}>\n            {actions.detailsButton.component}\n          </div>\n        )}\n\n        {/* Custom action buttons */}\n        {actions.customActions?.map(\n          (action, index) =>\n            action.visible !== false && (\n              <div\n                key={index}\n                className=\"flex shrink-0 cursor-pointer items-center justify-center rounded-[6px] border border-ods-border px-4 py-3 transition-colors hover:bg-ods-bg-hover\"\n                onClick={e => {\n                  e.stopPropagation();\n                  action.onClick?.();\n                }}\n              >\n                <span className=\"text-nowrap text-ods-text-primary text-h3\">{action.label}</span>\n              </div>\n            ),\n        )}\n      </div>\n\n      {/* Row 2: Status badge | Last seen */}\n      {(statusTag || device.lastSeen) && (\n        <div className=\"flex w-full items-center gap-2\">\n          {statusTag && (\n            <Tag\n              variant={statusTag.variant}\n              icon={statusTag.icon}\n              onClose={statusTag.onClose}\n              className={statusTag.className}\n              label={statusTag.label}\n            />\n          )}\n          {device.lastSeen && (\n            <span className=\"flex-1 truncate text-ods-text-secondary text-h6\">\n              Last Seen: {formatLastSeen(device.lastSeen)}\n            </span>\n          )}\n        </div>\n      )}\n\n      {/* Tags section */}\n      {device.tags && device.tags.length > 0 && (\n        <div className=\"flex w-full flex-wrap items-center gap-2\">\n          {device.tags.map((tag, index) => (\n            <Tag key={index} variant=\"outline\" label={tag} />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n","import type React from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface DeviceCardCompactProps extends React.HTMLAttributes<HTMLDivElement> {\n  deviceName?: string | null;\n  organization?: string | null;\n}\n\n/**\n * Compact device card variant for table cells\n * Shows only device name and organization in a stacked layout\n * Returns empty fragment if both values are missing\n */\nexport function DeviceCardCompact({ deviceName, organization, className, ...props }: DeviceCardCompactProps) {\n  // Check for valid values (not null, undefined, '-', or string 'null')\n  const hasName = deviceName && deviceName !== '-' && deviceName !== 'null';\n  const hasOrg = organization && organization !== '-' && organization !== 'null';\n\n  // If both are missing, return empty fragment\n  if (!hasName && !hasOrg) {\n    return <></>;\n  }\n\n  return (\n    <div className={cn('flex min-h-[60px] flex-col justify-center gap-1 py-2', className)} {...props}>\n      {hasName && (\n        <span className=\"truncate text-ods-text-primary text-h6\" title={deviceName}>\n          {deviceName}\n        </span>\n      )}\n      {hasOrg && (\n        <span className=\"truncate text-ods-text-secondary text-h6\" title={organization}>\n          {organization}\n        </span>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { Card } from './card';\nimport { StatusBadge } from './status-badge';\n\nexport interface FeatureCardItem {\n  icon?: React.ComponentType<{ size?: number; color?: string; className?: string }>;\n  iconColor?: string;\n  title: string;\n  badge?: {\n    text: string;\n    variant?: 'card' | 'button';\n    colorScheme?: 'cyan' | 'pink' | 'yellow' | 'green' | 'purple' | 'default';\n  };\n  content: React.ReactNode; // Allow any content to be injected\n  // Card-level customization props\n  removeAllBorders?: boolean; // Remove all borders for this card\n  noBackground?: boolean; // Remove default background\n  customBackground?: string; // Custom background color/class\n}\n\nexport interface FeatureCardGridProps {\n  items: FeatureCardItem[];\n  columns?: 2 | 3;\n  className?: string;\n  cardClassName?: string;\n  itemClassName?: string;\n  showBorders?: boolean;\n  roundedCorners?: boolean; // Flag to enable rounded corners\n  cardGap?: string; // Gap between cards (e.g., 'gap-4', 'gap-6')\n}\n\nexport function FeatureCardGrid({\n  items,\n  columns = 3,\n  className = '',\n  cardClassName = 'bg-ods-card border border-ods-border rounded-lg p-0 overflow-hidden',\n  itemClassName = 'bg-ods-bg p-10',\n  showBorders = true,\n  roundedCorners = false,\n  cardGap = '',\n}: FeatureCardGridProps) {\n  const gridCols = columns === 2 ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1 md:grid-cols-3';\n  const itemsPerRow = columns;\n  const rows = Math.ceil(items.length / itemsPerRow);\n\n  const getBorderClasses = (isLastRow: boolean, isLastInRow: boolean, globalIndex: number) => {\n    if (!showBorders) return '';\n\n    let classes = '';\n\n    // Right border - responsive logic\n    if (!isLastInRow) {\n      // On mobile (1 column), never show right border\n      // On desktop (2/3 columns), show right border except for last in row\n      classes += ' md:border-r border-ods-border';\n    }\n\n    // Bottom border logic\n    const isLastItem = globalIndex === items.length - 1;\n\n    // Mobile: bottom border for all except last item\n    if (!isLastItem) {\n      classes += ' border-b border-ods-border';\n    }\n\n    // Desktop: override mobile border, show bottom border for all rows except last\n    if (!isLastRow) {\n      classes += ' md:border-b border-ods-border';\n    } else {\n      // Last row on desktop - remove bottom border\n      classes += ' md:border-b-0';\n    }\n\n    return classes;\n  };\n\n  // Check if all cards have noBackground to modify card container\n  const allCardsHaveNoBackground = items.every(item => item.noBackground);\n  const allCardsHaveNoBorders = items.every(item => item.removeAllBorders);\n\n  let finalCardClassName = cardClassName;\n  if (allCardsHaveNoBackground) {\n    finalCardClassName = finalCardClassName.replace('bg-ods-card', 'bg-transparent');\n  }\n  if (allCardsHaveNoBorders) {\n    finalCardClassName = 'bg-transparent p-0 overflow-visible border-0 shadow-none rounded-none';\n  }\n\n  return (\n    <Card className={`${finalCardClassName} ${className}`}>\n      {Array.from({ length: rows }, (_, rowIndex) => {\n        const startIndex = rowIndex * itemsPerRow;\n        const rowItems = items.slice(startIndex, startIndex + itemsPerRow);\n        const isLastRow = rowIndex === rows - 1;\n\n        return (\n          <div key={rowIndex} className={`grid ${gridCols} ${cardGap}`}>\n            {rowItems.map((item, itemIndex) => {\n              const globalIndex = startIndex + itemIndex;\n              const isLastInRow = itemIndex === rowItems.length - 1;\n              const borderClasses = getBorderClasses(isLastRow, isLastInRow, globalIndex);\n\n              // Apply card-level customizations\n              let finalItemClassName = itemClassName;\n              let finalBorderClasses = borderClasses;\n\n              // Handle background customization\n              if (item.noBackground) {\n                finalItemClassName = finalItemClassName.replace('bg-ods-bg', 'bg-transparent');\n              } else if (item.customBackground) {\n                finalItemClassName = finalItemClassName.replace('bg-ods-bg', item.customBackground);\n              }\n\n              // Handle border removal\n              if (item.removeAllBorders) {\n                finalBorderClasses = '';\n              }\n\n              // Handle rounded corners\n              if (roundedCorners) {\n                finalItemClassName += ' rounded-lg';\n              }\n\n              return (\n                <div\n                  key={globalIndex}\n                  className={`${finalItemClassName}${finalBorderClasses} relative`}\n                  style={item.customBackground ? { backgroundColor: item.customBackground } : undefined}\n                >\n                  <div className={!item.icon && !item.title ? 'flex h-full flex-col' : 'space-y-6'}>\n                    {item.icon && (\n                      <div className=\"flex items-start justify-between\">\n                        <item.icon size={80} color={item.iconColor} />\n                        {item.badge && (\n                          <StatusBadge\n                            text={item.badge.text}\n                            variant={item.badge.variant || 'card'}\n                            colorScheme={item.badge.colorScheme || 'default'}\n                          />\n                        )}\n                      </div>\n                    )}\n\n                    {!item.icon && item.badge && (\n                      <div className=\"flex items-start justify-end\">\n                        <StatusBadge\n                          text={item.badge.text}\n                          variant={item.badge.variant || 'card'}\n                          colorScheme={item.badge.colorScheme || 'default'}\n                        />\n                      </div>\n                    )}\n\n                    <h3 className=\"whitespace-pre-line tracking-[-0.64px] text-ods-text-primary text-h2\">\n                      {item.title}\n                    </h3>\n\n                    {item.content}\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        );\n      })}\n    </Card>\n  );\n}\n","'use client';\n\nimport type React from 'react';\n\nexport interface FeatureListItemData {\n  icon: React.ReactNode;\n  title: string;\n  description: string;\n}\n\nexport interface FeatureListProps {\n  items: FeatureListItemData[];\n  className?: string;\n  iconBoxSize?: number;\n}\n\nexport function FeatureList({ items, className = '', iconBoxSize = 72 }: FeatureListProps) {\n  return (\n    <div className={`flex flex-col overflow-hidden rounded-[6px] border border-ods-border bg-ods-bg ${className}`}>\n      {items.map((item, index) => (\n        <div\n          key={index}\n          className={`flex w-full items-start gap-4 bg-ods-card p-4 ${\n            index < items.length - 1 ? 'border-b border-ods-border' : ''\n          }`}\n        >\n          <div\n            className=\"flex shrink-0 items-center justify-center rounded-[6px] border border-ods-border bg-ods-bg\"\n            style={{ width: iconBoxSize, height: iconBoxSize }}\n          >\n            {item.icon}\n          </div>\n          <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n            <p className=\"text-ods-text-primary text-h3\">{item.title}</p>\n            <p className=\"normal-case tracking-normal text-ods-text-secondary text-h6\">{item.description}</p>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport { ExternalLink, Info } from 'lucide-react';\nimport type { AnchorHTMLAttributes, ReactNode } from 'react';\nimport { Button } from './button';\nimport { FloatingTooltip } from './floating-tooltip';\n\n/**\n * Source-reference tooltip — a (i) info icon whose tooltip lists every source\n * behind a cited figure as a Button link row (value · short label,\n * external-link glyph) with an optional \"where it lives in the research\" line\n * under it. Rows are clickable because the FloatingTooltip's safePolygon\n * keeps it open while the pointer travels into the content; the (i) itself\n * opens the first source so the click-the-icon habit still works.\n *\n * The trigger renders inline (`as=\"span\"`) — a source (i) always sits inside\n * a line of text (a statement's last line, a money tail, a value line).\n * Hosts style the icon via `iconClassName`; the tooltip CONTENT is\n * self-contained ODS styling (it portals to <body>, outside any host-scoped\n * CSS variables).\n */\n\nexport interface SourceRef {\n  /** The figure this source backs, e.g. \"$121.2B\" — leads the button label. */\n  value: string;\n  /** Short source name after the value (buttons are nowrap — keep it tight). */\n  label: string;\n  /** Exactly where the number lives in the research (report · table/figure ·\n   *  column) — \"click the link and can't find the number\" is a credibility\n   *  bug. Rendered as a fine-print line under the button. */\n  where?: string;\n  href: string;\n}\n\nexport interface SourceTooltipProps {\n  /** One-line setup above the source rows (e.g. how the figures add up). */\n  intro?: ReactNode;\n  sources: SourceRef[];\n  /** Fine print under the rows (e.g. what the cited segment covers). */\n  note?: ReactNode;\n  /** Classes for the trigger's <Info> icon (size/color live with the host). */\n  iconClassName?: string;\n  /** Extra attributes for the trigger anchor — e.g. a host's click-intercept\n   *  opt-out vocabulary like the deck's `data-no-nav`. */\n  triggerAnchorProps?: AnchorHTMLAttributes<HTMLAnchorElement> & {\n    [k: `data-${string}`]: string | boolean | undefined;\n  };\n}\n\nexport function SourceTooltip({ intro, sources, note, iconClassName, triggerAnchorProps }: SourceTooltipProps) {\n  return (\n    <FloatingTooltip\n      as=\"span\"\n      side=\"top\"\n      delayDuration={0}\n      className=\"max-w-sm\"\n      content={\n        <span className=\"flex flex-col gap-2 py-0.5\">\n          {intro && <span className=\"font-semibold text-ods-text-primary\">{intro}</span>}\n          {sources.map(s => (\n            <span key={s.href} className=\"flex flex-col gap-1\">\n              <Button\n                variant=\"outline\"\n                size=\"compact\"\n                fullWidth\n                linkProps={{ href: s.href, target: '_blank', rel: 'noopener noreferrer' }}\n                rightIcon={<ExternalLink />}\n              >\n                {s.value} · {s.label}\n              </Button>\n              {s.where && <span className=\"text-ods-text-secondary\">{s.where}</span>}\n            </span>\n          ))}\n          {note && <span className=\"text-ods-text-secondary\">{note}</span>}\n        </span>\n      }\n    >\n      <a href={sources[0].href} target=\"_blank\" rel=\"noreferrer\" aria-label=\"Source research\" {...triggerAnchorProps}>\n        <Info className={iconClassName} />\n      </a>\n    </FloatingTooltip>\n  );\n}\n","'use client';\n\nexport interface HighlightCardItem {\n  category: string;\n  categoryColor: string;\n  highlightBg: string;\n  description: string;\n}\n\nexport interface HighlightCardProps {\n  item: HighlightCardItem;\n  className?: string;\n}\n\nexport function HighlightCard({ item, className = '' }: HighlightCardProps) {\n  // Calculate width for mobile and desktop\n  const mobileWidth = item.category.length * 12;\n  const desktopWidth = item.category.length * 14;\n\n  return (\n    <div className={`relative bg-ods-bg p-6 md:p-10 ${className}`}>\n      <div className=\"relative\">\n        {/* Mobile highlight */}\n        <div\n          className=\"absolute left-[80px] top-0 h-8 rounded-md md:hidden\"\n          style={{\n            backgroundColor: item.highlightBg,\n            width: `${mobileWidth}px`,\n          }}\n        />\n        {/* Desktop highlight */}\n        <div\n          className=\"absolute left-[95px] top-0 hidden h-10 rounded-md md:block\"\n          style={{\n            backgroundColor: item.highlightBg,\n            width: `${desktopWidth}px`,\n          }}\n        />\n        <h3 className=\"relative z-10 text-ods-text-primary text-h2\">\n          Your <span style={{ color: item.categoryColor }}>{item.category}</span> {item.description}\n        </h3>\n      </div>\n    </div>\n  );\n}\n\nexport interface HighlightCardGridProps {\n  items: HighlightCardItem[];\n  columns?: 2 | 3;\n  className?: string;\n  cardClassName?: string;\n}\n\nexport function HighlightCardGrid({\n  items,\n  columns = 2,\n  className = '',\n  cardClassName = 'bg-ods-card border border-ods-border rounded-lg p-0 overflow-hidden',\n}: HighlightCardGridProps) {\n  const gridCols = columns === 2 ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1 md:grid-cols-3';\n  const itemsPerRow = columns;\n  const rows = Math.ceil(items.length / itemsPerRow);\n\n  const getBorderClasses = (isLastRow: boolean, isLastInRow: boolean, globalIndex: number) => {\n    let classes = '';\n\n    // Right border - responsive logic\n    if (!isLastInRow) {\n      // On mobile (1 column), never show right border\n      // On desktop (2/3 columns), show right border except for last in row\n      classes += ' md:border-r border-ods-border';\n    }\n\n    // Bottom border logic\n    const isLastItem = globalIndex === items.length - 1;\n\n    // Mobile: bottom border for all except last item\n    if (!isLastItem) {\n      classes += ' border-b border-ods-border';\n    }\n\n    // Desktop: override mobile border, show bottom border for all rows except last\n    if (!isLastRow) {\n      classes += ' md:border-b border-ods-border';\n    } else {\n      // Last row on desktop - remove bottom border\n      classes += ' md:border-b-0';\n    }\n\n    return classes;\n  };\n\n  return (\n    <div className={`${cardClassName} ${className}`}>\n      {Array.from({ length: rows }, (_, rowIndex) => {\n        const startIndex = rowIndex * itemsPerRow;\n        const rowItems = items.slice(startIndex, startIndex + itemsPerRow);\n        const isLastRow = rowIndex === rows - 1;\n\n        return (\n          <div key={rowIndex} className={`grid ${gridCols}`}>\n            {rowItems.map((item, itemIndex) => {\n              const globalIndex = startIndex + itemIndex;\n              const isLastInRow = itemIndex === rowItems.length - 1;\n              const borderClasses = getBorderClasses(isLastRow, isLastInRow, globalIndex);\n\n              return <HighlightCard key={globalIndex} item={item} className={borderClasses} />;\n            })}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport { type HTMLAttributes, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\n\ninterface IconsBlockProps extends HTMLAttributes<HTMLDivElement> {\n  icons?: string[];\n  size?: 'sm' | 'md' | 'lg';\n}\n\nconst IconsBlock = forwardRef<HTMLDivElement, IconsBlockProps>(\n  ({ className, icons = [], size = 'md', ...props }, ref) => {\n    const sizeClasses = {\n      sm: 'h-4 w-4',\n      md: 'h-6 w-6',\n      lg: 'h-8 w-8',\n    };\n\n    return (\n      <div className={cn('flex items-center gap-2', className)} ref={ref} {...props}>\n        {icons.map((icon, index) => (\n          <div key={index} className={cn('rounded-md bg-secondary p-1', sizeClasses[size])}>\n            {/* Icon placeholder */}\n            <div className=\"h-full w-full rounded-sm bg-primary/20\" />\n          </div>\n        ))}\n      </div>\n    );\n  },\n);\nIconsBlock.displayName = 'IconsBlock';\n\nexport { IconsBlock };\n","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport { Chevron02DownIcon } from '../icons-v2-generated';\nimport { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from './actions-menu';\nimport { Button, type ButtonProps } from './button';\n\nexport interface DropdownButtonProps {\n  /** Button label rendered next to the chevron. */\n  label: React.ReactNode;\n  /** Optional leading icon rendered before the label. */\n  icon?: React.ReactNode;\n  /**\n   * Items shown in the dropdown (single flat group). For grouped menus with\n   * separators pass `groups` instead — exactly one of the two is required.\n   */\n  items?: ActionsMenuItem[];\n  /** Grouped items (with optional separators). Takes precedence over `items`. */\n  groups?: ActionsMenuGroup[];\n  /**\n   * Render the trigger as a standard `Button` of this variant (`outline`,\n   * `accent`, …) instead of the default card-colored seam trigger. Use this\n   * when the dropdown sits in a row of ordinary buttons and must match them.\n   */\n  variant?: ButtonProps['variant'];\n  /** Button size for the `variant` trigger. Ignored without `variant`. */\n  size?: ButtonProps['size'];\n  /** Spinner state for the trigger (either look). */\n  loading?: boolean;\n  disabled?: boolean;\n  /**\n   * Stretch the trigger to fill its container. Prefer this over a `w-full`\n   * className: the seam trigger's main half only absorbs the extra width — keeping\n   * the chevron flush against the trailing edge instead of floating mid-button —\n   * when the underlying `Button` is told it is full width, which a className\n   * cannot communicate.\n   */\n  fullWidth?: boolean;\n  className?: string;\n  ariaLabel?: string;\n  align?: 'start' | 'center' | 'end';\n  side?: 'top' | 'right' | 'bottom' | 'left';\n  /**\n   * Forwarded to the dropdown content. The standard Radix dropdown→dialog\n   * recipe: pass `(e) => e.preventDefault()` when a menu item opens a modal,\n   * otherwise closing the menu refocuses its trigger AFTER the dialog has\n   * taken focus, yanking focus back OUT of the modal the action just opened.\n   */\n  onCloseAutoFocus?: (event: Event) => void;\n}\n\n/**\n * THE multi-action button — a labeled trigger with a chevron opening an\n * `ActionsMenu`. One component, so the trigger styling, chevron, and menu\n * chrome cannot drift between surfaces.\n *\n * Two trigger looks — BOTH render through the unified `Button`:\n * - default (no `variant`): the card-colored seam trigger — `Button\n *   variant=\"outline\"` with the chevron in its `splitIcon` slot, so the label\n *   and chevron are separated by a divider, the whole surface is one click\n *   target, and the chevron rotates while open. Used standalone or via\n *   `PageActions` when a `PageActionButton` has `dropdownItems` set.\n * - `variant=\"outline\"` (or any Button variant): a standard `Button` with the\n *   chevron as its right icon, so the trigger matches sibling buttons in the\n *   same row — including `loading` and `leftIcon` support.\n *\n * Item clicks close the menu (ActionsMenuDropdown's default; opt out per item\n * with `closeOnSelect: false`).\n */\nexport function DropdownButton({\n  label,\n  icon,\n  items,\n  groups,\n  variant,\n  size = 'small-legacy',\n  loading = false,\n  disabled,\n  fullWidth,\n  className,\n  ariaLabel,\n  align = 'end',\n  side = 'bottom',\n  onCloseAutoFocus,\n}: DropdownButtonProps) {\n  const [open, setOpen] = React.useState(false);\n\n  // If the trigger becomes disabled/loading WHILE the menu is open, close it —\n  // and only ever block OPEN transitions below, so Escape/outside-click can\n  // still dismiss an already-open menu.\n  //\n  // Closed while rendering, not from an effect: the disabling render would\n  // otherwise commit with the menu still hanging off a control the user can no\n  // longer interact with, and only take it away on the pass after. Guarded on\n  // `open`, so the second render pass this triggers takes the early exit —\n  // and unlike a mask, an unrelated later re-enable cannot pop the menu back up.\n  if (open && (disabled || loading)) setOpen(false);\n\n  const handleOpenChange = React.useCallback(\n    (next: boolean) => {\n      if (next && (disabled || loading)) return;\n      setOpen(next);\n    },\n    [disabled, loading],\n  );\n\n  const resolvedAriaLabel = ariaLabel || (typeof label === 'string' ? label : undefined);\n\n  const trigger = variant ? (\n    <Button\n      variant={variant}\n      size={size}\n      disabled={disabled}\n      loading={loading}\n      fullWidth={fullWidth}\n      aria-label={resolvedAriaLabel}\n      leftIcon={icon}\n      rightIcon={\n        <Chevron02DownIcon className={cn('h-4 w-4 transition-transform duration-fast', open && 'rotate-180')} />\n      }\n      className={className}\n    >\n      {label}\n    </Button>\n  ) : (\n    // The unified `Button`'s split layout IS the seam anatomy (label, full-height\n    // divider, trailing glyph, one click target) — no bespoke <button> here, so\n    // the trigger can't drift from the house button chrome.\n    <Button\n      type=\"button\"\n      variant=\"outline\"\n      disabled={disabled}\n      loading={loading}\n      fullWidth={fullWidth}\n      aria-label={resolvedAriaLabel}\n      leftIcon={icon}\n      splitIcon={<Chevron02DownIcon className={cn('transition-transform duration-fast', open && 'rotate-180')} />}\n      className={cn(open && 'bg-ods-bg-hover', className)}\n    >\n      {label}\n    </Button>\n  );\n\n  return (\n    <ActionsMenuDropdown\n      groups={groups ?? [{ items: items ?? [] }]}\n      open={open}\n      onOpenChange={handleOpenChange}\n      align={align}\n      side={side}\n      onCloseAutoFocus={onCloseAutoFocus}\n      triggerAriaLabel={resolvedAriaLabel}\n      customTrigger={trigger}\n    />\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport Link from '../../embed-shims/next-link';\nimport { cn } from '../../utils/cn';\nimport { Ellipsis01Icon } from '../icons-v2-generated';\nimport { Button } from './button';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './dropdown-menu';\n\n/**\n * @deprecated Use `ActionsMenuItem` from `./actions-menu` with\n * `ActionsMenuDropdown` instead. `MoreActionsMenu` is being phased out in favor\n * of the `ActionsMenu*` family.\n */\nexport type MoreActionsItem = {\n  label: string;\n  /** Click handler. Optional when `href` is provided. */\n  onClick?: () => void;\n  /** If set, the item renders as a Next.js Link (real <a href> in the DOM). */\n  href?: string;\n  /** Only relevant with `href` — opens the link in a new tab. */\n  openInNewTab?: boolean;\n  icon?: React.ReactNode;\n  disabled?: boolean;\n  danger?: boolean;\n};\n\n/**\n * @deprecated Use `ActionsMenuDropdownProps` from `./actions-menu` instead.\n */\nexport interface MoreActionsMenuProps {\n  items: MoreActionsItem[];\n  align?: 'start' | 'center' | 'end';\n  side?: 'top' | 'right' | 'bottom' | 'left';\n  sideOffset?: number;\n  className?: string;\n  /** Appended to the dropdown content. To render the menu above a high-z\n   *  surface (drawer, modal), prefer wrapping that surface in a\n   *  `PortalContainerContext` provider rather than escalating z-index here. */\n  contentClassName?: string;\n  ariaLabel?: string;\n  /** Custom trigger element. When provided, replaces the default ellipsis icon button. */\n  trigger?: React.ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Called when the open state changes — use together with `open`. */\n  onOpenChange?: (open: boolean) => void;\n  /** Forwarded to the dropdown content. Call `e.preventDefault()` to stop\n   *  Radix returning focus (and its focus ring) to the trigger on close. */\n  onCloseAutoFocus?: (event: Event) => void;\n}\n\n/**\n * Compact, reusable menu triggered by an ellipsis icon button.\n * Built on top of Radix DropdownMenu used in the UI Kit.\n *\n * @deprecated Use `ActionsMenuDropdown` from `./actions-menu` instead — it\n * supports the same trigger override (`customTrigger`), controlled `open` /\n * `onOpenChange`, `onCloseAutoFocus`, and `danger` items, plus grouped items,\n * checkboxes, and submenus. This component will be removed in a future release.\n */\nexport function MoreActionsMenu({\n  items,\n  align = 'end',\n  side = 'bottom',\n  sideOffset = 6,\n  className,\n  contentClassName,\n  ariaLabel = 'More actions',\n  trigger,\n  open,\n  onOpenChange,\n  onCloseAutoFocus,\n}: MoreActionsMenuProps) {\n  return (\n    <DropdownMenu open={open} onOpenChange={onOpenChange}>\n      <DropdownMenuTrigger asChild>\n        {trigger || (\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            className={\n              className || 'flex items-center justify-center border-ods-border bg-ods-card hover:bg-ods-bg-hover'\n            }\n            aria-label={ariaLabel}\n          >\n            <Ellipsis01Icon size={24} className=\"text-ods-text-primary\" />\n          </Button>\n        )}\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        align={align}\n        side={side}\n        sideOffset={sideOffset}\n        onCloseAutoFocus={onCloseAutoFocus}\n        className={cn('min-w-[200px] rounded-[4px] border border-ods-border bg-ods-card p-0', contentClassName)}\n      >\n        {items.map((item, idx) => {\n          const itemClassName =\n            'flex items-center gap-2 px-4 py-3 bg-ods-bg hover:bg-ods-bg-hover focus:bg-ods-bg-hover border-b border-ods-border last:border-b-0 rounded-none cursor-pointer data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed';\n\n          const content = (\n            <>\n              {item.icon && (\n                <div\n                  className={cn(\n                    item.danger ? 'text-ods-error' : 'text-ods-text-secondary',\n                    '[&_svg]:size-6 [&_svg]:shrink-0',\n                  )}\n                >\n                  {item.icon}\n                </div>\n              )}\n              <span className=\"text-ods-text-primary text-h4\">{item.label}</span>\n            </>\n          );\n\n          const handleActivate = (e: React.SyntheticEvent) => {\n            e.stopPropagation();\n            if (!item.disabled) item.onClick?.();\n          };\n\n          // Link variant — real <a href> in the DOM, visible to crawlers\n          if (item.href) {\n            return (\n              <DropdownMenuItem key={`${item.label}-${idx}`} asChild disabled={item.disabled} className={itemClassName}>\n                <Link\n                  href={item.href}\n                  target={item.openInNewTab ? '_blank' : undefined}\n                  rel={item.openInNewTab ? 'noopener noreferrer' : undefined}\n                  aria-disabled={item.disabled || undefined}\n                  tabIndex={item.disabled ? -1 : undefined}\n                  onClick={e => {\n                    if (item.disabled) {\n                      e.preventDefault();\n                      e.stopPropagation();\n                      return;\n                    }\n                    if (item.onClick) handleActivate(e);\n                  }}\n                >\n                  {content}\n                </Link>\n              </DropdownMenuItem>\n            );\n          }\n\n          // Button variant — onClick only\n          return (\n            <DropdownMenuItem\n              key={`${item.label}-${idx}`}\n              onClick={handleActivate}\n              disabled={item.disabled}\n              className={itemClassName}\n            >\n              {content}\n            </DropdownMenuItem>\n          );\n        })}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n","'use client';\n\nimport { Monitor } from 'lucide-react';\nimport type React from 'react';\nimport Link from '../../embed-shims/next-link';\nimport { cn } from '../../utils/cn';\nimport { EntityImage } from './entity-image';\n\nexport interface Organization {\n  id: string;\n  organizationId?: string;\n  name: string;\n  imageUrl?: string | null;\n  industry?: string;\n  tier?: string;\n  websiteUrl?: string;\n  description?: string;\n  totalDevices?: number;\n  activeDevices?: number;\n  mrrUsd?: number;\n  /** Extra host columns ride along untouched — the card reads only the\n   *  fields declared above. */\n  [key: string]: unknown;\n}\n\nexport interface OrganizationCardProps {\n  organization: Organization;\n  fetchedImageUrl?: string;\n  className?: string;\n  href?: string;\n  showActionButton?: boolean;\n  actionButton?: {\n    icon: React.ReactNode;\n    label: string;\n    onClick: (org: Organization, e: React.MouseEvent) => void;\n    variant?: 'ghost' | 'primary';\n    disabled?: boolean;\n  };\n  footerStats?: Array<{\n    icon?: React.ReactNode;\n    value: string | number;\n    label?: string;\n  }>;\n  customFooter?: React.ReactNode;\n  deviceCount?: number;\n}\n\nexport function OrganizationCard({\n  organization,\n  fetchedImageUrl,\n  className,\n  href,\n  showActionButton = false,\n  actionButton,\n  footerStats,\n  customFooter,\n  deviceCount,\n}: OrganizationCardProps) {\n  const handleActionClick = (e: React.MouseEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    actionButton?.onClick(organization, e);\n  };\n\n  const card = (\n    <div\n      className={cn(\n        'relative flex w-full flex-col gap-3 overflow-clip rounded-[6px] border border-ods-border bg-ods-card p-4',\n        'transition-colors',\n        href && 'cursor-pointer hover:border-ods-border-hover hover:bg-ods-card-hover',\n        className,\n      )}\n    >\n      {/* Device count (top-right) */}\n      {deviceCount !== undefined && (\n        <div className=\"absolute right-4 top-4 flex shrink-0 items-center gap-2\">\n          <Monitor className=\"h-4 w-4 text-ods-text-secondary\" />\n          <span className=\"text-ods-text-secondary text-h6\">{deviceCount.toLocaleString()} devices</span>\n        </div>\n      )}\n\n      {/* Action button (top-right) - only if no device count */}\n      {!deviceCount && showActionButton && actionButton && (\n        <button\n          className={cn(\n            'absolute right-2 top-2 z-10 flex h-8 w-8 items-center justify-center rounded transition-colors',\n            actionButton.variant === 'primary'\n              ? 'bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover'\n              : 'text-ods-text-secondary hover:bg-ods-error-secondary hover:text-ods-error',\n          )}\n          onClick={handleActionClick}\n          disabled={actionButton.disabled}\n          aria-label={actionButton.label}\n        >\n          {actionButton.icon}\n        </button>\n      )}\n\n      {/* Header */}\n      <div className=\"flex w-full items-start gap-3\">\n        <EntityImage src={fetchedImageUrl || organization.imageUrl} alt={organization.name} />\n\n        <div className=\"flex min-w-0 flex-1 flex-col justify-center py-2\">\n          <h3 className=\"truncate text-ods-text-primary transition-colors text-h3\" title={organization.name}>\n            {organization.name}\n          </h3>\n          <p\n            className=\"truncate text-ods-text-secondary text-h6\"\n            title={organization.industry || organization.tier || organization.websiteUrl || 'Organization'}\n          >\n            {organization.industry || organization.tier || organization.websiteUrl || 'Organization'}\n          </p>\n        </div>\n      </div>\n\n      {/* Description */}\n      {organization.description && (\n        <div className=\"h-12 w-full overflow-hidden\">\n          <p className=\"line-clamp-2 text-ods-text-primary text-h4\" title={organization.description}>\n            {organization.description}\n          </p>\n        </div>\n      )}\n\n      {/* Footer */}\n      {customFooter ? (\n        customFooter\n      ) : footerStats && footerStats.length > 0 ? (\n        <div className=\"flex w-full min-w-0 items-center justify-between gap-2\">\n          <div className=\"flex min-w-0 flex-shrink items-center gap-3 md:gap-4\">\n            {footerStats.map((stat, index) => (\n              <div key={index} className=\"flex flex-shrink-0 items-center gap-1\">\n                {stat.icon}\n                <span className=\"text-ods-text-primary text-h6\">\n                  {typeof stat.value === 'number' ? stat.value.toLocaleString() : stat.value}\n                </span>\n                {stat.label && <span className=\"text-ods-text-secondary text-h6\">{stat.label}</span>}\n              </div>\n            ))}\n          </div>\n        </div>\n      ) : null}\n    </div>\n  );\n\n  if (href) {\n    return (\n      <Link href={href} className=\"block text-inherit no-underline\">\n        {card}\n      </Link>\n    );\n  }\n\n  return card;\n}\n","'use client';\n\nimport { ExternalLink } from 'lucide-react';\nimport type React from 'react';\nimport { useMemo, useState } from 'react';\nimport { useIsTruncated } from '../../hooks/ui/use-is-truncated';\nimport { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';\nimport { cn } from '../../utils/cn';\nimport { EyeIcon, OpenFrameLogo } from '../icons';\nimport { Copy01Icon } from '../icons-v2-generated/documents';\nimport { CheckIcon } from '../icons-v2-generated/signs-and-symbols';\nimport { FloatingTooltip } from './floating-tooltip';\nimport { TruncateText } from './truncate-text';\n\nexport type ServiceCardRowAction = {\n  copy?: boolean;\n  open?: boolean;\n  reveal?: boolean;\n};\n\nexport type ServiceCardRow = {\n  label?: string;\n  value: string;\n  href?: string;\n  copyValue?: string;\n  isSecret?: boolean;\n  monospace?: boolean;\n  actions?: ServiceCardRowAction;\n};\n\nexport type ServiceCardTag = {\n  label: string;\n};\n\nexport interface ServiceCardProps {\n  title: string;\n  subtitle?: string;\n  icon?: React.ReactNode;\n  tag?: ServiceCardTag;\n  rows: ServiceCardRow[];\n  className?: string;\n}\n\nfunction MaskedValue({ value, isRevealed }: { value: string; isRevealed: boolean }) {\n  if (isRevealed) return <span>{value}</span>;\n  return <span>{'•'.repeat(Math.min(value.length, 12))}</span>;\n}\n\nexport function ServiceCard({ title, subtitle, icon, tag, rows, className }: ServiceCardProps) {\n  const resolvedIcon = icon ?? (\n    <OpenFrameLogo\n      className=\"h-10 w-10\"\n      lowerPathColor={'var(--color-accent-primary)'}\n      upperPathColor={'var(--color-text-primary)'}\n    />\n  );\n\n  return (\n    <div className={cn('rounded-lg border border-ods-border bg-ods-card p-6', className)}>\n      {/* Header */}\n      <div className=\"mb-4 flex items-start justify-between gap-4\">\n        <div className=\"flex min-w-0 items-center gap-4\">\n          <div className=\"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg\">\n            {resolvedIcon}\n          </div>\n          <div className=\"min-w-0\">\n            <TruncateText variant=\"h3\">{title}</TruncateText>\n            {subtitle && (\n              <TruncateText variant=\"h6\" tone=\"secondary\">\n                {subtitle}\n              </TruncateText>\n            )}\n          </div>\n        </div>\n        {tag && (\n          <div className=\"self-start whitespace-nowrap rounded-full border border-ods-border bg-ods-bg px-3 py-1 font-semibold text-ods-text-primary text-h6\">\n            {tag.label}\n          </div>\n        )}\n      </div>\n\n      {/* Rows */}\n      <div className=\"space-y-3\">\n        {rows.map((row, idx) => (\n          <ServiceCardRowItem key={idx} row={row} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction ServiceCardRowItem({ row }: { row: ServiceCardRow }) {\n  const [revealed, setRevealed] = useState(false);\n  const { copy, copied } = useCopyToClipboard();\n  const { ref: valueRef, isTruncated: valueTruncated } = useIsTruncated<HTMLDivElement>(row.value);\n  const actions = useMemo<ServiceCardRowAction>(\n    () => ({ copy: true, open: !!row.href, reveal: !!row.isSecret, ...row.actions }),\n    [row],\n  );\n\n  const displayValue = row.isSecret ? (\n    <MaskedValue value={row.value} isRevealed={revealed} />\n  ) : (\n    <span>{row.value}</span>\n  );\n\n  const handleCopy = () => copy(row.copyValue ?? row.value);\n\n  const openInNewTab = () => {\n    if (!row.href) return;\n    window.open(row.href, '_blank', 'noopener,noreferrer');\n  };\n\n  return (\n    <div className=\"flex min-w-0 items-center gap-3\">\n      {row.label && <div className=\"w-20 shrink-0 text-ods-text-primary text-h6 md:w-24\">{row.label}</div>}\n      <div\n        className={cn(\n          'flex h-12 min-w-0 flex-1 items-center justify-between rounded-md border border-ods-border bg-ods-bg px-3 md:px-4',\n          row.monospace ? 'font-mono' : '',\n        )}\n      >\n        {/* No tooltip while a secret is masked — the old native `title` leaked the raw value on hover. */}\n        <FloatingTooltip\n          content={row.value}\n          side=\"top\"\n          disabled={!valueTruncated || (row.isSecret && !revealed)}\n          triggerClassName=\"min-w-0\"\n          className=\"max-w-xs [overflow-wrap:anywhere]\"\n        >\n          <div ref={valueRef} className=\"truncate text-ods-text-primary\">\n            {displayValue}\n          </div>\n        </FloatingTooltip>\n        <div className=\"flex flex-shrink-0 items-center gap-2 pl-3\">\n          {actions.reveal && (\n            <button\n              onClick={() => setRevealed(v => !v)}\n              className=\"rounded p-2 text-ods-text-secondary hover:bg-ods-card\"\n              aria-label={revealed ? 'Hide' : 'Reveal'}\n            >\n              <EyeIcon className=\"h-5 w-5\" off={revealed} />\n            </button>\n          )}\n\n          {actions.copy && (\n            <button\n              onClick={handleCopy}\n              className={cn(\n                'rounded p-2 transition-colors hover:bg-ods-card',\n                copied ? 'text-ods-success' : 'text-ods-text-secondary',\n              )}\n              aria-label={`Copy ${row.label ?? 'value'}`}\n            >\n              {copied ? <CheckIcon size={20} /> : <Copy01Icon size={20} />}\n            </button>\n          )}\n\n          {actions.open && row.href && (\n            <button\n              onClick={openInNewTab}\n              className=\"rounded p-2 text-ods-text-secondary hover:bg-ods-card\"\n              aria-label={`Open ${row.label ?? 'link'}`}\n            >\n              <ExternalLink className=\"h-5 w-5\" />\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { type ChangeEvent, type InputHTMLAttributes, forwardRef } from 'react';\nimport { cn } from '../../utils/cn';\n\ninterface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {\n  value?: number[];\n  onValueChange?: (value: number[]) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n}\n\nconst Slider = forwardRef<HTMLInputElement, SliderProps>(\n  ({ className, value = [0], onValueChange, min = 0, max = 100, step = 1, ...props }, ref) => {\n    const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n      const newValue = [Number(e.target.value)];\n      onValueChange?.(newValue);\n    };\n\n    return (\n      <input\n        type=\"range\"\n        className={cn(\n          'flex h-5 w-full cursor-pointer appearance-none items-center rounded-lg bg-secondary outline-none',\n          '[&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary',\n          '[&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-primary',\n          className,\n        )}\n        ref={ref}\n        value={value[0]}\n        onChange={handleChange}\n        min={min}\n        max={max}\n        step={step}\n        {...props}\n      />\n    );\n  },\n);\nSlider.displayName = 'Slider';\n\nexport { Slider };\n","'use client';\n\nimport type React from 'react';\n\nexport interface TitleContentBlockProps {\n  title: string;\n  content: React.ReactNode;\n  titleColor?: string;\n  titleClassName?: string;\n  contentClassName?: string;\n  containerClassName?: string;\n  titleAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div';\n  contentAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'div';\n  spacing?: 'sm' | 'md' | 'lg';\n}\n\nconst spacingMap = {\n  sm: 'space-y-1',\n  md: 'space-y-2',\n  lg: 'space-y-4',\n};\n\nexport function TitleContentBlock({\n  title,\n  content,\n  titleColor = 'text-ods-text-secondary',\n  titleClassName = 'text-h5 tracking-[-0.28px]',\n  contentClassName = 'text-h2 tracking-[-0.64px] text-ods-text-primary',\n  containerClassName = '',\n  titleAs: TitleElement = 'div',\n  contentAs: ContentElement = 'h4',\n  spacing = 'md',\n}: TitleContentBlockProps) {\n  const titleClasses = `${titleClassName} ${titleColor}`;\n\n  return (\n    <div className={`${spacingMap[spacing]} ${containerClassName}`}>\n      <TitleElement className={titleClasses}>{title}</TitleElement>\n      <ContentElement className={contentClassName}>{content}</ContentElement>\n    </div>\n  );\n}\n","'use client';\n\nimport { type ReactElement, useCallback, useRef, useState } from 'react';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';\n\n/**\n * The one tooltip allowed open at a time, across every instance — tapping a\n * second trigger dismisses the first, since touch has no \"mouse left\" to close\n * it. Module-level on purpose: instances live in unrelated subtrees.\n */\nlet dismissOpenTooltip: (() => void) | null = null;\n\nexport interface TouchFriendlyTooltipProps {\n  /** Tooltip text; when absent the children render bare, with no trigger wiring. */\n  content?: string;\n  side?: 'top' | 'right' | 'bottom' | 'left';\n  children: ReactElement;\n}\n\n/**\n * Hover tooltip that also works on touch, where Radix's hover/focus model never\n * opens: a tap toggles it instead. Wraps its own `TooltipProvider`, so it can be\n * dropped anywhere without a provider up the tree.\n */\nexport function TouchFriendlyTooltip({ content, side = 'left', children }: TouchFriendlyTooltipProps) {\n  const [open, setOpen] = useState(false);\n  const isTouchRef = useRef(false);\n  const close = useCallback(() => setOpen(false), []);\n\n  if (!content) return children;\n\n  const toggleFromTouch = () => {\n    setOpen(prev => {\n      const next = !prev;\n      if (next) {\n        if (dismissOpenTooltip && dismissOpenTooltip !== close) dismissOpenTooltip();\n        dismissOpenTooltip = close;\n      } else if (dismissOpenTooltip === close) {\n        dismissOpenTooltip = null;\n      }\n      return next;\n    });\n  };\n\n  return (\n    <TooltipProvider delayDuration={150}>\n      <Tooltip open={open} onOpenChange={setOpen}>\n        <TooltipTrigger\n          asChild\n          onPointerDown={e => {\n            isTouchRef.current = e.pointerType === 'touch';\n            if (!isTouchRef.current) return;\n            e.preventDefault();\n            toggleFromTouch();\n          }}\n          onFocus={e => {\n            if (isTouchRef.current) e.preventDefault();\n          }}\n          onClick={e => {\n            if (isTouchRef.current) e.preventDefault();\n          }}\n        >\n          {children}\n        </TooltipTrigger>\n        <TooltipContent side={side} className=\"max-w-xs\">\n          {content}\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n","'use client';\n\nimport { cn } from '../../utils/cn';\n\nexport interface ContentLoaderProps {\n  /**\n   * The type of content being loaded\n   */\n  variant?: 'card' | 'form' | 'detail' | 'list';\n  /**\n   * Number of items to show in the skeleton\n   */\n  items?: number;\n  /**\n   * Show a title skeleton\n   */\n  showTitle?: boolean;\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n  /**\n   * Container CSS classes\n   */\n  containerClassName?: string;\n}\n\n/**\n * Unified content loader component that replaces duplicate loading patterns\n * Uses ODS design tokens for consistent theming\n */\nexport function ContentLoader({\n  variant = 'card',\n  items = 4,\n  showTitle = true,\n  className,\n  containerClassName,\n}: ContentLoaderProps) {\n  const renderCardSkeleton = () => (\n    <div className=\"animate-pulse\">\n      {showTitle && <div className=\"mb-6 h-8 w-64 rounded bg-ods-skeleton\" />}\n      <div className=\"rounded-lg border border-ods-border bg-ods-card p-6\">\n        <div className=\"mb-6 grid grid-cols-1 gap-6 md:grid-cols-4\">\n          {Array.from({ length: items }).map((_, i) => (\n            <div key={i}>\n              <div className=\"mb-2 h-4 w-20 rounded bg-ods-skeleton\" />\n              <div className=\"h-6 w-32 rounded bg-ods-skeleton\" />\n            </div>\n          ))}\n        </div>\n        <div className=\"border-t border-ods-border pt-4\">\n          <div className=\"h-4 w-64 rounded bg-ods-skeleton\" />\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderFormSkeleton = () => (\n    <div className=\"animate-pulse\">\n      {showTitle && <div className=\"mb-6 h-8 w-64 rounded bg-ods-card\" />}\n      <div className=\"rounded-lg bg-ods-card p-6\">\n        <div className=\"space-y-4\">\n          {Array.from({ length: items }).map((_, i) => (\n            <div key={i}>\n              <div className=\"mb-2 h-4 w-32 rounded bg-ods-skeleton\" />\n              <div className=\"h-10 rounded bg-ods-skeleton\" />\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderDetailSkeleton = () => (\n    <div className=\"animate-pulse\">\n      {showTitle && <div className=\"mb-6 h-8 w-64 rounded bg-ods-skeleton\" />}\n      <div className=\"rounded-lg border border-ods-border bg-ods-card p-6\">\n        <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n          <div className=\"lg:col-span-2\">\n            <div className=\"mb-4 h-64 rounded bg-ods-skeleton\" />\n            <div className=\"space-y-2\">\n              {Array.from({ length: 3 }).map((_, i) => (\n                <div key={i} className=\"h-4 rounded bg-ods-skeleton\" />\n              ))}\n            </div>\n          </div>\n          <div>\n            <div className=\"mb-4 h-32 rounded bg-ods-skeleton\" />\n            <div className=\"space-y-2\">\n              {Array.from({ length: items }).map((_, i) => (\n                <div key={i} className=\"h-3 rounded bg-ods-skeleton\" />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderListSkeleton = () => (\n    <div className=\"animate-pulse space-y-3\">\n      {showTitle && <div className=\"mb-6 h-8 w-64 rounded bg-ods-skeleton\" />}\n      {Array.from({ length: items }).map((_, i) => (\n        <div key={i} className=\"rounded-lg border border-ods-border bg-ods-card p-4\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"h-12 w-12 rounded bg-ods-skeleton\" />\n            <div className=\"flex-1\">\n              <div className=\"mb-2 h-4 w-3/4 rounded bg-ods-skeleton\" />\n              <div className=\"h-3 w-1/2 rounded bg-ods-skeleton\" />\n            </div>\n            <div className=\"h-8 w-20 rounded bg-ods-skeleton\" />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n\n  const renderSkeleton = () => {\n    switch (variant) {\n      case 'form':\n        return renderFormSkeleton();\n      case 'detail':\n        return renderDetailSkeleton();\n      case 'list':\n        return renderListSkeleton();\n      case 'card':\n      default:\n        return renderCardSkeleton();\n    }\n  };\n\n  return (\n    <div className={cn('p-6', containerClassName)}>\n      <div className={cn(className)}>{renderSkeleton()}</div>\n    </div>\n  );\n}\n\n// Export individual skeleton variants for specific use cases\nexport const CardLoader = (props: Omit<ContentLoaderProps, 'variant'>) => <ContentLoader {...props} variant=\"card\" />;\n\nexport const FormLoader = (props: Omit<ContentLoaderProps, 'variant'>) => <ContentLoader {...props} variant=\"form\" />;\n\nexport const DetailLoader = (props: Omit<ContentLoaderProps, 'variant'>) => (\n  <ContentLoader {...props} variant=\"detail\" />\n);\n\nexport const ListLoader = (props: Omit<ContentLoaderProps, 'variant'>) => <ContentLoader {...props} variant=\"list\" />;\n","'use client';\n\nimport { type ReactNode, useEffect, useRef } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Chevron02RightIcon } from '../../icons-v2-generated';\nimport { Pagination } from '../../pagination';\nimport { Button } from '../button';\nimport { CursorPagination } from '../cursor-pagination';\nimport { PlaceholderRows, ReservedEmptyState } from '../data-table/data-table-skeleton';\nimport { TableEmptyState } from './table-empty-state';\nimport { TableHeader } from './table-header';\nimport { TableRow } from './table-row';\nimport { COMPACT_ROW_MIN_HEIGHT, TableCardSkeleton } from './table-skeleton';\nimport type { RowAction, TableColumn, TableProps, TableRowData } from './types';\nimport { useTableMotion } from './use-table-motion';\n\n/**\n * Injects synthetic columns (row actions and/or row-level chevron link) at the end of the columns array.\n */\nfunction injectSyntheticColumns<T>(\n  columns: TableColumn<T>[],\n  rowActions?: RowAction<T>[],\n  renderRowActions?: (item: T) => ReactNode,\n  rowHref?: (item: T) => string | null | undefined,\n  actionsColumnWidth?: string,\n): TableColumn<T>[] {\n  const hasActions = Boolean(rowActions?.length) || Boolean(renderRowActions);\n  const result = [...columns];\n  // Captured OUTSIDE `renderCell` below. The cell renders long after this\n  // function returns, so no narrowing of the optional `rowActions` parameter\n  // survives into it; defaulting here keeps the render a plain array map.\n  // `hasActions` guarantees this is non-empty whenever `renderRowActions` is\n  // absent, so the `[]` fallback is the unreachable branch, not a silent drop.\n  const actions = rowActions ?? [];\n\n  if (hasActions) {\n    const actionsColumn: TableColumn<T> = {\n      key: '__actions__',\n      label: '',\n      // A caller-fixed width keeps the header and every row on ONE grid\n      // (see `actionsColumnWidth` in types.ts); the default stays\n      // content-sized for back-compat.\n      width: actionsColumnWidth ?? 'min-w-[100px] w-auto shrink-0 flex-none',\n      align: 'right',\n      renderCell: (item: T) => (\n        <div className=\"pointer-events-auto flex items-center justify-end gap-2\" data-no-row-click>\n          {renderRowActions\n            ? renderRowActions(item)\n            : actions.map((action, actionIndex) => (\n                <Button\n                  key={actionIndex}\n                  variant=\"outline\"\n                  onClick={e => {\n                    e.stopPropagation();\n                    action.onClick(item);\n                  }}\n                  leftIcon={action.icon}\n                  className={action.className}\n                >\n                  {action.label}\n                </Button>\n              ))}\n        </div>\n      ),\n    };\n    result.push(actionsColumn);\n  }\n\n  if (rowHref) {\n    const chevronColumn: TableColumn<T> = {\n      key: '__chevron__',\n      label: '',\n      width: 'w-12 shrink-0 flex-none',\n      align: 'right',\n      renderCell: (item: T) => {\n        const href = rowHref(item);\n        if (!href) return null;\n        return (\n          <div className=\"pointer-events-auto flex items-center justify-end\" data-no-row-click>\n            <Button\n              href={href}\n              prefetch={false}\n              variant=\"outline\"\n              size=\"icon\"\n              leftIcon={<Chevron02RightIcon className=\"h-6 w-6\" />}\n              aria-label=\"View details\"\n            />\n          </div>\n        );\n      },\n    };\n    result.push(chevronColumn);\n  }\n\n  return result;\n}\n\n/** @deprecated Use `DataTable` from `data-table` instead. */\nexport function Table<T = TableRowData>({\n  data,\n  columns,\n  rowKey,\n  loading = false,\n  emptyMessage,\n  skeletonRows = 10,\n  minRows,\n  emptyState,\n  keepHeightWhenEmpty,\n  emptyDescription,\n  className,\n  containerClassName,\n  headerClassName,\n  rowClassName,\n  compact,\n  onRowClick,\n  rowActions,\n  renderRowActions,\n  rowHref,\n  sortBy,\n  sortDirection,\n  onSort,\n  filters,\n  onFilterChange,\n  selectable,\n  selectedRows = [],\n  onSelectionChange,\n  bulkActions,\n  showToolbar,\n  cursorPagination,\n  pagePagination,\n  paginationClassName,\n  infiniteScroll,\n  stickyHeader,\n  stickyHeaderOffset,\n  animateRowReorder,\n  actionsColumnWidth,\n}: TableProps<T>) {\n  // Opt-in row-reorder animation: framer-motion is loaded lazily (its own chunk)\n  // ONLY when `animateRowReorder` is set, so the default table stays motion-free.\n  const tableMotion = useTableMotion(Boolean(animateRowReorder));\n  const columnsWithActions = injectSyntheticColumns(columns, rowActions, renderRowActions, rowHref, actionsColumnWidth);\n\n  // ONE resolution of the two vocabularies, so the render below never asks\n  // which spelling the caller used. `minRows` is the current one; the two\n  // deprecated props shipped a release earlier and hosts pin exact versions,\n  // so they keep working rather than silently doing nothing.\n  const reservedRows = minRows ?? (keepHeightWhenEmpty ? skeletonRows : undefined);\n  const resolvedEmptyState =\n    emptyState ?? (emptyDescription ? { title: emptyMessage, description: emptyDescription } : undefined);\n  const getRowHref = (item: T): string | undefined => {\n    if (onRowClick || !rowHref) return undefined;\n    return rowHref(item) ?? undefined;\n  };\n  const getRowKey = (item: T, index: number): string => {\n    if (typeof rowKey === 'function') {\n      return rowKey(item);\n    }\n    const key = item[rowKey];\n    return key?.toString() || index.toString();\n  };\n\n  const getRowClassName = (item: T, index: number): string => {\n    if (typeof rowClassName === 'function') {\n      return rowClassName(item, index);\n    }\n    return rowClassName || '';\n  };\n\n  const isRowSelected = (item: T) => {\n    if (!selectable || !selectedRows) return false;\n    const key = getRowKey(item, -1);\n    return selectedRows.some(row => getRowKey(row, -1) === key);\n  };\n\n  const handleSelectRow = (item: T) => {\n    if (!onSelectionChange) return;\n\n    const key = getRowKey(item, -1);\n    const isSelected = isRowSelected(item);\n\n    if (isSelected) {\n      onSelectionChange(selectedRows.filter(row => getRowKey(row, -1) !== key));\n    } else {\n      onSelectionChange([...selectedRows, item]);\n    }\n  };\n\n  const handleSelectAll = () => {\n    if (!onSelectionChange) return;\n\n    if (selectedRows.length === data.length) {\n      onSelectionChange([]);\n    } else {\n      onSelectionChange([...data]);\n    }\n  };\n\n  const allSelected = selectedRows.length > 0 && selectedRows.length === data.length;\n  const someSelected = selectedRows.length > 0 && selectedRows.length < data.length;\n\n  // Infinite scroll: IntersectionObserver on sentinel div\n  const sentinelRef = useRef<HTMLDivElement>(null);\n  // Refreshed after every commit, declared before the observer effect so it\n  // wins the same flush. Not in the render body: the reader is the\n  // IntersectionObserver callback, which cannot fire before a commit.\n  const onLoadMoreRef = useRef(infiniteScroll?.onLoadMore);\n  useEffect(() => {\n    onLoadMoreRef.current = infiniteScroll?.onLoadMore;\n  });\n\n  useEffect(() => {\n    if (!infiniteScroll?.hasNextPage || infiniteScroll.isFetchingNextPage) return undefined;\n    const sentinel = sentinelRef.current;\n    if (!sentinel) return undefined;\n\n    const observer = new IntersectionObserver(\n      entries => {\n        if (entries[0]?.isIntersecting) {\n          onLoadMoreRef.current?.();\n        }\n      },\n      { rootMargin: '200px' },\n    );\n    observer.observe(sentinel);\n    return () => observer.disconnect();\n  }, [infiniteScroll?.hasNextPage, infiniteScroll?.isFetchingNextPage]);\n\n  return (\n    <div className={cn('flex w-full flex-col gap-1', containerClassName)}>\n      {/* Toolbar for bulk actions */}\n      {showToolbar && bulkActions && selectedRows.length > 0 && (\n        <div className=\"mb-2 flex items-center justify-between rounded-[6px] border border-ods-border bg-ods-card p-3\">\n          <span className=\"text-ods-text-secondary text-h6\">\n            {selectedRows.length} item{selectedRows.length !== 1 ? 's' : ''} selected\n          </span>\n          <div className=\"flex gap-2\">\n            {bulkActions.map((action, index) => (\n              <button\n                key={index}\n                onClick={() => action.onClick(selectedRows)}\n                disabled={action.requiresSelection && selectedRows.length === 0}\n                className={cn(\n                  'rounded border px-3 py-1.5 transition-colors text-h6',\n                  'border-ods-border bg-ods-card text-ods-text-primary hover:bg-ods-bg-active',\n                  action.className,\n                )}\n              >\n                {action.icon}\n                {action.label}\n              </button>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {/* Desktop Header */}\n      <TableHeader\n        columns={columnsWithActions}\n        className={headerClassName}\n        sortBy={sortBy}\n        sortDirection={sortDirection}\n        onSort={onSort}\n        filters={filters}\n        onFilterChange={onFilterChange}\n        selectable={selectable}\n        allSelected={allSelected}\n        someSelected={someSelected}\n        onSelectAll={handleSelectAll}\n        totalItemsCount={data.length}\n        stickyHeader={stickyHeader}\n        stickyHeaderOffset={stickyHeaderOffset}\n      />\n\n      {/* Table Body */}\n      <div className={cn('flex w-full flex-col gap-2', className)}>\n        {loading && data.length === 0 ? (\n          <TableCardSkeleton\n            columns={columns}\n            rows={skeletonRows}\n            compact={compact}\n            hasActions={(rowActions?.length ?? 0) > 0}\n            hasChevron={Boolean(rowHref)}\n          />\n        ) : data.length === 0 ? (\n          /* `minRows` promises a STABLE height, and an empty table is exactly\n             when a collapsing one is most visible — the pagination and\n             everything under it jump the moment a filter matches nothing. So\n             reserve the same slots and centre the empty state over them,\n             identically to `DataTableBody`. */\n          reservedRows ? (\n            <ReservedEmptyState\n              count={reservedRows}\n              gapClassName=\"gap-2\"\n              innerHeightClassName={compact ? COMPACT_ROW_MIN_HEIGHT : undefined}\n            >\n              <TableEmptyState message={emptyMessage} emptyState={resolvedEmptyState} />\n            </ReservedEmptyState>\n          ) : (\n            <TableEmptyState message={emptyMessage} emptyState={resolvedEmptyState} />\n          )\n        ) : (\n          <>\n            {/* Real data rows. When `animateRowReorder` is on, wrap ONLY these\n                in a `LayoutGroup` so a reorder (same key, new order) animates via\n                FLIP — the invisible placeholder rows below are intentionally left\n                outside it. `AnimatePresence` is not needed here: the row set is\n                stable across a reorder (no enter/exit); add it if a future task\n                animates rows being added/removed. */}\n            {(() => {\n              const rows = data.map((item, index) => (\n                <TableRow\n                  key={getRowKey(item, index)}\n                  item={item}\n                  columns={columnsWithActions}\n                  onClick={onRowClick}\n                  href={getRowHref(item)}\n                  className={getRowClassName(item, index)}\n                  index={index}\n                  compact={compact}\n                  selectable={selectable}\n                  selected={isRowSelected(item)}\n                  onSelect={handleSelectRow}\n                  animateRowReorder={animateRowReorder}\n                  motionDiv={tableMotion?.motionDiv}\n                />\n              ));\n              // LayoutGroup only once framer-motion has lazily resolved; until\n              // then (and when off) rows render as plain `<div>`s.\n              const LayoutGroup = tableMotion?.LayoutGroup;\n              return animateRowReorder && LayoutGroup ? <LayoutGroup>{rows}</LayoutGroup> : rows;\n            })()}\n            {/* Infinite scroll: skeleton rows */}\n            {infiniteScroll?.isFetchingNextPage && (\n              <TableCardSkeleton\n                columns={columns}\n                rows={infiniteScroll.skeletonRows ?? 3}\n                hasActions={(rowActions?.length ?? 0) > 0}\n                hasChevron={Boolean(rowHref)}\n              />\n            )}\n            {/* Infinite scroll: sentinel element */}\n            {infiniteScroll?.hasNextPage && <div ref={sentinelRef} className=\"h-1\" aria-hidden=\"true\" />}\n            {/* Invisible placeholder rows to maintain consistent table height\n                (disabled for infinite scroll). `minRows` when the host opted\n                in, else the legacy pad-to-`skeletonRows`. */}\n            {!infiniteScroll && (\n              <PlaceholderRows\n                count={Math.max(0, (reservedRows ?? skeletonRows) - data.length)}\n                innerHeightClassName={compact ? COMPACT_ROW_MIN_HEIGHT : undefined}\n              />\n            )}\n          </>\n        )}\n      </div>\n\n      {/* Pagination - only show when there's data and infinite scroll is not active */}\n      {!infiniteScroll && cursorPagination && data.length > 0 && (\n        <CursorPagination\n          hasNextPage={cursorPagination.hasNextPage}\n          hasPreviousPage={cursorPagination.hasPreviousPage}\n          isFirstPage={cursorPagination.isFirstPage}\n          startCursor={cursorPagination.startCursor}\n          endCursor={cursorPagination.endCursor}\n          currentCount={cursorPagination.currentCount ?? data.length}\n          totalCount={cursorPagination.totalCount}\n          itemName={cursorPagination.itemName}\n          loading={loading}\n          onNext={cursorPagination.onNext}\n          onPrevious={cursorPagination.onPrevious}\n          onReset={cursorPagination.onReset}\n          showInfo={cursorPagination.showInfo ?? true}\n          compact={cursorPagination.compact}\n          resetButtonLabel={cursorPagination.resetButtonLabel}\n          resetButtonIcon={cursorPagination.resetButtonIcon}\n          className={cn('mt-2 border-t border-ods-border pt-3', paginationClassName)}\n        />\n      )}\n\n      {!infiniteScroll && pagePagination && !cursorPagination && data.length > 0 && (\n        <div className={cn('mt-2 border-t border-ods-border pt-3', paginationClassName)}>\n          <Pagination\n            currentPage={pagePagination.currentPage}\n            totalPages={pagePagination.totalPages}\n            onPageChange={pagePagination.onPageChange}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { ChevronLeft, ChevronRight, RotateCcw, Home } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { Button } from './button';\n\nexport interface CursorPaginationProps {\n  hasNextPage: boolean;\n  hasPreviousPage?: boolean;\n  isFirstPage?: boolean;\n  startCursor?: string | null;\n  endCursor?: string | null;\n\n  currentCount?: number;\n  totalCount?: number | null;\n  itemName?: string;\n\n  loading?: boolean;\n\n  onNext?: (cursor: string) => void;\n  onPrevious?: (cursor: string) => void;\n  onReset?: () => void;\n\n  className?: string;\n  showInfo?: boolean;\n  compact?: boolean;\n  resetButtonLabel?: string;\n  resetButtonIcon?: 'home' | 'rotate';\n}\n\nexport function CursorPagination({\n  hasNextPage,\n  hasPreviousPage,\n  isFirstPage = true,\n  startCursor,\n  endCursor,\n  currentCount,\n  totalCount,\n  itemName = 'items',\n  loading = false,\n  onNext,\n  onPrevious,\n  onReset,\n  className,\n  showInfo = true,\n  compact = false,\n  resetButtonLabel = 'First',\n  resetButtonIcon = 'home',\n}: CursorPaginationProps) {\n  const handleNext = () => {\n    if (hasNextPage && endCursor && onNext) {\n      onNext(endCursor);\n    }\n  };\n\n  const handlePrevious = () => {\n    if (hasPreviousPage && startCursor && onPrevious) {\n      onPrevious(startCursor);\n    }\n  };\n\n  const handleReset = () => {\n    if (onReset) {\n      onReset();\n    }\n  };\n\n  const ResetIcon = resetButtonIcon === 'rotate' ? RotateCcw : Home;\n\n  const getDisplayText = () => {\n    if (!showInfo || !currentCount) return null;\n\n    if (totalCount) {\n      return `Showing ${currentCount} of ${totalCount} ${itemName}`;\n    } else if (hasNextPage) {\n      return `Showing ${currentCount}+ ${itemName}`;\n    } else {\n      return `Showing ${currentCount} ${itemName}`;\n    }\n  };\n\n  const displayText = getDisplayText();\n\n  return (\n    <div className={cn('flex items-center justify-between gap-4', compact ? 'py-2' : 'py-3', className)}>\n      {/* Info text */}\n      {showInfo && <div className=\"text-ods-text-secondary text-h6\">{displayText || <span>&nbsp;</span>}</div>}\n\n      {/* Navigation buttons */}\n      <div className=\"flex items-center gap-2\">\n        {onReset ? (\n          <Button\n            variant=\"outline\"\n            size={compact ? 'small-legacy' : 'default'}\n            onClick={handleReset}\n            disabled={isFirstPage || loading}\n            className={cn(compact && 'h-8 px-3')}\n            leftIcon={<ResetIcon className=\"h-4 w-4\" />}\n            aria-label=\"Go to first page\"\n          >\n            <span className={compact ? 'hidden md:inline' : ''}>{resetButtonLabel}</span>\n          </Button>\n        ) : (\n          hasPreviousPage !== undefined &&\n          onPrevious && (\n            <Button\n              variant=\"outline\"\n              size={compact ? 'small-legacy' : 'default'}\n              onClick={handlePrevious}\n              disabled={!hasPreviousPage || loading}\n              className={cn(compact && 'h-8 px-3')}\n              leftIcon={<ChevronLeft className=\"h-4 w-4\" />}\n              aria-label=\"Go to previous page\"\n            >\n              <span className={compact ? 'hidden md:inline' : ''}>Previous</span>\n            </Button>\n          )\n        )}\n\n        <Button\n          variant=\"outline\"\n          size={compact ? 'small-legacy' : 'default'}\n          onClick={handleNext}\n          disabled={!hasNextPage || loading}\n          className={cn(compact && 'h-8 px-3')}\n          rightIcon={<ChevronRight className=\"h-4 w-4\" />}\n          aria-label=\"Go to next page\"\n        >\n          <span className={compact ? 'hidden md:inline' : ''}>Next</span>\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nexport function CursorPaginationSimple({\n  hasNextPage,\n  hasPreviousPage,\n  isFirstPage = true,\n  onNext,\n  onPrevious,\n  onReset,\n  loading = false,\n  className,\n}: Pick<\n  CursorPaginationProps,\n  'hasNextPage' | 'hasPreviousPage' | 'isFirstPage' | 'onNext' | 'onPrevious' | 'onReset' | 'loading' | 'className'\n>) {\n  return (\n    <div className={cn('flex items-center gap-1', className)}>\n      {onReset ? (\n        <Button\n          variant=\"transparent\"\n          size=\"icon\"\n          onClick={onReset}\n          disabled={isFirstPage || loading}\n          className=\"h-8 w-8\"\n          leftIcon={<Home className=\"h-4 w-4\" />}\n          aria-label=\"First\"\n        />\n      ) : (\n        hasPreviousPage !== undefined &&\n        onPrevious && (\n          <Button\n            variant=\"transparent\"\n            size=\"icon\"\n            onClick={() => onPrevious?.('')}\n            disabled={!hasPreviousPage || loading}\n            className=\"h-8 w-8\"\n            leftIcon={<ChevronLeft className=\"h-4 w-4\" />}\n            aria-label=\"Previous\"\n          />\n        )\n      )}\n\n      <Button\n        variant=\"transparent\"\n        size=\"icon\"\n        onClick={() => onNext?.('')}\n        disabled={!hasNextPage || loading}\n        className=\"h-8 w-8\"\n        rightIcon={<ChevronRight className=\"h-4 w-4\" />}\n        aria-label=\"Next\"\n      />\n    </div>\n  );\n}\n","'use client';\n\nimport type { Table } from '@tanstack/react-table';\nimport { createContext, useContext, type ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport './types';\n\n// The context holds the table at its ERASED row type; each `useDataTableContext<T>`\n// call re-applies the consumer's own `T` (the sub-components are always\n// rendered inside a `<DataTable table={...}>` of that same `T`).\nconst DataTableContext = createContext<Table<unknown> | null>(null);\n\nexport function useDataTableContext<T = unknown>(): Table<T> {\n  const ctx = useContext(DataTableContext);\n  if (!ctx) {\n    throw new Error('<DataTable.Header/Body/...> must be used inside <DataTable table={...}>');\n  }\n  return ctx as Table<T>;\n}\n\nexport interface DataTableProps<T> {\n  table: Table<T>;\n  children: ReactNode;\n  className?: string;\n}\n\nexport function DataTableRoot<T>({ table, children, className }: DataTableProps<T>) {\n  // Erase the row type on the way in; `useDataTableContext<T>` restores it on\n  // the way out. `Table<T>` is invariant in `T`, so this assertion pair is\n  // what an erased context costs — it replaces a `Table<any>` context that\n  // erased the row type for every consumer, not just at the seam.\n  return (\n    <DataTableContext.Provider value={table as Table<unknown>}>\n      <div className={cn('flex w-full flex-col', className)}>{children}</div>\n    </DataTableContext.Provider>\n  );\n}\n","import type { FilterFn } from '@tanstack/react-table';\nimport type { TailwindBreakpoint } from './types';\n\n/** Ascending breakpoint scale `hideAt` is expressed in. */\nexport const BREAKPOINT_ORDER: TailwindBreakpoint[] = ['md', 'lg', 'xl', '2xl'];\n\n/**\n * Generates Tailwind hide classes based on breakpoint configuration.\n *\n * @example\n * getHideClasses('md')           // 'hidden md:flex'\n * getHideClasses(['md', 'lg'])   // 'md:hidden lg:hidden xl:flex'\n */\nexport function getHideClasses(hideAt?: TailwindBreakpoint | TailwindBreakpoint[]): string {\n  if (!hideAt) return '';\n\n  const breakpoints = Array.isArray(hideAt) ? hideAt : [hideAt];\n\n  if (breakpoints.length === 1) {\n    return `hidden ${breakpoints[0]}:flex`;\n  }\n\n  const maxIdx = Math.max(...breakpoints.map(bp => BREAKPOINT_ORDER.indexOf(bp)));\n  const hideClasses = breakpoints.map(bp => `${bp}:hidden`).join(' ');\n  const showBreakpoint = BREAKPOINT_ORDER[maxIdx + 1];\n  return showBreakpoint ? `${hideClasses} ${showBreakpoint}:flex` : hideClasses;\n}\n\n/**\n * The visibility `getHideClasses(hideAt)` actually produces, evaluated per\n * breakpoint: `[base, md, lg, xl, 2xl]`.\n *\n * The header needs to *know* this rather than emit it, because its cells carry a\n * second rule on top (only filterable columns show below `lg`) and the two cannot\n * be layered as classes — Tailwind orders `max-*` media queries BEFORE `min-*`\n * ones, so a `max-lg:` override loses to any `md:`/`lg:` class it is meant to\n * cancel. Combining the two rules here, on plain booleans, sidesteps that\n * entirely: the header emits one unambiguous min-width ladder.\n *\n * Must stay in step with `getHideClasses` — the body rows and the skeleton apply\n * those classes directly, and a disagreement is a header that shows a different\n * set of columns than the rows beneath it.\n */\nexport function getHideAtVisibility(hideAt?: TailwindBreakpoint | TailwindBreakpoint[]): boolean[] {\n  const steps = BREAKPOINT_ORDER.length + 1;\n  if (!hideAt) return Array.from({ length: steps }, () => true);\n\n  const breakpoints = Array.isArray(hideAt) ? hideAt : [hideAt];\n\n  // Single breakpoint → `hidden <bp>:flex`: hidden below it, visible from it.\n  if (breakpoints.length === 1) {\n    const showFrom = BREAKPOINT_ORDER.indexOf(breakpoints[0]) + 1;\n    return Array.from({ length: steps }, (_, step) => step >= showFrom);\n  }\n\n  // Array → one `<bp>:hidden` per entry plus a single `<next>:flex`, and no bare\n  // `hidden`, so the base step stays visible. Walk the scale upwards: later\n  // classes win, and the show breakpoint always sits past every hide one.\n  const maxIdx = Math.max(...breakpoints.map(bp => BREAKPOINT_ORDER.indexOf(bp)));\n  const showBreakpoint = BREAKPOINT_ORDER[maxIdx + 1];\n  const visible = [true];\n  for (const bp of BREAKPOINT_ORDER) {\n    let state = visible[visible.length - 1];\n    if (breakpoints.includes(bp)) state = false;\n    if (bp === showBreakpoint) state = true;\n    visible.push(state);\n  }\n  return visible;\n}\n\n/** Maps `meta.align` to flex justify classes. */\nexport function alignJustify(align?: 'left' | 'center' | 'right'): string {\n  switch (align) {\n    case 'center':\n      return 'justify-center text-center';\n    case 'right':\n      return 'justify-end text-right';\n    default:\n      return 'justify-start text-left';\n  }\n}\n\n/**\n * Default `filterFn` for columns that use `meta.filter` with multi-select options.\n * Passes the row when the selected filter values contain the row's cell value.\n *\n * Use with `clientSideFiltering: true` on `useDataTable`. For server-side filtering\n * (Relay/REST), TanStack doesn't call `filterFn` at all — `manualFiltering` is on\n * and you translate `columnFilters` state into backend query variables yourself.\n *\n * @example\n * const columns: ColumnDef<Device>[] = [\n *   {\n *     accessorKey: 'status',\n *     header: 'Status',\n *     filterFn: multiSelectFilterFn,\n *     meta: { filter: { options: STATUS_OPTIONS } },\n *   },\n * ]\n */\n// Row-type-agnostic: only `row.getValue(columnId)` is touched, never a row\n// field, so the filter is declared at the erased row type and works for any\n// `ColumnDef<T>` it is attached to.\nexport const multiSelectFilterFn: FilterFn<unknown> = (row, columnId, filterValue) => {\n  const values = filterValue as Array<string | number | boolean> | undefined;\n  if (!values || values.length === 0) return true;\n  const cellValue = row.getValue(columnId);\n  return values.some(v => v === cellValue || String(v) === String(cellValue));\n};\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { useDataTableContext } from './data-table';\nimport { getHideClasses } from './utils';\n\n/**\n * Consistent INNER row heights. The row card wraps these in a 1px border on\n * each side, so the outer block lands on the designed 68px / 80px total —\n * hence the 66/78 values here.\n */\nexport const ROW_HEIGHT_DESKTOP = 'h-[66px] md:h-[78px]';\nexport const ROW_HEIGHT_MOBILE = 'h-[66px]';\n\n/**\n * The row shell's inset and column gap.\n *\n * Shared by the real row (`data-table-row`), the skeleton rows below and the\n * pad rows in `data-table-body`, because a difference between them is a visible\n * shift on load — and there WAS one: all three placeholders used\n * `--spacing-system-sf` (12px) in their mobile variant while the real row uses\n * `--spacing-system-mf` (16px) at every width, so a loading table sat 4px closer\n * to the card edge than the loaded one and its content nudged over when data\n * arrived. Kept as a constant rather than three copies so that cannot drift again.\n */\nexport const ROW_SHELL_CLASSES = 'items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]';\n\nexport interface DataTableSkeletonProps {\n  rows?: number;\n  className?: string;\n  rowClassName?: string;\n  /**\n   * REPLACES the design row height, for a table whose rows are not that tall.\n   * Sizes the CARD, as a real row does.\n   * Appending a height through `rowClassName` cannot do this: tailwind-merge\n   * drops the plain `h-[66px]` but keeps `md:h-[78px]`, so the override held\n   * on a phone and lost on a desktop. See `DataTableBodyProps.rowHeightClassName`.\n   */\n  rowHeightClassName?: string;\n}\n\nexport function DataTableSkeleton({ rows = 10, className, rowClassName, rowHeightClassName }: DataTableSkeletonProps) {\n  const table = useDataTableContext();\n  const columns = table.getVisibleFlatColumns();\n  const firstColumnId = columns[0]?.id;\n\n  return (\n    <>\n      {Array.from({ length: rows }).map((_, index) => (\n        <div\n          key={index}\n          className={cn(\n            'relative animate-pulse overflow-hidden rounded-md border border-ods-border bg-ods-card',\n            // The slot height sits on the card, matching a real row.\n            rowHeightClassName,\n            className,\n          )}\n        >\n          <div\n            className={cn(\n              'hidden py-0 md:flex',\n              ROW_SHELL_CLASSES,\n              rowHeightClassName ? 'h-full' : ROW_HEIGHT_DESKTOP,\n              rowClassName,\n            )}\n          >\n            {columns.map(column => {\n              const meta = column.columnDef.meta;\n              return (\n                <div\n                  key={column.id}\n                  className={cn(\n                    'flex shrink-0 flex-col justify-center',\n                    meta?.width || 'flex-1',\n                    // Same responsive hiding the real header and row apply.\n                    // Without it a `hideAt` column was drawn in the skeleton and\n                    // absent from the loaded table, so below that breakpoint the\n                    // remaining columns were sized against a different total and\n                    // the whole row re-laid-out the moment data arrived.\n                    getHideClasses(meta?.hideAt),\n                  )}\n                >\n                  <div className=\"mb-[var(--spacing-system-xxs)] h-5 w-3/4 rounded-sm bg-ods-bg-surface\" />\n                  {index % 2 === 0 && column.id === firstColumnId && (\n                    <div className=\"h-4 w-1/2 rounded-sm bg-ods-bg-surface opacity-60\" />\n                  )}\n                </div>\n              );\n            })}\n          </div>\n          <div\n            className={cn(\n              'flex justify-start py-0 md:hidden',\n              ROW_SHELL_CLASSES,\n              rowHeightClassName ? 'h-full' : ROW_HEIGHT_MOBILE,\n              rowClassName,\n            )}\n          >\n            <div className=\"flex min-w-0 flex-1 flex-col justify-center py-[var(--spacing-system-sf)]\">\n              <div className=\"mb-[var(--spacing-system-xsf)] h-4 w-3/4 rounded-sm bg-ods-bg-surface\" />\n              <div className=\"h-3 w-1/2 rounded-sm bg-ods-bg-surface opacity-60\" />\n            </div>\n          </div>\n        </div>\n      ))}\n    </>\n  );\n}\n\n/**\n * The empty state, floating centred over a full page's worth of reserved row\n * slots. Both table bodies render this — an empty table is exactly when a\n * collapsing one is most visible (the pagination and everything under it jump\n * the moment a filter matches nothing), and two hand-matched copies of the\n * wrapper would drift the way the two placeholder components already had.\n */\nexport function ReservedEmptyState({\n  count,\n  gapClassName,\n  rowHeightClassName,\n  innerHeightClassName,\n  children,\n}: {\n  count: number;\n  gapClassName: string;\n  rowHeightClassName?: string;\n  innerHeightClassName?: string;\n  children: ReactNode;\n}) {\n  return (\n    <div className={cn('relative flex w-full flex-col', gapClassName)}>\n      <PlaceholderRows\n        count={count}\n        rowHeightClassName={rowHeightClassName}\n        innerHeightClassName={innerHeightClassName}\n      />\n      {/* `sticky left-0` + `w-screen max-w-full`: hosts wrap tables in a\n          horizontal scroller with a forced min-width, and an overlay centred on\n          that CANVAS puts the message off-screen on a phone. This centres it on\n          the visible area instead, and collapses to plain centring when the\n          table is not wider than its container. */}\n      <div className=\"absolute inset-0 flex items-center\">\n        <div className=\"sticky left-0 flex w-screen max-w-full justify-center\">{children}</div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * Invisible rows that occupy exactly one row slot each.\n *\n * THE height reservation for a table whose page is short or empty — every call\n * site uses it (data-table's pad + empty branches AND the legacy `Table`'s), so\n * a padded page and an empty one are the same height by construction rather\n * than by matching guesses in each component.\n */\nexport function PlaceholderRows({\n  count,\n  rowHeightClassName,\n  innerHeightClassName,\n}: {\n  count: number;\n  /** Height on the CARD, as `DataTableRow` carries it. */\n  rowHeightClassName?: string;\n  /** Height on the INNER row instead — the legacy `Table` puts it there and\n   *  lets the card's border add the remaining 2px, so its pad rows must too. */\n  innerHeightClassName?: string;\n}) {\n  return (\n    <>\n      {Array.from({ length: count }).map((_, i) => (\n        <div\n          key={`placeholder-${i}`}\n          // `border-transparent`, not no border: a real row card draws a 1px\n          // border on each side, so its outer block is the inner height PLUS 2.\n          // A pad row without one is 2px shorter than the row it stands in for\n          // — invisible per row, 54px over a 27-row remainder, which is the\n          // same shift `minRows` exists to prevent.\n          className={cn(\n            'pointer-events-none relative overflow-hidden rounded-md border border-transparent',\n            // The stated slot height belongs on the CARD, exactly as a real\n            // row carries it — a real row with a sub-row is taller than its\n            // cells, so reserving the cells' height here left every pad row\n            // short by the sub-row.\n            rowHeightClassName,\n          )}\n          aria-hidden=\"true\"\n        >\n          <div\n            className={cn(\n              'hidden py-0 md:flex',\n              ROW_SHELL_CLASSES,\n              innerHeightClassName ?? (rowHeightClassName ? 'h-full' : ROW_HEIGHT_DESKTOP),\n            )}\n          />\n          <div\n            className={cn(\n              'flex justify-start py-0 md:hidden',\n              ROW_SHELL_CLASSES,\n              innerHeightClassName ?? (rowHeightClassName ? 'h-full' : ROW_HEIGHT_MOBILE),\n            )}\n          />\n        </div>\n      ))}\n    </>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport { SearchIcon } from '../../icons-v2-generated';\nimport { NoData } from '../no-data';\nimport type { TableEmptyStateProps } from './types';\n\n/** @deprecated Use `DataTableEmpty` from `data-table` instead. */\nexport function TableEmptyState({ message, description, emptyState, icon, action, className }: TableEmptyStateProps) {\n  // Two vocabularies, one component. `description` is the shipped single-line\n  // prop; `emptyState` is the full NoData bag (icon, title, description,\n  // actions) and, spread last, wins over both — exactly as in `DataTableBody`.\n  return (\n    <NoData\n      icon={icon ?? <SearchIcon />}\n      title={message ?? 'No results found'}\n      description={description ?? (message == null ? 'Try adjusting your search or filters' : undefined)}\n      buttonLabel={action?.label}\n      onButtonClick={action?.onClick}\n      className={cn('py-[var(--spacing-system-xxl)]', className)}\n      {...emptyState}\n    />\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport { FiltersDropdown } from '../../features';\nimport { Filter02Icon } from '../../icons-v2-generated';\nimport type { FilterOption, TableFilters } from './types';\n\n/** @deprecated Use `DataTableColumnFilter` from `data-table` instead. */\nexport interface TableColumnFilterDropdownProps {\n  /** Column key used for filter state */\n  columnKey: string;\n  /** Column label for display and accessibility */\n  columnLabel: string;\n  /** Available filter options for the column */\n  filterOptions: FilterOption[];\n  /** Current filter state for all columns */\n  filters?: TableFilters;\n  /** Called when filters change */\n  onFilterChange: (filters: TableFilters) => void;\n  /** Dropdown placement */\n  placement?: 'bottom-start' | 'bottom-end' | 'bottom';\n  /** Additional class for the dropdown */\n  dropdownClassName?: string;\n}\n\n/** @deprecated Use `DataTableColumnFilter` from `data-table` instead. */\nexport function TableColumnFilterDropdown({\n  columnKey,\n  columnLabel,\n  filterOptions,\n  filters,\n  onFilterChange,\n  placement = 'bottom-start',\n  dropdownClassName = 'min-w-[240px]',\n}: TableColumnFilterDropdownProps) {\n  const activeCount = filters?.[columnKey]?.length || 0;\n\n  return (\n    <FiltersDropdown\n      triggerElement={\n        <div\n          className=\"flex cursor-pointer items-center gap-1.5 rounded px-1 py-0.5 transition-all duration-200 hover:bg-ods-bg-active\"\n          aria-label={`Filter by ${columnLabel}`}\n        >\n          <span className=\"select-none text-ods-text-secondary text-h5\">{columnLabel}</span>\n          <Filter02Icon\n            className={cn('h-4 w-4 transition-colors', activeCount > 0 ? 'text-ods-accent' : 'text-ods-text-secondary')}\n          />\n        </div>\n      }\n      sections={[\n        {\n          id: columnKey,\n          title: columnLabel,\n          type: 'checkbox',\n          options: filterOptions,\n          allowSelectAll: true,\n        },\n      ]}\n      onApply={appliedFilters => {\n        onFilterChange({\n          ...filters,\n          [columnKey]: appliedFilters[columnKey] || [],\n        });\n      }}\n      onReset={() => {\n        const newFilters = { ...filters };\n        delete newFilters[columnKey];\n        onFilterChange(newFilters);\n      }}\n      currentFilters={{ [columnKey]: filters?.[columnKey] || [] }}\n      placement={placement}\n      dropdownClassName={dropdownClassName}\n    />\n  );\n}\n","import type { TableColumn, TailwindBreakpoint } from './types';\n\n/** @deprecated Use `data-table` instead. */\nexport function getHideClasses(hideAt?: TailwindBreakpoint | TailwindBreakpoint[]): string {\n  if (!hideAt) {\n    return '';\n  }\n\n  // Convert single breakpoint to array for uniform handling\n  const breakpoints = Array.isArray(hideAt) ? hideAt : [hideAt];\n\n  // Breakpoint order for Tailwind (smallest to largest)\n  const breakpointOrder: TailwindBreakpoint[] = ['md', 'lg', 'xl', '2xl'];\n\n  // Find the largest breakpoint in the hideAt array\n  const maxBreakpointIndex = Math.max(...breakpoints.map(bp => breakpointOrder.indexOf(bp)));\n\n  // For single breakpoint: hide below, show at and above\n  // e.g., hideAt: 'md' -> 'hidden md:flex'\n  if (breakpoints.length === 1) {\n    return `hidden ${breakpoints[0]}:flex`;\n  }\n\n  // For multiple breakpoints: hide at specified breakpoints, show after the largest one\n  // e.g., hideAt: ['md', 'lg'] -> 'md:hidden lg:hidden xl:flex'\n  const hideClasses = breakpoints.map(bp => `${bp}:hidden`).join(' ');\n\n  // Find next breakpoint after the max to show the column\n  const nextBreakpointIndex = maxBreakpointIndex + 1;\n  const showBreakpoint = breakpointOrder[nextBreakpointIndex];\n\n  if (showBreakpoint) {\n    return `${hideClasses} ${showBreakpoint}:flex`;\n  }\n\n  // If no next breakpoint (e.g., hideAt: '2xl'), just hide\n  return hideClasses;\n}\n\n/** @deprecated Use `data-table` instead. */\nexport function isHiddenOnMobile<T>(column: TableColumn<T>): boolean {\n  if (!column.hideAt) {\n    return false;\n  }\n\n  const breakpoints = Array.isArray(column.hideAt) ? column.hideAt : [column.hideAt];\n\n  // Check if 'md' is in the list (mobile breakpoints)\n  return breakpoints.some(bp => bp === 'md');\n}\n","'use client';\n\nimport { useLgUp } from '../../../hooks/ui/use-media-query';\nimport { cn } from '../../../utils/cn';\nimport { Arrow01DownIcon } from '../../icons-v2-generated/arrows/arrow-01-down-icon';\nimport { Arrow01UpIcon } from '../../icons-v2-generated/arrows/arrow-01-up-icon';\nimport { SwitchVrIcon } from '../../icons-v2-generated/arrows/switch-vr-icon';\nimport { Checkbox } from '../checkbox';\nimport { TableColumnFilterDropdown } from './table-column-filter-dropdown';\nimport type { TableColumn, TableHeaderProps, TableRowData } from './types';\nimport { getHideClasses } from './utils';\n\n/** @deprecated Use `DataTableHeader` from `data-table` instead. */\nexport function TableHeader<T = TableRowData>({\n  columns,\n  className,\n  sortBy,\n  sortDirection,\n  onSort,\n  filters,\n  onFilterChange,\n  selectable,\n  allSelected,\n  someSelected,\n  onSelectAll,\n  totalItemsCount,\n  stickyHeader,\n  stickyHeaderOffset,\n}: TableHeaderProps<T>) {\n  const getAlignment = (align?: 'left' | 'center' | 'right') => {\n    switch (align) {\n      case 'center':\n        return 'justify-center';\n      case 'right':\n        return 'justify-end';\n      default:\n        return 'justify-start';\n    }\n  };\n\n  const handleSort = (column: TableColumn<T>) => {\n    if (!column.sortable || !onSort) return;\n\n    const columnKey = column.sortKey || column.key;\n    let newDirection: 'asc' | 'desc' = 'asc';\n\n    if (sortBy === columnKey) {\n      newDirection = sortDirection === 'asc' ? 'desc' : 'asc';\n    }\n\n    onSort(columnKey, newDirection);\n  };\n\n  const getSortIcon = (column: TableColumn<T>) => {\n    if (!column.sortable) return null;\n\n    const columnKey = column.sortKey || column.key;\n    const isActive = sortBy === columnKey;\n\n    if (!isActive) {\n      return <SwitchVrIcon className=\"h-4 w-4 text-ods-text-secondary\" />;\n    }\n\n    return sortDirection === 'asc' ? (\n      <Arrow01UpIcon className=\"h-4 w-4 text-ods-accent\" />\n    ) : (\n      <Arrow01DownIcon className=\"h-4 w-4 text-ods-accent\" />\n    );\n  };\n\n  const isLgUp = useLgUp() ?? false;\n\n  return (\n    <div\n      className={cn(\n        'relative hidden h-11 items-center gap-4 px-4 py-3 md:flex',\n        stickyHeader && `sticky z-10 bg-ods-bg ${stickyHeaderOffset || 'top-0'}`,\n        className,\n      )}\n    >\n      {/* Selection checkbox */}\n      {selectable && (\n        <div className=\"flex w-10 shrink-0 items-center justify-center\">\n          <Checkbox\n            checked={allSelected || (someSelected && !allSelected) ? true : false}\n            onCheckedChange={onSelectAll}\n            className=\"border-ods-border\"\n          />\n        </div>\n      )}\n\n      {columns.map(column => {\n        const isActionsColumn = column.key === '__actions__';\n        const filterable = column.filterable && column.filterOptions && onFilterChange;\n\n        // Hide columns on mobile if they are not actions or filterable\n        if (!isLgUp && !isActionsColumn && !filterable) {\n          return null;\n        }\n\n        return (\n          <div\n            key={column.key}\n            className={cn(\n              'flex items-center gap-2',\n              isLgUp && getAlignment(column.align),\n              isLgUp && (column.width || 'min-w-0 flex-1'),\n              column.className,\n              // Only apply hide classes if the column is not filterable or on tablet\n              !(filterable && !isLgUp) && getHideClasses(column.hideAt),\n              isActionsColumn && 'ml-auto',\n            )}\n          >\n            {isActionsColumn ? (\n              // Render total items count in actions column\n              totalItemsCount > 0 && (\n                <span className=\"whitespace-nowrap text-ods-text-secondary text-h6\">\n                  Showing {totalItemsCount} {totalItemsCount === 1 ? 'result' : 'results'}\n                </span>\n              )\n            ) : column.filterable && column.filterOptions && onFilterChange ? (\n              /* Filterable column — label + icon are both inside the dropdown trigger */\n              <TableColumnFilterDropdown\n                columnKey={column.key}\n                columnLabel={column.label}\n                filterOptions={column.filterOptions}\n                filters={filters}\n                onFilterChange={onFilterChange}\n              />\n            ) : (\n              /* Non-filterable column — regular label with optional sort */\n              <div\n                className={cn(\n                  'flex items-center gap-2',\n                  column.sortable && 'cursor-pointer transition-colors hover:text-ods-text-primary',\n                )}\n                onClick={() => handleSort(column)}\n              >\n                {column.renderHeader ? (\n                  <>\n                    {column.renderHeader()}\n                    {getSortIcon(column)}\n                  </>\n                ) : (\n                  <>\n                    <span className=\"whitespace-nowrap uppercase text-ods-text-secondary text-h5\">{column.label}</span>\n                    {getSortIcon(column)}\n                  </>\n                )}\n              </div>\n            )}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport type { TableCellProps } from './types';\n\n/** @deprecated Use `DataTable` from `data-table` instead. */\nexport function TableCell({ children, align = 'left', className, width }: TableCellProps) {\n  const getAlignment = () => {\n    switch (align) {\n      case 'center':\n        return 'justify-center text-center';\n      case 'right':\n        return 'justify-end text-right';\n      default:\n        return 'justify-start text-left';\n    }\n  };\n\n  return (\n    <div className={cn('flex flex-col overflow-hidden', getAlignment(), width || 'min-w-0 flex-1', className)}>\n      {typeof children === 'string' || typeof children === 'number' ? (\n        <span className=\"truncate text-ods-text-primary text-h4\">{children}</span>\n      ) : (\n        children\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport type { TableCardSkeletonProps } from './types';\n\n// INNER row heights: the bordered row card adds 1px top + bottom, so the\n// outer block totals the designed 68px / 80px.\nconst ROW_HEIGHT_DESKTOP = 'h-[66px] md:h-[78px]';\n/**\n * A `compact` row's floor. Compact rows are content-sized (`py-2`), but every\n * row in one table renders the same cell shapes, so pinning a minimum makes the\n * body's height a function of the ROW COUNT alone. Hosts do NOT reserve\n * `rows × 56` themselves any more: `skeletonRows` + `minRows` hold\n * a full page's height in every state (placeholders below), and a cell taller\n * than this floor (avatar + two lines) wants a non-compact, fixed-height row.\n */\nconst COMPACT_ROW_MIN_HEIGHT = 'min-h-[56px]';\nconst ROW_HEIGHT_MOBILE = 'h-[66px]';\n\n/** @deprecated Use `DataTableSkeleton` from `data-table` instead. */\nexport function TableCardSkeleton({\n  columns,\n  rows = 10,\n  compact = false,\n  hasActions = false,\n  hasChevron = false,\n  className,\n  rowClassName,\n}: TableCardSkeletonProps) {\n  // The multi-line placeholder belongs to the primary text column — the wide,\n  // GROWING one (`flex-1` / `flex-[n]`), not necessarily the first column, which\n  // may be a narrow leading action/icon column. Falls back to the first column.\n  const primaryKey = (columns.find(c => /flex-(1|\\[)/.test(c.width || '')) ?? columns[0])?.key;\n  return (\n    <>\n      {Array.from({ length: rows }).map((_, index) => (\n        <div\n          key={index}\n          className={cn(\n            'relative animate-pulse overflow-hidden rounded-[6px] border border-ods-border bg-ods-card',\n            className,\n          )}\n        >\n          {/* Desktop Skeleton */}\n          {/* Matches the ROW it stands in for: a compact row is content-sized\n              with a floor, a normal row is a fixed height. Using the taller of\n              the two here made every compact table jump on its first paint. */}\n          <div\n            className={cn(\n              'hidden items-center gap-4 px-4 md:flex',\n              compact ? cn('py-2', COMPACT_ROW_MIN_HEIGHT) : cn('py-0', ROW_HEIGHT_DESKTOP),\n              rowClassName,\n            )}\n          >\n            {columns.map(column => (\n              <div key={column.key} className={cn('flex shrink-0 flex-col justify-center', column.width || 'flex-1')}>\n                <div className=\"mb-1 h-5 w-3/4 rounded bg-ods-bg-surface\" />\n                {/* second line simulates multi-line content on the primary column */}\n                {index % 2 === 0 && column.key === primaryKey && (\n                  <div className=\"h-4 w-1/2 rounded bg-ods-bg-surface opacity-60\" />\n                )}\n              </div>\n            ))}\n\n            {/* Actions skeleton */}\n            {hasActions && (\n              <div className={cn('flex shrink-0 items-center gap-2', !hasChevron && 'ml-auto')}>\n                <div className=\"h-12 w-12 rounded bg-ods-bg-surface\" />\n                <div className=\"h-12 w-24 rounded bg-ods-bg-surface\" />\n              </div>\n            )}\n\n            {/* Chevron skeleton */}\n            {hasChevron && (\n              <div className={cn('flex w-12 shrink-0 items-center justify-end', !hasActions && 'ml-auto')}>\n                <div className=\"h-8 w-8 rounded bg-ods-bg-surface\" />\n              </div>\n            )}\n          </div>\n\n          {/* Mobile Skeleton */}\n          <div\n            className={cn(\n              'flex items-center justify-start gap-3 px-3 md:hidden',\n              // Mirror the real row on mobile too: a compact row is\n              // content-sized with a floor, a normal row a fixed height.\n              // Hard-wiring ROW_HEIGHT_MOBILE made every COMPACT table jump\n              // 68px -> 58px per row when the data replaced the skeleton.\n              compact ? cn('py-2', COMPACT_ROW_MIN_HEIGHT) : cn('py-0', ROW_HEIGHT_MOBILE),\n              rowClassName,\n            )}\n          >\n            {/* The inner padding is the NON-compact row's; a compact row is\n                sized by its own `py-2` + floor, so adding `py-3` here made the\n                compact mobile skeleton 78px against a real row's 58. */}\n            <div className={cn('flex min-w-0 flex-1 flex-col justify-center', compact ? 'py-0' : 'py-3')}>\n              <div className=\"mb-2 h-4 w-3/4 rounded bg-ods-bg-surface\" />\n              <div className=\"h-3 w-1/2 rounded bg-ods-bg-surface opacity-60\" />\n            </div>\n            {hasActions && <div className=\"h-12 w-12 shrink-0 rounded bg-ods-bg-surface\" />}\n            {hasChevron && <div className=\"h-8 w-8 shrink-0 rounded bg-ods-bg-surface\" />}\n          </div>\n        </div>\n      ))}\n    </>\n  );\n}\n\n/** @deprecated */\nexport { COMPACT_ROW_MIN_HEIGHT, ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE };\n","'use client';\n\nimport type React from 'react';\nimport Link from '../../../embed-shims/next-link';\nimport { cn } from '../../../utils/cn';\nimport { Checkbox } from '../checkbox';\nimport { TableCell } from './table-cell';\nimport { COMPACT_ROW_MIN_HEIGHT, ROW_HEIGHT_DESKTOP } from './table-skeleton';\nimport type { TableRowData, TableRowProps } from './types';\nimport { getHideClasses } from './utils';\n\n/** Narrow an unknown to something a dot-path segment can index into. */\nfunction isIndexable(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\n/** @deprecated Use `DataTableRow` from `data-table` instead. */\nexport function TableRow<T = TableRowData>({\n  item,\n  columns,\n  onClick,\n  href,\n  className,\n  index,\n  compact,\n  selectable,\n  selected,\n  onSelect,\n  animateRowReorder,\n  motionDiv,\n}: TableRowProps<T>) {\n  const isLinkMode = Boolean(href) && !onClick;\n  // Opt-in FLIP: the outer row becomes a `motion.div` (passed down lazily from\n  // Table, so framer-motion stays out of the default bundle) that animates only\n  // its position (`layout=\"position\"`) so reordering doesn't distort inner cell\n  // content (CircularProgress / ProgressBar). Plain `<div>` when off, or until\n  // framer-motion has loaded — zero cost on the default path.\n  const animate = Boolean(animateRowReorder && motionDiv);\n  const Row: React.ElementType = animate && motionDiv ? motionDiv : 'div';\n  const motionProps = animate\n    ? { layout: 'position' as const, transition: { layout: { duration: 0.35, ease: [0.22, 1, 0.36, 1] as const } } }\n    : {};\n\n  const handleRowClick = (e: React.MouseEvent) => {\n    const target = e.target as HTMLElement;\n    if (target.closest('[data-no-row-click]')) {\n      return;\n    }\n\n    if (onClick) {\n      onClick(item);\n    }\n  };\n\n  const handleSelect = () => {\n    if (onSelect) {\n      onSelect(item);\n    }\n  };\n\n  const getCellValue = (column: (typeof columns)[0]): React.ReactNode => {\n    if (column.renderCell) {\n      return column.renderCell(item, column);\n    }\n\n    // Access nested properties using dot notation\n    const keys = column.key.split('.');\n    let value: unknown = item;\n    for (const key of keys) {\n      if (!isIndexable(value)) {\n        value = undefined;\n        break;\n      }\n      value = value[key];\n    }\n\n    if (value === null || value === undefined) {\n      return '-';\n    }\n    if (typeof value === 'boolean') {\n      return value ? 'Yes' : 'No';\n    }\n    if (typeof value === 'object') {\n      return JSON.stringify(value);\n    }\n\n    return String(value);\n  };\n\n  return (\n    <Row\n      {...motionProps}\n      className={cn(\n        'relative overflow-hidden rounded-[6px] border border-ods-border bg-ods-card',\n        (onClick || isLinkMode) && 'cursor-pointer transition-colors hover:bg-ods-bg-active',\n        typeof className === 'function' ? className(item, index) : className,\n      )}\n      onClick={isLinkMode ? undefined : handleRowClick}\n    >\n      {isLinkMode && href && (\n        <Link href={href} prefetch={false} className=\"absolute inset-0\" aria-label=\"View details\" />\n      )}\n      <div\n        className={cn(\n          'relative flex items-center gap-4 px-4',\n          // `compact` rows are content-sized, but every row in a table shows\n          // the same cell shapes, so a MIN height makes the body's height a\n          // function of the row COUNT alone. That is what lets a host reserve\n          // space for a full page and stop the layout jumping between a full\n          // page, a short last page and the skeleton.\n          compact ? cn('py-2', COMPACT_ROW_MIN_HEIGHT) : cn('py-0', ROW_HEIGHT_DESKTOP),\n          isLinkMode && 'pointer-events-none',\n        )}\n      >\n        {/* Selection checkbox */}\n        {selectable && (\n          <div className=\"pointer-events-auto flex w-10 shrink-0 items-center justify-center\" data-no-row-click>\n            <Checkbox checked={selected} onCheckedChange={handleSelect} className=\"border-ods-border\" />\n          </div>\n        )}\n\n        {columns.map(column => (\n          <TableCell\n            key={column.key}\n            align={column.align}\n            width={column.width || 'flex-1 min-w-0'}\n            className={cn(column.className, getHideClasses(column.hideAt))}\n          >\n            {getCellValue(column)}\n          </TableCell>\n        ))}\n      </div>\n    </Row>\n  );\n}\n","'use client';\n\nimport { useEffect, useState, type ComponentType, type ElementType, type ReactNode } from 'react';\n\n/**\n * Framer Motion runtime resolved lazily for the opt-in row-reorder animation.\n * `motionDiv` is `motion.div`; `LayoutGroup` coordinates the FLIP.\n */\nexport interface TableMotionRuntime {\n  // Structural component types only — the base table must never statically\n  // reference framer-motion's own types (that would pull the module into the\n  // default bundle's type graph and, with it, the import).\n  motionDiv: ElementType;\n  LayoutGroup: ComponentType<{ id?: string; children?: ReactNode }>;\n}\n\n/**\n * Lazily loads `framer-motion` — via a dynamic `import()` so it lands in its own\n * chunk — ONLY when row-reorder animation is enabled. Consumers that never set\n * `animateRowReorder` never pull framer-motion into their bundle (the default\n * `Table`/`TableRow` path stays motion-free).\n *\n * Returns `null` until the module resolves (and always when `enabled` is false);\n * callers render plain DOM in the meantime, so the first paint is non-animated\n * and the FLIP kicks in once the chunk has loaded.\n */\nexport function useTableMotion(enabled: boolean): TableMotionRuntime | null {\n  const [runtime, setRuntime] = useState<TableMotionRuntime | null>(null);\n\n  useEffect(() => {\n    if (!enabled || runtime) return undefined;\n    let active = true;\n    import('framer-motion')\n      .then(m => {\n        if (active) setRuntime({ motionDiv: m.motion.div, LayoutGroup: m.LayoutGroup });\n      })\n      .catch(() => {\n        // Chunk-load failure → keep `runtime` null so the table degrades to\n        // plain (non-animated) rows, instead of surfacing an unhandled\n        // promise rejection / global chunk-load error.\n      });\n    return () => {\n      active = false;\n    };\n  }, [enabled, runtime]);\n\n  return enabled ? runtime : null;\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\n\n/** @deprecated Use `data-table` instead. */\nexport interface TableDescriptionCellProps {\n  /**\n   * The description text to display\n   */\n  text: string;\n  /**\n   * Optional additional CSS classes\n   */\n  className?: string;\n  /**\n   * Maximum number of lines to display before truncating\n   * @default 3\n   */\n  maxLines?: number;\n}\n\n/**\n * @deprecated Use `data-table` instead.\n */\nexport function TableDescriptionCell({ text, className, maxLines = 3 }: TableDescriptionCellProps) {\n  return (\n    <div className=\"flex-1\">\n      <div className=\"flex flex-col justify-center\">\n        <span\n          className={cn(\n            'break-words text-ods-text-secondary text-h6',\n            maxLines === 1 && 'truncate',\n            maxLines === 2 && 'line-clamp-2',\n            maxLines === 3 && 'line-clamp-3',\n            maxLines === 4 && 'line-clamp-4',\n            maxLines === 5 && 'line-clamp-5',\n            maxLines === 6 && 'line-clamp-6',\n            className,\n          )}\n        >\n          {text}\n        </span>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\n\n/** @deprecated Use `data-table` instead. */\nexport interface TableTimestampCellProps {\n  /**\n   * The timestamp to display (can be a Date, ISO string, or formatted string)\n   */\n  timestamp: string | Date;\n  /**\n   * The ID to display below the timestamp\n   */\n  id: string;\n  /**\n   * Optional label for the ID (e.g., \"Log ID\", \"Chat ID\")\n   * If not provided, just shows the ID\n   */\n  idLabel?: string;\n  /**\n   * Optional additional CSS classes for the container\n   */\n  className?: string;\n  /**\n   * Format the timestamp automatically if it's an ISO string or Date\n   * @default true\n   */\n  formatTimestamp?: boolean;\n}\n\n/**\n * Formats a timestamp for display\n * If already formatted (contains space or comma), returns as-is\n * Otherwise formats as locale string\n */\nfunction formatTimestampValue(timestamp: string | Date): string {\n  if (timestamp instanceof Date) {\n    return timestamp.toLocaleString();\n  }\n\n  // If it looks like an ISO string, format it\n  if (typeof timestamp === 'string') {\n    // Check if already formatted (contains typical formatting chars)\n    if (timestamp.includes(',') || (timestamp.includes('/') && timestamp.includes(':'))) {\n      return timestamp;\n    }\n\n    // Try to parse and format ISO strings\n    const date = new Date(timestamp);\n    if (!isNaN(date.getTime())) {\n      return date.toLocaleString();\n    }\n  }\n\n  // Return as-is if we can't parse it\n  return String(timestamp);\n}\n\n/** @deprecated Use `data-table` instead. */\nexport function TableTimestampCell({\n  timestamp,\n  id,\n  idLabel,\n  className,\n  formatTimestamp = true,\n}: TableTimestampCellProps) {\n  const displayTimestamp = formatTimestamp ? formatTimestampValue(timestamp) : String(timestamp);\n\n  return (\n    <div className={cn('flex shrink-0 flex-col justify-center', className)}>\n      <span className=\"truncate text-ods-text-primary text-h4\">{displayTimestamp}</span>\n      <span className=\"truncate text-ods-text-secondary text-h6\">{idLabel ? `${idLabel}: ${id}` : id}</span>\n    </div>\n  );\n}\n","'use client';\n\nimport { useMemo } from 'react';\nimport { useHorizontalScrollbar } from '../../../hooks/ui/use-horizontal-scrollbar';\nimport { cn } from '../../../utils/cn';\nimport { Download02Icon } from '../../icons-v2-generated/interface/download-02-icon';\nimport { Button } from '../button';\nimport { TableEmptyState } from '../table/table-empty-state';\nimport { QueryReportTableHeader } from './query-report-table-header';\nimport { QueryReportTableRow } from './query-report-table-row';\nimport { QueryReportTableSkeleton } from './query-report-table-skeleton';\nimport type { QueryReportTableProps } from './types';\nimport { deriveColumns, exportToCSV } from './utils';\n\nexport function QueryReportTable({\n  title,\n  data,\n  loading = false,\n  skeletonRows = 8,\n  skeletonColumns = 6,\n  emptyMessage,\n  columnWidth = 160,\n  columnOrder,\n  showExport = true,\n  exportFilename = 'query-results',\n  onExport,\n  headerActions,\n  variant = 'default',\n  className,\n  tableClassName,\n}: QueryReportTableProps) {\n  const isCompact = variant === 'compact';\n  const columns = useMemo(() => deriveColumns(data, columnOrder), [data, columnOrder]);\n\n  const handleExport = () => {\n    exportToCSV(data, columns, exportFilename);\n    onExport?.();\n  };\n\n  const tableMinWidth = columns.length * (columnWidth + 16); // columnWidth + gap\n\n  const {\n    scrollRef,\n    trackRef,\n    thumbRef,\n    thumbRatio,\n    canScrollLeft,\n    canScrollRight,\n    onScroll,\n    onTrackClick,\n    onTrackWheel,\n    onThumbPointerDown,\n    onThumbPointerMove,\n    onThumbPointerUp,\n  } = useHorizontalScrollbar();\n\n  return (\n    <div className={cn('flex w-full flex-col', isCompact ? 'gap-0' : 'gap-[var(--spacing-system-l)]', className)}>\n      {/* Title bar — hidden in compact mode and when empty */}\n      {!isCompact && (title || headerActions || (showExport && data.length > 0)) && (\n        <div className=\"flex items-end justify-between pt-[var(--spacing-system-l)]\">\n          <h2 className=\"text-ods-text-primary text-h2\">{title}</h2>\n          <div className=\"flex items-center gap-[var(--spacing-system-s)]\">\n            {headerActions}\n            {showExport && data.length > 0 && (\n              <Button\n                className=\"bg-ods-card max-md:hidden\"\n                variant=\"outline\"\n                leftIcon={<Download02Icon size={18} className=\"text-ods-text-secondary\" />}\n                onClick={handleExport}\n              >\n                Export CSV\n              </Button>\n            )}\n          </div>\n        </div>\n      )}\n\n      {/* Loading state */}\n      {loading && (\n        <QueryReportTableSkeleton\n          rows={skeletonRows}\n          columns={skeletonColumns}\n          columnWidth={columnWidth}\n          variant={variant}\n        />\n      )}\n\n      {/* Empty state */}\n      {!loading && data.length === 0 && <TableEmptyState message={emptyMessage} />}\n\n      {/* Table content */}\n      {!loading && data.length > 0 && (\n        <div className=\"flex flex-col\">\n          {/* Custom scrollbar track — hidden in compact mode */}\n          {!isCompact && thumbRatio > 0 && (\n            <div\n              ref={trackRef}\n              className=\"relative h-2 cursor-pointer rounded-full bg-ods-border\"\n              onClick={onTrackClick}\n              onWheel={onTrackWheel}\n            >\n              <div\n                ref={thumbRef}\n                data-scrollbar-thumb\n                className=\"absolute top-0 h-full rounded-full bg-ods-text-secondary transition-colors\"\n                style={{\n                  width: `${thumbRatio * 100}%`,\n                  cursor: 'grab',\n                }}\n                onPointerDown={onThumbPointerDown}\n                onPointerMove={onThumbPointerMove}\n                onPointerUp={onThumbPointerUp}\n              />\n            </div>\n          )}\n\n          {/* Scrollable table container */}\n          <div className=\"relative\">\n            <div ref={scrollRef} className={cn('overflow-x-auto', tableClassName)} onScroll={onScroll}>\n              <div style={{ minWidth: tableMinWidth }}>\n                <QueryReportTableHeader columns={columns} columnWidth={columnWidth} variant={variant} />\n                <div className={cn('flex flex-col', isCompact ? 'gap-0' : 'gap-[var(--spacing-system-xs)]')}>\n                  {data.map((row, index) => (\n                    <QueryReportTableRow\n                      key={index}\n                      row={row}\n                      columns={columns}\n                      columnWidth={columnWidth}\n                      variant={variant}\n                    />\n                  ))}\n                </div>\n              </div>\n            </div>\n\n            {/* Edge fades — hidden in compact mode */}\n            {!isCompact && canScrollLeft && (\n              <div className=\"pointer-events-none absolute inset-y-0 left-0 w-10 bg-gradient-to-r from-ods-bg to-transparent\" />\n            )}\n            {!isCompact && canScrollRight && (\n              <div className=\"pointer-events-none absolute inset-y-0 right-0 w-10 bg-gradient-to-l from-ods-bg to-transparent\" />\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { useRef, useState, useCallback } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip';\nimport type { QueryReportTableHeaderProps } from './types';\n\nexport function QueryReportTableHeader({\n  columns,\n  columnWidth,\n  variant = 'default',\n  className,\n}: QueryReportTableHeaderProps) {\n  const isCompact = variant === 'compact';\n\n  return (\n    <div className={cn('flex items-center gap-4 px-4', isCompact ? 'border-b border-ods-border py-2' : '', className)}>\n      {columns.map(column => (\n        <TruncatedHeaderCell key={column} value={column} width={columnWidth} />\n      ))}\n    </div>\n  );\n}\n\nfunction TruncatedHeaderCell({ value, width }: { value: string; width: number }) {\n  const textRef = useRef<HTMLDivElement>(null);\n  const [isTruncated, setIsTruncated] = useState(false);\n\n  const checkTruncation = useCallback(() => {\n    const el = textRef.current;\n    if (el) {\n      setIsTruncated(el.scrollWidth > el.clientWidth);\n    }\n  }, []);\n\n  return (\n    <TooltipProvider delayDuration={300}>\n      <Tooltip open={isTruncated ? undefined : false}>\n        <TooltipTrigger asChild onMouseEnter={checkTruncation}>\n          <div className=\"flex shrink-0 items-center\" style={{ width, height: 48 }}>\n            <div ref={textRef} className=\"truncate text-ods-text-secondary text-h5\">\n              {value}\n            </div>\n          </div>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\" className=\"max-w-xs break-words\">\n          {value}\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n","'use client';\n\nimport { useRef, useState, useCallback } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip';\nimport type { QueryReportTableRowProps } from './types';\n\nexport function QueryReportTableRow({\n  row,\n  columns,\n  columnWidth,\n  variant = 'default',\n  className,\n}: QueryReportTableRowProps) {\n  const isCompact = variant === 'compact';\n\n  return (\n    <div\n      className={cn(\n        isCompact\n          ? 'border-b border-ods-border'\n          : 'relative overflow-hidden rounded-[6px] border border-ods-border bg-ods-card',\n        className,\n      )}\n    >\n      <div className={cn('flex items-center gap-4 px-4', isCompact ? 'h-[56px]' : 'h-[80px]')}>\n        {columns.map(column => {\n          const value = row[column];\n          const displayValue = value === null || value === undefined ? '-' : String(value);\n          return (\n            <div\n              key={column}\n              className=\"flex shrink-0 flex-col justify-center overflow-hidden\"\n              style={{ width: columnWidth }}\n            >\n              <TruncatedCell\n                className={cn('text-ods-text-primary', isCompact ? 'text-h6' : 'text-h4')}\n                value={displayValue}\n              />\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\nfunction TruncatedCell({ value, className }: { value: string; className?: string }) {\n  const textRef = useRef<HTMLSpanElement>(null);\n  const [isTruncated, setIsTruncated] = useState(false);\n\n  const checkTruncation = useCallback(() => {\n    const el = textRef.current;\n    if (el) {\n      setIsTruncated(el.scrollWidth > el.clientWidth);\n    }\n  }, []);\n\n  return (\n    <TooltipProvider delayDuration={300}>\n      <Tooltip open={isTruncated ? undefined : false}>\n        <TooltipTrigger asChild onMouseEnter={checkTruncation}>\n          <span ref={textRef} className={cn('block truncate', className)}>\n            {value}\n          </span>\n        </TooltipTrigger>\n        <TooltipContent side=\"top\" className=\"max-w-xs break-words\">\n          {value}\n        </TooltipContent>\n      </Tooltip>\n    </TooltipProvider>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport type { QueryReportTableSkeletonProps } from './types';\n\nexport function QueryReportTableSkeleton({\n  rows,\n  columns,\n  columnWidth,\n  variant = 'default',\n  className,\n}: QueryReportTableSkeletonProps) {\n  const isCompact = variant === 'compact';\n\n  return (\n    <div className={cn('flex flex-col', isCompact ? 'gap-0' : 'gap-[var(--spacing-system-xs)]', className)}>\n      {/* Skeleton header */}\n      <div className={cn('flex items-center gap-4 px-4', isCompact ? 'border-b border-ods-border py-2' : '')}>\n        {Array.from({ length: columns }).map((_, i) => (\n          <div key={`header-${i}`} className=\"flex shrink-0 items-center\" style={{ width: columnWidth, height: 48 }}>\n            <div className=\"h-4 w-3/4 animate-pulse rounded bg-ods-bg-surface\" />\n          </div>\n        ))}\n      </div>\n\n      {/* Skeleton rows */}\n      {Array.from({ length: rows }).map((_, rowIndex) => (\n        <div\n          key={`row-${rowIndex}`}\n          className={cn(\n            'animate-pulse',\n            isCompact\n              ? 'border-b border-ods-border'\n              : 'relative overflow-hidden rounded-[6px] border border-ods-border bg-ods-card',\n          )}\n        >\n          <div className={cn('flex items-center gap-4 px-4', isCompact ? 'h-[56px]' : 'h-[80px]')}>\n            {Array.from({ length: columns }).map((_cell, colIndex) => (\n              <div key={`cell-${rowIndex}-${colIndex}`} className=\"shrink-0\" style={{ width: columnWidth }}>\n                <div\n                  className={cn('rounded bg-ods-bg-surface', isCompact ? 'h-4' : 'h-5')}\n                  style={{ width: `${55 + ((rowIndex * colIndex * 7) % 35)}%` }}\n                />\n              </div>\n            ))}\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n","import type { QueryResultRow } from './types';\n\nexport function deriveColumns(data: QueryResultRow[], columnOrder?: string[]): string[] {\n  if (data.length === 0) return [];\n\n  const allKeys = Object.keys(data[0]);\n\n  if (!columnOrder || columnOrder.length === 0) {\n    return allKeys;\n  }\n\n  // Start with ordered keys that actually exist in the data\n  const ordered = columnOrder.filter(key => allKeys.includes(key));\n  // Append any remaining keys not in the explicit order\n  const remaining = allKeys.filter(key => !columnOrder.includes(key));\n\n  return [...ordered, ...remaining];\n}\n\n/**\n * Escape a CSV cell value — wraps in quotes if it contains commas, quotes, or newlines.\n */\nfunction escapeCSVCell(value: string): string {\n  if (value.includes(',') || value.includes('\"') || value.includes('\\n') || value.includes('\\r')) {\n    return `\"${value.replace(/\"/g, '\"\"')}\"`;\n  }\n  return value;\n}\n\nexport function exportToCSV(data: QueryResultRow[], columns: string[], filename: string): void {\n  if (data.length === 0 || columns.length === 0) return;\n\n  const header = columns.map(escapeCSVCell).join(',');\n  const rows = data.map(row =>\n    columns\n      .map(col => {\n        const value = row[col];\n        if (value === null || value === undefined) return '';\n        return escapeCSVCell(String(value));\n      })\n      .join(','),\n  );\n\n  const csv = [header, ...rows].join('\\n');\n  const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });\n  const url = URL.createObjectURL(blob);\n\n  const link = document.createElement('a');\n  link.href = url;\n  link.download = `${filename}.csv`;\n  link.style.display = 'none';\n  document.body.appendChild(link);\n  link.click();\n\n  // Cleanup\n  document.body.removeChild(link);\n  URL.revokeObjectURL(url);\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport { SearchIcon } from '../../icons-v2-generated';\nimport { NoData, type NoDataProps } from '../no-data';\n\nexport interface DataTableEmptyProps extends NoDataProps {}\n\n/**\n * Empty state shown by `DataTable.Body` when there are no rows. Renders the\n * shared `NoData` empty state, centered, defaulting to a \"no search results\"\n * message (search icon, \"No results found\", \"Try adjusting your search or\n * filters\"). Every field can be overridden — pass any `NoData` prop\n * (icon/title/description/actions/buttonLabel/…).\n */\nexport function DataTableEmpty({ className, ...props }: DataTableEmptyProps) {\n  return (\n    <NoData\n      icon={<SearchIcon />}\n      title=\"No results found\"\n      description=\"Try adjusting your search or filters\"\n      {...props}\n      className={cn('py-[var(--spacing-system-xxl)]', className)}\n    />\n  );\n}\n","'use client';\n\nimport { flexRender, type Row } from '@tanstack/react-table';\nimport type React from 'react';\nimport { memo, useCallback, useRef, type ReactNode } from 'react';\nimport Link from '../../../embed-shims/next-link';\nimport { cn } from '../../../utils/cn';\nimport { ROW_HEIGHT_DESKTOP, ROW_SHELL_CLASSES } from './data-table-skeleton';\nimport { alignJustify, getHideClasses } from './utils';\n\nexport interface DataTableRowProps<T> {\n  row: Row<T>;\n  onClick?: (item: T) => void;\n  href?: string | null;\n  /** Dense row height. */\n  compact?: boolean;\n  /**\n   * Treat the design row height as a minimum: a cell rendering multi-line\n   * content (e.g. a line-clamped description) grows the row instead of being\n   * clipped. Default keeps the fixed height.\n   */\n  autoHeight?: boolean;\n  /**\n   * REPLACES the height this row would otherwise take (`compact`, `autoHeight`\n   * or the design default). The one place a table states how tall a row is, so\n   * the pad rows and the skeleton can reserve the SAME number — see\n   * `DataTableBodyProps.rowHeightClassName`.\n   *\n   * It sizes the row SLOT — the whole card — not the cells inside it. A row\n   * with a `subRow` is taller than its cells, so sizing the cells would leave\n   * the pad rows (which have no sub-row) short by exactly the sub-row: on a\n   * phone board that was 96px per missing row.\n   */\n  rowHeightClassName?: string;\n  className?: string;\n  /** Expandable content rendered below the cells, inside the same card. */\n  subRow?: ReactNode;\n}\n\n/**\n * Click-bubbling protocol: any element inside a cell that should NOT trigger\n * `onRowClick` / row navigation must carry the `data-no-row-click` attribute.\n * The row checks `target.closest('[data-no-row-click]')` and short-circuits:\n * in `onClick` mode it skips the consumer's handler; in link mode (when\n * `href` is set) it calls `e.preventDefault()` so `<Link>` does not navigate.\n *\n * Clicks originating from portaled descendants (e.g. `FloatingTooltip`,\n * dropdown menus rendered through `FloatingPortal`) bubble through React's\n * component tree and reach this handler, but their DOM target lives outside\n * the row subtree. The handler ignores any click whose target is not\n * physically contained within the row element — no `stopPropagation`\n * required at the source.\n *\n * In link mode the row IS the `<Link>` — content lives inside it, not under\n * an absolute overlay — so native browser link behaviour works: hover,\n * right-click \"Open in new tab\", middle-click, `Cmd+click`, focus outlines,\n * `:visited` styles, etc.\n *\n * Example column with action buttons:\n * ```tsx\n * {\n *   id: 'actions',\n *   cell: ({ row }) => (\n *     <div data-no-row-click className=\"flex gap-2 justify-end\">\n *       <Button onClick={() => edit(row.original)}>Edit</Button>\n *     </div>\n *   ),\n *   enableSorting: false,\n *   meta: { width: 'w-[160px] shrink-0', align: 'right' },\n * }\n * ```\n *\n * Wrapped in `React.memo` — for best perf in long lists, pass stable references\n * for `onClick` and `className` (if function) via `useCallback` / `useMemo` in\n * the consumer. `href` and string `className` are compared by value.\n */\nfunction DataTableRowImpl<T>({\n  row,\n  onClick,\n  href,\n  compact,\n  autoHeight,\n  rowHeightClassName,\n  className,\n  subRow,\n}: DataTableRowProps<T>) {\n  const hasSubRow = subRow != null && subRow !== false;\n  // A sub-row carries its own interactive controls, so it must not live inside the\n  // row-level <Link>; when present, the link wraps only the cells.\n  const isLinkMode = Boolean(href) && !onClick;\n  const isWholeCardLink = isLinkMode && !hasSubRow;\n  const containerRef = useRef<HTMLElement | null>(null);\n\n  const handleClick = useCallback(\n    (e: React.MouseEvent) => {\n      const target = e.target as HTMLElement;\n      // React-bubbled events from portaled descendants (tooltips, dropdowns, etc.)\n      // reach this handler even though their DOM target lives outside the row.\n      // Suppress them — and in link mode, preventDefault so `<Link>` does not navigate.\n      if (!containerRef.current?.contains(target)) {\n        if (isLinkMode) e.preventDefault();\n        return;\n      }\n      if (target.closest('[data-no-row-click]')) {\n        if (isLinkMode) e.preventDefault();\n        return;\n      }\n      onClick?.(row.original);\n    },\n    [onClick, row.original, isLinkMode],\n  );\n\n  const containerClassName = cn(\n    'overflow-hidden rounded-md border border-ods-border bg-ods-card text-inherit no-underline',\n    // A stated slot height goes HERE, on the card, with the cells flexing to\n    // fill whatever the sub-row leaves. `border-box` sizing means the number a\n    // caller writes is the number the row occupies, borders included.\n    rowHeightClassName ? `flex flex-col ${rowHeightClassName}` : 'block',\n    // With a sub-row the link wraps only the cells, so keep the clickable affordance off the whole card.\n    (onClick || isWholeCardLink) && 'cursor-pointer transition-colors hover:bg-ods-bg-active',\n    className,\n  );\n\n  const cells = (\n    <div\n      className={cn(\n        'flex',\n        ROW_SHELL_CLASSES,\n        rowHeightClassName\n          ? 'min-h-0 flex-1 py-0'\n          : compact\n            ? 'py-[var(--spacing-system-xsf)]'\n            : autoHeight\n              ? 'min-h-[66px] py-[var(--spacing-system-sf)] md:min-h-[78px]'\n              : `py-0 ${ROW_HEIGHT_DESKTOP}`,\n        hasSubRow && 'border-b border-ods-border',\n      )}\n    >\n      {row.getVisibleCells().map(cell => {\n        const meta = cell.column.columnDef.meta;\n        return (\n          <div\n            key={cell.id}\n            className={cn(\n              'flex flex-col overflow-hidden',\n              alignJustify(meta?.align),\n              meta?.width || 'min-w-0 flex-1',\n              meta?.cellClassName,\n              getHideClasses(meta?.hideAt),\n            )}\n          >\n            <CellContent>{flexRender(cell.column.columnDef.cell, cell.getContext())}</CellContent>\n          </div>\n        );\n      })}\n    </div>\n  );\n\n  if (isWholeCardLink && href) {\n    return (\n      <Link\n        href={href}\n        prefetch={false}\n        ref={containerRef as React.RefObject<HTMLAnchorElement>}\n        className={containerClassName}\n        onClick={handleClick}\n      >\n        {cells}\n      </Link>\n    );\n  }\n\n  return (\n    <div\n      ref={containerRef as React.RefObject<HTMLDivElement>}\n      className={containerClassName}\n      onClick={onClick ? handleClick : undefined}\n    >\n      {isLinkMode && href ? (\n        <Link\n          href={href}\n          prefetch={false}\n          className={cn(\n            'cursor-pointer text-inherit no-underline transition-colors hover:bg-ods-bg-active',\n            rowHeightClassName ? 'flex min-h-0 flex-1 flex-col' : 'block',\n          )}\n          onClick={handleClick}\n        >\n          {cells}\n        </Link>\n      ) : (\n        cells\n      )}\n      {hasSubRow && subRow}\n    </div>\n  );\n}\n\nexport const DataTableRow = memo(DataTableRowImpl) as typeof DataTableRowImpl;\n\n/** Wraps primitive string/number cell values in the default text style. */\nfunction CellContent({ children }: { children: ReactNode }) {\n  if (typeof children === 'string' || typeof children === 'number') {\n    return (\n      <span className=\"truncate text-ods-text-primary text-h4\" title={String(children)}>\n        {children}\n      </span>\n    );\n  }\n  return <>{children}</>;\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport type { NoDataProps } from '../no-data';\nimport { useDataTableContext } from './data-table';\nimport { DataTableEmpty } from './data-table-empty';\nimport { DataTableRow } from './data-table-row';\nimport { DataTableSkeleton, PlaceholderRows, ReservedEmptyState } from './data-table-skeleton';\n\nexport interface DataTableBodyProps<T = unknown> {\n  /** Show skeleton rows while `loading` is true and data is empty. */\n  loading?: boolean;\n  /** @deprecated Use `emptyState` instead. Legacy single-line message; mapped to the empty state's title. */\n  emptyMessage?: string;\n  /** Props for the empty state (NoData) shown when there are no rows. Overrides `emptyMessage`. */\n  emptyState?: NoDataProps;\n  /** Skeleton row count when loading. Default `10`. */\n  skeletonRows?: number;\n  className?: string;\n  /**\n   * Per-row class name. Prefer `useCallback` for function form to avoid\n   * breaking `React.memo` on rows.\n   */\n  rowClassName?: string | ((item: T, index: number) => string);\n  /** Dense row height. */\n  compact?: boolean;\n  /**\n   * Treat the design row height as a minimum so multi-line cell content grows\n   * the row instead of clipping. Default keeps the fixed height.\n   */\n  autoHeight?: boolean;\n  /**\n   * REPLACES the design row height for THIS table's rows, its pad rows and its\n   * skeleton alike — one number, so they cannot disagree.\n   *\n   * Why a prop and not a className: `minRows` promises a stable table height,\n   * but its pad rows and the skeleton were hard-coded to the design height\n   * while a row passing `autoHeight` renders as tall as its content. A table\n   * with 112px rows padded a short page with 78px placeholders and came up\n   * 34px per missing row too short — on a 15-row page with 5 results, 340px of\n   * jump against every other page. Appending a height through `rowClassName`\n   * cannot fix it either: tailwind-merge drops the plain `h-[66px]` but keeps\n   * the `md:h-[78px]` beside it, so the override holds on a phone and loses on\n   * a desktop. Pass it here and every row slot in the table agrees.\n   *\n   * Responsive values belong in the string itself, e.g. `'h-[200px] md:h-[112px]'`.\n   */\n  rowHeightClassName?: string;\n  /**\n   * Click anywhere on a row (except elements with `data-no-row-click`). Prefer\n   * `useCallback` to avoid breaking `React.memo` on rows.\n   */\n  onRowClick?: (item: T) => void;\n  /**\n   * Turn each row into a `next/link` to the returned URL. Ignored if\n   * `onRowClick` is set. Prefer `useCallback` for the same reason.\n   */\n  rowHref?: (item: T) => string | null | undefined;\n  /**\n   * Keep table height stable by padding with invisible rows when data is short.\n   * Typically set to the same value as `skeletonRows`. Pass `0` to disable.\n   */\n  minRows?: number;\n  /**\n   * Render expandable content below a row, inside the same card. Return a node to\n   * attach it (the cells row gains a bottom divider), or null/undefined for none.\n   * Interactive content inside must carry `data-no-row-click`.\n   */\n  renderSubRow?: (item: T) => ReactNode;\n}\n\n/**\n * Renders skeleton / empty state / rows based on table context state. Place\n * inside `<DataTable>`. Rows use `React.memo` for performance — memoize\n * `onRowClick` / `rowHref` / `rowClassName` (if function) with `useCallback`\n * in the consumer to get the full benefit.\n */\nexport function DataTableBody<T = unknown>({\n  loading,\n  emptyMessage,\n  emptyState,\n  skeletonRows = 10,\n  className,\n  rowClassName,\n  compact,\n  autoHeight,\n  rowHeightClassName,\n  onRowClick,\n  rowHref,\n  minRows,\n  renderSubRow,\n}: DataTableBodyProps<T>) {\n  const table = useDataTableContext<T>();\n  const rows = table.getRowModel().rows;\n\n  if (loading && rows.length === 0) {\n    return (\n      <div className={cn('flex w-full flex-col gap-[var(--spacing-system-xsf)]', className)}>\n        <DataTableSkeleton rows={skeletonRows} rowHeightClassName={rowHeightClassName} />\n      </div>\n    );\n  }\n\n  if (rows.length === 0) {\n    const empty = emptyState ? (\n      <DataTableEmpty {...emptyState} />\n    ) : emptyMessage != null ? (\n      <DataTableEmpty title={emptyMessage} description={undefined} />\n    ) : (\n      <DataTableEmpty />\n    );\n\n    // `minRows` promises a STABLE table height. An empty board is exactly when\n    // a collapsing table is most visible — the pagination and everything under\n    // it jump up the moment a filter matches nothing — so reserve the same row\n    // slots here and center the empty state over them.\n    if (minRows) {\n      return (\n        <ReservedEmptyState\n          count={minRows}\n          gapClassName={cn('gap-[var(--spacing-system-xsf)]', className)}\n          rowHeightClassName={rowHeightClassName}\n        >\n          {empty}\n        </ReservedEmptyState>\n      );\n    }\n\n    return <div className={cn('flex w-full flex-col gap-[var(--spacing-system-xsf)]', className)}>{empty}</div>;\n  }\n\n  const padCount = minRows ? Math.max(0, minRows - rows.length) : 0;\n\n  return (\n    <div className={cn('flex w-full flex-col gap-[var(--spacing-system-xsf)]', className)}>\n      {rows.map((row, index) => {\n        const item = row.original;\n        const href = rowHref?.(item) ?? undefined;\n        const cls = typeof rowClassName === 'function' ? rowClassName(item, index) : rowClassName;\n        return (\n          <DataTableRow<T>\n            key={row.id}\n            row={row}\n            onClick={onRowClick}\n            href={href}\n            compact={compact}\n            autoHeight={autoHeight}\n            rowHeightClassName={rowHeightClassName}\n            className={cls}\n            subRow={renderSubRow?.(item)}\n          />\n        );\n      })}\n      {padCount > 0 && <PlaceholderRows count={padCount} rowHeightClassName={rowHeightClassName} />}\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport { CursorPagination, type CursorPaginationProps } from '../cursor-pagination';\nimport { useDataTableContext } from './data-table';\n\nexport interface DataTableCursorFooterProps extends Omit<CursorPaginationProps, 'currentCount'> {\n  /**\n   * Override the shown current count. Defaults to the number of currently rendered rows\n   * from the table instance.\n   */\n  currentCount?: number;\n}\n\n/**\n * Cursor-based prev/next footer for a `DataTable`. Place after `<DataTable.Body>`.\n * Thin wrapper around `<CursorPagination>` with a border-top separator to match the legacy `Table` look.\n */\nexport function DataTableCursorFooter({ className, currentCount, showInfo, ...rest }: DataTableCursorFooterProps) {\n  const table = useDataTableContext();\n  const rowsCount = table.getRowModel().rows.length;\n\n  return (\n    <CursorPagination\n      {...rest}\n      currentCount={currentCount ?? rowsCount}\n      showInfo={showInfo ?? true}\n      className={cn(\n        'mt-[var(--spacing-system-xsf)] border-t border-ods-border pt-[var(--spacing-system-sf)]',\n        className,\n      )}\n    />\n  );\n}\n","'use client';\n\nimport { flexRender, type Header } from '@tanstack/react-table';\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Arrow01DownIcon, Arrow01UpIcon, SwitchVrIcon } from '../../icons-v2-generated';\nimport { useDataTableContext } from './data-table';\nimport { DataTableColumnFilter } from './data-table-column-filter';\nimport type { TailwindBreakpoint } from './types';\nimport { alignJustify, BREAKPOINT_ORDER, getHideAtVisibility } from './utils';\n\n/** Single-column sort descriptor consumed by the header. */\nexport interface DataTableSortState {\n  id: string;\n  desc: boolean;\n}\n\nexport interface DataTableHeaderProps {\n  className?: string;\n  /** Keep the header visible while scrolling. */\n  stickyHeader?: boolean;\n  /** Tailwind top class for sticky offset, e.g. `'top-[56px]'`. */\n  stickyHeaderOffset?: string;\n  /**\n   * Content rendered at the right edge of the header, on the same row as\n   * column labels. Use for row-count, header-level toolbar buttons, etc.\n   *\n   * @example\n   * <DataTable.Header rightSlot={<DataTable.RowCount itemName=\"device\" />} />\n   */\n  rightSlot?: ReactNode;\n  /**\n   * Current sort descriptor. The header only renders the direction indicator\n   * based on this value — it doesn't own the state. Pair with `onSortChange`\n   * and let the consumer decide what a click means (server query, in-memory\n   * sort, TanStack's row-model sort, …).\n   */\n  sort?: DataTableSortState | null;\n  /**\n   * Fires when a sortable column header is clicked. The consumer owns the\n   * toggle cycle (e.g. none → asc → desc → none) and the actual data sort.\n   */\n  onSortChange?: (columnId: string) => void;\n}\n\nexport function DataTableHeader({\n  className,\n  stickyHeader,\n  stickyHeaderOffset,\n  rightSlot,\n  sort = null,\n  onSortChange,\n}: DataTableHeaderProps) {\n  const table = useDataTableContext();\n\n  // Flat header group (nested headers can be added later if needed).\n  const headerGroup = table.getHeaderGroups()[0];\n  if (!headerGroup) return null;\n\n  // Below lg only the interactive columns are visible — the same\n  // `keepsCellOnTablet` predicate the cells themselves use, so the two cannot drift.\n  // If a table has none, every cell is hidden there, the flex row has no height, and\n  // an absolutely-positioned rightSlot has nothing to sit in — so that slot goes\n  // in-flow instead. Derived from column META, never from the viewport: the same\n  // answer on the server, on the first client paint and after, which is the whole\n  // point of this file no longer reading a media query.\n  const hasTabletVisibleCell = headerGroup.headers.some(\n    header => !header.isPlaceholder && keepsCellOnTablet(header.column.columnDef.meta),\n  );\n\n  return (\n    <div\n      className={cn(\n        'hidden md:flex md:flex-col',\n        stickyHeader && `sticky z-10 bg-ods-bg ${stickyHeaderOffset ?? 'top-0'}`,\n        className,\n      )}\n    >\n      <div className=\"relative flex items-stretch gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]\">\n        {headerGroup.headers.map(header => (\n          <HeaderCell key={header.id} header={header} sort={sort} onSortChange={onSortChange} />\n        ))}\n        {rightSlot && (\n          <div\n            className={cn(\n              'flex items-center',\n              hasTabletVisibleCell\n                ? 'absolute inset-y-0 right-[var(--spacing-system-mf)]'\n                : // No cell is visible below lg, so there this slot is the only thing\n                  // giving the row height and carries the same fixed 48 rather than\n                  // padding out to 44. From lg the cells are back and it returns to\n                  // the absolute placement. Safe as a `max-lg:` override: nothing\n                  // else in this string sets margin or height, so it competes only\n                  // with unprefixed defaults, never with a `md:`/`lg:` class.\n                  'max-lg:ml-auto max-lg:h-12 lg:absolute lg:inset-y-0 lg:right-[var(--spacing-system-mf)]',\n            )}\n          >\n            {rightSlot}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n/* ─────────────────────────────── internals ─────────────────────────────── */\n\ntype AnyHeader = Header<unknown, unknown>;\n\ninterface HeaderCellProps {\n  header: AnyHeader;\n  sort: DataTableSortState | null;\n  onSortChange?: (columnId: string) => void;\n}\n\ntype ColumnMeta = AnyHeader['column']['columnDef']['meta'];\n\n/**\n * Whether a column's header stays visible below `lg`, where the row is narrow\n * enough that only the controls a user can act on earn their space: the filter\n * dropdowns, the sort toggles, plus anything explicitly opted in via\n * `meta.alwaysShowHeader`.\n *\n * `meta.sortable` counts for the same reason `meta.filter` does, and used not\n * to: the sort arrow lives INSIDE the header cell, so hiding the cell below `lg`\n * did not merely hide a label — it removed the only way to reorder the table on\n * every screen narrower than a desktop. A consumer wanting it back had no legal\n * move: `meta.width` is neutralized here by a doubled-class rule, so the escape\n * was a specificity fight, and the one consumer who tried it ended up\n * re-implementing this whole decision in `max-lg:[&&&]:` classes.\n *\n * A sortable column whose CELLS are hidden below `lg` (`meta.hideAt`) still\n * keeps its toggle, exactly as a filterable one does: reordering by a column\n * this width has no room to print is as useful as filtering by one, and the two\n * rules staying identical is what keeps this predicate a single sentence.\n */\nfunction keepsCellOnTablet(meta: ColumnMeta): boolean {\n  return Boolean(meta?.filter) || meta?.sortable === true || meta?.alwaysShowHeader === true;\n}\n\n// Literal class maps — Tailwind's scanner needs the full class strings, which a\n// `${bp}:flex` template would not give it.\nconst SHOW_FROM: Record<TailwindBreakpoint, string> = {\n  md: 'md:flex',\n  lg: 'lg:flex',\n  xl: 'xl:flex',\n  '2xl': '2xl:flex',\n};\nconst HIDE_FROM: Record<TailwindBreakpoint, string> = {\n  md: 'md:hidden',\n  lg: 'lg:hidden',\n  xl: 'xl:hidden',\n  '2xl': '2xl:hidden',\n};\n/** Index of `lg` in the `[base, md, lg, xl, 2xl]` visibility array. */\nconst LG_STEP = BREAKPOINT_ORDER.indexOf('lg') + 1;\n\n/**\n * Visibility classes for one header cell, as a plain min-width ladder.\n *\n * Two rules stack here: the column's own `hideAt`, and \"below `lg` only the\n * cells a user can act on\". Layering them as classes does not work — a\n * `max-lg:` override cannot cancel a `md:hidden` from `hideAt`, because Tailwind\n * emits the `max-lg` block BEFORE the `md` one and equal-specificity rules are\n * decided by source order. So both rules are resolved on booleans first, and only\n * the result is turned into classes: at most one utility per breakpoint, each a\n * `min-width` variant, no ordering left to chance.\n */\nfunction getCellVisibilityClasses(\n  keepOnTablet: boolean,\n  hideAt: TailwindBreakpoint | TailwindBreakpoint[] | undefined,\n): string {\n  const fromHideAt = getHideAtVisibility(hideAt);\n  const visible = fromHideAt.map((shown, step) =>\n    keepOnTablet\n      ? // Reachable below lg whatever `hideAt` says; from lg it governs again.\n        step < LG_STEP || shown\n      : // Otherwise lg is a floor `hideAt` can raise but never lower.\n        shown && step >= LG_STEP,\n  );\n\n  const classes = [visible[0] ? 'flex' : 'hidden'];\n  for (let step = 1; step < visible.length; step++) {\n    if (visible[step] === visible[step - 1]) continue;\n    const breakpoint = BREAKPOINT_ORDER[step - 1];\n    classes.push(visible[step] ? SHOW_FROM[breakpoint] : HIDE_FROM[breakpoint]);\n  }\n  return classes.join(' ');\n}\n\n/**\n * One header cell.\n *\n * Everything responsive here is a CSS class, deliberately. This used to branch on\n * `useLgUp() ?? false`, which answers `undefined` until an effect has run — so the\n * FIRST render (server included) took the \"not lg\" path on every viewport: each\n * non-filterable cell returned `null`, and the survivors rendered with no width and\n * no alignment. One frame later the real values arrived, so the header visibly\n * filled in and the columns snapped into place — while `DataTableRow` and\n * `DataTableSkeleton`, which have always used plain `hideAt` classes, were correct\n * from the start. Header and body therefore disagreed about which columns exist and\n * how wide they are for exactly one frame, which is the flicker-and-jump on every\n * table's load.\n */\nfunction HeaderCell({ header, sort, onSortChange }: HeaderCellProps) {\n  if (header.isPlaceholder) return null;\n\n  const column = header.column;\n  const meta = column.columnDef.meta;\n  // Bind the filter config once. Branching on the local (rather than on a\n  // derived `Boolean(...)` flag) is what lets TypeScript narrow it to\n  // non-undefined in the JSX below without non-null assertions.\n  const filter = meta?.filter;\n  const keepOnTablet = keepsCellOnTablet(meta);\n  const align = meta?.align ?? 'left';\n  // Sort is opt-in via `meta.sortable`. Direction is fully consumer-driven via\n  // the `sort` prop; we do not consult TanStack's sort APIs here.\n  const canSort = meta?.sortable === true;\n  const sortDir: false | 'asc' | 'desc' = sort?.id === column.id ? (sort.desc ? 'desc' : 'asc') : false;\n\n  return (\n    <div\n      className={cn(\n        'items-stretch',\n        // Cells are hidden rather than dropped from the tree, so which columns\n        // exist never depends on when an effect ran.\n        getCellVisibilityClasses(keepOnTablet, meta?.hideAt),\n        // Width applies from lg only. Stated as \"always, neutralized below lg\"\n        // because `meta.width` is an opaque consumer string that cannot be\n        // prefixed. Only `keepOnTablet` cells need the override — the rest are\n        // hidden below lg anyway.\n        //\n        // `[&&]` doubles the class in the selector (`.cls.cls`, specificity 0-2-0),\n        // and it is load-bearing: consumers routinely write a RESPONSIVE width\n        // (`w-[80px] md:w-1/5` is the common shape), and between md and lg that\n        // `md:` rule and this `max-lg:` one both match. At equal specificity source\n        // order decides, and Tailwind emits the `max-lg` block BEFORE the `md` one\n        // — the same ordering documented for visibility above — so the tablet would\n        // keep a width the design drops there, leaving the cells spread across the\n        // row instead of packed beside their filters. An opaque consumer string\n        // cannot be outranked by ordering, so this wins on the cascade's other axis.\n        // Specificity rather than `!important`: it stays a normal declaration, so a\n        // consumer that genuinely needs a tablet width can still take it back with a\n        // more specific rule — `!important` could only be answered with another one.\n        meta?.width || 'min-w-0 flex-1',\n        keepOnTablet && 'max-lg:[&&]:w-auto max-lg:[&&]:flex-none max-lg:[&&]:basis-auto',\n        meta?.headerClassName,\n      )}\n    >\n      {filter ? (\n        <DataTableColumnFilter\n          column={column}\n          options={filter.options}\n          placement={filter.placement}\n          pending={filter.pending}\n          label={resolveHeaderLabel(header)}\n          align={align}\n        />\n      ) : (\n        <div\n          className={cn(\n            // Fixed 48px header height per design (20px label centered inside)\n            // instead of the padding-driven 44px.\n            'flex h-12 w-full select-none items-center gap-[var(--spacing-system-xsf)] rounded-sm transition-colors duration-200',\n            // Same \"always, neutralized below lg\" shape as the width above.\n            alignJustify(align),\n            keepOnTablet && 'max-lg:justify-start',\n            canSort && 'group cursor-pointer',\n          )}\n          onClick={canSort ? () => onSortChange?.(column.id) : undefined}\n        >\n          <HeaderLabel header={header} />\n          {canSort && <SortIcon sorted={sortDir} />}\n        </div>\n      )}\n    </div>\n  );\n}\n\n/**\n * THE header label styling. Exported because a column may supply a NODE header\n * (an icon beside the text), and without this those headers rendered unstyled\n * beside the string ones — same row, two different type treatments.\n */\nexport const DATA_TABLE_HEADER_LABEL_CLASS =\n  'whitespace-nowrap uppercase text-ods-text-secondary transition-colors duration-200 text-h5 group-hover:text-ods-text-primary';\n\nfunction HeaderLabel({ header }: { header: AnyHeader }) {\n  const headerDef = header.column.columnDef.header;\n  if (headerDef === undefined) return null;\n  if (typeof headerDef === 'string') {\n    return <span className={DATA_TABLE_HEADER_LABEL_CLASS}>{headerDef}</span>;\n  }\n  // Render-function or ReactNode: caller is responsible for styling.\n  return <>{flexRender(headerDef, header.getContext())}</>;\n}\n\nfunction SortIcon({ sorted }: { sorted: false | 'asc' | 'desc' }) {\n  if (sorted === 'asc') return <Arrow01UpIcon className=\"h-4 w-4 text-ods-accent\" />;\n  if (sorted === 'desc') return <Arrow01DownIcon className=\"h-4 w-4 text-ods-accent\" />;\n  return (\n    <SwitchVrIcon className=\"h-4 w-4 text-ods-text-secondary transition-colors duration-200 group-hover:text-ods-text-primary\" />\n  );\n}\n\nfunction resolveHeaderLabel(header: AnyHeader): string {\n  const h = header.column.columnDef.header;\n  return typeof h === 'string' ? h : header.column.id;\n}\n","'use client';\n\nimport type { Column } from '@tanstack/react-table';\nimport { useCallback, useMemo } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { FiltersDropdown, type FilterSection } from '../../features';\nimport { Filter02Icon } from '../../icons-v2-generated';\nimport type { DataTableFilterOption } from './types';\nimport { alignJustify } from './utils';\n\n// Stable reference for \"no filter selected\" — avoids creating a new `[]` per\n// render when `column.getFilterValue()` is `undefined`, which would otherwise\n// invalidate `currentFilters` useMemo every render.\nconst EMPTY_ARRAY: string[] = [];\n\nexport interface DataTableColumnFilterProps {\n  column: Column<unknown, unknown>;\n  options: DataTableFilterOption[];\n  placement?: 'bottom-start' | 'bottom-end' | 'bottom';\n  label: string;\n  align?: 'left' | 'center' | 'right';\n  /** Options are still loading — see `meta.filter.pending`. */\n  pending?: boolean;\n}\n\n/** Filter dropdown wrapper that reads/writes TanStack column filter state. */\nexport function DataTableColumnFilter({\n  column,\n  options,\n  placement = 'bottom-start',\n  label,\n  align = 'left',\n  pending = false,\n}: DataTableColumnFilterProps) {\n  const currentValue = column.getFilterValue() as string[] | undefined;\n  const activeCount = currentValue?.length ?? 0;\n\n  const sections = useMemo<FilterSection[]>(\n    () => [\n      {\n        id: column.id,\n        title: label,\n        type: 'checkbox',\n        options,\n        allowSelectAll: true,\n      },\n    ],\n    [column.id, label, options],\n  );\n\n  const currentFilters = useMemo(() => ({ [column.id]: currentValue ?? EMPTY_ARRAY }), [column.id, currentValue]);\n\n  const handleApply = useCallback(\n    (applied: Record<string, string[]>) => {\n      const next = applied[column.id] ?? [];\n      column.setFilterValue(next.length > 0 ? next : undefined);\n    },\n    [column],\n  );\n\n  const handleReset = useCallback(() => {\n    column.setFilterValue(undefined);\n  }, [column]);\n\n  // Nothing to choose from. Two ways to get here, and they look different:\n  //\n  //  - `pending`: the options are in flight. The funnel is drawn but inert, so\n  //    it does not appear out of nowhere when the query answers — the state a\n  //    skeleton, or a table whose facets load separately from its rows, asks for.\n  //  - otherwise: there are no options and there will be none. No funnel: a\n  //    control that cannot be opened reads as a broken one, and the design shows\n  //    the bare label. `meta.filter` still being declared is what keeps the\n  //    header cell alive on tablet (below `lg` only filterable cells show) and\n  //    keeps its 48px box — only the glyph goes.\n  const isEmpty = options.length === 0;\n\n  const trigger = (\n    <div\n      className={cn(\n        // Fixed 48px per design, same as the plain header cell next to it —\n        // NOT padding around the label. The two were drifting: 12px+20px+12px\n        // came out 44px here while the sibling was pinned to 48, so in a\n        // header mixing filterable and plain columns the labels sat on\n        // different centre lines and the filter's hit area was 4px short of\n        // the row it belongs to.\n        'group flex h-12 w-full select-none items-center gap-[var(--spacing-system-xsf)] rounded-sm transition-colors duration-200',\n        alignJustify(align),\n        isEmpty ? 'cursor-default' : 'cursor-pointer',\n      )}\n      // Only a real control announces itself as one. With no options this box\n      // is the plain header label the non-filterable columns beside it render.\n      aria-label={isEmpty ? undefined : `Filter by ${label}`}\n    >\n      <span\n        className={cn(\n          'whitespace-nowrap text-ods-text-secondary transition-colors duration-200 text-h5',\n          !isEmpty && 'group-hover:text-ods-text-primary',\n        )}\n      >\n        {label}\n      </span>\n      {/* Kept while PENDING — it holds its own width, so the label does not\n          shift sideways the moment the options resolve — and dropped once we\n          know there are none. */}\n      {(!isEmpty || pending) && (\n        <Filter02Icon\n          className={cn(\n            'h-4 w-4 transition-colors',\n            activeCount > 0 && 'text-ods-accent',\n            activeCount === 0 && 'text-ods-text-secondary',\n            activeCount === 0 && !isEmpty && 'group-hover:text-ods-text-primary',\n          )}\n        />\n      )}\n    </div>\n  );\n\n  // Same box, no dropdown: opening onto an empty list would be a worse answer\n  // than not opening at all.\n  if (isEmpty) {\n    return <div className=\"block w-full\">{trigger}</div>;\n  }\n\n  return (\n    <FiltersDropdown\n      className=\"!block w-full\"\n      triggerElement={trigger}\n      sections={sections}\n      currentFilters={currentFilters}\n      onApply={handleApply}\n      onReset={handleReset}\n      placement={placement}\n      dropdownClassName=\"min-w-60\"\n    />\n  );\n}\n","'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { DataTableSkeleton } from './data-table-skeleton';\n\nexport interface DataTableInfiniteFooterProps {\n  hasNextPage: boolean;\n  isFetchingNextPage: boolean;\n  onLoadMore: () => void;\n  /** Skeleton rows shown while fetching. Default `3`. */\n  skeletonRows?: number;\n  /** `IntersectionObserver` rootMargin. Default `'200px'`. */\n  rootMargin?: string;\n}\n\n/**\n * Infinite-scroll trigger for a `DataTable`. Place after `<DataTable.Body>`.\n * When the sentinel intersects the viewport, calls `onLoadMore`.\n */\nexport function DataTableInfiniteFooter({\n  hasNextPage,\n  isFetchingNextPage,\n  onLoadMore,\n  skeletonRows = 3,\n  rootMargin = '200px',\n}: DataTableInfiniteFooterProps) {\n  const sentinelRef = useRef<HTMLDivElement>(null);\n  // Refreshed after every commit, declared before the observer effect so it\n  // wins the same flush. Not in the render body: the reader is the\n  // IntersectionObserver callback, which cannot fire before a commit.\n  const onLoadMoreRef = useRef(onLoadMore);\n  useEffect(() => {\n    onLoadMoreRef.current = onLoadMore;\n  });\n\n  useEffect(() => {\n    if (!hasNextPage || isFetchingNextPage) return undefined;\n    const sentinel = sentinelRef.current;\n    if (!sentinel) return undefined;\n    const observer = new IntersectionObserver(\n      entries => {\n        if (entries[0]?.isIntersecting) onLoadMoreRef.current();\n      },\n      { rootMargin },\n    );\n    observer.observe(sentinel);\n    return () => observer.disconnect();\n  }, [hasNextPage, isFetchingNextPage, rootMargin]);\n\n  return (\n    <>\n      {isFetchingNextPage && <DataTableSkeleton rows={skeletonRows} />}\n      {hasNextPage && <div ref={sentinelRef} className=\"h-1\" aria-hidden=\"true\" />}\n    </>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport { useDataTableContext } from './data-table';\n\nexport interface DataTableRowCountProps {\n  /** Singular item name. Pluralized by adding an \"s\". */\n  itemName?: string;\n  /**\n   * Override the count. Use this for server-paginated tables where the table\n   * context only knows about loaded rows, not the total. Defaults to the\n   * number of rows currently in the table's row model.\n   */\n  totalCount?: number;\n  /** Custom pluralizer for irregular nouns. */\n  pluralize?: (count: number, itemName: string) => string;\n  /** Hide when the count is zero. Default `true`. */\n  hideWhenEmpty?: boolean;\n  className?: string;\n}\n\n/**\n * Renders \"8 devices\" (or your custom `itemName`) for the current row model.\n * Place wherever you want — above the table, in a toolbar, in a header right\n * slot.\n *\n * For server-paginated tables, pass `totalCount` explicitly — the table\n * context only knows about loaded rows.\n *\n * @example\n * <DataTable.Header rightSlot={\n *   <DataTable.RowCount itemName=\"device\" totalCount={pageInfo.total} />\n * } />\n */\nexport function DataTableRowCount({\n  itemName = 'result',\n  totalCount,\n  pluralize,\n  hideWhenEmpty = true,\n  className,\n}: DataTableRowCountProps) {\n  const table = useDataTableContext();\n  const count = totalCount ?? table.getRowModel().rows.length;\n\n  if (hideWhenEmpty && count === 0) return null;\n\n  const label = pluralize?.(count, itemName) ?? (count === 1 ? itemName : `${itemName}s`);\n\n  return (\n    <span className={cn('whitespace-nowrap text-ods-text-secondary text-h6', className)}>\n      {count} {label}\n    </span>\n  );\n}\n","'use client';\n\nimport {\n  getCoreRowModel,\n  getFilteredRowModel,\n  getSortedRowModel,\n  useReactTable,\n  type Table,\n  type TableOptions,\n} from '@tanstack/react-table';\n// Ensure ColumnMeta augmentation is loaded whenever useDataTable is used.\nimport './types';\n\n// Row-model factories are pure and safe to instantiate once per module.\n// Calling them inside render creates a new memoized getter on every render\n// whose internal cache is thrown away, causing avoidable row-model recomputes.\n// Instantiated at the erased row type: the factories never touch a row's\n// fields, and each use site re-applies the caller's `T` through the\n// `TableOptions<T>[...]` assertions below.\nconst coreRowModelFactory = getCoreRowModel<unknown>();\nconst sortedRowModelFactory = getSortedRowModel<unknown>();\nconst filteredRowModelFactory = getFilteredRowModel<unknown>();\n\nexport interface UseDataTableOptions<T> extends Omit<TableOptions<T>, 'getCoreRowModel'> {\n  /**\n   * Enable client-side sorting via `getSortedRowModel`.\n   * Default `false` — server sorts, TanStack only stores sort state.\n   */\n  clientSideSorting?: boolean;\n  /**\n   * Enable client-side filtering via `getFilteredRowModel`.\n   * Default `false` — server filters, TanStack only stores filter state.\n   */\n  clientSideFiltering?: boolean;\n}\n\n/**\n * Thin wrapper around `useReactTable` with sensible defaults for our\n * server-driven (Relay/REST) pagination setup. Turn on `clientSideSorting` /\n * `clientSideFiltering` for fully client-side tables.\n *\n * Row-model factories (`getCoreRowModel`, `getSortedRowModel`,\n * `getFilteredRowModel`) are instantiated once at module load instead of on\n * every render, preserving TanStack's internal row-model caches.\n *\n * ## Reference-stability contract (consumer's responsibility)\n *\n * TanStack tracks several options by reference. This hook just forwards them,\n * so the rules below apply unchanged. Each row covers a different cache layer\n * inside `useReactTable`:\n *\n * | Option                                  | Inline literal OK? | Why                                                                                          |\n * | --------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------- |\n * | `data`                                  | NO — `useMemo`     | Core row-model memo is keyed on `data` ref. New ref ⇒ full row-model rebuild every render.   |\n * | `columns`                               | NO — `useMemo`     | Column model is keyed on `columns` ref. Most expensive rebuild of all.                       |\n * | `getRowId`                              | recommended `useCallback` | Called during row-model construction. Safe with stable `data`, but inline costs nothing to fix. |\n * | `state` (the wrapper object)            | YES                | TanStack reads sub-fields, never compares the wrapper. Build it inline freely.               |\n * | `state.sorting`, `state.columnFilters`, `state.pagination`, etc. | NO — must be stable | Per-feature memos compare sub-field refs. Unstable sub-field ⇒ feature recomputes each render. |\n * | `onSortingChange`, `onColumnFiltersChange`, other `onXChange` callbacks | YES | Invoked, never compared. Inline arrows are fine.                                              |\n * | `meta`, `defaultColumn`                 | recommended memo   | Read on every cell render. Inline objects don't break correctness but defeat row `React.memo`. |\n *\n * ### Common upstream pitfalls that break the contract\n *\n * - `query.data ?? []` or `query.data ?? new Map()` in a hook return — the\n *   fallback is a fresh reference every render. Hoist the empty fallback to\n *   module scope as a frozen singleton.\n * - `Object.entries(urlParams).map(...)` in render — wrap in `useMemo` keyed\n *   on the source. `useApiParams` already returns reference-stable arrays for\n *   array-typed fields, so deps can name `params.tier` directly.\n * - Building `state` from a value that's recomputed inside the same render\n *   (e.g. `state={{ sorting: [{ id: sortKey, desc: false }] }}`) — memoise\n *   the array, not just the wrapper.\n *\n * ### Minimal correct usage\n *\n * ```ts\n * const data = useMemo(() => rows.map(toUi), [rows])\n * const columns = useMemo<ColumnDef<Row>[]>(() => [...], [])\n * const getRowId = useCallback((row: Row) => row.id, [])\n * const sorting = useMemo(() => [{ id: sortKey, desc: sortDesc }], [sortKey, sortDesc])\n *\n * const table = useDataTable({\n *   data,\n *   columns,\n *   getRowId,\n *   state: { sorting },           // wrapper inline — sub-field stable\n *   onSortingChange: handleSort,  // can be inline; memoise only if it goes\n *                                 // into another component's deps\n * })\n * ```\n */\nexport function useDataTable<T>({\n  clientSideSorting = false,\n  clientSideFiltering = false,\n  manualSorting,\n  manualFiltering,\n  getSortedRowModel: getSortedRowModelOverride,\n  getFilteredRowModel: getFilteredRowModelOverride,\n  ...rest\n}: UseDataTableOptions<T>): Table<T> {\n  // Opted out of the React Compiler on purpose — see the handle published at the\n  // end of this function. Nothing here is worth memoizing anyway: `setOptions`\n  // runs on every render regardless.\n  'use no memo';\n\n  const table = useReactTable<T>({\n    ...rest,\n    getCoreRowModel: coreRowModelFactory as TableOptions<T>['getCoreRowModel'],\n    getSortedRowModel: clientSideSorting\n      ? (getSortedRowModelOverride ?? (sortedRowModelFactory as TableOptions<T>['getSortedRowModel']))\n      : getSortedRowModelOverride,\n    getFilteredRowModel: clientSideFiltering\n      ? (getFilteredRowModelOverride ?? (filteredRowModelFactory as TableOptions<T>['getFilteredRowModel']))\n      : getFilteredRowModelOverride,\n    manualSorting: manualSorting ?? !clientSideSorting,\n    manualFiltering: manualFiltering ?? !clientSideFiltering,\n  });\n\n  // A fresh handle per render, and the reason this hook doesn't just return\n  // `useReactTable`'s value.\n  //\n  // `useReactTable` creates ONE instance for the life of the component and\n  // mutates it in place, so its identity is the same object whether a page of\n  // rows just arrived or nothing happened at all. Every consumer reads rows and\n  // state back out of that instance through context (`DataTable.Body`,\n  // `.Header`, `.RowCount`, `.CursorFooter`) — which worked only because the\n  // caller re-rendered and rebuilt their elements.\n  //\n  // Under the React Compiler it stops working, because the compiler caches on\n  // identity: `<DataTable table={table}>` and its children get cached on\n  // `table`, and inside the compiled `DataTableBody` the entire body —\n  // `table.getRowModel().rows` included — sits behind a `$[n] !== table` guard.\n  // A table that gains rows then renders nothing new. On an infinite-scroll\n  // table that is a loop: the sentinel never moves down, so\n  // `DataTable.InfiniteFooter` asks for the next page forever.\n  //\n  // The proxy forwards every read, write and TanStack mutation to that single\n  // instance, so nothing about the table's behaviour changes — only its\n  // identity, which is what puts it back inside React's reactive graph.\n  //\n  // The `use no memo` at the top of this hook is what keeps it a FRESH handle:\n  // without it the compiler caches this very object on `table` and hands back\n  // the same one forever, i.e. the bug. Keying a `useMemo` on\n  // `getRowModel()`/`getState()` instead is not better — `useReactTable`\n  // rebuilds `options.state` on every render anyway, so it would republish just\n  // as often while reading as though it did not.\n  return new Proxy(table, {});\n}\n","// Load ColumnMeta augmentation so consumers get typed `meta`.\nimport './types';\nimport { DataTableRoot } from './data-table';\nimport { DataTableBody } from './data-table-body';\nimport { DataTableCursorFooter } from './data-table-cursor-footer';\nimport { DataTableEmpty } from './data-table-empty';\nimport { DataTableHeader } from './data-table-header';\nimport { DataTableInfiniteFooter } from './data-table-infinite-footer';\nimport { DataTableRow } from './data-table-row';\nimport { DataTableRowCount } from './data-table-row-count';\nimport { DataTableSkeleton } from './data-table-skeleton';\n\n/**\n * Compound table primitive. Use `useDataTable` to build the table instance,\n * then compose `<DataTable>` + `<DataTable.Header/>` + `<DataTable.Body/>`\n * + `<DataTable.InfiniteFooter/>` or `<DataTable.CursorFooter/>` as needed.\n *\n * @example\n * const table = useDataTable({ data, columns, ... })\n * <DataTable table={table} className={...}>\n *   <DataTable.Header stickyHeader stickyHeaderOffset=\"top-[56px]\" />\n *   <DataTable.Body loading={isLoading} rowHref={d => `/devices/${d.id}`} />\n *   <DataTable.InfiniteFooter hasNextPage={hasNext} isFetchingNextPage={isLoadingNext} onLoadMore={() => loadNext(50)} />\n * </DataTable>\n */\nexport const DataTable = Object.assign(DataTableRoot, {\n  Header: DataTableHeader,\n  Body: DataTableBody,\n  Row: DataTableRow,\n  Skeleton: DataTableSkeleton,\n  Empty: DataTableEmpty,\n  InfiniteFooter: DataTableInfiniteFooter,\n  CursorFooter: DataTableCursorFooter,\n  RowCount: DataTableRowCount,\n});\n\n// Also export each piece by name for consumers that prefer named imports.\nexport {\n  DataTableRoot,\n  DataTableHeader,\n  DataTableBody,\n  DataTableRow,\n  DataTableSkeleton,\n  DataTableEmpty,\n  DataTableInfiniteFooter,\n  DataTableCursorFooter,\n  DataTableRowCount,\n};\n\nexport { useDataTableContext } from './data-table';\nexport { useDataTable } from './use-data-table';\nexport { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, ROW_SHELL_CLASSES } from './data-table-skeleton';\nexport { alignJustify, getHideClasses, multiSelectFilterFn } from './utils';\n\nexport type { DataTableFilterOption, TailwindBreakpoint } from './types';\nexport type { DataTableProps } from './data-table';\nexport { DATA_TABLE_HEADER_LABEL_CLASS } from './data-table-header';\nexport type { DataTableHeaderProps, DataTableSortState } from './data-table-header';\nexport type { DataTableBodyProps } from './data-table-body';\nexport type { DataTableRowProps } from './data-table-row';\nexport type { DataTableSkeletonProps } from './data-table-skeleton';\nexport type { DataTableEmptyProps } from './data-table-empty';\nexport type { DataTableInfiniteFooterProps } from './data-table-infinite-footer';\nexport type { DataTableCursorFooterProps } from './data-table-cursor-footer';\nexport type { DataTableRowCountProps } from './data-table-row-count';\nexport type { UseDataTableOptions } from './use-data-table';\n\n// Re-export commonly needed TanStack Table primitives so consumers don't need\n// a second dependency import line.\nexport {\n  flexRender,\n  createColumnHelper,\n  getCoreRowModel,\n  getExpandedRowModel,\n  getFacetedRowModel,\n  getFacetedUniqueValues,\n  getFilteredRowModel,\n  getGroupedRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n} from '@tanstack/react-table';\n\nexport type {\n  CellContext,\n  Column,\n  ColumnDef,\n  ColumnFiltersState,\n  ColumnHelper,\n  ColumnSort,\n  HeaderContext,\n  OnChangeFn,\n  Row,\n  RowSelectionState,\n  SortingState,\n  Table as DataTableInstance,\n  TableOptions,\n  VisibilityState,\n} from '@tanstack/react-table';\n","'use client';\n\nimport { forwardRef, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { EyeIcon } from '../icons-v2-generated/interface/eye-icon';\nimport { EyeOffIcon } from '../icons-v2-generated/interface/eye-off-icon';\nimport { Input, type InputProps } from './input';\n\nexport type PasswordInputProps = Omit<InputProps, 'type' | 'endAdornment'>;\n\n/** Password field with a show/hide toggle (eye icon), built on the base Input. */\nconst PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(({ disabled, className, ...props }, ref) => {\n  const [visible, setVisible] = useState(false);\n  const Icon = visible ? EyeOffIcon : EyeIcon;\n\n  return (\n    <Input\n      ref={ref}\n      type={visible ? 'text' : 'password'}\n      disabled={disabled}\n      // Ellipsize an over-long password/value when the field isn't focused (per the mockup)\n      className={cn('[&_input]:text-ellipsis', className)}\n      endAdornment={\n        <button\n          type=\"button\"\n          tabIndex={-1}\n          disabled={disabled}\n          aria-label={visible ? 'Hide password' : 'Show password'}\n          onClick={() => setVisible(value => !value)}\n          className=\"flex items-center text-ods-text-secondary transition-colors hover:text-ods-text-primary disabled:cursor-not-allowed\"\n        >\n          <Icon className=\"h-6 w-6\" />\n        </button>\n      }\n      {...props}\n    />\n  );\n});\nPasswordInput.displayName = 'PasswordInput';\n\nexport { PasswordInput };\n","'use client';\n\nimport type { CountryCode } from 'libphonenumber-js';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { KeyboardEvent } from 'react';\nimport { getCountryPhoneData, validatePhoneNumber, type CountryPhoneData } from '../../utils/country-phone-utils';\nimport { Input } from './input';\nimport { Select, SelectContent, SelectGroup, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from './select';\n\nexport interface PhoneInputProps {\n  value: string;\n  countryCode: CountryCode;\n  onPhoneChange: (phone: string) => void;\n  onCountryChange: (country: CountryCode) => void;\n  onValidationChange?: (isInvalid: boolean) => void;\n  disabled?: boolean;\n  placeholder?: string;\n  onKeyDown?: (e: KeyboardEvent) => void;\n}\n\nexport function PhoneInput({\n  value,\n  countryCode,\n  onPhoneChange,\n  onCountryChange,\n  onValidationChange,\n  disabled,\n  placeholder = 'Phone Number (optional)',\n  onKeyDown,\n}: PhoneInputProps) {\n  const { priority, others } = useMemo(() => getCountryPhoneData(), []);\n  const selectedCountry = useMemo(\n    () => [...priority, ...others].find(c => c.code === countryCode),\n    [countryCode, priority, others],\n  );\n\n  const [isInvalid, setIsInvalid] = useState(false);\n  const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);\n\n  const digitCount = useCallback((val: string) => val.replace(/[^0-9]/g, '').length, []);\n\n  const runValidation = useCallback(\n    (phone: string) => {\n      if (!phone || digitCount(phone) === 0) {\n        setIsInvalid(false);\n        onValidationChange?.(false);\n        return;\n      }\n      const invalid = !validatePhoneNumber(phone, countryCode);\n      setIsInvalid(invalid);\n      onValidationChange?.(invalid);\n    },\n    [countryCode, digitCount, onValidationChange],\n  );\n\n  const debouncedValidation = useCallback(\n    (phone: string) => {\n      if (debounceRef.current) clearTimeout(debounceRef.current);\n      debounceRef.current = setTimeout(() => runValidation(phone), 300);\n    },\n    [runValidation],\n  );\n\n  useEffect(() => {\n    return () => {\n      if (debounceRef.current) clearTimeout(debounceRef.current);\n    };\n  }, []);\n\n  return (\n    <div className=\"flex w-full min-w-0 gap-2\">\n      <Select\n        value={countryCode}\n        onValueChange={val => {\n          onCountryChange(val as CountryCode);\n          if (value) {\n            debouncedValidation(value);\n          }\n        }}\n        disabled={disabled}\n      >\n        <SelectTrigger className=\"w-[130px] shrink-0\">\n          <SelectValue>\n            {selectedCountry && (\n              <span className=\"flex items-center gap-1.5\">\n                <span>{selectedCountry.flag}</span>\n                <span>{selectedCountry.dialCode}</span>\n              </span>\n            )}\n          </SelectValue>\n        </SelectTrigger>\n        <SelectContent className=\"max-h-[300px] !w-[280px]\">\n          <SelectGroup>\n            {priority.map(country => (\n              <CountryOption key={country.code} country={country} />\n            ))}\n          </SelectGroup>\n          <SelectSeparator />\n          <SelectGroup>\n            {others.map(country => (\n              <CountryOption key={country.code} country={country} />\n            ))}\n          </SelectGroup>\n        </SelectContent>\n      </Select>\n\n      <Input\n        type=\"tel\"\n        value={value}\n        onChange={e => {\n          const val = e.target.value;\n          if (val === '' || /^[0-9\\-() ]*$/.test(val)) {\n            onPhoneChange(val);\n            if (digitCount(val) > 4) {\n              debouncedValidation(val);\n            } else if (digitCount(val) === 0) {\n              setIsInvalid(false);\n              onValidationChange?.(false);\n            }\n          }\n        }}\n        onBlur={() => runValidation(value)}\n        disabled={disabled}\n        placeholder={placeholder}\n        onKeyDown={onKeyDown}\n        className={`min-w-0 flex-1 ${isInvalid ? '!border-ods-warning' : ''}`}\n      />\n    </div>\n  );\n}\n\nfunction CountryOption({ country }: { country: CountryPhoneData }) {\n  return (\n    <SelectItem value={country.code} textValue={country.name}>\n      <span className=\"flex items-center gap-2\">\n        <span className=\"shrink-0\">{country.flag}</span>\n        <span className=\"shrink-0 text-ods-text-secondary\">{country.dialCode}</span>\n        <span className=\"inline-block max-w-[140px] overflow-hidden text-ellipsis whitespace-nowrap\">\n          {country.name}\n        </span>\n      </span>\n    </SelectItem>\n  );\n}\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';\nimport {\n  type ChangeEvent,\n  type KeyboardEvent,\n  type MouseEvent as ReactMouseEvent,\n  type ReactNode,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { useAutoLimitTags } from '../../hooks/ui/use-auto-limit-tags';\nimport { useDebounce } from '../../hooks/ui/use-debounce';\nimport { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';\nimport { cn } from '../../utils/cn';\nimport { SearchIcon } from '../icons-v2-generated';\nimport { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon';\nimport { HiddenTagsPopup } from './hidden-tags-popup';\nimport { Tag } from './tag';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n  id: string;\n  title: string;\n  description?: string;\n  path?: string;\n  type?: string;\n  icon?: ReactNode;\n  metadata?: Record<string, unknown>;\n}\n\nexport interface FilterChipData {\n  id: string;\n  label: string;\n  variant?: 'selected' | 'category' | 'subcategory' | 'tag';\n}\n\nexport interface SearchInputProps {\n  /** Placeholder text shown in the input */\n  placeholder?: string;\n  /** Controlled value */\n  value?: string;\n  /** Default value for uncontrolled mode */\n  defaultValue?: string;\n  /** Called when input value changes (raw, not debounced) */\n  onChange?: (value: string) => void;\n  /** Called when user presses Enter */\n  onSubmit?: (value: string) => void;\n  /** Search results to display in the dropdown */\n  results?: SearchResult[];\n  /** Whether results are loading */\n  isLoading?: boolean;\n  /** Called when a result row is selected.\n   *\n   *  `modifiers` carries the click event's modifier-key state when the\n   *  user picked the row via mouse — pass through so the consumer can\n   *  honor cmd/ctrl/shift/middle-click for background-tab navigation\n   *  (the row is a `<div role=\"option\">` rather than an `<a>`, so the\n   *  browser doesn't background-tab natively even with `target=\"_blank\"`).\n   *  Empty `{}` when the row was selected via keyboard Enter. */\n  onResultSelect?: (\n    result: SearchResult,\n    modifiers?: { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; altKey?: boolean; button?: number },\n  ) => void;\n  /** Debounce delay in ms. 0 disables debounce. Default 300 */\n  debounceMs?: number;\n  /** Custom renderer for a single result row */\n  renderResult?: (result: SearchResult, isHighlighted: boolean) => ReactNode;\n  /** Group results by a key derived from each result */\n  groupBy?: (result: SearchResult) => string;\n  /** Text shown when query meets minQueryLength but no results */\n  emptyResultsText?: string;\n  /** Force-control dropdown visibility. Default: auto */\n  showDropdown?: boolean;\n  /** Filter chips rendered inline before the input */\n  filterChips?: FilterChipData[];\n  /** Called when a filter chip is removed */\n  onFilterRemove?: (id: string) => void;\n  /** Element rendered before the input. Default: SearchIcon */\n  startAdornment?: ReactNode;\n  /** Element rendered after the input */\n  endAdornment?: ReactNode;\n  /** Extra class names for the outer container */\n  className?: string;\n  /** Extra class names for the dropdown */\n  dropdownClassName?: string;\n  /** Minimum characters before showing results. Default 2 */\n  minQueryLength?: number;\n  /**\n   * Read-only mode. The input is natively `disabled` (the greyed value +\n   * dimmed placeholder styles are already in `innerInputStyles`), the dropdown\n   * cannot be opened, chips lose their remove affordance and the clear button\n   * is hidden — so a bar used as a multi-select FIELD can be shown read-only\n   * without the host having to grey it out from the outside.\n   */\n  disabled?: boolean;\n  /** Maximum visible filter chips. \"auto\" measures available width. Default \"auto\" */\n  limitTags?: number | 'auto';\n  /** Custom render for the \"+N\" overflow text */\n  getLimitTagsText?: (more: number) => ReactNode;\n}\n\n// ---------------------------------------------------------------------------\n// Shared styles (consistent with Autocomplete / Input)\n// ---------------------------------------------------------------------------\n\nconst containerStyles = cn(\n  // Layout & spacing — matches lib Input component\n  'flex h-11 cursor-text items-center gap-2 rounded-[6px] border px-3 md:h-12',\n  'has-[:focus-visible]:outline-none',\n  'group',\n  'transition-colors duration-200',\n  // Theme palette — matches lib Input component\n  'border-ods-border bg-ods-card has-[:focus]:border-ods-accent',\n);\n\nconst innerInputStyles = cn(\n  'min-w-[60px] flex-1 border-none bg-transparent outline-none',\n  'text-h4',\n  'text-ods-text-primary placeholder:text-ods-text-secondary',\n  // Disabled - match Input exactly (value greys out, placeholder dims further)\n  'disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border',\n  'touch-manipulation',\n);\n\n// ---------------------------------------------------------------------------\n// Helper: chip variant → Tag variant mapping\n// ---------------------------------------------------------------------------\n\nfunction chipVariantToTagVariant(variant?: FilterChipData['variant']): 'primary' | 'outline' | 'badge' {\n  switch (variant) {\n    case 'selected':\n      return 'primary';\n    // Content tags render with the unified badge skin (ods-card + ods-border,\n    // mono uppercase) — identical to the public EntityTagBadges display.\n    case 'tag':\n      return 'badge';\n    case 'category':\n    case 'subcategory':\n    default:\n      return 'outline';\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function SearchInput({\n  placeholder = 'Search...',\n  value,\n  defaultValue = '',\n  onChange,\n  onSubmit,\n  results = [],\n  isLoading = false,\n  onResultSelect,\n  debounceMs = 300,\n  renderResult,\n  groupBy,\n  emptyResultsText = 'No results found',\n  showDropdown: showDropdownProp,\n  filterChips = [],\n  onFilterRemove,\n  startAdornment,\n  endAdornment,\n  className,\n  dropdownClassName,\n  minQueryLength = 2,\n  disabled = false,\n  limitTags = 'auto',\n  getLimitTagsText = (more: number) => `+${more}`,\n}: SearchInputProps) {\n  // ---- Controlled / uncontrolled ----\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const currentValue = onChange ? (value ?? '') : internalValue;\n\n  // ---- Debounce ----\n  const debouncedValue = useDebounce(currentValue, debounceMs);\n\n  // ---- Popover state ----\n  const [isOpen, setIsOpen] = useState(false);\n  const [highlightedIndex, setHighlightedIndex] = useState(-1);\n  const keyboardPadding = useKeyboardCollisionPadding();\n\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // ---- Auto-limit tags ----\n  const currentPlaceholder = filterChips.length > 0 ? 'Add filter...' : placeholder;\n\n  const {\n    visibleCount: rawVisibleCount,\n    middleRef,\n    measureRef,\n    textMeasureRef,\n    badgeRef,\n    inputRef,\n  } = useAutoLimitTags({\n    count: filterChips.length,\n    limitTags,\n    // When chips exist, pass empty placeholder so the hook only reserves input minWidth,\n    // not the full placeholder text width — gives more room for chips on narrow screens\n    placeholder: filterChips.length > 0 ? '' : placeholder,\n  });\n\n  // Always show at least 1 chip when chips exist (industry standard: Gmail, MUI, Ant Design)\n  const visibleCount = filterChips.length > 0 ? Math.max(1, rawVisibleCount) : rawVisibleCount;\n\n  // ---- Hidden tags popup ----\n  const hiddenTagsRef = useRef<HTMLDivElement>(null);\n  const hiddenTagsPopupRef = useRef<HTMLDivElement>(null);\n  const [showHiddenTags, setShowHiddenTags] = useState(false);\n\n  useEffect(() => {\n    if (!showHiddenTags) return undefined;\n    const handleClick = (e: MouseEvent) => {\n      const target = e.target as Node;\n      if (!hiddenTagsRef.current?.contains(target) && !hiddenTagsPopupRef.current?.contains(target)) {\n        setShowHiddenTags(false);\n      }\n    };\n    document.addEventListener('mousedown', handleClick);\n    return () => document.removeEventListener('mousedown', handleClick);\n  }, [showHiddenTags]);\n\n  // The popup is rendered OUTSIDE the overflow-hidden chip row, so it has to be\n  // pushed across to sit under the `+N` badge. Measured in a layout effect\n  // rather than during render: `getBoundingClientRect()` is a value React does\n  // not track, so a render-time read pinned the popup to wherever the badge\n  // happened to be at that one render and never followed it afterwards —\n  // removing a chip from the popup shifts the badge left, and the popup stayed\n  // behind. Unconditional so every commit re-measures; it runs before paint,\n  // so there is no visible jump.\n  useIsomorphicLayoutEffect(() => {\n    const popup = hiddenTagsPopupRef.current;\n    const badge = badgeRef.current;\n    if (!popup || !badge) return;\n    const originLeft = containerRef.current?.getBoundingClientRect().left ?? 0;\n    popup.style.left = `${badge.getBoundingClientRect().left - originLeft}px`;\n  });\n\n  // ---- Derived chip slicing ----\n  const hiddenCount = filterChips.length - visibleCount;\n  const visibleChips = filterChips.slice(0, visibleCount);\n  const hiddenChips = filterChips.slice(visibleCount);\n\n  // ---- Derive flat list (possibly grouped) ----\n  const { flatResults, groups } = useMemo(() => {\n    if (!groupBy) return { flatResults: results, groups: null };\n\n    const grouped = new Map<string, SearchResult[]>();\n    for (const r of results) {\n      const key = groupBy(r);\n      const arr = grouped.get(key);\n      if (arr) {\n        arr.push(r);\n      } else {\n        grouped.set(key, [r]);\n      }\n    }\n    return { flatResults: results, groups: grouped };\n  }, [results, groupBy]);\n\n  // ---- Auto-show logic ----\n  const meetsMinQuery = debouncedValue.length >= minQueryLength;\n  const autoShow = meetsMinQuery;\n  const dropdownVisible = !disabled && (showDropdownProp ?? (isOpen && autoShow));\n\n  // ---- Reset highlight when results change ----\n  // Adjusted while rendering, not from an effect: the highlight is drawn from\n  // this value in THIS render, so an effect commits one frame highlighting\n  // whatever result now happens to sit at the old index — and Enter pressed in\n  // that frame navigates to it.\n  const [highlightedFor, setHighlightedFor] = useState(flatResults.length);\n  if (highlightedFor !== flatResults.length) {\n    setHighlightedFor(flatResults.length);\n    setHighlightedIndex(-1);\n  }\n\n  // ---- Handlers ----\n  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n    const newVal = e.target.value;\n    if (onChange) {\n      onChange(newVal);\n    } else {\n      setInternalValue(newVal);\n    }\n    if (!isOpen) setIsOpen(true);\n    setHighlightedIndex(-1);\n  };\n\n  const handleClear = (e: ReactMouseEvent) => {\n    e.stopPropagation();\n    e.preventDefault();\n    if (onChange) {\n      onChange('');\n    } else {\n      setInternalValue('');\n    }\n    inputRef.current?.focus();\n  };\n\n  const handleResultClick = (result: SearchResult, e?: ReactMouseEvent) => {\n    onResultSelect?.(\n      result,\n      e\n        ? {\n            metaKey: e.metaKey,\n            ctrlKey: e.ctrlKey,\n            shiftKey: e.shiftKey,\n            altKey: e.altKey,\n            button: e.button,\n          }\n        : undefined,\n    );\n    setIsOpen(false);\n  };\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault();\n        if (!isOpen) setIsOpen(true);\n        setHighlightedIndex(prev => (prev < flatResults.length - 1 ? prev + 1 : 0));\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        setHighlightedIndex(prev => (prev > 0 ? prev - 1 : flatResults.length - 1));\n        break;\n      case 'Enter':\n        e.preventDefault();\n        if (highlightedIndex >= 0 && flatResults[highlightedIndex]) {\n          handleResultClick(flatResults[highlightedIndex]);\n        } else {\n          onSubmit?.(currentValue);\n        }\n        break;\n      case 'Escape':\n        setIsOpen(false);\n        setHighlightedIndex(-1);\n        break;\n      case 'Backspace':\n        if (!currentValue && filterChips.length > 0 && onFilterRemove) {\n          onFilterRemove(filterChips[filterChips.length - 1].id);\n        }\n        break;\n    }\n  };\n\n  const handleOpenChange = (open: boolean) => {\n    setIsOpen(open);\n  };\n\n  // ---- Default result renderer ----\n  const defaultRenderResult = (result: SearchResult, isHighlighted: boolean) => (\n    <div className=\"flex w-full min-w-0 items-center gap-3\">\n      {result.icon && <span className=\"flex-shrink-0 text-ods-text-secondary [&_svg]:size-4\">{result.icon}</span>}\n      <div className=\"min-w-0 flex-1\">\n        <div\n          className={cn('truncate text-h6', isHighlighted ? 'text-ods-accent' : 'text-ods-text-primary')}\n          title={result.title}\n        >\n          {result.title}\n        </div>\n        {result.description && (\n          <div className=\"mt-0.5 truncate text-ods-text-secondary text-h6\" title={result.description}>\n            {result.description}\n          </div>\n        )}\n      </div>\n      {result.type && <span className=\"flex-shrink-0 uppercase text-ods-text-muted text-h6\">{result.type}</span>}\n    </div>\n  );\n\n  // ---- Render a result row ----\n  const renderRow = (result: SearchResult, index: number) => {\n    const isHighlighted = index === highlightedIndex;\n    return (\n      <div\n        key={result.id}\n        role=\"option\"\n        aria-selected={isHighlighted}\n        className={cn(\n          'flex min-h-10 cursor-pointer items-center border-b border-ods-border px-3 transition-colors last:border-b-0',\n          isHighlighted && 'bg-ods-bg-hover',\n          !isHighlighted && 'hover:bg-ods-bg-hover',\n        )}\n        onClick={e => handleResultClick(result, e)}\n        onMouseEnter={() => setHighlightedIndex(index)}\n      >\n        {renderResult ? renderResult(result, isHighlighted) : defaultRenderResult(result, isHighlighted)}\n      </div>\n    );\n  };\n\n  // ---- Dropdown content ----\n  const renderDropdownContent = () => {\n    if (isLoading) {\n      return <div className=\"px-4 py-3 text-ods-text-secondary text-h6\">Loading...</div>;\n    }\n\n    if (flatResults.length === 0) {\n      return <div className=\"px-4 py-3 text-ods-text-secondary text-h6\">{emptyResultsText}</div>;\n    }\n\n    if (groups) {\n      let globalIndex = 0;\n      return Array.from(groups.entries()).map(([groupLabel, groupResults]) => (\n        <div key={groupLabel}>\n          <div className=\"bg-ods-bg px-4 py-2 font-semibold uppercase text-ods-text-secondary text-h6\">\n            {groupLabel}\n          </div>\n          {groupResults.map(result => {\n            const idx = globalIndex++;\n            return renderRow(result, idx);\n          })}\n        </div>\n      ));\n    }\n\n    return flatResults.map((result, index) => renderRow(result, index));\n  };\n\n  // ---- Determine if we have a value worth clearing ----\n  const hasValue = currentValue.length > 0;\n\n  return (\n    <div className={cn('relative', className)} ref={containerRef}>\n      <PopoverPrimitive.Root open={dropdownVisible} onOpenChange={handleOpenChange} modal={false}>\n        <PopoverPrimitive.Anchor asChild>\n          <div\n            className={cn(\n              containerStyles,\n              disabled\n                ? 'cursor-not-allowed opacity-60'\n                : 'hover:border-ods-border-hover hover:bg-ods-bg-hover active:border-ods-border-active active:bg-ods-bg-active',\n              dropdownVisible && '!border-ods-accent',\n            )}\n            onClick={() => {\n              if (disabled) return;\n              inputRef.current?.focus();\n              setIsOpen(true);\n            }}\n          >\n            {/* Start Adornment — pinned left, shrink-0 */}\n            <span className=\"flex-shrink-0 text-ods-text-secondary transition-colors duration-200 group-has-[:focus]:text-ods-accent [&_svg]:size-4 md:[&_svg]:size-6\">\n              {startAdornment !== undefined ? startAdornment : <SearchIcon />}\n            </span>\n\n            {/* Middle zone: chips + input — overflow hidden, single line */}\n            <div ref={middleRef} className=\"flex min-w-0 flex-1 items-center gap-2 overflow-hidden\">\n              {/* Visible filter chips */}\n              {visibleChips.map(chip => (\n                <Tag\n                  key={chip.id}\n                  variant={chipVariantToTagVariant(chip.variant)}\n                  label={chip.label}\n                  labelClassName=\"max-w-[120px] truncate\"\n                  onClose={onFilterRemove && !disabled ? () => onFilterRemove(chip.id) : undefined}\n                />\n              ))}\n\n              {/* \"+N\" overflow badge */}\n              {hiddenCount > 0 && (\n                <div ref={hiddenTagsRef} className=\"shrink-0\">\n                  <button\n                    ref={badgeRef}\n                    type=\"button\"\n                    onClick={e => {\n                      e.stopPropagation();\n                      setShowHiddenTags(prev => !prev);\n                    }}\n                    className={cn(\n                      'flex h-8 items-center px-2',\n                      'rounded-[6px] border border-ods-border bg-ods-card',\n                      'text-ods-text-secondary text-h5',\n                      'cursor-pointer transition-colors hover:bg-ods-bg-hover',\n                    )}\n                    aria-label={`${hiddenCount} more selected filters`}\n                  >\n                    {getLimitTagsText(hiddenCount)}\n                  </button>\n                </div>\n              )}\n\n              {/* Input */}\n              <input\n                ref={inputRef}\n                type=\"text\"\n                value={currentValue}\n                onChange={handleChange}\n                onKeyDown={handleKeyDown}\n                disabled={disabled}\n                onFocus={() => {\n                  if (disabled) return;\n                  setIsOpen(true);\n                  setShowHiddenTags(false);\n                }}\n                placeholder={currentPlaceholder}\n                className={innerInputStyles}\n              />\n            </div>\n\n            {/* End adornment / Clear — pinned right, shrink-0 */}\n            <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n              {hasValue && !disabled && (\n                <button\n                  type=\"button\"\n                  onClick={handleClear}\n                  className=\"flex items-center justify-center transition-opacity hover:opacity-70\"\n                  aria-label=\"Clear search\"\n                >\n                  <XmarkCircleIcon className=\"text-ods-text-secondary\" size={24} />\n                </button>\n              )}\n              {endAdornment}\n            </div>\n          </div>\n        </PopoverPrimitive.Anchor>\n\n        <PopoverPrimitive.Content\n          className={cn(\n            'z-50 mt-1 w-[var(--radix-popover-trigger-width)]',\n            'overflow-hidden rounded-[6px] border border-ods-border bg-ods-card shadow-lg',\n            'flex max-h-[var(--radix-popper-available-height)] flex-col',\n            'data-[state=open]:animate-in data-[state=closed]:animate-out',\n            'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n            'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n            'data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2',\n            dropdownClassName,\n          )}\n          sideOffset={4}\n          align=\"start\"\n          // The anchor is the search field that raises the software keyboard,\n          // so these suggestions are always positioned with the keyboard up —\n          // see useKeyboardCollisionPadding.\n          collisionPadding={{ bottom: keyboardPadding }}\n          onOpenAutoFocus={e => {\n            e.preventDefault();\n            inputRef.current?.focus();\n          }}\n          onInteractOutside={e => {\n            if (containerRef.current?.contains(e.target as Node)) {\n              e.preventDefault();\n            }\n          }}\n        >\n          <ScrollAreaPrimitive.Root className=\"flex min-h-0 flex-col overflow-hidden\">\n            <ScrollAreaPrimitive.Viewport className=\"max-h-[320px] min-h-0 w-full\">\n              <div role=\"listbox\">{renderDropdownContent()}</div>\n            </ScrollAreaPrimitive.Viewport>\n            <ScrollAreaPrimitive.Scrollbar className=\"hidden\" orientation=\"vertical\">\n              <ScrollAreaPrimitive.Thumb />\n            </ScrollAreaPrimitive.Scrollbar>\n          </ScrollAreaPrimitive.Root>\n        </PopoverPrimitive.Content>\n      </PopoverPrimitive.Root>\n\n      {/* Hidden tags popup — outside overflow-hidden, positioned under badge */}\n      {showHiddenTags && hiddenCount > 0 && (\n        <HiddenTagsPopup\n          ref={hiddenTagsPopupRef}\n          items={hiddenChips.map(chip => ({ label: chip.label, value: chip.id }))}\n          onRemove={removedTag => {\n            onFilterRemove?.(removedTag as string);\n            if (hiddenCount <= 1) setShowHiddenTags(false);\n          }}\n        />\n      )}\n\n      {/* Off-screen measurement: placeholder text width — fixed positioning avoids scroll contribution */}\n      <span\n        ref={textMeasureRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none fixed -left-[9999px] top-0 whitespace-nowrap text-ods-text-primary\"\n      >\n        {currentPlaceholder}\n      </span>\n\n      {/* Off-screen measurement: all chip widths */}\n      <div ref={measureRef} aria-hidden=\"true\" className=\"pointer-events-none fixed -left-[9999px] top-0 flex gap-2\">\n        {filterChips.map(chip => (\n          <Tag\n            key={`m-${chip.id}`}\n            variant={chipVariantToTagVariant(chip.variant)}\n            label={chip.label}\n            labelClassName=\"max-w-[120px] truncate\"\n            onClose={() => {}}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default SearchInput;\n","'use client';\n\nimport * as CheckboxPrimitive from '@radix-ui/react-checkbox';\nimport { Fragment } from 'react';\nimport { cn } from '../../utils/cn';\nimport { CheckboxCheckmarkIcon } from '../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon';\n\nexport interface FilterListItemProps {\n  title: string;\n  meta?: Array<string | number>;\n  selected?: boolean;\n  onToggle?: (selected: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n}\n\nexport function FilterListItem({\n  title,\n  meta,\n  selected = false,\n  onToggle,\n  disabled = false,\n  className,\n}: FilterListItemProps) {\n  const handleToggle = () => {\n    if (disabled) return;\n    onToggle?.(!selected);\n  };\n\n  return (\n    <div\n      role=\"button\"\n      tabIndex={disabled ? -1 : 0}\n      aria-pressed={selected}\n      aria-disabled={disabled}\n      onClick={handleToggle}\n      onKeyDown={e => {\n        if (disabled) return;\n        if (e.key === 'Enter' || e.key === ' ') {\n          e.preventDefault();\n          handleToggle();\n        }\n      }}\n      className={cn(\n        'flex w-full items-center gap-[var(--spacing-system-m)] px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)]',\n        'border-b border-ods-border last:border-b-0',\n        'outline-none transition-colors',\n        'focus-visible:ring-2 focus-visible:ring-ods-accent',\n        disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-ods-bg-hover',\n        selected ? 'bg-ods-accent-secondary hover:bg-ods-accent-secondary' : 'bg-ods-bg',\n        className,\n      )}\n    >\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <p className=\"truncate text-ods-text-primary text-h4\" title={title}>\n          {title}\n        </p>\n\n        {meta && meta.length > 0 && (\n          <div\n            className={cn(\n              'flex items-start gap-[var(--spacing-system-xxs)] text-h6',\n              selected ? 'text-ods-accent' : 'text-ods-text-secondary',\n            )}\n          >\n            {meta.map((item, index) => (\n              <Fragment key={index}>\n                {index > 0 && <span aria-hidden=\"true\">•</span>}\n                <span>{item}</span>\n              </Fragment>\n            ))}\n          </div>\n        )}\n      </div>\n\n      <CheckboxPrimitive.Root\n        checked={selected}\n        onCheckedChange={c => onToggle?.(c === true)}\n        onClick={e => e.stopPropagation()}\n        disabled={disabled}\n        aria-label={title}\n        className={cn(\n          'flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',\n          selected ? 'border-ods-accent bg-ods-accent' : 'border-ods-text-secondary bg-ods-card',\n        )}\n      >\n        <CheckboxPrimitive.Indicator className=\"flex items-center justify-center text-ods-text-on-accent\">\n          <CheckboxCheckmarkIcon size={10} />\n        </CheckboxPrimitive.Indicator>\n      </CheckboxPrimitive.Root>\n    </div>\n  );\n}\n\nexport interface FilterListProps<T = unknown> {\n  items: Array<{\n    id: string;\n    title: string;\n    meta?: Array<string | number>;\n    disabled?: boolean;\n    data?: T;\n  }>;\n  selectedIds: string[];\n  onChange: (selectedIds: string[]) => void;\n  multiple?: boolean;\n  className?: string;\n}\n\nexport function FilterList<T = unknown>({\n  items,\n  selectedIds,\n  onChange,\n  multiple = true,\n  className,\n}: FilterListProps<T>) {\n  const handleToggle = (id: string, selected: boolean) => {\n    if (multiple) {\n      onChange(selected ? [...selectedIds, id] : selectedIds.filter(v => v !== id));\n    } else {\n      onChange(selected ? [id] : []);\n    }\n  };\n\n  return (\n    <div role=\"listbox\" aria-multiselectable={multiple} className={cn('flex w-full flex-col', className)}>\n      {items.map(item => (\n        <FilterListItem\n          key={item.id}\n          title={item.title}\n          meta={item.meta}\n          selected={selectedIds.includes(item.id)}\n          disabled={item.disabled}\n          onToggle={selected => handleToggle(item.id, selected)}\n        />\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport {\n  type KeyboardEvent,\n  type MouseEvent as ReactMouseEvent,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\n\nimport { useAutoLimitTags } from '../../hooks/ui/use-auto-limit-tags';\nimport { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';\nimport { cn } from '../../utils/cn';\nimport { SearchIcon } from '../icons-v2-generated/interface/search-icon';\nimport { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon';\nimport { HiddenTagsPopup } from './hidden-tags-popup';\nimport { Tag } from './tag';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TagSearchOption<T = string> {\n  label: string;\n  value: T;\n}\n\nexport interface TagSearchInputProps<T = string> {\n  /** Active tags displayed inline */\n  tags: TagSearchOption<T>[];\n  /** Controlled search input value */\n  searchValue: string;\n  /** Called when input text changes */\n  onSearchChange: (value: string) => void;\n  /** Called when a single tag is removed */\n  onTagRemove: (value: T) => void;\n  /** Called when the clear-all button is clicked (clears tags + input) */\n  onClearAll?: () => void;\n  /** Placeholder when no tags are present */\n  placeholder?: string;\n  /** Placeholder when tags exist */\n  addMorePlaceholder?: string;\n  /** Whether the component is disabled */\n  disabled?: boolean;\n  /** Extra class names for the outer wrapper */\n  className?: string;\n  /** Show the clear-all (x) icon on the right. Default true */\n  showClearAll?: boolean;\n  /** Called when Enter is pressed */\n  onSubmit?: (value: string) => void;\n  /** Forward arbitrary key events */\n  onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n  /** Custom render for tag label */\n  renderTag?: (option: TagSearchOption<T>) => ReactNode;\n  /** Custom render function for the \"+N\" overflow text */\n  getLimitTagsText?: (more: number) => ReactNode;\n  /** Maximum number of visible tags. Set to \"auto\" for automatic calculation based on available width. Default \"auto\" */\n  limitTags?: number | 'auto';\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function TagSearchInput<T = string>({\n  tags,\n  searchValue,\n  onSearchChange,\n  onTagRemove,\n  onClearAll,\n  placeholder = 'Search...',\n  addMorePlaceholder = 'Add More...',\n  disabled = false,\n  className,\n  showClearAll = true,\n  onSubmit,\n  onKeyDown,\n  renderTag,\n  getLimitTagsText = (more: number) => `+${more}`,\n  limitTags = 'auto',\n}: TagSearchInputProps<T>) {\n  const currentPlaceholder = tags.length === 0 ? placeholder : addMorePlaceholder;\n\n  const { visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef } = useAutoLimitTags({\n    count: tags.length,\n    limitTags,\n    placeholder: currentPlaceholder,\n  });\n\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const hiddenTagsRef = useRef<HTMLDivElement>(null);\n  const hiddenTagsPopupRef = useRef<HTMLDivElement>(null);\n  const [showHiddenTags, setShowHiddenTags] = useState(false);\n\n  // ---- Close hidden tags popup on outside click ----\n  useEffect(() => {\n    if (!showHiddenTags) return undefined;\n    const handleClick = (e: MouseEvent) => {\n      const target = e.target as Node;\n      if (!hiddenTagsRef.current?.contains(target) && !hiddenTagsPopupRef.current?.contains(target)) {\n        setShowHiddenTags(false);\n      }\n    };\n    document.addEventListener('mousedown', handleClick);\n    return () => document.removeEventListener('mousedown', handleClick);\n  }, [showHiddenTags]);\n\n  // The popup is rendered OUTSIDE the overflow-hidden tag row, so it has to be\n  // pushed across to sit under the `+N` badge. Measured in a layout effect\n  // rather than during render: `getBoundingClientRect()` is a value React does\n  // not track, so a render-time read pinned the popup to wherever the badge\n  // happened to be at that one render and never followed it afterwards —\n  // removing a tag from the popup shifts the badge left, and the popup stayed\n  // behind. Unconditional so every commit re-measures; it runs before paint,\n  // so there is no visible jump.\n  useIsomorphicLayoutEffect(() => {\n    const popup = hiddenTagsPopupRef.current;\n    const badge = badgeRef.current;\n    if (!popup || !badge) return;\n    const originLeft = wrapperRef.current?.getBoundingClientRect().left ?? 0;\n    popup.style.left = `${badge.getBoundingClientRect().left - originLeft}px`;\n  });\n\n  // ---- Derived state ----\n  const hiddenCount = tags.length - visibleCount;\n  const visibleTags = tags.slice(0, visibleCount);\n  const hiddenTags = tags.slice(visibleCount);\n  const hasValue = tags.length > 0 || searchValue.length > 0;\n\n  // ---- Event handlers ----\n  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter' && onSubmit) {\n      e.preventDefault();\n      onSubmit(searchValue);\n    }\n    onKeyDown?.(e);\n  };\n\n  const handleClearAll = (e: ReactMouseEvent) => {\n    e.stopPropagation();\n    e.preventDefault();\n    onClearAll?.();\n    inputRef.current?.focus();\n  };\n\n  return (\n    <div ref={wrapperRef} className=\"relative\">\n      {/* ---- Visible container ---- */}\n      <div\n        className={cn(\n          'flex h-11 cursor-text items-center rounded-[6px] border md:h-12',\n          'transition-colors duration-200',\n          'border-ods-border bg-ods-card',\n          'has-[:focus]:border-ods-accent',\n          !disabled && 'hover:border-ods-border-hover hover:bg-ods-bg-hover',\n          disabled && 'bg-ods-bg',\n          disabled && 'cursor-not-allowed',\n          // Match Input: grey the text and the icons, don't fade the whole\n          // control (a blanket opacity also washes out the border). Icons only —\n          // this container has no adornment spans of its own, so a `[&_span]`\n          // rule would ONLY have reached the selected `Tag` chips, whose label is\n          // a span, and greyed them into their own fill in the light theme.\n          'has-[:disabled]:[&_svg]:text-ods-text-disabled',\n          className,\n        )}\n        onClick={() => {\n          if (!disabled) inputRef.current?.focus();\n        }}\n      >\n        {/* Search icon — pinned left, responsive size */}\n        <div className=\"flex shrink-0 items-center pl-3\">\n          <SearchIcon className=\"size-4 text-ods-text-secondary md:size-6\" />\n        </div>\n\n        {/* Middle zone: tags + input — overflow hidden so tags never push clear btn */}\n        <div ref={middleRef} className=\"flex min-w-0 flex-1 items-center gap-2 overflow-hidden px-2\">\n          {visibleTags.map(tag => (\n            <Tag\n              key={String(tag.value)}\n              variant=\"outline\"\n              label={renderTag ? renderTag(tag) : tag.label}\n              labelClassName=\"max-w-[120px] truncate\"\n              onClose={!disabled ? () => onTagRemove(tag.value) : undefined}\n            />\n          ))}\n\n          {/* \"+N\" overflow badge */}\n          {hiddenCount > 0 && (\n            <div ref={hiddenTagsRef} className=\"shrink-0\">\n              <button\n                ref={badgeRef}\n                type=\"button\"\n                onClick={e => {\n                  e.stopPropagation();\n                  setShowHiddenTags(prev => !prev);\n                }}\n                className={cn(\n                  'flex h-8 items-center px-2',\n                  'rounded-[6px] border border-ods-border bg-ods-card',\n                  'text-ods-text-secondary text-h5',\n                  'cursor-pointer transition-colors hover:bg-ods-bg-hover',\n                )}\n              >\n                {getLimitTagsText(hiddenCount)}\n              </button>\n            </div>\n          )}\n\n          {/* Search input */}\n          <input\n            ref={inputRef}\n            type=\"text\"\n            value={searchValue}\n            onChange={e => onSearchChange(e.target.value)}\n            onKeyDown={handleKeyDown}\n            placeholder={currentPlaceholder}\n            disabled={disabled}\n            className={cn(\n              'min-w-[60px] flex-1 border-none bg-transparent outline-none',\n              'text-h4',\n              'text-ods-text-primary placeholder:text-ods-text-secondary',\n              // Disabled - match Input exactly (value greys out, placeholder dims further)\n              'disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border',\n            )}\n          />\n        </div>\n\n        {/* Clear all — pinned right */}\n        {showClearAll && hasValue && !disabled && (\n          <button\n            type=\"button\"\n            onClick={handleClearAll}\n            className=\"flex shrink-0 cursor-pointer items-center justify-center pr-3 transition-opacity hover:opacity-70\"\n            aria-label=\"Clear all\"\n          >\n            <XmarkCircleIcon className=\"size-4 text-ods-text-secondary md:size-6\" />\n          </button>\n        )}\n      </div>\n\n      {/* ---- Hidden tags popup — outside overflow-hidden, positioned under badge ---- */}\n      {showHiddenTags && hiddenCount > 0 && (\n        <HiddenTagsPopup\n          ref={hiddenTagsPopupRef}\n          items={hiddenTags}\n          disabled={disabled}\n          onRemove={value => {\n            onTagRemove(value as T);\n            if (hiddenCount <= 1) setShowHiddenTags(false);\n          }}\n        />\n      )}\n\n      {/* ---- Off-screen measurement containers ---- */}\n      <span\n        ref={textMeasureRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none invisible absolute left-0 top-0 -z-10 whitespace-nowrap text-h4\"\n      >\n        {currentPlaceholder}\n      </span>\n\n      <div\n        ref={measureRef}\n        aria-hidden=\"true\"\n        className=\"pointer-events-none invisible absolute left-0 top-0 -z-10 flex gap-2\"\n      >\n        {tags.map(tag => (\n          <Tag\n            key={`m-${String(tag.value)}`}\n            variant=\"outline\"\n            label={renderTag ? renderTag(tag) : tag.label}\n            labelClassName=\"max-w-[120px] truncate\"\n            onClose={() => {}}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport type { ICommand } from '@uiw/react-md-editor';\nimport { Loader2, Upload } from 'lucide-react';\nimport type React from 'react';\nimport { useRef, useCallback, useState, useEffect } from 'react';\nimport dynamic from '../../embed-shims/next-dynamic';\nimport { cn } from '../../utils/cn';\n\nconst MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false });\n\nconst MARKDOWN_EDITOR_STYLE_ID = 'ods-markdown-editor-styles';\n\nconst mdEditorCSS = `\n:root { --md-editor-text-color: var(--color-text-primary) !important; }\n/* NAME COLLISION. --color-border-default is an ODS semantic alias AND a GitHub Primer\n   variable that @uiw/react-md-editor redeclares as #30363d on .wmde-markdown /\n   .wmde-markdown-var — and the editor root carries .wmde-markdown-var. The nearer\n   declaration wins, so every rule below that reads the alias inside the editor got the\n   vendor's blue-tinted grey, which is why the toolbar seam was drawn in a colour ODS\n   does not own.\n   inherit rather than a value: it makes the property take whatever the theme above\n   resolved, so the vendor's redeclaration is neutralised WITHOUT this file asserting a\n   colour of its own. That matters because ODS owns this name in more than one scope —\n   :root maps it to --ods-system-greys-soft-grey, and .theme-high-contrast to #ffffff.\n   Pinning the grey here would have quietly disabled the accessibility theme inside the\n   editor, which is the one place a hardcoded value cannot be checked against.\n   The .wmde-markdown half covers the vendor's own preview renderer, which a consumer\n   that does not pass renderPreview still gets. */\nbody .w-md-editor, body .w-md-editor .wmde-markdown { --color-border-default: inherit !important; }\n/* padding-bottom is a vendor default (1px) and it is INSIDE the border box, so both\n   panes stop 1px short of the frame's inner edge and the frame's own background shows\n   through as a hairline across the entire bottom -- a shade lighter than the preview,\n   which paints --color-bg. Zeroing it also makes the panes' 5px bottom radius exact\n   (the frame's inner radius is its 6px minus the 1px border) instead of an arc floating\n   1px above the one it is meant to follow. The resize handle is positioned against the\n   PADDING box, whose bottom edge the padding never moved, so it stays where it was.\n   The frame's background is covered edge to edge now, but it stays the source-pane\n   surface rather than the body one so that a sub-pixel gap could never read as a dark\n   band across the lighter pane. The toolbar and the preview set their own. */\nbody .w-md-editor { background-color: var(--color-bg-card) !important; border: 1px solid var(--ods-system-greys-soft-grey) !important; border-radius: 6px !important; padding-bottom: 0 !important; --md-editor-text-color: var(--color-text-primary) !important; --md-editor-box-shadow-color: transparent !important; box-shadow: none !important; }\nbody .w-md-editor-area, body .w-md-editor-input, body .w-md-editor-text, body .w-md-editor-text-pre, body .w-md-editor-text-input, body .w-md-editor-text-textarea { background-color: var(--color-bg-card) !important; color: var(--color-text-primary) !important; font-family: var(--font-family-body) !important; font-size: 18px !important; font-weight: 500 !important; line-height: 24px !important; padding: 16px !important; }\nbody .w-md-editor-text-textarea::placeholder { color: var(--color-text-secondary) !important; font-family: var(--font-family-body) !important; font-size: 18px !important; font-weight: 500 !important; }\nbody .w-md-editor-text .token, body .w-md-editor-text-pre .token, body .w-md-editor-text-textarea, body .w-md-editor-text-input { color: var(--color-text-primary) !important; }\nbody .w-md-editor-text-pre *, body .w-md-editor-text-textarea *, body .w-md-editor-text-input *, body .w-md-editor-text * { color: var(--color-text-primary) !important; }\nbody .w-md-editor-text-pre .token.title, body .w-md-editor-text-pre .token.bold, body .w-md-editor-text-pre .token.code, body .w-md-editor-text-pre .token.string, body .w-md-editor-text-pre .token.keyword { color: var(--color-text-primary) !important; }\nbody .w-md-editor-text-textarea, body .w-md-editor-text-input, body .w-md-editor-text-pre { color: var(--color-text-primary) !important; -webkit-text-fill-color: var(--color-text-primary) !important; text-shadow: none !important; filter: brightness(1) contrast(1) !important; }\nbody .w-md-editor { color: var(--color-text-primary) !important; }\nbody .w-md-editor *:not(.w-md-editor-toolbar *, .w-md-editor-preview *, .custom-preview-wrapper *) { color: var(--color-text-primary) !important; -webkit-text-fill-color: var(--color-text-primary) !important; }\n/* The toolbar's top corners follow the frame's own 6px radius. Rounding the toolbar\n   rather than clipping the whole editor with overflow:hidden on purpose: the toolbar's\n   dropdown commands render inside .w-md-editor and clipping would cut them off. */\n.w-md-editor-toolbar { background-color: var(--color-bg) !important; border-bottom: 1px solid var(--color-border-default) !important; border-radius: 6px 6px 0 0 !important; padding: 12px 16px !important; }\n.w-md-editor-toolbar ul li button { background: transparent !important; border: none !important; color: var(--color-text-primary) !important; padding: 8px 12px !important; border-radius: 6px !important; transition: all 0.2s ease !important; min-height: 36px !important; min-width: 36px !important; }\n.w-md-editor-toolbar ul li button:hover { background-color: var(--color-border-default) !important; color: var(--ods-accent) !important; }\n.w-md-editor-toolbar ul li button.active, .w-md-editor-toolbar ul li button[aria-pressed=\"true\"] { background-color: var(--ods-accent) !important; color: var(--color-text-on-accent) !important; }\n.w-md-editor-toolbar-divider { display: none !important; }\n/* --color-border-muted / --color-fg-muted keep a literal ODS value on purpose: those are\n   Primer-only names with no ODS counterpart, so there is nothing above to inherit. */\n.w-md-editor-preview { background-color: var(--color-bg) !important; color: var(--color-text-primary) !important; border-left: 1px solid var(--color-border-default) !important; --color-border-default: inherit !important; --color-border-muted: var(--ods-system-greys-soft-grey) !important; --color-fg-muted: var(--ods-system-greys-grey) !important; }\n/* The seam under the toolbar. Two vendor defaults broke it, both visible as one artefact:\n   .w-md-editor-text drew its own border-top in the same colour as the toolbar's\n   border-bottom, so the two sat adjacent as a 2px line (no border-top here any more), and\n   the panes carry a 5px radius whose curve pulls their background off the frame's straight\n   inner edge, reading as a notch in the corner. Squaring the TOP corners is the fix, NOT\n   overflow:hidden on .w-md-editor: a parent's clip cannot square off a child's own rounded\n   corner, and it would cut the toolbar's title dropdown (~240px tall) on any editor shorter\n   than that. The BOTTOM corners keep 5px on purpose -- that is exactly the frame's inner\n   radius (6px border-box minus the 1px border), so with the vendor's padding-bottom zeroed\n   above, the panes' arc now sits on the frame's own arc instead of beside it. In the split\n   view the source pane's bottom-right curve falls in the middle of the frame, where the\n   frame's background matches it, so it is invisible there. */\n.w-md-editor-area, .w-md-editor-input, .w-md-editor-text { border-radius: 0 0 5px 5px !important; }\n.w-md-editor-focus { border-color: var(--ods-accent) !important; }\nbody .w-md-editor .w-md-editor-bar { width: 24px !important; height: 12px !important; margin-left: auto !important; margin-right: 8px !important; cursor: ns-resize !important; }\nbody .w-md-editor .w-md-editor-bar svg { display: none !important; }\nbody .w-md-editor .w-md-editor-bar::after { content: '' !important; display: block !important; width: 24px !important; height: 4px !important; border-top: 2px solid var(--color-border-default) !important; border-bottom: 2px solid var(--color-border-default) !important; margin-top: 2px !important; border-radius: 1px !important; }\nbody .w-md-editor .w-md-editor-bar:hover::after { border-color: var(--color-text-secondary) !important; }\n`;\n\nfunction MarkdownEditorStyles() {\n  useEffect(() => {\n    if (document.getElementById(MARKDOWN_EDITOR_STYLE_ID)) return undefined;\n    const style = document.createElement('style');\n    style.id = MARKDOWN_EDITOR_STYLE_ID;\n    style.textContent = mdEditorCSS;\n    document.head.appendChild(style);\n    return () => {\n      style.remove();\n    };\n  }, []);\n  return null;\n}\n\nexport interface MarkdownEditorProps {\n  /** Markdown string content */\n  value: string;\n  /** Callback with updated markdown string */\n  onChange: (markdown: string) => void;\n  /** Placeholder text shown when editor is empty */\n  placeholder?: string;\n  /** Whether the editor is disabled */\n  disabled?: boolean;\n  /** Initial editor height in pixels (default: 600). User can resize via drag handle. */\n  height?: number;\n  /** Minimum editor height in pixels (default: 100) */\n  minHeight?: number;\n  /** Additional className for the wrapper */\n  className?: string;\n  /**\n   * File upload handler. Return the public URL of the uploaded file.\n   * If not provided, the upload button is hidden.\n   */\n  onUploadFile?: (file: File) => Promise<string>;\n  /** Called after a file is successfully uploaded and inserted */\n  onFileUploaded?: (url: string, filename: string) => void;\n  /** Custom preview renderer. Receives the markdown source string. */\n  renderPreview?: (source: string) => React.ReactNode;\n}\n\nexport function MarkdownEditor({\n  value,\n  onChange,\n  placeholder = '',\n  disabled = false,\n  height = 600,\n  minHeight = 100,\n  className,\n  onUploadFile,\n  onFileUploaded,\n  renderPreview,\n}: MarkdownEditorProps) {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [isUploading, setIsUploading] = useState(false);\n  const [uploadProgress, setUploadProgress] = useState('');\n  const [defaultExtraCommands, setDefaultExtraCommands] = useState<ICommand[]>([]);\n\n  useEffect(() => {\n    let cancelled = false;\n    void (async () => {\n      try {\n        const mod = await import('@uiw/react-md-editor');\n        if (cancelled) return;\n        if (mod.commands?.getExtraCommands) {\n          setDefaultExtraCommands(mod.commands.getExtraCommands());\n        }\n      } catch (error) {\n        // Chunk load failure — the editor still works, it just renders without\n        // the vendor's extra toolbar commands.\n        console.error('Failed to load markdown editor toolbar commands:', error);\n      }\n    })();\n    return () => {\n      cancelled = true;\n    };\n  }, []);\n\n  const handleChange = (val?: string) => {\n    onChange(val || '');\n  };\n\n  const insertTextAtCursor = useCallback(\n    (text: string) => {\n      const textarea = document.querySelector('.w-md-editor-text-textarea') as HTMLTextAreaElement;\n      if (textarea) {\n        const start = textarea.selectionStart;\n        const end = textarea.selectionEnd;\n        const newValue = value.slice(0, start) + text + value.slice(end);\n        onChange(newValue);\n      } else {\n        onChange(value + '\\n' + text);\n      }\n    },\n    [value, onChange],\n  );\n\n  const handleFileUpload = useCallback(\n    async (file: File) => {\n      if (!onUploadFile) return;\n\n      setIsUploading(true);\n      setUploadProgress(`Uploading ${file.name}...`);\n\n      try {\n        const url = await onUploadFile(file);\n        const isImage = file.type.startsWith('image/');\n        const markdown = isImage ? `![${file.name}](${url})` : `[${file.name}](${url})`;\n        insertTextAtCursor(markdown);\n        onFileUploaded?.(url, file.name);\n      } catch (error) {\n        console.error('File upload failed:', error);\n        setUploadProgress('Upload failed. Please try again.');\n      } finally {\n        setIsUploading(false);\n        setTimeout(() => setUploadProgress(''), 3000);\n      }\n    },\n    [onUploadFile, insertTextAtCursor, onFileUploaded],\n  );\n\n  const handleFileInputChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const input = e.target;\n      const file = input.files?.[0];\n      if (file) {\n        // Never rejects — reports failure through `uploadProgress`.\n        void handleFileUpload(file);\n        input.value = '';\n      }\n    },\n    [handleFileUpload],\n  );\n\n  const handlePaste = useCallback(\n    (e: React.ClipboardEvent) => {\n      if (!onUploadFile) return;\n      const items = e.clipboardData?.items;\n      if (!items) return;\n\n      for (const item of items) {\n        if (item.type.startsWith('image/')) {\n          e.preventDefault();\n          const file = item.getAsFile();\n          // Never rejects — reports failure through `uploadProgress`.\n          if (file) void handleFileUpload(file);\n          return;\n        }\n      }\n    },\n    [onUploadFile, handleFileUpload],\n  );\n\n  const CustomPreview = ({ source }: { source: string }) => {\n    if (renderPreview) {\n      return <div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>{renderPreview(source)}</div>;\n    }\n    return null;\n  };\n\n  const uploadCommand = onUploadFile\n    ? {\n        name: 'upload',\n        keyCommand: 'upload',\n        buttonProps: { 'aria-label': 'Upload file', title: 'Upload file' },\n        icon: isUploading ? <Loader2 className=\"h-3 w-3 animate-spin\" /> : <Upload className=\"h-3 w-3\" />,\n        execute: () => {\n          fileInputRef.current?.click();\n        },\n      }\n    : null;\n\n  const extraCommands = uploadCommand ? [...defaultExtraCommands, uploadCommand] : defaultExtraCommands;\n\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const isDraggingRef = useRef(false);\n  const mouseYRef = useRef(0);\n  const rafRef = useRef<number>(0);\n  const scrollParentRef = useRef<HTMLElement | Window>(window);\n\n  const EDGE_ZONE = 60;\n  const MAX_SCROLL_SPEED = 15;\n\n  const findScrollParent = useCallback((el: HTMLElement | null): HTMLElement | Window => {\n    let node = el?.parentElement;\n    while (node && node !== document.documentElement) {\n      const { overflowY } = window.getComputedStyle(node);\n      if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) {\n        return node;\n      }\n      node = node.parentElement;\n    }\n    return window;\n  }, []);\n\n  const scrollLoop = useCallback(function step() {\n    if (!isDraggingRef.current) return;\n    const parent = scrollParentRef.current;\n    const isWindow = parent === window;\n    const viewportBottom = isWindow ? window.innerHeight : (parent as HTMLElement).getBoundingClientRect().bottom;\n    const distFromBottom = viewportBottom - mouseYRef.current;\n    if (distFromBottom < EDGE_ZONE) {\n      const speed = Math.ceil(MAX_SCROLL_SPEED * (1 - distFromBottom / EDGE_ZONE));\n      if (isWindow) {\n        window.scrollBy(0, speed);\n      } else {\n        (parent as HTMLElement).scrollTop += speed;\n      }\n    }\n    rafRef.current = requestAnimationFrame(step);\n  }, []);\n\n  useEffect(() => {\n    const wrapper = wrapperRef.current;\n    if (!wrapper) return undefined;\n\n    const onMouseMove = (e: MouseEvent) => {\n      mouseYRef.current = e.clientY;\n    };\n\n    const onMouseDown = (e: MouseEvent) => {\n      if ((e.target as HTMLElement).closest('.w-md-editor-bar')) {\n        isDraggingRef.current = true;\n        mouseYRef.current = e.clientY;\n        scrollParentRef.current = findScrollParent(wrapper);\n        window.addEventListener('mousemove', onMouseMove);\n        rafRef.current = requestAnimationFrame(scrollLoop);\n      }\n    };\n\n    const onMouseUp = () => {\n      if (isDraggingRef.current) {\n        isDraggingRef.current = false;\n        window.removeEventListener('mousemove', onMouseMove);\n        cancelAnimationFrame(rafRef.current);\n      }\n    };\n\n    wrapper.addEventListener('mousedown', onMouseDown);\n    window.addEventListener('mouseup', onMouseUp);\n    return () => {\n      wrapper.removeEventListener('mousedown', onMouseDown);\n      window.removeEventListener('mouseup', onMouseUp);\n      window.removeEventListener('mousemove', onMouseMove);\n      cancelAnimationFrame(rafRef.current);\n    };\n  }, [scrollLoop, findScrollParent]);\n\n  return (\n    <div ref={wrapperRef} className={cn('advanced-blog-editor', className)} onPaste={handlePaste}>\n      {onUploadFile && (\n        <input\n          ref={fileInputRef}\n          type=\"file\"\n          className=\"hidden\"\n          onChange={handleFileInputChange}\n          accept=\"image/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.txt\"\n        />\n      )}\n\n      {!MDEditor && (\n        <div className=\"flex h-64 items-center justify-center\">\n          <Loader2 className=\"h-8 w-8 animate-spin\" />\n        </div>\n      )}\n      {MDEditor && (\n        <MDEditor\n          value={value}\n          onChange={handleChange}\n          preview=\"live\"\n          hideToolbar={false}\n          visibleDragbar={true}\n          height={height}\n          minHeight={minHeight}\n          extraCommands={extraCommands}\n          style={\n            {\n              '--md-editor-text-color': 'var(--color-text-primary)',\n              '--md-editor-bg-color': 'var(--color-bg-card)',\n              '--md-editor-border-color': 'var(--color-border)',\n            } as React.CSSProperties\n          }\n          components={\n            renderPreview\n              ? {\n                  preview: source => {\n                    return <CustomPreview source={source} />;\n                  },\n                }\n              : undefined\n          }\n          textareaProps={{\n            placeholder,\n            disabled,\n            style: {\n              fontFamily: 'var(--font-family-body)',\n              fontSize: '18px',\n              fontWeight: 500,\n              lineHeight: '24px',\n              color: 'var(--color-text-primary)',\n              backgroundColor: disabled ? 'var(--color-bg)' : 'var(--color-bg-card)',\n            },\n          }}\n          data-color-mode=\"dark\"\n        />\n      )}\n\n      {uploadProgress && <p className=\"mt-1 text-ods-text-secondary text-h6\">{uploadProgress}</p>}\n\n      <MarkdownEditorStyles />\n    </div>\n  );\n}\n","'use client';\n\nimport { Loader2 } from 'lucide-react';\nimport {\n  type ChangeEvent,\n  type DragEvent as ReactDragEvent,\n  type ReactNode,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { cn } from '../../utils/cn';\nimport { ImagePlusIcon } from '../icons-v2-generated/audio-and-visual/image-plus-icon';\nimport { FileIcon } from '../icons-v2-generated/documents/file-icon';\nimport { Download02Icon } from '../icons-v2-generated/interface/download-02-icon';\nimport { TrashIcon } from '../icons-v2-generated/interface/trash-icon';\nimport { FieldWrapper } from './field-wrapper';\n\n/** A managed file entry for async upload workflows (e.g. presigned URL uploads) */\nexport interface ManagedFileEntry {\n  /** Unique identifier for this file (e.g. temp attachment ID from backend) */\n  id: string;\n  /** Display file name */\n  fileName: string;\n  /** File size in bytes */\n  fileSize: number;\n  /** Upload status */\n  status: 'uploading' | 'uploaded' | 'error';\n  /** Error message when status is \"error\" */\n  error?: string;\n}\n\n/**\n * The metadata this component needs to validate and list a file. A browser\n * `File` satisfies it structurally, and so can a host's own file handle: a\n * native shell's OS picker hands back a path plus metadata and never a `File`,\n * because the bytes deliberately stay outside the WebView. See `pickFiles`.\n */\nexport interface FileUploadCandidate {\n  name: string;\n  size: number;\n  type: string;\n}\n\n/**\n * A file this component can hand back. Dropping, and the built-in\n * `<input type=\"file\">`, always produce browser `File`s no matter what\n * `pickFiles` returns — so `File` is part of the contract even when `T` is a\n * host's own handle type. Collapses to plain `File` in the default case.\n */\nexport type FileUploadValue<T extends FileUploadCandidate = File> = T | File;\n\nexport interface FileUploadProps<T extends FileUploadCandidate = File> {\n  /** Currently selected file(s) — use for simple, synchronous file handling */\n  value?: FileUploadValue<T> | FileUploadValue<T>[];\n  /** Callback when files change — used with `value` for simple mode */\n  onChange: (files: FileUploadValue<T> | FileUploadValue<T>[] | undefined) => void;\n  /**\n   * Managed file entries for async upload workflows.\n   * When provided, the file list renders from these entries instead of `value`.\n   * The dropzone still triggers `onChange` with new `File` objects so the consumer can upload them.\n   */\n  managedFiles?: ManagedFileEntry[];\n  /** Callback to remove a managed file by its id. Required when `managedFiles` is provided. */\n  onRemoveManagedFile?: (id: string) => void;\n  /** When provided, an uploaded managed file shows a download action that calls this with its id. */\n  onDownloadManagedFile?: (id: string) => void;\n  /** Accepted MIME types (e.g., \"image/*\", \".pdf,.doc\") */\n  accept?: string;\n  /** Maximum file size in bytes. Default: 10MB */\n  maxSize?: number;\n  /** Maximum number of files (only used when multiple=true). Default: 10 */\n  maxFiles?: number;\n  /** Allow multiple file selection. Default: false */\n  multiple?: boolean;\n  /** Primary label text. Default: \"Upload Files\" */\n  label?: string;\n  /** Description text below label. Default: \"(Click Here or Drag and Drop)\" */\n  description?: string;\n  /** Field label above the component */\n  fieldLabel?: string;\n  /** Whether the component is disabled */\n  disabled?: boolean;\n  /** Error message */\n  error?: string;\n  /** Additional className */\n  className?: string;\n  /** Custom icon element */\n  icon?: ReactNode;\n  /** Max height for the file list area (e.g., 200 or \"200px\"). When set, the file list scrolls independently. */\n  maxListHeight?: number | string;\n  /**\n   * When true, files dropped anywhere in the window are routed to this component,\n   * and the browser's default file-open behavior is suppressed. Use on screens/modals\n   * where this is the only drop target. Default: false.\n   */\n  acceptWindowDrops?: boolean;\n  /**\n   * Replaces the built-in `<input type=\"file\">` as the source of files. When set,\n   * clicking the dropzone calls this instead of opening the input, and whatever it\n   * resolves with runs through the same accept/size/count validation before\n   * reaching `onChange`. Resolve with an empty array when the user cancels; a\n   * rejection surfaces in this component's own error slot.\n   *\n   * For hosts whose picker a WebView cannot provide — a native shell's OS picker\n   * returns file handles rather than `File` objects, which is what widens `T`.\n   * Leave unset on the web, where the built-in input is the right answer.\n   */\n  pickFiles?: () => Promise<T[]>;\n}\n\nfunction formatFileSize(bytes: number): string {\n  if (bytes === 0) return '0 B';\n  const k = 1024;\n  const sizes = ['B', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;\n}\n\nfunction matchesAccept(file: FileUploadCandidate, accept: string): boolean {\n  if (!accept || accept === '*/*') return true;\n  const patterns = accept\n    .split(',')\n    .map(p => p.trim().toLowerCase())\n    .filter(Boolean);\n  if (patterns.length === 0) return true;\n  const fileType = (file.type || '').toLowerCase();\n  const fileName = file.name.toLowerCase();\n  return patterns.some(pattern => {\n    if (pattern.startsWith('.')) return fileName.endsWith(pattern);\n    if (pattern.endsWith('/*')) return fileType.startsWith(pattern.slice(0, -1));\n    return fileType === pattern;\n  });\n}\n\nfunction dragHasFiles(e: DragEvent): boolean {\n  const types = e.dataTransfer?.types;\n  if (!types) return false;\n  for (let i = 0; i < types.length; i++) {\n    if (types[i] === 'Files') return true;\n  }\n  return false;\n}\n\nexport function FileUpload<T extends FileUploadCandidate = File>({\n  value,\n  onChange,\n  managedFiles,\n  onRemoveManagedFile,\n  onDownloadManagedFile,\n  accept = '*/*',\n  maxSize = 10 * 1024 * 1024,\n  maxFiles = 10,\n  multiple = false,\n  label = 'Upload Files',\n  description = '(Click Here or Drag and Drop)',\n  fieldLabel,\n  disabled = false,\n  error,\n  className,\n  icon,\n  maxListHeight,\n  acceptWindowDrops = false,\n  pickFiles,\n}: FileUploadProps<T>) {\n  const [dragActive, setDragActive] = useState(false);\n  const [validationError, setValidationError] = useState<string | null>(null);\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  const isManaged = managedFiles !== undefined;\n\n  const files = useMemo(() => {\n    if (!value) return [];\n    return Array.isArray(value) ? value : [value];\n  }, [value]);\n\n  const currentCount = isManaged ? managedFiles.length : files.length;\n\n  const validateFiles = (incoming: FileUploadValue<T>[]): { accepted: FileUploadValue<T>[]; error: string | null } => {\n    if (incoming.length === 0) return { accepted: [], error: null };\n\n    const candidates = multiple ? incoming : incoming.slice(0, 1);\n\n    for (const file of candidates) {\n      if (!matchesAccept(file, accept)) {\n        return { accepted: [], error: `File \"${file.name}\" is not an accepted type` };\n      }\n      if (file.size > maxSize) {\n        return {\n          accepted: [],\n          error: `File \"${file.name}\" exceeds maximum size of ${formatFileSize(maxSize)}`,\n        };\n      }\n    }\n\n    if (multiple && currentCount + candidates.length > maxFiles) {\n      return {\n        accepted: [],\n        error: `You can attach at most ${maxFiles} ${maxFiles === 1 ? 'file' : 'files'}`,\n      };\n    }\n\n    return { accepted: candidates, error: null };\n  };\n\n  const handleFiles = (incoming: FileList | FileUploadValue<T>[]) => {\n    setValidationError(null);\n    const fileArray: FileUploadValue<T>[] = Array.isArray(incoming) ? incoming : Array.from(incoming);\n    if (fileArray.length === 0) return;\n\n    const { accepted, error: validationErr } = validateFiles(fileArray);\n    if (validationErr) {\n      setValidationError(validationErr);\n      return;\n    }\n    if (accepted.length === 0) return;\n\n    if (isManaged) {\n      onChange(multiple ? accepted : accepted[0]);\n    } else if (multiple) {\n      onChange([...files, ...accepted]);\n    } else {\n      onChange(accepted[0]);\n    }\n\n    if (fileInputRef.current) {\n      fileInputRef.current.value = '';\n    }\n  };\n\n  // Refreshed after every commit rather than in the render body: the reader is\n  // a drop/paste listener, which cannot fire before a commit.\n  const handleFilesRef = useRef(handleFiles);\n  useEffect(() => {\n    handleFilesRef.current = handleFiles;\n  });\n\n  const handleDrag = (e: ReactDragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    if (disabled) return;\n    if (e.type === 'dragenter' || e.type === 'dragover') {\n      setDragActive(true);\n    } else if (e.type === 'dragleave') {\n      setDragActive(false);\n    }\n  };\n\n  const handleDrop = (e: ReactDragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setDragActive(false);\n    if (disabled) return;\n    if (e.dataTransfer.files?.length) {\n      handleFiles(e.dataTransfer.files);\n    }\n  };\n\n  useEffect(() => {\n    if (!acceptWindowDrops || disabled) return undefined;\n\n    let dragCounter = 0;\n\n    const onWindowDragEnter = (e: DragEvent) => {\n      if (!dragHasFiles(e)) return;\n      e.preventDefault();\n      dragCounter++;\n      if (dragCounter === 1) setDragActive(true);\n    };\n    const onWindowDragOver = (e: DragEvent) => {\n      if (!dragHasFiles(e)) return;\n      e.preventDefault();\n    };\n    const onWindowDragLeave = (e: DragEvent) => {\n      if (!dragHasFiles(e)) return;\n      e.preventDefault();\n      dragCounter = Math.max(0, dragCounter - 1);\n      if (dragCounter === 0) setDragActive(false);\n    };\n    const onWindowDrop = (e: DragEvent) => {\n      if (!dragHasFiles(e)) return;\n      e.preventDefault();\n      dragCounter = 0;\n      setDragActive(false);\n      if (e.dataTransfer?.files?.length) {\n        handleFilesRef.current(e.dataTransfer.files);\n      }\n    };\n    const onWindowDragEnd = () => {\n      dragCounter = 0;\n      setDragActive(false);\n    };\n\n    window.addEventListener('dragenter', onWindowDragEnter);\n    window.addEventListener('dragover', onWindowDragOver);\n    window.addEventListener('dragleave', onWindowDragLeave);\n    window.addEventListener('drop', onWindowDrop);\n    window.addEventListener('dragend', onWindowDragEnd);\n\n    return () => {\n      window.removeEventListener('dragenter', onWindowDragEnter);\n      window.removeEventListener('dragover', onWindowDragOver);\n      window.removeEventListener('dragleave', onWindowDragLeave);\n      window.removeEventListener('drop', onWindowDrop);\n      window.removeEventListener('dragend', onWindowDragEnd);\n    };\n  }, [acceptWindowDrops, disabled]);\n\n  const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {\n    if (e.target.files?.length) {\n      handleFiles(e.target.files);\n    }\n  };\n\n  const handleRemoveFile = (index: number) => {\n    const updated = files.filter((_, i) => i !== index);\n    if (updated.length === 0) {\n      onChange(undefined);\n    } else {\n      onChange(multiple ? updated : updated[0]);\n    }\n    if (fileInputRef.current) {\n      fileInputRef.current.value = '';\n    }\n  };\n\n  const openDialog = async () => {\n    if (disabled) return;\n    if (!pickFiles) {\n      fileInputRef.current?.click();\n      return;\n    }\n    // A host picker is a real async operation that can fail (a second picker\n    // already on screen, an unreadable selection), and this runs from a click\n    // handler where a rejection would go unhandled.\n    try {\n      // Skip the empty (cancelled) case rather than routing it through\n      // handleFiles, whose first act is to clear a validation message the user\n      // may still be reading.\n      const picked = await pickFiles();\n      if (picked.length > 0) handleFiles(picked);\n    } catch (err) {\n      setValidationError(err instanceof Error ? err.message : 'Could not open the file picker');\n    }\n  };\n\n  const displayError = error || validationError || undefined;\n  const hasFiles = isManaged ? managedFiles.length > 0 : files.length > 0;\n  // Single-file mode hides the dropzone once a file is picked (no more to add);\n  // multiple keeps it visible to keep attaching.\n  const showDropzone = multiple || !hasFiles;\n\n  return (\n    <FieldWrapper label={fieldLabel} error={displayError} className={className}>\n      {/* Rendered even when `pickFiles` supersedes it: whether a host has a\n          native picker is only knowable on the client, and dropping this node\n          from the tree would make the prerendered HTML and the hydrated render\n          disagree. It is inert — nothing clicks it once `pickFiles` is set. */}\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept={accept}\n        multiple={multiple}\n        onChange={handleFileSelect}\n        className=\"hidden\"\n        disabled={disabled}\n      />\n\n      {/* File list (above) + upload dropzone — single bordered list, file-type icon + name/size + actions per row */}\n      <div className=\"flex flex-col gap-[var(--spacing-system-xsf)]\">\n        {hasFiles && (\n          <div\n            className={cn(\n              'flex flex-col rounded-[6px] border border-ods-border bg-ods-card transition-colors duration-200',\n              !maxListHeight && 'overflow-hidden',\n              dragActive && 'border-ods-accent',\n            )}\n            style={\n              maxListHeight\n                ? {\n                    maxHeight: typeof maxListHeight === 'number' ? `${maxListHeight}px` : maxListHeight,\n                    overflowY: 'auto',\n                  }\n                : undefined\n            }\n          >\n            {isManaged\n              ? managedFiles.map(entry => (\n                  <div\n                    key={entry.id}\n                    className={cn(\n                      'flex items-center gap-[var(--spacing-system-mf)] border-b border-ods-border px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)] last:border-b-0',\n                      entry.status === 'error' && 'bg-ods-error/5',\n                    )}\n                  >\n                    <div className=\"flex size-10 shrink-0 items-center justify-center rounded-[6px] border border-ods-border bg-ods-card\">\n                      <FileIcon className=\"size-6 text-ods-text-secondary\" />\n                    </div>\n                    <div className=\"min-w-0 flex-1\">\n                      <p className=\"truncate text-ods-text-primary text-h4\" title={entry.fileName}>\n                        {entry.fileName}\n                      </p>\n                      <div className=\"flex items-center gap-[var(--spacing-system-xsf)]\">\n                        <span className=\"text-ods-text-secondary text-h6\">{formatFileSize(entry.fileSize)}</span>\n                        {entry.status === 'uploading' && (\n                          <span className=\"flex items-center gap-[var(--spacing-system-xxs)] text-ods-text-secondary text-h6\">\n                            <Loader2 className=\"size-3 animate-spin\" />\n                            Uploading...\n                          </span>\n                        )}\n                        {entry.status === 'error' && (\n                          <span className=\"text-ods-error text-h6\">{entry.error || 'Upload failed'}</span>\n                        )}\n                      </div>\n                    </div>\n                    {onDownloadManagedFile && entry.status === 'uploaded' && (\n                      <button\n                        type=\"button\"\n                        onClick={() => onDownloadManagedFile(entry.id)}\n                        className=\"shrink-0 text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n                        aria-label={`Download ${entry.fileName}`}\n                      >\n                        <Download02Icon className=\"size-6\" />\n                      </button>\n                    )}\n                    {!disabled && (\n                      <button\n                        type=\"button\"\n                        onClick={() => onRemoveManagedFile?.(entry.id)}\n                        className=\"shrink-0 text-ods-error transition-opacity hover:opacity-80\"\n                        aria-label={`Remove ${entry.fileName}`}\n                      >\n                        <TrashIcon className=\"size-6\" />\n                      </button>\n                    )}\n                  </div>\n                ))\n              : files.map((file, index) => (\n                  <div\n                    key={`${file.name}-${index}`}\n                    className=\"flex items-center gap-[var(--spacing-system-mf)] border-b border-ods-border px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)] last:border-b-0\"\n                  >\n                    <div className=\"flex size-10 shrink-0 items-center justify-center rounded-[6px] border border-ods-border bg-ods-card\">\n                      <FileIcon className=\"size-6 text-ods-text-secondary\" />\n                    </div>\n                    <div className=\"min-w-0 flex-1\">\n                      <p className=\"truncate text-ods-text-primary text-h4\" title={file.name}>\n                        {file.name}\n                      </p>\n                      <p className=\"text-ods-text-secondary text-h6\">{formatFileSize(file.size)}</p>\n                    </div>\n                    {!disabled && (\n                      <button\n                        type=\"button\"\n                        onClick={() => handleRemoveFile(index)}\n                        className=\"shrink-0 text-ods-error transition-opacity hover:opacity-80\"\n                        aria-label={`Remove ${file.name}`}\n                      >\n                        <TrashIcon className=\"size-6\" />\n                      </button>\n                    )}\n                  </div>\n                ))}\n          </div>\n        )}\n\n        {showDropzone && (\n          <button\n            type=\"button\"\n            onDragEnter={handleDrag}\n            onDragLeave={handleDrag}\n            onDragOver={handleDrag}\n            onDrop={handleDrop}\n            onClick={openDialog}\n            disabled={disabled}\n            className={cn(\n              'flex w-full cursor-pointer items-center gap-[var(--spacing-system-xsf)] rounded-[6px] border border-dashed p-[var(--spacing-system-sf)] text-left',\n              'transition-colors duration-200',\n              'border-ods-border bg-ods-card',\n              dragActive && 'border-ods-accent bg-ods-accent/5',\n              !dragActive && 'hover:border-ods-accent/30',\n              disabled && 'cursor-not-allowed opacity-50 hover:border-ods-border',\n            )}\n          >\n            <div className=\"flex shrink-0 items-center rounded-full border border-ods-border bg-ods-card p-[var(--spacing-system-sf)]\">\n              {icon || <ImagePlusIcon className=\"size-6 text-ods-text-primary\" />}\n            </div>\n            <div className=\"flex min-w-0 flex-1 flex-col\">\n              <span className=\"text-ods-text-primary text-h4\">{label}</span>\n              <span className=\"text-ods-text-secondary text-h6\">{description}</span>\n            </div>\n          </button>\n        )}\n      </div>\n    </FieldWrapper>\n  );\n}\n","'use client';\n\nimport { Loader2 } from 'lucide-react';\nimport { type ChangeEvent, type DragEvent, type KeyboardEvent, type ReactNode, useRef, useState } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { useAuthedImageSrc } from '../../hooks/use-authed-image-src';\nimport { cn } from '../../utils/cn';\nimport { ImagePlusIcon } from '../icons-v2-generated/audio-and-visual/image-plus-icon';\nimport { TrashIcon } from '../icons-v2-generated/interface/trash-icon';\nimport { Refresh02VrIcon } from '../icons-v2-generated/media-playback/refresh-02-vr-icon';\nimport { FieldWrapper } from './field-wrapper';\n\nexport interface ImageUploaderProps {\n  /** Current image URL (remote, blob, or data URL). When set, the preview state is shown. */\n  value?: string;\n  /** Fired when the user picks or drops a new image. The consumer is responsible for uploading. */\n  onChange: (file: File) => void;\n  /** Fired when the user clicks the trash icon. Omit to hide the remove button. */\n  onRemove?: () => void;\n  /** Accepted MIME types or extensions. Default: \"image/*\" */\n  accept?: string;\n  /** Maximum file size in bytes. Default: 25MB */\n  maxSize?: number;\n  /** Empty-state primary label. */\n  label?: string;\n  /** Empty-state secondary description. */\n  description?: string;\n  /** Empty-state hint line. Pass `null` to hide it. Default: derived from `maxSize`. */\n  hint?: ReactNode | null;\n  /** Field label rendered above the component (via FieldWrapper). */\n  fieldLabel?: string;\n  /** Disables interaction. */\n  disabled?: boolean;\n  /** Show loading state — disables interaction and renders a spinner overlay. */\n  loading?: boolean;\n  /** External error message. Internal validation errors take precedence when present. */\n  error?: string;\n  /** CSS aspect-ratio for the dropzone, e.g. \"16 / 9\", \"1 / 1\". When set, overrides the default fixed height. */\n  aspectRatio?: string;\n  /** object-fit for the preview image. Default: \"cover\" */\n  objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';\n  /** Alt text for the preview image. */\n  alt?: string;\n  /** Extra className on the FieldWrapper root. */\n  className?: string;\n  /**\n   * Extra className on the dropzone element itself. Merged after the defaults, so it can\n   * override the built-in sizing (e.g. pass `h-[148px]` to replace the default `h-44`).\n   */\n  dropzoneClassName?: string;\n}\n\nconst DEFAULT_MAX_SIZE = 25 * 1024 * 1024;\n\nfunction formatSize(bytes: number): string {\n  const mb = bytes / (1024 * 1024);\n  if (mb >= 1) return Number.isInteger(mb) ? `${mb}MB` : `${mb.toFixed(1)}MB`;\n  const kb = bytes / 1024;\n  return Number.isInteger(kb) ? `${kb}KB` : `${kb.toFixed(0)}KB`;\n}\n\nfunction matchesAccept(file: File, accept: string): boolean {\n  if (!accept || accept === '*/*') return true;\n  const patterns = accept\n    .split(',')\n    .map(p => p.trim().toLowerCase())\n    .filter(Boolean);\n  if (patterns.length === 0) return true;\n  const fileType = (file.type || '').toLowerCase();\n  const fileName = file.name.toLowerCase();\n  return patterns.some(pattern => {\n    if (pattern.startsWith('.')) return fileName.endsWith(pattern);\n    if (pattern.endsWith('/*')) return fileType.startsWith(pattern.slice(0, -1));\n    return fileType === pattern;\n  });\n}\n\nexport function ImageUploader({\n  value,\n  onChange,\n  onRemove,\n  accept = 'image/*',\n  maxSize = DEFAULT_MAX_SIZE,\n  label = 'Cover Image',\n  description = '(Click Here or Drag and Drop)',\n  hint,\n  fieldLabel,\n  disabled = false,\n  loading = false,\n  error,\n  aspectRatio,\n  objectFit = 'cover',\n  alt = 'Uploaded image',\n  className,\n  dropzoneClassName,\n}: ImageUploaderProps) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [dragActive, setDragActive] = useState(false);\n  const [pressed, setPressed] = useState(false);\n  const [validationError, setValidationError] = useState<string | null>(null);\n\n  const interactive = !disabled && !loading;\n  // Bearer-mode shells: swap a gateway URL for an authed blob URL (native\n  // <img> loads can't carry Authorization); pass-through everywhere else,\n  // including local blob/data preview URLs. `hasImage` stays keyed on the\n  // caller's `value` so the preview chrome doesn't flash to the empty\n  // state while the blob fetch is in flight.\n  const resolvedValue = useAuthedImageSrc(value);\n  const hasImage = Boolean(value);\n  const displayError = error || validationError || undefined;\n  const resolvedHint = hint === undefined ? `Max. size: ${formatSize(maxSize)}` : hint;\n\n  const validateAndEmit = (file?: File) => {\n    if (!file) return;\n    setValidationError(null);\n    if (!matchesAccept(file, accept)) {\n      setValidationError('Unsupported file type');\n      return;\n    }\n    if (file.size > maxSize) {\n      setValidationError(`File exceeds maximum size of ${formatSize(maxSize)}`);\n      return;\n    }\n    onChange(file);\n  };\n\n  const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {\n    validateAndEmit(e.target.files?.[0]);\n    if (inputRef.current) inputRef.current.value = '';\n  };\n\n  const handleDrag = (e: DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    if (!interactive) return;\n    if (e.type === 'dragenter' || e.type === 'dragover') setDragActive(true);\n    else if (e.type === 'dragleave') setDragActive(false);\n  };\n\n  const handleDrop = (e: DragEvent) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setDragActive(false);\n    if (!interactive) return;\n    validateAndEmit(e.dataTransfer.files?.[0]);\n  };\n\n  const openDialog = () => {\n    if (!interactive) return;\n    inputRef.current?.click();\n  };\n\n  const handleRootKeyDown = (e: KeyboardEvent) => {\n    if (hasImage || !interactive) return;\n    if (e.key === 'Enter' || e.key === ' ') {\n      e.preventDefault();\n      openDialog();\n    }\n  };\n\n  const isActionState = dragActive || (pressed && !hasImage);\n\n  return (\n    <FieldWrapper label={fieldLabel} error={displayError} className={className}>\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept={accept}\n        onChange={handleFileSelect}\n        className=\"hidden\"\n        disabled={!interactive}\n      />\n\n      <div\n        onDragEnter={handleDrag}\n        onDragLeave={handleDrag}\n        onDragOver={handleDrag}\n        onDrop={handleDrop}\n        onClick={!hasImage ? openDialog : undefined}\n        onKeyDown={handleRootKeyDown}\n        onMouseDown={!hasImage ? () => setPressed(true) : undefined}\n        onMouseUp={!hasImage ? () => setPressed(false) : undefined}\n        onMouseLeave={!hasImage ? () => setPressed(false) : undefined}\n        role={!hasImage ? 'button' : undefined}\n        tabIndex={!hasImage && interactive ? 0 : undefined}\n        aria-disabled={!interactive || undefined}\n        style={aspectRatio ? { aspectRatio } : undefined}\n        className={cn(\n          'relative flex w-full flex-col items-center justify-center gap-[var(--spacing-system-l)] rounded-md p-[var(--spacing-system-l)]',\n          !aspectRatio && 'h-44',\n          'border border-dashed transition-colors duration-150',\n          hasImage\n            ? 'border-ods-border bg-ods-bg'\n            : isActionState\n              ? 'border-ods-border-active bg-ods-bg-active'\n              : 'border-ods-border bg-ods-card',\n          !hasImage &&\n            interactive &&\n            !isActionState &&\n            'cursor-pointer hover:border-ods-border-hover hover:bg-ods-bg-hover',\n          !interactive && 'opacity-60',\n          !hasImage && !interactive && 'cursor-not-allowed',\n          dropzoneClassName,\n        )}\n      >\n        {hasImage ? (\n          <>\n            <div className=\"relative min-h-0 w-full flex-1 overflow-hidden rounded-md\">\n              {resolvedValue && (\n                <Image\n                  src={resolvedValue}\n                  alt={alt}\n                  fill\n                  sizes=\"100vw\"\n                  // `useAuthedImageSrc` yields a `blob:` URL under bearer-token\n                  // hosts, which no image optimizer can fetch.\n                  unoptimized\n                  className={cn(\n                    'pointer-events-none',\n                    objectFit === 'cover' && 'object-cover',\n                    objectFit === 'contain' && 'object-contain',\n                    objectFit === 'fill' && 'object-fill',\n                    objectFit === 'none' && 'object-none',\n                    objectFit === 'scale-down' && 'object-scale-down',\n                  )}\n                />\n              )}\n            </div>\n\n            <div className=\"absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-4\">\n              <ActionIconButton onClick={openDialog} disabled={!interactive} ariaLabel=\"Replace image\">\n                <Refresh02VrIcon className=\"size-6\" />\n              </ActionIconButton>\n              {onRemove && (\n                <ActionIconButton onClick={onRemove} disabled={!interactive} ariaLabel=\"Remove image\">\n                  <TrashIcon className=\"size-6\" />\n                </ActionIconButton>\n              )}\n            </div>\n\n            {loading && (\n              <div className=\"absolute inset-0 flex items-center justify-center rounded-md bg-ods-overlay\">\n                <Loader2 className=\"size-8 animate-spin text-ods-text-on-dark\" />\n              </div>\n            )}\n          </>\n        ) : loading ? (\n          <Loader2 className=\"size-8 animate-spin text-ods-accent\" />\n        ) : (\n          <>\n            <div className=\"flex shrink-0 items-center justify-center rounded-full border border-ods-border bg-ods-card p-[var(--spacing-system-s)]\">\n              <ImagePlusIcon className=\"size-6 text-ods-text-primary\" />\n            </div>\n            <div className=\"flex w-full flex-col items-center text-center\">\n              <p className=\"text-ods-text-primary text-h4\">{label}</p>\n              <p className=\"text-ods-text-secondary text-h6\">{description}</p>\n              {resolvedHint && <p className=\"text-ods-text-secondary text-h6\">{resolvedHint}</p>}\n            </div>\n          </>\n        )}\n      </div>\n    </FieldWrapper>\n  );\n}\n\ninterface ActionIconButtonProps {\n  onClick: () => void;\n  disabled?: boolean;\n  ariaLabel: string;\n  children: ReactNode;\n}\n\nfunction ActionIconButton({ onClick, disabled, ariaLabel, children }: ActionIconButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      onClick={e => {\n        e.stopPropagation();\n        onClick();\n      }}\n      disabled={disabled}\n      aria-label={ariaLabel}\n      className={cn(\n        'flex shrink-0 items-center justify-center rounded-md p-3',\n        'bg-[var(--ods-system-greys-white)] text-ods-text-on-accent',\n        'transition-colors duration-150',\n        'hover:bg-[var(--ods-system-greys-white-hover)]',\n        'active:bg-[var(--ods-system-greys-white-action)]',\n        'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ods-accent',\n        'disabled:cursor-not-allowed disabled:opacity-50',\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n","'use client';\n\nimport { type ReactNode, useId } from 'react';\nimport { useDragAndDropEnabled } from '../../hooks/ui/use-drag-and-drop-enabled';\nimport { cn } from '../../utils/cn';\nimport type { SortableDragHandleProps } from '../features/sortable-list/use-sortable-item';\nimport { DraggerIcon } from '../icons-v2-generated/interface/dragger-icon';\nimport { TrashIcon } from '../icons-v2-generated/interface/trash-icon';\nimport { InfoCircleIcon } from '../icons-v2-generated/signs-and-symbols/info-circle-icon';\nimport { Button } from './button';\nimport { ColorPresetSelect, ColorPickerInput } from './color-preset-select';\nimport { Input } from './input';\nimport { Label } from './label';\nimport type { TagProps } from './tag';\nimport { TicketStatusTag } from './ticket-status-tag';\nimport { TouchFriendlyTooltip } from './touch-friendly-tooltip';\n\ntype SystemTagVariant = Extract<TagProps['variant'], 'outline' | 'primary'>;\n\nexport interface TicketStatusConfigRowProps {\n  variant: 'system' | 'custom';\n  /**\n   * Canonical ticket-status key for system rows (e.g. 'ACTIVE', 'RESOLVED'),\n   * or the row's unique id for custom rows. Forwarded to TicketStatusTag.\n   */\n  statusKey: string;\n  name: string;\n  onNameChange?: (value: string) => void;\n  color?: string;\n  presetKey?: string;\n  onColorChange?: (next: { color: string; preset?: string }) => void;\n  systemTooltip?: string;\n  systemTagVariant?: SystemTagVariant;\n  onDelete?: () => void;\n  deleteDisabled?: boolean;\n  deleteDisabledReason?: string;\n  /** From `useSortableItem` (via `TicketStatusConfigList`'s render args). */\n  dragHandleProps?: SortableDragHandleProps;\n  isDragging?: boolean;\n  /**\n   * The touch replacement for the drag handle (`SortableRowRenderArgs.moveButtons`).\n   * Rendered beside the delete button; the drag rail is hidden on touch anyway.\n   */\n  moveButtons?: ReactNode;\n}\n\nexport function TicketStatusConfigRow({\n  variant,\n  statusKey,\n  name,\n  onNameChange,\n  color,\n  presetKey,\n  onColorChange,\n  systemTooltip,\n  systemTagVariant,\n  onDelete,\n  deleteDisabled,\n  deleteDisabledReason,\n  dragHandleProps,\n  isDragging,\n  moveButtons,\n}: TicketStatusConfigRowProps) {\n  // Standalone system rows render outside any SortableList, so the row answers\n  // \"is there a drag rail at all?\" itself — same media gate the list uses.\n  const dragAndDropEnabled = useDragAndDropEnabled();\n  const nameInputId = useId();\n  const isSystem = variant === 'system';\n  const isCustomColor = !isSystem && presetKey === undefined;\n  const previewColor = isSystem ? undefined : color;\n  const showColorPicker = !isSystem && onColorChange && color !== undefined;\n  // Custom rows render as filled pills (no border); system rows take the\n  // page-configured variant (outline/primary). text-h5 already uppercases.\n  const previewVariant: SystemTagVariant | undefined = systemTagVariant ?? (isSystem ? undefined : 'primary');\n\n  // The fields carry a visible label, so everything that sits beside the\n  // 44/48px field row (rail, chip, controls) drops below the label line by\n  // exactly its height: the h4 line + the label's mb-1.\n  const fieldRowOffset = 'mt-[calc(var(--font-line-space-h4-body)+0.25rem)]';\n\n  const chip = (\n    <TicketStatusTag\n      status={statusKey}\n      label={name || ' '}\n      color={previewColor}\n      variant={previewVariant}\n      showIcon={isSystem}\n      className=\"max-w-full\"\n    />\n  );\n\n  return (\n    <div\n      className={cn(\n        // Transparent background — only the border outlines the row.\n        'flex w-full items-start gap-3 rounded-md border border-ods-border md:gap-[var(--spacing-system-m)]',\n        'p-[var(--spacing-system-m)]',\n        // The dragged row travels over its siblings, so it needs an opaque back.\n        isDragging && 'bg-ods-bg opacity-70 shadow-lg',\n      )}\n    >\n      {dragAndDropEnabled && (\n        <div\n          className={cn(\n            'flex h-11 w-8 shrink-0 items-center justify-center text-ods-text-secondary md:h-12',\n            fieldRowOffset,\n          )}\n        >\n          {isSystem ? (\n            <DraggerIcon size={24} aria-hidden className=\"opacity-40\" />\n          ) : (\n            <button\n              type=\"button\"\n              aria-label=\"Drag to reorder\"\n              className=\"flex size-6 cursor-grab items-center justify-center rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ods-focus active:cursor-grabbing\"\n              {...dragHandleProps}\n            >\n              <DraggerIcon size={24} />\n            </button>\n          )}\n        </div>\n      )}\n\n      {/* Every row spans the same equal column slots — two on tablet\n          (name + color for custom, name + chip for system), four on desktop\n          (name, color, a reserved slot, chip zone) with invisible spacers\n          standing in for the cells a row doesn't use — so the name field is one\n          width on every row and the columns align across the whole list.\n          The trailing controls are a WRAP ITEM (ordered per breakpoint), not a\n          full-height sibling column, so the lower rows span the full card width\n          and the chip pins to the card's right edge on tablet/mobile. */}\n      <div\n        className={cn(\n          'min-w-0 flex-1 items-start gap-x-3 gap-y-[var(--spacing-system-m)] md:gap-x-[var(--spacing-system-m)]',\n          isSystem\n            ? 'flex flex-wrap'\n            : // Mobile custom rows are a strict two-column grid, per the mock:\n              // fields (name / color / hex, all one width) on the left, the\n              // controls and the chip stacked in the right column. From md up\n              // the same children flow as a wrapping flex row.\n              'grid grid-cols-[minmax(0,1fr)_auto] md:flex md:flex-wrap',\n        )}\n      >\n        {/* System rows keep the chip beside the name on every width, so the name\n            shares its row. */}\n        <div className=\"order-1 flex min-w-0 grow basis-0 flex-col gap-[var(--spacing-system-xxs)]\">\n          <Label variant=\"large\" htmlFor={nameInputId}>\n            Status Name\n          </Label>\n          <Input\n            id={nameInputId}\n            value={name}\n            onChange={e => onNameChange?.(e.target.value)}\n            disabled={isSystem}\n            readOnly={isSystem}\n            maxLength={50}\n          />\n        </div>\n\n        {showColorPicker ? (\n          <div className=\"order-3 flex min-w-0 grow basis-full flex-col gap-[var(--spacing-system-xxs)] md:order-2 md:basis-0\">\n            <Label variant=\"large\">Color</Label>\n            <ColorPresetSelect value={color} presetKey={presetKey} onChange={onColorChange} />\n            {/* Custom hex: a second field row inside the Color column, directly\n                under the select (per design). */}\n            {isCustomColor && (\n              <div className=\"mt-[var(--spacing-system-xxs)] flex min-w-0\">\n                <ColorPickerInput value={color} onChange={next => onColorChange({ color: next, preset: undefined })} />\n              </div>\n            )}\n          </div>\n        ) : (\n          <div aria-hidden className=\"hidden min-w-0 grow basis-0 lg:order-2 lg:block\" />\n        )}\n\n        {/* Reserved desktop column slot (the mock's \"No activity Indicator\" zone). */}\n        <div aria-hidden className=\"hidden min-w-0 grow basis-0 lg:order-3 lg:block\" />\n\n        {isSystem ? (\n          <div\n            className={cn(\n              'order-2 flex h-11 min-w-0 grow basis-0 items-center justify-end md:h-12 lg:order-4',\n              fieldRowOffset,\n            )}\n          >\n            {chip}\n          </div>\n        ) : (\n          <>\n            {/* Desktop (lg+): the chip rides the first row, in its own column\n                before the controls. */}\n            <div\n              className={cn(\n                'hidden h-12 min-w-0 grow basis-0 items-center justify-end lg:order-4 lg:flex',\n                fieldRowOffset,\n              )}\n            >\n              {chip}\n            </div>\n            {/* Tablet: the chip takes a full row of its own, pinned to the\n                card's right edge (the controls above no longer narrow it). */}\n            <div className=\"hidden min-w-0 basis-full items-center justify-end md:order-4 md:flex lg:hidden\">\n              {chip}\n            </div>\n            {/* Mobile: the chip fills the second grid column beside the color\n                field, left-aligned with the controls above it. */}\n            <div className={cn('order-4 flex h-11 min-w-0 items-center md:hidden', fieldRowOffset)}>{chip}</div>\n          </>\n        )}\n\n        <div\n          className={cn(\n            'flex h-11 shrink-0 items-center justify-end gap-[var(--spacing-system-s)] md:h-12 lg:order-5',\n            isSystem ? 'order-3' : 'order-2 md:order-3',\n            fieldRowOffset,\n          )}\n        >\n          {!isSystem && moveButtons}\n          {isSystem ? (\n            <div className=\"flex w-11 justify-center md:w-12\">\n              <TouchFriendlyTooltip content={systemTooltip}>\n                <button\n                  type=\"button\"\n                  aria-label={systemTooltip ?? 'System status'}\n                  className=\"flex size-6 items-center justify-center text-ods-text-secondary outline-none focus-visible:ring-2 focus-visible:ring-ods-focus\"\n                >\n                  <InfoCircleIcon size={24} />\n                </button>\n              </TouchFriendlyTooltip>\n            </div>\n          ) : (\n            <TouchFriendlyTooltip content={deleteDisabled ? deleteDisabledReason : undefined}>\n              <span className=\"inline-flex\">\n                <Button\n                  type=\"button\"\n                  variant=\"outline\"\n                  size=\"icon\"\n                  aria-label=\"Delete status\"\n                  disabled={deleteDisabled}\n                  onClick={deleteDisabled ? undefined : onDelete}\n                  className={cn('size-11 md:size-12', deleteDisabled && 'pointer-events-none')}\n                >\n                  <TrashIcon className=\"text-ods-error\" />\n                </Button>\n              </span>\n            </TouchFriendlyTooltip>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { HexColorPicker } from 'react-colorful';\nimport { cn } from '../../utils/cn';\nimport { HEX_PATTERN, hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../../utils/ods-color-utils';\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';\nimport { Popover, PopoverContent, PopoverTrigger } from '../popover';\nimport { ActionsMenuDropdown, type ActionsMenuGroup } from './actions-menu';\nimport { ColorSwatch } from './color-swatch';\nimport { Input } from './input';\nimport { InputTrigger } from './input-trigger';\n\nexport interface ColorPreset {\n  key: string;\n  label: string;\n  color: string;\n}\n\n// Ticket-status palette.\nexport const TICKET_STATUS_COLOR_PRESETS: readonly ColorPreset[] = [\n  { key: 'green', label: 'Green', color: '#5ea62e' },\n  { key: 'lime', label: 'Lime', color: '#8bc34a' },\n  { key: 'teal', label: 'Teal', color: '#4db6ac' },\n  { key: 'sky', label: 'Sky', color: '#4fc3f7' },\n  { key: 'periwinkle', label: 'Periwinkle', color: '#7e9cd8' },\n  { key: 'lavender', label: 'Lavender', color: '#b39ddb' },\n  { key: 'yellow', label: 'Yellow', color: '#e1b32f' },\n  { key: 'sand', label: 'Sand', color: '#f0c674' },\n  { key: 'peach', label: 'Peach', color: '#f39c7a' },\n  { key: 'red', label: 'Red', color: '#f36666' },\n  { key: 'pink', label: 'Pink', color: '#e988a8' },\n  { key: 'neutral', label: 'Neutral', color: '#b0b0b0' },\n];\n\nexport const CUSTOM_PRESET_KEY = 'custom';\nexport const DEFAULT_CUSTOM_STATUS_COLOR = '#888888';\n\nfunction TriggerChevron() {\n  return (\n    <Chevron02DownIcon size={24} className=\"transition-transform duration-200 group-data-[state=open]:rotate-180\" />\n  );\n}\n\nexport interface ColorPresetSelectProps {\n  value: string;\n  presetKey?: string;\n  onChange: (next: { color: string; preset?: string }) => void;\n  presets?: readonly ColorPreset[];\n  disabled?: boolean;\n  className?: string;\n}\n\nexport function ColorPresetSelect({\n  value,\n  presetKey,\n  onChange,\n  presets = TICKET_STATUS_COLOR_PRESETS,\n  disabled,\n  className,\n}: ColorPresetSelectProps) {\n  const selectedPreset = presets.find(p => p.key === presetKey);\n  const isCustom = !selectedPreset;\n  const displayLabel = selectedPreset?.label ?? 'Custom';\n  const displayColor = selectedPreset?.color ?? value;\n\n  const groups: ActionsMenuGroup[] = [\n    {\n      items: [\n        ...presets.map(p => ({\n          id: p.key,\n          label: p.label,\n          icon: <ColorSwatch color={p.color} />,\n          onClick: () => onChange({ color: p.color, preset: p.key }),\n        })),\n        {\n          id: CUSTOM_PRESET_KEY,\n          label: 'Custom',\n          icon: <ColorSwatch color={isCustom ? value : DEFAULT_CUSTOM_STATUS_COLOR} />,\n          onClick: () => onChange({ color: value, preset: undefined }),\n        },\n      ],\n    },\n  ];\n\n  return (\n    <ActionsMenuDropdown\n      groups={groups}\n      align=\"start\"\n      sideOffset={4}\n      customTrigger={\n        <InputTrigger\n          disabled={disabled}\n          aria-label={`Color: ${displayLabel}`}\n          startIcon={<ColorSwatch color={displayColor} />}\n          selectedLabel={displayLabel}\n          endIcon={<TriggerChevron />}\n          className={cn('group', className)}\n        />\n      }\n    />\n  );\n}\n\n// ---------------------------------------------------------------------------\n// ColorPickerInput — saturation/value + hue + HEX/RGB/HSL inputs\n// Public contract: `value` and `onChange` are always hex (#rrggbb).\n// Conversion helpers live in `utils/ods-color-utils`.\n// ---------------------------------------------------------------------------\n\nconst FORMATS = ['HEX', 'RGB', 'HSL'] as const;\ntype ColorFormat = (typeof FORMATS)[number];\n\n// Channel inputs are number-shaped and centered; HEX is text-shaped and left-aligned.\n// Both target the Input component's inner <input> via the descendant selector.\n// `flex-1 min-w-0` lets each input share the row width and shrink below content;\n// without it, Input's `w-full` outer makes siblings overflow the popover.\nconst channelInputClass = 'flex-1 min-w-0 [&_input]:text-center [&_input]:tabular-nums';\nconst hexInputClass = 'flex-1 min-w-0 [&_input]:uppercase';\n\nfunction HexInputs({ value, onChange }: { value: string; onChange: (hex: string) => void }) {\n  // Adopt an externally-changed colour (the picker's canvas / hue slider drive\n  // this input too). Adjusted while rendering — React's documented prop-sync\n  // pattern — not from an effect: the input's `value` IS `draft`, so an effect\n  // painted one frame of the previous hex while dragging the canvas.\n  const [draft, setDraft] = useState(value);\n  const [syncedValue, setSyncedValue] = useState(value);\n  if (syncedValue !== value) {\n    setSyncedValue(value);\n    setDraft(value);\n  }\n\n  const commit = () => {\n    const candidate = (draft.startsWith('#') ? draft : `#${draft}`).toLowerCase();\n    if (HEX_PATTERN.test(candidate)) onChange(candidate);\n    else setDraft(value);\n  };\n\n  return (\n    <Input\n      type=\"text\"\n      value={draft}\n      onChange={e => setDraft(e.target.value)}\n      onBlur={commit}\n      onKeyDown={e => {\n        if (e.key === 'Enter') {\n          e.preventDefault();\n          commit();\n        }\n      }}\n      spellCheck={false}\n      aria-label=\"Hex color value\"\n      className={hexInputClass}\n    />\n  );\n}\n\nfunction ChannelInput({\n  value,\n  min,\n  max,\n  ariaLabel,\n  onCommit,\n}: {\n  value: number;\n  min: number;\n  max: number;\n  ariaLabel: string;\n  onCommit: (next: number) => void;\n}) {\n  // Same prop-sync as HexInputs above, and for the same reason.\n  const [draft, setDraft] = useState(String(value));\n  const [syncedValue, setSyncedValue] = useState(value);\n  if (syncedValue !== value) {\n    setSyncedValue(value);\n    setDraft(String(value));\n  }\n\n  const commit = () => {\n    const parsed = Number.parseInt(draft, 10);\n    if (!Number.isFinite(parsed)) {\n      setDraft(String(value));\n      return;\n    }\n    onCommit(Math.max(min, Math.min(max, parsed)));\n  };\n\n  return (\n    <Input\n      type=\"text\"\n      inputMode=\"numeric\"\n      pattern=\"[0-9]*\"\n      value={draft}\n      onChange={e => setDraft(e.target.value.replace(/[^0-9]/g, ''))}\n      onBlur={commit}\n      onKeyDown={e => {\n        if (e.key === 'Enter') {\n          e.preventDefault();\n          commit();\n        }\n      }}\n      aria-label={ariaLabel}\n      className={channelInputClass}\n    />\n  );\n}\n\nfunction RgbInputs({ value, onChange }: { value: string; onChange: (hex: string) => void }) {\n  const rgb = hexToRgb(value) ?? { r: 0, g: 0, b: 0 };\n  const update = (next: { r?: number; g?: number; b?: number }) => {\n    const r = next.r ?? rgb.r;\n    const g = next.g ?? rgb.g;\n    const b = next.b ?? rgb.b;\n    onChange(rgbToHex(r, g, b));\n  };\n  return (\n    <>\n      <ChannelInput value={rgb.r} min={0} max={255} ariaLabel=\"Red\" onCommit={r => update({ r })} />\n      <ChannelInput value={rgb.g} min={0} max={255} ariaLabel=\"Green\" onCommit={g => update({ g })} />\n      <ChannelInput value={rgb.b} min={0} max={255} ariaLabel=\"Blue\" onCommit={b => update({ b })} />\n    </>\n  );\n}\n\nfunction HslInputs({ value, onChange }: { value: string; onChange: (hex: string) => void }) {\n  const rgb = hexToRgb(value) ?? { r: 0, g: 0, b: 0 };\n  const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);\n  const update = (next: { h?: number; s?: number; l?: number }) => {\n    const h = next.h ?? hsl.h;\n    const s = next.s ?? hsl.s;\n    const l = next.l ?? hsl.l;\n    const out = hslToRgb(h, s, l);\n    onChange(rgbToHex(out.r, out.g, out.b));\n  };\n  return (\n    <>\n      <ChannelInput value={hsl.h} min={0} max={360} ariaLabel=\"Hue\" onCommit={h => update({ h })} />\n      <ChannelInput value={hsl.s} min={0} max={100} ariaLabel=\"Saturation\" onCommit={s => update({ s })} />\n      <ChannelInput value={hsl.l} min={0} max={100} ariaLabel=\"Lightness\" onCommit={l => update({ l })} />\n    </>\n  );\n}\n\nexport interface ColorPickerInputProps {\n  value: string;\n  onChange: (color: string) => void;\n  disabled?: boolean;\n  className?: string;\n}\n\nexport function ColorPickerInput({ value, onChange, disabled, className }: ColorPickerInputProps) {\n  const [open, setOpen] = useState(false);\n  const [format, setFormat] = useState<ColorFormat>('HEX');\n\n  const formatGroups: ActionsMenuGroup[] = [\n    {\n      items: FORMATS.map(f => ({\n        id: f,\n        label: f,\n        onClick: () => setFormat(f),\n      })),\n    },\n  ];\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <InputTrigger\n          disabled={disabled}\n          aria-label=\"Pick custom color\"\n          startIcon={<ColorSwatch color={value} />}\n          selectedLabel={<span className=\"uppercase\">{value}</span>}\n          endIcon={<TriggerChevron />}\n          className={cn('group', className)}\n        />\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        sideOffset={4}\n        className=\"z-[1400] w-[320px] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-sf)] text-ods-text-primary shadow-md [&_.react-colorful\\\\_\\\\_hue]:!mt-[var(--spacing-system-m)] [&_.react-colorful\\\\_\\\\_hue]:!h-4 [&_.react-colorful\\\\_\\\\_last-control]:!rounded-lg [&_.react-colorful\\\\_\\\\_saturation]:!rounded-sm [&_.react-colorful]:!h-[190px] [&_.react-colorful]:!w-full\"\n      >\n        <div className=\"flex flex-col gap-[var(--spacing-system-m)]\">\n          <HexColorPicker color={value} onChange={onChange} />\n          <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n            <ActionsMenuDropdown\n              groups={formatGroups}\n              align=\"start\"\n              sideOffset={4}\n              contentClassName=\"z-[1500]\"\n              customTrigger={\n                <InputTrigger\n                  aria-label={`Color format: ${format}`}\n                  selectedLabel={format}\n                  endIcon={<TriggerChevron />}\n                  className=\"group w-24 shrink-0\"\n                />\n              }\n            />\n            <div className=\"flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xs)]\">\n              {format === 'HEX' && <HexInputs value={value} onChange={onChange} />}\n              {format === 'RGB' && <RgbInputs value={value} onChange={onChange} />}\n              {format === 'HSL' && <HslInputs value={value} onChange={onChange} />}\n            </div>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { type ComponentPropsWithoutRef, type ElementRef, forwardRef } from 'react';\n\nimport { useKeyboardCollisionPadding } from '../hooks/ui/use-keyboard-collision-padding';\nimport { cn } from '../utils/cn';\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = forwardRef<\n  ElementRef<typeof PopoverPrimitive.Content>,\n  ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, collisionPadding, ...props }, ref) => {\n  // Any popover holding a field — the colour picker's hex/RGB inputs, a search\n  // box — is positioned against a collision viewport that does not know the\n  // software keyboard exists. See useKeyboardCollisionPadding.\n  const keyboardPadding = useKeyboardCollisionPadding();\n  return (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        ref={ref}\n        align={align}\n        sideOffset={sideOffset}\n        collisionPadding={collisionPadding ?? { bottom: keyboardPadding }}\n        className={cn(\n          'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n          className,\n        )}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  );\n});\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n","'use client';\n\nimport type { ColumnDef, Row } from '@tanstack/react-table';\nimport { useMemo, useState, useEffect, useCallback } from 'react';\nimport { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';\nimport { cn } from '../../utils/cn';\nimport { formatTime } from '../../utils/format-date';\nimport { Copy02Icon, SearchIcon } from '../icons-v2-generated';\nimport { DataTable } from './data-table';\nimport { useDataTable } from './data-table/use-data-table';\nimport { NoData } from './no-data';\nimport type { QueryResultRow } from './query-report-table';\nimport { ScrollShadow } from './scroll-fade';\nimport { Tag } from './tag';\n\n/*\n * Live test-run building blocks: the UI (timing stats, one-row skeleton,\n * results table) and the run state machine for \"run a query/script somewhere\n * and stream tabular results back\" flows. The transport is NOT part of this\n * module — consumers own campaign creation (Fleet live queries, script runs,\n * ...) and pass a `TestRunCampaignState`-shaped object into\n * `useTestRunState`.\n */\n\nexport function formatTestRunDuration(ms: number): string {\n  const totalSeconds = Math.floor(ms / 1000);\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = totalSeconds % 60;\n  return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;\n}\n\nconst RESULT_COLUMN_MIN_WIDTH = 176;\nconst SKELETON_COLUMNS = 6;\n\n/** Column-bar skeleton (header cells + one card row): fixed 160px columns on\n    every breakpoint, scrolling horizontally when they do not fit. Bars use\n    bg-ods-bg-surface like the table skeletons — the bg-ods-skeleton token is\n    the same color as the card background and would be invisible on the row. */\nexport function TestResultsSkeleton() {\n  return (\n    <ScrollShadow axis=\"horizontal\">\n      {/* No gap between the header bars and the row card — the real table's\n          header and body sit flush. */}\n      <div className=\"flex w-max min-w-full flex-col\">\n        <div className=\"flex items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]\">\n          {Array.from({ length: SKELETON_COLUMNS }).map((_, i) => (\n            <div key={`skeleton-header-${i}`} className=\"flex h-12 w-[160px] shrink-0 items-center\">\n              <div className=\"h-4 w-3/4 animate-pulse rounded-sm bg-ods-bg-surface\" />\n            </div>\n          ))}\n        </div>\n        <div className=\"animate-pulse overflow-hidden rounded-[6px] border border-ods-border bg-ods-card\">\n          {/* Same INNER row heights as the DataTable (66/78px + the 1px card\n              border on each side = 68/80px total, per design). */}\n          <div className=\"flex h-[66px] items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] md:h-[78px]\">\n            {Array.from({ length: SKELETON_COLUMNS }).map((_, i) => (\n              <div key={`skeleton-cell-${i}`} className=\"w-[160px] shrink-0\">\n                <div className=\"h-5 w-3/4 rounded-sm bg-ods-bg-surface\" />\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </ScrollShadow>\n  );\n}\n\n/**\n * Live test results rendered with the core DataTable (identical header,\n * skeleton, and card-row styling to the standard tables). Columns are derived\n * from the keys of the returned rows; wide result sets scroll horizontally.\n * Unlike the default tables, headers stay visible on every breakpoint.\n */\nexport function TestResultsTable({ rows, loading }: { rows: QueryResultRow[]; loading: boolean }) {\n  const columnKeys = useMemo(() => (rows.length > 0 ? Object.keys(rows[0]) : []), [rows]);\n\n  const columns = useMemo<ColumnDef<QueryResultRow>[]>(() => {\n    return columnKeys.map(key => ({\n      id: key,\n      accessorFn: (row: QueryResultRow) => row[key],\n      header: key,\n      enableSorting: false,\n      cell: ({ row }: { row: Row<QueryResultRow> }) => {\n        const value = row.original[key];\n        return (\n          <span className=\"truncate text-ods-text-primary text-h4\">\n            {value === null || value === undefined ? '-' : String(value)}\n          </span>\n        );\n      },\n      meta: {\n        width: 'flex-1 min-w-[176px]',\n        // Show headers on every breakpoint; headerClassName sizes the cells\n        // below lg where meta.width is not applied.\n        alwaysShowHeader: true,\n        headerClassName: 'flex-1 min-w-[176px]',\n      },\n    }));\n  }, [columnKeys]);\n\n  const table = useDataTable<QueryResultRow>({\n    data: rows,\n    columns,\n    getRowId: (_row: QueryResultRow, index: number) => String(index),\n    enableSorting: false,\n  });\n\n  if (loading) {\n    return <TestResultsSkeleton />;\n  }\n\n  return (\n    <ScrollShadow axis=\"horizontal\">\n      <div style={{ minWidth: Math.max(1, columnKeys.length) * RESULT_COLUMN_MIN_WIDTH }}>\n        <DataTable table={table}>\n          {/* \"flex\" overrides the header's default \"hidden md:flex\" so the\n              headers render on mobile too. */}\n          <DataTable.Header className=\"flex flex-col\" />\n          <DataTable.Body loading={false} />\n        </DataTable>\n      </div>\n    </ScrollShadow>\n  );\n}\n\nexport interface TestRunResultsProps {\n  isActive: boolean;\n  displayRows: QueryResultRow[];\n  firstError: string | null;\n  className?: string;\n}\n\n/** Red error banner with a copy-to-clipboard action, per the test-run design. */\nexport function TestRunErrorBanner({ error, className }: { error: string; className?: string }) {\n  const { copy } = useCopyToClipboard({ successDescription: 'Error message copied to clipboard' });\n  return (\n    <div\n      role=\"alert\"\n      className={cn(\n        'flex items-center justify-between gap-[var(--spacing-system-s)]',\n        'rounded-md border border-ods-error bg-ods-error-secondary',\n        'px-[var(--spacing-system-mf)] py-[var(--spacing-system-s)]',\n        className,\n      )}\n    >\n      <p className=\"min-w-0 break-words text-ods-error text-h4\">{error}</p>\n      <button\n        type=\"button\"\n        aria-label=\"Copy error message\"\n        onClick={() => copy(error)}\n        className=\"shrink-0 rounded-sm text-ods-error transition-opacity hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus\"\n      >\n        <Copy02Icon className=\"size-5\" />\n      </button>\n    </div>\n  );\n}\n\n/** Error line + empty state + result table for a finished/running test run. */\nexport function TestRunResults({ isActive, displayRows, firstError, className }: TestRunResultsProps) {\n  return (\n    <div className={cn('flex flex-col gap-[var(--spacing-system-xsf)]', className)}>\n      {firstError && <TestRunErrorBanner error={firstError} />}\n      {!isActive && displayRows.length === 0 ? (\n        // With zero rows the error line alone explains the outcome; a\n        // \"No results returned\" empty state next to it would mislead.\n        // Otherwise: fixed-height empty state matching the 1-row\n        // skeleton/result height (48px header + 80px row, no gap) so the\n        // block does not jump between loading/result/empty.\n        !firstError && (\n          <NoData icon={<SearchIcon />} title=\"No results returned\" className=\"h-[128px] justify-center !py-0\" />\n        )\n      ) : (\n        <TestResultsTable rows={displayRows} loading={isActive && displayRows.length === 0} />\n      )}\n    </div>\n  );\n}\n\n/** Started / Duration stat cell shown in a test-run controls row. */\nexport function TimingStat({ value, label, className }: { value: string; label: string; className?: string }) {\n  return (\n    <div className={cn('flex min-w-0 flex-col justify-center', className)}>\n      <span className=\"truncate text-ods-text-secondary text-h4\">{value}</span>\n      <span className=\"truncate text-ods-text-secondary text-h6\">{label}</span>\n    </div>\n  );\n}\n\n/** Overall run outcome shown in a test-run controls row. */\nexport type TestRunStatus = 'idle' | 'running' | 'success' | 'error';\n\nconst TEST_RUN_STATUS_TAGS: Record<\n  Exclude<TestRunStatus, 'idle'>,\n  { label: string; variant: 'warning' | 'success' | 'error' }\n> = {\n  running: { label: 'IN PROCESS', variant: 'warning' },\n  success: { label: 'SUCCESS', variant: 'success' },\n  error: { label: 'ERROR', variant: 'error' },\n};\n\n/** Status stat cell: \"-\" before the first run, then a colored state tag. */\nexport function TestRunStatusStat({ status, className }: { status: TestRunStatus; className?: string }) {\n  const tag = status === 'idle' ? null : TEST_RUN_STATUS_TAGS[status];\n  return (\n    <div className={cn('flex min-w-0 flex-col items-start justify-center gap-[var(--spacing-system-xxs)]', className)}>\n      {tag ? (\n        <Tag label={tag.label} variant={tag.variant} className=\"pointer-events-none\" />\n      ) : (\n        <span className=\"truncate text-ods-text-secondary text-h4\">-</span>\n      )}\n      <span className=\"truncate text-ods-text-secondary text-h6\">Status</span>\n    </div>\n  );\n}\n\n/**\n * The transport-side contract `useTestRunState` observes. Matches the shape\n * of a live-campaign hook (e.g. Fleet live queries in OpenFrame) without\n * depending on any concrete backend.\n */\nexport interface TestRunCampaignState {\n  isRunning: boolean;\n  startedAt: Date | null;\n  /** '' before the first run, then 'pending' → 'finished'. */\n  campaignStatus: string;\n  results: QueryResultRow[];\n  errors: Array<{ error: string }>;\n  stopCampaign: () => void;\n}\n\n/**\n * State machine for one live test run: wraps a campaign-shaped object with\n * the run/stop/reset flow, elapsed-duration ticking, and the display flags\n * test panels render from. The consumer starts the actual campaign inside the\n * `start` callback passed to `run`:\n *\n * ```tsx\n * const campaign = useLiveCampaign()               // app-owned transport\n * const test = useTestRunState(campaign)\n * const handleRun = () => test.run(() => campaign.startCampaign(sql, [hostId]))\n * ```\n */\nexport function useTestRunState(campaign: TestRunCampaignState) {\n  // Sticky \"a run has been started\" flag: campaignStatus resets to '' for a\n  // moment when a new run starts, which would flash the initial Run button\n  // between \"Test Again\" clicks without it.\n  const [hasRun, setHasRun] = useState(false);\n  // True while the campaign is being created (before isRunning flips) —\n  // without it the button would flash \"Test Again\" right after a Run click.\n  const [isStarting, setIsStarting] = useState(false);\n  const [durationMs, setDurationMs] = useState(0);\n\n  useEffect(() => {\n    // Bound once, at effect time. The ticking closure below outlives this\n    // scope, so a narrowing of `campaign.startedAt` would not reach it — and\n    // the effect already re-runs whenever `startedAt` changes, so the captured\n    // value is never stale.\n    const startedAt = campaign.startedAt;\n    if (!startedAt) return undefined;\n    // The wall clock is the external system here and `setInterval` is the\n    // subscription to it; the seed read shares the subscription's callback\n    // rather than being a separate setState in the effect body. A finished run\n    // takes that one read and stops, freezing at the final elapsed time.\n    const tick = () => setDurationMs(Date.now() - startedAt.getTime());\n    tick();\n    if (!campaign.isRunning) return undefined;\n    const interval = setInterval(tick, 1000);\n    return () => clearInterval(interval);\n  }, [campaign.startedAt, campaign.isRunning]);\n\n  const run = useCallback(async (start: () => Promise<unknown>) => {\n    setHasRun(true);\n    setDurationMs(0);\n    setIsStarting(true);\n    try {\n      await start();\n    } finally {\n      setIsStarting(false);\n    }\n  }, []);\n\n  // Stop/reset also drop isStarting so the Stop button doesn't stay \"active\"\n  // for the moment between the user's click and the pending start promise\n  // settling (its finally clears the flag again — harmless).\n  const stop = useCallback(() => {\n    setIsStarting(false);\n    campaign.stopCampaign();\n  }, [campaign]);\n\n  /** Stop anything in flight and clear the run state (panel close/cancel). */\n  const reset = useCallback(() => {\n    setIsStarting(false);\n    campaign.stopCampaign();\n    setDurationMs(0);\n    setHasRun(false);\n  }, [campaign]);\n\n  const isActive = campaign.isRunning || isStarting;\n  const isFinished = campaign.campaignStatus === 'finished';\n  const showResults = isActive || (hasRun && isFinished);\n\n  const firstError = isFinished && campaign.errors.length > 0 ? campaign.errors[0].error : null;\n\n  // Overall run outcome for the Status stat: '-' until a run starts, then\n  // IN PROCESS while active, and SUCCESS/ERROR once the campaign finishes.\n  const status: TestRunStatus = isActive\n    ? 'running'\n    : hasRun && isFinished\n      ? firstError\n        ? 'error'\n        : 'success'\n      : 'idle';\n\n  // Started/Duration show zeros until the current run's timing is real:\n  // hasRun=false after a reset, and campaignStatus resets while the next\n  // run is being created, so stale values never linger on screen.\n  const showTiming = hasRun && campaign.startedAt !== null && (campaign.isRunning || isFinished);\n\n  const startedLabel = showTiming && campaign.startedAt ? formatTime(campaign.startedAt) : '00:00 PM';\n  const durationLabel = showTiming ? formatTestRunDuration(durationMs) : '00:00:00';\n\n  // Humanize raw result keys for the table headers (snake_case -> spaced\n  // words); the row data itself is shown exactly as returned.\n  const displayRows = useMemo<QueryResultRow[]>(\n    () =>\n      campaign.results.map(row =>\n        Object.fromEntries(Object.entries(row).map(([key, value]) => [key.replace(/_/g, ' '), value])),\n      ),\n    [campaign.results],\n  );\n\n  return {\n    hasRun,\n    isActive,\n    isFinished,\n    showResults,\n    status,\n    firstError,\n    startedLabel,\n    durationLabel,\n    displayRows,\n    run,\n    stop,\n    reset,\n  };\n}\n","'use client';\n\nimport { cn } from '../../utils/cn';\nimport { Download02Icon, FileIcon, TrashIcon } from '../icons-v2-generated';\nimport { SquareAvatar } from './square-avatar';\n\nexport interface TicketAttachment {\n  id: string;\n  fileName: string;\n  fileSize: string;\n  thumbnailSrc?: string;\n  onDownload?: () => void;\n  /** When set, a trash button is rendered before the download button. */\n  onDelete?: () => void;\n}\n\nexport interface TicketAttachmentsListProps {\n  attachments: TicketAttachment[];\n  className?: string;\n  /** `compact` shrinks padding / icon / text / download-button for\n   *  in-message rendering (the ticket conversation feed). Default keeps the\n   *  roomier full-row layout for any other surface. */\n  size?: 'default' | 'compact';\n}\n\nexport function TicketAttachmentsList({ attachments, className, size = 'default' }: TicketAttachmentsListProps) {\n  if (attachments.length === 0) return null;\n  const compact = size === 'compact';\n\n  return (\n    <div className={cn('overflow-hidden rounded-[6px] border border-ods-border', className)}>\n      {attachments.map((attachment, index) => (\n        <div\n          key={attachment.id}\n          className={cn(\n            'flex items-center bg-ods-card',\n            compact ? 'gap-2 px-2 py-1.5' : 'gap-4 px-4 py-3',\n            index < attachments.length - 1 && 'border-b border-ods-border',\n          )}\n        >\n          {attachment.thumbnailSrc ? (\n            <SquareAvatar\n              src={attachment.thumbnailSrc}\n              alt={attachment.fileName}\n              size={compact ? 'sm' : 'md'}\n              className=\"shrink-0\"\n            />\n          ) : (\n            <div\n              className={cn(\n                'flex shrink-0 items-center justify-center rounded-[6px] border border-ods-border bg-ods-card',\n                compact ? 'size-8' : 'size-10',\n              )}\n            >\n              <FileIcon className={cn('text-ods-text-secondary', compact ? 'size-4' : 'size-6')} />\n            </div>\n          )}\n          <div className=\"min-w-0 flex-1 overflow-hidden\">\n            <p\n              className={cn('truncate text-ods-text-primary', compact ? 'text-h5' : 'text-h4')}\n              title={attachment.fileName}\n            >\n              {attachment.fileName}\n            </p>\n            {attachment.fileSize && <p className=\"text-ods-text-secondary text-h6\">{attachment.fileSize}</p>}\n          </div>\n          {attachment.onDelete && (\n            <button\n              type=\"button\"\n              onClick={attachment.onDelete}\n              className=\"shrink-0 text-ods-text-secondary transition-colors hover:text-ods-error\"\n              aria-label={`Delete ${attachment.fileName}`}\n            >\n              <TrashIcon className={compact ? 'size-4' : 'size-5'} />\n            </button>\n          )}\n          {attachment.onDownload && (\n            <button\n              type=\"button\"\n              onClick={attachment.onDownload}\n              className=\"shrink-0 text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n              aria-label={`Download ${attachment.fileName}`}\n            >\n              <Download02Icon className={compact ? 'size-4' : 'size-6'} />\n            </button>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface TicketDetailSectionProps {\n  /** Section label displayed as uppercase heading */\n  label: string;\n  children: ReactNode;\n  className?: string;\n}\n\nexport function TicketDetailSection({ label, children, className }: TicketDetailSectionProps) {\n  return (\n    <div className={cn('flex flex-col gap-1', className)}>\n      <p className=\"text-ods-text-secondary text-h5\">{label}</p>\n      {children}\n    </div>\n  );\n}\n","'use client';\n\nimport { type KeyboardEvent, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Send03Icon } from '../icons-v2-generated';\nimport { Input } from './input';\nimport { type TicketNote, TicketNoteCard } from './ticket-note-card';\n\nexport interface TicketNotesSectionProps {\n  notes: TicketNote[];\n  onAddNote?: (text: string) => void;\n  onEditNote?: (id: string, text: string) => void;\n  onDeleteNote?: (id: string) => void;\n  /** Disables the input and send button while a note is being added */\n  isAddingNote?: boolean;\n  className?: string;\n}\n\nexport function TicketNotesSection({\n  notes,\n  onAddNote,\n  onEditNote,\n  onDeleteNote,\n  isAddingNote,\n  className,\n}: TicketNotesSectionProps) {\n  const [noteText, setNoteText] = useState('');\n\n  const handleSend = () => {\n    const trimmed = noteText.trim();\n    if (!trimmed || !onAddNote || isAddingNote) return;\n    onAddNote(trimmed);\n    setNoteText('');\n  };\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'Enter' && !e.shiftKey) {\n      e.preventDefault();\n      handleSend();\n    }\n  };\n\n  return (\n    <div className={cn('flex flex-col gap-2', className)}>\n      {notes.map(note => (\n        <TicketNoteCard key={note.id} note={note} onEdit={onEditNote} onDelete={onDeleteNote} />\n      ))}\n\n      {onAddNote && (\n        <div className=\"flex items-center gap-2\">\n          <div className=\"flex-1\">\n            <Input\n              value={noteText}\n              onChange={e => setNoteText(e.target.value)}\n              onKeyDown={handleKeyDown}\n              placeholder=\"Enter your Note Here\"\n              disabled={isAddingNote}\n            />\n          </div>\n          <button\n            type=\"button\"\n            onClick={handleSend}\n            disabled={!noteText.trim() || isAddingNote}\n            className={cn(\n              'flex h-11 w-11 shrink-0 items-center justify-center rounded-[6px] md:h-12 md:w-12',\n              'border border-ods-border bg-ods-card',\n              'transition-colors hover:bg-ods-bg-hover',\n              'disabled:cursor-not-allowed disabled:opacity-50',\n            )}\n            aria-label=\"Send note\"\n          >\n            <Send03Icon className=\"size-6 text-ods-text-secondary\" />\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { type KeyboardEvent, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { CheckIcon, PenEditIcon, TrashIcon, XmarkIcon } from '../icons-v2-generated';\nimport { Input } from './input';\nimport { SquareAvatar } from './square-avatar';\n\nexport interface TicketNote {\n  id: string;\n  text: string;\n  authorName: string;\n  authorAvatar?: string;\n  createdAt: string;\n  isOwn: boolean;\n}\n\nexport interface TicketNoteCardProps {\n  note: TicketNote;\n  onEdit?: (id: string, text: string) => void;\n  onDelete?: (id: string) => void;\n  className?: string;\n}\n\nexport function TicketNoteCard({ note, onEdit, onDelete, className }: TicketNoteCardProps) {\n  const [isEditing, setIsEditing] = useState(false);\n  const [editText, setEditText] = useState(note.text);\n\n  const handleSave = () => {\n    const trimmed = editText.trim();\n    if (!trimmed || !onEdit) return;\n    onEdit(note.id, trimmed);\n    setIsEditing(false);\n  };\n\n  const handleCancel = () => {\n    setEditText(note.text);\n    setIsEditing(false);\n  };\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'Enter' && !e.shiftKey) {\n      e.preventDefault();\n      handleSave();\n    }\n    if (e.key === 'Escape') {\n      handleCancel();\n    }\n  };\n\n  return (\n    <div className={cn('flex items-start gap-4 rounded-[6px] p-3', 'border border-ods-border bg-ods-card', className)}>\n      <SquareAvatar\n        src={note.authorAvatar}\n        alt={note.authorName}\n        fallback={note.authorName}\n        size=\"sm\"\n        variant=\"round\"\n        className=\"shrink-0\"\n      />\n      <div className=\"min-w-0 flex-1 overflow-hidden\">\n        {isEditing ? (\n          <div className=\"flex items-center gap-2\">\n            <div className=\"flex-1\">\n              <Input value={editText} onChange={e => setEditText(e.target.value)} onKeyDown={handleKeyDown} autoFocus />\n            </div>\n            <button\n              type=\"button\"\n              onClick={handleSave}\n              disabled={!editText.trim()}\n              className=\"shrink-0 text-ods-success transition-colors hover:text-ods-success/80 disabled:opacity-50\"\n              aria-label=\"Save note\"\n            >\n              <CheckIcon className=\"size-5\" />\n            </button>\n            <button\n              type=\"button\"\n              onClick={handleCancel}\n              className=\"shrink-0 text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n              aria-label=\"Cancel editing\"\n            >\n              <XmarkIcon className=\"size-5\" />\n            </button>\n          </div>\n        ) : (\n          <>\n            <p className=\"text-ods-text-primary text-h4\">{note.text}</p>\n            <p className=\"truncate text-ods-text-secondary text-h6\" title={`${note.authorName} • ${note.createdAt}`}>\n              {note.authorName} &bull; {note.createdAt}\n            </p>\n          </>\n        )}\n      </div>\n      {note.isOwn && !isEditing && (\n        <div className=\"flex shrink-0 items-center gap-4\">\n          {onDelete && (\n            <button\n              type=\"button\"\n              onClick={() => onDelete(note.id)}\n              className=\"text-ods-text-secondary transition-colors hover:text-ods-error\"\n              aria-label=\"Delete note\"\n            >\n              <TrashIcon className=\"size-6\" />\n            </button>\n          )}\n          {onEdit && (\n            <button\n              type=\"button\"\n              onClick={() => setIsEditing(true)}\n              className=\"text-ods-text-secondary transition-colors hover:text-ods-text-primary\"\n              aria-label=\"Edit note\"\n            >\n              <PenEditIcon className=\"size-6\" />\n            </button>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';\nimport { UserIcon } from '../icons-v2-generated/users/user-icon';\nimport { AssigneeDropdown, type TicketAssigneeOption } from './assignee-dropdown';\nimport { SimpleMarkdownRenderer } from './markdown/simple-markdown-renderer';\nimport { SquareAvatar } from './square-avatar';\nimport { Tag } from './tag';\nimport { type TicketAttachment, TicketAttachmentsList } from './ticket-attachments-list';\nimport { TicketDetailSection } from './ticket-detail-section';\nimport type { TicketNote } from './ticket-note-card';\nimport { TicketNotesSection } from './ticket-notes-section';\nimport { TicketStatusTag, type TicketStatusTagOption } from './ticket-status-tag';\n\nexport type { TicketAssigneeOption };\n\nexport interface TicketInfoSectionProps {\n  /** Organization name and image */\n  organization?: {\n    name: string;\n    imageSrc?: string;\n  };\n  /** User name */\n  user?: string;\n  /** Device info */\n  device?: {\n    name: string;\n    icon?: ReactNode;\n    onClick?: () => void;\n  };\n  /** Status tag */\n  status?: string;\n  /** Display label for the status tag (e.g. a custom status name). */\n  statusLabel?: string;\n  /** Hex color for the status tag (e.g. a custom status color). */\n  statusColor?: string;\n  /** When provided, the status tag becomes an inline changer with these options. */\n  statusOptions?: TicketStatusTagOption[];\n  /** Called with the chosen status id from the inline status dropdown. */\n  onStatusSelect?: (id: string) => void;\n  /** Disables the inline status dropdown while a change is in flight. */\n  isStatusPending?: boolean;\n  /** Locks the status: renders the plain tag instead of the dropdown (e.g. a pending approval blocks the transition). */\n  isStatusDisabled?: boolean;\n  /** Reason shown in a tooltip over the status tag when `isStatusDisabled`. */\n  statusDisabledReason?: string;\n  /** Expand button click handler */\n  onExpand?: () => void;\n  /** Whether the section is expanded */\n  expanded?: boolean;\n  /** Additional className */\n  className?: string;\n\n  // --- Expanded view props ---\n\n  /** Assigned person info with inline dropdown */\n  assigned?: {\n    currentAssignee?: {\n      id: string;\n      name: string;\n      avatarSrc?: string;\n      /** Deleted account (DELETED / SELF_DELETED) — renders the red user-x placeholder + red name. */\n      deleted?: boolean;\n    };\n    options: TicketAssigneeOption[];\n    isLoading?: boolean;\n    isPending?: boolean;\n    onAssign: (userId: string | null) => void;\n  };\n  /** Created date string */\n  createdAt?: string;\n  /** Ticket description — markdown/HTML content */\n  description?: string;\n  /** File attachments (view-only with download) */\n  attachments?: TicketAttachment[];\n  /** Tag labels */\n  tags?: string[];\n  /** Notes */\n  notes?: TicketNote[];\n  onAddNote?: (text: string) => void;\n  onEditNote?: (id: string, text: string) => void;\n  onDeleteNote?: (id: string) => void;\n  /** Disables the note input while a note is being added */\n  isAddingNote?: boolean;\n}\n\nfunction InfoCell({\n  value,\n  label,\n  icon,\n  onClick,\n}: {\n  value: string;\n  label: string;\n  icon?: ReactNode;\n  onClick?: () => void;\n}) {\n  return (\n    <div className=\"min-w-0 flex-1 overflow-hidden\">\n      <div className=\"flex flex-col justify-center\">\n        <div className=\"flex w-full min-w-0 items-center gap-1\">\n          {icon && (\n            <span className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary\">{icon}</span>\n          )}\n          {onClick ? (\n            <button\n              type=\"button\"\n              onClick={onClick}\n              className=\"cursor-pointer truncate text-left text-ods-text-primary transition-colors text-h4 hover:text-ods-accent\"\n              title={value}\n            >\n              {value}\n            </button>\n          ) : (\n            <span className=\"truncate text-ods-text-primary text-h4\" title={value}>\n              {value}\n            </span>\n          )}\n        </div>\n        <span className=\"truncate text-ods-text-secondary text-h6\">{label}</span>\n      </div>\n    </div>\n  );\n}\n\nexport function TicketInfoSection({\n  organization,\n  device,\n  status,\n  statusLabel,\n  statusColor,\n  statusOptions,\n  onStatusSelect,\n  isStatusPending,\n  isStatusDisabled,\n  statusDisabledReason,\n  onExpand,\n  expanded = false,\n  className,\n  assigned,\n  createdAt,\n  description,\n  attachments,\n  tags,\n  notes,\n  onAddNote,\n  onEditNote,\n  onDeleteNote,\n  isAddingNote,\n}: TicketInfoSectionProps) {\n  return (\n    <div className={cn('overflow-hidden rounded-[6px] border border-ods-border', className)}>\n      {/* Header row */}\n      <div className=\"grid grid-cols-2 items-center gap-4 border-b border-ods-border bg-ods-card px-4 py-3 lg:grid-cols-[1fr_1fr_1fr_auto]\">\n        {/* Organization with image */}\n        <div className=\"flex min-w-0 items-center gap-2\">\n          <SquareAvatar\n            src={organization?.imageSrc}\n            alt={organization?.name}\n            fallback={organization?.name || 'Org'}\n            size=\"md\"\n            className=\"shrink-0\"\n          />\n          <InfoCell value={organization?.name || 'Unassigned'} label=\"Organization\" />\n        </div>\n\n        {/* Assigned */}\n        <div className=\"min-w-0\">\n          {assigned ? (\n            <AssigneeDropdown\n              currentAssignee={assigned.currentAssignee}\n              options={assigned.options}\n              isLoading={assigned.isLoading}\n              isPending={assigned.isPending}\n              onAssign={assigned.onAssign}\n            />\n          ) : (\n            <div className=\"min-w-0\">\n              <div className=\"flex items-center gap-1 text-ods-text-secondary text-h4\">\n                <UserIcon className=\"size-4 shrink-0\" />\n                <span className=\"truncate\">Unassigned</span>\n              </div>\n              <span className=\"block truncate text-ods-text-secondary text-h6\">Assigned</span>\n            </div>\n          )}\n        </div>\n\n        {/* Device */}\n        <InfoCell value={device?.name || 'Unassigned'} label=\"Device\" icon={device?.icon} onClick={device?.onClick} />\n\n        {/* Status tag + expand button */}\n        <div className=\"flex min-w-0 items-center gap-4\">\n          {(status || statusLabel) && (\n            <div className=\"min-w-0\">\n              <TicketStatusTag\n                status={status ?? ''}\n                label={statusLabel}\n                color={statusColor}\n                options={statusOptions}\n                onSelect={onStatusSelect}\n                isPending={isStatusPending}\n                disabled={isStatusDisabled}\n                disabledReason={statusDisabledReason}\n              />\n            </div>\n          )}\n          {onExpand && (\n            <button\n              type=\"button\"\n              onClick={onExpand}\n              className={cn(\n                'flex shrink-0 items-center justify-center rounded-[6px] p-3',\n                'border border-ods-border bg-ods-card',\n                'transition-colors duration-150 hover:bg-ods-bg-hover',\n              )}\n              aria-label={expanded ? 'Collapse' : 'Expand'}\n            >\n              <Chevron02DownIcon\n                className={cn(\n                  'size-6 text-ods-text-primary transition-transform duration-200',\n                  expanded && 'rotate-180',\n                )}\n              />\n            </button>\n          )}\n        </div>\n      </div>\n\n      {/* Expanded content */}\n      {expanded && (\n        <>\n          {/* Second info row: Created */}\n          {createdAt && (\n            <div className=\"grid grid-cols-2 items-center gap-4 border-b border-ods-border bg-ods-bg px-4 py-3\">\n              <InfoCell value={createdAt} label=\"Created\" />\n            </div>\n          )}\n\n          {/* Content area */}\n          <div className=\"flex flex-col gap-4 border-b border-ods-border bg-ods-bg p-4\">\n            {/* Description */}\n            {description && <SimpleMarkdownRenderer content={description} />}\n\n            {/* Attachments */}\n            {attachments && attachments.length > 0 && (\n              <TicketDetailSection label=\"Attachments\">\n                <TicketAttachmentsList attachments={attachments} />\n              </TicketDetailSection>\n            )}\n\n            {/* Tags */}\n            {tags && tags.length > 0 && (\n              <TicketDetailSection label=\"Tags\">\n                <div className=\"flex min-w-0 flex-wrap gap-2\">\n                  {tags.map(tag => (\n                    <Tag key={tag} label={tag} variant=\"outline\" className=\"max-w-full\" />\n                  ))}\n                </div>\n              </TicketDetailSection>\n            )}\n\n            {/* Notes */}\n            {(notes || onAddNote) && (\n              <TicketDetailSection label=\"Notes\">\n                <TicketNotesSection\n                  notes={notes || []}\n                  onAddNote={onAddNote}\n                  onEditNote={onEditNote}\n                  onDeleteNote={onDeleteNote}\n                  isAddingNote={isAddingNote}\n                />\n              </TicketDetailSection>\n            )}\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { type ReactNode, useState } from 'react';\nimport { CheckIcon } from '../icons-v2-generated';\nimport { Autocomplete, type AutocompleteOption } from './autocomplete';\nimport { Button } from './button';\nimport { ColorSwatch } from './color-swatch';\nimport { ModalV2, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2';\nimport { SquareAvatar } from './square-avatar';\nimport { TruncateText } from './truncate-text';\n\nexport interface TakeOverStatusOption extends AutocompleteOption {\n  color: string;\n}\n\nexport interface TakeOverAssigneeOption extends AutocompleteOption {\n  /** Full (already resolved) avatar URL. */\n  imageUrl?: string;\n}\n\nexport interface TakeOverTicketSelection {\n  statusId: string;\n  assigneeId: string;\n}\n\nexport interface TakeOverTicketModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  /** Highlighted ticket reference in the description, e.g. \"1003: Email client synchronization issues\". */\n  ticketRef: string;\n  /** Allowed target statuses, in display order (consumer decides grouping, e.g. custom statuses first). */\n  statusOptions: TakeOverStatusOption[];\n  /** Assignable users, in display order (consumer decides ordering, e.g. signed-in user first). */\n  assigneeOptions: TakeOverAssigneeOption[];\n  assigneesLoading?: boolean;\n  /** Pre-selected status; falls back to the first status option. */\n  initialStatusId?: string | null;\n  /** Pre-selected assignee; no fallback — pass the signed-in user id for the design default. */\n  initialAssigneeId?: string | null;\n  /** Confirm in-flight: buttons lock and the modal refuses to close. */\n  isPending?: boolean;\n  onConfirm: (selection: TakeOverTicketSelection) => void;\n}\n\n/** Status row with color swatch — shared by the ticket lifecycle modals\n *  (Take Over / Reopen), which present the same Status + Assigned pair. */\nexport function renderTicketStatusOption(option: AutocompleteOption, isSelected: boolean): ReactNode {\n  const { label, color } = option as TakeOverStatusOption;\n  return (\n    <div className=\"flex w-full min-w-0 items-center justify-between gap-[var(--spacing-system-xs)]\">\n      <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xs)]\">\n        <ColorSwatch color={color} />\n        <div className=\"min-w-0\">\n          <TruncateText className=\"text-inherit\">{label}</TruncateText>\n        </div>\n      </div>\n      {isSelected && <CheckIcon className=\"text-ods-accent\" size={20} />}\n    </div>\n  );\n}\n\n/** Assignee row with round avatar — shared by the ticket lifecycle modals. */\nexport function renderTicketAssigneeOption(option: AutocompleteOption, isSelected: boolean): ReactNode {\n  const { label, imageUrl } = option as TakeOverAssigneeOption;\n  return (\n    <div className=\"flex w-full min-w-0 items-center justify-between gap-[var(--spacing-system-xs)]\">\n      <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xs)]\">\n        <SquareAvatar src={imageUrl} alt={label} fallback={label} size=\"sm\" variant=\"round\" />\n        <div className=\"min-w-0\">\n          <TruncateText className=\"text-inherit\">{label}</TruncateText>\n        </div>\n      </div>\n      {isSelected && <CheckIcon className=\"text-ods-accent\" size={20} />}\n    </div>\n  );\n}\n\n/**\n * Take Over Ticket confirmation dialog (Figma openframe---tickets 8482-112154 /\n * 8482-112169): shown when a technician takes a ticket over from the AI\n * assistant. Presentational — the consumer supplies the option lists, their\n * ordering and default selections, and performs the actual take-over in\n * `onConfirm` (status transition + assignment + switching the dialog to direct\n * mode, or an atomic backend mutation once available).\n *\n * Desktop footer shows only the accent Take Over button (X closes); mobile\n * bottom-sheet adds a Cancel button, per design.\n */\nexport function TakeOverTicketModal({\n  isOpen,\n  onClose,\n  ticketRef,\n  statusOptions,\n  assigneeOptions,\n  assigneesLoading,\n  initialStatusId,\n  initialAssigneeId,\n  isPending = false,\n  onConfirm,\n}: TakeOverTicketModalProps) {\n  const [statusId, setStatusId] = useState<string | null>(initialStatusId ?? null);\n  const [assigneeId, setAssigneeId] = useState<string | null>(initialAssigneeId ?? null);\n\n  // Re-seed the selections every time the modal opens. Adjusted while\n  // rendering, not from an effect: the modal does not unmount when it closes,\n  // so an effect painted the OPENING frame with the previous session's status\n  // and assignee still selected before replacing them. The mount case is\n  // covered by the initialisers above, exactly as the effect's first run was.\n  const [seededWith, setSeededWith] = useState({ isOpen, initialStatusId, initialAssigneeId });\n  if (\n    seededWith.isOpen !== isOpen ||\n    seededWith.initialStatusId !== initialStatusId ||\n    seededWith.initialAssigneeId !== initialAssigneeId\n  ) {\n    setSeededWith({ isOpen, initialStatusId, initialAssigneeId });\n    if (isOpen) {\n      setStatusId(initialStatusId ?? null);\n      setAssigneeId(initialAssigneeId ?? null);\n    }\n  }\n\n  const selectedStatusId = statusId ?? statusOptions[0]?.value ?? null;\n  const selectedStatus = statusOptions.find(o => o.value === selectedStatusId);\n  const selectedAssignee = assigneeOptions.find(o => o.value === assigneeId);\n\n  const handleConfirm = () => {\n    if (!selectedStatusId || !assigneeId || isPending) return;\n    onConfirm({ statusId: selectedStatusId, assigneeId });\n  };\n\n  return (\n    <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className=\"text-left md:max-w-[600px]\">\n      <ModalV2Header>\n        <ModalV2Title>Take Over Ticket</ModalV2Title>\n      </ModalV2Header>\n\n      <p className=\"text-ods-text-primary text-h4\">\n        The ticket <span className=\"text-ods-accent\">{ticketRef}</span> will move to the selected status and be assigned\n        to the technician. A direct chat with the user will start, and the AI assistant will stop working on this\n        ticket.\n      </p>\n\n      <div className=\"flex w-full flex-col gap-[var(--spacing-system-l)] md:flex-row\">\n        <div className=\"min-w-0 flex-1\">\n          <Autocomplete\n            label=\"Status\"\n            options={statusOptions}\n            value={selectedStatusId}\n            onChange={setStatusId}\n            placeholder=\"Select Status\"\n            startAdornment={selectedStatus ? <ColorSwatch color={selectedStatus.color} /> : undefined}\n            renderOption={renderTicketStatusOption}\n          />\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <Autocomplete\n            label=\"Assigned\"\n            options={assigneeOptions}\n            value={assigneeId}\n            onChange={setAssigneeId}\n            placeholder=\"Select Technician\"\n            loading={assigneesLoading}\n            startAdornment={\n              selectedAssignee ? (\n                <SquareAvatar\n                  src={selectedAssignee.imageUrl}\n                  alt={selectedAssignee.label}\n                  fallback={selectedAssignee.label}\n                  size=\"sm\"\n                  variant=\"round\"\n                />\n              ) : undefined\n            }\n            renderOption={renderTicketAssigneeOption}\n          />\n        </div>\n      </div>\n\n      <ModalV2Footer>\n        <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={isPending} className=\"flex-1 md:hidden\">\n          Cancel\n        </Button>\n        <div className=\"hidden flex-1 md:block\" />\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          onClick={handleConfirm}\n          loading={isPending}\n          disabled={!selectedStatusId || !assigneeId}\n          className=\"flex-1\"\n        >\n          Take Over\n        </Button>\n      </ModalV2Footer>\n    </ModalV2>\n  );\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { Autocomplete } from './autocomplete';\nimport { Button } from './button';\nimport { ColorSwatch } from './color-swatch';\nimport { ModalV2, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2';\nimport { SquareAvatar } from './square-avatar';\nimport {\n  renderTicketAssigneeOption,\n  renderTicketStatusOption,\n  type TakeOverAssigneeOption,\n  type TakeOverStatusOption,\n} from './take-over-ticket-modal';\nimport { Textarea } from './textarea';\n\n/** Backend trims and caps the reason at 1000 chars; cap it client-side too. */\nconst REASON_MAX_LENGTH = 1000;\n\nexport interface ReopenTicketSelection {\n  statusId: string;\n  assigneeId: string;\n  /** Trimmed; `null` when the field was left blank. */\n  reason: string | null;\n}\n\nexport interface ReopenTicketModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  /** Highlighted ticket reference in the description, e.g. \"1003: Email client synchronization issues\". */\n  ticketRef: string;\n  /** Allowed target statuses, in display order. Per design, the consumer\n   *  pre-selects Tech Required by default (annotation on the mock: \"tech\n   *  required by default, but may be changed to custom status\"). */\n  statusOptions: TakeOverStatusOption[];\n  /** Assignable users, in display order (consumer decides ordering, e.g. signed-in user first). */\n  assigneeOptions: TakeOverAssigneeOption[];\n  assigneesLoading?: boolean;\n  /** Pre-selected status; falls back to the first status option. */\n  initialStatusId?: string | null;\n  /** Pre-selected assignee (the design default restores the ticket's previous\n   *  assignee). No fallback — with none selected the CTA stays locked, same\n   *  as Take Over: both Status and Assigned are required to confirm. */\n  initialAssigneeId?: string | null;\n  /** Confirm in-flight: buttons lock and the modal refuses to close. */\n  isPending?: boolean;\n  onConfirm: (selection: ReopenTicketSelection) => void;\n}\n\n/**\n * Reopen Ticket dialog (Figma openframe---tickets 8456-17581): shown when a\n * technician reopens a Resolved/Archived ticket. Same Status + Assigned pair\n * as {@link TakeOverTicketModal} — both REQUIRED before the CTA unlocks —\n * plus an optional free-text reason.\n * Presentational — the consumer supplies the option lists, their ordering and\n * default selections, and performs the actual reopen in `onConfirm`.\n *\n * Desktop footer shows only the accent Reopen button (X closes); mobile\n * bottom-sheet adds a Cancel button, matching Take Over.\n */\nexport function ReopenTicketModal({\n  isOpen,\n  onClose,\n  ticketRef,\n  statusOptions,\n  assigneeOptions,\n  assigneesLoading,\n  initialStatusId,\n  initialAssigneeId,\n  isPending = false,\n  onConfirm,\n}: ReopenTicketModalProps) {\n  const [statusId, setStatusId] = useState<string | null>(initialStatusId ?? null);\n  const [assigneeId, setAssigneeId] = useState<string | null>(initialAssigneeId ?? null);\n  const [reason, setReason] = useState('');\n\n  // Re-seed the selections every time the modal opens. Adjusted while\n  // rendering, not from an effect: the modal does not unmount when it closes,\n  // so an effect painted the OPENING frame with the previous session's status,\n  // assignee and reason still filled in before replacing them. The mount case\n  // is covered by the initialisers above, exactly as the effect's first run was.\n  const [seededWith, setSeededWith] = useState({ isOpen, initialStatusId, initialAssigneeId });\n  if (\n    seededWith.isOpen !== isOpen ||\n    seededWith.initialStatusId !== initialStatusId ||\n    seededWith.initialAssigneeId !== initialAssigneeId\n  ) {\n    setSeededWith({ isOpen, initialStatusId, initialAssigneeId });\n    if (isOpen) {\n      setStatusId(initialStatusId ?? null);\n      setAssigneeId(initialAssigneeId ?? null);\n      setReason('');\n    }\n  }\n\n  const selectedStatusId = statusId ?? statusOptions[0]?.value ?? null;\n  const selectedStatus = statusOptions.find(o => o.value === selectedStatusId);\n  const selectedAssignee = assigneeOptions.find(o => o.value === assigneeId);\n\n  const handleConfirm = () => {\n    if (!selectedStatusId || !assigneeId || isPending) return;\n    const trimmedReason = reason.trim();\n    onConfirm({\n      statusId: selectedStatusId,\n      assigneeId,\n      reason: trimmedReason ? trimmedReason.slice(0, REASON_MAX_LENGTH) : null,\n    });\n  };\n\n  return (\n    <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className=\"text-left md:max-w-[600px]\">\n      <ModalV2Header>\n        <ModalV2Title>Reopen Ticket</ModalV2Title>\n      </ModalV2Header>\n\n      <p className=\"text-ods-text-primary text-h4\">\n        The ticket <span className=\"text-ods-accent\">{ticketRef}</span> will return to the selected status and appear on\n        the board. Assigned admins and the user will be notified.\n      </p>\n\n      <div className=\"flex w-full flex-col gap-[var(--spacing-system-l)] md:flex-row\">\n        <div className=\"min-w-0 flex-1\">\n          <Autocomplete\n            label=\"Status\"\n            options={statusOptions}\n            value={selectedStatusId}\n            onChange={setStatusId}\n            placeholder=\"Select Status\"\n            startAdornment={selectedStatus ? <ColorSwatch color={selectedStatus.color} /> : undefined}\n            renderOption={renderTicketStatusOption}\n          />\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <Autocomplete\n            label=\"Assigned\"\n            options={assigneeOptions}\n            value={assigneeId}\n            onChange={setAssigneeId}\n            placeholder=\"Select Technician\"\n            loading={assigneesLoading}\n            startAdornment={\n              selectedAssignee ? (\n                <SquareAvatar\n                  src={selectedAssignee.imageUrl}\n                  alt={selectedAssignee.label}\n                  fallback={selectedAssignee.label}\n                  size=\"sm\"\n                  variant=\"round\"\n                />\n              ) : undefined\n            }\n            renderOption={renderTicketAssigneeOption}\n          />\n        </div>\n      </div>\n\n      <div className=\"w-full\">\n        <Textarea\n          label=\"Reason\"\n          value={reason}\n          onChange={event => setReason(event.target.value)}\n          placeholder=\"Describe what's still not working (optional)\"\n          maxLength={REASON_MAX_LENGTH}\n          disabled={isPending}\n        />\n      </div>\n\n      <ModalV2Footer>\n        <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={isPending} className=\"flex-1 md:hidden\">\n          Cancel\n        </Button>\n        <div className=\"hidden flex-1 md:block\" />\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          onClick={handleConfirm}\n          loading={isPending}\n          disabled={!selectedStatusId || !assigneeId}\n          className=\"flex-1\"\n        >\n          Reopen Ticket\n        </Button>\n      </ModalV2Footer>\n    </ModalV2>\n  );\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { Button } from './button';\nimport { CheckboxBlock } from './checkbox-block';\nimport { ModalV2, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2';\nimport { Textarea } from './textarea';\n\n/** Backend trims and caps the reason at 1000 chars; cap it client-side too. */\nconst REASON_MAX_LENGTH = 1000;\n\nexport interface ReopenTicketRequestSelection {\n  /** Trimmed; `null` when the field was left blank. */\n  reason: string | null;\n  /** Present ONLY when the handoff option was shown — the tech-closed\n   *  variant omits the field entirely (mirrors `TicketReopenInput`). */\n  handoffToTechnician?: boolean;\n}\n\nexport interface ReopenTicketRequestModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  /**\n   * Fae resolved the ticket (`Ticket.resolvedBy === END_USER`): reopening\n   * resumes the AI conversation, and the checkbox lets the user route to a\n   * technician instead. When a technician closed the ticket the option is\n   * hidden — reopening goes to a technician regardless, and the selection\n   * omits `handoffToTechnician`.\n   */\n  showHandoffOption?: boolean;\n  /** Confirm in-flight: buttons lock and the modal refuses to close. */\n  isPending?: boolean;\n  onConfirm: (selection: ReopenTicketRequestSelection) => void;\n}\n\n/**\n * Client-side Reopen Ticket dialog (Figma openframe---fae-chat 346-10518):\n * the end user reopening their own resolved ticket from the closed chat.\n * Unlike the admin `ReopenTicketModal` there is no status/assignee pair —\n * the backend picks the target via `requestTicketReopen`; the user only\n * supplies an optional reason and (for Fae-closed tickets) the handoff\n * choice. Presentational — the consumer performs the reopen in `onConfirm`.\n */\nexport function ReopenTicketRequestModal({\n  isOpen,\n  onClose,\n  showHandoffOption = false,\n  isPending = false,\n  onConfirm,\n}: ReopenTicketRequestModalProps) {\n  const [reason, setReason] = useState('');\n  const [handoff, setHandoff] = useState(false);\n\n  // Re-seed the inputs every time the modal opens. Adjusted while rendering,\n  // not from an effect: the modal does not unmount when it closes, so an effect\n  // painted the OPENING frame with the previous session's reason still typed in\n  // before clearing it.\n  const [wasOpen, setWasOpen] = useState(isOpen);\n  if (wasOpen !== isOpen) {\n    setWasOpen(isOpen);\n    if (isOpen) {\n      setReason('');\n      setHandoff(false);\n    }\n  }\n\n  const handleConfirm = () => {\n    if (isPending) return;\n    const trimmedReason = reason.trim();\n    onConfirm({\n      reason: trimmedReason ? trimmedReason.slice(0, REASON_MAX_LENGTH) : null,\n      ...(showHandoffOption ? { handoffToTechnician: handoff } : {}),\n    });\n  };\n\n  return (\n    <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className=\"text-left md:max-w-[600px]\">\n      <ModalV2Header>\n        <ModalV2Title>Reopen Ticket</ModalV2Title>\n      </ModalV2Header>\n\n      <p className=\"text-ods-text-primary text-h4\">\n        This ticket will be reopened and the AI assistant will continue helping you.\n      </p>\n\n      <div className=\"w-full\">\n        <Textarea\n          label=\"Reason\"\n          value={reason}\n          onChange={event => setReason(event.target.value)}\n          placeholder=\"Describe what's still not working (optional)\"\n          maxLength={REASON_MAX_LENGTH}\n          disabled={isPending}\n        />\n      </div>\n\n      {showHandoffOption && (\n        <CheckboxBlock\n          id=\"reopen-handoff-to-technician\"\n          checked={handoff}\n          onCheckedChange={setHandoff}\n          label=\"Hand off to a Technician\"\n          description=\"A technician will review the ticket and reply when available.\"\n          disabled={isPending}\n        />\n      )}\n\n      <ModalV2Footer>\n        <Button type=\"button\" variant=\"outline\" onClick={onClose} disabled={isPending} className=\"flex-1\">\n          Cancel\n        </Button>\n        <Button type=\"button\" variant=\"accent\" onClick={handleConfirm} loading={isPending} className=\"flex-1\">\n          Reopen Ticket\n        </Button>\n      </ModalV2Footer>\n    </ModalV2>\n  );\n}\n","'use client';\n\n/**\n * Shared filter section wrapper with responsive layout.\n *\n * Provides the consistent card container for any filter row (status,\n * platform, section, etc.) Single row on desktop with flex-wrap,\n * stacks naturally on mobile.\n *\n * Lifted from hub `components/admin/shared/filter-section.tsx` so\n * lib catalog views can render their own filter pills without an\n * injected slot.\n */\n\nimport { Filter } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { Button } from './button';\n\nexport interface FilterPillRowOption {\n  value: string;\n  label: string;\n}\n\nexport interface FilterPillRowProps {\n  /** Label shown next to the filter icon, e.g. \"Section\", \"Platform\". */\n  label: string;\n  /** The currently selected filter value. */\n  selectedValue: string;\n  /** Callback when a filter option is selected. */\n  onValueChange: (value: string) => void;\n  /** Available filter options. */\n  options: FilterPillRowOption[];\n  /** Optional count label, e.g. \"Showing 1-10 of 42 items\". */\n  countLabel?: string;\n  /** Optional children to render instead of options (for custom filter content). */\n  children?: ReactNode;\n}\n\nexport function FilterPillRow({\n  label,\n  selectedValue,\n  onValueChange,\n  options,\n  countLabel,\n  children,\n}: FilterPillRowProps) {\n  return (\n    <div className=\"flex flex-wrap items-center gap-3 rounded-lg border border-ods-border bg-ods-card p-4\">\n      <div className=\"flex items-center gap-2\">\n        <Filter className=\"h-4 w-4 text-ods-accent\" />\n        <span className=\"text-ods-text-secondary text-h5\">{label}</span>\n      </div>\n      {children ||\n        options.map(opt => (\n          <Button\n            key={opt.value}\n            type=\"button\"\n            variant={selectedValue === opt.value ? 'accent' : 'outline'}\n            size=\"small-legacy\"\n            onClick={() => onValueChange(opt.value)}\n            className=\"text-h3\"\n          >\n            {opt.label}\n          </Button>\n        ))}\n      {countLabel && <div className=\"ml-auto shrink-0 text-ods-text-secondary text-h6\">{countLabel}</div>}\n    </div>\n  );\n}\n","'use client';\n\nimport type { CSSProperties, ReactNode } from 'react';\nimport { useNearViewport, NEAR_VIEWPORT_ROOT_MARGIN } from '../../hooks/use-near-viewport';\n\nexport interface LazyMountProps {\n  /** Mounted the first time the box comes within `rootMargin` of the viewport; never unmounted after. */\n  children: ReactNode;\n  /** What fills the box until then. The box itself must already have its final size (aspect-ratio / min-height). */\n  placeholder?: ReactNode;\n  className?: string;\n  style?: CSSProperties;\n  /** Lookahead — defaults to the shared `NEAR_VIEWPORT_ROOT_MARGIN`. */\n  rootMargin?: string;\n}\n\n/**\n * Sized box whose CONTENT mounts only near the viewport (shared fire-once\n * IntersectionObserver via `useNearViewport`). The box owns the layout —\n * pass the aspect-ratio / min-height on it — so lazy mounting never shifts\n * anything: the content appears inside a space that was already reserved.\n * Grids of players or large images render their chrome immediately and pay\n * for each preview only when it is about to be seen.\n */\nexport function LazyMount({\n  children,\n  placeholder = null,\n  className,\n  style,\n  rootMargin = NEAR_VIEWPORT_ROOT_MARGIN,\n}: LazyMountProps) {\n  const { ref, isNear } = useNearViewport<HTMLDivElement>(rootMargin);\n  return (\n    <div ref={ref} className={className} style={style}>\n      {isNear ? children : placeholder}\n    </div>\n  );\n}\n","import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\n/**\n * The inset icon tile — the bordered `ods-bg` square (or panel) an icon sits\n * in. The one home for the chrome going forward (older inline tiles migrate as\n * they are touched); the size is the caller's (`size-16` in a card row,\n * `h-40 w-full` as a step panel).\n */\nexport function IconTile({ className, children }: { className?: string; children: ReactNode }) {\n  return (\n    <div\n      className={cn(\n        'flex shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg',\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\n\nconst TONE_CLASS = {\n  /** Consent rows: the accent, as every form's policy links draw it. */\n  accent: 'text-ods-accent underline underline-offset-2 hover:text-ods-accent/80',\n  /** Quiet copy (the sign-up agreement line): secondary text, plain underline. */\n  secondary: 'text-ods-text-secondary underline',\n} as const;\n\n/**\n * A link to a policy page inside consent copy (Terms, Privacy) — opens in a\n * new tab and does NOT toggle the checkbox whose label it sits in.\n */\nexport function PolicyLink({\n  href,\n  tone = 'accent',\n  children,\n}: {\n  href: string;\n  tone?: keyof typeof TONE_CLASS;\n  children: ReactNode;\n}) {\n  return (\n    <a\n      href={href}\n      className={cn(TONE_CLASS[tone])}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      onClick={e => e.stopPropagation()}\n    >\n      {children}\n    </a>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent } from 'react';\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error';\nimport { Input } from '../../ui/input';\nimport { PasswordInput } from '../../ui/password-input';\n\nexport interface AccountDetailsFieldsProps {\n  firstName: string;\n  lastName: string;\n  password: string;\n  confirmPassword: string;\n  onFirstNameChange: (value: string) => void;\n  onLastNameChange: (value: string) => void;\n  onPasswordChange: (value: string) => void;\n  onConfirmPasswordChange: (value: string) => void;\n  errors?: {\n    firstName?: string;\n    lastName?: string;\n    password?: string;\n    confirmPassword?: string;\n  };\n  disabled?: boolean;\n  onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;\n  /**\n   * `paired` puts the two names on one row and the two passwords on another; `stacked` gives each\n   * field its own row. Paired is unconditional, with no breakpoint — the approved design pairs\n   * them at ~514px and Tailwind's `sm` is 640px, which would stack them on every phone.\n   */\n  layout?: 'stacked' | 'paired';\n}\n\n/**\n * The name-and-password block every account-creating form collects.\n *\n * Shared so the four fields cannot drift apart across the forms that render them — they already\n * had, with one copy wiring `useDeferredError` for blur and another not, so the same field\n * surfaced its error at a different moment depending on which screen you reached it from.\n */\nexport function AccountDetailsFields({\n  firstName,\n  lastName,\n  password,\n  confirmPassword,\n  onFirstNameChange,\n  onLastNameChange,\n  onPasswordChange,\n  onConfirmPasswordChange,\n  errors,\n  disabled = false,\n  onKeyDown,\n  layout = 'stacked',\n}: AccountDetailsFieldsProps) {\n  const firstNameErr = useDeferredError(errors?.firstName, firstName);\n  const lastNameErr = useDeferredError(errors?.lastName, lastName);\n  const passwordErr = useDeferredError(errors?.password, password);\n  const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword);\n\n  const names = [\n    <Input\n      key=\"firstName\"\n      label=\"First Name\"\n      placeholder=\"Enter First Name\"\n      value={firstName}\n      error={firstNameErr.error}\n      disabled={disabled}\n      onBlur={firstNameErr.onBlur}\n      onChange={event => onFirstNameChange(event.target.value)}\n      onKeyDown={onKeyDown}\n    />,\n    <Input\n      key=\"lastName\"\n      label=\"Last Name\"\n      placeholder=\"Enter Last Name\"\n      value={lastName}\n      error={lastNameErr.error}\n      disabled={disabled}\n      onBlur={lastNameErr.onBlur}\n      onChange={event => onLastNameChange(event.target.value)}\n      onKeyDown={onKeyDown}\n    />,\n  ];\n\n  const passwords = [\n    <PasswordInput\n      key=\"password\"\n      label=\"Password\"\n      placeholder=\"Enter Password\"\n      value={password}\n      error={passwordErr.error}\n      disabled={disabled}\n      onBlur={passwordErr.onBlur}\n      onChange={event => onPasswordChange(event.target.value)}\n      onKeyDown={onKeyDown}\n    />,\n    <PasswordInput\n      key=\"confirmPassword\"\n      label=\"Confirm Password\"\n      placeholder=\"Confirm Password\"\n      value={confirmPassword}\n      error={confirmErr.error}\n      disabled={disabled}\n      onBlur={confirmErr.onBlur}\n      onChange={event => onConfirmPasswordChange(event.target.value)}\n      onKeyDown={onKeyDown}\n    />,\n  ];\n\n  if (layout === 'stacked') {\n    return (\n      <>\n        {names}\n        {passwords}\n      </>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"grid grid-cols-2 gap-[var(--spacing-system-l)]\">{names}</div>\n      <div className=\"grid grid-cols-2 gap-[var(--spacing-system-l)]\">{passwords}</div>\n    </>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { OpenFrameLogo } from '../../icons';\nimport { FileOffIcon } from '../../icons-v2-generated/documents/file-off-icon';\nimport { CreditCardXmarkIcon } from '../../icons-v2-generated/finance/credit-card-xmark-icon';\nimport { FlaskVialIcon } from '../../icons-v2-generated/school/flask-vial-icon';\nimport { XmarkAltIcon } from '../../icons-v2-generated/signs-and-symbols/xmark-alt-icon';\nimport { Button } from '../../ui/button';\n\nexport interface AuthBenefit {\n  icon: ReactNode;\n  label: string;\n}\n\nexport interface AuthBenefitsPanelProps {\n  title?: string;\n  description?: string;\n  benefits?: AuthBenefit[];\n  learnMoreLabel?: string;\n  learnMoreUrl?: string;\n  className?: string;\n}\n\nconst DEFAULT_TITLE = 'The All-in-One Open Platform for MSPs';\n\nconst DEFAULT_DESCRIPTION =\n  'All your core ops in one place - built for MSPs who are done duct-taping tools together. Unified stack, AI-ready, no vendor tax. Just solid software that lets you run lean and fast.';\n\nconst DEFAULT_BENEFITS: AuthBenefit[] = [\n  { icon: <CreditCardXmarkIcon />, label: 'No card required' },\n  { icon: <XmarkAltIcon />, label: 'Cancel Anytime' },\n  { icon: <FlaskVialIcon />, label: '14 day free trial' },\n  { icon: <FileOffIcon />, label: 'No Contract' },\n];\n\n/**\n * Marketing panel shown alongside the auth forms — title, blurb, benefit chips\n * and a \"learn more\" link. Presentational; content is overridable via props.\n */\nexport function AuthBenefitsPanel({\n  title = DEFAULT_TITLE,\n  description = DEFAULT_DESCRIPTION,\n  benefits = DEFAULT_BENEFITS,\n  learnMoreLabel = 'Learn More About OpenFrame',\n  learnMoreUrl = 'https://www.flamingo.run/',\n  className,\n}: AuthBenefitsPanelProps) {\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-xl)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      <h2 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h2>\n      <p className=\"text-ods-text-primary text-h4\">{description}</p>\n\n      <div className=\"flex flex-wrap content-center items-center gap-[var(--spacing-system-m)]\">\n        {benefits.map(benefit => (\n          <div key={benefit.label} className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n            <span className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary [&_svg]:size-4\">\n              {benefit.icon}\n            </span>\n            <span className=\"whitespace-nowrap text-ods-text-primary text-h4\">{benefit.label}</span>\n          </div>\n        ))}\n      </div>\n\n      <Button\n        variant=\"outline\"\n        href={learnMoreUrl}\n        openInNewTab\n        leftIcon={\n          <OpenFrameLogo\n            className=\"h-6 w-6\"\n            lowerPathColor=\"var(--color-accent-primary)\"\n            upperPathColor=\"var(--color-text-primary)\"\n          />\n        }\n        className=\"w-full md:w-auto md:self-start\"\n      >\n        {learnMoreLabel}\n      </Button>\n    </div>\n  );\n}\n","'use client';\n\nimport { OpenFrameLogo } from '../../icons';\nimport { FlamingoLogoIcon } from '../../icons-v2-generated/logos/flamingo-logo-icon';\n\n/** OpenFrame logo + wordmark, shown at the top of the auth screens. */\nexport function OpenFrameWordmark() {\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n      <OpenFrameLogo\n        className=\"h-10 w-10\"\n        lowerPathColor=\"var(--color-accent-primary)\"\n        upperPathColor=\"var(--color-text-primary)\"\n      />\n      <span className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">OpenFrame</span>\n    </div>\n  );\n}\n\nexport interface PoweredByFlamingoProps {\n  /** Keep the small 56×16 logo at every breakpoint (default grows to 83×24 from tablet up). */\n  compact?: boolean;\n}\n\n/** \"Powered by Flamingo\" footer mark. */\nexport function PoweredByFlamingo({ compact = false }: PoweredByFlamingoProps) {\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-xs)] text-ods-text-secondary\">\n      <span className=\"text-h6\">Powered by</span>\n      {/* logo is 416×120 (≈3.47:1) — set explicit w/h so it isn't squashed to a square. */}\n      <FlamingoLogoIcon className={compact ? 'h-4 w-14' : 'h-4 w-14 md:h-6 md:w-[83px]'} />\n    </div>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { AuthBenefitsPanel } from './auth-benefits-panel';\nimport { OpenFrameWordmark, PoweredByFlamingo } from './auth-branding';\n\nexport interface AuthShellProps {\n  /** Tab selector (Sign Up / Login) rendered above the form. */\n  tabs?: ReactNode;\n  /** Main form content (Create Organization, Login, …). */\n  children: ReactNode;\n  /** Marketing panel. Defaults to <AuthBenefitsPanel />. */\n  benefits?: ReactNode;\n  /** Pinned to the bottom-left of the form column on desktop only (e.g. \"Back to Login\"). */\n  footer?: ReactNode;\n  className?: string;\n}\n\n/**\n * Responsive layout shell for the auth pages. Desktop shows a two-column split\n * (form left, marketing right); tablet and mobile stack into a single centered\n * column (tabs → form → benefits → powered-by). Content is top-aligned. The\n * wordmark appears only in the desktop marketing column — narrow screens go\n * without it (in the native mobile shell a top logo sat under the status bar\n * and was clipped inconsistently across devices).\n *\n * `of-auth-shell` on the root is a stable hook for shell/consumer CSS (the\n * native mobile shell pads it by the top safe-area inset).\n */\nexport function AuthShell({ tabs, children, benefits, footer, className }: AuthShellProps) {\n  const benefitsNode = benefits ?? <AuthBenefitsPanel />;\n\n  return (\n    <div\n      className={cn('of-auth-shell min-h-screen w-full bg-ods-bg lg:flex lg:h-screen lg:overflow-hidden', className)}\n    >\n      {/* Main column — form */}\n      <div className=\"flex min-h-screen w-full flex-col items-center p-[var(--spacing-system-l)] lg:h-full lg:min-h-0 lg:w-1/2 lg:justify-start lg:overflow-y-auto lg:px-[var(--spacing-system-xl)] lg:py-[var(--spacing-system-xxl)]\">\n        <div\n          className={cn(\n            'flex w-full max-w-[600px] flex-col items-center gap-[var(--spacing-system-l)]',\n            footer && 'lg:min-h-full',\n          )}\n        >\n          {/* Full width on mobile; fixed 320px from tablet up (matches desktop) */}\n          {tabs && <div className=\"w-full md:max-w-[320px]\">{tabs}</div>}\n          <div className=\"w-full\">{children}</div>\n\n          {/* Benefits + powered-by — narrow screens only */}\n          <div className=\"flex w-full flex-col items-center gap-[var(--spacing-system-l)] lg:hidden\">\n            {benefitsNode}\n            <PoweredByFlamingo />\n          </div>\n\n          {/* Footer — pinned to the bottom-left of the column on desktop only */}\n          {footer && <div className=\"hidden w-full self-start lg:mt-auto lg:block\">{footer}</div>}\n        </div>\n      </div>\n\n      {/* Marketing column — desktop only */}\n      <div className=\"hidden border-l border-ods-border bg-ods-card lg:flex lg:h-full lg:w-1/2 lg:flex-col lg:items-center lg:justify-start lg:gap-[var(--spacing-system-xxl)] lg:overflow-y-auto lg:px-[var(--spacing-system-xl)] lg:py-[var(--spacing-system-xxl)]\">\n        <OpenFrameWordmark />\n        <div className=\"w-full max-w-lg\">{benefitsNode}</div>\n        <PoweredByFlamingo />\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\n\nexport interface BackToLoginLinkProps {\n  onClick: () => void;\n  label?: string;\n  className?: string;\n}\n\n/** Underlined \"Back to Login\" text link used across the auth screens. */\nexport function BackToLoginLink({ onClick, label = 'Back to Login', className }: BackToLoginLinkProps) {\n  return (\n    <button type=\"button\" onClick={onClick} className={cn('text-ods-text-secondary underline text-h4', className)}>\n      {label}\n    </button>\n  );\n}\n","'use client';\n\nexport interface LabeledDividerProps {\n  label: string;\n}\n\n/** A horizontal rule with a centred label. */\nexport function LabeledDivider({ label }: LabeledDividerProps) {\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-s)]\">\n      <div className=\"h-px flex-1 bg-ods-border\" />\n      <span className=\"text-ods-text-secondary text-h6\">{label}</span>\n      <div className=\"h-px flex-1 bg-ods-border\" />\n    </div>\n  );\n}\n","'use client';\n\nimport type { ComponentType } from 'react';\nimport { OpenFrameLogo } from '../../icons';\nimport { AppleLogoIcon } from '../../icons-v2-generated/brand-logos/apple-logo-icon';\nimport { GoogleLogoIcon } from '../../icons-v2-generated/brand-logos/google-logo-icon';\nimport { MicrosoftLogoIcon } from '../../icons-v2-generated/brand-logos/microsoft-logo-icon';\nimport { Button } from '../../ui/button';\n\n/** SSO providers offered on the auth forms. */\nexport type AuthSsoProvider = 'openframe' | 'google' | 'microsoft' | 'apple';\n\nexport const PROVIDER_META: Record<AuthSsoProvider, { name: string; Icon: ComponentType<{ className?: string }> }> = {\n  openframe: {\n    name: 'OpenFrame',\n    Icon: ({ className }) => (\n      <OpenFrameLogo\n        className={className}\n        lowerPathColor=\"var(--color-accent-primary)\"\n        upperPathColor=\"var(--color-text-primary)\"\n      />\n    ),\n  },\n  google: { name: 'Google', Icon: GoogleLogoIcon },\n  microsoft: { name: 'Microsoft', Icon: MicrosoftLogoIcon },\n  apple: { name: 'Apple', Icon: AppleLogoIcon },\n};\n\nexport interface SsoProviderButtonsProps {\n  providers: AuthSsoProvider[];\n  onSsoClick?: (provider: AuthSsoProvider) => void;\n  /** Verb prefix for provider buttons, e.g. \"Continue with\". */\n  actionLabel?: string;\n  disabled?: boolean;\n  /**\n   * When set, only these providers are clickable and the rest render disabled —\n   * e.g. unlock just the providers a discovered tenant actually offers.\n   * `disabled` still wins over this list.\n   */\n  enabledProviders?: AuthSsoProvider[];\n}\n\n/** Stacked full-width SSO provider buttons shared by the auth forms. */\nexport function SsoProviderButtons({\n  providers,\n  onSsoClick,\n  actionLabel = 'Continue with',\n  disabled = false,\n  enabledProviders,\n}: SsoProviderButtonsProps) {\n  return (\n    <div className=\"flex flex-col gap-[var(--spacing-system-l)]\">\n      {providers.map(provider => {\n        const meta = PROVIDER_META[provider];\n        const Icon = meta.Icon;\n        return (\n          <Button\n            key={provider}\n            type=\"button\"\n            variant=\"outline\"\n            fullWidth\n            disabled={disabled || (enabledProviders ? !enabledProviders.includes(provider) : false)}\n            // Disabled buttons recede to the page background (like a disabled input), per design\n            className=\"disabled:bg-ods-bg\"\n            leftIcon={<Icon className=\"h-5 w-5\" />}\n            onClick={() => onSsoClick?.(provider)}\n          >\n            {`${actionLabel} ${meta.name}`}\n          </Button>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent, ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Button } from '../../ui/button';\nimport { AccountDetailsFields } from './account-details-fields';\nimport { LabeledDivider } from './labeled-divider';\nimport type { AuthSsoProvider } from './sso-providers';\nimport { SsoProviderButtons } from './sso-providers';\n\nexport interface CompleteAccountFormProps {\n  /** Controlled field values */\n  firstName: string;\n  lastName: string;\n  password: string;\n  confirmPassword: string;\n  onFirstNameChange: (value: string) => void;\n  onLastNameChange: (value: string) => void;\n  onPasswordChange: (value: string) => void;\n  onConfirmPasswordChange: (value: string) => void;\n  /** Primary submit (\"Start Free Trial\") */\n  onSubmit: () => void;\n  /** Secondary action rendered left of the submit (e.g. \"Back to Organization\"). */\n  onBack?: () => void;\n  /** SSO alternatives offered above the fields (\"Continue with …\"). */\n  ssoProviders?: AuthSsoProvider[];\n  onSsoClick?: (provider: AuthSsoProvider) => void;\n  title?: string;\n  subtitle?: string;\n  dividerLabel?: string;\n  submitLabel?: string;\n  backLabel?: string;\n  ssoActionLabel?: string;\n  submitDisabled?: boolean;\n  loading?: boolean;\n  disabled?: boolean;\n  errors?: {\n    firstName?: string;\n    lastName?: string;\n    password?: string;\n    confirmPassword?: string;\n  };\n  className?: string;\n  /** Extra consumer-provided fields rendered below the built-in fields, above the actions. */\n  children?: ReactNode;\n}\n\n/**\n * Account details form shared by the Sign Up (\"Complete your Account\") and\n * Accept Invitation screens. Presentational + controlled — SSO shortcuts on\n * top, then name + password fields (\"or create account\").\n */\nexport function CompleteAccountForm({\n  firstName,\n  lastName,\n  password,\n  confirmPassword,\n  onFirstNameChange,\n  onLastNameChange,\n  onPasswordChange,\n  onConfirmPasswordChange,\n  onSubmit,\n  onBack,\n  ssoProviders,\n  onSsoClick,\n  title = 'Complete your Account',\n  subtitle = 'Fill in the details below to get started',\n  dividerLabel = 'or create account',\n  submitLabel = 'Start Free Trial',\n  backLabel = 'Back to Organization',\n  ssoActionLabel = 'Continue with',\n  submitDisabled = false,\n  loading = false,\n  disabled = false,\n  errors,\n  className,\n  children,\n}: CompleteAccountFormProps) {\n  const fieldsDisabled = disabled || loading;\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {\n      onSubmit();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      {/* SSO shortcuts + divider */}\n      {ssoProviders && ssoProviders.length > 0 && (\n        <>\n          <SsoProviderButtons\n            providers={ssoProviders}\n            onSsoClick={onSsoClick}\n            actionLabel={ssoActionLabel}\n            disabled={fieldsDisabled}\n          />\n          <LabeledDivider label={dividerLabel} />\n        </>\n      )}\n\n      <AccountDetailsFields\n        firstName={firstName}\n        lastName={lastName}\n        password={password}\n        confirmPassword={confirmPassword}\n        onFirstNameChange={onFirstNameChange}\n        onLastNameChange={onLastNameChange}\n        onPasswordChange={onPasswordChange}\n        onConfirmPasswordChange={onConfirmPasswordChange}\n        errors={errors}\n        disabled={fieldsDisabled}\n        onKeyDown={handleKeyDown}\n      />\n\n      {children}\n\n      {/* Actions — optional back + submit */}\n      <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n        {onBack ? (\n          <Button\n            type=\"button\"\n            variant=\"transparent\"\n            fullWidth\n            className=\"flex-1\"\n            disabled={fieldsDisabled}\n            onClick={onBack}\n          >\n            {backLabel}\n          </Button>\n        ) : (\n          // Spacer keeps the button on the right half, matching the design\n          <div className=\"flex-1\" />\n        )}\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          fullWidth\n          className=\"flex-1\"\n          loading={loading}\n          disabled={disabled || submitDisabled}\n          onClick={onSubmit}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { PolicyLink } from '../../ui/policy-link';\n\nexport interface TermsAgreementLabelProps {\n  termsUrl?: string;\n  privacyPolicyUrl?: string;\n}\n\n/** \"Agree to Terms & Privacy Policy by signing up.\" — shared checkbox label. */\nexport function TermsAgreementLabel({ termsUrl = '#', privacyPolicyUrl = '#' }: TermsAgreementLabelProps) {\n  return (\n    <span className=\"text-ods-text-primary text-h4\">\n      {'Agree to '}\n      <PolicyLink href={termsUrl} tone=\"secondary\">\n        Terms\n      </PolicyLink>\n      {' & '}\n      <PolicyLink href={privacyPolicyUrl} tone=\"secondary\">\n        Privacy Policy\n      </PolicyLink>\n      {' by signing up.'}\n    </span>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent, ReactNode } from 'react';\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error';\nimport { cn } from '../../../utils/cn';\nimport { Button } from '../../ui/button';\nimport { CheckboxBlock } from '../../ui/checkbox-block';\nimport { Input } from '../../ui/input';\nimport { AccountDetailsFields } from './account-details-fields';\nimport { LabeledDivider } from './labeled-divider';\nimport type { AuthSsoProvider } from './sso-providers';\nimport { SsoProviderButtons } from './sso-providers';\nimport { TermsAgreementLabel } from './terms-agreement-label';\n\nexport interface CreateOrganizationFormProps {\n  /**\n   * Controlled field values. Omit `email` entirely for flows that neither collect nor show an\n   * address — the native Apple signup, where the provider asserted it and may only have handed the\n   * app a relay alias the user has never seen. Pass it with `emailReadOnly` to show it uneditable.\n   */\n  email?: string;\n  organizationName: string;\n  domain: string;\n  agreedToTerms: boolean;\n  // Change handlers\n  /** Omit together with `email` when the form collects no address. */\n  onEmailChange?: (value: string) => void;\n  onOrganizationNameChange: (value: string) => void;\n  onDomainChange: (value: string) => void;\n  onAgreedToTermsChange: (checked: boolean) => void;\n  /** Primary submit (\"Continue\") */\n  onSubmit: () => void;\n  /**\n   * Personal details and credentials, collected on the same screen as the organization. Supply the\n   * four handlers to render them; omit them and the form stays organization-only (the SSO paths,\n   * where the provider is the credential and there is no password to set).\n   */\n  firstName?: string;\n  lastName?: string;\n  password?: string;\n  confirmPassword?: string;\n  onFirstNameChange?: (value: string) => void;\n  onLastNameChange?: (value: string) => void;\n  onPasswordChange?: (value: string) => void;\n  onConfirmPasswordChange?: (value: string) => void;\n  /**\n   * Renders the email as a read-only field instead of an input, for flows where the address was\n   * fixed by an earlier step and this form cannot change it — a signup that collected it on a\n   * previous step, or an SSO login where the provider asserted it and the server reads it from the\n   * session. `onEmailChange` is not called in this mode.\n   */\n  emailReadOnly?: boolean;\n  /** Label above the read-only email, e.g. \"Signed in with Google\". Ignored unless `emailReadOnly`. */\n  emailReadOnlyLabel?: string;\n  /** Overrides the default heading. */\n  title?: string;\n  /** Overrides the default sub-heading. */\n  subtitle?: ReactNode;\n  /** Suffix rendered inside the domain input, e.g. \".openframe.ai\" */\n  domainSuffix?: string;\n  domainPlaceholder?: string;\n  termsUrl?: string;\n  privacyPolicyUrl?: string;\n  submitLabel?: string;\n  /**\n   * Extra fields, rendered after the account block and before the terms — the same slot\n   * CompleteAccountForm offers, for inputs a deployment adds conditionally (a dev-only PR number).\n   */\n  children?: ReactNode;\n  /** Renders a back action beside the submit when supplied (e.g. to return to an earlier step). */\n  onBack?: () => void;\n  backLabel?: string;\n  /** Disables just the primary submit (fields stay editable). */\n  submitDisabled?: boolean;\n  loading?: boolean;\n  disabled?: boolean;\n  errors?: {\n    email?: string;\n    organizationName?: string;\n    domain?: string;\n    terms?: string;\n    firstName?: string;\n    lastName?: string;\n    password?: string;\n    confirmPassword?: string;\n  };\n  /** Informational status under the email field (e.g. live availability). `errors.email` wins. */\n  emailStatus?: { message: string; variant: 'error' | 'warning' | 'success' | 'muted' };\n  /** Informational status under the domain field (e.g. live availability). `errors.domain` wins. */\n  domainStatus?: { message: string; variant: 'error' | 'warning' | 'success' | 'muted' };\n  /** Extra content rendered under the domain field, e.g. suggested available domains. */\n  domainSlot?: ReactNode;\n  /**\n   * SSO registration alternatives rendered below the primary submit behind an\n   * \"or continue with\" divider. The form fields stay editable — gate the\n   * buttons with `ssoDisabled` (e.g. until the form validates).\n   */\n  ssoProviders?: AuthSsoProvider[];\n  onSsoClick?: (provider: AuthSsoProvider) => void;\n  /** Disables the provider buttons (e.g. until the form validates). */\n  ssoDisabled?: boolean;\n  /** Verb prefix for provider buttons, e.g. \"Continue with\". */\n  ssoActionLabel?: string;\n  /** Divider text between the primary submit and the SSO buttons. */\n  dividerLabel?: string;\n  className?: string;\n}\n\n/**\n * Create Organization form (Sign Up tab). Presentational + controlled — the\n * consumer owns state, validation and submission. Covers the empty, filled and\n * SSO states from the auth redesign.\n */\nexport function CreateOrganizationForm({\n  email,\n  organizationName,\n  domain,\n  agreedToTerms,\n  onEmailChange,\n  onOrganizationNameChange,\n  onDomainChange,\n  onAgreedToTermsChange,\n  onSubmit,\n  firstName = '',\n  lastName = '',\n  password = '',\n  confirmPassword = '',\n  onFirstNameChange,\n  onLastNameChange,\n  onPasswordChange,\n  onConfirmPasswordChange,\n  emailReadOnly = false,\n  emailReadOnlyLabel = 'Email',\n  title = 'Create Organization',\n  subtitle = 'Start your journey with OpenFrame.',\n  domainSuffix,\n  domainPlaceholder = 'company-name',\n  termsUrl = '#',\n  privacyPolicyUrl = '#',\n  submitLabel = 'Continue',\n  children,\n  onBack,\n  backLabel = 'Back',\n  submitDisabled = false,\n  loading = false,\n  disabled = false,\n  errors,\n  emailStatus,\n  domainStatus,\n  domainSlot,\n  ssoProviders,\n  onSsoClick,\n  ssoDisabled = false,\n  ssoActionLabel = 'Continue with',\n  dividerLabel = 'or continue with',\n  className,\n}: CreateOrganizationFormProps) {\n  const hasSso = !!ssoProviders && ssoProviders.length > 0;\n  const fieldsDisabled = disabled || loading;\n\n  // Validation messages are deferred while the user is typing (shown on blur or after a pause).\n  const emailErr = useDeferredError(errors?.email, email ?? '');\n  const orgNameErr = useDeferredError(errors?.organizationName, organizationName);\n  const domainErr = useDeferredError(errors?.domain, domain);\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    // `submitDisabled` too, not just `fieldsDisabled`: Enter must be the same gate as the button.\n    // The fields stay editable while the form is incomplete, so checking only `fieldsDisabled`\n    // let Enter submit a form the button was still refusing.\n    if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {\n      onSubmit();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      {/* Email + Organization Name — single column on every breakpoint */}\n      {email === undefined ? null : emailReadOnly ? (\n        <Input type=\"email\" label={emailReadOnlyLabel} value={email} readOnly disabled />\n      ) : (\n        <Input\n          type=\"email\"\n          label=\"Email\"\n          placeholder=\"username@mail.com\"\n          value={email}\n          error={emailErr.error ?? emailStatus?.message}\n          errorVariant={emailErr.error ? 'error' : emailStatus?.variant}\n          disabled={fieldsDisabled}\n          onBlur={emailErr.onBlur}\n          onChange={event => onEmailChange?.(event.target.value)}\n          onKeyDown={handleKeyDown}\n        />\n      )}\n      <Input\n        label=\"Organization Name\"\n        placeholder=\"Your Company Name\"\n        value={organizationName}\n        error={orgNameErr.error}\n        disabled={fieldsDisabled}\n        onBlur={orgNameErr.onBlur}\n        onChange={event => onOrganizationNameChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      {/* Domain */}\n      <div className=\"flex flex-col\">\n        <Input\n          label=\"Domain\"\n          placeholder={domainPlaceholder}\n          value={domain}\n          error={domainErr.error ?? domainStatus?.message}\n          errorVariant={domainErr.error ? 'error' : domainStatus?.variant}\n          disabled={fieldsDisabled}\n          onBlur={domainErr.onBlur}\n          endAdornment={domainSuffix ? <span className=\"whitespace-nowrap\">{domainSuffix}</span> : undefined}\n          onChange={event => onDomainChange(event.target.value)}\n          onKeyDown={handleKeyDown}\n        />\n        {domainSlot && <div className=\"pt-[var(--spacing-system-s)]\">{domainSlot}</div>}\n      </div>\n\n      {/* All four handlers or none: the account block is one decision, and checking them here\n          rather than through a derived boolean is what lets TypeScript narrow them. */}\n      {onFirstNameChange && onLastNameChange && onPasswordChange && onConfirmPasswordChange && (\n        <AccountDetailsFields\n          layout=\"paired\"\n          firstName={firstName}\n          lastName={lastName}\n          password={password}\n          confirmPassword={confirmPassword}\n          onFirstNameChange={onFirstNameChange}\n          onLastNameChange={onLastNameChange}\n          onPasswordChange={onPasswordChange}\n          onConfirmPasswordChange={onConfirmPasswordChange}\n          errors={errors}\n          disabled={fieldsDisabled}\n          onKeyDown={handleKeyDown}\n        />\n      )}\n\n      {children}\n\n      {/* Terms & Privacy — deliberately NOT `truncateLabel`. That prop suits a\n          one-line value; this label is a sentence carrying the Terms and Privacy\n          Policy links, and ellipsizing it clipped the row to \"Agree to Terms &\n          Privacy Policy by sig…\" below ~500px, so on a phone the user could not\n          read what they were agreeing to. Wrapping is the only acceptable\n          overflow behaviour for consent copy. */}\n      <CheckboxBlock\n        id=\"create-org-terms\"\n        label={<TermsAgreementLabel termsUrl={termsUrl} privacyPolicyUrl={privacyPolicyUrl} />}\n        checked={agreedToTerms}\n        disabled={fieldsDisabled}\n        error={errors?.terms}\n        onCheckedChange={onAgreedToTermsChange}\n      />\n\n      {/* Actions — optional back + submit */}\n      <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n        {onBack && (\n          <Button\n            type=\"button\"\n            variant=\"transparent\"\n            fullWidth\n            className=\"flex-1\"\n            disabled={fieldsDisabled}\n            onClick={onBack}\n          >\n            {backLabel}\n          </Button>\n        )}\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          fullWidth\n          className=\"flex-1\"\n          loading={loading}\n          disabled={disabled || submitDisabled}\n          onClick={onSubmit}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n\n      {/* SSO registration alternatives */}\n      {hasSso && (\n        <>\n          <LabeledDivider label={dividerLabel} />\n          <SsoProviderButtons\n            providers={ssoProviders}\n            onSsoClick={onSsoClick}\n            actionLabel={ssoActionLabel}\n            disabled={disabled || loading || ssoDisabled}\n          />\n        </>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport { XmarkIcon } from '../../icons-v2-generated/signs-and-symbols/xmark-icon';\nimport { Button } from '../../ui/button';\n\nexport interface InviteLinkInvalidModalProps {\n  onBackToLogin: () => void;\n  /** Dismiss (X). Defaults to onBackToLogin. */\n  onClose?: () => void;\n  title?: string;\n  description?: string;\n  backLabel?: string;\n  className?: string;\n}\n\n/**\n * Notice shown when an invitation link is expired or invalid. Full-screen dark\n * backdrop with a single card — pinned to the bottom on mobile, centered from\n * tablet up; both the X and the button return to login.\n */\nexport function InviteLinkInvalidModal({\n  onBackToLogin,\n  onClose,\n  title = 'Invite link not valid',\n  description = 'This invitation link has expired or is no longer valid. Contact your administrator for a new invitation.',\n  backLabel = 'Back to Login',\n  className,\n}: InviteLinkInvalidModalProps) {\n  return (\n    <div\n      className={cn(\n        'flex min-h-screen w-full items-end justify-center bg-ods-bg p-[var(--spacing-system-l)] md:items-center',\n        className,\n      )}\n    >\n      <div className=\"flex w-full max-w-[600px] flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]\">\n        {/* Title + close */}\n        <div className=\"flex items-center gap-[var(--spacing-system-mf)]\">\n          <h1 className=\"flex-1 tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n          {/* Close (X) — desktop/tablet only, per design */}\n          <button\n            type=\"button\"\n            aria-label=\"Close\"\n            onClick={onClose ?? onBackToLogin}\n            className=\"hidden shrink-0 text-ods-text-primary md:block\"\n          >\n            <XmarkIcon className=\"h-6 w-6\" />\n          </button>\n        </div>\n\n        <p className=\"text-ods-text-primary text-h4\">{description}</p>\n\n        {/* Action — right half, matching the design */}\n        <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n          <div className=\"hidden flex-1 md:block\" />\n          <Button type=\"button\" variant=\"accent\" fullWidth className=\"md:flex-1\" onClick={onBackToLogin}>\n            {backLabel}\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { PolicyLink } from '../../ui/policy-link';\n\nexport interface LegalLinksProps {\n  termsUrl: string;\n  privacyPolicyUrl: string;\n}\n\n/**\n * \"Terms of Service • Privacy Policy\" — the quiet legal row at the foot of an auth card, for the\n * screens that have no consent checkbox to carry the links (Login, the email step of Sign Up).\n */\nexport function LegalLinks({ termsUrl, privacyPolicyUrl }: LegalLinksProps) {\n  return (\n    <div className=\"flex justify-center gap-[var(--spacing-system-xs)] text-ods-text-secondary text-h6\">\n      <PolicyLink href={termsUrl} tone=\"secondary\">\n        Terms of Service\n      </PolicyLink>\n      <span aria-hidden=\"true\">•</span>\n      <PolicyLink href={privacyPolicyUrl} tone=\"secondary\">\n        Privacy Policy\n      </PolicyLink>\n    </div>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent, ReactNode } from 'react';\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error';\nimport { cn } from '../../../utils/cn';\nimport { AlertTriangleIcon } from '../../icons-v2-generated/interface/alert-triangle-icon';\nimport { Button } from '../../ui/button';\nimport { Input } from '../../ui/input';\nimport { LabeledDivider } from './labeled-divider';\nimport { LegalLinks } from './legal-links';\nimport type { AuthSsoProvider } from './sso-providers';\nimport { SsoProviderButtons } from './sso-providers';\n\nexport interface LoginFormProps {\n  /** Controlled email value */\n  email: string;\n  onEmailChange: (value: string) => void;\n  emailPlaceholder?: string;\n  loading?: boolean;\n  disabled?: boolean;\n  errors?: {\n    email?: string;\n  };\n  /** Informational status under the email field (e.g. live tenant discovery). `errors.email` wins. */\n  emailStatus?: { message: string; variant: 'error' | 'warning' | 'success' | 'muted' };\n  /**\n   * SSO providers, rendered ABOVE the email field with a divider between the two. Always visible;\n   * gate clickability with `ssoDisabled` / `ssoEnabledProviders`.\n   */\n  ssoProviders: AuthSsoProvider[];\n  onSsoClick?: (provider: AuthSsoProvider) => void;\n  /** Disables every provider button (e.g. until the email passes discovery). */\n  ssoDisabled?: boolean;\n  /** When set, only these providers are clickable; the rest stay disabled. */\n  ssoEnabledProviders?: AuthSsoProvider[];\n  /** Verb prefix for provider buttons, e.g. \"Continue with\". */\n  ssoActionLabel?: string;\n  /** Label on the divider between the provider buttons and the email field. */\n  dividerLabel?: string;\n  /**\n   * The discovered tenant's own SSO options, rendered UNDER the email field — unlike `ssoProviders`\n   * above, these depend on knowing the tenant, so they cannot be offered before an address is\n   * typed. Shares `onSsoClick`.\n   *\n   * Three states, and the empty array is not the same as nothing: `undefined` means discovery has\n   * not resolved a tenant yet and the slot stays empty, while `[]` means it resolved and the tenant\n   * has none — which is an answer worth showing rather than silence.\n   */\n  customSsoProviders?: AuthSsoProvider[];\n  /** Shown in place of the custom SSO buttons when the resolved tenant has none configured. */\n  noCustomSsoLabel?: string;\n  /** Primary submit under the email field. Omit the handler to hide the button. */\n  onSubmitClick?: () => void;\n  submitLabel?: string;\n  /** Disables just the primary submit (fields stay editable). */\n  submitDisabled?: boolean;\n  /** Overrides the default heading. */\n  title?: string;\n  /** Overrides the default sub-heading. */\n  subtitle?: ReactNode;\n  /**\n   * Public legal pages, linked in a quiet \"Terms of Service • Privacy Policy\" row at the foot of\n   * the card. Both are needed for the row to render: this screen has no consent checkbox to carry\n   * the links, so the row is the only place they appear.\n   */\n  termsUrl?: string;\n  privacyPolicyUrl?: string;\n  className?: string;\n}\n\n/**\n * Login form (Login tab). Presentational + controlled — the consumer owns\n * state, validation and discovery. Single-screen design: the provider buttons come first and need\n * nothing typed, then the email field below them. Whether the providers are gated is the\n * consumer's call via `ssoDisabled`; the identity-first flows leave them open, because the server\n * resolves the tenant from the identity the provider asserts.\n */\nexport function LoginForm({\n  email,\n  onEmailChange,\n  emailPlaceholder = 'username@mail.com',\n  loading = false,\n  disabled = false,\n  errors,\n  emailStatus,\n  ssoProviders,\n  onSsoClick,\n  ssoDisabled = false,\n  ssoEnabledProviders,\n  ssoActionLabel = 'Continue with',\n  dividerLabel = 'or continue with OpenFrame SSO',\n  customSsoProviders,\n  noCustomSsoLabel = 'No additional SSO configurations for this domain',\n  onSubmitClick,\n  submitLabel = 'Continue',\n  submitDisabled = false,\n  title = 'Login to OpenFrame',\n  subtitle = 'Enter your email to access your organization.',\n  termsUrl,\n  privacyPolicyUrl,\n  className,\n}: LoginFormProps) {\n  const fieldDisabled = disabled || loading;\n\n  // Validation messages are deferred while the user is typing (shown on blur or after a pause).\n  const emailErr = useDeferredError(errors?.email, email);\n\n  // Enter drives the same action as the visible button, under the same gate. A form with no\n  // submit (the Login tab, where the providers are the only way on) simply does nothing.\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key !== 'Enter' || fieldDisabled || submitDisabled) return;\n    onSubmitClick?.();\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      {/* Providers come FIRST. They need nothing typed, and putting the email above them taught\n          people to fill it in before tapping one — the behaviour that made a provider button look\n          broken while it waited on a field it never needed. */}\n      {ssoProviders.length > 0 && (\n        <SsoProviderButtons\n          providers={ssoProviders}\n          onSsoClick={onSsoClick}\n          actionLabel={ssoActionLabel}\n          disabled={disabled || loading || ssoDisabled}\n          enabledProviders={ssoEnabledProviders}\n        />\n      )}\n\n      {ssoProviders.length > 0 && <LabeledDivider label={dividerLabel} />}\n\n      {/* Email */}\n      <Input\n        type=\"email\"\n        label=\"Email\"\n        placeholder={emailPlaceholder}\n        value={email}\n        error={emailErr.error ?? emailStatus?.message}\n        errorVariant={emailErr.error ? 'error' : emailStatus?.variant}\n        disabled={fieldDisabled}\n        onBlur={emailErr.onBlur}\n        onChange={event => onEmailChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      {customSsoProviders &&\n        (customSsoProviders.length > 0 ? (\n          <SsoProviderButtons\n            providers={customSsoProviders}\n            onSsoClick={onSsoClick}\n            actionLabel={ssoActionLabel}\n            disabled={disabled || loading}\n          />\n        ) : (\n          // Same footprint as the button it stands in for, so nothing shifts when a tenant does\n          // have options. Tokens are the design's: soft-grey surface, grey text and icon, the\n          // bold-body step, a 24px icon, and 12/16 for the inset and the gap.\n          <div\n            role=\"status\"\n            className=\"flex h-11 w-full items-center gap-[var(--spacing-system-m)] rounded-md bg-ods-bg-surface px-[var(--spacing-system-s)] text-ods-text-secondary text-h3 md:h-12\"\n          >\n            <AlertTriangleIcon className=\"h-6 w-6 shrink-0\" />\n            {noCustomSsoLabel}\n          </div>\n        ))}\n\n      {onSubmitClick && (\n        // Full width on a phone, half the row and right-aligned from `md` up, matching the design.\n        // The spacer, rather than a width on the button, keeps it flush with the fields' right edge.\n        <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n          <div className=\"hidden flex-1 md:block\" />\n          <Button\n            type=\"button\"\n            variant=\"accent\"\n            fullWidth\n            className=\"md:flex-1\"\n            loading={loading}\n            disabled={submitDisabled || fieldDisabled}\n            onClick={onSubmitClick}\n          >\n            {submitLabel}\n          </Button>\n        </div>\n      )}\n\n      {termsUrl && privacyPolicyUrl && <LegalLinks termsUrl={termsUrl} privacyPolicyUrl={privacyPolicyUrl} />}\n    </div>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent } from 'react';\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error';\nimport { cn } from '../../../utils/cn';\nimport { Button } from '../../ui/button';\nimport { Input } from '../../ui/input';\nimport { PasswordInput } from '../../ui/password-input';\n\nexport interface OpenFrameSsoLoginFormProps {\n  /** Controlled field values */\n  email: string;\n  password: string;\n  onEmailChange: (value: string) => void;\n  onPasswordChange: (value: string) => void;\n  /** Primary submit (\"Continue\") */\n  onSubmit: () => void;\n  onForgotPassword: () => void;\n  submitDisabled?: boolean;\n  loading?: boolean;\n  disabled?: boolean;\n  errors?: {\n    email?: string;\n    password?: string;\n  };\n  title?: string;\n  subtitle?: string;\n  emailLabel?: string;\n  passwordLabel?: string;\n  emailPlaceholder?: string;\n  passwordPlaceholder?: string;\n  forgotPasswordLabel?: string;\n  submitLabel?: string;\n  className?: string;\n}\n\n/**\n * OpenFrame SSO login form. Presentational + controlled — email + password to\n * sign in via OpenFrame's own SSO. Rendered inside SsoAuthShell.\n */\nexport function OpenFrameSsoLoginForm({\n  email,\n  password,\n  onEmailChange,\n  onPasswordChange,\n  onSubmit,\n  onForgotPassword,\n  submitDisabled = false,\n  loading = false,\n  disabled = false,\n  errors,\n  title = 'OpenFrame Single Sign-On',\n  subtitle = 'Enter your email and password to access your organization.',\n  emailLabel = 'Email',\n  passwordLabel = 'Password',\n  emailPlaceholder = 'username@mail.com',\n  passwordPlaceholder = 'Enter Your Password',\n  forgotPasswordLabel = 'Forgot Password?',\n  submitLabel = 'Continue',\n  className,\n}: OpenFrameSsoLoginFormProps) {\n  const fieldsDisabled = disabled || loading;\n\n  // Validation messages are deferred while the user is typing (shown on blur or after a pause).\n  const emailErr = useDeferredError(errors?.email, email);\n  const passwordErr = useDeferredError(errors?.password, password);\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {\n      onSubmit();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      <Input\n        type=\"email\"\n        label={emailLabel}\n        placeholder={emailPlaceholder}\n        value={email}\n        error={emailErr.error}\n        disabled={fieldsDisabled}\n        onBlur={emailErr.onBlur}\n        onChange={event => onEmailChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      <PasswordInput\n        label={passwordLabel}\n        placeholder={passwordPlaceholder}\n        value={password}\n        error={passwordErr.error}\n        disabled={fieldsDisabled}\n        onBlur={passwordErr.onBlur}\n        onChange={event => onPasswordChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      {/* Forgot password + Continue */}\n      <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n        <Button\n          type=\"button\"\n          variant=\"transparent\"\n          fullWidth\n          className=\"flex-1\"\n          disabled={fieldsDisabled}\n          onClick={onForgotPassword}\n        >\n          {forgotPasswordLabel}\n        </Button>\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          fullWidth\n          className=\"flex-1\"\n          loading={loading}\n          disabled={disabled || submitDisabled}\n          onClick={onSubmit}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent } from 'react';\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error';\nimport { cn } from '../../../utils/cn';\nimport { Button } from '../../ui/button';\nimport { Input } from '../../ui/input';\nimport { AccountDetailsFields } from './account-details-fields';\n\nexport interface OpenFrameSsoSignUpFormProps {\n  /** Controlled field values */\n  email: string;\n  firstName: string;\n  lastName: string;\n  password: string;\n  confirmPassword: string;\n  onEmailChange: (value: string) => void;\n  onFirstNameChange: (value: string) => void;\n  onLastNameChange: (value: string) => void;\n  onPasswordChange: (value: string) => void;\n  onConfirmPasswordChange: (value: string) => void;\n  /** Primary submit (\"Continue\") */\n  onSubmit: () => void;\n  onForgotPassword: () => void;\n  /** Locks the email field, e.g. when it was verified on a previous step. */\n  emailReadOnly?: boolean;\n  submitDisabled?: boolean;\n  loading?: boolean;\n  disabled?: boolean;\n  errors?: {\n    email?: string;\n    firstName?: string;\n    lastName?: string;\n    password?: string;\n    confirmPassword?: string;\n  };\n  title?: string;\n  subtitle?: string;\n  emailLabel?: string;\n  submitLabel?: string;\n  forgotPasswordLabel?: string;\n  className?: string;\n}\n\n/**\n * OpenFrame SSO sign-up form. Presentational + controlled — create OpenFrame SSO\n * credentials (email, name, password).\n */\nexport function OpenFrameSsoSignUpForm({\n  email,\n  firstName,\n  lastName,\n  password,\n  confirmPassword,\n  onEmailChange,\n  onFirstNameChange,\n  onLastNameChange,\n  onPasswordChange,\n  onConfirmPasswordChange,\n  onSubmit,\n  onForgotPassword,\n  emailReadOnly = false,\n  submitDisabled = false,\n  loading = false,\n  disabled = false,\n  errors,\n  title = 'OpenFrame Single Sign-On',\n  subtitle = 'Enter your email and password to access your organization.',\n  emailLabel = 'Email',\n  submitLabel = 'Continue',\n  forgotPasswordLabel = 'Forgot Password?',\n  className,\n}: OpenFrameSsoSignUpFormProps) {\n  const fieldsDisabled = disabled || loading;\n\n  // Validation messages are deferred while the user is typing (shown on blur or after a pause).\n  const emailErr = useDeferredError(errors?.email, email);\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {\n      onSubmit();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      <Input\n        type=\"email\"\n        label={emailLabel}\n        placeholder=\"username@mail.com\"\n        value={email}\n        error={emailErr.error}\n        disabled={fieldsDisabled || emailReadOnly}\n        onBlur={emailErr.onBlur}\n        onChange={event => onEmailChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      <AccountDetailsFields\n        firstName={firstName}\n        lastName={lastName}\n        password={password}\n        confirmPassword={confirmPassword}\n        onFirstNameChange={onFirstNameChange}\n        onLastNameChange={onLastNameChange}\n        onPasswordChange={onPasswordChange}\n        onConfirmPasswordChange={onConfirmPasswordChange}\n        errors={errors}\n        disabled={fieldsDisabled}\n        onKeyDown={handleKeyDown}\n      />\n\n      {/* Forgot password + Continue */}\n      <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n        <Button\n          type=\"button\"\n          variant=\"transparent\"\n          fullWidth\n          className=\"flex-1\"\n          disabled={fieldsDisabled}\n          onClick={onForgotPassword}\n        >\n          {forgotPasswordLabel}\n        </Button>\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          fullWidth\n          className=\"flex-1\"\n          loading={loading}\n          disabled={disabled || submitDisabled}\n          onClick={onSubmit}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport type { KeyboardEvent } from 'react';\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error';\nimport { cn } from '../../../utils/cn';\nimport { Button } from '../../ui/button';\nimport { Input } from '../../ui/input';\nimport { BackToLoginLink } from './back-to-login-link';\n\nexport interface PasswordResetFormProps {\n  /** Controlled field values */\n  password: string;\n  confirmPassword: string;\n  onPasswordChange: (value: string) => void;\n  onConfirmPasswordChange: (value: string) => void;\n  /** Primary submit (\"Reset Password\") */\n  onSubmit: () => void;\n  /** Secondary action (\"Cancel\") */\n  onCancel: () => void;\n  /** In-card \"Back to Login\" shown on tablet/mobile; desktop uses the shell footer. */\n  onBackToLogin?: () => void;\n  /** Disables just the primary submit (fields stay editable). */\n  submitDisabled?: boolean;\n  loading?: boolean;\n  disabled?: boolean;\n  errors?: {\n    password?: string;\n    confirmPassword?: string;\n  };\n  title?: string;\n  subtitle?: string;\n  newPasswordLabel?: string;\n  confirmPasswordLabel?: string;\n  newPasswordPlaceholder?: string;\n  confirmPasswordPlaceholder?: string;\n  submitLabel?: string;\n  cancelLabel?: string;\n  className?: string;\n}\n\n/**\n * Password Reset form. Presentational + controlled — the consumer owns state,\n * validation and submission. Covers the empty and filled states from the auth\n * redesign. \"Back to Login\" is provided by the AuthShell footer (desktop).\n */\nexport function PasswordResetForm({\n  password,\n  confirmPassword,\n  onPasswordChange,\n  onConfirmPasswordChange,\n  onSubmit,\n  onCancel,\n  onBackToLogin,\n  submitDisabled = false,\n  loading = false,\n  disabled = false,\n  errors,\n  title = 'Reset your Password',\n  subtitle = 'Enter your new password below',\n  newPasswordLabel = 'New Password',\n  confirmPasswordLabel = 'Confirm Password',\n  newPasswordPlaceholder = 'Enter your Password',\n  confirmPasswordPlaceholder = 'Enter your Password',\n  submitLabel = 'Reset Password',\n  cancelLabel = 'Cancel',\n  className,\n}: PasswordResetFormProps) {\n  const fieldsDisabled = disabled || loading;\n\n  // Validation messages are deferred while the user is typing (shown on blur or after a pause).\n  const passwordErr = useDeferredError(errors?.password, password);\n  const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword);\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {\n      onSubmit();\n    }\n  };\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      {/* New password */}\n      <Input\n        type=\"password\"\n        label={newPasswordLabel}\n        placeholder={newPasswordPlaceholder}\n        value={password}\n        error={passwordErr.error}\n        disabled={fieldsDisabled}\n        onBlur={passwordErr.onBlur}\n        onChange={event => onPasswordChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      {/* Confirm password */}\n      <Input\n        type=\"password\"\n        label={confirmPasswordLabel}\n        placeholder={confirmPasswordPlaceholder}\n        value={confirmPassword}\n        error={confirmErr.error}\n        disabled={fieldsDisabled}\n        onBlur={confirmErr.onBlur}\n        onChange={event => onConfirmPasswordChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n\n      {/* Actions — Cancel + Reset, side by side on every breakpoint */}\n      <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          fullWidth\n          className=\"flex-1\"\n          disabled={fieldsDisabled}\n          onClick={onCancel}\n        >\n          {cancelLabel}\n        </Button>\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          fullWidth\n          className=\"flex-1\"\n          loading={loading}\n          disabled={disabled || submitDisabled}\n          onClick={onSubmit}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n\n      {/* Back to Login — in-card on tablet/mobile; desktop shows it in the shell footer */}\n      {onBackToLogin && <BackToLoginLink onClick={onBackToLogin} className=\"self-start lg:hidden\" />}\n    </div>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { OpenFrameWordmark, PoweredByFlamingo } from './auth-branding';\n\nexport interface SsoAuthShellProps {\n  /** Centered card content (OpenFrame SSO login / sign-up form). */\n  children: ReactNode;\n  className?: string;\n}\n\n/**\n * Centered single-column layout for the OpenFrame SSO (IdP) screens: logo on top,\n * the form card centered, \"Powered by Flamingo\" at the bottom. No marketing panel.\n */\nexport function SsoAuthShell({ children, className }: SsoAuthShellProps) {\n  return (\n    <div\n      className={cn(\n        'flex min-h-screen w-full flex-col items-center justify-between gap-[var(--spacing-system-xl)] bg-ods-bg p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      <OpenFrameWordmark />\n      <div className=\"flex w-full max-w-[600px] flex-col items-center\">{children}</div>\n      <PoweredByFlamingo compact />\n    </div>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Button } from '../../ui/button';\nimport { CheckboxBlock } from '../../ui/checkbox-block';\nimport { SquareAvatar } from '../../ui/square-avatar';\nimport { Tag } from '../../ui/tag';\nimport { TermsAgreementLabel } from './terms-agreement-label';\n\nexport interface SsoJoinFormProps {\n  /** Display name of the identity the provider asserted, e.g. \"Roman Smith\". Falls back to `email`. */\n  name?: string;\n  /** The asserted address. Stands in for the name when the provider gave none. */\n  email?: string;\n  /** Provider picture, when there is one; the initials of the name otherwise. */\n  avatarUrl?: string;\n  /** The organization being joined. */\n  organizationName: string;\n  /** Role names as the server reports them (`ADMIN`), one tag each. Nothing is drawn for a plain member. */\n  roles?: string[];\n  agreedToTerms: boolean;\n  onAgreedToTermsChange: (checked: boolean) => void;\n  /** Primary action (\"Create Account\"). Only reachable once the terms are agreed. */\n  onSubmit: () => void;\n  /** Leaves without creating anything (\"Back to Login\"). */\n  onBack: () => void;\n  title?: string;\n  subtitle?: ReactNode;\n  submitLabel?: string;\n  backLabel?: string;\n  termsUrl?: string;\n  privacyPolicyUrl?: string;\n  /** Held while the submit navigates away: everything locks and the submit spins. */\n  loading?: boolean;\n  disabled?: boolean;\n  className?: string;\n}\n\n/**\n * \"One Last Step\" - the consent gate an SSO flow shows before it creates a user (accepting an\n * invitation, or a first login through a shared domain). Presentational + controlled: the consumer\n * loads the pending identity, owns the checkbox state and performs the submit navigation.\n *\n * The identity row is read-only on purpose. The server reads the identity from its own session\n * and would ignore anything edited here; the row exists so the person can see WHICH account and\n * WHICH organization they are about to be joined with - and walk away if it is the wrong one.\n */\nexport function SsoJoinForm({\n  name,\n  email,\n  avatarUrl,\n  organizationName,\n  roles = [],\n  agreedToTerms,\n  onAgreedToTermsChange,\n  onSubmit,\n  onBack,\n  title = 'One Last Step',\n  subtitle = \"Confirm the account you're joining with.\",\n  submitLabel = 'Create Account',\n  backLabel = 'Back to Login',\n  termsUrl,\n  privacyPolicyUrl,\n  loading = false,\n  disabled = false,\n  className,\n}: SsoJoinFormProps) {\n  const fieldsDisabled = disabled || loading;\n  const displayName = name?.trim() || email?.trim() || '';\n\n  return (\n    <div\n      className={cn(\n        'flex w-full flex-col gap-[var(--spacing-system-l)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-xl)]',\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex flex-col\">\n        <h1 className=\"tracking-[-0.64px] text-ods-text-primary text-h2\">{title}</h1>\n        <p className=\"text-ods-text-secondary text-h4\">{subtitle}</p>\n      </div>\n\n      {/* Identity row: who is joining, where, as what. Page-background surface inside the card, per design. */}\n      <div className=\"flex items-center gap-[var(--spacing-system-s)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-m)]\">\n        <SquareAvatar\n          size=\"lg\"\n          variant=\"round\"\n          src={avatarUrl}\n          alt={displayName}\n          fallback={displayName}\n          initialsClassName=\"text-ods-text-secondary text-h6\"\n        />\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <span className=\"truncate text-ods-text-primary text-h4\">{displayName}</span>\n          <span className=\"truncate text-ods-text-secondary text-h6\">{organizationName}</span>\n        </div>\n        {roles.length > 0 && (\n          <div className=\"flex shrink-0 items-center gap-[var(--spacing-system-xs)]\">\n            {roles.map(role => (\n              <Tag key={role} variant=\"grey\" label={role} />\n            ))}\n          </div>\n        )}\n      </div>\n\n      <CheckboxBlock\n        id=\"sso-join-terms\"\n        label={<TermsAgreementLabel termsUrl={termsUrl} privacyPolicyUrl={privacyPolicyUrl} />}\n        checked={agreedToTerms}\n        disabled={fieldsDisabled}\n        onCheckedChange={onAgreedToTermsChange}\n      />\n\n      {/* Actions - back + submit, half the row each, on every breakpoint */}\n      <div className=\"flex items-center gap-[var(--spacing-system-l)]\">\n        <Button type=\"button\" variant=\"outline\" fullWidth className=\"flex-1\" disabled={fieldsDisabled} onClick={onBack}>\n          {backLabel}\n        </Button>\n        <Button\n          type=\"button\"\n          variant=\"accent\"\n          fullWidth\n          className=\"flex-1\"\n          loading={loading}\n          disabled={fieldsDisabled || !agreedToTerms}\n          onClick={onSubmit}\n        >\n          {submitLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { useState } from 'react';\nimport type { MouseEvent } from 'react';\nimport { MicrosoftIcon, GoogleLogo, SlackIcon, GitHubIcon, AppleIcon } from '../icons';\nimport { Button } from '../ui/button';\n\ninterface ProviderButtonProps {\n  provider: 'microsoft' | 'google' | 'slack' | 'github' | 'apple';\n  onClick: () => Promise<void> | void;\n  disabled?: boolean;\n  loading?: boolean;\n}\n\nconst PROVIDER_CONFIG = {\n  microsoft: {\n    displayName: 'Continue with Microsoft',\n    icon: MicrosoftIcon,\n  },\n  google: {\n    displayName: 'Continue with Google',\n    icon: GoogleLogo,\n  },\n  slack: {\n    displayName: 'Continue with Slack',\n    icon: SlackIcon,\n  },\n  github: {\n    displayName: 'Continue with GitHub',\n    icon: GitHubIcon,\n  },\n  apple: {\n    displayName: 'Continue with Apple',\n    icon: AppleIcon,\n  },\n};\n\nexport function ProviderButton({ provider, onClick, disabled = false, loading = false }: ProviderButtonProps) {\n  const [internalLoading, setInternalLoading] = useState(false);\n  const config = PROVIDER_CONFIG[provider];\n  const IconComponent = config.icon;\n\n  // Use external loading state if provided, otherwise use internal state\n  const isLoading = loading || internalLoading;\n\n  const handleClick = async (e: MouseEvent) => {\n    // Prevent the click from bubbling up to parent elements (like modal backdrop)\n    e.stopPropagation();\n    e.preventDefault();\n\n    if (disabled || isLoading) {\n      return;\n    }\n\n    try {\n      // Only set internal loading if no external loading is managed\n      if (!loading) {\n        setInternalLoading(true);\n      }\n      await onClick();\n    } catch (error) {\n      console.error(`${provider} sign-in error:`, error);\n    } finally {\n      if (!loading) {\n        setInternalLoading(false);\n      }\n    }\n  };\n\n  return (\n    <Button\n      type=\"button\"\n      onClick={handleClick}\n      disabled={disabled || isLoading}\n      variant=\"outline\"\n      size=\"default\"\n      leftIcon={isLoading ? null : <IconComponent className=\"h-5 w-5\" />}\n      className={`h-[56px] w-full !font-bold !text-h6 hover:bg-ods-bg-hover md:!w-full ${isLoading ? 'cursor-wait' : 'cursor-pointer'} `}\n      aria-label={config.displayName}\n    >\n      {isLoading ? 'Signing in...' : config.displayName}\n    </Button>\n  );\n}\n","'use client';\n\nimport { ProviderButton } from './provider-button';\n\nexport interface SSOConfigStatus {\n  provider: string;\n  enabled: boolean;\n  clientId?: string;\n}\n\ninterface AuthProvidersListProps {\n  /** List of enabled SSO providers */\n  enabledProviders: SSOConfigStatus[];\n  /** Callback when provider is clicked - implement your auth logic here */\n  onProviderClick: (provider: string) => Promise<void>;\n  /** Loading state for providers */\n  loading?: boolean;\n  /** Show divider above providers list */\n  showDivider?: boolean;\n  /** Custom divider text */\n  dividerText?: string;\n  /** Layout orientation */\n  orientation?: 'vertical' | 'horizontal';\n}\n\nexport function AuthProvidersList({\n  enabledProviders,\n  onProviderClick,\n  loading = false,\n  showDivider = true,\n  dividerText = 'or',\n  orientation = 'vertical',\n}: AuthProvidersListProps) {\n  if (!enabledProviders.length) {\n    return null;\n  }\n\n  const handleProviderClick = async (provider: string) => {\n    try {\n      await onProviderClick(provider);\n    } catch (error) {\n      console.error(`❌ Auth Providers List: Error with ${provider}:`, error);\n    }\n  };\n\n  return (\n    <div className=\"space-y-3\">\n      {showDivider && (\n        <div className=\"relative\">\n          <div className=\"absolute inset-0 flex items-center\">\n            <div className=\"w-full border-t border-ods-border\" />\n          </div>\n          <div className=\"relative flex justify-center text-h6\">\n            <span className=\"bg-ods-card px-2 text-ods-text-secondary\">{dividerText}</span>\n          </div>\n        </div>\n      )}\n\n      <div className={`${orientation === 'horizontal' ? 'flex gap-2' : 'space-y-2'}`}>\n        {enabledProviders\n          .filter(provider => provider.enabled)\n          .map(provider => (\n            <ProviderButton\n              key={provider.provider}\n              provider={provider.provider as 'microsoft' | 'google' | 'slack' | 'github' | 'apple'}\n              onClick={() => handleProviderClick(provider.provider)}\n              disabled={loading}\n              loading={loading}\n            />\n          ))}\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { Trash2, Plus, ChevronDown, ChevronUp, Eye, EyeOff } from 'lucide-react';\nimport { useState } from 'react';\nimport type { ChangelogEntry } from '../../types/product-release';\nimport { Button, Input, Textarea, Label } from '../ui';\n\ninterface ChangelogManagerProps {\n  title: string;\n  entries: ChangelogEntry[];\n  onChange: (entries: ChangelogEntry[]) => void;\n  className?: string;\n  /** Expand all items - useful after AI enrichment fills entries */\n  expandAll?: boolean;\n  /**\n   * When true, render a per-entry public/internal visibility toggle (Eye/EyeOff icon)\n   * in each entry's header. New entries default to 'public'. Used by investor updates\n   * — leave undefined for product releases so they keep their existing UX.\n   */\n  showVisibilityToggle?: boolean;\n}\n\nexport function ChangelogManager({\n  title,\n  entries,\n  onChange,\n  className = '',\n  expandAll = false,\n  showVisibilityToggle = false,\n}: ChangelogManagerProps) {\n  const entryCount = entries.length;\n  const allExpanded = () => new Set(Array.from({ length: entryCount }, (_, i) => i));\n\n  // Mounting already-expanded is a one-shot initial value, so it is a lazy\n  // `useState` initialiser rather than the first run of an effect.\n  const [expandedIndices, setExpandedIndices] = useState<Set<number>>(() =>\n    expandAll && entryCount > 0 ? allExpanded() : new Set(),\n  );\n\n  // When expandAll changes to true and there are entries, expand all — and\n  // likewise when enrichment appends entries while it is already true. Adjusted\n  // while rendering (React's prop-sync pattern): the prop flip has already\n  // scheduled this render, so expanding here shows the entries in it instead of\n  // painting them collapsed and re-rendering to open them.\n  const [expandSync, setExpandSync] = useState({ expandAll, entryCount });\n  if (expandSync.expandAll !== expandAll || expandSync.entryCount !== entryCount) {\n    setExpandSync({ expandAll, entryCount });\n    if (expandAll && entryCount > 0) {\n      setExpandedIndices(allExpanded());\n    }\n  }\n\n  const addEntry = () => {\n    const newEntry: ChangelogEntry = {\n      title: '',\n      description: '',\n      ...(showVisibilityToggle && { visibility: 'public' as const }),\n    };\n    onChange([...entries, newEntry]);\n    // Expand the newly added entry\n    setExpandedIndices(prev => new Set([...prev, entries.length]));\n  };\n\n  const toggleVisibility = (index: number) => {\n    const updated = [...entries];\n    const current = updated[index].visibility ?? 'public';\n    updated[index] = { ...updated[index], visibility: current === 'public' ? 'internal' : 'public' };\n    onChange(updated);\n  };\n\n  const removeEntry = (index: number) => {\n    onChange(entries.filter((_, i) => i !== index));\n    // Remove from expanded and adjust indices for items after removed one\n    setExpandedIndices(prev => {\n      const newSet = new Set<number>();\n      prev.forEach(i => {\n        if (i < index) newSet.add(i);\n        else if (i > index) newSet.add(i - 1);\n      });\n      return newSet;\n    });\n  };\n\n  const updateEntry = (index: number, field: keyof ChangelogEntry, value: string) => {\n    const updated = [...entries];\n    updated[index] = { ...updated[index], [field]: value };\n    onChange(updated);\n  };\n\n  const toggleExpanded = (index: number) => {\n    setExpandedIndices(prev => {\n      const newSet = new Set(prev);\n      if (newSet.has(index)) {\n        newSet.delete(index);\n      } else {\n        newSet.add(index);\n      }\n      return newSet;\n    });\n  };\n\n  return (\n    <div className={`space-y-3 ${className}`}>\n      <div className=\"flex items-center justify-between\">\n        <Label>{title}</Label>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"small-legacy\"\n          onClick={addEntry}\n          leftIcon={<Plus className=\"h-4 w-4\" />}\n          className=\"text-h6\"\n        >\n          Add Entry\n        </Button>\n      </div>\n\n      {entries.map((entry, index) => {\n        const isExpanded = expandedIndices.has(index);\n        const hasContent = entry.title.trim().length > 0;\n\n        return (\n          <div key={index} className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-bg-surface\">\n            {/* Header - always visible */}\n            <div className=\"flex items-center gap-3 p-3\">\n              <Button\n                type=\"button\"\n                variant=\"transparent\"\n                size=\"icon\"\n                onClick={() => toggleExpanded(index)}\n                className=\"shrink-0\"\n              >\n                {isExpanded ? <ChevronUp className=\"h-4 w-4\" /> : <ChevronDown className=\"h-4 w-4\" />}\n              </Button>\n\n              <div className=\"min-w-0 flex-1\">\n                {hasContent ? (\n                  <p className=\"truncate text-ods-text-primary text-h6\">{entry.title}</p>\n                ) : (\n                  <p className=\"italic text-ods-text-secondary text-h6\">New entry (click to edit)</p>\n                )}\n              </div>\n\n              {showVisibilityToggle && (\n                <Button\n                  type=\"button\"\n                  variant=\"transparent\"\n                  size=\"icon\"\n                  onClick={() => toggleVisibility(index)}\n                  className=\"shrink-0\"\n                  title={(entry.visibility ?? 'public') === 'public' ? 'Visible to investors' : 'Internal only'}\n                >\n                  {(entry.visibility ?? 'public') === 'public' ? (\n                    <Eye className=\"h-4 w-4 text-ods-accent\" />\n                  ) : (\n                    <EyeOff className=\"h-4 w-4 text-ods-text-secondary\" />\n                  )}\n                </Button>\n              )}\n\n              <Button\n                type=\"button\"\n                variant=\"transparent\"\n                size=\"icon\"\n                onClick={() => removeEntry(index)}\n                className=\"shrink-0 text-ods-error hover:bg-ods-error-secondary hover:text-ods-error-hover\"\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            </div>\n\n            {/* Expanded content */}\n            {isExpanded && (\n              <div className=\"space-y-3 border-t border-ods-border px-3 pb-3 pt-3\">\n                {/* Title */}\n                <div className=\"space-y-1\">\n                  <Label className=\"text-ods-text-secondary\">Title *</Label>\n                  <Input\n                    placeholder=\"e.g., New dark mode theme support\"\n                    value={entry.title}\n                    onChange={e => updateEntry(index, 'title', e.target.value)}\n                    onKeyDown={e => e.key === 'Enter' && e.preventDefault()}\n                    className=\"bg-ods-bg\"\n                  />\n                </div>\n\n                {/* Description */}\n                <div className=\"space-y-1\">\n                  <Label className=\"text-ods-text-secondary\">Description</Label>\n                  <Textarea\n                    placeholder=\"Detailed explanation of the change...\"\n                    value={entry.description || ''}\n                    onChange={e => updateEntry(index, 'description', e.target.value)}\n                    rows={2}\n                    className=\"bg-ods-bg\"\n                  />\n                </div>\n              </div>\n            )}\n          </div>\n        );\n      })}\n\n      {entries.length === 0 && (\n        <div className=\"rounded-lg border border-ods-border bg-ods-bg-surface px-4 py-4 text-center\">\n          <p className=\"text-ods-text-secondary text-h6\">\n            No entries added. Click \"Add Entry\" to create {title.toLowerCase()}.\n          </p>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport type { ChangelogEntry } from '../../types/product-release';\nimport { ChangelogManager } from './changelog-manager';\n\ninterface ChangelogSectionsManagerProps {\n  featuresAdded: ChangelogEntry[];\n  bugsFixed: ChangelogEntry[];\n  improvements: ChangelogEntry[];\n  breakingChanges: ChangelogEntry[];\n  onFeaturesAddedChange: (entries: ChangelogEntry[]) => void;\n  onBugsFixedChange: (entries: ChangelogEntry[]) => void;\n  onImprovementsChange: (entries: ChangelogEntry[]) => void;\n  onBreakingChangesChange: (entries: ChangelogEntry[]) => void;\n  className?: string;\n  /** Expand all items in all sections - useful after AI enrichment fills entries */\n  expandAll?: boolean;\n}\n\nexport function ChangelogSectionsManager({\n  featuresAdded,\n  bugsFixed,\n  improvements,\n  breakingChanges,\n  onFeaturesAddedChange,\n  onBugsFixedChange,\n  onImprovementsChange,\n  onBreakingChangesChange,\n  className = '',\n  expandAll = false,\n}: ChangelogSectionsManagerProps) {\n  return (\n    <div className={`space-y-6 rounded-lg border border-ods-border bg-ods-card p-6 ${className}`}>\n      <h3 className=\"text-ods-text-primary text-h5\">Changelog</h3>\n\n      {/* Features Added */}\n      <ChangelogManager\n        title=\"Features Added\"\n        entries={featuresAdded}\n        onChange={onFeaturesAddedChange}\n        expandAll={expandAll}\n      />\n\n      {/* Bugs Fixed */}\n      <ChangelogManager title=\"Bugs Fixed\" entries={bugsFixed} onChange={onBugsFixedChange} expandAll={expandAll} />\n\n      {/* Improvements */}\n      <ChangelogManager\n        title=\"Improvements\"\n        entries={improvements}\n        onChange={onImprovementsChange}\n        expandAll={expandAll}\n      />\n\n      {/* Breaking Changes */}\n      <ChangelogManager\n        title=\"Breaking Changes\"\n        entries={breakingChanges}\n        onChange={onBreakingChangesChange}\n        expandAll={expandAll}\n      />\n    </div>\n  );\n}\n","'use client';\n\nimport { Lightbulb } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { ArrayEntryManager } from './array-entry-manager';\n\nexport interface ClickUpTaskLink {\n  clickup_task_id: string; // Just the task ID\n  display_order?: number;\n}\n\ninterface ClickUpTasksManagerProps {\n  tasks: ClickUpTaskLink[];\n  onChange: (tasks: ClickUpTaskLink[]) => void;\n  title?: ReactNode; // Support string or ReactNode for badge integration\n  className?: string;\n  /**\n   * Per-row label renderer (passthrough to ArrayEntryManager) — lets a host\n   * hydrate the bare task id with the mirror's title/status row.\n   */\n  renderLabel?: (item: ClickUpTaskLink, index: number) => ReactNode;\n}\n\nexport function ClickUpTasksManager({\n  tasks,\n  onChange,\n  title = 'ClickUp Roadmap Tasks', // Default title\n  className = '',\n  renderLabel,\n}: ClickUpTasksManagerProps) {\n  return (\n    <ArrayEntryManager\n      title={title}\n      items={tasks}\n      onChange={onChange}\n      fieldKey=\"clickup_task_id\"\n      placeholder=\"Task ID (e.g., 86cq2uabk)\"\n      emptyMessage='No ClickUp tasks linked. Click \"Add Task\" to link roadmap items.'\n      addButtonText=\"Add Task\"\n      icon={<Lightbulb className=\"h-5 w-5 text-ods-text-secondary\" />}\n      className={className}\n      renderLabel={renderLabel}\n    />\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { Copy02Icon } from '../icons-v2-generated/documents/copy-02-icon';\nimport { Button } from '../ui/button';\n\nexport interface CommandBoxAction {\n  /** Button label */\n  label: string;\n  /** Click handler */\n  onClick: () => void;\n  /** Button variant */\n  variant?: 'accent' | 'outline' | 'transparent' | 'destructive';\n  /** Icon to display before the label */\n  icon?: React.ReactNode;\n  /** Whether the button is disabled */\n  disabled?: boolean;\n  /** Whether the button is in loading state */\n  loading?: boolean;\n}\n\nexport interface CommandBoxProps {\n  /** The command text to display */\n  command: string;\n  /** Title displayed above the command box */\n  title?: string;\n  /** Primary action button (displayed on the right) */\n  primaryAction?: CommandBoxAction;\n  /** Secondary action button (displayed before primary) */\n  secondaryAction?: CommandBoxAction;\n  /** Additional CSS classes for the container */\n  className?: string;\n  /** Additional CSS classes for the command text */\n  commandClassName?: string;\n  /** Maximum lines to show (uses line-clamp, 0 for unlimited) */\n  maxLines?: number;\n  /** When set, shows a copy icon button in the top-right corner of the box */\n  onCopy?: () => void;\n  /** Accessible label for the corner copy button */\n  copyAriaLabel?: string;\n}\n\n/**\n * CommandBox - Unified component for displaying commands with action buttons\n *\n * Features:\n * - Displays command text in monospace font\n * - Optional title\n * - Primary and secondary action buttons\n * - Configurable line clamping\n * - Consistent ODS styling\n *\n * Usage Example:\n * ```tsx\n * import { CommandBox } from '@flamingo/ui-kit/components/features'\n * import { Copy, Play } from 'lucide-react'\n *\n * <CommandBox\n *   title=\"Device Add Command\"\n *   command=\"curl -L https://example.com/install.sh | bash\"\n *   primaryAction={{\n *     label: 'Copy Command',\n *     onClick: handleCopy,\n *     icon: <Copy className=\"w-5 h-5\" />,\n *     variant: 'primary'\n *   }}\n *   secondaryAction={{\n *     label: 'Run on Current Machine',\n *     onClick: handleRun,\n *     icon: <Play className=\"w-5 h-5\" />,\n *     variant: 'outline'\n *   }}\n * />\n * ```\n */\n// Static mapping for line-clamp classes (Tailwind needs static class names at build time)\nconst lineClampClasses: Record<number, string> = {\n  1: 'line-clamp-1',\n  2: 'line-clamp-2',\n  3: 'line-clamp-3',\n  4: 'line-clamp-4',\n  5: 'line-clamp-5',\n  6: 'line-clamp-6',\n};\n\nexport function CommandBox({\n  command,\n  title,\n  primaryAction,\n  secondaryAction,\n  className,\n  commandClassName,\n  maxLines = 0,\n  onCopy,\n  copyAriaLabel = 'Copy command',\n}: CommandBoxProps) {\n  // Get static line-clamp class or undefined for unlimited\n  const lineClampClass = maxLines > 0 ? lineClampClasses[maxLines] : undefined;\n\n  const commandText = (\n    <div\n      className={cn(\n        'break-all text-ods-text-primary text-code',\n        onCopy && 'min-w-0 flex-1',\n        lineClampClass,\n        commandClassName,\n      )}\n    >\n      {command}\n    </div>\n  );\n\n  return (\n    <div className={cn('flex flex-col gap-2', className)}>\n      {title && <div className=\"text-ods-text-primary text-h4\">{title}</div>}\n      <div className=\"rounded-[6px] border border-ods-border bg-ods-bg p-4\">\n        {onCopy ? (\n          <div className=\"flex items-start gap-4\">\n            {commandText}\n            <button\n              type=\"button\"\n              onClick={onCopy}\n              aria-label={copyAriaLabel}\n              className=\"shrink-0 rounded-md text-ods-text-secondary transition-colors hover:text-ods-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus\"\n            >\n              <Copy02Icon className=\"h-5 w-5\" />\n            </button>\n          </div>\n        ) : (\n          commandText\n        )}\n        {(primaryAction || secondaryAction) && (\n          <div className=\"mt-4 flex flex-col gap-3 md:flex-row md:justify-end\">\n            {secondaryAction && (\n              <Button\n                variant={secondaryAction.variant || 'outline'}\n                leftIcon={secondaryAction.icon}\n                onClick={secondaryAction.onClick}\n                disabled={secondaryAction.disabled}\n                loading={secondaryAction.loading}\n                className=\"w-full md:w-auto\"\n              >\n                {secondaryAction.label}\n              </Button>\n            )}\n            {primaryAction && (\n              <Button\n                variant={primaryAction.variant || 'accent'}\n                leftIcon={primaryAction.icon}\n                onClick={primaryAction.onClick}\n                disabled={primaryAction.disabled}\n                loading={primaryAction.loading}\n                className=\"w-full md:w-auto\"\n              >\n                {primaryAction.label}\n              </Button>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n","import Link from 'next/link';\nimport type { TagAssoc } from '../../types/blog';\nimport { StatusBadge } from '../ui/status-badge';\n\ninterface EntityTagBadgesProps {\n  /** Flat `<entity>_tags[]` association array (from entity_tags hydration). */\n  tags?: TagAssoc[] | null;\n  /**\n   * When set, each badge links to `${basePath}?tags=${slug}` (clickable, SPA nav\n   * via next/link). Omit for a non-interactive display (badges with no slug also\n   * render non-interactive even when basePath is set).\n   */\n  basePath?: string;\n  /** Cap visible badges; the remainder collapse into a \"+N\" badge. */\n  max?: number;\n  /** Extra classes on the wrapper row. */\n  className?: string;\n}\n\n// The ONE tag-badge skin (OpenFrame design): rounded-rect, font-mono uppercase\n// StatusBadge on ods-card + ods-border. Clickable badges add the accent hover.\n// Exported so other tag-shaped chips (e.g. FaqAccordion's section badge)\n// render the IDENTICAL skin without re-declaring it.\nexport const TAG_BADGE_CLASS = 'bg-ods-card border border-ods-border';\nconst BADGE_CLASS = TAG_BADGE_CLASS;\n\n/**\n * THE single tag-badge renderer for the whole product (hub + lib). Renders the\n * OpenFrame `StatusBadge` chip skin for a flat `<entity>_tags[]` array, optionally\n * clickable (links each tag to its filtered list via `basePath`). The hub's\n * `EntityTagList` delegates here so there is exactly one tag-display design.\n * Renders nothing when there are no tags.\n */\nexport function EntityTagBadges({ tags, basePath, max, className }: EntityTagBadgesProps) {\n  const items = (tags || []).filter((t): t is TagAssoc => !!t && !!t.name);\n  if (items.length === 0) return null;\n\n  // Only null/undefined means \"no cap\"; max={0} shows none, negatives clamp to 0.\n  const visibleLimit = max == null ? items.length : Math.max(0, max);\n  const shown = items.slice(0, visibleLimit);\n  const overflow = items.length - shown.length;\n\n  return (\n    <div className={`flex w-full flex-wrap items-center gap-2 ${className || ''}`}>\n      {shown.map(tag => {\n        const key = tag.tag_id ?? tag.id ?? tag.slug ?? tag.name;\n        const label = (tag.name || '').toUpperCase();\n        if (basePath && tag.slug) {\n          return (\n            <Link key={key} href={`${basePath}?tags=${tag.slug}`} className=\"inline-flex\">\n              <StatusBadge\n                text={label}\n                variant=\"card\"\n                className={`${BADGE_CLASS} cursor-pointer transition-colors hover:border-ods-accent`}\n              />\n            </Link>\n          );\n        }\n        return <StatusBadge key={key} text={label} variant=\"card\" className={BADGE_CLASS} />;\n      })}\n      {overflow > 0 && (\n        <StatusBadge text={`+${overflow}`} variant=\"card\" className={`${BADGE_CLASS} text-ods-text-secondary`} />\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport { Component, type ErrorInfo, type ReactNode } from 'react';\n\ninterface ErrorBoundaryProps {\n  fallback: ReactNode;\n  children: ReactNode;\n}\n\ninterface ErrorBoundaryState {\n  hasError: boolean;\n}\n\nexport class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {\n  constructor(props: ErrorBoundaryProps) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(_: Error): ErrorBoundaryState {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    console.error('Error caught by ErrorBoundary:', error, errorInfo);\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return this.props.fallback;\n    }\n\n    return this.props.children;\n  }\n}\n","'use client';\n\nimport type React from 'react';\nimport { useState, useRef, useEffect, useCallback, useMemo } from 'react';\nimport { cn } from '../../utils/cn';\nimport { fetchPriorityProp } from '../../utils/fetch-priority';\nimport { Badge } from '../ui/badge';\nimport { SectionSelector } from './section-selector';\n\n// ============================================================================\n// EXACT SPECIFICATIONS - NO LEGACY COMPATIBILITY\n// ============================================================================\n\n// 1. EXACT BREAKPOINTS\nconst DESKTOP_BREAKPOINT = 768; // px\n\n// 2. CONFIGURABLE CONTENT DIMENSIONS (externalized from .env.local and config)\nconst getContentDimensions = (config?: FigmaPrototypeViewerConfig) => {\n  // Default dimensions\n  const defaultMobile = { width: 343, height: 600 };\n  const defaultDesktop = { width: 944, height: 600 };\n\n  // Environment variables take highest priority\n  const envMobileWidth = process.env.NEXT_PUBLIC_FIGMA_MOBILE_WIDTH\n    ? parseInt(process.env.NEXT_PUBLIC_FIGMA_MOBILE_WIDTH)\n    : null;\n  const envMobileHeight = process.env.NEXT_PUBLIC_FIGMA_MOBILE_HEIGHT\n    ? parseInt(process.env.NEXT_PUBLIC_FIGMA_MOBILE_HEIGHT)\n    : null;\n  const envDesktopWidth = process.env.NEXT_PUBLIC_FIGMA_DESKTOP_WIDTH\n    ? parseInt(process.env.NEXT_PUBLIC_FIGMA_DESKTOP_WIDTH)\n    : null;\n  const envDesktopHeight = process.env.NEXT_PUBLIC_FIGMA_DESKTOP_HEIGHT\n    ? parseInt(process.env.NEXT_PUBLIC_FIGMA_DESKTOP_HEIGHT)\n    : null;\n\n  // Resolve mobile dimensions (env vars > config > defaults)\n  const mobileWidth = envMobileWidth ?? config?.mobileContentDimensions?.width ?? defaultMobile.width;\n  const mobileHeight = envMobileHeight ?? config?.mobileContentDimensions?.height ?? defaultMobile.height;\n\n  // Resolve desktop dimensions (env vars > config > defaults)\n  const desktopWidth = envDesktopWidth ?? config?.desktopContentDimensions?.width ?? defaultDesktop.width;\n  const desktopHeight = envDesktopHeight ?? config?.desktopContentDimensions?.height ?? defaultDesktop.height;\n\n  return {\n    mobile: { width: mobileWidth, height: mobileHeight },\n    desktop: { width: desktopWidth, height: desktopHeight },\n  } as const;\n};\n\n// 3. VIEW MODES\ntype ViewMode = 'DESKTOP' | 'MOBILE' | 'MOBILE_TOUCH';\n\n// 4. IFRAME STATES\ntype IframeState =\n  | 'INITIAL' // Not loaded yet\n  | 'LOADING' // Loading in progress\n  | 'READY' // Fully loaded and functional\n  | 'RELOADING' // Reloading due to device change\n  | 'SLEEP_RECOVERY'; // Recovering from phone sleep\n\n// 5. UNIFIED STATE INTERFACE\ninterface UnifiedState {\n  // Core States\n  viewMode: ViewMode;\n  iframeState: IframeState;\n\n  // Device Detection\n  screenWidth: number;\n  isTouchDevice: boolean;\n\n  // Scaling & Layout\n  scaling: {\n    scale: number;\n    marginX: number;\n    marginY: number;\n    scaledDimensions: { width: number; height: number };\n  };\n\n  // UI Control States\n  showSectionSelector: boolean;\n  showIframe: boolean;\n  showSkeleton: boolean;\n  showTouchOverlay: boolean;\n  showTouchBadge: boolean;\n  sectionsDisabled: boolean;\n\n  // Navigation State\n  activeSection: string;\n  isNavigating: boolean;\n\n  // Figma Integration\n  isInitialized: boolean;\n  currentNodeId: string | null;\n  embedUrl: string;\n  iframeKey: number;\n}\n\n// ============================================================================\n// CORE INTERFACES\n// ============================================================================\n\nexport interface FigmaPrototypeSection {\n  id: string;\n  number: string;\n  title: string;\n  description?: string;\n  startingNodeId: string;\n  mobileStartingNodeId?: string;\n}\n\nexport interface FigmaPrototypeViewerConfig {\n  // File configuration - can specify different files for mobile/desktop\n  fileKey?: string; // Legacy: single file for both (deprecated)\n  mobileFileKey?: string; // Separate mobile Figma file key\n  desktopFileKey?: string; // Separate desktop Figma file key\n\n  title: string;\n  sections: FigmaPrototypeSection[];\n  className?: string;\n  iframeClassName?: string;\n  controlsClassName?: string;\n  activeControlClassName?: string;\n  defaultSectionId?: string;\n  height?: string;\n  onSectionChange?: (sectionId: string) => void;\n\n  // Starting points - can be overridden by environment variables\n  mobileStartingPoint?: string; // Default mobile starting point\n  desktopStartingPoint?: string; // Default desktop starting point\n\n  // Content dimensions - can be overridden by environment variables\n  mobileContentDimensions?: { width: number; height: number }; // Mobile content size\n  desktopContentDimensions?: { width: number; height: number }; // Desktop content size\n\n  hideControls?: boolean;\n}\n\ninterface FigmaPrototypeViewerProps {\n  config: FigmaPrototypeViewerConfig;\n  activeSection?: string;\n  onSectionClick?: (sectionId: string) => void;\n}\n\n// Figma Events\ntype FigmaEventType = 'INITIAL_LOAD' | 'NEW_STATE' | 'PRESENTED_NODE_CHANGED';\n\ninterface FigmaEvent {\n  type: FigmaEventType;\n  data?: {\n    presentedNodeId?: string;\n    /** Everything else Figma's Embed Kit ships on the event — unread here. */\n    [key: string]: unknown;\n  };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null;\n}\n\n/**\n * Narrow a `postMessage` payload to a Figma Embed Kit event, or null when it\n * is not one of the three events this viewer handles.\n *\n * `MessageEvent.data` is typed `any` by the DOM lib, so the handler used to\n * read `.type` off an `any` and then assert the whole payload was a\n * `FigmaEvent`. The origin check upstream says the message came FROM Figma; it\n * says nothing about the shape Figma sent, and Embed Kit ships several event\n * types through this one channel.\n */\nfunction toFigmaEvent(data: unknown): FigmaEvent | null {\n  if (!isRecord(data)) return null;\n  const type = data.type;\n  if (type !== 'INITIAL_LOAD' && type !== 'NEW_STATE' && type !== 'PRESENTED_NODE_CHANGED') return null;\n  if (!isRecord(data.data)) return { type };\n\n  const presentedNodeId = data.data.presentedNodeId;\n  return {\n    type,\n    data: {\n      ...data.data,\n      presentedNodeId: typeof presentedNodeId === 'string' ? presentedNodeId : undefined,\n    },\n  };\n}\n\ninterface FigmaNavigationCommand {\n  type: 'NAVIGATE_TO_FRAME_AND_CLOSE_OVERLAYS';\n  data: { nodeId: string };\n}\n\n// ============================================================================\n// CORE CALCULATION FUNCTIONS\n// ============================================================================\n\n/**\n * Determines the current view mode based on screen size and touch capability\n */\nfunction getViewMode(screenWidth: number, isTouchDevice: boolean): ViewMode {\n  if (screenWidth >= DESKTOP_BREAKPOINT && !isTouchDevice) {\n    return 'DESKTOP';\n  } else if (screenWidth < DESKTOP_BREAKPOINT && !isTouchDevice) {\n    return 'MOBILE';\n  } else {\n    return 'MOBILE_TOUCH';\n  }\n}\n\n/**\n * Calculates optimal scaling and positioning for iframe content\n */\nfunction calculateScaling(\n  viewMode: ViewMode,\n  containerDimensions: { width: number; height: number },\n  config?: FigmaPrototypeViewerConfig,\n) {\n  const contentDimensions = getContentDimensions(config);\n  const contentSize = viewMode === 'DESKTOP' ? contentDimensions.desktop : contentDimensions.mobile;\n\n  const scaleX = containerDimensions.width / contentSize.width;\n  const scaleY = containerDimensions.height / contentSize.height;\n  const scale = Math.min(scaleX, scaleY, 1); // Never scale up, only down\n\n  // Calculate margins for centering\n  const scaledWidth = contentSize.width * scale;\n  const scaledHeight = contentSize.height * scale;\n  const marginX = (containerDimensions.width - scaledWidth) / 2;\n  const marginY = (containerDimensions.height - scaledHeight) / 2;\n\n  return {\n    scale,\n    marginX: Math.max(marginX, 4), // Very minimal margin to reduce white space\n    marginY: Math.max(marginY, 0), // Allow zero top margin to push content up\n    scaledDimensions: { width: scaledWidth, height: scaledHeight },\n  };\n}\n\n/**\n * Resolves the appropriate file key based on viewMode and environment variables\n */\nfunction resolveFileKey(config: FigmaPrototypeViewerConfig, viewMode: ViewMode): string {\n  // Check environment variables first (highest priority)\n  if (viewMode === 'DESKTOP') {\n    const envDesktopFileKey = process.env.NEXT_PUBLIC_FIGMA_DESKTOP_FILE_KEY;\n    if (envDesktopFileKey) return envDesktopFileKey;\n\n    // Fall back to config\n    if (config.desktopFileKey) return config.desktopFileKey;\n  } else {\n    const envMobileFileKey = process.env.NEXT_PUBLIC_FIGMA_MOBILE_FILE_KEY;\n    if (envMobileFileKey) return envMobileFileKey;\n\n    // Fall back to config\n    if (config.mobileFileKey) return config.mobileFileKey;\n  }\n\n  // Legacy fallback to single fileKey\n  if (config.fileKey) return config.fileKey;\n\n  // Throw error if no file key found\n  throw new Error(\n    `No Figma file key found for ${viewMode} mode. Please set NEXT_PUBLIC_FIGMA_${viewMode}_FILE_KEY or provide ${viewMode.toLowerCase()}FileKey in config.`,\n  );\n}\n\n/**\n * Resolves the starting point node ID based on viewMode, environment variables, and config\n */\nfunction resolveStartingPoint(\n  config: FigmaPrototypeViewerConfig,\n  viewMode: ViewMode,\n  section?: FigmaPrototypeSection,\n): string {\n  // If a specific section is provided, use its starting node\n  if (section) {\n    if (viewMode !== 'DESKTOP' && section.mobileStartingNodeId) {\n      return section.mobileStartingNodeId;\n    }\n    return section.startingNodeId;\n  }\n\n  // Check environment variables for default starting points (highest priority)\n  if (viewMode === 'DESKTOP') {\n    const envDesktopStarting = process.env.NEXT_PUBLIC_FIGMA_DESKTOP_STARTING_POINT;\n    if (envDesktopStarting) return envDesktopStarting;\n\n    // Fall back to config\n    if (config.desktopStartingPoint) return config.desktopStartingPoint;\n  } else {\n    const envMobileStarting = process.env.NEXT_PUBLIC_FIGMA_MOBILE_STARTING_POINT;\n    if (envMobileStarting) return envMobileStarting;\n\n    // Fall back to config\n    if (config.mobileStartingPoint) return config.mobileStartingPoint;\n  }\n\n  // Final fallback to first section\n  const firstSection = config.sections[0];\n  if (firstSection) {\n    return resolveStartingPoint(config, viewMode, firstSection);\n  }\n\n  throw new Error(\n    `No starting point found for ${viewMode} mode. Please set NEXT_PUBLIC_FIGMA_${viewMode}_STARTING_POINT or provide ${viewMode.toLowerCase()}StartingPoint in config.`,\n  );\n}\n\n/**\n * Generates Figma embed URL with exact parameters\n */\nfunction generateEmbedUrl(\n  config: FigmaPrototypeViewerConfig,\n  viewMode: ViewMode,\n  startingNodeId: string,\n  clientId: string,\n): string {\n  const fileKey = resolveFileKey(config, viewMode);\n  const embedHost = typeof window !== 'undefined' ? window.location.hostname : 'localhost';\n\n  const params = new URLSearchParams({\n    'node-id': startingNodeId.replace(':', '-'),\n    'client-id': clientId,\n    'embed-host': embedHost,\n    'hide-ui': '1',\n    'hotspot-hints': '0',\n    scaling: viewMode === 'DESKTOP' ? 'scale-down-width' : 'scale-down', // Change to scale-down for mobile\n    'starting-point-node-id': startingNodeId.replace(':', '-'),\n    mode: 'prototype',\n    chrome: 'DOCUMENTATION',\n  });\n\n  return `https://embed.figma.com/proto/${fileKey}?${params.toString()}`;\n}\n\n// ============================================================================\n// UNIFIED DRAWING FUNCTION\n// ============================================================================\n\ninterface UnifiedUIProps {\n  state: UnifiedState;\n  handlers: {\n    onSectionClick: (sectionId: string) => void;\n    onTouchStart: (e: React.TouchEvent) => void;\n  };\n  config: FigmaPrototypeViewerConfig;\n  iframeRef: React.RefObject<HTMLIFrameElement>;\n}\n\n/**\n * Unified drawing surface: renders all UI elements based on current state.\n *\n * A real COMPONENT, not a `renderUnifiedUI(...)` helper called from the\n * parent's render body. Handing a plain function the parent's event handlers\n * and the iframe ref is a call that reads refs during render as far as the\n * compiler is concerned — and, function-returning-JSX being what it is, the\n * subtree also had no identity of its own for memoization or devtools.\n */\nfunction UnifiedUI({ state, handlers, config, iframeRef }: UnifiedUIProps) {\n  const {\n    viewMode,\n    showSectionSelector,\n    showIframe,\n    showSkeleton,\n    showTouchOverlay,\n    showTouchBadge,\n    sectionsDisabled,\n    embedUrl,\n    iframeKey,\n  } = state;\n\n  return (\n    <div\n      className={cn(config.hideControls ? '' : 'grid grid-cols-1 gap-10 lg:grid-cols-[296px_1fr]', config.className)}\n    >\n      {/* Section Controls */}\n      {showSectionSelector && !config.hideControls && (\n        <div style={{ zIndex: 10 }}>\n          <SectionSelector\n            sections={config.sections.map(section => ({\n              id: section.id,\n              title: section.title,\n              description: section.description,\n              number: section.number,\n            }))}\n            activeSection={state.activeSection}\n            onSectionChange={handlers.onSectionClick}\n            disabled={sectionsDisabled}\n            className={cn('relative', config.controlsClassName)}\n            buttonClassName={config.controlsClassName}\n            activeButtonClassName={config.activeControlClassName}\n            layout=\"vertical\"\n            minHeight={viewMode !== 'DESKTOP' ? '76px' : '96px'}\n          />\n        </div>\n      )}\n\n      {/* Figma Container */}\n      <div\n        className={cn(\n          'relative w-full overflow-hidden rounded-lg border border-ods-border',\n          // viewMode === 'DESKTOP' && 'border border-ods-border',\n          config.iframeClassName,\n        )}\n        style={{\n          height:\n            viewMode === 'DESKTOP'\n              ? config.height || '800px'\n              : (() => {\n                  const contentDimensions = getContentDimensions(config);\n                  const mobileHeight = contentDimensions.mobile.height;\n                  // Use content height + some padding, but respect viewport limits\n                  const calculatedHeight = Math.max(mobileHeight + 100, 400); // minimum 400px\n                  return `${Math.min(calculatedHeight, window?.innerHeight * 0.85 || 650)}px`;\n                })(),\n          minHeight:\n            viewMode === 'DESKTOP'\n              ? 'auto'\n              : (() => {\n                  const contentDimensions = getContentDimensions(config);\n                  return `${Math.max(contentDimensions.mobile.height, 400)}px`;\n                })(),\n          maxHeight:\n            viewMode === 'DESKTOP'\n              ? 'none'\n              : (() => {\n                  const contentDimensions = getContentDimensions(config);\n                  return `${contentDimensions.mobile.height + 200}px`;\n                })(),\n          border: viewMode === 'DESKTOP' ? undefined : 'none',\n        }}\n      >\n        {/* Touch Overlay - Only for MOBILE_TOUCH mode */}\n        {showTouchOverlay && (\n          <div\n            className=\"absolute inset-0 h-full w-full\"\n            data-touch-overlay=\"true\"\n            style={{\n              pointerEvents: 'auto',\n              touchAction: 'pan-y pinch-zoom',\n              zIndex: 2,\n              background: 'transparent',\n              position: 'absolute',\n              top: 0,\n              left: 0,\n              right: 0,\n              bottom: 0,\n            }}\n            onTouchStart={handlers.onTouchStart}\n          />\n        )}\n\n        {/* Touch Badge - Only for MOBILE_TOUCH mode */}\n        {showTouchBadge && (\n          <div className=\"absolute bottom-4 left-4 z-10\">\n            <Badge variant=\"secondary\" className=\"bg-ods-overlay text-ods-text-on-dark backdrop-blur-sm\">\n              Tap twice to click\n            </Badge>\n          </div>\n        )}\n\n        {/* Figma Iframe */}\n        <iframe\n          ref={iframeRef}\n          key={iframeKey}\n          src={embedUrl}\n          className=\"h-full w-full border-0\"\n          style={{\n            background: 'transparent',\n            zIndex: 1,\n            display: showIframe ? 'block' : 'none',\n            visibility: showIframe ? 'visible' : 'hidden',\n            // Let Figma handle all scaling internally\n            width: '100%',\n            height: '100%',\n            transform: 'none',\n            // Remove any default iframe styling\n            border: 'none',\n            outline: 'none',\n            overflow: 'hidden',\n          }}\n          allow=\"clipboard-write; clipboard-read; fullscreen\"\n          referrerPolicy=\"no-referrer\"\n          sandbox=\"allow-scripts allow-same-origin allow-popups allow-presentation\"\n          title={config.title}\n          loading=\"eager\"\n          {...fetchPriorityProp('high')}\n        />\n\n        {/* Loading Skeleton */}\n        {showSkeleton && (\n          <div className=\"absolute inset-0 z-10 flex h-full w-full animate-pulse items-center justify-center rounded-lg bg-ods-skeleton\">\n            <div className=\"text-center\">\n              <div className=\"text-ods-text-primary text-h2\">\n                Warming up <span className=\"text-ods-open-yellow\">Fae</span> and{' '}\n                <span className=\"text-ods-open-yellow\">Mingo</span>\n              </div>\n              <div className=\"mt-2 text-ods-text-secondary text-h6\">Loading AI Co-Pilots...</div>\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n// ============================================================================\n// MAIN COMPONENT\n// ============================================================================\n\nexport const FigmaPrototypeViewer: React.FC<FigmaPrototypeViewerProps> = ({\n  config,\n  activeSection: externalActiveSection,\n  onSectionClick: externalOnSectionClick,\n}) => {\n  // Pulled out of `config` once: the postMessage listener below depends on\n  // these two and nothing else, so it must not re-subscribe every time a caller\n  // rebuilds the config object.\n  const { sections: configSections, onSectionChange } = config;\n\n  // Environment & Client Setup\n  const clientId = process.env.NEXT_PUBLIC_FIGMA_CLIENT_ID || 'UTQPwZHR9OZp68TTGPFFi5';\n  const showDebugPanel = process.env.NEXT_PUBLIC_FIGMA_DEBUG === 'true';\n\n  // Refs\n  const iframeRef = useRef<HTMLIFrameElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const navTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const touchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  // Device Detection State\n  const [screenWidth, setScreenWidth] = useState(\n    typeof window !== 'undefined' ? window.innerWidth : DESKTOP_BREAKPOINT,\n  );\n\n  // Simple, reliable touch detection\n  // Read once at mount and never updated — a device does not grow a touch\n  // screen mid-session, and the pointer type is not what drives the layout.\n  const [isTouchDevice] = useState(() => {\n    if (typeof window === 'undefined') return false;\n    // Most reliable method: check if touch events are supported\n    return 'ontouchstart' in window;\n  });\n\n  // Iframe Management State\n  const [iframeState, setIframeState] = useState<IframeState>('INITIAL');\n  const [iframeKey, setIframeKey] = useState(0);\n  const [isInitialized, setIsInitialized] = useState(false);\n  const [currentNodeId, setCurrentNodeId] = useState<string | null>(null);\n\n  // Navigation State\n  const [internalActiveSection, setInternalActiveSection] = useState<string>(\n    config.defaultSectionId || config.sections[0]?.id || '',\n  );\n  const activeSection = externalActiveSection || internalActiveSection;\n  const [isNavigating, setIsNavigating] = useState(false);\n\n  // Container Dimensions for Scaling\n  const [containerDimensions, setContainerDimensions] = useState({ width: 1200, height: 800 });\n\n  // Calculate Current View Mode\n  const viewMode = useMemo(() => getViewMode(screenWidth, isTouchDevice), [screenWidth, isTouchDevice]);\n\n  // Calculate Scaling\n  const scaling = useMemo(\n    () => calculateScaling(viewMode, containerDimensions, config),\n    [viewMode, containerDimensions, config],\n  );\n\n  // Generate Embed URL\n  const embedUrl = useMemo(() => {\n    try {\n      const startingNodeId = resolveStartingPoint(config, viewMode);\n      return generateEmbedUrl(config, viewMode, startingNodeId, clientId);\n    } catch (error) {\n      console.error('[FigmaPrototypeViewer] Failed to generate embed URL:', error);\n      return '';\n    }\n  }, [config, viewMode, clientId]);\n\n  // Create Unified State\n  const unifiedState: UnifiedState = useMemo(\n    () => ({\n      viewMode,\n      iframeState,\n      screenWidth,\n      isTouchDevice,\n      scaling,\n      showSectionSelector: true,\n      showIframe: iframeState === 'READY',\n      showSkeleton: iframeState !== 'READY',\n      showTouchOverlay: viewMode === 'MOBILE_TOUCH' && iframeState === 'READY',\n      showTouchBadge: viewMode === 'MOBILE_TOUCH' && iframeState === 'READY',\n      sectionsDisabled: iframeState !== 'READY' || isNavigating,\n      activeSection,\n      isNavigating,\n      isInitialized,\n      currentNodeId,\n      embedUrl,\n      iframeKey,\n    }),\n    [\n      viewMode,\n      iframeState,\n      screenWidth,\n      isTouchDevice,\n      scaling,\n      activeSection,\n      isNavigating,\n      isInitialized,\n      currentNodeId,\n      embedUrl,\n      iframeKey,\n    ],\n  );\n\n  // ============================================================================\n  // EVENT HANDLERS\n  // ============================================================================\n\n  // Window Resize Handler\n  useEffect(() => {\n    const handleResize = () => {\n      const newWidth = window.innerWidth;\n      setScreenWidth(newWidth);\n\n      // Update container dimensions\n      if (containerRef.current) {\n        const rect = containerRef.current.getBoundingClientRect();\n        setContainerDimensions({ width: rect.width, height: rect.height });\n      }\n    };\n\n    // Initial measurement\n    handleResize();\n\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  // Touch device detection is now done once at initialization for consistency\n\n  // View Mode Change Handler (triggers iframe reload)\n  //\n  // This was already React's \"adjust state while rendering\" pattern — a\n  // previous-value tracker and a compare — just written as an effect, which is\n  // the one place it costs an extra render pass AND lets the render in between\n  // commit the stale desktop/mobile iframe as `READY`. Moved into the render\n  // body where the pattern belongs; the tracker update makes it idempotent, so\n  // the pass React schedules for it takes the early exit.\n  const [lastViewMode, setLastViewMode] = useState(viewMode);\n  if (lastViewMode !== viewMode) {\n    setLastViewMode(viewMode);\n    if (iframeState === 'READY') {\n      setIframeState('RELOADING');\n      setIframeKey(prev => prev + 1);\n    }\n  }\n\n  // Visibility Change Handler (phone sleep recovery)\n  useEffect(() => {\n    const handleVisibilityChange = () => {\n      if (document.visibilityState === 'visible' && iframeState === 'READY') {\n        console.log('[Sleep Recovery] Reloading iframe after sleep');\n        setIframeState('SLEEP_RECOVERY');\n        setIframeKey(prev => prev + 1);\n      }\n    };\n\n    document.addEventListener('visibilitychange', handleVisibilityChange);\n    return () => document.removeEventListener('visibilitychange', handleVisibilityChange);\n  }, [iframeState]);\n\n  // Figma Event Handler\n  useEffect(() => {\n    const handleMessage = (event: MessageEvent) => {\n      if (event.origin !== 'https://www.figma.com') return;\n\n      const figmaEvent = toFigmaEvent(event.data);\n\n      if (figmaEvent) {\n        console.log('[Figma Event]', figmaEvent.type, viewMode);\n\n        switch (figmaEvent.type) {\n          case 'INITIAL_LOAD':\n          case 'NEW_STATE':\n            setIsInitialized(true);\n            setIframeState('READY');\n            break;\n\n          case 'PRESENTED_NODE_CHANGED':\n            if (figmaEvent.data?.presentedNodeId) {\n              setCurrentNodeId(figmaEvent.data.presentedNodeId);\n\n              // Auto-sync active section if not manually navigating\n              if (!isNavigating) {\n                const matchingSection = configSections.find(s => {\n                  const desktopMatch =\n                    s.startingNodeId === figmaEvent.data?.presentedNodeId ||\n                    s.startingNodeId.replace(':', '-') === figmaEvent.data?.presentedNodeId;\n                  const mobileMatch =\n                    s.mobileStartingNodeId === figmaEvent.data?.presentedNodeId ||\n                    s.mobileStartingNodeId?.replace(':', '-') === figmaEvent.data?.presentedNodeId;\n                  return desktopMatch || mobileMatch;\n                });\n\n                if (matchingSection && matchingSection.id !== activeSection) {\n                  if (!externalActiveSection) {\n                    setInternalActiveSection(matchingSection.id);\n                  }\n                  onSectionChange?.(matchingSection.id);\n                }\n              }\n            }\n            break;\n        }\n      }\n    };\n\n    window.addEventListener('message', handleMessage);\n    return () => window.removeEventListener('message', handleMessage);\n  }, [configSections, activeSection, isNavigating, externalActiveSection, onSectionChange, viewMode]);\n\n  // Navigation Function\n  const navigateToSection = useCallback(\n    (sectionId: string) => {\n      const section = config.sections.find(s => s.id === sectionId);\n      if (!section || !iframeRef.current?.contentWindow || !isInitialized) {\n        return;\n      }\n\n      setIsNavigating(true);\n\n      // Use resolver to get the correct node ID for this section\n      const nodeId = resolveStartingPoint(config, viewMode, section);\n\n      // Update active section\n      if (!externalActiveSection) {\n        setInternalActiveSection(sectionId);\n      }\n      config.onSectionChange?.(sectionId);\n\n      // Send Figma command\n      const command: FigmaNavigationCommand = {\n        type: 'NAVIGATE_TO_FRAME_AND_CLOSE_OVERLAYS',\n        data: { nodeId },\n      };\n\n      try {\n        iframeRef.current.contentWindow.postMessage(command, 'https://www.figma.com');\n        console.log('[Navigation]', sectionId, '→', nodeId, viewMode);\n      } catch (error) {\n        console.error('[Navigation Error]', error);\n      }\n\n      // Reset navigation flag (tracked via ref for cleanup on unmount)\n      if (navTimerRef.current) clearTimeout(navTimerRef.current);\n      navTimerRef.current = setTimeout(() => setIsNavigating(false), 500);\n    },\n    [config, isInitialized, viewMode, externalActiveSection],\n  );\n\n  // Section Click Handler\n  const handleSectionClick = useCallback(\n    (sectionId: string) => {\n      const sectionsDisabled = iframeState !== 'READY' || isNavigating;\n\n      if (sectionId === activeSection || sectionsDisabled) {\n        return;\n      }\n\n      if (externalOnSectionClick) {\n        externalOnSectionClick(sectionId);\n      } else {\n        navigateToSection(sectionId);\n      }\n    },\n    [activeSection, iframeState, isNavigating, externalOnSectionClick, navigateToSection],\n  );\n\n  // Touch Handler for Mobile Touch Mode\n  const handleTouchStart = useCallback((e: React.TouchEvent) => {\n    // Simple approach: on any touch, disable overlay for longer to allow normal interaction,\n    // then re-enable for scrolling\n    const overlayElement = e.currentTarget as HTMLElement;\n\n    console.log('[Touch] Touch detected, allowing iframe interaction');\n\n    // Disable overlay to allow touch to pass through to iframe\n    overlayElement.style.pointerEvents = 'none';\n\n    // Re-enable overlay after interaction window (tracked via ref for cleanup on unmount)\n    if (touchTimerRef.current) clearTimeout(touchTimerRef.current);\n    touchTimerRef.current = setTimeout(() => {\n      if (overlayElement && overlayElement.style.pointerEvents === 'none') {\n        overlayElement.style.pointerEvents = 'auto';\n        console.log('[Touch] Overlay re-enabled for scrolling');\n      }\n    }, 500);\n  }, []);\n\n  // External Active Section Change\n  useEffect(() => {\n    if (externalActiveSection && externalActiveSection !== activeSection && isInitialized) {\n      navigateToSection(externalActiveSection);\n    }\n  }, [externalActiveSection, activeSection, isInitialized, navigateToSection]);\n\n  // Cleanup all pending timers on unmount\n  useEffect(() => {\n    return () => {\n      if (navTimerRef.current) clearTimeout(navTimerRef.current);\n      if (touchTimerRef.current) clearTimeout(touchTimerRef.current);\n    };\n  }, []);\n\n  // ============================================================================\n  // RENDER\n  // ============================================================================\n\n  return (\n    <div ref={containerRef}>\n      <UnifiedUI\n        state={unifiedState}\n        handlers={{\n          onSectionClick: handleSectionClick,\n          onTouchStart: handleTouchStart,\n        }}\n        config={config}\n        iframeRef={iframeRef as React.RefObject<HTMLIFrameElement>}\n      />\n\n      {/* Debug Panel */}\n      {showDebugPanel && (\n        <div className=\"mt-6 rounded-md border border-ods-border bg-ods-card p-4\">\n          <h3 className=\"mb-3 text-ods-text-primary text-h3\">🔍 Debug Panel</h3>\n\n          <div className=\"grid grid-cols-2 gap-4 text-h6\">\n            <div>\n              <span className=\"text-ods-text-secondary\">View Mode:</span>{' '}\n              <span className=\"font-mono text-ods-text-primary\">{viewMode}</span>\n            </div>\n            <div>\n              <span className=\"text-ods-text-secondary\">Iframe State:</span>{' '}\n              <span className=\"font-mono text-ods-text-primary\">{iframeState}</span>\n            </div>\n            <div>\n              <span className=\"text-ods-text-secondary\">Screen:</span>{' '}\n              <span className=\"text-ods-text-primary\">{screenWidth}px</span>\n            </div>\n            <div>\n              <span className=\"text-ods-text-secondary\">Touch:</span>{' '}\n              <span className=\"text-ods-text-primary\">{isTouchDevice ? 'Yes' : 'No'}</span>\n            </div>\n            <div>\n              <span className=\"text-ods-text-secondary\">Scale:</span>{' '}\n              <span className=\"text-ods-text-primary\">{scaling.scale.toFixed(2)}</span>\n            </div>\n            <div>\n              <span className=\"text-ods-text-secondary\">Section:</span>{' '}\n              <span className=\"text-ods-text-primary\">{activeSection}</span>\n            </div>\n            <div className=\"col-span-2\">\n              <span className=\"text-ods-text-secondary\">Active Node ID:</span>{' '}\n              <span className=\"font-mono text-ods-text-primary\">{currentNodeId || 'Not detected'}</span>\n            </div>\n          </div>\n\n          <div className=\"mt-3 text-ods-text-secondary text-h6\">\n            Margins: {scaling.marginX.toFixed(0)}px × {scaling.marginY.toFixed(0)}px\n          </div>\n\n          {/* Additional debug info */}\n          <div className=\"mt-3 border-t border-ods-border pt-3\">\n            <div className=\"space-y-1 text-ods-text-secondary text-h6\">\n              <div>\n                <span className=\"text-ods-text-secondary\">File Key:</span>{' '}\n                <span className=\"font-mono text-ods-text-primary\">\n                  {(() => {\n                    try {\n                      return resolveFileKey(config, viewMode);\n                    } catch {\n                      return 'Error';\n                    }\n                  })()}\n                </span>\n              </div>\n              <div>\n                <span className=\"text-ods-text-secondary\">Starting Point:</span>{' '}\n                <span className=\"font-mono text-ods-text-primary\">\n                  {(() => {\n                    try {\n                      return resolveStartingPoint(config, viewMode);\n                    } catch {\n                      return 'Error';\n                    }\n                  })()}\n                </span>\n              </div>\n              <div>\n                <span className=\"text-ods-text-secondary\">Content Dimensions:</span>{' '}\n                <span className=\"font-mono text-ods-text-primary\">\n                  {(() => {\n                    try {\n                      const dims = getContentDimensions(config);\n                      const current = viewMode === 'DESKTOP' ? dims.desktop : dims.mobile;\n                      return `${current.width}x${current.height}`;\n                    } catch {\n                      return 'Error';\n                    }\n                  })()}\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default FigmaPrototypeViewer;\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { Button } from '../ui/button';\nimport { StatusBadge } from '../ui/status-badge';\n\nexport interface SectionItem {\n  id: string;\n  title: string;\n  subtitle?: string;\n  description?: string;\n  number?: string;\n  disabled?: boolean;\n  leftIcon?: React.ReactNode;\n  badge?: {\n    text: string;\n    variant?: 'card' | 'button';\n    colorScheme?: 'cyan' | 'pink' | 'yellow' | 'green' | 'purple' | 'default';\n  };\n  screenshots?: {\n    src: string;\n    alt: string;\n    position: 'left' | 'center' | 'right';\n  }[];\n}\n\nexport interface SectionSelectorProps {\n  sections: SectionItem[];\n  activeSection: string;\n  onSectionChange: (sectionId: string) => void;\n  disabled?: boolean;\n  className?: string;\n  buttonClassName?: string;\n  activeButtonClassName?: string;\n  layout?: 'vertical' | 'wrap';\n  buttonWidth?: 'auto' | 'full' | 'responsive';\n  minHeight?: string;\n  showDescription?: boolean;\n}\n\n// Button component for consistency\nconst SectionButton: React.FC<{\n  section: SectionItem;\n  isActive: boolean;\n  disabled: boolean;\n  onClick: () => void;\n  layout: 'vertical' | 'wrap';\n  widthClasses: string;\n  buttonClassName?: string;\n  activeButtonClassName?: string;\n  minHeight?: string;\n  showDescription?: boolean;\n}> = ({\n  section,\n  isActive,\n  disabled,\n  onClick,\n  layout,\n  widthClasses,\n  buttonClassName,\n  activeButtonClassName,\n  minHeight = layout === 'vertical' ? '96px' : '76px',\n  showDescription = true,\n}) => {\n  const titleClasses = 'text-ods-text-primary';\n  const subtitleClasses = 'text-ods-text-secondary';\n  const numberClasses = 'text-ods-accent';\n\n  const isDisabled = section.disabled || disabled;\n\n  return (\n    <Button\n      onClick={onClick}\n      disabled={isDisabled}\n      variant=\"outline\"\n      className={cn(\n        '!items-start !justify-start bg-ods-card !text-left shadow-ods-card hover:bg-ods-card-hover',\n        isActive && 'border-ods-accent',\n        widthClasses,\n        buttonClassName,\n        isActive && activeButtonClassName,\n        layout === 'vertical'\n          ? '!h-auto !min-h-[80px] !px-4 !py-4'\n          : '!h-auto !min-h-[76px] !whitespace-normal !text-left',\n      )}\n      style={{\n        minHeight,\n        touchAction: 'manipulation',\n        WebkitTapHighlightColor: 'transparent',\n        textAlign: 'left',\n        justifyContent: 'flex-start',\n        alignItems: 'flex-start',\n      }}\n    >\n      {layout === 'vertical' ? (\n        // Vertical layout with optional number prefix and leftIcon\n        <div className=\"flex w-full items-start gap-3\">\n          {section.leftIcon && (\n            <div className=\"mt-0.5 shrink-0 text-ods-text-primary opacity-70\">{section.leftIcon}</div>\n          )}\n          {section.number && <span className={cn(numberClasses, 'shrink-0 text-h3')}>{section.number}</span>}\n          <div className=\"min-w-0 flex-1 text-left\">\n            <div className=\"flex flex-wrap items-center gap-2\">\n              <p className={cn(titleClasses, 'whitespace-normal break-words text-h4')}>{section.title}</p>\n              {section.badge && (\n                <StatusBadge\n                  text={section.badge.text}\n                  variant={section.badge.variant || 'button'}\n                  colorScheme={section.badge.colorScheme || 'default'}\n                />\n              )}\n            </div>\n            {section.description && showDescription && (\n              <p className={cn(subtitleClasses, 'mt-1 whitespace-normal break-words text-h6')}>{section.description}</p>\n            )}\n          </div>\n        </div>\n      ) : (\n        // Wrap layout with title, subtitle, and optional leftIcon\n        <div className=\"flex h-full w-full items-start justify-start gap-3\" style={{ textAlign: 'left' }}>\n          {section.leftIcon && (\n            <div className=\"mt-0.5 shrink-0 text-ods-text-primary opacity-70\">{section.leftIcon}</div>\n          )}\n          <div className=\"flex flex-1 flex-col items-start justify-start gap-1\">\n            <div className=\"flex w-full flex-wrap items-start gap-2\">\n              <span className={cn(titleClasses, 'text-h3')} style={{ textAlign: 'left' }}>\n                {section.title}\n              </span>\n              {section.badge && (\n                <StatusBadge\n                  text={section.badge.text}\n                  variant={section.badge.variant || 'button'}\n                  colorScheme={section.badge.colorScheme || 'default'}\n                />\n              )}\n            </div>\n            {section.subtitle && (\n              <div className={cn(subtitleClasses, 'w-full text-h6')} style={{ textAlign: 'left' }}>\n                {section.subtitle}\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n    </Button>\n  );\n};\n\nexport const SectionSelector: React.FC<SectionSelectorProps> = ({\n  sections,\n  activeSection,\n  onSectionChange,\n  disabled = false,\n  className,\n  buttonClassName,\n  activeButtonClassName,\n  layout = 'vertical',\n  buttonWidth = 'auto',\n  minHeight,\n  showDescription = true,\n}) => {\n  const containerClasses = cn(\n    layout === 'wrap' ? 'flex flex-wrap gap-2 md:gap-4 lg:gap-6' : 'flex flex-col gap-2',\n    className,\n  );\n\n  const getButtonWidthClasses = () => {\n    switch (buttonWidth) {\n      case 'full':\n        return 'w-full';\n      case 'responsive':\n        return 'w-full md:w-[calc(50%-8px)] lg:w-auto';\n      default:\n        return '';\n    }\n  };\n\n  const widthClasses = getButtonWidthClasses();\n\n  return (\n    <div className={containerClasses}>\n      {sections.map(section => (\n        <SectionButton\n          key={section.id}\n          section={section}\n          isActive={activeSection === section.id}\n          disabled={disabled}\n          onClick={() => onSectionChange(section.id)}\n          layout={layout}\n          widthClasses={widthClasses}\n          buttonClassName={buttonClassName}\n          activeButtonClassName={activeButtonClassName}\n          minHeight={minHeight}\n          showDescription={showDescription}\n        />\n      ))}\n    </div>\n  );\n};\n\nexport default SectionSelector;\n","'use client';\n\nimport type { FC, ReactNode, RefObject } from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';\nimport { cn } from '../../utils/cn';\nimport { CheckboxCheckmarkIcon } from '../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon';\nimport { Button } from '../ui';\n\n// Types for filter configuration\nexport interface FilterOption {\n  id: string;\n  label: string;\n  value: string | number | boolean;\n  count?: number;\n  type?: 'option' | 'separator';\n}\n\nexport interface FilterSection {\n  id: string;\n  title: string;\n  type: 'checkbox' | 'radio' | 'select';\n  options: FilterOption[];\n  allowSelectAll?: boolean;\n  defaultSelected?: string[];\n}\n\nexport interface FiltersDropdownProps {\n  triggerElement?: ReactNode; // Custom trigger element\n  triggerLabel?: string; // Label for default trigger button\n  sections: FilterSection[];\n  onApply: (filters: Record<string, string[]>) => void;\n  onReset?: () => void;\n  className?: string;\n  dropdownClassName?: string;\n  /**\n   * Currently applied filters to preserve state when reopening.\n   * Pass the same filters that were applied via onApply callback.\n   *\n   * @example\n   * ```tsx\n   * const { appliedFilters, handleApply } = useFiltersDropdown(sections)\n   *\n   * <FiltersDropdown\n   *   sections={sections}\n   *   onApply={handleApply}\n   *   currentFilters={appliedFilters}\n   *   triggerLabel=\"STATUS\"\n   * />\n   * ```\n   */\n  currentFilters?: Record<string, string[]>;\n  placement?: 'bottom-start' | 'bottom-end' | 'bottom';\n  /**\n   * Enable responsive mobile behavior (full width on mobile)\n   * @default true\n   */\n  responsive?: boolean;\n}\n\n// Custom checkbox component\nconst FilterCheckbox: FC<{\n  checked: boolean;\n  disabled?: boolean;\n  className?: string;\n}> = ({ checked, disabled = false, className }) => {\n  return (\n    <div\n      role=\"checkbox\"\n      aria-checked={checked}\n      className={cn(\n        'relative h-6 w-6 shrink-0 rounded-[6px] transition-all duration-150',\n        checked ? 'bg-ods-accent' : 'bg-ods-bg-surface',\n        !checked && 'border-2 border-ods-border',\n        disabled && 'opacity-50',\n        className,\n      )}\n    >\n      {checked && (\n        <div className=\"absolute inset-0 flex items-center justify-center text-ods-text-on-accent\">\n          <CheckboxCheckmarkIcon size={10} />\n        </div>\n      )}\n    </div>\n  );\n};\n\n// Animation timings (ms). Must stay in sync with the `duration-200` Tailwind\n// class on the dropdown panel below.\nconst ANIMATION_MS = 200;\n\nexport const FiltersDropdown: FC<FiltersDropdownProps> = ({\n  triggerElement,\n  triggerLabel = 'Filters',\n  sections,\n  onApply,\n  onReset,\n  className,\n  dropdownClassName,\n  currentFilters,\n  placement = 'bottom-start',\n  responsive = true,\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  // `shouldRender` controls mount/unmount.\n  // `isVisible` controls the open/closed *visual* state and drives the CSS transition.\n  // Splitting them lets us mount in the closed visual state, then flip to open on\n  // the next frame so the browser actually transitions (instead of teleporting).\n  const [shouldRender, setShouldRender] = useState(false);\n  const [isVisible, setIsVisible] = useState(false);\n  const [measuredMobile, setIsMobile] = useState(false);\n  const [actualPlacement, setActualPlacement] = useState(placement);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const triggerRef = useRef<HTMLButtonElement | HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // Drive mount/unmount and visual state from `isOpen`.\n  //\n  // Open  : mount with closed styles → wait two frames, flip to visible → CSS transition runs.\n  // Close : flip to invisible → CSS transition runs → unmount after duration.\n  //\n  // Because we use CSS transitions (not @keyframes), interrupting mid-animation\n  // smoothly continues from the current computed value — no flicker.\n  // The two INSTANT halves are adjusted while rendering: mounting on open and\n  // dropping the visible class on close are both edges the render already knows\n  // about, and doing them here rather than from the effect removes a render\n  // pass from every open and every close. Each is guarded on the state it\n  // writes, so the pass React schedules for the adjustment takes the early exit.\n  // The effect below is left owning only the two halves that genuinely need a\n  // timer: the double-rAF entry and the delayed unmount.\n  if (isOpen && !shouldRender) setShouldRender(true);\n  if (!isOpen && isVisible) setIsVisible(false);\n\n  useEffect(() => {\n    if (isOpen) {\n      // Double rAF: the first frame guarantees the element is mounted and the\n      // closed styles have been painted; the second flips to the open state so\n      // the browser sees a transitionable property change. A single rAF can\n      // sometimes fire in the same frame as mount, suppressing the transition.\n      let id2 = 0;\n      const id1 = requestAnimationFrame(() => {\n        id2 = requestAnimationFrame(() => setIsVisible(true));\n      });\n      return () => {\n        cancelAnimationFrame(id1);\n        cancelAnimationFrame(id2);\n      };\n    }\n    const t = setTimeout(() => setShouldRender(false), ANIMATION_MS);\n    return () => clearTimeout(t);\n  }, [isOpen]);\n\n  // Check if mobile on mount and resize. `responsive: false` means \"never treat\n  // this as mobile\" — a fact about the PROP, so it masks the measurement below\n  // instead of being written into it from the effect.\n  const isMobile = responsive && measuredMobile;\n\n  useEffect(() => {\n    if (!responsive) return undefined;\n\n    const checkMobile = () => {\n      setIsMobile(window.innerWidth < 640); // sm breakpoint\n    };\n\n    checkMobile();\n    window.addEventListener('resize', checkMobile);\n    return () => window.removeEventListener('resize', checkMobile);\n  }, [responsive]);\n\n  useEffect(() => {\n    if (!isOpen || isMobile || !triggerRef.current) return undefined;\n\n    const calculateOptimalPlacement = () => {\n      const trigger = triggerRef.current;\n      if (!trigger) return;\n\n      const triggerRect = trigger.getBoundingClientRect();\n      const dropdownWidth = 320; // Fixed width from the dropdown\n      const viewportWidth = window.innerWidth;\n\n      const spaceRight = viewportWidth - triggerRect.right;\n      const spaceLeft = triggerRect.left;\n\n      let optimalPlacement = placement;\n\n      if (placement === 'bottom-start' && spaceRight < dropdownWidth && spaceLeft >= dropdownWidth) {\n        optimalPlacement = 'bottom-end';\n      } else if (placement === 'bottom-end' && spaceLeft < dropdownWidth && spaceRight >= dropdownWidth) {\n        optimalPlacement = 'bottom-start';\n      } else if (placement === 'bottom' && (spaceLeft < dropdownWidth / 2 || spaceRight < dropdownWidth / 2)) {\n        optimalPlacement = spaceLeft > spaceRight ? 'bottom-end' : 'bottom-start';\n      }\n\n      setActualPlacement(optimalPlacement);\n    };\n\n    calculateOptimalPlacement();\n    window.addEventListener('resize', calculateOptimalPlacement);\n\n    return () => window.removeEventListener('resize', calculateOptimalPlacement);\n  }, [isOpen, isMobile, placement]);\n\n  // Mobile: the panel is `fixed`, so its `top` is a live measurement of where\n  // the trigger currently sits. Applied to the node in a layout effect rather\n  // than read during render — `getBoundingClientRect()` in the render body\n  // measured the PREVIOUS commit's layout, so the panel opened against a\n  // trigger position that could already be wrong (page scrolled between\n  // renders, chips above the trigger wrapping to a second line) and had no way\n  // to correct itself. Unconditional so every commit re-measures; runs before\n  // paint, so the panel never appears in the wrong place first.\n  useIsomorphicLayoutEffect(() => {\n    const dropdown = dropdownRef.current;\n    if (!dropdown) return;\n    if (!isMobile) {\n      // Desktop is `absolute` + placement classes — an inline top from a\n      // previous mobile layout would fight them.\n      dropdown.style.top = '';\n      return;\n    }\n    const trigger = triggerRef.current;\n    dropdown.style.top = `${trigger ? trigger.getBoundingClientRect().bottom + window.scrollY + 8 : 0}px`;\n  });\n\n  // Initialize state with current filters or defaults\n  const [selectedFilters, setSelectedFilters] = useState<Record<string, string[]>>(() => {\n    if (currentFilters) {\n      return { ...currentFilters };\n    }\n    const initial: Record<string, string[]> = {};\n    sections.forEach(section => {\n      initial[section.id] = section.defaultSelected || [];\n    });\n    return initial;\n  });\n\n  // Sync with external changes to currentFilters. Keyed on the serialised\n  // CONTENT, not the object identity — a parent that rebuilds the same filters\n  // object every render must not stomp local state. Read back from the same\n  // string so the effect has no stale-closure gap.\n  // Adjusted while rendering — React's documented prop-sync pattern — rather\n  // than from an effect: the checkboxes render from `selectedFilters`, so an\n  // externally applied filter set arrived one frame after the panel had already\n  // painted the previous selection.\n  const currentFiltersStr = currentFilters ? JSON.stringify(currentFilters) : '';\n  const [syncedFiltersStr, setSyncedFiltersStr] = useState(currentFiltersStr);\n  if (syncedFiltersStr !== currentFiltersStr) {\n    setSyncedFiltersStr(currentFiltersStr);\n    if (currentFiltersStr) {\n      setSelectedFilters(JSON.parse(currentFiltersStr) as Record<string, string[]>);\n    }\n  }\n\n  // Closing must NOT reset `actualPlacement`.\n  //\n  // The panel stays mounted through its exit transition (see `shouldRender`), so\n  // a reset here lands on something still on screen: a dropdown that had flipped\n  // to the opposite side snaps back the moment it starts fading, sliding roughly\n  // its own width sideways on the way out. It showed up on whichever filter sits\n  // nearest an edge — the last column on tablet, and the same column on desktop\n  // once the window is narrow enough to force the flip.\n  //\n  // Nothing is lost by dropping it: opening recomputes the placement from the\n  // trigger's current rect (see the effect above), so the value left behind is\n  // never read again.\n\n  // Close dropdown when clicking outside\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      // Check if click is outside the entire component container\n      if (containerRef.current && !containerRef.current.contains(event.target as Node)) {\n        setIsOpen(false);\n      }\n    };\n\n    if (!isOpen) return undefined;\n\n    // Use a small delay to avoid closing immediately after opening\n    const timer = setTimeout(() => {\n      document.addEventListener('mousedown', handleClickOutside);\n    }, 0);\n\n    return () => {\n      clearTimeout(timer);\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [isOpen]);\n\n  // Handle escape key\n  useEffect(() => {\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key === 'Escape' && isOpen) {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener('keydown', handleEscape);\n    return () => document.removeEventListener('keydown', handleEscape);\n  }, [isOpen]);\n\n  const handleToggleOption = (sectionId: string, optionId: string, sectionType: string) => {\n    setSelectedFilters(prev => {\n      const current = prev[sectionId] || [];\n\n      if (sectionType === 'radio') {\n        return {\n          ...prev,\n          [sectionId]: [optionId],\n        };\n      } else {\n        if (current.includes(optionId)) {\n          return {\n            ...prev,\n            [sectionId]: current.filter(id => id !== optionId),\n          };\n        } else {\n          return {\n            ...prev,\n            [sectionId]: [...current, optionId],\n          };\n        }\n      }\n    });\n  };\n\n  const handleSelectAll = (sectionId: string, section: FilterSection) => {\n    const allOptionIds = section.options.map(opt => opt.id);\n    const currentSelection = selectedFilters[sectionId] || [];\n    const isAllSelected = allOptionIds.every(id => currentSelection.includes(id));\n\n    setSelectedFilters(prev => ({\n      ...prev,\n      [sectionId]: isAllSelected ? [] : allOptionIds,\n    }));\n  };\n\n  const handleReset = () => {\n    const defaults: Record<string, string[]> = {};\n    sections.forEach(section => {\n      defaults[section.id] = section.defaultSelected || [];\n    });\n    setSelectedFilters(defaults);\n    onReset?.();\n    setIsOpen(false);\n  };\n\n  const handleApply = () => {\n    onApply(selectedFilters);\n    // No placement reset — see the note above the outside-click effect. `Reset`\n    // never did one, which is why only Apply and the dismiss paths slid.\n    setIsOpen(false);\n  };\n\n  const getActiveFiltersCount = () => {\n    return Object.values(selectedFilters).reduce((acc: number, curr: string[]) => acc + curr.length, 0);\n  };\n\n  // Dropdown positioning classes based on placement and mobile state\n  const getDropdownPositionClasses = () => {\n    if (isMobile) {\n      // On mobile, center horizontally with left offset for minimized sidebar\n      // Vertically position right under the trigger button\n      return 'top-full mt-2';\n    }\n\n    // Desktop positioning based on placement prop\n    const desktopClasses = {\n      'bottom-start': 'top-full left-0 mt-2',\n      'bottom-end': 'top-full right-0 mt-2',\n      bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',\n    };\n\n    return desktopClasses[actualPlacement];\n  };\n\n  return (\n    <div ref={containerRef} className={cn('relative inline-block', className)}>\n      {/* Trigger */}\n      {triggerElement ? (\n        <div ref={triggerRef as RefObject<HTMLDivElement>} onClick={() => setIsOpen(!isOpen)}>\n          {triggerElement}\n        </div>\n      ) : (\n        <button\n          ref={triggerRef as RefObject<HTMLButtonElement>}\n          onClick={() => setIsOpen(!isOpen)}\n          className={cn(\n            'flex items-center gap-1.5 transition-colors text-h5',\n            getActiveFiltersCount() > 0\n              ? 'text-ods-accent hover:text-ods-accent/80'\n              : 'text-ods-text-secondary hover:text-ods-text-primary',\n          )}\n        >\n          {triggerLabel}\n          {getActiveFiltersCount() > 0 && <span className=\"size-1.5 rounded-full bg-ods-accent\" />}\n        </button>\n      )}\n\n      {/* Dropdown Panel — kept mounted briefly on close to play exit transition. */}\n      {shouldRender && (\n        <div\n          ref={dropdownRef}\n          className={cn(\n            'z-50 origin-top',\n            // CSS transitions (not @keyframes) so mid-animation interruption\n            // interpolates smoothly from the current computed value.\n            'transition-[opacity,transform] duration-200 ease-out',\n            'will-change-[opacity,transform]',\n            isVisible ? 'translate-y-0 scale-100 opacity-100' : 'pointer-events-none -translate-y-2 scale-95 opacity-0',\n            isMobile ? 'fixed left-4 right-4 mx-auto max-w-[320px]' : 'absolute w-[320px]',\n            getDropdownPositionClasses(),\n            dropdownClassName,\n          )}\n          // No `style` here: the mobile `top` is a DOM measurement and is owned\n          // by the layout effect above.\n        >\n          <div className=\"flex flex-col overflow-hidden rounded-md border border-ods-border bg-ods-bg p-4 shadow-xl\">\n            <div className=\"max-h-[250px] min-h-0 flex-1 overflow-y-auto\">\n              {sections.map((section, sectionIndex) => {\n                const sectionSelection = selectedFilters[section.id] || [];\n                const allSelected = section.options.every(opt => sectionSelection.includes(opt.id));\n\n                return (\n                  <div key={section.id} className={cn('space-y-2', sectionIndex > 0 && 'mt-4')}>\n                    {/* Section Header — sticky so title + Select All stay visible while options scroll. */}\n                    <div className=\"sticky top-0 z-10 flex items-center justify-between bg-ods-bg pb-2\">\n                      <h3 className=\"text-ods-text-secondary text-h5\">{section.title}</h3>\n                      {section.allowSelectAll && section.type === 'checkbox' && (\n                        <button\n                          onClick={() => handleSelectAll(section.id, section)}\n                          className=\"text-ods-text-secondary underline transition-colors text-h6 hover:text-ods-text-primary\"\n                        >\n                          {allSelected ? 'Deselect All' : 'Select All'}\n                        </button>\n                      )}\n                    </div>\n\n                    {/* Options Container */}\n                    <div className=\"overflow-hidden rounded-md border border-ods-border bg-ods-bg\">\n                      {section.options.map((option, index) => {\n                        // Handle separator type\n                        if (option.type === 'separator') {\n                          return (\n                            <div key={`${section.id}-separator-${index}`} className=\"my-1 border-t border-ods-border\" />\n                          );\n                        }\n\n                        const isSelected = sectionSelection.includes(option.id);\n                        const isLast = index === section.options.length - 1;\n\n                        return (\n                          <button\n                            type=\"button\"\n                            key={`${section.id}-${option.id}-${index}`}\n                            onClick={() => handleToggleOption(section.id, option.id, section.type)}\n                            className={cn(\n                              'flex w-full items-center gap-[var(--spacing-system-s)] p-[var(--spacing-system-s)] text-left',\n                              isSelected ? 'bg-ods-bg-surface' : 'bg-ods-bg',\n                              !isLast && 'border-b border-ods-border',\n                              'transition-colors hover:bg-ods-bg-hover',\n                            )}\n                          >\n                            <FilterCheckbox checked={isSelected} />\n                            <span\n                              className=\"min-w-0 flex-1 truncate text-ods-text-primary text-h4\"\n                              title={option.label}\n                            >\n                              {option.label}\n                            </span>\n                            {option.count !== undefined && (\n                              <span className=\"shrink-0 text-ods-text-secondary text-h6\">\n                                {option.count.toLocaleString()}\n                              </span>\n                            )}\n                          </button>\n                        );\n                      })}\n                    </div>\n                  </div>\n                );\n              })}\n            </div>\n            {/* Action Buttons */}\n            <div className=\"mt-4 flex shrink-0 gap-3\">\n              <Button variant=\"outline\" onClick={handleReset} size=\"default\" className=\"md:w-full!\">\n                Reset\n              </Button>\n              <Button variant=\"accent\" onClick={handleApply} size=\"default\" className=\"md:w-full!\">\n                Apply\n              </Button>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\n// Export convenience hook for managing filter state\nexport const useFiltersDropdown = (initialSections: FilterSection[]) => {\n  const [appliedFilters, setAppliedFilters] = useState<Record<string, string[]>>(() => {\n    const initial: Record<string, string[]> = {};\n    initialSections.forEach(section => {\n      if (section.defaultSelected) {\n        initial[section.id] = section.defaultSelected;\n      }\n    });\n    return initial;\n  });\n\n  const handleApply = (filters: Record<string, string[]>) => {\n    setAppliedFilters(filters);\n  };\n\n  const handleReset = () => {\n    const defaults: Record<string, string[]> = {};\n    initialSections.forEach(section => {\n      defaults[section.id] = section.defaultSelected || [];\n    });\n    setAppliedFilters(defaults);\n  };\n\n  const getActiveFiltersCount = () => {\n    return Object.values(appliedFilters).reduce((acc: number, curr: string[]) => acc + curr.length, 0);\n  };\n\n  return {\n    appliedFilters,\n    handleApply,\n    handleReset,\n    getActiveFiltersCount,\n  };\n};\n","'use client';\n\nimport { ExternalLink } from 'lucide-react';\nimport { ArrayEntryManager } from './array-entry-manager';\n\nexport interface GitHubReleaseLink {\n  github_release_url: string; // Full GitHub release URL\n  display_order?: number;\n}\n\ninterface GitHubReleasesManagerProps {\n  releases: GitHubReleaseLink[];\n  onChange: (releases: GitHubReleaseLink[]) => void;\n  className?: string;\n}\n\nexport function GitHubReleasesManager({ releases, onChange, className = '' }: GitHubReleasesManagerProps) {\n  return (\n    <ArrayEntryManager\n      title=\"GitHub Releases\"\n      items={releases}\n      onChange={onChange}\n      fieldKey=\"github_release_url\"\n      placeholder=\"https://github.com/org/repo/releases/tag/v2.0.0\"\n      emptyMessage='No GitHub releases linked. Click \"Add Release\" to link releases.'\n      addButtonText=\"Add Release\"\n      icon={<ExternalLink className=\"h-5 w-5 text-ods-text-secondary\" />}\n      className={className}\n    />\n  );\n}\n","'use client';\n\nimport { FileText } from 'lucide-react';\nimport { ArrayEntryManager } from './array-entry-manager';\n\nexport interface KBLink {\n  kb_article_path: string; // Relative path like /api/authentication/api-keys.md\n  display_order?: number;\n}\n\ninterface KnowledgeBaseLinksManagerProps {\n  links: KBLink[];\n  onChange: (links: KBLink[]) => void;\n  className?: string;\n}\n\nexport function KnowledgeBaseLinksManager({ links, onChange, className = '' }: KnowledgeBaseLinksManagerProps) {\n  return (\n    <ArrayEntryManager\n      title=\"Knowledge Base Articles\"\n      items={links}\n      onChange={onChange}\n      fieldKey=\"kb_article_path\"\n      placeholder=\"Relative path (e.g., /api/authentication/api-keys.md)\"\n      emptyMessage='No knowledge base articles linked. Click \"Add Article\" to link articles.'\n      addButtonText=\"Add Article\"\n      icon={<FileText className=\"h-5 w-5 text-ods-text-secondary\" />}\n      className={className}\n    />\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { createContext, useContext, useState, useEffect } from 'react';\nimport { Progress } from '../ui/progress';\n\ntype LoadingContextType = {\n  isLoading: boolean;\n  setIsLoading: (loading: boolean) => void;\n};\n\nconst LoadingContext = createContext<LoadingContextType | undefined>(undefined);\n\nexport function LoadingProvider({ children }: { children: React.ReactNode }) {\n  const [isLoading, setIsLoading] = useState(false);\n  const [progress, setProgress] = useState(0);\n\n  // The jump to 10% (start) and 100% (finish) is a reset driven by a value we\n  // already have while rendering, so it happens here rather than in the effect.\n  //\n  // It also fixes a flash: the effect's `else` branch ran on MOUNT too, so every\n  // LoadingProvider painted a full-width white bar at 100% and then spent 500ms\n  // fading it away before anything had ever loaded. Edge-triggering means a\n  // mount that was never loading shows no bar at all.\n  const [progressFor, setProgressFor] = useState(isLoading);\n  if (progressFor !== isLoading) {\n    setProgressFor(isLoading);\n    setProgress(isLoading ? 10 : 100);\n  }\n\n  // Simulate progress when loading\n  useEffect(() => {\n    if (isLoading) {\n      const interval = setInterval(() => {\n        setProgress(prevProgress => {\n          if (prevProgress >= 90) {\n            clearInterval(interval);\n            return prevProgress;\n          }\n          return prevProgress + (90 - prevProgress) * 0.1;\n        });\n      }, 200);\n      return () => clearInterval(interval);\n    }\n\n    const timeout = setTimeout(() => {\n      setProgress(0);\n    }, 500);\n    return () => clearTimeout(timeout);\n  }, [isLoading]);\n\n  return (\n    <LoadingContext.Provider value={{ isLoading, setIsLoading }}>\n      {progress > 0 && (\n        <Progress\n          value={progress}\n          className=\"fixed left-0 right-0 top-0 z-50 h-1 w-full rounded-none bg-transparent\"\n          indicatorClassName=\"bg-white transition-all duration-300 ease-in-out\"\n        />\n      )}\n      {children}\n    </LoadingContext.Provider>\n  );\n}\n\nexport function useLoading() {\n  const context = useContext(LoadingContext);\n  if (context === undefined) {\n    throw new Error('useLoading must be used within a LoadingProvider');\n  }\n  return context;\n}\n","'use client';\n\nimport { Upload, Image as ImageIcon, Video as VideoIcon, Trash2, Loader2, GripVertical, Plus } from 'lucide-react';\nimport type React from 'react';\nimport { useState, useRef, useCallback } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { isVideoMedia } from '../../utils/media-type';\nimport { Button, Card } from '../ui';\n\nimport { Video } from './video';\nexport interface MediaItem {\n  id?: string | number; // Optional for new items\n  media_type: 'image' | 'video' | 'screenshot' | 'demo';\n  media_url: string;\n  title?: string;\n  description?: string;\n  display_order?: number;\n  _uploading?: boolean;\n}\n\ninterface MediaGalleryManagerProps {\n  media: MediaItem[];\n  onChange: (media: MediaItem[]) => void;\n  onUpload: (file: File, mediaType: 'image' | 'video') => Promise<string>;\n  isUploading?: boolean;\n  showInModal?: boolean;\n  modalTitle?: string;\n  className?: string;\n}\n\n/**\n * Unified Media Gallery Manager\n * Handles upload, display, reordering, and deletion of media items\n * Used across events, product releases, and any content with media galleries\n */\nexport function MediaGalleryManager({\n  media,\n  onChange,\n  onUpload,\n  isUploading = false,\n  className = '',\n}: MediaGalleryManagerProps) {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [deletingIndex, setDeletingIndex] = useState<number | null>(null);\n  const [draggedIndex, setDraggedIndex] = useState<number | null>(null);\n\n  const handleFileSelect = useCallback(\n    async (event: React.ChangeEvent<HTMLInputElement>) => {\n      const file = event.target.files?.[0];\n      if (!file) return;\n\n      // Determine media type\n      let mediaType: 'image' | 'video';\n      if (file.type.startsWith('image/')) {\n        mediaType = 'image';\n      } else if (file.type.startsWith('video/')) {\n        mediaType = 'video';\n      } else {\n        return;\n      }\n\n      try {\n        const url = await onUpload(file, mediaType);\n\n        // Add new media item\n        onChange([\n          ...media,\n          {\n            media_type: mediaType === 'image' ? 'screenshot' : 'demo',\n            media_url: url,\n            title: file.name,\n            display_order: media.length,\n          },\n        ]);\n\n        // Reset file input\n        if (fileInputRef.current) {\n          fileInputRef.current.value = '';\n        }\n      } catch (error) {\n        console.error('Upload failed:', error);\n      }\n    },\n    [media, onChange, onUpload],\n  );\n\n  const handleDeleteMedia = useCallback(\n    (index: number) => {\n      setDeletingIndex(index);\n      onChange(media.filter((_, i) => i !== index));\n      setDeletingIndex(null);\n    },\n    [media, onChange],\n  );\n\n  const handleDragStart = useCallback((index: number) => {\n    setDraggedIndex(index);\n  }, []);\n\n  const handleDragOver = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n  }, []);\n\n  const handleDrop = useCallback(\n    (e: React.DragEvent, targetIndex: number) => {\n      e.preventDefault();\n\n      if (draggedIndex === null || draggedIndex === targetIndex) {\n        setDraggedIndex(null);\n        return;\n      }\n\n      const newMedia = [...media];\n      const [draggedItem] = newMedia.splice(draggedIndex, 1);\n      newMedia.splice(targetIndex, 0, draggedItem);\n\n      onChange(newMedia.map((item, i) => ({ ...item, display_order: i })));\n      setDraggedIndex(null);\n    },\n    [media, draggedIndex, onChange],\n  );\n\n  const renderMediaItem = useCallback(\n    (mediaItem: MediaItem, index: number) => {\n      const isDeleting = deletingIndex === index;\n\n      return (\n        <Card\n          key={index}\n          className=\"group relative border-ods-border transition-colors hover:border-ods-accent/30\"\n          draggable\n          onDragStart={() => handleDragStart(index)}\n          onDragOver={handleDragOver}\n          onDrop={e => handleDrop(e, index)}\n        >\n          {/* Drag Handle */}\n          <div className=\"absolute left-2 top-2 z-10 cursor-move opacity-0 transition-opacity group-hover:opacity-100\">\n            <GripVertical className=\"h-4 w-4 text-ods-text-on-dark drop-shadow\" />\n          </div>\n\n          {/* Delete Button */}\n          <div className=\"absolute right-2 top-2 z-10 opacity-0 transition-opacity group-hover:opacity-100\">\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"small-legacy\"\n              onClick={() => handleDeleteMedia(index)}\n              disabled={isDeleting}\n              className=\"h-8 w-8 border-ods-error bg-ods-error p-0 hover:bg-ods-error-hover\"\n            >\n              {isDeleting ? (\n                <Loader2 className=\"h-4 w-4 animate-spin text-ods-text-on-dark\" />\n              ) : (\n                <Trash2 className=\"h-4 w-4 text-ods-text-on-dark\" />\n              )}\n            </Button>\n          </div>\n\n          {/* Media Content */}\n          <div className=\"relative aspect-video overflow-hidden rounded-lg bg-ods-bg\">\n            {isVideoMedia(mediaItem) || mediaItem.media_type === 'demo' ? (\n              // <Video> SSOT (MuxPlayer) — plays Mux HLS + MP4 alike;\n              // fit=\"cover\" crops to the aspect-video cell.\n              <Video kind=\"file\" url={mediaItem.media_url} fit=\"cover\" className=\"h-full w-full\" />\n            ) : (\n              <Image\n                src={mediaItem.media_url}\n                alt={mediaItem.title || 'Media'}\n                fill\n                className=\"object-cover\"\n                sizes=\"(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw\"\n              />\n            )}\n          </div>\n\n          {/* Media Info */}\n          <div className=\"p-3\">\n            <div className=\"mb-1 flex items-center gap-2\">\n              {isVideoMedia(mediaItem) || mediaItem.media_type === 'demo' ? (\n                <VideoIcon className=\"h-4 w-4 text-ods-text-secondary\" />\n              ) : (\n                <ImageIcon className=\"h-4 w-4 text-ods-text-secondary\" />\n              )}\n              <span className=\"capitalize text-ods-text-primary text-h6\">{mediaItem.media_type}</span>\n            </div>\n            {mediaItem.title && <p className=\"truncate text-ods-text-secondary text-h6\">{mediaItem.title}</p>}\n          </div>\n        </Card>\n      );\n    },\n    [deletingIndex, handleDragStart, handleDragOver, handleDrop, handleDeleteMedia],\n  );\n\n  const content = (\n    <div className={`space-y-6 ${className}`}>\n      {/* Upload Section */}\n      <div className=\"rounded-lg border-2 border-dashed border-ods-border p-6 text-center transition-colors hover:border-ods-accent/50\">\n        <div className=\"flex flex-col items-center gap-4\">\n          <div className=\"flex h-12 w-12 items-center justify-center rounded-full bg-ods-card\">\n            {isUploading ? (\n              <Loader2 className=\"h-6 w-6 animate-spin text-ods-accent\" />\n            ) : (\n              <Upload className=\"h-6 w-6 text-ods-accent\" />\n            )}\n          </div>\n          <div>\n            <h3 className=\"mb-1 text-ods-text-primary text-h3\">{isUploading ? 'Uploading...' : 'Upload Media'}</h3>\n            <p className=\"text-ods-text-secondary text-h6\">Drag and drop or click to select images and videos</p>\n            <p className=\"mt-1 text-ods-text-secondary text-h6\">Maximum file size: 50MB</p>\n          </div>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={() => fileInputRef.current?.click()}\n            disabled={isUploading}\n            leftIcon={<Plus className=\"h-4 w-4\" />}\n            className=\"font-bold text-h6\"\n          >\n            {isUploading ? 'Uploading...' : 'Select Files'}\n          </Button>\n        </div>\n      </div>\n\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept=\"image/*,video/*\"\n        onChange={handleFileSelect}\n        className=\"hidden\"\n        disabled={isUploading}\n      />\n\n      {/* Media Grid */}\n      {media.length === 0 ? (\n        <div className=\"py-8 text-center\">\n          <ImageIcon className=\"mx-auto mb-4 h-12 w-12 text-ods-text-secondary\" />\n          <h3 className=\"mb-2 text-ods-text-primary text-h3\">No media uploaded yet</h3>\n          <p className=\"text-ods-text-secondary text-h6\">Upload your first image or video to get started</p>\n        </div>\n      ) : (\n        <div>\n          <div className=\"mb-4 flex items-center justify-between\">\n            <h3 className=\"text-ods-text-primary text-h3\">Media Gallery ({media.length})</h3>\n            <p className=\"text-ods-text-secondary text-h6\">Drag to reorder</p>\n          </div>\n          <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3\">\n            {media.map((item, index) => renderMediaItem(item, index))}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n\n  return content;\n}\n","'use client';\n\nimport { ArrowRight } from 'lucide-react';\nimport type React from 'react';\nimport { Button } from '../ui/button';\n\ninterface MoreAboutButtonProps {\n  onClick?: () => void;\n  href?: string;\n  className?: string;\n}\n\nconst MoreAboutButton: React.FC<MoreAboutButtonProps> = ({ href = '/openframe', className = '' }) => {\n  return (\n    <Button\n      size=\"default\"\n      variant=\"transparent\"\n      className={`flex-shrink-0 text-[var(--ods-open-yellow-base)] ${className}`}\n      rightIcon={<ArrowRight className=\"h-6 w-6\" />}\n      href={href}\n      openInNewTab={!!href}\n    >\n      Learn more\n    </Button>\n  );\n};\n\nexport { MoreAboutButton };\nexport default MoreAboutButton;\n","'use client';\n\n/**\n * OSTypeBadgeGroup Component\n *\n * Displays multiple OS type badges in a horizontal group.\n * Used for showing supported platforms in scripts and other multi-OS contexts.\n */\n\nimport { cn } from '../../utils/cn';\nimport { OSTypeBadge } from './os-type-badge';\n\nexport interface OSTypeBadgeGroupProps {\n  /** Array of OS type strings (case-insensitive, handles aliases) */\n  osTypes: string[];\n  /** Additional CSS classes */\n  className?: string;\n  /** Icon size class (default: w-4 h-4) */\n  iconSize?: string;\n  /** Maximum number of badges to display before showing \"+N more\" */\n  maxDisplay?: number;\n}\n\n/**\n * OSTypeBadgeGroup - Displays multiple OS type icons in a group (icons only, no labels)\n *\n * @example\n * ```tsx\n * <OSTypeBadgeGroup osTypes={['windows', 'darwin', 'linux']} />\n * <OSTypeBadgeGroup osTypes={['macos', 'linux']} maxDisplay={2} />\n * ```\n */\nexport function OSTypeBadgeGroup({ osTypes, className = '', iconSize = 'w-5 h-5', maxDisplay }: OSTypeBadgeGroupProps) {\n  if (!osTypes || osTypes.length === 0) {\n    return null;\n  }\n\n  const displayOsTypes = maxDisplay && osTypes.length > maxDisplay ? osTypes.slice(0, maxDisplay) : osTypes;\n\n  const remainingCount = maxDisplay && osTypes.length > maxDisplay ? osTypes.length - maxDisplay : 0;\n\n  return (\n    <div className={cn('flex flex-wrap items-center gap-2', className)}>\n      {displayOsTypes.map((osType, index) => (\n        <OSTypeBadge key={`${osType}-${index}`} osType={osType} iconOnly iconSize={iconSize} />\n      ))}\n      {remainingCount > 0 && <span className=\"text-ods-text-secondary text-h6\">+{remainingCount} more</span>}\n    </div>\n  );\n}\n","'use client';\n\nimport { motion, useScroll, useTransform, useMotionValue, useSpring } from 'framer-motion';\nimport type React from 'react';\nimport { useEffect, useState, useRef } from 'react';\nimport Image from '../../embed-shims/next-image';\n\ninterface ParallaxImageShowcaseProps {\n  images: {\n    src: string;\n    alt: string;\n    position: 'left' | 'center' | 'right';\n  }[];\n  className?: string;\n  layout?: 'non-boxed' | 'boxed' | 'grid'; // New layout prop\n  logoElement?: React.ReactNode; // Custom logo for boxed layout\n  intensity?: number; // Animation intensity (used for grid layout)\n  shadow?: boolean; // Add shadow to images (used for grid layout)\n}\n\nexport const ParallaxImageShowcase: React.FC<ParallaxImageShowcaseProps> = ({\n  images,\n  className = '',\n  layout = 'non-boxed',\n  logoElement,\n  intensity,\n  shadow = false,\n}) => {\n  // ANIMATION INTENSITY CONTROL\n  // 0.1 = very gentle, 1 = normal, 5 = aggressive, 10 = super aggressive\n  const INTENSITY = intensity || 1; // Use provided intensity or default to 1\n\n  // Scroll animation - works everywhere on the page\n  const { scrollY } = useScroll();\n\n  // Mouse animation - works globally\n  const mouseX = useMotionValue(0);\n  const mouseY = useMotionValue(0);\n\n  // Smooth spring for mouse\n  const springConfig = { stiffness: 100, damping: 30 };\n  const mouseXSpring = useSpring(mouseX, springConfig);\n  const mouseYSpring = useSpring(mouseY, springConfig);\n\n  // Track component position for global mouse calculations\n  const [componentRect, setComponentRect] = useState<DOMRect | null>(null);\n  const componentRef = useRef<HTMLDivElement>(null);\n\n  // Update component position on mount and resize\n  useEffect(() => {\n    const updateRect = () => {\n      if (componentRef.current) {\n        setComponentRect(componentRef.current.getBoundingClientRect());\n      }\n    };\n\n    updateRect();\n    window.addEventListener('resize', updateRect);\n    window.addEventListener('scroll', updateRect);\n\n    return () => {\n      window.removeEventListener('resize', updateRect);\n      window.removeEventListener('scroll', updateRect);\n    };\n  }, []);\n\n  // Global mouse tracking\n  useEffect(() => {\n    const handleGlobalMouseMove = (e: MouseEvent) => {\n      if (!componentRect) return;\n\n      const centerX = componentRect.left + componentRect.width / 2;\n      const centerY = componentRect.top + componentRect.height / 2;\n      const x = (e.clientX - centerX) / componentRect.width;\n      const y = (e.clientY - centerY) / componentRect.height;\n\n      // Clamp values to reasonable range\n      const clampedX = Math.max(-1, Math.min(1, x));\n      const clampedY = Math.max(-1, Math.min(1, y));\n\n      mouseX.set(clampedX * 20);\n      mouseY.set(clampedY * 20);\n    };\n\n    window.addEventListener('mousemove', handleGlobalMouseMove);\n\n    return () => {\n      window.removeEventListener('mousemove', handleGlobalMouseMove);\n    };\n  }, [componentRect, mouseX, mouseY]);\n\n  // SCROLL TRANSFORMS - Values multiplied by intensity\n  const scrollX = useTransform(scrollY, [0, 1000], [0, 5 * INTENSITY]);\n  const scrollY_ = useTransform(scrollY, [0, 1000], [0, -10 * INTENSITY]);\n  const scrollRotate = useTransform(scrollY, [0, 1000], [0, 0.5 * INTENSITY]);\n\n  // MOUSE TRANSFORMS - Values multiplied by intensity\n  const mouseTransformX = useTransform(mouseXSpring, [-20, 20], [-2 * INTENSITY, 2 * INTENSITY]);\n  const mouseTransformY = useTransform(mouseYSpring, [-20, 20], [-2 * INTENSITY, 2 * INTENSITY]);\n  const mouseRotate = useTransform(mouseXSpring, [-20, 20], [-0.3 * INTENSITY, 0.3 * INTENSITY]);\n\n  // COMBINE scroll and mouse - same for all images\n  const x = useTransform([scrollX, mouseTransformX], ([s, m]) => (s as number) + (m as number));\n  const y = useTransform([scrollY_, mouseTransformY], ([s, m]) => (s as number) + (m as number));\n  const rotate = useTransform([scrollRotate, mouseRotate], ([s, m]) => (s as number) + (m as number));\n\n  // Grid-layout transforms. Hoisted out of the `layout === 'grid'` branch\n  // below: they were six `useTransform` calls inside an `if`, so a `layout`\n  // prop that ever changed rendered a different NUMBER of hooks than the\n  // previous pass — React's \"rendered more hooks than during the previous\n  // render\" crash. A `useTransform` only allocates a MotionValue, so holding\n  // all six unconditionally costs nothing when the branch is not taken.\n  const scrollY_Grid = useTransform(scrollY, [0, 1000], [0, -3 * INTENSITY]);\n  const scrollRotateGrid = useTransform(scrollY, [0, 1000], [0, 0.05 * INTENSITY]);\n  const mouseTransformYGrid = useTransform(mouseYSpring, [-20, 20], [-1 * INTENSITY, 1 * INTENSITY]);\n  const mouseRotateGrid = useTransform(mouseXSpring, [-20, 20], [-0.03 * INTENSITY, 0.03 * INTENSITY]);\n  const yGrid = useTransform([scrollY_Grid, mouseTransformYGrid], ([s, m]) => (s as number) + (m as number));\n  const rotateGrid = useTransform([scrollRotateGrid, mouseRotateGrid], ([s, m]) => (s as number) + (m as number));\n\n  // Get images by position\n  const leftImage = images.find(img => img.position === 'left');\n  const centerImage = images.find(img => img.position === 'center');\n  const rightImage = images.find(img => img.position === 'right');\n\n  // Grid Layout: Simple 3-column grid with gentler animations\n  // Grid Layout: simple 3-column grid with the gentler transforms above.\n  if (layout === 'grid') {\n    return (\n      <div ref={componentRef} className={`relative flex h-full w-full items-center ${className}`}>\n        <div className=\"grid w-full grid-cols-1 gap-4 md:grid-cols-3\">\n          {/* Image #1 - Left */}\n          {leftImage && (\n            <motion.div\n              className=\"relative w-full\"\n              style={{\n                y: yGrid,\n                rotate: rotateGrid,\n              }}\n            >\n              <Image\n                src={leftImage.src}\n                alt={leftImage.alt}\n                width={800}\n                height={500}\n                sizes=\"(max-width: 768px) 100vw, 33vw\"\n                className=\"h-auto w-full object-contain\"\n                style={shadow ? { boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)' } : {}}\n              />\n            </motion.div>\n          )}\n\n          {/* Image #2 - Center */}\n          {centerImage && (\n            <motion.div\n              className=\"relative w-full\"\n              style={{\n                y: yGrid,\n                rotate: rotateGrid,\n              }}\n            >\n              <Image\n                src={centerImage.src}\n                alt={centerImage.alt}\n                width={800}\n                height={500}\n                sizes=\"(max-width: 768px) 100vw, 33vw\"\n                className=\"h-auto w-full object-contain\"\n                style={shadow ? { boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)' } : {}}\n              />\n            </motion.div>\n          )}\n\n          {/* Image #3 - Right */}\n          {rightImage && (\n            <motion.div\n              className=\"relative w-full\"\n              style={{\n                y: yGrid,\n                rotate: rotateGrid,\n              }}\n            >\n              <Image\n                src={rightImage.src}\n                alt={rightImage.alt}\n                width={800}\n                height={500}\n                sizes=\"(max-width: 768px) 100vw, 33vw\"\n                className=\"h-auto w-full object-contain\"\n                style={shadow ? { boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)' } : {}}\n              />\n            </motion.div>\n          )}\n        </div>\n      </div>\n    );\n  }\n\n  // Boxed Layout: Clean two-row grid structure with overlaps\n  if (layout === 'boxed') {\n    return (\n      <div ref={componentRef} className={`relative h-full w-full overflow-hidden ${className}`}>\n        {/* Row 1: Logo Component */}\n        {logoElement && <div className=\"absolute left-1/2 top-4 z-30 -translate-x-1/2 scale-50\">{logoElement}</div>}\n\n        {/* Row 2: Two images with overlaps and margin from row 1 */}\n        <div className=\"absolute inset-x-0 top-32 h-80\">\n          {/* Left half: Image overlaps to the left */}\n          {leftImage && (\n            <motion.div className=\"absolute left-20 z-10 h-full w-full lg:w-1/2\" style={{ x: x, y: y, rotate: rotate }}>\n              <Image\n                src={leftImage.src}\n                alt={leftImage.alt}\n                fill\n                sizes=\"50vw\"\n                className=\"ods-card/50 border border-ods-border object-cover object-[left_top]\"\n              />\n              <div className=\"absolute inset-0 shadow-2xl\" />\n            </motion.div>\n          )}\n\n          {/* Right half: Image overlaps to the right */}\n          {centerImage && (\n            <motion.div\n              className=\"absolute right-20 top-20 z-20 h-full w-full lg:w-1/2\"\n              style={{ x: x, y: y, rotate: rotate }}\n            >\n              <Image\n                src={centerImage.src}\n                alt={centerImage.alt}\n                fill\n                sizes=\"50vw\"\n                className=\"ods-card/50 border border-ods-border object-cover object-[left_top]\"\n              />\n              <div className=\"absolute inset-0 shadow-2xl\" />\n            </motion.div>\n          )}\n        </div>\n      </div>\n    );\n  }\n\n  // Non-Boxed Layout (original implementation)\n  return (\n    <div ref={componentRef} className={`relative h-full w-full ${className}`}>\n      {/* Image #3 - Behind all - z-index 1 */}\n      {rightImage && (\n        <motion.div\n          className=\"absolute right-[5%] top-[5%] z-[1] h-[60%] w-[80%] lg:right-[-15%] lg:top-[-10%] lg:h-[85%] lg:w-[120%]\"\n          style={{\n            x: x,\n            y: y,\n            rotate: rotate,\n          }}\n        >\n          <Image\n            src={rightImage.src}\n            alt={rightImage.alt}\n            fill\n            sizes=\"(max-width: 1024px) 80vw, 120vw\"\n            className=\"object-contain\"\n          />\n        </motion.div>\n      )}\n\n      {/* Image #2 - Middle layer - z-index 2 */}\n      {centerImage && (\n        <motion.div\n          className=\"absolute bottom-[10%] right-[0%] z-[2] h-[55%] w-[75%] lg:bottom-[-15%] lg:right-[-20%] lg:h-[80%] lg:w-[100%]\"\n          style={{\n            x: x,\n            y: y,\n            rotate: rotate,\n          }}\n        >\n          <Image\n            src={centerImage.src}\n            alt={centerImage.alt}\n            fill\n            sizes=\"(max-width: 1024px) 75vw, 100vw\"\n            className=\"object-contain\"\n          />\n        </motion.div>\n      )}\n\n      {/* Image #1 - On top - z-index 3 */}\n      {leftImage && (\n        <motion.div\n          className=\"absolute left-[-5%] top-[10%] z-[3] h-[65%] w-[85%] lg:left-[-25%] lg:top-[0%] lg:h-[90%] lg:w-[110%]\"\n          style={{\n            x: x,\n            y: y,\n            rotate: rotate,\n          }}\n        >\n          <Image\n            src={leftImage.src}\n            alt={leftImage.alt}\n            fill\n            sizes=\"(max-width: 1024px) 85vw, 110vw\"\n            className=\"object-contain\"\n          />\n        </motion.div>\n      )}\n    </div>\n  );\n};\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { Copy02Icon } from '../icons-v2-generated/documents/copy-02-icon';\n\nexport interface PathsDisplayProps {\n  /**\n   * Array of file/folder paths to display\n   */\n  paths: readonly string[] | string[];\n\n  /**\n   * Callback when a path is copied\n   */\n  onCopyPath?: (path: string) => void;\n\n  /**\n   * Optional title above the paths list\n   */\n  title?: string;\n\n  /**\n   * Optional description text\n   */\n  description?: string;\n\n  /**\n   * Additional CSS classes for the container\n   */\n  className?: string;\n\n  /**\n   * Whether to show copy buttons (default: true)\n   */\n  showCopyButtons?: boolean;\n\n  /**\n   * Size of the copy icon (default: 'w-6 h-6')\n   */\n  copyIconSize?: string;\n\n  /**\n   * Optional icon rendered at the start of every row, before the path text.\n   * Useful for showing a platform logo next to a command (e.g. the Windows\n   * logo beside \"Run PowerShell as Local Administrator\").\n   */\n  leadingIcon?: React.ReactNode;\n}\n\n/**\n * PathsDisplay - Unified component for displaying file/folder paths with copy functionality\n *\n * Features:\n * - Displays a list of paths in a styled container\n * - Optional copy-to-clipboard functionality\n * - Monospace font for paths\n * - Consistent ODS styling\n *\n * Usage Example:\n * ```tsx\n * import { PathsDisplay } from '@flamingo/ui-kit/components/features'\n *\n * const windowsPaths = [\n *   'C:\\\\Program Files\\\\OpenFrame\\\\',\n *   'C:\\\\ProgramData\\\\OpenFrame\\\\'\n * ]\n *\n * <PathsDisplay\n *   paths={windowsPaths}\n *   title=\"Antivirus Exclusions\"\n *   description=\"Add these folders to your antivirus exclusions list\"\n *   onCopyPath={(path) => {\n *     navigator.clipboard.writeText(path)\n *     toast({ title: 'Copied', description: 'Path copied to clipboard' })\n *   }}\n * />\n * ```\n */\nexport function PathsDisplay({\n  paths,\n  onCopyPath,\n  title,\n  description,\n  className,\n  showCopyButtons = true,\n  copyIconSize = 'w-6 h-6',\n  leadingIcon,\n}: PathsDisplayProps) {\n  if (!paths || paths.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className={cn('flex flex-col gap-3', className)}>\n      {title && <div className=\"text-ods-text-primary text-h4\">{title}</div>}\n      {description && <div className=\"text-ods-text-secondary text-h6\">{description}</div>}\n      <div className=\"overflow-hidden rounded-[6px] border border-ods-border bg-ods-bg\">\n        {paths.map(path => (\n          <div key={path} className=\"flex items-center gap-4 border-b border-ods-border p-4 last:border-b-0\">\n            {leadingIcon && <span className=\"shrink-0 text-ods-text-primary\">{leadingIcon}</span>}\n            <span className=\"min-w-0 flex-1 truncate text-ods-text-primary text-h4\">{path}</span>\n            {showCopyButtons && onCopyPath && (\n              <button\n                type=\"button\"\n                onClick={() => onCopyPath(path)}\n                aria-label={`Copy ${path}`}\n                className=\"shrink-0 rounded-md text-ods-text-secondary transition-colors hover:text-ods-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus\"\n              >\n                <Copy02Icon className={copyIconSize} />\n              </button>\n            )}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Pre-defined path sets for common use cases\n */\nexport const OPENFRAME_PATHS = {\n  windows: [\n    'C:\\\\Program Files\\\\OpenFrame',\n    'C:\\\\ProgramData\\\\OpenFrame',\n    'C:\\\\ProgramData\\\\OpenFrameInstall',\n    'C:\\\\Program Files\\\\Orbit',\n  ],\n  darwin: [\n    '/Library/LaunchDaemons/com.openframe.client.plist',\n    '/Library/Application Support/OpenFrame/meshcentral-agent/',\n  ],\n  linux: ['/opt/openframe/'],\n} as const;\n\nexport type OpenFramePathsPlatform = keyof typeof OPENFRAME_PATHS;\n\n/**\n * Get OpenFrame paths for a specific platform\n */\nexport function getOpenFramePaths(platform: OpenFramePathsPlatform): string[] {\n  return [...OPENFRAME_PATHS[platform]];\n}\n\n/**\n * Doctor command per platform. Run to diagnose installation issues and repair\n * the agent (works even if the agent didn't install correctly).\n */\nexport const OPENFRAME_DOCTOR_COMMANDS: Record<OpenFramePathsPlatform, string> = {\n  // PowerShell: Invoke-WebRequest / .\\openframe-client.exe, mirroring the\n  // Windows install command pattern.\n  windows:\n    \"Set-Location ~; Invoke-WebRequest -Uri 'github.com/openframe-client' -OutFile 'openframe-client.exe'; .\\\\openframe-client.exe doctor\",\n  darwin: 'cd ~; wget github.com/openframe-client; openframe-client doctor',\n  linux: 'cd ~; wget github.com/openframe-client; openframe-client doctor',\n} as const;\n\n/**\n * Get the OpenFrame doctor command for a specific platform\n */\nexport function getOpenFrameDoctorCommand(platform: OpenFramePathsPlatform): string {\n  return OPENFRAME_DOCTOR_COMMANDS[platform];\n}\n","'use client';\n\nimport type { PlatformConfig } from '../../types/platform';\nimport { getSmallPlatformIcon } from '../../utils/platform-config';\nimport { Button } from '../ui/button';\n\n// Platform icons are now unified in platform-config utils\n\nexport interface PlatformFilterComponentProps {\n  selectedPlatform: string;\n  onPlatformChange: (platform: string) => void;\n  platforms: PlatformConfig[];\n  className?: string;\n  showIcons?: boolean;\n  size?: 'small-legacy' | 'default';\n}\n\nexport function PlatformFilterComponent({\n  selectedPlatform,\n  onPlatformChange,\n  platforms = [],\n  className = '',\n  showIcons = true,\n  size = 'small-legacy',\n}: PlatformFilterComponentProps) {\n  return (\n    <div className={`flex flex-wrap items-center gap-2 ${className}`}>\n      <Button\n        type=\"button\"\n        variant={selectedPlatform === 'all' ? 'accent' : 'outline'}\n        size={size}\n        onClick={() => onPlatformChange('all')}\n        className=\"text-h3\"\n      >\n        All Platforms\n      </Button>\n      {platforms.map(platform => (\n        <Button\n          key={platform.value}\n          type=\"button\"\n          variant={selectedPlatform === platform.value ? 'accent' : 'outline'}\n          size={size}\n          onClick={() => onPlatformChange(platform.value)}\n          leftIcon={showIcons ? getSmallPlatformIcon(platform.value) : undefined}\n          className=\"text-h3\"\n        >\n          {platform.label}\n        </Button>\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport { Check } from 'lucide-react';\nimport type React from 'react';\nimport { useEffect, useRef } from 'react';\nimport { UnifiedSkeleton } from '../loading';\nimport { Badge, Input } from '../ui';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';\n\n/** Option ids are whatever the consumer keys its rows by — string (UUID / enum value) or numeric row id. */\nexport type SelectableOptionId = string | number;\n\nexport interface SelectableOption<Id extends SelectableOptionId = string> {\n  id: Id; // Selection ID (UUID / value for platforms, numeric row id for DB-keyed rows)\n  name: string; // Primary identifier (platform enum or item name)\n  displayName?: string; // Optional display name (for platforms)\n  description?: string;\n  isActive?: boolean;\n  createdAt?: string;\n  updatedAt?: string;\n  icon?: React.ReactNode;\n  color?: string;\n  disabled?: boolean; // If true, option is shown grayed out and not selectable\n  disabledReason?: string; // Tooltip shown on hover when disabled — explains WHY it's unavailable\n  section?: string; // Optional section ID to group options\n}\n\nexport interface SectionDefinition {\n  id: string; // Section identifier (matches option.section)\n  label: string; // Display label for the section\n  icon?: React.ReactNode; // Optional icon for the section header\n  description?: string; // Optional description shown under section label\n}\n\n/** SERVER-SIDE search: the selector renders the input; the PARENT owns the\n *  query state and re-fetches `options` (never a client-side .filter of a\n *  pre-fetched list). While `isLoading`, the input stays mounted (typing\n *  must not lose focus to a skeleton swap). */\nexport interface PushButtonSelectorSearch {\n  value: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  ariaLabel?: string;\n}\n\n/** Lazy paging: the option list becomes a FIXED-HEIGHT scroll viewport with\n *  an IntersectionObserver sentinel near the bottom — the selector calls\n *  `onLoadMore` as the user scrolls; the parent appends to `options`. */\nexport interface PushButtonSelectorLazyLoad {\n  hasMore: boolean;\n  isFetchingMore: boolean;\n  onLoadMore: () => void;\n  /** CSS height of the scroll viewport (default '336px' ≈ 6 rows). */\n  maxHeight?: string;\n}\n\ninterface PushButtonSelectorProps<Id extends SelectableOptionId = string> {\n  options: SelectableOption<Id>[];\n  selectedIds: Id[];\n  onSelectionChange: (selectedIds: Id[]) => void;\n  multiSelect?: boolean;\n  title?: string;\n  helpText?: string;\n  className?: string;\n  selectionSummary?: boolean;\n  optional?: boolean;\n  isLoading?: boolean;\n  error?: string | null;\n  skeletonCount?: number;\n  sections?: SectionDefinition[]; // Optional sections for grouping options\n  /** Built-in server-side search input (see PushButtonSelectorSearch). */\n  search?: PushButtonSelectorSearch;\n  /** Built-in fixed-height scroll + infinite paging (see PushButtonSelectorLazyLoad). */\n  lazyLoad?: PushButtonSelectorLazyLoad;\n  /** Rendered inside the list when `options` is empty and not loading. */\n  emptyMessage?: React.ReactNode;\n  /** Rendered under the list (result counts, hints). */\n  footer?: React.ReactNode;\n}\n\n// Composed from the app-wide UnifiedSkeleton system (components/loading) —\n// the same base every other loading surface uses (bg-ods-skeleton pulse,\n// prefers-reduced-motion support, role=\"status\" a11y). Each skeleton row\n// mirrors a LOADED option row exactly (bg-ods-bg card, p-4, icon + two\n// 20px-tall text lines + selection box) so the list never jumps on load.\nfunction PushButtonSelectorSkeleton({ count = 3, hasTitle }: { count?: number; hasTitle?: boolean }) {\n  return (\n    <div className=\"space-y-3\" role=\"status\" aria-label=\"Loading options\">\n      {hasTitle && <UnifiedSkeleton className=\"h-5 w-20\" aria-label=\"Loading title\" />}\n      <div className=\"space-y-3\">\n        {Array.from({ length: count }, (_, i) => (\n          <div key={i} className=\"rounded-lg border border-ods-border bg-ods-bg p-4\">\n            <div className=\"flex items-center justify-between gap-4\">\n              <div className=\"flex items-center gap-3\">\n                <UnifiedSkeleton className=\"h-8 w-8\" aria-label=\"Loading icon\" />\n                <div>\n                  <UnifiedSkeleton variant=\"text\" className=\"mb-2 h-4 w-24\" aria-label=\"Loading name\" />\n                  <UnifiedSkeleton variant=\"text\" className=\"h-4 w-36\" aria-label=\"Loading description\" />\n                </div>\n              </div>\n              <UnifiedSkeleton className=\"h-6 w-6\" aria-label=\"Loading selection state\" />\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n// Error component using ODS error tokens\nfunction PushButtonSelectorError({ message, title }: { message: string; title?: string }) {\n  return (\n    <div className=\"space-y-3\">\n      {title && <h3 className=\"text-ods-text-primary text-h5\">{title}</h3>}\n      <div className=\"rounded-lg border border-ods-error bg-ods-error-secondary p-4\">\n        <div className=\"text-ods-error text-h6\">⚠️ {message}</div>\n      </div>\n    </div>\n  );\n}\n\nexport function PushButtonSelector<Id extends SelectableOptionId = string>({\n  options,\n  selectedIds,\n  onSelectionChange,\n  multiSelect = true,\n  title,\n  helpText,\n  className = '',\n  selectionSummary = false,\n  optional = false,\n  isLoading = false,\n  error = null,\n  skeletonCount = 3,\n  sections,\n  search,\n  lazyLoad,\n  emptyMessage,\n  footer,\n}: PushButtonSelectorProps<Id>) {\n  // Lazy-load sentinel: observe within the scroll viewport so onLoadMore\n  // fires as the user approaches the bottom. Hooks run unconditionally\n  // (before the early returns) per the rules of hooks. Pass a STABLE\n  // onLoadMore (react-query's fetchNextPage is) — an inline arrow re-arms\n  // the observer each render (harmless, just wasteful).\n  const scrollRef = useRef<HTMLDivElement | null>(null);\n  const sentinelRef = useRef<HTMLDivElement | null>(null);\n  const lazyHasMore = lazyLoad?.hasMore ?? false;\n  const lazyFetching = lazyLoad?.isFetchingMore ?? false;\n  const onLoadMore = lazyLoad?.onLoadMore;\n  const optionCount = options.length;\n  useEffect(() => {\n    const sentinel = sentinelRef.current;\n    const root = scrollRef.current;\n    if (!sentinel || !root || !onLoadMore || !lazyHasMore) return undefined;\n    const observer = new IntersectionObserver(\n      entries => {\n        if (entries.some(e => e.isIntersecting) && !lazyFetching) {\n          onLoadMore();\n        }\n      },\n      { root, rootMargin: '80px' },\n    );\n    observer.observe(sentinel);\n    return () => {\n      observer.disconnect();\n    };\n    // optionCount: re-arm when the list grows (the sentinel moves).\n  }, [lazyHasMore, lazyFetching, onLoadMore, optionCount]);\n\n  const searchInput = search ? (\n    <Input\n      value={search.value}\n      onChange={e => search.onChange(e.target.value)}\n      placeholder={search.placeholder}\n      aria-label={search.ariaLabel ?? search.placeholder ?? 'Search'}\n    />\n  ) : null;\n\n  // Framed mode (search and/or lazyLoad): loading and error are NOT early\n  // returns — the frame (title, search input, fixed-height viewport, footer)\n  // must stay pixel-identical across loading/loaded/empty/error, with only\n  // the viewport CONTENT swapping. An early-return skeleton dropped the\n  // bordered viewport + footer and rendered shorter rows, so the whole\n  // section collapsed and jumped on every debounced search.\n  const framed = !!search || !!lazyLoad;\n\n  // LOADING / ERROR STATES — legacy (unframed) consumers keep the historical\n  // whole-component swap, byte-identical.\n  if (isLoading && !framed) {\n    return (\n      <div className={className}>\n        <PushButtonSelectorSkeleton count={skeletonCount} hasTitle={!!title} />\n      </div>\n    );\n  }\n  if (error && !framed) {\n    return (\n      <div className={className}>\n        <PushButtonSelectorError message={error} title={title} />\n      </div>\n    );\n  }\n\n  // VALIDATION: Only filter invalid selectedIds if options are loaded\n  const validSelectedIds =\n    options.length > 0 ? selectedIds.filter(id => options.some(option => option.id === id)) : selectedIds; // Keep all IDs if options not loaded yet\n\n  // Dev warning for debugging (only when options are loaded)\n  if (process.env.NODE_ENV === 'development' && options.length > 0 && validSelectedIds.length !== selectedIds.length) {\n    const invalidIds = selectedIds.filter(id => !options.some(opt => opt.id === id));\n    console.warn('[PushButtonSelector] Invalid selected IDs filtered:', invalidIds);\n  }\n\n  const toggleSelection = (optionId: Id) => {\n    if (multiSelect) {\n      const isSelected = validSelectedIds.includes(optionId);\n      if (isSelected) {\n        onSelectionChange(validSelectedIds.filter(id => id !== optionId));\n      } else {\n        onSelectionChange([...validSelectedIds, optionId]);\n      }\n    } else {\n      // Single select mode\n      onSelectionChange(validSelectedIds.includes(optionId) ? [] : [optionId]);\n    }\n  };\n\n  const getSelectedOptions = () => options.filter(option => validSelectedIds.includes(option.id));\n\n  // Helper to render a single option\n  const renderOption = (option: SelectableOption<Id>) => {\n    const isSelected = validSelectedIds.includes(option.id);\n\n    const optionEl = (\n      <div\n        key={option.id}\n        className={`group rounded-lg border p-4 transition-all duration-200 ${\n          option.disabled\n            ? `${isSelected ? 'cursor-pointer' : 'cursor-not-allowed'} border-ods-border bg-ods-card opacity-40`\n            : isSelected\n              ? 'cursor-pointer border-ods-accent bg-ods-bg-surface shadow-sm'\n              : 'cursor-pointer border-ods-border bg-ods-bg hover:border-ods-border-hover hover:bg-ods-bg-hover'\n        } `}\n        // Disabled options can't be newly SELECTED, but an already-selected one\n        // (e.g. it later became unavailable) must still be removable.\n        onClick={() => (!option.disabled || isSelected) && toggleSelection(option.id)}\n      >\n        <div className=\"flex items-center justify-between gap-4\">\n          <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n            {option.icon && (\n              <div\n                className={`flex-shrink-0 transition-transform duration-200 ${isSelected ? 'scale-110' : 'group-hover:scale-105'}`}\n              >\n                {option.icon}\n              </div>\n            )}\n            <div className=\"min-w-0 flex-1\">\n              <div className=\"font-semibold text-ods-text-primary text-h6\">{option.displayName || option.name}</div>\n              {option.description && (\n                <div className=\"line-clamp-2 text-ods-text-secondary text-h6\" title={option.description}>\n                  {option.description}\n                </div>\n              )}\n            </div>\n          </div>\n\n          {/* Selection Indicator */}\n          <div\n            className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded border-2 transition-all duration-200 ${\n              isSelected\n                ? 'scale-110 border-ods-accent bg-ods-accent'\n                : 'border-ods-border group-hover:border-ods-border-hover'\n            } `}\n          >\n            {isSelected && <Check className=\"h-4 w-4 font-bold text-ods-text-primary\" strokeWidth={3} />}\n          </div>\n        </div>\n      </div>\n    );\n\n    // Disabled options explain WHY on hover via the unified Tooltip.\n    if (option.disabled && option.disabledReason) {\n      return (\n        <Tooltip key={option.id}>\n          <TooltipTrigger asChild>{optionEl}</TooltipTrigger>\n          <TooltipContent className=\"max-w-xs\">{option.disabledReason}</TooltipContent>\n        </Tooltip>\n      );\n    }\n\n    return optionEl;\n  };\n\n  // Group options by section if sections are provided\n  const renderOptionsContent = () => {\n    if (!sections || sections.length === 0) {\n      // No sections - render flat list\n      return <div className=\"space-y-3\">{options.map(renderOption)}</div>;\n    }\n\n    // Group options by section\n    const optionsBySection = new Map<string, SelectableOption<Id>[]>();\n    const ungroupedOptions: SelectableOption<Id>[] = [];\n\n    options.forEach(option => {\n      if (option.section) {\n        const existing = optionsBySection.get(option.section) || [];\n        optionsBySection.set(option.section, [...existing, option]);\n      } else {\n        ungroupedOptions.push(option);\n      }\n    });\n\n    return (\n      <div className=\"space-y-4\">\n        {/* Render sections in order */}\n        {sections.map(section => {\n          const sectionOptions = optionsBySection.get(section.id) || [];\n          if (sectionOptions.length === 0) return null;\n\n          return (\n            <div key={section.id} className=\"space-y-2\">\n              {/* Section Header */}\n              <div className=\"flex items-center gap-2 px-1\">\n                {section.icon && <div className=\"text-ods-text-secondary\">{section.icon}</div>}\n                <div>\n                  <div className=\"font-semibold text-ods-text-primary text-h6\">{section.label}</div>\n                  {section.description && <div className=\"text-ods-text-tertiary text-h6\">{section.description}</div>}\n                </div>\n              </div>\n              {/* Section Options */}\n              <div className=\"space-y-2\">{sectionOptions.map(renderOption)}</div>\n            </div>\n          );\n        })}\n\n        {/* Render ungrouped options at the end */}\n        {ungroupedOptions.length > 0 && <div className=\"space-y-2\">{ungroupedOptions.map(renderOption)}</div>}\n      </div>\n    );\n  };\n\n  // ~82px per row: enough skeleton rows to fill the fixed viewport, so the\n  // loading state occupies the SAME height as the loaded list.\n  const framedSkeletonCount = lazyLoad ? Math.max(skeletonCount, 4) : skeletonCount;\n  const listContent = isLoading ? (\n    <PushButtonSelectorSkeleton count={framedSkeletonCount} />\n  ) : error ? (\n    <PushButtonSelectorError message={error} />\n  ) : (\n    <>\n      {options.length === 0 && emptyMessage !== undefined ? (\n        <div className=\"text-ods-text-secondary text-h6\">{emptyMessage}</div>\n      ) : (\n        renderOptionsContent()\n      )}\n      {lazyLoad ? (\n        <>\n          <div ref={sentinelRef} />\n          {lazyLoad.isFetchingMore ? (\n            <div className=\"py-2 text-center text-ods-text-secondary text-h6\">Loading more…</div>\n          ) : null}\n        </>\n      ) : null}\n    </>\n  );\n\n  return (\n    <TooltipProvider delayDuration={150}>\n      <div className={`space-y-4 ${className}`}>\n        {title && <h3 className=\"text-ods-text-primary text-h5\">{title}</h3>}\n\n        {searchInput}\n\n        {lazyLoad ? (\n          <div\n            ref={scrollRef}\n            className=\"space-y-4 overflow-y-auto rounded-lg border border-ods-border p-2\"\n            style={{ height: lazyLoad.maxHeight ?? '336px' }}\n          >\n            {listContent}\n          </div>\n        ) : (\n          listContent\n        )}\n\n        {footer}\n\n        {/* Selection Summary */}\n        {!isLoading && !error && selectionSummary && validSelectedIds.length > 0 && (\n          <div className=\"rounded-lg border border-ods-success bg-ods-success-secondary p-4\">\n            <div className=\"mb-2 flex items-center gap-2\">\n              <div className=\"h-2 w-2 rounded-full bg-ods-success\"></div>\n              <span className=\"text-ods-success text-h6\">\n                {validSelectedIds.length} {multiSelect ? 'items' : 'item'} selected\n              </span>\n            </div>\n\n            <div className=\"flex flex-wrap gap-2\">\n              {getSelectedOptions().map(option => (\n                <Badge key={option.id} className=\"bg-ods-accent text-ods-text-primary text-h6\">\n                  {option.displayName || option.name}\n                </Badge>\n              ))}\n            </div>\n          </div>\n        )}\n\n        {/* Help Text */}\n        {helpText && <div className=\"text-ods-text-secondary text-h6\">{helpText}</div>}\n\n        {/* Empty State Warning */}\n        {validSelectedIds.length === 0 && title && !optional && (\n          <div className=\"rounded-lg border border-ods-error bg-ods-error-secondary p-3\">\n            <div className=\"text-ods-error text-h6\">⚠️ Please select at least one option</div>\n          </div>\n        )}\n      </div>\n    </TooltipProvider>\n  );\n}\n","import type React from 'react';\nimport { cn } from '../../utils/cn';\n\ninterface UnifiedSkeletonProps extends React.HTMLAttributes<HTMLDivElement> {\n  className?: string;\n  variant?: 'default' | 'text' | 'circular' | 'rectangular';\n  width?: string | number;\n  height?: string | number;\n  /**\n   * Disable animation for users who prefer reduced motion\n   */\n  animate?: boolean;\n  /**\n   * Accessibility label for screen readers\n   */\n  'aria-label'?: string;\n}\n\n/**\n * UnifiedSkeleton Component\n *\n * Base skeleton component with consistent styling across the application.\n *\n * Design Specifications:\n * - Color: #2A2A2A base with #3A3A3A pulse highlight\n * - Animation: 1.5s pulse duration, ease-in-out timing\n * - Responsive: Mobile-first approach with proper scaling\n * - Accessibility: ARIA labels and reduced motion support\n *\n * @param variant - Shape variant: default (rounded), text (rounded-sm), circular, rectangular\n * @param width - Custom width (defaults to full width)\n * @param height - Custom height (defaults to auto based on variant)\n * @param animate - Enable/disable animation (respects prefers-reduced-motion)\n * @param aria-label - Accessibility label for screen readers\n */\nexport function UnifiedSkeleton({\n  className,\n  variant = 'default',\n  width,\n  height,\n  animate = true,\n  'aria-label': ariaLabel,\n  ...props\n}: UnifiedSkeletonProps) {\n  const baseClasses = 'bg-ods-skeleton';\n\n  const variantClasses = {\n    default: 'rounded-md',\n    text: 'rounded-sm',\n    circular: 'rounded-full',\n    rectangular: 'rounded-none',\n  };\n\n  const defaultHeights = {\n    default: 'h-4',\n    text: 'h-4',\n    circular: 'h-12 w-12',\n    rectangular: 'h-4',\n  };\n\n  const animationClasses = animate ? 'animate-pulse motion-reduce:animate-none' : '';\n\n  // An `aspect-*` utility sizes the box itself; a default height would win\n  // over it (explicit height beats aspect-ratio) and collapse the box to 16px.\n  const sizedByAspect = /(?:^|\\s)aspect-/.test(className ?? '');\n\n  const style: React.CSSProperties = {\n    width: width,\n    height: height,\n  };\n\n  return (\n    <div\n      className={cn(\n        baseClasses,\n        variantClasses[variant],\n        !height && !style.height && !sizedByAspect && defaultHeights[variant],\n        !width && !style.width && variant !== 'circular' && 'w-full',\n        animationClasses,\n        className,\n      )}\n      style={style}\n      role=\"status\"\n      aria-label={ariaLabel || 'Loading content'}\n      {...props}\n    />\n  );\n}\n\n/**\n * Text skeleton variants for consistent typography loading\n */\nexport const TextSkeleton = {\n  /**\n   * Large heading skeleton (h1, h2)\n   */\n  Heading: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"text\"\n      className={cn('h-8 md:h-10 lg:h-12', className)}\n      aria-label=\"Loading heading\"\n      {...props}\n    />\n  ),\n\n  /**\n   * Medium heading skeleton (h3, h4)\n   */\n  Subheading: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"text\"\n      className={cn('h-6 md:h-7 lg:h-8', className)}\n      aria-label=\"Loading subheading\"\n      {...props}\n    />\n  ),\n\n  /**\n   * Body text skeleton\n   */\n  Body: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton variant=\"text\" className={cn('h-4 md:h-5', className)} aria-label=\"Loading text\" {...props} />\n  ),\n\n  /**\n   * Small text skeleton (captions, metadata)\n   */\n  Caption: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton variant=\"text\" className={cn('h-3 md:h-4', className)} aria-label=\"Loading caption\" {...props} />\n  ),\n};\n\n/**\n * Interactive element skeletons\n */\nexport const InteractiveSkeleton = {\n  /**\n   * Button skeleton with proper touch targets\n   */\n  Button: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"default\"\n      className={cn('h-10 w-32 md:h-12 md:w-40', className)}\n      aria-label=\"Loading button\"\n      {...props}\n    />\n  ),\n\n  /**\n   * Input field skeleton\n   */\n  Input: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"default\"\n      className={cn('h-10 w-full md:h-12', className)}\n      aria-label=\"Loading input field\"\n      {...props}\n    />\n  ),\n\n  /**\n   * Filter chip skeleton\n   */\n  Chip: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"default\"\n      className={cn('h-8 w-20 rounded-full', className)}\n      aria-label=\"Loading filter\"\n      {...props}\n    />\n  ),\n};\n\n/**\n * Media skeletons for images and icons\n */\nexport const MediaSkeleton = {\n  /**\n   * Avatar/profile image skeleton\n   */\n  Avatar: ({\n    size = 'md',\n    className,\n    ...props\n  }: Omit<UnifiedSkeletonProps, 'variant'> & { size?: 'sm' | 'md' | 'lg' }) => {\n    const sizeClasses = {\n      sm: 'h-8 w-8',\n      md: 'h-12 w-12',\n      lg: 'h-16 w-16',\n    };\n\n    return (\n      <UnifiedSkeleton\n        variant=\"circular\"\n        className={cn(sizeClasses[size], className)}\n        aria-label=\"Loading profile image\"\n        {...props}\n      />\n    );\n  },\n\n  /**\n   * Card image skeleton (1200×630 OG aspect — matches entity-cards default\n   * so loading state never causes a height shift when card data arrives)\n   */\n  CardImage: ({ className, ...props }: Omit<UnifiedSkeletonProps, 'variant'>) => (\n    <UnifiedSkeleton\n      variant=\"default\"\n      className={cn('aspect-[1200/630] w-full', className)}\n      aria-label=\"Loading image\"\n      {...props}\n    />\n  ),\n\n  /**\n   * Icon skeleton\n   */\n  Icon: ({\n    size = 'md',\n    className,\n    ...props\n  }: Omit<UnifiedSkeletonProps, 'variant'> & { size?: 'sm' | 'md' | 'lg' }) => {\n    const sizeClasses = {\n      sm: 'h-4 w-4',\n      md: 'h-6 w-6',\n      lg: 'h-8 w-8',\n    };\n\n    return (\n      <UnifiedSkeleton\n        variant=\"default\"\n        className={cn(sizeClasses[size], className)}\n        aria-label=\"Loading icon\"\n        {...props}\n      />\n    );\n  },\n};\n","import { cn } from '../../utils/cn';\nimport { TextSkeleton, MediaSkeleton, InteractiveSkeleton } from './unified-skeleton';\n\ninterface CardSkeletonProps {\n  className?: string;\n  /**\n   * Card layout variant\n   */\n  variant?: 'vendor' | 'blog' | 'category' | 'alternative';\n  /**\n   * Show action buttons area\n   */\n  showActions?: boolean;\n  /**\n   * Show metadata footer\n   */\n  showMetadata?: boolean;\n  /** Optional tailwind classes to override the card container background & border */\n  containerClassName?: string;\n}\n\n/**\n * Unified card skeleton component for consistent card loading states\n *\n * Supports different card types used across the application:\n * - vendor: Vendor cards with logo, title, description, and actions\n * - blog: Blog post cards with image, title, summary, and metadata  \n * - category: Category cards with icon, title, and description\n * - alternative: Alternative vendor cards in comparison lists\n */\nexport function CardSkeleton({\n  className,\n  containerClassName,\n  variant = 'vendor',\n  showActions = true,\n  showMetadata = true,\n  ...props\n}: CardSkeletonProps) {\n  const cardContent = {\n    vendor: <VendorCardContent showMetadata={showMetadata} />,\n    blog: <BlogCardContent showActions={showActions} showMetadata={showMetadata} />,\n    category: <CategoryCardContent />,\n    alternative: <AlternativeCardContent showActions={showActions} />,\n  };\n\n  return (\n    <div\n      className={cn(\n        containerClassName || 'border border-ods-border bg-ods-card',\n        'overflow-hidden rounded-lg',\n        // Flex layouts for certain variants\n        variant === 'blog' && 'flex h-full flex-col',\n        variant === 'vendor' && 'flex h-full flex-col',\n        className,\n      )}\n      role=\"status\"\n      aria-label={`Loading ${variant} card`}\n      {...props}\n    >\n      {cardContent[variant]}\n    </div>\n  );\n}\n\n/**\n * Vendor card skeleton content - matches exact VendorCard structure\n */\nfunction VendorCardContent({ showMetadata }: { showMetadata: boolean }) {\n  return (\n    <div className=\"flex flex-col gap-3 p-4\">\n      {/* Header Section - Row layout matching actual VendorCard */}\n      <div className=\"flex w-full items-start gap-3\">\n        {/* Logo Frame - 60px width fixed, matching actual structure */}\n        <div className=\"flex h-[60px] w-[60px] flex-shrink-0 items-center justify-center rounded-lg border border-ods-border bg-ods-bg p-2\">\n          <MediaSkeleton.Avatar size=\"sm\" className=\"h-11 w-11\" />\n        </div>\n\n        {/* Text Container - Column layout, matching actual structure */}\n        <div className=\"flex min-w-0 flex-1 flex-col justify-center space-y-1 py-2\">\n          {/* Title - Single line with proper width */}\n          <TextSkeleton.Subheading className=\"w-3/4\" />\n          {/* Category - Single line, shorter */}\n          <TextSkeleton.Caption className=\"w-1/2\" />\n        </div>\n      </div>\n\n      {/* Description Section - Fixed 48px height matching actual VendorCard */}\n      <div className=\"flex h-12 w-full items-center overflow-hidden\">\n        <div className=\"w-full space-y-1\">\n          <TextSkeleton.Body className=\"w-full\" />\n          <TextSkeleton.Body className=\"w-2/3\" />\n        </div>\n      </div>\n\n      {/* Footer Section - Responsive layout matching actual structure */}\n      {showMetadata && (\n        <div className=\"flex w-full min-w-0 items-center justify-between gap-2\">\n          {/* Stats Container - Flexible width, no overflow */}\n          <div className=\"flex min-w-0 flex-shrink items-center gap-3 md:gap-4\">\n            {/* OpenMSP Score skeleton */}\n            <div className=\"flex flex-shrink-0 items-center gap-1\">\n              <MediaSkeleton.Icon size=\"sm\" className=\"h-5 w-5\" />\n              <TextSkeleton.Caption className=\"w-8\" />\n            </div>\n\n            {/* GitHub Stats skeleton */}\n            <div className=\"flex flex-shrink-0 items-center gap-1\">\n              <MediaSkeleton.Icon size=\"sm\" className=\"h-5 w-5\" />\n              <TextSkeleton.Caption className=\"w-10\" />\n            </div>\n          </div>\n\n          {/* Tag Section - Contained within card boundaries */}\n          <div className=\"flex-shrink-0\">\n            <div className=\"flex items-center gap-1.5 rounded border border-ods-border bg-ods-bg px-2.5 py-1.5\">\n              <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n                <MediaSkeleton.Icon size=\"sm\" className=\"h-2.5 w-2.5\" />\n              </div>\n              <TextSkeleton.Caption className=\"w-16\" />\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\n/**\n * Blog card skeleton content - matches fixed height structure\n */\nfunction BlogCardContent({ showActions, showMetadata }: { showActions: boolean; showMetadata: boolean }) {\n  return (\n    <>\n      {/* Image Section — OG 1200×630 aspect (matches blog-card.tsx loaded state) */}\n      <div className=\"blog-card-image-container relative aspect-[1200/630] w-full overflow-hidden bg-ods-bg\">\n        <MediaSkeleton.CardImage />\n      </div>\n\n      {/* Content - Fixed height structure to match BlogCard */}\n      <div className=\"flex flex-grow flex-col p-4\">\n        {/* Title Section - Fixed 2 lines with vertical centering */}\n        <div className=\"mb-3 flex min-h-[50.4px] items-center md:min-h-[56px] lg:min-h-[61.6px]\">\n          <div className=\"w-full space-y-1\">\n            <TextSkeleton.Subheading className=\"w-full\" />\n            <TextSkeleton.Subheading className=\"w-3/4\" />\n          </div>\n        </div>\n\n        {/* Chips Section - Fixed single line height */}\n        <div className=\"mb-3 flex h-[28px] items-center gap-2\">\n          <InteractiveSkeleton.Chip className=\"w-16\" />\n          <InteractiveSkeleton.Chip className=\"w-12\" />\n        </div>\n\n        {/* Description Section - Fixed 2 lines with vertical centering */}\n        <div className=\"mb-3 flex min-h-[42px] items-center md:min-h-[45px] lg:min-h-[48px]\">\n          <div className=\"w-full space-y-1\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-1/2\" />\n          </div>\n        </div>\n\n        {/* Actions - only if requested */}\n        {showActions && (\n          <div className=\"pt-2\">\n            <InteractiveSkeleton.Button className=\"h-8 w-24\" />\n          </div>\n        )}\n\n        {/* Metadata footer - Matches BlogMeta horizontal layout */}\n        {showMetadata && (\n          <div className=\"mt-auto\">\n            <div className=\"flex items-center justify-between gap-4 border-t border-ods-border pt-4\">\n              {/* Author section - matches AuthorMeta */}\n              <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n                <MediaSkeleton.Avatar size=\"sm\" />\n                <TextSkeleton.Caption className=\"w-16\" />\n              </div>\n\n              {/* Date and reading time section - matches BlogMeta right side */}\n              <div className=\"flex shrink-0 items-center gap-3 text-ods-text-muted\">\n                <TextSkeleton.Caption className=\"w-12\" />\n                {/* Separator dot */}\n                <div className=\"h-1 w-1 rounded-full bg-ods-skeleton\"></div>\n                <TextSkeleton.Caption className=\"w-16\" />\n              </div>\n            </div>\n          </div>\n        )}\n      </div>\n    </>\n  );\n}\n\n/**\n * Category card skeleton content\n */\nfunction CategoryCardContent() {\n  return (\n    <div className=\"space-y-4 p-4 md:p-6\">\n      {/* Icon grid */}\n      <div className=\"flex gap-2 md:gap-3\">\n        {Array.from({ length: 6 }).map((_, index) => (\n          <MediaSkeleton.Icon key={index} size=\"lg\" className=\"flex-shrink-0\" />\n        ))}\n      </div>\n\n      {/* Content */}\n      <div className=\"space-y-2\">\n        <div className=\"space-y-1\">\n          <TextSkeleton.Subheading className=\"w-3/4\" />\n          <TextSkeleton.Caption className=\"w-1/2\" />\n        </div>\n\n        <div className=\"flex items-start justify-between gap-4 md:items-end md:gap-6\">\n          <div className=\"flex-1 space-y-2\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-2/3\" />\n          </div>\n\n          <InteractiveSkeleton.Button className=\"h-10 w-10 flex-shrink-0 md:h-12 md:w-12\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * Alternative card skeleton content (for vendor alternatives/comparisons)\n */\nfunction AlternativeCardContent({ showActions }: { showActions: boolean }) {\n  return (\n    <div className=\"flex items-start gap-4 p-4\">\n      <MediaSkeleton.Avatar size=\"md\" className=\"flex-shrink-0\" />\n\n      <div className=\"flex-1 space-y-2\">\n        <div className=\"flex items-center justify-between\">\n          <TextSkeleton.Subheading className=\"w-1/3\" />\n          <TextSkeleton.Caption className=\"w-16\" />\n        </div>\n\n        <div className=\"space-y-1\">\n          <TextSkeleton.Body className=\"w-full\" />\n          <TextSkeleton.Body className=\"w-5/6\" />\n        </div>\n\n        {showActions && (\n          <div className=\"pt-2\">\n            <InteractiveSkeleton.Button className=\"h-8 w-20\" />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Grid of card skeletons for loading lists\n */\nexport function CardSkeletonGrid({\n  count = 6,\n  variant = 'vendor',\n  className,\n  containerClassName,\n  ...props\n}: {\n  count?: number;\n  variant?: CardSkeletonProps['variant'];\n  className?: string;\n  containerClassName?: string;\n} & Omit<CardSkeletonProps, 'variant'>) {\n  return (\n    <div\n      className={cn(\n        'grid gap-4 md:gap-6',\n        // Responsive grid based on card type\n        variant === 'vendor' && 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',\n        variant === 'blog' && 'grid-cols-1 md:grid-cols-2',\n        variant === 'category' && 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',\n        variant === 'alternative' && 'grid-cols-1',\n        className,\n      )}\n      role=\"status\"\n      aria-label={`Loading ${count} ${variant} cards`}\n    >\n      {Array.from({ length: count }, (_, index) => (\n        <CardSkeleton key={index} variant={variant} containerClassName={containerClassName} {...props} />\n      ))}\n    </div>\n  );\n}\n","import type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { CardSkeletonGrid } from './card-skeleton';\nimport { UnifiedSkeleton, TextSkeleton, MediaSkeleton, InteractiveSkeleton } from './unified-skeleton';\n\ninterface PageLayoutSkeletonProps {\n  className?: string;\n}\n\n/**\n * Announcement bar skeleton that matches the AnnouncementBar component\n */\nexport function AnnouncementBarSkeleton() {\n  return (\n    <div className=\"relative w-full animate-pulse bg-ods-skeleton\">\n      <div className=\"relative flex w-full flex-row items-center\">\n        <div className=\"relative box-border flex w-full flex-row items-center justify-start gap-4 py-3 pl-4 pr-12 md:gap-6 md:py-4 md:pl-6 md:pr-16\">\n          {/* Logo skeleton */}\n          <div className=\"relative h-6 w-6 shrink-0 rounded bg-ods-border md:h-8 md:w-8\"></div>\n\n          {/* Text content skeleton */}\n          <div className=\"min-w-0 flex-1 space-y-1 md:space-y-2\">\n            <div className=\"h-[14px] w-3/4 max-w-md rounded bg-ods-border md:h-[18px]\"></div>\n            <div className=\"hidden h-[12px] w-full max-w-lg rounded bg-ods-border md:block md:h-[18px]\"></div>\n          </div>\n\n          {/* Close button skeleton */}\n          <div className=\"absolute right-2 top-2 h-6 w-6 rounded bg-ods-border\"></div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * Header skeleton that matches the ClientOnlyHeader placeholder but with proper animations\n */\nexport function HeaderSkeleton() {\n  return (\n    <header className=\"sticky top-0 z-40 flex w-full animate-pulse items-center justify-between border-b border-ods-border bg-ods-card bg-ods-card/95 px-4 py-3 backdrop-blur-sm md:px-[80px] md:py-[12px]\">\n      {/* Left: Logo skeleton */}\n      <div className=\"flex min-w-0 flex-1 items-center justify-start\">\n        <div className=\"h-[26px] w-[110px] rounded bg-ods-skeleton md:h-8 md:w-[137px]\"></div>\n      </div>\n\n      {/* Center: Navigation skeleton - hidden on mobile, visible on desktop */}\n      <nav className=\"hidden min-w-0 flex-1 basis-1/3 items-center justify-center gap-2 md:flex\">\n        <div className=\"flex items-center gap-2\">\n          <InteractiveSkeleton.Button className=\"h-10 w-24\" />\n          <InteractiveSkeleton.Button className=\"h-10 w-24\" />\n          <InteractiveSkeleton.Button className=\"h-10 w-24\" />\n        </div>\n      </nav>\n\n      {/* Right: Actions skeleton */}\n      <div className=\"flex min-w-0 flex-1 items-center justify-end gap-4\">\n        {/* Mobile: Show hamburger skeleton */}\n        <div className=\"md:hidden\">\n          <InteractiveSkeleton.Button className=\"h-10 w-10\" />\n        </div>\n\n        {/* Desktop: Show action buttons skeletons */}\n        <div className=\"hidden items-center gap-4 md:flex\">\n          <InteractiveSkeleton.Button className=\"h-10 w-10\" />\n          <InteractiveSkeleton.Button className=\"h-10 w-32\" />\n          <InteractiveSkeleton.Button className=\"h-10 w-20\" />\n        </div>\n      </div>\n    </header>\n  );\n}\n\n/**\n * Hero section skeleton for static content areas\n */\nexport function HeroSkeleton() {\n  return (\n    <section\n      className=\"flex w-full animate-pulse flex-col items-center justify-center px-4 py-12 text-center md:px-8 md:py-20\"\n      style={{\n        background: 'radial-gradient(circle at 50% 0%, #242323 0%, #1A1A1A 100%)',\n      }}\n    >\n      {/* Title skeleton */}\n      <TextSkeleton.Heading className=\"mb-4 h-12 w-full max-w-4xl md:mb-6 md:h-20 lg:h-24\" />\n\n      {/* Subtitle skeleton */}\n      <div className=\"mb-8 w-full max-w-4xl space-y-3 px-2 md:mb-10\">\n        <TextSkeleton.Body className=\"h-5 md:h-7\" />\n        <TextSkeleton.Body className=\"mx-auto h-5 w-3/4 md:h-7\" />\n      </div>\n\n      {/* CTA Button skeleton */}\n      <InteractiveSkeleton.Button className=\"h-12 w-full md:w-64\" />\n    </section>\n  );\n}\n\n/**\n * Search container skeleton with filters\n */\nexport function SearchContainerSkeleton({\n  className,\n  showFilters = true,\n}: PageLayoutSkeletonProps & { showFilters?: boolean }) {\n  return (\n    <div className={cn('space-y-4', className || '')}>\n      {/* Search input and button */}\n      <div className=\"flex gap-2 md:gap-4\">\n        <InteractiveSkeleton.Input className=\"flex-1\" />\n        <InteractiveSkeleton.Button className=\"w-32 md:w-40\" />\n      </div>\n\n      {/* Filter chips */}\n      {showFilters && (\n        <div className=\"flex flex-wrap gap-2\">\n          <InteractiveSkeleton.Chip />\n          <InteractiveSkeleton.Chip className=\"w-24\" />\n          <InteractiveSkeleton.Chip className=\"w-16\" />\n          <InteractiveSkeleton.Chip className=\"w-20\" />\n        </div>\n      )}\n    </div>\n  );\n}\n\n/**\n * Category sidebar skeleton for filtering pages\n */\nexport function CategorySidebarSkeleton({ className }: PageLayoutSkeletonProps) {\n  // Matches the actual MultiLevelNavigation sidebar with folder/file icons, README badges, and chevrons\n  const items = [\n    { type: 'file', width: 'w-20', hasBadge: false },\n    { type: 'folder', width: 'w-24', hasBadge: true },\n    { type: 'folder', width: 'w-20', hasBadge: true },\n    { type: 'folder', width: 'w-24', hasBadge: true },\n    { type: 'folder', width: 'w-16', hasBadge: true },\n    { type: 'folder', width: 'w-20', hasBadge: true },\n    { type: 'folder', width: 'w-28', hasBadge: true },\n    { type: 'folder', width: 'w-16', hasBadge: true },\n    { type: 'folder', width: 'w-24', hasBadge: true },\n    { type: 'file', width: 'w-28' },\n    { type: 'file', width: 'w-36' },\n  ];\n\n  return (\n    <div className={cn('w-full lg:w-[320px]', className)}>\n      {/* DATA ROOM label */}\n      <UnifiedSkeleton className=\"mb-4 h-[14px] w-24 rounded\" />\n\n      {/* Navigation items — each has card background matching actual MultiLevelNavigation */}\n      <div className=\"space-y-1.5\">\n        {items.map((item, index) => (\n          <div\n            key={index}\n            className={cn(\n              'flex min-h-[50px] items-center justify-between rounded-lg border border-ods-border px-4 py-4',\n              index === 0 ? 'bg-ods-accent/20' : 'bg-ods-card',\n            )}\n          >\n            <div className=\"flex items-center gap-2.5\">\n              <UnifiedSkeleton className=\"h-4 w-4 shrink-0 rounded\" />\n              <UnifiedSkeleton className={`h-[14px] ${item.width} rounded`} />\n            </div>\n            <div className=\"flex h-[18px] items-center gap-2\">\n              {item.hasBadge && <UnifiedSkeleton className=\"h-[18px] w-14 rounded\" />}\n              {item.type === 'folder' && <UnifiedSkeleton className=\"h-4 w-4 rounded\" />}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Breadcrumb navigation skeleton\n */\nexport function BreadcrumbSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn('mb-6 flex items-center space-x-1', className)}>\n      <TextSkeleton.Caption className=\"w-16\" />\n      <UnifiedSkeleton variant=\"default\" className=\"h-4 w-4 rounded-full\" />\n      <TextSkeleton.Caption className=\"w-24\" />\n      <UnifiedSkeleton variant=\"default\" className=\"h-4 w-4 rounded-full\" />\n      <TextSkeleton.Caption className=\"w-32\" />\n      <UnifiedSkeleton variant=\"default\" className=\"h-4 w-4 rounded-full\" />\n      <TextSkeleton.Caption className=\"w-24\" />\n    </div>\n  );\n}\n\n/**\n * Results header skeleton with count and sorting\n */\nexport function ResultsHeaderSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn('mb-6 flex flex-col items-start justify-between gap-4 md:flex-row md:items-center', className)}>\n      <div className=\"space-y-1\">\n        <TextSkeleton.Body className=\"w-48\" />\n        {/* <TextSkeleton.Caption className=\"w-32\" /> */}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Two-column layout skeleton (sidebar + main content)\n */\nexport function TwoColumnLayoutSkeleton({\n  className,\n  sidebarContent,\n  mainContent,\n  sidebarPosition = 'left',\n}: PageLayoutSkeletonProps & {\n  sidebarContent?: React.ReactNode;\n  mainContent?: React.ReactNode;\n  sidebarPosition?: 'left' | 'right';\n}) {\n  const sidebar = sidebarContent || <CategorySidebarSkeleton />;\n  const main = mainContent || <CardSkeletonGrid count={6} />;\n\n  return (\n    <div\n      className={cn(\n        'grid grid-cols-1 gap-6 lg:grid-cols-[320px_1fr] lg:gap-8',\n        sidebarPosition === 'right' && 'lg:grid-cols-[1fr_320px]',\n        className,\n      )}\n    >\n      {sidebarPosition === 'left' ? (\n        <>\n          <aside className=\"order-2 lg:order-1\">{sidebar}</aside>\n          <main className=\"order-1 lg:order-2\">{main}</main>\n        </>\n      ) : (\n        <>\n          <main className=\"order-1\">{main}</main>\n          <aside className=\"order-2\">{sidebar}</aside>\n        </>\n      )}\n    </div>\n  );\n}\n\n/**\n * Article/blog post layout skeleton\n */\nexport function ArticleLayoutSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <article className={cn('mx-auto max-w-4xl', className)}>\n      {/* Article header */}\n      <header className=\"mb-8 space-y-4 md:mb-12 md:space-y-6\">\n        {/* Category/tags */}\n        <div className=\"flex gap-2\">\n          <InteractiveSkeleton.Chip />\n          <InteractiveSkeleton.Chip className=\"w-16\" />\n        </div>\n\n        {/* Title */}\n        <div className=\"space-y-3\">\n          <TextSkeleton.Heading className=\"w-full\" />\n          <TextSkeleton.Heading className=\"w-3/4\" />\n        </div>\n\n        {/* Metadata */}\n        <div className=\"flex items-center gap-4 border-t border-ods-divider pt-4\">\n          <div className=\"flex items-center gap-2\">\n            <MediaSkeleton.Avatar size=\"sm\" />\n            <TextSkeleton.Caption className=\"w-24\" />\n          </div>\n          <TextSkeleton.Caption className=\"w-20\" />\n          <TextSkeleton.Caption className=\"w-16\" />\n        </div>\n      </header>\n\n      {/* Featured image */}\n      <div className=\"mb-8 md:mb-12\">\n        <MediaSkeleton.CardImage className=\"h-64 w-full md:h-96\" />\n      </div>\n\n      {/* Article content */}\n      <div className=\"prose prose-invert max-w-none space-y-6\">\n        {/* Paragraphs */}\n        {Array.from({ length: 12 }).map((_, index) => (\n          <div key={index} className=\"space-y-2\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-5/6\" />\n            <TextSkeleton.Body className=\"w-3/4\" />\n          </div>\n        ))}\n\n        {/* Subheading in content */}\n        <div className=\"py-4\">\n          <TextSkeleton.Subheading className=\"mb-4 w-2/3\" />\n          <div className=\"space-y-2\">\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-4/5\" />\n          </div>\n        </div>\n      </div>\n    </article>\n  );\n}\n\n/**\n * Vendor Detail Layout Skeleton - Complete vendor detail page structure\n * Matches the refactored vendor detail page with proper platform colors and responsive layout\n */\nexport function VendorDetailLayoutSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <main className={cn('min-h-screen bg-ods-bg', className)}>\n      <div className=\"mx-auto max-w-[1920px] px-6 py-6 md:px-20 md:py-10\">\n        {/* Breadcrumb */}\n        <BreadcrumbSkeleton className=\"mb-6\" />\n\n        {/* Main Layout Container */}\n        <div className=\"flex flex-col lg:flex-row lg:gap-10\">\n          {/* Left Content Area */}\n          <div className=\"min-w-0 flex-1\">\n            {/* Vendor Hero Section */}\n            <div className=\"mb-10\">\n              {/* Header - Logo and Title Side by Side */}\n              <div className=\"mb-6 flex gap-6\">\n                <div className=\"h-20 w-20 flex-shrink-0 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n\n                <div className=\"flex flex-col gap-3\">\n                  <div className=\"flex flex-col gap-2\">\n                    {/* Large title skeleton */}\n                    <div className=\"h-12 w-80 max-w-full animate-pulse rounded bg-ods-skeleton md:h-16 lg:h-20\"></div>\n                    {/* Category text */}\n                    <div className=\"h-5 w-32 animate-pulse rounded bg-ods-skeleton md:h-6\"></div>\n                  </div>\n\n                  {/* Pricing tags */}\n                  <div className=\"flex items-center gap-2\">\n                    <div className=\"h-8 w-20 animate-pulse rounded bg-ods-skeleton\"></div>\n                    <div className=\"h-8 w-16 animate-pulse rounded bg-ods-skeleton\"></div>\n                  </div>\n                </div>\n              </div>\n\n              {/* Vendor Image Display Skeleton */}\n              <div className=\"mb-2 h-[300px] w-full animate-pulse rounded-lg border border-ods-border bg-ods-card md:h-[400px] lg:h-[500px]\"></div>\n              <div className=\"text-center\">\n                <div className=\"mx-auto h-4 w-24 animate-pulse rounded bg-ods-skeleton\"></div>\n              </div>\n            </div>\n\n            {/* Mobile Sidebar - Show on mobile only, positioned after title */}\n            <div className=\"mb-10 lg:hidden\">\n              <div className=\"space-y-4\">\n                {/* Deploy Button */}\n                <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n\n                {/* Voting Buttons */}\n                <div className=\"space-y-2\">\n                  <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                  <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                </div>\n\n                {/* GitHub Score Section */}\n                <div className=\"overflow-hidden rounded-lg border border-ods-border\">\n                  {/* Header */}\n                  <div className=\"flex items-center gap-3 bg-ods-card p-4\">\n                    <div className=\"h-8 w-8 animate-pulse rounded bg-ods-skeleton\"></div>\n                    <div className=\"flex flex-col gap-1\">\n                      <div className=\"h-4 w-20 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-6 w-12 animate-pulse rounded bg-ods-skeleton\"></div>\n                    </div>\n                  </div>\n\n                  {/* Stats */}\n                  <div className=\"space-y-3 p-4\">\n                    {Array.from({ length: 4 }).map((_, i) => (\n                      <div key={i} className=\"flex items-center gap-3\">\n                        <div className=\"h-6 w-6 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"flex items-end gap-1\">\n                          <div className=\"h-4 w-12 animate-pulse rounded bg-ods-skeleton\"></div>\n                          <div className=\"h-3 w-8 animate-pulse rounded bg-ods-skeleton\"></div>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n\n                {/* Action Buttons */}\n                <div className=\"space-y-3\">\n                  <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                  <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                  <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                </div>\n              </div>\n            </div>\n\n            {/* Alternatives Container */}\n            <div className=\"mb-20 flex flex-col gap-6\">\n              <div className=\"h-8 w-48 animate-pulse rounded bg-ods-skeleton md:h-10\"></div>\n\n              {/* Open Source Alternatives */}\n              <div className=\"flex flex-col gap-4\">\n                <div className=\"h-5 w-48 animate-pulse rounded bg-ods-skeleton\"></div>\n                <div className=\"flex flex-wrap gap-2\">\n                  {Array.from({ length: 6 }).map((_, i) => (\n                    <div\n                      key={i}\n                      className=\"h-10 w-24 animate-pulse rounded-lg border border-ods-border bg-ods-card\"\n                    ></div>\n                  ))}\n                </div>\n              </div>\n\n              {/* Commercial Alternatives */}\n              <div className=\"flex flex-col gap-4\">\n                <div className=\"h-5 w-52 animate-pulse rounded bg-ods-skeleton\"></div>\n                <div className=\"flex flex-wrap gap-2\">\n                  {Array.from({ length: 4 }).map((_, i) => (\n                    <div\n                      key={i}\n                      className=\"h-10 w-28 animate-pulse rounded-lg border border-ods-border bg-ods-card\"\n                    ></div>\n                  ))}\n                </div>\n              </div>\n            </div>\n\n            {/* About Section */}\n            <div className=\"mb-20 flex flex-col gap-6\">\n              <div className=\"h-8 w-64 animate-pulse rounded bg-ods-skeleton md:h-10\"></div>\n\n              <div className=\"rounded-lg border border-ods-border bg-ods-card p-6 md:p-8\">\n                <div className=\"space-y-4\">\n                  <div className=\"h-6 animate-pulse rounded bg-ods-skeleton\"></div>\n                  <div className=\"h-6 animate-pulse rounded bg-ods-skeleton\"></div>\n                  <div className=\"h-6 w-5/6 animate-pulse rounded bg-ods-skeleton\"></div>\n                  <div className=\"h-6 w-4/5 animate-pulse rounded bg-ods-skeleton\"></div>\n                </div>\n              </div>\n            </div>\n\n            {/* Key Features Section */}\n            <div className=\"mb-20 flex flex-col gap-6\">\n              <div className=\"h-8 w-72 animate-pulse rounded bg-ods-skeleton md:h-10\"></div>\n              <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n                {Array.from({ length: 6 }).map((_, i) => (\n                  <div key={i} className=\"flex gap-4 rounded-lg border border-ods-border bg-ods-card p-4\">\n                    <div className=\"h-12 w-12 flex-shrink-0 animate-pulse rounded bg-ods-skeleton\"></div>\n                    <div className=\"flex-1 space-y-2\">\n                      <div className=\"h-5 w-3/4 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-4 animate-pulse rounded bg-ods-skeleton\"></div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n            </div>\n\n            {/* Pros and Cons Section */}\n            <div className=\"mb-20 flex flex-col gap-6\">\n              <div className=\"h-8 w-80 animate-pulse rounded bg-ods-skeleton md:h-10\"></div>\n              <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n                {/* Pros Column */}\n                <div className=\"flex flex-col gap-6\">\n                  <div className=\"h-5 w-16 animate-pulse rounded bg-ods-skeleton\"></div>\n                  {Array.from({ length: 3 }).map((_, i) => (\n                    <div key={i} className=\"flex gap-3 rounded-lg border border-ods-border bg-ods-card p-4\">\n                      <div className=\"h-12 w-12 flex-shrink-0 animate-pulse rounded border border-ods-border bg-ods-bg\"></div>\n                      <div className=\"flex-1 space-y-2\">\n                        <div className=\"h-5 w-2/3 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n                {/* Cons Column */}\n                <div className=\"flex flex-col gap-6\">\n                  <div className=\"h-5 w-16 animate-pulse rounded bg-ods-skeleton\"></div>\n                  {Array.from({ length: 3 }).map((_, i) => (\n                    <div key={i} className=\"flex gap-3 rounded-lg border border-ods-border bg-ods-card p-4\">\n                      <div className=\"h-12 w-12 flex-shrink-0 animate-pulse rounded border border-ods-border bg-ods-bg\"></div>\n                      <div className=\"flex-1 space-y-2\">\n                        <div className=\"h-5 w-2/3 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            </div>\n\n            {/* Alternatives Section */}\n            <div className=\"mb-20 flex flex-col gap-6\">\n              <div className=\"h-8 w-72 animate-pulse rounded bg-ods-skeleton md:h-10\"></div>\n              <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n                {Array.from({ length: 6 }).map((_, i) => (\n                  <div key={i} className=\"space-y-4 rounded-lg border border-ods-border bg-ods-card p-6\">\n                    {/* Header */}\n                    <div className=\"flex items-start gap-4\">\n                      <div className=\"h-16 w-16 flex-shrink-0 animate-pulse rounded-lg border border-ods-border bg-ods-bg\"></div>\n                      <div className=\"flex-1 space-y-2\">\n                        <div className=\"h-6 w-3/4 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 w-1/2 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                      <div className=\"h-6 w-20 animate-pulse rounded bg-ods-skeleton\"></div>\n                    </div>\n\n                    {/* Description */}\n                    <div className=\"space-y-2\">\n                      <div className=\"h-4 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-4 w-5/6 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-4 w-4/5 animate-pulse rounded bg-ods-skeleton\"></div>\n                    </div>\n\n                    {/* Footer */}\n                    <div className=\"flex items-center justify-between border-t border-ods-border pt-2\">\n                      <div className=\"h-4 w-16 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-4 w-20 animate-pulse rounded bg-ods-skeleton\"></div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n              {/* Show All Button */}\n              <div className=\"h-12 animate-pulse rounded-md bg-ods-skeleton\"></div>\n            </div>\n\n            {/* Comments Section */}\n            <div className=\"mb-20 flex flex-col gap-6\">\n              <div className=\"h-8 w-64 animate-pulse rounded bg-ods-skeleton md:h-10\"></div>\n\n              {/* Comment Form Skeleton */}\n              <div className=\"space-y-3\">\n                <div className=\"flex gap-3\">\n                  <div className=\"flex-1 overflow-hidden rounded-lg border border-ods-border bg-ods-bg\">\n                    {/* Title Section */}\n                    <div className=\"border-b border-ods-border p-3\">\n                      <div className=\"mb-2 h-3 w-8 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-6 w-full animate-pulse rounded bg-ods-skeleton\"></div>\n                    </div>\n\n                    {/* Description Section */}\n                    <div className=\"p-3\">\n                      <div className=\"mb-2 h-3 w-16 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"space-y-2\">\n                        <div className=\"h-4 w-full animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 w-3/4 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 w-1/2 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                    </div>\n                  </div>\n\n                  {/* Send Button */}\n                  <div className=\"flex items-stretch\">\n                    <div className=\"h-[120px] w-20 animate-pulse rounded-lg bg-ods-skeleton\"></div>\n                  </div>\n                </div>\n              </div>\n\n              {/* Sample Comment Cards */}\n              <div className=\"space-y-4\">\n                {Array.from({ length: 3 }).map((_, i) => (\n                  <div key={i} className=\"rounded-lg border border-ods-border bg-ods-card p-4\">\n                    {/* Comment Header */}\n                    <div className=\"mb-3 flex flex-col gap-3 min-[420px]:flex-row min-[420px]:items-center min-[420px]:justify-between min-[420px]:gap-2\">\n                      <div className=\"flex items-center gap-2\">\n                        {/* User Info */}\n                        <div className=\"flex items-center gap-2 rounded-lg border border-ods-border bg-ods-card px-3 py-2\">\n                          <div className=\"h-8 w-8 animate-pulse rounded-lg bg-ods-skeleton\"></div>\n                          <div className=\"h-4 w-20 animate-pulse rounded bg-ods-skeleton\"></div>\n                        </div>\n\n                        {/* Timestamp */}\n                        <div className=\"h-4 w-12 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                    </div>\n\n                    {/* Comment Content */}\n                    <div className=\"space-y-2\">\n                      <div className=\"h-6 w-2/3 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"space-y-1\">\n                        <div className=\"h-4 w-full animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 w-4/5 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-4 w-3/5 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n            </div>\n          </div>\n\n          {/* Right Sidebar - Desktop Only */}\n          <div className=\"hidden w-[290px] flex-shrink-0 lg:block\">\n            <div className=\"space-y-4\">\n              {/* Deploy Button */}\n              <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n\n              {/* Voting Buttons */}\n              <div className=\"space-y-2\">\n                <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n              </div>\n\n              {/* GitHub Score Section */}\n              <div className=\"overflow-hidden rounded-lg border border-ods-border\">\n                {/* Header */}\n                <div className=\"flex items-center gap-3 bg-ods-card p-4\">\n                  <div className=\"h-8 w-8 animate-pulse rounded bg-ods-skeleton\"></div>\n                  <div className=\"flex flex-col gap-1\">\n                    <div className=\"h-4 w-20 animate-pulse rounded bg-ods-skeleton\"></div>\n                    <div className=\"h-6 w-12 animate-pulse rounded bg-ods-skeleton\"></div>\n                  </div>\n                </div>\n\n                {/* Stats */}\n                <div className=\"space-y-3 p-4\">\n                  {Array.from({ length: 4 }).map((_, i) => (\n                    <div key={i} className=\"flex items-center gap-3\">\n                      <div className=\"h-6 w-6 animate-pulse rounded bg-ods-skeleton\"></div>\n                      <div className=\"flex items-end gap-1\">\n                        <div className=\"h-4 w-12 animate-pulse rounded bg-ods-skeleton\"></div>\n                        <div className=\"h-3 w-8 animate-pulse rounded bg-ods-skeleton\"></div>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n\n              {/* Action Buttons */}\n              <div className=\"space-y-3\">\n                <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n                <div className=\"h-12 animate-pulse rounded-lg border border-ods-border bg-ods-card\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </main>\n  );\n}\n\n/**\n * Stats/features section skeleton for homepage\n */\nexport function StatsSectionSkeleton({ className, columns = 3 }: PageLayoutSkeletonProps & { columns?: number }) {\n  return (\n    <div\n      className={cn(\n        'mb-12 grid gap-6 md:mb-16',\n        columns === 3 && 'grid-cols-1 md:grid-cols-3',\n        columns === 4 && 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',\n        className,\n      )}\n    >\n      {Array.from({ length: columns }).map((_, i) => (\n        <div key={i} className=\"rounded-lg border border-ods-border bg-ods-card p-6\">\n          <div className=\"space-y-4\">\n            <MediaSkeleton.Icon size=\"lg\" />\n            <TextSkeleton.Subheading className=\"w-3/4\" />\n            <div className=\"space-y-2\">\n              <TextSkeleton.Body className=\"w-full\" />\n              <TextSkeleton.Body className=\"w-3/4\" />\n            </div>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/**\n * Blog Card Grid Skeleton - Always displays exactly 4 blog card skeletons\n * Used for consistent blog page layout with predictable height\n */\nexport function BlogCardGridSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn('space-y-6', className)}>\n      <CardSkeletonGrid count={4} variant=\"blog\" className=\"grid-cols-1 md:grid-cols-2\" />\n    </div>\n  );\n}\n\n/**\n * Vendor Grid Skeleton - Always displays exactly 12 vendor card skeletons\n * Used for consistent vendor page layout with predictable height\n */\nexport function VendorGridSkeleton({ className }: PageLayoutSkeletonProps) {\n  return (\n    <div className={cn('space-y-6', className)}>\n      <CardSkeletonGrid count={12} variant=\"vendor\" className=\"grid-cols-1 md:grid-cols-2 lg:grid-cols-3\" />\n    </div>\n  );\n}\n\n/**\n * Slack Community Section Skeleton\n * Matches SlackCommunity component structure with title, channel list, and chat interface\n */\nexport function SlackCommunitySkeleton() {\n  return (\n    <section className=\"w-full bg-ods-bg px-4 py-12 md:px-20 md:py-20 lg:px-20\" aria-label=\"Slack Community Loading\">\n      {/* Frame 651 Container */}\n      <div className=\"flex w-full flex-col gap-4 md:gap-6\">\n        {/* Title Skeleton */}\n        <div className=\"w-full\">\n          <div className=\"h-8 max-w-md animate-pulse rounded-lg bg-ods-skeleton md:h-12 lg:h-14\"></div>\n        </div>\n\n        {/* Content Area - Channel List + Chat Interface */}\n        <div className=\"flex w-full min-w-0 flex-col gap-4 md:gap-6 lg:flex-row lg:items-start lg:justify-end\">\n          {/* Channel List Skeleton */}\n          <div className=\"flex w-full flex-shrink-0 animate-pulse flex-col overflow-hidden rounded border border-ods-border bg-ods-bg lg:w-[290px] lg:max-w-[290px]\">\n            <div className=\"space-y-4 p-4\">\n              {/* Header */}\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"flex items-center justify-between\">\n                  <div className=\"h-4 w-24 rounded bg-ods-skeleton\"></div>\n                  <div className=\"h-3 w-16 rounded bg-ods-skeleton\"></div>\n                </div>\n                <div className=\"h-3 w-32 rounded bg-ods-skeleton\"></div>\n              </div>\n\n              {/* Channels */}\n              <div className=\"flex flex-col gap-1\">\n                <div className=\"mb-2 h-3 w-16 rounded bg-ods-skeleton\"></div>\n                {Array.from({ length: 5 }).map((_, i) => (\n                  <div key={i} className=\"flex min-h-[48px] items-center gap-2 px-2 py-3\">\n                    <div className=\"h-4 w-4 rounded bg-ods-skeleton\"></div>\n                    <div className=\"flex-1\">\n                      <div className=\"mb-1 h-3 w-24 rounded bg-ods-skeleton\"></div>\n                      <div className=\"h-2 w-16 rounded bg-ods-skeleton\"></div>\n                    </div>\n                  </div>\n                ))}\n              </div>\n\n              {/* Stats */}\n              <div className=\"mt-2 border-t border-ods-border pt-3\">\n                <div className=\"space-y-2\">\n                  <div className=\"flex justify-between\">\n                    <div className=\"h-3 w-16 rounded bg-ods-skeleton\"></div>\n                    <div className=\"h-3 w-8 rounded bg-ods-skeleton\"></div>\n                  </div>\n                  <div className=\"flex justify-between\">\n                    <div className=\"h-3 w-20 rounded bg-ods-skeleton\"></div>\n                    <div className=\"h-3 w-8 rounded bg-ods-skeleton\"></div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          {/* Chat Interface Skeleton */}\n          <div className=\"flex h-[450px] min-h-[450px] min-w-0 flex-1 animate-pulse flex-col rounded-lg border border-ods-border bg-ods-card md:h-[500px] md:min-h-[500px] lg:h-[600px] lg:min-h-[600px]\">\n            {/* Header */}\n            <div className=\"flex h-[56px] flex-shrink-0 items-center justify-between border-b border-ods-border bg-ods-bg p-4 md:h-[60px] md:p-6\">\n              <div className=\"flex items-center gap-2\">\n                <div className=\"h-3 w-3 rounded bg-ods-skeleton md:h-4 md:w-4\"></div>\n                <div className=\"h-3 w-24 rounded bg-ods-skeleton md:h-4 md:w-32\"></div>\n              </div>\n              <div className=\"h-3 w-12 rounded bg-ods-skeleton md:w-16\"></div>\n            </div>\n\n            {/* Messages */}\n            <div className=\"min-h-[280px] flex-1 space-y-3 overflow-hidden bg-ods-bg p-4 md:min-h-[320px] md:space-y-4 md:p-6 lg:min-h-[420px]\">\n              {Array.from({ length: 6 }).map((_, i) => (\n                <div key={i} className=\"flex gap-2 md:gap-3\">\n                  <div className=\"h-10 w-10 flex-shrink-0 rounded-lg bg-ods-skeleton md:h-12 md:w-12\"></div>\n                  <div className=\"flex-1\">\n                    <div className=\"mb-1 flex items-center gap-2\">\n                      <div className=\"h-3 w-16 rounded bg-ods-skeleton md:w-20\"></div>\n                      <div className=\"h-3 w-12 rounded bg-ods-skeleton md:w-16\"></div>\n                    </div>\n                    <div className=\"h-3 w-full rounded bg-ods-skeleton md:h-4\"></div>\n                  </div>\n                </div>\n              ))}\n            </div>\n\n            {/* Input Area */}\n            <div className=\"h-[72px] flex-shrink-0 border-t border-ods-border bg-ods-card p-4 md:h-[80px] md:p-6\">\n              <div className=\"flex items-end justify-center gap-3 md:justify-end\">\n                <div className=\"h-10 w-28 rounded-lg bg-ods-skeleton md:h-12 md:w-32\"></div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ContentLoadingContainerProps {\n  /**\n   * Whether the content is currently loading\n   */\n  isLoading: boolean;\n  /**\n   * The actual content to display when not loading\n   */\n  children: ReactNode;\n  /**\n   * The skeleton component to show during loading\n   */\n  skeletonComponent: ReactNode;\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n  /**\n   * Minimum height to prevent layout jumps\n   */\n  minHeight?: string;\n  /**\n   * Loading overlay opacity (0-1)\n   */\n  loadingOpacity?: number;\n  /**\n   * Transition duration in milliseconds\n   */\n  transitionDuration?: number;\n}\n\n/**\n * ContentLoadingContainer\n *\n * A unified loading container that wraps card grids and manages loading states\n * while keeping UI controls persistent. This component:\n *\n * - Shows skeleton loading only in content area\n * - Maintains consistent layout dimensions during loading\n * - Provides smooth transitions between loading and loaded states\n * - Prevents layout jumps by preserving container height\n * - Supports customizable skeleton components for different content types\n *\n * Usage:\n * ```tsx\n * <ContentLoadingContainer\n *   isLoading={isLoadingVendors}\n *   skeletonComponent={<CardSkeletonGrid variant=\"vendor\" count={12} />}\n *   minHeight=\"min-h-[800px]\"\n * >\n *   <VendorGrid vendors={vendors} />\n * </ContentLoadingContainer>\n * ```\n */\nexport function ContentLoadingContainer({\n  isLoading,\n  children,\n  skeletonComponent,\n  className,\n  minHeight = 'min-h-[300px] md:min-h-[800px]',\n  loadingOpacity = 1,\n  transitionDuration = 300,\n}: ContentLoadingContainerProps) {\n  return (\n    <div\n      className={cn('relative w-full transition-all ease-in-out', minHeight, className)}\n      style={{\n        transitionDuration: `${transitionDuration}ms`,\n      }}\n      role=\"region\"\n      aria-label={isLoading ? 'Loading content' : 'Content loaded'}\n      aria-live=\"polite\"\n    >\n      {/* Loading Skeleton Overlay */}\n      {isLoading && (\n        <div\n          className=\"absolute inset-0 z-10 bg-ods-bg\"\n          style={{\n            opacity: loadingOpacity,\n            transition: `opacity ${transitionDuration}ms ease-in-out`,\n          }}\n          role=\"status\"\n          aria-label=\"Loading content\"\n        >\n          {skeletonComponent}\n        </div>\n      )}\n\n      {/* Actual Content */}\n      <div\n        className={cn('relative transition-opacity ease-in-out', isLoading && 'opacity-0')}\n        style={{\n          transitionDuration: `${transitionDuration}ms`,\n        }}\n        aria-hidden={isLoading}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Hook for managing content loading states with common patterns\n */\nexport function useContentLoading(isLoading: boolean) {\n  const containerProps = {\n    'aria-busy': isLoading,\n    'data-loading': isLoading,\n  };\n\n  const getSkeletonCount = (contentType: 'vendor' | 'blog') => {\n    return contentType === 'vendor' ? 12 : 6;\n  };\n\n  const getMinHeight = (contentType: 'vendor' | 'blog') => {\n    // Vendor cards are typically taller, need more space\n    return contentType === 'vendor' ? 'min-h-[400px] md:min-h-[900px]' : 'min-h-[300px] md:min-h-[800px]';\n  };\n\n  return {\n    containerProps,\n    getSkeletonCount,\n    getMinHeight,\n  };\n}\n","import { cn } from '../../utils/cn';\nimport { UnifiedSkeleton, TextSkeleton, MediaSkeleton, InteractiveSkeleton } from './unified-skeleton';\n\ninterface ContentSkeletonProps {\n  className?: string;\n}\n\n/**\n * Paragraph skeleton with varying line lengths for natural appearance\n */\nexport function ParagraphSkeleton({ className, lines = 4 }: ContentSkeletonProps & { lines?: number }) {\n  const lineWidths = ['w-full', 'w-full', 'w-5/6', 'w-3/4', 'w-4/5', 'w-2/3'];\n\n  return (\n    <div className={cn('space-y-2', className)}>\n      {Array.from({ length: lines }).map((_, index) => (\n        <TextSkeleton.Body key={index} className={lineWidths[index % lineWidths.length]} />\n      ))}\n    </div>\n  );\n}\n\n/**\n * List skeleton for navigation menus, categories, etc.\n */\nexport function ListSkeleton({\n  className,\n  items = 5,\n  showIcons = false,\n  showActions = false,\n}: ContentSkeletonProps & {\n  items?: number;\n  showIcons?: boolean;\n  showActions?: boolean;\n}) {\n  return (\n    <div className={cn('space-y-3', className)}>\n      {Array.from({ length: items }).map((_, index) => (\n        <div key={index} className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n            <TextSkeleton.Body className=\"w-32 md:w-48\" />\n          </div>\n          {showActions && (\n            <div className=\"flex items-center gap-2\">\n              <TextSkeleton.Caption className=\"w-8\" />\n              <UnifiedSkeleton variant=\"default\" className=\"h-4 w-4\" />\n            </div>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/**\n * Table skeleton for data displays\n */\nexport function TableSkeleton({\n  className,\n  rows = 5,\n  columns = 4,\n}: ContentSkeletonProps & {\n  rows?: number;\n  columns?: number;\n}) {\n  return (\n    <div className={cn('space-y-4', className)}>\n      {/* Table header */}\n      <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n        {Array.from({ length: columns }).map((_, index) => (\n          <TextSkeleton.Subheading key={index} className=\"w-3/4\" />\n        ))}\n      </div>\n\n      {/* Table rows */}\n      <div className=\"space-y-3\">\n        {Array.from({ length: rows }).map((_, rowIndex) => (\n          <div\n            key={rowIndex}\n            className=\"grid gap-4 border-b border-ods-divider py-2\"\n            style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}\n          >\n            {Array.from({ length: columns }).map((_cell, colIndex) => (\n              <TextSkeleton.Body key={colIndex} className=\"w-2/3\" />\n            ))}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\n/**\n * Form skeleton for input fields and form layouts\n */\nexport function FormSkeleton({ className, fields = 4 }: ContentSkeletonProps & { fields?: number }) {\n  return (\n    <div className={cn('space-y-6', className)}>\n      {Array.from({ length: fields }).map((_, index) => (\n        <div key={index} className=\"space-y-2\">\n          <TextSkeleton.Body className=\"w-24\" />\n          <InteractiveSkeleton.Input />\n          {index % 3 === 0 && <TextSkeleton.Caption className=\"w-48\" />}\n        </div>\n      ))}\n\n      <div className=\"flex gap-4 pt-4\">\n        <InteractiveSkeleton.Button />\n        <InteractiveSkeleton.Button className=\"bg-ods-border\" />\n      </div>\n    </div>\n  );\n}\n\n/**\n * Navigation menu skeleton\n */\nexport function NavigationSkeleton({\n  className,\n  items = 6,\n  horizontal = true,\n}: ContentSkeletonProps & {\n  items?: number;\n  horizontal?: boolean;\n}) {\n  return (\n    <nav\n      className={cn('flex gap-4 md:gap-6', !horizontal && 'flex-col', className)}\n      role=\"status\"\n      aria-label=\"Loading navigation\"\n    >\n      {Array.from({ length: items }).map((_, index) => (\n        <TextSkeleton.Body key={index} className=\"w-16 md:w-20\" />\n      ))}\n    </nav>\n  );\n}\n\n/**\n * Profile/user info skeleton\n */\nexport function ProfileSkeleton({\n  className,\n  showBio = true,\n  showStats = true,\n}: ContentSkeletonProps & {\n  showBio?: boolean;\n  showStats?: boolean;\n}) {\n  return (\n    <div className={cn('space-y-4', className)}>\n      {/* Avatar and basic info */}\n      <div className=\"flex items-start gap-4\">\n        <MediaSkeleton.Avatar size=\"lg\" />\n        <div className=\"flex-1 space-y-2\">\n          <TextSkeleton.Subheading className=\"w-1/2\" />\n          <TextSkeleton.Caption className=\"w-1/3\" />\n          <TextSkeleton.Body className=\"w-2/3\" />\n        </div>\n      </div>\n\n      {/* Bio/description */}\n      {showBio && (\n        <div className=\"space-y-2\">\n          <TextSkeleton.Body className=\"w-full\" />\n          <TextSkeleton.Body className=\"w-4/5\" />\n          <TextSkeleton.Body className=\"w-3/4\" />\n        </div>\n      )}\n\n      {/* Stats */}\n      {showStats && (\n        <div className=\"flex gap-6\">\n          <div className=\"text-center\">\n            <TextSkeleton.Subheading className=\"mx-auto w-8\" />\n            <TextSkeleton.Caption className=\"mx-auto w-12\" />\n          </div>\n          <div className=\"text-center\">\n            <TextSkeleton.Subheading className=\"mx-auto w-8\" />\n            <TextSkeleton.Caption className=\"mx-auto w-16\" />\n          </div>\n          <div className=\"text-center\">\n            <TextSkeleton.Subheading className=\"mx-auto w-8\" />\n            <TextSkeleton.Caption className=\"mx-auto w-14\" />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\n/**\n * Comment/review skeleton for user-generated content\n */\nexport function CommentSkeleton({\n  className,\n  showRating = false,\n  showReplies = false,\n}: ContentSkeletonProps & {\n  showRating?: boolean;\n  showReplies?: boolean;\n}) {\n  return (\n    <div className={cn('space-y-3', className)}>\n      {/* User info */}\n      <div className=\"flex items-center gap-3\">\n        <MediaSkeleton.Avatar size=\"sm\" />\n        <div className=\"flex flex-1 items-center gap-4\">\n          <TextSkeleton.Body className=\"w-24\" />\n          <TextSkeleton.Caption className=\"w-16\" />\n          {showRating && (\n            <div className=\"flex gap-1\">\n              {Array.from({ length: 5 }).map((_, i) => (\n                <MediaSkeleton.Icon key={i} size=\"sm\" />\n              ))}\n            </div>\n          )}\n        </div>\n      </div>\n\n      {/* Comment content */}\n      <div className=\"ml-11 space-y-2\">\n        <TextSkeleton.Body className=\"w-full\" />\n        <TextSkeleton.Body className=\"w-3/4\" />\n        <TextSkeleton.Body className=\"w-1/2\" />\n      </div>\n\n      {/* Actions */}\n      <div className=\"ml-11 flex gap-4\">\n        <TextSkeleton.Caption className=\"w-12\" />\n        <TextSkeleton.Caption className=\"w-16\" />\n        <TextSkeleton.Caption className=\"w-8\" />\n      </div>\n\n      {/* Replies */}\n      {showReplies && (\n        <div className=\"ml-11 space-y-4 border-l border-ods-divider pl-4\">\n          {Array.from({ length: 2 }).map((_, index) => (\n            <div key={index} className=\"space-y-2\">\n              <div className=\"flex items-center gap-3\">\n                <MediaSkeleton.Avatar size=\"sm\" />\n                <TextSkeleton.Body className=\"w-20\" />\n                <TextSkeleton.Caption className=\"w-12\" />\n              </div>\n              <div className=\"ml-11\">\n                <TextSkeleton.Body className=\"w-full\" />\n                <TextSkeleton.Body className=\"w-2/3\" />\n              </div>\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\n/**\n * Feature list skeleton for product features, specifications, etc.\n */\nexport function FeatureListSkeleton({\n  className,\n  features = 6,\n  showIcons = true,\n  grouped = false,\n}: ContentSkeletonProps & {\n  features?: number;\n  showIcons?: boolean;\n  grouped?: boolean;\n}) {\n  if (grouped) {\n    // Grouped features with sections\n    return (\n      <div className={cn('space-y-6', className)}>\n        {Array.from({ length: 3 }).map((_, groupIndex) => (\n          <div key={groupIndex} className=\"space-y-3\">\n            <TextSkeleton.Subheading className=\"w-1/3\" />\n            <div className=\"space-y-2\">\n              {Array.from({ length: features / 3 }).map((_feature, featureIndex) => (\n                <div key={featureIndex} className=\"flex items-center gap-3\">\n                  {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n                  <TextSkeleton.Body className=\"w-2/3\" />\n                </div>\n              ))}\n            </div>\n          </div>\n        ))}\n      </div>\n    );\n  }\n\n  // Simple feature list\n  return (\n    <div className={cn('space-y-3', className)}>\n      {Array.from({ length: features }).map((_, index) => (\n        <div key={index} className=\"flex items-center gap-3\">\n          {showIcons && <MediaSkeleton.Icon size=\"sm\" />}\n          <TextSkeleton.Body className=\"w-2/3\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/**\n * Timeline/activity skeleton\n */\nexport function TimelineSkeleton({ className, items = 5 }: ContentSkeletonProps & { items?: number }) {\n  return (\n    <div className={cn('space-y-6', className)}>\n      {Array.from({ length: items }).map((_, index) => (\n        <div key={index} className=\"flex gap-4\">\n          <div className=\"flex flex-col items-center\">\n            <MediaSkeleton.Icon size=\"sm\" />\n            {index < items - 1 && <div className=\"mt-2 h-12 w-px bg-ods-border\" />}\n          </div>\n          <div className=\"flex-1 space-y-2\">\n            <div className=\"flex items-center gap-3\">\n              <TextSkeleton.Body className=\"w-1/3\" />\n              <TextSkeleton.Caption className=\"w-16\" />\n            </div>\n            <TextSkeleton.Body className=\"w-full\" />\n            <TextSkeleton.Body className=\"w-3/4\" />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\n/**\n * Pricing/plan skeleton\n */\nexport function PricingSkeleton({ className, plans = 3 }: ContentSkeletonProps & { plans?: number }) {\n  return (\n    <div\n      className={cn(\n        'grid gap-6',\n        plans === 2 && 'grid-cols-1 md:grid-cols-2',\n        plans === 3 && 'grid-cols-1 md:grid-cols-3',\n        plans === 4 && 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',\n        className,\n      )}\n    >\n      {Array.from({ length: plans }).map((_, index) => (\n        <div key={index} className=\"rounded-lg border border-ods-border bg-ods-card p-6\">\n          <div className=\"space-y-4\">\n            {/* Plan name */}\n            <TextSkeleton.Subheading className=\"w-1/2\" />\n\n            {/* Price */}\n            <div className=\"space-y-1\">\n              <TextSkeleton.Heading className=\"w-1/3\" />\n              <TextSkeleton.Caption className=\"w-1/4\" />\n            </div>\n\n            {/* Features */}\n            <div className=\"space-y-3 py-4\">\n              {Array.from({ length: 5 }).map((_feature, featureIndex) => (\n                <div key={featureIndex} className=\"flex items-center gap-2\">\n                  <MediaSkeleton.Icon size=\"sm\" />\n                  <TextSkeleton.Body className=\"w-3/4\" />\n                </div>\n              ))}\n            </div>\n\n            {/* CTA Button */}\n            <InteractiveSkeleton.Button className=\"w-full\" />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\n\ninterface ProfileLoadingSkeletonProps {\n  className?: string;\n}\n\nexport function ProfileLoadingSkeleton({ className }: ProfileLoadingSkeletonProps) {\n  return (\n    <div className={cn('space-y-6', className)}>\n      {/* Header skeleton */}\n      <div className=\"flex items-center space-x-4\">\n        <div className=\"h-16 w-16 animate-pulse rounded-full bg-ods-skeleton\" />\n        <div className=\"space-y-2\">\n          <div className=\"h-4 w-32 animate-pulse rounded bg-ods-skeleton\" />\n          <div className=\"h-3 w-24 animate-pulse rounded bg-ods-skeleton\" />\n        </div>\n      </div>\n\n      {/* Content skeleton */}\n      <div className=\"space-y-4\">\n        <div className=\"h-4 w-full animate-pulse rounded bg-ods-skeleton\" />\n        <div className=\"h-4 w-3/4 animate-pulse rounded bg-ods-skeleton\" />\n        <div className=\"h-4 w-1/2 animate-pulse rounded bg-ods-skeleton\" />\n      </div>\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { UnifiedSkeleton, TextSkeleton, InteractiveSkeleton } from './unified-skeleton';\n\ninterface SkeletonProps {\n  className?: string;\n  /**\n   * Number of input fields to render on the right column (defaults to 4).\n   */\n  fields?: number;\n}\n\n/**\n * MspProfileFormSkeleton\n * -----------------------------------------------------------------------------\n * Loading state for the <MspProfileForm /> component used in the Share-Your-Stack\n * wizard (and any other area that re-uses the form).\n *\n * Design Notes:\n * – Two-column grid matching the live component (logo upload on the left,\n *   text / numeric inputs on the right).\n * – Left column: square/circular media skeleton to represent logo uploader.\n * – Right column: label + input pair per field.\n * – Mobile (<768px) collapses to one column via grid existing classes.\n * – Uses UnifiedSkeleton system to stay consistent with global loading design.\n */\nexport function MspProfileFormSkeleton({ className, fields = 4 }: SkeletonProps) {\n  return (\n    <div\n      className={cn('grid grid-cols-1 items-stretch gap-6 md:grid-cols-2', className)}\n      role=\"status\"\n      aria-label=\"Loading MSP profile form\"\n    >\n      {/* Left – Logo uploader placeholder */}\n      <div className=\"flex h-full flex-col space-y-2\">\n        {/* Label skeleton */}\n        <TextSkeleton.Body className=\"w-28\" />\n        <div className=\"min-h-[180px] flex-1 md:min-h-full\">\n          <UnifiedSkeleton className=\"h-full w-full rounded-lg\" aria-label=\"Loading company logo\" />\n        </div>\n      </div>\n\n      {/* Right – Input fields grid (mirrors form) */}\n      <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n        {Array.from({ length: fields }).map((_, idx) => (\n          <div key={idx} className=\"flex flex-col space-y-2\">\n            <TextSkeleton.Body className=\"w-40\" />\n            <InteractiveSkeleton.Input />\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { UnifiedSkeleton, TextSkeleton } from './unified-skeleton';\n\ninterface CategoryCardSkeletonProps {\n  className?: string;\n}\n\nexport function CategoryCardSkeleton({ className }: CategoryCardSkeletonProps) {\n  return (\n    <article\n      className={cn(\n        'box-border flex min-w-0 flex-col rounded-[12px] border border-ods-border bg-ods-card p-8',\n        className,\n      )}\n      role=\"status\"\n      aria-label=\"Loading category card\"\n    >\n      {/* Icons row */}\n      <div className=\"mb-8 flex items-center justify-center gap-6\">\n        {Array.from({ length: 10 }).map((_, i) => (\n          <UnifiedSkeleton key={i} variant=\"circular\" className=\"h-10 w-10 flex-shrink-0\" aria-label=\"Loading icon\" />\n        ))}\n      </div>\n\n      {/* Text block */}\n      <div className=\"flex flex-1 flex-col space-y-3\">\n        <TextSkeleton.Heading className=\"w-3/4\" />\n        <TextSkeleton.Body className=\"w-1/2\" />\n        <TextSkeleton.Body className=\"w-full\" />\n      </div>\n\n      {/* Arrow button placeholder */}\n      <div className=\"mt-4 flex justify-end\">\n        <UnifiedSkeleton className=\"h-12 w-12 rounded-[6px]\" aria-label=\"Loading button\" />\n      </div>\n    </article>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { TextSkeleton, UnifiedSkeleton } from './unified-skeleton';\n\ninterface CategoryVendorSelectorSkeletonProps {\n  /** number of subcategory blocks to render */\n  subcategories?: number;\n  className?: string;\n}\n\nexport function CategoryVendorSelectorSkeleton({ subcategories = 3, className }: CategoryVendorSelectorSkeletonProps) {\n  return (\n    <div\n      className={cn('rounded-lg border border-ods-border bg-ods-card p-6', className)}\n      role=\"status\"\n      aria-label=\"Loading category section\"\n    >\n      {/* Header */}\n      <div className=\"mb-6 max-w-xl space-y-2\">\n        <TextSkeleton.Heading className=\"w-1/2\" />\n        <TextSkeleton.Body className=\"w-3/4\" />\n      </div>\n\n      {/* Subcategory blocks */}\n      <div className=\"space-y-6\">\n        {Array.from({ length: subcategories }).map((_, idx) => (\n          <div key={idx} className=\"space-y-2\">\n            {/* Subcategory title */}\n            <TextSkeleton.Subheading className=\"w-1/3\" />\n            {/* Vendor slots container */}\n            <div className=\"grid grid-cols-2 gap-4 md:grid-cols-3\">\n              {Array.from({ length: 2 }).map((__, j) => (\n                <UnifiedSkeleton key={j} className=\"h-20 rounded-lg\" />\n              ))}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { UnifiedSkeleton, TextSkeleton } from './unified-skeleton';\n\ninterface WizardLayoutSkeletonProps {\n  steps?: number;\n  className?: string;\n}\n\nexport function WizardLayoutSkeleton({ steps = 6, className }: WizardLayoutSkeletonProps) {\n  return (\n    <div className={cn('space-y-6', className)} role=\"status\" aria-label=\"Loading wizard layout\">\n      {/* Progress bar */}\n      <div className=\"flex flex-wrap items-center gap-2\">\n        {Array.from({ length: steps }).map((_, i) => (\n          <UnifiedSkeleton key={i} className=\"h-6 w-24 rounded\" />\n        ))}\n      </div>\n\n      {/* Header */}\n      <div className=\"max-w-2xl space-y-2\">\n        <TextSkeleton.Heading className=\"w-2/3\" />\n        <TextSkeleton.Body className=\"w-1/2\" />\n      </div>\n\n      {/* Bottom navigation buttons */}\n      <div className=\"mt-10 flex justify-between\">\n        <UnifiedSkeleton className=\"h-10 w-24 rounded\" />\n        <UnifiedSkeleton className=\"h-10 w-32 rounded\" />\n      </div>\n    </div>\n  );\n}\n","import type React from 'react';\n\ninterface MarginReportSkeletonProps {\n  /** Enable pulse animation (default: true) */\n  animate?: boolean;\n  /** Optional explanation text shown above overlay content */\n  description?: React.ReactNode;\n  /** Optional React node displayed over skeleton (button, loader, etc.) */\n  overlayContent?: React.ReactNode;\n}\n\nexport function MarginReportSkeleton({ animate = true, description, overlayContent }: MarginReportSkeletonProps) {\n  return (\n    <main className={`bg-ods-bg ${animate ? 'animate-pulse' : ''} relative min-h-screen`}>\n      <div className=\"mx-auto max-w-[1920px] space-y-10 px-6 py-6 md:px-20 md:py-10\">\n        {/* Header */}\n        <div className=\"space-y-3\">\n          <div className=\"h-10 w-72 rounded bg-ods-skeleton\" />\n          <div className=\"h-4 w-80 rounded bg-ods-skeleton\" />\n        </div>\n\n        {/* Summary cards */}\n        <div className=\"grid grid-cols-1 gap-6 md:grid-cols-3\">\n          {Array.from({ length: 3 }).map((_, i) => (\n            <div key={i} className=\"h-32 rounded border border-ods-border bg-ods-card\" />\n          ))}\n        </div>\n\n        {/* MSP Profile & Report Info cards */}\n        <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2\">\n          {/* MSP profile skeleton */}\n          <div className=\"flex animate-pulse items-center gap-4 rounded-lg border border-ods-border bg-ods-card p-6\">\n            <div className=\"h-14 w-14 rounded-lg bg-ods-skeleton\" />\n            <div className=\"flex-1 space-y-2\">\n              <div className=\"h-4 w-3/4 rounded bg-ods-skeleton\" />\n              <div className=\"h-3 w-1/2 rounded bg-ods-skeleton\" />\n            </div>\n          </div>\n\n          {/* Report info skeleton */}\n          <div className=\"flex animate-pulse flex-col gap-4 rounded-lg border border-ods-border bg-ods-card p-6\">\n            <div className=\"flex items-center justify-between gap-4\">\n              <div className=\"flex items-center gap-2\">\n                <div className=\"h-10 w-10 rounded-lg bg-ods-skeleton\" />\n                <div className=\"space-y-1\">\n                  <div className=\"h-4 w-32 rounded bg-ods-skeleton\" />\n                  <div className=\"h-3 w-20 rounded bg-ods-skeleton\" />\n                </div>\n              </div>\n              <div className=\"h-6 w-36 rounded bg-ods-skeleton\" />\n            </div>\n            <div className=\"mt-4 h-4 w-40 rounded bg-ods-skeleton\" />\n          </div>\n        </div>\n\n        {/* Vendor solution lists (Commercial & Open-Source) */}\n        <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2\">\n          {['Commercial Stack', 'Open-Source Stack'].map((label, idx) => (\n            <div\n              key={idx}\n              className=\"flex animate-pulse flex-col overflow-hidden rounded-lg border border-ods-border bg-ods-card\"\n            >\n              {/* list header */}\n              <div className=\"flex items-center justify-between px-6 py-4\">\n                <div className=\"h-6 w-40 rounded bg-ods-skeleton\" />\n                <div className=\"flex items-center gap-2\">\n                  <div className=\"h-5 w-20 rounded bg-ods-skeleton\" />\n                  <div className=\"h-4 w-10 rounded bg-ods-skeleton\" />\n                </div>\n              </div>\n\n              {/* vendor rows */}\n              <div className=\"flex flex-1 flex-col gap-3 p-3\">\n                {Array.from({ length: 5 }).map((_, j) => (\n                  <div\n                    key={j}\n                    className=\"flex items-center justify-between rounded-lg border border-ods-border bg-ods-bg px-4 py-3\"\n                  >\n                    {/* left section: icon + text */}\n                    <div className=\"flex min-w-0 items-center gap-3\">\n                      <div className=\"h-12 w-12 flex-shrink-0 rounded-lg bg-ods-skeleton\" />\n                      <div className=\"flex min-w-0 flex-col\">\n                        <div className=\"h-4 w-32 rounded bg-ods-skeleton\" />\n                        <div className=\"mt-1 hidden h-3 w-24 rounded bg-ods-skeleton md:block\" />\n                      </div>\n                    </div>\n                    {/* right addon: cost text */}\n                    <div className=\"flex flex-shrink-0 items-center gap-1\">\n                      <div className=\"h-4 w-16 rounded bg-ods-skeleton\" />\n                      <div className=\"h-3 w-8 rounded bg-ods-skeleton\" />\n                    </div>\n                  </div>\n                ))}\n              </div>\n            </div>\n          ))}\n        </div>\n\n        {/* Strategic Recommendations header placeholder */}\n        <div className=\"h-6 w-60 rounded bg-ods-skeleton\" />\n\n        {/* Recommendations grid */}\n        <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n          {Array.from({ length: 3 }).map((_, i) => (\n            <div key={i} className=\"h-48 rounded border border-ods-border bg-ods-card\" />\n          ))}\n        </div>\n\n        {/* OpenFrame value section */}\n        <div className=\"space-y-6 rounded-3xl border border-ods-border p-8\">\n          {/* Section header */}\n          <div className=\"flex items-start gap-6\">\n            <div className=\"min-w-0 flex-1 space-y-2\">\n              <div className=\"h-8 w-72 rounded bg-ods-skeleton\" />\n              <div className=\"h-4 w-3/4 rounded bg-ods-skeleton\" />\n            </div>\n            {/* Logo placeholder */}\n            <div className=\"h-12 w-12 shrink-0 rounded-md bg-ods-skeleton\" />\n          </div>\n\n          {/* Value cards grid */}\n          <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n            {Array.from({ length: 3 }).map((_, i) => (\n              <div key={i} className=\"h-32 rounded border border-ods-border bg-ods-card\" />\n            ))}\n          </div>\n        </div>\n      </div>\n\n      {/* CTA Overlay */}\n      {overlayContent && (\n        <div className=\"pointer-events-none absolute inset-0 z-10 rounded-lg bg-ods-card/80\">\n          {/* Button centered relative to viewport */}\n          <div className=\"pointer-events-auto fixed left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center gap-4 px-4 text-center\">\n            {description && <h3>{description}</h3>}\n            {overlayContent}\n          </div>\n        </div>\n      )}\n    </main>\n  );\n}\n","import { cn } from '../../utils/cn';\n\ninterface Props {\n  rows?: number;\n  className?: string;\n}\n\nexport function UsersGridSkeleton({ rows = 10, className }: Props) {\n  const skeletonRows = Array.from({ length: rows });\n  return (\n    <div className={cn('animate-pulse overflow-x-auto rounded-lg border border-ods-border bg-ods-card', className)}>\n      <table className=\"min-w-full divide-y divide-ods-border\">\n        <thead className=\"bg-ods-skeleton\">\n          <tr>\n            {['Name', 'Email', 'Role', 'Created', 'Last Sign-In'].map(h => (\n              <th key={h} className=\"px-4 py-3 text-left text-ods-text-primary text-h6\">\n                {h}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody className=\"divide-y divide-ods-border\">\n          {skeletonRows.map((_, idx) => (\n            <tr key={idx}>\n              {Array.from({ length: 5 }).map((__, cell) => (\n                <td key={cell} className=\"whitespace-nowrap px-4 py-3\">\n                  <div className=\"h-4 w-full rounded bg-ods-border\"></div>\n                </td>\n              ))}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n}\n","'use client';\n\nimport React from 'react';\nimport { useThemeAware } from '../hooks/use-theme-aware';\nimport { cn } from '../utils/cn';\n\ninterface DynamicSkeletonProps {\n  /**\n   * Type of skeleton animation\n   */\n  animation?: 'pulse' | 'shimmer' | 'wave' | 'static';\n\n  /**\n   * Skeleton variant based on content type\n   */\n  variant?: 'text' | 'avatar' | 'card' | 'button' | 'image' | 'custom';\n\n  /**\n   * Size preset for common elements\n   */\n  size?: 'sm' | 'md' | 'lg' | 'xl';\n\n  /**\n   * Custom dimensions\n   */\n  width?: string | number;\n  height?: string | number;\n\n  /**\n   * Number of lines for text skeleton\n   */\n  lines?: number;\n\n  /**\n   * Platform-aware styling\n   */\n  platformAware?: boolean;\n\n  /**\n   * Accessibility enhancements\n   */\n  includeAriaLabel?: boolean;\n\n  /**\n   * Custom CSS classes\n   */\n  className?: string;\n\n  /**\n   * Child elements (for container skeletons)\n   */\n  children?: React.ReactNode;\n}\n\nconst sizePresets = {\n  sm: { width: '4rem', height: '1rem' },\n  md: { width: '8rem', height: '1.5rem' },\n  lg: { width: '12rem', height: '2rem' },\n  xl: { width: '16rem', height: '2.5rem' },\n};\n\nconst variantStyles = {\n  text: 'rounded',\n  avatar: 'rounded-full aspect-square',\n  card: 'rounded-lg',\n  button: 'rounded-md',\n  image: 'rounded-lg aspect-video',\n  custom: '',\n};\n\nexport function DynamicSkeleton({\n  animation = 'shimmer',\n  variant = 'text',\n  size = 'md',\n  width,\n  height,\n  lines = 1,\n  platformAware = true,\n  includeAriaLabel = true,\n  className,\n  children,\n}: DynamicSkeletonProps) {\n  const { platform, isDark, accentColor } = useThemeAware();\n\n  // Calculate dimensions\n  const dimensions = React.useMemo(() => {\n    if (width || height) {\n      return {\n        width: typeof width === 'number' ? `${width}px` : width,\n        height: typeof height === 'number' ? `${height}px` : height,\n      };\n    }\n\n    if (variant === 'avatar') {\n      const avatarSize = sizePresets[size].height;\n      return { width: avatarSize, height: avatarSize };\n    }\n\n    return sizePresets[size];\n  }, [width, height, size, variant]);\n\n  // Platform-specific skeleton colors\n  const platformStyles = React.useMemo(() => {\n    if (!platformAware) return {};\n\n    const baseOpacity = isDark ? 0.1 : 0.15;\n    const accentOpacity = isDark ? 0.05 : 0.08;\n\n    return {\n      '--skeleton-base': `color-mix(in srgb, var(--color-bg-skeleton) ${baseOpacity * 100}%, transparent)`,\n      '--skeleton-highlight': `color-mix(in srgb, ${accentColor} ${accentOpacity * 100}%, var(--color-bg-skeleton))`,\n      '--skeleton-accent': `color-mix(in srgb, ${accentColor} 10%, transparent)`,\n    } as React.CSSProperties;\n  }, [platformAware, isDark, accentColor]);\n\n  // Animation classes\n  const animationClasses = {\n    pulse: 'animate-pulse',\n    shimmer: 'ods-loading-dynamic',\n    wave: 'skeleton-wave',\n    static: '',\n  };\n\n  // Base skeleton classes\n  const baseClasses = cn(\n    'bg-ods-skeleton',\n    variantStyles[variant],\n    animationClasses[animation],\n    platformAware && `skeleton-platform-${platform}`,\n    className,\n  );\n\n  // For text skeletons with multiple lines\n  if (variant === 'text' && lines > 1) {\n    return (\n      <div\n        className=\"space-y-2\"\n        style={platformStyles}\n        role=\"status\"\n        aria-label={includeAriaLabel ? 'Loading content...' : undefined}\n      >\n        {Array.from({ length: lines }, (_, index) => (\n          <div\n            key={index}\n            className={cn(\n              baseClasses,\n              index === lines - 1 && 'w-3/4', // Last line shorter\n            )}\n            style={{\n              ...dimensions,\n              width: index === lines - 1 ? '75%' : dimensions.width,\n            }}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  // Single skeleton element\n  return (\n    <div\n      className={baseClasses}\n      style={{ ...dimensions, ...platformStyles }}\n      role=\"status\"\n      aria-label={includeAriaLabel ? 'Loading content...' : undefined}\n    >\n      {children}\n    </div>\n  );\n}\n\n/**\n * Pre-configured skeleton for common UI patterns\n */\nexport const SkeletonPresets = {\n  /**\n   * Card skeleton with header, content, and actions\n   */\n  Card: ({ showActions = true, showImage = false }: { showActions?: boolean; showImage?: boolean }) => (\n    <div className=\"space-y-4 rounded-lg border border-ods-border bg-ods-card p-6\">\n      {showImage && <DynamicSkeleton variant=\"image\" className=\"h-40 w-full\" />}\n      <div className=\"space-y-2\">\n        <DynamicSkeleton variant=\"text\" size=\"lg\" />\n        <DynamicSkeleton variant=\"text\" lines={2} size=\"md\" />\n      </div>\n      {showActions && (\n        <div className=\"flex gap-2 pt-2\">\n          <DynamicSkeleton variant=\"button\" width=\"6rem\" height=\"2.5rem\" />\n          <DynamicSkeleton variant=\"button\" width=\"4rem\" height=\"2.5rem\" />\n        </div>\n      )}\n    </div>\n  ),\n\n  /**\n   * User profile skeleton\n   */\n  Profile: () => (\n    <div className=\"flex items-start gap-4 p-4\">\n      <DynamicSkeleton variant=\"avatar\" size=\"lg\" />\n      <div className=\"flex-1 space-y-2\">\n        <DynamicSkeleton variant=\"text\" size=\"lg\" width=\"8rem\" />\n        <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"12rem\" />\n        <DynamicSkeleton variant=\"text\" lines={2} size=\"md\" />\n      </div>\n    </div>\n  ),\n\n  /**\n   * Navigation skeleton\n   */\n  Navigation: ({ items = 5 }: { items?: number }) => (\n    <nav className=\"space-y-2 p-4\">\n      {Array.from({ length: items }, (_, index) => (\n        <div key={index} className=\"flex items-center gap-3\">\n          <DynamicSkeleton variant=\"avatar\" size=\"sm\" />\n          <DynamicSkeleton variant=\"text\" size=\"md\" />\n        </div>\n      ))}\n    </nav>\n  ),\n\n  /**\n   * Table skeleton\n   */\n  Table: ({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) => (\n    <div className=\"space-y-3\">\n      {/* Header */}\n      <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n        {Array.from({ length: columns }, (_, index) => (\n          <DynamicSkeleton key={`header-${index}`} variant=\"text\" size=\"md\" />\n        ))}\n      </div>\n      {/* Rows */}\n      {Array.from({ length: rows }, (_, rowIndex) => (\n        <div key={`row-${rowIndex}`} className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n          {Array.from({ length: columns }, (_cell, colIndex) => (\n            <DynamicSkeleton key={`cell-${rowIndex}-${colIndex}`} variant=\"text\" size=\"sm\" />\n          ))}\n        </div>\n      ))}\n    </div>\n  ),\n\n  /**\n   * Vendor grid skeleton (specific to the app)\n   */\n  VendorGrid: ({ items = 6 }: { items?: number }) => (\n    <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n      {Array.from({ length: items }, (_, index) => (\n        <div key={index} className=\"space-y-3 rounded-lg border border-ods-border bg-ods-card p-4\">\n          <div className=\"flex items-center gap-3\">\n            <DynamicSkeleton variant=\"avatar\" size=\"md\" />\n            <div className=\"flex-1\">\n              <DynamicSkeleton variant=\"text\" size=\"md\" width=\"6rem\" />\n              <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"4rem\" className=\"mt-1\" />\n            </div>\n          </div>\n          <DynamicSkeleton variant=\"text\" lines={2} size=\"sm\" />\n          <div className=\"flex gap-2\">\n            <DynamicSkeleton variant=\"custom\" width=\"3rem\" height=\"1.25rem\" className=\"rounded-full\" />\n            <DynamicSkeleton variant=\"custom\" width=\"4rem\" height=\"1.25rem\" className=\"rounded-full\" />\n          </div>\n        </div>\n      ))}\n    </div>\n  ),\n\n  /**\n   * Blog post skeleton\n   */\n  BlogPost: () => (\n    <article className=\"space-y-6\">\n      <div className=\"space-y-2\">\n        <DynamicSkeleton variant=\"text\" size=\"xl\" width=\"75%\" />\n        <div className=\"flex gap-4 text-sm\">\n          <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"4rem\" />\n          <DynamicSkeleton variant=\"text\" size=\"sm\" width=\"6rem\" />\n        </div>\n      </div>\n      <DynamicSkeleton variant=\"image\" className=\"h-64 w-full\" />\n      <div className=\"space-y-3\">\n        {Array.from({ length: 4 }, (_, index) => (\n          <DynamicSkeleton key={index} variant=\"text\" lines={3} size=\"md\" />\n        ))}\n      </div>\n    </article>\n  ),\n};\n\n/** Every preset's props merged into one all-optional bag. */\nexport interface SkeletonPresetProps {\n  showActions?: boolean;\n  showImage?: boolean;\n  items?: number;\n  rows?: number;\n  columns?: number;\n}\n\n/**\n * Platform-aware loading container that shows different skeletons based on platform\n */\nexport function PlatformSkeletonContainer({\n  children,\n  isLoading,\n  skeletonType = 'Card',\n  skeletonProps = {},\n}: {\n  children: React.ReactNode;\n  isLoading: boolean;\n  skeletonType?: keyof typeof SkeletonPresets;\n  /** Props for the chosen preset. Every preset takes an all-optional bag, so\n   *  the union of them is what any one of them accepts. */\n  skeletonProps?: SkeletonPresetProps;\n}) {\n  const { platform } = useThemeAware();\n\n  if (!isLoading) {\n    return <>{children}</>;\n  }\n\n  const SkeletonComponent = SkeletonPresets[skeletonType];\n\n  return (\n    <div className={`platform-skeleton skeleton-${platform}`}>\n      <SkeletonComponent {...skeletonProps} />\n    </div>\n  );\n}\n\n/**\n * Enhanced skeleton with progressive enhancement\n */\nexport function ProgressiveSkeleton({\n  // Named only to keep it out of the `...props` spread: the stage NAMES are\n  // unused, `currentStage` alone selects the skeleton shape below.\n  stages: _stages = ['basic', 'detailed', 'interactive'],\n  currentStage = 0,\n  children,\n  ...props\n}: DynamicSkeletonProps & {\n  stages?: string[];\n  currentStage?: number;\n}) {\n  const { platform } = useThemeAware();\n\n  // Show more detailed skeleton as loading progresses\n  const stageConfig = {\n    0: { animation: 'pulse' as const, variant: 'text' as const },\n    1: { animation: 'shimmer' as const, variant: 'card' as const },\n    2: { animation: 'wave' as const, variant: 'custom' as const },\n  };\n\n  const config = stageConfig[currentStage as keyof typeof stageConfig] || stageConfig[0];\n\n  return (\n    <DynamicSkeleton\n      {...props}\n      {...config}\n      className={cn(props.className, `skeleton-stage-${currentStage}`, `skeleton-platform-${platform}`)}\n    >\n      {children}\n    </DynamicSkeleton>\n  );\n}\n","// Stub implementation for useThemeAware hook\n\nexport function useThemeAware() {\n  return {\n    themeColors: {},\n    platformContext: 'default',\n    isDarkMode: false,\n    platform: 'default',\n    isDark: false,\n    accentColor: '#FFC008',\n  };\n}\n","import { cn } from '../../utils/cn';\n\nexport interface OrganizationIconSkeletonProps {\n  /**\n   * Size variant (matches OrganizationIcon/VendorIcon sizes)\n   * - xs: 24px (w-6 h-6)\n   * - sm: 32px (w-8 h-8) - for devices table\n   * - md: 40px (w-10 h-10) - for organizations table, dashboard (default)\n   * - lg: 48px (w-12 h-12)\n   * - l: 56px (w-14 h-14)\n   * - xl: 64px (w-16 h-16) - for detail views\n   */\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'l' | 'xl';\n\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n\n  /**\n   * Show background container (default: true)\n   */\n  showBackground?: boolean;\n\n  /**\n   * Background style variant (default: 'dark')\n   */\n  backgroundStyle?: 'dark' | 'light' | 'white';\n}\n\n/**\n * Size classes matching VendorIcon/OrganizationIcon exactly\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\n/**\n * Background style classes matching VendorIcon/OrganizationIcon exactly\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 * OrganizationIconSkeleton - Loading skeleton for OrganizationIcon\n *\n * Matches VendorIcon styling exactly for 100% visual parity.\n * Matches the exact dimensions of OrganizationIcon to prevent layout jumps.\n * Use this in loading states before organization data is available.\n *\n * Usage:\n *\n * ```typescript\n * // In table skeleton (matches current usage)\n * <OrganizationIconSkeleton size=\"md\" />\n *\n * // In device card skeleton\n * <OrganizationIconSkeleton size=\"sm\" />\n *\n * // In detail view skeleton\n * <OrganizationIconSkeleton size=\"xl\" />\n *\n * // Without background\n * <OrganizationIconSkeleton size=\"sm\" showBackground={false} />\n * ```\n */\nexport function OrganizationIconSkeleton({\n  size = 'md',\n  className = '',\n  showBackground = true,\n  backgroundStyle = 'dark',\n}: OrganizationIconSkeletonProps) {\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} role=\"status\" aria-label=\"Loading organization icon\">\n      <div className=\"h-1/2 w-1/2 animate-pulse rounded-sm bg-ods-border\" />\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { OrganizationIconSkeleton } from './organization-icon-skeleton';\nimport { TextSkeleton, MediaSkeleton } from './unified-skeleton';\n\nexport interface OrganizationCardSkeletonProps {\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n\n  /**\n   * Show footer stats area\n   */\n  showFooter?: boolean;\n\n  /**\n   * Show description area\n   */\n  showDescription?: boolean;\n\n  /** Optional tailwind classes to override the card container background & border */\n  containerClassName?: string;\n}\n\n/**\n * OrganizationCardSkeleton - Loading skeleton matching OrganizationCard exact layout\n *\n * Matches VendorCard skeleton structure for 100% visual parity.\n *\n * Structure:\n * - Header: 60x60px org logo + title + subtitle\n * - Description: Fixed 48px height with 2-line clamp\n * - Footer: Stats display area\n *\n * Prevents layout jumps by matching exact dimensions.\n */\nexport function OrganizationCardSkeleton({\n  className,\n  containerClassName,\n  showFooter = true,\n  showDescription = true,\n}: OrganizationCardSkeletonProps) {\n  return (\n    <div\n      className={cn(\n        containerClassName || 'border border-ods-border bg-ods-card',\n        'flex h-full flex-col overflow-hidden rounded-lg',\n        className,\n      )}\n      role=\"status\"\n      aria-label=\"Loading organization card\"\n    >\n      <div className=\"flex flex-col gap-3 p-4\">\n        {/* Header Section - Row layout matching OrganizationCard/VendorCard */}\n        <div className=\"flex w-full items-start gap-3\">\n          {/* Logo Frame - 60px width fixed, matching actual structure */}\n          <OrganizationIconSkeleton\n            size=\"xl\"\n            backgroundStyle=\"dark\"\n            showBackground={true}\n            className=\"h-[60px] w-[60px]\"\n          />\n\n          {/* Text Container - Column layout, matching actual structure */}\n          <div className=\"flex min-w-0 flex-1 flex-col justify-center space-y-1 py-2\">\n            {/* Title - Single line with proper width */}\n            <TextSkeleton.Subheading className=\"w-3/4\" />\n            {/* Subtitle (industry/tier) - Single line, shorter */}\n            <TextSkeleton.Caption className=\"w-1/2\" />\n          </div>\n        </div>\n\n        {/* Description Section - Fixed 48px height matching VendorCard */}\n        {showDescription && (\n          <div className=\"flex h-12 w-full items-center overflow-hidden\">\n            <div className=\"w-full space-y-1\">\n              <TextSkeleton.Body className=\"w-full\" />\n              <TextSkeleton.Body className=\"w-2/3\" />\n            </div>\n          </div>\n        )}\n\n        {/* Footer Section - Stats display */}\n        {showFooter && (\n          <div className=\"flex w-full min-w-0 items-center justify-between gap-2\">\n            {/* Stats Container */}\n            <div className=\"flex min-w-0 flex-shrink items-center gap-3 md:gap-4\">\n              {/* Stat 1 */}\n              <div className=\"flex flex-shrink-0 items-center gap-1\">\n                <MediaSkeleton.Icon size=\"sm\" className=\"h-5 w-5\" />\n                <TextSkeleton.Caption className=\"w-8\" />\n              </div>\n\n              {/* Stat 2 */}\n              <div className=\"flex flex-shrink-0 items-center gap-1\">\n                <MediaSkeleton.Icon size=\"sm\" className=\"h-5 w-5\" />\n                <TextSkeleton.Caption className=\"w-10\" />\n              </div>\n            </div>\n\n            {/* Tag/Badge Section */}\n            <div className=\"flex-shrink-0\">\n              <div className=\"flex items-center gap-1.5 rounded border border-ods-border bg-ods-bg px-2.5 py-1.5\">\n                <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-sm bg-ods-border\">\n                  <MediaSkeleton.Icon size=\"sm\" className=\"h-2.5 w-2.5\" />\n                </div>\n                <TextSkeleton.Caption className=\"w-16\" />\n              </div>\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\n/**\n * OrganizationCardSkeletonGrid - Grid of organization card skeletons\n *\n * Matches responsive grid layout:\n * - Mobile: 1 column\n * - Tablet (md): 2 columns\n * - Desktop (xl): 3 columns\n */\nexport function OrganizationCardSkeletonGrid({\n  count = 12,\n  className,\n  containerClassName,\n  showFooter = true,\n  showDescription = true,\n}: {\n  count?: number;\n  className?: string;\n  containerClassName?: string;\n  showFooter?: boolean;\n  showDescription?: boolean;\n}) {\n  return (\n    <div\n      className={cn('grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3', className)}\n      role=\"status\"\n      aria-label={`Loading ${count} organization cards`}\n    >\n      {Array.from({ length: count }, (_, index) => (\n        <OrganizationCardSkeleton\n          key={index}\n          containerClassName={containerClassName}\n          showFooter={showFooter}\n          showDescription={showDescription}\n        />\n      ))}\n    </div>\n  );\n}\n","import { cn } from '../../utils/cn';\nimport { OrganizationIconSkeleton } from './organization-icon-skeleton';\n\nexport interface DeviceCardSkeletonProps {\n  /**\n   * Additional CSS classes\n   */\n  className?: string;\n}\n\n/**\n * DeviceCardSkeleton - Loading skeleton matching DeviceCard exact layout\n *\n * Matches the structure of DeviceCard:\n * - Row 1: Device icon + Device name + More button\n * - Row 2: OS badge + Organization icon + Organization name\n * - Row 3: Status badge + Last seen\n *\n * Prevents layout jumps by matching exact dimensions.\n */\nexport function DeviceCardSkeleton({ className }: DeviceCardSkeletonProps) {\n  return (\n    <div\n      className={cn('h-full rounded-[6px] border border-ods-border bg-ods-card', className)}\n      role=\"status\"\n      aria-label=\"Loading device card\"\n    >\n      {/* Row 1: Device icon + Device name + More button */}\n      <div className=\"flex items-center gap-4 px-4 py-3\">\n        {/* Device type icon (8x8 container) */}\n        <div className=\"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-[6px] border border-ods-border bg-ods-bg\">\n          <div className=\"h-4 w-4 animate-pulse rounded bg-ods-border\" />\n        </div>\n\n        {/* Device name */}\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"h-6 w-3/4 animate-pulse rounded bg-ods-border\" />\n        </div>\n\n        {/* More button */}\n        <div className=\"h-12 w-12 flex-shrink-0 animate-pulse rounded-[6px] bg-ods-border\" />\n      </div>\n\n      {/* Row 2: OS badge + Organization */}\n      <div className=\"flex items-center gap-4 px-4 py-2\">\n        {/* OS badge */}\n        <div className=\"h-6 w-24 flex-shrink-0 animate-pulse rounded bg-ods-border\" />\n\n        {/* Organization icon */}\n        <OrganizationIconSkeleton size=\"sm\" />\n\n        {/* Organization name */}\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"h-5 w-1/2 animate-pulse rounded bg-ods-border\" />\n        </div>\n      </div>\n\n      {/* Row 3: Status badge + Last seen */}\n      <div className=\"flex items-center gap-4 px-4 py-2\">\n        {/* Status badge */}\n        <div className=\"h-6 w-20 flex-shrink-0 animate-pulse rounded-full bg-ods-border\" />\n\n        {/* Last seen */}\n        <div className=\"flex-1\">\n          <div className=\"h-5 w-40 animate-pulse rounded bg-ods-border\" />\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/**\n * DeviceCardSkeletonGrid - Grid of device card skeletons\n *\n * Matches DevicesGrid layout with responsive columns:\n * - Mobile: 1 column\n * - Tablet (md): 2 columns\n * - Desktop (lg): 3 columns\n * - Large (xl): 4 columns\n */\nexport function DeviceCardSkeletonGrid({ count = 12, className }: { count?: number; className?: string }) {\n  return (\n    <div\n      className={cn('grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4', className)}\n      role=\"status\"\n      aria-label={`Loading ${count} device cards`}\n    >\n      {Array.from({ length: count }, (_, index) => (\n        <DeviceCardSkeleton key={index} />\n      ))}\n    </div>\n  );\n}\n","'use client';\n\nimport { Trash2, Plus, Image as ImageIcon, Video as VideoIcon, Upload, Loader2, GripVertical } from 'lucide-react';\nimport { useState, useRef } from 'react';\nimport type { ChangeEvent, DragEvent } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { isVideoMedia } from '../../utils/media-type';\nimport { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui';\n\nimport { Video } from './video';\nexport interface ReleaseMediaItem {\n  media_type: 'image' | 'video' | 'screenshot' | 'demo';\n  media_url: string;\n  title?: string;\n  description?: string;\n  display_order?: number;\n  _file?: File; // Temporary file before upload\n  _uploading?: boolean; // Upload in progress\n}\n\ninterface ReleaseMediaManagerProps {\n  media: ReleaseMediaItem[];\n  onChange: (media: ReleaseMediaItem[]) => void;\n  onUpload: (file: File, mediaType: 'image' | 'video' | 'screenshot' | 'demo') => Promise<string>; // Returns uploaded URL\n  className?: string;\n}\n\nexport function ReleaseMediaManager({ media, onChange, onUpload, className = '' }: ReleaseMediaManagerProps) {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [uploadingIndex, setUploadingIndex] = useState<number | null>(null);\n\n  const handleFileSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (!file) return;\n\n    // Determine media type\n    let mediaType: 'image' | 'video' | 'screenshot' | 'demo';\n    if (file.type.startsWith('image/')) {\n      mediaType = 'screenshot';\n    } else if (file.type.startsWith('video/')) {\n      mediaType = 'demo';\n    } else {\n      return;\n    }\n\n    // Add media item with uploading state\n    const newIndex = media.length;\n    const newMedia: ReleaseMediaItem = {\n      media_type: mediaType,\n      media_url: '',\n      title: file.name,\n      _file: file,\n      _uploading: true,\n    };\n\n    onChange([...media, newMedia]);\n    setUploadingIndex(newIndex);\n\n    try {\n      // Upload file\n      const url = await onUpload(file, mediaType);\n\n      // Update with uploaded URL\n      const updated = [...media, { ...newMedia, media_url: url, _file: undefined, _uploading: false }];\n      onChange(updated);\n    } catch {\n      // Remove failed upload\n      onChange(media);\n    } finally {\n      setUploadingIndex(null);\n      if (fileInputRef.current) {\n        fileInputRef.current.value = '';\n      }\n    }\n  };\n\n  const removeMedia = (index: number) => {\n    onChange(media.filter((_, i) => i !== index));\n  };\n\n  const updateMedia = (index: number, field: keyof ReleaseMediaItem, value: string) => {\n    const updated = [...media];\n    updated[index] = { ...updated[index], [field]: value };\n    onChange(updated);\n  };\n\n  const handleDragStart = (index: number) => (e: DragEvent) => {\n    const transfer = e.dataTransfer;\n    transfer.effectAllowed = 'move';\n    transfer.setData('text/plain', index.toString());\n  };\n\n  const handleDragOver = (e: DragEvent) => {\n    e.preventDefault();\n    const transfer = e.dataTransfer;\n    transfer.dropEffect = 'move';\n  };\n\n  const handleDrop = (targetIndex: number) => (e: DragEvent) => {\n    e.preventDefault();\n    const sourceIndex = parseInt(e.dataTransfer.getData('text/plain'));\n\n    if (sourceIndex === targetIndex) return;\n\n    const newMedia = [...media];\n    const [draggedItem] = newMedia.splice(sourceIndex, 1);\n    newMedia.splice(targetIndex, 0, draggedItem);\n\n    onChange(newMedia.map((item, i) => ({ ...item, display_order: i })));\n  };\n\n  const getIcon = (type: string) => {\n    switch (type) {\n      case 'video':\n      case 'demo':\n        return <VideoIcon className=\"h-5 w-5 text-ods-text-secondary\" />;\n      default:\n        return <ImageIcon className=\"h-5 w-5 text-ods-text-secondary\" />;\n    }\n  };\n\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* Upload Section */}\n      <div className=\"rounded-lg border-2 border-dashed border-ods-border p-6 text-center transition-colors hover:border-ods-accent/50\">\n        <div className=\"flex flex-col items-center gap-4\">\n          <div className=\"flex h-12 w-12 items-center justify-center rounded-full bg-ods-card\">\n            {uploadingIndex !== null ? (\n              <Loader2 className=\"h-6 w-6 animate-spin text-ods-accent\" />\n            ) : (\n              <Upload className=\"h-6 w-6 text-ods-accent\" />\n            )}\n          </div>\n          <div>\n            <h3 className=\"mb-1 text-ods-text-primary text-h3\">\n              {uploadingIndex !== null ? 'Uploading...' : 'Upload Media'}\n            </h3>\n            <p className=\"text-ods-text-secondary text-h6\">Drag and drop or click to select images and videos</p>\n            <p className=\"mt-1 text-ods-text-secondary text-h6\">Maximum file size: 50MB</p>\n          </div>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            onClick={() => fileInputRef.current?.click()}\n            disabled={uploadingIndex !== null}\n            leftIcon={<Plus className=\"h-4 w-4\" />}\n            className=\"font-bold text-h6\"\n          >\n            {uploadingIndex !== null ? 'Uploading...' : 'Select Files'}\n          </Button>\n        </div>\n      </div>\n\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept=\"image/*,video/*\"\n        onChange={handleFileSelect}\n        className=\"hidden\"\n        disabled={uploadingIndex !== null}\n      />\n\n      {/* Media Grid */}\n      {media.length > 0 && (\n        <div>\n          <div className=\"mb-4 flex items-center justify-between\">\n            <Label>Media Gallery ({media.length})</Label>\n            <p className=\"text-ods-text-secondary text-h6\">Drag to reorder</p>\n          </div>\n\n          <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n            {media.map((item, index) => (\n              <div\n                key={index}\n                draggable={!item._uploading}\n                onDragStart={handleDragStart(index)}\n                onDragOver={handleDragOver}\n                onDrop={handleDrop(index)}\n                className=\"group relative overflow-hidden rounded-lg border border-ods-border bg-ods-bg-surface transition-colors hover:border-ods-accent/30\"\n              >\n                {/* Drag Handle */}\n                <div className=\"absolute left-2 top-2 z-10 cursor-move opacity-0 transition-opacity group-hover:opacity-100\">\n                  <GripVertical className=\"h-4 w-4 text-ods-text-on-dark drop-shadow\" />\n                </div>\n\n                {/* Delete Button */}\n                <div className=\"absolute right-2 top-2 z-10 opacity-0 transition-opacity group-hover:opacity-100\">\n                  <Button\n                    type=\"button\"\n                    variant=\"outline\"\n                    size=\"small-legacy\"\n                    onClick={() => removeMedia(index)}\n                    disabled={item._uploading}\n                    className=\"h-8 w-8 border-ods-error bg-ods-error p-0 hover:bg-ods-error-hover\"\n                  >\n                    <Trash2 className=\"h-4 w-4 text-ods-text-on-dark\" />\n                  </Button>\n                </div>\n\n                {/* Media Preview */}\n                {item.media_url && (\n                  <div className=\"relative aspect-video bg-ods-bg\">\n                    {isVideoMedia(item) || item.media_type === 'demo' ? (\n                      // <Video> SSOT (MuxPlayer) — plays Mux HLS + MP4 alike;\n                      // fit=\"cover\" crops to the aspect-video cell.\n                      <Video kind=\"file\" url={item.media_url} fit=\"cover\" className=\"h-full w-full\" />\n                    ) : (\n                      <Image\n                        src={item.media_url}\n                        alt={item.title || 'Media'}\n                        fill\n                        className=\"object-cover\"\n                        sizes=\"(max-width: 768px) 100vw, 50vw\"\n                      />\n                    )}\n                  </div>\n                )}\n\n                {item._uploading && (\n                  <div className=\"flex aspect-video items-center justify-center bg-ods-bg\">\n                    <div className=\"flex flex-col items-center gap-2\">\n                      <Loader2 className=\"h-8 w-8 animate-spin text-ods-accent\" />\n                      <span className=\"text-ods-text-secondary text-h6\">Uploading...</span>\n                    </div>\n                  </div>\n                )}\n\n                {/* Media Info */}\n                <div className=\"space-y-2 p-3\">\n                  <div className=\"flex items-center gap-2\">\n                    {getIcon(item.media_type)}\n                    <Select\n                      value={item.media_type}\n                      onValueChange={(value: string) => updateMedia(index, 'media_type', value)}\n                      disabled={item._uploading}\n                    >\n                      <SelectTrigger className=\"h-8 bg-ods-bg text-h6\">\n                        <SelectValue />\n                      </SelectTrigger>\n                      <SelectContent className=\"bg-ods-card\">\n                        <SelectItem value=\"image\">Image</SelectItem>\n                        <SelectItem value=\"video\">Video</SelectItem>\n                        <SelectItem value=\"screenshot\">Screenshot</SelectItem>\n                        <SelectItem value=\"demo\">Demo</SelectItem>\n                      </SelectContent>\n                    </Select>\n                  </div>\n\n                  <Input\n                    placeholder=\"Title (optional)\"\n                    value={item.title}\n                    onChange={e => updateMedia(index, 'title', e.target.value)}\n                    onKeyDown={e => e.key === 'Enter' && e.preventDefault()}\n                    className=\"h-8 bg-ods-bg text-h6\"\n                    disabled={item._uploading}\n                  />\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {media.length === 0 && (\n        <div className=\"rounded-lg border border-ods-border bg-ods-bg-surface px-4 py-8 text-center\">\n          <ImageIcon className=\"mx-auto mb-4 h-12 w-12 text-ods-text-secondary\" />\n          <h3 className=\"mb-2 text-ods-text-primary text-h3\">No media uploaded yet</h3>\n          <p className=\"text-ods-text-secondary text-h6\">Upload your first image or video to get started</p>\n        </div>\n      )}\n    </div>\n  );\n}\n","'use client';\n\nimport React from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { cn } from '../../utils/cn';\nimport { CheckCircleIcon } from '../icons-v2-generated/signs-and-symbols/check-circle-icon';\nimport { Tag, type TagProps } from '../ui/tag';\n\nexport interface SelectButtonProps {\n  title: string;\n  description?: string;\n  selected?: boolean;\n  disabled?: boolean;\n  icon?: React.ReactNode;\n  image?: {\n    src: string;\n    alt: string;\n  };\n  tag?: string;\n  tagVariant?: TagProps['variant'];\n  onClick?: () => void;\n  className?: string;\n}\n\nexport const SelectButton = React.forwardRef<HTMLButtonElement, SelectButtonProps>(\n  (\n    {\n      title,\n      description,\n      selected = false,\n      disabled = false,\n      icon,\n      image,\n      tag,\n      tagVariant = 'outline',\n      onClick,\n      className,\n    },\n    ref,\n  ) => {\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        role=\"option\"\n        aria-selected={selected}\n        disabled={disabled}\n        onClick={onClick}\n        className={cn(\n          'group flex h-11 items-center gap-1 rounded-[6px] border px-3 py-3 transition-colors duration-200 md:h-16 md:gap-2 md:px-4',\n          'w-full text-left font-body',\n          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus',\n          'disabled:pointer-events-none disabled:opacity-50',\n          // Hover / active move the BACKGROUND only, like Input — the accent\n          // border is reserved for the selected state, so an unselected button\n          // no longer flashes \"selected\" under the cursor.\n          selected\n            ? 'border-ods-accent bg-ods-open-yellow-light hover:bg-ods-open-yellow-light-hover active:bg-ods-open-yellow-light-action'\n            : 'border-ods-border bg-ods-card hover:bg-ods-bg-hover active:bg-ods-bg-active',\n          className,\n        )}\n      >\n        {icon && (\n          <span\n            className={cn(\n              'flex size-4 shrink-0 items-center justify-center md:size-6',\n              selected ? 'text-ods-accent' : 'text-ods-text-secondary',\n            )}\n          >\n            {icon}\n          </span>\n        )}\n\n        {image && (\n          <span className=\"size-10 shrink-0 overflow-hidden rounded\">\n            <Image\n              src={image.src}\n              alt={image.alt}\n              className=\"size-full object-cover\"\n              width={40}\n              height={40}\n              unoptimized\n            />\n          </span>\n        )}\n\n        <span className=\"flex min-w-0 flex-1 flex-col justify-center overflow-hidden\">\n          <span className=\"truncate text-ods-text-primary text-h4\" title={title}>\n            {title}\n          </span>\n          {description && (\n            <span\n              className={cn(\n                'hidden truncate text-h6 md:flex',\n                selected ? 'text-ods-accent' : 'text-ods-text-secondary',\n              )}\n              title={description}\n            >\n              {description}\n            </span>\n          )}\n        </span>\n\n        {tag && <Tag variant={tagVariant} className=\"shrink-0\" label={tag} />}\n\n        {selected && <CheckCircleIcon className=\"size-4 shrink-0 text-ods-accent md:size-6\" />}\n      </button>\n    );\n  },\n);\n\nSelectButton.displayName = 'SelectButton';\n","'use client';\n\nimport { Globe, ExternalLink, Upload, X, Loader2 } from 'lucide-react';\nimport { useState } from 'react';\nimport type { ChangeEvent } from 'react';\nimport Image from '../../embed-shims/next-image';\nimport { cn } from '../../utils';\nimport { SEO_DESCRIPTION_MAX_LENGTH } from '../../utils/seo-description';\n// SSOT for the field cap (server-safe constant). The seo_title renders as the\n// page <title> verbatim (no brand suffix), so this is the full ~60-char budget.\nimport { SEO_TITLE_MAX_LENGTH } from '../../utils/seo-title';\nimport { Input, Textarea, Label, Button, Field } from '../ui';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { ConfidenceBadge } from './ai-enrich/ConfidenceBadge';\n\n/** SEO Description textarea and the OG upload box sit side by side on desktop —\n *  ONE height constant for both, so the pair stays flush instead of each\n *  picking its own (`rows` on one, `min-h` on the other, drifting apart). */\nconst SEO_MEDIA_BOX_HEIGHT = 'h-[280px]';\n\nexport interface SEOEditorPreviewProps {\n  // SEO fields - must be strings (not undefined)\n  seoTitle: string;\n  seoDescription: string;\n  seoKeywords: string;\n  ogImageUrl: string;\n\n  // Auto-populate sources - must be strings (not undefined)\n  title: string;\n  summary: string;\n  featuredImage: string;\n\n  // Change handlers\n  onSeoTitleChange: (value: string) => void;\n  onSeoDescriptionChange: (value: string) => void;\n  onSeoKeywordsChange: (value: string) => void;\n  onOgImageUrlChange: (value: string) => void;\n\n  // Upload handler (provided by parent since it needs API endpoint)\n  onOgImageUpload?: (file: File) => Promise<string>;\n\n  // AI confidence scores\n  aiConfidenceSeoTitle?: number;\n  aiConfidenceSeoDescription?: number;\n  aiConfidenceSeoKeywords?: number;\n\n  // Optional\n  domain?: string;\n  disabled?: boolean;\n  className?: string;\n}\n\n/**\n * Unified SEO Editor with Live Preview\n * Complete self-contained component for managing SEO meta tags and OG preview\n * Used across blog posts, case studies, and product releases\n */\nexport function SEOEditorPreview({\n  seoTitle,\n  seoDescription,\n  seoKeywords,\n  ogImageUrl,\n  title,\n  summary,\n  featuredImage,\n  onSeoTitleChange,\n  onSeoDescriptionChange,\n  onSeoKeywordsChange,\n  onOgImageUrlChange,\n  onOgImageUpload,\n  aiConfidenceSeoTitle,\n  aiConfidenceSeoDescription,\n  aiConfidenceSeoKeywords,\n  domain = 'openmsp.ai',\n  disabled = false,\n  className = '',\n}: SEOEditorPreviewProps) {\n  const [imageError, setImageError] = useState(false);\n  const [isUploading, setIsUploading] = useState(false);\n  const [fileInputRef, setFileInputRef] = useState<HTMLInputElement | null>(null);\n\n  // SEO title length state — alerts the editor when the title would render too long.\n  const seoTitleLength = (seoTitle || '').length;\n  const seoTitleTooLong = seoTitleLength > SEO_TITLE_MAX_LENGTH;\n\n  // SEO description length state — the column is varchar(160); flag over-length so\n  // the editor (or a programmatically-set value) can be brought within budget.\n  const seoDescriptionLength = (seoDescription || '').length;\n  const seoDescriptionTooLong = seoDescriptionLength > SEO_DESCRIPTION_MAX_LENGTH;\n\n  // Use fallback values if OG fields are empty\n  const displayTitle = seoTitle.trim() || title || 'Untitled';\n  const displayDescription = seoDescription.trim() || summary || 'No description';\n  const hasOgImage = ogImageUrl.trim();\n  const hasFeaturedImage = featuredImage.trim();\n  const displayImage = hasOgImage || hasFeaturedImage;\n\n  const handleImageUpload = async (event: ChangeEvent<HTMLInputElement>) => {\n    if (!onOgImageUpload) return;\n\n    const file = event.target.files?.[0];\n    if (!file) return;\n\n    setIsUploading(true);\n    try {\n      const url = await onOgImageUpload(file);\n      onOgImageUrlChange(url);\n    } catch (error) {\n      console.error('OG image upload failed:', error);\n    } finally {\n      setIsUploading(false);\n    }\n  };\n\n  return (\n    <div className={cn('space-y-6 rounded-lg border border-ods-border bg-ods-card p-6', className)}>\n      <h3 className=\"text-ods-text-primary text-h5\">SEO & Open Graph</h3>\n\n      {/* SEO Title & Keywords - Same Row. EVERY field goes through `Field`, so\n          both columns share one label-row geometry; badges + the char counter\n          ride the label row (labelExtras / labelEnd) instead of adding stray\n          rows that misalign siblings. */}\n      <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n        <div className=\"space-y-2\">\n          <Field\n            label=\"SEO Title\"\n            error={\n              seoTitleTooLong\n                ? `Too long: search engines may truncate this title (keep it under ${SEO_TITLE_MAX_LENGTH})`\n                : null\n            }\n            labelExtras={\n              aiConfidenceSeoTitle !== undefined ? (\n                <>\n                  <AIGeneratedBadge />\n                  <ConfidenceBadge confidence={aiConfidenceSeoTitle} showLabel={true} showPercentage={true} size=\"sm\" />\n                </>\n              ) : undefined\n            }\n            labelEnd={\n              <span\n                className={cn(\n                  'tabular-nums text-h6',\n                  seoTitleTooLong ? 'font-semibold text-ods-error' : 'text-ods-text-secondary',\n                )}\n              >\n                {seoTitleLength}/{SEO_TITLE_MAX_LENGTH}\n              </span>\n            }\n          >\n            {f => (\n              <Input\n                {...f}\n                value={seoTitle || ''}\n                onChange={e => onSeoTitleChange(e.target.value)}\n                disabled={disabled}\n                maxLength={SEO_TITLE_MAX_LENGTH}\n                invalid={seoTitleTooLong}\n                placeholder=\"Enter SEO meta title...\"\n                className=\"border-ods-border bg-ods-bg text-ods-text-primary\"\n              />\n            )}\n          </Field>\n          {!seoTitle && title && <p className=\"text-ods-accent text-h6\">Auto-populated from title</p>}\n        </div>\n\n        <Field\n          label=\"SEO Keywords\"\n          labelExtras={\n            aiConfidenceSeoKeywords !== undefined ? (\n              <>\n                <AIGeneratedBadge />\n                <ConfidenceBadge\n                  confidence={aiConfidenceSeoKeywords}\n                  showLabel={true}\n                  showPercentage={true}\n                  size=\"sm\"\n                />\n              </>\n            ) : undefined\n          }\n        >\n          {f => (\n            <Input\n              {...f}\n              value={seoKeywords || ''}\n              onChange={e => onSeoKeywordsChange(e.target.value)}\n              disabled={disabled}\n              placeholder=\"Enter SEO keywords...\"\n              className=\"border-ods-border bg-ods-bg text-ods-text-primary\"\n            />\n          )}\n        </Field>\n      </div>\n\n      {/* SEO Description & OG Image - Same Row */}\n      <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n        <div className=\"space-y-2\">\n          <Field\n            label=\"SEO Description\"\n            error={\n              seoDescriptionTooLong\n                ? `Too long: search engines may truncate this description (keep it under ${SEO_DESCRIPTION_MAX_LENGTH})`\n                : null\n            }\n            labelExtras={\n              aiConfidenceSeoDescription !== undefined ? (\n                <>\n                  <AIGeneratedBadge />\n                  <ConfidenceBadge\n                    confidence={aiConfidenceSeoDescription}\n                    showLabel={true}\n                    showPercentage={true}\n                    size=\"sm\"\n                  />\n                </>\n              ) : undefined\n            }\n            labelEnd={\n              <span\n                className={cn(\n                  'tabular-nums text-h6',\n                  seoDescriptionTooLong ? 'font-semibold text-ods-error' : 'text-ods-text-secondary',\n                )}\n              >\n                {seoDescriptionLength}/{SEO_DESCRIPTION_MAX_LENGTH}\n              </span>\n            }\n          >\n            {f => (\n              <Textarea\n                {...f}\n                value={seoDescription || ''}\n                onChange={e => onSeoDescriptionChange(e.target.value)}\n                disabled={disabled}\n                maxLength={SEO_DESCRIPTION_MAX_LENGTH}\n                invalid={seoDescriptionTooLong}\n                placeholder=\"Enter SEO meta description...\"\n                className={cn('resize-none border-ods-border bg-ods-bg text-ods-text-primary', SEO_MEDIA_BOX_HEIGHT)}\n              />\n            )}\n          </Field>\n          {!seoDescription && summary && <p className=\"text-ods-accent text-h6\">Auto-populated from summary</p>}\n        </div>\n\n        <div className=\"flex flex-col gap-[var(--spacing-system-xxs)]\">\n          {/* Group label for the upload widget — Field's exact label treatment,\n              so the OG column's label row aligns with the Description Field. */}\n          <Label>OG Image</Label>\n\n          {/* OG Image Upload/Display — pt matches Field's label→control offset\n              so this box's top aligns with the Description textarea's top. */}\n          <div className=\"relative pt-[var(--spacing-system-xxs)]\">\n            {displayImage && !imageError ? (\n              <div className={cn('group relative', SEO_MEDIA_BOX_HEIGHT)}>\n                <Image\n                  src={displayImage}\n                  alt=\"OG Image\"\n                  fill\n                  className=\"rounded-lg object-contain\"\n                  onError={() => setImageError(true)}\n                />\n                <div className=\"absolute inset-0 flex items-center justify-center gap-2 rounded-lg bg-black bg-opacity-0 transition-all group-hover:bg-opacity-40\">\n                  {onOgImageUpload && (\n                    <Button\n                      type=\"button\"\n                      variant=\"outline\"\n                      size=\"icon\"\n                      onClick={() => fileInputRef?.click()}\n                      disabled={disabled || isUploading}\n                      className=\"rounded-full bg-white text-black opacity-0 hover:bg-gray-100 group-hover:opacity-100\"\n                    >\n                      <Upload className=\"h-4 w-4\" />\n                    </Button>\n                  )}\n                  <Button\n                    type=\"button\"\n                    variant=\"outline\"\n                    size=\"icon\"\n                    onClick={() => onOgImageUrlChange('')}\n                    disabled={disabled}\n                    className=\"rounded-full bg-white text-black opacity-0 hover:bg-gray-100 group-hover:opacity-100\"\n                  >\n                    <X className=\"h-4 w-4\" />\n                  </Button>\n                </div>\n              </div>\n            ) : (\n              <div\n                className={cn(\n                  SEO_MEDIA_BOX_HEIGHT,\n                  'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-ods-border bg-ods-bg-hover transition-colors hover:border-ods-accent',\n                )}\n                onClick={() => onOgImageUpload && fileInputRef?.click()}\n              >\n                {isUploading ? (\n                  <Loader2 className=\"h-8 w-8 animate-spin text-ods-accent\" />\n                ) : (\n                  <>\n                    <Upload className=\"mb-2 h-8 w-8 text-ods-text-secondary\" />\n                    <span className=\"text-ods-text-secondary text-h6\">\n                      {onOgImageUpload ? 'Click to upload OG image' : 'No image'}\n                    </span>\n                  </>\n                )}\n              </div>\n            )}\n\n            {onOgImageUpload && (\n              <input\n                ref={setFileInputRef}\n                type=\"file\"\n                accept=\"image/*\"\n                onChange={handleImageUpload}\n                className=\"hidden\"\n                disabled={disabled || isUploading}\n              />\n            )}\n          </div>\n\n          {!ogImageUrl && featuredImage && <p className=\"text-ods-accent text-h6\">Using featured image</p>}\n        </div>\n      </div>\n\n      {/* Live Preview */}\n      <div className=\"border-t border-ods-border pt-4\">\n        <div className=\"mb-3 flex items-center gap-2\">\n          <Globe className=\"h-4 w-4 text-ods-text-secondary\" />\n          <span className=\"text-ods-text-secondary text-h6\">Social Media Preview</span>\n        </div>\n\n        {/* OG Card Preview */}\n        <div className=\"max-w-[500px] overflow-hidden rounded-lg border border-ods-border bg-ods-bg\">\n          {/* Image Section */}\n          <div className=\"relative h-[260px] w-full bg-ods-bg-active\">\n            {displayImage && !imageError ? (\n              <Image\n                src={displayImage}\n                alt={displayTitle}\n                fill\n                className=\"object-cover\"\n                onError={() => setImageError(true)}\n              />\n            ) : (\n              <div className=\"absolute inset-0 flex items-center justify-center\">\n                <div className=\"text-center\">\n                  <Globe className=\"mx-auto mb-2 h-12 w-12 text-ods-text-secondary\" />\n                  <p className=\"text-ods-text-secondary text-h6\">No preview image</p>\n                </div>\n              </div>\n            )}\n          </div>\n\n          {/* Content Section */}\n          <div className=\"space-y-2 p-4\">\n            {/* Domain */}\n            <div className=\"flex items-center gap-1\">\n              <ExternalLink className=\"h-3 w-3 text-ods-text-secondary\" />\n              <span className=\"uppercase text-ods-text-secondary text-h6\">{domain}</span>\n            </div>\n\n            {/* Title */}\n            <h3 className=\"line-clamp-2 font-semibold text-ods-text-primary text-h6\">{displayTitle}</h3>\n\n            {/* Description */}\n            <p className=\"line-clamp-3 text-ods-text-secondary text-h6\">{displayDescription}</p>\n          </div>\n        </div>\n\n        {/* Fallback Indicators */}\n        <div className=\"mt-3 space-y-1\">\n          {!seoTitle.trim() && title && <p className=\"text-ods-accent text-h6\">• Using title as SEO title</p>}\n          {!seoDescription.trim() && summary && (\n            <p className=\"text-ods-accent text-h6\">• Using summary as SEO description</p>\n          )}\n          {!ogImageUrl.trim() && featuredImage.trim() && (\n            <p className=\"text-ods-accent text-h6\">• Using featured image as OG image</p>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../../utils/cn';\nimport {\n  getConfidenceLevel,\n  getConfidenceLabel,\n  getConfidenceBorderClass,\n  getConfidenceTextClass,\n  getConfidenceBgClass,\n} from '../../../utils/confidence-helpers';\n\nexport interface ConfidenceBadgeProps {\n  confidence?: number;\n  showLabel?: boolean;\n  showPercentage?: boolean;\n  size?: 'sm' | 'md' | 'lg';\n  className?: string;\n}\n\nconst sizeStyles = {\n  sm: 'text-h6 px-1.5 py-0.5',\n  md: 'text-h6 px-2 py-1',\n  lg: 'text-h6 px-3 py-1.5',\n};\n\nexport const ConfidenceBadge: React.FC<ConfidenceBadgeProps> = ({\n  confidence,\n  showLabel = true,\n  showPercentage = true,\n  size = 'sm',\n  className,\n}) => {\n  const level = getConfidenceLevel(confidence);\n\n  if (level === 'none') {\n    return null;\n  }\n\n  const label = getConfidenceLabel(confidence);\n  const borderClass = getConfidenceBorderClass(confidence);\n  const textClass = getConfidenceTextClass(confidence);\n  const bgClass = getConfidenceBgClass(confidence);\n\n  return (\n    <span\n      className={cn(\n        'inline-flex items-center gap-1 rounded-full border',\n        sizeStyles[size],\n        borderClass,\n        textClass,\n        bgClass,\n        className,\n      )}\n    >\n      {showLabel && <span>{label}</span>}\n      {showPercentage && confidence !== undefined && <span className=\"opacity-75\">({confidence}%)</span>}\n    </span>\n  );\n};\n","'use client';\n\nimport { Trash2, User } from 'lucide-react';\nimport { normalizeSocialPlatform } from '../../utils/social-platforms';\nimport { SOCIAL_ICON_COMPONENTS } from '../social-icon-row';\nimport { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui';\n\nexport interface SocialLink {\n  platform: string;\n  url: string;\n  username?: string;\n  /** `internal` links render only on company-hub (server-enforced); default `external`. */\n  visibility?: 'external' | 'internal';\n}\n\nexport interface SocialPlatform {\n  id: string;\n  name: string;\n  display_name: string;\n  icon_name: string;\n  url_pattern?: string;\n  placeholder?: string;\n  enabled: boolean;\n}\n\ninterface SocialLinksManagerProps {\n  links: SocialLink[];\n  onChange: (links: SocialLink[]) => void;\n  platforms?: SocialPlatform[];\n  className?: string;\n}\n\n// Default platforms if none provided (empty array to encourage using dynamic data)\nconst defaultPlatforms: SocialPlatform[] = [];\n\n// Icon mapping - dynamically loaded based on database icon_name\n\nexport function SocialLinksManager({\n  links,\n  onChange,\n  platforms = defaultPlatforms,\n  className = '',\n}: SocialLinksManagerProps) {\n  const addLink = () => {\n    const firstPlatform = platforms[0]?.name || 'website';\n    onChange([...links, { platform: firstPlatform, url: '' }]);\n  };\n\n  const removeLink = (index: number) => {\n    onChange(links.filter((_, i) => i !== index));\n  };\n\n  const updateLink = (index: number, field: keyof SocialLink, value: string) => {\n    const updated = [...links];\n    updated[index] = { ...updated[index], [field]: value };\n    onChange(updated);\n  };\n\n  const getIcon = (link: SocialLink, platform?: SocialPlatform) => {\n    // Use database icon_name if available, fallback to platform name\n    const iconKey = platform?.icon_name || link.platform;\n    const key = normalizeSocialPlatform(iconKey);\n    if (!key) return null;\n    const IconComponent = SOCIAL_ICON_COMPONENTS[key];\n    return <IconComponent className=\"h-5 w-5 text-ods-text-secondary\" />;\n  };\n\n  return (\n    <div className={`space-y-3 ${className}`}>\n      {links.map((link, index) => {\n        const platform = platforms.find(p => p.name === link.platform);\n        const Icon = getIcon(link, platform);\n\n        return (\n          <div\n            key={index}\n            className=\"flex items-center gap-3 rounded-lg border border-ods-border bg-ods-bg-surface p-3\"\n          >\n            <div className=\"flex h-8 w-8 items-center justify-center\">{Icon}</div>\n\n            <div className=\"grid flex-1 grid-cols-2 gap-3\">\n              <div>\n                <Select value={link.platform} onValueChange={value => updateLink(index, 'platform', value)}>\n                  <SelectTrigger className=\"w-full\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {platforms.map(p => (\n                      <SelectItem key={p.name} value={p.name}>\n                        {p.display_name}\n                      </SelectItem>\n                    ))}\n                  </SelectContent>\n                </Select>\n              </div>\n\n              <Input\n                placeholder={platform?.placeholder || 'Profile URL'}\n                value={link.url}\n                onChange={e => updateLink(index, 'url', e.target.value)}\n                onKeyDown={e => {\n                  if (e.key === 'Enter') {\n                    e.preventDefault();\n                    e.stopPropagation();\n                  }\n                }}\n              />\n            </div>\n\n            <Select\n              value={link.visibility ?? 'external'}\n              onValueChange={value => updateLink(index, 'visibility', value)}\n            >\n              <SelectTrigger className=\"w-[120px] shrink-0\" aria-label=\"Social link visibility\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"external\">Public</SelectItem>\n                <SelectItem value=\"internal\">Internal</SelectItem>\n              </SelectContent>\n            </Select>\n\n            <Button\n              type=\"button\"\n              variant=\"transparent\"\n              size=\"icon\"\n              onClick={() => removeLink(index)}\n              className=\"text-ods-error hover:bg-ods-error-secondary hover:text-ods-error-hover\"\n            >\n              <Trash2 className=\"h-4 w-4\" />\n            </Button>\n          </div>\n        );\n      })}\n\n      {links.length === 0 && (\n        <div className=\"py-8 text-center text-ods-text-secondary\">\n          <p className=\"text-h6\">No social links added yet.</p>\n        </div>\n      )}\n\n      <Button\n        variant=\"outline\"\n        onClick={addLink}\n        className=\"w-full\"\n        type=\"button\"\n        leftIcon={<User className=\"h-4 w-4\" />}\n      >\n        Add Social Link\n      </Button>\n    </div>\n  );\n}\n","'use client';\n\nimport { Mail, Music } from 'lucide-react';\nimport { normalizeSocialPlatform, type SocialIconLink } from '../utils/social-platforms';\nimport {\n  GitHubIcon,\n  RedditIcon,\n  XLogo,\n  LinkedInIcon,\n  LumaIcon,\n  WhatsAppIcon,\n  GlobeIcon,\n  MessageCircleIcon,\n  TelegramIcon,\n  YouTubeIcon,\n  InstagramIcon,\n  FacebookIcon,\n  SlackIcon,\n  CopyIcon,\n} from './icons';\nimport { Button } from './ui/button';\n\n/** Re-exported for call sites that already import the link type from this module. */\nexport type { SocialIconLink };\n\ninterface SocialIconRowProps {\n  className?: string;\n  links?: SocialIconLink[];\n  variant?: 'accent' | 'outline' | 'transparent' | 'destructive' | null | undefined;\n  /** Quiet metadata row for page-level identity/share slots: 32px ghost\n   *  icon buttons (size=\"icon-sm\", 16px glyphs), gap-2, w-fit container,\n   *  variant defaulting to \"transparent\" (an explicit `variant` still wins).\n   *  Default false: 44/48px buttons stretching across the container —\n   *  the original card-width behavior (TMCG member cards, footers). */\n  compact?: boolean;\n  /** Render the `external` links and the `internal` links as two groups split\n   *  by a thin divider (only when BOTH groups are non-empty). Visibility is\n   *  presentation-only here — the caller must still gate which links it passes\n   *  server-side. Default false keeps a single flat row. */\n  groupByVisibility?: boolean;\n}\n\nconst defaultLinks: SocialIconLink[] = [\n  { platform: 'github', href: 'https://github.com/flamingo-stack', label: 'GitHub' },\n  { platform: 'linkedin', href: 'https://linkedin.com/company/flamingo.run', label: 'LinkedIn' },\n  { platform: 'facebook', href: 'https://www.facebook.com/flamingoai.msp', label: 'Facebook' },\n];\n\ntype SocialIconComponent = (props: { className?: string }) => React.ReactElement;\n\n/**\n * Platform name → glyph. The ONLY thing about a social platform that lives in\n * code, because an icon is a component and a table cannot hold one; the DB\n * names the glyph it wants through `social_platforms.icon_name`.\n *\n * Deliberately an OPEN record, not `satisfies Record<SomeUnion, …>`: the set of\n * platforms is the `social_platforms` table's to decide, and a row added there\n * must render — with the globe, until somebody adds art for it — rather than\n * fail to type-check against a list in this file.\n */\nexport const SOCIAL_ICON_COMPONENTS: Record<string, SocialIconComponent> = {\n  github: props => <GitHubIcon {...props} />,\n  twitter: props => <XLogo {...props} />,\n  reddit: props => <RedditIcon {...props} variant=\"white\" />,\n  linkedin: props => <LinkedInIcon {...props} />,\n  luma: props => <LumaIcon {...props} />,\n  whatsapp: props => <WhatsAppIcon {...props} />,\n  email: props => <Mail {...props} />,\n  website: props => <GlobeIcon {...props} />,\n  slack: props => <SlackIcon {...props} injectedColor=\"white\" />,\n  discord: props => <MessageCircleIcon {...props} />,\n  telegram: props => <TelegramIcon {...props} />,\n  youtube: props => <YouTubeIcon {...props} />,\n  instagram: props => <InstagramIcon {...props} />,\n  facebook: props => <FacebookIcon {...props} />,\n  tiktok: props => <Music {...props} />,\n  // CopyIcon's default fill is grey and would mismatch its row-mates — force the\n  // themed foreground via the ODS token (tracks the theme).\n  copy: props => <CopyIcon {...props} color=\"var(--color-text-primary)\" />,\n};\n\n/** The globe stands in for any platform with no art yet — never a blank slot. */\nfunction renderSocialIcon(platform: string) {\n  const key = normalizeSocialPlatform(platform) ?? 'website';\n  const Icon = SOCIAL_ICON_COMPONENTS[key] ?? SOCIAL_ICON_COMPONENTS.website;\n  return <Icon className=\"h-5 w-5\" />;\n}\n\nexport function SocialIconRow({\n  className = '',\n  links = defaultLinks,\n  variant,\n  compact = false,\n  groupByVisibility = false,\n}: SocialIconRowProps) {\n  // ── Compact design rationale ──────────────────────────────────────────\n  // Page-level identity/share rows read as METADATA, not CTAs. The major\n  // design systems converge on one recipe for this slot: a ~32px ghost icon\n  // button with a ~16px glyph, tight 8px gap, transparent at rest, subtle\n  // background state-layer on hover (Carbon \"ghost\" sm, Primer \"invisible\"\n  // medium, shadcn ghost+icon-sm — all 32px; Material 3 \"standard\" icon\n  // button = state-layer hover). Author headers on content platforms\n  // (Medium / dev.to / Substack) use the same quiet treatment. Hence\n  // compact: size=\"icon-sm\" + variant defaulting to \"transparent\" — the\n  // hover affordance comes from the bg state layer (hover:bg-ods-bg-hover\n  // inside the variant) because the brand icons carry fixed fills, not\n  // currentColor. An explicit `variant` prop still wins (e.g. outline\n  // chips). Non-compact keeps the legacy outline default + full-width\n  // stretch untouched.\n  const resolvedVariant = variant !== undefined ? variant : compact ? 'transparent' : 'outline';\n\n  const renderButton = (link: SocialIconLink, index: number) => {\n    const ariaLabel = link.label || link.platform;\n    return link.onClick ? (\n      <Button\n        key={index}\n        type=\"button\"\n        variant={resolvedVariant}\n        size={compact ? 'icon-sm' : 'icon'}\n        className={compact ? undefined : 'flex-1'}\n        aria-label={ariaLabel}\n        onClick={link.onClick}\n      >\n        {renderSocialIcon(link.platform)}\n      </Button>\n    ) : (\n      // Props-based linking — Button renders the anchor itself\n      // (openInNewTab carries target=\"_blank\" + rel=\"noopener noreferrer\");\n      // no asChild/<a> nesting.\n      <Button\n        key={index}\n        variant={resolvedVariant}\n        size={compact ? 'icon-sm' : 'icon'}\n        className={compact ? undefined : 'flex-1'}\n        href={link.href}\n        openInNewTab\n        aria-label={ariaLabel}\n      >\n        {renderSocialIcon(link.platform)}\n      </Button>\n    );\n  };\n\n  const rowClass = `flex flex-row items-center ${compact ? 'gap-2 w-fit' : 'gap-3 w-full'} ${className}`;\n\n  // Two-group mode: external links, a thin divider, then internal links — but only\n  // when both groups are present (otherwise it's just a flat row, no stray divider).\n  if (groupByVisibility) {\n    const external = links.filter(l => l.visibility !== 'internal');\n    const internal = links.filter(l => l.visibility === 'internal');\n    if (external.length > 0 && internal.length > 0) {\n      return (\n        <div className={rowClass}>\n          {external.map((l, i) => renderButton(l, i))}\n          <span aria-hidden className=\"my-1 w-px shrink-0 self-stretch bg-ods-border\" />\n          {internal.map((l, i) => renderButton(l, external.length + i))}\n        </div>\n      );\n    }\n  }\n\n  return <div className={rowClass}>{links.map((link, index) => renderButton(link, index))}</div>;\n}\n","'use client';\n\nimport { type CSSProperties, type ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils';\nimport { OpenFrameLogo } from '../icons';\nimport { Button, type ButtonProps } from '../ui/button';\nimport { StatusBadge } from '../ui/status-badge';\n\nexport interface StartWithOpenFrameButtonProps extends Omit<ButtonProps, 'variant' | 'size' | 'leftIcon'> {\n  children?: ReactNode;\n  mode?: 'outline' | 'yellow' | 'pink' | 'purple' | 'cyan';\n  buttonSize?: 'sm' | 'md' | 'lg';\n  loading?: boolean;\n  buttonBackgroundColor?: string;\n  buttonTextColor?: string;\n}\n\n/**\n * \"Start with OpenFrame\" button for Flamingo header\n * – Five modes: 'outline' (default), 'yellow', 'pink', 'purple', and 'cyan'\n * – Shows OpenFrame icon on the left\n * – Same pattern as OpenFrame Github button in hero section\n * – Cyan mode uses custom background/text colors like JoinWaitlistButton\n */\nexport const StartWithOpenFrameButton = forwardRef<HTMLButtonElement, StartWithOpenFrameButtonProps>(\n  (\n    {\n      children = 'Start Free Trial',\n      mode = 'outline',\n      className,\n      buttonSize,\n      loading = false,\n      buttonBackgroundColor,\n      buttonTextColor,\n      ...props\n    },\n    ref,\n  ) => {\n    const isYellow = mode === 'yellow';\n    const isPink = mode === 'pink' || mode === 'purple';\n    const isCyan = mode === 'cyan';\n\n    // Map buttonSize to Button component's size prop\n    const mappedSize =\n      buttonSize === 'md'\n        ? 'default'\n        : buttonSize === 'sm'\n          ? 'small-legacy'\n          : buttonSize === 'lg'\n            ? 'default'\n            : undefined;\n\n    // Determine button variant and class names based on mode\n    let buttonVariant: 'accent' | 'outline' = 'outline';\n    let modeClassName = '';\n    let iconLowerPath = 'var(--ods-open-yellow-base)';\n    let iconUpperPath = 'var(--ods-system-greys-white)';\n    let customStyle: CSSProperties = {};\n\n    if (isYellow) {\n      buttonVariant = 'accent';\n      modeClassName = 'bg-ods-accent hover:bg-ods-accent-hover text-ods-text-on-accent border-ods-accent';\n      iconLowerPath = 'var(--ods-system-greys-white)';\n      iconUpperPath = 'var(--ods-system-greys-black)';\n    } else if (isPink) {\n      buttonVariant = 'accent';\n      modeClassName =\n        'bg-ods-flamingo-pink hover:bg-ods-flamingo-pink-hover text-ods-text-on-accent border-ods-flamingo-pink';\n      iconLowerPath = 'var(--ods-system-greys-white)';\n      iconUpperPath = 'var(--ods-system-greys-black)';\n    } else if (isCyan) {\n      // Cyan mode: similar to JoinWaitlistButton with custom colors\n      buttonVariant = 'accent';\n      modeClassName =\n        'bg-ods-flamingo-cyan hover:bg-ods-flamingo-cyan-hover text-ods-text-on-accent border-ods-flamingo-cyan';\n      // Allow override with custom colors if provided\n      if (buttonBackgroundColor || buttonTextColor) {\n        customStyle = {\n          backgroundColor: buttonBackgroundColor,\n          color: buttonTextColor,\n        };\n      }\n      // For cyan mode with black text, use white/black icon for contrast\n      iconLowerPath = '#ffffff';\n      iconUpperPath = '#1A1A1A';\n    }\n\n    return (\n      <Button\n        ref={ref}\n        {...props}\n        size={mappedSize}\n        variant={buttonVariant}\n        loading={loading}\n        className={cn(modeClassName, className)}\n        style={customStyle}\n        leftIcon={\n          !loading ? (\n            <OpenFrameLogo className=\"h-5 w-5\" lowerPathColor={iconLowerPath} upperPathColor={iconUpperPath} />\n          ) : undefined\n        }\n        rightIcon={<StatusBadge text=\"Beta\" variant=\"button\" colorScheme=\"yellow\" />}\n      >\n        {children}\n      </Button>\n    );\n  },\n);\nStartWithOpenFrameButton.displayName = 'StartWithOpenFrameButton';\n","'use client';\n\nimport { Filter } from 'lucide-react';\nimport { Button } from '../ui/button';\n\nexport interface StatusOption {\n  value: string;\n  label: string;\n}\n\nexport interface StatusFilterComponentProps {\n  selectedStatus: string;\n  onStatusChange: (status: string) => void;\n  statusOptions: StatusOption[];\n  showCount?: boolean;\n  count?: number;\n  className?: string;\n  /** Status values (incl. `'all'`) to render but DISABLE — the buttons stay\n   *  visible (so the full set is always shown) but are non-clickable. Lets a\n   *  dashboard expose every status while gating which ones a given viewer may\n   *  actually select (e.g. non-management can only pick `published`). */\n  disabledValues?: string[];\n  /**\n   * Row label. Defaults to `Status`, so every existing call site is unchanged.\n   *\n   * Set it to reuse this row for any other single-select facet (Discipline,\n   * Level, …) instead of hand-rolling a look-alike: a duplicated row renders\n   * markup identical to this one, and React then pairs the copy against this\n   * component during hydration and reports a mismatch on the label text.\n   */\n  label?: string;\n  /**\n   * Render the built-in \"All\" clear-selection button (default true). Turn it\n   * off for facets with no all-of-them state — e.g. a View toggle\n   * (Everyone / My Sessions), where one option is always selected. Added so\n   * those rows reuse THIS component instead of hand-rolling identical markup\n   * (which React then pairs against this component during hydration).\n   */\n  showAll?: boolean;\n}\n\n/**\n * StatusFilterComponent - Reusable status filter for admin dashboards\n *\n * Displays a row of status filter buttons matching the blog posts dashboard pattern\n */\nexport function StatusFilterComponent({\n  selectedStatus,\n  onStatusChange,\n  statusOptions,\n  showCount = false,\n  count = 0,\n  className = '',\n  disabledValues = [],\n  label = 'Status',\n  showAll = true,\n}: StatusFilterComponentProps) {\n  // Filter out 'all' from options since we render it separately\n  const filteredOptions = statusOptions.filter(option => option.value !== 'all');\n\n  return (\n    <div\n      className={`flex flex-wrap items-center gap-3 rounded-lg border border-ods-border bg-ods-card p-4 ${className}`}\n    >\n      <div className=\"flex items-center gap-2\">\n        <Filter className=\"h-4 w-4 text-ods-accent\" />\n        <span className=\"text-ods-text-secondary text-h5\">{label}</span>\n      </div>\n\n      {/* All button */}\n      {showAll && (\n        <Button\n          type=\"button\"\n          variant={selectedStatus === 'all' ? 'accent' : 'outline'}\n          size=\"small-legacy\"\n          onClick={() => onStatusChange('all')}\n          disabled={disabledValues.includes('all')}\n          className=\"text-h3\"\n        >\n          All\n        </Button>\n      )}\n\n      {/* Status option buttons */}\n      {filteredOptions.map(option => (\n        <Button\n          key={option.value}\n          type=\"button\"\n          variant={selectedStatus === option.value ? 'accent' : 'outline'}\n          size=\"small-legacy\"\n          onClick={() => onStatusChange(option.value)}\n          disabled={disabledValues.includes(option.value)}\n          className=\"text-h3\"\n        >\n          {option.label}\n        </Button>\n      ))}\n\n      {/* Optional count display */}\n      {showCount && <div className=\"ml-auto shrink-0 text-ods-text-secondary text-h6\">{count} items</div>}\n    </div>\n  );\n}\n","'use client';\n\nimport { Search, X, Plus, Loader2 } from 'lucide-react';\nimport { useState } from 'react';\nimport { cn } from '../../utils';\nimport { Button } from '../ui';\n\ninterface Tag {\n  id: number;\n  name: string;\n  slug: string;\n}\n\ninterface TagsSelectorProps {\n  availableTags: Tag[];\n  selectedTagIds: number[];\n  onTagsChange: (tagIds: number[]) => void;\n  onCreateTag?: (tagName: string) => Promise<Tag | null>;\n  maxTags?: number;\n  placeholder?: string;\n  className?: string;\n  disabled?: boolean;\n  allowCreate?: boolean;\n}\n\n/**\n * Unified Tags Selector Component\n * Used across blog posts, case studies, and product releases\n * Features:\n * - Search autocomplete\n * - Chip display inside search input\n * - Tag limit (default 10)\n * - Removable chips\n * - Auto-opens on focus (like blog post wizard)\n * - Create new tags (when allowCreate is true and onCreateTag is provided)\n */\nexport function TagsSelector({\n  availableTags,\n  selectedTagIds,\n  onTagsChange,\n  onCreateTag,\n  maxTags = 10,\n  placeholder = 'Search tags...',\n  className,\n  disabled = false,\n  allowCreate = true,\n}: TagsSelectorProps) {\n  const [searchQuery, setSearchQuery] = useState('');\n  const [showDropdown, setShowDropdown] = useState(false);\n  const [isCreating, setIsCreating] = useState(false);\n\n  const handleTagAdd = (tagId: number) => {\n    if (!selectedTagIds.includes(tagId) && selectedTagIds.length < maxTags) {\n      onTagsChange([...selectedTagIds, tagId]);\n      setSearchQuery('');\n    }\n  };\n\n  const handleTagRemove = (tagId: number) => {\n    onTagsChange(selectedTagIds.filter(id => id !== tagId));\n  };\n\n  const handleCreateTag = async () => {\n    if (!onCreateTag || !searchQuery.trim() || isCreating) return;\n\n    setIsCreating(true);\n    try {\n      const newTag = await onCreateTag(searchQuery.trim());\n      if (newTag) {\n        // Add the new tag to selection\n        onTagsChange([...selectedTagIds, newTag.id]);\n        setSearchQuery('');\n        setShowDropdown(false);\n      }\n    } catch (error) {\n      console.error('Failed to create tag:', error);\n    } finally {\n      setIsCreating(false);\n    }\n  };\n\n  const getFilteredTagsForAutocomplete = () => {\n    return availableTags.filter(\n      tag => !selectedTagIds.includes(tag.id) && tag.name.toLowerCase().includes(searchQuery.toLowerCase()),\n    );\n  };\n\n  const getSelectedTags = () => {\n    return availableTags.filter(tag => selectedTagIds.includes(tag.id));\n  };\n\n  // Check if the search query exactly matches an existing tag (case-insensitive)\n  const tagExistsWithName = (name: string) => {\n    return availableTags.some(tag => tag.name.toLowerCase() === name.toLowerCase());\n  };\n\n  const canCreateNewTag = allowCreate && onCreateTag && searchQuery.trim() && !tagExistsWithName(searchQuery.trim());\n  const filteredTags = getFilteredTagsForAutocomplete();\n\n  return (\n    <div className={cn('space-y-2', className)}>\n      <div className=\"relative\">\n        {/* Search Icon */}\n        <div className=\"pointer-events-none absolute left-3 top-3 z-10 flex items-center\">\n          <Search className=\"h-4 w-4 text-ods-text-secondary\" />\n        </div>\n\n        {/* Input Container with Chips Inside */}\n        <div\n          className={cn(\n            'w-full rounded-lg border border-ods-border bg-ods-bg',\n            'focus-within:border-ods-accent focus-within:ring-2 focus-within:ring-ods-accent',\n            'flex min-h-[42px] flex-wrap items-center gap-1.5 p-2 pl-10 transition-all duration-200',\n            disabled && 'cursor-not-allowed opacity-50',\n          )}\n        >\n          {/* Selected Tag Chips Inside Search Bar */}\n          {getSelectedTags().map(tag => (\n            <span\n              key={tag.id}\n              className=\"inline-flex items-center gap-1 rounded-md border border-ods-accent/30 bg-ods-accent/15 py-1 pl-2.5 pr-1 text-ods-text-primary text-h6\"\n            >\n              {tag.name}\n              {!disabled && (\n                <button\n                  type=\"button\"\n                  onClick={e => {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    handleTagRemove(tag.id);\n                  }}\n                  className=\"inline-flex h-4 w-4 items-center justify-center rounded-full text-ods-text-secondary transition-colors hover:bg-ods-accent/20 hover:text-ods-text-primary\"\n                  aria-label={`Remove ${tag.name}`}\n                >\n                  <X className=\"h-3 w-3\" />\n                </button>\n              )}\n            </span>\n          ))}\n\n          {/* Search Input - Takes remaining space */}\n          <input\n            type=\"text\"\n            value={searchQuery}\n            onChange={e => {\n              setSearchQuery(e.target.value);\n              setShowDropdown(true);\n            }}\n            onFocus={() => {\n              if (selectedTagIds.length < maxTags) {\n                setShowDropdown(true);\n              }\n            }}\n            onBlur={() => {\n              setTimeout(() => setShowDropdown(false), 200);\n            }}\n            onKeyDown={e => {\n              if (e.key === 'Enter' && canCreateNewTag) {\n                e.preventDefault();\n                // handleCreateTag never rejects — it try/catch/finally's internally.\n                void handleCreateTag();\n              }\n            }}\n            placeholder={\n              selectedTagIds.length >= maxTags\n                ? 'Maximum tags reached'\n                : selectedTagIds.length === 0\n                  ? placeholder\n                  : 'Add more...'\n            }\n            disabled={disabled || selectedTagIds.length >= maxTags}\n            className={cn(\n              'min-w-[100px] flex-1 border-none bg-transparent py-1 text-ods-text-primary outline-none text-h4 placeholder:text-ods-text-secondary focus:border-0 focus:outline-none focus:ring-0',\n              selectedTagIds.length >= maxTags && 'cursor-not-allowed opacity-50',\n            )}\n          />\n\n          {/* Clear Search Button */}\n          {searchQuery && (\n            <Button\n              variant=\"transparent\"\n              type=\"button\"\n              onClick={() => {\n                setSearchQuery('');\n                setShowDropdown(false);\n              }}\n              leftIcon={<X className=\"h-3 w-3\" />}\n              className=\"h-5 min-h-0 w-5 min-w-0 shrink-0 p-0 text-ods-text-secondary hover:bg-transparent hover:text-ods-text-primary\"\n              aria-label=\"Clear search\"\n            />\n          )}\n        </div>\n\n        {/* Autocomplete Dropdown - Shows all available tags on focus */}\n        {showDropdown && !disabled && (\n          <div className=\"absolute z-50 mt-1 max-h-60 w-full overflow-y-auto rounded-lg border border-ods-border bg-ods-card shadow-lg\">\n            <div className=\"p-3\">\n              {/* Create New Tag Option */}\n              {canCreateNewTag && (\n                <Button\n                  variant=\"outline\"\n                  type=\"button\"\n                  onClick={handleCreateTag}\n                  disabled={isCreating}\n                  leftIcon={isCreating ? <Loader2 className=\"h-3 w-3 animate-spin\" /> : <Plus className=\"h-3 w-3\" />}\n                  className=\"mb-2 flex h-auto w-full flex-row items-center gap-1.5 rounded border-dashed border-ods-accent bg-ods-bg px-2 py-1 text-ods-accent !text-h6 hover:bg-ods-card\"\n                >\n                  Create tag: <strong>\"{searchQuery.trim()}\"</strong>\n                </Button>\n              )}\n\n              {/* Existing Tags */}\n              {filteredTags.length > 0 ? (\n                <div className=\"flex flex-wrap gap-1.5\">\n                  {filteredTags.map(tag => (\n                    <Button\n                      key={tag.id}\n                      variant=\"outline\"\n                      type=\"button\"\n                      onClick={() => handleTagAdd(tag.id)}\n                      className=\"h-auto rounded border-ods-border bg-ods-bg px-2 py-0.5 text-ods-text-primary !text-h6 hover:border-ods-accent hover:bg-ods-card\"\n                    >\n                      {tag.name}\n                    </Button>\n                  ))}\n                </div>\n              ) : !canCreateNewTag ? (\n                <div className=\"w-full px-4 py-4 text-center\">\n                  <p className=\"text-ods-text-secondary text-h6\">\n                    {searchQuery.trim() ? `No tags found for \"${searchQuery}\"` : 'No tags available'}\n                  </p>\n                </div>\n              ) : null}\n            </div>\n          </div>\n        )}\n      </div>\n\n      {/* Tag Counter */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"text-ods-text-secondary text-h6\">\n          {selectedTagIds.length} / {maxTags} tags selected\n        </div>\n        {selectedTagIds.length >= maxTags && <span className=\"text-ods-error text-h6\">(Maximum reached)</span>}\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { AlertTriangle } from 'lucide-react';\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface WarningBlockProps {\n  /** Text rendered inside the highlighted warning banner header. */\n  title: React.ReactNode;\n  /**\n   * Icon shown at the start of the banner. Defaults to a triangle alert icon.\n   * Pass `null` to hide the icon entirely.\n   */\n  icon?: React.ReactNode;\n  /**\n   * Body content rendered below the banner — description paragraphs, one or more\n   * `PathsDisplay` lists, command rows, etc. Fully composable so a single block\n   * can mix text and multiple path groups (see the New Device warning blocks).\n   */\n  children?: React.ReactNode;\n  /** Extra classes for the outer card container. */\n  className?: string;\n}\n\nconst DEFAULT_ICON = <AlertTriangle className=\"h-6 w-6\" />;\n\n/**\n * WarningBlock - a card with a highlighted \"attention / warning\" banner header\n * followed by arbitrary body content.\n *\n * The banner reproduces the ODS warning callout (warning color on a muted\n * warning background). Body content is fully composable: pass description\n * paragraphs, one or more `PathsDisplay` lists, command rows, etc. as children.\n *\n * Spacing follows the ODS scale: card padding 16px / gap 12px, banner padding\n * 12px / gap 16px. Typography uses `text-h3`, which is responsive (14px on\n * mobile, 18px from `md` up) and bold by design-token default.\n */\nexport function WarningBlock({ title, icon = DEFAULT_ICON, children, className }: WarningBlockProps) {\n  return (\n    <div className={cn('flex flex-col gap-3 rounded-[6px] border border-ods-border bg-ods-card p-4', className)}>\n      <div className=\"flex items-start gap-4 overflow-hidden rounded-[6px] bg-ods-warning-secondary p-3 text-ods-warning\">\n        {icon != null && <span className=\"shrink-0\">{icon}</span>}\n        <p className=\"min-w-0 flex-1 text-h3\">{title}</p>\n      </div>\n      {children}\n    </div>\n  );\n}\n","'use client';\n\nimport type { VideoTeaser } from '../../types/video-processing';\nimport { DEFAULT_VIDEO_BITES_TITLE } from './video-bites-shared';\nimport { VideoBitesStrip } from './video-bites-strip';\n\n/**\n * @deprecated Transitional shim — use `<VideoBitesStrip>` directly.\n *\n * The old aspect-ratio GRID rendered here was replaced by the unified\n * strip (`video-bites-strip.tsx`, Figma node 4033-90364). This wrapper\n * keeps the historical 4-prop signature compiling for hub call sites\n * between the lib version bump and the hub-side migration.\n *\n * Scheduled deletion: next lib minor after the hub migration's grep gate\n * confirms zero consumers. Do NOT add features here.\n */\n\nexport interface VideoBitesDisplayProps {\n  /** Array of video bites/teasers to display. */\n  bites: VideoTeaser[];\n  /** Title for the section. */\n  title?: string;\n  /** Whether to filter to only show published bites. Default `true`. */\n  filterPublished?: boolean;\n  /** Whether to show the title section heading. Default `true`. */\n  showTitle?: boolean;\n}\n\nexport function VideoBitesDisplay({\n  bites,\n  title = DEFAULT_VIDEO_BITES_TITLE,\n  filterPublished = true,\n  showTitle = true,\n}: VideoBitesDisplayProps) {\n  return <VideoBitesStrip bites={bites} title={title} filterPublished={filterPublished} showTitle={showTitle} />;\n}\n","'use client';\n\n/**\n * THE generic, embeddable per-platform demo-video widget, in the two placements\n * hosts actually mount: <FloatingWalkthroughVideo> and <InlineWalkthroughVideo>.\n *\n * SHARED GRAMMAR. A collapsed card (bite-identical hover grammar via\n * <VideoHoverPreviewSurface>) that opens a large in-page theater (Radix Dialog\n * primitives, NOT native fullscreen) showing ONLY the video: a bare 16:9 stage\n * with the player's own controls + captions. No card chrome, no summary.\n *   - FLOATING pins that card to a bottom corner over the page — left unless\n *     the video data says `position: 'right'`.\n *   - INLINE lays it out in the host's own flow, filling its container at 16:9.\n *     Every overlay-only behaviour drops out with the pinning: no fixed\n *     positioning, no z-layer, no shadow, no appear delay (nothing to stagger\n *     against an already-painted page), no footer fade (no footer to collide\n *     with) and no dismissal (an X is an overlay's \"get out of my way\"; in a\n *     host's own layout it just leaves a hole, and the cookie is per-platform,\n *     so one dismissal in the corner would silently empty the in-page block).\n *\n * ONE ENGINE, TWO PRESETS. <WalkthroughVideo> below takes `placement` and is\n * deliberately NOT exported: placement is a constant at every call site, so the\n * public API is two components that say what they are instead of one that takes\n * a mode — a <FloatingWalkthroughVideo placement=\"inline\"> reads as a\n * contradiction, and naming a family after one of its variants is what produced\n * it. The presets are one line each; everything below the wrapper — the player\n * mode machine, the audio invariant, the corner map, dismissal — stays shared,\n * because forking THAT is how the two placements would drift.\n *\n * The split pays off in the types: the overlay-only knobs are absent from\n * `InlineWalkthroughVideoProps` outright, so \"floating only\" is a compile error\n * rather than a line of prose each caller has to read.\n *\n * All UI lives here in the lib so every platform site AND the react-embedding\n * example mount the same component; the host supplies only the video data.\n *\n * STATE MODEL (read before adding a boolean — the recurring bugs in this file\n * all came from ad-hoc flags contradicting each other):\n *   `cardMode`  — ONE discriminant: 'resume' | 'preview' | 'poster'. Derived,\n *                 never stored, so the two players can't both be mounted.\n *   `cardMuted` / `cardPaused` — transport state for whichever card player is\n *                 live. Both toggles ALWAYS render in both states; a control\n *                 that hides itself on use is a dead end (that bug shipped\n *                 twice). Never gate a control's visibility on its own value.\n *   `cardFallback` — autoplay-BLOCKED prompt only. Distinct from `cardMuted`\n *                 (user intent); conflating them is what made unmute vanish.\n *   `handoff`   — theater→card continuation snapshot. Clearing it UNMOUNTS the\n *                 mini player, so never clear it to express \"paused\".\n * Layering: the media layer is `pointer-events-none` so a paused/idle player\n * can never swallow a click; every non-control click reaches the activation\n * overlay. Corners are exclusive: transport TL, dismiss TR, title+presenter BL.\n *\n * Audio invariant (both directions): never two overlapping streams, nor a blip.\n *   - open: pause the hover preview synchronously (pointerdown), force the\n *     surface inactive for the whole open duration, reset the YouTube suspend.\n *   - close: snapshot then pause the theater player (file) / suspend (YouTube).\n *     NOTE: Radix wraps Portal and Content in `Presence`, which dispatches\n *     UNMOUNT from its OWN layout effect — so the Content survives one extra\n *     render after `open` flips false, even with no exit animation. That extra\n *     render is what makes the falling-edge `useLayoutEffect` below safe: the\n *     theater player is still mounted when it runs. (An earlier version of this\n *     note claimed the unmount happens in the same commit; it does not, and\n *     that mistake is what produced a since-deleted 37-finding lint exemption.)\n */\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport type React from 'react';\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport { useIsHydrated } from '../../hooks/ui/use-is-hydrated';\nimport { cn } from '../../utils/cn';\nimport {\n  WALKTHROUGH_VIDEO_DISMISS_KEY,\n  isWalkthroughDismissed,\n  dismissWalkthrough,\n} from '../../utils/dismissal-storage';\nimport { WALKTHROUGH_OPEN_QUERY_PARAM } from '../../utils/walkthrough-deep-link';\nimport { VolumeUpIcon } from '../icons-v2-generated/audio-and-visual/volume-up-icon';\nimport { VolumeXmarkIcon } from '../icons-v2-generated/audio-and-visual/volume-xmark-icon';\nimport { PauseIcon } from '../icons-v2-generated/media-playback/pause-icon';\nimport { PlayIcon } from '../icons-v2-generated/media-playback/play-icon';\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';\nimport { Button } from '../ui/button';\nimport { DialogPortal, DialogOverlay } from '../ui/dialog';\nimport { SquareAvatar } from '../ui/square-avatar';\nimport { CardHitLayer } from './card-hit-layer';\nimport { Video, type VideoPlayerHandle, type VideoMutedFallbackState } from './video';\nimport { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';\nimport { VideoHoverPreviewSurface } from './video-hover-preview';\n\n/** Wire-shape data for the widget. Kept as the shared contract the hub DAL\n *  re-exports as `PublicWalkthroughVideo & { id }`. `mainVideoUrl`/`youtubeUrl`\n *  stay SEPARATE — YouTube wins when both are set (card matches theater). */\nexport interface WalkthroughVideoData {\n  /** Row id — used for id-match cookie dismissal (a new video re-shows the\n   *  card even after an old one was dismissed). */\n  id?: string | number;\n  mainVideoUrl?: string | null;\n  youtubeUrl?: string | null;\n  posterUrl?: string | null;\n  /** RELATIVE VTT path (/api/captions/...); embedders prefix their proxy base. */\n  captionsUrl?: string | null;\n  title?: string | null;\n  presenterAvatarUrl?: string | null;\n  /** Which bottom corner the collapsed card pins to. Admin-controlled per\n   *  video; anything other than 'right' (including absent) means left. */\n  position?: 'left' | 'right' | null;\n}\n\n/** Everything the two placements share — the widget's actual contract. */\ninterface WalkthroughVideoBaseProps {\n  video: WalkthroughVideoData | null | undefined;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  defaultOpen?: boolean;\n  /** With `defaultOpen`: the initial theater starts PAUSED instead of\n   *  autoplaying — for deep links (`?walkthrough=1`), where the open has no\n   *  user gesture and unrequested audio/motion would be hostile. Applies to\n   *  the initial `defaultOpen` session only: it ends when the NEXT open —\n   *  gesture-driven (card click) or host-driven (`open` flipping true) —\n   *  mounts a fresh theater, which autoplays as usual. */\n  defaultOpenPaused?: boolean;\n  /** Query-param NAME that deep-links into the theater: when present in\n   *  `window.location.search` at first client render, the theater opens\n   *  immediately and PAUSED — `defaultOpen + defaultOpenPaused` decided\n   *  inside the component, so hosts don't defer their first render to read\n   *  the URL. Defaults to `WALKTHROUGH_OPEN_QUERY_PARAM` ('walkthrough');\n   *  pass '' to disable. Presence-based (any value counts), read ONCE at\n   *  mount (deep links arrive by full page load, not client navigation).\n   *  SSR-safe: the server renders closed, and the theater lives in a portal,\n   *  so the hydrated (non-portal) markup is identical either way. */\n  deepLinkParam?: string;\n  label?: string;\n  className?: string;\n}\n\n/**\n * <FloatingWalkthroughVideo>: the shared contract plus the knobs that only mean\n * something for an OVERLAY. Each of these is inert inline — the appear delay\n * staggers against a page the visitor hasn't seen yet, dismissal is an overlay\n * affordance, and the footer fade (with the `pathname` that re-queries its\n * target) exists to keep a pinned card off the page chrome. So they live here,\n * not in the base.\n */\nexport interface FloatingWalkthroughVideoProps extends WalkthroughVideoBaseProps {\n  appearDelayMs?: number;\n  /** Cookie-based dismissal (id-match, mirrors the announcement bar). `false`\n   *  disables the X entirely. `storageKey` is the per-platform cookie name. */\n  dismissal?: { storageKey?: string } | false;\n  /** Fades the card out while `hideNearSelector` is in view. */\n  hideNearSelector?: string;\n  /** Route identity from the host (the lib can't observe navigation). Changing\n   *  it re-queries the footer IO target. */\n  pathname?: string;\n}\n\n/** <InlineWalkthroughVideo>: the shared contract and nothing else — see above. */\nexport type InlineWalkthroughVideoProps = WalkthroughVideoBaseProps;\n\n/** Engine props. Internal: `placement` never reaches a call site. */\ntype WalkthroughVideoProps = FloatingWalkthroughVideoProps & { placement: 'floating' | 'inline' };\n\n/** \"Paused at the end\" — used by BOTH the close snapshot and the reopen seed,\n *  which must agree or a finished video reopens at its own last frame. */\nconst isAtEnd = (duration: number, time: number) => duration > 0 && duration - time < 1;\n\n/** The theater's load-time seed, computed from whatever card player is LIVE.\n *  Pure and module-scope on purpose: the only reader is the host-open sync,\n *  which runs during render (see there for why it cannot be an effect), and a\n *  helper keeps that block down to a single ref read instead of six. */\nfunction theaterSeedFromLive(\n  live: VideoPlayerHandle | null,\n  fallbackTime: number,\n  fallbackMuted: boolean,\n): { time: number; muted: boolean } {\n  const time = live?.getCurrentTime() ?? 0;\n  const atEnd = isAtEnd(live?.getDuration() ?? 0, time);\n  return {\n    // At end -> start over, never rewind to an unrelated older anchor.\n    time: atEnd ? 0 : time > 0.5 ? time : fallbackTime,\n    // Trust the live element only when it has actually been playing; a\n    // freshly remounted poster-mode player reports muted=false and would\n    // silently discard the user's last explicit mute.\n    muted: live && time > 0.5 ? live.getMuted() : fallbackMuted,\n  };\n}\n\nconst WALKTHROUGH_Z = 'z-[9980]'; // one layer BELOW the chat dock (z-[9990]).\n\ninterface Handoff {\n  time: number;\n  muted: boolean;\n  playing: boolean;\n}\n\nfunction WalkthroughVideo({\n  video,\n  open: openProp,\n  onOpenChange,\n  defaultOpen,\n  defaultOpenPaused,\n  deepLinkParam = WALKTHROUGH_OPEN_QUERY_PARAM,\n  label = 'Play Demo Video',\n  placement,\n  // Keeps the FLOATING card off the critical path. The inline preset passes 0:\n  // an in-page block is part of a page the visitor is already looking at, so\n  // staggering it in would only jump the layout.\n  appearDelayMs = 3000,\n  dismissal = {},\n  hideNearSelector = 'footer',\n  pathname,\n  className,\n}: WalkthroughVideoProps): React.ReactElement | null {\n  const inline = placement === 'inline';\n  const dismissEnabled = dismissal !== false;\n  const storageKey = (dismissEnabled && dismissal.storageKey) || WALKTHROUGH_VIDEO_DISMISS_KEY;\n  // id-match dismissal key: a new video (new id) re-shows the card. No id\n  // (embedder) → a stable presence marker so dismissal still works.\n  const dismissalId = video?.id != null ? String(video.id) : 'dismissed';\n\n  // --- mount gate (never LCP; cookie read happens post-delay, no hydration mismatch) ---\n  const [mounted, setMounted] = useState(false);\n  const [dismissed, setDismissed] = useState(false);\n  const hasVideo = video != null;\n  const videoId = video?.id;\n  useEffect(() => {\n    let idleHandle: number | null = null;\n    // Wait for the data: on the embedder path `video` starts null, so the\n    // dismissal id was 'dismissed' on the first pass and a dismissed card\n    // flashed for the length of a second appear-delay before hiding.\n    if (!hasVideo) return undefined; // wait for data — see the id-based dep below\n    const timer = setTimeout(() => {\n      const reveal = () => {\n        if (dismissEnabled && isWalkthroughDismissed(storageKey, dismissalId)) {\n          setDismissed(true);\n          return;\n        }\n        // Cleared on the way through: a client-only embedder can swap to a NEW\n        // video id in-session (the cookie is id-matched, so the new one isn't\n        // dismissed), and a sticky `dismissed` kept the card hidden until a\n        // full reload — contradicting the id-match behaviour documented above.\n        setDismissed(false);\n        setMounted(true);\n      };\n      const ric = (window as unknown as { requestIdleCallback?: (cb: () => void) => number }).requestIdleCallback;\n      if (typeof ric === 'function') idleHandle = ric(reveal);\n      else reveal();\n    }, appearDelayMs);\n    return () => {\n      clearTimeout(timer);\n      const cic = (window as unknown as { cancelIdleCallback?: (h: number) => void }).cancelIdleCallback;\n      if (idleHandle !== null && typeof cic === 'function') cic(idleHandle);\n    };\n    // `videoId`, not the object: a new identity (embedder refetch, re-seeded\n    // RSC payload) would otherwise restart the appear delay.\n  }, [appearDelayMs, dismissEnabled, storageKey, dismissalId, hasVideo, videoId]);\n\n  // --- environment preferences (read after hydration; no SSR access) ---\n  // Derived, not stored: both inputs are plain reads of the environment, and\n  // the only reason they could not happen during render was that neither exists\n  // on the server. `useIsHydrated` states exactly that gate, so the answer is\n  // available in the first render that can act on it instead of arriving a\n  // commit later — the commit in which a reduced-motion visitor had already\n  // been handed a hoverable preview.\n  const hydrated = useIsHydrated();\n  const previewSuppressed = useMemo(() => {\n    if (!hydrated) return false;\n    try {\n      const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;\n      const conn = (navigator as unknown as { connection?: { saveData?: boolean; effectiveType?: string } }).connection;\n      const saveData = conn?.saveData === true || conn?.effectiveType === '2g' || conn?.effectiveType === 'slow-2g';\n      return Boolean(reducedMotion || saveData);\n    } catch {\n      return false;\n    }\n  }, [hydrated]);\n\n  // --- controlled/uncontrolled open ---\n  // Deep link: read ONCE, synchronously, on the first client render — the\n  // theater's autoplay props are load-time, so the decision must exist before\n  // the render that mounts it (an effect would be one commit too late). The\n  // lazy initializer never re-runs, so client navigation can't re-trigger it.\n  const [deepLinkHit] = useState(() => {\n    if (!deepLinkParam || typeof window === 'undefined') return false;\n    try {\n      return new URLSearchParams(window.location.search).has(deepLinkParam);\n    } catch {\n      return false;\n    }\n  });\n  const [openState, setOpenState] = useState(Boolean(defaultOpen) || deepLinkHit);\n  const open = openProp !== undefined ? openProp : openState;\n  // The paused deep-link session — see `defaultOpenPaused`. Load-time-safe:\n  // the theater player's autoplay props are read once at construction, and\n  // this is set before the first render that mounts it. Cleared on the next\n  // OPEN transition only (the prevOpen sync below), right before a fresh\n  // theater constructs — never on close, where the closing player is still\n  // mounted and a prop flip would restart it (see the sync's comment).\n  const [pausedOpenSession, setPausedOpenSession] = useState(Boolean(defaultOpen && defaultOpenPaused) || deepLinkHit);\n\n  // --- refs & continuation state ---\n  const previewHandleRef = useRef<VideoPlayerHandle | null>(null);\n  const theaterHandleRef = useRef<VideoPlayerHandle | null>(null);\n  // Why the theater reports its fallback state even though it renders no card\n  // control: `blocked`/`muted` distinguish \"the user muted this\" from \"the\n  // browser refused sound\". Only the former may become standing intent.\n  const theaterForcedMuteRef = useRef(false);\n  const resumeHandleRef = useRef<VideoPlayerHandle | null>(null);\n  const closeButtonRef = useRef<HTMLButtonElement | null>(null);\n  // TRUE only when the user pressed Mute. An autoplay-forced mute (cardFallback)\n  // is NOT intent, so the theater must not inherit it — conflating the two is\n  // what made the theater open silently after a blocked hover preview.\n  const userMutedRef = useRef(false);\n  const endedLatchRef = useRef(false);\n\n  const [hovered, setHovered] = useState(false);\n  const [handoff, setHandoff] = useState<Handoff | null>(null);\n  const [suspended, setSuspended] = useState(false);\n  const [theaterStart, setTheaterStart] = useState<{ time: number; muted: boolean }>({ time: 0, muted: false });\n  const [footerObservation, setFooterObservation] = useState<{ key: string; hidden: boolean } | null>(null);\n  const [cardFallback, setCardFallback] = useState<VideoMutedFallbackState>({ muted: false, blocked: false });\n  // Card-player transport state, driving the persistent mute/play TOGGLES.\n  // Separate from `cardFallback` (which is the autoplay-blocked prompt).\n  const [cardMuted, setCardMuted] = useState(false);\n  const [cardPaused, setCardPaused] = useState(false);\n  // Renderable mirror of `userMutedRef` — the ref is for synchronous reads in\n  // callbacks, this is what the player subtree observes. Deliberately NOT\n  // `cardMuted`: that also tracks policy-forced mutes.\n  const [userMuted, setUserMuted] = useState(false);\n\n  const isYouTube = Boolean(video?.youtubeUrl);\n  // PRESENCE-based: any handoff means \"the card owns this video at this\n  // timestamp\"; `playing` only seeds the pause toggle. Keying it on `playing`\n  // made a paused close (or a tab-switch) a one-way door — the card lost its\n  // transport AND its hover preview with no way back. `!open` keeps a\n  // host-controlled `open` from running the card and theater at once.\n  const resumeMode = !open && handoff !== null && !isYouTube;\n  const previewAllowed = !previewSuppressed && !isYouTube;\n\n  // --- footer fade (re-query on pathname change; null-tolerant) ---\n  // The observation is TAGGED with the conditions it was made under, and the\n  // flag is derived by comparing that tag to the current ones. That is what\n  // removes the three `setFooterHidden(false)` resets the effect used to\n  // perform — each of them a synchronous setState in an effect body — and it\n  // covers a case they did not: an observation left over from a selector that\n  // has since stopped matching now expires instead of pinning the card hidden.\n  const footerFadeKey = `${mounted ? 1 : 0}|${inline ? 1 : 0}|${hideNearSelector}|${pathname}`;\n  const footerHidden = footerObservation?.key === footerFadeKey && footerObservation.hidden;\n  useEffect(() => {\n    if (!mounted) return undefined;\n    // An inline card scrolls WITH the page — fading it out over the footer\n    // would blank the very block the visitor scrolled down to.\n    if (inline) return undefined;\n    const el = typeof document !== 'undefined' ? document.querySelector(hideNearSelector) : null;\n    if (!el || typeof IntersectionObserver === 'undefined') return undefined;\n    const io = new IntersectionObserver(\n      entries => setFooterObservation({ key: footerFadeKey, hidden: entries[0]?.isIntersecting ?? false }),\n      { rootMargin: '0px 0px -40px 0px' },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [mounted, inline, hideNearSelector, pathname, footerFadeKey]);\n\n  // --- tab-hidden: pause resume-mode playback, keep the timestamp ---\n  useEffect(() => {\n    if (!resumeMode) return undefined;\n    const onVis = () => {\n      if (document.visibilityState === 'hidden') {\n        const h = resumeHandleRef.current;\n        if (h) {\n          // Pause in place. Rewriting the handoff here used to unmount the\n          // mini player, so a background tab-switch silently destroyed it.\n          h.pause();\n          setCardPaused(true);\n        }\n      }\n    };\n    document.addEventListener('visibilitychange', onVis);\n    return () => document.removeEventListener('visibilitychange', onVis);\n  }, [resumeMode]);\n\n  // Faded out behind the footer: stop audio. The card is `pointer-events-none`\n  // while hidden, so a still-playing player would be unreachable — the same\n  // reason the tab-hidden handler exists.\n  // Depends on cardMode too: a player that MOUNTS while the card is already\n  // hidden (e.g. the footer scrolled into view during the theater) would\n  // otherwise autoplay behind an invisible, pointer-inert card.\n  // The two STATE halves are adjusted while rendering: behind the footer the\n  // card is pointer-inert, so \"paused\" and \"not hovered\" are simply what being\n  // hidden MEANS. Both are guarded on the state they write, so the render pass\n  // React schedules for them takes the early exit — and doing it here means the\n  // transport toggles never commit a frame reading \"playing\" over a card the\n  // visitor cannot reach.\n  if (footerHidden && !cardPaused) setCardPaused(true);\n  if (footerHidden && hovered) setHovered(false);\n\n  useEffect(() => {\n    if (!footerHidden) return;\n    const h = resumeHandleRef.current ?? previewHandleRef.current;\n    try {\n      h?.pause();\n    } catch {\n      /* ignore */\n    }\n    // `resumeMode`/`hovered` (not the derived cardMode, declared below) are the\n    // inputs that decide whether a player is mounted.\n  }, [footerHidden, resumeMode, hovered]);\n\n  // Render-synced mirror of the derived `cardMode`, which is declared BELOW\n  // the callbacks that need it. Reading state declared later from a memoized\n  // callback is the exact trap that made every hover-preview open restart at\n  // 0:00 (the closure kept its render-0 value), so this is a ref, not a dep.\n  const cardModeRef = useRef<'resume' | 'preview' | 'poster'>('poster');\n\n  // Set by the card's OWN handlers, consumed by the controlled-mode sync\n  // below. Without it, a controlled host flipping `open` in response to\n  // `onOpenChange` looks identical to a genuinely host-originated open, and\n  // the sync clobbers the seed/snapshot the handler just computed.\n  //\n  // STATE, not a ref: the sync's rising edge has to read it during render, and\n  // it is a plain \"which transition did I originate\" latch, not an imperative\n  // handle. Every write is an event handler that flips `open` in the same\n  // batch, so the value is always committed by the render that observes it.\n  // Compared, never cleared — a discarded render attempt decides identically.\n  const [selfDrivenFor, setSelfDrivenFor] = useState<boolean | null>(null);\n\n  // Set on every close so the focus Radix restores to the hit layer can't be\n  // mistaken for the user tabbing to the card. CONSUMED BY THE FOCUS HANDLER\n  // (see commitOpen) — the timer beside it is only a safety valve.\n  const justClosedRef = useRef(false);\n  const justClosedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const commitOpen = useCallback(\n    (next: boolean) => {\n      if (!next) {\n        // CONSUMED BY THE FOCUS HANDLER, not by a timer racing Radix.\n        // Radix's FocusScope restores focus from its OWN `setTimeout(…, 0)`,\n        // registered during cleanup — i.e. AFTER any timer we schedule here.\n        // A timer-cleared latch was therefore always false by the time the\n        // restore landed, and Escape-close still restarted preview audio.\n        // The timer below is only a safety valve for the case where no focus\n        // restoration arrives at all (host-driven close, no focus to return).\n        justClosedRef.current = true;\n        if (justClosedTimerRef.current) clearTimeout(justClosedTimerRef.current);\n        justClosedTimerRef.current = setTimeout(() => {\n          justClosedTimerRef.current = null;\n          justClosedRef.current = false;\n        }, 400);\n      }\n      if (openProp === undefined) setOpenState(next);\n      onOpenChange?.(next);\n    },\n    [openProp, onOpenChange],\n  );\n\n  // Synchronous preview pause on pointerdown — the window-capture activation\n  // waiter unmutes a pre-activation preview on the first pointerdown; without\n  // this the opening/dismissing press itself blips at 50% volume.\n  const pausePreviewNow = useCallback(() => {\n    // Whichever player is live — in resume mode that is the continuation\n    // player, so pausing only the preview left a playing card audible through\n    // the whole pointerdown -> click -> commit window.\n    const h = resumeHandleRef.current ?? previewHandleRef.current;\n    try {\n      h?.pause();\n    } catch {\n      /* ignore */\n    }\n    // Keep the toggle honest: an aborted press (pointerdown then drag away)\n    // otherwise left it reading \"Pause\" over a paused video.\n    setCardPaused(true);\n  }, [setCardPaused]);\n\n  const openTheater = useCallback(() => {\n    // Captured BEFORE `pausePreviewNow()` below. NOTE this only helps keyboard\n    // activation: a mouse click already ran the hit layer's `onPointerDown`\n    // pause, so on that path the element IS paused here and the test below\n    // still reduces to `liveTime > 0.5`. Kept because Enter/Space dispatches\n    // no pointerdown, and there the live playing state is real.\n    const handleBeforePause = resumeHandleRef.current ?? previewHandleRef.current;\n    const liveWasPlaying = handleBeforePause?.getPaused() === false;\n    // Idempotent: keyboard activation (Enter/Space) dispatches no pointerdown,\n    // so the synchronous audio-stop guard would otherwise be skipped.\n    pausePreviewNow();\n    // Seed the theater from whatever the card was ACTUALLY playing, so the\n    // theater continues at the same second in every direction:\n    //   resume player (live) > hover preview (live) > paused handoff snapshot.\n    // Reading the preview handle is what makes \"hover-preview → click → theater\"\n    // continue mid-video instead of restarting at 0. Pausing on pointerdown does\n    // not reset currentTime, so the value is still accurate here.\n    // Source chosen by MODE. Picking \"whichever handle reports > 0.5s\" let a\n    // stale hover-preview position (0:05) shadow a stored handoff (3:20) on\n    // desktop while mobile — which never hovers — resumed correctly: the same\n    // gesture gave different results per platform.\n    // Read the refs directly — no render-scoped state. The previous version\n    // consulted `cardActive`, which is declared BELOW this callback and is not\n    // a dependency, so the memoized closure always saw its render-0 value\n    // (false) and every hover-preview -> theater open restarted at 0:00.\n    // Mode-gated: the card is `position: fixed`, so its preview player is\n    // permanently in-viewport and is NEVER unmounted — it keeps currentTime\n    // from a hover abandoned minutes ago. Reading it unconditionally made\n    // \"Play Demo Video\" silently start 10s in. Only a LIVE mode may seed.\n    const liveHandle = resumeHandleRef.current ?? (cardModeRef.current === 'preview' ? previewHandleRef.current : null);\n    const liveTime = liveHandle?.getCurrentTime() ?? 0;\n    // A preview that ran to completion keeps its currentTime, so without the\n    // same at-end guard the close path uses, clicking the card would open the\n    // theater seeked to the final frame and it would instantly re-end.\n    const liveDuration = liveHandle?.getDuration() ?? 0;\n    const liveAtEnd = isAtEnd(liveDuration, liveTime);\n    const start = {\n      // At end -> start over. Falling back to `handoff.time` here would rewind\n      // to an unrelated older anchor (e.g. 3:20) instead of restarting.\n      time: liveAtEnd ? 0 : liveTime > 0.5 ? liveTime : (handoff?.time ?? 0),\n      // Live element wins over remembered intent (the theater's own chrome may\n      // have changed it) — EXCEPT when the element is muted only because\n      // autoplay policy forced it, which is not intent.\n      // `cardFallback.muted` is armed BOTH by autoplay policy and by our own\n      // `startMuted` (i.e. by the user's mute), so it alone cannot mean\n      // \"forced\". Only a fallback the user never asked for is policy-forced;\n      // otherwise an explicitly muted video reopened at full volume.\n      // Trust the live element only when it has actually been playing; a\n      // freshly remounted poster-mode player always reports muted=false and\n      // would silently discard the user's last explicit mute.\n      muted:\n        cardFallback.muted && !userMutedRef.current\n          ? false\n          : liveHandle && (liveTime > 0.5 || liveWasPlaying)\n            ? liveHandle.getMuted()\n            : userMutedRef.current,\n    };\n    setTheaterStart(start);\n    setHovered(false); // force preview inactive\n    setSuspended(false); // reset YouTube suspend for this open\n    endedLatchRef.current = false;\n    setHandoff(null); // resume player unmounts\n    setSelfDrivenFor(true);\n    commitOpen(true);\n  }, [handoff, cardFallback.muted, commitOpen, pausePreviewNow, setHovered, setSuspended, setHandoff]);\n\n  // Sync for open/close transitions the card did NOT originate — a host\n  // driving the controlled `open` prop from its own UI. Card-originated\n  // transitions are latched by `selfDrivenFor` and skipped here, because the\n  // handler already did the seeding (and a controlled host flips `open` in\n  // RESPONSE to that handler, which is indistinguishable from a host-driven\n  // change without the latch). `defaultOpen` is NOT covered: it is the\n  // uncontrolled initial value, and the initial `theaterStart` already equals\n  // what this seed would compute.\n  //\n  // WHY THE RISING EDGE IS STILL IN RENDER (it is NOT about load-time props —\n  // that argument was tested and is false): the theater's Content is behind\n  // Radix `Presence`, which starts `unmounted` and only dispatches MOUNT from\n  // its own layout effect, so MuxPlayer is constructed one render AFTER the\n  // flip either way — a `useLayoutEffect` seeds it just as early (measured).\n  // The real constraint is the SOURCE: `resumeMode` collapses in this very\n  // render, so `VideoHoverPreviewSurface` re-keys its inner <Video> and the\n  // resume player is destroyed in this commit. Its `useImperativeHandle`\n  // cleanup nulls `resumeHandleRef` in the mutation phase, i.e. BEFORE any\n  // layout effect here can run. Render is the last point at which the live\n  // mini-player is readable at all — everything that does NOT read it moved\n  // to `useLayoutEffect` below.\n  const [prevOpen, setPrevOpen] = useState(open);\n  if (open !== prevOpen) {\n    setPrevOpen(open);\n    // RISING edge only: the paused deep-link session ends when the NEXT\n    // (gesture- or host-driven) open mounts a fresh theater — this re-render\n    // commits the cleared flag before that content constructs, so it\n    // autoplays as usual. NEVER clear on close: Radix keeps the closing\n    // theater mounted through its exit animation, and flipping the flag then\n    // flips `autoPlayUnmuted` false→true on the STILL-MOUNTED player, whose\n    // autoplay kick effect resurrects playback — audible after close and\n    // doubled against the card's resume player. Idempotent, so a\n    // discarded-and-retried render attempt decides the same way.\n    if (open && pausedOpenSession) setPausedOpenSession(false);\n    // Compared, not cleared: React can discard and re-run a render attempt\n    // (concurrent interruption, error retry, a host wrapping setOpen in\n    // startTransition). A read-and-clear would decide differently on the\n    // retry; an equality test gives the same answer every time.\n    const selfDriven = selfDrivenFor === open;\n    if (openProp !== undefined && !selfDriven) {\n      if (open) {\n        // A host open has no originating gesture, but resume mode DOES have a\n        // live element — reading only `handoff` discarded however long the mini\n        // player had been running since the close. Mirrors openTheater's seed.\n        // `userMuted` (state), not `userMutedRef`: the two are kept in lockstep\n        // and a ref must not be read during render.\n        setTheaterStart(theaterSeedFromLive(resumeHandleRef.current, handoff?.time ?? 0, userMuted));\n        setHovered(false);\n        setSuspended(false);\n        setHandoff(null);\n      } else if (isYouTube) {\n        // The file theater's falling edge needs the live player and lives in\n        // the layout effect below; YouTube only flips a flag, so it stays here\n        // where the still-mounted iframe sees it in this same commit.\n        setSuspended(true);\n      }\n    }\n  }\n\n  // The half of the host-driven sync that does NOT have to be in render.\n  // `useLayoutEffect`, not `useEffect`: on the falling edge the theater player\n  // is still mounted during this commit (Radix `Presence` only dispatches\n  // UNMOUNT from its own layout effect, so the Content is torn down in the\n  // NEXT render) — a passive effect would run after that teardown and the\n  // snapshot would read a dead handle. Verified, not assumed.\n  //\n  // Its own ref latch rather than `prevOpen`: that state has already been\n  // advanced by the render above by the time this runs. Both latches compare\n  // rather than clear, so a discarded render attempt changes nothing.\n  const prevOpenCommittedRef = useRef(open);\n  useLayoutEffect(() => {\n    if (prevOpenCommittedRef.current === open) return;\n    prevOpenCommittedRef.current = open;\n    if (open) {\n      // Cleared before the theater Content is constructed (next render), so\n      // `onEnded` from the previous session can never leak into this one.\n      // Out of render because a ref write is a side effect: a discarded\n      // render attempt used to clear a latch for a session never committed.\n      // Unconditional (not gated on `selfDriven` the way the seed is): every\n      // other rising edge already cleared it in `openTheater`, so this is a\n      // no-op there rather than a second policy.\n      endedLatchRef.current = false;\n      return;\n    }\n    if (openProp === undefined || selfDrivenFor === open || isYouTube) return;\n    // Falling edge: the host bypassed handleOpenChange, so without this the\n    // position, the mute intent and the ended state are all simply lost.\n    const h = theaterHandleRef.current;\n    if (!h) return;\n    const time = h.getCurrentTime();\n    const muted = h.getMuted();\n    const paused = h.getPaused();\n    const atEnd = isAtEnd(h.getDuration(), time);\n    try {\n      h.pause();\n    } catch {\n      /* already gone */\n    }\n    if (!(muted && theaterForcedMuteRef.current && !userMutedRef.current)) {\n      userMutedRef.current = muted;\n      setUserMuted(muted);\n    }\n    setHandoff((endedLatchRef.current && paused && atEnd) || time < 1 ? null : { time, muted, playing: !paused });\n    // `selfDrivenFor` is a dep only so the closure is current; a change to it\n    // alone cannot re-run the body — the latch above takes the early exit.\n  }, [open, openProp, isYouTube, selfDrivenFor, setHandoff]);\n\n  const handleOpenChange = useCallback(\n    (next: boolean) => {\n      if (next) {\n        openTheater();\n        return;\n      }\n      // This close IS the card's own — a controlled host will flip `open` in\n      // response, and the sync above must not re-snapshot the player we are\n      // about to pause (it would read paused=true and downgrade a playing\n      // handoff to paused).\n      setSelfDrivenFor(false);\n      // Closing: snapshot + stop the theater player BEFORE the exit animation.\n      if (isYouTube) {\n        setSuspended(true);\n        commitOpen(false);\n        return;\n      }\n      const h = theaterHandleRef.current;\n      if (h) {\n        const time = h.getCurrentTime();\n        const muted = h.getMuted();\n        // The theater's own chrome can mute/unmute; that IS user intent, so it\n        // must flow back or reopening would contradict what they just did.\n        // EXCEPT when the element is muted only because unmuted autoplay was\n        // rejected: promoting that into intent silently muted every later hover\n        // preview and every later theater open, for the whole session, with no\n        // gesture from the user. Mirrors the same test `openTheater` already\n        // applies in the opposite direction.\n        const policyForcedMute = muted && theaterForcedMuteRef.current && !userMutedRef.current;\n        if (!policyForcedMute) {\n          userMutedRef.current = muted;\n          setUserMuted(muted);\n        }\n        const paused = h.getPaused();\n        const duration = h.getDuration();\n        const atEnd = isAtEnd(duration, time);\n        h.pause();\n        if (!paused) {\n          // Live playing snapshot always wins (beats a stale ended latch).\n          setHandoff({ time, muted, playing: true });\n        } else if (endedLatchRef.current && atEnd) {\n          setHandoff(null); // finished — plain poster, no continuation\n        } else if (time < 1) {\n          setHandoff(null); // degenerate close — keep hover preview enabled\n        } else {\n          setHandoff({ time, muted, playing: false });\n        }\n      }\n      commitOpen(false);\n    },\n    [isYouTube, commitOpen, openTheater],\n  );\n\n  const onCardFallbackChange = useCallback(\n    (s: VideoMutedFallbackState) => {\n      setCardFallback(s);\n      setCardMuted(s.muted); // sync the toggle with fallback / chrome unmutes\n    },\n    [setCardFallback, setCardMuted],\n  );\n\n  // Seed transport state whenever a card player (re)starts. Reads the handoff\n  // through a ref: it is the value AT START, and as a dependency it would\n  // re-seed the toggles every time the host refreshed the object mid-playback.\n  const handoffRef = useRef(handoff);\n  useEffect(() => {\n    handoffRef.current = handoff;\n  });\n  useEffect(() => {\n    if (resumeMode) {\n      setCardMuted(handoffRef.current?.muted ?? false);\n      setCardPaused(handoffRef.current?.playing === false);\n    }\n  }, [resumeMode]);\n  // A fresh hover starts a fresh preview — clear the paused flag, and re-seed\n  // the mute toggle from the user's standing intent so an explicit mute is not\n  // silently undone by the hover-start path (which unmutes the element).\n  //\n  // Adjusted while rendering rather than from an effect, so the toggles are\n  // already correct in the render that mounts the preview player instead of\n  // being corrected on the pass after it. Reads the `userMuted` STATE, not\n  // `userMutedRef` — a ref must not be read during render, and the two are kept\n  // in lockstep (every write to the ref sets the state on the next line).\n  const [hoverSeeded, setHoverSeeded] = useState(hovered);\n  if (hoverSeeded !== hovered) {\n    setHoverSeeded(hovered);\n    if (hovered) {\n      setCardPaused(false);\n      setCardMuted(userMuted);\n    }\n  }\n\n  const dismiss = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation();\n      pausePreviewNow();\n      if (dismissEnabled) dismissWalkthrough(storageKey, dismissalId);\n      setDismissed(true);\n    },\n    [dismissEnabled, storageKey, dismissalId, pausePreviewNow],\n  );\n\n  // Card controls act on whichever player is live (resume continuation or the\n  // hover preview). The internal glyph is unreachable under the activation\n  // overlay (hideMutedBadge), so these are the only affordances.\n  const activeHandle = useCallback(\n    () => (resumeMode ? resumeHandleRef.current : previewHandleRef.current),\n    [resumeMode],\n  );\n\n  /** Big centered glyph — the muted-fallback prompt (unmute, or play when even\n   *  muted autoplay was blocked). Unchanged bite grammar. */\n  const onUnmuteOrPlay = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation();\n      const h = activeHandle();\n      if (!h) return;\n      try {\n        // Branch on the LABEL this button is currently rendering, so the action\n        // always matches the words. Guarding the unmute on `!userMutedRef.current`\n        // (the previous attempt) turned the \"Unmute\" case into a no-op — and\n        // because the corner mute toggle hides while this glyph is up, that left\n        // the card with NO way to restore sound. A control that hides itself on\n        // use is a dead end; see the state-model note at the top of this file.\n        // Recomputed here, NOT read from the render-scoped `controlIsPlay`\n        // (declared below this callback — reading it would repeat the stale\n        // closure bug that silently restarted the theater at 0:00).\n        const actionIsPlay = cardFallback.blocked || cardPaused;\n        if (actionIsPlay) {\n          void h.play();\n          setCardPaused(false);\n        } else {\n          h.setMuted(false);\n          setCardMuted(false);\n          userMutedRef.current = false;\n          setUserMuted(false);\n          void h.play();\n          setCardPaused(false);\n        }\n      } catch {\n        /* ignore */\n      }\n    },\n    [cardFallback.blocked, cardPaused, activeHandle, setCardPaused, setCardMuted, setUserMuted],\n  );\n\n  /** Mute TOGGLE — stays on screen in both states so the user can come back. */\n  const onToggleMute = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation();\n      const h = activeHandle();\n      if (!h) return;\n      // Live element, not the `cardMuted` snapshot: the module-level activation\n      // waiter can unmute between this button's pointerdown and its click, which\n      // made the first press of a button labelled \"Unmute\" mute instead.\n      // Read the LIVE element, never `cardMuted`: the two diverge whenever the\n      // player's own chrome changed mute without telling us. (`getMuted()`\n      // returns `false` rather than nullish on a torn-down element, so a `??`\n      // fallback here would silently invert the button — hence the null guard\n      // above rather than a default.)\n      const next = !h.getMuted();\n      try {\n        h.setMuted(next);\n        if (!next && !cardPaused) void h.play(); // never override an explicit pause\n        setCardMuted(next);\n        userMutedRef.current = next;\n        setUserMuted(next);\n      } catch {\n        /* ignore */\n      }\n    },\n    [cardPaused, activeHandle, setCardMuted, setUserMuted],\n  );\n\n  /** Play/pause TOGGLE. Deliberately does NOT tear down the resume player\n   *  (an earlier version cleared the handoff, which unmounted the mini player\n   *  and made the controls vanish with no way back). */\n  const onTogglePlay = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation();\n      const h = activeHandle();\n      if (!h) return;\n      try {\n        // Same predicate the label renders from (`controlIsPlay`). Branching on\n        // raw `cardPaused` meant that in the blocked state the button read \"Play\"\n        // but took the pause branch, so the first press did nothing.\n        if (cardFallback.blocked || cardPaused) {\n          void h.play();\n          setCardPaused(false);\n        } else {\n          h.pause();\n          setCardPaused(true);\n        }\n      } catch {\n        /* ignore */\n      }\n    },\n    [cardFallback.blocked, cardPaused, activeHandle, setCardPaused],\n  );\n\n  const summaryTitle = video?.title || undefined;\n\n  // Already proxied by the host (the hub's DAL applies the prefix AND its\n  // skip-list). Threading a prefix through the widget meant each host restated\n  // it and silently dropped the skip-list.\n  const presenterAvatarSrc = video?.presenterAvatarUrl ?? null;\n\n  const embedUrlKey = useMemo(\n    () => `${video?.mainVideoUrl ?? ''}|${video?.youtubeUrl ?? ''}`,\n    [video?.mainVideoUrl, video?.youtubeUrl],\n  );\n\n  // Gate on `!resumeMode`, NOT `!handoff`: a non-playing handoff is only a\n  // remembered timestamp. Gating on `handoff` froze the card as an inert poster\n  // after closing a paused theater (no hover preview, no transport, no way back\n  // except reopening) for the rest of the session.\n  const cardActive = !open && previewAllowed && hovered && !resumeMode;\n\n  // ONE derived mode drives every branch below. Deriving a single discriminant\n  // (instead of testing raw booleans at each call site) is what keeps the card\n  // consistent: 'resume' and 'preview' are mutually exclusive by construction,\n  // so a player can never be half-mounted or a control half-shown. Derived\n  // ABOVE the early return so the latest-ref effect below it stays\n  // unconditional.\n  const cardMode: 'resume' | 'preview' | 'poster' = resumeMode ? 'resume' : cardActive ? 'preview' : 'poster';\n\n  // Latest-ref mirror of `cardMode`, read only by `openTheater` (an event\n  // handler) to decide which live player it is handing off from. Refreshed in\n  // an unconditional effect rather than assigned during render: a render\n  // attempt React discards (concurrent interruption, error retry) must not\n  // leave the ref describing a card layout that was never committed, or the\n  // next click would hand off from a player that is not on screen.\n  useEffect(() => {\n    cardModeRef.current = cardMode;\n  });\n\n  if (!video || (!video.mainVideoUrl && !video.youtubeUrl)) return null;\n\n  // The collapsed card is suppressed by the appear-delay mount gate and by\n  // dismissal — but the THEATER (Dialog) is NOT. A host that controls\n  // `open={true}` must be able to force the theater even before the card has\n  // appeared or after it was dismissed, so the gate lives on the card only.\n  const showCard = mounted && !dismissed;\n\n  // `cardMode === 'resume'` is exactly `resumeMode`, which already required\n  // `handoff !== null` — but the derived string discriminant carries none of\n  // that for the compiler. Re-derive the handoff once so the resume props\n  // below read it directly instead of asserting it back into existence.\n  const resumeHandoff = resumeMode ? handoff : null;\n\n  // Big centered glyph = the muted-fallback prompt (bite grammar), nothing else.\n  // Gated on an actual player: `cardFallback` is only ever rewritten by a\n  // mounted one, so after it unmounts (e.g. the resume clip ended) a stale\n  // `muted:true` left an orphan glyph that played from 0 with sound and no\n  // transport controls.\n  const showBigUnmute = cardFallback.muted && cardMode !== 'poster';\n  // \"Play\" whenever the action will start playback: autoplay was blocked, or\n  // the user paused. Otherwise the label understated what the button does.\n  const controlIsPlay = cardFallback.blocked || cardPaused;\n  // Transport toggles stay visible for as long as a card player is mounted —\n  // in BOTH states of each toggle, so muting/pausing is always reversible.\n  const showPlaybackControls = cardMode !== 'poster';\n\n  // --- collapsed card (div root + overlay button + sibling X/unmute controls) ---\n  const collapsed = (\n    <div\n      className={cn(\n        // `group/card` is the SHARED hover contract: VideoHoverPreviewSurface's\n        // badge accents off it. It must live on an element that actually\n        // receives pointer events — the media layer is pointer-events-none, so\n        // a group inside it can never match :hover.\n        'group/card pointer-events-auto relative overflow-hidden rounded-md border border-ods-border bg-ods-card',\n        // The lift is what separates a FLOATING card from the page beneath it;\n        // an in-flow block sitting inside a host card needs no shadow at all.\n        !inline && 'shadow-2xl',\n        // MOBILE = PiP dimensions, not a shrunken desktop card. This is an\n        // UNINVITED overlay on the smallest screens, so it is sized against\n        // the corner-PiP conventions, NOT against system PiP (which is\n        // user-invoked content and is allowed ~60% of the shortest edge):\n        //   - AOSP's legacy corner spec asks for 23% of screen width, then\n        //     clamps up to `default_minimal_size_pip_resizable_task` (108dp in\n        //     BOTH dimensions) — at 16:9 that floor IS 192x108, and on any\n        //     modern phone the clamp is what actually applies. Hence the cap.\n        //   - 192x108 lands at ~6% of a 390x844 screen, inside the \"small\n        //     player\" band (<=20% of screen) rather than the \"large\" one.\n        // 52vw keeps it proportional on narrow devices (320px -> 166x93) and\n        // the 192px cap takes over from ~369px up, i.e. on most phones.\n        // Do NOT go below ~150px: the corner chrome (32px transport pair +\n        // 32px dismiss + the title pill) stops fitting, and the card's own\n        // controls — not the poster — are what set the real floor here.\n        'aspect-video transition-opacity duration-200',\n        // Inline: the HOST owns the width — the card only keeps 16:9 within it.\n        inline ? 'w-full' : 'w-[min(52vw,192px)] sm:w-80',\n        footerHidden ? 'pointer-events-none opacity-0' : 'opacity-100',\n        className,\n      )}\n      // `mouse` only: pointerenter also fires for touch, which started a\n      // preview on every tap. Focus mirrors hover so keyboard users can reach\n      // the transport controls at all.\n      onPointerEnter={e => {\n        if (e.pointerType === 'mouse' && previewAllowed && !resumeMode) setHovered(true);\n      }}\n      onPointerLeave={e => {\n        if (e.pointerType === 'mouse') setHovered(false);\n      }}\n      // Re-arm hover after a close that left the pointer parked on the card.\n      onPointerMove={e => {\n        if (e.pointerType === 'mouse' && previewAllowed && !resumeMode && !hovered) setHovered(true);\n      }}\n      // Keyboard intent only. Chrome focuses buttons on click, so a raw focus\n      // mirror autostarted an unmuted preview with the pointer nowhere near\n      // the card. `:focus-visible` alone is NOT enough: Radix restores focus\n      // to the hit layer on close, and after an Escape-close that restored\n      // focus IS :focus-visible — the guard admitted precisely the case it was\n      // written to block, so closing with Escape restarted audio in the\n      // corner. Hence the explicit just-closed latch.\n      onFocusCapture={e => {\n        const el = e.target as HTMLElement;\n        // `matches` THROWS SyntaxError on engines that don't know\n        // :focus-visible (Safari < 15.4) — that would escape a React handler.\n        let keyboard = false;\n        try {\n          keyboard = el.matches?.(':focus-visible') ?? false;\n        } catch {\n          keyboard = false;\n        }\n        // Consume the latch: the FIRST focus after a close is Radix returning\n        // focus to this element, never the user arriving at it.\n        if (justClosedRef.current) {\n          justClosedRef.current = false;\n          if (justClosedTimerRef.current) {\n            clearTimeout(justClosedTimerRef.current);\n            justClosedTimerRef.current = null;\n          }\n          return;\n        }\n        if (previewAllowed && !resumeMode && keyboard) setHovered(true);\n      }}\n      onBlurCapture={e => {\n        if (!e.currentTarget.contains(e.relatedTarget)) setHovered(false);\n      }}\n    >\n      {/* Media layer is CLICK-TRANSPARENT: a paused/idle player must never\n          swallow the click. Every click that isn't on a control falls through\n          to the activation overlay below and opens the theater. */}\n      <div className=\"pointer-events-none absolute inset-0\">\n        {/* ONE player component for BOTH modes. The card used to render a\n            bespoke resume player next to the shared preview surface, and that\n            duplication is exactly why desktop (preview) and mobile (poster/\n            resume) diverged. Same component, same props, one behaviour. */}\n        <VideoHoverPreviewSurface\n          key={`card-${embedUrlKey}`}\n          // File player only. The theater resolves YouTube-wins; the card must\n          // not mount MuxPlayer on a youtube.com URL (it renders the poster).\n          url={video.mainVideoUrl || ''}\n          posterUrl={video.posterUrl}\n          active={cardActive}\n          isClone={false}\n          badge=\"play\"\n          fit=\"cover\"\n          // Mirrors the card's own responsive width above — a flat 320px hint\n          // made every phone fetch a poster ~1.7x wider than the box.\n          posterSizes=\"(max-width: 639px) 192px, 320px\"\n          preload=\"none\"\n          hideMutedBadge\n          onMutedFallbackChange={onCardFallbackChange}\n          previewHandleRef={cardMode === 'resume' ? resumeHandleRef : previewHandleRef}\n          mutedIntent={userMuted}\n          continuation={cardMode === 'resume'}\n          // `!footerHidden`: mounting with autoplay and pausing in an effect\n          // loses to MuxPlayer re-issuing play when media is ready, which left\n          // audio running behind an invisible, pointer-inert card.\n          autoPlay={cardMode === 'resume' && handoff?.playing !== false && !footerHidden}\n          startTime={resumeHandoff?.time}\n          startMuted={resumeHandoff?.muted ?? false}\n          onEnded={cardMode === 'resume' ? () => setHandoff(null) : () => setCardPaused(true)}\n        />\n      </div>\n\n      {/* CONTENTLESS full-card hit layer. Deliberately a bare <button>, NOT the\n          DS <Button>: that component is an inline-flex, content-sized,\n          centre-aligned control, so as a full-bleed layer it collapsed around\n          its child — which dragged the title pill to the top-left (overlapping\n          the transport) and left most of the card unclickable. A hit layer has\n          no content and no typography; separating it from the label is what\n          makes both positions deterministic. */}\n      <CardHitLayer\n        label={video.title ? `${label}: ${video.title}` : label}\n        onPointerDown={pausePreviewNow}\n        onClick={openTheater}\n        className=\"z-20\"\n      />\n\n      {/* Title pill — its own positioned element (bottom-left), never a child\n          of the hit layer. pointer-events-none so clicks fall through to it. */}\n      <span className=\"pointer-events-none absolute bottom-[var(--spacing-system-xsf)] left-[var(--spacing-system-xsf)] z-20 flex max-w-[calc(100%-2*var(--spacing-system-xsf))] items-center gap-[var(--spacing-system-xsf)] rounded-full bg-ods-overlay py-[var(--spacing-system-xxs)] pl-[var(--spacing-system-xxs)] pr-[var(--spacing-system-sf)] text-ods-text-primary text-h6\">\n        {presenterAvatarSrc ? (\n          <SquareAvatar src={presenterAvatarSrc} alt=\"\" sizePx={20} variant=\"round\" className=\"shrink-0 border-0\" />\n        ) : (\n          <VideoPlayBadge size=\"sm\" className=\"h-5 w-5 shrink-0\" />\n        )}\n        {/* The video's OWN title is the point of the pill; `label` is only the\n            generic fallback for a video that carries no title. */}\n        <span className=\"truncate\">{summaryTitle || label}</span>\n      </span>\n\n      {/* BIG centred glyph — the muted-fallback prompt (bite grammar). It IS\n          interactive, but ONLY exists while the card is muted, so the two\n          intents never compete: muted → this button unmutes; once unmuted it\n          unmounts and the same click lands on the hit layer and opens the\n          theater. `size=\"icon-glyph\"` keeps the glyph at its own 56px instead\n          of the DS default `[&_svg]:h-5`; `variant=\"glyph\"` means no button\n          surface paints behind a glyph that already carries its own scrim.\n\n          BELOW `sm` IT IS NOT RENDERED — see the corner mute button's\n          mirrored `sm:hidden` below; the two form ONE breakpoint switch and\n          must always be edited together. At the mobile PiP height (~108px)\n          the 56px glyph physically cannot clear both the 32px transport band\n          and the title pill: the three need ~124px of vertical stack, which\n          16:9 does not give back until the card is ~224px wide — i.e. barely\n          smaller than the oversized card this sizing exists to fix. Nothing\n          is lost, because the ONLY state that shows this glyph is resume mode\n          (`cardMode !== 'poster'`), and resume mode always mounts the corner\n          transport pair, which offers the same play AND unmute actions at\n          32px — above the 24px WCAG 2.5.8 (AA) target floor. */}\n      {showBigUnmute && (\n        <Button\n          variant=\"glyph\"\n          size=\"icon-glyph\"\n          aria-label={controlIsPlay ? 'Play' : 'Unmute'}\n          title={controlIsPlay ? 'Play' : 'Unmute'}\n          onPointerDown={e => e.stopPropagation()}\n          onClick={onUnmuteOrPlay}\n          className=\"absolute inset-0 z-30 m-auto hidden sm:inline-flex\"\n        >\n          {controlIsPlay ? <VideoPlayBadge /> : <VideoUnmuteGlyph />}\n        </Button>\n      )}\n\n      {/* Transport toggles — mute/unmute + play/pause. Rendered in BOTH states\n          (never self-hiding) so every action is reversible.\n          CORNER MAP (each control owns exactly one corner, nothing overlaps):\n            top-left = transport   top-right = dismiss\n            bottom-left = title pill (presenter avatar lives INSIDE it) */}\n      {showPlaybackControls && (\n        <div className=\"absolute left-[var(--spacing-system-xsf)] top-[var(--spacing-system-xsf)] z-30 flex items-center gap-[var(--spacing-system-xxs)]\">\n          {/* Hidden while the big centre glyph owns unmuting, so the two are\n              never on screen with the same label and different behaviour —\n              but ONLY from `sm` up, because below `sm` that glyph is not\n              rendered at all (see its comment above). `sm:hidden`, not\n              `hidden`: a plain `hidden` here would combine with the glyph's\n              own `hidden sm:inline-flex` to leave the mobile card with NO\n              unmute affordance whatsoever — precisely the self-hiding dead\n              end called out in the state-model note at the top of this file.\n              These two classNames are ONE switch; never change one alone. */}\n          <Button\n            variant=\"overlay\"\n            size=\"icon-sm\"\n            aria-label={cardMuted ? 'Unmute' : 'Mute'}\n            title={cardMuted ? 'Unmute' : 'Mute'}\n            onPointerDown={e => e.stopPropagation()}\n            onClick={onToggleMute}\n            className={cn(showBigUnmute && !controlIsPlay && 'sm:hidden')}\n          >\n            {cardMuted ? <VolumeXmarkIcon /> : <VolumeUpIcon />}\n          </Button>\n          <Button\n            variant=\"overlay\"\n            size=\"icon-sm\"\n            aria-label={controlIsPlay ? 'Play' : 'Pause'}\n            title={controlIsPlay ? 'Play' : 'Pause'}\n            onPointerDown={e => e.stopPropagation()}\n            onClick={onTogglePlay}\n          >\n            {controlIsPlay ? <PlayIcon /> : <PauseIcon />}\n          </Button>\n        </div>\n      )}\n\n      {/* dismiss (highest) */}\n      {dismissEnabled && (\n        <Button\n          variant=\"overlay\"\n          size=\"icon-sm\"\n          aria-label=\"Dismiss video\"\n          onPointerDown={pausePreviewNow}\n          onClick={dismiss}\n          className=\"absolute right-[var(--spacing-system-xsf)] top-[var(--spacing-system-xsf)] z-40\"\n        >\n          <XmarkIcon />\n        </Button>\n      )}\n    </div>\n  );\n\n  return (\n    <>\n      {showCard &&\n        (inline ? (\n          // No wrapper: a positioning / z-index / safe-area shell around an\n          // in-flow block is exactly what stops it sizing to its container.\n          collapsed\n        ) : (\n          <div\n            className={cn(\n              'pointer-events-none fixed bottom-0 p-[var(--spacing-system-mf)]',\n              video.position === 'right' ? 'right-0' : 'left-0',\n              WALKTHROUGH_Z,\n            )}\n            style={{ paddingBottom: 'max(var(--spacing-system-mf), env(safe-area-inset-bottom))' }}\n          >\n            {collapsed}\n          </div>\n        ))}\n\n      <DialogPrimitive.Root open={open} onOpenChange={handleOpenChange}>\n        <DialogPortal>\n          <DialogOverlay />\n          <DialogPrimitive.Content\n            onOpenAutoFocus={e => {\n              // Prevent Radix's default (first focusable) and move focus to the\n              // close button so keyboard/AT users land inside the dialog.\n              e.preventDefault();\n              closeButtonRef.current?.focus();\n            }}\n            className={cn(\n              // PURE theater: just the video, centered on a dimmed backdrop.\n              // No card chrome, no padding, no summary — the 16:9 stage IS the\n              // dialog, so nothing competes with the video at any breakpoint.\n              // `158svh` is the width at which a 16:9 stage is exactly 89svh\n              // tall, so the height bound is expressed as a WIDTH cap — the\n              // stage keeps its aspect ratio and simply gets narrower on short\n              // viewports, instead of overflowing a fixed centred box that\n              // clips equally off the top and bottom (which put the close\n              // button off-screen on 1366x768 laptops and landscape phones).\n              // Two declarations on purpose: an engine without `svh` (Safari\n              // < 15.4) treats the whole min() as invalid, and on a `fixed`\n              // element that falls back to width:auto and the stage collapses.\n              // The first rule is the safe floor; the second wins where svh\n              // parses.\n              'fixed left-1/2 top-1/2 z-[9999] max-w-none -translate-x-1/2 -translate-y-1/2',\n              'w-[min(92vw,1400px)] w-[min(92vw,1400px,158svh)]',\n              'focus:outline-none',\n            )}\n          >\n            <DialogPrimitive.Title className=\"sr-only\">{summaryTitle || 'Walkthrough video'}</DialogPrimitive.Title>\n            <Video\n              kind={video.youtubeUrl ? 'youtube' : 'file'}\n              url={(video.youtubeUrl || video.mainVideoUrl) as string}\n              poster={video.posterUrl}\n              captionsUrl={video.captionsUrl}\n              title={summaryTitle}\n              layout=\"wide\"\n              startTime={theaterStart.time}\n              playerHandleRef={theaterHandleRef}\n              // Muted still means PLAYING: without this a deliberately muted\n              // card opened a paused theater, breaking parity with the\n              // unmuted path. EXCEPT the paused deep-link session, which\n              // starts fully stopped: no autoplay flag at all, and for\n              // YouTube no autoActivate either — the activated iframe\n              // hardcodes `autoplay=1`, so staying on the facade IS the\n              // paused presentation (clicking it activates and plays).\n              autoPlay={theaterStart.muted && !pausedOpenSession}\n              autoPlayUnmuted={!theaterStart.muted && !pausedOpenSession}\n              startMuted={theaterStart.muted}\n              autoActivate={!pausedOpenSession}\n              suspended={suspended}\n              onMutedFallbackChange={st => {\n                theaterForcedMuteRef.current = st.muted;\n              }}\n              onEnded={() => {\n                endedLatchRef.current = true;\n              }}\n            />\n            {/* Radix `asChild` so the shared Button IS the close trigger (same\n                pattern as every other dialog close in the lib). */}\n            <DialogPrimitive.Close asChild>\n              <Button\n                ref={closeButtonRef}\n                variant=\"overlay\"\n                size=\"icon-sm\"\n                aria-label=\"Close\"\n                className=\"absolute right-[var(--spacing-system-sf)] top-[var(--spacing-system-sf)] z-10\"\n              >\n                <XmarkIcon />\n              </Button>\n            </DialogPrimitive.Close>\n          </DialogPrimitive.Content>\n        </DialogPortal>\n      </DialogPrimitive.Root>\n    </>\n  );\n}\n\n/**\n * The widget pinned into a bottom corner over the page — the app-shell / site\n * placement. Every host that wants \"a demo video somewhere on the page, out of\n * the way\" mounts this.\n */\nexport function FloatingWalkthroughVideo(props: FloatingWalkthroughVideoProps): React.ReactElement {\n  return <WalkthroughVideo {...props} placement=\"floating\" />;\n}\n\n/**\n * The same widget as an in-flow block, sized by whatever the host puts it in —\n * for a page that gives the video a slot of its own rather than a corner.\n *\n * The two overlay defaults are fixed here rather than left to the caller: an\n * in-page block has nothing to stagger against (`appearDelayMs={0}`), and a\n * dismissable one would leave the host's layout with a hole while sharing the\n * floating card's per-platform cookie (`dismissal={false}`). Both are stated\n * once here instead of at every call site, where forgetting either is silent.\n */\nexport function InlineWalkthroughVideo(props: InlineWalkthroughVideoProps): React.ReactElement {\n  return <WalkthroughVideo {...props} placement=\"inline\" appearDelayMs={0} dismissal={false} />;\n}\n","'use client';\n\n/**\n * useWalkthroughVideo — client read hook for the per-platform walkthrough video.\n *\n * SSOT for fetching + shaping the widget's data on the CLIENT (any embedder\n * reuses it). The public endpoint returns the RAW body `{ walkthroughVideo }`\n * (no {success,data} wrapper). The video's `captionsUrl` is a RELATIVE\n * `/api/captions/...` path; it is rebased automatically onto\n * `ChatRuntime.endpoints.captionsUrlPrefix` (the standard runtime-endpoint\n * wiring) — same-origin hosts leave that unset and get the URL unchanged.\n * `transformCaptionsUrl` remains as an explicit override for hosts outside\n * a runtime provider.\n *\n * SSR hosts (the hub) resolve the video server-side and pass it to the widget\n * directly; they don't need this hook. It exists for client-only embedders.\n */\n\nimport { useQuery } from '@tanstack/react-query';\nimport { useCaptions } from './use-captions';\nimport type { WalkthroughVideoData } from './walkthrough-video';\n\nexport interface UseWalkthroughVideoOptions {\n  /** Absolute or proxied URL of the public GET route. */\n  endpoint: string;\n  /** SSR/initial seed to avoid a loading flash. */\n  initialData?: WalkthroughVideoData | null;\n  enabled?: boolean;\n  /** Rewrite the RELATIVE captionsUrl (e.g. prefix a `/content` proxy base).\n   *  OPTIONAL override — when unset, the URL is rebased onto\n   *  `ChatRuntime.endpoints.captionsUrlPrefix` (see `rebaseCaptionsUrl`), so\n   *  embedders inside a runtime provider need no wiring here at all. */\n  transformCaptionsUrl?: (relativeUrl: string) => string;\n}\n\nexport interface UseWalkthroughVideoResult {\n  video: WalkthroughVideoData | null;\n  isLoading: boolean;\n}\n\nexport function useWalkthroughVideo(opts: UseWalkthroughVideoOptions): UseWalkthroughVideoResult {\n  const { endpoint, initialData, enabled = true, transformCaptionsUrl } = opts;\n  const captions = useCaptions();\n\n  const query = useQuery<WalkthroughVideoData | null>({\n    queryKey: ['walkthrough-video', endpoint],\n    enabled,\n    initialData: initialData ?? undefined,\n    // `null` is a legitimate cached VALUE to React Query, so an embedder\n    // passing `initialData={null}` to mean \"no seed\" got status:'success' and\n    // no fetch for the whole staleTime window. Treat null as absent.\n    staleTime: 5 * 60 * 1000,\n    // Returns the RAW video, cached under the endpoint key. A real HTTP\n    // failure THROWS so React Query retries and surfaces isError, instead of\n    // caching a fake \"no video\" for the whole stale window. The hub route\n    // answers \"no walkthrough for this platform\" as 200 + null, not 404; the\n    // 404 arm is kept for embedders whose proxy 404s a missing resource.\n    queryFn: async () => {\n      const res = await fetch(endpoint);\n      if (res.status === 404) return null;\n      if (!res.ok) throw new Error(`Walkthrough video request failed (${res.status})`);\n      const body = (await res.json()) as { walkthroughVideo: WalkthroughVideoData | null };\n      return body?.walkthroughVideo ?? null;\n    },\n    // Per-observer transform — NEVER mutate the cached data. `select` also runs\n    // over `initialData`, so the SSR seed is rewritten too. Keeping the proxy\n    // rewrite here (not in queryFn) means a second consumer of the same\n    // endpoint can't inherit this observer's proxied captionsUrl from the cache.\n    select: video => {\n      if (!video?.captionsUrl) return video;\n      if (transformCaptionsUrl && video.captionsUrl.startsWith('/')) {\n        return { ...video, captionsUrl: transformCaptionsUrl(video.captionsUrl) };\n      }\n      // Default: rebase onto the configured captions base. Same-origin hosts\n      // leave it unset and get the URL back unchanged — a no-op for them.\n      const rebased = captions.rebase(video.captionsUrl);\n      return rebased === video.captionsUrl ? video : { ...video, captionsUrl: rebased };\n    },\n  });\n\n  return { video: query.data ?? null, isLoading: query.isLoading };\n}\n","'use client';\n\nimport { Upload, X, Video as VideoIcon } from 'lucide-react';\nimport type React from 'react';\nimport { useState, useCallback } from 'react';\nimport { YouTubeIcon } from '../icons/youtube-icon';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Button } from '../ui/button';\nimport { Input } from '../ui/input';\nimport { Label } from '../ui/label';\nimport { Video } from './video';\n\nexport type VideoSourceType = 'youtube' | 'uploaded';\n\nexport interface VideoSourceSelectorProps {\n  /** Current video source type selection */\n  videoSourceType: VideoSourceType;\n  /** Callback when video source type changes */\n  onVideoSourceTypeChange: (type: VideoSourceType) => void;\n  /** YouTube URL value */\n  youtubeUrl: string;\n  /** Callback when YouTube URL changes */\n  onYoutubeUrlChange: (url: string) => void;\n  /** Uploaded video URL value */\n  mainVideoUrl: string;\n  /** Callback when uploaded video URL changes */\n  /**\n   * Fired with the uploaded/typed URL. May return a promise — the upload\n   * spinner stays up until it settles, so post-upload work (persisting the\n   * URL, capturing + persisting the poster) finishes BEFORE the loading\n   * state clears. Without this, the modal can be closed mid-capture and the\n   * thumbnail is silently lost.\n   */\n  onMainVideoUrlChange: (url: string) => void | Promise<void>;\n  /** Callback to handle video upload - receives file and returns URL or throws error */\n  onUploadVideo: (file: File, onProgress?: (progress: number) => void) => Promise<string>;\n  /** Optional: Show AI generated badge on uploaded video */\n  showAIBadge?: boolean;\n  /** Optional: Whether the video was AI generated */\n  isAIGenerated?: boolean;\n  /** Optional: Custom label for YouTube section */\n  youtubeLabel?: string;\n  /** Optional: Custom label for upload section */\n  uploadLabel?: string;\n  /** Optional: Custom placeholder for YouTube URL input */\n  youtubePlaceholder?: string;\n  /** Optional: Custom helper text for YouTube section */\n  youtubeHelperText?: string;\n  /** Optional: Custom empty state text for upload section */\n  uploadEmptyText?: string;\n  /** Optional: Disable the component */\n  disabled?: boolean;\n  /** Optional: Custom video preview component */\n  VideoPreviewComponent?: React.ComponentType<{\n    videoUrl: string;\n    onDelete: () => void;\n    isAIGenerated?: boolean;\n  }>;\n  /** Optional: Upload progress bar component */\n  UploadProgressComponent?: React.ComponentType<{\n    progress: number;\n    message: string;\n    className?: string;\n  }>;\n  /** Optional: Section title */\n  title?: string;\n  /** Optional: Show section title */\n  showTitle?: boolean;\n  /** Optional: Additional class name */\n  className?: string;\n}\n\n/**\n * VideoSourceSelector - Unified component for selecting between YouTube URL and uploaded video\n *\n * This component provides a toggle between YouTube URL input and video upload,\n * without clearing data when switching between options.\n */\nexport function VideoSourceSelector({\n  videoSourceType,\n  onVideoSourceTypeChange,\n  youtubeUrl,\n  onYoutubeUrlChange,\n  mainVideoUrl,\n  onMainVideoUrlChange,\n  onUploadVideo,\n  showAIBadge = true,\n  isAIGenerated = false,\n  youtubeLabel = 'YouTube Video URL',\n  uploadLabel = 'Uploaded Video',\n  youtubePlaceholder = 'https://youtube.com/watch?v=...',\n  youtubeHelperText = 'YouTube videos will be embedded. AI processing is not available for YouTube links.',\n  uploadEmptyText = 'No video uploaded yet. Click \"Upload Video\" to add one.',\n  disabled = false,\n  VideoPreviewComponent,\n  UploadProgressComponent,\n  title = 'Video',\n  showTitle = true,\n  className = '',\n}: VideoSourceSelectorProps) {\n  const [isUploading, setIsUploading] = useState(false);\n  const [uploadProgress, setUploadProgress] = useState(0);\n  const [uploadMessage, setUploadMessage] = useState('');\n  const [uploadError, setUploadError] = useState<string | null>(null);\n\n  const handleUploadClick = useCallback(() => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = 'video/*';\n    input.onchange = async (e: Event) => {\n      const target = e.target as HTMLInputElement;\n      const file = target.files?.[0];\n      if (!file) return;\n\n      setIsUploading(true);\n      setUploadProgress(0);\n      setUploadMessage('Uploading video...');\n      setUploadError(null);\n\n      try {\n        const url = await onUploadVideo(file, progress => {\n          setUploadProgress(progress);\n        });\n        setUploadProgress(100);\n        setUploadMessage('Upload complete — selecting thumbnail...');\n        // Await the host's attach chain (persist URL -> capture poster ->\n        // persist poster). isUploading holds until it settles, so closing\n        // the modal can no longer race the 15-60s thumbnail capture.\n        await onMainVideoUrlChange(url);\n        setUploadMessage('Video attached!');\n      } catch (err) {\n        setUploadError(err instanceof Error ? err.message : 'Failed to upload video');\n      } finally {\n        setIsUploading(false);\n        setTimeout(() => {\n          setUploadProgress(0);\n          setUploadMessage('');\n        }, 1000);\n      }\n    };\n    input.click();\n  }, [onUploadVideo, onMainVideoUrlChange]);\n\n  const handleDeleteVideo = useCallback(() => {\n    setUploadError(null);\n    // `onMainVideoUrlChange` is declared `void | Promise<void>` and the host's\n    // implementation persists the change. A rejected detach was previously\n    // unhandled, so the card cleared and the user believed the video was\n    // removed when the server still had it — surface it in the same error slot\n    // the upload path uses.\n    void Promise.resolve(onMainVideoUrlChange('')).catch((err: unknown) => {\n      setUploadError(err instanceof Error ? err.message : 'Failed to remove video');\n    });\n  }, [onMainVideoUrlChange]);\n\n  return (\n    <div className={`space-y-4 rounded-lg border border-ods-border bg-ods-card p-6 ${className}`}>\n      {/* Section Title */}\n      {showTitle && (\n        <h3 className=\"flex items-center gap-2 text-ods-text-primary text-h5\">\n          <VideoIcon className=\"h-5 w-5\" />\n          {title}\n        </h3>\n      )}\n\n      {/* Video Source Type Toggle */}\n      <div className=\"flex gap-2\">\n        <Button\n          type=\"button\"\n          variant={videoSourceType === 'youtube' ? 'accent' : 'outline'}\n          onClick={() => onVideoSourceTypeChange('youtube')}\n          leftIcon={<YouTubeIcon className=\"h-4 w-4\" color=\"currentColor\" />}\n          disabled={disabled}\n        >\n          YouTube URL\n        </Button>\n        <Button\n          type=\"button\"\n          variant={videoSourceType === 'uploaded' ? 'accent' : 'outline'}\n          onClick={() => onVideoSourceTypeChange('uploaded')}\n          leftIcon={<Upload className=\"h-4 w-4\" />}\n          disabled={disabled}\n        >\n          Upload Video\n        </Button>\n      </div>\n\n      {/* YouTube URL Input */}\n      {videoSourceType === 'youtube' && (\n        <div className=\"space-y-2\">\n          <Label>{youtubeLabel}</Label>\n          <Input\n            value={youtubeUrl}\n            onChange={e => onYoutubeUrlChange(e.target.value)}\n            placeholder={youtubePlaceholder}\n            className=\"bg-ods-bg\"\n            disabled={disabled}\n          />\n          {youtubeHelperText && <p className=\"text-ods-text-secondary text-h6\">{youtubeHelperText}</p>}\n        </div>\n      )}\n\n      {/* Uploaded Video Section */}\n      {videoSourceType === 'uploaded' && (\n        <div>\n          <div className=\"mb-2 flex items-center justify-between\">\n            <div className=\"flex items-center gap-2\">\n              <Label>{uploadLabel}</Label>\n              {showAIBadge && isAIGenerated && <AIGeneratedBadge />}\n            </div>\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"small-legacy\"\n              leftIcon={<Upload className=\"h-4 w-4\" />}\n              onClick={handleUploadClick}\n              disabled={disabled || isUploading}\n            >\n              Upload Video\n            </Button>\n          </div>\n\n          {/* Upload Progress */}\n          {isUploading && UploadProgressComponent && (\n            <UploadProgressComponent progress={uploadProgress} message={uploadMessage} className=\"mb-3\" />\n          )}\n\n          {/* Simple progress bar fallback if no custom component */}\n          {isUploading && !UploadProgressComponent && (\n            <div className=\"mb-3\">\n              <div className=\"mb-1 flex items-center gap-2\">\n                <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-ods-border\">\n                  <div\n                    className=\"h-full bg-ods-accent transition-all duration-300\"\n                    style={{ width: `${uploadProgress}%` }}\n                  />\n                </div>\n                <span className=\"text-ods-text-secondary text-h6\">{uploadProgress}%</span>\n              </div>\n              <p className=\"text-ods-text-secondary text-h6\">{uploadMessage}</p>\n            </div>\n          )}\n\n          {/* Upload Error */}\n          {uploadError && <p className=\"mb-3 text-ods-error text-h6\">{uploadError}</p>}\n\n          {/* Video Preview */}\n          {mainVideoUrl ? (\n            VideoPreviewComponent ? (\n              <VideoPreviewComponent\n                videoUrl={mainVideoUrl}\n                onDelete={handleDeleteVideo}\n                isAIGenerated={isAIGenerated}\n              />\n            ) : (\n              <DefaultVideoPreview videoUrl={mainVideoUrl} onDelete={handleDeleteVideo} disabled={disabled} />\n            )\n          ) : (\n            <p className=\"italic text-ods-text-secondary text-h6\">{uploadEmptyText}</p>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\ninterface DefaultVideoPreviewProps {\n  /** Uploaded video URL — MP4 or Mux HLS manifest, both playable via <Video>. */\n  videoUrl: string;\n  onDelete: () => void;\n  disabled?: boolean;\n}\n\n/**\n * Default preview for {@link VideoSourceSelector} when no\n * `VideoPreviewComponent` is supplied. Renders through the `<Video>` SSOT\n * (MuxPlayer), which plays Mux HLS manifests directly on every browser —\n * no HLS→MP4 resolve step needed for playback.\n */\nfunction DefaultVideoPreview({ videoUrl, onDelete, disabled }: DefaultVideoPreviewProps) {\n  return (\n    <div className=\"relative overflow-hidden rounded-lg border border-ods-border\">\n      {/* aspect-video box: MuxPlayer fills its container (width/height 100%),\n          so the wrapper must reserve the height the intrinsic-sized native\n          <video> used to provide. */}\n      <div className=\"aspect-video max-h-[300px] w-full\">\n        <Video kind=\"file\" url={videoUrl} />\n      </div>\n      <button\n        type=\"button\"\n        onClick={onDelete}\n        aria-label=\"Delete video\"\n        className=\"absolute right-2 top-2 rounded-full bg-black/60 p-1.5 transition-colors hover:bg-black/80\"\n        disabled={disabled}\n      >\n        <X className=\"h-4 w-4 text-ods-text-on-dark\" />\n      </button>\n    </div>\n  );\n}\n\nexport default VideoSourceSelector;\n","'use client';\n\nimport { StandardCcIcon } from '../icons-v2-generated';\nimport { Badge } from '../ui/badge';\nimport { Label } from '../ui/label';\nimport { Textarea } from '../ui/textarea';\n\nexport interface SubtitlesEditorProps {\n  /** SRT subtitle content */\n  subtitles?: string;\n  /** Callback when subtitles change */\n  onSubtitlesChange?: (value: string) => void;\n  /** Custom label for subtitles field */\n  label?: string;\n  /** Custom helper text */\n  helperText?: string;\n  /** Custom placeholder */\n  placeholder?: string;\n  /** Minimum height for the textarea */\n  minHeight?: number;\n  /** Show the AI Generated badge next to the label (when content exists) */\n  isAIGenerated?: boolean;\n  /** Whether the field is disabled */\n  disabled?: boolean;\n  /** DOM id for the textarea/label pair (unique per instance on a page) */\n  id?: string;\n  /** Additional class name */\n  className?: string;\n}\n\n/**\n * SubtitlesEditor — THE single SRT-editing surface.\n *\n * Extracted from TranscriptSummaryEditor so the MAIN video's subtitles\n * (srt_content) and the HIGHLIGHT reel's subtitles (highlight_srt_content)\n * render through the exact same component: same textarea, same AI badge,\n * same helper-text layout. Renders nothing when neither content nor a\n * change handler is provided (same convention the inline block had).\n */\nexport function SubtitlesEditor({\n  subtitles,\n  onSubtitlesChange,\n  label = 'Subtitles (SRT)',\n  helperText = 'SRT subtitle content generated from transcription. Editable for fine-tuning.',\n  placeholder = '1\\n00:00:00,000 --> 00:00:02,500\\nHello, welcome to...',\n  minHeight = 200,\n  isAIGenerated = false,\n  disabled = false,\n  id = 'subtitles',\n  className = '',\n}: SubtitlesEditorProps) {\n  if (!subtitles && !onSubtitlesChange) return null;\n\n  return (\n    <div className={className}>\n      <div className=\"mb-2\">\n        <div className=\"flex items-center gap-2\">\n          <Label htmlFor={id}>{label}</Label>\n          {isAIGenerated && subtitles && (\n            <Badge variant=\"secondary\" className=\"flex items-center gap-1\">\n              <StandardCcIcon size={12} />\n              AI Generated\n            </Badge>\n          )}\n        </div>\n        <p className=\"mt-1 text-ods-text-secondary text-h6\">{helperText}</p>\n      </div>\n      <div\n        className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-card\"\n        style={{ minHeight: `${minHeight}px` }}\n      >\n        <Textarea\n          id={id}\n          value={subtitles || ''}\n          onChange={e => onSubtitlesChange?.(e.target.value)}\n          placeholder={placeholder}\n          disabled={disabled || !onSubtitlesChange}\n          className=\"h-full w-full resize-none border-0 bg-transparent p-4 text-ods-text-primary text-code placeholder:text-ods-text-secondary/50 focus:outline-none focus:ring-0\"\n          style={{ minHeight: `${minHeight}px`, lineHeight: '1.6' }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default SubtitlesEditor;\n","'use client';\n\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Label } from '../ui/label';\nimport { Textarea } from '../ui/textarea';\nimport { ConfidenceBadge } from './ai-enrich/ConfidenceBadge';\nimport { SubtitlesEditor } from './subtitles-editor';\n\nexport interface TranscriptSummaryEditorProps {\n  /** Video summary text value - AI-generated from video transcription (defaults to empty string if undefined) */\n  videoSummary?: string;\n  /** Callback when video summary changes */\n  onVideoSummaryChange: (value: string) => void;\n  /** Transcript text value (defaults to empty string if undefined) */\n  transcript?: string;\n  /** Callback when transcript changes */\n  onTranscriptChange: (value: string) => void;\n  /** Whether content was AI generated */\n  isAIGenerated?: boolean;\n  /** Confidence score for video summary (0-100) */\n  videoSummaryConfidence?: number | null;\n  /** Confidence score for transcript (0-100) */\n  transcriptConfidence?: number | null;\n  /** Custom label for video summary field */\n  videoSummaryLabel?: string;\n  /** Custom helper text for video summary */\n  videoSummaryHelperText?: string;\n  /** Custom placeholder for video summary */\n  videoSummaryPlaceholder?: string;\n  /** Custom label for transcript field */\n  transcriptLabel?: string;\n  /** Custom helper text for transcript */\n  transcriptHelperText?: string;\n  /** Custom placeholder for transcript */\n  transcriptPlaceholder?: string;\n  /** Minimum height for video summary textarea */\n  videoSummaryMinHeight?: number;\n  /** Minimum height for transcript textarea */\n  transcriptMinHeight?: number;\n  /** SRT subtitle content */\n  subtitles?: string;\n  /** Callback when subtitles change */\n  onSubtitlesChange?: (value: string) => void;\n  /** Custom label for subtitles field */\n  subtitlesLabel?: string;\n  /** Custom helper text for subtitles */\n  subtitlesHelperText?: string;\n  /** Custom placeholder for subtitles */\n  subtitlesPlaceholder?: string;\n  /** Minimum height for subtitles textarea */\n  subtitlesMinHeight?: number;\n  /** Whether the fields are disabled */\n  disabled?: boolean;\n  /** Additional class name for the container */\n  className?: string;\n}\n\n/**\n * TranscriptSummaryEditor - Unified component for editing transcript and summary with AI badges\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities,\n * showing AI generation badges and confidence scores when applicable.\n */\nexport function TranscriptSummaryEditor({\n  videoSummary = '',\n  onVideoSummaryChange,\n  transcript = '',\n  onTranscriptChange,\n  isAIGenerated = false,\n  videoSummaryConfidence,\n  transcriptConfidence,\n  videoSummaryLabel = 'Video Summary (Markdown supported)',\n  videoSummaryHelperText = 'AI-generated summary from video content.',\n  videoSummaryPlaceholder = 'Brief summary of the video content...',\n  transcriptLabel = 'Transcript',\n  transcriptHelperText = 'Full transcript with speaker diarization and timestamps',\n  transcriptPlaceholder = '**[00:00] Speaker Name:** Text here...',\n  videoSummaryMinHeight = 200,\n  transcriptMinHeight = 300,\n  subtitles,\n  onSubtitlesChange,\n  subtitlesLabel = 'Subtitles (SRT)',\n  subtitlesHelperText = 'SRT subtitle content generated from transcription. Editable for fine-tuning.',\n  subtitlesPlaceholder = '1\\n00:00:00,000 --> 00:00:02,500\\nHello, welcome to...',\n  subtitlesMinHeight = 200,\n  disabled = false,\n  className = '',\n}: TranscriptSummaryEditorProps) {\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* Video Summary Editor */}\n      <div>\n        <div className=\"mb-2\">\n          <div className=\"flex items-center gap-2\">\n            <Label htmlFor=\"video-summary\">{videoSummaryLabel}</Label>\n            {isAIGenerated && <AIGeneratedBadge />}\n            {videoSummaryConfidence !== undefined && videoSummaryConfidence !== null && (\n              <ConfidenceBadge confidence={videoSummaryConfidence} showLabel={true} showPercentage={true} size=\"sm\" />\n            )}\n          </div>\n          <p className=\"mt-1 text-ods-text-secondary text-h6\">{videoSummaryHelperText}</p>\n        </div>\n        <div\n          className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-card\"\n          style={{ minHeight: `${videoSummaryMinHeight}px` }}\n        >\n          <Textarea\n            id=\"video-summary\"\n            value={videoSummary}\n            onChange={e => onVideoSummaryChange(e.target.value)}\n            placeholder={videoSummaryPlaceholder}\n            disabled={disabled}\n            className=\"h-full w-full resize-none border-0 bg-transparent p-4 text-ods-text-primary text-code placeholder:text-ods-text-secondary/50 focus:outline-none focus:ring-0\"\n            style={{ minHeight: `${videoSummaryMinHeight}px`, lineHeight: '1.6' }}\n          />\n        </div>\n      </div>\n\n      {/* Transcript Editor */}\n      <div>\n        <div className=\"mb-2\">\n          <div className=\"flex items-center gap-2\">\n            <Label htmlFor=\"transcript\">{transcriptLabel}</Label>\n            {isAIGenerated && <AIGeneratedBadge />}\n            {transcriptConfidence !== undefined && transcriptConfidence !== null && (\n              <ConfidenceBadge confidence={transcriptConfidence} showLabel={true} showPercentage={true} size=\"sm\" />\n            )}\n          </div>\n          <p className=\"mt-1 text-ods-text-secondary text-h6\">{transcriptHelperText}</p>\n        </div>\n        <div\n          className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-card\"\n          style={{ minHeight: `${transcriptMinHeight}px` }}\n        >\n          <Textarea\n            id=\"transcript\"\n            value={transcript}\n            onChange={e => onTranscriptChange(e.target.value)}\n            placeholder={transcriptPlaceholder}\n            disabled={disabled}\n            className=\"h-full w-full resize-none border-0 bg-transparent p-4 text-ods-text-primary text-code placeholder:text-ods-text-secondary/50 focus:outline-none focus:ring-0\"\n            style={{ minHeight: `${transcriptMinHeight}px`, lineHeight: '1.6' }}\n          />\n        </div>\n      </div>\n\n      {/* Subtitles Editor — the shared SubtitlesEditor SSOT (same component\n          the highlight section uses for highlight_srt_content). */}\n      <SubtitlesEditor\n        id=\"subtitles\"\n        subtitles={subtitles}\n        onSubtitlesChange={onSubtitlesChange}\n        label={subtitlesLabel}\n        helperText={subtitlesHelperText}\n        placeholder={subtitlesPlaceholder}\n        minHeight={subtitlesMinHeight}\n        isAIGenerated={isAIGenerated}\n        disabled={disabled}\n      />\n    </div>\n  );\n}\n\nexport default TranscriptSummaryEditor;\n","'use client';\n\nimport { Sparkles, Upload } from 'lucide-react';\nimport type React from 'react';\nimport { useState } from 'react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Badge } from '../ui/badge';\nimport { Button } from '../ui/button';\nimport { Label } from '../ui/label';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';\nimport { AIEnrichSection } from './ai-enrich/AIEnrichSection';\nimport type { AIRequiredField } from './ai-enrich/AIEnrichSection';\nimport { Video } from './video';\n\nexport interface HighlightVideoSectionProps {\n  /** Current highlight video URL */\n  highlightVideoUrl?: string | null;\n  /** Highlight video thumbnail URL */\n  highlightVideoThumbnail?: string | null;\n  /** Highlight video duration in milliseconds */\n  highlightVideoDurationMs?: number | null;\n  /** Highlight video source type */\n  highlightVideoSource?: 'manual' | 'ai_generated' | null;\n  /** Target duration in seconds for AI generation */\n  targetDurationSeconds: number;\n  /** Callback when target duration changes */\n  onTargetDurationChange: (seconds: number) => void;\n  /** Callback to trigger highlight generation */\n  onGenerateHighlight: () => void;\n  /** Whether highlight generation is in progress */\n  isGenerating?: boolean;\n  /** Progress percentage for generation (0-100) */\n  generationProgress?: number;\n  /** Status message during generation */\n  generationStatusMessage?: string;\n  /** Generation status (cancelled maps to error) */\n  generationStatus?: 'idle' | 'loading' | 'success' | 'error';\n  /** Whether highlight can be generated */\n  canGenerateHighlight?: boolean;\n  /** Required fields for generation */\n  requiredFields?: AIRequiredField[];\n  /** Message when generation is disabled */\n  disabledMessage?: string;\n  /** Whether a previous highlight exists */\n  hasExistingHighlight?: boolean;\n  /** Callback to cancel generation */\n  onCancelGeneration?: () => void;\n  /** Whether cancellation is in progress */\n  isCancelling?: boolean;\n  /** Callback to upload highlight video manually */\n  onUploadHighlight: (file: File) => Promise<string>;\n  /** Whether upload is in progress */\n  isUploading?: boolean;\n  /** Callback when highlight video is deleted */\n  onDeleteHighlight?: () => void;\n  /** Custom video preview component */\n  VideoPreviewComponent?: React.ComponentType<{\n    videoUrl: string;\n    thumbnailUrl?: string;\n    onDelete?: () => void;\n  }>;\n  /** Whether section is disabled (e.g., YouTube selected) */\n  disabled?: boolean;\n  /** Additional class name */\n  className?: string;\n}\n\n/**\n * HighlightVideoSection - Unified component for highlight video generation and management\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities,\n * including duration configuration, AI generation trigger, manual upload, and video preview.\n */\nexport function HighlightVideoSection({\n  highlightVideoUrl,\n  highlightVideoThumbnail,\n  highlightVideoDurationMs,\n  highlightVideoSource,\n  targetDurationSeconds,\n  onTargetDurationChange,\n  onGenerateHighlight,\n  isGenerating = false,\n  generationProgress,\n  generationStatusMessage,\n  generationStatus,\n  canGenerateHighlight = true,\n  requiredFields = [],\n  disabledMessage = 'Upload a video and run transcription first',\n  hasExistingHighlight = false,\n  onCancelGeneration,\n  isCancelling = false,\n  onUploadHighlight,\n  isUploading = false,\n  onDeleteHighlight,\n  VideoPreviewComponent,\n  disabled = false,\n  className = '',\n}: HighlightVideoSectionProps) {\n  const [uploadError, setUploadError] = useState<string | null>(null);\n\n  const handleUploadClick = () => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = 'video/*';\n    input.onchange = async (e: Event) => {\n      const target = e.target as HTMLInputElement;\n      const file = target.files?.[0];\n      if (!file) return;\n\n      setUploadError(null);\n      try {\n        await onUploadHighlight(file);\n      } catch (err) {\n        setUploadError(err instanceof Error ? err.message : 'Failed to upload video');\n      }\n    };\n    input.click();\n  };\n\n  const formatDuration = (ms: number) => {\n    const minutes = Math.floor(ms / 60000);\n    const seconds = Math.floor((ms % 60000) / 1000);\n    return `${minutes}:${String(seconds).padStart(2, '0')}`;\n  };\n\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* Highlight Video Configuration */}\n      <div className=\"space-y-3 rounded-lg border border-ods-border bg-ods-card p-4\">\n        <div className=\"flex items-center gap-4\">\n          <div className=\"flex-1\">\n            <Label>Target Duration</Label>\n            <Select\n              value={targetDurationSeconds.toString()}\n              onValueChange={value => onTargetDurationChange(parseInt(value))}\n              disabled={disabled}\n            >\n              <SelectTrigger className=\"mt-1 bg-ods-bg-surface\">\n                <SelectValue />\n              </SelectTrigger>\n              <SelectContent className=\"bg-ods-card\">\n                <SelectItem value=\"60\">1 minute</SelectItem>\n                <SelectItem value=\"120\">2 minutes</SelectItem>\n                <SelectItem value=\"180\">3 minutes (Recommended)</SelectItem>\n                <SelectItem value=\"240\">4 minutes</SelectItem>\n                <SelectItem value=\"300\">5 minutes</SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n        </div>\n      </div>\n\n      {/* AI Generation Section */}\n      <AIEnrichSection\n        title=\"Highlight Video\"\n        description={`Generate a ${Math.floor(targetDurationSeconds / 60)}-minute summary video using Claude AI + Shotstack`}\n        icon={<Sparkles className=\"h-5 w-5\" />}\n        buttonLabel={hasExistingHighlight ? 'Regenerate Highlight' : 'Generate Highlight'}\n        loadingLabel=\"Generating highlight...\"\n        onEnrich={onGenerateHighlight}\n        loading={isGenerating}\n        canEnrich={canGenerateHighlight && !disabled}\n        requiredFields={requiredFields}\n        status={generationStatus}\n        statusMessage={\n          generationStatusMessage\n            ? generationProgress && generationProgress > 0\n              ? `${generationStatusMessage} (${generationProgress}%)`\n              : generationStatusMessage\n            : undefined\n        }\n        disabledMessage={disabled ? \"Switch to 'Upload Video' to enable AI processing\" : disabledMessage}\n        showCancel={!!onCancelGeneration}\n        onCancel={onCancelGeneration}\n        isCancelling={isCancelling}\n      />\n\n      {/* Highlight Video Preview + Manual Upload */}\n      <div className=\"space-y-2\">\n        <div className=\"mb-2 flex items-center justify-between\">\n          <div className=\"flex items-center gap-2\">\n            <Label>Highlight Video</Label>\n            {highlightVideoSource === 'ai_generated' && <AIGeneratedBadge />}\n            {highlightVideoDurationMs && (\n              <Badge variant=\"outline\" className=\"text-h6\">\n                {formatDuration(highlightVideoDurationMs)}\n              </Badge>\n            )}\n          </div>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"small-legacy\"\n            leftIcon={<Upload className=\"h-4 w-4\" />}\n            onClick={handleUploadClick}\n            disabled={isUploading || disabled}\n          >\n            {isUploading ? 'Uploading...' : 'Upload Highlight'}\n          </Button>\n        </div>\n\n        {uploadError && <p className=\"text-ods-error text-h6\">{uploadError}</p>}\n\n        {highlightVideoUrl ? (\n          VideoPreviewComponent ? (\n            <VideoPreviewComponent\n              videoUrl={highlightVideoUrl}\n              thumbnailUrl={highlightVideoThumbnail || undefined}\n              onDelete={onDeleteHighlight}\n            />\n          ) : (\n            // Default simple preview — the <Video> SSOT (MuxPlayer) plays\n            // MP4 and Mux HLS alike; aspect-video box reserves the height.\n            <div className=\"relative overflow-hidden rounded-lg border border-ods-border bg-black\">\n              <div className=\"aspect-video max-h-[300px] w-full\">\n                <Video kind=\"file\" url={highlightVideoUrl} poster={highlightVideoThumbnail || undefined} />\n              </div>\n              {onDeleteHighlight && (\n                <button\n                  type=\"button\"\n                  onClick={onDeleteHighlight}\n                  className=\"absolute right-2 top-2 rounded-full bg-black/60 p-1.5 transition-colors hover:bg-black/80\"\n                >\n                  <span className=\"sr-only\">Delete</span>\n                  <svg className=\"h-4 w-4 text-ods-text-on-dark\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n                    <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n                  </svg>\n                </button>\n              )}\n            </div>\n          )\n        ) : (\n          <p className=\"italic text-ods-text-secondary text-h6\">\n            No highlight video yet. Use AI generation above or upload manually.\n          </p>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport default HighlightVideoSection;\n","'use client';\n\nimport { CheckCircle, AlertCircle, Loader2, X } from 'lucide-react';\nimport type React from 'react';\nimport { cn } from '../../../utils/cn';\nimport { SparklesIcon } from '../../icons/sparkles-icon';\nimport { Badge } from '../../ui/badge';\nimport { Button } from '../../ui/button';\nimport { Textarea } from '../../ui/textarea';\nimport { AIEnrichButton } from './AIEnrichButton';\nimport { AIWarningsSection } from './AIWarningsSection';\n\nexport interface ConfidenceField {\n  label: string;\n  key: string;\n  confidence?: number;\n}\n\n/**\n * Represents a field required for AI enrichment.\n * Used to drive both the canEnrich logic and display of AI-required badges.\n */\nexport interface AIRequiredField {\n  /** Form field key (e.g., 'version', 'email') */\n  key: string;\n  /** Display label (e.g., 'Version', 'Email') */\n  label: string;\n  /** Current state - is field filled? */\n  isFilled: boolean;\n}\n\nexport interface AIEnrichSectionProps {\n  // Button state\n  onEnrich: () => void;\n  loading?: boolean;\n  disabled?: boolean;\n  canEnrich?: boolean;\n\n  // Status\n  status?: 'idle' | 'loading' | 'success' | 'error';\n  statusMessage?: string;\n  overallConfidence?: number;\n\n  // Warnings\n  warnings?: string[];\n\n  // Confidence fields to display (optional - shown as simple list)\n  confidenceFields?: ConfidenceField[];\n\n  // Required fields for AI enrichment - displays missing fields when disabled\n  requiredFields?: AIRequiredField[];\n\n  // Custom content (like created tags info)\n  children?: React.ReactNode;\n  /**\n   * Settings panel rendered INSIDE the card, between the header and the action\n   * button (same slot the custom-instructions block occupies). Combined\n   * sections (clips, highlight) pass their config here so it sits under the\n   * title that names it — rendered ABOVE the card, a config panel reads as\n   * belonging to whatever section happens to precede it.\n   */\n  configSlot?: React.ReactNode;\n\n  // Actions\n  onClear?: () => void;\n  showClearButton?: boolean;\n  onCancel?: () => void;\n  showCancel?: boolean;\n  isCancelling?: boolean;\n\n  // Labels\n  title?: string;\n  description?: string;\n  buttonLabel?: string;\n  loadingLabel?: string;\n  disabledMessage?: string;\n\n  // Styling\n  variant?: 'default' | 'compact';\n  className?: string;\n  icon?: React.ReactNode;\n\n  // Editor-provided custom instructions textarea (opt-in).\n  // When showCustomInstructions is true, a controlled <Textarea> is rendered\n  // above the action button. The parent owns the string — purely controlled.\n  // The same value is sent to the backend and injected into the Claude prompt\n  // via lib/utils/ai-instructions.ts → buildEditorFocusBlock().\n  showCustomInstructions?: boolean;\n  customInstructions?: string;\n  onCustomInstructionsChange?: (value: string) => void;\n  customInstructionsLabel?: string;\n  customInstructionsPlaceholder?: string;\n  customInstructionsHelperText?: string;\n  customInstructionsMaxLength?: number;\n}\n\nexport const AIEnrichSection: React.FC<AIEnrichSectionProps> = ({\n  onEnrich,\n  loading = false,\n  disabled = false,\n  canEnrich = true,\n  status,\n  statusMessage,\n  overallConfidence,\n  warnings,\n  requiredFields,\n  children,\n  configSlot,\n  onClear,\n  showClearButton = true,\n  onCancel,\n  showCancel = false,\n  isCancelling = false,\n  title = 'AI Enrichment',\n  description,\n  buttonLabel = 'AI Enrich',\n  loadingLabel = 'Enriching...',\n  disabledMessage = 'Fill in required fields to enable AI enrichment.',\n  variant = 'default',\n  className,\n  icon,\n  showCustomInstructions = false,\n  customInstructions,\n  onCustomInstructionsChange,\n  customInstructionsLabel = 'Focus / additional instructions',\n  customInstructionsPlaceholder = \"Optional — steer the AI. e.g. 'Lead with the new SSO + audit log features; downplay the minor UI tweaks.'\",\n  customInstructionsHelperText,\n  customInstructionsMaxLength = 5000,\n}) => {\n  const hasResults = status === 'success' || status === 'error';\n  const shouldDisable = disabled || !canEnrich;\n\n  // Get list of unfilled required fields for display\n  const unfilledFields = requiredFields?.filter(f => !f.isFilled) || [];\n\n  return (\n    <div\n      className={cn(\n        'rounded-lg border border-ods-border bg-ods-card',\n        variant === 'default' ? 'space-y-4 p-6' : 'space-y-3 p-4',\n        className,\n      )}\n    >\n      {/* Row 1: Icon + Title + Description */}\n      <div className=\"flex items-center gap-3\">\n        {icon || <SparklesIcon size={20} className=\"text-ods-text-secondary\" />}\n        <div className=\"flex-1\">\n          <h3 className=\"text-ods-text-primary text-h5\">{title}</h3>\n          {description && <p className=\"mt-1 text-ods-text-secondary text-h6\">{description}</p>}\n        </div>\n      </div>\n\n      {/* Editor-provided settings (opt-in) — e.g. clip aspect ratio, highlight duration */}\n      {configSlot}\n\n      {/* Editor-provided custom instructions (opt-in) */}\n      {showCustomInstructions && (\n        <div className=\"space-y-1.5\">\n          <div className=\"flex items-center justify-between\">\n            <label htmlFor=\"ai-enrich-custom-instructions\" className=\"text-ods-text-primary text-h6\">\n              {customInstructionsLabel}\n            </label>\n            {customInstructionsMaxLength !== undefined && (\n              <span className=\"text-ods-text-secondary text-h6\">\n                {(customInstructions ?? '').length}/{customInstructionsMaxLength}\n              </span>\n            )}\n          </div>\n          <Textarea\n            id=\"ai-enrich-custom-instructions\"\n            value={customInstructions ?? ''}\n            onChange={e => onCustomInstructionsChange?.(e.target.value)}\n            placeholder={customInstructionsPlaceholder}\n            disabled={loading}\n            maxLength={customInstructionsMaxLength}\n            rows={3}\n            className=\"resize-y\"\n          />\n          {customInstructionsHelperText && (\n            <p className=\"text-ods-text-secondary text-h6\">{customInstructionsHelperText}</p>\n          )}\n        </div>\n      )}\n\n      {/* Row 2: Buttons */}\n      <div className=\"flex flex-col gap-3\">\n        <AIEnrichButton\n          onClick={() => {\n            console.log('[AIEnrichSection] 🔘 Button clicked');\n            console.log('[AIEnrichSection] Loading:', loading);\n            console.log('[AIEnrichSection] Disabled:', shouldDisable);\n            console.log('[AIEnrichSection] CanEnrich:', canEnrich);\n            onEnrich();\n          }}\n          loading={loading}\n          disabled={shouldDisable}\n          label={buttonLabel}\n          loadingLabel={loadingLabel}\n          size=\"md\"\n          className=\"!w-full\"\n        />\n        {showCancel && onCancel && loading && (\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"small-legacy\"\n            onClick={onCancel}\n            disabled={isCancelling}\n            leftIcon={isCancelling ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <X className=\"h-4 w-4\" />}\n            className=\"!w-full\"\n          >\n            {isCancelling ? 'Cancelling...' : 'Cancel Processing'}\n          </Button>\n        )}\n      </div>\n\n      {/* Disabled message with unfilled fields */}\n      {shouldDisable && !loading && (\n        <div className=\"space-y-2\">\n          <p className=\"text-ods-text-secondary text-h6\">{disabledMessage}</p>\n          {unfilledFields.length > 0 && (\n            <div className=\"flex flex-wrap gap-2\">\n              {unfilledFields.map(field => (\n                <span\n                  key={field.key}\n                  className=\"inline-flex items-center gap-1 rounded-full bg-ods-flamingo-cyan/10 px-2 py-0.5 text-ods-flamingo-cyan/70 text-h6\"\n                >\n                  <span className=\"h-1.5 w-1.5 rounded-full bg-ods-flamingo-cyan/50\" />\n                  {field.label}\n                </span>\n              ))}\n            </div>\n          )}\n        </div>\n      )}\n\n      {/* Loading state with status message */}\n      {loading && statusMessage && (\n        <div className=\"flex items-center gap-3 rounded-lg bg-ods-bg-surface p-3\">\n          <Loader2 className=\"h-5 w-5 animate-spin text-ods-accent\" />\n          <span className=\"text-ods-text-primary text-h6\">{statusMessage}</span>\n        </div>\n      )}\n\n      {/* Results section */}\n      {hasResults && (\n        <div className=\"space-y-4\">\n          {/* Status indicator - simple and clean */}\n          <div\n            className={cn(\n              'flex items-center gap-3 rounded-lg p-3',\n              status === 'success' ? 'bg-ods-success/10' : 'bg-ods-error/10',\n            )}\n          >\n            {status === 'success' ? (\n              <CheckCircle className=\"h-5 w-5 text-ods-success\" />\n            ) : (\n              <AlertCircle className=\"h-5 w-5 text-ods-error\" />\n            )}\n            <span className={cn('text-h6', status === 'success' ? 'text-ods-success' : 'text-ods-error')}>\n              {statusMessage || (status === 'success' ? 'Enrichment complete' : 'Enrichment failed')}\n            </span>\n            {overallConfidence !== undefined && status === 'success' && (\n              <Badge variant=\"success\" className=\"ml-auto\">\n                {overallConfidence}% confidence\n              </Badge>\n            )}\n          </div>\n\n          {/* Warnings */}\n          {warnings && warnings.length > 0 && <AIWarningsSection warnings={warnings} />}\n\n          {/* Custom children content (like created tags info) */}\n          {children}\n\n          {/* Clear button */}\n          {showClearButton && onClear && (\n            <Button type=\"button\" variant=\"outline\" size=\"small-legacy\" onClick={onClear}>\n              Clear Results\n            </Button>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../../utils/cn';\nimport { SparklesIcon } from '../../icons/sparkles-icon';\nimport { Button } from '../../ui/button';\n\nexport interface AIEnrichButtonProps {\n  onClick: () => void;\n  disabled?: boolean;\n  loading?: boolean;\n  label?: string;\n  loadingLabel?: string;\n  size?: 'sm' | 'md' | 'lg';\n  variant?: 'primary' | 'secondary' | 'outline';\n  className?: string;\n}\n\nconst sizeMap = {\n  sm: 'small-legacy' as const,\n  md: 'default' as const,\n  lg: 'default' as const,\n};\n\nconst variantMap = {\n  primary: 'accent' as const,\n  secondary: 'outline' as const,\n  outline: 'outline' as const,\n};\n\nexport const AIEnrichButton: React.FC<AIEnrichButtonProps> = ({\n  onClick,\n  disabled = false,\n  loading = false,\n  label = 'AI Enrich',\n  loadingLabel = 'Enriching...',\n  size = 'md',\n  variant = 'outline',\n  className,\n}) => {\n  return (\n    <Button\n      type=\"button\"\n      onClick={onClick}\n      disabled={disabled || loading}\n      loading={loading}\n      size={sizeMap[size]}\n      variant={variantMap[variant]}\n      leftIcon={!loading && <SparklesIcon size={18} color=\"currentColor\" />}\n      className={cn('gap-2', className)}\n    >\n      {loading ? loadingLabel : label}\n    </Button>\n  );\n};\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../../utils/cn';\n\nexport interface AIWarningsSectionProps {\n  warnings: string[];\n  title?: string;\n  className?: string;\n}\n\nexport const AIWarningsSection: React.FC<AIWarningsSectionProps> = ({ warnings, title = 'AI Warnings', className }) => {\n  if (!warnings || warnings.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className={cn('rounded-lg border border-ods-warning/30 bg-ods-warning/10 p-4', className)}>\n      <div className=\"flex items-start gap-2\">\n        <svg\n          className=\"mt-0.5 h-5 w-5 flex-shrink-0 text-ods-warning\"\n          fill=\"none\"\n          viewBox=\"0 0 24 24\"\n          stroke=\"currentColor\"\n        >\n          <path\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth={2}\n            d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n          />\n        </svg>\n        <div className=\"flex-1\">\n          <h4 className=\"mb-2 font-semibold text-ods-warning text-h6\">{title}</h4>\n          <ul className=\"space-y-1\">\n            {warnings.map((warning, index) => (\n              <li key={index} className=\"list-inside list-disc text-ods-warning/80 text-h6\">\n                {warning}\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    </div>\n  );\n};\n","'use client';\n\nimport { Field } from '../ui/field';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';\n\nexport interface HighlightConfigSectionProps {\n  /** Current target duration in seconds */\n  targetDurationSeconds: number;\n  /** Callback when target duration changes */\n  onTargetDurationChange: (seconds: number) => void;\n  /** Whether the section is disabled */\n  disabled?: boolean;\n  /** Additional class name */\n  className?: string;\n}\n\n/**\n * HighlightConfigSection - Unified component for highlight video configuration\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities,\n * including duration selection in a styled horizontal layout. Highlight captions\n * are generated automatically as a selectable text track (same pattern as the\n * main video) — there is no subtitle-burning option anymore.\n */\nexport function HighlightConfigSection({\n  targetDurationSeconds,\n  onTargetDurationChange,\n  disabled = false,\n  className = '',\n}: HighlightConfigSectionProps) {\n  return (\n    <div className={`space-y-3 rounded-lg border border-ods-border bg-ods-card p-4 ${className}`}>\n      <div className=\"flex items-center gap-4\">\n        <div className=\"flex-1\">\n          {/* Field, not a raw Label + margin hack — same label system as every\n              other form field so adjacent columns stay on one baseline. */}\n          <Field label=\"Target Duration\">\n            {f => (\n              <Select\n                value={targetDurationSeconds.toString()}\n                onValueChange={value => onTargetDurationChange(parseInt(value))}\n                disabled={disabled}\n              >\n                <SelectTrigger id={f.id} className=\"bg-ods-bg\">\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent className=\"bg-ods-card\">\n                  <SelectItem value=\"60\">1 minute</SelectItem>\n                  <SelectItem value=\"120\">2 minutes</SelectItem>\n                  <SelectItem value=\"180\">3 minutes (Recommended)</SelectItem>\n                  <SelectItem value=\"240\">4 minutes</SelectItem>\n                  <SelectItem value=\"300\">5 minutes</SelectItem>\n                </SelectContent>\n              </Select>\n            )}\n          </Field>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default HighlightConfigSection;\n","'use client';\n\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Label } from '../ui/label';\nimport { Textarea } from '../ui/textarea';\nimport { ConfidenceBadge } from './ai-enrich/ConfidenceBadge';\n\nexport interface EntitySummaryEditorProps {\n  /** Summary text value (defaults to empty string if undefined) */\n  summary?: string;\n  /** Callback when summary changes */\n  onSummaryChange: (value: string) => void;\n  /** Whether content was AI generated */\n  isAIGenerated?: boolean;\n  /** Confidence score for summary (0-100) */\n  summaryConfidence?: number | null;\n  /** Custom label for summary field */\n  label?: string;\n  /** Custom helper text */\n  helperText?: string;\n  /** Custom placeholder */\n  placeholder?: string;\n  /** Minimum height for textarea */\n  minHeight?: number;\n  /** Whether the field is disabled */\n  disabled?: boolean;\n  /** Additional class name for the container */\n  className?: string;\n}\n\n/**\n * EntitySummaryEditor - Unified component for editing entity summaries with AI badges\n *\n * This component provides a consistent UI for entity summaries (e.g., Release Summary, Interview Summary)\n * separate from video summaries. Shows AI generation badges and confidence scores when applicable.\n */\nexport function EntitySummaryEditor({\n  summary = '',\n  onSummaryChange,\n  isAIGenerated = false,\n  summaryConfidence,\n  label = 'Summary (Markdown supported)',\n  helperText = 'Entity summary. Displayed on cards and detail page.',\n  placeholder = 'Brief summary...',\n  minHeight = 200,\n  disabled = false,\n  className = '',\n}: EntitySummaryEditorProps) {\n  return (\n    <div className={className}>\n      <div className=\"mb-2\">\n        <div className=\"flex items-center gap-2\">\n          <Label htmlFor=\"entity-summary\">{label}</Label>\n          {isAIGenerated && <AIGeneratedBadge />}\n          {summaryConfidence !== undefined && summaryConfidence !== null && (\n            <ConfidenceBadge confidence={summaryConfidence} showLabel={true} showPercentage={true} size=\"sm\" />\n          )}\n        </div>\n        <p className=\"mt-1 text-ods-text-secondary text-h6\">{helperText}</p>\n      </div>\n      <div\n        className=\"overflow-hidden rounded-lg border border-ods-border bg-ods-card\"\n        style={{ minHeight: `${minHeight}px` }}\n      >\n        <Textarea\n          id=\"entity-summary\"\n          value={summary}\n          onChange={e => onSummaryChange(e.target.value)}\n          placeholder={placeholder}\n          disabled={disabled}\n          className=\"h-full w-full resize-none border-0 bg-transparent p-4 text-ods-text-primary text-code placeholder:text-ods-text-secondary/50 focus:outline-none focus:ring-0\"\n          style={{ minHeight: `${minHeight}px`, lineHeight: '1.6' }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default EntitySummaryEditor;\n","'use client';\n\nimport { Sparkles } from 'lucide-react';\nimport { AIEnrichSection } from './ai-enrich';\nimport type { AIRequiredField } from './ai-enrich';\n\nexport interface TranscribeSummarizeSectionProps {\n  /** Handler to trigger transcription processing */\n  onTranscribe: () => void;\n  /** Whether transcription is currently processing */\n  isProcessing: boolean;\n  /** Whether the generate button can be clicked */\n  canEnrich: boolean;\n  /** Required fields to display */\n  requiredFields: AIRequiredField[];\n  /** Current status of the operation */\n  status?: 'loading' | 'success' | 'error' | undefined;\n  /** Status message to display */\n  statusMessage?: string;\n  /** Message shown when button is disabled */\n  disabledMessage?: string;\n  /** Whether cancel is available */\n  showCancel?: boolean;\n  /** Handler to cancel processing */\n  onCancel?: () => void;\n  /** Whether cancellation is in progress */\n  isCancelling?: boolean;\n  /** Whether result already exists (affects button label) */\n  hasResult?: boolean;\n  /** Custom title */\n  title?: string;\n  /** Custom description */\n  description?: string;\n  /** Custom button label */\n  buttonLabel?: string;\n  /** Custom loading label */\n  loadingLabel?: string;\n}\n\n/**\n * TranscribeSummarizeSection - Unified AIEnrichSection wrapper for video transcription\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities\n * for the \"Transcribe & Summarize\" video processing step.\n */\nexport function TranscribeSummarizeSection({\n  onTranscribe,\n  isProcessing,\n  canEnrich,\n  requiredFields,\n  status,\n  statusMessage,\n  disabledMessage = 'Upload a video first',\n  showCancel = true,\n  onCancel,\n  isCancelling = false,\n  hasResult = false,\n  title = 'Transcribe & Summarize',\n  description = 'Generate transcript with speaker diarization and AI summary',\n  buttonLabel,\n  loadingLabel = 'Processing...',\n}: TranscribeSummarizeSectionProps) {\n  const defaultButtonLabel = hasResult ? 'Regenerate' : 'Generate';\n\n  return (\n    <AIEnrichSection\n      title={title}\n      description={description}\n      icon={<Sparkles className=\"h-5 w-5\" />}\n      buttonLabel={buttonLabel || defaultButtonLabel}\n      loadingLabel={loadingLabel}\n      onEnrich={onTranscribe}\n      loading={isProcessing}\n      canEnrich={canEnrich}\n      requiredFields={requiredFields}\n      status={status}\n      statusMessage={statusMessage}\n      disabledMessage={disabledMessage}\n      showCancel={showCancel}\n      onCancel={onCancel}\n      isCancelling={isCancelling}\n    />\n  );\n}\n\nexport default TranscribeSummarizeSection;\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../../utils/cn';\nimport {\n  getConfidenceBorderClass,\n  getConfidenceTextClass,\n  getConfidenceLabel,\n} from '../../../utils/confidence-helpers';\nimport { SparklesIcon } from '../../icons/sparkles-icon';\n\nexport type AIStatus = 'idle' | 'loading' | 'success' | 'error';\n\nexport interface AIStatusIndicatorProps {\n  status: AIStatus;\n  confidence?: number;\n  message?: string;\n  showConfidence?: boolean;\n  className?: string;\n}\n\nconst statusConfig = {\n  idle: {\n    color: 'text-ods-text-secondary',\n    bgColor: 'bg-transparent',\n    label: 'Ready',\n  },\n  loading: {\n    color: 'text-ods-flamingo-cyan',\n    bgColor: 'bg-ods-flamingo-cyan/10',\n    label: 'Processing...',\n  },\n  success: {\n    color: 'text-ods-success',\n    bgColor: 'bg-ods-success/10',\n    label: 'Enriched',\n  },\n  error: {\n    color: 'text-ods-error',\n    bgColor: 'bg-ods-error/10',\n    label: 'Failed',\n  },\n};\n\nexport const AIStatusIndicator: React.FC<AIStatusIndicatorProps> = ({\n  status,\n  confidence,\n  message,\n  showConfidence = true,\n  className,\n}) => {\n  const config = statusConfig[status];\n\n  const displayText = message || config.label;\n  const confidenceLabel =\n    showConfidence && confidence !== undefined ? `${getConfidenceLabel(confidence)} (${confidence}%)` : null;\n\n  return (\n    <div className={cn('inline-flex items-center gap-2 rounded-lg px-3 py-1.5', config.bgColor, className)}>\n      <SparklesIcon\n        size={16}\n        className={cn(\n          status === 'loading' && 'animate-pulse',\n          confidence !== undefined && status === 'success' ? getConfidenceTextClass(confidence) : config.color,\n        )}\n      />\n      <span\n        className={cn(\n          'text-h6',\n          confidence !== undefined && status === 'success' ? getConfidenceTextClass(confidence) : config.color,\n        )}\n      >\n        {displayText}\n      </span>\n      {confidenceLabel && status === 'success' && (\n        <span\n          className={cn(\n            'rounded-full border px-2 py-0.5 text-h6',\n            getConfidenceBorderClass(confidence),\n            getConfidenceTextClass(confidence),\n          )}\n        >\n          {confidenceLabel}\n        </span>\n      )}\n    </div>\n  );\n};\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../../utils/cn';\nimport { SparklesIcon } from '../../icons/sparkles-icon';\n\nexport interface AIRequiredBadgeProps {\n  className?: string;\n  /** Show a smaller inline variant */\n  size?: 'sm' | 'md';\n}\n\n/**\n * Badge component to indicate a field is required for AI enrichment.\n * Uses ODS cyan color with sparkles icon.\n */\nexport const AIRequiredBadge: React.FC<AIRequiredBadgeProps> = ({ className, size = 'sm' }) => {\n  return (\n    <span\n      className={cn(\n        'inline-flex items-center gap-1 rounded-full text-h6',\n        'bg-ods-flamingo-cyan/10 text-ods-flamingo-cyan',\n        size === 'sm' && 'ml-2 px-2 py-0.5',\n        size === 'md' && 'ml-2 px-2.5 py-1',\n        className,\n      )}\n    >\n      <SparklesIcon size={size === 'sm' ? 12 : 14} color=\"var(--ods-flamingo-cyan-base)\" />\n      <span className=\"font-body\">AI-required</span>\n    </span>\n  );\n};\n","'use client';\n\nimport { Sparkles } from 'lucide-react';\nimport { AIEnrichSection } from './ai-enrich';\nimport type { AIRequiredField } from './ai-enrich';\n\nexport interface VideoClipsSectionProps {\n  /** Handler to trigger video clips extraction */\n  onExtractClips: () => void;\n  /** Whether extraction is currently processing */\n  isProcessing: boolean;\n  /** Whether the generate button can be clicked */\n  canEnrich: boolean;\n  /** Required fields to display */\n  requiredFields: AIRequiredField[];\n  /** Current status of the operation */\n  status?: 'loading' | 'success' | 'error' | undefined;\n  /** Status message to display */\n  statusMessage?: string;\n  /** Message shown when button is disabled */\n  disabledMessage?: string;\n  /** Whether cancel is available */\n  showCancel?: boolean;\n  /** Handler to cancel processing */\n  onCancel?: () => void;\n  /** Whether cancellation is in progress */\n  isCancelling?: boolean;\n  /** Whether result already exists (affects button label) */\n  hasResult?: boolean;\n  /** Custom title */\n  title?: string;\n  /** Custom description */\n  description?: string;\n  /** Custom button label */\n  buttonLabel?: string;\n  /** Custom loading label */\n  loadingLabel?: string;\n}\n\n/**\n * VideoClipsSection - Unified AIEnrichSection wrapper for video clips extraction\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities\n * for the \"Video Clips\" extraction step. Use with VideoBitesEditor for the full experience.\n */\nexport function VideoClipsSection({\n  onExtractClips,\n  isProcessing,\n  canEnrich,\n  requiredFields,\n  status,\n  statusMessage,\n  disabledMessage = 'Upload a video first',\n  showCancel = true,\n  onCancel,\n  isCancelling = false,\n  hasResult = false,\n  title = 'Video Clips',\n  description = 'Extract marketing-ready video clips from the video',\n  buttonLabel,\n  loadingLabel = 'Extracting clips...',\n}: VideoClipsSectionProps) {\n  const defaultButtonLabel = hasResult ? 'Regenerate Clips' : 'Generate Clips';\n\n  return (\n    <AIEnrichSection\n      title={title}\n      description={description}\n      icon={<Sparkles className=\"h-5 w-5\" />}\n      buttonLabel={buttonLabel || defaultButtonLabel}\n      loadingLabel={loadingLabel}\n      onEnrich={onExtractClips}\n      loading={isProcessing}\n      canEnrich={canEnrich}\n      requiredFields={requiredFields}\n      status={status}\n      statusMessage={statusMessage}\n      disabledMessage={disabledMessage}\n      showCancel={showCancel}\n      onCancel={onCancel}\n      isCancelling={isCancelling}\n    />\n  );\n}\n\nexport default VideoClipsSection;\n","'use client';\n\nimport { Sparkles } from 'lucide-react';\nimport { AIEnrichSection } from './ai-enrich';\nimport type { AIRequiredField } from './ai-enrich';\n\nexport interface HighlightGenerationSectionProps {\n  /** Handler to trigger highlight video generation */\n  onGenerateHighlight: () => void;\n  /** Whether generation is currently processing */\n  isProcessing: boolean;\n  /** Whether the generate button can be clicked */\n  canEnrich: boolean;\n  /** Required fields to display */\n  requiredFields: AIRequiredField[];\n  /** Current status of the operation */\n  status?: 'loading' | 'success' | 'error' | undefined;\n  /** Status message to display */\n  statusMessage?: string;\n  /** Message shown when button is disabled */\n  disabledMessage?: string;\n  /** Whether cancel is available */\n  showCancel?: boolean;\n  /** Handler to cancel processing */\n  onCancel?: () => void;\n  /** Whether cancellation is in progress */\n  isCancelling?: boolean;\n  /** Whether result already exists (affects button label) */\n  hasResult?: boolean;\n  /** Target highlight duration in seconds (for description) */\n  targetDurationSeconds?: number;\n  /** Custom title */\n  title?: string;\n  /** Custom description */\n  description?: string;\n  /** Custom button label */\n  buttonLabel?: string;\n  /** Custom loading label */\n  loadingLabel?: string;\n}\n\n/**\n * HighlightGenerationSection - Unified AIEnrichSection wrapper for highlight video generation\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities\n * for the \"Highlight Video\" generation step. Use with HighlightConfigSection for the full experience.\n */\nexport function HighlightGenerationSection({\n  onGenerateHighlight,\n  isProcessing,\n  canEnrich,\n  requiredFields,\n  status,\n  statusMessage,\n  disabledMessage = 'Upload a video and run transcription first',\n  showCancel = true,\n  onCancel,\n  isCancelling = false,\n  hasResult = false,\n  targetDurationSeconds = 180,\n  title = 'Highlight Video',\n  description,\n  buttonLabel,\n  loadingLabel = 'Generating highlight...',\n}: HighlightGenerationSectionProps) {\n  const defaultButtonLabel = hasResult ? 'Regenerate Highlight' : 'Generate Highlight';\n  const defaultDescription = `Generate a ${Math.floor(targetDurationSeconds / 60)}-minute summary video using Claude AI + Shotstack`;\n\n  return (\n    <AIEnrichSection\n      title={title}\n      description={description || defaultDescription}\n      icon={<Sparkles className=\"h-5 w-5\" />}\n      buttonLabel={buttonLabel || defaultButtonLabel}\n      loadingLabel={loadingLabel}\n      onEnrich={onGenerateHighlight}\n      loading={isProcessing}\n      canEnrich={canEnrich}\n      requiredFields={requiredFields}\n      status={status}\n      statusMessage={statusMessage}\n      disabledMessage={disabledMessage}\n      showCancel={showCancel}\n      onCancel={onCancel}\n      isCancelling={isCancelling}\n    />\n  );\n}\n\nexport default HighlightGenerationSection;\n","'use client';\n\nimport { Upload } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Badge } from '../ui/badge';\nimport { Button } from '../ui/button';\nimport { Label } from '../ui/label';\n\nexport interface HighlightVideoPreviewProps {\n  /** Highlight video URL */\n  highlightVideoUrl?: string | null;\n  /** Video thumbnail URL */\n  highlightVideoThumbnail?: string | null;\n  /** Video duration in milliseconds */\n  highlightVideoDurationMs?: number | null;\n  /** Whether video was AI generated */\n  highlightVideoSource?: 'manual' | 'ai_generated' | null;\n  /** Handler to upload a new highlight video - receives File, should return uploaded URL */\n  onUpload: (file: File) => Promise<string>;\n  /** Handler to delete the highlight video */\n  onDelete: () => Promise<void>;\n  /** Whether upload is in progress */\n  isUploading?: boolean;\n  /** Custom label */\n  label?: string;\n  /** Custom empty state message */\n  emptyMessage?: string;\n  /** Upload progress component (optional) */\n  uploadProgressComponent?: ReactNode;\n  /** Video preview component - receives video data, should render the preview */\n  renderVideoPreview: (props: {\n    videoUrl: string;\n    title: string;\n    thumbnailUrl?: string;\n    onDelete: () => Promise<void>;\n  }) => ReactNode;\n}\n\n/**\n * HighlightVideoPreview - Unified component for highlight video preview and manual upload\n *\n * This component provides a consistent UI for both CustomerInterview and ProductRelease entities\n * for displaying and manually uploading highlight videos.\n */\nexport function HighlightVideoPreview({\n  highlightVideoUrl,\n  highlightVideoThumbnail,\n  highlightVideoDurationMs,\n  highlightVideoSource,\n  onUpload,\n  onDelete,\n  isUploading = false,\n  label = 'Highlight Video',\n  emptyMessage = 'No highlight video yet. Use AI generation above or upload manually.',\n  uploadProgressComponent,\n  renderVideoPreview,\n}: HighlightVideoPreviewProps) {\n  const handleUploadClick = () => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = 'video/*';\n    input.onchange = async (e: Event) => {\n      const target = e.target as HTMLInputElement;\n      const file = target.files?.[0];\n      if (!file) return;\n      await onUpload(file);\n    };\n    input.click();\n  };\n\n  const formatDuration = (ms: number) => {\n    const minutes = Math.floor(ms / 60000);\n    const seconds = Math.floor((ms % 60000) / 1000);\n    return `${minutes}:${String(seconds).padStart(2, '0')}`;\n  };\n\n  return (\n    <div className=\"space-y-2\">\n      <div className=\"mb-2 flex items-center justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <Label>{label}</Label>\n          {highlightVideoSource === 'ai_generated' && <AIGeneratedBadge />}\n          {highlightVideoDurationMs && (\n            <Badge variant=\"outline\" className=\"text-h6\">\n              {formatDuration(highlightVideoDurationMs)}\n            </Badge>\n          )}\n        </div>\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"small-legacy\"\n          leftIcon={<Upload className=\"h-4 w-4\" />}\n          onClick={handleUploadClick}\n          disabled={isUploading}\n        >\n          {isUploading ? 'Uploading...' : 'Upload Highlight'}\n        </Button>\n      </div>\n\n      {uploadProgressComponent}\n\n      {highlightVideoUrl ? (\n        renderVideoPreview({\n          videoUrl: highlightVideoUrl,\n          title: 'Highlight Video',\n          thumbnailUrl: highlightVideoThumbnail || undefined,\n          onDelete,\n        })\n      ) : (\n        <p className=\"italic text-ods-text-secondary text-h6\">{emptyMessage}</p>\n      )}\n    </div>\n  );\n}\n\nexport default HighlightVideoPreview;\n","'use client';\n\nimport { Sparkles } from 'lucide-react';\nimport { AIEnrichSection } from './ai-enrich';\nimport type { AIRequiredField } from './ai-enrich';\nimport { TranscriptSummaryEditor } from './transcript-summary-editor';\n\nexport interface TranscribeAndSummarizeCombinedSectionProps {\n  // ===== AIEnrichSection Props =====\n  /** Handler to trigger transcription processing */\n  onTranscribe: () => void;\n  /** Whether transcription is currently processing */\n  isProcessing: boolean;\n  /** Whether the generate button can be clicked */\n  canEnrich: boolean;\n  /** Required fields to display */\n  requiredFields: AIRequiredField[];\n  /** Current status of the operation */\n  status?: 'loading' | 'success' | 'error' | undefined;\n  /** Status message to display */\n  statusMessage?: string;\n  /** Message shown when button is disabled */\n  disabledMessage?: string;\n  /** Whether cancel is available */\n  showCancel?: boolean;\n  /** Handler to cancel processing */\n  onCancel?: () => void;\n  /** Whether cancellation is in progress */\n  isCancelling?: boolean;\n  /** Whether result already exists (affects button label) */\n  hasResult?: boolean;\n  /** Custom button label */\n  buttonLabel?: string;\n  /** Custom title */\n  title?: string;\n  /** Custom description */\n  description?: string;\n  /** Custom loading label */\n  loadingLabel?: string;\n\n  // ===== TranscriptSummaryEditor Props =====\n  /** Video summary text */\n  videoSummary?: string;\n  /** Handler for video summary changes */\n  onVideoSummaryChange: (value: string) => void;\n  /** Transcript text */\n  transcript?: string;\n  /** Handler for transcript changes */\n  onTranscriptChange: (value: string) => void;\n  /** Whether content was AI generated */\n  isAIGenerated?: boolean;\n  /** Confidence score for video summary */\n  videoSummaryConfidence?: number | null;\n  /** Confidence score for transcript */\n  transcriptConfidence?: number | null;\n  /** Custom label for video summary field */\n  videoSummaryLabel?: string;\n  /** Custom helper text for video summary */\n  videoSummaryHelperText?: string;\n  /** Custom placeholder for video summary */\n  videoSummaryPlaceholder?: string;\n  /** Custom label for transcript field */\n  transcriptLabel?: string;\n  /** Custom helper text for transcript */\n  transcriptHelperText?: string;\n  /** Custom placeholder for transcript */\n  transcriptPlaceholder?: string;\n  /** Minimum height for transcript textarea */\n  transcriptMinHeight?: number;\n  /** SRT subtitle content */\n  subtitles?: string;\n  /** Callback when subtitles change */\n  onSubtitlesChange?: (value: string) => void;\n  /** Whether fields are disabled */\n  disabled?: boolean;\n  /** Additional class name */\n  className?: string;\n  /** Whether SRT subtitles were generated alongside transcription */\n  hasSubtitles?: boolean;\n}\n\n/**\n * TranscribeAndSummarizeCombinedSection - Unified component combining AI Enrich button + Transcript/Summary editor\n *\n * This component provides a complete UI for video transcription with:\n * 1. AIEnrichSection for triggering transcription and showing status\n * 2. TranscriptSummaryEditor for displaying/editing the video summary and transcript\n *\n * Used by both CustomerInterview and ProductRelease modals.\n */\nexport function TranscribeAndSummarizeCombinedSection({\n  // AIEnrichSection props\n  onTranscribe,\n  isProcessing,\n  canEnrich,\n  requiredFields,\n  status,\n  statusMessage,\n  disabledMessage = 'Upload a video first',\n  showCancel = true,\n  onCancel,\n  isCancelling = false,\n  hasResult = false,\n  buttonLabel,\n  title = 'Transcribe & Summarize',\n  description = 'Generate transcript with speaker diarization and AI summary',\n  loadingLabel = 'Processing...',\n  // TranscriptSummaryEditor props\n  videoSummary = '',\n  onVideoSummaryChange,\n  transcript = '',\n  onTranscriptChange,\n  isAIGenerated = false,\n  videoSummaryConfidence,\n  transcriptConfidence,\n  videoSummaryLabel = 'Video Summary (Markdown supported)',\n  videoSummaryHelperText = 'AI-generated summary from video content.',\n  videoSummaryPlaceholder = 'Summary of the video content...',\n  transcriptLabel = 'Video Transcript',\n  transcriptHelperText = 'Full video transcript with speaker diarization and timestamps',\n  transcriptPlaceholder = '**[00:00] Speaker Name:** Text here...',\n  transcriptMinHeight,\n  subtitles,\n  onSubtitlesChange,\n  disabled = false,\n  className = '',\n}: TranscribeAndSummarizeCombinedSectionProps) {\n  const defaultButtonLabel = hasResult ? 'Regenerate' : 'Generate';\n\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* 1. AI Enrich Button Section */}\n      <AIEnrichSection\n        title={title}\n        description={description}\n        icon={<Sparkles className=\"h-5 w-5\" />}\n        buttonLabel={buttonLabel || defaultButtonLabel}\n        loadingLabel={loadingLabel}\n        onEnrich={onTranscribe}\n        loading={isProcessing}\n        canEnrich={canEnrich}\n        requiredFields={requiredFields}\n        status={status}\n        statusMessage={statusMessage}\n        disabledMessage={disabledMessage}\n        showCancel={showCancel}\n        onCancel={onCancel}\n        isCancelling={isCancelling}\n      />\n\n      {/* 2. Transcript, Summary & Subtitles Editor */}\n      <TranscriptSummaryEditor\n        videoSummary={videoSummary}\n        onVideoSummaryChange={onVideoSummaryChange}\n        transcript={transcript}\n        onTranscriptChange={onTranscriptChange}\n        isAIGenerated={isAIGenerated}\n        videoSummaryConfidence={videoSummaryConfidence}\n        transcriptConfidence={transcriptConfidence}\n        videoSummaryLabel={videoSummaryLabel}\n        videoSummaryHelperText={videoSummaryHelperText}\n        videoSummaryPlaceholder={videoSummaryPlaceholder}\n        transcriptLabel={transcriptLabel}\n        transcriptHelperText={transcriptHelperText}\n        transcriptPlaceholder={transcriptPlaceholder}\n        transcriptMinHeight={transcriptMinHeight}\n        subtitles={subtitles}\n        onSubtitlesChange={onSubtitlesChange}\n        disabled={disabled}\n      />\n    </div>\n  );\n}\n\nexport default TranscribeAndSummarizeCombinedSection;\n","'use client';\n\nimport { Sparkles, Upload } from 'lucide-react';\nimport type { ReactNode } from 'react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Badge } from '../ui/badge';\nimport { Button } from '../ui/button';\nimport { Label } from '../ui/label';\nimport { AIEnrichSection } from './ai-enrich';\nimport type { AIRequiredField } from './ai-enrich';\nimport { HighlightConfigSection } from './highlight-config-section';\nimport { SubtitlesEditor } from './subtitles-editor';\n\nexport interface HighlightVideoCombinedSectionProps {\n  // ===== HighlightConfigSection Props =====\n  /** Target highlight duration in seconds */\n  targetDurationSeconds: number;\n  /** Handler for duration changes */\n  onTargetDurationChange: (seconds: number) => void;\n  /** Whether config section is disabled */\n  configDisabled?: boolean;\n\n  // ===== HighlightGenerationSection Props =====\n  /** Handler to trigger highlight generation */\n  onGenerateHighlight: () => void;\n  /** Whether generation is currently processing */\n  isProcessing: boolean;\n  /** Whether the generate button can be clicked */\n  canEnrich: boolean;\n  /** Required fields to display */\n  requiredFields: AIRequiredField[];\n  /** Current status of the operation */\n  status?: 'loading' | 'success' | 'error' | undefined;\n  /** Status message to display */\n  statusMessage?: string;\n  /** Message shown when button is disabled */\n  disabledMessage?: string;\n  /** Whether cancel is available */\n  showCancel?: boolean;\n  /** Handler to cancel processing */\n  onCancel?: () => void;\n  /** Whether cancellation is in progress */\n  isCancelling?: boolean;\n  /** Whether result already exists (affects button label) */\n  hasResult?: boolean;\n  /** Custom title */\n  title?: string;\n  /** Custom description - if not provided, auto-generated from duration */\n  description?: string;\n  /** Custom button label */\n  buttonLabel?: string;\n  /** Custom loading label */\n  loadingLabel?: string;\n\n  // ===== HighlightVideoPreview Props =====\n  /** Highlight video URL */\n  highlightVideoUrl?: string | null;\n  /** Video thumbnail URL */\n  highlightVideoThumbnail?: string | null;\n  /** Video duration in milliseconds */\n  highlightVideoDurationMs?: number | null;\n  /** Whether video was AI generated */\n  highlightVideoSource?: 'manual' | 'ai_generated' | null;\n  /** Handler to upload a new highlight video - receives File, should return uploaded URL */\n  onUpload: (file: File) => Promise<string>;\n  /** Handler to delete the highlight video */\n  onDelete: () => Promise<void>;\n  /** Whether upload is in progress */\n  isUploading?: boolean;\n  /** Custom label for preview section */\n  previewLabel?: string;\n  /** Custom empty state message */\n  emptyMessage?: string;\n  /** Upload progress component (optional) */\n  uploadProgressComponent?: ReactNode;\n\n  // ===== Highlight Subtitles (SRT) Props =====\n  /** Highlight captions SRT content (highlight_srt_content) */\n  subtitles?: string;\n  /** Callback when highlight subtitles change */\n  onSubtitlesChange?: (value: string) => void;\n  /** Whether the highlight subtitles were AI generated */\n  isSubtitlesAIGenerated?: boolean;\n  /** Video preview component - receives video data, should render the preview */\n  renderVideoPreview: (props: {\n    videoUrl: string;\n    title: string;\n    thumbnailUrl?: string;\n    onDelete: () => Promise<void>;\n  }) => ReactNode;\n\n  /** Additional class name */\n  className?: string;\n}\n\n/**\n * HighlightVideoCombinedSection - Unified component combining Config + AI Enrich + Preview\n *\n * This component provides a complete UI for highlight video generation with:\n * 1. HighlightConfigSection for duration and subtitle settings\n * 2. AIEnrichSection for triggering generation and showing status\n * 3. HighlightVideoPreview for viewing/uploading highlight videos\n *\n * Used by both CustomerInterview and ProductRelease modals.\n */\nexport function HighlightVideoCombinedSection({\n  // Config props\n  targetDurationSeconds,\n  onTargetDurationChange,\n  configDisabled = false,\n  // Generation props\n  onGenerateHighlight,\n  isProcessing,\n  canEnrich,\n  requiredFields,\n  status,\n  statusMessage,\n  disabledMessage = 'Upload a video and run transcription first',\n  showCancel = true,\n  onCancel,\n  isCancelling = false,\n  hasResult = false,\n  title = 'Highlight Video',\n  description,\n  buttonLabel,\n  loadingLabel = 'Generating highlight...',\n  // Preview props\n  highlightVideoUrl,\n  highlightVideoThumbnail,\n  highlightVideoDurationMs,\n  highlightVideoSource,\n  onUpload,\n  onDelete,\n  isUploading = false,\n  previewLabel = 'Highlight Video',\n  emptyMessage = 'No highlight video yet. Use AI generation above or upload manually.',\n  uploadProgressComponent,\n  // Highlight subtitles props\n  subtitles,\n  onSubtitlesChange,\n  isSubtitlesAIGenerated = false,\n  renderVideoPreview,\n  // Common\n  className = '',\n}: HighlightVideoCombinedSectionProps) {\n  const defaultButtonLabel = hasResult ? 'Regenerate Highlight' : 'Generate Highlight';\n  const defaultDescription = `Generate a ${Math.floor(targetDurationSeconds / 60)}-minute summary video using Claude AI + Shotstack`;\n\n  const formatDuration = (ms: number) => {\n    const minutes = Math.floor(ms / 60000);\n    const seconds = Math.floor((ms % 60000) / 1000);\n    return `${minutes}:${String(seconds).padStart(2, '0')}`;\n  };\n\n  const handleUploadClick = () => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = 'video/*';\n    input.onchange = async (e: Event) => {\n      const target = e.target as HTMLInputElement;\n      const file = target.files?.[0];\n      if (!file) return;\n      await onUpload(file);\n    };\n    input.click();\n  };\n\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* 2. AI Enrich Button Section */}\n      <AIEnrichSection\n        configSlot={\n          <HighlightConfigSection\n            targetDurationSeconds={targetDurationSeconds}\n            onTargetDurationChange={onTargetDurationChange}\n            disabled={configDisabled}\n          />\n        }\n        title={title}\n        description={description || defaultDescription}\n        icon={<Sparkles className=\"h-5 w-5\" />}\n        buttonLabel={buttonLabel || defaultButtonLabel}\n        loadingLabel={loadingLabel}\n        onEnrich={onGenerateHighlight}\n        loading={isProcessing}\n        canEnrich={canEnrich}\n        requiredFields={requiredFields}\n        status={status}\n        statusMessage={statusMessage}\n        disabledMessage={disabledMessage}\n        showCancel={showCancel}\n        onCancel={onCancel}\n        isCancelling={isCancelling}\n      />\n\n      {/* 3. Preview + Manual Upload Section */}\n      <div className=\"space-y-2\">\n        <div className=\"mb-2 flex items-center justify-between\">\n          <div className=\"flex items-center gap-2\">\n            <Label>{previewLabel}</Label>\n            {highlightVideoSource === 'ai_generated' && <AIGeneratedBadge />}\n            {highlightVideoDurationMs && (\n              <Badge variant=\"outline\" className=\"text-h6\">\n                {formatDuration(highlightVideoDurationMs)}\n              </Badge>\n            )}\n          </div>\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"small-legacy\"\n            leftIcon={<Upload className=\"h-4 w-4\" />}\n            onClick={handleUploadClick}\n            disabled={isUploading}\n          >\n            {isUploading ? 'Uploading...' : 'Upload Highlight'}\n          </Button>\n        </div>\n\n        {uploadProgressComponent}\n\n        {highlightVideoUrl ? (\n          renderVideoPreview({\n            videoUrl: highlightVideoUrl,\n            title: 'Highlight Video',\n            thumbnailUrl: highlightVideoThumbnail || undefined,\n            onDelete,\n          })\n        ) : (\n          <p className=\"italic text-ods-text-secondary text-h6\">{emptyMessage}</p>\n        )}\n      </div>\n\n      {/* 4. Highlight Subtitles (SRT) — the SAME SubtitlesEditor the main\n          video's TranscriptSummaryEditor renders, bound to\n          highlight_srt_content instead of srt_content. */}\n      <SubtitlesEditor\n        id=\"highlight-subtitles\"\n        subtitles={subtitles}\n        onSubtitlesChange={onSubtitlesChange}\n        label=\"Highlight Subtitles (SRT)\"\n        helperText=\"SRT captions transcribed from the highlight reel (AssemblyAI). Served as a selectable text track on the highlight player. Editable for fine-tuning.\"\n        isAIGenerated={isSubtitlesAIGenerated}\n      />\n    </div>\n  );\n}\n\nexport default HighlightVideoCombinedSection;\n","'use client';\n\nimport { cn } from '../../utils/cn';\nimport { GridViewIcon, TableViewIcon } from '../icons';\nimport { ToggleGroup, ToggleGroupItem } from '../ui';\n\ntype ViewMode = 'grid' | 'table';\n\ninterface ViewToggleProps {\n  /**\n   * Current selected view mode\n   */\n  value: ViewMode;\n  /**\n   * Callback fired when view mode changes\n   */\n  onValueChange: (value: ViewMode) => void;\n  /**\n   * Whether the toggle is disabled\n   */\n  disabled?: boolean;\n  /**\n   * Additional CSS classes for the toggle group\n   */\n  className?: string;\n  /**\n   * Size of the toggle buttons\n   */\n  size?: 'default' | 'sm' | 'lg';\n  /**\n   * Custom ARIA label for accessibility\n   */\n  'aria-label'?: string;\n}\n\n/**\n * ViewToggle - A 2-state button component for switching between grid and table views\n *\n * Built on top of Radix UI ToggleGroup for accessibility and proper keyboard navigation.\n * Uses ODS design tokens for consistent theming across platforms.\n *\n * @example\n * ```tsx\n * const [viewMode, setViewMode] = useState<'grid' | 'table'>('table')\n *\n * <ViewToggle\n *   value={viewMode}\n *   onValueChange={setViewMode}\n *   className=\"bg-ods-card border border-ods-border\"\n * />\n * ```\n */\nexport function ViewToggle({\n  value,\n  onValueChange,\n  disabled = false,\n  className,\n  size = 'default',\n  'aria-label': ariaLabel = 'Switch between grid and table view',\n}: ViewToggleProps) {\n  return (\n    <ToggleGroup\n      type=\"single\"\n      value={value}\n      onValueChange={(newValue: ViewMode) => {\n        // Only update if we have a valid value (user clicked a different option)\n        if (newValue && newValue !== value) {\n          onValueChange(newValue);\n        }\n      }}\n      className={cn('flex rounded-[6px] border border-ods-border bg-ods-card p-1', className)}\n      disabled={disabled}\n      aria-label={ariaLabel}\n    >\n      <ToggleGroupItem\n        value=\"grid\"\n        size={size}\n        className={cn(\n          'rounded p-2 transition-all duration-200',\n          value === 'grid'\n            ? 'bg-ods-accent text-ods-text-on-accent'\n            : 'text-ods-text-secondary hover:bg-ods-bg-hover hover:text-ods-text-primary',\n          disabled && 'cursor-not-allowed opacity-50',\n        )}\n        aria-label=\"Grid view\"\n        disabled={disabled}\n      >\n        <GridViewIcon\n          className=\"h-5 w-5\"\n          color={\n            disabled\n              ? 'var(--ods-system-greys-grey)'\n              : value === 'grid'\n                ? 'var(--ods-system-greys-black)'\n                : 'var(--ods-system-greys-grey)'\n          }\n        />\n      </ToggleGroupItem>\n\n      <ToggleGroupItem\n        value=\"table\"\n        size={size}\n        className={cn(\n          'rounded p-2 transition-all duration-200',\n          value === 'table'\n            ? 'bg-ods-accent text-ods-text-on-accent'\n            : 'text-ods-text-secondary hover:bg-ods-bg-hover hover:text-ods-text-primary',\n          disabled && 'cursor-not-allowed opacity-50',\n        )}\n        aria-label=\"Table view\"\n        disabled={disabled}\n      >\n        <TableViewIcon\n          className=\"h-5 w-5\"\n          color={\n            disabled\n              ? 'var(--ods-system-greys-grey)'\n              : value === 'table'\n                ? 'var(--ods-system-greys-black)'\n                : 'var(--ods-system-greys-grey)'\n          }\n        />\n      </ToggleGroupItem>\n    </ToggleGroup>\n  );\n}\n\n// Type exports for consumers\nexport type { ViewMode, ViewToggleProps };\n","'use client';\n\nimport { ChevronDown, Shield } from 'lucide-react';\nimport type { FC } from 'react';\nimport { useState } from 'react';\nimport type {\n  ApprovalLevel,\n  PermissionCategory,\n  PermissionPolicy,\n  PolicyConfigurationPanelProps,\n} from '../../types/permissions';\nimport { cn } from '../../utils/cn';\nimport { useCollapsible } from '../chat/hooks/use-collapsible';\nimport { ToolIcon } from '../tool-icon';\nimport { Button } from '../ui/button';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu';\n\nconst approvalLevelLabels: Record<ApprovalLevel, string> = {\n  ALLOW: 'Allow',\n  ASK_USER: 'Ask User',\n  ASK_TECHNICIAN: 'Ask Technician',\n  DENY: 'Restrict',\n};\n\nconst approvalLevelOptions = (Object.keys(approvalLevelLabels) as ApprovalLevel[]).map(value => ({\n  value,\n  label: approvalLevelLabels[value],\n}));\n\nconst POLICY_LEVEL_WIDTH = 'w-[180px]';\nconst CATEGORY_LEVEL_WIDTH = 'w-[256px]';\n\nconst getApprovalLevelLabel = (level: ApprovalLevel): string => approvalLevelLabels[level] ?? level;\n\nconst ApprovalLevelDropdown: FC<{\n  value: ApprovalLevel | undefined;\n  onChange: (level: ApprovalLevel | undefined) => void;\n  /** Adds the \"Clear Global Permission\" item (category bulk dropdown). */\n  allowClear?: boolean;\n  triggerClassName?: string;\n  contentClassName?: string;\n}> = ({ value, onChange, allowClear = false, triggerClassName, contentClassName }) => (\n  <DropdownMenu>\n    <DropdownMenuTrigger asChild>\n      <Button\n        variant=\"outline\"\n        rightIcon={<ChevronDown className=\"h-6 w-6 text-ods-text-secondary\" />}\n        className={cn(\n          'flex h-auto items-center justify-between gap-[var(--spacing-system-xsf)] rounded-md border border-ods-border bg-ods-card !px-[var(--spacing-system-xsf)] py-[var(--spacing-system-xsf)] transition-colors !text-h6 hover:bg-ods-bg-hover',\n          value ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n          triggerClassName,\n        )}\n      >\n        {value ? getApprovalLevelLabel(value) : 'Set Global Permission'}\n      </Button>\n    </DropdownMenuTrigger>\n    <DropdownMenuContent align=\"end\" className={contentClassName}>\n      {allowClear && (\n        <DropdownMenuItem onClick={() => onChange(undefined)} className=\"text-h6\">\n          Clear Global Permission\n        </DropdownMenuItem>\n      )}\n      {approvalLevelOptions.map(option => (\n        <DropdownMenuItem key={option.value} onClick={() => onChange(option.value)} className=\"text-h6\">\n          {option.label}\n        </DropdownMenuItem>\n      ))}\n    </DropdownMenuContent>\n  </DropdownMenu>\n);\n\nconst PolicyRow: FC<{\n  policy: PermissionPolicy;\n  categoryId: string;\n  editMode: boolean;\n  onPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;\n}> = ({ policy, categoryId, editMode, onPermissionChange }) => {\n  const handleChange = (level: ApprovalLevel | undefined) => {\n    if (level) onPermissionChange(categoryId, policy.id, level);\n  };\n\n  return (\n    <div className=\"flex items-start gap-[var(--spacing-system-m)] border-b border-ods-border bg-ods-bg px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] last:border-b-0 md:items-center\">\n      {/* Tool Icon */}\n      <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg\">\n        <ToolIcon toolType={policy.toolName} size={16} />\n      </div>\n\n      {/* Policy Info — on mobile the approval level stacks below the pattern */}\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <p className=\"truncate text-ods-text-primary text-h4\" title={policy.name}>\n          {policy.name}\n        </p>\n        <p className=\"break-all text-ods-text-secondary text-h6\">{policy.commandPattern}</p>\n        {editMode ? (\n          <div className=\"mt-[var(--spacing-system-xsf)] md:hidden\">\n            <ApprovalLevelDropdown\n              value={policy.approvalLevel}\n              onChange={handleChange}\n              triggerClassName=\"w-full\"\n              contentClassName={POLICY_LEVEL_WIDTH}\n            />\n          </div>\n        ) : policy.approvalLevel ? (\n          <span className=\"text-ods-text-primary text-h4 md:hidden\">{getApprovalLevelLabel(policy.approvalLevel)}</span>\n        ) : null}\n      </div>\n\n      {/* Approval Level column (desktop only) */}\n      <div className=\"hidden shrink-0 md:block\">\n        {editMode ? (\n          <ApprovalLevelDropdown\n            value={policy.approvalLevel}\n            onChange={handleChange}\n            triggerClassName={POLICY_LEVEL_WIDTH}\n            contentClassName={POLICY_LEVEL_WIDTH}\n          />\n        ) : (\n          <div className={cn('px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)]', POLICY_LEVEL_WIDTH)}>\n            {policy.approvalLevel && (\n              <span className=\"text-ods-text-primary text-h4\">{getApprovalLevelLabel(policy.approvalLevel)}</span>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst CategorySection: FC<{\n  category: PermissionCategory;\n  editMode: boolean;\n  isExpanded: boolean;\n  /** Last bulk level applied to this category (display-only, edit mode). */\n  bulkLevel: ApprovalLevel | undefined;\n  onToggle: (categoryId: string) => void;\n  onBulkLevelChange: (categoryId: string, level: ApprovalLevel | undefined) => void;\n  onPolicyPermissionChange: (categoryId: string, policyId: string, level: ApprovalLevel) => void;\n}> = ({ category, editMode, isExpanded, bulkLevel, onToggle, onBulkLevelChange, onPolicyPermissionChange }) => {\n  const { innerRef, containerStyle } = useCollapsible({ expanded: isExpanded, durationMs: 300 });\n  const handleBulkChange = (level: ApprovalLevel | undefined) => onBulkLevelChange(category.id, level);\n\n  return (\n    <>\n      {/* Category Header */}\n      <div\n        className=\"flex cursor-pointer items-center gap-[var(--spacing-system-m)] border-t border-ods-border bg-ods-card py-[var(--spacing-system-s)] pl-[var(--spacing-system-m)] pr-[var(--spacing-system-xsf)] transition-colors first:border-t-0 hover:bg-ods-bg-hover\"\n        onClick={() => onToggle(category.id)}\n      >\n        {/* Category Icon */}\n        <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg\">\n          <div className=\"text-ods-text-secondary\">{category.icon ?? <Shield className=\"h-4 w-4\" />}</div>\n        </div>\n\n        {/* Category Info — on mobile the bulk dropdown stacks below the count */}\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <p className=\"truncate text-ods-text-primary text-h4\" title={category.name}>\n            {category.name}\n          </p>\n          <p className=\"text-ods-text-secondary text-h6\">{category.policies.length} Configurations</p>\n          {editMode && (\n            <div className=\"mt-[var(--spacing-system-xsf)] md:hidden\" onClick={e => e.stopPropagation()}>\n              <ApprovalLevelDropdown\n                value={bulkLevel}\n                onChange={handleBulkChange}\n                allowClear\n                triggerClassName=\"w-full\"\n                contentClassName={CATEGORY_LEVEL_WIDTH}\n              />\n            </div>\n          )}\n        </div>\n\n        {/* Global Permission column (desktop only) */}\n        {editMode ? (\n          <div className=\"hidden shrink-0 md:block\" onClick={e => e.stopPropagation()}>\n            <ApprovalLevelDropdown\n              value={bulkLevel}\n              onChange={handleBulkChange}\n              allowClear\n              triggerClassName={CATEGORY_LEVEL_WIDTH}\n              contentClassName={CATEGORY_LEVEL_WIDTH}\n            />\n          </div>\n        ) : (\n          <div\n            className={cn(\n              'hidden shrink-0 px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] md:block',\n              CATEGORY_LEVEL_WIDTH,\n            )}\n          />\n        )}\n\n        {/* Expand/Collapse Button */}\n        <Button\n          type=\"button\"\n          variant=\"transparent\"\n          onClick={e => {\n            e.stopPropagation();\n            onToggle(category.id);\n          }}\n          leftIcon={\n            <ChevronDown\n              className={cn(\n                'h-6 w-6 text-ods-text-primary transition-transform duration-300',\n                isExpanded && 'rotate-180',\n              )}\n            />\n          }\n          className=\"h-auto w-auto shrink-0 rounded-md\"\n          aria-label={isExpanded ? 'Collapse' : 'Expand'}\n        />\n      </div>\n\n      {/* Policies List */}\n      <div\n        style={{\n          ...containerStyle,\n          transition: `${containerStyle.transition}, opacity 300ms ease-in-out, visibility 300ms`,\n          // visibility drops collapsed content from the tab order and a11y\n          // tree; its discrete transition delays hiding until the fade ends.\n          visibility: isExpanded ? 'visible' : 'hidden',\n        }}\n        className={isExpanded ? 'opacity-100' : 'opacity-0'}\n      >\n        <div ref={innerRef} className=\"px-[var(--spacing-system-mf)] pb-[var(--spacing-system-mf)] pt-0\">\n          <div className=\"overflow-hidden rounded-md border border-ods-border\">\n            {category.policies.map(policy => (\n              <PolicyRow\n                key={policy.id}\n                policy={policy}\n                categoryId={category.id}\n                editMode={editMode}\n                onPermissionChange={onPolicyPermissionChange}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n    </>\n  );\n};\n\n/**\n * Grouped list of permission categories with per-policy approval dropdowns.\n * Presentation state — which categories are expanded and the last bulk level\n * picked per category — lives inside the panel, so callers pass pure data and\n * background data refreshes never collapse the user's view. Bulk selections\n * reset when `editMode` changes.\n *\n * Responsive: below `md` the approval level / dropdowns stack inside the text\n * column (per the ODS mobile design); at `md+` they render as fixed-width\n * right-hand columns.\n */\n/** One shared empty map, so a reset that changes nothing changes no identity. */\nconst NO_BULK_LEVELS: Record<string, ApprovalLevel | undefined> = {};\n\nexport const PolicyConfigurationPanel: FC<PolicyConfigurationPanelProps> = ({\n  categories,\n  editMode,\n  onPolicyPermissionChange,\n  onCategoryPermissionChange,\n  className,\n}) => {\n  const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(new Set());\n  const [bulkLevels, setBulkLevels] = useState<Record<string, ApprovalLevel | undefined>>(NO_BULK_LEVELS);\n\n  // `editMode` is the reset TRIGGER, not an input — the body reads nothing.\n  // Adjusted while rendering (React's documented prop-sync pattern) rather than\n  // from an effect: the toggle has already scheduled this render, so the stale\n  // bulk selections are gone in it instead of being painted once and then\n  // cleared by a second pass.\n  const [bulkLevelsFor, setBulkLevelsFor] = useState(editMode);\n  if (bulkLevelsFor !== editMode) {\n    setBulkLevelsFor(editMode);\n    setBulkLevels(NO_BULK_LEVELS);\n  }\n\n  const handleToggle = (categoryId: string) => {\n    setExpandedIds(prev => {\n      const next = new Set(prev);\n      if (next.has(categoryId)) next.delete(categoryId);\n      else next.add(categoryId);\n      return next;\n    });\n  };\n\n  const handleBulkLevelChange = (categoryId: string, level: ApprovalLevel | undefined) => {\n    setBulkLevels(prev => ({ ...prev, [categoryId]: level }));\n    if (level) {\n      onCategoryPermissionChange(categoryId, level);\n    }\n  };\n\n  return (\n    <div className={cn('overflow-hidden rounded-md border border-ods-border bg-ods-card', className)}>\n      {categories.map(category => (\n        <CategorySection\n          key={category.id}\n          category={category}\n          editMode={editMode}\n          isExpanded={expandedIds.has(category.id)}\n          bulkLevel={bulkLevels[category.id]}\n          onToggle={handleToggle}\n          onBulkLevelChange={handleBulkLevelChange}\n          onPolicyPermissionChange={onPolicyPermissionChange}\n        />\n      ))}\n    </div>\n  );\n};\n\nPolicyConfigurationPanel.displayName = 'PolicyConfigurationPanel';\n","'use client';\n\nimport { getCountries } from 'libphonenumber-js';\nimport type { CountryCode } from 'libphonenumber-js';\nimport { useEffect, useState } from 'react';\nimport type { KeyboardEvent } from 'react';\nimport { useIsHydrated } from '../../hooks/ui/use-is-hydrated';\nimport { useHumanitySignals } from '../../hooks/use-humanity-signals';\nimport { useToast } from '../../hooks/use-toast';\nimport { cn } from '../../utils/cn';\nimport { formatPhoneE164 } from '../../utils/country-phone-utils';\nimport { hasGenericEmailDomain } from '../../utils/generic-domain-utils';\nimport type { HumanitySignals } from '../../utils/humanity-signals';\nimport { OpenFrameLogo } from '../icons';\nimport { Button } from '../ui/button';\nimport { CheckboxBlock } from '../ui/checkbox-block';\nimport { HoneypotField } from '../ui/honeypot-field';\nimport { Input } from '../ui/input';\nimport { PhoneInput } from '../ui/phone-input';\nimport { PolicyLink } from '../ui/policy-link';\n\nexport interface WaitlistFormProps {\n  /** Optional ID for the form container (for anchor links) */\n  id?: string;\n  /** Optional CSS classes for the container */\n  className?: string;\n  /**\n   * Registration handler — called with email, optional E.164 phone, and the\n   * invisible bot-protection signals (honeypot + timing) to forward into the\n   * POST body. Must throw on failure (toast is handled by the form). `signals`\n   * is optional for backward compatibility with older callers.\n   */\n  onRegister: (email: string, phone?: string, signals?: HumanitySignals) => Promise<void>;\n  /** Whether a registration request is currently in flight */\n  isSubmitting?: boolean;\n  /** Whether registration completed successfully */\n  isSuccess?: boolean;\n  /** Pre-filled email (e.g. from auth context) */\n  defaultEmail?: string;\n  /** Pre-filled phone (e.g. from user profile) */\n  defaultPhone?: string;\n  /** Geo-detection API endpoint. Defaults to \"/api/geo\". Set to null to disable. */\n  geoApiUrl?: string | null;\n  /** Label on the submit button. Defaults to \"Get Beta Access\" */\n  submitLabel?: string;\n  /** Label shown after success. Defaults to \"You're in!\" */\n  successLabel?: string;\n  /** Label shown on the SMS consent checkbox */\n  smsCheckboxLabel?: string;\n  /** Warning shown when a generic email domain is detected */\n  genericEmailHint?: string;\n  /** Warning shown when phone validation fails */\n  invalidPhoneHint?: string;\n  /** URL for the Terms of Service link in the consent text */\n  termsOfServiceUrl?: string;\n  /** URL for the Privacy Policy link in the consent text */\n  privacyPolicyUrl?: string;\n  /** SMS consent text shown below the checkbox label */\n  consentText?: string;\n}\n\n/**\n * WaitlistForm\n *\n * Platform-agnostic waitlist registration form.\n * All app-specific logic (auth, API calls, platform detection) is injected via props.\n *\n * Features:\n * - Email + optional phone with country code selector\n * - Generic email domain warning\n * - Phone validation warning\n * - Auto geo-detection for country code\n * - Enter key support\n * - Loading and success states\n * - Hydration-safe skeleton\n */\nexport function WaitlistForm({\n  id = 'waitlist-form',\n  className,\n  onRegister,\n  isSubmitting = false,\n  isSuccess = false,\n  defaultEmail = '',\n  defaultPhone = '',\n  geoApiUrl = '/api/geo',\n  submitLabel = 'Get Beta Access',\n  successLabel = \"You're in!\",\n  smsCheckboxLabel = 'Send me an SMS if my email gets caught by spam filters',\n  genericEmailHint = 'Use a work email \\u2014 personal emails may not be verified or approved.',\n  invalidPhoneHint = 'Invalid phone number format.',\n  termsOfServiceUrl,\n  privacyPolicyUrl,\n  consentText = 'I agree to receive recurring automated text messages at the phone number provided. Msg & data rates may apply. Msg frequency varies. Reply HELP for help and STOP to cancel.',\n}: WaitlistFormProps) {\n  const [email, setEmail] = useState(defaultEmail);\n  const [phone, setPhone] = useState(defaultPhone);\n  const [countryCode, setCountryCode] = useState<CountryCode>('US');\n  const { toast } = useToast();\n  const { honeypotInputProps, getSignals, resetSignals } = useHumanitySignals();\n  const [smsConsent, setSmsConsent] = useState(false);\n  const isClient = useIsHydrated();\n  const [isPhoneInvalid, setIsPhoneInvalid] = useState(false);\n  const [showConsentError, setShowConsentError] = useState(false);\n\n  const isMailDomainGeneric = hasGenericEmailDomain(email);\n\n  // Sync defaultEmail when it changes (e.g. auth loads). Adjusted while\n  // rendering — React's documented prop-sync pattern — rather than from an\n  // effect: the field renders from `email`, so an effect showed the empty box\n  // for one frame after auth resolved and then filled it in.\n  const [syncedDefaultEmail, setSyncedDefaultEmail] = useState(defaultEmail);\n  if (syncedDefaultEmail !== defaultEmail) {\n    setSyncedDefaultEmail(defaultEmail);\n    if (defaultEmail) {\n      setEmail(defaultEmail);\n    }\n  }\n\n  // Geo detection (client-only; `isClient` above is the hydration gate).\n  useEffect(() => {\n    if (!geoApiUrl) return;\n\n    // `getCountries()` IS the runtime source of truth for the `CountryCode`\n    // union, so membership in it is the honest narrowing for a wire string —\n    // the geo endpoint's body is untyped (`Response.json()` is `any`) and the\n    // previous `country as CountryCode` asserted the very thing the `has`\n    // check was there to establish.\n    const supportedCountries = new Set<string>(getCountries());\n    const isSupportedCountry = (value: string): value is CountryCode => supportedCountries.has(value);\n\n    fetch(geoApiUrl)\n      .then((res): Promise<unknown> => res.json())\n      .then(body => {\n        const country = typeof body === 'object' && body !== null && 'country' in body ? body.country : undefined;\n        if (typeof country === 'string' && isSupportedCountry(country)) {\n          setCountryCode(country);\n        }\n      })\n      .catch(() => {\n        /* keep default US */\n      });\n  }, [geoApiUrl]);\n\n  const handleSubmit = async () => {\n    if (isSubmitting) return;\n    if (!email.trim()) {\n      toast({\n        title: 'Email required',\n        description: 'Please enter a valid email address.',\n        variant: 'destructive',\n      });\n      return;\n    }\n\n    if (phone.trim() && !smsConsent) {\n      setShowConsentError(true);\n      return;\n    }\n\n    const finalPhone = phone ? formatPhoneE164(phone, countryCode) : undefined;\n\n    try {\n      await onRegister(email, finalPhone, getSignals());\n      resetSignals();\n    } catch {\n      // caller's onRegister should handle its own error toasts if needed\n    }\n  };\n\n  if (!isClient) {\n    return (\n      <div\n        className={cn(\n          'flex flex-col gap-[var(--spacing-system-l)] rounded-[6px] border border-ods-border bg-ods-bg p-[var(--spacing-system-m)]',\n          className,\n        )}\n      >\n        {/* Email input skeleton */}\n        <div className=\"h-12 w-full animate-pulse rounded-[6px] border border-ods-border bg-ods-card\" />\n        {/* Phone input skeleton */}\n        <div className=\"flex w-full gap-[var(--spacing-system-xs)]\">\n          <div className=\"h-12 w-[130px] shrink-0 animate-pulse rounded-[6px] border border-ods-border bg-ods-card\" />\n          <div className=\"h-12 flex-1 animate-pulse rounded-[6px] border border-ods-border bg-ods-card\" />\n        </div>\n        {/* Disclaimer + button skeleton */}\n        <div className=\"flex w-full flex-col items-end gap-[var(--spacing-system-m)]\">\n          <div className=\"w-full animate-pulse rounded-[6px] border border-ods-border bg-ods-bg px-4 py-6\" />\n          <div className=\"h-12 w-[200px] animate-pulse rounded-[6px] border border-ods-border bg-ods-card\" />\n        </div>\n      </div>\n    );\n  }\n\n  const showEmailWarning = isMailDomainGeneric;\n  const showPhoneWarning = isPhoneInvalid;\n\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      // `handleSubmit` never rejects — it try/catches `onRegister` itself.\n      void handleSubmit();\n    }\n  };\n\n  return (\n    <div\n      id={id}\n      className={cn(\n        'flex flex-col items-end gap-[var(--spacing-system-l)] rounded-[6px] border border-ods-border bg-ods-bg p-[var(--spacing-system-m)]',\n        className,\n      )}\n    >\n      {/* Invisible honeypot — real users never fill it; bots that fill every field trip it. */}\n      <HoneypotField {...honeypotInputProps} />\n\n      {/* Email Input */}\n      <Input\n        type=\"email\"\n        value={email}\n        onChange={e => setEmail(e.target.value)}\n        required\n        disabled={isSubmitting}\n        placeholder=\"Enter your Business Email\"\n        onKeyDown={handleKeyDown}\n        error={showEmailWarning ? genericEmailHint : undefined}\n        errorVariant=\"warning\"\n      />\n\n      {/* Phone Input */}\n      <div className=\"relative w-full\">\n        <PhoneInput\n          value={phone}\n          countryCode={countryCode}\n          onPhoneChange={val => {\n            setPhone(val);\n            if (!val.trim()) setShowConsentError(false);\n          }}\n          onCountryChange={setCountryCode}\n          onValidationChange={setIsPhoneInvalid}\n          disabled={isSubmitting}\n          placeholder=\"Phone (optional)\"\n          onKeyDown={handleKeyDown}\n        />\n        {showPhoneWarning && (\n          <p\n            className=\"absolute bottom-0 left-0 translate-y-full truncate text-ods-warning text-h6\"\n            title={invalidPhoneHint}\n          >\n            {invalidPhoneHint}\n          </p>\n        )}\n      </div>\n      {/* SMS Consent + Button Section */}\n      <div className=\"flex w-full flex-col items-end gap-[var(--spacing-system-l)]\">\n        {/* SMS Consent Checkbox */}\n        <CheckboxBlock\n          checked={smsConsent}\n          onCheckedChange={checked => {\n            setSmsConsent(checked);\n            if (checked) setShowConsentError(false);\n          }}\n          error={showConsentError ? 'Please agree to SMS notifications to continue.' : undefined}\n          disabled={isSubmitting}\n          label={smsCheckboxLabel}\n          description={\n            <>\n              {consentText}\n              {' View our '}\n              <PolicyLink href={termsOfServiceUrl || '#'}>Terms of Service</PolicyLink>\n              {' and '}\n              <PolicyLink href={privacyPolicyUrl || '#'}>Privacy Policy</PolicyLink>.\n            </>\n          }\n        />\n\n        {/* Submit Button — right-aligned */}\n        <Button\n          type=\"button\"\n          loading={isSubmitting}\n          disabled={isSubmitting}\n          leftIcon={<OpenFrameLogo />}\n          onClick={handleSubmit}\n          className=\"w-full md:w-auto\"\n        >\n          {isSuccess ? successLabel : submitLabel}\n        </Button>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport {\n  type ReactNode,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useReducer,\n  useRef,\n  useState,\n} from 'react';\nimport type { AddNotificationInput, Notification, RenderNotificationTile } from './types';\n\ninterface NotificationsContextValue {\n  notifications: Notification[];\n  unreadCount: number;\n  isOpen: boolean;\n  showPopups: boolean;\n  addNotification: (input: AddNotificationInput) => string;\n  upsertNotification: (input: AddNotificationInput & { id: string }) => string;\n  setNotifications: (list: Notification[]) => void;\n  markRead: (id: string) => void;\n  markAllRead: () => void;\n  markSettled: (id: string) => void;\n  remove: (id: string) => void;\n  clear: () => void;\n  open: () => void;\n  close: () => void;\n  toggle: () => void;\n  setShowPopups: (value: boolean) => void;\n  /** OS-level notification preference; only meaningful when `desktopPopupsConfigured`. */\n  showDesktopPopups: boolean;\n  setShowDesktopPopups: (value: boolean) => void;\n  /** True when the host app wired desktop notifications (passed `onShowDesktopPopupsChange`). */\n  desktopPopupsConfigured: boolean;\n  onHistoryClick?: () => void;\n  /** Destination for the history button's \"open in a new tab\" split action. */\n  historyHref?: string;\n  hasMore: boolean;\n  isLoadingMore: boolean;\n  loadMore?: () => void;\n  renderTile?: RenderNotificationTile;\n}\n\nconst NotificationsContext = createContext<NotificationsContextValue | null>(null);\n\nexport interface NotificationsActions {\n  /** Called after the local reducer marks an item read; persist server-side. */\n  onMarkRead?: (id: string) => void | Promise<void>;\n  /** Called after the local reducer marks all read; persist server-side. */\n  onMarkAllRead?: () => void | Promise<void>;\n  /** Called after the local reducer removes an item; persist server-side. */\n  onRemove?: (id: string) => void | Promise<void>;\n  /** Called after the local reducer marks an item settled; optional persistence. */\n  onMarkSettled?: (id: string) => void | Promise<void>;\n}\n\nexport interface NotificationsProviderProps {\n  children: ReactNode;\n  initialNotifications?: Notification[];\n  maxNotifications?: number;\n  defaultShowPopups?: boolean;\n  onShowPopupsChange?: (value: boolean) => void;\n  /** Desktop (OS-level) notification preference; providing `onShowDesktopPopupsChange` opts into the drawer toggle. */\n  defaultShowDesktopPopups?: boolean;\n  onShowDesktopPopupsChange?: (value: boolean) => void;\n  onHistoryClick?: () => void;\n  /** Destination for the history button's \"open in a new tab\" split action. */\n  historyHref?: string;\n  actions?: NotificationsActions;\n  /** Pagination — when omitted, the drawer hides its load-more sentinel. */\n  hasMore?: boolean;\n  isLoadingMore?: boolean;\n  onLoadMore?: () => void;\n  /** Override how individual tiles render (drawer + popups); falls back to `NotificationTile`. */\n  renderTile?: RenderNotificationTile;\n}\n\ntype Action =\n  | { type: 'add'; notification: Notification; max: number }\n  | { type: 'upsert'; notification: Notification; max: number }\n  | { type: 'set'; notifications: Notification[] }\n  | { type: 'markRead'; id: string }\n  | { type: 'markAllRead' }\n  | { type: 'markSettled'; id: string }\n  | { type: 'remove'; id: string }\n  | { type: 'clear' };\n\nfunction mergeNotification(base: Notification, incoming: Notification): Notification {\n  const out: Notification = { ...base };\n  for (const key of Object.keys(incoming) as (keyof Notification)[]) {\n    const value = incoming[key];\n    if (value !== undefined) (out as unknown as Record<string, unknown>)[key] = value;\n  }\n\n  out.read = (incoming.read ?? false) || (base.read ?? false) || false;\n  out.settled = (incoming.settled ?? false) || (base.settled ?? false) || false;\n  return out;\n}\n\nfunction reducer(state: Notification[], action: Action): Notification[] {\n  switch (action.type) {\n    case 'add': {\n      const existing = state.find(n => n.id === action.notification.id);\n      if (existing) {\n        const merged = mergeNotification(existing, action.notification);\n        return [merged, ...state.filter(n => n.id !== action.notification.id)];\n      }\n      const next = [action.notification, ...state];\n      return next.length > action.max ? next.slice(0, action.max) : next;\n    }\n    case 'upsert': {\n      const existing = state.find(n => n.id === action.notification.id);\n      if (existing) {\n        const merged = mergeNotification(existing, action.notification);\n        return state.map(n => (n.id === action.notification.id ? merged : n));\n      }\n      const next = [action.notification, ...state];\n      return next.length > action.max ? next.slice(0, action.max) : next;\n    }\n    case 'set': {\n      const existingById = new Map(state.map(n => [n.id, n]));\n      return action.notifications.map(incoming => {\n        const existing = existingById.get(incoming.id);\n        if (!existing) return incoming;\n        return {\n          ...incoming,\n          read: incoming.read || existing.read,\n          settled: incoming.settled || existing.settled,\n        };\n      });\n    }\n    case 'markRead':\n      return state.map(n => (n.id === action.id ? { ...n, read: true } : n));\n    case 'markAllRead':\n      return state.map(n => (n.read ? n : { ...n, read: true }));\n    case 'markSettled':\n      return state.map(n => (n.id === action.id && !n.settled ? { ...n, settled: true } : n));\n    case 'remove':\n      return state.filter(n => n.id !== action.id);\n    case 'clear':\n      return [];\n    default:\n      return state;\n  }\n}\n\nfunction generateId() {\n  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n    return crypto.randomUUID();\n  }\n  return `n_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function NotificationsProvider({\n  children,\n  initialNotifications = [],\n  maxNotifications = 50,\n  defaultShowPopups = true,\n  onShowPopupsChange,\n  defaultShowDesktopPopups = false,\n  onShowDesktopPopupsChange,\n  onHistoryClick,\n  historyHref,\n  actions,\n  hasMore = false,\n  isLoadingMore = false,\n  onLoadMore,\n  renderTile,\n}: NotificationsProviderProps) {\n  const [notifications, dispatch] = useReducer(reducer, initialNotifications);\n  const [isOpen, setIsOpen] = useState(false);\n  const [showPopups, setShowPopupsState] = useState(defaultShowPopups);\n  const [showDesktopPopups, setShowDesktopPopupsState] = useState(defaultShowDesktopPopups);\n  const desktopPopupsConfigured = onShowDesktopPopupsChange !== undefined;\n\n  const actionsRef = useRef(actions);\n  useEffect(() => {\n    actionsRef.current = actions;\n  }, [actions]);\n\n  const addNotification = useCallback(\n    (input: AddNotificationInput) => {\n      const id = input.id ?? generateId();\n      const notification: Notification = {\n        ...input,\n        id,\n        createdAt: input.createdAt ?? Date.now(),\n        read: input.read ?? false,\n      };\n      dispatch({ type: 'add', notification, max: maxNotifications });\n      return id;\n    },\n    [maxNotifications],\n  );\n\n  const upsertNotification = useCallback(\n    (input: AddNotificationInput & { id: string }) => {\n      const notification: Notification = {\n        ...input,\n        id: input.id,\n        createdAt: input.createdAt ?? Date.now(),\n        read: input.read ?? false,\n      };\n      dispatch({ type: 'upsert', notification, max: maxNotifications });\n      return input.id;\n    },\n    [maxNotifications],\n  );\n\n  const setNotifications = useCallback((list: Notification[]) => {\n    dispatch({ type: 'set', notifications: list });\n  }, []);\n\n  const markRead = useCallback((id: string) => {\n    dispatch({ type: 'markRead', id });\n    void actionsRef.current?.onMarkRead?.(id);\n  }, []);\n\n  const markAllRead = useCallback(() => {\n    dispatch({ type: 'markAllRead' });\n    void actionsRef.current?.onMarkAllRead?.();\n  }, []);\n\n  const markSettled = useCallback((id: string) => {\n    dispatch({ type: 'markSettled', id });\n    void actionsRef.current?.onMarkSettled?.(id);\n  }, []);\n\n  const remove = useCallback((id: string) => {\n    dispatch({ type: 'remove', id });\n    void actionsRef.current?.onRemove?.(id);\n  }, []);\n\n  const clear = useCallback(() => dispatch({ type: 'clear' }), []);\n\n  const open = useCallback(() => setIsOpen(true), []);\n  const close = useCallback(() => setIsOpen(false), []);\n  const toggle = useCallback(() => setIsOpen(v => !v), []);\n\n  const setShowPopups = useCallback(\n    (value: boolean) => {\n      setShowPopupsState(value);\n      onShowPopupsChange?.(value);\n    },\n    [onShowPopupsChange],\n  );\n\n  const setShowDesktopPopups = useCallback(\n    (value: boolean) => {\n      setShowDesktopPopupsState(value);\n      onShowDesktopPopupsChange?.(value);\n    },\n    [onShowDesktopPopupsChange],\n  );\n\n  const unreadCount = useMemo(() => notifications.reduce((acc, n) => (n.read ? acc : acc + 1), 0), [notifications]);\n\n  const value = useMemo<NotificationsContextValue>(\n    () => ({\n      notifications,\n      unreadCount,\n      isOpen,\n      showPopups,\n      addNotification,\n      upsertNotification,\n      setNotifications,\n      markRead,\n      markAllRead,\n      markSettled,\n      remove,\n      clear,\n      open,\n      close,\n      toggle,\n      setShowPopups,\n      showDesktopPopups,\n      setShowDesktopPopups,\n      desktopPopupsConfigured,\n      onHistoryClick,\n      historyHref,\n      hasMore,\n      isLoadingMore,\n      loadMore: onLoadMore,\n      renderTile,\n    }),\n    [\n      notifications,\n      unreadCount,\n      isOpen,\n      showPopups,\n      showDesktopPopups,\n      setShowDesktopPopups,\n      desktopPopupsConfigured,\n      addNotification,\n      upsertNotification,\n      setNotifications,\n      markRead,\n      markAllRead,\n      markSettled,\n      remove,\n      clear,\n      open,\n      close,\n      toggle,\n      setShowPopups,\n      onHistoryClick,\n      historyHref,\n      hasMore,\n      isLoadingMore,\n      onLoadMore,\n      renderTile,\n    ],\n  );\n\n  return <NotificationsContext.Provider value={value}>{children}</NotificationsContext.Provider>;\n}\n\nexport function useNotifications(): NotificationsContextValue {\n  const ctx = useContext(NotificationsContext);\n  if (!ctx) {\n    throw new Error('useNotifications must be used inside <NotificationsProvider>');\n  }\n  return ctx;\n}\n\nexport function useOptionalNotifications(): NotificationsContextValue | null {\n  return useContext(NotificationsContext);\n}\n","'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { useNotificationPermission } from '../../../hooks/ui';\nimport { cn } from '../../../utils/cn';\nimport { ClockHistoryIcon, ArrowRightUpIcon } from '../../icons-v2-generated';\nimport { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon';\nimport { useAppLayoutDrawerContainer } from '../../navigation/app-layout-context';\nimport { AppLayoutDrawer, AppLayoutDrawerContent } from '../../navigation/app-layout-drawer';\nimport { SplitButton } from '../../ui/button';\nimport { Drawer, DrawerContent, DrawerTitle } from '../../ui/drawer';\nimport { Switch } from '../../ui/switch';\nimport { NotificationTile } from './notification-tile';\nimport { useOptionalNotifications } from './notifications-context';\nimport type { Notification, RenderNotificationTile } from './types';\n\nexport interface NotificationDrawerProps {\n  className?: string;\n  liveDurationMs?: number;\n  loadMoreRootMargin?: string;\n  renderTile?: RenderNotificationTile;\n}\n\nexport function NotificationDrawer({\n  className,\n  liveDurationMs,\n  loadMoreRootMargin = '200px',\n  renderTile: renderTileProp,\n}: NotificationDrawerProps) {\n  const ctx = useOptionalNotifications();\n  // Inside AppLayout the drawer renders in-layout (dims and covers only the\n  // main content area — header and sidebar stay interactive). Outside of it\n  // (Storybook, non-AppLayout hosts) it falls back to the viewport Drawer.\n  const layoutContainer = useAppLayoutDrawerContainer();\n\n  if (!ctx) return null;\n\n  const {\n    notifications,\n    unreadCount,\n    isOpen,\n    showPopups,\n    open,\n    close,\n    markRead,\n    markAllRead,\n    markSettled,\n    setShowPopups,\n    showDesktopPopups,\n    setShowDesktopPopups,\n    desktopPopupsConfigured,\n    onHistoryClick,\n    historyHref,\n    hasMore,\n    isLoadingMore,\n    loadMore,\n    renderTile: ctxRenderTile,\n  } = ctx;\n\n  const renderTile = renderTileProp ?? ctxRenderTile;\n\n  const unreadNotifications = notifications.filter(n => !n.read);\n\n  const content = (\n    <>\n      <div className=\"px-[var(--spacing-system-m)] pt-[var(--spacing-system-m)]\">\n        <DrawerTitle\n          hideClose\n          className=\"truncate\"\n          actions={\n            <button\n              type=\"button\"\n              disabled={unreadCount === 0}\n              onClick={markAllRead}\n              className=\"self-center text-ods-text-secondary underline transition-colors text-h6 hover:text-ods-text-primary disabled:opacity-40 max-md:focus:outline-none max-md:focus-visible:outline-none\"\n            >\n              Mark All Complete\n            </button>\n          }\n        >\n          New Notifications\n        </DrawerTitle>\n      </div>\n\n      <DrawerScrollList\n        unreadNotifications={unreadNotifications}\n        liveDurationMs={liveDurationMs}\n        onComplete={markRead}\n        onSettle={markSettled}\n        hasMore={hasMore}\n        isLoadingMore={isLoadingMore}\n        loadMore={loadMore}\n        loadMoreRootMargin={loadMoreRootMargin}\n        renderTile={renderTile}\n      />\n\n      <div className=\"flex flex-col gap-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]\">\n        <div className=\"overflow-hidden rounded-md border border-ods-border bg-ods-card\">\n          <ShowNotificationsToggleRow checked={showPopups} onChange={setShowPopups} />\n          {desktopPopupsConfigured && (\n            <DesktopNotificationsToggleRow checked={showDesktopPopups} onChange={setShowDesktopPopups} />\n          )}\n        </div>\n        <NotificationsHistoryButton\n          onClick={\n            onHistoryClick\n              ? () => {\n                  onHistoryClick();\n                  close();\n                }\n              : undefined\n          }\n          historyHref={historyHref}\n        />\n      </div>\n    </>\n  );\n\n  if (layoutContainer) {\n    return (\n      <AppLayoutDrawer open={isOpen} onOpenChange={v => (v ? open() : close())}>\n        <AppLayoutDrawerContent\n          side=\"right\"\n          aria-describedby={undefined}\n          // md matches the content's default mobileBreakpoint: below it the\n          // panel is forced full-bleed (w-full), so the fixed width must not\n          // apply there or the panel would detach from the right edge.\n          className={cn('gap-[var(--spacing-system-m)] p-[var(--spacing-system-zero)] md:w-[26rem]', className)}\n        >\n          {content}\n        </AppLayoutDrawerContent>\n      </AppLayoutDrawer>\n    );\n  }\n\n  return (\n    <Drawer open={isOpen} onOpenChange={v => (v ? open() : close())}>\n      <DrawerContent\n        side=\"right\"\n        aria-describedby={undefined}\n        offsetHeader\n        className={cn(\n          'w-[calc(100vw-2rem)] gap-[var(--spacing-system-m)] p-[var(--spacing-system-zero)] sm:w-[26rem] sm:max-w-[calc(100vw-2rem)]',\n          className,\n        )}\n      >\n        {content}\n      </DrawerContent>\n    </Drawer>\n  );\n}\n\ninterface DrawerScrollListProps {\n  unreadNotifications: Notification[];\n  liveDurationMs?: number;\n  onComplete: (id: string) => void;\n  onSettle: (id: string) => void;\n  hasMore: boolean;\n  isLoadingMore: boolean;\n  loadMore?: () => void;\n  loadMoreRootMargin: string;\n  renderTile?: RenderNotificationTile;\n}\n\nfunction DrawerScrollList({\n  unreadNotifications,\n  liveDurationMs,\n  onComplete,\n  onSettle,\n  hasMore,\n  isLoadingMore,\n  loadMore,\n  loadMoreRootMargin,\n  renderTile,\n}: DrawerScrollListProps) {\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const sentinelRef = useRef<HTMLDivElement>(null);\n  // Refreshed after every commit, declared before the observer effect so it\n  // wins the same flush. Not in the render body: the reader is the\n  // IntersectionObserver callback, which cannot fire before a commit.\n  const loadMoreRef = useRef(loadMore);\n  useEffect(() => {\n    loadMoreRef.current = loadMore;\n  });\n\n  useEffect(() => {\n    if (!loadMore || !hasMore || isLoadingMore) return undefined;\n    const sentinel = sentinelRef.current;\n    const root = scrollRef.current;\n    if (!sentinel || !root) return undefined;\n    const observer = new IntersectionObserver(\n      entries => {\n        if (entries[0]?.isIntersecting) loadMoreRef.current?.();\n      },\n      { root, rootMargin: loadMoreRootMargin },\n    );\n    observer.observe(sentinel);\n    return () => observer.disconnect();\n  }, [hasMore, isLoadingMore, loadMore, loadMoreRootMargin]);\n\n  const isEmpty = unreadNotifications.length === 0;\n  return (\n    <div\n      ref={scrollRef}\n      className=\"flex flex-1 flex-col gap-[var(--spacing-system-xs)] overflow-y-auto px-[var(--spacing-system-m)]\"\n    >\n      {isEmpty && !isLoadingMore ? (\n        <EmptyState />\n      ) : (\n        <>\n          {unreadNotifications.map(n => {\n            const custom = renderTile?.(n, { onComplete, onSettle, liveDurationMs });\n            if (custom) return <div key={n.id}>{custom}</div>;\n            return (\n              <NotificationTile\n                key={n.id}\n                notification={n}\n                liveDurationMs={liveDurationMs}\n                onComplete={onComplete}\n                onSettle={onSettle}\n              />\n            );\n          })}\n          {isLoadingMore && <DrawerLoadingTiles />}\n          {loadMore && hasMore && <div ref={sentinelRef} className=\"h-1\" aria-hidden=\"true\" />}\n        </>\n      )}\n    </div>\n  );\n}\n\nfunction DrawerLoadingTiles() {\n  return (\n    <>\n      <div\n        aria-hidden=\"true\"\n        className=\"h-16 w-full shrink-0 animate-pulse rounded-md border border-ods-border bg-ods-card\"\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"h-16 w-full shrink-0 animate-pulse rounded-md border border-ods-border bg-ods-card\"\n      />\n    </>\n  );\n}\n\nfunction EmptyState() {\n  return (\n    <div className=\"flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-xs)] py-[var(--spacing-system-l)]\">\n      <BellOffIcon size={24} className=\"text-ods-text-secondary\" />\n      <p className=\"text-ods-text-secondary text-h6\">No new notifications</p>\n    </div>\n  );\n}\n\ninterface ToggleRowProps {\n  checked: boolean;\n  onChange: (value: boolean) => void;\n}\n\nfunction ShowNotificationsToggleRow({ checked, onChange }: ToggleRowProps) {\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-s)] border-b border-ods-border p-[var(--spacing-system-sf)] last:border-b-0\">\n      <Switch checked={checked} onCheckedChange={onChange} aria-label=\"Show pop-up notifications\" />\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"text-ods-text-primary text-h4\">Show Notifications</p>\n        <p className=\"text-ods-text-secondary text-h6\">Show pop-up messages for new alerts</p>\n      </div>\n    </div>\n  );\n}\n\n/** Switching on prompts for browser permission and commits only when granted; denied renders disabled (can't re-prompt programmatically); unsupported (SSR, iOS Safari) hides the row. */\nfunction DesktopNotificationsToggleRow({ checked, onChange }: ToggleRowProps) {\n  const { supported, permission, request } = useNotificationPermission();\n  if (!supported) return null;\n\n  const blocked = permission === 'denied';\n\n  const handleChange = async (value: boolean) => {\n    if (!value || permission === 'granted') {\n      onChange(value);\n      return;\n    }\n    if ((await request()) === 'granted') onChange(true);\n  };\n\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-s)] border-b border-ods-border p-[var(--spacing-system-sf)] last:border-b-0\">\n      <Switch\n        checked={checked && permission === 'granted'}\n        disabled={blocked}\n        onCheckedChange={handleChange}\n        aria-label=\"Show desktop notifications\"\n      />\n      <div className=\"min-w-0 flex-1\">\n        <p className={cn('text-h4', blocked ? 'text-ods-text-secondary' : 'text-ods-text-primary')}>\n          Desktop Notifications\n        </p>\n        <p className=\"text-ods-text-secondary text-h6\">\n          {blocked ? 'Enable notifications in your browser settings' : 'Notify when this tab is in the background'}\n        </p>\n      </div>\n    </div>\n  );\n}\n\ninterface HistoryButtonProps {\n  onClick?: () => void;\n  historyHref?: string;\n}\n\nfunction NotificationsHistoryButton({ onClick, historyHref }: HistoryButtonProps) {\n  return (\n    <SplitButton\n      variant=\"outline\"\n      fullWidth\n      onClick={onClick}\n      mainDisabled={!onClick}\n      leftIcon={<ClockHistoryIcon className=\"text-ods-text-secondary\" />}\n      groupAriaLabel=\"Notifications history\"\n      iconAction={{\n        icon: <ArrowRightUpIcon className=\"text-ods-text-secondary\" />,\n        'aria-label': 'Open notifications history in a new tab',\n        href: historyHref,\n        openInNewTab: true,\n        disabled: !historyHref,\n      }}\n    >\n      Notifications History\n    </SplitButton>\n  );\n}\n","'use client';\n\nimport { createContext, useContext } from 'react';\n\n/**\n * Container element that wraps `<main>` and serves as the portal target for\n * `AppLayoutDrawer`. Drawers rendered into this container sit on top of the\n * main content area only — the sidebar and header remain visible and\n * interactive. Null when AppLayout hasn't mounted (or when used outside of it).\n *\n * Lives in its own module (not app-layout.tsx) so in-layout panels\n * (AppLayoutDrawer, NotificationDrawer, TimeTrackerHeaderButton) can consume\n * these contexts without importing AppLayout itself — AppLayout renders those\n * panels, so importing it back would create a module cycle.\n */\nexport const AppLayoutDrawerContainerContext = createContext<HTMLElement | null>(null);\n\nexport function useAppLayoutDrawerContainer(): HTMLElement | null {\n  return useContext(AppLayoutDrawerContainerContext);\n}\n\n/**\n * Coordination between AppLayout's in-layout panels (`AppLayoutDrawer`, the\n * time-tracker popup) and the mobile burger menu. At most ONE of them may be\n * open at a time — each dims the main area, so stacking them reads as broken:\n *   - a panel that opens calls `notifyDrawerDidOpen(self)` so the layout\n *     closes the burger menu AND every other registered panel;\n *   - each panel registers a close handle so opening the menu via the burger\n *     button (or another panel) closes it. Null outside of AppLayout.\n */\nexport interface AppLayoutDrawerHandle {\n  close: () => void;\n}\n\nexport interface AppLayoutDrawerCoordination {\n  /** `self` is skipped when closing the other registered panels. */\n  notifyDrawerDidOpen: (self?: AppLayoutDrawerHandle) => void;\n  /** Returns an unregister cleanup. */\n  registerDrawer: (handle: AppLayoutDrawerHandle) => () => void;\n}\n\nexport const AppLayoutDrawerCoordinationContext = createContext<AppLayoutDrawerCoordination | null>(null);\n\nexport function useAppLayoutDrawerCoordination(): AppLayoutDrawerCoordination | null {\n  return useContext(AppLayoutDrawerCoordinationContext);\n}\n","'use client';\n\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n  type CSSProperties,\n  type ComponentPropsWithoutRef,\n  type ComponentRef,\n  type KeyboardEvent,\n  type PointerEvent,\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\n\nimport { cn } from '../../utils/cn';\nimport { clamp } from '../../utils/common';\nimport {\n  DrawerBody,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  OVERLAY_BACKDROP_CLASS,\n  type DrawerSide,\n} from '../ui/drawer';\nimport {\n  type AppLayoutDrawerHandle,\n  useAppLayoutDrawerContainer,\n  useAppLayoutDrawerCoordination,\n} from './app-layout-context';\n\n/**\n * AppLayoutDrawer is a Drawer variant that renders **inside** AppLayout's main\n * content area instead of as a viewport-level overlay. Header and sidebar stay\n * visible and interactive while it is open.\n *\n * Implementation differs from the standard Drawer in three ways:\n *   1. `DialogPrimitive.Portal` targets the AppLayout container (provided via\n *      React Context) rather than `document.body`.\n *   2. Positioning is `absolute` (clipped to the container) instead of `fixed`.\n *   3. The Dialog is non-modal (`modal={false}`) — outside content is not\n *      inert, so header/sidebar interactions still work while the drawer is open.\n *\n * Caveat of non-modal mode: Radix's built-in `DialogPrimitive.Overlay` returns\n * null (see @radix-ui/react-dialog source). We render our own overlay div as a\n * sibling of `DialogPrimitive.Content` in the portal so it (a) provides the\n * standard dim backdrop and (b) catches pointer events over the main area —\n * otherwise clicks pass through to underlying buttons, causing a close-and-\n * reopen flicker when the click target is also a trigger.\n *\n * Everything else (visual chrome, slide animation, resize handle, sub-components)\n * matches the standard Drawer. Sub-components (header/title/body/footer) are\n * re-exported aliases of the originals so styling stays in lockstep.\n */\n\nconst DrawerOpenContext = createContext<boolean>(false);\n\ninterface AppLayoutDrawerRootProps extends ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {}\n\nconst AppLayoutDrawerRoot = ({\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  modal = false,\n  children,\n  ...rest\n}: AppLayoutDrawerRootProps) => {\n  // Shadow Dialog.Root's open state so descendants (specifically the overlay\n  // rendered inside Content) can drive their own `data-state` for animations.\n  // Radix doesn't expose its internal open state via a public context.\n  const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\n  const isControlled = openProp !== undefined;\n  const open = isControlled ? openProp : internalOpen;\n\n  const handleOpenChange = useCallback(\n    (next: boolean) => {\n      if (!isControlled) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [isControlled, onOpenChange],\n  );\n\n  // Coordinate with AppLayout's other in-layout panels and the mobile burger\n  // menu: opening this drawer closes them, and opening any of them closes\n  // this drawer via the registered close handle. The handle identity must stay\n  // stable across renders — the same object is registered AND passed as `self`\n  // so the coordinator can skip it when closing the rest — so it is built once\n  // by `useState`'s lazy initialiser and reads the current open state through\n  // latest-refs.\n  const coordination = useAppLayoutDrawerCoordination();\n  // Refreshed in an unconditional effect, not the render body: `close()` is\n  // only ever invoked by the coordinator in response to ANOTHER panel opening,\n  // which is always past a commit, and a render attempt React discards must\n  // not be able to leave the handle pointing at a callback that never ran.\n  const openRef = useRef(open);\n  const handleOpenChangeRef = useRef(handleOpenChange);\n  useEffect(() => {\n    openRef.current = open;\n    handleOpenChangeRef.current = handleOpenChange;\n  });\n\n  const [selfHandle] = useState<AppLayoutDrawerHandle>(() => ({\n    close: () => {\n      if (openRef.current) handleOpenChangeRef.current(false);\n    },\n  }));\n\n  useEffect(() => {\n    if (open) coordination?.notifyDrawerDidOpen(selfHandle);\n  }, [open, coordination, selfHandle]);\n\n  useEffect(() => coordination?.registerDrawer(selfHandle), [coordination, selfHandle]);\n\n  return (\n    <DrawerOpenContext.Provider value={open}>\n      <DialogPrimitive.Root {...rest} open={open} onOpenChange={handleOpenChange} modal={modal}>\n        {children}\n      </DialogPrimitive.Root>\n    </DrawerOpenContext.Provider>\n  );\n};\nAppLayoutDrawerRoot.displayName = 'AppLayoutDrawer';\n\nconst AppLayoutDrawerTrigger = DialogPrimitive.Trigger;\nconst AppLayoutDrawerClose = DialogPrimitive.Close;\n\n// z-[103] (overlay z-[102]) — above MobileBurgerMenu (backdrop z-[100],\n// panel z-[101]) so an in-layout drawer opened over the mobile menu is not\n// covered by it. The drawer is absolutely positioned inside the main-area\n// container, so header/sidebar are unaffected regardless of z.\nconst appLayoutDrawerVariants = cva(\n  'absolute z-[103] flex outline-none focus:outline-none focus-visible:outline-none data-[state=closed]:duration-300 data-[state=open]:duration-300 data-[state=open]:animate-in data-[state=closed]:animate-out',\n  {\n    variants: {\n      side: {\n        right:\n          'inset-y-0 right-0 items-center data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n        left: 'inset-y-0 left-0 items-center data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left',\n        top: 'inset-x-0 top-0 justify-center data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n        bottom:\n          'inset-x-0 bottom-0 justify-center data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n      },\n      flush: {\n        false: '',\n        true: '',\n      },\n    },\n    compoundVariants: [\n      { side: 'right', flush: false, class: 'py-[var(--spacing-system-m)] pr-[var(--spacing-system-m)]' },\n      { side: 'left', flush: false, class: 'py-[var(--spacing-system-m)] pl-[var(--spacing-system-m)]' },\n      { side: 'top', flush: false, class: 'px-[var(--spacing-system-m)] pt-[var(--spacing-system-m)]' },\n      { side: 'bottom', flush: false, class: 'px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]' },\n      { side: 'right', flush: true, class: 'md:py-[var(--spacing-system-m)] md:pr-[var(--spacing-system-m)]' },\n      { side: 'left', flush: true, class: 'md:py-[var(--spacing-system-m)] md:pl-[var(--spacing-system-m)]' },\n      { side: 'top', flush: true, class: 'md:px-[var(--spacing-system-m)] md:pt-[var(--spacing-system-m)]' },\n      { side: 'bottom', flush: true, class: 'md:px-[var(--spacing-system-m)] md:pb-[var(--spacing-system-m)]' },\n    ],\n    defaultVariants: {\n      side: 'right',\n      flush: false,\n    },\n  },\n);\n\nconst appLayoutDrawerPanelVariants = cva(\n  'relative flex flex-col overflow-hidden bg-ods-card outline-none focus:outline-none focus-visible:outline-none',\n  {\n    variants: {\n      side: {\n        right: 'h-full',\n        left: 'h-full',\n        top: 'w-full',\n        bottom: 'w-full',\n      },\n      flush: {\n        false: 'gap-4 rounded-md border border-ods-border p-4',\n        true: '',\n      },\n    },\n    compoundVariants: [\n      { side: 'right', flush: true, class: 'md:rounded-md md:border md:border-ods-border' },\n      { side: 'left', flush: true, class: 'md:rounded-md md:border md:border-ods-border' },\n      { side: 'top', flush: true, class: 'md:rounded-md md:border md:border-ods-border' },\n      { side: 'bottom', flush: true, class: 'md:rounded-md md:border md:border-ods-border' },\n    ],\n    defaultVariants: {\n      side: 'right',\n      flush: false,\n    },\n  },\n);\n\nconst HORIZONTAL_SIDES: ReadonlySet<DrawerSide> = new Set(['left', 'right']);\n\n/**\n * Persist mode (`forceMount`) keeps the panel mounted across close, so the\n * `slide-out-*` / `fade-out` exit animations run and then — because\n * `tailwindcss-animate`'s `.animate-out` does NOT set `animation-fill-mode:\n * forwards` — the panel/overlay snap back to their resting frame (on-screen,\n * opacity 1). The element ends up fully visible again, which reads as \"the\n * drawer won't close\" (and dims the whole content area via the backdrop).\n *\n * Pinning `fill-mode: forwards` for the closed state holds the exit animation's\n * final frame (off-screen / transparent) after it ends. It can't be replaced by\n * a static resting transform/opacity: that would override the animation's\n * `from` value (= current computed style) and collapse the animation to a no-op.\n *\n * Not applied without persist — Radix unmounts `Content` after the animation,\n * so there's nothing to snap back.\n */\nconst PERSIST_CLOSED_HOLD = 'data-[state=closed]:fill-mode-forwards';\n\ninterface UseContainedResizableSizeArgs {\n  enabled: boolean;\n  isHorizontal: boolean;\n  minSize: number;\n  maxSize: number;\n  defaultSize: number;\n  storageKey?: string;\n  container: HTMLElement | null;\n}\n\nfunction useContainedResizableSize({\n  enabled,\n  isHorizontal,\n  minSize,\n  maxSize,\n  defaultSize,\n  storageKey,\n  container,\n}: UseContainedResizableSizeArgs) {\n  const [available, setAvailable] = useState(0);\n\n  useEffect(() => {\n    if (!enabled || !container) return undefined;\n    const update = () => {\n      setAvailable(isHorizontal ? container.clientWidth : container.clientHeight);\n    };\n    update();\n    const ro = new ResizeObserver(update);\n    ro.observe(container);\n    return () => ro.disconnect();\n  }, [enabled, container, isHorizontal]);\n\n  const clampToContainer = useCallback(\n    (value: number) => {\n      // Reserve 40px (the `system-m` outside-edge padding from the wrapper\n      // plus a matching gap on the inside edge) so the panel sits symmetrically\n      // inside the container. This also keeps the resize grip on-screen at\n      // maximum extent.\n      const effectiveMax = available > 0 ? Math.min(maxSize, available - 40) : maxSize;\n      return clamp(value, minSize, Math.max(minSize, effectiveMax));\n    },\n    [available, minSize, maxSize],\n  );\n\n  const readInitial = useCallback(() => {\n    if (!enabled) return defaultSize;\n    if (typeof window === 'undefined') return defaultSize;\n    if (!storageKey) return defaultSize;\n    try {\n      const raw = window.localStorage.getItem(storageKey);\n      if (!raw) return defaultSize;\n      const parsed = parseFloat(raw);\n      if (!Number.isFinite(parsed)) return defaultSize;\n      return parsed;\n    } catch {\n      return defaultSize;\n    }\n  }, [enabled, storageKey, defaultSize]);\n\n  const [rawSize, setSizeRaw] = useState<number>(readInitial);\n\n  // Re-clamp the stored size whenever the container resizes so a previously\n  // saved size never overflows after the user shrinks the viewport.\n  //\n  // Clamped where it is READ rather than written back into state from an\n  // effect: `available` is already state (the ResizeObserver above owns it), so\n  // the container shrinking re-renders regardless — and deriving here means the\n  // panel is never painted overflowing its container for the frame between the\n  // resize and the corrective commit. Keeping the raw value also means widening\n  // the viewport again restores the size the user actually chose, instead of\n  // leaving it permanently trimmed to the narrowest width ever seen.\n  const size = enabled ? clampToContainer(rawSize) : rawSize;\n\n  const setSize = useCallback((next: number) => setSizeRaw(clampToContainer(next)), [clampToContainer]);\n\n  useEffect(() => {\n    if (!enabled || !storageKey || typeof window === 'undefined') return;\n    try {\n      window.localStorage.setItem(storageKey, String(Math.round(size)));\n    } catch {\n      // ignore quota / disabled-storage\n    }\n  }, [enabled, size, storageKey]);\n\n  return { size, setSize };\n}\n\ninterface AppLayoutDrawerResizeHandleProps {\n  side: DrawerSide;\n  size: number;\n  minSize: number;\n  maxSize: number;\n  onSize: (next: number) => void;\n  ariaLabel?: string;\n}\n\nfunction AppLayoutDrawerResizeHandle({\n  side,\n  size,\n  minSize,\n  maxSize,\n  onSize,\n  ariaLabel,\n}: AppLayoutDrawerResizeHandleProps) {\n  const isHorizontal = HORIZONTAL_SIDES.has(side);\n  const startRef = useRef<{ x: number; y: number; size: number } | null>(null);\n\n  const direction = side === 'right' || side === 'bottom' ? -1 : 1;\n\n  const handlePointerDown = (e: PointerEvent<HTMLDivElement>) => {\n    if (e.button !== 0 && e.pointerType === 'mouse') return;\n    e.preventDefault();\n    startRef.current = { x: e.clientX, y: e.clientY, size };\n    e.currentTarget.setPointerCapture(e.pointerId);\n    document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';\n    document.body.style.userSelect = 'none';\n  };\n\n  const handlePointerMove = (e: PointerEvent<HTMLDivElement>) => {\n    const start = startRef.current;\n    if (!start) return;\n    const delta = isHorizontal ? e.clientX - start.x : e.clientY - start.y;\n    onSize(start.size + delta * direction);\n  };\n\n  const endDrag = (e: PointerEvent<HTMLDivElement>) => {\n    if (!startRef.current) return;\n    startRef.current = null;\n    try {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {\n      // ignore — pointer may already be released\n    }\n    document.body.style.cursor = '';\n    document.body.style.userSelect = '';\n  };\n\n  const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    const step = e.shiftKey ? 40 : 16;\n    if (isHorizontal) {\n      if (e.key === 'ArrowLeft') {\n        e.preventDefault();\n        onSize(size + step * (side === 'right' ? 1 : -1));\n      } else if (e.key === 'ArrowRight') {\n        e.preventDefault();\n        onSize(size + step * (side === 'right' ? -1 : 1));\n      }\n    } else {\n      if (e.key === 'ArrowUp') {\n        e.preventDefault();\n        onSize(size + step * (side === 'bottom' ? 1 : -1));\n      } else if (e.key === 'ArrowDown') {\n        e.preventDefault();\n        onSize(size + step * (side === 'bottom' ? -1 : 1));\n      }\n    }\n    if (e.key === 'Home') {\n      e.preventDefault();\n      onSize(minSize);\n    } else if (e.key === 'End') {\n      e.preventDefault();\n      onSize(maxSize);\n    }\n  };\n\n  const trackPosition =\n    side === 'right'\n      ? 'right-full top-4 bottom-4 w-6 items-center justify-end pr-1'\n      : side === 'left'\n        ? 'left-full top-4 bottom-4 w-6 items-center justify-start pl-1'\n        : side === 'bottom'\n          ? 'bottom-full left-4 right-4 h-6 justify-center items-end pb-1'\n          : 'top-full left-4 right-4 h-6 justify-center items-start pt-1';\n\n  const cursorClass = isHorizontal ? 'cursor-col-resize' : 'cursor-row-resize';\n  const gripClass = isHorizontal ? 'h-10 w-1' : 'w-10 h-1';\n\n  return (\n    <div\n      role=\"separator\"\n      tabIndex={0}\n      aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}\n      aria-valuenow={Math.round(size)}\n      aria-valuemin={minSize}\n      aria-valuemax={maxSize}\n      aria-label={ariaLabel ?? (isHorizontal ? 'Resize panel width' : 'Resize panel height')}\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={endDrag}\n      onPointerCancel={endDrag}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        'group absolute z-20 flex touch-none select-none',\n        'outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',\n        trackPosition,\n        cursorClass,\n      )}\n    >\n      <div aria-hidden className={cn('rounded-full bg-ods-border', gripClass)} />\n    </div>\n  );\n}\n\nexport interface AppLayoutDrawerContentProps\n  extends\n    Omit<ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, 'style'>,\n    VariantProps<typeof appLayoutDrawerVariants> {\n  flush?: boolean;\n  resizable?: boolean;\n  minSize?: number;\n  maxSize?: number;\n  defaultSize?: number;\n  storageKey?: string;\n  /** Pixel breakpoint below which `resizable` is disabled and inline size is\n   *  not applied (so the panel can render full-area on mobile). Defaults to\n   *  800 — the library's Tailwind `md` breakpoint, so the JS mobile switch\n   *  and the `md:` panel styles flip together. */\n  mobileBreakpoint?: number;\n  overlayClassName?: string;\n  resizeAriaLabel?: string;\n  style?: CSSProperties;\n  panelStyle?: CSSProperties;\n  panelClassName?: string;\n  /** Override the portal container. Defaults to AppLayout's main-area container\n   *  (from context). Pass `null` to opt out of portaling. */\n  container?: HTMLElement | null;\n  /** Keep the panel mounted across close so its subtree state survives an\n   *  open → close → open cycle (no remount, no refetch, no flicker). Lazy:\n   *  nothing renders until the first open; after that the panel stays mounted\n   *  (Radix `forceMount`) and the slide animation runs off `data-state`.\n   *  Default `false` — preserves the standard unmount-on-close behaviour for\n   *  every other consumer. */\n  keepMounted?: boolean;\n  /** When `true` (default), clicks on the dim overlay over the main content\n   *  area close the drawer. Clicks on AppLayout chrome (header, sidebar) NEVER\n   *  close the drawer regardless of this flag — chrome interactions shouldn't\n   *  accidentally dismiss a persistent panel. Pass `false` to make the drawer\n   *  fully persistent (X button or Escape only). Consumer-provided\n   *  `onInteractOutside` still runs first and can preventDefault to override. */\n  dismissOnInteractOutside?: boolean;\n  /** Debug helper — when `true`, on each open the component snapshots the\n   *  scroll metrics (scrollLeft / scrollWidth / clientWidth / overflow-x) of\n   *  every ancestor of the portal container, at mount, after RAF, +150ms,\n   *  +350ms, and on close. Use to diagnose layout-shift on open: look for an\n   *  ancestor that gains `scrollLeft > 0` or has `overflow-x: visible` while\n   *  `scrollWidth > clientWidth`. Remove the prop once diagnosed. */\n  debugLayoutShift?: boolean;\n}\n\nconst AppLayoutDrawerContent = forwardRef<ComponentRef<typeof DialogPrimitive.Content>, AppLayoutDrawerContentProps>(\n  (\n    {\n      side = 'right',\n      flush = false,\n      resizable = false,\n      minSize = 320,\n      // No upper cap by default — the AppLayout container width is the natural\n      // limit. Consumers can still pass an explicit `maxSize` to clamp tighter.\n      maxSize = Number.POSITIVE_INFINITY,\n      defaultSize,\n      storageKey,\n      mobileBreakpoint = 800,\n      overlayClassName,\n      resizeAriaLabel,\n      className,\n      style,\n      panelStyle,\n      panelClassName,\n      container,\n      dismissOnInteractOutside = true,\n      onInteractOutside,\n      debugLayoutShift = false,\n      keepMounted = false,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const contextContainer = useAppLayoutDrawerContainer();\n    const portalContainer = container !== undefined ? container : contextContainer;\n    const open = useContext(DrawerOpenContext);\n\n    // Persist mode: lazily mount on the first open, then never unmount. Radix\n    // unmounts `Dialog.Content` on close by default, which would wipe the\n    // panel's subtree state (and trigger a full refetch) on every reopen.\n    // `forceMount` keeps it mounted; `hasOpened` defers that until the user\n    // opens the panel at least once (no work, no exit-animation flash before\n    // first open).\n    // Seed from `open` so an initially-open (defaultOpen / controlled open=true)\n    // drawer mounts on the first paint instead of flashing `null` for one frame.\n    // Latched while rendering, not from an effect: the effect committed the\n    // FIRST open with `persist` still false, so `forceMount` only arrived on\n    // the render after — the exact one-frame `null` the seeding above exists to\n    // avoid. Guarded on `hasOpened`, so the extra pass takes the early exit.\n    const [hasOpened, setHasOpened] = useState<boolean>(Boolean(open));\n    if (open && !hasOpened) setHasOpened(true);\n    const persist = keepMounted && hasOpened;\n\n    // Diagnostic: walk ancestors and snapshot horizontal-scroll metrics\n    // around the open transition. See `debugLayoutShift` prop doc.\n    useEffect(() => {\n      if (!debugLayoutShift) return undefined;\n      if (!open) return undefined;\n      if (typeof window === 'undefined') return undefined;\n      if (!portalContainer) return undefined;\n\n      const collect = () => {\n        const list: Element[] = [];\n        let el: Element | null = portalContainer;\n        while (el) {\n          list.push(el);\n          el = el.parentElement;\n        }\n        // Add documentElement explicitly in case ancestor chain stopped early.\n        if (!list.includes(document.documentElement)) {\n          list.push(document.documentElement);\n        }\n        return list;\n      };\n\n      const snapshot = (label: string) => {\n        const data = collect().map((el, i) => {\n          const cs = window.getComputedStyle(el);\n          const overflows = el.scrollWidth > el.clientWidth;\n          const cls = String((el as HTMLElement).className ?? '')\n            .replace(/\\s+/g, ' ')\n            .slice(0, 60);\n          return {\n            depth: i,\n            tag: el.tagName.toLowerCase() + ((el as HTMLElement).id ? '#' + (el as HTMLElement).id : ''),\n            class: cls,\n            'overflow-x': cs.overflowX,\n            scrollLeft: el.scrollLeft,\n            scrollWidth: el.scrollWidth,\n            clientWidth: el.clientWidth,\n            hOverflow: overflows ? '⚠️' : '',\n          };\n        });\n        console.groupCollapsed(`[AppLayoutDrawer] ${label}`);\n        console.table(data);\n        console.groupEnd();\n      };\n\n      snapshot('open: t0 (mount)');\n      const raf = requestAnimationFrame(() => snapshot('open: t≈16ms (RAF 1)'));\n      const t150 = window.setTimeout(() => snapshot('open: t≈150ms (mid-anim)'), 150);\n      const t350 = window.setTimeout(() => snapshot('open: t≈350ms (post-anim)'), 350);\n\n      return () => {\n        cancelAnimationFrame(raf);\n        clearTimeout(t150);\n        clearTimeout(t350);\n        snapshot('close');\n      };\n    }, [open, portalContainer, debugLayoutShift]);\n\n    const resolvedSide: DrawerSide = side ?? 'right';\n    const isHorizontal = HORIZONTAL_SIDES.has(resolvedSide);\n    const initialSize = defaultSize ?? (isHorizontal ? 560 : 480);\n\n    const [isMobile, setIsMobile] = useState(false);\n    useEffect(() => {\n      if (typeof window === 'undefined') return undefined;\n      const mq = window.matchMedia(`(max-width: ${mobileBreakpoint - 1}px)`);\n      const update = () => setIsMobile(mq.matches);\n      update();\n      mq.addEventListener('change', update);\n      return () => mq.removeEventListener('change', update);\n    }, [mobileBreakpoint]);\n\n    const { size, setSize } = useContainedResizableSize({\n      enabled: resizable,\n      isHorizontal,\n      minSize,\n      maxSize,\n      defaultSize: initialSize,\n      storageKey,\n      container: portalContainer,\n    });\n\n    const applyInlineSize = resizable && !isMobile;\n    const sizeStyle: CSSProperties = applyInlineSize ? (isHorizontal ? { width: size } : { height: size }) : {};\n\n    // Persist mode, pre-first-open: render nothing so the panel mounts lazily.\n    // (All hooks above run unconditionally — this early return is hooks-safe.)\n    if (keepMounted && !hasOpened) return null;\n\n    return (\n      <DialogPrimitive.Portal container={portalContainer} forceMount={persist || undefined}>\n        {/* Overlay rendered manually: Radix's DialogPrimitive.Overlay returns\n            null in non-modal mode, so we render a plain div here. Setting\n            `pointer-events-auto` ensures it catches clicks on the main area\n            so they don't fall through to buttons underneath — that would\n            otherwise fire both Radix's outside-close AND the button's\n            own onClick, producing a close-then-reopen flicker. */}\n        <div\n          aria-hidden\n          data-state={open ? 'open' : 'closed'}\n          className={cn(\n            'absolute inset-0 z-[102] outline-none data-[state=closed]:pointer-events-none data-[state=open]:pointer-events-auto data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n            OVERLAY_BACKDROP_CLASS,\n            // Persist mode only: hold the fade-out's final (transparent) frame\n            // so the backdrop doesn't snap back to opacity 1 and dim the whole\n            // content area while the drawer is \"closed\" (see PERSIST_CLOSED_HOLD).\n            persist && PERSIST_CLOSED_HOLD,\n            overlayClassName,\n          )}\n        />\n        <DialogPrimitive.Content\n          ref={ref}\n          forceMount={persist || undefined}\n          // Persist mode: the panel stays mounted while closed (slid off-screen\n          // by the exit animation). Make sure that off-screen content can't\n          // catch clicks or be reached by tab/screen-reader.\n          {...(persist && !open ? { inert: true as const, 'aria-hidden': true } : {})}\n          className={cn(\n            appLayoutDrawerVariants({ side, flush }),\n            // Mobile: span the whole container and drop the outer gap so the\n            // panel renders edge-to-edge full-screen. (`p-0` overrides the\n            // variant's wrapper padding via tailwind-merge.)\n            isMobile && 'inset-0 p-0',\n            'data-[state=closed]:pointer-events-none',\n            // Persist mode only: hold the slide-out's final (off-screen) frame\n            // so the panel doesn't snap back into view (see PERSIST_CLOSED_HOLD).\n            persist && PERSIST_CLOSED_HOLD,\n          )}\n          style={style}\n          onInteractOutside={event => {\n            onInteractOutside?.(event);\n            if (event.defaultPrevented) return;\n            // Chrome (header, sidebar) sits OUTSIDE the portal container.\n            // Those clicks never dismiss — they navigate, etc. Only clicks\n            // INSIDE the portal container (the overlay over main) can dismiss.\n            const target = event.target as Node | null;\n            const isInsideContainer = !!target && !!portalContainer?.contains(target);\n            if (!isInsideContainer) {\n              event.preventDefault();\n              return;\n            }\n            if (!dismissOnInteractOutside) event.preventDefault();\n          }}\n          {...props}\n        >\n          {/* The resize grip sits just OUTSIDE the panel's inner edge\n              (`right-full` etc.), so the wrapper's `translateX(100%)` close\n              transform doesn't carry it off-screen — it stays as a ~12px sliver\n              at the container edge. In persist mode it's also non-interactive\n              while closed, so drop it entirely until the panel reopens. */}\n          {applyInlineSize && (!persist || open) ? (\n            <AppLayoutDrawerResizeHandle\n              side={resolvedSide}\n              size={size}\n              minSize={minSize}\n              maxSize={maxSize}\n              onSize={setSize}\n              ariaLabel={resizeAriaLabel}\n            />\n          ) : null}\n          <div\n            className={cn(\n              appLayoutDrawerPanelVariants({ side, flush }),\n              // Mobile: fill the container (the side already pins one axis) and\n              // drop the card chrome so the panel is edge-to-edge full-screen.\n              // Inner padding/gap is preserved so content isn't glued to the\n              // edges. tailwind-merge lets these override the variant classes.\n              isMobile && (isHorizontal ? 'w-full' : 'h-full'),\n              isMobile && 'rounded-none border-0',\n              className,\n              panelClassName,\n            )}\n            style={{ ...sizeStyle, ...panelStyle }}\n          >\n            {children}\n          </div>\n        </DialogPrimitive.Content>\n      </DialogPrimitive.Portal>\n    );\n  },\n);\nAppLayoutDrawerContent.displayName = 'AppLayoutDrawerContent';\n\n// Sub-components are visually identical to the base Drawer's — re-export as\n// aliases so the namespace stays consistent for consumers.\nconst AppLayoutDrawerHeader = DrawerHeader;\nconst AppLayoutDrawerTitle = DrawerTitle;\nconst AppLayoutDrawerDescription = DrawerDescription;\nconst AppLayoutDrawerBody = DrawerBody;\nconst AppLayoutDrawerFooter = DrawerFooter;\n\nexport {\n  AppLayoutDrawerRoot as AppLayoutDrawer,\n  AppLayoutDrawerTrigger,\n  AppLayoutDrawerClose,\n  AppLayoutDrawerContent,\n  AppLayoutDrawerHeader,\n  AppLayoutDrawerTitle,\n  AppLayoutDrawerDescription,\n  AppLayoutDrawerBody,\n  AppLayoutDrawerFooter,\n};\n","'use client';\n\nimport { useEffect, useState, type ReactNode } from 'react';\nimport Image from '../../../embed-shims/next-image';\nimport { cn } from '../../../utils/cn';\nimport { formatTicketRelativeTime } from '../../../utils/date-utils';\nimport { CheckCircleIcon } from '../../icons-v2-generated/signs-and-symbols/check-circle-icon';\nimport { XmarkIcon } from '../../icons-v2-generated/signs-and-symbols/xmark-icon';\nimport { dotColorByVariant, progressColorByVariant } from '../../ui/toaster';\nimport type { Notification, NotificationSeverity, NotificationVariant } from './types';\n\n/** Backend severity → tile color variant; overrides `notification.variant` when present. */\nconst variantBySeverity: Record<NotificationSeverity, NotificationVariant> = {\n  INFO: 'info',\n  SUCCESS: 'success',\n  WARNING: 'warning',\n  DANGER: 'error',\n};\n\n/** Severity color applied to the header type label and icon slot. */\nconst typeColorByVariant: Record<NotificationVariant, string> = {\n  default: 'text-ods-text-secondary',\n  info: 'text-ods-text-secondary',\n  success: 'text-ods-success',\n  warning: 'text-ods-warning',\n  error: 'text-ods-error',\n};\n\nconst headerControlClass =\n  'absolute inset-0 flex items-center justify-center text-ods-text-secondary transition-opacity duration-200 hover:text-ods-text-primary';\n\nexport interface NotificationTileProps {\n  notification: Notification;\n  liveDurationMs?: number;\n  onComplete: (id: string) => void;\n  onSettle?: (id: string) => void;\n  className?: string;\n  /** Action row rendered below the body inside the padded section (e.g. approval buttons). */\n  actions?: ReactNode;\n  /**\n   * Extra content rendered below the padded section (e.g. a collapsible approval command\n   * section). Bring your own top divider — the tile no longer draws one, so a zero-height\n   * collapsed child doesn't leave a stray border above the card's bottom edge.\n   */\n  children?: ReactNode;\n  /** Pin the tile: cancel the live auto-dismiss countdown (timer + progress bar) without settling it. */\n  paused?: boolean;\n}\n\nexport function NotificationTile({\n  notification,\n  liveDurationMs = 4000,\n  onComplete,\n  onSettle,\n  className,\n  actions,\n  children,\n  paused = false,\n}: NotificationTileProps) {\n  const {\n    id,\n    variant = 'default',\n    severity,\n    type,\n    icon,\n    imageUrl,\n    title,\n    description,\n    createdAt,\n    read,\n    settled,\n  } = notification;\n\n  // Unknown severity strings (backend data can outrun the union) degrade to `variant`.\n  const accentVariant = (severity ? variantBySeverity[severity] : undefined) ?? variant;\n\n  // A blocked/broken image (ad-blockers routinely kill external avatars) falls\n  // through to the icon/category/dot chain instead of a broken-image glyph.\n  const [failedImageUrl, setFailedImageUrl] = useState<string | null>(null);\n  const image = imageUrl && imageUrl !== failedImageUrl ? imageUrl : undefined;\n\n  // Gate on the Date, not the number: NaN, Infinity AND finite-but-out-of-range\n  // epochs (e.g. nanoseconds) all make toISOString() throw.\n  const createdAtDate = new Date(createdAt);\n  const createdAtIso = Number.isNaN(createdAtDate.getTime()) ? null : createdAtDate.toISOString();\n\n  // Sampled once at mount, not memoised on `createdAt`: `useMemo` is a\n  // performance hint React may re-run at any time, and re-running this one\n  // would silently restart the live window. A lazy state initialiser is the\n  // one place a component is allowed to read the clock.\n  const [initialElapsed] = useState(() => Date.now() - createdAt);\n  const isLive = !read && !settled && initialElapsed < liveDurationMs;\n  const counting = isLive && !paused;\n\n  useEffect(() => {\n    if (!counting) return undefined;\n    const remaining = Math.max(0, liveDurationMs - initialElapsed);\n    const timer = window.setTimeout(() => {\n      onSettle?.(id);\n    }, remaining);\n    return () => window.clearTimeout(timer);\n  }, [id, counting, initialElapsed, liveDurationMs, onSettle]);\n\n  return (\n    <output\n      className={cn(\n        'relative block w-full shrink-0 overflow-hidden rounded-md border border-ods-border bg-ods-card',\n        className,\n      )}\n    >\n      <div\n        className={cn(\n          'relative flex flex-col gap-[var(--spacing-system-xs)] p-[var(--spacing-system-s)]',\n          isLive && 'pb-[var(--spacing-system-m)]',\n        )}\n      >\n        {/* Header: icon/image + type label (both severity-colored) + time + dismiss/complete control. */}\n        <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n          <span className={cn('flex size-4 shrink-0 items-center justify-center', typeColorByVariant[accentVariant])}>\n            {image ? (\n              <Image\n                src={image}\n                alt=\"\"\n                onError={() => setFailedImageUrl(image)}\n                className=\"size-4 rounded-[2px] object-cover\"\n                width={16}\n                height={16}\n                unoptimized\n              />\n            ) : (\n              (icon ?? <span className={cn('size-1.5 rounded-full', dotColorByVariant[accentVariant])} />)\n            )}\n          </span>\n\n          {type ? (\n            <p className={cn('min-w-0 flex-1 truncate text-h5', typeColorByVariant[accentVariant])} title={type}>\n              {type}\n            </p>\n          ) : (\n            <span className=\"min-w-0 flex-1\" />\n          )}\n\n          {!isLive && createdAtIso ? (\n            <time dateTime={createdAtIso} className=\"shrink-0 whitespace-nowrap text-ods-text-secondary text-h6\">\n              {formatTicketRelativeTime(createdAtIso)}\n            </time>\n          ) : null}\n\n          {/* Live X and settled check swap in the same 16px slot; the inactive\n              one is removed from the a11y tree and disabled, not just faded. */}\n          <span className=\"relative size-4 shrink-0\">\n            {[\n              {\n                active: isLive,\n                label: 'Dismiss notification',\n                icon: <XmarkIcon size={16} />,\n                onClick: () => onSettle?.(id),\n              },\n              {\n                active: !isLive,\n                label: 'Mark notification complete',\n                icon: <CheckCircleIcon size={16} />,\n                onClick: () => onComplete(id),\n              },\n            ].map(({ active, label, icon: actionIcon, onClick }) => (\n              <button\n                key={label}\n                type=\"button\"\n                onClick={onClick}\n                aria-label={label}\n                aria-hidden={!active}\n                disabled={!active}\n                tabIndex={active ? 0 : -1}\n                className={cn(headerControlClass, !active && 'pointer-events-none opacity-0')}\n              >\n                {actionIcon}\n              </button>\n            ))}\n          </span>\n        </div>\n\n        <div className=\"flex min-w-0 flex-col\">\n          {title ? (\n            <p className=\"truncate text-ods-text-primary text-h4\" title={typeof title === 'string' ? title : undefined}>\n              {title}\n            </p>\n          ) : null}\n          {description ? (\n            <p\n              className=\"line-clamp-3 text-ods-text-secondary text-h6\"\n              title={typeof description === 'string' ? description : undefined}\n            >\n              {description}\n            </p>\n          ) : null}\n        </div>\n\n        {actions}\n\n        {/* Progress sits at the section's bottom edge (above any children). */}\n        {counting ? (\n          <div\n            aria-hidden\n            className={cn('absolute inset-x-0 -bottom-px h-1 origin-left', progressColorByVariant[accentVariant])}\n            style={{\n              animation: `toast-progress ${liveDurationMs}ms linear forwards`,\n              animationDelay: `-${initialElapsed}ms`,\n            }}\n          />\n        ) : null}\n      </div>\n\n      {children}\n    </output>\n  );\n}\n","'use client';\n\nimport { useEffect, useState, type KeyboardEvent, type MouseEvent } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { NotificationTile } from './notification-tile';\nimport { useOptionalNotifications } from './notifications-context';\nimport type { RenderNotificationTile, Notification } from './types';\n\nexport type NotificationPopupsPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';\n\nexport interface NotificationPopupsProps {\n  className?: string;\n  liveDurationMs?: number;\n  maxVisible?: number;\n  position?: NotificationPopupsPosition;\n  hideWhenDrawerOpen?: boolean;\n  renderTile?: RenderNotificationTile;\n}\n\nconst positionClasses: Record<NotificationPopupsPosition, string> = {\n  'top-right': 'top-4 right-4',\n  'top-left': 'top-4 left-4',\n  'bottom-right': 'bottom-4 right-4 flex-col-reverse',\n  'bottom-left': 'bottom-4 left-4 flex-col-reverse',\n};\n\nexport function NotificationPopups({\n  className,\n  liveDurationMs = 4000,\n  maxVisible = 4,\n  position = 'top-right',\n  hideWhenDrawerOpen = true,\n  renderTile: renderTileProp,\n}: NotificationPopupsProps) {\n  const ctx = useOptionalNotifications();\n\n  // The age filter below needs the CURRENT time, and reading a clock in the\n  // render body makes the output depend on something React cannot replay. So\n  // `now` is state, advanced by an interval that only runs while something\n  // could still age out — an idle popup container ticks nothing.\n  //\n  // Both hooks sit above every early return on purpose: `ctx` may be absent\n  // and `showPopups` may flip at any time, and a hook below those branches\n  // would change how many hooks this component renders.\n  const [now, setNow] = useState(() => Date.now());\n  const hasAgeingNotifications = Boolean(ctx?.notifications.some(n => !n.read && !n.settled));\n  useEffect(() => {\n    if (!hasAgeingNotifications) return undefined;\n    const id = setInterval(() => setNow(Date.now()), 500);\n    return () => clearInterval(id);\n  }, [hasAgeingNotifications]);\n\n  if (!ctx) return null;\n\n  const { notifications, showPopups, isOpen, open, markRead, markSettled, renderTile: ctxRenderTile } = ctx;\n  const renderTile = renderTileProp ?? ctxRenderTile;\n\n  if (!showPopups) return null;\n  if (hideWhenDrawerOpen && isOpen) return null;\n\n  // Drops anything created while the popups were hidden — a notification that\n  // arrived behind an open drawer must not pop up late when the drawer closes.\n  const live = notifications\n    .filter(n => !n.read && !n.settled && now - n.createdAt < liveDurationMs)\n    .slice(0, maxVisible);\n\n  if (live.length === 0) return null;\n\n  const activate = (notification: Notification) => {\n    if (notification.onClick) {\n      notification.onClick();\n      markRead(notification.id);\n      markSettled(notification.id);\n      return;\n    }\n    open();\n    markSettled(notification.id);\n  };\n\n  const handleBodyClick = (notification: Notification) => (event: MouseEvent<HTMLDivElement>) => {\n    // Nested interactive controls (X, check button) handle their own actions.\n    if ((event.target as HTMLElement).closest('button')) return;\n    activate(notification);\n  };\n\n  const handleBodyKeyDown = (notification: Notification) => (event: KeyboardEvent<HTMLDivElement>) => {\n    if ((event.target as HTMLElement).closest('button')) return;\n    if (event.key !== 'Enter' && event.key !== ' ') return;\n    event.preventDefault();\n    activate(notification);\n  };\n\n  return (\n    <div\n      aria-live=\"polite\"\n      className={cn(\n        'pointer-events-none fixed z-[9999] flex w-[26rem] max-w-[calc(100vw-2rem)] flex-col gap-[var(--spacing-system-xs)]',\n        positionClasses[position],\n        className,\n      )}\n    >\n      {live.map(n => {\n        const custom = renderTile?.(n, { onComplete: markRead, onSettle: markSettled, liveDurationMs });\n        if (custom) {\n          return (\n            <div key={n.id} className=\"pointer-events-auto\">\n              {custom}\n            </div>\n          );\n        }\n        return (\n          // `role=\"button\"` rather than a real <button>: the row hosts its own\n          // nested interactive elements, which a <button> may not contain.\n          <div\n            key={n.id}\n            role=\"button\"\n            tabIndex={0}\n            onClick={handleBodyClick(n)}\n            onKeyDown={handleBodyKeyDown(n)}\n            className=\"pointer-events-auto cursor-pointer rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent\"\n          >\n            <NotificationTile\n              notification={n}\n              liveDurationMs={liveDurationMs}\n              onComplete={markRead}\n              onSettle={markSettled}\n            />\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","'use client';\n\nimport { useMemo, useState } from 'react';\nimport { ApprovalBatchMessage, ApprovalStatusTag } from '../../chat/approval-batch-message';\nimport { ExpandChevron } from '../../chat/expand-chevron';\nimport { useCollapsible } from '../../chat/hooks/use-collapsible';\nimport type { ChatApprovalStatus } from '../../chat/types';\nimport { Button } from '../../ui/button';\nimport { NotificationTile } from './notification-tile';\nimport { approvalMetaToBatchData, getApprovalMeta, resolutionToStatus } from './types';\nimport type { Notification } from './types';\n\nexport interface ApprovalRequestNotificationTileProps {\n  notification: Notification;\n  onApprove: (approvalRequestId: string) => void | Promise<void>;\n  onReject: (approvalRequestId: string) => void | Promise<void>;\n  onComplete: (id: string) => void;\n  onSettle?: (id: string) => void;\n  liveDurationMs?: number;\n  defaultExpanded?: boolean;\n  className?: string;\n}\n\nexport function ApprovalRequestNotificationTile({\n  notification,\n  onApprove,\n  onReject,\n  onComplete,\n  onSettle,\n  liveDurationMs,\n  defaultExpanded = false,\n  className,\n}: ApprovalRequestNotificationTileProps) {\n  const [expanded, setExpanded] = useState(defaultExpanded);\n  // Toggling the command section pins the tile so a live pop-up doesn't\n  // auto-dismiss out from under the user mid-read.\n  const [pinned, setPinned] = useState(defaultExpanded);\n  const [processing, setProcessing] = useState(false);\n  // Optimistic status for this user's own click; backend resolution (below) wins once it arrives.\n  const [localStatus, setLocalStatus] = useState<ChatApprovalStatus | null>(null);\n\n  const approval = getApprovalMeta(notification);\n  const resolvedStatus = resolutionToStatus(approval?.resolution);\n  const status: ChatApprovalStatus = resolvedStatus !== 'pending' ? resolvedStatus : (localStatus ?? 'pending');\n\n  const batchData = useMemo(() => (approval ? approvalMetaToBatchData(approval) : null), [approval]);\n\n  const { innerRef, containerStyle } = useCollapsible({ expanded });\n\n  if (!approval || !batchData) return null;\n\n  const tileNotification: Notification = {\n    ...notification,\n    type: notification.type ?? 'Approval Required',\n  };\n\n  const commandWord = batchData.toolCalls.length > 1 ? 'commands' : 'command';\n  const toggleLabel = `${expanded ? 'Hide' : 'Show'} ${commandWord} section`;\n\n  const resolve = async (\n    nextStatus: 'approved' | 'rejected',\n    action: (approvalRequestId: string) => void | Promise<void>,\n  ) => {\n    setProcessing(true);\n    setLocalStatus(nextStatus);\n    try {\n      await action(batchData.approvalRequestId);\n    } catch {\n      setLocalStatus(null); // roll back the optimistic flip; the request failed\n      return;\n    } finally {\n      setProcessing(false);\n    }\n    // Outside the try: a throwing consumer callback must not roll back a request\n    // that already succeeded server-side.\n    onComplete(notification.id);\n  };\n\n  return (\n    <NotificationTile\n      notification={tileNotification}\n      liveDurationMs={liveDurationMs}\n      onComplete={onComplete}\n      onSettle={onSettle}\n      className={className}\n      paused={pinned || processing}\n      actions={\n        <div className=\"flex w-full items-center gap-[var(--spacing-system-xs)]\">\n          {status === 'pending' ? (\n            <>\n              <Button\n                variant=\"accent\"\n                size=\"small\"\n                onClick={() => resolve('approved', onApprove)}\n                disabled={processing}\n              >\n                Approve\n              </Button>\n              <Button\n                variant=\"outline\"\n                size=\"small\"\n                onClick={() => resolve('rejected', onReject)}\n                disabled={processing}\n              >\n                Reject\n              </Button>\n            </>\n          ) : (\n            <span className=\"flex min-w-0 items-center gap-[var(--spacing-system-xsf)]\">\n              <ApprovalStatusTag status={status} />\n              {approval.resolvedByName ? (\n                <span className=\"truncate text-ods-text-secondary text-h6\">by {approval.resolvedByName}</span>\n              ) : null}\n            </span>\n          )}\n          <Button\n            variant=\"outline\"\n            size=\"small\"\n            onClick={() => {\n              setExpanded(prev => !prev);\n              setPinned(true);\n            }}\n            aria-expanded={expanded}\n            aria-label={toggleLabel}\n            className=\"ml-auto w-6 shrink-0 px-0 md:w-8\"\n          >\n            <ExpandChevron expanded={expanded} />\n          </Button>\n        </div>\n      }\n    >\n      <div style={containerStyle}>\n        {/* Divider lives inside the measured element — useCollapsible caps the\n            container at the inner scrollHeight, which excludes the inner's own borders. */}\n        <div ref={innerRef}>\n          <div className=\"border-t border-ods-border\">\n            <ApprovalBatchMessage\n              data={batchData}\n              showExecutionStatus={false}\n              showFooterActions={false}\n              maxBodyHeight=\"50vh\"\n              className=\"mb-0 rounded-none border-0\"\n            />\n          </div>\n        </div>\n      </div>\n    </NotificationTile>\n  );\n}\n","import type { ReactNode } from 'react';\nimport { APPROVAL_STATUS, type ApprovalBatchData, type ChatApprovalStatus } from '../../chat/types';\n\nexport type NotificationVariant = 'default' | 'success' | 'warning' | 'error' | 'info';\n\nexport type NotificationSeverity = 'INFO' | 'SUCCESS' | 'WARNING' | 'DANGER';\n\nexport interface Notification {\n  id: string;\n  variant?: NotificationVariant;\n  /** Kind label rendered uppercase in the tile header (e.g. \"New Customer Created\"). */\n  type?: string;\n  /** Custom icon for the tile header slot; inherits the severity color via `currentColor`. */\n  icon?: ReactNode;\n  /**\n   * Image URL for the tile header slot; takes precedence over `icon`. Proxy external\n   * URLs on the consumer side (ad-blockers routinely block direct third-party avatars);\n   * on load failure the tile falls back to `icon`/dot.\n   */\n  imageUrl?: string;\n  title: ReactNode;\n  description?: ReactNode;\n  createdAt: number;\n  read?: boolean;\n  settled?: boolean;\n  severity?: NotificationSeverity;\n  category?: string;\n  meta?: Record<string, unknown>;\n  onClick?: () => void;\n}\n\nexport type AddNotificationInput = Omit<Notification, 'id' | 'createdAt' | 'read' | 'settled'> & {\n  id?: string;\n  createdAt?: number;\n  read?: boolean;\n};\n\n/** Per-notification tile renderer; return a node to override the default `NotificationTile`, or `undefined` to fall back. */\nexport type RenderNotificationTile = (\n  notification: Notification,\n  helpers: { onComplete: (id: string) => void; onSettle: (id: string) => void; liveDurationMs?: number },\n) => ReactNode;\n\n/** Discriminator carried in `notification.meta.contextType` for Mingo approval requests. */\nexport const ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE = 'ADMIN_APPROVAL_REQUEST';\n\n/** A single planned tool invocation awaiting approval. */\nexport interface ApprovalToolCallMeta {\n  toolExecutionRequestId?: string | null;\n  toolName: string;\n  toolTitle?: string | null;\n  toolExplanation?: string | null;\n  toolType?: string | null;\n  requiresApproval?: boolean;\n  approvalType?: string | null;\n  toolCallArguments?: Record<string, unknown> | null;\n}\n\n/** Approval payload stashed in `notification.meta` for an `ADMIN_APPROVAL_REQUEST` notification. */\nexport interface ApprovalNotificationMeta {\n  approvalRequestId: string;\n  dialogId?: string | null;\n  ticketId?: string | null;\n  approvalType?: string | null;\n  /** Backend `ApprovalResolution`: PENDING | APPROVED | REJECTED | CANCELLED. Null until resolved. */\n  resolution?: string | null;\n  /** Display name of the user who resolved the request; null until resolved (or for system actions). */\n  resolvedByName?: string | null;\n  toolCalls: ApprovalToolCallMeta[];\n}\n\nexport function isApprovalNotification(notification: Notification): boolean {\n  return notification.meta?.contextType === ADMIN_APPROVAL_REQUEST_CONTEXT_TYPE;\n}\n\n/** Safely read the approval payload off a notification; returns null when absent or malformed. */\nexport function getApprovalMeta(notification: Notification): ApprovalNotificationMeta | null {\n  if (!isApprovalNotification(notification)) return null;\n  const meta = notification.meta;\n  const approvalRequestId = meta?.approvalRequestId;\n  const toolCalls = meta?.toolCalls;\n  if (typeof approvalRequestId !== 'string' || !Array.isArray(toolCalls)) return null;\n  return {\n    approvalRequestId,\n    dialogId: (meta?.dialogId as string | undefined) ?? null,\n    ticketId: (meta?.ticketId as string | undefined) ?? null,\n    approvalType: (meta?.approvalType as string | undefined) ?? null,\n    resolution: (meta?.resolution as string | undefined) ?? null,\n    resolvedByName: (meta?.resolvedByName as string | undefined) ?? null,\n    toolCalls: toolCalls as ApprovalToolCallMeta[],\n  };\n}\n\n/** Map the backend `ApprovalResolution` carried on a notification to a tile/batch status. */\nexport function resolutionToStatus(resolution: unknown): ChatApprovalStatus {\n  switch (typeof resolution === 'string' ? resolution.toUpperCase() : undefined) {\n    case 'APPROVED':\n      return APPROVAL_STATUS.APPROVED;\n    case 'REJECTED':\n      return APPROVAL_STATUS.REJECTED;\n    case 'CANCELLED':\n      return APPROVAL_STATUS.CANCELLED;\n    default:\n      return APPROVAL_STATUS.PENDING;\n  }\n}\n\n/** Build the `ApprovalBatchMessage` data payload from an approval notification's meta. */\nexport function approvalMetaToBatchData(approval: ApprovalNotificationMeta): ApprovalBatchData {\n  return {\n    approvalRequestId: approval.approvalRequestId,\n    approvalType: approval.approvalType ?? '',\n    toolCalls: approval.toolCalls.map((tc, index) => ({\n      toolExecutionRequestId: tc.toolExecutionRequestId ?? `${approval.approvalRequestId}:${index}`,\n      toolName: tc.toolName,\n      toolTitle: tc.toolTitle ?? undefined,\n      toolExplanation: tc.toolExplanation ?? undefined,\n      toolType: tc.toolType ?? undefined,\n      requiresApproval: tc.requiresApproval ?? true,\n      approvalType: tc.approvalType ?? null,\n      toolCallArguments: tc.toolCallArguments ?? null,\n    })),\n  };\n}\n","'use client';\n\nimport { type ReactNode, createContext, useCallback, useContext, useState } from 'react';\nimport type { TimeTrackerData } from './types';\n\ninterface TimeTrackerContextValue extends TimeTrackerData {\n  isOpen: boolean;\n  open: () => void;\n  close: () => void;\n  toggle: () => void;\n}\n\nconst TimeTrackerContext = createContext<TimeTrackerContextValue | null>(null);\n\nexport interface TimeTrackerProviderProps extends TimeTrackerData {\n  children: ReactNode;\n  defaultOpen?: boolean;\n  /**\n   * Whether the tracker is available to this host at all (feature flag, session,\n   * entitlement). `false` provides NO context — `useOptionalTimeTracker()` reads\n   * `null` and every surface hides itself, exactly as if this provider were absent.\n   *\n   * It exists so a host never has to mount this CONDITIONALLY. A host that writes\n   * `enabled ? <TimeTrackerProvider>{children}</TimeTrackerProvider> : <>{children}</>`\n   * changes the element TYPE at that position the moment its flag answers, and React\n   * tears down and remounts everything below — the whole app shell, in the case this\n   * was added for. Mount it always and pass the answer here instead.\n   */\n  enabled?: boolean;\n}\n\nexport function TimeTrackerProvider({\n  children,\n  defaultOpen = false,\n  enabled = true,\n  ...data\n}: TimeTrackerProviderProps) {\n  const [isOpen, setIsOpen] = useState(defaultOpen);\n\n  const open = useCallback(() => setIsOpen(true), []);\n  const close = useCallback(() => setIsOpen(false), []);\n  const toggle = useCallback(() => setIsOpen(v => !v), []);\n\n  // `data` is a fresh object each render, so the value identity tracks the host's\n  // render — the per-second timer tick lives in `useTrackerClock` (local state),\n  // not here, so consumers don't re-render on every tick.\n  const value: TimeTrackerContextValue = { ...data, isOpen, open, close, toggle };\n\n  return <TimeTrackerContext.Provider value={enabled ? value : null}>{children}</TimeTrackerContext.Provider>;\n}\n\nexport function useTimeTracker(): TimeTrackerContextValue {\n  const ctx = useContext(TimeTrackerContext);\n  if (!ctx) {\n    throw new Error('useTimeTracker must be used inside <TimeTrackerProvider>');\n  }\n  return ctx;\n}\n\nexport function useOptionalTimeTracker(): TimeTrackerContextValue | null {\n  return useContext(TimeTrackerContext);\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { cn } from '../../../utils/cn';\nimport {\n  ArrowRightUpIcon,\n  CheckCircleIcon,\n  CheckIcon,\n  Chevron02RightIcon,\n  ClockHistoryIcon,\n  DotsLoaderIcon,\n  PauseIcon,\n  PlayIcon,\n  PlusCircleIcon,\n  XmarkIcon,\n} from '../../icons-v2-generated';\nimport { Autocomplete, type AutocompleteOption } from '../../ui/autocomplete';\nimport { Button } from '../../ui/button/button';\nimport { SplitButton } from '../../ui/button/split-button';\nimport { SquareAvatar } from '../../ui/square-avatar';\nimport { Tag } from '../../ui/tag';\nimport { Textarea } from '../../ui/textarea';\nimport type { TimeTrackerData, TimeTrackerEntry, TimeTrackerStatus } from './types';\nimport { useTrackerClock } from './use-tracker-clock';\n\ninterface CustomerAutocompleteOption extends AutocompleteOption {\n  imageUrl?: string;\n}\n\nfunction renderCustomerOption(option: AutocompleteOption, isSelected: boolean) {\n  const { label, imageUrl } = option as CustomerAutocompleteOption;\n  return (\n    <div className=\"flex w-full min-w-0 items-center justify-between gap-[var(--spacing-system-xs)]\">\n      <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xs)]\">\n        <SquareAvatar src={imageUrl} alt={label} fallback={label} size=\"sm\" variant=\"square\" />\n        <span className=\"truncate\" title={label}>\n          {label}\n        </span>\n      </div>\n      {isSelected && <CheckIcon className=\"text-ods-accent\" size={20} />}\n    </div>\n  );\n}\n\nexport interface TimeTrackerPanelProps extends TimeTrackerData {\n  onClose: () => void;\n  className?: string;\n}\n\nexport function TimeTrackerPanel({\n  status,\n  runningSince,\n  accumulatedMs,\n  ticketOptions,\n  selectedTicketId,\n  onSelectedTicketChange,\n  onTicketSearch,\n  ticketsLoading,\n  customerOptions,\n  selectedCustomerId,\n  onSelectedCustomerChange,\n  onCustomerSearch,\n  customersLoading,\n  customerLocked,\n  notes,\n  onNotesChange,\n  lastEntries,\n  onStart,\n  onPause,\n  onResume,\n  onCancel,\n  onSubmit,\n  onManualEntry,\n  onOpenMyTime,\n  onOpenMyTimeMenu,\n  onEntryClick,\n  isStarting,\n  isSubmitting,\n  onClose,\n  className,\n}: TimeTrackerPanelProps) {\n  const elapsedLabel = useTrackerClock({ status, runningSince, accumulatedMs });\n\n  const [showValidationError, setShowValidationError] = useState(false);\n\n  const isRunning = status === 'tracking';\n  const isActive = status !== 'ready';\n  const hasContent = selectedTicketId != null || notes.trim() !== '';\n  const showFieldError = isActive && showValidationError && !hasContent;\n\n  // Reset the validation flag once a session ends so the next one starts clean.\n  // Adjusted while rendering, not from an effect: the flag is only ever READ\n  // through `showFieldError` above, so publishing the reset a commit later\n  // bought nothing and cost a render pass on every session end.\n  const [wasReady, setWasReady] = useState(!isActive);\n  if (wasReady !== !isActive) {\n    setWasReady(!isActive);\n    if (!isActive) setShowValidationError(false);\n  }\n\n  const handleSubmit = () => {\n    if (!hasContent) {\n      setShowValidationError(true);\n      return;\n    }\n    onSubmit();\n  };\n\n  const handleManualEntry = onManualEntry\n    ? () => {\n        onClose();\n        onManualEntry();\n      }\n    : undefined;\n  const handleEntryClick = onEntryClick\n    ? (entry: TimeTrackerEntry) => {\n        onClose();\n        onEntryClick(entry);\n      }\n    : undefined;\n  const handleOpenMyTime = onOpenMyTime\n    ? () => {\n        onClose();\n        onOpenMyTime();\n      }\n    : undefined;\n  const handleCancel = () => {\n    onClose();\n    onCancel();\n  };\n\n  const ticketAutocompleteOptions: AutocompleteOption[] = ticketOptions.map(t => ({\n    label: t.label,\n    value: t.id,\n  }));\n\n  const showCustomer = !!onSelectedCustomerChange;\n  const customerAutocompleteOptions: CustomerAutocompleteOption[] = (customerOptions ?? []).map(c => ({\n    label: c.label,\n    value: c.id,\n    imageUrl: c.imageUrl,\n  }));\n  const selectedCustomer = customerAutocompleteOptions.find(o => o.value === selectedCustomerId);\n  const customerStartAdornment = selectedCustomer ? (\n    <SquareAvatar\n      src={selectedCustomer.imageUrl}\n      alt={selectedCustomer.label}\n      fallback={selectedCustomer.label}\n      size=\"sm\"\n      variant=\"square\"\n    />\n  ) : undefined;\n\n  const visibleEntries = lastEntries.slice(0, 3);\n\n  return (\n    <div\n      className={cn(\n        'relative flex max-h-[calc(100vh-6rem)] w-full flex-col gap-[var(--spacing-system-l)] overflow-y-auto rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]',\n        className,\n      )}\n    >\n      <Button\n        variant=\"transparent\"\n        size=\"icon-sm\"\n        onClick={onClose}\n        aria-label=\"Close time tracker\"\n        className=\"absolute right-[var(--spacing-system-sf)] top-[var(--spacing-system-sf)] hidden text-ods-text-secondary hover:text-ods-text-primary md:inline-flex [&_svg]:!size-6\"\n      >\n        <XmarkIcon />\n      </Button>\n\n      {/* Status + timer + ticket assignment */}\n      <div className=\"flex flex-col gap-[var(--spacing-system-m)]\">\n        <div className=\"flex items-center gap-[var(--spacing-system-s)]\">\n          <StatusTag status={status} />\n          {isActive && (\n            <Button\n              variant=\"transparent\"\n              onClick={handleCancel}\n              className=\"h-auto p-0 font-medium text-ods-text-secondary underline text-h6 hover:bg-transparent hover:text-ods-text-primary md:h-auto\"\n            >\n              Cancel Entry\n            </Button>\n          )}\n        </div>\n\n        <div className=\"flex items-stretch overflow-hidden rounded-md border border-ods-border\">\n          <div className=\"flex flex-1 items-center border-r border-ods-border bg-ods-bg px-[var(--spacing-system-m)] py-[var(--spacing-system-s)]\">\n            <span\n              className={cn(\n                '!font-mono font-bold tabular-nums text-h3',\n                isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n              )}\n            >\n              {elapsedLabel}\n            </span>\n          </div>\n\n          <Button\n            size=\"icon\"\n            variant={isActive ? 'transparent' : 'accent'}\n            onClick={status === 'tracking' ? onPause : status === 'paused' ? onResume : onStart}\n            disabled={isStarting}\n            aria-label={isRunning ? 'Pause tracking' : isActive ? 'Resume tracking' : 'Start tracking'}\n            className=\"h-auto w-auto rounded-none transition-none [&_svg]:!size-6\"\n          >\n            {isRunning ? <PauseIcon /> : <PlayIcon />}\n          </Button>\n\n          {isActive && (\n            <Button\n              size=\"icon\"\n              variant=\"accent\"\n              onClick={handleSubmit}\n              disabled={isSubmitting}\n              aria-label=\"Finish and save tracking\"\n              className=\"h-auto w-auto rounded-none border-l border-ods-border [&_svg]:!size-6\"\n            >\n              <CheckCircleIcon />\n            </Button>\n          )}\n        </div>\n\n        <div className={cn('grid gap-[var(--spacing-system-m)]', showCustomer && 'grid-cols-2')}>\n          <Autocomplete\n            value={selectedTicketId}\n            onChange={onSelectedTicketChange}\n            options={ticketAutocompleteOptions}\n            placeholder=\"Select Ticket\"\n            loading={ticketsLoading}\n            invalid={showFieldError}\n            error={showFieldError ? 'Required if no notes added' : undefined}\n            disableClientFilter={!!onTicketSearch}\n            onInputChange={(value, reason) => {\n              if (reason === 'input') onTicketSearch?.(value);\n            }}\n          />\n          {showCustomer && (\n            <Autocomplete\n              value={selectedCustomerId ?? null}\n              onChange={onSelectedCustomerChange}\n              options={customerAutocompleteOptions}\n              placeholder=\"Select Customer\"\n              loading={customersLoading}\n              disabled={customerLocked}\n              disableClientFilter={!!onCustomerSearch}\n              onInputChange={(value, reason) => {\n                if (reason === 'input') onCustomerSearch?.(value);\n              }}\n              startAdornment={customerStartAdornment}\n              renderOption={renderCustomerOption}\n            />\n          )}\n        </div>\n      </div>\n\n      <Textarea\n        value={notes}\n        onChange={e => onNotesChange(e.target.value)}\n        invalid={showFieldError}\n        error={showFieldError ? 'Required if no ticket assigned' : undefined}\n        placeholder=\"Additional Notes (optional if ticket selected)\"\n        className=\"max-h-[280px]\"\n      />\n\n      {/* Last entries + footer */}\n      <div className=\"flex flex-col gap-[var(--spacing-system-m)]\">\n        {visibleEntries.length === 0 ? (\n          <div className=\"flex min-h-[268px] flex-col items-center justify-center gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)] text-center text-ods-text-secondary\">\n            <ClockHistoryIcon className=\"size-6\" />\n            <div className=\"flex flex-col\">\n              <p className=\"font-medium text-h4\">No time logged</p>\n              <p className=\"text-h6\">Last entries will appear here</p>\n            </div>\n          </div>\n        ) : (\n          <div className=\"flex flex-col gap-[var(--spacing-system-xs)]\">\n            <p className=\"font-mono uppercase tracking-wide text-ods-text-secondary text-h6\">Last Entries</p>\n            <div className=\"overflow-hidden rounded-md border border-ods-border bg-ods-card\">\n              {visibleEntries.map(entry => (\n                <LastEntryRow key={entry.id} entry={entry} onClick={handleEntryClick} />\n              ))}\n            </div>\n          </div>\n        )}\n\n        <div className=\"grid grid-cols-2 gap-[var(--spacing-system-m)]\">\n          <Button\n            variant=\"outline\"\n            className=\"min-w-0\"\n            onClick={handleManualEntry}\n            disabled={!onManualEntry}\n            leftIcon={<PlusCircleIcon className=\"text-ods-text-secondary\" />}\n          >\n            Manual Entry\n          </Button>\n          <SplitButton\n            variant=\"outline\"\n            fullWidth\n            className=\"min-w-0\"\n            onClick={handleOpenMyTime}\n            disabled={!onOpenMyTime && !onOpenMyTimeMenu}\n            mainDisabled={!onOpenMyTime}\n            groupAriaLabel=\"Open my time\"\n            iconAction={{\n              icon: <ArrowRightUpIcon size={24} />,\n              'aria-label': 'Open my time in a new view',\n              onClick: onOpenMyTimeMenu,\n              disabled: !onOpenMyTimeMenu,\n              openInNewTab: true,\n            }}\n          >\n            Open My Time\n          </SplitButton>\n        </div>\n      </div>\n\n      <Button variant=\"outline\" fullWidth onClick={onClose} className=\"md:hidden\">\n        Close\n      </Button>\n    </div>\n  );\n}\n\nconst STATUS_TAG_SKIN = 'gap-[var(--spacing-system-xs)] font-mono font-medium uppercase tracking-tight';\n\nfunction StatusTag({ status }: { status: TimeTrackerStatus }) {\n  if (status === 'tracking') {\n    return (\n      <Tag\n        variant=\"success\"\n        className={STATUS_TAG_SKIN}\n        icon={<DotsLoaderIcon className=\"h-4 w-4\" />}\n        label=\"Tracking\"\n      />\n    );\n  }\n  if (status === 'paused') {\n    return (\n      <Tag variant=\"warning\" className={STATUS_TAG_SKIN} icon={<PauseIcon className=\"h-4 w-4\" />} label=\"Paused\" />\n    );\n  }\n  return <Tag variant=\"grey\" className={STATUS_TAG_SKIN} label=\"Ready to Track\" />;\n}\n\ninterface LastEntryRowProps {\n  entry: TimeTrackerEntry;\n  onClick?: (entry: TimeTrackerEntry) => void;\n}\n\nfunction LastEntryRow({ entry, onClick }: LastEntryRowProps) {\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-m)] border-b border-ods-border bg-ods-bg px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] last:border-b-0\">\n      <div className=\"flex w-[88px] shrink-0 flex-col justify-center\">\n        <span className=\"truncate font-medium text-ods-text-primary text-h4\">{entry.durationLabel}</span>\n        <span className=\"truncate text-ods-text-secondary text-h6\">{entry.dateLabel}</span>\n      </div>\n\n      <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n        <span className=\"truncate font-medium text-ods-text-primary text-h4\">{entry.title}</span>\n        {entry.description && <span className=\"truncate text-ods-text-secondary text-h6\">{entry.description}</span>}\n      </div>\n\n      <Button\n        variant=\"outline\"\n        size=\"icon\"\n        onClick={() => onClick?.(entry)}\n        disabled={!onClick}\n        aria-label={`Open entry: ${entry.title}`}\n        className=\"h-12 w-12 shrink-0 [&_svg]:!size-6\"\n      >\n        <Chevron02RightIcon />\n      </Button>\n    </div>\n  );\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\nimport type { TimeTrackerStatus } from './types';\n\nexport interface UseTrackerClockArgs {\n  status: TimeTrackerStatus;\n  /** Epoch ms the current running segment started (only used while tracking). */\n  runningSince?: number | null;\n  /** Elapsed ms accrued before the current running segment. */\n  accumulatedMs?: number;\n}\n\nfunction formatElapsed(totalMs: number): string {\n  const totalSeconds = Math.max(0, Math.floor(totalMs / 1000));\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = totalSeconds % 60;\n  const pad = (n: number) => String(n).padStart(2, '0');\n  return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;\n}\n\n/**\n * Display-only timer. Ticks once per second while `status === 'tracking'`; otherwise\n * renders the static `accumulatedMs`. Returns a formatted `HH:MM:SS` string.\n */\nexport function useTrackerClock({ status, runningSince, accumulatedMs = 0 }: UseTrackerClockArgs): string {\n  const [now, setNow] = useState(() => Date.now());\n\n  // The wall clock is the external system here, and `setInterval` is the\n  // subscription to it. The seed read shares the subscription's callback rather\n  // than being a separate setState in the effect body — without it the label\n  // would show the previous segment's elapsed time for up to a second after\n  // tracking starts.\n  useEffect(() => {\n    if (status !== 'tracking') return undefined;\n    const tick = () => setNow(Date.now());\n    tick();\n    const id = setInterval(tick, 1000);\n    return () => clearInterval(id);\n  }, [status, runningSince]);\n\n  const liveMs = status === 'tracking' && runningSince ? now - runningSince : 0;\n  return formatElapsed(accumulatedMs + liveMs);\n}\n","'use client';\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { useEffect, useRef, useState } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { ClockHistoryIcon } from '../../icons-v2-generated/date-and-time/clock-history-icon';\nimport {\n  type AppLayoutDrawerHandle,\n  useAppLayoutDrawerContainer,\n  useAppLayoutDrawerCoordination,\n} from '../../navigation/app-layout-context';\nimport { HeaderButton } from '../../navigation/header-button';\nimport { OVERLAY_BACKDROP_CLASS } from '../../ui/drawer';\nimport { useOptionalTimeTracker } from './time-tracker-context';\nimport { TimeTrackerPanel } from './time-tracker-panel';\nimport { useTrackerClock } from './use-tracker-clock';\n\nexport interface TimeTrackerHeaderButtonProps {\n  /** Dimming/disabled passthrough from the header. */\n  className?: string;\n  disabled?: boolean;\n}\n\n/**\n * Header affordance for the time tracker. Renders nothing unless wrapped in a\n * `<TimeTrackerProvider>`. The trigger is the standard `HeaderButton`; when a\n * session is active it also shows the live elapsed time. The popup is a Radix\n * Popover anchored under the button (not a modal/drawer). Inside AppLayout the\n * dim backdrop covers only the main content area (header and sidebar stay\n * interactive) and the popup joins the layout's panel coordination — opening\n * it closes any open in-layout drawer and vice versa. Outside AppLayout it\n * falls back to a viewport-wide backdrop.\n */\nexport function TimeTrackerHeaderButton({ className, disabled }: TimeTrackerHeaderButtonProps) {\n  const ctx = useOptionalTimeTracker();\n  const elapsedLabel = useTrackerClock({\n    status: ctx?.status ?? 'ready',\n    runningSince: ctx?.runningSince,\n    accumulatedMs: ctx?.accumulatedMs,\n  });\n  const layoutContainer = useAppLayoutDrawerContainer();\n  const coordination = useAppLayoutDrawerCoordination();\n\n  const isOpen = ctx?.isOpen ?? false;\n  // Latest-refs the stable handle below reads. Refreshed in an unconditional\n  // effect, not the render body: `close()` is only ever invoked by the\n  // coordinator in response to ANOTHER panel opening, which is always past a\n  // commit, and a render attempt React discards must not be able to leave the\n  // handle pointing at a `close` callback that never took effect.\n  const isOpenRef = useRef(isOpen);\n  const closeRef = useRef(ctx?.close);\n  useEffect(() => {\n    isOpenRef.current = isOpen;\n    closeRef.current = ctx?.close;\n  });\n\n  // Stable handle: the same object must be registered AND passed as `self`\n  // to notifyDrawerDidOpen so the coordinator can skip it when closing the\n  // other panels. Built once by `useState`'s lazy initialiser; it closes over\n  // the refs above, which never change identity.\n  const [selfHandle] = useState<AppLayoutDrawerHandle>(() => ({\n    close: () => {\n      if (isOpenRef.current) closeRef.current?.();\n    },\n  }));\n\n  useEffect(() => {\n    if (isOpen) coordination?.notifyDrawerDidOpen(selfHandle);\n  }, [isOpen, coordination, selfHandle]);\n\n  useEffect(() => coordination?.registerDrawer(selfHandle), [coordination, selfHandle]);\n\n  if (!ctx) return null;\n\n  const { open, close, status } = ctx;\n  const isPaused = status === 'paused';\n  const isActive = status === 'tracking' || isPaused;\n\n  return (\n    <PopoverPrimitive.Root open={isOpen} onOpenChange={next => (next ? open() : close())}>\n      <PopoverPrimitive.Trigger asChild>\n        <HeaderButton\n          aria-label=\"Time tracker\"\n          isActive={isOpen}\n          disabled={disabled}\n          className={cn(\n            'outline-none',\n            // ODS top-navigation spec (Figma 2797-5978): the active cell keeps\n            // its square icon-only footprint on mobile and becomes a fixed\n            // 144px cell with the live clock from md up.\n            isActive &&\n              'md:w-auto md:min-w-[144px] md:justify-center md:gap-[var(--spacing-system-xsf)] md:px-[var(--spacing-system-sf)]',\n            className,\n          )}\n          icon={\n            <>\n              <ClockHistoryIcon\n                className={cn('h-6 w-6', isPaused ? 'text-ods-text-secondary' : isActive && 'text-ods-accent')}\n              />\n              {isActive && (\n                <span\n                  className={cn(\n                    'hidden !font-mono text-h5 md:inline md:text-h4',\n                    isPaused ? 'text-ods-text-secondary' : 'text-ods-text-primary',\n                  )}\n                >\n                  {elapsedLabel}\n                </span>\n              )}\n            </>\n          }\n        />\n      </PopoverPrimitive.Trigger>\n      {/* Popovers have no built-in overlay — portal a dim backdrop behind the\n          panel. Clicks land on it (outside Content), so Radix still closes the\n          popover. Own Portal: Radix's Portal slots a single child. Inside\n          AppLayout the backdrop portals into the main-area container (absolute,\n          AppLayoutDrawer-overlay tier) so header/sidebar stay undimmed and\n          interactive; standalone it dims the whole viewport. */}\n      <PopoverPrimitive.Portal container={layoutContainer ?? undefined}>\n        <div\n          aria-hidden=\"true\"\n          data-state={isOpen ? 'open' : 'closed'}\n          className={cn(\n            layoutContainer ? 'absolute inset-0 z-[102]' : 'fixed inset-0 z-[1299]',\n            OVERLAY_BACKDROP_CLASS,\n            'data-[state=open]:animate-in data-[state=open]:fade-in-0',\n            'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',\n          )}\n        />\n      </PopoverPrimitive.Portal>\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Content\n          align=\"end\"\n          sideOffset={8}\n          collisionPadding={8}\n          className={cn(\n            'z-[1300] w-[460px] max-w-[calc(100vw-1rem)] outline-none',\n            'data-[state=open]:animate-in data-[state=closed]:animate-out',\n            'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n            'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',\n          )}\n        >\n          <TimeTrackerPanel {...ctx} onClose={close} />\n        </PopoverPrimitive.Content>\n      </PopoverPrimitive.Portal>\n    </PopoverPrimitive.Root>\n  );\n}\n","'use client';\n\nimport { cn } from '../../utils/cn';\n\n/**\n * The ONE unread-indicator dot (`bg-ods-warning`, top-right of an icon\n * inside a `relative` wrapper). Previously duplicated in\n * `NotificationsHeaderButton` and `NavigationSidebarItemButton` — both\n * now render THIS, as does `TicketAlertsButton`.\n *\n * Size variants preserve the two pre-existing renders exactly (neither\n * may silently change):\n *   - `responsive` — header cells: `w-1.5 h-1.5 md:w-2 md:h-2`\n *   - `fixed`      — sidebar collapsed icon: `w-2 h-2`\n */\nexport interface UnreadDotProps {\n  size?: 'responsive' | 'fixed';\n  className?: string;\n}\n\nexport function UnreadDot({ size = 'responsive', className }: UnreadDotProps) {\n  return (\n    <span\n      className={cn(\n        'absolute right-0 top-0 rounded-full bg-ods-warning',\n        size === 'responsive' ? 'h-1.5 w-1.5 md:h-2 md:w-2' : 'h-2 w-2',\n        className,\n      )}\n    />\n  );\n}\n\n/**\n * Count variant of the unread indicator — a small accent pill anchored\n * top-right of an icon inside a `relative` wrapper, clamped at \"9+\".\n * Same family as `UnreadDot` (dot = \"something new\", count = \"N new\");\n * the sidebar's expanded-row pill stays its own larger treatment.\n */\nexport interface UnreadCountBadgeProps {\n  count: number;\n  className?: string;\n}\n\nexport function UnreadCountBadge({ count, className }: UnreadCountBadgeProps) {\n  if (count <= 0) return null;\n  return (\n    <span\n      className={cn(\n        'absolute -right-1.5 -top-1.5 flex items-center justify-center',\n        'h-4 min-w-4 rounded-full bg-ods-accent px-1',\n        'leading-none text-ods-text-on-accent text-badge',\n        className,\n      )}\n    >\n      {count > 9 ? '9+' : count}\n    </span>\n  );\n}\n\nexport default UnreadDot;\n","'use client';\n\nimport type React from 'react';\nimport { cn } from '../../utils/cn';\nimport { UnreadDot } from './unread-dot';\n\nexport interface HeaderButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Whether the button is in active/pressed state */\n  isActive?: boolean;\n  /** Icon to display in the button */\n  icon: React.ReactNode;\n  /** Render the shared `UnreadDot` at the icon's top-right (the\n   *  notifications-bell treatment, promoted to the cell primitive so\n   *  indicator cells never copy the dot markup). */\n  showUnreadDot?: boolean;\n  /** Additional class names */\n  className?: string;\n}\n\nexport function HeaderButton({\n  isActive = false,\n  icon,\n  showUnreadDot = false,\n  className,\n  ...props\n}: HeaderButtonProps) {\n  return (\n    <button\n      className={cn(\n        'flex shrink-0 items-center justify-center',\n        'transition-colors duration-200',\n        // Square cell width follows the TopNavigation size (56px small /\n        // 72px big) via the shell's --top-nav-cell var; 56px fallback for\n        // standalone use.\n        'h-full w-[var(--top-nav-cell,3.5rem)]',\n        isActive\n          ? 'bg-ods-bg-active text-ods-text-primary'\n          : // Transparent at rest so the cell inherits the bar's background\n            // (TopNavigation `backgroundClassName` can differ per platform).\n            'bg-transparent text-ods-text-secondary hover:bg-ods-bg-hover',\n        className,\n      )}\n      {...props}\n    >\n      {showUnreadDot ? (\n        <span className=\"relative inline-flex\">\n          {icon}\n          <UnreadDot />\n        </span>\n      ) : (\n        icon\n      )}\n    </button>\n  );\n}\n\nexport default HeaderButton;\n","'use client';\n\nimport { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';\nimport { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';\nimport { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/utils/prevent-unhandled';\nimport { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';\nimport { createPortal } from 'react-dom';\nimport { DRAG_AND_DROP_MEDIA_QUERY } from '../../../hooks/ui/use-drag-and-drop-enabled';\nimport { useHorizontalScrollbar } from '../../../hooks/ui/use-horizontal-scrollbar';\nimport { useIsomorphicLayoutEffect } from '../../../hooks/ui/use-isomorphic-layout-effect';\nimport { useMediaQuery } from '../../../hooks/ui/use-media-query';\nimport { autoScrollBothWays } from '../../../utils/auto-scroll-ancestors';\nimport { cn } from '../../../utils/cn';\nimport { nearestScrollableAncestor } from '../../../utils/scroll-parent';\nimport {\n  aimAnchor,\n  aimFromTarget,\n  aimToChange,\n  type ArrowKey,\n  type BoardAim,\n  initialAim,\n  laneOf,\n  moveAim,\n} from './aim';\nimport { BoardColumn } from './board-column';\nimport { BoardLiftContext, type DropAim, DropAimContext, isSameAim } from './drop-aim';\nimport { cardById, DROP_LINE_ATTRIBUTE } from './lane-geometry';\nimport { applyPendingMove, hasMoveSettled, LandingCardContext, type PendingMove } from './pending-move';\nimport { resolveBoardDrop } from './resolve-drop';\nimport { DRAG_PREVIEW_OPACITY, TicketCardView } from './ticket-card';\nimport { TouchBoard } from './touch-board';\nimport type { BoardChange, BoardColumnDef, BoardTicket } from './types';\nimport { useBoardCollapse } from './use-board-collapse';\n\n/** How long the board may keep standing in for `columns` while waiting for the\n *  host's answer. Only a host that drops the change on the floor ever reaches\n *  this — it exists so a failed move can't be shown as done forever, not as a\n *  timing assumption. */\nconst PENDING_MOVE_TIMEOUT_MS = 2000;\n\n/** How long a dropped card takes to travel from the pointer into its slot. */\nconst DROP_MS = 160;\nconst DROP_EASING = 'cubic-bezier(0.2, 0, 0, 1)';\n\n/** Above the header, the in-layout drawers and the header menus (z-[104]),\n *  BELOW modals (`ModalV2` overlay z-[1300]). The carried card itself is the\n *  browser's drag image and needs no z-index at all; this stacks only the\n *  160ms landing glide, which never leaves the lanes - a release anywhere\n *  else cancels, and no modal or drawer is open while a card is in the air.\n *  The one modal the glide can meet is the one the drop itself opens (Take\n *  Over / Reopen, mounted in the same commit as the glide), and that modal has\n *  to cover the glide, not be flown over by it. */\nconst DRAG_PREVIEW_Z = 1200;\n\nconst ARROW_KEYS: ArrowKey[] = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];\n\n/** What it takes to draw the carried card again, at rest, exactly where the\n *  browser's drag image was when it was let go. Measured once at drag start. */\ninterface DragGeometry {\n  ticket: BoardColumnDef['tickets'][number];\n  columnColor?: string;\n  width: number;\n  /** Pointer-to-card-corner offset, so the copy lands under the same grip. */\n  grabX: number;\n  grabY: number;\n}\n\n/** The short-lived copy that glides into the slot after a drop. */\ninterface LandingPreview extends DragGeometry {\n  /** Where the pointer was when the card was released. */\n  startX: number;\n  startY: number;\n}\n\nexport interface BoardProps {\n  columns: BoardColumnDef[];\n  onChange: (change: BoardChange) => void;\n  onLoadMore?: (columnId: string) => void;\n  onAddTicket?: (columnId: string) => void;\n  onArchiveColumn?: (columnId: string) => void;\n  getTicketHref?: (ticketId: string) => string;\n  renderAssignSlot?: (ticket: BoardColumnDef['tickets'][number]) => ReactNode;\n  onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  onReject?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  collapseStorageKey?: string;\n  loadMoreRootMargin?: string;\n  className?: string;\n}\n\n/**\n * Picks which board exists for this input mode, and that is all it does.\n *\n * - **Drag board** — pointer devices from the `md` breakpoint up: the full\n *   drag-and-drop implementation below.\n * - **Touch board** (`touch-board.tsx`) — everything else: a swipeable\n *   scroll-snap pager of full-height lanes, registering no DnD at all. The\n *   HTML5 drag events Pragmatic builds on never fire under touch, so on those\n *   devices a drag board is dead affordances stapled to 400px lanes.\n *\n * The rule is `DRAG_AND_DROP_MEDIA_QUERY` (pointer AND hover AND width — see\n * `use-drag-and-drop-enabled.ts` for why neither alone survives an iPad or a\n * touchscreen laptop), read live: attaching a mouse or crossing the breakpoint\n * swaps the subtree without a reload.\n *\n * While the answer is unknown (SSR and the hydration render) neither subtree\n * exists — mounting one is a guess that runs the loser's effects and, for the\n * drag board, registers DnD on a device that may be a phone. The empty shell\n * keeps the container's footprint; `useMediaQuery` resolves in a pre-paint\n * layout effect, so on the client the shell is never actually painted.\n */\nexport function Board(props: BoardProps) {\n  const dragAndDropEnabled = useMediaQuery(DRAG_AND_DROP_MEDIA_QUERY);\n  if (dragAndDropEnabled === undefined) {\n    return <div aria-hidden className={cn('flex h-full min-h-0 flex-col', props.className)} />;\n  }\n  return dragAndDropEnabled ? <DragBoard {...props} /> : <TouchBoard {...props} />;\n}\n\n/**\n * The board renders `columns` and nothing else — there is no local mirror of\n * them, and a drag in progress changes no state here at all.\n *\n * That is the whole design. A card that previews its move by actually moving in\n * the data has to re-enter React on every lane crossing, which re-mounts it\n * under a new parent and re-measures the board; guarding that cascade is what\n * the old dnd-kit implementation spent a frame-freeze and a unit-tested rules\n * module on. Here the preview is a line drawn by the card being hovered, the\n * data moves once, on drop, and the cascade has nothing to feed on.\n */\nfunction DragBoard({\n  columns,\n  onChange,\n  onLoadMore,\n  onAddTicket,\n  onArchiveColumn,\n  getTicketHref,\n  renderAssignSlot,\n  onApprove,\n  onReject,\n  collapseStorageKey,\n  loadMoreRootMargin,\n  className,\n}: BoardProps) {\n  const { collapsed, toggle } = useBoardCollapse(collapseStorageKey);\n\n  const {\n    scrollRef,\n    trackRef,\n    thumbRef,\n    thumbRatio,\n    onScroll,\n    onTrackClick,\n    onTrackWheel,\n    onThumbPointerDown,\n    onThumbPointerMove,\n    onThumbPointerUp,\n  } = useHorizontalScrollbar();\n\n  // The monitor is registered once for the board's lifetime; these keep it\n  // reading current values without tearing it down on every render.\n  const columnsRef = useRef(columns);\n  const onChangeRef = useRef(onChange);\n\n  // `useHorizontalScrollbar` hands back a callback ref, and auto-scroll needs\n  // the node itself — so keep our own alongside it on the same element.\n  const boardRef = useRef<HTMLDivElement | null>(null);\n  const scrollerRef = useRef<HTMLDivElement | null>(null);\n  const setScroller = useCallback(\n    (node: HTMLDivElement | null) => {\n      scrollerRef.current = node;\n      scrollRef(node);\n    },\n    [scrollRef],\n  );\n\n  // Where the drop points. One answer for the whole board, handed to the cards\n  // so the preview follows it wherever it came from — see `drop-aim.ts`.\n  const [aim, setAim] = useState<DropAim | null>(null);\n\n  // A card lifted with the keyboard. Jira's model, and the only one that works\n  // here: an arrow key cannot commit a move of its own, because each one would\n  // be a separate write to the server — so the card is lifted, walked to where\n  // it belongs, and put down once.\n  const [lifted, setLifted] = useState<{ ticketId: string; aim: BoardAim } | null>(null);\n  const [announcement, setAnnouncement] = useState('');\n\n  // The card being carried is the browser's own drag image (see\n  // `ticket-card.tsx`), so nothing here follows the pointer and a drag re-renders\n  // nothing. This is measured at drag start and only used at the end, to put a\n  // copy back where that image was and glide it into place.\n  const dragGeometry = useRef<DragGeometry | null>(null);\n  const [landingPreview, setLandingPreview] = useState<LandingPreview | null>(null);\n  const previewRef = useRef<HTMLDivElement | null>(null);\n\n  // A move that has been reported but not answered yet. The board shows it\n  // itself for that gap — see `pending-move.ts` for why the card cannot simply\n  // wait for the host, and why waiting is what made a drop look like the ticket\n  // disappearing and reappearing.\n  const [reportedMove, setPendingMove] = useState<PendingMove | null>(null);\n  // Handing back once the move has landed matters: a host that rolls a failed\n  // move back puts the card in its old slot, and a move still sitting here would\n  // keep showing it as though it had worked.\n  //\n  // `hasMoveSettled` is pure over `columns`, so that answer is available WHILE\n  // rendering — it deliberately reads the RAW `columns`, because the question is\n  // whether the host has caught up and `view` below is derived from this answer.\n  // Publishing it from an effect instead committed one whole frame with the card\n  // still held in its pending position after the host had already caught up.\n  const pendingMove = reportedMove && !hasMoveSettled(columns, reportedMove) ? reportedMove : null;\n  const view = useMemo(() => (pendingMove ? applyPendingMove(columns, pendingMove) : columns), [columns, pendingMove]);\n  // EVERYTHING downstream reads `view`, never `columns`: it is what is on\n  // screen, so it is also what a drag, a keyboard move and a drop are about.\n  // `columns` is only ever consulted to ask whether the host has caught up.\n  //\n  // Published in an unconditional effect rather than in the render body, and\n  // declared BEFORE the monitor effect so it is filled first in the same\n  // flush. The monitor only ever reads these from a pointer or keyboard event,\n  // which is always past a commit — and a render attempt React discards must\n  // not be able to leave the monitor aiming at a board layout nobody saw.\n  useEffect(() => {\n    columnsRef.current = view;\n    onChangeRef.current = onChange;\n  });\n\n  // The card still travelling from the pointer into the slot it landed in. The\n  // preview is over it for that moment, so the card itself is hidden.\n  const [landing, setLanding] = useState<string | null>(null);\n  const landingTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  /** Where the pointer was last seen, in the same terms the preview is placed. */\n  const pointRef = useRef({ x: 0, y: 0 });\n\n  // ---- keyboard move -------------------------------------------------------\n\n  const laneLabel = useCallback((columnId: string) => view.find(c => c.id === columnId)?.label ?? columnId, [view]);\n\n  /** Lanes the lifted card is allowed into — the same rules a pointer drop obeys. */\n  const acceptsLifted = useCallback(\n    (fromColumnId: string) => (column: BoardColumnDef) => {\n      if (column.dropDisabled || collapsed[column.id]) return false;\n      if (column.id === fromColumnId) return true;\n      return !column.allowedFromColumns || column.allowedFromColumns.includes(fromColumnId);\n    },\n    [collapsed],\n  );\n\n  const lift = useCallback(\n    (ticketId: string) => {\n      const start = initialAim(view, ticketId);\n      if (!start) return;\n      setLifted({ ticketId, aim: start });\n      const title = laneOf(view, ticketId)?.tickets.find(t => t.id === ticketId)?.title ?? 'Ticket';\n      setAnnouncement(`${title} lifted. Use the arrow keys to move it, space to drop it, escape to put it back.`);\n    },\n    [view],\n  );\n\n  const drop = useCallback(() => {\n    if (!lifted) return;\n    const change = aimToChange(view, lifted.ticketId, lifted.aim);\n    setLifted(null);\n    if (!change) {\n      setAnnouncement('Put back where it was.');\n      return;\n    }\n    setAnnouncement(`Dropped into ${laneLabel(change.toColumnId)}.`);\n    setPendingMove({\n      ticketId: change.ticketId,\n      fromColumnId: change.fromColumnId,\n      toColumnId: change.toColumnId,\n      afterTicketId: change.afterTicketId,\n    });\n    onChange(change);\n  }, [lifted, view, laneLabel, onChange]);\n\n  const walk = useCallback(\n    (key: ArrowKey) => {\n      if (!lifted) return;\n      const from = laneOf(view, lifted.ticketId)?.id ?? lifted.aim.columnId;\n      const next = moveAim(view, lifted.ticketId, lifted.aim, key, acceptsLifted(from));\n      if (next === lifted.aim) return;\n      setLifted({ ...lifted, aim: next });\n      const total = (view.find(c => c.id === next.columnId)?.tickets ?? []).filter(\n        t => t.id !== lifted.ticketId,\n      ).length;\n      setAnnouncement(`${laneLabel(next.columnId)}, position ${next.index + 1} of ${total + 1}.`);\n    },\n    [lifted, view, acceptsLifted, laneLabel],\n  );\n\n  // Handled on the board rather than on each card: once a card is lifted the\n  // keys belong to the move, not to whatever happens to hold focus.\n  useEffect(() => {\n    if (!lifted) return undefined;\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === 'Escape') {\n        setLifted(null);\n        setAnnouncement('Move cancelled.');\n      } else if (event.key === ' ' || event.key === 'Enter') {\n        drop();\n      } else if (ARROW_KEYS.includes(event.key as ArrowKey)) {\n        walk(event.key as ArrowKey);\n      } else {\n        return;\n      }\n      // Arrows would scroll the lane out from under the card, and space would\n      // scroll the page.\n      event.preventDefault();\n    };\n    window.addEventListener('keydown', onKeyDown);\n    return () => window.removeEventListener('keydown', onKeyDown);\n  }, [lifted, drop, walk]);\n\n  // A card walked with the keyboard has to be visible where it is being walked\n  // to. A pointer drag brings its own scrolling — auto-scroll follows it to the\n  // edges — but arrow keys move nothing on screen, so a slot below the fold, or\n  // in a lane scrolled off the side of the board, was being aimed at completely\n  // out of sight. `lifted` is a new object on every step, so this runs per step.\n  useEffect(() => {\n    if (!lifted) return;\n    boardRef.current\n      ?.querySelector(`[${DROP_LINE_ATTRIBUTE}]`)\n      ?.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n  }, [lifted]);\n\n  /** The lifted card feeds the same preview a pointer drag does, so the room\n   *  opening and the faded copy need to know nothing about keyboards. */\n  const keyboardAim = useMemo<DropAim | null>(() => {\n    if (!lifted) return null;\n    const ticket = laneOf(view, lifted.ticketId)?.tickets.find(t => t.id === lifted.ticketId);\n    if (!ticket) return null;\n    const anchor = aimToChange(view, lifted.ticketId, lifted.aim)\n      ? aimAnchor(view, lifted.ticketId, lifted.aim)\n      : { ticketId: null, edge: null };\n    return { columnId: lifted.aim.columnId, ...anchor, ticket };\n  }, [lifted, view]);\n\n  // The backstop for a host that never answers at all. Settling is handled by\n  // the derivation above, so this timer no longer needs `columns` as a dep — and\n  // that was a bug: every lane polls on its own timer, so each unrelated\n  // `columns` identity change tore this effect down and started a FRESH 2s\n  // window. Under any active polling faster than the timeout the deadline was\n  // pushed back forever, and a move the host silently dropped stayed painted on\n  // the board indefinitely instead of falling back after 2s.\n  useEffect(() => {\n    if (!pendingMove) return undefined;\n    const timer = setTimeout(() => setPendingMove(null), PENDING_MOVE_TIMEOUT_MS);\n    return () => clearTimeout(timer);\n  }, [pendingMove]);\n\n  useEffect(() => {\n    const scroller = scrollerRef.current;\n\n    /** What the innermost drop target is currently saying, in the one shape the\n     *  cards need. A card and a lane answer in the same terms, so this does not\n     *  care which one replied — and nothing outside a lane replies at all. */\n    const readAim = (source: { data: Record<string | symbol, unknown> }, target?: Record<string | symbol, unknown>) => {\n      const ticket = source.data.ticket as BoardTicket | undefined;\n      if (!target || !ticket) return null;\n      const ticketId = String(source.data.ticketId);\n      const slot = aimFromTarget(columnsRef.current, ticketId, target);\n      if (!slot) return null;\n      // No line where the card already is. A drop there changes nothing, and\n      // marking a place that is not a move is noise around the card you are\n      // holding — the one thing every board gets right by NOT drawing it.\n      const anchor = aimToChange(columnsRef.current, ticketId, slot)\n        ? aimAnchor(columnsRef.current, ticketId, slot)\n        : { ticketId: null, edge: null };\n      return { columnId: slot.columnId, ...anchor, ticket };\n    };\n\n    const aimAt = (\n      source: { data: Record<string | symbol, unknown> },\n      targets: { data: Record<string | symbol, unknown> }[],\n    ) => {\n      const next = readAim(source, targets[0]?.data);\n      // A pointer moving inside one card re-resolves to the same answer every\n      // frame; keeping the previous object stops that re-rendering the board.\n      setAim(prev => (isSameAim(prev, next) ? prev : next));\n    };\n\n    return combine(\n      // Dragging towards the edge of the board scrolls the lanes sideways —\n      // and keeps scrolling once the pointer is past that edge.\n      scroller ? autoScrollBothWays(scroller) : () => {},\n      // ...and towards the edge of the SCREEN scrolls whatever the page is\n      // actually scrolled by, which is the layout's `<main>`, not the document.\n      // Auto-scroll is registered on those; a DROP is not. Nothing outside the\n      // lanes is a drop target: a card carried over the filters, the header or\n      // the sidebar is aimed at nothing, draws nothing, and cancels when\n      // released. Resolving a lane from a pointer that has left the board — by\n      // matching it horizontally to the column above or below it — was tried and\n      // is not what a board does; the answer has to be somewhere the user is\n      // actually pointing.\n      monitorForElements({\n        canMonitor: ({ source }) => source.data.type === 'ticket',\n\n        onDragStart: ({ source, location }) => {\n          const ticket = source.data.ticket as BoardColumnDef['tickets'][number] | undefined;\n          if (!ticket) return;\n          const rect = source.element.getBoundingClientRect();\n          const { clientX, clientY } = location.current.input;\n          pointRef.current = { x: clientX, y: clientY };\n          // Nothing outside the lanes is a drop target, and a native drag with\n          // no target under it falls back to the browser's own drop effect —\n          // which is `copy`, i.e. the green plus cursor, over the filters, the\n          // header and the sidebar. This is Pragmatic's own answer: it holds the\n          // effect at `move` everywhere, so the cursor stays a plain drag cursor\n          // wherever the card is carried. It changes nothing about WHERE the\n          // card can land — the board still resolves that from its own targets,\n          // and releasing outside them still cancels.\n          preventUnhandled.start();\n          // A previous card may still be gliding into place; this drag owns the\n          // preview from here, so that one arrives instantly instead.\n          setLanding(null);\n          dragGeometry.current = {\n            ticket,\n            columnColor: columnsRef.current.find(c => c.id === source.data.columnId)?.color,\n            width: rect.width,\n            grabX: clientX - rect.left,\n            grabY: clientY - rect.top,\n          };\n        },\n\n        onDropTargetChange: ({ source, location }) => aimAt(source, location.current.dropTargets),\n\n        onDrag: ({ source, location }) => {\n          // The edge can flip without the set of targets changing, so the aim is\n          // re-read here too — `onDrag` is throttled to an animation frame.\n          aimAt(source, location.current.dropTargets);\n          const { clientX, clientY } = location.current.input;\n          pointRef.current = { x: clientX, y: clientY };\n        },\n\n        onDrop: ({ source, location }) => {\n          preventUnhandled.stop();\n          setAim(null);\n          // Innermost first: a card if the pointer was over one, otherwise the\n          // lane it was dropped into. Empty means dropped outside the board.\n          const change = resolveBoardDrop(columnsRef.current, {\n            source: source.data,\n            target: location.current.dropTargets[0]?.data,\n          });\n          // Released where nothing would change, or outside the board entirely.\n          // The browser takes its drag image away by itself and the card un-fades\n          // on its own `onDrop`, in this same event, so there is nothing to undo.\n          if (!change) return;\n\n          // The move goes on screen NOW, from here, and the host's answer\n          // replaces an identical picture whenever it arrives.\n          setPendingMove({\n            ticketId: change.ticketId,\n            fromColumnId: change.fromColumnId,\n            toColumnId: change.toColumnId,\n            afterTicketId: change.afterTicketId,\n          });\n          // ...and a copy of the card takes over from the drag image for one\n          // short glide into the slot that move just opened. Both land in the\n          // same commit, so the slot is already there to be measured — see the\n          // effect below.\n          const geometry = dragGeometry.current;\n          if (geometry) {\n            setLandingPreview({ ...geometry, startX: pointRef.current.x, startY: pointRef.current.y });\n            setLanding(change.ticketId);\n          }\n          onChangeRef.current(change);\n        },\n      }),\n    );\n  }, []);\n\n  /**\n   * The dropped card's glide from where it was released into its slot.\n   *\n   * Runs in the commit that carries the move itself, so the slot already exists\n   * and can be measured; the card in it is hidden (see `LandingCardContext`) and\n   * this copy is what the eye follows the rest of the way. Without it the\n   * browser's drag image simply vanishes at the pointer and the card is suddenly\n   * elsewhere — the discontinuity the whole drop sequence exists to remove.\n   *\n   * A plain FLIP, written out rather than split between the render and here: the\n   * copy is placed where the drag image was, that position is forced to resolve,\n   * and only then is it given a destination. Skipping the forced resolve means\n   * the browser only ever sees the final value and animates from nothing — an\n   * element's very first style resolution never starts a transition.\n   */\n  useIsomorphicLayoutEffect(() => {\n    if (!landing) return undefined;\n\n    const finish = () => {\n      landingTimer.current = null;\n      setLanding(null);\n      setLandingPreview(null);\n    };\n\n    const preview = previewRef.current;\n    const card = boardRef.current && cardById(boardRef.current, landing);\n    if (!preview || !landingPreview || !card) {\n      finish();\n      return undefined;\n    }\n\n    const to = card.getBoundingClientRect();\n    // The slot has to be somewhere the eye can actually follow the card to. A\n    // lane clips its own overflow, so a slot hanging over its edge would take a\n    // card that is whole one frame and cut in half the next; the card simply\n    // appears in place instead, which is never wrong — only less pretty.\n    const lane = nearestScrollableAncestor(card)?.getBoundingClientRect();\n    if (lane && (to.top < lane.top || to.bottom > lane.bottom)) {\n      finish();\n      return undefined;\n    }\n\n    const { startX, startY } = landingPreview;\n    preview.style.transition = 'none';\n    preview.style.opacity = String(DRAG_PREVIEW_OPACITY);\n    preview.style.transform = `translate(${startX}px, ${startY}px)`;\n    const from = preview.getBoundingClientRect();\n    void preview.offsetHeight;\n\n    // Settles solid as it lands, so handing over to the real card underneath is\n    // not also a jump in brightness.\n    preview.style.transition = `transform ${DROP_MS}ms ${DROP_EASING}, opacity ${DROP_MS}ms ${DROP_EASING}`;\n    preview.style.opacity = '1';\n    preview.style.transform = `translate(${startX + to.left - from.left}px, ${startY + to.top - from.top}px)`;\n    landingTimer.current = setTimeout(finish, DROP_MS);\n    return () => {\n      if (landingTimer.current) clearTimeout(landingTimer.current);\n      landingTimer.current = null;\n    };\n  }, [landing, landingPreview]);\n\n  // The released card, on its way into its slot. Alive for one short animation\n  // and no longer: while the card is actually being carried there is nothing\n  // here at all — that is the browser's drag image (see `ticket-card.tsx`).\n  //\n  // A portal, because a lane clips its own overflow and this travels across the\n  // whole board. Position, opacity and transition are all written by the effect\n  // above and deliberately not by React: a re-render for an unrelated reason\n  // must not put the card back at the pointer half way down.\n  const preview =\n    landingPreview &&\n    createPortal(\n      <div\n        ref={previewRef}\n        aria-hidden\n        // `[&_*]:!pointer-events-none` and not just the root: the card body\n        // re-enables pointer events on its own parts (the assignee slot, the\n        // timestamp tooltip), and this sits over the board while it settles.\n        className=\"pointer-events-none fixed left-0 top-0 [&_*]:!pointer-events-none\"\n        style={{\n          zIndex: DRAG_PREVIEW_Z,\n          width: landingPreview.width,\n          // The grip offset lives here, so the animated property is a plain\n          // translate in pointer coordinates and nothing else.\n          margin: `${-landingPreview.grabY}px 0 0 ${-landingPreview.grabX}px`,\n        }}\n      >\n        {/* Rendered with the host's assignee slot, like the drag image it takes\n            over from: without it the card visibly changes at the handover. It is\n            a live control, but an inert copy of one — nothing in this subtree\n            can be pointed at (see above) — and it shares its queries with the\n            card it was copied from. */}\n        <TicketCardView\n          ticket={landingPreview.ticket}\n          columnColor={landingPreview.columnColor}\n          renderAssignSlot={renderAssignSlot}\n          // Still held, to match the drag image at the moment it hands over.\n          className=\"shadow-card-hover\"\n        />\n      </div>,\n      document.body,\n    );\n\n  return (\n    <LandingCardContext.Provider value={landing}>\n      <DropAimContext.Provider value={aim ?? keyboardAim}>\n        <BoardLiftContext.Provider value={lift}>\n          {preview}\n          <div ref={boardRef} className={cn('flex h-full flex-col', className)}>\n            <div\n              ref={setScroller}\n              onScroll={onScroll}\n              className=\"flex min-h-0 flex-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n            >\n              {view.map((column, i) => {\n                const prev = view[i - 1];\n                const next = view[i + 1];\n                const joinLeft = !!(column.system && prev?.system);\n                const joinRight = !!(column.system && next?.system);\n                const showGap = i > 0 && !joinLeft;\n                return (\n                  <Fragment key={column.id}>\n                    {showGap && <div aria-hidden className=\"w-[var(--spacing-system-mf)] shrink-0\" />}\n                    <BoardColumn\n                      column={column}\n                      collapsed={!!collapsed[column.id]}\n                      onToggleCollapse={toggle}\n                      onAddTicket={onAddTicket}\n                      onArchive={onArchiveColumn}\n                      getTicketHref={getTicketHref}\n                      renderAssignSlot={renderAssignSlot}\n                      onApprove={onApprove}\n                      onReject={onReject}\n                      onLoadMore={onLoadMore}\n                      loadMoreRootMargin={loadMoreRootMargin}\n                      joinLeft={joinLeft}\n                      joinRight={joinRight}\n                    />\n                  </Fragment>\n                );\n              })}\n            </div>\n\n            <p aria-live=\"polite\" className=\"sr-only\">\n              {announcement}\n            </p>\n\n            {thumbRatio > 0 && (\n              <div\n                ref={trackRef}\n                onClick={onTrackClick}\n                onWheel={onTrackWheel}\n                className=\"relative mt-[var(--spacing-system-mf)] h-2 shrink-0 cursor-pointer rounded-full bg-ods-border\"\n              >\n                <div\n                  ref={thumbRef}\n                  data-scrollbar-thumb\n                  className=\"absolute top-0 h-full rounded-full bg-ods-text-secondary transition-colors\"\n                  style={{ width: `${thumbRatio * 100}%`, cursor: 'grab' }}\n                  onPointerDown={onThumbPointerDown}\n                  onPointerMove={onThumbPointerMove}\n                  onPointerUp={onThumbPointerUp}\n                />\n              </div>\n            )}\n          </div>\n        </BoardLiftContext.Provider>\n      </DropAimContext.Provider>\n    </LandingCardContext.Provider>\n  );\n}\n","import {\n  autoScrollForElements,\n  autoScrollWindowForElements,\n} from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';\nimport { unsafeOverflowAutoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/unsafe-overflow/element';\nimport { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';\nimport { scrollableAncestorsOf } from './scroll-parent';\n\n/**\n * How far past a scroller's edge a drag still counts as \"scroll this\".\n *\n * Big on purpose: past the edge there is nothing else to hit, so the only\n * question is whether the intent is obvious, and dragging beyond a container is\n * about as obvious as intent gets.\n */\nconst OVERFLOW_REACH = 5000;\n\nconst overflow = () => ({\n  forTopEdge: { top: OVERFLOW_REACH, left: OVERFLOW_REACH, right: OVERFLOW_REACH },\n  forBottomEdge: { bottom: OVERFLOW_REACH, left: OVERFLOW_REACH, right: OVERFLOW_REACH },\n  forLeftEdge: { left: OVERFLOW_REACH, top: OVERFLOW_REACH, bottom: OVERFLOW_REACH },\n  forRightEdge: { right: OVERFLOW_REACH, top: OVERFLOW_REACH, bottom: OVERFLOW_REACH },\n});\n\n/** Straight up and straight down only — no sideways reach at all. */\nconst verticalOverflow = () => ({\n  forTopEdge: { top: OVERFLOW_REACH },\n  forBottomEdge: { bottom: OVERFLOW_REACH },\n});\n\n/**\n * Registers drag auto-scroll for one scroller, both **inside** its edges and\n * **past** them.\n *\n * Pragmatic's over-element scrolling only engages in a band just inside the\n * edge, so a drag that has already left the container stops scrolling it — you\n * have to hunt for the strip where it works. dnd-kit kept scrolling however far\n * out you went, and `unsafeOverflowAutoScrollForElements` is Pragmatic's own\n * answer for that; \"unsafe\" here only means it can reach a container the pointer\n * is not over, which is exactly what is wanted.\n */\nexport function autoScrollBothWays(element: HTMLElement): () => void {\n  return combine(\n    autoScrollForElements({ element }),\n    unsafeOverflowAutoScrollForElements({ element, getOverflow: overflow }),\n  );\n}\n\n/**\n * Auto-scroll for one of several scrollers standing side by side — a board lane.\n *\n * Same as {@link autoScrollBothWays} except that its reach past the edges is\n * vertical only, and that is the whole point. Lanes sit shoulder to shoulder, so\n * a reach that also spreads sideways puts a pointer below the board inside\n * EVERY lane's overflow region at once and scrolls all of them together. Keeping\n * the region to the lane's own width means the pointer is below exactly one\n * lane, and only that lane moves — which is what a board is expected to do.\n */\nexport function autoScrollColumn(element: HTMLElement): () => void {\n  return combine(\n    autoScrollForElements({ element }),\n    unsafeOverflowAutoScrollForElements({ element, getOverflow: verticalOverflow }),\n  );\n}\n\n/**\n * Registers auto-scroll on every scrollable ancestor of `element`, and on the\n * window.\n *\n * dnd-kit walked the ancestor chain for you; Pragmatic makes each scroller opt\n * in by name, which is why dragging towards the edge of the screen stopped\n * scrolling anything after the migration. Registering the window alone would\n * have fixed nothing either: the app's real scroller is the layout's `<main>`,\n * not the document — so the chain has to be walked.\n *\n * Registration is passive; a scroller only moves while a drag is actually near\n * its edge, so it is safe to call this once on mount.\n *\n * Register this ONCE per page region, not once per thing that can be dragged:\n * Pragmatic keeps one entry per registration and each entry scrolls on its own,\n * so registering the same `<main>` from five lanes scrolls it five times as\n * fast.\n */\nexport function autoScrollAncestors(element: HTMLElement): () => void {\n  const cleanups = scrollableAncestorsOf(element).map(autoScrollBothWays);\n  // Cheap and harmless when the document itself does not scroll, and the only\n  // thing that works when it does.\n  cleanups.push(autoScrollWindowForElements());\n  return combine(...cleanups);\n}\n","/** Anything the browser can scroll. */\nconst SCROLLABLE = /(auto|scroll|overlay)/;\n\n/** Every scrollable ancestor of `element`, innermost first. */\nexport function scrollableAncestorsOf(element: HTMLElement): HTMLElement[] {\n  const found: HTMLElement[] = [];\n  for (let node = element.parentElement; node && node !== document.body; node = node.parentElement) {\n    const style = getComputedStyle(node);\n    if (SCROLLABLE.test(style.overflowY) || SCROLLABLE.test(style.overflowX)) found.push(node);\n  }\n  return found;\n}\n\n/**\n * The nearest scrollable ancestor, or `null`.\n *\n * Stands in for \"the working area this thing lives in\": in an app whose layout\n * scrolls a `<main>` rather than the document, that element is exactly the\n * region a user considers to be the page.\n */\nexport function nearestScrollableAncestor(element: HTMLElement): HTMLElement | null {\n  return scrollableAncestorsOf(element)[0] ?? null;\n}\n","import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';\nimport type { BoardChange, BoardColumnDef } from './types';\n\n/**\n * Where a card being moved currently points: a lane, and a slot among the cards\n * ALREADY in it — the card being moved never counts as one of them.\n *\n * This is the board's ONE description of a landing place, and everything is\n * expressed in it: the pointer, the keyboard, the line that gets drawn and the\n * change that gets reported. An index cannot say the same thing two ways, which\n * matters more than it sounds — \"below card X\" and \"above card Y\" are the same\n * slot, and drawing a line for whichever one a hit test happened to return makes\n * it jump between two positions as the pointer crosses the boundary.\n */\nexport interface BoardAim {\n  columnId: string;\n  /** Insertion index among the other cards, so `0..others.length`. */\n  index: number;\n}\n\nexport type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';\n\n/** Whether a lane will take this card — the board's own rules, passed in so this\n *  module stays arithmetic. */\ntype LaneFilter = (column: BoardColumnDef) => boolean;\n\nconst othersIn = (columns: readonly BoardColumnDef[], columnId: string, ticketId: string) =>\n  (columns.find(c => c.id === columnId)?.tickets ?? []).filter(t => t.id !== ticketId);\n\n/** The lane a card is in right now, or `null` if it is in none of them. */\nexport function laneOf(columns: readonly BoardColumnDef[], ticketId: string): BoardColumnDef | null {\n  return columns.find(c => c.tickets.some(t => t.id === ticketId)) ?? null;\n}\n\n/** Where a card starts: exactly where it already sits. */\nexport function initialAim(columns: readonly BoardColumnDef[], ticketId: string): BoardAim | null {\n  const lane = laneOf(columns, ticketId);\n  if (!lane) return null;\n  // Its index among the others is its own index: dropping it back at that slot\n  // puts it exactly where it came from.\n  return { columnId: lane.id, index: lane.tickets.findIndex(t => t.id === ticketId) };\n}\n\n/**\n * The aim after one arrow key, or the aim unchanged when the move would leave\n * the board or land in a lane that will not take the card.\n *\n * Up and down step through slots; left and right step through lanes, skipping\n * any the card is not allowed into rather than stopping dead at it — a lane the\n * user cannot use should cost a keypress, not end the journey.\n */\n/**\n * The slot a resolved drop target points at.\n *\n * Card targets carry the edge the pointer is nearest; a lane target means \"past\n * the last card\". Either way the answer comes out as an index, so the caller\n * never has to care which kind of target replied.\n */\nexport function aimFromTarget(\n  columns: readonly BoardColumnDef[],\n  ticketId: string,\n  target: Record<string | symbol, unknown>,\n): BoardAim | null {\n  const columnId = String(target.columnId);\n  if (!columns.some(c => c.id === columnId)) return null;\n  const others = othersIn(columns, columnId, ticketId);\n  if (target.type !== 'ticket') return { columnId, index: others.length };\n  const over = others.findIndex(t => t.id === String(target.ticketId));\n  if (over < 0) return { columnId, index: others.length };\n  return { columnId, index: over + (extractClosestEdge(target) === 'bottom' ? 1 : 0) };\n}\n\nexport function moveAim(\n  columns: readonly BoardColumnDef[],\n  ticketId: string,\n  aim: BoardAim,\n  key: ArrowKey,\n  accepts: LaneFilter,\n): BoardAim {\n  if (key === 'ArrowUp' || key === 'ArrowDown') {\n    const limit = othersIn(columns, aim.columnId, ticketId).length;\n    const index = Math.min(Math.max(aim.index + (key === 'ArrowUp' ? -1 : 1), 0), limit);\n    return index === aim.index ? aim : { ...aim, index };\n  }\n\n  const step = key === 'ArrowLeft' ? -1 : 1;\n  const from = columns.findIndex(c => c.id === aim.columnId);\n  for (let i = from + step; i >= 0 && i < columns.length; i += step) {\n    const column = columns[i];\n    if (!accepts(column)) continue;\n    // Keep the slot the card had, as far as the new lane allows.\n    const limit = othersIn(columns, column.id, ticketId).length;\n    return { columnId: column.id, index: Math.min(aim.index, limit) };\n  }\n  return aim;\n}\n\n/** The card the preview hangs off, and which side of it — `null` for a lane with\n *  nothing else in it. */\nexport function aimAnchor(\n  columns: readonly BoardColumnDef[],\n  ticketId: string,\n  aim: BoardAim,\n): { ticketId: string | null; edge: 'top' | 'bottom' | null } {\n  const others = othersIn(columns, aim.columnId, ticketId);\n  if (others.length === 0) return { ticketId: null, edge: null };\n  if (aim.index < others.length) return { ticketId: others[aim.index].id, edge: 'top' };\n  return { ticketId: others[others.length - 1].id, edge: 'bottom' };\n}\n\n/**\n * The move to report, or `null` when the card would land back where it started.\n *\n * Same rule as a pointer drop, and deliberately so: a keyboard move that ends\n * where it began must be as silent as a card picked up and put down again.\n */\nexport function aimToChange(columns: readonly BoardColumnDef[], ticketId: string, aim: BoardAim): BoardChange | null {\n  const lane = laneOf(columns, ticketId);\n  if (!lane) return null;\n\n  const others = othersIn(columns, aim.columnId, ticketId);\n  const afterTicketId = others[aim.index - 1]?.id ?? null;\n  const beforeTicketId = others[aim.index]?.id ?? null;\n\n  if (lane.id === aim.columnId) {\n    const currentIndex = lane.tickets.findIndex(t => t.id === ticketId);\n    if ((lane.tickets[currentIndex - 1]?.id ?? null) === afterTicketId) return null;\n  }\n\n  return { ticketId, fromColumnId: lane.id, toColumnId: aim.columnId, afterTicketId, beforeTicketId };\n}\n","'use client';\n\nimport { attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';\nimport { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';\nimport { memo, useCallback, useEffect, useRef, useState, type ReactNode } from 'react';\nimport { autoScrollColumn } from '../../../utils/auto-scroll-ancestors';\nimport { cn } from '../../../utils/cn';\nimport { TagIcon } from '../../icons-v2-generated/shopping/tag-icon';\nimport { BoardColumnHeader } from './board-column-header';\nimport { tintOnDark } from './color-utils';\nimport { useDropAim } from './drop-aim';\nimport { DROP_LINE_ATTRIBUTE, nearestCardIn } from './lane-geometry';\nimport { TicketCard } from './ticket-card';\nimport { TicketCardSkeleton } from './ticket-card-skeleton';\nimport type { BoardColumnDef, BoardTicket } from './types';\nimport { useLaneScrollAnchor } from './use-lane-scroll-anchor';\n\nexport interface BoardColumnProps {\n  column: BoardColumnDef;\n  collapsed?: boolean;\n  /** Takes the id so the board can pass one stable handler to every lane.\n   *  Absent = the lane cannot collapse and renders no collapse arrow. */\n  onToggleCollapse?: (columnId: string) => void;\n  onAddTicket?: (columnId: string) => void;\n  onArchive?: (columnId: string) => void;\n  getTicketHref?: (ticketId: string) => string;\n  renderAssignSlot?: (ticket: BoardTicket) => ReactNode;\n  onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  onReject?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  onLoadMore?: (columnId: string) => void;\n  loadMoreRootMargin?: string;\n  joinLeft?: boolean;\n  joinRight?: boolean;\n  /**\n   * The touch-pager variant of the lane: registers NO drag-and-drop at all (no\n   * drop target, no auto-scroll, cards forced non-draggable — the HTML5 drag\n   * events Pragmatic builds on never fire under touch) and words the empty\n   * state without \"drag\". Everything else — header, collapse, load-more — is\n   * the same lane.\n   */\n  touchMode?: boolean;\n  /** Merged last, so the touch pager can override the fixed lane width. */\n  className?: string;\n}\n\n/** Memoized: `Board` rebuilds only the two columns a drag touches, so the\n *  untouched ones keep their object identity and can skip the render entirely.\n *  Without this, every pointer move that shifts a card re-renders all lanes. */\nexport const BoardColumn = memo(function BoardColumnImpl({\n  column,\n  collapsed = false,\n  onToggleCollapse,\n  onAddTicket,\n  onArchive,\n  getTicketHref,\n  renderAssignSlot,\n  onApprove,\n  onReject,\n  onLoadMore,\n  loadMoreRootMargin = '200px 0px',\n  joinLeft = false,\n  joinRight = false,\n  touchMode = false,\n  className,\n}: BoardColumnProps) {\n  // The drop zone is the WHOLE lane, header included — not just the list. A\n  // target that stops at the list leaves dead strips (the header, the padding)\n  // where the pointer is over a column yet hits nothing. Collapsed lanes stay\n  // out: they render no list to drop into.\n  //\n  // A card sitting inside this lane is a drop target too, and Pragmatic reports\n  // the innermost one first — so the lane only ever answers for the gaps, and\n  // \"which card\" never has to be guessed from rect overlap.\n  const laneRef = useRef<HTMLDivElement>(null);\n  const [isOver, setIsOver] = useState(false);\n\n  useEffect(() => {\n    const element = laneRef.current;\n    if (!element || collapsed || touchMode || column.dropDisabled) return undefined;\n\n    return dropTargetForElements({\n      element,\n      canDrop: ({ source }) => {\n        if (source.data.type !== 'ticket') return false;\n        const from = source.data.columnId;\n        if (from === column.id) return true;\n        return !column.allowedFromColumns || column.allowedFromColumns.includes(String(from));\n      },\n      // A drop that lands between cards — most of the lane, once a hovered card\n      // has opened the room the preview is drawn in — answers with the nearest\n      // card and the edge of it the pointer is on, exactly as that card would\n      // have. Reporting a bare lane instead sent the ticket to the BOTTOM of the\n      // column, which is what made small moves look like they had not worked and\n      // large ones like the only thing that did.\n      getData: ({ input, element: lane, source }) => {\n        const nearest = nearestCardIn(lane, input.clientY, String(source.data.ticketId));\n        const ticketId = nearest?.dataset.ticketId;\n        // An empty lane, or one whose only card is the one being dragged: the\n        // drop appends, which is the only place it could go.\n        if (!nearest || !ticketId) return { type: 'column', columnId: column.id };\n        return attachClosestEdge(\n          { type: 'ticket', ticketId, columnId: column.id },\n          { input, element: nearest, allowedEdges: ['top', 'bottom'] },\n        );\n      },\n      onDragEnter: () => setIsOver(true),\n      onDragLeave: () => setIsOver(false),\n      onDrop: () => setIsOver(false),\n    });\n  }, [column.id, column.dropDisabled, column.allowedFromColumns, collapsed, touchMode]);\n\n  // The header's own prop stays zero-arg; the id is bound here, where it is\n  // already known, rather than by the board building one closure per lane.\n  const handleToggleCollapse = useCallback(() => onToggleCollapse?.(column.id), [onToggleCollapse, column.id]);\n\n  return (\n    <div\n      ref={laneRef}\n      className={cn(\n        'flex h-full shrink-0 flex-col gap-[var(--spacing-system-sf)] overflow-hidden rounded-md border border-ods-border p-[var(--spacing-system-sf)]',\n        'transition-[width] duration-300 ease-out',\n        collapsed ? 'w-14' : 'w-[400px]',\n        column.system && 'bg-ods-card',\n        joinLeft && 'rounded-l-none border-l-0',\n        joinRight && 'rounded-r-none',\n        isOver && 'outline outline-2 -outline-offset-2 outline-ods-focus',\n        className,\n      )}\n      style={column.system ? undefined : { backgroundColor: tintOnDark(column.color) }}\n    >\n      <BoardColumnHeader\n        column={column}\n        collapsed={collapsed}\n        onToggleCollapse={onToggleCollapse ? handleToggleCollapse : undefined}\n        onAddTicket={!collapsed && onAddTicket ? () => onAddTicket(column.id) : undefined}\n        onArchive={!collapsed && column.archivable && onArchive ? () => onArchive(column.id) : undefined}\n      />\n      {!collapsed && (\n        <>\n          <div aria-hidden className=\"-mx-[var(--spacing-system-sf)] h-px shrink-0 bg-ods-border\" />\n          <ColumnBody\n            column={column}\n            getTicketHref={getTicketHref}\n            renderAssignSlot={renderAssignSlot}\n            onApprove={onApprove}\n            onReject={onReject}\n            onLoadMore={onLoadMore}\n            loadMoreRootMargin={loadMoreRootMargin}\n            touchMode={touchMode}\n          />\n        </>\n      )}\n    </div>\n  );\n});\n\ninterface ColumnBodyProps {\n  column: BoardColumnDef;\n  getTicketHref?: (ticketId: string) => string;\n  renderAssignSlot?: (ticket: BoardTicket) => ReactNode;\n  onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  onReject?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  onLoadMore?: (columnId: string) => void;\n  loadMoreRootMargin: string;\n  touchMode: boolean;\n}\n\nfunction ColumnBody({\n  column,\n  getTicketHref,\n  renderAssignSlot,\n  onApprove,\n  onReject,\n  onLoadMore,\n  loadMoreRootMargin,\n  touchMode,\n}: ColumnBodyProps) {\n  const scrollRef = useRef<HTMLDivElement | null>(null);\n  const sentinelRef = useRef<HTMLDivElement | null>(null);\n\n  useLaneScrollAnchor(scrollRef, column.tickets);\n\n  // Dragging near the top or bottom of a long lane scrolls THIS lane, and only\n  // this one — the reach past the lane's edges is vertical, so a pointer below\n  // the board is below exactly one column. The page's own scrollers are\n  // registered once by the board, not once per lane: registering them here gave\n  // `<main>` one auto-scroller per column, all pulling at the same time.\n  // Touch mode registers nothing: there is no drag to scroll for.\n  useEffect(() => {\n    const element = scrollRef.current;\n    if (!element || touchMode) return undefined;\n    return autoScrollColumn(element);\n  }, [touchMode]);\n\n  // Refreshed in an unconditional effect, declared before the observer effect\n  // so it wins the same flush. Not in the render body: a render attempt React\n  // discards must not install a loader for a column that never committed.\n  const loadMoreRef = useRef(onLoadMore);\n  const columnIdRef = useRef(column.id);\n  useEffect(() => {\n    loadMoreRef.current = onLoadMore;\n    columnIdRef.current = column.id;\n  });\n\n  useEffect(() => {\n    if (!column.hasMore || column.isLoadingMore) return undefined;\n    const sentinel = sentinelRef.current;\n    const root = scrollRef.current;\n    if (!sentinel || !root || !loadMoreRef.current) return undefined;\n\n    const observer = new IntersectionObserver(\n      entries => {\n        if (entries.some(e => e.isIntersecting)) {\n          loadMoreRef.current?.(columnIdRef.current);\n        }\n      },\n      { root, rootMargin: loadMoreRootMargin },\n    );\n    observer.observe(sentinel);\n    return () => observer.disconnect();\n  }, [column.hasMore, column.isLoadingMore, loadMoreRootMargin]);\n\n  return (\n    // `overflow-anchor:none` hands scroll anchoring to `useLaneScrollAnchor`\n    // wholesale. The browser's own anchoring is unusable here — it silently\n    // skips transformed elements, so it works between drags and does nothing\n    // during one — and leaving both on double-corrects every insertion.\n    <div\n      ref={scrollRef}\n      // `py-[5px]`: the insertion line sits in the middle of the gap between two\n      // cards, and the first and last card have no gap on their outer side. This\n      // is the sliver it lives in there — without it the line is clipped away by\n      // this element's own overflow exactly when a card is aimed at either end.\n      className=\"flex min-h-0 flex-1 flex-col gap-[var(--spacing-system-xs)] overflow-y-auto py-[5px] [overflow-anchor:none]\"\n    >\n      {column.isLoading ? (\n        <SkeletonStack />\n      ) : column.tickets.length === 0 ? (\n        <EmptyState columnId={column.id} touchMode={touchMode} />\n      ) : (\n        column.tickets.map(t => (\n          <TicketCard\n            key={t.id}\n            ticket={t}\n            columnId={column.id}\n            columnColor={column.color}\n            href={getTicketHref?.(t.id)}\n            renderAssignSlot={renderAssignSlot}\n            onApprove={onApprove}\n            onReject={onReject}\n            dragDisabled={touchMode || column.dragDisabled}\n            dropDisabled={touchMode || column.dropDisabled}\n            allowedFromColumns={column.allowedFromColumns}\n          />\n        ))\n      )}\n      {column.isLoadingMore && !column.isLoading && <TicketCardSkeleton />}\n      {column.hasMore && <div ref={sentinelRef} aria-hidden className=\"h-1 shrink-0\" />}\n    </div>\n  );\n}\n\nfunction SkeletonStack({ count = 4 }: { count?: number }) {\n  // Position IS the identity here: the rows are interchangeable placeholders in\n  // a fixed-length list, so the index is the correct key. Random keys remounted\n  // every row whenever this re-rendered with a different `count`.\n  return (\n    <>\n      {Array.from({ length: count }, (_, i) => (\n        <TicketCardSkeleton key={`skeleton-${i}`} />\n      ))}\n    </>\n  );\n}\n\n/** Also carries the insertion line when a drop is aimed here, since the lane has\n *  no card to hang it off. */\nfunction EmptyState({ columnId, touchMode }: { columnId: string; touchMode: boolean }) {\n  const aim = useDropAim();\n  if (aim && aim.columnId === columnId) {\n    return <div aria-hidden {...{ [DROP_LINE_ATTRIBUTE]: '' }} className=\"h-0.5 shrink-0 rounded-full bg-ods-accent\" />;\n  }\n  return (\n    <div className=\"flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-lf)] p-[var(--spacing-system-lf)] text-center text-ods-text-secondary\">\n      <TagIcon className=\"h-6 w-6 shrink-0\" />\n      <div className=\"flex w-full flex-col\">\n        <p className=\"text-h4\">No tickets here</p>\n        <p className=\"text-h6\">\n          {/* No drag to offer on touch — a ticket arrives here by a status change. */}\n          {touchMode\n            ? \"Change a ticket's status to move it to this column\"\n            : 'Drag a ticket here or change its status to move it to this column'}\n        </p>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { Arrow02LeftIcon, Arrow02RightIcon, BoxArchiveIcon, InfoCircleIcon, PlusIcon } from '../../icons-v2-generated';\nimport { Button } from '../../ui/button/button';\nimport { TicketStatusTag, resolveTicketStatus } from '../../ui/ticket-status-tag';\nimport { TouchFriendlyTooltip } from '../../ui/touch-friendly-tooltip';\nimport type { BoardColumnDef } from './types';\n\nexport interface BoardColumnHeaderProps {\n  column: BoardColumnDef;\n  collapsed?: boolean;\n  /** Absent = the column cannot collapse, and no collapse arrow is rendered. */\n  onToggleCollapse?: () => void;\n  onAddTicket?: () => void;\n  onArchive?: () => void;\n}\n\nexport function BoardColumnHeader({\n  column,\n  collapsed = false,\n  onToggleCollapse,\n  onAddTicket,\n  onArchive,\n}: BoardColumnHeaderProps) {\n  const count = column.total ?? column.tickets.length;\n  const tagStatus = column.statusKey ?? column.id;\n  // Known statuses (ACTIVE, RESOLVED, …) render with their canonical Tag\n  // variant/icon; only unrecognized ids (custom-status UUIDs) fall back to `color`.\n  const useStatusVariant = resolveTicketStatus(tagStatus) !== null;\n  // The dense header size on every input mode — the mobile mock keeps the\n  // lane's info/collapse buttons small, same as desktop.\n  const actionClass = 'h-8 w-8 p-0 md:h-8 md:w-8';\n\n  if (collapsed) {\n    return (\n      <div className=\"flex h-full flex-col items-center gap-[var(--spacing-system-xsf)]\">\n        {onToggleCollapse && (\n          <Button\n            variant=\"transparent\"\n            size=\"icon\"\n            className={actionClass}\n            onClick={onToggleCollapse}\n            aria-label=\"Expand column\"\n          >\n            <Arrow02RightIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n          </Button>\n        )}\n        <TicketStatusTag\n          status={tagStatus}\n          label={column.label}\n          color={useStatusVariant ? undefined : column.color}\n          className=\"h-auto [writing-mode:vertical-rl]\"\n        />\n        <span className=\"text-h5\" style={{ color: column.color }}>\n          {count}\n        </span>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex items-center gap-[var(--spacing-system-xsf)]\">\n      <div className=\"flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xsf)]\">\n        <TicketStatusTag status={tagStatus} label={column.label} color={useStatusVariant ? undefined : column.color} />\n        <span className=\"text-h5\" style={useStatusVariant ? undefined : { color: column.color }}>\n          {count}\n        </span>\n      </div>\n      <div className=\"flex shrink-0 items-center gap-[var(--spacing-system-xxs)]\">\n        {column.tooltip && (\n          <TouchFriendlyTooltip content={column.tooltip} side=\"bottom\">\n            <Button variant=\"transparent\" size=\"icon\" className={actionClass} aria-label={column.tooltip}>\n              <InfoCircleIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n            </Button>\n          </TouchFriendlyTooltip>\n        )}\n        {onToggleCollapse && (\n          <Button\n            variant=\"transparent\"\n            size=\"icon\"\n            className={actionClass}\n            onClick={onToggleCollapse}\n            aria-label=\"Collapse column\"\n          >\n            <Arrow02LeftIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n          </Button>\n        )}\n        {onArchive && (\n          <Button\n            variant=\"transparent\"\n            size=\"icon\"\n            className={actionClass}\n            onClick={onArchive}\n            aria-label=\"Archive resolved tickets\"\n          >\n            <BoxArchiveIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n          </Button>\n        )}\n        {onAddTicket && (\n          <Button\n            variant=\"transparent\"\n            size=\"icon\"\n            className={actionClass}\n            onClick={onAddTicket}\n            aria-label=\"Add ticket\"\n          >\n            <PlusIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n","/** Pre-computed alpha blend: mix `hex` at 10% over a #161616 background. */\nexport function tintOnDark(hex: string): string {\n  const n = parseInt(hex.replace('#', ''), 16);\n  const r = (n >> 16) & 0xff;\n  const g = (n >> 8) & 0xff;\n  const b = n & 0xff;\n  const mix = (c: number) => Math.round(0.1 * c + 0.9 * 22);\n  const toHex = (v: number) => v.toString(16).padStart(2, '0');\n  return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;\n}\n","'use client';\n\nimport type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';\nimport { createContext, useContext } from 'react';\nimport type { BoardTicket } from './types';\n\n/**\n * Where the drop currently points, resolved once for the whole board.\n *\n * Each card used to work this out for itself, from its own hover events. That\n * only ever answers for a card the pointer is physically over, so the moment the\n * pointer left the lanes — over the filters, between two columns, below the last\n * card — the preview vanished even though the drop would still have landed\n * somewhere. Resolving it in one place and handing it down means the preview\n * follows the answer wherever the answer came from.\n */\nexport interface DropAim {\n  columnId: string;\n  /** The card the preview hangs off, or `null` for a lane with nothing in it. */\n  ticketId: string | null;\n  /** Which side of that card. Meaningless when `ticketId` is `null`. */\n  edge: Edge | null;\n  /** The card being carried, drawn faded in the room it opened. */\n  ticket: BoardTicket;\n}\n\nexport const DropAimContext = createContext<DropAim | null>(null);\n\n/**\n * Picking a card up from the keyboard. Separate from the aim so a card can offer\n * it without subscribing to something that changes on every pointer move.\n */\nexport const BoardLiftContext = createContext<((ticketId: string) => void) | null>(null);\n\nexport function useBoardLift(): ((ticketId: string) => void) | null {\n  return useContext(BoardLiftContext);\n}\n\n/** Null while no drag is pointing anywhere the board would accept. */\nexport function useDropAim(): DropAim | null {\n  return useContext(DropAimContext);\n}\n\n/** Whether two aims would draw the same thing — used to keep a pointer moving\n *  inside one card from re-rendering the board on every frame. */\nexport function isSameAim(a: DropAim | null, b: DropAim | null): boolean {\n  if (a === b) return true;\n  if (!a || !b) return false;\n  return a.columnId === b.columnId && a.ticketId === b.ticketId && a.edge === b.edge;\n}\n","'use client';\n\nimport { useRef, type RefObject } from 'react';\nimport { useIsomorphicLayoutEffect } from '../../../hooks/ui/use-isomorphic-layout-effect';\nimport type { BoardTicket } from './types';\n\n/** Attribute the lane's cards carry so the anchor can find them again. */\nexport const TICKET_ID_ATTRIBUTE = 'data-ticket-id';\n\ninterface CardPosition {\n  id: string;\n  /**\n   * Position in LAYOUT space, absolute within the lane — not the painted one.\n   * `offsetTop` ignores transforms, so a card mid-animation reads at its resting\n   * position and the correction below stays honest. Absolute rather than\n   * viewport-relative so that scrolling between two list changes — the drag's own\n   * auto-scroll, most of all — cannot stale the reading.\n   */\n  offsetTop: number;\n}\n\nfunction cardPositions(root: HTMLElement): CardPosition[] {\n  return Array.from(root.querySelectorAll<HTMLElement>(`[${TICKET_ID_ATTRIBUTE}]`)).map(element => ({\n    id: element.getAttribute(TICKET_ID_ATTRIBUTE) ?? '',\n    offsetTop: element.offsetTop,\n  }));\n}\n\n/**\n * Keeps a scrolled lane visually still when its list changes above the viewport.\n *\n * Browsers do this themselves — it is called scroll anchoring — but the lane turns\n * theirs off (`overflow-anchor:none`), because leaving both on double-corrects\n * every insertion. So a ticket arriving from a live update is inserted above the\n * viewport with nothing to compensate, and the whole list slides down by a card.\n *\n * NOTE: the original reason for taking this over by hand was that Chrome skips\n * anchor candidates carrying a `transform`, and dnd-kit transformed every card in\n * the lane during a drag. Nothing transforms cards any more, so the browser's own\n * anchoring may well be enough now — worth testing against live updates before\n * deleting this and the `overflow-anchor:none` that goes with it.\n *\n * The fix is one measurement: whichever card sits at the top of the viewport,\n * note where it is, and after the list changes put the viewport back the same\n * distance from it.\n *\n * Deliberately inert at the very top of a lane (`scrollTop === 0`), matching what\n * browsers do — there a new ticket is meant to be seen, not scrolled away.\n */\nexport function useLaneScrollAnchor(\n  scrollRef: RefObject<HTMLDivElement | null>,\n  tickets: readonly BoardTicket[],\n): void {\n  const positionsRef = useRef<CardPosition[]>([]);\n\n  useIsomorphicLayoutEffect(() => {\n    const root = scrollRef.current;\n    if (!root) return;\n\n    const previous = positionsRef.current;\n    const scrollTop = root.scrollTop;\n    // Measured once and reused: this runs for every lane on every list change —\n    // live updates, the host's refetch, every committed move — and reading\n    // `offsetTop` after the DOM changed forces a synchronous layout.\n    const current = cardPositions(root);\n\n    if (previous.length > 0 && scrollTop > 0) {\n      // The card at the top of the viewport right now, by its previous position.\n      const anchor = previous.find(card => card.offsetTop >= scrollTop) ?? previous[previous.length - 1];\n      const moved = current.find(card => card.id === anchor.id);\n      if (moved) {\n        const drift = moved.offsetTop - anchor.offsetTop;\n        if (Math.abs(drift) >= 1) root.scrollTop = scrollTop + drift;\n      }\n    }\n\n    positionsRef.current = current;\n  }, [tickets]);\n}\n","import { TICKET_ID_ATTRIBUTE } from './use-lane-scroll-anchor';\n\n/** Marks the line showing where a drop would land, wherever it is drawn — on a\n *  card or in an empty lane. The keyboard needs to find it to scroll it into\n *  view; a pointer drag brings its own scrolling. */\nexport const DROP_LINE_ATTRIBUTE = 'data-board-drop-line';\n\n/**\n * The card in `lane` whose middle is nearest `pointerY`, ignoring the one being\n * dragged, or `null` if the lane holds no other card.\n *\n * Live rects rather than anything cached: cards carry margins mid-drag that push\n * them about. The dragged card is skipped by name and not by having no box —\n * it keeps its slot in the lane until another card takes over holding the room,\n * so it is a full-height rectangle sitting right where the pointer is.\n */\nexport function nearestCardIn(lane: ParentNode, pointerY: number, draggedTicketId: string): HTMLElement | null {\n  let best: HTMLElement | null = null;\n  let bestDistance = Number.POSITIVE_INFINITY;\n  for (const card of lane.querySelectorAll<HTMLElement>(`[${TICKET_ID_ATTRIBUTE}]`)) {\n    if (card.dataset.ticketId === draggedTicketId) continue;\n    const rect = card.getBoundingClientRect();\n    if (rect.height === 0) continue;\n    const distance = Math.abs(pointerY - (rect.top + rect.height / 2));\n    if (distance < bestDistance) {\n      bestDistance = distance;\n      best = card;\n    }\n  }\n  return best;\n}\n\n/**\n * One card by ticket id, anywhere under `root`.\n *\n * A loop rather than an attribute selector: ticket ids are opaque global ids\n * from the host and go straight into a selector string otherwise.\n */\nexport function cardById(root: ParentNode, ticketId: string): HTMLElement | null {\n  for (const card of root.querySelectorAll<HTMLElement>(`[${TICKET_ID_ATTRIBUTE}]`)) {\n    if (card.dataset.ticketId === ticketId) return card;\n  }\n  return null;\n}\n","'use client';\n\nimport { attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';\nimport { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';\nimport { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';\nimport { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';\nimport { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';\nimport {\n  memo,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactNode,\n} from 'react';\nimport Link from '../../../embed-shims/next-link';\nimport { cn } from '../../../utils/cn';\nimport { formatTicketRelativeTime, formatTicketFullTimestamp } from '../../../utils/date-utils';\nimport { holdMoveDragEffect } from '../../../utils/drag-effect';\nimport { getReadableTextColor } from '../../../utils/ods-color-utils';\nimport {\n  ClockIcon,\n  DotsLoaderIcon,\n  LaptopIcon,\n  Flag02Icon,\n  MessagesIcon,\n  UserCheckIcon,\n} from '../../icons-v2-generated';\nimport { DeletedUserAvatar } from '../../ui/deleted-user-avatar';\nimport { SquareAvatar } from '../../ui/square-avatar';\nimport { Tag } from '../../ui/tag';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip';\nimport { BoardTicketApproval } from './board-ticket-approval';\nimport { useBoardLift, useDropAim } from './drop-aim';\nimport { DROP_LINE_ATTRIBUTE } from './lane-geometry';\nimport { useIsLanding } from './pending-move';\nimport type { BoardPriority, BoardTicket, BoardTicketActivityKind } from './types';\nimport { TICKET_ID_ATTRIBUTE } from './use-lane-scroll-anchor';\n\nconst PRIORITY_COLOR_CLASS: Record<BoardPriority, string> = {\n  low: 'text-ods-text-secondary',\n  medium: 'text-ods-info',\n  high: 'text-ods-warning',\n  urgent: 'text-ods-error',\n};\n\n/** How faded the card is where it would land. Matches what dnd-kit left behind. */\nconst DRAGGED_CARD_OPACITY = 0.4;\n\n/** How solid the card under the pointer is. Jira's look: carried, not lifted off\n *  the page entirely. */\nexport const DRAG_PREVIEW_OPACITY = 0.9;\n\nconst MAX_VISIBLE_TAGS = 2;\nconst MAX_VISIBLE_ASSIGNEES = 3;\n\nconst ACTIVITY_DEFAULT_LABEL: Record<BoardTicketActivityKind, string> = {\n  'ai-working': 'AI assistant is working',\n  'user-typing': 'User typing',\n  'waiting-external': 'Waiting for client response',\n  stale: 'No activity',\n};\n\n/** Shared card shell (border / padding / bg). Same footprint for the draggable\n *  board card and the static {@link TicketCardView}. */\nconst TICKET_CARD_SHELL =\n  'relative flex flex-col gap-[var(--spacing-system-sf)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-sf)] select-none text-left';\n\n// =============================================================================\n// Presentational body — the ONE ticket-card design, no drag/link/board context.\n// Reused by the draggable `TicketCard` AND the static `TicketCardView`.\n// =============================================================================\n\nexport interface TicketCardBodyProps {\n  ticket: BoardTicket;\n  columnColor?: string;\n  renderAssignSlot?: (ticket: BoardTicket) => ReactNode;\n  /** Approval callbacks receive the request id directly (the draggable\n   *  `TicketCard` adapts its own `(ticketId, requestId)` signature onto these). */\n  onApprove?: (requestId?: string) => void | Promise<void>;\n  onReject?: (requestId?: string) => void | Promise<void>;\n}\n\n/** The card's inner content: title + device/org, priority + assignees, tags,\n *  timestamp, \"New Message\", and the approval row. Pure — driven only by props. */\nexport function TicketCardBody({ ticket, columnColor, renderAssignSlot, onApprove, onReject }: TicketCardBodyProps) {\n  const showNewMessage = !!ticket.hasNewMessage && !!columnColor;\n  const newMessageTextColor = columnColor ? getReadableTextColor(columnColor) : undefined;\n\n  const showDeviceRow = !!(ticket.deviceHostnames?.length || ticket.organizationName);\n  const deviceText = [ticket.deviceHostnames?.join(', '), ticket.organizationName].filter(Boolean).join(', ');\n\n  const hasRightSection = !!(ticket.priority || ticket.assignees?.length || renderAssignSlot);\n  const rightSection = hasRightSection ? (\n    <div className=\"pointer-events-auto flex shrink-0 items-center gap-[var(--spacing-system-xsf)]\">\n      {ticket.priority && (\n        <Flag02Icon\n          className={cn('size-4', PRIORITY_COLOR_CLASS[ticket.priority])}\n          aria-label={`Priority: ${ticket.priority}`}\n        />\n      )}\n      {renderAssignSlot ? (\n        renderAssignSlot(ticket)\n      ) : ticket.assignees?.length ? (\n        <div className=\"flex -space-x-2\">\n          {ticket.assignees\n            .slice(0, MAX_VISIBLE_ASSIGNEES)\n            .map(a =>\n              a.deleted ? (\n                <DeletedUserAvatar\n                  key={a.id}\n                  size=\"sm\"\n                  accessibleLabel={`Deleted user: ${a.name ?? a.initials ?? a.id}`}\n                />\n              ) : (\n                <SquareAvatar\n                  key={a.id}\n                  src={a.avatarUrl}\n                  alt={a.name ?? a.initials ?? a.id}\n                  fallback={a.name ?? a.initials}\n                  size=\"sm\"\n                  variant=\"round\"\n                />\n              ),\n            )}\n          {ticket.assignees.length > MAX_VISIBLE_ASSIGNEES && (\n            <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-ods-border bg-ods-bg text-ods-text-secondary text-h6\">\n              +{ticket.assignees.length - MAX_VISIBLE_ASSIGNEES}\n            </div>\n          )}\n        </div>\n      ) : null}\n    </div>\n  ) : null;\n\n  const timestampLabel = ticket.createdAt ? formatTicketRelativeTime(ticket.createdAt) : null;\n  const tooltipLabel = ticket.createdAt ? formatTicketFullTimestamp(ticket.createdAt) : null;\n  // Last line of the details block (Figma tickets 8001-90111): `3891 • 5 min ago`.\n  // The number is display-ready text the host passes (no `#` in the design) and\n  // may be empty - the hub hero card and tickets without one send '' - so either\n  // half stands on its own and the line disappears with both missing.\n  const metaLabel = [ticket.ticketNumber || null, timestampLabel].filter(Boolean).join(' \\u2022 ');\n\n  return (\n    <>\n      <div className=\"flex items-start gap-[var(--spacing-system-sf)]\">\n        <div className=\"flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-zero)]\" title={ticket.title}>\n          <p className=\"truncate text-ods-text-primary text-h3\">{ticket.title}</p>\n          {showDeviceRow && (\n            <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xxs)] text-ods-text-secondary text-h6\">\n              <LaptopIcon className=\"size-4 shrink-0\" />\n              <span className=\"truncate\" title={deviceText}>\n                {deviceText}\n              </span>\n            </div>\n          )}\n        </div>\n        {rightSection}\n      </div>\n      {ticket.tags?.length ? <TicketTagRow tags={ticket.tags} /> : null}\n      {metaLabel &&\n        (tooltipLabel ? (\n          <TooltipProvider>\n            <Tooltip>\n              <TooltipTrigger asChild>\n                <p className=\"pointer-events-auto truncate text-ods-text-secondary text-h6\">{metaLabel}</p>\n              </TooltipTrigger>\n              <TooltipContent>{tooltipLabel}</TooltipContent>\n            </Tooltip>\n          </TooltipProvider>\n        ) : (\n          <p className=\"truncate text-ods-text-secondary text-h6\">{metaLabel}</p>\n        ))}\n      {ticket.escalatedByUser && (\n        <div className=\"flex items-center gap-[var(--spacing-system-xxs)] text-ods-open-yellow text-h6\">\n          <UserCheckIcon className=\"size-4 shrink-0\" />\n          <span className=\"truncate\">Escalated by User</span>\n        </div>\n      )}\n      {ticket.activity && (\n        <div\n          className={cn(\n            'flex items-center gap-[var(--spacing-system-xxs)] text-h6',\n            ticket.activity.kind === 'stale' ? 'text-ods-open-yellow' : 'text-ods-text-secondary',\n          )}\n        >\n          {ticket.activity.kind === 'stale' ? (\n            <ClockIcon className=\"size-4 shrink-0\" />\n          ) : (\n            <DotsLoaderIcon className=\"size-4 shrink-0\" />\n          )}\n          <span className=\"truncate\">{ticket.activity.label ?? ACTIVITY_DEFAULT_LABEL[ticket.activity.kind]}</span>\n        </div>\n      )}\n      {showNewMessage && (\n        <Tag\n          label=\"New Message\"\n          icon={<MessagesIcon size={16} color={newMessageTextColor} />}\n          className=\"w-fit shrink-0\"\n          style={{ backgroundColor: columnColor, color: newMessageTextColor }}\n        />\n      )}\n      {ticket.pendingApproval && (\n        <BoardTicketApproval pendingApproval={ticket.pendingApproval} onApprove={onApprove} onReject={onReject} />\n      )}\n    </>\n  );\n}\n\n// =============================================================================\n// Static card — the real board card design, rendered from props with NO\n// drag-and-drop / board context (for embeds, marketing heroes, previews).\n// =============================================================================\n\nexport interface TicketCardViewProps extends TicketCardBodyProps {\n  className?: string;\n  /** Merged over the shell's own — the board uses it to fade the copy it leaves\n   *  behind and to place the one that follows the pointer. */\n  style?: CSSProperties;\n}\n\n/**\n * The exact board `TicketCard` visual, standalone — same shell + `TicketCardBody`\n * as the draggable card, but registering no drag behaviour, so it renders\n * anywhere from static props. Use this outside a `<Board>`.\n */\nexport function TicketCardView({ className, style, ...bodyProps }: TicketCardViewProps) {\n  const { ticket, columnColor } = bodyProps;\n  const showNewMessage = !!ticket.hasNewMessage && !!columnColor;\n  return (\n    <div\n      className={cn(TICKET_CARD_SHELL, className)}\n      style={showNewMessage ? { borderColor: columnColor, ...style } : style}\n    >\n      <div className=\"relative z-10 flex flex-col gap-[var(--spacing-system-sf)]\">\n        <TicketCardBody {...bodyProps} />\n      </div>\n    </div>\n  );\n}\n\n// =============================================================================\n// Draggable board card\n// =============================================================================\n\nexport interface TicketCardProps {\n  ticket: BoardTicket;\n  columnId: string;\n  columnColor?: string;\n  href?: string;\n  dragDisabled?: boolean;\n  /** The owning column's drop rules. A card is a drop target in its own right,\n   *  and Pragmatic does not let a lane's `canDrop` veto its children — so the\n   *  rules have to be answered here too, or a blocked lane still accepts a drop\n   *  straight onto one of its cards. */\n  dropDisabled?: boolean;\n  allowedFromColumns?: string[];\n  renderAssignSlot?: (ticket: BoardTicket) => ReactNode;\n  onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>;\n  onReject?: (ticketId: string, requestId?: string) => void | Promise<void>;\n}\n\n/** Memoized: a drag moves one card, but it re-renders the column lists around\n *  it, and a board carries dozens of cards whose props did not change. Hosts\n *  must pass stable `renderAssignSlot` / `onApprove` / `onReject` (useCallback)\n *  for this to bite — an inline arrow re-renders every card on every keystroke\n *  anywhere in the page. */\nexport const TicketCard = memo(function TicketCardImpl({\n  ticket,\n  columnId,\n  columnColor,\n  href,\n  dragDisabled,\n  dropDisabled,\n  allowedFromColumns,\n  renderAssignSlot,\n  onApprove,\n  onReject,\n}: TicketCardProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  // Read at drag start rather than captured: the registration below is keyed on\n  // the ticket's id, so capturing the object would hand a renamed or re-tagged\n  // ticket to the drag — and to the preview drawn from it — as it was when the\n  // card first mounted.\n  // Refreshed in an unconditional effect rather than in the render body: it is\n  // only ever read at drag start, which is past a commit, and a render attempt\n  // React discards must not be able to hand the drag a ticket nobody saw.\n  const ticketRef = useRef(ticket);\n  useEffect(() => {\n    ticketRef.current = ticket;\n  });\n  const [isDragging, setIsDragging] = useState(false);\n  // Set by the board for the moment after a drop in which this card is already\n  // in its new slot and the drag preview is still gliding into it.\n  const isLanding = useIsLanding(ticket.id);\n\n  // Where the drop points, worked out once for the whole board — see\n  // `drop-aim.ts` for why this stopped being each card's own business.\n  const aim = useDropAim();\n  // Matched on the LANE as well as the card, and both halves are load-bearing.\n  // The board resolves one landing place, in one named lane; a ticket id is only\n  // unique within a lane here, because each lane is its own query and a ticket\n  // whose status just changed is listed by the new lane before the old one has\n  // caught up. Matching on the id alone drew the line in both lanes at once —\n  // including a lane that refuses the card altogether, since the aim it was\n  // drawing came from the other one.\n  const closestEdge = aim && aim.columnId === columnId && aim.ticketId === ticket.id ? aim.edge : null;\n  // Not lane-scoped, and deliberately: `aim.columnId` is where the card is\n  // GOING, while the card being carried is still sitting in the lane it came\n  // from. True for a pointer drag and for a keyboard lift alike — the aim always\n  // names the card being moved, whichever picked it up.\n  const isSource = isDragging || aim?.ticket.id === ticket.id;\n\n  const lift = useBoardLift();\n  const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {\n    // Space lifts; Enter is left to the link, which opens the ticket.\n    if (e.key !== ' ' || dragDisabled || !lift) return;\n    e.preventDefault();\n    lift(ticket.id);\n  };\n  // Advertised only where it works: the touch board provides no lift, and a\n  // drag-disabled card cannot be lifted either.\n  const keyShortcuts = dragDisabled || !lift ? undefined : 'Space';\n\n  // Both roles live on the same element, and both keep their state HERE. That is\n  // the point of the whole drag stack: hovering this card re-renders this card,\n  // not the board — nothing subscribes upward, so cost does not grow with how\n  // many cards are mounted.\n  useEffect(() => {\n    const element = ref.current;\n    if (!element || dragDisabled) return undefined;\n\n    const identity = { type: 'ticket' as const, ticketId: ticket.id, columnId };\n\n    return combine(\n      // Before anything else: the drag has to start as a \"move\", or its first\n      // frame is drawn with the copy cursor — see `drag-effect.ts`.\n      holdMoveDragEffect(element),\n      draggable({\n        element,\n        // Read at drag start rather than captured, so a card renamed or re-tagged\n        // since it mounted travels as it is now. No height: it was here for a\n        // lane that opened room for the card, and nothing opens room any more —\n        // it was costing a forced layout read at the start of every drag to\n        // produce a field no one read.\n        getInitialData: () => ({ ...identity, ticket: ticketRef.current }),\n        /**\n         * The card under the pointer is the browser's own drag image, styled by\n         * us. Drawing it ourselves as a positioned element was tried and has one\n         * unfixable flaw: an element in the page cannot leave the page. Carry a\n         * card up towards the address bar and the pointer crosses into the\n         * browser's own chrome, where no `dragover` reaches the document — so\n         * the card freezes at the top edge and only catches up when the drag\n         * ends. A native drag image is moved by the browser itself and floats\n         * over everything, which is why Jira's card does not stick there.\n         *\n         * A clone rather than a second React tree: this is a photograph, taken\n         * once, and a copy of the card's own DOM is a photograph of exactly what\n         * is on screen — assignee slot and all — with no providers to re-supply\n         * and no chance of it rendering a loading state into the picture.\n         */\n        onGenerateDragPreview: ({ nativeSetDragImage, location, source }) => {\n          const { width } = source.element.getBoundingClientRect();\n          setCustomNativeDragPreview({\n            nativeSetDragImage,\n            // Holds the card under the same part of itself it was grabbed by.\n            getOffset: preserveOffsetOnSource({ element: source.element, input: location.current.input }),\n            render: ({ container }) => {\n              const clone = source.element.cloneNode(true) as HTMLElement;\n              // The card is a flex item stretched by its lane; on its own it\n              // would shrink to its contents.\n              clone.style.width = `${width}px`;\n              clone.style.opacity = String(DRAG_PREVIEW_OPACITY);\n              clone.classList.add('shadow-card-hover');\n              container.appendChild(clone);\n            },\n          });\n        },\n        onDragStart: () => setIsDragging(true),\n        onDrop: () => setIsDragging(false),\n      }),\n      dropTargetForElements({\n        element,\n        canDrop: ({ source }) => {\n          if (source.data.type !== 'ticket' || source.data.ticketId === ticket.id) return false;\n          const from = String(source.data.columnId);\n          if (from === columnId) return true;\n          if (dropDisabled) return false;\n          return !allowedFromColumns || allowedFromColumns.includes(from);\n        },\n        // The edge rides along in the drop target's own data, so the board reads\n        // \"above or below this card\" straight off the drop it receives.\n        getData: ({ input, element: self }) =>\n          attachClosestEdge({ ...identity }, { input, element: self, allowedEdges: ['top', 'bottom'] }),\n        // Sticky, so crossing the gap between two cards — which is lane padding\n        // and belongs to no card's box — keeps answering with the card just\n        // left, rather than handing the question to the lane for those few\n        // pixels and back again. Both answer with the same slot (the lane\n        // resolves the nearest card and its edge), so this is about not\n        // re-deriving it from a different target every few pixels, NOT about\n        // reaching room a card has opened: nothing moves during a drag here.\n        getIsSticky: () => true,\n      }),\n    );\n  }, [ticket.id, columnId, dragDisabled, dropDisabled, allowedFromColumns]);\n\n  const showNewMessage = !!ticket.hasNewMessage && !!columnColor;\n\n  const style: CSSProperties = {};\n  if (showNewMessage) style.borderColor = columnColor;\n\n  const handleClick = (e: MouseEvent) => {\n    if (isDragging) e.preventDefault();\n  };\n\n  // Held as one memoized element, not re-created per render. A card being\n  // dragged over re-renders on every edge flip — it owns the hover state that\n  // draws the preview — and the body is the expensive half: avatars, tags,\n  // tooltips, a host's assignee picker with its own queries. Keeping the same\n  // element object lets React bail out of that whole subtree, so only the thin\n  // wrapper around it re-renders.\n  const body = useMemo(\n    () => (\n      <TicketCardBody\n        ticket={ticket}\n        columnColor={columnColor}\n        renderAssignSlot={renderAssignSlot}\n        onApprove={onApprove ? requestId => onApprove(ticket.id, requestId) : undefined}\n        onReject={onReject ? requestId => onReject(ticket.id, requestId) : undefined}\n      />\n    ),\n    [ticket, columnColor, renderAssignSlot, onApprove, onReject],\n  );\n\n  // No transition on the margins below, and that is not a style choice: room is\n  // opened at one card and given back at another, and a transition interrupted\n  // part-way — which is every target change while the pointer keeps moving —\n  // restarts from where it got to, so the two ends run at different rates and\n  // the lane's height wanders. The rearrangement is instant instead.\n  const cardClasses = cn(TICKET_CARD_SHELL, !dragDisabled && 'cursor-pointer');\n\n  const outerProps = {\n    ref,\n    style,\n    className: cardClasses,\n    // Read by the lane's scroll anchor to keep the list still when tickets are\n    // inserted above the viewport — see `use-lane-scroll-anchor.ts`.\n    [TICKET_ID_ATTRIBUTE]: ticket.id,\n  };\n\n  const innerWrapperClass = 'relative z-10 flex flex-col gap-[var(--spacing-system-sf)]';\n\n  if (isSource) {\n    // Stays exactly where it is, faded. NOTHING about the lane moves during a\n    // drag — no room opens, no slot closes — so there is no layout to keep\n    // balanced and no height to wander. That is what makes the line below\n    // affordable, and it is why this is also the cheap option.\n    //\n    // Safe to do from state: React commits this AFTER the `dragstart` handler\n    // returns, and it is only removing the source synchronously inside that\n    // handler that lets a browser abort the drag.\n    style.opacity = DRAGGED_CARD_OPACITY;\n  } else if (isLanding) {\n    // Already here, and the preview is on its way to this exact box. Hidden and\n    // not removed: the slot it holds is what the preview is aiming at, and it is\n    // what keeps the lane from moving again the instant it settled.\n    style.visibility = 'hidden';\n  }\n\n  /**\n   * Where the card would land, drawn as a line rather than as an opened gap.\n   *\n   * Opening a card's worth of room and drawing a copy of the card in it costs a\n   * lane relayout plus a whole card render — avatars, tags, tooltips, the host's\n   * assignee slot — on every edge flip, and the flips come as fast as the\n   * pointer moves. A line is one absolutely positioned span, changes no layout,\n   * and says the same thing.\n   */\n  const insertionLine = closestEdge ? (\n    <span\n      aria-hidden\n      {...{ [DROP_LINE_ATTRIBUTE]: '' }}\n      className={cn(\n        // Nothing sticks out sideways or beyond the line's own 2px: the lane\n        // scrolls, and a scroll container clips whatever leaves its padding box\n        // — an end cap or a knob on this would be cut off against the first and\n        // last card in every lane.\n        'pointer-events-none absolute inset-x-0 z-10 h-0.5 rounded-full bg-ods-accent',\n        // Centred in the lane's own gap, so it reads as sitting between two\n        // cards rather than as belonging to one of them. The lane reserves the\n        // matching sliver at its ends (see `board-column.tsx`).\n        closestEdge === 'top' ? '-top-[5px]' : '-bottom-[5px]',\n      )}\n    />\n  ) : null;\n\n  if (href) {\n    return (\n      <div {...outerProps}>\n        {insertionLine}\n        <Link\n          href={href}\n          draggable={false}\n          prefetch={false}\n          onClick={handleClick}\n          onKeyDown={handleKeyDown}\n          aria-label={ticket.title}\n          aria-keyshortcuts={keyShortcuts}\n          className=\"absolute inset-0 z-0 rounded-md focus-visible:outline-none\"\n        />\n        <div className={cn('pointer-events-none', innerWrapperClass)}>{body}</div>\n      </div>\n    );\n  }\n\n  return (\n    <div {...outerProps}>\n      {insertionLine}\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        onKeyDown={handleKeyDown}\n        aria-label={ticket.title}\n        aria-keyshortcuts={keyShortcuts}\n        className=\"absolute inset-0 z-0 cursor-pointer rounded-md focus-visible:outline-none\"\n      />\n      <div className={cn('pointer-events-none', innerWrapperClass)}>{body}</div>\n    </div>\n  );\n});\n\nfunction TicketTagRow({ tags }: { tags: string[] }) {\n  const visible = tags.slice(0, MAX_VISIBLE_TAGS);\n  const hidden = tags.length - visible.length;\n\n  return (\n    <div className=\"flex h-8 flex-wrap items-start gap-[var(--spacing-system-xxs)] overflow-clip\">\n      {visible.map(tag => (\n        <Tag key={tag} variant=\"outline\" label={tag} />\n      ))}\n      {hidden > 0 && <Tag variant=\"outline\" label={`+${hidden}`} />}\n    </div>\n  );\n}\n","/**\n * Holds a native drag started from `element` to the \"move\" effect, from its very\n * first frame.\n *\n * The browser picks the drag cursor from `dataTransfer.dropEffect`, and the\n * value it starts each drag with comes from `effectAllowed`. Pragmatic never\n * writes `effectAllowed`, so it stays `uninitialized` — which the browser reads\n * as \"all\", whose initial `dropEffect` is `copy`. That is the green plus cursor\n * flashing for one frame at the start of every drag, before the first `dragover`\n * has had a chance to say otherwise.\n *\n * This is a raw listener rather than a Pragmatic callback because `effectAllowed`\n * can only be written during the `dragstart` event itself, and none of\n * Pragmatic's callbacks are handed the event.\n *\n * Distinct from `preventUnhandled`, which answers the other half of the same\n * question: this sets what the drag starts as, that keeps it from falling back\n * to \"no drop\" over everything that is not a drop target.\n */\nexport function holdMoveDragEffect(element: HTMLElement): () => void {\n  const onDragStart = (event: DragEvent) => {\n    const transfer = event.dataTransfer;\n    if (transfer) transfer.effectAllowed = 'move';\n  };\n  element.addEventListener('dragstart', onDragStart);\n  return () => element.removeEventListener('dragstart', onDragStart);\n}\n","'use client';\n\nimport { useState } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { ApprovalBatchMessage } from '../../chat/approval-batch-message';\nimport { useCollapsible } from '../../chat/hooks/use-collapsible';\nimport { Chevron02DownIcon, Chevron02UpIcon, DotsLoaderIcon, ShieldCheckIcon } from '../../icons-v2-generated';\nimport type { BoardTicketPendingApproval } from './types';\n\nexport interface BoardTicketApprovalProps {\n  pendingApproval: BoardTicketPendingApproval;\n  onApprove?: (requestId?: string) => void | Promise<void>;\n  onReject?: (requestId?: string) => void | Promise<void>;\n}\n\n/**\n * Inline, collapsed-by-default approval section for a board ticket card. Expands to\n * the shared {@link ApprovalBatchMessage} so the request can be approved/rejected\n * without leaving the board. The header switches between the client variant (grey,\n * \"Pending client approval\") and the technician/admin variant (yellow shield,\n * \"Technician approval required\") on `approvalType`.\n */\nexport function BoardTicketApproval({ pendingApproval, onApprove, onReject }: BoardTicketApprovalProps) {\n  const [expanded, setExpanded] = useState(false);\n  const { innerRef, containerStyle } = useCollapsible({ expanded });\n  const isAdmin = pendingApproval.approvalType === 'ADMIN';\n\n  return (\n    <div className=\"pointer-events-auto flex flex-col\">\n      <button\n        type=\"button\"\n        onClick={() => setExpanded(v => !v)}\n        aria-expanded={expanded}\n        className=\"flex w-full cursor-pointer items-center gap-[var(--spacing-system-xxs)] text-h6\"\n      >\n        <span\n          className={cn(\n            'flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xxs)]',\n            isAdmin ? 'text-ods-open-yellow' : 'text-ods-text-secondary',\n          )}\n        >\n          {isAdmin ? (\n            <ShieldCheckIcon className=\"size-4 shrink-0\" />\n          ) : (\n            <DotsLoaderIcon size={16} className=\"shrink-0\" />\n          )}\n          <span className=\"flex-1 truncate text-left\">\n            {isAdmin ? 'Technician approval required' : 'Pending client approval'}\n          </span>\n        </span>\n        {expanded ? (\n          <Chevron02UpIcon className=\"size-4 shrink-0 text-ods-text-secondary\" />\n        ) : (\n          <Chevron02DownIcon className=\"size-4 shrink-0 text-ods-text-secondary\" />\n        )}\n      </button>\n\n      <div style={containerStyle}>\n        <div ref={innerRef} className=\"pt-[var(--spacing-system-xs)]\">\n          <ApprovalBatchMessage\n            className=\"mb-0\"\n            data={{\n              approvalRequestId: pendingApproval.id,\n              approvalType: pendingApproval.approvalType ?? 'CLIENT',\n              toolCalls: pendingApproval.toolCalls ?? [],\n            }}\n            status=\"pending\"\n            showExecutionStatus={false}\n            onApprove={onApprove}\n            onReject={onReject}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n","'use client';\n\nimport { createContext, useContext } from 'react';\nimport type { BoardColumnDef } from './types';\n\n/** The move that was reported, so we can show it and recognise it arriving. */\nexport interface PendingMove {\n  ticketId: string;\n  /** The lane it came from — what tells a lane change apart from a reorder. */\n  fromColumnId: string;\n  toColumnId: string;\n  /** The card it was dropped after, or `null` for the head of the lane. */\n  afterTicketId: string | null;\n}\n\n/**\n * `columns` with a move that has been reported but not yet answered already\n * applied — what the board renders between the drop and the host's new data.\n *\n * A drop cannot repaint the board from the host's data in the same frame it\n * happens: the host is told about the move through `onChange` and answers with\n * new `columns`, and no data layer worth using is synchronous about that —\n * react-query, for one, notifies its subscribers from a `setTimeout(0)`, which\n * is a whole macrotask (and therefore at least one paint) after the drop event.\n * Rendering the host's answer literally therefore means the card is still in its\n * old lane for that gap and arrives in the new one afterwards.\n *\n * Hiding the card across the gap was the previous answer, and it is why a drop\n * read as the ticket vanishing and reappearing somewhere else. Showing the move\n * immediately is the same optimistic update every board of this kind does, and\n * it makes the handover invisible: the host's answer, when it comes, describes\n * exactly the picture already on screen.\n *\n * Untouched columns keep their identity so the memoized lanes can skip the\n * render.\n */\nexport function applyPendingMove(columns: BoardColumnDef[], move: PendingMove): BoardColumnDef[] {\n  const ticket = columns.flatMap(column => column.tickets).find(t => t.id === move.ticketId);\n  if (!ticket || !columns.some(column => column.id === move.toColumnId)) return columns;\n\n  return columns.map(column => {\n    const without = column.tickets.filter(t => t.id !== move.ticketId);\n    if (column.id !== move.toColumnId) {\n      return without.length === column.tickets.length ? column : { ...column, tickets: without };\n    }\n    return { ...column, tickets: insertAfter(without, ticket, move.afterTicketId) };\n  });\n}\n\nfunction insertAfter<T extends { id: string }>(tickets: T[], ticket: T, afterId: string | null): T[] {\n  if (afterId === null) return [ticket, ...tickets];\n  const after = tickets.findIndex(t => t.id === afterId);\n  // The card it was dropped after is gone — archived, or moved by someone else\n  // while this drag was in the air. The end of the lane is the honest answer,\n  // and the host's own reply overrides it a moment later anyway.\n  const at = after < 0 ? tickets.length : after + 1;\n  return [...tickets.slice(0, at), ticket, ...tickets.slice(at)];\n}\n\n/**\n * The id of the card currently flying from the pointer into the slot it was\n * dropped in — hidden for exactly that long, because the drag preview is\n * covering it.\n *\n * Only the one card cares, which is why this is an id and not a flag on every\n * card.\n */\nexport const LandingCardContext = createContext<string | null>(null);\n\n/** True while this card is under the preview travelling into its slot. */\nexport function useIsLanding(ticketId: string): boolean {\n  return useContext(LandingCardContext) === ticketId;\n}\n\n/**\n * Whether THIS move has reached `columns`, so the board can stop standing in for\n * the host and render its data plainly again.\n *\n * Two weaker tests were tried and both hand back too early, at which point the\n * board snaps to whatever the host currently says — the card jumping back to its\n * old lane for a frame:\n *\n * - The identity of the `columns` array. A host on react-query hands the board a\n *   fresh one whenever anything notifies — a cancelled refetch, a loading flag,\n *   a live update on an untouched lane.\n * - The card no longer sitting at its old index. Each lane here polls on its own\n *   timer and takes live updates, so a ticket arriving above this one shifts the\n *   index and reads as \"the move landed\" when nothing of the sort happened.\n *\n * A lane change and a reorder need different tests, though, and using the strict\n * one for both is what left a card to jump seconds after it landed:\n *\n * - **Across lanes**, arriving in the new lane IS the host applying the move.\n *   Which slot it ends up in is then the host's business — a server that sorts a\n *   lane its own way will not reproduce the dropped index, and holding out for\n *   one it will never send means showing a position that is quietly wrong until\n *   the timeout corrects it in a visible jump. The old lane has to have let go\n *   of the card as well: the two lanes are separate queries answering at their\n *   own pace, and standing aside in between would show the card in both.\n * - **Within one lane** the card is in that lane either way, so only the exact\n *   slot distinguishes \"applied\" from \"not yet\".\n *\n * Answering \"not yet\" for a move the host never applies is safe: the board gives\n * up on it after a timeout regardless, and the card returns to where the host\n * says it is — which is the correct thing to show once a move has failed.\n */\nexport function hasMoveSettled(columns: readonly BoardColumnDef[], move: PendingMove): boolean {\n  const tickets = columns.find(c => c.id === move.toColumnId)?.tickets;\n  if (!tickets) return false;\n  const index = tickets.findIndex(t => t.id === move.ticketId);\n  if (index < 0) return false;\n  if (move.fromColumnId === move.toColumnId) {\n    return (tickets[index - 1]?.id ?? null) === move.afterTicketId;\n  }\n  return !columns.find(c => c.id === move.fromColumnId)?.tickets.some(t => t.id === move.ticketId);\n}\n","'use client';\n\nimport { type HTMLAttributes, forwardRef } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Skeleton } from '../../ui/skeleton';\n\nexport interface TicketCardSkeletonProps extends HTMLAttributes<HTMLDivElement> {}\n\nexport const TicketCardSkeleton = forwardRef<HTMLDivElement, TicketCardSkeletonProps>(\n  ({ className, ...props }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\n        'flex flex-col gap-[var(--spacing-system-sf)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-sf)]',\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex items-start gap-[var(--spacing-system-sf)]\">\n        <div className=\"flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]\">\n          <div className=\"flex items-center text-h3\">\n            <Skeleton className=\"h-4 w-3/4\" />\n          </div>\n          <div className=\"flex items-center gap-[var(--spacing-system-xxs)] text-h6\">\n            <Skeleton className=\"size-4 shrink-0 rounded-sm\" />\n            <Skeleton className=\"h-3 w-1/2\" />\n          </div>\n        </div>\n        <div className=\"flex shrink-0 items-center gap-[var(--spacing-system-xsf)]\">\n          <Skeleton className=\"size-4 rounded-sm\" />\n          <Skeleton className=\"size-8 rounded-full\" />\n        </div>\n      </div>\n      <div className=\"flex h-8 items-center gap-[var(--spacing-system-xxs)]\">\n        <Skeleton className=\"h-8 w-16 rounded-md\" />\n        <Skeleton className=\"h-8 w-12 rounded-md\" />\n      </div>\n    </div>\n  ),\n);\nTicketCardSkeleton.displayName = 'TicketCardSkeleton';\n","import { aimFromTarget, aimToChange } from './aim';\nimport type { BoardChange, BoardColumnDef } from './types';\n\n/** What a drop carries: the card that moved, and what it was dropped onto. */\ninterface DropInput {\n  /** `getInitialData` from the dragged card. */\n  source: Record<string | symbol, unknown>;\n  /** The innermost drop target under the pointer — a card, or a lane. */\n  target: Record<string | symbol, unknown> | undefined;\n}\n\n/**\n * Turns a drop into the move to report, or `null` when nothing moved.\n *\n * Both steps live in `aim.ts`, shared with the keyboard and with the line drawn\n * during the drag: the target becomes a slot, the slot becomes a change. Having\n * the drop work it out separately is how a board ends up drawing a line in one\n * place and committing a move to another.\n */\nexport function resolveBoardDrop(\n  columns: readonly BoardColumnDef[],\n  { source, target }: DropInput,\n): BoardChange | null {\n  if (!target) return null;\n  const ticketId = String(source.ticketId);\n  const aim = aimFromTarget(columns, ticketId, target);\n  return aim && aimToChange(columns, ticketId, aim);\n}\n","'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useLocalStorage } from '../../../hooks/ui/use-local-storage';\n\nexport type BoardCollapseMap = Record<string, boolean>;\n\nexport interface UseBoardCollapseReturn {\n  collapsed: BoardCollapseMap;\n  toggle: (columnId: string) => void;\n  setCollapsed: (columnId: string, value: boolean) => void;\n}\n\nexport function useBoardCollapse(storageKey?: string): UseBoardCollapseReturn {\n  const [persisted, setPersisted] = useLocalStorage<BoardCollapseMap>(storageKey ?? '__board_collapse_unused__', {});\n  const [memory, setMemory] = useState<BoardCollapseMap>({});\n\n  const collapsed = storageKey ? persisted : memory;\n  const setMap = storageKey ? setPersisted : setMemory;\n\n  // Both callbacks keep one identity for the hook's lifetime. `useLocalStorage`\n  // returns an unmemoized setter, so depending on it directly hands the board a\n  // new `toggle` on every render — and a board that memoizes its columns then\n  // re-renders all of them on every drag frame, which is the one thing the memo\n  // is there to prevent. The ref keeps the latest setter without the identity.\n  // Refreshed in an unconditional effect, not the render body: both readers\n  // are click handlers, which can only run after a commit, so they still get\n  // the current setter — and a render attempt React discards can no longer\n  // leave a setter behind that was never committed.\n  const setMapRef = useRef(setMap);\n  useEffect(() => {\n    setMapRef.current = setMap;\n  });\n\n  const toggle = useCallback((columnId: string) => {\n    setMapRef.current(prev => ({ ...prev, [columnId]: !prev[columnId] }));\n  }, []);\n\n  const setCollapsed = useCallback((columnId: string, value: boolean) => {\n    setMapRef.current(prev => ({ ...prev, [columnId]: value }));\n  }, []);\n\n  return { collapsed, toggle, setCollapsed };\n}\n","'use client';\n\nimport { cn } from '../../../utils/cn';\nimport type { BoardProps } from './board';\nimport { BoardColumn } from './board-column';\nimport { useBoardCollapse } from './use-board-collapse';\n\n/**\n * The board for touch and narrow viewports: full-height lanes in a horizontal\n * scroll-snap track, one mostly-filling the viewport with the next one peeking\n * in from the right, switched by swiping (per the tickets Figma mocks — no\n * separate column navigation is rendered).\n *\n * Nothing drag-and-drop exists in this tree at all — no monitor, no drop\n * targets, no auto-scroll, no keyboard lift (every lane renders with\n * `touchMode`, see `board-column.tsx`). The HTML5 drag events Pragmatic builds\n * on never fire under touch, so a drag here could only ever be a dead\n * affordance. A card's column is changed from the ticket itself (its dialog),\n * not from the board, which is why this component never calls `onChange`.\n *\n * Collapse works exactly as on the drag board (same lane header buttons, same\n * `collapseStorageKey`, so the choice survives a mode switch); a collapsed lane\n * is a narrow vertical stop in the same snap track. Only the custom horizontal\n * scrollbar is absent — in a snap pager the track itself is the navigation.\n */\nexport function TouchBoard({\n  columns,\n  onLoadMore,\n  onAddTicket,\n  onArchiveColumn,\n  getTicketHref,\n  renderAssignSlot,\n  onApprove,\n  onReject,\n  collapseStorageKey,\n  loadMoreRootMargin,\n  className,\n}: BoardProps) {\n  const { collapsed, toggle } = useBoardCollapse(collapseStorageKey);\n\n  return (\n    <div className={cn('flex h-full min-h-0 flex-col', className)}>\n      <div\n        // `snap-start`, not `snap-center`: the mocks left-align the current\n        // lane with the page padding and let the next one peek in at the right.\n        className=\"flex min-h-0 flex-1 snap-x snap-mandatory gap-[var(--spacing-system-mf)] overflow-x-auto overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n      >\n        {columns.map(column => (\n          <BoardColumn\n            key={column.id}\n            column={column}\n            touchMode\n            collapsed={!!collapsed[column.id]}\n            onToggleCollapse={toggle}\n            // The peek IS the column navigation: capped under the track width by\n            // one gutter more than the gap, so the next lane is always visible.\n            // A collapsed lane keeps the lane's own `w-14` strip.\n            className={cn(\n              'snap-start',\n              !collapsed[column.id] && 'w-[min(400px,calc(100%_-_2*var(--spacing-system-mf)))]',\n            )}\n            onAddTicket={onAddTicket}\n            onArchive={onArchiveColumn}\n            getTicketHref={getTicketHref}\n            renderAssignSlot={renderAssignSlot}\n            onApprove={onApprove}\n            onReject={onReject}\n            onLoadMore={onLoadMore}\n            loadMoreRootMargin={loadMoreRootMargin}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n","import type { PendingToolCallData } from '../../chat/types';\nimport type { TicketStatus } from '../../ui/ticket-status-tag';\n\nexport type BoardPriority = 'low' | 'medium' | 'high' | 'urgent';\n\n/** Latest pending tool-approval request attached to a board ticket (from `Ticket.pendingApproval`). */\nexport interface BoardTicketPendingApproval {\n  id: string;\n  approvalType?: string;\n  command?: string;\n  explanation?: string;\n  createdAt?: string;\n  toolCalls?: PendingToolCallData[];\n}\n\nexport interface BoardTicketAssignee {\n  id: string;\n  initials?: string;\n  avatarUrl?: string;\n  name?: string;\n  /** Deleted account (DELETED / SELF_DELETED) — renders the red user-x placeholder instead of the avatar. */\n  deleted?: boolean;\n}\n\n/**\n * Live-activity footer indicator on a board card. Indicators are mutually\n * exclusive by design — the consumer decides which single one a ticket shows\n * (staleness has the lowest priority and must be suppressed when any other\n * signal — new message, approval, escalation — is present).\n */\nexport type BoardTicketActivityKind = 'ai-working' | 'user-typing' | 'waiting-external' | 'stale';\n\nexport interface BoardTicketActivity {\n  kind: BoardTicketActivityKind;\n  /**\n   * Overrides the built-in label for the kind. Required in practice for\n   * 'stale', whose label carries the computed duration (\"No activity for\n   * 2 hours\") that only the consumer can know — and tick over time.\n   */\n  label?: string;\n}\n\nexport interface BoardTicket {\n  id: string;\n  title: string;\n  /**\n   * Display-ready ticket number (`'3891'`, no `#` - the design never prefixes\n   * it), shown before the relative time on the card's last details line.\n   * `''` hides it; the hub hero card passes that.\n   */\n  ticketNumber: string;\n  status: string;\n\n  deviceHostnames?: string[];\n  organizationName?: string;\n  priority?: BoardPriority;\n  assignees?: BoardTicketAssignee[];\n  tags?: string[];\n  createdAt?: string;\n  hasNewMessage?: boolean;\n  pendingApproval?: BoardTicketPendingApproval;\n  /**\n   * The client asked for a human and confirmed the handoff, so this ticket\n   * reached its lane by escalation rather than an inactivity auto-escalation or\n   * an admin takeover. Called out on the card because it changes how urgently a\n   * technician should pick it up.\n   */\n  escalatedByUser?: boolean;\n  /** Single live-activity indicator rendered as the card's footer row. */\n  activity?: BoardTicketActivity;\n}\n\nexport interface BoardColumnDef {\n  id: string;\n  /**\n   * Canonical status key for header styling (e.g. 'ACTIVE', 'RESOLVED'), when the\n   * column `id` is not itself a known status (e.g. a custom-status UUID). The\n   * header resolves its TicketStatusTag variant/icon from this, falling back to\n   * `id`. Leave undefined for custom statuses so they render from `color`.\n   */\n  statusKey?: string;\n  label: string;\n  color: string;\n  /**\n   * What this column's status means, shown from an info icon in the header\n   * (e.g. the system-status descriptions from the status settings page). No\n   * icon is rendered when absent.\n   */\n  tooltip?: string;\n  tickets: BoardTicket[];\n\n  total?: number;\n  hasMore?: boolean;\n  isLoading?: boolean;\n  isLoadingMore?: boolean;\n\n  system?: boolean;\n  dropDisabled?: boolean;\n  dragDisabled?: boolean;\n  allowedFromColumns?: string[];\n  archivable?: boolean;\n}\n\nexport interface BoardChange {\n  ticketId: string;\n  fromColumnId: string;\n  toColumnId: string;\n  afterTicketId: string | null;\n  beforeTicketId: string | null;\n}\n\nconst STATUS_DEFAULTS: Record<TicketStatus, { label: string; color: string }> = {\n  ACTIVE: { label: 'Active', color: '#5ea62e' },\n  AI_ASSISTANCE: { label: 'AI Handling', color: '#888888' },\n  TECH_REQUIRED: { label: 'Tech Required', color: '#e1b32f' },\n  ON_HOLD: { label: 'On Hold', color: '#f36666' },\n  RESOLVED: { label: 'Resolved', color: '#888888' },\n  ARCHIVED: { label: 'Archived', color: '#3a3a3a' },\n};\n\nexport function columnFromTicketStatus(\n  status: TicketStatus,\n  tickets: BoardTicket[],\n  overrides: Partial<Omit<BoardColumnDef, 'id' | 'tickets'>> = {},\n): BoardColumnDef {\n  const defaults = STATUS_DEFAULTS[status];\n  return {\n    id: status,\n    label: overrides.label ?? defaults.label,\n    color: overrides.color ?? defaults.color,\n    tooltip: overrides.tooltip,\n    tickets,\n    total: overrides.total,\n    hasMore: overrides.hasMore,\n    isLoading: overrides.isLoading,\n    isLoadingMore: overrides.isLoadingMore,\n    system: overrides.system,\n    dropDisabled: overrides.dropDisabled,\n    dragDisabled: overrides.dragDisabled,\n    allowedFromColumns: overrides.allowedFromColumns,\n    archivable: overrides.archivable,\n  };\n}\n\nexport function groupTicketsByStatus(tickets: BoardTicket[], statuses: TicketStatus[]): BoardColumnDef[] {\n  const buckets: Record<TicketStatus, BoardTicket[]> = {\n    ACTIVE: [],\n    AI_ASSISTANCE: [],\n    TECH_REQUIRED: [],\n    ON_HOLD: [],\n    RESOLVED: [],\n    ARCHIVED: [],\n  };\n  for (const t of tickets) {\n    const canonical = canonicalize(t.status);\n    if (canonical) buckets[canonical].push(t);\n  }\n  return statuses.map(s => columnFromTicketStatus(s, buckets[s]));\n}\n\nfunction canonicalize(status: string): TicketStatus | null {\n  const upper = status.toUpperCase().replace(/\\s+/g, '_');\n  if (upper === 'ACTION_REQUIRED') return 'TECH_REQUIRED';\n  if (\n    upper === 'ACTIVE' ||\n    upper === 'AI_ASSISTANCE' ||\n    upper === 'TECH_REQUIRED' ||\n    upper === 'ON_HOLD' ||\n    upper === 'RESOLVED' ||\n    upper === 'ARCHIVED'\n  ) {\n    return upper;\n  }\n  return null;\n}\n","/**\n * The arithmetic behind a vertical sortable list: where a dragged item would\n * come to rest at each index, which of those places it is currently nearest to,\n * and how far that pushes everything else.\n *\n * Pure on purpose. The DOM is read exactly once, when the drag starts, and\n * everything after that is numbers — so the rules can be unit-tested instead of\n * discovered in a browser, and a card moving out of the way can never change the\n * answer that moved it and start an oscillation.\n */\n\n/**\n * Share of one slot's travel that the slot an item already holds keeps as a\n * handicap.\n *\n * Without it an item ends up sitting exactly ON a boundary the instant it\n * crosses one: leaving a slot costs half an item of dragging and coming back\n * costs a pixel, which reads as the list changing its mind about the rules. The\n * handicap turns that into a dead band, so both cost the same movement — and a\n * boundary just crossed cannot be re-crossed by jitter, or by the hand simply\n * stopping.\n */\nexport const SLOT_STICKINESS = 0.25;\n\nexport interface DragSlots {\n  /** Every item's top edge, before anything moved, measured from the list's own\n   *  top rather than the viewport or the document — so a scroll during the drag\n   *  (the page, the layout's `<main>`, any ancestor) leaves them all valid. */\n  tops: number[];\n  /** Every item's bottom edge, same frame of reference. */\n  bottoms: number[];\n  /** The dragged item's own height. */\n  height: number;\n  /** The index the drag started from. */\n  from: number;\n  /** How far a displaced item travels: the dragged item's height + the row gap. */\n  step: number;\n}\n\n/**\n * Reads the list once, at drag start. The only DOM access in this module.\n *\n * `origin` is the list's own top edge in viewport coordinates; everything is\n * stored relative to it, so auto-scrolling an ancestor mid-drag moves the list\n * and the pointer together and none of this goes stale.\n */\nexport function measureSlots(items: readonly HTMLElement[], from: number, gap: number, origin: number): DragSlots {\n  const rects = items.map(item => item.getBoundingClientRect());\n  return {\n    tops: rects.map(rect => rect.top - origin),\n    bottoms: rects.map(rect => rect.bottom - origin),\n    height: rects[from].height,\n    from,\n    step: rects[from].height + gap,\n  };\n}\n\n/**\n * Where the dragged item's centre ends up if it is dropped at `index`.\n *\n * At or above its own index it takes over that item's top edge; below it, it\n * backs up against that item's bottom edge. With items of one height those are\n * the same point; with mixed heights they are not, and it is the resting place\n * the user is aiming at — not the neighbour's centre.\n */\nfunction restingCentre(slots: DragSlots, index: number): number {\n  return index <= slots.from ? slots.tops[index] + slots.height / 2 : slots.bottoms[index] - slots.height / 2;\n}\n\n/**\n * The slot the dragged item is now nearest to landing in, given the one it\n * currently holds and how far it has been dragged.\n *\n * Nearest — not \"has crossed\" — is what dnd-kit sorted by, and it is why the\n * list opens up when it does: nearest flips at the midpoint between two slots,\n * so half an item of travel is enough, where waiting for the item to cross a\n * neighbour outright takes twice as long and reads as late.\n */\nexport function resolveSlot(slots: DragSlots, held: number, offset: number): number {\n  const centre = slots.tops[slots.from] + slots.height / 2 + offset;\n  let best = Math.abs(centre - restingCentre(slots, held)) - slots.step * SLOT_STICKINESS;\n  let next = held;\n  for (let index = 0; index < slots.tops.length; index++) {\n    if (index === held) continue;\n    const distance = Math.abs(centre - restingCentre(slots, index));\n    if (distance < best) {\n      best = distance;\n      next = index;\n    }\n  }\n  return next;\n}\n\n/** How far the item at `index` has to stand aside while the drop is aimed at `to`. */\nexport function shiftFor(slots: DragSlots, index: number, to: number): number {\n  if (index === slots.from) return 0;\n  if (index > slots.from && index <= to) return -slots.step;\n  if (index < slots.from && index >= to) return slots.step;\n  return 0;\n}\n","'use client';\n\nimport { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';\nimport { dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';\nimport { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/utils/prevent-unhandled';\nimport {\n  createContext,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\nimport { useDragAndDropEnabled } from '../../../hooks/ui/use-drag-and-drop-enabled';\nimport { useIsomorphicLayoutEffect } from '../../../hooks/ui/use-isomorphic-layout-effect';\nimport { autoScrollAncestors, autoScrollBothWays } from '../../../utils/auto-scroll-ancestors';\nimport { type DragSlots, measureSlots, resolveSlot, shiftFor } from './slot-geometry';\n\n/** Marks the elements the list reorders. Set by `useSortableItem`, never by hand. */\nexport const SORTABLE_ITEM_ATTRIBUTE = 'data-sortable-item';\n\n/** How long a displaced item takes to slide into the slot it is giving up. */\nconst SHIFT_MS = 200;\n/** How long the dropped item takes to travel from the pointer into its slot. */\nconst DROP_MS = 150;\nconst EASING = 'cubic-bezier(0.2, 0, 0, 1)';\n\ninterface SortableListContextValue {\n  /** Distinguishes this list's drags from any other list's on the page. */\n  dragType: string;\n  /** The consumer's `disabled` prop: everything off, move buttons included. */\n  disabled: boolean;\n  /**\n   * The input-type gate (`useDragAndDropEnabled`): `false` on touch/narrow\n   * viewports, where no drag is registered and rows render explicit\n   * `SortableMoveButtons` instead of a handle.\n   */\n  dragAndDropEnabled: boolean;\n  /** Reorders the list. Used by the drop, a handle's arrow keys AND the move buttons. */\n  reorder: (from: number, to: number) => void;\n  /** Every registered item, in DOM order. */\n  readItems: () => HTMLElement[];\n}\n\nconst SortableListContext = createContext<SortableListContextValue | null>(null);\n\nexport function useSortableListContext(): SortableListContextValue {\n  const context = useContext(SortableListContext);\n  if (!context) throw new Error('useSortableItem must be used inside a <SortableList>');\n  return context;\n}\n\nexport interface SortableListProps {\n  /** Called once, on drop or on an arrow key — never mid-drag. */\n  onReorder: (from: number, to: number) => void;\n  /**\n   * Names an item for the live region, e.g. `index => scripts[index].name`.\n   * Without it a move is announced as \"Item moved to position 2 of 5\".\n   */\n  getItemLabel?: (index: number) => string | undefined;\n  disabled?: boolean;\n  className?: string;\n  children: ReactNode;\n}\n\n/**\n * A vertical list whose items can be reordered by dragging a handle.\n *\n * **The drag moves nothing but transforms.** The item being dragged keeps its\n * slot and is translated to follow the pointer; the items it passes are\n * translated by exactly its height plus the row gap, into the slot it vacated.\n * The list's layout — and so the height of the page around it — is therefore\n * identical from the first frame of the drag to the last, which is what makes\n * the slide safe to animate. Opening and closing real space instead (margins,\n * `display: none`) cannot be: interrupt one of those transitions by moving the\n * pointer, or reorder the DOM on drop, and the two ends run at different rates\n * and the page jumps.\n *\n * The native drag image is switched off — the browser draws it washed out and\n * offers no way to style it — so what follows the pointer is the real item, at\n * full opacity, styled by the consumer off `isDragging`.\n *\n * Deliberately NOT what the ticket board does: a board moves cards between\n * columns and its cards are drop targets in their own right. Here the answer is\n * geometry over a single column, which is why it fits in one small module. The\n * rules it sorts by live in `slot-geometry.ts`.\n */\nexport function SortableList({ onReorder, getItemLabel, disabled = false, className, children }: SortableListProps) {\n  const listRef = useRef<HTMLDivElement>(null);\n  // False on touch/narrow viewports: no drag is initialized anywhere in the\n  // list, and rows fall back to explicit move buttons.\n  const dragAndDropEnabled = useDragAndDropEnabled();\n  // Stable across renders and never rendered, so there is no hydration to\n  // mismatch — unlike keying a drag on a random per-item id.\n  const dragType = useId();\n  const [announcement, setAnnouncement] = useState('');\n\n  // Refreshed in an unconditional effect rather than in the render body: both\n  // are read only from a drag or keyboard-move handler, which cannot run\n  // before a commit, and a render attempt React discards must not be able to\n  // leave a reorder callback behind that never took effect.\n  const onReorderRef = useRef(onReorder);\n  const getItemLabelRef = useRef(getItemLabel);\n  useEffect(() => {\n    onReorderRef.current = onReorder;\n    getItemLabelRef.current = getItemLabel;\n  });\n\n  // Where every item sat on screen the instant the drop happened — the \"First\"\n  // of the FLIP played once the reorder has committed. A LOOKUP TABLE keyed by\n  // element, not a list to iterate: the \"Last\" is measured on the items the\n  // list actually has after the reorder commit (re-queried from the DOM), and\n  // this only answers \"where was this one?\". An item the reorder removed is\n  // therefore skipped instead of being styled as a detached node.\n  const flipFrom = useRef<Map<HTMLElement, number> | null>(null);\n  const [flipTick, setFlipTick] = useState(0);\n  const flipTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const readItems = useCallback(\n    () => [...(listRef.current?.querySelectorAll<HTMLElement>(`[${SORTABLE_ITEM_ATTRIBUTE}]`) ?? [])],\n    [],\n  );\n\n  /** The one way this list ever changes: say what happened, then do it. */\n  const reorder = useCallback(\n    (from: number, to: number) => {\n      if (from === to) return;\n      const label = getItemLabelRef.current?.(from) ?? 'Item';\n      setAnnouncement(`${label} moved to position ${to + 1} of ${readItems().length}.`);\n      onReorderRef.current(from, to);\n    },\n    [readItems],\n  );\n\n  useEffect(() => {\n    const list = listRef.current;\n    if (!list || disabled || !dragAndDropEnabled) return undefined;\n\n    /** Alive for one drag; this effect body is its whole scope. */\n    let drag: (DragSlots & { items: HTMLElement[]; to: number; startY: number }) | null = null;\n\n    return combine(\n      // Dragging towards the edge of the screen — or past it — scrolls whatever\n      // actually scrolls there: the page, the layout's `<main>`, or the list\n      // itself when it has its own overflow.\n      autoScrollAncestors(list),\n      autoScrollBothWays(list),\n      // One target for the whole list: which slot a drop lands in is answered by\n      // geometry, so this only has to say \"inside the list, or outside it\".\n      dropTargetForElements({\n        element: list,\n        canDrop: ({ source }) => source.data.type === dragType,\n      }),\n      monitorForElements({\n        canMonitor: ({ source }) => source.data.type === dragType,\n\n        onDragStart: ({ source, location }) => {\n          // Only the list is a drop target, so a drag carried anywhere else on\n          // the page has none — and a native drag with no target under it falls\n          // back to the browser's own drop effect, which is `copy` and draws the\n          // green plus cursor. This holds it at `move` for the whole drag.\n          preventUnhandled.start();\n          const items = readItems();\n          const from = items.indexOf(source.element);\n          if (from < 0) return;\n          const gap = Number.parseFloat(getComputedStyle(list).rowGap) || 0;\n          // The list's own top is the frame of reference for the whole drag, so\n          // that auto-scrolling under the pointer changes nothing but the origin.\n          const origin = list.getBoundingClientRect().top;\n          drag = {\n            ...measureSlots(items, from, gap, origin),\n            items,\n            to: from,\n            startY: location.current.input.clientY - origin,\n          };\n        },\n\n        onDrag: ({ location }) => {\n          const current = drag;\n          if (!current) return;\n          // Re-read the origin every frame: auto-scroll moves the list under a\n          // pointer that has not moved, and the item has to keep up with it.\n          const offset = location.current.input.clientY - list.getBoundingClientRect().top - current.startY;\n\n          // Vertical only, the way `restrictToVerticalAxis` pinned it, and with\n          // no transition: this one tracks the pointer exactly.\n          const dragged = current.items[current.from];\n          dragged.style.transition = 'none';\n          dragged.style.transform = `translateY(${offset}px)`;\n\n          const to = resolveSlot(current, current.to, offset);\n          // Re-applying an unchanged shift would restart its transition every\n          // frame, so the items would creep instead of sliding.\n          if (to === current.to) return;\n          current.to = to;\n\n          for (const [index, item] of current.items.entries()) {\n            if (index === current.from) continue;\n            const shift = shiftFor(current, index, to);\n            item.style.transition = `transform ${SHIFT_MS}ms ${EASING}`;\n            item.style.transform = shift ? `translateY(${shift}px)` : '';\n          }\n        },\n\n        onDrop: ({ location }) => {\n          preventUnhandled.stop();\n          const finished = drag;\n          drag = null;\n          if (!finished) return;\n\n          // Read where everything IS, transforms and all, before letting go of\n          // them — this is what the drop animation plays back from.\n          flipFrom.current = new Map(finished.items.map(item => [item, item.getBoundingClientRect().top]));\n          if (flipTimer.current) clearTimeout(flipTimer.current);\n          for (const item of finished.items) {\n            item.style.transition = 'none';\n            item.style.transform = '';\n          }\n\n          // Empty means released outside the list, or cancelled with Escape —\n          // the item still has to travel back to its own slot, so the animation\n          // is scheduled either way.\n          const cancelled = location.current.dropTargets.length === 0;\n          setFlipTick(tick => tick + 1);\n          if (!cancelled) reorder(finished.from, finished.to);\n        },\n      }),\n    );\n  }, [dragType, disabled, dragAndDropEnabled, readItems, reorder]);\n\n  // Runs after the commit that carries BOTH the reorder and the tick above, so\n  // the DOM is already in its final order — which is why `onReorder` never has\n  // to wait for an animation. Every item that ended up somewhere other than\n  // where it looked is put back there and released; the items that stood aside\n  // during the drag are already in place and come out with a delta of zero, so\n  // this animates the dropped item and nothing else.\n  useIsomorphicLayoutEffect(() => {\n    const from = flipFrom.current;\n    flipFrom.current = null;\n    if (!from) return;\n\n    const moving: HTMLElement[] = [];\n    // The \"Last\" is measured on the list as it stands NOW — re-queried, not\n    // replayed from the pre-drop capture, so an item the reorder dropped is\n    // simply absent instead of being animated as a node no longer in the tree.\n    for (const item of readItems()) {\n      const top = from.get(item);\n      if (top === undefined) continue;\n      const delta = top - item.getBoundingClientRect().top;\n      if (!delta) continue;\n      item.style.transition = 'none';\n      item.style.transform = `translateY(${delta}px)`;\n      moving.push(item);\n    }\n    if (moving.length === 0) return;\n\n    // One forced reflow, so the browser takes the inverted position as the\n    // animation's starting point instead of coalescing it away unseen.\n    void listRef.current?.offsetHeight;\n\n    for (const item of moving) {\n      item.style.transition = `transform ${DROP_MS}ms ${EASING}`;\n      item.style.transform = '';\n    }\n    flipTimer.current = setTimeout(() => {\n      for (const item of moving) item.style.transition = '';\n    }, DROP_MS);\n  }, [flipTick, readItems]);\n\n  useEffect(\n    () => () => {\n      if (flipTimer.current) clearTimeout(flipTimer.current);\n    },\n    [],\n  );\n\n  const context = useMemo<SortableListContextValue>(\n    () => ({ dragType, disabled, dragAndDropEnabled, reorder, readItems }),\n    [dragType, disabled, dragAndDropEnabled, reorder, readItems],\n  );\n\n  return (\n    <SortableListContext.Provider value={context}>\n      <div ref={listRef} className={className}>\n        {children}\n        {/* Absolutely positioned, so it is not a flex item and adds no gap. */}\n        <p aria-live=\"polite\" className=\"sr-only\">\n          {announcement}\n        </p>\n      </div>\n    </SortableListContext.Provider>\n  );\n}\n","'use client';\n\nimport { useRef } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { Arrow01DownIcon } from '../../icons-v2-generated/arrows/arrow-01-down-icon';\nimport { Arrow01UpIcon } from '../../icons-v2-generated/arrows/arrow-01-up-icon';\nimport { Button } from '../../ui/button';\nimport { useSortableListContext } from './sortable-list';\n\nexport interface SortableMoveButtonsProps {\n  /** The row's current position — the caller renders the rows, so it knows. */\n  index: number;\n  /** How many rows the list has; the down button is disabled on the last one. */\n  count: number;\n  /** Names the row in the buttons' accessible labels, e.g. \"On Hold\". */\n  label?: string;\n  className?: string;\n}\n\n/**\n * The touch replacement for a drag handle: an up/down pair rendered by a row of\n * a {@link SortableList} when `dragAndDropEnabled` is `false` (see\n * `useSortableItem`). Explicit buttons because on touch nothing else survives\n * contact with the page: a drag fights scrolling, long-press fights the context\n * menu, and the HTML5 drag events behind Pragmatic never fire at all.\n *\n * Moves go through the list's `reorder`, so they hit the same `onReorder`\n * callback and the same live region as a drop or a handle's arrow keys.\n *\n * One joined pair per the design: outer corners rounded, a single hairline\n * between the halves, each half disabled at its end of the list.\n */\nexport function SortableMoveButtons({ index, count, label, className }: SortableMoveButtonsProps) {\n  const { disabled, reorder } = useSortableListContext();\n  const upRef = useRef<HTMLButtonElement>(null);\n  const downRef = useRef<HTMLButtonElement>(null);\n\n  const move = (step: -1 | 1) => {\n    const to = index + step;\n    if (to < 0 || to >= count) return;\n    reorder(index, to);\n    // The row this pair sits in keeps its DOM nodes, but the pressed half may\n    // come back disabled (the row reached an end) — hand focus to the other\n    // half so a keyboard user is not dropped to the page body.\n    requestAnimationFrame(() => {\n      if (step === -1 && to === 0) downRef.current?.focus();\n      if (step === 1 && to === count - 1) upRef.current?.focus();\n    });\n  };\n\n  const subject = label ?? 'item';\n\n  return (\n    <div className={cn('inline-flex shrink-0', className)}>\n      <Button\n        ref={upRef}\n        type=\"button\"\n        variant=\"outline\"\n        size=\"icon\"\n        aria-label={`Move ${subject} up`}\n        disabled={disabled || index <= 0}\n        onClick={() => move(-1)}\n        className=\"rounded-r-none border-r-0\"\n      >\n        <Arrow01UpIcon />\n      </Button>\n      <Button\n        ref={downRef}\n        type=\"button\"\n        variant=\"outline\"\n        size=\"icon\"\n        aria-label={`Move ${subject} down`}\n        disabled={disabled || index >= count - 1}\n        onClick={() => move(1)}\n        className=\"rounded-l-none\"\n      >\n        <Arrow01DownIcon />\n      </Button>\n    </div>\n  );\n}\n","'use client';\n\nimport { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';\nimport { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';\nimport { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';\nimport { type KeyboardEvent, useCallback, useEffect, useState } from 'react';\nimport { holdMoveDragEffect } from '../../../utils/drag-effect';\nimport { SORTABLE_ITEM_ATTRIBUTE, useSortableListContext } from './sortable-list';\n\nexport interface SortableItem {\n  /** Goes on the item's root element — the thing that moves. */\n  itemRef: (node: HTMLElement | null) => void;\n  /**\n   * Goes on the grip. Only the grip starts a drag, so text selection and normal\n   * clicking inside the item keep working, and it carries the keyboard\n   * alternative — so spread it rather than picking it apart.\n   */\n  dragHandleProps: SortableDragHandleProps;\n  isDragging: boolean;\n  /**\n   * The input-type gate: `false` on touch/narrow viewports (and during SSR).\n   * When `false` the item registers no drag at all — render explicit move\n   * controls (`SortableMoveButtons`) instead of the drag handle.\n   */\n  dragAndDropEnabled: boolean;\n}\n\n/** What `dragHandleProps` carries — spread it onto the grip, don't pick it apart. */\nexport interface SortableDragHandleProps {\n  // Callback refs, not `Ref<HTMLElement>`: a ref OBJECT of the wider type\n  // cannot be attached to a `<button>` or a `<div>`, a callback taking the\n  // wider type can.\n  ref: (node: HTMLElement | null) => void;\n  onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;\n}\n\n/**\n * Registers one item of a {@link SortableList}.\n *\n * The item's own element is the draggable, and the list finds it by the marker\n * this hook sets — so nothing about a row's identity has to survive a round trip\n * through the DOM, and there is no per-item id to mismatch during hydration.\n *\n * Pragmatic drag and drop deliberately ships no keyboard dragging, so the grip\n * carries its own: Arrow Up / Arrow Down move the item one place. One press, one\n * move — no lift-and-place mode to get stuck in, and it works identically for\n * screen-reader users, who hear the result from the list's live region.\n */\nexport function useSortableItem(): SortableItem {\n  const { dragType, disabled, dragAndDropEnabled, reorder, readItems } = useSortableListContext();\n  // Both stop the drag registration and the keyboard path; only the consumer's\n  // `disabled` also stops the move buttons, which are the touch replacement.\n  const inert = disabled || !dragAndDropEnabled;\n  // State rather than refs: registration has to wait until the consumer has\n  // actually rendered its grip, and only a re-render tells us it did.\n  const [item, setItem] = useState<HTMLElement | null>(null);\n  const [handle, setHandle] = useState<HTMLElement | null>(null);\n  const [isDragging, setIsDragging] = useState(false);\n\n  const itemRef = useCallback((node: HTMLElement | null) => {\n    // Set here rather than rendered as a prop: it is this module's private\n    // bookkeeping, and a consumer forgetting to spread it would break the list\n    // silently.\n    node?.setAttribute(SORTABLE_ITEM_ATTRIBUTE, '');\n    setItem(node);\n  }, []);\n\n  useEffect(() => {\n    if (!item || !handle || inert) return undefined;\n    return combine(\n      // The drag has to start as a \"move\", or its first frame is drawn with the\n      // copy cursor — see `drag-effect.ts`.\n      holdMoveDragEffect(item),\n      draggable({\n        element: item,\n        dragHandle: handle,\n        getInitialData: () => ({ type: dragType }),\n        // The list moves the real item instead — see `SortableList`.\n        onGenerateDragPreview: ({ nativeSetDragImage }) => disableNativeDragPreview({ nativeSetDragImage }),\n        onDragStart: () => setIsDragging(true),\n        onDrop: () => setIsDragging(false),\n      }),\n    );\n  }, [item, handle, inert, dragType]);\n\n  const onKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLElement>) => {\n      const step = event.key === 'ArrowUp' ? -1 : event.key === 'ArrowDown' ? 1 : 0;\n      if (!step || inert) return;\n      const items = readItems();\n      const from = items.indexOf(event.currentTarget.closest(`[${SORTABLE_ITEM_ATTRIBUTE}]`) as HTMLElement);\n      const to = from + step;\n      if (from < 0 || to < 0 || to >= items.length) return;\n      event.preventDefault();\n      reorder(from, to);\n      // The grip is re-rendered at its new position; keep the focus on it so a\n      // run of presses keeps moving the same item.\n      requestAnimationFrame(() => handle?.focus());\n    },\n    [inert, readItems, reorder, handle],\n  );\n\n  return { itemRef, dragHandleProps: { ref: setHandle, onKeyDown }, isDragging, dragAndDropEnabled };\n}\n","'use client';\n\nimport type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { SortableList } from '../sortable-list/sortable-list';\nimport { SortableMoveButtons } from '../sortable-list/sortable-move-buttons';\nimport { type SortableDragHandleProps, useSortableItem } from '../sortable-list/use-sortable-item';\n\nexport interface SortableRowRenderArgs {\n  /** Spread onto the grip whole — it carries the ref and the keyboard path. */\n  dragHandleProps: SortableDragHandleProps;\n  isDragging: boolean;\n  /**\n   * The touch replacement for the grip: an up/down `SortableMoveButtons` pair,\n   * non-null exactly when drag is unavailable (touch/narrow viewports). Render\n   * it where the row's reorder control belongs — and no grip beside it.\n   */\n  moveButtons: ReactNode | null;\n}\n\nexport interface TicketStatusConfigListProps<T extends { id: string }> {\n  items: T[];\n  onReorder: (oldIndex: number, newIndex: number) => void;\n  renderRow: (item: T, args: SortableRowRenderArgs) => ReactNode;\n  /** Names a row for the live region and the move buttons, e.g. its status name. */\n  getItemLabel?: (index: number) => string | undefined;\n  className?: string;\n}\n\n/**\n * A reorderable list of config rows (ticket statuses, AI quick actions), built\n * on {@link SortableList} — Pragmatic drag and drop on desktop, an explicit\n * up/down pair on touch, where the HTML5 drag events never fire.\n *\n * `id` keys React rows only; the reorder itself is DOM-order based, so\n * duplicates in `items` cannot collide.\n */\nexport function TicketStatusConfigList<T extends { id: string }>({\n  items,\n  onReorder,\n  renderRow,\n  getItemLabel,\n  className,\n}: TicketStatusConfigListProps<T>) {\n  return (\n    <SortableList\n      onReorder={onReorder}\n      getItemLabel={getItemLabel}\n      className={cn('flex w-full flex-col gap-[var(--spacing-system-xs)]', className)}\n    >\n      {items.map((item, index) => (\n        <SortableRow key={item.id} index={index} count={items.length} label={getItemLabel?.(index)}>\n          {args => renderRow(item, args)}\n        </SortableRow>\n      ))}\n    </SortableList>\n  );\n}\n\nfunction SortableRow({\n  index,\n  count,\n  label,\n  children,\n}: {\n  index: number;\n  count: number;\n  label?: string;\n  children: (args: SortableRowRenderArgs) => ReactNode;\n}) {\n  const { itemRef, dragHandleProps, isDragging, dragAndDropEnabled } = useSortableItem();\n\n  return (\n    <div\n      ref={itemRef}\n      // Raise the actively dragged row above its siblings — without this the\n      // stacking follows DOM order, so a row dragged DOWN slides UNDER the rows\n      // below it while a row dragged up correctly renders on top.\n      className={isDragging ? 'relative z-10' : undefined}\n    >\n      {children({\n        dragHandleProps,\n        isDragging,\n        moveButtons: dragAndDropEnabled ? null : <SortableMoveButtons index={index} count={count} label={label} />,\n      })}\n    </div>\n  );\n}\n"]}