@syntrologie/adapt-product 2.41.0 → 2.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../node_modules/@lit/context/src/lib/create-context.ts", "../../../sdk-contracts/dist/canvas-context.js", "../../../sdk-contracts/dist/dive-deeper.js", "../../../sdk-contracts/dist/mount-plumbing.js", "../../../sdk-contracts/dist/routes.js", "../../../sdk-contracts/dist/schemas.js", "../src/presets-store.ts", "../src/scoring/pool-selection.ts", "../src/widgets/SyntroPdpLit.ts", "../src/decision/pdp.ts", "../src/decision/pdp-compose.ts", "../src/decision/pdp-effects.ts", "../src/native/nativeSlotSwap.ts", "../src/scoring/scored-template.ts", "../src/takeover/resurrectionGuard.ts", "../src/takeover/sectionDetach.ts", "../src/takeover/engine.ts", "../src/takeover/fetchPlan.ts", "../src/takeover/regionCurtain.ts", "../src/takeover/ports.ts", "../src/widgets/PdpSectionHeaderLit.ts", "../src/widgets/PdpSlotLit.ts", "../src/takeover/renderModules.ts", "../src/widgets/PdpOtherProductsCarouselLit.ts", "../src/widgets/OtherProductTileLit.ts", "../src/widgets/PdpOtherProductsGridLit.ts", "../src/native/nativeSlotWatcher.ts", "../src/widgets/ProductCardLit.ts", "../src/bind/dom-selector.ts", "../src/bind/jsonld.ts", "../src/bind/opengraph.ts", "../src/bind/shopify-ajax.ts", "../src/bind/woocommerce-store-api.ts", "../src/bind/index.ts", "../src/bind/controller.ts", "../src/decision/card.ts", "../src/variantCache.ts", "../src/decision/card-effects.ts", "../src/sanitizer.ts", "../src/widgets/ensureStylesInRoot.ts", "../src/widgets/renderHealth.ts", "../src/widgets/renderHealthReporter.ts", "../src/widgets/VariantPanelLit.ts", "../src/widgets/ProductComparisonLit.ts", "../src/widgets/ProductGridLit.ts", "../src/widgets/ProductHeroLit.ts", "../src/widgets/ProductRecoCarouselLit.ts", "../src/widgets/PdpTrendingNewsLit.ts", "../src/widgets/entrance.ts", "../src/widgets/PdpTrendingNewsMagazineLit.ts", "../src/widgets/PdpTrendingNewsHeroLit.ts", "../src/widgets/PdpPeerFeedMasonryLit.ts", "../src/widgets/PdpPeerFeedCarouselLit.ts", "../src/widgets/PdpChartLit.ts", "../src/widgets/PdpChartBarStackedLit.ts", "../src/widgets/PdpChartMetricPullLit.ts", "../src/widgets/PdpQaAccordionLit.ts", "../src/widgets/PdpQaSideBySideLit.ts", "../src/widgets/PdpRegionalFullBleedLit.ts", "../src/widgets/PdpRegionalInlineNoteLit.ts", "../src/widgets/PdpSlotPlaceholderLit.ts", "../src/widgets/PdpRibbonLit.ts", "../src/widgets/PdpQaLit.ts", "../src/widgets/PdpRegionalLit.ts", "../src/widgets/PdpPeerFeedLit.ts", "../src/runtime.ts"],
4
- "sourcesContent": ["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\n\n/**\n * The Context type defines a type brand to associate a key value with the context value type\n */\nexport type Context<KeyType, ValueType> = KeyType & {__context__: ValueType};\n\n/**\n * @deprecated use Context instead\n */\nexport type ContextKey<KeyType, ValueType> = Context<KeyType, ValueType>;\n\n/**\n * A helper type which can extract a Context value type from a Context type\n */\nexport type ContextType<Key extends Context<unknown, unknown>> =\n Key extends Context<unknown, infer ValueType> ? ValueType : never;\n\n/**\n * Creates a typed Context.\n *\n * Contexts are compared with strict equality.\n *\n * If you want two separate `createContext()` calls to referer to the same\n * context, then use a key that will by equal under strict equality like a\n * string for `Symbol.for()`:\n *\n * ```ts\n * // true\n * createContext('my-context') === createContext('my-context')\n * // true\n * createContext(Symbol.for('my-context')) === createContext(Symbol.for('my-context'))\n * ```\n *\n * If you want a context to be unique so that it's guaranteed to not collide\n * with other contexts, use a key that's unique under strict equality, like\n * a `Symbol()` or object.:\n *\n * ```\n * // false\n * createContext({}) === createContext({})\n * // false\n * createContext(Symbol('my-context')) === createContext(Symbol('my-context'))\n * ```\n *\n * @param key a context key value\n * @template ValueType the type of value that can be provided by this context.\n * @returns the context key value cast to `Context<K, ValueType>`\n */\nexport function createContext<ValueType, K = unknown>(key: K) {\n return key as Context<K, ValueType>;\n}\n", "/**\n * Canvas runtime context \u2014 the shared @lit/context symbol both\n * runtime-sdk (the provider) and canvas-sdk / canvas authors (the\n * consumers) use to thread a narrow runtime handle through the canvas\n * element tree.\n *\n * Living here keeps the symbol identity stable across both packages.\n * If canvas-sdk created its own symbol with `createContext(...)`, it\n * would never match the one runtime-sdk publishes, and `<sc-mount>`\n * would silently see `undefined` instead of the widget registry.\n *\n * The shape declared here is a NARROW VIEW of `SmartCanvasRuntime`.\n * Canvas-side code reads only this subset. The runtime-sdk's\n * `SmartCanvasRuntime` type is a structural superset.\n */\nimport { createContext } from '@lit/context';\n/**\n * The @lit/context symbol. Both runtime-sdk's ContextProvider and\n * canvas-sdk's ContextConsumer must import THIS exact symbol \u2014 not a\n * symbol with the same string name \u2014 for context propagation to work.\n */\nexport const canvasRuntimeContext = createContext('syntrologie:canvas-runtime');\n", "/**\n * \"Dive deeper\" contract \u2014 the ONE canonical way a space-constrained tile\n * (FAQ, comparison, \u2026) hands its content context back to the chat conversation\n * as a deep-dive turn.\n *\n * A tile that can only show a shallow slice of its content (a comparison matrix\n * capped to a few rows, an FAQ answer clamped to a few lines) renders a \"Dive\n * deeper\" affordance. Tapping it dispatches a `window` CustomEvent carrying the\n * tile's content summary; the chat-bar mountable listens for exactly this event\n * and turns it into a user-visible chat turn so the agent can help \u2014 no\n * re-scrolling the cramped tile.\n *\n * THE PROMPT STATES AN UNMET NEED. IT NEVER ASKS A QUESTION (BUG-1784153527).\n * Emitters used to author `Tell me more \u2014 how do A vs B compare?` / `Tell me\n * more about: <question>` \u2014 direct imperatives, in the visitor's own voice. The\n * agent dutifully answered them and never asked what was actually missing,\n * which is catastrophic exactly when the tile is the problem: a live\n * \"VelaLift Sports Bra vs RunCloud Quarter Sock Pack\" comparison (two products\n * with no decision between them) got a straight-faced answer that had to\n * conclude \"the real question isn't which is better\u2026\".\n *\n * A question demands an answer; a complaint invites a question. The tap means\n * \"this tile didn't land\", so the turn says only that \u2014 the specifics (product\n * names, the question text, what's on screen) ride in `title` / `context` and\n * the turn-origin envelope, where they ground the agent WITHOUT putting an\n * answerable question in the visitor's mouth.\n *\n * This lives in the neutral `@syntrologie/sdk-contracts` (not in any single\n * adaptive) so BOTH the emitters (adaptive-product comparison, adaptive-faq)\n * and the listener (adaptive-chatbot chat bar) share one wire shape without\n * depending on each other. The event is canvas-agnostic \u2014 it never references a\n * deck, a slot, or a specific host canvas.\n */\n/** The `window` CustomEvent name carrying a {@link DiveDeeperDetail}. */\nexport const DIVE_DEEPER_EVENT = 'syntro:chat:dive-deeper';\n/**\n * Dispatch a {@link DIVE_DEEPER_EVENT} on `window`. Safe in non-DOM contexts\n * (SSR / tests without a window) \u2014 it no-ops. Emitters call this; they never\n * reach into the chat session directly, keeping the adaptive side decoupled\n * from the chat implementation.\n */\nexport function dispatchDiveDeeper(detail) {\n if (typeof window === 'undefined')\n return;\n window.dispatchEvent(new CustomEvent(DIVE_DEEPER_EVENT, { detail }));\n}\n", "/**\n * Mount contract types and helper for adaptive widget mountables.\n *\n * The `WidgetRegistry` in `@syntrologie/runtime-sdk` delivers props to each\n * mountable as `{ ...tile.props, instanceId, runtime, tileId? }` spread flat\n * (see `MountableContract.test.ts` in runtime-sdk for the end-to-end lockdown).\n *\n * Adaptives that strip plumbing manually maintain private blacklists that\n * silently drift when the contract grows (PR #2234 and #2238 documented this).\n * `stripMountPlumbing` centralizes the list so adding a new plumbing key in\n * the future is a one-line change here that every adaptive picks up automatically.\n *\n * Adaptives whose widget schemas use Zod `.strict()` MUST call this before\n * validating, or strict-mode will reject the runtime-injected keys and the\n * widget will silently render its empty/error state.\n */\nexport const MOUNT_PLUMBING_KEYS = ['instanceId', 'runtime', 'tileId'];\nexport function stripMountPlumbing(config) {\n if (!config || typeof config !== 'object') {\n return {};\n }\n const out = { ...config };\n for (const key of MOUNT_PLUMBING_KEYS) {\n delete out[key];\n }\n return out;\n}\n", "/**\n * Canonical route normalization. See `routes.md` for rules and\n * `normalize-route.cases.json` for the parity corpus shared with the\n * Python implementation in syntrologie_common/sdk/routing.py.\n *\n * Two exports \u2014 `normalizeRoute` for literal paths, `normalizeRoutePattern`\n * for activation patterns containing `*`, `**`, `:param`. Today they share\n * an implementation because the rules happen to be wildcard-safe (no\n * lowercase, unreserved-only decode, slash collapse preserves `**`).\n * The seam is preserved as separate exports so the API can diverge\n * without consumer churn if rules change.\n */\n// RFC 3986 reserved characters (gen-delims + sub-delims). When a `%XX`\n// sequence decodes to one of these bytes, we keep the percent-encoded\n// form \u2014 decoding would re-segment the path or change its meaning.\nconst RESERVED_BYTES = new Set([\n 0x21, // !\n 0x23, // #\n 0x24, // $\n 0x26, // &\n 0x27, // '\n 0x28, // (\n 0x29, // )\n 0x2a, // *\n 0x2b, // +\n 0x2c, // ,\n 0x2f, // /\n 0x3a, // :\n 0x3b, // ;\n 0x3d, // =\n 0x3f, // ?\n 0x40, // @\n 0x5b, // [\n 0x5d, // ]\n]);\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: false });\n/** Decode `%XX` sequences for unreserved bytes only. Collapses\n * adjacent `%XX` runs into a UTF-8 decode so `%C3%A9` \u2192 `\u00E9`. */\nfunction decodeUnreservedOnly(input) {\n let out = '';\n let pending = [];\n const flushPending = () => {\n if (pending.length === 0)\n return;\n const bytes = new Uint8Array(pending);\n out += utf8Decoder.decode(bytes);\n pending = [];\n };\n let i = 0;\n while (i < input.length) {\n const ch = input[i];\n if (ch === '%' && i + 2 < input.length && isHex(input[i + 1]) && isHex(input[i + 2])) {\n const byte = parseInt(input.slice(i + 1, i + 3), 16);\n if (RESERVED_BYTES.has(byte)) {\n flushPending();\n // Keep raw, but normalize hex case to uppercase so the\n // canonical form is stable across input casing.\n out += `%${input.slice(i + 1, i + 3).toUpperCase()}`;\n i += 3;\n }\n else {\n pending.push(byte);\n i += 3;\n }\n }\n else {\n flushPending();\n out += ch;\n i += 1;\n }\n }\n flushPending();\n return out;\n}\nfunction isHex(c) {\n return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');\n}\n/** Strip query string and hash fragment. */\nfunction stripQueryAndHash(s) {\n const q = s.indexOf('?');\n if (q !== -1)\n s = s.slice(0, q);\n const h = s.indexOf('#');\n if (h !== -1)\n s = s.slice(0, h);\n return s;\n}\n/**\n * Normalize a literal path (e.g. `window.location.pathname`, an\n * action's `route` field, a wiki route key).\n *\n * Throws `TypeError` if the input is not an absolute path. Callers\n * that want a soft API should use {@link normalizeRouteWithChange}.\n */\nexport function normalizeRoute(path) {\n if (typeof path !== 'string' || path.length === 0) {\n throw new TypeError('normalizeRoute: input must be a non-empty string');\n }\n if (!path.startsWith('/')) {\n throw new TypeError(`normalizeRoute: input must be absolute (start with '/'); got ${JSON.stringify(path)}`);\n }\n let s = stripQueryAndHash(path);\n s = decodeUnreservedOnly(s);\n s = s.replace(/\\/+/g, '/');\n if (s.length > 1 && s.endsWith('/'))\n s = s.slice(0, -1);\n return s;\n}\n/**\n * Normalize an activation route pattern. Preserves `*`, `**`,\n * `:param` exactly. Today equivalent to {@link normalizeRoute} \u2014 kept\n * as a separate export so rules can diverge later without API churn.\n */\nexport function normalizeRoutePattern(pattern) {\n return normalizeRoute(pattern);\n}\n/**\n * Normalize a route and report whether the input was already\n * canonical. Used by authoring tools to decide whether to emit a\n * warning to the LLM.\n */\nexport function normalizeRouteWithChange(path) {\n const canonical = normalizeRoute(path);\n return { canonical, changed: canonical !== path };\n}\n/** Pattern-side counterpart of {@link normalizeRouteWithChange}. */\nexport function normalizeRoutePatternWithChange(pattern) {\n const canonical = normalizeRoutePattern(pattern);\n return { canonical, changed: canonical !== pattern };\n}\n/**\n * Case-insensitive comparison of two already-canonical paths. Use\n * this anywhere two routes are compared for equality (wiki lookups,\n * non-pattern action route gates) \u2014 preserves casing in the inputs\n * while honoring case-insensitive routing on the customer's site.\n */\nexport function routesMatch(a, b) {\n return a.toLowerCase() === b.toLowerCase();\n}\n", "/**\n * Shared Zod schemas for decision strategies, conditions, and event scoping.\n *\n * These are the canonical definitions \u2014 runtime-sdk and all adaptive packages\n * should import from here instead of duplicating.\n */\nimport { z } from 'zod';\n// =============================================================================\n// ANCHOR ID SCHEMA\n// =============================================================================\nexport const AnchorIdZ = z\n .object({\n selector: z.string(),\n route: z.union([z.string(), z.array(z.string())]),\n})\n .strict();\n// =============================================================================\n// AUTHORING FIELDS \u2014 id / title / description / validation\n//\n// Shared fields every action carries. `id` is the action identifier the\n// runtime uses to dispatch, dedupe, and drop/replace actions \u2014 it is NOT\n// stripped before serving. `title` / `description` / `validation` are\n// authoring-only metadata stripped server-side in `to_runtime_config`\n// (platform/backend/app/domains/experiments/helpers.py).\n//\n// They all appear in the JSON Schema (and therefore in the tactician's\n// prompt) because the LLM needs to know they are valid action properties \u2014\n// otherwise schema validation would reject what the prompt commands.\n//\n// Each action variant should `.extend(AuthoringFieldsZ)` alongside any\n// triggerWhen/condition extensions.\n// =============================================================================\nexport const AuthoringFieldsZ = {\n id: z.string().optional().describe('Stable action identifier (e.g. \"act_3db6a14d2ab0\").'),\n title: z\n .string()\n .max(200)\n .optional()\n .describe('Authoring-only: short label shown on the action plan dashboard. Stripped before serving to the runtime SDK.'),\n description: z\n .string()\n .max(1000)\n .optional()\n .describe('Authoring-only: one-sentence explanation of what this action does and why. Stripped before serving to the runtime SDK.'),\n validation: z\n .array(z.string().max(500))\n .max(10)\n .optional()\n .describe('Authoring-only: ordered steps a reviewer can follow to trigger this action and visually confirm it works. Each entry is one step. Stripped before serving to the runtime SDK.'),\n};\n// =============================================================================\n// TRIGGER VOCABULARY \u2014 canonical lists of valid event names, metric keys, etc.\n// These flow through to the JSON schema as enums and are used by the LLM prompt.\n// =============================================================================\n/** Events that can be counted in event_count conditions.\n *\n * Every value here must be an event the runtime actually emits \u2014 either a\n * PostHog-autocapture normalization (ui.click/scroll/input/change/submit) or\n * an event-processor detector (ui.hover/idle/scroll_thrash/focus_bounce/\n * hesitate/rage_click). Do not add aspirational names; a trigger counting an\n * event nothing emits never fires.\n */\nexport const COUNTABLE_EVENTS = [\n // User interactions (from PostHog autocapture normalization)\n 'ui.click',\n 'ui.scroll',\n 'ui.input',\n 'ui.change',\n 'ui.submit',\n // Behavioral detectors (from event-processor)\n 'ui.hover',\n 'ui.idle',\n 'ui.scroll_thrash',\n 'ui.focus_bounce',\n 'ui.hesitate',\n 'ui.rage_click',\n // Navigation\n 'nav.page_view',\n 'nav.page_leave',\n];\nexport const CountableEventZ = z\n .enum(COUNTABLE_EVENTS)\n .describe('Event name to count. ui.* = user interactions and behavioral detectors (hesitate, rage_click, scroll_thrash, focus_bounce, idle, hover); nav.* = page navigation.');\n/** Valid session metric keys. */\nexport const SESSION_METRIC_KEYS = ['time_on_page', 'page_views', 'scroll_depth'];\nexport const SessionMetricKeyZ = z\n .enum(SESSION_METRIC_KEYS)\n .describe('Session metric key. time_on_page = seconds on current page, page_views = pages visited this session, scroll_depth = 0-100 percentage.');\n/** Element chain match field prefixes for counter filters. */\nexport const ELEMENT_MATCH_FIELDS = ['tag_name', '$el_text'];\n// Note: attr__* is a dynamic prefix (attr__data-id, attr__class, attr__href, etc.)\n// and cannot be enumerated. The match key is either one of ELEMENT_MATCH_FIELDS\n// or starts with \"attr__\".\n// =============================================================================\n// CONDITION SCHEMAS\n// =============================================================================\nexport const PageUrlConditionZ = z\n .object({\n type: z.literal('page_url'),\n url: z.string().describe('URL path to match (e.g. \"/pricing\", \"/dashboard\")'),\n})\n .describe('Fires when the current page URL matches. Use for page-specific actions. ' +\n 'Example: {\"type\": \"page_url\", \"url\": \"/pricing\"}');\nexport const RouteConditionZ = z\n .object({\n type: z.literal('route'),\n routeId: z.string().describe('Named route ID from the route filter'),\n})\n .describe('Fires when the current route matches a named route ID.');\nexport const AnchorVisibleConditionZ = z\n .object({\n type: z.literal('anchor_visible'),\n anchorId: z.string().describe('CSS selector of the anchor element'),\n state: z\n .enum(['visible', 'present', 'absent'])\n .describe('\"visible\" = in viewport, \"present\" = in DOM, \"absent\" = not in DOM'),\n})\n .describe(\"Fires based on a DOM element's visibility state. \" +\n 'Example: {\"type\": \"anchor_visible\", \"anchorId\": \"#cta-button\", \"state\": \"visible\"}');\nexport const EventOccurredConditionZ = z\n .object({\n type: z.literal('event_occurred'),\n eventName: z.string().describe('Event name (e.g. \"ui.click\", \"$pageview\")'),\n withinMs: z.number().optional().describe('Time window in ms. Omit = any time this session.'),\n})\n .describe('Fires when a specific event has occurred during this session. ' +\n 'Example: {\"type\": \"event_occurred\", \"eventName\": \"ui.click\", \"withinMs\": 5000}');\nexport const StateEqualsConditionZ = z\n .object({\n type: z.literal('state_equals'),\n key: z\n .string()\n .describe('Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set().'),\n value: z.unknown().describe('Expected value to match against'),\n})\n .describe('Checks the SDK persistent state store (localStorage). ONLY for host-app state set via syntro.state.set() \u2014 ' +\n 'NOT for user attributes like region, device, or UTM params (those are handled by segment targeting). ' +\n 'Do NOT use this for targeting. If you do not know the valid state keys, do not use this condition type.');\nexport const ViewportConditionZ = z\n .object({\n type: z.literal('viewport'),\n minWidth: z.number().optional().describe('Minimum viewport width in pixels'),\n maxWidth: z.number().optional().describe('Maximum viewport width in pixels'),\n minHeight: z.number().optional().describe('Minimum viewport height in pixels'),\n maxHeight: z.number().optional().describe('Maximum viewport height in pixels'),\n})\n .describe('Fires based on viewport (screen) size. Use for responsive behavior. ' +\n 'Example: {\"type\": \"viewport\", \"minWidth\": 768} \u2014 fires on tablet and larger.');\nexport const SessionMetricConditionZ = z\n .object({\n type: z.literal('session_metric'),\n key: SessionMetricKeyZ,\n operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),\n threshold: z.number().describe('Numeric threshold to compare against'),\n})\n .describe('Fires when a session metric crosses a threshold. Valid keys: \"time_on_page\" (seconds), ' +\n '\"page_views\" (count), \"scroll_depth\" (0-100). ' +\n 'Example: {\"type\": \"session_metric\", \"key\": \"time_on_page\", \"operator\": \"gte\", \"threshold\": 30}');\nexport const DismissedConditionZ = z\n .object({\n type: z.literal('dismissed'),\n key: z.string().describe('Dismissal key (usually a tile or action ID)'),\n inverted: z\n .boolean()\n .optional()\n .describe('When true, fires if NOT dismissed (default behavior)'),\n})\n .describe('Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.');\nexport const CooldownActiveConditionZ = z\n .object({\n type: z.literal('cooldown_active'),\n key: z.string().describe('Cooldown key'),\n inverted: z.boolean().optional().describe('When true, fires if cooldown is NOT active'),\n})\n .describe('Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.');\nexport const FrequencyLimitConditionZ = z\n .object({\n type: z.literal('frequency_limit'),\n key: z.string().describe('Frequency counter key'),\n limit: z.number().describe('Maximum allowed count'),\n inverted: z.boolean().optional().describe('When true, fires if limit NOT reached'),\n})\n .describe('Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.');\nexport const MatchOpZ = z\n .object({\n equals: z.union([z.string(), z.number(), z.boolean()]).optional(),\n contains: z.string().optional(),\n})\n .refine((operator) => Number(operator.equals !== undefined) + Number(operator.contains !== undefined) === 1, {\n message: 'Exactly one of equals or contains must be specified.',\n})\n .describe('Match operator for counter filters. Exactly one of equals or contains must be specified.');\nexport const CounterDefZ = z\n .object({\n events: z\n .array(CountableEventZ)\n .min(1)\n .describe('Event names to count. Use values from the countable events enum.'),\n match: z\n .record(z.string(), MatchOpZ)\n .optional()\n .describe('Property filters. Keys are event prop names or element-chain fields ' +\n '(tag_name, $el_text, attr__*). All entries AND together.'),\n})\n .describe('Defines what events to count. Registered as an accumulator predicate at config-load time.');\nexport const EventCountConditionZ = z\n .object({\n type: z.literal('event_count'),\n key: z.string().describe('Unique key for this counter (used for accumulator registration)'),\n operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),\n count: z.number().int().min(0).describe('Target count threshold'),\n withinMs: z\n .number()\n .positive()\n .optional()\n .describe('Time window in ms. Omit = count across entire session.'),\n counter: CounterDefZ.optional().describe('Inline counter definition. Defines what events to count.'),\n})\n .describe('Fires when accumulated event count crosses a threshold. Most powerful trigger type. ' +\n 'Example: {\"type\": \"event_count\", \"key\": \"pricing-clicks\", \"operator\": \"gte\", \"count\": 3, ' +\n '\"counter\": {\"events\": [\"ui.click\"], \"match\": {\"attr__data-cta\": {\"contains\": \"pricing\"}}}}');\nexport const ConditionZ = z.discriminatedUnion('type', [\n PageUrlConditionZ,\n RouteConditionZ,\n AnchorVisibleConditionZ,\n EventOccurredConditionZ,\n StateEqualsConditionZ,\n ViewportConditionZ,\n SessionMetricConditionZ,\n DismissedConditionZ,\n CooldownActiveConditionZ,\n FrequencyLimitConditionZ,\n EventCountConditionZ,\n]);\n// =============================================================================\n// STRATEGY SCHEMAS\n// =============================================================================\nexport const RuleZ = z\n .object({\n conditions: z\n .array(ConditionZ)\n .describe('Array of conditions \u2014 ALL must match (AND logic) for this rule to fire.'),\n value: z\n .unknown()\n .describe('Value returned when all conditions match. For triggerWhen: true = fire the action.'),\n})\n .describe('A single rule. ALL conditions must match (AND logic). Rules in a strategy are evaluated ' +\n 'top-to-bottom \u2014 first rule where all conditions match wins and returns its value.');\nexport const RuleStrategyZ = z\n .object({\n type: z.literal('rules'),\n rules: z\n .array(RuleZ)\n .describe('Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins.'),\n default: z\n .unknown()\n .describe('Fallback value when no rule matches. For triggerWhen: false = do not fire by default.'),\n})\n .describe('Rule-based strategy. Evaluates rules top-to-bottom. First rule where ALL conditions match ' +\n 'returns its value. If no rule matches, returns default. ' +\n 'For triggerWhen: set value=true on matching rules, default=false.');\nexport const ScoreStrategyZ = z\n .object({\n type: z.literal('score'),\n field: z.string(),\n threshold: z.number(),\n above: z.unknown(),\n below: z.unknown(),\n})\n .describe('Score-based strategy. Compares a field value against a threshold.');\nexport const ModelStrategyZ = z\n .object({\n type: z.literal('model'),\n modelId: z.string(),\n inputs: z.array(z.string()),\n outputMapping: z.record(z.string(), z.unknown()),\n default: z.unknown(),\n})\n .describe('ML model strategy. Sends inputs to a model and maps outputs.');\nexport const ExternalStrategyZ = z\n .object({\n type: z.literal('external'),\n endpoint: z.string(),\n method: z.enum(['GET', 'POST']).optional(),\n default: z.unknown(),\n timeoutMs: z.number().optional(),\n})\n .describe('External API strategy. Calls an endpoint to determine the value.');\nexport const DecisionStrategyZ = z.discriminatedUnion('type', [\n RuleStrategyZ,\n ScoreStrategyZ,\n ModelStrategyZ,\n ExternalStrategyZ,\n]);\n/** Canonical Zod schema for the optional triggerWhen field on actions and adaptive items. */\nexport const TriggerWhenZ = DecisionStrategyZ.nullable().optional();\n// =============================================================================\n// TRIGGER DOCUMENTATION \u2014 examples and match field docs\n// Exported as constants so the schema generator can inject them into the\n// JSON schema. The Python prompt builder reads them from the schema.\n// =============================================================================\n/** Complete triggerWhen examples showing the full rules wrapper structure. */\nexport const TRIGGER_EXAMPLES = [\n {\n name: 'Click count on a specific element',\n description: 'Fire when user clicks an element with data-id=\"hero-cta\" 2+ times',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'event_count',\n key: 'cta-clicks',\n operator: 'gte',\n count: 2,\n counter: {\n events: ['ui.click'],\n match: { 'attr__data-id': { equals: 'hero-cta' } },\n },\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'Time on page threshold',\n description: 'Fire after user spends 30+ seconds on the page',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'session_metric',\n key: 'time_on_page',\n operator: 'gte',\n threshold: 30,\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'Element visible in viewport',\n description: 'Fire when a DOM element becomes visible',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'anchor_visible',\n anchorId: '#pricing-section',\n state: 'visible',\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'No trigger (fire immediately)',\n description: 'Action fires as soon as the segment matches \u2014 no in-session condition needed',\n triggerWhen: null,\n },\n];\n/** Documentation for counter.match field keys. */\nexport const MATCH_FIELD_DOCS = {\n tag_name: 'HTML tag name (e.g. \"button\", \"a\", \"input\")',\n $el_text: 'Visible text content of the element',\n 'attr__*': 'HTML attribute prefixed with attr__. Example: attr__data-id matches the data-id attribute, ' +\n 'attr__class matches the class attribute, attr__href matches the href attribute.',\n};\n// =============================================================================\n// EVENT SCOPE SCHEMA\n// =============================================================================\n/** Scopes a widget to specific events/URLs. */\nexport const EventScopeZ = z.object({\n events: z.array(z.string()),\n urlContains: z.string().optional(),\n props: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),\n});\n// =============================================================================\n// NOTIFY SCHEMA\n// =============================================================================\n/** Toast notification config for triggerWhen transitions. */\nexport const NotifyZ = z\n .object({\n title: z.string().optional(),\n body: z.string().optional(),\n icon: z.string().optional(),\n})\n .nullable()\n .optional();\n", "/**\n * PDP presets store (WI-4) \u2014 the ONE accessor for the per-product preset\n * table that `<syntro-pdp>` and the native slot watcher read.\n *\n * Two producers feed it:\n * 1. Config-driven delivery: when the SDK config carries `pdpPresetsUrl`,\n * the runtime-sdk bootstrap fetches the JSON and writes the well-known\n * window globals below (see runtime-sdk `fetchers/pdpPresetsFetcher.ts`).\n * Window is the channel BY DESIGN: adaptive-product ships as its own CDN\n * chunk, so module-scoped state here can never be set by the runtime\n * bundle.\n * 2. Legacy demo boot scripts (vela / healthmaxxer) that hand-set\n * `window.__SYNTRO_PDP_PRESETS__` before the SDK boots.\n *\n * Reads are LAZY \u2014 every call re-reads the global, so late assignment (a\n * slow bootstrap fetch, a demo script racing the element upgrade) is always\n * observed and absence is never cached. On fetch failure the store simply\n * stays empty and the PDP fails closed (renders nothing) \u2014 never a broken\n * box.\n */\n\n/** One config-set slot value: the widget kind, an optional header, its content. */\nexport interface PdpSlotPreset {\n kind: string;\n header?: unknown;\n data: unknown;\n}\n\n/** `presets[product][archetype][slot] = PdpSlotPreset` \u2014 the config's SET values. */\nexport type PdpPresetTable = Record<string, Record<string, Record<string, PdpSlotPreset>>>;\n\n/** Well-known globals \u2014 the cross-bundle contract with the runtime-sdk\n * bootstrap fetcher AND the legacy demo boot scripts. Do not rename. */\ninterface PresetsWindow {\n __SYNTRO_PDP_PRESETS__?: PdpPresetTable;\n /** Set by the bootstrap fetcher while a config-driven fetch is in flight. */\n __SYNTRO_PDP_PRESETS_PROMISE__?: Promise<unknown>;\n}\n\nfunction presetsWindow(): PresetsWindow | undefined {\n if (typeof window === 'undefined') return undefined;\n return window as unknown as PresetsWindow;\n}\n\n/** The current preset table, or undefined when none has been provided yet. */\nexport function getPdpPresets(): PdpPresetTable | undefined {\n return presetsWindow()?.__SYNTRO_PDP_PRESETS__;\n}\n\n/**\n * Store a preset table. Writes through to the window global so every bundle\n * (element chunk, native-watcher chunk, runtime bundle) sees the same table.\n */\nexport function setPdpPresets(table: PdpPresetTable): void {\n const win = presetsWindow();\n if (win) win.__SYNTRO_PDP_PRESETS__ = table;\n}\n\n/**\n * True when a config-driven presets fetch has been STARTED (its readiness\n * promise is on the window \u2014 in flight or settled). Lets synchronous-path\n * consumers skip the async gate entirely when no `pdpPresetsUrl` delivery is\n * configured, preserving their exact pre-WI-4 microtask timing.\n */\nexport function pdpPresetsFetchStarted(): boolean {\n const pending = presetsWindow()?.__SYNTRO_PDP_PRESETS_PROMISE__;\n return pending != null && typeof pending.then === 'function';\n}\n\n/**\n * Readiness gate for consumers that fire once (the element's compose, the\n * watcher's per-product decision): resolves when any in-flight config-driven\n * presets fetch has settled \u2014 immediately when none is in flight. NEVER\n * rejects; a failed fetch resolves with the store left empty (fail-closed).\n */\nexport function pdpPresetsReady(): Promise<void> {\n const pending = presetsWindow()?.__SYNTRO_PDP_PRESETS_PROMISE__;\n if (!pending || typeof pending.then !== 'function') return Promise.resolve();\n return Promise.resolve(pending).then(\n () => undefined,\n () => undefined\n );\n}\n", "/**\n * Mount-time pool selection \u2014 the client half of the two-axis model.\n *\n * Cached PDP presets carry a POOL of items, each tagged with the decision\n * archetype(s) its evidence serves (`archetypes: string[]`). At mount, the\n * scored decision archetype picks what surfaces:\n *\n * - SHARP label \u2192 items tagged with it lead, untagged (general) items fill,\n * then the rest. The module specializes without another generation call.\n * - UNKNOWN \u2192 the sprinkle: one item per distinct tag (round-robin, first-seen\n * tag order), so every blocking question gets its evidence somewhere on the\n * page, then untagged/remaining fill.\n *\n * Pure functions \u2014 no DOM, no network \u2014 so the selection rule is unit-testable\n * and identical across the qa / trending-news / peer-feed widgets.\n */\n\nexport const UNKNOWN_DECISION = 'unknown';\n\n/** Display caps per module kind \u2014 pools are bigger than what mounts. */\nexport const DISPLAY_MAX: Record<string, number> = {\n qa: 5,\n 'trending-news': 5,\n 'peer-feed': 4,\n};\n\nexport interface TaggedItem {\n archetypes?: string[];\n}\n\nfunction tagsOf(item: TaggedItem): string[] {\n return Array.isArray(item.archetypes) ? item.archetypes : [];\n}\n\n/**\n * Order a tagged pool for one visitor and cap it at `max`.\n * Never returns fewer than min(pool length, max) items \u2014 selection only\n * reorders and trims, it never empties a module.\n */\nexport function selectPoolItems<T extends TaggedItem>(\n items: T[],\n decision: string | undefined,\n max: number\n): T[] {\n if (items.length <= max) return [...items];\n\n const sharp = decision && decision !== UNKNOWN_DECISION;\n if (sharp) {\n const matched = items.filter((i) => tagsOf(i).includes(decision));\n const general = items.filter((i) => tagsOf(i).length === 0);\n const rest = items.filter((i) => !matched.includes(i) && !general.includes(i));\n return [...matched, ...general, ...rest].slice(0, max);\n }\n\n // Sprinkle: one item per distinct tag in first-seen order (an item covers\n // every tag it carries), then untagged, then whatever remains.\n const picked: T[] = [];\n const covered = new Set<string>();\n for (const item of items) {\n const tags = tagsOf(item);\n if (tags.length === 0) continue;\n if (tags.some((t) => covered.has(t))) continue;\n picked.push(item);\n for (const t of tags) covered.add(t);\n if (picked.length >= max) return picked.slice(0, max);\n }\n for (const item of items) {\n if (picked.length >= max) break;\n if (!picked.includes(item) && tagsOf(item).length === 0) picked.push(item);\n }\n for (const item of items) {\n if (picked.length >= max) break;\n if (!picked.includes(item)) picked.push(item);\n }\n return picked.slice(0, max);\n}\n\n/**\n * Apply pool selection to a cached slot preset's payload, by module kind.\n * Kinds without a pooled item list (chart / regional / other-products) and\n * malformed payloads pass through untouched.\n */\nexport function selectPresetData(\n kind: string,\n data: unknown,\n decision: string | undefined\n): unknown {\n const max = DISPLAY_MAX[kind];\n if (!max || typeof data !== 'object' || data === null) return data;\n const record = data as Record<string, unknown>;\n const listKey = kind === 'peer-feed' ? 'peers' : 'items';\n const list = record[listKey];\n if (!Array.isArray(list)) return data;\n return { ...record, [listKey]: selectPoolItems(list as TaggedItem[], decision, max) };\n}\n", "// Request/response types for the runtime-backend /api/pdp/* endpoints are\n// DERIVED from the generated contract (single source of truth) \u2014 a Pydantic\n// change on ModuleRequest/ModuleResponse/DossierResponse breaks this compile\n// instead of drifting. See docs/adr/2026-07-05-runtime-backend-generated-types.md.\nimport type {\n DossierMetric,\n DossierResponse,\n ModuleRequest,\n ModuleResponse,\n} from '@syntrologie/runtime-contracts';\nimport { html, LitElement, type PropertyValues, render } from 'lit';\nimport {\n createInitialPdpState,\n type PdpEvent,\n type PdpState,\n type PdpWhyBoxView,\n selectPdpView,\n transitionPdp,\n} from '../decision/pdp';\nimport {\n buildPdpPlan,\n createPdpComposeAdapter,\n orderPdpSlots,\n PDP_SLOT_IDS,\n type PdpPlanEnvelope,\n type PdpPlanTaskClient,\n} from '../decision/pdp-compose';\nimport { createPdpEffectExecutor, type PdpEffectRun } from '../decision/pdp-effects';\n// Native takeover (FEAT-1784346204): on pages that server-render their own FAQ /\n// review sections (the Task 3 `/pdp/dynamic` shape), the element swaps content\n// INTO those host sections instead of rendering its own duplicate ones.\nimport {\n findNativeSlot,\n NATIVE_KIND_BY_SLOT,\n type NativeSwapHandle,\n nativeFieldsFromContent,\n swapNativeSlot,\n} from '../native/nativeSlotSwap';\n// WI-4: presets come through the store accessor \u2014 populated by the\n// runtime-sdk bootstrap fetch (config `pdpPresetsUrl`) or by legacy demo\n// boot scripts; never read the window global directly here.\nimport { getPdpPresets, pdpPresetsReady } from '../presets-store';\n// Widget-props contract (unified schema `adaptive-product:pdp` entry).\nimport type { SyntroPdpProps } from '../schema';\n// The scored-template orchestration lives in the adaptive (not the runtime) \u2014 it's\n// the PDP's composition logic. The runtime stays thin (score + authedFetch).\nimport { selectPresetData } from '../scoring/pool-selection';\nimport { resolveScoredTemplate, type SlotPreset } from '../scoring/scored-template';\n// Page-takeover engine (FEAT-1785977524): when the widget props carry a\n// `takeover` config, the element becomes the takeover root and this engine \u2014\n// not the six-slot compose path \u2014 owns everything it renders.\nimport { runTakeover, type TakeoverResult } from '../takeover/engine';\nimport { createTakeoverPlanFetcher } from '../takeover/fetchPlan';\nimport { attachLifecycleDispose, createDomPorts } from '../takeover/ports';\nimport { createTakeoverModuleRenderer } from '../takeover/renderModules';\nimport type { PdpTakeoverConfig } from '../takeover/schema';\nimport './PdpSectionHeaderLit';\nimport './PdpOtherProductsCarouselLit.js';\nimport './PdpOtherProductsGridLit.js';\n\n// \u2500\u2500\u2500 visitor signal types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface VisitorAttribution {\n referrer: string;\n referring_domain: string;\n initial_url: string;\n initial_pathname: string;\n utm_source: string;\n utm_medium: string;\n utm_campaign: string;\n utm_content: string;\n utm_term: string;\n}\n\nexport interface VisitorGeo {\n country: string;\n country_code: string;\n region: string;\n city: string;\n is_eu: boolean;\n}\n\nexport interface VisitorDevice {\n user_agent: string;\n locale: string;\n viewport_w: number | null;\n viewport_h: number | null;\n screen_w: number | null;\n screen_h: number | null;\n dpr: number | null;\n is_touch: boolean;\n}\n\nexport interface VisitorSignals {\n attribution: VisitorAttribution;\n geo: VisitorGeo;\n device: VisitorDevice;\n session_metrics: Record<string, string | number | boolean>;\n extras: Record<string, string | number | boolean>;\n}\n\n// \u2500\u2500\u2500 module plan / mutation types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MountedInstance {\n instance_id: string;\n element_type: 'tile' | 'action' | 'item';\n template_id: string;\n placement: { type: 'slot'; slot: string } | { type: string; [key: string]: unknown };\n content: Record<string, unknown>;\n state: string;\n mounted_by: { agent_id: string; turn_id: string };\n mounted_at: string;\n version: number;\n}\n\nexport interface MutationEnvelope {\n type: 'syntro.element.mutation';\n schema_version: 'v0';\n mutations: Array<{ op: 'mount'; instance: MountedInstance }>;\n}\n\nexport type {\n PdpModuleHeaderClient,\n PdpPlanEnvelope,\n PdpPlanTaskClient,\n} from '../decision/pdp-compose';\n\n// \u2500\u2500\u2500 internal helpers for reading the SDK global \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** `presets[product][archetype][slot] = SlotPreset` \u2014 the config's SET values. */\ntype PdpPresetTable = Record<string, Record<string, Record<string, SlotPreset>>>;\n\nexport interface RuntimeContext {\n page: {\n locale?: string;\n routeId?: string;\n title?: string;\n url: string;\n };\n session: {\n pageHistory?: Array<{ ts: number; url: string }>;\n sessionId?: string;\n startTs: number;\n };\n surface: {\n device: string;\n host: string;\n mode: string;\n type: string;\n };\n viewport: {\n height: number;\n width: number;\n };\n}\n\n/** Canonical product identity shape (mirrors `@syntrologie/sdk-contracts`\n * ProductIdentity \u2014 structural type so the adaptive bundle stays free of\n * runtime-sdk imports). */\nexport interface PageProductIdentity {\n productHandle: string;\n productUrl: string;\n productTitle?: string;\n}\n\nexport interface SmartCanvasRuntime {\n apps?: {\n has?(appId: string): boolean;\n };\n telemetry?: {\n track?(eventName: string, props?: Record<string, unknown>): void;\n };\n /**\n * WI-6b: page-URL product resolver, set at bootstrap when the active\n * platform adapter can parse product page URLs. The attr-less element\n * (config placement injects one static HTML string across all product\n * pages) resolves its own product through this hook.\n */\n getPageProductIdentity?: () => PageProductIdentity | null;\n /**\n * The runtime's NavigationMonitor (subscribe-only view). Absent on bare\n * test mounts \u2014 the element skips SPA re-resolution gracefully then.\n * NEVER patch history here; the monitor is the single History API patch\n * point (runtime-sdk gotcha #7).\n */\n navigation?: {\n subscribe(listener: (url: string, method: string) => void): () => void;\n };\n}\n\ninterface SynOSGlobal {\n runtime?: {\n context: { get(): RuntimeContext };\n sessionMetrics?: { getAll(): Record<string, number> };\n telemetry?: {\n getDistinctId?(): string | undefined;\n getSessionId?(): string | undefined;\n getSegmentFlags?(): Record<string, boolean> | undefined;\n getAllFeatureFlags?(): Record<string, boolean | string> | undefined;\n };\n };\n /** On-demand scoring (the runtime's thin `score` primitive). */\n score?: (spec: unknown) => Promise<string>;\n /** Batched two-axis scoring through one backend model call. */\n scoreMulti?: (specs: unknown[]) => Promise<string[]>;\n /**\n * Authenticated transport to the runtime-backend (the runtime owns auth; the\n * adaptive owns which endpoint + payload). Used for the live module endpoint.\n */\n authedFetch?: (path: string, init?: RequestInit) => Promise<Response>;\n}\n\nfunction _readSynOS(): SynOSGlobal | undefined {\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { SynOS?: SynOSGlobal }).SynOS;\n}\n\nfunction _readRuntime(): SynOSGlobal['runtime'] | undefined {\n return _readSynOS()?.runtime;\n}\n\n/** Archetype slug \u2192 description (`window.__SYNTRO_PDP_ARCHETYPES__`). The\n * description tells the live sub-agent what the bucket MEANS. */\nfunction _readPdpArchetypes(): Record<string, string> | undefined {\n /* v8 ignore next -- SSR guard for a browser global reader; hydrated behavior is covered through scoring tests. */\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { __SYNTRO_PDP_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_ARCHETYPES__;\n}\n\n/** Axis A: the question blocking the purchase. Hosts that omit this keep single-axis PDPs. */\nfunction _readPdpDecisionArchetypes(): Record<string, string> | undefined {\n /* v8 ignore next -- SSR guard for a browser global reader; hydrated behavior is covered through scoring tests. */\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { __SYNTRO_PDP_DECISION_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_DECISION_ARCHETYPES__;\n}\n\n/**\n * Generate one uncached PDP module via the runtime-backend. The adaptive owns the\n * endpoint (`/api/pdp/module`) + its request/response shape; the runtime's\n * `authedFetch` owns auth. Returns the module payload, or null on any failure\n * (the slot just stays an empty reserved box \u2014 no throw, no layout shift).\n */\nasync function composeModule(\n authedFetch: NonNullable<SynOSGlobal['authedFetch']>,\n args: {\n productName: string;\n productId: string | null;\n kind: string;\n variant: string | null;\n archetype: { slug: string; description: string };\n decisionArchetype: { slug: string; description: string } | null;\n behaviorSummary: string;\n signals: Record<string, unknown>;\n }\n): Promise<Record<string, unknown> | null> {\n try {\n // Typed against the backend ModuleRequest contract \u2014 snake_case field names\n // and value types are checked at compile time.\n const requestBody: ModuleRequest = {\n product_name: args.productName,\n product_id: args.productId,\n // `kind` flows from runtime slot-id parsing (`pdp-slot-<kind>`), so it's a\n // plain string here; the backend validates it against the PdpModuleKind\n // enum (422 on an unknown kind). The other fields ARE contract-checked.\n kind: args.kind as ModuleRequest['kind'],\n variant: args.variant,\n archetype: { slug: args.archetype.slug, description: args.archetype.description },\n decision_archetype: args.decisionArchetype,\n coverage: 'focused',\n behavior_summary: args.behaviorSummary,\n // geo/attribution/device \u2014 the regional module needs the visitor's region.\n signals: args.signals,\n };\n const resp = await authedFetch('/api/pdp/module', {\n method: 'POST',\n body: JSON.stringify(requestBody),\n });\n if (!resp.ok) return null;\n const data = (await resp.json()) as Pick<ModuleResponse, 'payload'>;\n return data.payload ?? null;\n } catch {\n return null;\n }\n}\n\n/** Build a mount envelope for a live-generated module (mirrors the cached mount). */\nfunction _buildLiveMount(\n slot: string,\n kind: string,\n payload: Record<string, unknown>\n): MutationEnvelope {\n return {\n type: 'syntro.element.mutation',\n schema_version: 'v0',\n mutations: [\n {\n op: 'mount',\n instance: {\n instance_id: `live-${slot}`,\n element_type: 'tile',\n template_id: `pdp-${kind}`,\n placement: { type: 'slot', slot },\n content: payload,\n state: 'mounted',\n mounted_by: { agent_id: 'live', turn_id: 'live' },\n mounted_at: new Date().toISOString(),\n version: 1,\n },\n },\n ],\n };\n}\n\n/**\n * Per-product PDP presets via the WI-4 store accessor (config `pdpPresetsUrl`\n * fetch OR legacy hand-set `window.__SYNTRO_PDP_PRESETS__` \u2014 the store reads\n * the global lazily, so late producers are always observed).\n */\nfunction _readPdpPresets(): PdpPresetTable | undefined {\n return getPdpPresets() as PdpPresetTable | undefined;\n}\n\n// \u2500\u2500\u2500 \"Why box\" \u2014 surfaces the two model outputs + demo metrics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface PdpExplainMetric {\n value: string;\n label: string;\n /** Forensic one-liner shown in the \"What we measured\" drawer. */\n hint?: string;\n}\ninterface PdpExplain {\n archetypeLabel: string;\n archetypeDesc: string;\n decisionLabel: string | null;\n decisionDesc: string | null;\n summary: string;\n /** Hero row \u2014 punchy, scannable. */\n metrics: PdpExplainMetric[];\n /** Full forensic dossier \u2014 revealed in the \"What we measured\" drawer. */\n dossier: PdpExplainMetric[];\n}\n\n/** Title-case a slug: \"aging-cognitive\" \u2192 \"Aging Cognitive\". */\nfunction _titleCase(slug: string): string {\n return slug\n .split(/[-_\\s]+/)\n .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : ''))\n .join(' ');\n}\n\n/** Count non-empty leaf values in the (nested) signal bag \u2014 a real number to show off. */\nfunction _countSignals(signals: Record<string, unknown>): number {\n let n = 0;\n const walk = (v: unknown): void => {\n if (v && typeof v === 'object') {\n for (const x of Object.values(v as Record<string, unknown>)) walk(x);\n } else if (v !== '' && v != null) {\n // Count `false` too: \"is_touch=false\" / \"is_eu=false\" are real measurements,\n // not absent signals \u2014 excluding them would deflate the honest count.\n n++;\n }\n };\n walk(signals);\n return n;\n}\n\n/** Count top-level signal categories carrying data (attribution, geo, device, \u2026) \u2014 real. */\nfunction _signalDimensions(signals: Record<string, unknown>): number {\n let n = 0;\n for (const v of Object.values(signals)) {\n if (v && typeof v === 'object') {\n if (Object.keys(v as Record<string, unknown>).length) n++;\n } else if (v !== '' && v != null && v !== false) {\n n++;\n }\n }\n return n;\n}\n\n/**\n * A stable hex signature of the visitor's full signal bag (FNV-1a \u2192 8 hex digits).\n * Opaque on purpose: it doesn't claim a population statistic, it shows that we\n * distilled the depth of this visitor's behavior into a unique fingerprint.\n */\nfunction _fingerprint(signals: Record<string, unknown>): string {\n const s = JSON.stringify(signals);\n let h = 2166136261;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\n/** Read one numeric value from the signal bag's nested `session_metrics`, or null. */\nfunction _sessionMetric(signals: Record<string, unknown>, key: string): number | null {\n const sm = (signals as { session_metrics?: Record<string, unknown> }).session_metrics;\n const v = sm?.[key];\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\n/**\n * Build the \"why box\" content from the two model outputs + the visitor's REAL\n * session signals. Every metric is either a true measurement (signals analyzed,\n * scroll depth, time engaged, pages, models run, latency) or an opaque signature\n * (the behavioral fingerprint). NO invented population statistics (\"similar\n * visitors matched\", \"top N%\") \u2014 those are the only refutable kind, because they\n * assert something about OTHER people we can't show. Everything here is provable\n * from this visitor's own session.\n *\n * Tier 1 (here): SDK-local real metrics. Tier 2 (backend) will fold in the\n * event-stream forensics (interaction rhythm, engagement entropy, friction\n * signals, cursor travel) computed from `recent_observations`.\n */\nfunction buildExplain(opts: {\n archetype: string;\n description: string;\n decisionArchetype: string;\n decisionDescription: string;\n summary: string;\n signals: Record<string, unknown>;\n latencyMs: number;\n}): PdpExplain {\n const {\n archetype,\n description,\n decisionArchetype,\n decisionDescription,\n summary,\n signals,\n latencyMs,\n } = opts;\n const blockingQuestion =\n /Blocked on:\\s*([\\s\\S]*?)\\s*Resolved by:/.exec(decisionDescription)?.[1] ?? decisionDescription;\n // Transparency (Parker, 2026-07-16: \"if it is unknown.. score it as unknown\"):\n // show the decision row whenever the axis was SCORED \u2014 including an explicit\n // 'unknown' decline \u2014 and only hide it when the axis was never scored at all\n // (feature off / no scoreMulti). `decisionKnown` still gates the real blocking\n // question; an 'unknown' shows \"Unknown\" with a plain not-enough-signal note.\n const decisionScored = decisionArchetype.length > 0;\n const decisionKnown = decisionScored && decisionArchetype !== 'unknown';\n const signalsCount = _countSignals(signals);\n const dims = _signalDimensions(signals);\n const fingerprint = `0x${_fingerprint(signals)}`;\n const latency = `${Math.max(1, Math.round(latencyMs))}ms`;\n const scrollDepth = _sessionMetric(signals, 'scroll_depth');\n const timeOnPage = _sessionMetric(signals, 'time_on_page');\n const pageViews = _sessionMetric(signals, 'page_views');\n\n // Build the dossier from real values; only include session metrics we actually\n // captured (they may be absent on a fresh first paint).\n const dossier: PdpExplainMetric[] = [\n {\n value: `${signalsCount}`,\n label: 'behavioral signals analyzed',\n hint: 'Distinct non-empty data points across your attribution, device, geo, and live session.',\n },\n {\n value: `${dims}`,\n label: 'signal dimensions correlated',\n hint: 'Independent categories of behavior we cross-referenced to place you.',\n },\n ];\n if (scrollDepth != null)\n dossier.push({\n value: `${Math.round(scrollDepth)}%`,\n label: 'page depth reached',\n hint: 'How far down this page you explored before we personalized it.',\n });\n if (timeOnPage != null)\n dossier.push({\n value: `${(timeOnPage / 1000).toFixed(1)}s`,\n label: 'time engaged on page',\n hint: 'Active time you spent here \u2014 a direct read on deliberation.',\n });\n if (pageViews != null)\n dossier.push({\n value: `${pageViews}`,\n label: 'pages this session',\n hint: 'The depth of your journey across the site so far.',\n });\n dossier.push(\n {\n value: '2',\n label: 'AI models run live',\n hint: 'A two-axis archetype classifier (what you do + what is blocking your decision) and a behavior-summary model, evaluated in real time on your signals.',\n },\n {\n value: latency,\n label: 'personalization latency',\n hint: 'Time to score your behavior and assemble this page.',\n },\n {\n value: fingerprint,\n label: 'behavioral fingerprint',\n hint: 'A unique signature distilled from the full depth of your session signals.',\n }\n );\n\n // Hero row: the 5 punchiest, scannable at a glance.\n const hero: PdpExplainMetric[] = [\n { value: `${signalsCount}`, label: 'behavioral signals analyzed' },\n ];\n if (timeOnPage != null)\n hero.push({ value: `${(timeOnPage / 1000).toFixed(1)}s`, label: 'time engaged' });\n if (scrollDepth != null)\n hero.push({ value: `${Math.round(scrollDepth)}%`, label: 'of the page explored' });\n hero.push(\n { value: '2', label: 'AI models run live' },\n { value: fingerprint, label: 'behavioral fingerprint' }\n );\n\n return {\n archetypeLabel: _titleCase(archetype),\n archetypeDesc: description,\n decisionLabel: decisionScored ? _titleCase(decisionArchetype) : null,\n decisionDesc: decisionKnown\n ? blockingQuestion\n : decisionScored\n ? 'Engaged, but no clear blocking question yet \u2014 still exploring.'\n : null,\n summary,\n metrics: hero,\n dossier,\n };\n}\n\n/**\n * Tier 2: fetch the backend behavioral forensics (event-stream dossier \u2014 entropy,\n * interaction rhythm, hesitation / friction / hover counts) computed from the\n * visitor's `recent_observations`. Fail-soft to [] on any error \u2014 the why box\n * still shows the Tier-1 local metrics. The adaptive owns the endpoint; the\n * runtime's authedFetch owns auth (Bearer token + X-Distinct-Id).\n */\nasync function fetchDossier(\n authedFetch: NonNullable<SynOSGlobal['authedFetch']>\n): Promise<PdpExplainMetric[]> {\n try {\n const resp = await authedFetch('/api/pdp/dossier', { method: 'POST' });\n if (!resp.ok) return [];\n const data: unknown = await resp.json();\n // The wire shape is the backend DossierResponse (metrics: DossierMetric[]);\n // still validated at runtime since the response is untrusted.\n const rows = (data as Partial<DossierResponse>).metrics;\n if (!Array.isArray(rows)) return [];\n return rows\n .filter(\n (m): m is DossierMetric => !!m && typeof m.value === 'string' && typeof m.label === 'string'\n )\n .map((m) => ({\n value: m.value,\n label: m.label,\n hint: typeof m.hint === 'string' ? m.hint : undefined,\n }));\n } catch {\n return [];\n }\n}\n\n// \u2500\u2500\u2500 signal gatherers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Each gatherer guards against SSR / missing browser globals so they can be\n// imported in Node test environments without blowing up.\n\nfunction gatherDevice(context: RuntimeContext | undefined): VisitorDevice {\n if (typeof window === 'undefined') {\n return {\n user_agent: '',\n locale: context?.page.locale ?? '',\n viewport_w: context?.viewport.width ?? null,\n viewport_h: context?.viewport.height ?? null,\n screen_w: null,\n screen_h: null,\n dpr: null,\n is_touch: false,\n };\n }\n const nav = window.navigator;\n const scr = window.screen;\n return {\n user_agent: nav?.userAgent ?? '',\n locale: context?.page.locale ?? nav?.language ?? '',\n viewport_w: context?.viewport.width ?? window.innerWidth ?? null,\n viewport_h: context?.viewport.height ?? window.innerHeight ?? null,\n screen_w: scr?.width ?? null,\n screen_h: scr?.height ?? null,\n dpr: window.devicePixelRatio || null,\n is_touch: 'ontouchstart' in window || (nav?.maxTouchPoints ?? 0) > 0,\n };\n}\n\n/**\n * UTM + referrer + initial URL \u2014 read from PostHog person properties\n * (where the SDK writes them via setPersonPropertiesOnce).\n *\n * TODO: swap to `runtime.telemetry.getProperty(name)` once that getter lands.\n */\nfunction gatherAttribution(): VisitorAttribution {\n if (typeof window === 'undefined') {\n return {\n referrer: '',\n referring_domain: '',\n initial_url: '',\n initial_pathname: '',\n utm_source: '',\n utm_medium: '',\n utm_campaign: '',\n utm_content: '',\n utm_term: '',\n };\n }\n interface PosthogLike {\n get_property?: (name: string) => unknown;\n }\n const ph = (window as unknown as { posthog?: PosthogLike }).posthog;\n const get = (key: string): string => {\n const v = ph?.get_property?.(key);\n return typeof v === 'string' ? v : '';\n };\n const refLive = document.referrer || '';\n let refDomain = get('$initial_referring_domain');\n if (!refDomain && refLive) {\n try {\n refDomain = new URL(refLive).hostname;\n } catch {\n // ignore \u2014 referrer wasn't a valid URL\n }\n }\n return {\n referrer: get('$initial_referrer') || refLive,\n referring_domain: refDomain,\n initial_url: get('$initial_url') || `${window.location.origin}${window.location.pathname}`,\n initial_pathname: get('$initial_pathname') || window.location.pathname,\n utm_source: get('$initial_utm_source'),\n utm_medium: get('$initial_utm_medium'),\n utm_campaign: get('$initial_utm_campaign'),\n utm_content: get('$initial_utm_content'),\n utm_term: get('$initial_utm_term'),\n };\n}\n\n/**\n * Geo \u2014 read from the localStorage cache the SDK writes at bootstrap time\n * (key `syntro_geo`).\n *\n * TODO: swap to `runtime.context.get().geo` once the SDK exposes that field.\n */\nfunction gatherGeo(): VisitorGeo {\n interface CachedGeo {\n country?: string;\n country_code?: string;\n region?: string;\n city?: string;\n is_eu?: boolean | string;\n }\n let cached: CachedGeo | undefined;\n try {\n if (typeof sessionStorage === 'undefined' && typeof localStorage === 'undefined') {\n cached = undefined;\n } else {\n // The SDK may write to localStorage; guard in case storage is blocked.\n const raw = typeof localStorage !== 'undefined' ? localStorage.getItem('syntro_geo') : null;\n cached = raw ? (JSON.parse(raw) as CachedGeo) : undefined;\n }\n } catch {\n cached = undefined;\n }\n return {\n country: String(cached?.country ?? ''),\n country_code: String(cached?.country_code ?? ''),\n region: String(cached?.region ?? ''),\n city: String(cached?.city ?? ''),\n is_eu: cached?.is_eu === true || cached?.is_eu === 'true',\n };\n}\n\nfunction gatherSessionMetrics(\n runtime: SynOSGlobal['runtime']\n): Record<string, string | number | boolean> {\n const metrics = runtime?.sessionMetrics?.getAll() ?? {};\n const out: Record<string, string | number | boolean> = {};\n for (const [key, value] of Object.entries(metrics)) {\n if (typeof value === 'number' && Number.isFinite(value)) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction gatherExtras(\n runtime: SynOSGlobal['runtime'],\n context: RuntimeContext | undefined\n): Record<string, string | number | boolean> {\n const out: Record<string, string | number | boolean> = {};\n\n // Segment membership flags \u2014 runtime telemetry API.\n const segmentFlags = runtime?.telemetry?.getSegmentFlags?.() ?? {};\n for (const [key, value] of Object.entries(segmentFlags)) {\n if (typeof value === 'boolean') out[`segment.${key}`] = value;\n }\n\n if (context) {\n out['surface.type'] = context.surface.type;\n out['surface.host'] = context.surface.host;\n out['surface.device'] = context.surface.device;\n out['surface.mode'] = context.surface.mode;\n out['page.url'] = context.page.url;\n if (context.page.title) out['page.title'] = context.page.title;\n if (context.page.routeId) out['page.route'] = context.page.routeId;\n if (context.session.pageHistory) {\n out['session.page_count'] = context.session.pageHistory.length;\n // The TRAIL, not just the count: which pages the visitor actually\n // walked is the persona signal the archetype classifier scores on. A\n // browse-only session (chat never opened \u2192 observer never started \u2192\n // zero server-side events) previously gave the classifier NOTHING\n // persona-shaped, so it correctly declined to \"unknown\" forever.\n const trail = _renderPageTrail(context.session.pageHistory);\n if (trail) out['session.page_trail'] = trail;\n }\n } else if (typeof window !== 'undefined') {\n out['page.url'] = window.location.href;\n out['page.title'] = document.title;\n }\n\n return out;\n}\n\n// Bound the trail so the signals bag stays well under the backend's 8000-char\n// cap even alongside a long chat excerpt.\nconst _PAGE_TRAIL_MAX_CHARS = 600;\n\n/**\n * Render the session page history as a compact \"\u2192\"-joined path trail (origin\n * stripped, query kept \u2014 `/shop?sort=new` is signal). Newest entries win: we\n * walk backwards and stop before the joined string would exceed the bound.\n */\nfunction _renderPageTrail(history: Array<{ url: string; ts: number }>): string {\n const paths: string[] = [];\n let length = 0;\n for (let i = history.length - 1; i >= 0; i--) {\n let path = history[i]!.url;\n try {\n const u = new URL(path);\n path = `${u.pathname}${u.search}`;\n } catch {\n /* relative or malformed \u2014 use as-is */\n }\n const cost = path.length + (paths.length > 0 ? 3 : 0); // ' \u2192 ' separator\n if (length + cost > _PAGE_TRAIL_MAX_CHARS) break;\n paths.unshift(path);\n length += cost;\n }\n return paths.join(' \u2192 ');\n}\n\n// The runtime PDP endpoints cap chat_excerpt at ~8000 chars. Stay below the\n// limit so the backend doesn't re-trim. Captured excerpts can be larger than\n// the limit \u2014 long multi-turn sessions easily exceed 10k chars.\n/**\n * Inject the global section-layout stylesheet ONCE into document.head,\n * guarded by element id \u2014 same pattern as PdpSlotLit.ensureSlotStyles and\n * PdpSectionHeaderLit.ensureSectionHeaderStyles. These rules use attribute\n * selectors (`[data-syntro-pdp-section]`, `[data-syntro-pdp-root]`) that\n * are global by nature, so they must live in head exactly once rather than\n * being re-emitted inside the per-instance Lit render (which would duplicate\n * the rule set for every mounted `<syntro-pdp>`).\n *\n * Per-slot accents drive a DEDICATED `--syntro-pdp-section-accent` custom\n * property (NOT `--syntro-pdp-primary`). The section-header reads it with a\n * fallback to the host's brand primary, so the SDK never silently mutates a\n * customer-set `--syntro-pdp-primary`; a host that wants brand-only eyebrows\n * can set `--syntro-pdp-section-accent: var(--syntro-pdp-primary)`.\n */\nfunction ensureSectionLayoutStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-layout-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-layout-style';\n s.textContent = SECTION_LAYOUT_CSS;\n document.head.appendChild(s);\n}\n\nconst SECTION_LAYOUT_CSS = `\n[data-syntro-pdp-root] {\n --sc-pdp-max-w: var(--syntro-pdp-max-width, 1080px);\n --sc-pdp-section-pad-y: var(--syntro-pdp-section-pad-y, clamp(40px, 6vw, 80px));\n --sc-pdp-section-pad-x: var(--syntro-pdp-section-pad-x, clamp(20px, 4vw, 40px));\n /* Portable banding: default to a tint of the host's OWN text color\n (currentColor), so the alternating band adapts to any theme \u2014 light or\n dark \u2014 instead of a hardcoded near-black that's invisible on dark hosts\n and wrong on light ones. A client can pin an exact band color (e.g. a\n brand surface) via --syntro-pdp-card-bg-b. */\n --sc-pdp-band-alt: var(--syntro-pdp-card-bg-b, color-mix(in srgb, currentColor 5%, transparent));\n /* The root spans the FULL width of wherever the host mounts <syntro-pdp>,\n so the alternating section bands read as full-horizontal slices (like a\n storefront homepage). Per-section content is re-constrained to\n --sc-pdp-max-w by the inner wrapper. Mount the element full-width (no\n constraining container) to get edge-to-edge bands. */\n}\n[data-syntro-pdp-section] {\n position: relative;\n /* Vertical rhythm only \u2014 the band is full-bleed, so horizontal insets +\n max-width live on the inner wrapper, not here. No radius (a full-width\n band shouldn't have rounded corners at the viewport edge). Asymmetric:\n a tighter top so the eyebrow sits up near the band's top edge (the color\n transition), a generous bottom for breathing room under the content. */\n padding-top: var(--sc-pdp-section-pad-top, clamp(24px, 3vw, 36px));\n padding-bottom: var(--sc-pdp-section-pad-y);\n}\n/* Even sections carry a full-width band; odd sit on the host's base bg. The\n alternating color IS the separator \u2014 contiguous slices, no gap or divider. */\n[data-syntro-pdp-section]:nth-of-type(even) {\n background: var(--sc-pdp-band-alt);\n}\n/* Feature band: one section takes a full brand surface (e.g. a cream slice,\n like a storefront homepage) when the host opts in via\n --syntro-pdp-card-bg-feature. Defaults to the regular alternating tint, so\n hosts that don't set it see no change. Flips the section's color context\n (fg/muted/currentColor) to the feature foreground so the header + the body\n widgets (which read the same tokens) stay readable on the lighter surface.\n MUST follow the :nth-of-type(even) rule to win source order. */\n[data-syntro-pdp-section]:nth-of-type(4) {\n background: var(--syntro-pdp-card-bg-feature, var(--sc-pdp-band-alt));\n color: var(--syntro-pdp-fg-feature, inherit);\n --syntro-pdp-fg: var(--syntro-pdp-fg-feature, inherit);\n --syntro-pdp-muted: var(--syntro-pdp-muted-feature, inherit);\n}\n/* The content layer: constrained + centered inside the full-bleed band.\n margin-top separates the body from the headline (which now renders one\n layer up, at the full-width band level). */\n[data-syntro-pdp-section-inner] {\n max-width: var(--sc-pdp-max-w);\n margin-inline: auto;\n padding-inline: var(--sc-pdp-section-pad-x);\n margin-top: clamp(20px, 3vw, 32px);\n}\n/* Skeleton (pre-plan) stays constrained \u2014 only streamed sections go\n full-bleed, so the loading state doesn't jump edge-to-edge then back. */\n[data-syntro-pdp-root][data-syntro-pdp-skeleton] {\n max-width: var(--sc-pdp-max-w);\n margin-inline: auto;\n padding-inline: 16px;\n}\n/* Per-slot accent \u2192 a DEDICATED variable, never --syntro-pdp-primary. */\n[data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] { --syntro-pdp-section-accent: hsl(28 70% 58%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-chart'] { --syntro-pdp-section-accent: hsl(178 45% 50%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-qa'] { --syntro-pdp-section-accent: hsl(38 75% 60%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-regional'] { --syntro-pdp-section-accent: hsl(205 50% 55%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-peer-feed'] { --syntro-pdp-section-accent: hsl(265 45% 60%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-other-products'] { --syntro-pdp-section-accent: hsl(150 38% 50%); }\n\n/* Editorial 2-column layout for the trending-news section (desktop): the\n section title/eyebrow sits in a narrow left column and the articles fill a\n wider right column \u2014 a magazine masthead next to its stories. This trades\n the full-bleed band (a grid can't be both full-width AND content-centered)\n for the side-by-side reading rhythm; the section reads as a deliberate\n magazine block on the base background. Mobile keeps the stacked layout. */\n@media (min-width: 720px) {\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] {\n background: transparent;\n display: grid;\n grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr);\n gap: clamp(32px, 4vw, 64px);\n max-width: var(--sc-pdp-max-w);\n margin-inline: auto;\n padding-inline: var(--sc-pdp-section-pad-x);\n align-items: start;\n }\n /* Title column: header left-aligned (not the full-band centered eyebrow),\n and gently sticky so it holds while the stories scroll past. */\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] > syntro-pdp-section-header {\n position: sticky;\n top: clamp(24px, 8vh, 72px);\n }\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] .syntro-pdp-section-eyebrow {\n justify-content: flex-start;\n }\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] .syntro-pdp-section-headline {\n max-width: none;\n margin-inline: 0;\n padding-inline: 0;\n }\n /* Stories column: the body fills the wider right column (drop the centered\n inner constraint + the header-gap top margin \u2014 the grid gap spaces it). */\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] > [data-syntro-pdp-section-inner] {\n max-width: none;\n margin: 0;\n padding: 0;\n }\n}\n`;\n\nconst CHAT_EXCERPT_KEY = 'syntro.chat.lastExcerpt';\nconst CHAT_EXCERPT_MAX = 7500;\n\nfunction readChatExcerpt(): string {\n try {\n if (typeof sessionStorage === 'undefined') return '';\n const raw = sessionStorage.getItem(CHAT_EXCERPT_KEY) ?? '';\n return raw.length > CHAT_EXCERPT_MAX ? `\u2026${raw.slice(-CHAT_EXCERPT_MAX + 1)}` : raw;\n } catch {\n return '';\n }\n}\n\n/**\n * Collect every personalization signal the SDK has access to client-side.\n * Never throws \u2014 missing signals degrade to empty values.\n */\nexport function gatherVisitorSignals(): VisitorSignals {\n const runtime = _readRuntime();\n const context = runtime?.context.get();\n return {\n attribution: gatherAttribution(),\n geo: gatherGeo(),\n device: gatherDevice(context),\n session_metrics: gatherSessionMetrics(runtime),\n extras: gatherExtras(runtime, context),\n };\n}\n\n/**\n * Behavioral-signal count for the takeover engine's cold-start gate. Derived\n * from the SAME signal bag the element scores with, but counting only the\n * BEHAVIORAL subset (live session metrics + a captured chat excerpt): device,\n * geo, and attribution exist on every first paint, so counting them would\n * make `hasSignals()` a constant true and the no-curtain progressive path\n * unreachable. 0 \u21D2 first-visit cold start (spec: \"no behavioral signals\").\n */\nfunction countTakeoverSignals(signals: VisitorSignals & { chat_excerpt: string }): number {\n return _countSignals(signals.session_metrics) + (signals.chat_excerpt ? 1 : 0);\n}\n\n// \u2500\u2500\u2500 TEMPLATE_TO_TAG dispatch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Maps (template_id, optional variant) \u2192 custom element tag name.\n// Keys are `\"<kind>\"` for base variants and `\"<kind>/<variant>\"` for specific\n// variants. resolveTagForMount() tries the variant key first, then the base\n// key, then returns null for unknown templates.\n\nexport type TemplateToTagKey = string;\n\n// Variant keys MUST be the exact `<kind>/<variant>` values the planner\n// emits \u2014 i.e. the backend PdpModuleKind + the *full* variant enum value\n// (TrendingNewsVariant, ChartVariant, QaVariant, \u2026). An abbreviated key\n// (e.g. `qa/accordion` vs the emitted `qa/accordion-multi`) silently\n// fails the lookup and every module falls back to its base widget, so the\n// dedicated variant layouts never render. `test_resolveTag` pins this.\nexport const TEMPLATE_TO_TAG: Record<TemplateToTagKey, string> = {\n // trending-news (TrendingNewsVariant)\n 'trending-news': 'syntro-pdp-trending-news',\n 'trending-news/magazine-column': 'syntro-pdp-trending-news-magazine',\n 'trending-news/hero-quote': 'syntro-pdp-trending-news-hero',\n // chart (ChartVariant)\n chart: 'syntro-pdp-chart',\n 'chart/bar-stacked': 'syntro-pdp-chart-bar',\n 'chart/metric-pull': 'syntro-pdp-chart-metric',\n // qa (QaVariant)\n qa: 'syntro-pdp-qa',\n 'qa/accordion-multi': 'syntro-pdp-qa-accordion',\n 'qa/side-by-side': 'syntro-pdp-qa-side',\n // regional (RegionalVariant)\n regional: 'syntro-pdp-regional',\n 'regional/full-bleed': 'syntro-pdp-regional-fullbleed',\n 'regional/inline-note': 'syntro-pdp-regional-inline',\n // peer-feed (PeerFeedVariant)\n 'peer-feed': 'syntro-pdp-peer-feed',\n 'peer-feed/voice-carousel': 'syntro-pdp-peer-feed-carousel',\n 'peer-feed/masonry-quote': 'syntro-pdp-peer-feed-masonry',\n // other-products (OtherProductsVariant) \u2014 already canonical\n 'other-products': 'syntro-pdp-other-products-carousel',\n 'other-products/carousel': 'syntro-pdp-other-products-carousel',\n 'other-products/two-column-grid': 'syntro-pdp-other-products-grid',\n};\n\n/**\n * Resolve the custom element tag to mount for a given plan task instance.\n *\n * Resolution order:\n * 1. `\"<kind>/<variant>\"` if `variant` is non-null\n * 2. `\"<kind>\"` (base) \u2014 covers unknown-variant fallback\n * 3. `null` if the kind itself is unknown\n */\nexport function resolveTagForMount(inst: PdpPlanTaskClient): string | null {\n if (inst.variant) {\n const variantKey = `${inst.kind}/${inst.variant}`;\n if (variantKey in TEMPLATE_TO_TAG) {\n return TEMPLATE_TO_TAG[variantKey];\n }\n // Variant string didn't match \u2014 surface this as a planner-prompt\n // drift signal. The base-key fallback below silently mounts the\n // default variant; without this log it's invisible.\n if (inst.kind in TEMPLATE_TO_TAG) {\n console.warn(\n `syntro-pdp.variant_drift: planner emitted unknown variant ` +\n `\"${inst.variant}\" for kind=\"${inst.kind}\" \u2014 falling back to ` +\n `default variant via TEMPLATE_TO_TAG[\"${inst.kind}\"]. Fix the ` +\n `planner prompt or add this variant to TEMPLATE_TO_TAG.`\n );\n }\n }\n const baseKey = inst.kind;\n if (baseKey in TEMPLATE_TO_TAG) {\n return TEMPLATE_TO_TAG[baseKey];\n }\n return null;\n}\n\n/**\n * Test seam \u2014 exposes private module-scope functions for unit testing only.\n * Do NOT use outside of test files.\n */\nexport const _testing = {\n buildExplain,\n gatherAttribution,\n gatherDevice,\n gatherExtras,\n gatherGeo,\n gatherSessionMetrics,\n gatherVisitorSignals,\n readChatExcerpt,\n resolveTagForMount,\n TEMPLATE_TO_TAG,\n titleCase: _titleCase,\n};\n\ntype MountOutcome =\n | { status: 'mounted'; slot: string }\n | { status: 'missed'; slot: string }\n | { status: 'ignored'; slot: string }\n // Native takeover: content was swapped into the host's OWN native section, or\n // the host defaults were deliberately kept for a fully-unknown visitor.\n | { status: 'mounted-native'; slot: string }\n | { status: 'native-default-kept'; slot: string };\n\n/**\n * <syntro-pdp> \u2014 the dynamic personalized PDP surface.\n *\n * Config-first flow: gathers visitor signals from the SDK runtime, scores the\n * visitor's archetype, mounts the host-supplied config presets, and generates\n * the remaining live slots via the runtime container's /api/pdp/module and\n * /api/pdp/dossier endpoints. Customer code mounts this one element; everything\n * else is internal to the SDK.\n *\n * Attributes (kebab-case, set by host markup):\n * - product-id (required)\n * - product-name (required)\n * - product-blurb (optional)\n *\n * Properties (set by mountable, not host markup):\n * - runtimeRef: SmartCanvasRuntime \u2014 passed by the AppLoader through\n * the existing widget-mountable mechanism.\n */\nexport class SyntroPdpLit extends LitElement {\n static override properties = {\n productId: { type: String, attribute: 'product-id' },\n productName: { type: String, attribute: 'product-name' },\n productBlurb: { type: String, attribute: 'product-blurb' },\n runtimeRef: { attribute: false },\n takeover: { attribute: false },\n _decisionState: { state: true },\n _pageUnresolvable: { state: true },\n _presetsSettled: { state: true },\n };\n\n productId = '';\n productName = '';\n productBlurb = '';\n\n /**\n * Page-takeover anatomy (FEAT-1785977524), normally delivered through the\n * widget `props` below. Presence at CONNECT time latches the element into\n * takeover mode: the engine owns the root and the six-slot compose path\n * never starts. Setting it after a legacy connect is ignored (warned) \u2014\n * flipping modes on a live element could leave both paths' DOM behind.\n */\n takeover: PdpTakeoverConfig | undefined = undefined;\n\n private _propsValue: SyntroPdpProps | undefined = undefined;\n\n /**\n * Widget props (the `adaptive-product:pdp` mountable sets `el.props` before\n * appending). Mapped IMMEDIATELY in the setter \u2014 not in willUpdate \u2014 so\n * `takeover` is already on the element when connectedCallback latches the\n * mode. Absent fields leave the element's current values untouched.\n */\n get props(): SyntroPdpProps | undefined {\n return this._propsValue;\n }\n\n set props(value: SyntroPdpProps | undefined) {\n this._propsValue = value;\n if (!value) return;\n if (value.productId) this.productId = value.productId;\n if (value.productName) this.productName = value.productName;\n if (value.productBlurb) this.productBlurb = value.productBlurb;\n if (value.takeover) this.takeover = value.takeover;\n }\n\n /**\n * SDK runtime. Null until the mountable wires it in. We only render\n * the personalized surface once runtimeRef is present AND the\n * activation gate passes.\n */\n runtimeRef: SmartCanvasRuntime | null = null;\n\n /** Canonical decision and render state. */\n private _decisionState: PdpState = createInitialPdpState();\n\n /**\n * WI-6b self-identification state. `_selfIdentify` latches true when the\n * element connects WITHOUT a product-id attr (config-placement shape) \u2014\n * from then on the page URL, not the attr, owns identity, and SPA\n * navigations re-resolve it. `_pageUnresolvable` (reactive) makes render()\n * produce NOTHING \u2014 fail-closed, consistent with the missing-preset\n * behavior: never a broken box on a non-product page.\n */\n private _selfIdentify = false;\n private _pageUnresolvable = false;\n private _navUnsub: (() => void) | null = null;\n\n /**\n * WI-4 presets readiness. False while a config-driven `pdpPresetsUrl`\n * fetch may still land; flips true (reactive) once `pdpPresetsReady()`\n * settles \u2014 which is immediate when no fetch is in flight. Once settled,\n * an element whose product still has no preset row renders NOTHING\n * (fail-closed) instead of an eternal loading skeleton.\n */\n private _presetsSettled = false;\n private _presetsReadyWired = false;\n\n private _composeStartedAt = 0;\n private _lastArchetype = '';\n private _mountDurations = new Map<string, number>();\n\n /**\n * Native takeover state (FEAT-1784346204). `_nativeSlots` holds the slot ids\n * whose native host section exists OUTSIDE this element \u2014 the render loop\n * skips those so the element never renders a duplicate qa/peer-feed section.\n * `_nativeHandles` tracks each active swap so it can be restored (before a\n * re-swap, and on disconnect).\n */\n private _nativeSlots = new Set<string>();\n private _nativeHandles = new Map<string, NativeSwapHandle>();\n\n /**\n * Takeover mode latch \u2014 decided ONCE, at the element's FIRST connect, from\n * the `takeover` prop, and kept for the element's whole lifetime. While\n * true, Lit never renders into this element (see shouldUpdate): the engine\n * renders imperatively and its deactivateRoot() calls replaceChildren() on\n * this root, which would destroy Lit's child parts. The latch is one-way in\n * BOTH directions \u2014 an element that ever rendered legacy has live Lit parts\n * (engaging the engine would render next to them, then destroy them), and\n * an element the engine ever owned has no parts left for Lit to resume.\n */\n private _takeoverMode = false;\n private _takeoverModeLatched = false;\n\n /** The single in-flight/settled takeover run for this connected lifetime. */\n private _takeoverRun: {\n disposed: boolean;\n detach: (() => void) | null;\n result: Promise<TakeoverResult | null>;\n } | null = null;\n\n /**\n * Settles when the previous run's dispose has fully completed. A re-run\n * (reconnect, or a synchronous move between parents) chains on this so a\n * LATE dispose from the torn-down run can never deactivate the root the\n * new run is rendering into.\n */\n private _takeoverDisposal: Promise<unknown> = Promise.resolve();\n\n /** A previous run deactivated the root (display:none + emptied); a re-run\n * after reconnect must undo that before the engine renders again. */\n private _takeoverRanBefore = false;\n\n private _takeoverLateWarned = false;\n\n /**\n * bfcache re-entry (review F4). pagehide disposes the run\n * (attachLifecycleDispose) but a bfcache restore does NOT re-fire\n * connectedCallback \u2014 without this listener the restored page would sit on\n * the native fallback forever. `persisted` is true only for bfcache\n * restores; the normal-load pageshow is owned by the connect path. Arrow\n * function per Lit event-handler convention (stable identity for\n * add/removeEventListener).\n */\n private _onTakeoverPageShow = (event: Event): void => {\n if (!(event as PageTransitionEvent).persisted) return;\n if (!this._takeoverMode || !this.isConnected) return;\n const config = this.takeover;\n if (!config) return;\n // Clear the pagehide-disposed run record (engine dispose is idempotent \u2014\n // pinned by the F12 test), then re-enter through the same\n // _takeoverDisposal serialization every re-run uses.\n this._teardownTakeover();\n this._startTakeover(config);\n };\n\n /**\n * The scored axes from the current config pass \u2014 the SAME values\n * `_fireFromConfig` resolved (sport-archetype preset key + decision archetype).\n * Captured at scoring time so they are already on the element when mounts\n * apply, which is BEFORE `_fireFromConfig` returns. Both default to 'unknown'.\n */\n private _resolvedArchetype = 'unknown';\n private _resolvedDecision = 'unknown';\n\n private _effectExecutor = createPdpEffectExecutor({\n dispatch: (event) => this._dispatchDecision(event),\n startCompose: (generation, run) => this._startDecisionCompose(generation, run),\n captureTelemetry: (name, props) => this._captureDecisionTelemetry(name, props),\n warn: (code, props) => this._warnDecision(code, props),\n cleanup: () => this._cleanupDecisionEffects(),\n });\n\n private _dispatchDecision(event: PdpEvent): void {\n const transition = transitionPdp(this._decisionState, event);\n this._decisionState = transition.state;\n for (const effect of transition.effects) void this._effectExecutor.execute(effect);\n }\n\n private _onDossierToggle = (e: Event): void => {\n const open = (e.target as HTMLDetailsElement).open;\n this._dispatchDecision({ type: 'dossier-toggled', open });\n };\n\n override createRenderRoot() {\n // Light DOM so existing customer CSS (brand-token bridge, page\n // layout) cascades into the inner section frames.\n return this;\n }\n\n override connectedCallback(): void {\n // \u2500\u2500 Takeover mode (FEAT-1785977524) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Latched at FIRST connect: prop present \u21D2 the engine owns this element\n // INSTEAD OF the six-slot compose path \u2014 none of the decision-machine\n // sync below runs, so the legacy path can never render alongside a\n // takeover. Prop absent \u21D2 the takeover code is fully inert and the\n // legacy path below is byte-identical to pre-takeover behavior. Later\n // reconnects KEEP the first-connect mode (see the latch field docs);\n // switching modes requires a fresh element.\n //\n // KNOWN LIMITATION (editor-sdk live-update path): props patched AFTER\n // this latch silently no-op for engine-captured values \u2014 _startTakeover\n // snapshots productId/productName, the signal bag, and the decision\n // vocabulary when the run starts, and the takeover config itself is only\n // read here. A live config edit must re-mount the widget (fresh element)\n // to take effect; in-place patches will appear to do nothing.\n if (!this._takeoverModeLatched) {\n this._takeoverModeLatched = true;\n this._takeoverMode = Boolean(this.takeover);\n } else if (!this._takeoverMode && this.takeover && !this._takeoverLateWarned) {\n this._takeoverLateWarned = true;\n console.warn(\n '[syntro-pdp] takeover prop set on an element that already connected on the ' +\n 'standard compose path \u2014 ignored (mode is latched per element). Mount a ' +\n 'fresh element with `takeover` in its widget props.'\n );\n }\n if (this._takeoverMode) {\n super.connectedCallback();\n ensureSectionLayoutStyles();\n if (typeof window !== 'undefined') {\n // remove-then-add keeps this idempotent under re-entrant connects.\n window.removeEventListener('pageshow', this._onTakeoverPageShow);\n window.addEventListener('pageshow', this._onTakeoverPageShow);\n }\n if (this.takeover) this._startTakeover(this.takeover);\n return;\n }\n const reconnecting = this._decisionState.connection === 'disconnected';\n super.connectedCallback();\n ensureSectionLayoutStyles();\n // WI-6b: no product-id attr at connect \u2192 the element owns its identity\n // from the page URL (config placement injects one static HTML string\n // across all product pages). Latched \u2014 an attr-driven element never\n // flips into self-identify mode.\n if (!this.productId) this._selfIdentify = true;\n // On vela's server-rendered `/pdp/dynamic` pages the native sections are in\n // the initial HTML, so they exist by the time the element upgrades.\n this._refreshNativeSlots();\n this._syncDecisionRuntime(reconnecting);\n this._syncDecisionPortal();\n // WI-4: gate the one-shot fire on presets readiness. When a\n // config-driven fetch is in flight at connect, `configured` was false in\n // the sync above; re-sync once the fetch settles so a late-arriving\n // table starts the compose (startCompose only fires while the stream is\n // idle, so an already-running compose is never disturbed).\n if (!this._presetsReadyWired) {\n this._presetsReadyWired = true;\n void pdpPresetsReady().then(() => {\n this._presetsSettled = true;\n if (this.isConnected) this._syncDecisionRuntime(false);\n });\n }\n }\n\n override disconnectedCallback(): void {\n if (this._takeoverMode) {\n super.disconnectedCallback();\n if (typeof window !== 'undefined') {\n window.removeEventListener('pageshow', this._onTakeoverPageShow);\n }\n // Abort-and-restore (spec: SPA navigation / unmount mid-takeover):\n // detached host sections reattach, the root deactivates. A reconnect\n // re-evaluates takeover fresh via connectedCallback.\n this._teardownTakeover();\n return;\n }\n super.disconnectedCallback();\n // WI-6b: drop the SPA-navigation subscription (re-wired on reconnect).\n this._navUnsub?.();\n this._navUnsub = null;\n // Restore every native host section to its ORIGINAL nodes (same references \u2014\n // a React host may still hold refs to them).\n for (const handle of this._nativeHandles.values()) handle.restore();\n this._nativeHandles.clear();\n this._dispatchDecision({ type: 'disconnected' });\n }\n\n override shouldUpdate(changed: PropertyValues): boolean {\n // Takeover mode: Lit must NEVER render into this element. The engine owns\n // the root imperatively, and deactivateRoot() calls replaceChildren() \u2014\n // fatal to Lit child parts if any were ever created here.\n if (this._takeoverMode) return false;\n // A takeover prop arriving AFTER a legacy connect is refused: the compose\n // path may already have rendered, and engaging now could leave both\n // paths' DOM in the document. Reconnect with the prop set to take over.\n if (changed.has('takeover') && this.takeover && !this._takeoverLateWarned) {\n this._takeoverLateWarned = true;\n console.warn(\n '[syntro-pdp] takeover prop set after the element already connected on the ' +\n 'standard compose path \u2014 ignored. Provide `takeover` in the widget props ' +\n 'before the element connects (or re-mount it).'\n );\n }\n return true;\n }\n\n override willUpdate(changed: PropertyValues): void {\n if (changed.has('runtimeRef')) {\n this._syncDecisionRuntime(false);\n }\n // Re-probe before every render so section suppression stays correct even if\n // the host sections upgraded after the element connected (lazy hydration).\n this._refreshNativeSlots();\n this._syncDecisionPortal();\n }\n\n /**\n * Probe the document for native host sections (`data-syntro-slot=\"qa\"` /\n * `\"peer-feed\"`) that live OUTSIDE this element, and record their slot ids.\n * The render loop skips these slots; the mount path swaps into them.\n */\n private _refreshNativeSlots(): void {\n if (typeof document === 'undefined') return;\n this._nativeSlots.clear();\n for (const [slotId, kind] of Object.entries(NATIVE_KIND_BY_SLOT)) {\n const native = findNativeSlot(document, kind);\n if (native && !this.contains(native)) this._nativeSlots.add(slotId);\n }\n }\n\n /**\n * A visitor is personalized unless BOTH scored axes declined to 'unknown'.\n * A fully-unknown visitor keeps the host's default native content untouched.\n */\n private _isPersonalizedVisitor(): boolean {\n return !(this._resolvedArchetype === 'unknown' && this._resolvedDecision === 'unknown');\n }\n\n override updated(_changed: PropertyValues): void {\n // If the host page exposes an above-the-fold portal, render the page-wide\n // \"why box\" THERE \u2014 leading the page, ahead of the standard product card \u2014\n // instead of inline above the modules. render() omits the inline copy when\n // the portal exists, so the portal is the single source of truth.\n //\n const view = selectPdpView(this._decisionState);\n const portal = this._whyPortalEl();\n if (!portal) return;\n render(view.whyBox.destination === 'portal' ? this._renderWhyBox(view.whyBox) : '', portal);\n }\n\n /** The host page's above-the-fold mount for the page-wide why box, if any. */\n private _whyPortalEl(): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n return document.querySelector<HTMLElement>('[data-syntro-pdp-why-portal]');\n }\n\n // \u2500\u2500 WI-6b: page-URL self-identification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Resolve identity from the page URL when in self-identify mode. Runs at\n * every runtime sync (connect + runtimeRef arrival) \u2014 idempotent. A miss\n * marks the page unresolvable: render() then produces NOTHING (fail-closed)\n * and a warning names the URL. While runtimeRef is still null we stay\n * quiet \u2014 the mountable wires it in moments later and we re-run.\n */\n private _resolvePageProduct(): void {\n if (!this._selfIdentify || !this.runtimeRef) return;\n const identity = this.runtimeRef.getPageProductIdentity?.() ?? null;\n if (identity) {\n this._pageUnresolvable = false;\n this.productId = identity.productHandle;\n // Product name is OPTIONAL on the resolved path \u2014 a page URL carries no\n // human-readable name; preset/module payloads supply the content, and\n // composeModule tolerates an empty product_name.\n if (!this.productName && identity.productTitle) this.productName = identity.productTitle;\n return;\n }\n if (!this._pageUnresolvable) {\n this._pageUnresolvable = true;\n this.productId = '';\n const url = typeof window !== 'undefined' ? window.location.href : '';\n console.warn(\n `[syntro-pdp] no product-id attribute and the page URL did not resolve to a ` +\n `product \u2014 rendering nothing (fail-closed). url=${url}`\n );\n }\n }\n\n /**\n * Subscribe to the runtime's NavigationMonitor so SPA/PJAX product-page\n * changes re-resolve identity and re-fire (WI-6b). Self-identified elements\n * only \u2014 an attr-driven element's identity is pinned by its attr. Skips\n * gracefully when the runtime lacks `navigation` (bare test mounts).\n */\n private _wireNavigation(): void {\n if (!this._selfIdentify || this._navUnsub) return;\n const nav = this.runtimeRef?.navigation;\n if (!nav || typeof nav.subscribe !== 'function') return;\n this._navUnsub = nav.subscribe((url) => this._onPageNavigated(url));\n }\n\n /** Re-resolve on navigation; on a product change reset the decision machine\n * through its sanctioned disconnect/reconnect path and re-fire. */\n private _onPageNavigated(navUrl: string): void {\n if (!this._selfIdentify || !this.isConnected || !this.runtimeRef) return;\n const identity = this.runtimeRef.getPageProductIdentity?.() ?? null;\n const next = identity?.productHandle ?? '';\n if (next === this.productId) return; // same product (or still unresolvable)\n // Reset: 'disconnected' tears down the current generation (restores native\n // swaps' twin state via cleanup effects); the reconnect sync below starts a\n // fresh compose for the new product \u2014 or goes dark when unresolvable.\n this._dispatchDecision({ type: 'disconnected' });\n if (identity) {\n this._pageUnresolvable = false;\n this.productId = identity.productHandle;\n if (identity.productTitle) this.productName = identity.productTitle;\n } else {\n this.productId = '';\n if (!this._pageUnresolvable) {\n this._pageUnresolvable = true;\n console.warn(\n `[syntro-pdp] navigation left the product surface \u2014 rendering nothing ` +\n `(fail-closed). url=${navUrl}`\n );\n }\n }\n this._syncDecisionRuntime(true);\n }\n\n private _syncDecisionRuntime(reconnecting: boolean): void {\n this._resolvePageProduct();\n this._wireNavigation();\n let appActive = false;\n try {\n appActive = Boolean(this.runtimeRef?.apps?.has?.('adaptive-product'));\n } catch {\n // Runtime integrations remain fail-soft during the shadow cutover.\n }\n const configured = Boolean(\n _readPdpPresets()?.[this.productId] && typeof _readSynOS()?.score === 'function'\n );\n const status = {\n runtimeAvailable: this.runtimeRef !== null,\n appActive,\n configured,\n };\n this._dispatchDecision(\n reconnecting ? { type: 'reconnected', ...status } : { type: 'runtime-updated', ...status }\n );\n }\n\n private _syncDecisionPortal(): void {\n this._dispatchDecision({ type: 'portal-updated', present: this._whyPortalEl() !== null });\n }\n\n // \u2500\u2500 Takeover wiring (FEAT-1785977524) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Run the takeover engine with real ports: this element as the root,\n * `SynOS.authedFetch` as the plan/module transport and the existing slot\n * machinery as the renderer. Activation is config delivery itself: the\n * served config either carries `takeover` or it doesn't. Runs at\n * most once per connected lifetime (`_takeoverRun` latch); disconnect tears\n * it down and a reconnect re-evaluates fresh.\n */\n private _startTakeover(config: PdpTakeoverConfig): void {\n if (this._takeoverRun) return; // double-start guard (reconnect races)\n if (!this.productId) {\n // The server contract REQUIRES product_id (min 1) \u2014 a takeover call\n // without one would 422. No silent default (`feedback_no_silent_defaults`):\n // skip the takeover entirely, loudly, and leave the native page as-is.\n this._track('takeover.missing_product_id', {});\n console.warn(\n '[syntro-pdp] takeover requires a product id (widget props / product-id attr) \u2014 ' +\n 'skipping takeover, native page untouched.'\n );\n return;\n }\n const synos = _readSynOS();\n const authedFetch = synos?.authedFetch;\n const signals = { ...gatherVisitorSignals(), chat_excerpt: readChatExcerpt() };\n const decisionDescriptions = _readPdpDecisionArchetypes() ?? {};\n const telemetry = (event: string, props?: Record<string, unknown>): void =>\n this._track(event, props);\n\n // The engine forwards only the PLAN to renderModules; the server-scored\n // decision archetype crosses from fetchPlan to the renderer through this\n // holder (preset-row selection + live-module context both need it).\n const scored = { decisionArchetype: 'unknown' };\n const fetchPlan = authedFetch\n ? createTakeoverPlanFetcher({\n authedFetch,\n config,\n productId: this.productId,\n productName: this.productName,\n decisionArchetypes: decisionDescriptions,\n // No client-side summary exists on the takeover path (the server\n // scores); the field mirrors /api/pdp/module's for the scorer.\n behaviorSummary: '',\n signals,\n })\n : null;\n\n const run: NonNullable<SyntroPdpLit['_takeoverRun']> = {\n disposed: false,\n detach: null,\n result: Promise.resolve(null),\n };\n\n const renderModules = createTakeoverModuleRenderer({\n root: this,\n getPresetRow: (decisionArchetype) => {\n const table = _readPdpPresets()?.[this.productId];\n return table?.[decisionArchetype] ?? table?.unknown;\n },\n resolveTag: (kind, variant) =>\n resolveTagForMount({\n slot: `pdp-slot-${kind}`,\n kind,\n variant,\n header: { eyebrow: '', title: '', accent: null, subtitle: null },\n } as PdpPlanTaskClient),\n requestLiveModule: async (kind, decisionArchetype) => {\n if (!authedFetch) return null;\n return composeModule(authedFetch, {\n productName: this.productName,\n // Guaranteed non-empty: _startTakeover gates on a missing product id.\n productId: this.productId,\n kind,\n variant: null,\n // The takeover round trip scores only the decision axis (the server\n // resolves the plan from it); the sport-archetype axis is unscored\n // on this path, so it is sent as an explicit 'unknown' \u2014 never a\n // silently-invented value.\n archetype: { slug: 'unknown', description: '' },\n decisionArchetype:\n decisionArchetype === 'unknown'\n ? null\n : {\n slug: decisionArchetype,\n description: decisionDescriptions[decisionArchetype] ?? '',\n },\n behaviorSummary: '',\n signals,\n });\n },\n telemetry,\n getDecisionArchetype: () => scored.decisionArchetype,\n });\n\n const ports = createDomPorts(config, {\n root: this,\n signalCount: countTakeoverSignals(signals),\n fetchPlan: fetchPlan\n ? async () => {\n const response = await fetchPlan();\n scored.decisionArchetype = response.decisionArchetype;\n return response;\n }\n : async () => {\n // No authed transport \u21D2 no plan \u21D2 the engine fails open.\n throw new Error('SynOS.authedFetch unavailable \u2014 cannot fetch takeover plan');\n },\n // Torn-down guard: the progressive path's dispose is a no-op (nothing\n // was detached), so its BACKGROUND render has no cancellation handle \u2014\n // this gate is what stops a stale run from appending into a root a\n // newer run may own by then.\n renderModules: async (plan, onModuleFailed) => {\n if (run.disposed) return;\n return renderModules(plan, onModuleFailed);\n },\n telemetry,\n });\n\n // Chain on the PREVIOUS run's disposal: a synchronous move between\n // parents tears down run N and starts run N+1 in the same task \u2014 without\n // this ordering, run N's late dispose() would deactivate (hide + empty)\n // the root while run N+1 is rendering into it.\n run.result = this._takeoverDisposal\n .then((): Promise<TakeoverResult | null> | null => {\n if (run.disposed) return null; // torn down before the engine started\n // A prior run's dispose deactivated the root \u2014 undo before rendering.\n if (this._takeoverRanBefore) {\n this.style.removeProperty('display');\n this.replaceChildren();\n }\n this._takeoverRanBefore = true;\n return runTakeover(config, ports, { commitTimeoutMs: config.commitTimeoutMs });\n })\n .then((result) => {\n if (!result) return null;\n telemetry('takeover.outcome', { outcome: result.outcome });\n if (run.disposed) {\n // Torn down (disconnect) before the engine settled \u2014 dispose NOW so\n // any committed detach is rolled back.\n result.dispose();\n return result;\n }\n run.detach = attachLifecycleDispose(result);\n return result;\n })\n .catch((err) => {\n // runTakeover is designed never to reject; belt and braces so a\n // defect can never strand a detached page.\n telemetry('takeover.run_failed', { error: String(err).slice(0, 200) });\n return null;\n });\n this._takeoverRun = run;\n }\n\n /** Dispose the current takeover run (idempotent). */\n private _teardownTakeover(): void {\n const run = this._takeoverRun;\n if (!run) return;\n this._takeoverRun = null;\n run.disposed = true;\n run.detach?.();\n run.detach = null;\n this._takeoverDisposal = run.result.then((result) => result?.dispose());\n }\n\n /**\n * Emit a telemetry event via the runtime's track API.\n * Wrapped in try/catch so a broken telemetry integration never crashes the\n * compose flow. Always includes `source: 'sdk'` in props.\n */\n private _track(eventName: string, props: Record<string, unknown> = {}): void {\n try {\n this.runtimeRef?.telemetry?.track?.(eventName, { ...props, source: 'sdk' });\n } catch {\n // Never let telemetry failures surface to callers.\n }\n }\n\n private _captureDecisionTelemetry(name: string, props: Record<string, unknown>): void {\n const enriched = { ...props };\n if (name === 'syntro_pdp_plan_received' || name === 'syntro_pdp_done') {\n enriched.duration_ms = Math.round(performance.now() - this._composeStartedAt);\n }\n if (name === 'syntro_pdp_done') enriched.archetype = this._lastArchetype;\n if (name === 'syntro_pdp_section_mounted' && typeof props.slot === 'string') {\n enriched.duration_ms = this._mountDurations.get(props.slot) ?? 0;\n }\n this._track(name, enriched);\n }\n\n private _warnDecision(code: string, props: Record<string, unknown>): void {\n if (code === 'missing-config') {\n console.warn(\n `[syntro-pdp] no PDP presets for product \"${this.productId}\" (or the ` +\n 'scored-template primitive is unavailable) \u2014 skipping personalization. ' +\n 'Deliver presets via the SDK config `pdpPresetsUrl` (or the legacy ' +\n '__SYNTRO_PDP_PRESETS__ boot global) to enable the config-first PDP.'\n );\n return;\n }\n if (code === 'mount-miss' && typeof props.slot === 'string') {\n console.warn(\n `syntro-pdp.mount_miss: no slot container for \"${props.slot}\" even after updateComplete.`\n );\n return;\n }\n console.warn(`[syntro-pdp] ${code}`, props);\n }\n\n private _clearDecisionPortal(): void {\n const portal = this._whyPortalEl();\n if (portal) render('', portal);\n }\n\n private _cleanupDecisionEffects(): void {\n this._clearDecisionPortal();\n }\n\n private async _startDecisionCompose(generation: number, run: PdpEffectRun): Promise<void> {\n const productPresets = _readPdpPresets()?.[this.productId];\n const synos = _readSynOS();\n if (!productPresets || typeof synos?.score !== 'function') {\n throw new Error('PDP configuration became unavailable');\n }\n await this._fireFromConfig(\n productPresets,\n synos.score,\n synos.authedFetch,\n generation,\n run,\n synos.scoreMulti\n );\n }\n\n /** Mount each instance into its slot. Shared by config and compose. */\n private async _applyMounts(\n env: MutationEnvelope,\n warnOnMiss = true,\n isCurrent: () => boolean = () => true\n ): Promise<MountOutcome[]> {\n const outcomes: MountOutcome[] = [];\n // The slot containers render reactively from decision state; wait\n // for that pending render so the containers exist before we mount into them.\n await this.updateComplete;\n if (!isCurrent()) return outcomes;\n for (const m of env.mutations) {\n if (!isCurrent()) return outcomes;\n if (m.op !== 'mount') continue;\n const inst = m.instance;\n if (inst.placement.type !== 'slot') continue;\n const slotId = (inst.placement as { type: 'slot'; slot: string }).slot;\n const slotStartedAt = performance.now();\n\n // \u2500\u2500 Native takeover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // If this slot has a native host section OUTSIDE the element, swap into\n // it instead of mounting the element's own widget (the section render\n // loop already skipped rendering our twin). Runs BEFORE the internal\n // container lookup because a suppressed slot has no internal container.\n const nativeKind = NATIVE_KIND_BY_SLOT[slotId];\n if (nativeKind) {\n const native =\n typeof document !== 'undefined' ? findNativeSlot(document, nativeKind) : null;\n if (native && !this.contains(native)) {\n if (!this._isPersonalizedVisitor()) {\n // Fully-unknown visitor \u2014 the host's DEFAULT content is what shows.\n // If a prior (personalized) generation swapped this slot, restore it\n // first: without this, a re-fire that declines to unknown would leave\n // the stale personalized content in the host DOM while claiming the\n // defaults were kept.\n this._nativeHandles.get(slotId)?.restore();\n this._nativeHandles.delete(slotId);\n console.info(`syntro-pdp.native_default_kept: ${slotId}`);\n outcomes.push({ status: 'native-default-kept', slot: slotId });\n continue;\n }\n const fields = nativeFieldsFromContent(nativeKind, inst.content);\n // A container can only carry one live swap \u2014 restore the previous one\n // (e.g. a re-fired generation) before swapping again.\n this._nativeHandles.get(slotId)?.restore();\n this._nativeHandles.delete(slotId);\n const handle = swapNativeSlot(native, fields);\n if (handle) this._nativeHandles.set(slotId, handle);\n outcomes.push({ status: handle ? 'mounted-native' : 'missed', slot: slotId });\n continue;\n }\n }\n\n const container = this.querySelector(`[data-syntro-pdp-slot=\"${slotId}\"]`);\n if (!container) {\n if (warnOnMiss) {\n console.warn(\n `syntro-pdp.mount_miss: no slot container for \"${slotId}\" even after updateComplete.`\n );\n }\n outcomes.push({ status: 'missed', slot: slotId });\n continue;\n }\n const task =\n this._decisionState.plan.status === 'ready'\n ? this._decisionState.plan.plan.tasks.find((candidate) => candidate.slot === slotId)\n : undefined;\n const tag = task ? resolveTagForMount(task) : null;\n if (!tag) {\n outcomes.push({ status: 'ignored', slot: slotId });\n continue;\n }\n\n container.innerHTML = '';\n const el = document.createElement(tag) as HTMLElement & { data?: unknown };\n el.data = inst.content;\n container.appendChild(el);\n\n this._mountDurations.set(slotId, Math.round(performance.now() - slotStartedAt));\n outcomes.push({ status: 'mounted', slot: slotId });\n }\n return outcomes;\n }\n\n /** Render from config: score the archetype \u2192 mount the preset set-values. */\n private async _fireFromConfig(\n productPresets: Record<string, Record<string, SlotPreset>>,\n score: NonNullable<SynOSGlobal['score']>,\n authedFetch: SynOSGlobal['authedFetch'],\n generation: number,\n run: PdpEffectRun,\n scoreMulti?: SynOSGlobal['scoreMulti']\n ): Promise<void> {\n const startedAt = performance.now();\n this._composeStartedAt = startedAt;\n this._mountDurations.clear();\n // Reset the scored axes for this pass \u2014 the wrapped scorers below set them\n // the instant the model resolves, which is BEFORE any mount applies.\n this._resolvedArchetype = 'unknown';\n this._resolvedDecision = 'unknown';\n // `unknown` is the fallback preset key. The classifier adds its own\n // decline option, so it must not appear in the declared enum values.\n const archetypeValues = Object.keys(productPresets).filter((value) => value !== 'unknown');\n const archetypeDescriptions = _readPdpArchetypes() ?? {};\n const decisionDescriptions = _readPdpDecisionArchetypes() ?? {};\n const decisionValues = Object.keys(decisionDescriptions);\n const signals = { ...gatherVisitorSignals(), chat_excerpt: readChatExcerpt() };\n\n try {\n // Set when onScored dispatched the explanation early \u2014 the post-compose\n // dispatch then skips, so the early (score-time) latency metric and\n // content are never overwritten with one that counts live-module time.\n let explanationDispatched = false;\n let scoredArgs: { archetype: string; decisionArchetype: string } | null = null;\n const composeAdapter = createPdpComposeAdapter({\n scoreSummary: () =>\n authedFetch\n ? score({ model_id: 'summary', name: 'behavior_summary', type: 'string', signals })\n : Promise.resolve('unknown'),\n requestDossier: () =>\n authedFetch ? fetchDossier(authedFetch) : Promise.resolve<PdpExplainMetric[]>([]),\n resolveTemplate: (_currentGeneration, composeUnset) =>\n resolveScoredTemplate<PdpPlanEnvelope, MutationEnvelope>({\n // Wrap the scorer so the resolved SPORT archetype lands on the\n // element the instant it scores \u2014 the native-takeover check needs it\n // before the set-slot mounts apply. In the two-axis path scoreMulti\n // runs instead (wrapped below) and this only serves the archetype.\n score: async (spec) => {\n const value = await score(spec);\n this._resolvedArchetype = value;\n return value;\n },\n scoreSpec: {\n model_id: 'llm',\n name: 'archetype',\n type: 'enum',\n values: archetypeValues,\n value_descriptions: Object.fromEntries(\n archetypeValues\n .filter((value) => archetypeDescriptions[value])\n .map((value) => [value, archetypeDescriptions[value]])\n ),\n signals,\n },\n decisionSpec:\n decisionValues.length > 0 && scoreMulti\n ? {\n model_id: 'llm',\n name: 'decision_archetype',\n type: 'enum',\n values: decisionValues,\n value_descriptions: decisionDescriptions,\n signals,\n }\n : undefined,\n // Wrap the batched scorer to capture BOTH axes (sport archetype +\n // decision archetype) at scoring time \u2014 mirrors resolveScoredTemplate's\n // own `?? 'unknown'` normalization so the two never disagree.\n scoreMulti: scoreMulti\n ? async (specs) => {\n const results = await scoreMulti(specs);\n this._resolvedArchetype = results[0] ?? 'unknown';\n this._resolvedDecision = results[1] ?? 'unknown';\n return results;\n }\n : undefined,\n transformPreset: (preset, decisionValue) => ({\n ...preset,\n data: selectPresetData(preset.kind, preset.data, decisionValue),\n }),\n presets: productPresets,\n slotIds: [...PDP_SLOT_IDS],\n orderSlots: orderPdpSlots,\n fallbackValue: 'unknown',\n buildPlan: buildPdpPlan,\n buildMount: (slot: string, preset: SlotPreset): MutationEnvelope => ({\n type: 'syntro.element.mutation',\n schema_version: 'v0',\n mutations: [\n {\n op: 'mount',\n instance: {\n instance_id: `cfg-${slot}`,\n element_type: 'tile',\n template_id: `pdp-${preset.kind}`,\n placement: { type: 'slot', slot },\n content: preset.data as Record<string, unknown>,\n state: 'mounted',\n mounted_by: { agent_id: 'config', turn_id: 'config' },\n mounted_at: new Date().toISOString(),\n version: 1,\n },\n },\n ],\n }),\n emitPlan: (plan) => {\n if (!run.isCurrent()) return;\n run.dispatch({ type: 'plan-received', generation, plan });\n },\n emitMount: async (env) => {\n if (!run.isCurrent()) return;\n const outcomes = await this._applyMounts(env, false, run.isCurrent);\n if (!run.isCurrent()) return;\n for (const outcome of outcomes) {\n if (outcome.status === 'mounted') {\n run.dispatch({ type: 'mount-succeeded', generation, slot: outcome.slot });\n } else if (outcome.status === 'missed') {\n run.dispatch({ type: 'mount-missed', generation, slot: outcome.slot });\n }\n }\n },\n composeUnset: authedFetch ? composeUnset : undefined,\n }),\n requestModule: async ({ slot, archetype, decisionArchetype, behaviorSummary }) => {\n if (!authedFetch || !run.isCurrent()) return;\n const kind = slot.replace('pdp-slot-', '');\n const payload = await composeModule(authedFetch, {\n productName: this.productName,\n productId: this.productId || null,\n kind,\n variant: null,\n archetype: {\n slug: archetype,\n description: archetypeDescriptions[archetype] ?? '',\n },\n decisionArchetype:\n decisionArchetype === 'unknown'\n ? null\n : {\n slug: decisionArchetype,\n description: decisionDescriptions[decisionArchetype] ?? '',\n },\n behaviorSummary,\n signals,\n });\n if (!run.isCurrent()) return;\n if (!payload) {\n run.dispatch({ type: 'module-failed', generation, slot });\n return;\n }\n const outcomes = await this._applyMounts(\n _buildLiveMount(slot, kind, payload),\n false,\n run.isCurrent\n );\n if (!run.isCurrent()) return;\n const outcome = outcomes.find((candidate) => candidate.slot === slot);\n if (outcome?.status === 'missed') {\n run.dispatch({ type: 'mount-missed', generation, slot });\n return;\n }\n if (\n outcome?.status !== 'mounted' &&\n // A native takeover (swap into the host section, or defaults kept) is\n // a success, not a failure \u2014 unreachable on vela today (native slots\n // are always config-SET), but keeps the live path honest if a native\n // slot is ever left unset. The follow-on reasoning dispatch is inert\n // because the native section is suppressed (never rendered).\n outcome?.status !== 'mounted-native' &&\n outcome?.status !== 'native-default-kept'\n ) {\n run.dispatch({ type: 'module-failed', generation, slot });\n return;\n }\n run.dispatch({ type: 'module-resolved', generation, slot });\n const rationale =\n typeof payload.rationale === 'string' && payload.rationale ? payload.rationale : '';\n const text =\n rationale ||\n `Generated live by the ${kind} agent for your \u201C${_titleCase(archetype)}\u201D archetype.`;\n run.dispatch({ type: 'live-reasoning-resolved', generation, slot, text });\n },\n // Why-box early exit (BUG-1783990580): the explanation needs only the\n // scored axes \u2014 surface it the INSTANT the score exists; the behavior\n // summary patches in when it lands (onSummary below), and the live\n // modules never gate it (24\u201385s trailing observed live).\n onScored: ({ archetype, decisionArchetype }) => {\n if (!run.isCurrent() || explanationDispatched) return;\n explanationDispatched = true;\n scoredArgs = { archetype, decisionArchetype };\n const durationMs = performance.now() - startedAt;\n const early = buildExplain({\n archetype,\n description: archetypeDescriptions[archetype] ?? '',\n decisionArchetype,\n decisionDescription: decisionDescriptions[decisionArchetype] ?? '',\n summary: '',\n signals,\n latencyMs: durationMs,\n });\n run.dispatch({\n type: 'explanation-resolved',\n generation,\n explanation: { ...early },\n durationMs,\n });\n run.dispatch({ type: 'dossier-resolved', generation, metrics: early.dossier });\n },\n // The summary line folds into the visible explanation when it lands \u2014\n // same pattern as the Tier-2 dossier fold below.\n onSummary: ({ behaviorSummary }) => {\n if (!run.isCurrent() || !scoredArgs || !behaviorSummary) return;\n const patched = buildExplain({\n archetype: scoredArgs.archetype,\n description: archetypeDescriptions[scoredArgs.archetype] ?? '',\n decisionArchetype: scoredArgs.decisionArchetype,\n decisionDescription: decisionDescriptions[scoredArgs.decisionArchetype] ?? '',\n summary: behaviorSummary,\n signals,\n latencyMs: performance.now() - startedAt,\n });\n run.dispatch({ type: 'explanation-resolved', generation, explanation: { ...patched } });\n },\n });\n\n const composed = await composeAdapter.compose(generation);\n if (!run.isCurrent()) return;\n const result = composed.template;\n this._lastArchetype = result.value;\n if (!explanationDispatched) {\n const explanation = buildExplain({\n archetype: result.value,\n description: archetypeDescriptions[result.value] ?? '',\n decisionArchetype: result.decisionValue,\n decisionDescription: decisionDescriptions[result.decisionValue] ?? '',\n summary: composed.summary === 'unknown' ? '' : composed.summary,\n signals,\n latencyMs: performance.now() - startedAt,\n });\n run.dispatch({\n type: 'explanation-resolved',\n generation,\n explanation: { ...explanation },\n durationMs: performance.now() - startedAt,\n });\n run.dispatch({ type: 'dossier-resolved', generation, metrics: explanation.dossier });\n }\n\n void composed.dossier.then((rows) => {\n if (!rows.length || !run.isCurrent()) return;\n run.dispatch({ type: 'dossier-resolved', generation, metrics: rows });\n });\n\n const archLabel = _titleCase(result.value);\n const decisionLabel =\n result.decisionValue === 'unknown' ? null : _titleCase(result.decisionValue);\n for (const slot of result.setSlots) {\n const text = decisionLabel\n ? `Chosen from the content library \u2014 matched to your \u201C${archLabel}\u201D archetype, showing the items that answer your \u201C${decisionLabel}\u201D question first.`\n : `Chosen from the content library \u2014 matched to your \u201C${archLabel}\u201D archetype.`;\n run.dispatch({ type: 'cached-reasoning-resolved', generation, slot, text });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200);\n throw new Error(message);\n }\n }\n\n /**\n * The demo \"why box\": surfaces the archetype + behavior-summary model outputs\n * and a row of impressive personalization metrics at the very top of the PDP\n * (below the host banner).\n *\n * The NARRATIVE MUST LEAD THE PAGE: a placeholder renders the instant the\n * compose flow starts streaming (before the archetype/summary land), then\n * fills IN PLACE when the decision explanation arrives. Previously it rendered nothing until\n * both models scored, so on a live load it popped in AFTER modules had already\n * mounted \u2014 the reasoning banner trailed the content instead of leading it.\n * Renders nothing only before streaming begins (pre-plan) or once done with no\n * explanation (nothing to say).\n */\n private _renderWhyBox(view: PdpWhyBoxView) {\n if (view.status === 'hidden') return '';\n if (view.status === 'loading') return this._renderWhyBoxLoading();\n const e = view.explanation;\n return html`\n <style>\n /* Token bridge: every color resolves from a --syntro-pdp-* var the\n * host (vela pages + SPA) publishes, falling back to a NEUTRAL host\n * palette elsewhere. No standalone navy/sky billboard \u2014 closed, this\n * reads as a subordinate line of the demo band above it. */\n [data-syntro-pdp-why] {\n margin: 0; padding: 4px 0;\n font-family: var(--syntro-pdp-sans, system-ui, -apple-system, 'Segoe UI', sans-serif);\n color: var(--syntro-pdp-fg, #1a1a1a);\n }\n /* Closed: one quiet muted line \u2014 no card chrome (no bg/border/shadow). */\n /* Collapsed: reads as a LINK, not fine print \u2014 accent color, dotted\n underline (solid on hover), a visible chevron, and a fit-content hit\n area (holistic-alignment pass, 2026-07-18). */\n [data-syntro-pdp-why] > summary {\n cursor: pointer; list-style: none; user-select: none;\n display: inline-flex; align-items: center; gap: 6px; width: fit-content;\n font-size: 12.5px; font-weight: 600;\n color: var(--syntro-pdp-accent, #555);\n text-decoration: underline dotted 1px;\n text-underline-offset: 3px;\n }\n [data-syntro-pdp-why] > summary:hover,\n [data-syntro-pdp-why] > summary:focus-visible {\n text-decoration: underline solid 1px; outline: none;\n }\n [data-syntro-pdp-why] > summary::-webkit-details-marker { display: none; }\n [data-syntro-pdp-why] > summary::after {\n content: '\u25BE'; font-size: 13px; line-height: 1;\n transition: transform 150ms ease;\n }\n [data-syntro-pdp-why][open] > summary::after { transform: rotate(180deg); }\n /* Open: NOTHING moves. The summary line stays exactly where it was\n (no card padding displacing it); the content unfolds BELOW it as a\n quiet text block in the same column \u2014 same left edge, no box, no\n second screen (holistic pass round 2, 2026-07-18). */\n [data-syntro-pdp-why][open] { margin: 0; padding: 0; background: none; border: none; }\n [data-syntro-pdp-why][open] > summary { margin-bottom: 6px; }\n [data-syntro-pdp-why-note] { font-size: 11px; color: var(--syntro-pdp-muted, #666); margin: 0 0 4px; }\n [data-syntro-pdp-why-archetype] { font-size: 13px; margin: 3px 0; color: var(--syntro-pdp-fg, #1a1a1a); }\n [data-syntro-pdp-why-archetype] strong { color: var(--syntro-pdp-primary, #1a1a1a); }\n [data-syntro-pdp-why-summary] {\n font-size: 12.5px; font-style: italic; color: var(--syntro-pdp-muted, #666); margin: 6px 0 8px;\n border-left: 2px solid var(--syntro-pdp-accent, #555); padding-left: 10px;\n }\n /* Metrics: a compact inline row \u2014 value\u00B7label pairs \u2014 not dashboard\n tiles. */\n [data-syntro-pdp-why-metrics] {\n display: flex; flex-wrap: wrap; gap: 4px 20px; margin-top: 6px;\n }\n [data-syntro-pdp-why-metric] { display: inline-flex; align-items: baseline; gap: 6px; }\n [data-syntro-pdp-why-metric-value] {\n font-size: 14px; font-weight: 700; color: var(--syntro-pdp-fg, #1a1a1a); line-height: 1.2;\n font-variant-numeric: tabular-nums;\n }\n [data-syntro-pdp-why-metric-label] {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--syntro-pdp-muted, #666);\n }\n [data-syntro-pdp-why-dossier] { margin-top: 8px; }\n [data-syntro-pdp-why-dossier] > summary {\n cursor: pointer; list-style: none; user-select: none; width: fit-content;\n font-size: 11px; font-weight: 600; color: var(--syntro-pdp-accent, #555);\n text-decoration: underline dotted 1px; text-underline-offset: 3px;\n }\n [data-syntro-pdp-why-dossier] > summary:hover { text-decoration: underline solid 1px; }\n [data-syntro-pdp-why-dossier] > summary::-webkit-details-marker { display: none; }\n [data-syntro-pdp-why-dossier] > summary::before { content: ''; }\n [data-syntro-pdp-why-dossier] > summary::after { content: ' \u25BE'; font-size: 11px; }\n [data-syntro-pdp-why-dossier][open] > summary::after { content: ' \u25B4'; }\n [data-syntro-pdp-why-dossier-list] { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }\n [data-syntro-pdp-why-dossier-row] { display: flex; gap: 12px; align-items: baseline; }\n [data-syntro-pdp-why-dossier-value] {\n flex: 0 0 auto; min-width: 56px; font-size: 13px; font-weight: 700; color: var(--syntro-pdp-fg, #1a1a1a);\n font-variant-numeric: tabular-nums;\n }\n [data-syntro-pdp-why-dossier-text] { display: flex; flex-direction: column; }\n [data-syntro-pdp-why-dossier-label] { font-size: 12px; font-weight: 600; color: var(--syntro-pdp-fg, #1a1a1a); }\n [data-syntro-pdp-why-dossier-hint] { font-size: 11.5px; color: var(--syntro-pdp-muted, #666); line-height: 1.4; }\n </style>\n <details data-syntro-pdp-why>\n <summary>How Syntrologie decided</summary>\n <div data-syntro-pdp-why-note>\n Shown in demo mode only \u2014 a peek under the hood at the live personalization.\n </div>\n <div data-syntro-pdp-why-archetype>\n Shopping as: <strong>${e.archetypeLabel}</strong>${\n e.archetypeDesc ? html` \u2014 ${e.archetypeDesc}` : ''\n }\n </div>\n ${\n e.decisionLabel\n ? html`<div data-syntro-pdp-why-archetype>\n What they're weighing: <strong>${e.decisionLabel}</strong>${\n e.decisionDesc ? html` \u2014 \u201C${e.decisionDesc}\u201D` : ''\n }\n </div>`\n : ''\n }\n ${e.summary ? html`<div data-syntro-pdp-why-summary>\"${e.summary}\"</div>` : ''}\n <div data-syntro-pdp-why-metrics>\n ${e.metrics.map(\n (m) => html`<div data-syntro-pdp-why-metric>\n <span data-syntro-pdp-why-metric-value>${m.value}</span>\n <span data-syntro-pdp-why-metric-label>${m.label}</span>\n </div>`\n )}\n </div>\n ${\n view.dossier.length\n ? html`<details data-syntro-pdp-why-dossier ?open=${view.dossierOpen} @toggle=${this._onDossierToggle}>\n <summary>What was measured</summary>\n <div data-syntro-pdp-why-dossier-list>\n ${view.dossier.map(\n (m) => html`<div data-syntro-pdp-why-dossier-row>\n <span data-syntro-pdp-why-dossier-value>${m.value}</span>\n <div data-syntro-pdp-why-dossier-text>\n <span data-syntro-pdp-why-dossier-label>${m.label}</span>\n ${m.hint ? html`<span data-syntro-pdp-why-dossier-hint>${m.hint}</span>` : ''}\n </div>\n </div>`\n )}\n </div>\n </details>`\n : ''\n }\n </details>\n `;\n }\n\n /**\n * The narrative placeholder: the same quiet \"How Syntrologie decided\" line as\n * the real why box (so it reads as \"the reasoning is loading\", not a different\n * element), with a shimmer bar where the archetype + summary will land.\n * Carries the same `data-syntro-pdp-why` marker plus `data-syntro-pdp-why-loading`,\n * so the swap to the filled box is a state change, not a first appearance.\n */\n private _renderWhyBoxLoading() {\n return html`\n <style>\n [data-syntro-pdp-why] {\n margin: 0; padding: 4px 0;\n font-family: var(--syntro-pdp-sans, system-ui, -apple-system, 'Segoe UI', sans-serif);\n color: var(--syntro-pdp-fg, #1a1a1a);\n }\n /* Same quiet collapsed line as the filled box \u2014 the fill-in is a state\n * change, not a first appearance. The \u25B8 is decorative here (nothing to\n * expand yet). */\n [data-syntro-pdp-why-loading-line] {\n display: inline-flex; align-items: center; gap: 6px; width: fit-content;\n font-size: 12.5px; font-weight: 600;\n color: var(--syntro-pdp-accent, #555);\n text-decoration: underline dotted 1px;\n text-underline-offset: 3px;\n opacity: 0.7;\n }\n [data-syntro-pdp-why-loading-line]::after {\n content: '\u25BE'; font-size: 13px; line-height: 1;\n }\n [data-syntro-pdp-why-shimmer] {\n height: 10px; border-radius: 5px; margin: 8px 0 0;\n background: linear-gradient(90deg, rgba(0,0,0,0.06) 25%, rgba(0,0,0,0.12) 37%, rgba(0,0,0,0.06) 63%);\n background-size: 400% 100%;\n animation: syntro-pdp-why-shimmer 1.4s ease infinite;\n }\n [data-syntro-pdp-why-shimmer][data-w='60'] { width: 60%; }\n @keyframes syntro-pdp-why-shimmer {\n 0% { background-position: 100% 0; }\n 100% { background-position: 0 0; }\n }\n </style>\n <div data-syntro-pdp-why data-syntro-pdp-why-loading aria-busy=\"true\">\n <span data-syntro-pdp-why-loading-line>How Syntrologie decided</span>\n <div data-syntro-pdp-why-shimmer data-w=\"60\"></div>\n </div>\n `;\n }\n\n override render() {\n // WI-6b fail-closed: a self-identifying element on a page that resolves\n // to no product renders NOTHING \u2014 no root, no skeleton, no broken box.\n if (this._pageUnresolvable) return html``;\n\n // WI-4 fail-closed: presets delivery has SETTLED (config fetch done or\n // none configured) and this product still has no preset row \u2014 nothing\n // will ever compose, so render NOTHING instead of an eternal skeleton.\n // Lazy store read: a late legacy global re-enters through the reactive\n // sync path (runtimeRef arrival / reconnect), which starts the compose\n // and moves the stream out of idle before this guard matters.\n if (\n this._presetsSettled &&\n this._decisionState.stream.status === 'idle' &&\n this._decisionState.plan.status === 'empty' &&\n !_readPdpPresets()?.[this.productId]\n ) {\n return html``;\n }\n\n // Inline-styled skeleton so the SDK ships its own loading affordance\n // without depending on host-page CSS. Light DOM is fine here \u2014 the\n // selectors are namespaced to `data-syntro-pdp-skeleton` so they\n // can't collide with customer styles.\n const skeletonStyles = SyntroPdpLit._skeletonStyles;\n const view = selectPdpView(this._decisionState);\n\n if (view.layout === 'skeleton') {\n // Pre-plan: render 3 generic section skeletons so the page reads\n // as \"loading personalized content\" instead of \"blank below the\n // fold.\" Counts/heights are tuned to roughly match the live\n // sections we expect (header + content frame).\n return html`\n ${skeletonStyles}\n <div data-syntro-pdp-root data-state=\"pending\" data-syntro-pdp-skeleton>\n ${[0, 1, 2].map(\n (i) => html`\n <section\n data-syntro-pdp-skeleton-section\n style=\"animation-delay: ${i * 0.15}s\"\n >\n <div data-syntro-pdp-skeleton-header>\n <div data-syntro-pdp-skeleton-eyebrow></div>\n <div data-syntro-pdp-skeleton-title></div>\n <div data-syntro-pdp-skeleton-subtitle></div>\n </div>\n <div data-syntro-pdp-skeleton-body></div>\n </section>\n `\n )}\n </div>\n `;\n }\n\n return html`\n ${skeletonStyles}\n <div data-syntro-pdp-root data-state=${view.rootState}>\n ${view.whyBox.destination === 'inline' ? this._renderWhyBox(view.whyBox) : ''}\n ${view.slots.map((slot) => {\n if (!slot.visible) return '';\n const task = slot.task;\n // Native takeover: the host renders its OWN qa/peer-feed section, so\n // never render a duplicate \u2014 the mount path swaps into the host one.\n if (this._nativeSlots.has(task.slot)) return '';\n return html`\n <section data-syntro-pdp-section data-slot=${task.slot}>\n <syntro-pdp-section-header\n .eyebrow=${task.header.eyebrow}\n .title=${task.header.title}\n .accent=${task.header.accent ?? ''}\n .subtitle=${task.header.subtitle ?? ''}\n ></syntro-pdp-section-header>\n <div data-syntro-pdp-section-inner>\n <div\n data-syntro-pdp-slot=${task.slot}\n data-slot-state=${slot.status}\n ></div>\n ${\n /*\n * Per-slot shimmer rendered as a SIBLING of the slot\n * container, NOT a child. `onMutation` clears the slot\n * via `container.innerHTML = ''` and `appendChild` to\n * mount the real module \u2014 that imperative DOM op would\n * collide with Lit's child-part markers if the shimmer\n * lived inside the slot. As a sibling, Lit owns the\n * shimmer's full lifecycle (mount / unmount on state\n * change) without ever touching the imperatively-managed\n * slot subtree. See adversarial-review failure mode #2.\n */\n slot.showLoading\n ? html`<div data-syntro-pdp-slot-shimmer>\n <div data-syntro-pdp-skeleton-body></div>\n </div>`\n : ''\n }\n </div>\n </section>\n `;\n })}\n </div>\n `;\n }\n\n /**\n * Self-contained skeleton (shimmer) CSS. Rendered inside the component\n * during the pre-plan / streaming phase. The global section-layout +\n * per-slot accent rules live in the head-injected `SECTION_LAYOUT_CSS`\n * (see `ensureSectionLayoutStyles`), not here, so they're emitted once\n * per document rather than once per `<syntro-pdp>` instance.\n */\n private static readonly _skeletonStyles = html`\n <style>\n [data-syntro-pdp-skeleton] {\n display: flex;\n flex-direction: column;\n gap: var(--sc-pdp-skeleton-gap, 32px);\n padding: var(--sc-pdp-skeleton-pad, 16px 0);\n }\n [data-syntro-pdp-skeleton-section] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n opacity: 0;\n animation: sc-pdp-skel-fade 0.4s ease forwards;\n }\n [data-syntro-pdp-skeleton-header] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n [data-syntro-pdp-skeleton-eyebrow],\n [data-syntro-pdp-skeleton-title],\n [data-syntro-pdp-skeleton-subtitle],\n [data-syntro-pdp-skeleton-body] {\n border-radius: var(--sc-pdp-skeleton-radius, 8px);\n background: linear-gradient(\n 90deg,\n var(--sc-pdp-skeleton-base, rgba(0, 0, 0, 0.06)) 0%,\n var(--sc-pdp-skeleton-shine, rgba(0, 0, 0, 0.11)) 50%,\n var(--sc-pdp-skeleton-base, rgba(0, 0, 0, 0.06)) 100%\n );\n background-size: 200% 100%;\n animation: sc-pdp-skel-shimmer 1.4s ease-in-out infinite;\n }\n [data-syntro-pdp-skeleton-eyebrow] {\n height: 10px;\n width: 35%;\n }\n [data-syntro-pdp-skeleton-title] {\n height: 22px;\n width: 75%;\n }\n [data-syntro-pdp-skeleton-subtitle] {\n height: 12px;\n width: 55%;\n }\n [data-syntro-pdp-skeleton-body] {\n height: var(--sc-pdp-skeleton-body-h, 140px);\n width: 100%;\n margin-top: 4px;\n }\n @keyframes sc-pdp-skel-shimmer {\n 0% {\n background-position: 100% 0;\n }\n 100% {\n background-position: -100% 0;\n }\n }\n @keyframes sc-pdp-skel-fade {\n to {\n opacity: 1;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-syntro-pdp-skeleton-eyebrow],\n [data-syntro-pdp-skeleton-title],\n [data-syntro-pdp-skeleton-subtitle],\n [data-syntro-pdp-skeleton-body],\n [data-syntro-pdp-skeleton-section] {\n animation: none;\n opacity: 1;\n }\n }\n </style>\n `;\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp')) {\n customElements.define('syntro-pdp', SyntroPdpLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp': SyntroPdpLit;\n }\n}\n", "export type PdpConnection = 'connected' | 'disconnected';\n\nexport type PdpActivationState =\n | { status: 'waiting-runtime' }\n | { status: 'waiting-app' }\n | { status: 'active' };\n\nexport type PdpStreamState =\n | { status: 'idle' }\n | { status: 'streaming' }\n | { status: 'done' }\n | { status: 'error'; message: string };\n\nexport interface PdpModuleHeader {\n eyebrow: string;\n title: string;\n accent: string | null;\n subtitle: string | null;\n}\n\nexport interface PdpPlanTask {\n slot: string;\n kind: 'trending-news' | 'chart' | 'qa' | 'regional' | 'peer-feed' | 'other-products';\n variant: string | null;\n header: PdpModuleHeader;\n}\n\nexport interface PdpPlan {\n rationale: string;\n tasks: PdpPlanTask[];\n}\n\nexport type PdpPlanState = { status: 'empty' } | { status: 'ready'; plan: PdpPlan };\n\nexport type PdpSlotStatus = 'pending' | 'mounted' | 'failed';\nexport type PdpSlotState = Record<string, PdpSlotStatus>;\n\nexport interface PdpExplainMetric {\n value: string;\n label: string;\n hint?: string;\n}\n\nexport interface PdpExplanation {\n archetypeLabel: string;\n archetypeDesc: string;\n decisionLabel: string | null;\n decisionDesc: string | null;\n summary: string;\n metrics: PdpExplainMetric[];\n}\n\nexport type PdpExplanationState =\n | { status: 'empty' }\n | { status: 'loading' }\n | { status: 'ready'; explanation: PdpExplanation };\n\nexport interface PdpSlotReasoning {\n source: 'cached' | 'live';\n text: string;\n}\n\nexport type PdpReasoningState = Record<string, PdpSlotReasoning>;\n\nexport type PdpDossierState =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'ready'; metrics: PdpExplainMetric[] }\n | { status: 'error'; message: string };\n\nexport interface PdpState {\n generation: number;\n connection: PdpConnection;\n activation: PdpActivationState;\n stream: PdpStreamState;\n plan: PdpPlanState;\n slots: PdpSlotState;\n explanation: PdpExplanationState;\n reasoning: PdpReasoningState;\n dossier: PdpDossierState;\n dossierOpen: boolean;\n portalPresent: boolean;\n}\n\nexport type PdpEvent =\n | {\n type: 'runtime-updated';\n runtimeAvailable: boolean;\n appActive: boolean;\n configured: boolean;\n }\n | { type: 'plan-received'; generation: number; plan: PdpPlan }\n | { type: 'mount-succeeded'; generation: number; slot: string }\n | { type: 'mount-missed'; generation: number; slot: string }\n | { type: 'module-resolved'; generation: number; slot: string }\n | { type: 'module-failed'; generation: number; slot: string }\n | { type: 'cached-reasoning-resolved'; generation: number; slot: string; text: string }\n | { type: 'live-reasoning-resolved'; generation: number; slot: string; text: string }\n | {\n type: 'explanation-resolved';\n generation: number;\n explanation: PdpExplanation;\n durationMs?: number;\n }\n | { type: 'dossier-resolved'; generation: number; metrics: PdpExplainMetric[] }\n | { type: 'compose-succeeded'; generation: number }\n | { type: 'compose-failed'; generation: number; message: string }\n | { type: 'portal-updated'; present: boolean }\n | { type: 'dossier-toggled'; open: boolean }\n | { type: 'disconnected' }\n | {\n type: 'reconnected';\n runtimeAvailable: boolean;\n appActive: boolean;\n configured: boolean;\n };\n\nexport type PdpEffect =\n | { type: 'start-compose'; generation: number }\n | { type: 'capture-telemetry'; name: string; props: Record<string, unknown> }\n | { type: 'warn'; code: string; props: Record<string, unknown> }\n | { type: 'cleanup' };\n\nexport interface PdpTransition {\n state: PdpState;\n effects: PdpEffect[];\n}\n\nexport interface PdpSlotView {\n task: PdpPlanTask;\n status: PdpSlotStatus;\n visible: boolean;\n showLoading: boolean;\n reasoning: PdpSlotReasoning | null;\n}\n\nexport type PdpWhyBoxView =\n | { status: 'hidden'; destination: null }\n | { status: 'loading'; destination: 'inline' | 'portal' }\n | {\n status: 'ready';\n destination: 'inline' | 'portal';\n explanation: PdpExplanation;\n dossier: PdpExplainMetric[];\n dossierOpen: boolean;\n };\n\nexport type PdpTerminalView =\n | { status: 'none' }\n | { status: 'done' }\n | { status: 'error'; message: string };\n\nexport interface PdpView {\n layout: 'skeleton' | 'content';\n rootState: 'pending' | PdpStreamState['status'];\n slots: PdpSlotView[];\n whyBox: PdpWhyBoxView;\n terminal: PdpTerminalView;\n}\n\nexport function createInitialPdpState(generation = 0): PdpState {\n return {\n generation,\n connection: 'connected',\n activation: { status: 'waiting-runtime' },\n stream: { status: 'idle' },\n plan: { status: 'empty' },\n slots: {},\n explanation: { status: 'empty' },\n reasoning: {},\n dossier: { status: 'idle' },\n dossierOpen: true,\n portalPresent: false,\n };\n}\n\nexport function selectPdpView(state: PdpState): PdpView {\n const terminal: PdpTerminalView =\n state.stream.status === 'done'\n ? { status: 'done' }\n : state.stream.status === 'error'\n ? { status: 'error', message: state.stream.message }\n : { status: 'none' };\n\n const destination = state.portalPresent ? 'portal' : 'inline';\n const canRenderWhyBox = state.plan.status === 'ready' || state.portalPresent;\n const whyBox: PdpWhyBoxView =\n canRenderWhyBox && state.explanation.status === 'ready'\n ? {\n status: 'ready',\n destination,\n explanation: state.explanation.explanation,\n dossier: state.dossier.status === 'ready' ? state.dossier.metrics : [],\n dossierOpen: state.dossierOpen,\n }\n : canRenderWhyBox && state.stream.status === 'streaming'\n ? { status: 'loading', destination }\n : { status: 'hidden', destination: null };\n\n if (state.plan.status === 'empty') {\n return {\n layout: 'skeleton',\n rootState: 'pending',\n slots: [],\n whyBox,\n terminal,\n };\n }\n\n return {\n layout: 'content',\n rootState: state.stream.status,\n slots: state.plan.plan.tasks.map((task) => {\n const status = state.slots[task.slot] ?? 'pending';\n return {\n task,\n status,\n visible: status !== 'failed',\n showLoading: status === 'pending',\n reasoning: state.reasoning[task.slot] ?? null,\n };\n }),\n whyBox,\n terminal,\n };\n}\n\nfunction unchanged(state: PdpState): PdpTransition {\n return { state, effects: [] };\n}\n\nfunction telemetry(name: string, props: Record<string, unknown>): PdpEffect {\n return { type: 'capture-telemetry', name, props };\n}\n\nfunction withActivation(state: PdpState, status: PdpActivationState['status']): PdpTransition {\n return state.activation.status === status\n ? unchanged(state)\n : { state: { ...state, activation: { status } }, effects: [] };\n}\n\nfunction startCompose(state: PdpState, generation: number): PdpTransition {\n return {\n state: {\n ...createInitialPdpState(generation),\n activation: { status: 'active' },\n stream: { status: 'streaming' },\n dossierOpen: state.dossierOpen,\n portalPresent: state.portalPresent,\n },\n effects: [{ type: 'start-compose', generation }],\n };\n}\n\nfunction applyRuntimeStatus(\n state: PdpState,\n event: Extract<PdpEvent, { type: 'runtime-updated' | 'reconnected' }>,\n generation: number\n): PdpTransition {\n if (state.stream.status !== 'idle') return unchanged(state);\n if (!event.runtimeAvailable) return withActivation(state, 'waiting-runtime');\n if (!event.appActive) return withActivation(state, 'waiting-app');\n if (!event.configured) {\n if (state.activation.status === 'active' && state.stream.status === 'idle') {\n return unchanged(state);\n }\n return {\n state: { ...state, activation: { status: 'active' } },\n effects: [{ type: 'warn', code: 'missing-config', props: {} }],\n };\n }\n return startCompose(state, generation);\n}\n\nfunction taskForSlot(state: PdpState, slot: string): PdpPlanTask | undefined {\n return state.plan.status === 'ready'\n ? state.plan.plan.tasks.find((task) => task.slot === slot)\n : undefined;\n}\n\nfunction markSlotMounted(state: PdpState, slot: string): PdpTransition {\n if (state.slots[slot] !== 'pending') return unchanged(state);\n const task = taskForSlot(state, slot);\n if (!task) return unchanged(state);\n return {\n state: { ...state, slots: { ...state.slots, [slot]: 'mounted' } },\n effects: [\n telemetry('syntro_pdp_section_mounted', {\n slot,\n kind: task.kind,\n }),\n ],\n };\n}\n\nfunction setSlotReasoning(\n state: PdpState,\n slot: string,\n source: PdpSlotReasoning['source'],\n text: string\n): PdpTransition {\n if (state.slots[slot] !== 'mounted' || state.reasoning[slot] || text.length === 0) {\n return unchanged(state);\n }\n return {\n state: {\n ...state,\n reasoning: { ...state.reasoning, [slot]: { source, text } },\n },\n effects: [],\n };\n}\n\nexport function transitionPdp(state: PdpState, event: PdpEvent): PdpTransition {\n if (event.type === 'runtime-updated') {\n if (state.connection !== 'connected') return unchanged(state);\n return applyRuntimeStatus(state, event, state.generation + 1);\n }\n\n if (event.type === 'disconnected') {\n if (state.connection === 'disconnected') return unchanged(state);\n const generation = state.generation + 1;\n return {\n state: {\n ...createInitialPdpState(generation),\n connection: 'disconnected',\n dossierOpen: state.dossierOpen,\n portalPresent: state.portalPresent,\n },\n effects: [{ type: 'cleanup' }],\n };\n }\n\n if (event.type === 'reconnected') {\n if (state.connection === 'connected') return unchanged(state);\n const connected = { ...state, connection: 'connected' as const };\n return applyRuntimeStatus(connected, event, state.generation);\n }\n\n if (event.type === 'portal-updated') {\n return state.portalPresent === event.present\n ? unchanged(state)\n : { state: { ...state, portalPresent: event.present }, effects: [] };\n }\n\n if (event.type === 'dossier-toggled') {\n return state.dossierOpen === event.open\n ? unchanged(state)\n : { state: { ...state, dossierOpen: event.open }, effects: [] };\n }\n\n if (event.generation !== state.generation || state.connection !== 'connected') {\n return unchanged(state);\n }\n const acceptsDoneDossier = event.type === 'dossier-resolved' && state.stream.status === 'done';\n if (state.stream.status !== 'streaming' && !acceptsDoneDossier) return unchanged(state);\n\n switch (event.type) {\n case 'plan-received': {\n if (state.plan.status === 'ready') return unchanged(state);\n const slots: PdpSlotState = {};\n for (const task of event.plan.tasks) slots[task.slot] = 'pending';\n return {\n state: { ...state, plan: { status: 'ready', plan: event.plan }, slots },\n effects: [telemetry('syntro_pdp_plan_received', { task_count: event.plan.tasks.length })],\n };\n }\n case 'mount-succeeded':\n case 'module-resolved':\n return markSlotMounted(state, event.slot);\n case 'mount-missed':\n return state.slots[event.slot] === 'pending'\n ? {\n state,\n effects: [{ type: 'warn', code: 'mount-miss', props: { slot: event.slot } }],\n }\n : unchanged(state);\n case 'module-failed':\n return state.slots[event.slot] === 'pending'\n ? {\n state: { ...state, slots: { ...state.slots, [event.slot]: 'failed' } },\n effects: [],\n }\n : unchanged(state);\n case 'cached-reasoning-resolved':\n return setSlotReasoning(state, event.slot, 'cached', event.text);\n case 'live-reasoning-resolved':\n return setSlotReasoning(state, event.slot, 'live', event.text);\n case 'explanation-resolved': {\n // The summary patch re-dispatches this event; only the FIRST resolution\n // is the user-visible fill worth instrumenting (why-box latency audit\n // 2026-07-14).\n const firstResolution = state.explanation.status !== 'ready';\n return {\n state: {\n ...state,\n explanation: { status: 'ready', explanation: event.explanation },\n },\n effects:\n firstResolution && event.durationMs !== undefined\n ? [\n telemetry('syntro_pdp_explanation_resolved', {\n duration_ms: Math.round(event.durationMs),\n }),\n ]\n : [],\n };\n }\n case 'dossier-resolved':\n if (event.metrics.length === 0) return unchanged(state);\n return {\n state: {\n ...state,\n dossier: {\n status: 'ready',\n metrics:\n state.dossier.status === 'ready'\n ? [...state.dossier.metrics, ...event.metrics]\n : event.metrics,\n },\n },\n effects: [],\n };\n case 'compose-succeeded':\n return {\n state: { ...state, stream: { status: 'done' } },\n effects: [telemetry('syntro_pdp_done', { source: 'config' })],\n };\n case 'compose-failed':\n return {\n state: { ...state, stream: { status: 'error', message: event.message } },\n effects: [telemetry('syntro_pdp_error', { message: event.message, source: 'config' })],\n };\n }\n}\n", "import type { ScoredTemplateResult, SlotPreset } from '../scoring/scored-template';\nimport type { PdpExplainMetric } from './pdp';\n\nexport interface PdpModuleHeaderClient {\n eyebrow: string;\n title: string;\n accent: string | null;\n subtitle: string | null;\n}\n\nexport interface PdpPlanTaskClient {\n slot: string;\n kind: 'trending-news' | 'chart' | 'qa' | 'regional' | 'peer-feed' | 'other-products';\n variant: string | null;\n header: PdpModuleHeaderClient;\n}\n\nexport interface PdpPlanEnvelope {\n rationale: string;\n tasks: PdpPlanTaskClient[];\n}\n\n/**\n * Fixed PDP slot set and canonical live-slot order. Configured slots lead in\n * this order; remaining live slots keep stable page positions regardless of\n * response latency, so heavy modules cannot leapfrog lighter content.\n */\nexport const PDP_SLOT_IDS = [\n 'pdp-slot-qa',\n 'pdp-slot-trending-news',\n 'pdp-slot-peer-feed',\n 'pdp-slot-chart',\n 'pdp-slot-regional',\n 'pdp-slot-other-products',\n] as const;\n\nconst LIVE_SLOT_ORDER: readonly string[] = PDP_SLOT_IDS;\n\nconst LIVE_SLOT_HEADERS: Record<string, { eyebrow: string; title: string }> = {\n qa: { eyebrow: 'Answers', title: 'Questions like yours' },\n 'trending-news': { eyebrow: 'In the news', title: 'Recent research' },\n 'peer-feed': { eyebrow: 'From people like you', title: 'What others say' },\n chart: { eyebrow: 'Formulation', title: \"What's inside\" },\n regional: { eyebrow: 'In your area', title: 'Regional trend' },\n 'other-products': { eyebrow: 'Complete your stack', title: 'Others also take' },\n};\n\nconst FALLBACK_HEADER = { eyebrow: '', title: '' };\n\nexport function orderPdpSlots(slotIds: string[], isSet: (slot: string) => boolean): string[] {\n const configured = slotIds.filter((slot) => isSet(slot));\n const live = slotIds\n .filter((slot) => !isSet(slot))\n .sort((a, b) => LIVE_SLOT_ORDER.indexOf(a) - LIVE_SLOT_ORDER.indexOf(b));\n return [...configured, ...live];\n}\n\nexport function buildPdpPlan(\n slots: Array<{ slot: string; preset: SlotPreset | undefined }>\n): PdpPlanEnvelope {\n return {\n rationale: '',\n tasks: slots.map(({ slot, preset }) => {\n const kind = preset ? preset.kind : slot.replace('pdp-slot-', '');\n const header = preset ? preset.header : (LIVE_SLOT_HEADERS[kind] ?? FALLBACK_HEADER);\n return {\n slot,\n kind: kind as PdpPlanTaskClient['kind'],\n variant: preset?.variant ?? null,\n header: header as PdpModuleHeaderClient,\n };\n }),\n };\n}\n\nexport interface PdpComposeModuleRequest {\n generation: number;\n slot: string;\n archetype: string;\n decisionArchetype: string;\n behaviorSummary: string;\n}\n\nexport type PdpComposeUnset = (\n unsetSlots: string[],\n archetype: string,\n decisionArchetype?: string\n) => Promise<void>;\n\nexport interface PdpComposePorts {\n scoreSummary(generation: number): Promise<string>;\n requestDossier(generation: number): Promise<PdpExplainMetric[]>;\n resolveTemplate(generation: number, composeUnset: PdpComposeUnset): Promise<ScoredTemplateResult>;\n requestModule(request: PdpComposeModuleRequest): Promise<void> | void;\n /** Early exit for the scored axes \u2014 fired the INSTANT the score exists,\n * before the behavior summary resolves and before the live modules are\n * awaited, so the why-box never trails either (BUG-1783990580; the\n * summary alone was worth ~0.5s). Only invoked when live slots exist;\n * the all-preset path resolves compose() fast anyway. */\n onScored?(args: { generation: number; archetype: string; decisionArchetype: string }): void;\n\n /** Follow-up to onScored: the behavior summary, when it lands. Consumers\n * patch it into the already-visible explanation (dossier-fold pattern). */\n onSummary?(args: { generation: number; behaviorSummary: string }): void;\n}\n\nexport interface PdpComposeResult {\n template: ScoredTemplateResult;\n summary: string;\n dossier: Promise<PdpExplainMetric[]>;\n}\n\nexport interface PdpComposeAdapter {\n compose(generation: number): Promise<PdpComposeResult>;\n}\n\nfunction startFailSoft<T>(operation: () => Promise<T> | T, fallback: T): Promise<T> {\n try {\n return Promise.resolve(operation()).catch(() => fallback);\n } catch {\n return Promise.resolve(fallback);\n }\n}\n\nexport function createPdpComposeAdapter(ports: PdpComposePorts): PdpComposeAdapter {\n const compose = async (generation: number): Promise<PdpComposeResult> => {\n const summaryPromise = startFailSoft(() => ports.scoreSummary(generation), 'unknown');\n const dossier = startFailSoft(() => ports.requestDossier(generation), []);\n\n const template = await ports.resolveTemplate(\n generation,\n async (unsetSlots, archetype, decisionArchetype = 'unknown') => {\n try {\n ports.onScored?.({ generation, archetype, decisionArchetype });\n } catch {\n // The early explanation is best-effort; never break composition.\n }\n const summary = await summaryPromise;\n const behaviorSummary = summary === 'unknown' ? '' : summary;\n try {\n ports.onSummary?.({ generation, behaviorSummary });\n } catch {\n // Summary patch is best-effort; never break composition.\n }\n await Promise.all(\n unsetSlots.map((slot) =>\n startFailSoft(\n () =>\n ports.requestModule({\n generation,\n slot,\n archetype,\n decisionArchetype,\n behaviorSummary,\n }),\n undefined\n )\n )\n );\n }\n );\n\n return {\n template,\n summary: await summaryPromise,\n dossier,\n };\n };\n\n return { compose };\n}\n", "import type { PdpEffect, PdpEvent } from './pdp';\n\nexport interface PdpEffectRun {\n dispatch(event: PdpEvent): boolean;\n isCurrent(): boolean;\n}\n\nexport interface PdpEffectPorts {\n dispatch(event: PdpEvent): void;\n startCompose(generation: number, run: PdpEffectRun): Promise<void> | void;\n captureTelemetry(name: string, props: Record<string, unknown>): Promise<void> | void;\n warn(code: string, props: Record<string, unknown>): Promise<void> | void;\n cleanup(): Promise<void> | void;\n}\n\nexport interface PdpEffectExecutor {\n execute(effect: PdpEffect): Promise<void>;\n}\n\nexport function createPdpEffectExecutor(ports: PdpEffectPorts): PdpEffectExecutor {\n let lifecycle = 0;\n\n const execute = async (effect: PdpEffect): Promise<void> => {\n try {\n switch (effect.type) {\n case 'start-compose': {\n lifecycle += 1;\n const startedIn = lifecycle;\n const run: PdpEffectRun = {\n isCurrent: () => startedIn === lifecycle,\n dispatch: (event) => {\n if (startedIn !== lifecycle) return false;\n try {\n ports.dispatch(event);\n return true;\n } catch {\n return false;\n }\n },\n };\n try {\n await ports.startCompose(effect.generation, run);\n } catch (error) {\n run.dispatch({\n type: 'compose-failed',\n generation: effect.generation,\n message:\n error instanceof Error && error.message\n ? error.message\n : typeof error === 'string' && error\n ? error\n : 'PDP compose failed',\n });\n return;\n }\n run.dispatch({ type: 'compose-succeeded', generation: effect.generation });\n return;\n }\n case 'capture-telemetry':\n await ports.captureTelemetry(effect.name, effect.props);\n return;\n case 'warn':\n await ports.warn(effect.code, effect.props);\n return;\n case 'cleanup':\n lifecycle += 1;\n await ports.cleanup();\n return;\n }\n } catch {\n // Host integrations must not crash the widget orchestration path.\n }\n };\n\n return { execute };\n}\n", "/**\n * Native slot swap \u2014 the design-agnostic content-swap engine (FEAT-1784346204).\n *\n * A host page renders its OWN FAQ/review sections and marks them with the\n * data-attribute contract below. Personalization never re-renders the design:\n * we clone one of the host's existing items as the template, fill its\n * `[data-syntro-field]` text nodes, and replace the item set. The swapped\n * section is literally the host's DOM shape. `restore()` reattaches the\n * ORIGINAL nodes (same references \u2014 a React host may hold refs to them).\n */\n\nexport const NATIVE_SLOT_ATTR = 'data-syntro-slot';\nexport const NATIVE_ITEM_ATTR = 'data-syntro-item';\nexport const NATIVE_FIELD_ATTR = 'data-syntro-field';\n/** Same string as adaptive-reviews' PRODUCT_ID_ATTRIBUTE \u2014 kept as an\n * independent constant so adaptive-product gains no cross-package dep. */\nexport const NATIVE_PRODUCT_ATTR = 'data-syntro-product-id';\n\nexport type NativeSlotKind = 'qa' | 'peer-feed';\nexport type NativeItemFields = Record<string, string>;\n\nexport interface NativeSwapHandle {\n container: HTMLElement;\n restore(): void;\n}\n\n/** Track actively-swapped containers to prevent double-swap corruption. */\nconst activeSwaps = new WeakSet<HTMLElement>();\n\nexport function findNativeSlot(root: ParentNode, kind: NativeSlotKind): HTMLElement | null {\n return root.querySelector<HTMLElement>(`[${NATIVE_SLOT_ATTR}=\"${kind}\"]`);\n}\n\nexport function nativeProductId(container: HTMLElement): string | null {\n return (\n container.closest<HTMLElement>(`[${NATIVE_PRODUCT_ATTR}]`)?.getAttribute(NATIVE_PRODUCT_ATTR) ??\n null\n );\n}\n\n/**\n * PDP slot id \u2192 native-section kind. Only these two slots have a native (host\n * server-rendered) counterpart; every other slot renders as usual. Shared by\n * `SyntroPdpLit` (page-owning element) and `nativeSlotWatcher` (SPA modal).\n */\nexport const NATIVE_KIND_BY_SLOT: Record<string, NativeSlotKind> = {\n 'pdp-slot-qa': 'qa',\n 'pdp-slot-peer-feed': 'peer-feed',\n};\n\n/**\n * Map a mounted instance's `content` payload to the flat `[field \u2192 text]` items\n * the native swap engine fills. qa \u2192 {question, answer}; peer-feed maps the\n * review shape onto the host's field names (quote\u2192text, persona\u2192reviewer,\n * time_ago\u2192meta). Malformed/absent lists degrade to an empty item set (the swap\n * engine then no-ops and leaves the host defaults).\n */\nexport function nativeFieldsFromContent(\n kind: NativeSlotKind,\n content: Record<string, unknown>\n): NativeItemFields[] {\n if (kind === 'qa') {\n const items = Array.isArray(content.items)\n ? (content.items as Array<{ question?: unknown; answer?: unknown }>)\n : [];\n return items.map((item) => ({\n question: String(item.question ?? ''),\n answer: String(item.answer ?? ''),\n }));\n }\n const peers = Array.isArray(content.peers)\n ? (content.peers as Array<{ quote?: unknown; persona?: unknown; time_ago?: unknown }>)\n : [];\n return peers.map((peer) => ({\n text: String(peer.quote ?? ''),\n reviewer: String(peer.persona ?? ''),\n meta: String(peer.time_ago ?? ''),\n }));\n}\n\nexport function swapNativeSlot(\n container: HTMLElement,\n items: NativeItemFields[]\n): NativeSwapHandle | null {\n if (activeSwaps.has(container)) {\n console.warn('syntro-pdp.native_swap_skipped: container already swapped \u2014 restore() first');\n return null;\n }\n const originals = [...container.querySelectorAll<HTMLElement>(`[${NATIVE_ITEM_ATTR}]`)];\n if (originals.length === 0 || items.length === 0) {\n console.warn(\n `syntro-pdp.native_swap_skipped: ${originals.length === 0 ? 'no template item in container' : 'no items to swap in'}`\n );\n return null;\n }\n const template = originals[0];\n const anchor = document.createComment('syntro-native-slot');\n template.before(anchor);\n for (const original of originals) original.remove();\n\n const clones = items.map((fields) => {\n const clone = template.cloneNode(true) as HTMLElement;\n for (const [name, value] of Object.entries(fields)) {\n const target = clone.querySelector<HTMLElement>(`[${NATIVE_FIELD_ATTR}=\"${name}\"]`);\n if (target) target.textContent = value;\n }\n return clone;\n });\n anchor.after(...clones);\n\n activeSwaps.add(container);\n\n return {\n container,\n restore: () => {\n for (const clone of clones) clone.remove();\n anchor.after(...originals);\n anchor.remove();\n activeSwaps.delete(container);\n },\n };\n}\n", "/**\n * Scored-template orchestration for the PDP \u2014 score \u2192 resolve presets \u2192 reserve\n * every slot box \u2192 fill the cached (set) slots from config \u2192 stream the rest.\n *\n * Lives in the adaptive (not runtime-sdk): it's the PDP's composition logic, used\n * by exactly one adaptive, and pure. The runtime stays thin (`score` +\n * `authedFetch`). It was previously parked in runtime-sdk and handed over via\n * `window.SynOS` ONLY to dodge the adaptive\u2192runtime import cycle \u2014 but the logic\n * is adaptive-domain (presets, slots, plan, mounts), so it belongs here.\n */\n\n// DERIVED from the runtime-backend contract (single source of truth), same as\n// runtime-sdk's score-client \u2014 so the two ScoreSpecs can't drift. A Pydantic\n// change to ScoreRequest/ScoreOutputSpec breaks this compile.\n// See docs/adr/2026-07-05-runtime-backend-generated-types.md.\nimport type {\n ScoreOutputSpec as BackendScoreOutputSpec,\n ScoreRequest as BackendScoreRequest,\n} from '@syntrologie/runtime-contracts';\n\n/** What an element declares it wants scored (the runtime `score` primitive's input). */\nexport interface ScoreSpec {\n model_id: BackendScoreRequest['model_id'];\n name: BackendScoreOutputSpec['name'];\n type: BackendScoreOutputSpec['type'];\n values?: BackendScoreOutputSpec['values'];\n /**\n * Per-enum-value descriptions passed to the classifier (\"label \u2014 description\").\n * Feeds the model what each archetype MEANS so it doesn't classify opaque slugs\n * and give up to `unknown`. Forwarded verbatim to the `/api/score` output block.\n */\n value_descriptions?: BackendScoreOutputSpec['value_descriptions'];\n signals?: BackendScoreRequest['signals'];\n}\n\n/** One config-set slot value: the widget kind, an optional header, and its content. */\nexport interface SlotPreset {\n kind: string;\n /** Optional variant layout key (e.g. `accordion-multi`) \u2014 selects the\n * dedicated variant widget via `TEMPLATE_TO_TAG` (`<kind>/<variant>`).\n * Absent/null mounts the base widget. */\n variant?: string | null;\n header?: unknown;\n data: unknown;\n}\n\n/** `presets[scoredValue][slotId] = SlotPreset` \u2014 the config's SET values. */\nexport type PresetTable = Record<string, Record<string, SlotPreset>>;\n\nexport interface ScoredTemplateOptions<Plan, Mount> {\n /** Score the variable (e.g. `SynOS.score`). */\n score: (spec: ScoreSpec) => Promise<string>;\n /** What to score: model_id, name, enum values, and the visitor signals. */\n scoreSpec: ScoreSpec;\n /**\n * Axis A \u2014 the decision archetype spec (\"what is blocking the purchase\").\n * When provided together with `scoreMulti`, both axes are scored in ONE\n * backend model call; the decision value drives `transformPreset` (pool\n * selection) and is passed to `composeUnset` for the live slots. Absent \u2192\n * exactly the single-axis behavior.\n */\n decisionSpec?: ScoreSpec;\n /** Batched scorer (e.g. `SynOS.scoreMulti`) \u2014 several specs, one model call. */\n scoreMulti?: (specs: ScoreSpec[]) => Promise<string[]>;\n /**\n * Transform a SET slot's preset before mounting \u2014 the pool-selection hook\n * (pick which tagged items surface for this visitor's decision archetype).\n */\n transformPreset?: (preset: SlotPreset, decisionValue: string) => SlotPreset;\n /** Per-(scored value) per-slot SET values. */\n presets: PresetTable;\n /** The fixed slot set this template renders. */\n slotIds: string[];\n /** Value to fall back to when the scored value has no preset row (default 'unknown'). */\n fallbackValue?: string;\n /**\n * Build the plan from EVERY slot. `preset` is `undefined` for unset slots,\n * which still reserve a skeleton box so live/streamed arrivals fill a reserved\n * box instead of reflowing the page (no layout shift).\n */\n buildPlan: (slots: Array<{ slot: string; preset: SlotPreset | undefined }>) => Plan;\n /** Build one mount from a slot preset (set slots only). */\n buildMount: (slot: string, preset: SlotPreset) => Mount;\n /** Apply the plan (reserve slot skeletons / headers) before mounting. */\n emitPlan: (plan: Plan) => void;\n /** Apply one mount. */\n emitMount: (mount: Mount) => Promise<void> | void;\n /**\n * Stream the slots the presets didn't set (live compose). Receives the scored\n * value (and the decision value when a decisionSpec was scored) so the caller\n * can personalize the live request. Optional.\n */\n composeUnset?: (unsetSlots: string[], value: string, decisionValue?: string) => Promise<void>;\n /**\n * Reorder the slots for the PLAN \u2014 i.e. the PAGE order. Called after scoring\n * with the full slot list and set-membership, so instant (preset) slots can\n * float to the top and slow live slots can sink to the bottom (sections fill\n * top-down as responses land instead of a slow section blocking the fold).\n * Purely presentational: does not change which slots mount or compose.\n * Default: the given slotIds order.\n */\n orderSlots?: (slotIds: string[], isSet: (slot: string) => boolean) => string[];\n}\n\nexport interface ScoredTemplateResult {\n /** The scored value used to resolve presets. */\n value: string;\n /** The scored decision-archetype value ('unknown' when no decisionSpec). */\n decisionValue: string;\n /** Slots filled from config. */\n setSlots: string[];\n /** Slots left to the live stream (compose). */\n unsetSlots: string[];\n}\n\n/**\n * Score \u2192 resolve the scored value's preset row \u2192 reserve a box for EVERY slot \u2192\n * fill the SET slots from config immediately \u2192 stream only the UNSET slots.\n */\nexport async function resolveScoredTemplate<Plan, Mount>(\n opts: ScoredTemplateOptions<Plan, Mount>\n): Promise<ScoredTemplateResult> {\n let value: string;\n let decisionValue = 'unknown';\n if (opts.decisionSpec && opts.scoreMulti) {\n // Both axes from ONE backend model call. A short result must not clobber\n // the 'unknown' default with undefined.\n const scored = await opts.scoreMulti([opts.scoreSpec, opts.decisionSpec]);\n value = scored[0] ?? 'unknown';\n decisionValue = scored[1] ?? 'unknown';\n } else {\n value = await opts.score(opts.scoreSpec);\n }\n const variant = opts.presets[value] ?? opts.presets[opts.fallbackValue ?? 'unknown'] ?? {};\n\n const setSlots = opts.slotIds.filter((s) => s in variant);\n const unsetSlots = opts.slotIds.filter((s) => !(s in variant));\n\n // Reserve a box for EVERY slot up front (set AND unset) \u2014 streamed/live\n // arrivals then fill a reserved skeleton instead of reflowing (no layout shift).\n // Page order comes from orderSlots (presentational only) when provided.\n const pageOrder = opts.orderSlots\n ? opts.orderSlots([...opts.slotIds], (slot) => slot in variant)\n : opts.slotIds;\n if (pageOrder.length > 0) {\n opts.emitPlan(opts.buildPlan(pageOrder.map((slot) => ({ slot, preset: variant[slot] }))));\n }\n // Fill the cached (set) slots immediately from config \u2014 no model in the render\n // path. transformPreset (pool selection) picks which tagged items surface for\n // this visitor's decision archetype before the mount is built.\n for (const slot of setSlots) {\n const preset = opts.transformPreset\n ? opts.transformPreset(variant[slot], decisionValue)\n : variant[slot];\n await opts.emitMount(opts.buildMount(slot, preset));\n }\n // Stream the unset slots (live compose).\n if (unsetSlots.length > 0 && opts.composeUnset) {\n await opts.composeUnset(unsetSlots, value, decisionValue);\n }\n\n return { value, decisionValue, setSlots, unsetSlots };\n}\n", "/**\n * Watches the takeover region for host JS (React reconciliation, WooCommerce\n * AJAX fragments, theme scripts) re-rendering a section we detached. The\n * engine DISCARDS resurrected clones (they are fresh host re-renders, not\n * the original nodes we hold for restore). Budget mirrors adaptive-content's\n * reconciliation-guard: 3 retries, 50ms debounce; exhausted => the engine\n * aborts the takeover and restores the originals \u2014 never duplicate host\n * content next to its Syntro replacement.\n */\n\nexport interface ResurrectionGuardOptions {\n region: HTMLElement;\n /** Nodes the takeover itself rendered \u2014 never treated as resurrections,\n * even when a host anchor selector happens to match them (review F3). */\n ignore?: (el: HTMLElement) => boolean;\n anchors: Map<string, string>;\n onResurrected(sectionId: string, el: HTMLElement): void;\n /** Receives the still-attached resurrected element so the caller can\n * discard it before restoring originals \u2014 never leave a duplicate. */\n onBudgetExhausted(sectionId: string, el: HTMLElement): void;\n maxRetries?: number;\n debounceMs?: number;\n}\n\nexport interface ResurrectionGuardHandle {\n stop(): void;\n unwatch(sectionId: string): void;\n}\n\nexport function startResurrectionGuard(opts: ResurrectionGuardOptions): ResurrectionGuardHandle {\n const maxRetries = opts.maxRetries ?? 3;\n const debounceMs = opts.debounceMs ?? 50;\n const watched = new Map(opts.anchors);\n const counts = new Map<string, number>();\n let timer: ReturnType<typeof setTimeout> | null = null;\n let stopped = false;\n\n const sweep = (): void => {\n if (stopped) return;\n // Collect ALL present clones first so a budget exhaustion can discard\n // every one of them \u2014 the abort path restores originals, and a surviving\n // sibling clone would sit next to its restored original (duplicate).\n const present: Array<{ sectionId: string; el: HTMLElement; n: number }> = [];\n for (const [sectionId, selector] of watched) {\n let matches: HTMLElement[] = [];\n try {\n matches = Array.from(opts.region.querySelectorAll<HTMLElement>(selector));\n } catch {\n continue; // malformed selector \u2014 nothing to watch\n }\n const el = matches.find((m) => !(opts.ignore?.(m) ?? false));\n if (!el) continue;\n present.push({ sectionId, el, n: (counts.get(sectionId) ?? 0) + 1 });\n }\n const exhausted = present.find((p) => p.n > maxRetries);\n if (exhausted) {\n for (const p of present) p.el.remove();\n opts.onBudgetExhausted(exhausted.sectionId, exhausted.el);\n return; // engine aborts; it calls stop()\n }\n for (const { sectionId, el, n } of present) {\n counts.set(sectionId, n);\n opts.onResurrected(sectionId, el);\n }\n };\n\n const observer = new MutationObserver(() => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(sweep, debounceMs);\n });\n observer.observe(opts.region, { childList: true, subtree: true });\n\n return {\n stop() {\n stopped = true;\n if (timer) clearTimeout(timer);\n observer.disconnect();\n },\n unwatch(sectionId) {\n watched.delete(sectionId);\n counts.delete(sectionId);\n },\n };\n}\n", "/**\n * Section detach/reattach for PDP takeover \u2014 nativeSlotSwap's restore\n * semantics (comment anchor + ORIGINAL node references, React-ref safe)\n * without the clone-and-fill step. Never hostPatcher setStyle: its revert\n * strips untouched inline styles/attrs (spec rev 2, adversarial finding 1).\n */\n\nexport interface DetachHandle {\n readonly section: HTMLElement;\n readonly restored: boolean;\n restore(): void;\n}\n\n/** Sections currently detached by us \u2014 prevents double-detach corruption. */\nconst activeDetaches = new WeakSet<HTMLElement>();\n\nexport function detachSection(section: HTMLElement): DetachHandle | null {\n if (activeDetaches.has(section)) return null;\n if (!section.parentNode) return null;\n activeDetaches.add(section);\n\n const anchor = document.createComment('syntro-takeover-section');\n section.before(anchor);\n section.remove();\n\n let restored = false;\n return {\n section,\n get restored() {\n return restored;\n },\n restore() {\n if (restored) return;\n restored = true;\n if (anchor.parentNode) anchor.after(section);\n anchor.remove();\n activeDetaches.delete(section);\n },\n };\n}\n", "/**\n * PDP takeover orchestration \u2014 staged with an explicit COMMIT POINT.\n * Nothing detaches until score + plan are ready; every failure before the\n * commit point ends with the native page untouched by construction (spec\n * rev 2 \"The takeover mechanics\"). Ports pattern mirrors\n * decision/pdp-compose.ts: the engine is pure orchestration; DOM effects\n * arrive injected. The takeover root is the already-mounted <syntro-pdp>\n * element (config mounts it via core:mountWidget \u2014 no parallel mechanism).\n *\n * Resurrected sections are host RE-RENDERS (fresh nodes), not our originals:\n * they are discarded. Only original sections \u2014 held as DetachHandles \u2014 are\n * ever restored.\n */\n\nimport { type ResurrectionGuardHandle, startResurrectionGuard } from './resurrectionGuard';\nimport type { PdpTakeoverConfig, TakeoverPlan, TakeoverPlanResponse } from './schema';\nimport { type DetachHandle, detachSection } from './sectionDetach';\n\nexport interface TakeoverPorts {\n hasSignals(): boolean;\n findSection(anchor: string): HTMLElement | null;\n measureSection(el: HTMLElement): number;\n reserve(minHeightPx: number): void;\n releaseReservation(): void;\n region(): HTMLElement;\n /** True for nodes the takeover itself rendered \u2014 the resurrection guard\n * must never treat our own sections as host resurrections (review F3). */\n ownsNode(el: HTMLElement): boolean;\n /** Curtains the takeover surface. The ports decide the targets (root +\n * to-be-suppressed host sections \u2014 NEVER a shared ancestor, which would\n * blank the buy box / LCP element; review F1). Returns lift(). */\n curtain(): () => void;\n deactivateRoot(): void;\n /** One server round trip: scores the visitor AND returns that archetype's\n * plan, resolved centrally. Ordering intelligence lives server-side so a\n * model improvement propagates fleet-wide without config re-publishes. */\n fetchPlan(): Promise<TakeoverPlanResponse>;\n renderModules(\n plan: TakeoverPlan,\n onModuleFailed: (suppressedSectionId: string | null) => void\n ): Promise<void>;\n telemetry(event: string, props?: Record<string, unknown>): void;\n}\n\nexport type TakeoverOutcome = 'progressive' | 'committed' | 'aborted-precommit';\n\nexport interface TakeoverResult {\n outcome: TakeoverOutcome;\n dispose(): void;\n}\n\n/**\n * Native module kinds that SUPERSEDE a host-section role: rendering the kind\n * next to an attached host section of that role duplicates content (the\n * two-Q&As bug, 2026-08-09). Committed path: the server suppresses the role\n * (mirror map server-side, plan_rules.py). Progressive path (no curtain, no\n * suppression possible): the KIND is dropped instead \u2014 the host keeps its\n * section, we keep our non-overlapping modules.\n */\nconst KIND_SUPERSEDES_ROLE: Record<string, string> = {\n qa: 'faq',\n 'peer-feed': 'social-proof',\n};\n\nconst DEFAULT_COMMIT_TIMEOUT_MS = 500;\nconst NOOP_DISPOSE = (): void => {};\n\nexport async function runTakeover(\n config: PdpTakeoverConfig,\n ports: TakeoverPorts,\n opts: { commitTimeoutMs?: number } = {}\n): Promise<TakeoverResult> {\n // No separate kill switch: rollout AND kill live at CONFIG DELIVERY (the\n // GrowthBook flag that serves the config variant, fetched per page load).\n // If runTakeover was called, the served config carries `takeover` \u2014 that IS\n // the gate. A second runtime flag would be a parallel control plane\n // (feedback_sdk_config_single_source_of_truth; Parker, 2026-08-07).\n\n // Cold-start fast path: no signals worth scoring => no curtain, and the\n // server plan (it resolves 'unknown' server-side) renders progressively\n // whenever it arrives. Suppression is STRIPPED on this path \u2014 with no\n // curtain there is no safe window to detach host content, so the risk\n // stays zero by construction.\n if (!ports.hasSignals()) {\n const declaredRoles = new Set(config.hostSections.map((s) => s.role));\n void Promise.resolve()\n .then(() => ports.fetchPlan())\n .then((response) =>\n ports.renderModules(\n {\n // Dropped on this path (nothing may detach, so the host's own\n // sections all stay attached \u2014 rendering an overlap duplicates\n // content, the exact class the spec forbids):\n // 1. host-derived entries (kind:sectionId) \u2014 review F2;\n // 2. native kinds superseding a DECLARED host role (two-Q&As\n // bug: our qa next to their attached FAQ).\n order: response.plan.order.filter((entry) => {\n if (entry.includes(':')) return false;\n const superseded = KIND_SUPERSEDES_ROLE[entry];\n return !(superseded && declaredRoles.has(superseded));\n }),\n suppress: [],\n },\n () => {}\n )\n )\n .catch(() => ports.telemetry('takeover.render_failed', { path: 'progressive' }));\n return { outcome: 'progressive', dispose: NOOP_DISPOSE };\n }\n\n const commitTimeoutMs = opts.commitTimeoutMs ?? DEFAULT_COMMIT_TIMEOUT_MS;\n const region = ports.region();\n const lift = ports.curtain();\n\n // Race the plan fetch against the curtain budget. Timeout OR rejection\n // before commit => the visitor sees the untouched native page (nothing\n // has been detached). Distinct telemetry per cause: ops must be able to\n // tell backend-down from slow-backend (review F5).\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timedOut: unique symbol = Symbol('timeout');\n const fetchFailed: unique symbol = Symbol('fetch-failed');\n const response = await Promise.race([\n ports.fetchPlan().catch((): typeof fetchFailed => fetchFailed),\n new Promise<typeof timedOut>((r) => {\n timeoutId = setTimeout(() => r(timedOut), commitTimeoutMs);\n }),\n ]).finally(() => clearTimeout(timeoutId));\n\n if (response === timedOut || response === fetchFailed) {\n ports.deactivateRoot();\n lift();\n if (response === timedOut) {\n ports.telemetry('takeover.precommit_timeout', { budgetMs: commitTimeoutMs });\n } else {\n ports.telemetry('takeover.plan_fetch_failed', { budgetMs: commitTimeoutMs });\n }\n return { outcome: 'aborted-precommit', dispose: NOOP_DISPOSE };\n }\n\n // \u2500\u2500 COMMIT POINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const plan = response.plan;\n const sectionsById = new Map(config.hostSections.map((s) => [s.id, s]));\n\n // Measure + reserve BEFORE detaching so surrounding content doesn't jump.\n const toSuppress: Array<{ id: string; el: HTMLElement }> = [];\n let reservedPx = 0;\n for (const id of plan.suppress) {\n const section = sectionsById.get(id);\n if (!section) {\n // Server/config drift defense: the plan may only suppress sections the\n // config DECLARES (with a selector). An undeclared id is skipped, never\n // guessed at.\n ports.telemetry('takeover.suppress_undeclared', { sectionId: id });\n continue;\n }\n let el: HTMLElement | null = null;\n try {\n el = ports.findSection(section.anchor);\n } catch {\n // Malformed selector (querySelector throws SyntaxError) \u2014 same\n // treatment as an anchor that matched nothing.\n }\n if (!el) {\n ports.telemetry('takeover.section_anchor_missing', { sectionId: id });\n continue;\n }\n reservedPx += ports.measureSection(el);\n toSuppress.push({ id, el });\n }\n ports.reserve(reservedPx);\n\n const handles = new Map<string, DetachHandle>(); // ORIGINAL sections only\n for (const { id, el } of toSuppress) {\n const handle = detachSection(el);\n if (handle) handles.set(id, handle);\n }\n\n let disposed = false;\n let guard: ResurrectionGuardHandle | undefined;\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n guard?.stop();\n for (const handle of handles.values()) handle.restore();\n handles.clear();\n ports.deactivateRoot();\n lift();\n };\n\n guard = startResurrectionGuard({\n region,\n ignore: (el) => ports.ownsNode(el),\n anchors: new Map(toSuppress.map(({ id }) => [id, sectionsById.get(id)!.anchor] as const)),\n onResurrected: (sectionId, el) => {\n // Host re-render \u2014 a fresh clone, not our original. Discard it.\n ports.telemetry('takeover.section_resurrected', { sectionId });\n el.remove();\n },\n onBudgetExhausted: (sectionId, el) => {\n ports.telemetry('takeover.resurrection_budget_exhausted', { sectionId });\n el.remove(); // discard the final clone BEFORE restoring the original\n dispose();\n },\n });\n\n const onModuleFailed = (suppressedSectionId: string | null): void => {\n if (!suppressedSectionId) return;\n const handle = handles.get(suppressedSectionId);\n if (!handle) return;\n // Reattaching the original must not count as a resurrection.\n guard?.unwatch(suppressedSectionId);\n handle.restore();\n handles.delete(suppressedSectionId);\n ports.telemetry('takeover.module_failed_section_restored', {\n sectionId: suppressedSectionId,\n });\n };\n\n // Curtain lifts NOW \u2014 baked payloads render synchronously and live slots\n // show skeletons, both presentable. Holding until every module settles\n // would quietly exceed the 500ms budget (plan rev 2, finding 13).\n // A WHOLESALE renderer rejection (not a per-module failure) must not\n // strand detached sections: dispose restores everything, fail-open.\n void Promise.resolve()\n .then(() => ports.renderModules(plan, onModuleFailed))\n .catch(() => {\n ports.telemetry('takeover.render_failed', { path: 'committed' });\n dispose();\n })\n .finally(() => ports.releaseReservation());\n lift();\n\n return { outcome: 'committed', dispose };\n}\n", "/**\n * Real `fetchPlan` for the takeover engine \u2014 ONE POST to the runtime-backend's\n * `/api/pdp/plan` through the runtime's `authedFetch` (the same authenticated\n * transport the element's `/api/pdp/module` calls use: Bearer token +\n * X-Distinct-Id, BUG-1784152746 \u2014 the transport owns headers, this module owns\n * endpoint + payload). The server scores the visitor's decision archetype AND\n * returns that archetype's plan in the same round trip (spec rev 3).\n *\n * The response is untrusted: it is validated against the frozen\n * `TakeoverPlanResponse` wire shape (zod, strict) before it is returned \u2014 an\n * invalid payload REJECTS, which the engine treats as backend-down and fails\n * open to the native page. No silent defaults, no partial acceptance.\n */\n\nimport { z } from 'zod';\nimport type { TakeoverPorts } from './engine';\nimport { type PdpTakeoverConfig, type TakeoverPlanResponse, takeoverPlanSchema } from './schema';\n\n/** Wire shape of the `/api/pdp/plan` response \u2014 `TakeoverPlanResponse`. */\nexport const takeoverPlanResponseSchema = z\n .object({\n decisionArchetype: z.string().min(1),\n plan: takeoverPlanSchema,\n })\n .strict();\n\n// Compile-time proof the zod shape and the frozen contract interface agree.\ntype _AssertResponseShape =\n z.infer<typeof takeoverPlanResponseSchema> extends TakeoverPlanResponse\n ? TakeoverPlanResponse extends z.infer<typeof takeoverPlanResponseSchema>\n ? true\n : never\n : never;\nconst _responseShapeMatchesContract: _AssertResponseShape = true;\nvoid _responseShapeMatchesContract;\n\nexport interface TakeoverPlanFetcherDeps {\n /** `window.SynOS.authedFetch` \u2014 the runtime-owned authenticated transport. */\n authedFetch: (path: string, init?: RequestInit) => Promise<Response>;\n config: PdpTakeoverConfig;\n /** REQUIRED server-side (min 1) \u2014 the caller gates: no product id, no\n * takeover call at all (`takeover.missing_product_id`). Never defaulted. */\n productId: string;\n productName: string;\n /** Decision-archetype vocabulary (slug \u2192 description) \u2014 the same axis-A\n * context the element's scoring path sends, so the server can score. */\n decisionArchetypes: Record<string, string>;\n /** Client-side behavior summary when one exists; '' otherwise. Context for\n * the server-side scorer, mirroring the `/api/pdp/module` field. */\n behaviorSummary: string;\n /** The full visitor signal bag the element already gathers for scoring. */\n signals: Record<string, unknown>;\n}\n\nexport function createTakeoverPlanFetcher(\n deps: TakeoverPlanFetcherDeps\n): TakeoverPorts['fetchPlan'] {\n return async () => {\n const resp = await deps.authedFetch('/api/pdp/plan', {\n method: 'POST',\n // FROZEN SEAM SHAPE \u2014 field set AND order are pinned byte-for-byte by\n // `src/__fixtures__/takeover-plan-request.json`; the runtime-backend PR\n // commits the same fixture. Change both sides together or not at all.\n // Deliberately NO client-scored archetype/decision slugs: the server\n // scores (spec rev 3) \u2014 the client sends vocabulary + signals only.\n body: JSON.stringify({\n product_id: deps.productId,\n product_name: deps.productName,\n // Page anatomy: ids + roles only. Anchors are host DOM selectors \u2014\n // client-side facts the server has no business receiving.\n host_sections: deps.config.hostSections.map(({ id, role }) => ({ id, role })),\n decision_archetypes: deps.decisionArchetypes,\n behavior_summary: deps.behaviorSummary,\n signals: deps.signals,\n ingest_snapshot: deps.config.ingestSnapshot,\n // The client's configurable wait (commitTimeoutMs, default 500):\n // the server may classify inline within it (bounded inline classify).\n budget_ms: deps.config.commitTimeoutMs ?? 500,\n }),\n });\n if (!resp.ok) {\n throw new Error(`takeover plan fetch failed: HTTP ${resp.status}`);\n }\n const data: unknown = await resp.json();\n return takeoverPlanResponseSchema.parse(data);\n };\n}\n", "/**\n * Region-scoped anti-flicker curtain (spec rev 2 \"Curtain the region\").\n * Same pattern as runtime-sdk's initAntiFlicker but applied to an arbitrary\n * region element and owned by this adaptive: adaptive-product deliberately\n * has no runtime-sdk dependency (see nativeSlotSwap.ts's cross-package-dep\n * note), and runtime-sdk's schema generation consumes THIS package's dist,\n * so importing runtime-sdk here would create a build cycle.\n * The failsafe timeout guarantees the region always becomes visible.\n */\n\nconst CLASS = 'syntro-takeover-curtain';\nconst STYLE_ATTR = 'data-syntro-takeover-curtain';\n\n/** Ref-count for the shared style element: concurrent curtains (multiple\n * targets, or two takeover tiles) must not have the first lift() strip the\n * style the others still depend on. Removed only when the LAST lifts. */\nlet styleRefs = 0;\n\nfunction acquireStyle(): void {\n styleRefs += 1;\n if (document.head.querySelector(`style[${STYLE_ATTR}]`)) return;\n const styleEl = document.createElement('style');\n styleEl.setAttribute(STYLE_ATTR, '');\n styleEl.textContent = `.${CLASS} { opacity: 0 !important; }`;\n document.head.appendChild(styleEl);\n}\n\nfunction releaseStyle(): void {\n styleRefs = Math.max(0, styleRefs - 1);\n if (styleRefs === 0) {\n document.head.querySelector(`style[${STYLE_ATTR}]`)?.remove();\n }\n}\n\nexport function applyRegionCurtain(\n region: HTMLElement,\n opts: { timeoutMs?: number } = {}\n): () => void {\n const timeoutMs = opts.timeoutMs ?? 3_000;\n acquireStyle();\n region.classList.add(CLASS);\n\n let lifted = false;\n const lift = (): void => {\n if (lifted) return;\n lifted = true;\n clearTimeout(timeoutId);\n region.classList.remove(CLASS);\n releaseStyle();\n };\n const timeoutId = setTimeout(lift, timeoutMs);\n return lift;\n}\n\n/** Curtain several elements as one unit; the returned lift releases all. */\nexport function applyCurtainToTargets(\n targets: HTMLElement[],\n opts: { timeoutMs?: number } = {}\n): () => void {\n const lifts = targets.map((t) => applyRegionCurtain(t, opts));\n return () => {\n for (const lift of lifts) lift();\n };\n}\n", "/**\n * DOM/runtime adapters for the takeover engine. The engine stays pure; this\n * module owns every real-world effect. Activation is gated by CONFIG\n * DELIVERY (the served config either carries `takeover` or it doesn't) \u2014\n * there is deliberately no separate runtime flag.\n * The root is the config-mounted <syntro-pdp> element \u2014 the runtime owns its\n * mount lifecycle; deactivateRoot only empties/collapses it, never removes it.\n */\nimport type { TakeoverPorts, TakeoverResult } from './engine';\nimport { applyCurtainToTargets } from './regionCurtain';\nimport type { PdpTakeoverConfig } from './schema';\n\nexport interface DomPortDeps {\n root: HTMLElement;\n /** One round trip to the runtime-backend: scores the visitor and returns\n * the server-resolved plan. Stage 2 wires the real HTTP call. */\n fetchPlan: TakeoverPorts['fetchPlan'];\n signalCount: number;\n renderModules: TakeoverPorts['renderModules'];\n telemetry: TakeoverPorts['telemetry'];\n}\n\nexport function createDomPorts(config: PdpTakeoverConfig, deps: DomPortDeps): TakeoverPorts {\n return {\n hasSignals: () => deps.signalCount > 0,\n findSection(anchor) {\n try {\n return document.querySelector<HTMLElement>(anchor);\n } catch {\n return null; // malformed selector \u2014 engine treats as anchor missing\n }\n },\n measureSection: (el) => el.getBoundingClientRect().height,\n reserve(minHeightPx) {\n if (minHeightPx > 0) deps.root.style.minHeight = `${Math.round(minHeightPx)}px`;\n },\n releaseReservation() {\n deps.root.style.removeProperty('min-height');\n },\n region: () => deps.root.parentElement ?? deps.root,\n ownsNode: (el) => deps.root.contains(el),\n curtain() {\n // Curtain ONLY the takeover surface: the root plus each declared host\n // section. Never a shared ancestor \u2014 on a real PDP the root's parent\n // wraps the whole product page, and blanking it hides the buy box and\n // the LCP element for the plan-fetch window (review F1).\n const targets: HTMLElement[] = [deps.root];\n for (const section of config.hostSections) {\n try {\n const el = document.querySelector<HTMLElement>(section.anchor);\n if (el) targets.push(el);\n } catch {\n // malformed selector \u2014 engine handles it at suppress time\n }\n }\n // Failsafe must outlast the configurable wait: budget + 1s headroom,\n // never below the 3s default.\n const failsafe = Math.max(3_000, (config.commitTimeoutMs ?? 500) + 1_000);\n return applyCurtainToTargets(targets, { timeoutMs: failsafe });\n },\n deactivateRoot() {\n deps.root.replaceChildren();\n deps.root.style.display = 'none';\n },\n fetchPlan: deps.fetchPlan,\n renderModules: deps.renderModules,\n telemetry: deps.telemetry,\n };\n}\n\n/**\n * SPA-navigation / bfcache safety: dispose the takeover when the page hides.\n * NOTE: pagehide-dispose alone leaves a bfcache-restored page on the native\n * fallback (connectedCallback does not re-fire on restore). The takeover\n * OWNER must also listen for `pageshow` with `event.persisted` and re-enter\n * takeover \u2014 that wiring lives at the call site, not here.\n */\nexport function attachLifecycleDispose(result: TakeoverResult): () => void {\n const onHide = (): void => result.dispose();\n window.addEventListener('pagehide', onHide, { once: true });\n return () => window.removeEventListener('pagehide', onHide);\n}\n", "import { html, LitElement, nothing } from 'lit';\n\n/**\n * <syntro-pdp-section-header> \u2014 the eyebrow + title + italic subtitle\n * pattern that frames every agent-authored PDP section. Fonts inherit the\n * host page by default (override via --syntro-pdp-sans / --syntro-pdp-display)\n * so the section reads as native typography, not an imposed type system.\n *\n * Props:\n * - `eyebrow` (string, required) \u2014 small-caps label above the title\n * (e.g. \"Signals \u00B7 this week\", \"Compared\")\n * - `title` (string, required) \u2014 main display title\n * - `accent` (string, optional) \u2014 substring inside `title` rendered\n * italic in primary color. Matched\n * case-insensitively, first occurrence\n * only. If omitted, the whole title\n * renders plain.\n * - `subtitle` (string, optional) \u2014 italic subtitle / LLM rationale.\n *\n * Brand customization:\n * --syntro-pdp-primary primary accent color\n * --syntro-pdp-fg heading foreground\n * --syntro-pdp-muted subtitle / eyebrow color\n * --syntro-pdp-display display font override (inherits host font if unset)\n */\n\nfunction ensureSectionHeaderStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-section-header-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-section-header-style';\n s.textContent = SECTION_HEADER_CSS;\n document.head.appendChild(s);\n}\n\nconst SECTION_HEADER_CSS = `\n/* The header renders at the FULL-WIDTH section (band) level so the eyebrow\n can center across the whole horizontal slice, while the headline below\n re-constrains to the content column and left-aligns with the body. */\nsyntro-pdp-section-header { display: block; }\n.syntro-pdp-section-header {\n --sh-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n /* The eyebrow dot + title accent use the per-slot section accent when\n SyntroPdpLit sets one; otherwise they fall back to the host's brand\n primary. The SDK never overwrites --syntro-pdp-primary itself, so a\n customer's brand token stays intact everywhere it's read. */\n --sh-accent: var(--syntro-pdp-section-accent, var(--sh-primary));\n --sh-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --sh-muted: var(--syntro-pdp-muted, hsl(30 8% 55%));\n --sh-display: var(--syntro-pdp-display, inherit);\n font-family: var(--syntro-pdp-sans, inherit);\n}\n\n/* Eyebrow: centered across the full-width band (the kicker). */\n.syntro-pdp-section-eyebrow {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 10px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--sh-muted);\n /* Sit up near the band's top edge with a clear gap down to the title \u2014\n the eyebrow reads as a kicker on the color transition, not a title label. */\n margin-bottom: clamp(28px, 4vw, 44px);\n font-weight: 500;\n}\n/* Headline: re-constrain to the content column + left-align so the title and\n subtitle line up on the left with the body, while the band stays full-width.\n Same max-width + horizontal inset as the body's inner wrapper (inherited\n --sc-pdp-* tokens) so their left edges match. */\n.syntro-pdp-section-headline {\n max-width: var(--sc-pdp-max-w, 1080px);\n margin-inline: auto;\n padding-inline: var(--sc-pdp-section-pad-x, clamp(20px, 4vw, 40px));\n text-align: left;\n}\n.syntro-pdp-section-eyebrow-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sh-accent);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--sh-accent) 16%, transparent);\n animation: syntro-pdp-pulse-dot 2200ms ease-in-out infinite;\n}\n\n.syntro-pdp-section-title {\n font-family: var(--sh-display);\n font-weight: 400;\n /* Smaller upper bound + wider wrap target so 30-45 char titles fit a\n single line at the typical demo content width. The 22ch / 56px\n pair from v1 produced ubiquitous 2-line headers \u2014 every editorial\n title in the live planner output was wrapping. */\n font-size: clamp(28px, 2.6vw, 38px);\n line-height: 1.1;\n letter-spacing: -0.015em;\n color: var(--sh-fg);\n font-variation-settings: 'SOFT' 80, 'WONK' 1;\n margin: 0;\n max-width: 46ch;\n text-align: left;\n}\n.syntro-pdp-section-title em {\n font-style: italic;\n font-variation-settings: 'SOFT' 100, 'WONK' 1;\n color: var(--sh-accent);\n}\n\n.syntro-pdp-section-subtitle {\n margin: 14px 0 0;\n font-style: italic;\n font-family: var(--sh-display);\n font-size: 16.5px;\n line-height: 1.55;\n color: var(--sh-muted);\n max-width: 60ch;\n text-align: left;\n}\n\n@media (max-width: 640px) {\n .syntro-pdp-section-title {\n font-size: clamp(24px, 6.5vw, 30px);\n max-width: 30ch;\n }\n .syntro-pdp-section-subtitle {\n font-size: 14.5px;\n margin-top: 10px;\n }\n}\n\n@keyframes syntro-pdp-pulse-dot {\n 0%, 100% { opacity: 0.6; transform: scale(1); }\n 50% { opacity: 1; transform: scale(1.15); }\n}\n`;\n\n/**\n * Find the first case-insensitive occurrence of `needle` inside `haystack`\n * and return a 3-tuple of [before, match, after]. Returns null if not found\n * so the caller can render the title plain.\n */\nfunction splitTitleAt(haystack: string, needle: string): [string, string, string] | null {\n if (!needle) return null;\n const idx = haystack.toLowerCase().indexOf(needle.toLowerCase());\n if (idx < 0) return null;\n return [\n haystack.slice(0, idx),\n haystack.slice(idx, idx + needle.length),\n haystack.slice(idx + needle.length),\n ];\n}\n\nexport class PdpSectionHeaderLit extends LitElement {\n static override properties = {\n eyebrow: { type: String },\n title: { type: String },\n accent: { type: String },\n subtitle: { type: String },\n };\n\n eyebrow = '';\n title = '';\n accent = '';\n subtitle = '';\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureSectionHeaderStyles();\n }\n\n override render() {\n const split = this.accent ? splitTitleAt(this.title, this.accent) : null;\n return html`\n <header class=\"syntro-pdp-section-header\">\n <div class=\"syntro-pdp-section-eyebrow\">\n <span class=\"syntro-pdp-section-eyebrow-dot\" aria-hidden=\"true\"></span>\n <span>${this.eyebrow}</span>\n </div>\n <div class=\"syntro-pdp-section-headline\">\n <h2 class=\"syntro-pdp-section-title\">\n ${split ? html`${split[0]}<em>${split[1]}</em>${split[2]}` : this.title}\n </h2>\n ${\n this.subtitle\n ? html`<p class=\"syntro-pdp-section-subtitle\">${this.subtitle}</p>`\n : nothing\n }\n </div>\n </header>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-section-header')) {\n customElements.define('syntro-pdp-section-header', PdpSectionHeaderLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-section-header': PdpSectionHeaderLit;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\n\n/**\n * <syntro-pdp-slot> \u2014 wraps a single PDP slot container with its skeleton /\n * pending / failed state. The host renders this element with a `slot-id` and\n * `kind`; the runtime imperatively mounts the LLM-authored custom element\n * INSIDE the inner mount container via `el.data = ...; mount.appendChild(el)`.\n *\n * The skeleton animates as a sibling of the mount container (grid-stacked)\n * so it can be hidden via the `state` attribute without React/Lit reconciling\n * the imperatively-appended child out of existence.\n *\n * Props:\n * - `slot-id` (string, required) \u2014 the DOM id the runtime targets to mount\n * (also exposed on the inner mount div for\n * backwards compatibility with the\n * existing wire contract).\n * - `kind` ('trending-news' | 'chart' | string) \u2014 chooses the skeleton\n * copy and accent.\n * - `state` ('pending' | 'mounted' | 'failed') \u2014 controls which layer\n * is visible. Default: 'pending'.\n *\n * Brand customization:\n * --syntro-pdp-primary primary accent color\n * --syntro-pdp-fg body foreground\n * --syntro-pdp-muted muted text\n * --syntro-pdp-border slot border\n * --syntro-pdp-card-bg slot background top\n * --syntro-pdp-card-bg-b slot background bottom\n * --syntro-pdp-display display font override (inherits host font if unset)\n */\n\nfunction ensureSlotStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-slot-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-slot-style';\n s.textContent = SLOT_CSS;\n document.head.appendChild(s);\n}\n\nconst SLOT_CSS = `\n.syntro-pdp-slot {\n --sl-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --sl-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --sl-muted: var(--syntro-pdp-muted, hsl(30 8% 55%));\n --sl-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --sl-bg-a: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --sl-bg-b: var(--syntro-pdp-card-bg-b, hsl(25 10% 9%));\n --sl-display: var(--syntro-pdp-display, inherit);\n\n position: relative;\n display: grid;\n grid-template-areas: 'stack';\n font-family: var(--syntro-pdp-sans, inherit);\n}\n.syntro-pdp-slot > .syntro-pdp-slot-mount,\n.syntro-pdp-slot > .syntro-pdp-slot-pending,\n.syntro-pdp-slot > .syntro-pdp-slot-failed {\n grid-area: stack;\n}\n\n.syntro-pdp-slot-mount {\n min-height: 220px;\n}\n\n/* When mutation arrived, hide the pending layer */\n.syntro-pdp-slot[data-state='mounted'] .syntro-pdp-slot-pending,\n.syntro-pdp-slot[data-state='failed'] .syntro-pdp-slot-pending {\n display: none;\n}\n.syntro-pdp-slot[data-state='mounted'] .syntro-pdp-slot-failed,\n.syntro-pdp-slot[data-state='pending'] .syntro-pdp-slot-failed {\n display: none;\n}\n\n/* Pending \u2014 a quietly-animated waiting state. */\n.syntro-pdp-slot-pending {\n position: relative;\n border: 1px dashed var(--sl-border);\n border-radius: 22px;\n min-height: 220px;\n padding: 36px 28px;\n background: linear-gradient(180deg, var(--sl-bg-a), var(--sl-bg-b));\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n}\n.syntro-pdp-slot-pending-shimmer {\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n transparent,\n color-mix(in srgb, var(--sl-primary) 7%, transparent),\n transparent\n );\n transform: translateX(-100%);\n animation: syntro-pdp-slot-sweep 1800ms ease-in-out infinite;\n}\n.syntro-pdp-slot-pending-row {\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.syntro-pdp-slot-pending-dot {\n flex: 0 0 auto;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--sl-primary);\n box-shadow: 0 0 0 6px color-mix(in srgb, var(--sl-primary) 14%, transparent);\n animation: syntro-pdp-slot-pulse 1600ms ease-in-out infinite;\n}\n.syntro-pdp-slot-pending-label {\n font-family: var(--sl-display);\n font-style: italic;\n font-size: 18px;\n font-variation-settings: 'SOFT' 70;\n color: var(--sl-muted);\n margin: 0;\n line-height: 1.4;\n}\n\n/* Failed \u2014 the planner or sub-agent failed; the host page still works. */\n.syntro-pdp-slot-failed {\n border: 1px solid var(--sl-border);\n border-radius: 22px;\n min-height: 180px;\n padding: 24px 28px;\n background: linear-gradient(180deg, var(--sl-bg-a), var(--sl-bg-b));\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 6px;\n}\n.syntro-pdp-slot-failed-title {\n font-family: var(--sl-display);\n font-style: italic;\n font-size: 17px;\n color: var(--sl-fg);\n margin: 0;\n font-variation-settings: 'SOFT' 70;\n}\n.syntro-pdp-slot-failed-body {\n color: var(--sl-muted);\n font-size: 13.5px;\n margin: 0;\n line-height: 1.5;\n}\n\n@media (max-width: 640px) {\n .syntro-pdp-slot-pending,\n .syntro-pdp-slot-failed {\n border-radius: 18px;\n padding: 24px 18px;\n min-height: 180px;\n }\n .syntro-pdp-slot-pending-label {\n font-size: 15.5px;\n }\n}\n\n@keyframes syntro-pdp-slot-sweep {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(100%); }\n}\n@keyframes syntro-pdp-slot-pulse {\n 0%, 100% {\n opacity: 0.6;\n box-shadow: 0 0 0 6px color-mix(in srgb, var(--sl-primary) 14%, transparent);\n }\n 50% {\n opacity: 1;\n box-shadow: 0 0 0 8px color-mix(in srgb, var(--sl-primary) 26%, transparent);\n }\n}\n`;\n\nconst PENDING_COPY: Record<string, string> = {\n 'trending-news': 'Composing this section\u2026',\n chart: 'Drawing the comparison\u2026',\n qa: 'Picking the questions worth answering\u2026',\n regional: 'Locating you in time and place\u2026',\n 'peer-feed': 'Finding people like you\u2026',\n};\n\nconst PENDING_FALLBACK = 'Composing\u2026';\n\nexport class PdpSlotLit extends LitElement {\n static override properties = {\n slotId: { type: String, attribute: 'slot-id' },\n kind: { type: String },\n state: { type: String, reflect: true },\n };\n\n /** DOM id the host's mutation runtime targets when appending the widget. */\n slotId = '';\n kind = '';\n state: 'pending' | 'mounted' | 'failed' = 'pending';\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureSlotStyles();\n }\n\n override willUpdate(): void {\n // Reflect state via a data attribute so the CSS rules above can drive\n // the layer visibility without us having to conditionally render mounts.\n // We always render the mount div so the runtime's appendChild lands.\n this.dataset.state = this.state;\n }\n\n override render() {\n const pendingLabel = PENDING_COPY[this.kind] ?? PENDING_FALLBACK;\n return html`\n <div class=\"syntro-pdp-slot\" data-state=${this.state} data-kind=${this.kind}>\n <div\n class=\"syntro-pdp-slot-mount\"\n id=${this.slotId || nothing}\n data-pdp-slot=${this.kind}\n ></div>\n <div class=\"syntro-pdp-slot-pending\" aria-hidden=${this.state !== 'pending'}>\n <div class=\"syntro-pdp-slot-pending-shimmer\" aria-hidden=\"true\"></div>\n <div class=\"syntro-pdp-slot-pending-row\">\n <span class=\"syntro-pdp-slot-pending-dot\" aria-hidden=\"true\"></span>\n <p class=\"syntro-pdp-slot-pending-label\">${pendingLabel}</p>\n </div>\n </div>\n <div class=\"syntro-pdp-slot-failed\" aria-hidden=${this.state !== 'failed'}>\n <p class=\"syntro-pdp-slot-failed-title\">\n Personalized view couldn't load.\n </p>\n <p class=\"syntro-pdp-slot-failed-body\">\n The standard page still has everything you need.\n </p>\n </div>\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-slot')) {\n customElements.define('syntro-pdp-slot', PdpSlotLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-slot': PdpSlotLit;\n }\n}\n", "/**\n * Real `renderModules` for the takeover engine \u2014 renders the server plan's\n * `order` into the takeover root using the EXISTING slot machinery: the\n * `<syntro-pdp-slot>` wrapper (skeleton/failed states), the\n * `<syntro-pdp-section-header>` element, the section frame attributes the\n * head-injected SECTION_LAYOUT_CSS styles, and the existing preset/live\n * content split (baked payloads from the presets store mount immediately;\n * live kinds keep the `/api/pdp/module` streaming path behind a skeleton).\n * ZERO new placement mechanisms (see `sdk-widget-placement`).\n *\n * Plan entries are `<kind>` (Syntro-native) or `<kind>:<hostSectionId>`\n * (a baked module derived from a detached host section). An entry whose\n * widget kind is unknown (Stage 2b kinds: evidence-table, metric-grid,\n * stepper, timeline) is reported through `onModuleFailed(hostSectionId)` so\n * the engine reattaches that host section \u2014 never a blank box, never a\n * dropped section.\n *\n * Failure containment: every per-module failure is contained (slot flips to\n * 'failed', or the section is removed when it replaced host content). The\n * returned promise only rejects on a wholesale renderer defect \u2014 which the\n * engine answers by disposing the whole takeover (fail open).\n */\n\nimport { buildPdpPlan } from '../decision/pdp-compose';\nimport { selectPresetData } from '../scoring/pool-selection';\nimport type { SlotPreset } from '../scoring/scored-template';\n// Side-effect imports: register the slot + section-header custom elements.\n// Neither module imports takeover code, so no cycle.\nimport '../widgets/PdpSectionHeaderLit';\nimport '../widgets/PdpSlotLit';\nimport type { TakeoverPorts } from './engine';\n\nexport interface TakeoverModuleRendererDeps {\n /** The takeover root \u2014 the `<syntro-pdp>` element itself. */\n root: HTMLElement;\n /** Preset row (slot id \u2192 SlotPreset) for a decision archetype; the caller\n * owns the presets-store read and the `unknown`-row fallback. */\n getPresetRow(decisionArchetype: string): Record<string, SlotPreset> | undefined;\n /** TEMPLATE_TO_TAG resolution \u2014 null means the kind has no widget yet. */\n resolveTag(kind: string, variant: string | null): string | null;\n /** Live compose via `/api/pdp/module`; resolves null on any failure. */\n requestLiveModule(\n kind: string,\n decisionArchetype: string\n ): Promise<Record<string, unknown> | null>;\n telemetry(event: string, props?: Record<string, unknown>): void;\n /** The server-scored decision archetype \u2014 captured from the fetchPlan\n * response by the caller (the engine forwards only the plan). */\n getDecisionArchetype(): string;\n}\n\n/** `\"<kind>\"` or `\"<kind>:<hostSectionId>\"` \u2192 its parts. */\nexport function parsePlanEntry(entry: string): { kind: string; hostSectionId: string | null } {\n const i = entry.indexOf(':');\n if (i === -1) return { kind: entry, hostSectionId: null };\n return { kind: entry.slice(0, i), hostSectionId: entry.slice(i + 1) || null };\n}\n\ninterface SlotElementLike extends HTMLElement {\n slotId: string;\n kind: string;\n state: 'pending' | 'mounted' | 'failed';\n updateComplete: Promise<unknown>;\n}\n\ninterface SectionHeaderLike extends HTMLElement {\n eyebrow: string;\n title: string;\n accent: string;\n subtitle: string;\n}\n\nexport function createTakeoverModuleRenderer(\n deps: TakeoverModuleRendererDeps\n): TakeoverPorts['renderModules'] {\n return async (plan, onModuleFailed) => {\n const decisionArchetype = deps.getDecisionArchetype();\n const row = deps.getPresetRow(decisionArchetype) ?? {};\n /** Work that must settle before the renderer resolves: scaffolds + baked\n * mounts. Live fetches stream in AFTER the curtain lifts by design. */\n const bakedWork: Array<Promise<void>> = [];\n\n plan.order.forEach((entry, index) => {\n const { kind, hostSectionId } = parsePlanEntry(entry);\n const slotId = `pdp-slot-${kind}`;\n const preset = row[slotId];\n const tag = deps.resolveTag(kind, preset?.variant ?? null);\n if (!tag) {\n // Stage 2b kind (or plan drift): no widget exists for this entry.\n deps.telemetry('takeover.module_kind_unknown', { entry, kind });\n onModuleFailed(hostSectionId);\n return;\n }\n\n // Section scaffold \u2014 same frame the six-slot render produces, so the\n // head-injected section layout CSS and per-slot accents apply as-is.\n const envelope = buildPdpPlan([{ slot: slotId, preset }]);\n const task = envelope.tasks[0];\n const section = document.createElement('section');\n section.toggleAttribute('data-syntro-pdp-section', true);\n section.setAttribute('data-slot', slotId);\n section.setAttribute('data-syntro-takeover-entry', entry);\n const header = document.createElement('syntro-pdp-section-header') as SectionHeaderLike;\n header.eyebrow = task.header.eyebrow ?? '';\n header.title = task.header.title ?? '';\n header.accent = task.header.accent ?? '';\n header.subtitle = task.header.subtitle ?? '';\n const inner = document.createElement('div');\n inner.toggleAttribute('data-syntro-pdp-section-inner', true);\n const slotEl = document.createElement('syntro-pdp-slot') as SlotElementLike;\n // Unique per entry: duplicate kinds (two host-derived sections of the\n // same kind) must not produce duplicate mount ids.\n slotEl.slotId = `syntro-takeover-${index}-${slotId}`;\n slotEl.kind = kind;\n slotEl.state = 'pending';\n inner.appendChild(slotEl);\n section.append(header, inner);\n deps.root.appendChild(section);\n\n const fail = (): void => {\n if (hostSectionId) {\n // The module replaced detached host content and the engine is about\n // to reattach it \u2014 remove our section so the restored original never\n // sits next to a failed Syntro box.\n section.remove();\n } else {\n slotEl.state = 'failed';\n }\n deps.telemetry('takeover.module_failed', { entry, kind });\n onModuleFailed(hostSectionId);\n };\n\n const mountPayload = async (payload: Record<string, unknown>): Promise<void> => {\n await slotEl.updateComplete;\n const mount = slotEl.querySelector<HTMLElement>('.syntro-pdp-slot-mount');\n if (!mount) {\n fail();\n return;\n }\n mount.replaceChildren();\n const widget = document.createElement(tag) as HTMLElement & { data?: unknown };\n widget.data = payload;\n mount.appendChild(widget);\n slotEl.state = 'mounted';\n };\n\n if (preset) {\n // Baked payload \u2014 pool-selected for the scored decision archetype,\n // exactly like the six-slot path's transformPreset hook.\n const data = selectPresetData(preset.kind, preset.data, decisionArchetype) as Record<\n string,\n unknown\n >;\n bakedWork.push(mountPayload(data).catch(fail));\n } else {\n // Live kind \u2014 skeleton now, module streams in whenever it lands.\n // Deliberately NOT awaited by the renderer: the engine lifts the\n // curtain on skeletons; failures are contained via fail().\n void deps\n .requestLiveModule(kind, decisionArchetype)\n .then((payload) => (payload ? mountPayload(payload) : fail()))\n .catch(fail);\n }\n });\n\n await Promise.all(bakedWork);\n };\n}\n", "import { html, LitElement } from 'lit';\n\nimport './OtherProductTileLit.js';\nimport type { OtherProductTileData } from './OtherProductTileLit.js';\n\n/**\n * <pdp-other-products-carousel> \u2014 horizontal scroll carousel of cross-sell tiles.\n *\n * Each tile is an <other-product-tile>. Scroll-snap on the x-axis, edge-fade\n * mask to hint at overflow. Collapses to a wider card on narrow viewports.\n *\n * Props:\n * - `data` (OtherProductsPayload): `{ tiles, rationale }`.\n */\n\nexport interface OtherProductsPayload {\n tiles: OtherProductTileData[];\n rationale: string;\n}\n\nfunction ensureCarouselStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-other-products-carousel-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-other-products-carousel-style';\n s.textContent = CAROUSEL_CSS;\n document.head.appendChild(s);\n}\n\nconst CAROUSEL_CSS = `\n/* Host containment \u2014 must not bust the PDP container width. */\nsyntro-pdp-other-products-carousel {\n display: block;\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.opc-carousel {\n --opc-sans: var(--syntro-pdp-sans, inherit);\n font-family: var(--opc-sans);\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n/* Flex track (not grid) \u2014 grid auto-sizes to content even with overflow:auto,\n which would cause whole-page horizontal scroll. */\n.carousel {\n display: flex;\n flex-wrap: nowrap;\n gap: 0.75rem;\n overflow-x: auto;\n overflow-y: hidden;\n scroll-snap-type: x mandatory;\n scrollbar-width: thin;\n scrollbar-color: var(--syntro-pdp-border, hsl(30 6% 20%)) transparent;\n padding: 0.5rem 0.25rem 0.75rem;\n width: 100%;\n min-width: 0;\n /* Edge fade to hint at overflow content */\n mask-image: linear-gradient(90deg, transparent 0, black 12px, black calc(100% - 20px), transparent 100%);\n -webkit-mask-image: linear-gradient(90deg, transparent 0, black 12px, black calc(100% - 20px), transparent 100%);\n}\n.carousel::-webkit-scrollbar {\n height: 4px;\n}\n\n.opc-item {\n flex: 0 0 calc(25% - 0.6rem);\n scroll-snap-align: start;\n min-width: 220px;\n max-width: 300px;\n box-sizing: border-box;\n}\n\n@media (max-width: 720px) {\n .opc-item {\n flex: 0 0 80%;\n }\n}\n`;\n\nexport class PdpOtherProductsCarouselLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: OtherProductsPayload | null = null;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureCarouselStyles();\n }\n\n override render() {\n if (!this.data) return html``;\n return html`\n <div class=\"opc-carousel\">\n <div class=\"carousel\" role=\"list\">\n ${this.data.tiles.map(\n (tile) => html`\n <div class=\"opc-item\" role=\"listitem\">\n <syntro-pdp-other-product-tile .tile=${tile}></syntro-pdp-other-product-tile>\n </div>\n `\n )}\n </div>\n </div>\n `;\n }\n}\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('syntro-pdp-other-products-carousel')\n) {\n customElements.define('syntro-pdp-other-products-carousel', PdpOtherProductsCarouselLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-other-products-carousel': PdpOtherProductsCarouselLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\n/**\n * <other-product-tile> \u2014 a single cross-sell / upsell product tile.\n *\n * Renders a compact card with: optional image, relationship badge,\n * product name + tagline, an AI-authored why_blurb, evidence chips\n * (social-proof signals), and a price + CTA footer.\n *\n * Designed as the leaf node for <pdp-other-products-carousel> and\n * <pdp-other-products-grid>. It is also usable standalone.\n *\n * Props:\n * - `tile` (OtherProductTileData): full tile descriptor from the backend.\n */\n\nexport interface OtherProductTileEvidenceChip {\n kind: string;\n display: string;\n}\n\nexport interface OtherProductTileData {\n recommended_product_id: string;\n name: string;\n tagline: string;\n image_url: string | null;\n price_cents: number | null;\n badges: string[];\n relationship: 'discovery' | 'complement' | 'alternative';\n why_blurb: string;\n evidence_chips: OtherProductTileEvidenceChip[];\n}\n\nconst RELATIONSHIP_LABELS: Record<OtherProductTileData['relationship'], string> = {\n discovery: 'Recommended for you',\n complement: 'Pairs with this',\n alternative: 'Alternative',\n};\n\nfunction ensureOtherProductTileStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-other-product-tile-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-other-product-tile-style';\n s.textContent = OTHER_PRODUCT_TILE_CSS;\n document.head.appendChild(s);\n}\n\nconst OTHER_PRODUCT_TILE_CSS = `\n.opt-card {\n --opt-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --opt-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --opt-bg: var(--syntro-pdp-card-bg, hsl(25 10% 12%));\n --opt-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --opt-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --opt-chip-bg: var(--syntro-pdp-chip-bg, hsl(150 20% 16%));\n --opt-chip-fg: var(--syntro-pdp-chip-fg, hsl(150 32% 72%));\n --opt-sans: var(--syntro-pdp-sans, inherit);\n --opt-display: var(--syntro-pdp-display, inherit);\n\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 0.875rem;\n border: 1px solid var(--opt-border);\n border-radius: 14px;\n background: var(--opt-bg);\n font-family: var(--opt-sans);\n color: var(--opt-fg);\n transition: border-color 180ms ease;\n}\n.opt-card:hover {\n border-color: color-mix(in srgb, var(--opt-primary) 30%, var(--opt-border));\n}\n\n.opt-image {\n aspect-ratio: 1 / 1;\n border-radius: 8px;\n overflow: hidden;\n background: hsl(25 10% 15%);\n}\n.opt-image img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.opt-relationship {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.14em;\n color: var(--opt-primary);\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 0.3em;\n}\n/* The raw relationship key (.opt-relationship-type) is always in the DOM for\n machine-readable queries and test assertions. It is visually hidden \u2014 the\n human-readable label from RELATIONSHIP_LABELS is the visible text. */\n.opt-relationship-type {\n font-size: 0;\n width: 0;\n overflow: hidden;\n position: absolute;\n}\n\n.opt-name {\n font-weight: 700;\n font-size: 1rem;\n margin: 0;\n line-height: 1.2;\n color: var(--opt-fg);\n}\n\n.opt-tagline {\n font-size: 0.82rem;\n color: var(--opt-fg-muted);\n margin: 0;\n line-height: 1.3;\n}\n\n.opt-why {\n font-family: var(--opt-display);\n font-style: italic;\n font-size: 13.5px;\n line-height: 1.45;\n margin: 0;\n color: var(--opt-fg);\n font-variation-settings: 'SOFT' 70;\n}\n\n.opt-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 0.3rem;\n}\n.opt-chip {\n font-size: 0.68rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n background: var(--opt-chip-bg);\n color: var(--opt-chip-fg);\n font-weight: 500;\n letter-spacing: 0.02em;\n}\n\n.opt-footer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 0.25rem;\n gap: 0.5rem;\n}\n.opt-price {\n font-weight: 700;\n font-size: 0.95rem;\n font-variant-numeric: tabular-nums;\n}\n.opt-cta {\n border: 1px solid var(--opt-border);\n background: transparent;\n color: var(--opt-fg);\n padding: 0.35rem 0.75rem;\n border-radius: 8px;\n cursor: pointer;\n font-size: 0.8rem;\n font-family: var(--opt-sans);\n font-weight: 500;\n transition: border-color 150ms ease, background 150ms ease;\n}\n.opt-cta:hover {\n border-color: var(--opt-primary);\n background: color-mix(in srgb, var(--opt-primary) 10%, transparent);\n}\n`;\n\nexport class OtherProductTileLit extends LitElement {\n static override properties = {\n tile: { attribute: false },\n };\n\n tile: OtherProductTileData | null = null;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureOtherProductTileStyles();\n }\n\n override render() {\n const t = this.tile;\n if (!t) {\n return html``;\n }\n const relationshipLabel = RELATIONSHIP_LABELS[t.relationship];\n const price = t.price_cents == null ? '' : `$${(t.price_cents / 100).toFixed(0)}`;\n return html`\n <article class=\"opt-card\" data-relationship=\"${t.relationship}\">\n ${\n t.image_url\n ? html`<div class=\"opt-image\">\n <img src=\"${t.image_url}\" alt=\"${t.name}\" loading=\"lazy\" />\n </div>`\n : null\n }\n <div class=\"opt-relationship\" data-relationship=\"${t.relationship}\">\n ${relationshipLabel ? html`${relationshipLabel}` : null}\n <span class=\"opt-relationship-type\">${t.relationship}</span>\n </div>\n <h3 class=\"opt-name\">${t.name}</h3>\n ${t.tagline ? html`<p class=\"opt-tagline\">${t.tagline}</p>` : null}\n ${t.why_blurb ? html`<p class=\"opt-why\">${t.why_blurb}</p>` : null}\n ${\n t.evidence_chips.length\n ? html`<div class=\"opt-chips\">\n ${t.evidence_chips.map((c) => html`<span class=\"opt-chip\">${c.display}</span>`)}\n </div>`\n : null\n }\n <div class=\"opt-footer\">\n <span class=\"opt-price\">${price}</span>\n <button type=\"button\" class=\"opt-cta\" data-action=\"view-product\">View</button>\n </div>\n </article>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-other-product-tile')) {\n customElements.define('syntro-pdp-other-product-tile', OtherProductTileLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-other-product-tile': OtherProductTileLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\nimport './OtherProductTileLit.js';\nimport type { OtherProductsPayload } from './PdpOtherProductsCarouselLit.js';\n\n/**\n * <pdp-other-products-grid> \u2014 2-column grid of cross-sell tiles.\n *\n * Collapses to a single column on narrow viewports.\n *\n * Props:\n * - `data` (OtherProductsPayload): `{ tiles, rationale }`.\n */\n\nfunction ensureGridStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-other-products-grid-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-other-products-grid-style';\n s.textContent = GRID_CSS;\n document.head.appendChild(s);\n}\n\nconst GRID_CSS = `\nsyntro-pdp-other-products-grid {\n display: block;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n}\n\n.grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0.75rem;\n}\n\n@media (max-width: 720px) {\n .grid {\n grid-template-columns: 1fr;\n }\n}\n`;\n\nexport class PdpOtherProductsGridLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: OtherProductsPayload | null = null;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureGridStyles();\n }\n\n override render() {\n if (!this.data) return html``;\n return html`\n <div class=\"grid\" role=\"list\">\n ${this.data.tiles.map(\n (tile) => html`\n <syntro-pdp-other-product-tile role=\"listitem\" .tile=${tile}></syntro-pdp-other-product-tile>\n `\n )}\n </div>\n `;\n }\n}\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('syntro-pdp-other-products-grid')\n) {\n customElements.define('syntro-pdp-other-products-grid', PdpOtherProductsGridLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-other-products-grid': PdpOtherProductsGridLit;\n }\n}\n", "/**\n * Native slot watcher \u2014 the quick-view-modal half of the native-takeover\n * feature (FEAT-1784346204).\n *\n * On the vela SPA there is NO `<syntro-pdp>` element: the quick-view modal\n * server-renders its OWN native FAQ / review sections (the Task 4 shape). This\n * watcher observes those sections appearing, scores the visitor ONCE per product,\n * swaps the personalized preset content into the host sections (via the Task 1\n * `swapNativeSlot` engine), mounts a compact trending-news \"extras\" section, and\n * speaks a small DOM-event protocol that the demo toggle (Task 7) drives:\n *\n * listens (document): `syntro:demo-toggle` \u2014 CustomEvent<{ on: boolean }>\n * emits (document): `syntro:pdp-native-swap` \u2014 CustomEvent<{\n * state: 'swapped' | 'restored' | 'default-kept' | 'unavailable';\n * productId: string;\n * summary: string; // \"Tailored for: runner \u00B7 fit_risk\"; '' when not swapped\n * canToggle: boolean; // true whenever flipping the toggle can visibly change\n * // anything \u2014 a personalized overlap swap OR the cold\n * // generic extras. false for `unavailable` and for a\n * // cold visitor whose product has no `unknown`-row\n * // trending-news to mount.\n * extrasMounted: boolean; // whether the compact trending-news extras section is\n * // currently in the DOM. A cold-with-extras visitor\n * // emits `default-kept` + extrasMounted:true.\n * archetype: string; // raw scorer sport slug ('' unless swapped) \u2014 the\n * // band maps it to HUMAN words and never prints it.\n * decision: string; // raw scorer decision slug ('' unless swapped).\n * }>\n *\n * The four states are unchanged. A fully-unknown visitor on a product that HAS an\n * `unknown` preset row with trending-news still keeps the overlap sections\n * (qa/peer-feed) at their defaults, but mounts the generic extras \u2014 parity with\n * `<syntro-pdp>`, which renders the unknown preset's extra sections. That case\n * emits `default-kept` + canToggle:true + extrasMounted:true.\n *\n * It is INERT on pages owned by `<syntro-pdp>` (that element does its own native\n * takeover): if one exists in the document when the watcher starts, it never\n * observes. Single watcher per page; module-scoped state; never throws.\n */\n\n// WI-4: presets come through the store accessor \u2014 populated by the\n// runtime-sdk bootstrap fetch (config `pdpPresetsUrl`) or by legacy demo\n// boot scripts; never read the window global directly here.\nimport { getPdpPresets, pdpPresetsFetchStarted, pdpPresetsReady } from '../presets-store';\nimport { DISPLAY_MAX, selectPoolItems, type TaggedItem } from '../scoring/pool-selection';\nimport { gatherVisitorSignals } from '../widgets/SyntroPdpLit';\nimport {\n NATIVE_KIND_BY_SLOT,\n NATIVE_PRODUCT_ATTR,\n NATIVE_SLOT_ATTR,\n type NativeItemFields,\n type NativeSlotKind,\n type NativeSwapHandle,\n nativeFieldsFromContent,\n nativeProductId,\n swapNativeSlot,\n} from './nativeSlotSwap';\n\n// \u2500\u2500\u2500 global reads (mirror SyntroPdpLit's access patterns) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** One config-set slot value: the widget kind, an optional header, and its content. */\ninterface SlotPreset {\n kind: string;\n header?: unknown;\n data: unknown;\n}\n/** `presets[product][archetype][slot] = SlotPreset` \u2014 the config's SET values. */\ntype PdpPresetTable = Record<string, Record<string, Record<string, SlotPreset>>>;\n\ninterface ScoreSpec {\n model_id: string;\n name: string;\n type: string;\n values?: string[];\n value_descriptions?: Record<string, string>;\n signals?: Record<string, unknown>;\n}\ninterface SynOSGlobal {\n score?: (spec: ScoreSpec) => Promise<string>;\n scoreMulti?: (specs: ScoreSpec[]) => Promise<string[]>;\n}\n\nfunction readSynOS(): SynOSGlobal | undefined {\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { SynOS?: SynOSGlobal }).SynOS;\n}\n/** Presets via the WI-4 store accessor (config `pdpPresetsUrl` fetch OR the\n * legacy hand-set window global \u2014 read lazily, absence never cached). */\nfunction readPresets(): PdpPresetTable | undefined {\n return getPdpPresets() as PdpPresetTable | undefined;\n}\nfunction readArchetypeDescriptions(): Record<string, string> {\n if (typeof window === 'undefined') return {};\n return (\n (window as unknown as { __SYNTRO_PDP_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_ARCHETYPES__ ?? {}\n );\n}\nfunction readDecisionDescriptions(): Record<string, string> {\n if (typeof window === 'undefined') return {};\n return (\n (window as unknown as { __SYNTRO_PDP_DECISION_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_DECISION_ARCHETYPES__ ?? {}\n );\n}\n\n// The runtime PDP endpoints cap chat_excerpt; the element folds it into signals,\n// so mirror that here (best-effort \u2014 a missing excerpt degrades to '').\nconst CHAT_EXCERPT_KEY = 'syntro.chat.lastExcerpt';\nconst CHAT_EXCERPT_MAX = 7500;\nfunction readChatExcerpt(): string {\n try {\n if (typeof sessionStorage === 'undefined') return '';\n const raw = sessionStorage.getItem(CHAT_EXCERPT_KEY) ?? '';\n return raw.length > CHAT_EXCERPT_MAX ? `\u2026${raw.slice(-CHAT_EXCERPT_MAX + 1)}` : raw;\n } catch {\n return '';\n }\n}\n\nconst TRENDING_NEWS_TAG = 'syntro-pdp-trending-news';\nconst SECTION_HEADER_TAG = 'syntro-pdp-section-header';\nconst TRENDING_NEWS_SLOT = 'pdp-slot-trending-news';\nconst EXTRAS_ATTR = 'data-syntro-native-extras';\nconst UNKNOWN = 'unknown';\n\n// \u2500\u2500\u2500 decision (computed once per product) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface ScorePair {\n archetype: string;\n decision: string;\n}\n\ntype Decision =\n | { kind: 'unavailable' }\n | {\n kind: 'default-kept';\n /** the generic (`unknown`-row) trending-news payload for a cold visitor's\n * extras section, or null when there is nothing to mount. When non-null the\n * toggle is meaningful: it mounts/removes these extras. */\n extrasData: { payload: Record<string, unknown>; header: ExtrasHeader | null } | null;\n }\n | {\n kind: 'personalized';\n summary: string;\n /** Raw scorer slugs \u2014 carried through so the demo band can map them to\n * HUMAN words (never printed as slugs). */\n archetype: string;\n decision: string;\n /** slot id \u2192 the flat host-field items to fill (native qa / peer-feed). */\n fieldsBySlot: Map<string, NativeItemFields[]>;\n /** the pool-selected trending-news payload for the extras section, or null. */\n extrasData: { payload: Record<string, unknown>; header: ExtrasHeader | null } | null;\n };\n\n// \u2500\u2500\u2500 module-scoped state (single watcher per page) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Per-product score, computed at most once per pageload. */\nconst scoreCache = new Map<string, Promise<ScorePair>>();\n\ninterface Session {\n productId: string;\n decision: Decision;\n /** slot id \u2192 live swap handle (for restore on toggle-off / product switch). */\n handles: Map<string, NativeSwapHandle>;\n extras: HTMLElement | null;\n swapped: boolean;\n /** last emitted state \u2014 avoids re-emitting an unchanged state on spurious scans. */\n lastState: string | null;\n /** For NON-personalized (default-kept/unavailable) sessions: the native\n * container that anchored the last emit. When it detaches (modal closed) the\n * reconcile clears `lastState` so a re-appearing container re-emits. */\n anchor: HTMLElement | null;\n}\nlet session: Session | null = null;\n\n/** Monotonic request counter \u2014 only the latest-requested product may install a\n * session after its (possibly slow) score resolves. Guards the compute race. */\nlet requestSeq = 0;\n\n/** Personalization defaults ON (the demo's \"wow\"); the toggle turns it off/on. */\nlet toggleOn = true;\n\nlet observer: MutationObserver | null = null;\n/** True once a `<syntro-pdp>` was found at start \u2014 the watcher then stays inert. */\nlet disabled = false;\nlet currentStop: (() => void) | null = null;\n\nconst pending = new Set<string>();\nlet flushScheduled = false;\n\n// \u2500\u2500\u2500 scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function doScore(\n productPresets: Record<string, Record<string, SlotPreset>>\n): Promise<ScorePair> {\n const synos = readSynOS();\n const score = synos?.score;\n if (typeof score !== 'function') return { archetype: UNKNOWN, decision: UNKNOWN };\n const scoreMulti = synos?.scoreMulti;\n const signals = { ...gatherVisitorSignals(), chat_excerpt: readChatExcerpt() };\n const archetypeValues = Object.keys(productPresets).filter((v) => v !== UNKNOWN);\n const archetypeDescriptions = readArchetypeDescriptions();\n const decisionDescriptions = readDecisionDescriptions();\n const decisionValues = Object.keys(decisionDescriptions);\n\n const archetypeSpec: ScoreSpec = {\n model_id: 'llm',\n name: 'archetype',\n type: 'enum',\n values: archetypeValues,\n value_descriptions: Object.fromEntries(\n archetypeValues\n .filter((v) => archetypeDescriptions[v])\n .map((v) => [v, archetypeDescriptions[v]])\n ),\n signals,\n };\n\n // Two-axis scoring (sport archetype + decision archetype) through one model\n // call when both a decision taxonomy and the batched scorer are available \u2014\n // exactly the SyntroPdpLit path.\n if (decisionValues.length > 0 && scoreMulti) {\n const decisionSpec: ScoreSpec = {\n model_id: 'llm',\n name: 'decision_archetype',\n type: 'enum',\n values: decisionValues,\n value_descriptions: decisionDescriptions,\n signals,\n };\n const results = await scoreMulti([archetypeSpec, decisionSpec]);\n return { archetype: results[0] ?? UNKNOWN, decision: results[1] ?? UNKNOWN };\n }\n const archetype = await score(archetypeSpec);\n return { archetype: archetype ?? UNKNOWN, decision: UNKNOWN };\n}\n\nfunction scoreOnce(\n productId: string,\n productPresets: Record<string, Record<string, SlotPreset>>\n): Promise<ScorePair> {\n let p = scoreCache.get(productId);\n if (!p) {\n p = doScore(productPresets);\n scoreCache.set(productId, p);\n }\n return p;\n}\n\n// \u2500\u2500\u2500 field / extras selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction poolOf(preset: SlotPreset, kind: NativeSlotKind): TaggedItem[] {\n const data = preset.data as Record<string, unknown> | null;\n const list = kind === 'peer-feed' ? data?.peers : data?.items;\n return Array.isArray(list) ? (list as TaggedItem[]) : [];\n}\n\n/** Build the flat host-field items for one native slot from its scored preset. */\nfunction fieldsForSlot(\n row: Record<string, SlotPreset>,\n slotId: string,\n kind: NativeSlotKind,\n decision: string\n): NativeItemFields[] {\n const preset = row[slotId];\n if (!preset) return [];\n const selected = selectPoolItems(poolOf(preset, kind), decision, DISPLAY_MAX[kind] ?? Infinity);\n const content = kind === 'peer-feed' ? { peers: selected } : { items: selected };\n return nativeFieldsFromContent(kind, content);\n}\n\n/** The preset slot's section-header shape (eyebrow/title/accent/subtitle). */\ninterface ExtrasHeader {\n eyebrow?: string | null;\n title?: string | null;\n accent?: string | null;\n subtitle?: string | null;\n}\n\n/** Pool-selected trending-news payload + the slot's section header, so the\n * modal's extras carry the same heading the page's section wrapper renders\n * (a header-less article pile reads as a rendering glitch \u2014 Parker,\n * 2026-07-18). */\nfunction extrasDataFor(\n row: Record<string, SlotPreset>,\n decision: string\n): { payload: Record<string, unknown>; header: ExtrasHeader | null } | null {\n const preset = row[TRENDING_NEWS_SLOT];\n if (!preset || typeof preset.data !== 'object' || preset.data === null) return null;\n const data = preset.data as Record<string, unknown>;\n const items = Array.isArray(data.items) ? (data.items as TaggedItem[]) : [];\n const selected = selectPoolItems(items, decision, DISPLAY_MAX['trending-news'] ?? Infinity);\n const header =\n typeof preset.header === 'object' && preset.header !== null\n ? (preset.header as ExtrasHeader)\n : null;\n return { payload: { ...data, items: selected }, header };\n}\n\n/** A visitor is personalized unless BOTH scored axes declined to 'unknown'. */\nfunction isPersonalized(pair: ScorePair): boolean {\n return !(pair.archetype === UNKNOWN && pair.decision === UNKNOWN);\n}\n\nfunction summaryFor(pair: ScorePair): string {\n return pair.decision !== UNKNOWN\n ? `Tailored for: ${pair.archetype} \u00B7 ${pair.decision}`\n : `Tailored for: ${pair.archetype}`;\n}\n\nasync function computeDecision(productId: string): Promise<Decision> {\n // WI-4 ready gate: an in-flight config-driven presets fetch must land\n // before we snapshot the table \u2014 otherwise a modal opening during boot\n // would settle a (cached) 'unavailable' decision from an empty store.\n // Conditional so the no-fetch path keeps its exact synchronous timing\n // (the latest-request-wins race guard depends on score being called\n // within scanNow's first microtask). Never rejects.\n if (pdpPresetsFetchStarted()) await pdpPresetsReady();\n const productPresets = readPresets()?.[productId];\n const score = readSynOS()?.score;\n if (!productPresets || typeof score !== 'function') {\n console.warn('syntro-pdp.native_watch_unavailable', {\n productId,\n reason: !productPresets ? 'no-presets' : 'no-score',\n });\n return { kind: 'unavailable' };\n }\n try {\n const pair = await scoreOnce(productId, productPresets);\n if (!isPersonalized(pair)) {\n // Cold visitor: the overlap sections (qa/peer-feed) stay at their defaults,\n // but the generic extras still mount \u2014 the `unknown` sport-archetype row's\n // trending-news pool, sprinkle-selected (decision 'unknown'). This is the\n // same content `<syntro-pdp>` shows an unknown visitor. When the product has\n // no `unknown` row (or it carries no trending-news) there is nothing to\n // mount and the toggle stays inert.\n const unknownRow = productPresets[UNKNOWN];\n const extrasData = unknownRow ? extrasDataFor(unknownRow, UNKNOWN) : null;\n return { kind: 'default-kept', extrasData };\n }\n // Row for the scored archetype; fall back to the 'unknown' row when the\n // archetype has no dedicated preset row (keep-defaults otherwise).\n const row = productPresets[pair.archetype] ?? productPresets[UNKNOWN];\n if (!row) return { kind: 'default-kept', extrasData: null };\n const fieldsBySlot = new Map<string, NativeItemFields[]>();\n for (const [slotId, kind] of Object.entries(NATIVE_KIND_BY_SLOT)) {\n fieldsBySlot.set(slotId, fieldsForSlot(row, slotId, kind, pair.decision));\n }\n return {\n kind: 'personalized',\n summary: summaryFor(pair),\n archetype: pair.archetype,\n decision: pair.decision,\n fieldsBySlot,\n extrasData: extrasDataFor(row, pair.decision),\n };\n } catch (err) {\n console.warn('syntro-pdp.native_watch_unavailable', { productId, error: String(err) });\n return { kind: 'unavailable' };\n }\n}\n\n// \u2500\u2500\u2500 DOM helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** The native slot container for a product, ignoring any inside a `<syntro-pdp>`. */\nfunction slotContainer(productId: string, kind: NativeSlotKind): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n for (const el of document.querySelectorAll<HTMLElement>(`[${NATIVE_SLOT_ATTR}=\"${kind}\"]`)) {\n if (el.closest('syntro-pdp')) continue;\n if (nativeProductId(el) === productId) return el;\n }\n return null;\n}\n\ntype SwapEventState = 'swapped' | 'restored' | 'default-kept' | 'unavailable';\n\nfunction emit(\n state: SwapEventState,\n productId: string,\n summary: string,\n canToggle: boolean,\n extrasMounted: boolean,\n archetype = '',\n decision = ''\n): void {\n if (typeof document === 'undefined') return;\n document.dispatchEvent(\n new CustomEvent('syntro:pdp-native-swap', {\n detail: { state, productId, summary, canToggle, extrasMounted, archetype, decision },\n })\n );\n}\n\nfunction removeExtras(): void {\n if (!session) return;\n session.extras?.remove();\n session.extras = null;\n}\n\nfunction restoreHandles(): void {\n if (!session) return;\n for (const handle of session.handles.values()) {\n // Only restore containers still in the tree; a closed modal already removed\n // them, so restore() would reattach originals to a detached anchor.\n if (handle.container.isConnected) handle.restore();\n }\n session.handles.clear();\n}\n\nfunction tearDownSwap(): void {\n restoreHandles();\n removeExtras();\n if (session) session.swapped = false;\n}\n\n/**\n * Mount the compact trending-news extras section for a product, appended after the\n * peer-feed section's PARENT block (falling back to the qa section). Shared by the\n * personalized swap and the cold generic-extras path. Returns true when it mounted\n * (a resolvable container existed) so the caller can decide whether anything\n * visibly changed. Records the node on `session.extras`.\n */\nfunction mountExtras(\n productId: string,\n extrasData: { payload: Record<string, unknown>; header: ExtrasHeader | null }\n): boolean {\n if (!session || typeof document === 'undefined') return false;\n const anchor = slotContainer(productId, 'peer-feed') ?? slotContainer(productId, 'qa');\n if (!anchor) return false;\n const wrap = document.createElement('div');\n wrap.setAttribute(EXTRAS_ATTR, '');\n // Same section-header component the page's <syntro-pdp> wrapper renders \u2014\n // fed from the preset slot's own header (\"In the news / Recent research\").\n const header = document.createElement(SECTION_HEADER_TAG) as HTMLElement & {\n eyebrow?: string;\n title?: string;\n accent?: string;\n subtitle?: string;\n };\n header.eyebrow = extrasData.header?.eyebrow ?? 'In the news';\n header.title = extrasData.header?.title ?? '';\n header.accent = extrasData.header?.accent ?? '';\n header.subtitle = extrasData.header?.subtitle ?? '';\n wrap.appendChild(header);\n const news = document.createElement(TRENDING_NEWS_TAG) as HTMLElement & { data?: unknown };\n news.data = extrasData.payload;\n wrap.appendChild(news);\n // Insert after the native section's PARENT block \u2014 on the real modal DOM the\n // <section data-syntro-slot=\"peer-feed\"> is nested inside a Reviews card, so\n // anchoring on the section itself would cram the extras INSIDE that card.\n // Guard: if the parent IS the product-id panel host (flat markup) or is\n // missing, fall back to the container itself, so the extras can never escape\n // the modal panel.\n const panelHost = anchor.closest(`[${NATIVE_PRODUCT_ATTR}]`);\n const parent = anchor.parentElement;\n const insertAfter = parent && parent !== panelHost ? parent : anchor;\n insertAfter.after(wrap);\n session.extras = wrap;\n return true;\n}\n\n// \u2500\u2500\u2500 render (apply desired DOM for the current session + toggle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Apply the desired swap for a personalized session. Returns the number of\n * native slots successfully swapped \u2014 the caller must NOT report/emit 'swapped'\n * when this is 0 (e.g. the product's containers are not in the DOM yet, or were\n * already detached by a modal close), or the pill would claim a swap that never\n * touched the page.\n */\nfunction applySwap(\n productId: string,\n decision: Extract<Decision, { kind: 'personalized' }>\n): number {\n if (!session) return 0;\n // Swap each native slot that is present with a resolvable container.\n for (const [slotId, kind] of Object.entries(NATIVE_KIND_BY_SLOT)) {\n const container = slotContainer(productId, kind);\n if (!container) continue;\n const fields = decision.fieldsBySlot.get(slotId) ?? [];\n // A container carries at most one live swap \u2014 restore a stale one first.\n session.handles.get(slotId)?.restore();\n session.handles.delete(slotId);\n const handle = swapNativeSlot(container, fields);\n if (handle) session.handles.set(slotId, handle);\n }\n // Extras: a compact trending-news section. Skip when there is no trending-news\n // preset for this archetype.\n if (decision.extrasData) mountExtras(productId, decision.extrasData);\n const swappedCount = session.handles.size;\n // Only mark the session swapped when at least one container was actually\n // rewritten. Leaving `swapped` false lets a later render retry once the\n // containers appear.\n if (swappedCount > 0) session.swapped = true;\n return swappedCount;\n}\n\n/** The native container currently anchoring a non-personalized session (used to\n * detect a modal close \u2192 reopen so the pill can re-emit). */\nfunction anchorContainer(productId: string): HTMLElement | null {\n return slotContainer(productId, 'peer-feed') ?? slotContainer(productId, 'qa');\n}\n\n/**\n * Render a cold (fully-unknown) session. The overlap sections are left at their\n * defaults; the ONLY thing the toggle drives is the generic extras section (when\n * the product's `unknown` row carries trending-news). `canToggle` is true exactly\n * when there ARE such extras to mount/remove.\n */\nfunction renderColdSession(\n productId: string,\n decision: Extract<Decision, { kind: 'default-kept' }>\n): void {\n if (!session) return;\n const canToggle = decision.extrasData !== null;\n\n // Reconcile detachment: if the anchoring container detached (modal closed) OR\n // the mounted extras detached, reset so a re-appearing modal re-mounts and\n // re-emits \u2014 the pill must resolve correctly on every surface, incl. re-open.\n const anchorDetached = session.anchor !== null && !session.anchor.isConnected;\n const extrasDetached = session.extras !== null && !session.extras.isConnected;\n if (anchorDetached || extrasDetached) {\n session.anchor = null;\n if (extrasDetached) session.extras = null;\n session.lastState = null;\n }\n\n // Desired extras presence follows the toggle (only meaningful when extras exist).\n const wantExtras = canToggle && toggleOn;\n let changed = false;\n if (wantExtras && session.extras === null && decision.extrasData) {\n if (mountExtras(productId, decision.extrasData)) changed = true;\n } else if (!wantExtras && session.extras !== null) {\n removeExtras();\n changed = true;\n }\n\n const extrasMounted = session.extras !== null;\n // Emit on first appearance of the state, or whenever the extras presence flips.\n if (session.lastState !== 'default-kept' || changed) {\n session.lastState = 'default-kept';\n session.anchor = anchorContainer(productId);\n emit('default-kept', productId, '', canToggle, extrasMounted);\n }\n}\n\nfunction renderSession(): void {\n if (!session) return;\n const { productId, decision } = session;\n\n if (decision.kind === 'unavailable') {\n // Reconcile the anchoring container: if the one that anchored the last emit\n // has detached (modal closed), clear lastState so a re-appearing container\n // re-emits \u2014 the pill must resolve on every surface, including on re-open.\n if (session.anchor && !session.anchor.isConnected) {\n session.anchor = null;\n session.lastState = null;\n }\n if (session.lastState !== 'unavailable') {\n session.lastState = 'unavailable';\n session.anchor = anchorContainer(productId);\n emit('unavailable', productId, '', false, false);\n }\n return;\n }\n\n if (decision.kind === 'default-kept') {\n renderColdSession(productId, decision);\n return;\n }\n\n // Personalized: reconcile a stale swap (modal re-opened \u2192 old nodes detached).\n if (session.swapped) {\n const stale =\n [...session.handles.values()].some((h) => !h.container.isConnected) ||\n (session.extras !== null && !session.extras.isConnected);\n if (stale) {\n session.handles.clear();\n session.extras = null;\n session.swapped = false;\n session.lastState = null;\n }\n }\n\n if (toggleOn && !session.swapped) {\n // Only report 'swapped' when a container was actually rewritten \u2014 a swap of\n // zero containers (product's slots absent, e.g. superseded/closed) must not\n // claim a swap.\n const swappedCount = applySwap(productId, decision);\n if (swappedCount > 0) {\n session.lastState = 'swapped';\n emit(\n 'swapped',\n productId,\n decision.summary,\n true,\n session.extras !== null,\n decision.archetype,\n decision.decision\n );\n }\n } else if (!toggleOn && session.swapped) {\n tearDownSwap();\n session.lastState = 'restored';\n emit('restored', productId, '', true, false);\n }\n}\n\n// \u2500\u2500\u2500 flush (batched appearing-product processing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function onProductAppeared(productId: string): Promise<void> {\n if (session && session.productId !== productId) {\n // Product switch: the previous product's containers are gone (or being\n // replaced) \u2014 drop its swap without emitting for the old product.\n tearDownSwap();\n session = null;\n }\n if (!session) {\n // Capture a request token BEFORE the await. Only the latest-requested\n // product may install the session once its score resolves \u2014 otherwise a\n // slow compute for a product the visitor already left could resolve FIRST\n // and lock the session to the wrong product (its containers are gone),\n // falsely claiming a swap while the current product keeps defaults.\n const token = ++requestSeq;\n const decision = await computeDecision(productId);\n // A late-arriving decision for a product that has since been superseded is\n // discarded \u2014 either another product already took the session, or a newer\n // request has been issued since this one started.\n if (session || token !== requestSeq) return;\n session = {\n productId,\n decision,\n handles: new Map(),\n extras: null,\n swapped: false,\n lastState: null,\n anchor: null,\n };\n }\n renderSession();\n}\n\nasync function flush(): Promise<void> {\n flushScheduled = false;\n if (disabled) {\n pending.clear();\n return;\n }\n // A <syntro-pdp> can mount AFTER the watcher started \u2014 the config-driven\n // boundary mount, including the page-takeover root (Stage 2). The start-time\n // self-guard can't see a late mount, so re-check per flush: a page owned by\n // the element (it does its own native takeover / page takeover) must never\n // also be touched by the watcher.\n if (typeof document !== 'undefined' && document.querySelector('syntro-pdp')) {\n pending.clear();\n return;\n }\n const ids = [...pending];\n pending.clear();\n for (const id of ids) await onProductAppeared(id);\n}\n\nfunction scheduleFlush(): void {\n if (flushScheduled || disabled) return;\n flushScheduled = true;\n queueMicrotask(() => void flush());\n}\n\n/** Scan one added subtree for native slot containers with a resolvable product id. */\nfunction collectFromNode(node: Node): void {\n if (!(node instanceof HTMLElement)) return;\n const containers: HTMLElement[] = [];\n if (node.matches(`[${NATIVE_SLOT_ATTR}]`)) containers.push(node);\n containers.push(...node.querySelectorAll<HTMLElement>(`[${NATIVE_SLOT_ATTR}]`));\n for (const c of containers) {\n if (c.closest('syntro-pdp')) continue;\n const pid = nativeProductId(c);\n if (pid) pending.add(pid);\n }\n}\n\n// \u2500\u2500\u2500 event handlers (arrow functions \u2014 project convention) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst onDemoToggle = (e: Event): void => {\n const detail = (e as CustomEvent<{ on?: boolean }>).detail;\n if (!detail || typeof detail.on !== 'boolean') return;\n toggleOn = detail.on;\n renderSession();\n};\n\nconst onMutations: MutationCallback = (records) => {\n for (const record of records) {\n for (const node of record.addedNodes) collectFromNode(node);\n }\n scheduleFlush();\n};\n\n/** Attach the body observer. Deferred to DOMContentLoaded when `document.body`\n * isn't parsed yet at start (module-eval) time. */\nconst onDomReady = (): void => {\n if (disabled || observer || typeof document === 'undefined' || !document.body) return;\n observer = new MutationObserver(onMutations);\n observer.observe(document.body, { childList: true, subtree: true });\n};\n\n// \u2500\u2500\u2500 public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Start the native slot watcher. Idempotent: a second call is a no-op that\n * returns the SAME stop function (single watcher per page).\n *\n * No-ops (never observes) when a `<syntro-pdp>` element already owns the page \u2014\n * that element performs its own native takeover.\n */\nexport function startNativeSlotWatcher(): () => void {\n if (currentStop) return currentStop;\n\n if (typeof document !== 'undefined' && document.querySelector('syntro-pdp')) {\n // Page owned by <syntro-pdp>: stay inert. Still return a stop so callers can\n // treat activation uniformly.\n disabled = true;\n currentStop = () => {\n disabled = false;\n currentStop = null;\n };\n return currentStop;\n }\n\n disabled = false;\n if (typeof document !== 'undefined') {\n document.addEventListener('syntro:demo-toggle', onDemoToggle);\n if (document.body) {\n observer = new MutationObserver(onMutations);\n observer.observe(document.body, { childList: true, subtree: true });\n } else {\n // Module-eval before <body> is parsed (runtime.ts side-effect): observing a\n // null body would THROW and abort the rest of runtime.ts. Defer to DOM ready.\n document.addEventListener('DOMContentLoaded', onDomReady, { once: true });\n }\n }\n\n currentStop = () => {\n observer?.disconnect();\n observer = null;\n if (typeof document !== 'undefined') {\n document.removeEventListener('syntro:demo-toggle', onDemoToggle);\n document.removeEventListener('DOMContentLoaded', onDomReady);\n }\n tearDownSwap();\n session = null;\n requestSeq = 0;\n scoreCache.clear();\n pending.clear();\n flushScheduled = false;\n toggleOn = true;\n currentStop = null;\n };\n return currentStop;\n}\n\n/**\n * Test seam. Fully resets module state between tests (production code never calls\n * this). `scanNow` synchronously scans the whole document for native containers\n * and drives the SAME flush logic the observer uses \u2014 a deterministic stand-in\n * for jsdom's async MutationObserver.\n */\nexport const _testing = {\n async scanNow(): Promise<void> {\n if (typeof document !== 'undefined') collectFromNode(document.body);\n await flush();\n },\n readSynOSForTest: readSynOS,\n readPresetsForTest: readPresets,\n readArchetypeDescriptionsForTest: readArchetypeDescriptions,\n readDecisionDescriptionsForTest: readDecisionDescriptions,\n readChatExcerptForTest: readChatExcerpt,\n doScoreForTest: doScore,\n fieldsForSlotForTest: fieldsForSlot,\n extrasDataForTest: extrasDataFor,\n slotContainerForTest: slotContainer,\n mountExtrasForTest: mountExtras,\n emitForTest: emit,\n tearDownSwapForTest: tearDownSwap,\n reset(): void {\n observer?.disconnect();\n observer = null;\n if (typeof document !== 'undefined') {\n document.removeEventListener('syntro:demo-toggle', onDemoToggle);\n document.removeEventListener('DOMContentLoaded', onDomReady);\n }\n session = null;\n requestSeq = 0;\n scoreCache.clear();\n pending.clear();\n flushScheduled = false;\n toggleOn = true;\n disabled = false;\n currentStop = null;\n },\n};\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport { BindController } from '../bind/controller';\nimport {\n type CardEvent,\n type CardState,\n createInitialCardState,\n selectCardView,\n transitionCard,\n} from '../decision/card';\nimport { createCardEffectExecutor } from '../decision/card-effects';\nimport { getProductPlatformAdapter, type Variant } from '../platformAdapter';\nimport { sanitizeHtml } from '../sanitizer';\nimport { type CardProps, cardSchema, VISIBLE_FACT_KEYS, type VisibleFactKey } from '../schema';\nimport type { Product } from '../schema-primitives';\nimport { ensureStylesInRoot } from './ensureStylesInRoot';\nimport { reportRenderHealth } from './renderHealthReporter';\nimport './VariantPanelLit.js';\n\n/**\n * Typed interface for the runtime SDK's public window surface.\n * Catches symbol typos at compile time (e.g. __SYNTRO__ vs SynOS).\n * The runtime SDK sets `window.SynOS.handle` at api.ts:277/347.\n */\ninterface SynOSWindow {\n SynOS?: {\n handle?: { close?: () => void };\n runtime?: {\n events?: {\n publish?: (name: string, props: Record<string, unknown>) => void;\n // The ONLY transport that reaches PostHog. The EventBus forwards\n // emit()'d events to its posthogCapture sink (bootstrap-runtime wires\n // it to telemetry.track) after a consent gate; publish() does NOT \u2014\n // it only notifies in-process canvas subscribers. emit() enforces\n // validateEventName: a custom event MUST be `app:{category}:{action}`\n // (colon segments, no `app:` prefix \u2192 silently dropped).\n emit?: (name: string, props: Record<string, unknown>) => unknown;\n };\n context?: {\n get?(): { surface?: { device?: string } };\n subscribe?(cb: (ctx: { surface?: { device?: string } }) => void): () => void;\n };\n };\n };\n}\n\n/** Typed accessor \u2014 reads `window` on each call so tests can stub it at runtime. */\nfunction synWindow(): SynOSWindow {\n /* v8 ignore next -- ProductCard is a browser custom element; event/no-event paths cover this accessor in jsdom. */\n return (typeof window !== 'undefined' ? window : {}) as SynOSWindow;\n}\n\n/** Publish a named event through the runtime event bus (window.SynOS.runtime.events). */\nfunction _publishEvent(name: string, props: Record<string, unknown>): void {\n /* v8 ignore next -- SSR telemetry guard; telemetry publish and missing-publish behavior are covered in jsdom. */\n if (typeof window === 'undefined') return;\n try {\n const publish = synWindow().SynOS?.runtime?.events?.publish;\n if (typeof publish === 'function') {\n publish(name, props);\n }\n } catch {\n // Telemetry must never crash the widget.\n }\n}\n\n/**\n * Capture a telemetry event on the PostHog-reaching path.\n *\n * `publish()` only notifies in-process canvas subscribers \u2014 it never forwards\n * to PostHog. `emit()` is the sanctioned capture path: the EventBus forwards\n * emit()'d events to its posthogCapture sink (wired in bootstrap-runtime to the\n * consent-gated `telemetry.track`). We never call posthog directly \u2014 emit() is\n * the SDK's one consent-handled door to PostHog.\n *\n * `name` MUST be a valid custom-event name (`app:{category}:{action}` with\n * colon segments). A dotted name or a name missing the `app:` prefix is\n * silently dropped by the EventBus's validateEventName, which would re-create\n * the exact silent-failure this telemetry exists to surface.\n */\nfunction _captureTelemetry(name: string, props: Record<string, unknown>): void {\n /* v8 ignore next -- SSR telemetry guard; emit and missing-emit behavior are covered in jsdom. */\n if (typeof window === 'undefined') return;\n try {\n const emit = synWindow().SynOS?.runtime?.events?.emit;\n if (typeof emit === 'function') {\n emit(name, props);\n }\n } catch {\n // Telemetry must never crash the widget.\n }\n}\n\n/**\n * Derive a sorted, de-duplicated list of dotted field paths from Zod issues\n * (e.g. `['product.ctas', 'product.image.src']`). Used as the NON-PII context\n * for the `render_failed` telemetry event so the dashboard can see WHICH\n * required field made the card render blank. Only the structural path is\n * emitted \u2014 never the offending value, which could carry customer content.\n */\nfunction _invalidPathsFromIssues(\n issues: ReadonlyArray<{ path: ReadonlyArray<PropertyKey> }>\n): string[] {\n const paths = new Set<string>();\n for (const issue of issues) {\n // A non-empty path joins to a dotted field; an empty path means the root\n // object failed (e.g. `product` missing entirely) \u2014 record it as 'product'\n // so the signal is never blank.\n const dotted = issue.path.map((seg) => String(seg)).join('.');\n paths.add(dotted.length > 0 ? dotted : 'product');\n }\n return Array.from(paths).sort();\n}\n\n/**\n * The authoritative \"did not render\" telemetry event. Routed through emit()\n * (the PostHog-reaching path), so it MUST satisfy validateEventName:\n * `app:{category}:{action}` with colon segments. A dotted name would be\n * silently dropped \u2014 the very failure this signal exists to catch.\n */\nconst RENDER_FAILED_EVENT = 'app:product_card:render_failed';\n\n// Runtime self-detection (clipped / blank) is emitted through the shared\n// `reportRenderHealth` reporter \u2014 the same field-RUM counterpart every sibling\n// tile widget now uses (`app:product_card:clipped` / `:blank`). NON-PII: only\n// element paths + pixel numbers ever leave the widget.\n\ntype DeviceClass = 'mobile' | 'tablet' | 'desktop';\n\n/** The runtime's device class (surface.device), or 'desktop' when unavailable. */\nfunction _readDevice(): DeviceClass {\n /* v8 ignore next -- SSR device fallback for a browser element; runtime context branches are covered in jsdom. */\n if (typeof window === 'undefined') return 'desktop';\n try {\n const d = synWindow().SynOS?.runtime?.context?.get?.()?.surface?.device;\n return d === 'mobile' || d === 'tablet' ? d : 'desktop';\n } catch {\n return 'desktop';\n }\n}\n\n// \u2500\u2500 Card front-face LAYOUT tokens (canvas-owned, like the variant panel) \u2500\u2500\u2500\u2500\n//\n// The card owns the design VOCABULARY (pill spec chips, price-in-CTA-label,\n// panel color/radius tokens, compact rating, one-line tagline) \u2014 that's the\n// canvas-agnostic design system, so it lives inline here. But LAYOUT belongs\n// to the host canvas: a chat drawer / PDP wants a clean STACKED card, while\n// velvet's short-and-wide deck band wants a HORIZONTAL two-column card. Rather\n// than baking velvet's layout inline (which forced every surface into it),\n// layout is PARAMETERIZED via CSS custom properties with content-neutral\n// defaults. Only token VALUES cross the shadow boundary; no selectors, no\n// baked layout \u2014 the same discipline the VariantPanelLit --sc-variant-* tokens\n// use (and velvet sets both on the SAME deck-card rule).\n//\n// --sc-card-cols grid-template-columns of the front face's\n// image | content grid.\n// Default: density thumb + content \u2014 `72px 1fr`\n// (compact) / `110px 1fr` (standard), i.e. a small\n// top-aligned header-row thumbnail beside the content\n// (a clean STACKED card).\n// Velvet sets `minmax(72px, 30%) 1fr` \u2014 a real\n// two-column split where the image fills its column.\n//\n// --sc-card-image-max image size cap. Default a modest thumb\n// (`72px` compact / `110px` standard). Velvet sets\n// `100%` so the image fills (column-width) its column.\n//\n// (Which rows the image SPANS is decided by the named\n// `image` grid area \u2014 see --sc-card-areas \u2014 not a\n// separate row token; a grid-row override would unset\n// the area's row span.)\n//\n// --sc-card-image-h image height. Default `auto` (a square-ish thumb\n// sized by --sc-card-image-max). Velvet sets `100%`\n// so the full-height panel fills the spanned rows.\n//\n// --sc-card-image-aspect image aspect-ratio. Default `1 / 1` (a square\n// thumb). Velvet sets `auto` so height:100% +\n// object-fit:cover win and the picture fills the tall\n// column instead of forcing a square row.\n//\n// --sc-card-image-align image align-self within its grid area. Default\n// `start` (top-aligned thumb). Velvet sets `stretch`\n// so the image paints the whole full-height area.\n//\n// --sc-card-chip-max-h spec-chip-row cap. Chips wrap freely by default\n// (`none` = no cap, multi-row wrap). Velvet sets a\n// one-row height (`1.9rem`) with the row's\n// `overflow: hidden` clipping to a single row \u2014 the\n// card is a glance preview in the bounded band. A\n// max-height + overflow:hidden is the cleanest CSS\n// mechanism: it needs no JS row-counting and degrades\n// to \"show as many chips as fit\" instead of a hard\n// count.\n//\n// --sc-card-col-gap gap between the image and content columns.\n// Default density-based; velvet may tighten it.\n//\n// --sc-card-areas grid-template-areas of the front face. This is the\n// LAYOUT-STRUCTURE token that a plain column/row token\n// can't express: WHERE the CTA sits relative to the\n// image and content. Three named areas: `image`,\n// `content`, `cta`.\n// Default (stacked/neutral):\n// \"image content\"\n// \"cta cta\"\n// \u2014 a top-aligned thumb beside the content, then a\n// FULL-WIDTH CTA row spanning below both (the clean\n// chat/PDP card). Velvet sets:\n// \"image content\"\n// \"image cta\"\n// so the image becomes a full-height LEFT feature\n// column (spans both rows) and the CTA sits in the\n// RIGHT column at the bottom \u2014 mirroring where the\n// variant picker's CTA lives. Grid-area NAMES are\n// referenced by var() on each child (image-area/\n// content-area/cta-area tokens below), so the areas\n// string and the child placements stay in sync.\n//\n// --sc-card-rows grid-template-rows of the front face. Default\n// `auto auto` (content row + CTA row). Velvet keeps\n// `1fr auto` so the content row absorbs the slack and\n// the CTA hugs the bottom of the tall band.\n//\n// --sc-card-cta-justify justify-self of the CTA row within its grid area.\n// Default `stretch` (full-width row spans the card).\n// Velvet sets `stretch` too but the area is only the\n// right column, so the CTA fills just that column.\n// Exposed as a token so a canvas can right-align a\n// compact CTA if it wants.\n//\n// Scoped flip mechanics owned by the card itself (the FAQ-widget precedent).\n// The prior both-faces approach leaked the variant panel onto non-velvet\n// surfaces because hiding depended on velvet's stylesheet; this stylesheet\n// travels with the widget, so the 3D context + backface hiding are correct on\n// every surface (velvet, chat, PDP). Faces are grid-stacked in one cell so the\n// container self-sizes to the tallest face \u2014 no fixed height needed inside\n// velvet's clipped deck band. Timing reads velvet's --vc-transition-* tokens\n// with a sane fallback so the flip works uncustomized.\n// Right-edge fade mask for the spec-chip scroll tracks. A chip row that\n// overflows the card width becomes a horizontally-scrollable track; the mask\n// fades the trailing edge so overflow reads as \"more, scrollable\" rather than a\n// hard slice. Mirrors PdpOtherProductsCarouselLit's carousel mask. The left\n// edge is left solid (chips start flush) \u2014 only the RIGHT edge fades since chip\n// rows only ever overflow to the right.\nconst SPEC_CHIP_FADE_MASK =\n 'linear-gradient(90deg, black 0, black calc(100% - 18px), transparent 100%)';\n\nconst CARD_FLIP_CSS = `\n /* Hide the scrollbar chrome on the declared, scrollable spec-chip tracks\n (data-clip-ok scroll containers) while keeping them scrollable. The inline\n styles set scrollbar-width:none (Firefox); WebKit needs this pseudo. */\n .sc-product-card [data-spec-chip-scroll]::-webkit-scrollbar { display: none; }\n\n /* Perspective ONLY while the flip container exists \u2014 a static perspective on\n the RESTING card promotes all its text onto composited GPU layers, which\n rasterizes titles soft/fuzzy at fractional positions (user-visible blur).\n :has() is supported across our targets; when absent (very old engines) the\n flip simply loses depth, never correctness. */\n .sc-product-card:has([data-card-flip]) { perspective: 1200px; }\n .sc-product-card [data-card-flip] {\n display: grid;\n transform-style: preserve-3d;\n transform: rotateY(180deg);\n animation: sc-card-flip-in var(--vc-transition-duration, 320ms)\n var(--vc-transition-easing, cubic-bezier(0.22, 1, 0.36, 1));\n }\n .sc-product-card [data-card-flip] > [data-face] {\n grid-area: 1 / 1;\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n }\n .sc-product-card [data-card-flip] > [data-face=\"back\"] { transform: rotateY(180deg); }\n @keyframes sc-card-flip-in {\n from { transform: rotateY(0deg); }\n to { transform: rotateY(180deg); }\n }\n @media (prefers-reduced-motion: reduce) {\n .sc-product-card [data-card-flip] { animation: none; }\n }\n`;\n\nexport class ProductCardLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n _device: { state: true },\n };\n\n static readonly _testing = {\n captureTelemetry: _captureTelemetry,\n publishEvent: _publishEvent,\n readDevice: _readDevice,\n synWindow,\n };\n\n props: CardProps | undefined = undefined;\n resolved: Record<string, { image?: string; price?: string }> = {};\n validationError = false;\n /** Device class \u2014 drives the effective density (mobile is forced compact). */\n _device: DeviceClass = 'desktop';\n\n _decisionState: CardState = createInitialCardState();\n\n #effectExecutor = createCardEffectExecutor({\n dispatch: (event) => this.#dispatchCard(event),\n publishEvent: _publishEvent,\n captureTelemetry: _captureTelemetry,\n closeSdk: () => synWindow().SynOS?.handle?.close?.(),\n bindProduct: () => this.#startBind(),\n reportRenderHealth: () => this.#scheduleHealthCheck(),\n cleanup: () => this.#cleanupHostResources(),\n schedule: (callback, delayMs) => setTimeout(callback, delayMs),\n cancelScheduled: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n });\n\n #controller: BindController | undefined;\n #unsubDevice: (() => void) | undefined;\n /** Fingerprint of the last reported schema-validation failure (sorted\n * invalid paths). Guards `render_failed` telemetry against re-render spam:\n * the same bad props re-rendering must not re-emit. Reset on a successful\n * parse so a later DIFFERENT failure still reports. */\n #lastFailureFingerprint: string | null = null;\n /** Set of render-health \"states\" already measured, so a self-detection event\n * fires AT MOST ONCE per instance per state (density \u00D7 face \u00D7 velvet \u00D7\n * product). Prevents event spam across re-renders of the same visual state. */\n #reportedHealthStates = new Set<string>();\n /** Pending rAF/timeout handle for the render-health measurement, cleared on\n * the next scheduled pass or on disconnect. */\n #healthRaf: number | undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n this.#dispatchCard({ type: 'reconnected' });\n this.#dispatchCard({\n type: 'commerce-updated',\n available: getProductPlatformAdapter() !== null,\n });\n ensureStylesInRoot(this, 'syntro-product-card-flip-style', CARD_FLIP_CSS);\n // Anchor `color` on the host so descendants inherit from this\n // widget's own theme token, not from the customer's\n // `body { color: ... }` rule. Light DOM is intentional (so the\n // host's brand fonts, layout, and spacing tokens cascade in \u2014\n // see the matching comment on `SyntroPdpLit.createRenderRoot`),\n // but foreground color is the WIDGET's responsibility, not the\n // host's: the card renders on its own `--sc-tile-background`\n // and the host body color was set for a totally different\n // background pair. Without this anchor, hosts that ship a dark\n // theme (`body { color: #eeebe8 }` on healthmaxxer) cascade pale\n // cream text into every descendant of this card \u2014 the host's\n // `--sc-tile-text-color` token was correct in spec but\n // never reached the text because the article's local `color`\n // rule didn't anchor to the host element. Setting it here\n // re-roots inheritance for every text node below.\n //\n // NOTE: Only the product canvas-tile widgets (Card, Comparison,\n // Grid, Hero) need this. The PDP widgets (PdpRibbon, PdpQa,\n // PdpPeerFeed, etc.) render on the host's PDP page where the\n // host's body color IS the intended contrast pair \u2014 leave those\n // alone.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n\n // Device-aware density: a mobile card surfaces LESS (compact) so it keeps a\n // smaller vertical profile and can't bully the chat; desktop keeps the config\n // density (room to breathe). Reactive \u2014 re-densifies if the surface changes.\n this._device = _readDevice();\n const ctx = synWindow().SynOS?.runtime?.context;\n this.#unsubDevice = ctx?.subscribe?.((c) => {\n const d = c?.surface?.device;\n const next: DeviceClass = d === 'mobile' || d === 'tablet' ? d : 'desktop';\n if (next !== this._device) this._device = next;\n });\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#dispatchCard({\n type: 'commerce-updated',\n available: getProductPlatformAdapter() !== null,\n });\n const observation = this.#readProductObservation();\n this.#dispatchCard({ type: 'props-replaced', ...observation });\n\n if (!observation.valid) this.#startBind();\n }\n }\n\n override updated(): void {\n // Schedule a runtime render-health measurement after the paint settles.\n // Only meaningful on the FRONT face with valid props (the resting card the\n // visitor sees); a validation error already fires render_failed.\n if (this.validationError || !this.props || this._decisionState.face !== 'front') return;\n this.#scheduleHealthCheck();\n }\n\n /** Schedule the self-detection pass on the next frame (+ a small timeout so\n * lazy images and web fonts have a chance to settle). rAF-then-timeout is the\n * cheapest way to run AFTER the browser has laid out and painted. */\n #scheduleHealthCheck(): void {\n if (typeof window === 'undefined') return;\n if (this.#healthRaf !== undefined) return; // one pending pass is enough\n const raf =\n typeof window.requestAnimationFrame === 'function'\n ? window.requestAnimationFrame.bind(window)\n : (cb: FrameRequestCallback) => window.setTimeout(() => cb(0), 0);\n this.#healthRaf = raf(() => {\n window.setTimeout(() => {\n this.#healthRaf = undefined;\n this.#measureAndReportHealth();\n }, 0);\n }) as unknown as number;\n }\n\n /**\n * Measure this card's render health and emit a self-detection event at most\n * ONCE per instance per visual state. Wrapped end-to-end in try/catch \u2014 a\n * measurement failure must NEVER crash the widget or the host page.\n */\n #measureAndReportHealth(): void {\n try {\n const card = this.querySelector('.sc-product-card');\n if (!card) return;\n const parsed = this.props ? cardSchema.safeParse(this.props) : null;\n if (!parsed?.success) return;\n const density = this._device === 'mobile' ? 'compact' : parsed.data.density;\n // Velvet sets the card layout tokens; detect it structurally from a token\n // the canvas sets (--sc-card-secondary-display:none) so we don't couple to\n // a canvas import. Best-effort NON-PII context only.\n const velvet =\n getComputedStyle(card).getPropertyValue('--sc-card-secondary-display').trim() === 'none';\n // De-dupe key: one report per (product \u00D7 density \u00D7 velvet) visual state.\n const stateKey = `${parsed.data.product.id}|${density}|${velvet ? 'v' : 'n'}`;\n if (this.#reportedHealthStates.has(stateKey)) return;\n // Mark consumed BEFORE reporting so a clean render isn't re-measured every\n // frame and a finding never double-fires for the same visual state.\n this.#reportedHealthStates.add(stateKey);\n // Measure + emit through the shared reporter: `app:product_card:blank` /\n // `:clipped` with { density, velvet, widget_kind } context (and, on a\n // Tier-1 identity truncation, reason:'identity-truncated').\n reportRenderHealth(card, {\n category: 'product_card',\n widgetKind: 'adaptive-product:card',\n context: { density, velvet },\n });\n } catch {\n // Self-detection must never crash the widget.\n }\n }\n\n override disconnectedCallback(): void {\n this.#dispatchCard({ type: 'disconnected' });\n super.disconnectedCallback();\n }\n\n #cleanupHostResources(): void {\n this.#controller?.destroy();\n this.#unsubDevice?.();\n this.#unsubDevice = undefined;\n if (this.#healthRaf !== undefined && typeof window !== 'undefined') {\n if (typeof window.cancelAnimationFrame === 'function') {\n window.cancelAnimationFrame(this.#healthRaf);\n }\n this.#healthRaf = undefined;\n }\n }\n\n #dispatchCard(event: CardEvent): void {\n const previousState = this._decisionState;\n const transition = transitionCard(this._decisionState, event);\n this._decisionState = transition.state;\n if (this.isConnected && transition.state !== previousState) this.requestUpdate();\n for (const effect of transition.effects) void this.#effectExecutor.execute(effect);\n }\n\n /** animationend on the flip container \u2192 the turn is done, render flat. */\n #onFlipAnimationEnd = (e: AnimationEvent): void => {\n if (e.animationName === 'sc-card-flip-in') this.#dispatchCard({ type: 'animation-settled' });\n };\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n return;\n }\n const result = cardSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n console.warn('[adaptive-product] invalid card props:', result.error.issues);\n // The card is about to render BLANK (render() returns html`` on\n // validationError). Without a signal that's an invisible failure \u2014 the\n // visitor sees an empty space and nothing downstream knows. Emit the\n // authoritative \"did not render\" event through emit() \u2014 the ONLY\n // consent-handled transport that actually reaches PostHog (publish()\n // only notifies in-process subscribers). Fire once per distinct failure\n // so a re-render with the same bad props doesn't spam the pipeline.\n const invalidPaths = _invalidPathsFromIssues(result.error.issues);\n const fingerprint = invalidPaths.join('|');\n if (fingerprint !== this.#lastFailureFingerprint) {\n this.#lastFailureFingerprint = fingerprint;\n _captureTelemetry(RENDER_FAILED_EVENT, {\n invalid_paths: invalidPaths.join(','),\n // Best-effort non-PII context \u2014 only set if the bad props happen to\n // carry an id; never any free-text/customer content.\n product_id:\n typeof (this.props as { product?: { id?: unknown } })?.product?.id === 'string'\n ? (this.props as { product: { id: string } }).product.id\n : null,\n widget_kind: 'adaptive-product:card',\n });\n }\n return;\n }\n this.#lastFailureFingerprint = null;\n this.validationError = false;\n const parsed = result.data;\n this.#controller = new BindController([parsed.product], (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n private _onVariantBack = (): void => {\n this.#syncDecisionInputsBeforeIntent();\n this.#dispatchCard({ type: 'back-activated' });\n };\n\n private _onVariantSelectionChange = (e: CustomEvent): void => {\n this.#syncDecisionInputsBeforeIntent();\n const detail = e.detail as { attribute_key: string; value_id: string };\n this.#dispatchCard({\n type: 'selection-changed',\n attributeKey: detail.attribute_key,\n valueId: detail.value_id,\n });\n };\n\n private _onVariantAdd = (e: CustomEvent): void => {\n this.#syncDecisionInputsBeforeIntent();\n const detail = e.detail as { variant: Variant };\n this.#dispatchCard({ type: 'variant-add-activated', variantId: detail.variant.id });\n };\n\n private _onVariantRetry = (): void => {\n this.#syncDecisionInputsBeforeIntent();\n this.#dispatchCard({ type: 'retry-activated' });\n };\n\n #readProductObservation(): {\n productId: string | null;\n valid: boolean;\n } {\n const parsed = this.props ? cardSchema.safeParse(this.props) : null;\n const rawProductId = (this.props as { product?: { id?: unknown } } | undefined)?.product?.id;\n return {\n productId: parsed?.success\n ? parsed.data.product.id\n : typeof rawProductId === 'string'\n ? rawProductId\n : null,\n valid: parsed?.success === true,\n };\n }\n\n #syncDecisionInputsBeforeIntent(): void {\n this.#dispatchCard({\n type: 'commerce-updated',\n available: getProductPlatformAdapter() !== null,\n });\n const observation = this.#readProductObservation();\n const product = this._decisionState.product;\n const isCurrent =\n observation.valid === (product.status === 'valid') && observation.productId === product.id;\n if (!isCurrent) this.#dispatchCard({ type: 'props-replaced', ...observation });\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.props) return html``;\n const parsed = cardSchema.safeParse(this.props);\n if (!parsed.success) return html``;\n // Mobile forces compact (smaller vertical profile, less content); desktop/tablet\n // keep the config-authored density.\n const density = this._device === 'mobile' ? 'compact' : parsed.data.density;\n const { product, visibleFacts } = parsed.data;\n const resolved = this.resolved[product.id];\n const view = selectCardView(this._decisionState);\n\n const articleStyles = _buildArticleStyles(density, view.face);\n\n // The decision core resolves view-cart, direct-add, and variant-picker\n // behavior from the current state; this shell only supplies user intent.\n const onPrimaryCtaClick = (e: Event): void => {\n e.preventDefault();\n this.#syncDecisionInputsBeforeIntent();\n const reducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n this.#dispatchCard({ type: 'primary-activated', reducedMotion });\n };\n\n // Build a version of ctas with the primary CTA label morphed when addedToCart\n const ctasWithMorphedLabel = product.ctas.map((c, i) => {\n if (i === 0 && view.primaryCta.mode === 'view-cart') {\n return { ...c, label: 'View cart \u2192' };\n }\n return c;\n });\n\n const frontContent = renderProductCardInner(\n product,\n density,\n resolved,\n visibleFacts,\n ctasWithMorphedLabel,\n onPrimaryCtaClick,\n !view.primaryCta.disabled\n );\n\n // The back-face variant picker markup, shared by the animating and settled\n // back states (only the wrapping container differs).\n const backFace = html`<syntro-variant-panel\n .payload=${view.panel.payload}\n .productTitle=${product.name ?? ''}\n .productImage=${product.image ? { src: product.image.src, alt: product.image.alt } : null}\n .state=${\n view.panel.status === 'error'\n ? 'error'\n : view.panel.status === 'ready'\n ? 'idle'\n : 'loading'\n }\n .error=${view.panel.error}\n @variant-panel-back=${this._onVariantBack}\n @variant-panel-add=${this._onVariantAdd}\n @variant-panel-retry=${this._onVariantRetry}\n @variant-panel-selection-change=${this._onVariantSelectionChange}\n ></syntro-variant-panel>\n ${view.addError ? html`<div data-add-to-cart-error>${view.addError}</div>` : nothing}`;\n\n // Render ONLY the active face. The back face (variant picker) must never be\n // present on the resting card \u2014 it appears solely after Add to cart flips to\n // 'back'. Rendering both faces and relying on a host canvas stylesheet to\n // hide the back (velvet's `[data-active-face]` rotate rules) leaked the\n // variant panel onto the resting card in every non-velvet surface (chat,\n // PDP). A single-face swap is self-contained and correct everywhere.\n //\n // The 3D flip container (perspective + rotateY, a composited layer) exists\n // ONLY while `view.flipAnimating` \u2014 i.e. for the duration of the turn. Once the\n // flip settles it is torn down and the back face renders FLAT: no\n // perspective (the `.sc-product-card:has([data-card-flip])` rule no longer\n // matches), no rotateY. A face left on a transformed/composited layer\n // rasterizes its text with grayscale AA (soft/fuzzy); the settled-flat face\n // renders subpixel-sharp.\n const backView =\n view.face === 'back' && view.flipAnimating\n ? // Mid-flip: both faces coexist inside a preserve-3d container. The\n // front turns away (inert + aria-hidden) while the back turns in.\n html`<div data-card-flip @animationend=${this.#onFlipAnimationEnd}>\n <div data-face=\"front\" aria-hidden=\"true\" inert>${frontContent}</div>\n <div data-face=\"back\">${backFace}</div>\n </div>`\n : view.face === 'back'\n ? // Settled back: flat, untransformed \u2014 no flip container, no perspective.\n html`<div data-face=\"back\">${backFace}</div>`\n : html`<div data-face=\"front\">\n ${frontContent}\n ${view.addError ? html`<div data-add-to-cart-error>${view.addError}</div>` : nothing}\n </div>`;\n\n return html`\n <article\n class=\"sc-product-card\"\n data-active-face=${view.face}\n data-density=${density}\n data-labels=${product.labels ? JSON.stringify(product.labels) : nothing}\n style=${styleMap(articleStyles)}\n >\n ${backView}\n </article>\n `;\n }\n}\n\nconst AVAILABILITY_LABELS: Record<NonNullable<Product['availability']>, string> = {\n in_stock: 'In stock',\n preorder: 'Preorder',\n backorder: 'Backorder',\n out_of_stock: 'Out of stock',\n};\n\ninterface SpecGroup {\n section: string;\n rows: Array<{ name: string; value: string; unit?: string; emphasis: boolean }>;\n}\n\n// Merge v2 specs[] with legacy v1 attributes[] (shimmed as \"About\" section).\n// Order: specs sections in author order, then \"About\" appended for any\n// legacy attributes present. New configs author specs[] directly; old\n// configs keep working until migrated.\nfunction buildSpecGroups(product: Product): SpecGroup[] {\n const groups = new Map<string, SpecGroup>();\n for (const spec of product.specs ?? []) {\n let group = groups.get(spec.section);\n if (!group) {\n group = { section: spec.section, rows: [] };\n groups.set(spec.section, group);\n }\n group.rows.push({\n name: spec.name,\n value: spec.value,\n unit: spec.unit,\n emphasis: spec.emphasis ?? false,\n });\n }\n if (product.attributes && product.attributes.length > 0) {\n const about: SpecGroup = groups.get('About') ?? { section: 'About', rows: [] };\n for (const attr of product.attributes) {\n about.rows.push({\n name: attr.label,\n value: attr.value,\n emphasis: attr.emphasis ?? false,\n });\n }\n if (!groups.has('About')) groups.set('About', about);\n }\n return Array.from(groups.values());\n}\n\n/** Build the article-level inline styles (shared by class render and standalone renderProductCard). */\nfunction _buildArticleStyles(density: 'compact' | 'standard', face: 'front' | 'back' = 'front') {\n return {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: density === 'compact' ? '0.5rem' : '0.625rem',\n // Inner padding so the card content doesn't hug the tile chrome.\n // When mounted chromeless (e.g. velvet deck), the article IS the\n // visible card; without this it reads as flush-edge. Standard\n // density gets slightly more breathing room.\n //\n // On the BACK face the variant panel supplies its own padding\n // (--sc-variant-pad); keeping the article padding too would double it and\n // eat the short deck band's vertical budget (risking a clipped CTA). Zero\n // it on the back so the panel owns the full band.\n padding: face === 'back' ? '0' : density === 'compact' ? '0.875rem' : '1rem',\n color: 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))',\n // \u2500\u2500 Root overflow invariant (item c) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The article IS the visible card. These three make it structurally\n // impossible for ANY descendant to paint past the card edge, no matter\n // what LLM-authored content lands inside:\n // box-sizing \u2192 padding counts INSIDE the width, so the card never\n // exceeds its allotted track by its own padding.\n // max-width \u2192 the card can never be wider than its container (the\n // deck-card / chat column / grid cell it's mounted in).\n // min-width:0 \u2192 as a flex/grid child, the card can SHRINK below its\n // content's intrinsic min size instead of forcing its\n // parent wider.\n // overflow \u2192 last-resort clip. Combined with the per-region\n // min-width:0 + terminal wrap fallbacks below, content\n // ADAPTS (wraps/shrinks) before this ever engages \u2014 but\n // if some future content still escapes, it's contained,\n // not spilled across the host page. Safe with the 3D\n // flip: the faces Y-rotate, so their horizontal footprint\n // never exceeds the card box at any rotation angle.\n boxSizing: 'border-box' as const,\n maxWidth: '100%',\n minWidth: '0',\n overflow: 'hidden',\n };\n}\n\n/**\n * Render the inner content of a product card (header row + body + CTAs) without\n * the article wrapper. Used by ProductCardLit.render() for the front face.\n */\nfunction renderProductCardInner(\n product: Product,\n density: 'compact' | 'standard',\n resolved: { image?: string; price?: string } | undefined,\n visibleFacts: readonly VisibleFactKey[] | undefined,\n ctas: Product['ctas'],\n onPrimaryCtaClick?: ((e: Event) => void | Promise<void>) | undefined,\n // When no platform commerce adapter is available (e.g. unknown platform / lite\n // bundle), the add-to-cart CTA renders disabled rather than silently no-opping.\n commerceAvailable = true\n): TemplateResult {\n const imgSrc = resolved?.image ?? product.image.src;\n const showFraming = density === 'standard' && product.framing;\n const specGroups = buildSpecGroups(product);\n // When visibleFacts is omitted, every populated block renders in the\n // default author order (back-compat). When set, only listed keys\n // render and the list's order is the render order.\n // CORE facts are the card's contract (user-drawn spec: name+rating /\n // tagline / price+availability / category / CTAs) \u2014 they render whenever\n // the DATA exists, regardless of the LLM's visibleFacts curation. Live\n // failure this prevents: a mount with visibleFacts:[\"tagline\",\"specs\"]\n // stripped rating+price and shipped a bare name-and-buttons card.\n // visibleFacts curates only the OPTIONAL extras (specs, framing).\n const CORE_FACTS: readonly VisibleFactKey[] = [\n 'rating',\n 'tagline',\n 'price',\n 'availability',\n 'badge',\n ] as VisibleFactKey[];\n const isVisible = (key: VisibleFactKey): boolean =>\n CORE_FACTS.includes(key) || (visibleFacts === undefined ? true : visibleFacts.includes(key));\n const factOrder: readonly VisibleFactKey[] = Array.from(\n new Set([...CORE_FACTS, ...(visibleFacts ?? VISIBLE_FACT_KEYS)])\n );\n\n // When salePrice is set, it becomes the active amount and `price` strikes through.\n // Bind-resolved price always wins for the active amount (live discount feeds).\n const hasSalePrice = product.salePrice !== undefined;\n const activePriceText = resolved?.price ?? product.salePrice?.amount ?? product.price?.amount;\n const activeCadence = hasSalePrice ? product.salePrice?.cadence : product.price?.cadence;\n\n // LAYOUT is token-driven (see the --sc-card-* comment block at the top of\n // this file). The template ALWAYS renders an image | content grid; the tokens\n // decide whether that reads as a clean STACKED card (neutral default: small\n // top-aligned header-row thumb beside the content) or velvet's HORIZONTAL\n // two-column card (image fills a tall left column). The card ships neutral\n // defaults; velvet sets the horizontal values on its deck-card rule.\n //\n // Neutral defaults reproduce the pre-redesign stacked structure: a density\n // thumb (72px compact / 110px standard) beside the content, CTAs in a\n // full-width row below (via the default --sc-card-areas). Velvet overrides\n // --sc-card-cols to a wider split, --sc-card-image-max/-h to 100%, and flips\n // --sc-card-areas so the image area spans both rows (a full-height left\n // feature column) and the CTA sits in the right column bottom.\n // Item 2 \u2014 COMPACT (mobile / velvet-deck) cards use an OVERLAY layout: the\n // \u2605rating and price move OUT of the content column and INTO the image's upper\n // corners as small pills, and the CTAs STACK vertically. That frees the\n // content column's bottom row on the space-constrained mobile card. Standard\n // density keeps the classic stacked card (rating in header, price in body,\n // horizontal CTA row). Velvet forces compact, so its deck card gets the\n // overlay treatment through this same path \u2014 the 132px image column + 3/4\n // aspect (\u2192 132\u00D7176) already come from velvet's --sc-card-* tokens; the\n // overlays position within whatever image box the tokens produce.\n const overlay = density === 'compact';\n // The compact image is a 132px feature column (132\u00D7176 at the default 3/4\n // aspect) so the overlay pills have a legible canvas. Standard keeps a 110px\n // thumbnail. Velvet overrides --sc-card-cols / -image-* regardless.\n const thumbPx = density === 'compact' ? 132 : 110;\n // `minmax(0, 1fr)` NOT `1fr`: a bare `1fr` track has an implicit `auto`\n // minimum, so a wide/unbreakable child (long name, 60-char token) inflates\n // the track past the card and the grid overflows. `minmax(0, \u2026)` lets the\n // content track shrink to zero, so it can never blow the card out. The image\n // track keeps its fixed thumb width (or velvet's token).\n const defaultCols = `${thumbPx}px minmax(0, 1fr)`;\n const defaultColGap = density === 'compact' ? '0.75rem' : '1rem';\n // Named grid areas make the CTA placement a LAYOUT-STRUCTURE token (see the\n // --sc-card-areas comment block). Neutral: CTA spans a full-width row below\n // image+content. Velvet: image spans both rows (feature column), CTA sits in\n // the right column bottom. The image/content/cta children reference the area\n // NAMES via var()-with-fallback so a canvas can override placement + areas\n // atomically.\n const defaultAreas = `\"image content\" \"cta cta\"`;\n const rootGridStyles = {\n display: 'grid',\n // Neutral: `72px 1fr` / `110px 1fr` (thumb | content). Velvet: `minmax(72px,30%) 1fr`.\n gridTemplateColumns: `var(--sc-card-cols, ${defaultCols})`,\n // Neutral: content row then CTA row (both auto). Velvet keeps `1fr auto` so\n // the content row absorbs slack and the CTA hugs the band bottom.\n gridTemplateRows: 'var(--sc-card-rows, auto auto)',\n gridTemplateAreas: `var(--sc-card-areas, ${defaultAreas})`,\n columnGap: `var(--sc-card-col-gap, ${defaultColGap})`,\n rowGap: density === 'compact' ? '0.5rem' : '0.625rem',\n // `start` keeps the neutral thumb top-aligned; velvet's full-height image\n // reads as `stretch` because it sets height:100% + grid-row 1/-1 itself.\n alignItems: 'start' as const,\n // The grid is itself a child of the (flex-column) face wrapper \u2014 keep it\n // shrinkable and capped so a velvet token override (`--sc-card-cols`) with\n // a bare `1fr` still can't push the grid past the card.\n minWidth: '0',\n maxWidth: '100%',\n };\n const contentColStyles = {\n gridArea: 'content',\n display: 'flex',\n flexDirection: 'column' as const,\n gap: density === 'compact' ? '0.4rem' : '0.45rem',\n minWidth: '0',\n };\n const titleColStyles = {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: '0.2rem',\n minWidth: '0',\n };\n // The image is a rounded thumbnail by default and a full-height panel under\n // velvet. Every dimension that differs between the two layouts reads a token:\n // --sc-card-image-max width cap (default thumbPx; velvet 100%)\n // --sc-card-image-h height (default auto square-ish thumb; velvet 100%)\n // --sc-card-image-aspect aspect-ratio (default 1/1; velvet auto)\n // --sc-card-image-align align-self (default start; velvet stretch)\n // Which rows the image SPANS is the named `image` grid area (--sc-card-areas),\n // not a row token. object-fit:cover keeps the framing tidy in both modes.\n const imageStyles = {\n // The image lives in the `image` grid area (placement comes ENTIRELY from\n // the named area \u2014 no separate grid-row override, which would unset the\n // area's row span and collapse the image back into row 1). Neutral: the area\n // is row 1 left (`\"image content\" \"cta cta\"`), a top-aligned thumb. Velvet:\n // the area spans both rows (`\"image content\" \"image cta\"`), so the image is\n // a full-height feature column.\n gridArea: 'image',\n width: '100%',\n maxWidth: `var(--sc-card-image-max, ${thumbPx}px)`,\n // Neutral: `auto` \u2192 a square-ish thumb sized by max-width + aspect-ratio.\n // Velvet: `100%` \u2192 fill the spanned rows' height.\n height: 'var(--sc-card-image-h, auto)',\n // Neutral keeps a 1/1 thumb; velvet sets `auto` so height:100% + object-fit\n // cover win and the picture fills the tall column instead of forcing a\n // square row.\n aspectRatio: 'var(--sc-card-image-aspect, 1 / 1)',\n // A definite floor so `align-self: stretch` + `height:100%` resolve to the\n // grid area's full block size (velvet's full-height feature panel) rather\n // than the image's intrinsic size.\n minHeight: '0',\n // Grid child: allow the image column to shrink below its intrinsic size so\n // it never inflates its track and pushes the content column off-card.\n minWidth: '0',\n objectFit: 'cover' as const,\n borderRadius: 'var(--sc-radius-md, 10px)',\n display: 'block',\n // Neutral: top-aligned thumb (`start`). Velvet: `stretch` fills the area.\n alignSelf: 'var(--sc-card-image-align, start)' as const,\n };\n // \u2500\u2500 Item 2 overlay media wrapper + pills (COMPACT only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The image lives inside a position:relative wrapper so the rating/price\n // pills can anchor to its corners. The wrapper carries the `image` grid area\n // + sizing tokens; the image fills it. On standard density the raw <img>\n // renders directly (no wrapper) so nothing changes there.\n const mediaWrapperStyles = {\n gridArea: 'image',\n position: 'relative' as const,\n width: '100%',\n maxWidth: `var(--sc-card-image-max, ${thumbPx}px)`,\n height: 'var(--sc-card-image-h, auto)',\n // 132 \u00D7 (4/3) = 176 \u2192 the spec's 132\u00D7176 portrait. Velvet's token wins.\n aspectRatio: 'var(--sc-card-image-aspect, 3 / 4)',\n alignSelf: 'var(--sc-card-image-align, start)',\n minWidth: '0',\n minHeight: '0',\n };\n const overlayImgStyles = {\n width: '100%',\n height: '100%',\n objectFit: 'cover' as const,\n borderRadius: 'var(--sc-radius-md, 10px)',\n display: 'block',\n minWidth: '0',\n minHeight: '0',\n };\n // Small legible pill over the image. Solid translucent fill (NO\n // backdrop-filter \u2014 the compositing contract bans filter/backdrop-filter on\n // text-bearing surfaces). white-space:normal + overflow-wrap:anywhere +\n // a bounded max-width mean the pill WRAPS within the image rather than\n // overflowing the card, so even a pathological price never escapes the\n // article (keeps the clip battery green).\n const overlayPillBase = {\n position: 'absolute' as const,\n top: '8px',\n display: 'inline-flex',\n alignItems: 'center',\n background: 'rgba(17, 17, 17, 0.66)',\n color: '#ffffff',\n borderRadius: '9999px',\n padding: '0.18rem 0.5rem',\n fontSize: '0.72rem',\n lineHeight: '1.25',\n fontWeight: '600',\n boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',\n maxWidth: 'calc(100% - 16px)',\n minWidth: '0',\n boxSizing: 'border-box' as const,\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n };\n const overlayRatingStyles = { ...overlayPillBase, left: '8px' };\n const overlayPriceStyles = { ...overlayPillBase, right: '8px' };\n // Inline-styled price block \u2014 strikethrough on original, larger sale amount,\n // small cadence suffix. Keeps the card readable without host CSS.\n const priceBlockStyles = {\n display: 'flex',\n flexDirection: 'row',\n alignItems: 'baseline',\n flexWrap: 'wrap' as const,\n gap: '0.4rem',\n minWidth: '0',\n };\n // Opacity dim was tuned for solid-white hosts. Against a translucent\n // host (velvet's cream backdrop-filter), 0.6 reads as illegible \u2014 bumped\n // to 0.78. Same for every other opacity-based dim in this file: each\n // value's floor is \"still legible on a 0.85-opacity surface.\"\n // Terminal wrap fallback (item b): a long, comma-joined price token\n // (\"$1,299.99\") has no break opportunity, so `overflow-wrap: anywhere` is the\n // last-resort policy that lets it break mid-token rather than overflow the\n // (possibly very narrow) content column. The sale amount is Tier-1 identity\n // text \u2014 it must never be ellipsized; wrapping keeps it fully readable.\n const priceOriginalStyles = {\n textDecoration: 'line-through',\n opacity: '0.78',\n fontSize: '0.9em',\n overflowWrap: 'anywhere' as const,\n };\n const priceSaleStyles = {\n fontWeight: '600',\n fontSize: '1.05em',\n color: 'var(--sc-tile-title-color, inherit)',\n minWidth: '0',\n overflowWrap: 'anywhere' as const,\n };\n const priceCadenceStyles = {\n fontSize: '0.82em',\n opacity: '0.82',\n overflowWrap: 'anywhere' as const,\n };\n // \u2500\u2500 Pill vocabulary (from VariantPanelLit) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Spec chips fuse name + value into one pill (e.g. \"Lion's Mane 1000mg\").\n // 9999px radius, 1px --sc-color-border hairline, 13px/600, tight 6px 14px\n // paddings at standard; slightly tighter at compact to fit the deck band.\n // Chip-row flow is token-driven. By default chips WRAP FREELY \u2014 a chat/PDP\n // card has room for a multi-row chip cloud. A tight canvas (velvet) sets\n // --sc-card-chip-wrap: nowrap for a SINGLE line.\n //\n // A single nowrap line WILL overflow the card width for long chips\n // (\"Bacopa320mg\"). A hard `overflow: hidden` clip was BANNED \u2014 a chip sliced\n // mid-word reads as breakage, and the content becomes unreachable. Instead the\n // nowrap row is a horizontally SCROLLABLE track (overflow-x: auto) with hidden\n // scrollbars and a right-edge FADE MASK (same pattern as\n // PdpOtherProductsCarouselLit): the overflow is declared, reachable by scroll,\n // and visually hinted by the fade. The container carries `data-clip-ok`\n // because its clipping is a *declared, scrollable* affordance \u2014 not a silent\n // hard-clip. On the WRAP default (chat/PDP roomy card) there is no single-row\n // overflow, so the mask/scroll are inert and it reads as a plain chip cloud.\n const specChipRowStyles = {\n display: 'flex',\n flexWrap: 'var(--sc-card-chip-wrap, wrap)' as never,\n gap: '0.35rem',\n // Scrollable when nowrap overflows; wrap mode never overflows so this is\n // inert there. `hidden` on the cross axis keeps a wrapped cloud from\n // scrolling vertically.\n overflowX: 'auto' as const,\n overflowY: 'hidden' as const,\n // Flex child: min-width:0 so the (scrollable) track shrinks with the card\n // instead of forcing it wider \u2014 the scroll/fade is the affordance, but the\n // ROW must never dictate the card width.\n minWidth: '0',\n // Hide the scrollbar chrome (Firefox + WebKit handled via the shared CSS\n // rule injected in connectedCallback for [data-clip-ok-scroll]).\n scrollbarWidth: 'none' as const,\n // Right-edge fade to hint at more chips beyond the card edge.\n maskImage: SPEC_CHIP_FADE_MASK,\n WebkitMaskImage: SPEC_CHIP_FADE_MASK,\n };\n const specChipStyles = {\n display: 'inline-flex',\n alignItems: 'baseline',\n gap: density === 'compact' ? '0.28rem' : '0.22rem',\n border: '1px solid var(--sc-color-border, rgba(0,0,0,0.14))',\n background: 'var(--sc-color-surface, transparent)',\n color: 'var(--sc-tile-title-color, inherit)',\n borderRadius: '9999px',\n padding: density === 'compact' ? '0.2rem 0.6rem' : '0.18rem 0.55rem',\n fontSize: density === 'compact' ? '0.72rem' : '0.7rem',\n lineHeight: '1.3',\n whiteSpace: 'nowrap' as const,\n flexShrink: '0',\n };\n // Tiny section label (11px/600, letter-spacing 0.01em, title color @0.85).\n const specHeadingStyles = {\n fontSize: '11px',\n letterSpacing: '0.01em',\n color: 'var(--sc-tile-title-color, inherit)',\n opacity: '0.85',\n margin: '0 0 0.3rem',\n fontWeight: '600',\n };\n // Standard-density spec table keeps a structured layout, but the row values\n // read in the pill language's palette (title color for names/values).\n const specGroupStyles = {\n display: 'flex',\n flexDirection: 'column',\n gap: '0.25rem',\n minWidth: '0',\n };\n // Standard chips lay out in a 2-column grid so N specs collapse to\n // ceil(N/2) rows \u2014 the two-rows-of-chips the horizontal-first design calls\n // for. A flex-wrap row would stack one-per-line when a chip is wider than\n // half the content column, making a 3-spec card too tall for the bounded\n // deck band. The image is narrow (30%) at standard specifically so each grid\n // cell is wide enough to hold a full \"Lion's Mane 1000mg\" chip WITHOUT\n // ellipsis. justify-items:start keeps chips their natural width (left-packed),\n // not stretched edge-to-edge.\n const specRowsStyles = {\n listStyle: 'none',\n margin: '0',\n padding: '0',\n display: 'grid',\n // Content-sized columns: each column is exactly as wide as its widest chip,\n // so chip text is NEVER clipped mid-word. Two chips per row keeps the card\n // short (ceil(N/2) rows) for the bounded deck band.\n gridTemplateColumns: 'max-content max-content',\n justifyContent: 'start' as const,\n gap: '0.3rem 0.35rem',\n // A `max-content` grid can grow wider than the content column (\"Alpha-GPC\n // 300 mg\" chips), and a hard `overflow: hidden` was slicing those chips\n // mid-word (the neutral standard card's spec rows escaped 8-94px). Replace\n // the hard clip with a declared, scrollable, fade-masked track \u2014 reachable\n // and hinted, never sliced. When the rows fit (roomy content column) the\n // scroll/mask are inert.\n overflowX: 'auto' as const,\n overflowY: 'hidden' as const,\n // Flex/grid child: shrinkable so the max-content chip grid never inflates\n // the content column past the card.\n minWidth: '0',\n scrollbarWidth: 'none' as const,\n maskImage: SPEC_CHIP_FADE_MASK,\n WebkitMaskImage: SPEC_CHIP_FADE_MASK,\n };\n const specValueStyles = {\n fontVariantNumeric: 'tabular-nums' as const,\n };\n const specsContainerStyles = {\n display: 'flex',\n flexDirection: 'column',\n gap: '0.5rem',\n minWidth: '0',\n };\n const availabilityStyles = {\n // Canvas-ownable display: a height-bounded band (velvet's deck card) can't\n // afford a variable fact row without clipping the CTA. Default keeps the\n // row visible on roomy surfaces (chat / PDP); velvet sets\n // --sc-card-availability-display:none so the fixed deck budget stays\n // reserved for identity/tagline/price/chips/CTA.\n display: 'var(--sc-card-availability-display, inline-block)' as never,\n fontSize: '0.8rem',\n opacity: '0.85',\n };\n // CTAs live in the `cta` grid area. Neutral: that area is a FULL-WIDTH row\n // below image+content, so \"Add to cart \u00B7 $49\" and \"Product Page \u203A\" sit on one\n // line spanning the card. Velvet: the area is the RIGHT column bottom (image\n // owns the full-height left), so the CTA row sits under the content \u2014 exactly\n // where the variant picker's CTA sits. flex-wrap lets the secondary link drop\n // below the primary pill when the (narrower) velvet right column can't hold\n // both on one line, instead of clipping.\n const ctasStyles = {\n gridArea: 'cta',\n justifySelf: 'var(--sc-card-cta-justify, stretch)',\n alignSelf: 'end' as const,\n display: 'flex',\n // Direction is canvas-tunable. Item 2: COMPACT (mobile) cards STACK the\n // CTAs vertically (Add to cart above Product Page \u203A) so the freed content\n // row isn't re-cramped by a horizontal button pair; standard keeps them on\n // one row. Velvet's --sc-card-cta-direction token still wins if set.\n flexDirection: `var(--sc-card-cta-direction, ${overlay ? 'column' : 'row'})` as never,\n // Stacked CTAs stretch to a consistent width; the row layout centers.\n alignItems: `var(--sc-card-cta-align, ${overlay ? 'stretch' : 'center'})` as never,\n flexWrap: 'var(--sc-card-cta-wrap, nowrap)' as never,\n justifyContent: 'space-between',\n gap: 'var(--sc-card-cta-gap, 0.5rem)',\n marginTop: '0',\n minWidth: '0',\n };\n // Primary CTA = panel-green pill with the LIVE price in the label\n // (Add to cart \u00B7 $49), mirroring the variant panel's price-in-CTA. Secondary\n // = a quiet ghost text link (\"Product Page \u203A\"). The reading order matches the\n // visual order: do the thing, or dig into details.\n const primaryCtaStyles = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n textAlign: 'center' as const,\n padding: density === 'compact' ? '0.5rem 1rem' : '0.55rem 1.15rem',\n borderRadius: '9999px',\n border: '1px solid var(--sc-color-primary, #3d8a5e)',\n background: 'var(--sc-color-primary, #3d8a5e)',\n color: 'var(--sc-color-on-primary, #ffffff)',\n fontWeight: '600',\n fontSize: '0.85rem',\n textDecoration: 'none',\n lineHeight: '1.15',\n // Item (b): the CTA label is Tier-1 identity text \u2014 ellipsis is BANNED.\n // A long label (\"Add to cart \u00B7 $1,299.99 per unit bundle\") must never\n // overflow its row. Instead of a nowrap pill that spills off the card, the\n // PILL itself shrinks (min-width:0, flex-shrink:1, max-width:100%) and its\n // label WRAPS to a second line (`white-space: normal` + `overflow-wrap:\n // anywhere`). The pill grows taller, never wider than the card.\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n minWidth: '0',\n maxWidth: '100%',\n flexShrink: '1',\n };\n const secondaryCtaStyles = {\n // Canvas-hideable: velvet's narrow right column can't fit the secondary\n // pill beside the primary without a mid-word clip, so it sets\n // --sc-card-secondary-display: none \u2014 the product page stays reachable via\n // the NAME link (see data-product-name-link).\n display: 'var(--sc-card-secondary-display, inline-flex)' as never,\n alignItems: 'center',\n justifyContent: 'center',\n gap: '0.15rem',\n padding: '0.4rem 0.5rem',\n borderRadius: '9999px',\n border: '0',\n background: 'transparent',\n color: 'var(--sc-color-primary, #3d8a5e)',\n fontWeight: '600',\n fontSize: '0.82rem',\n textDecoration: 'none',\n textAlign: 'center' as const,\n lineHeight: '1.15',\n // Same terminal policy as the primary pill: shrink + wrap, never overflow.\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n minWidth: '0',\n maxWidth: '100%',\n flexShrink: '1',\n };\n\n // Full-width CTA row (rendered below the image|content grid). Kept as its own\n // template so it can live OUTSIDE the two-column grid and span the whole card.\n const ctaRow = html`<div class=\"sc-product-card__ctas\" style=${styleMap(ctasStyles)}>\n ${ctas.map((c, i) => {\n // The add-to-cart CTA is disabled when no commerce adapter is present.\n const disabled = c.actionId === 'add_to_cart' && !commerceAvailable;\n const ctaStyles = {\n ...(i === 0 ? primaryCtaStyles : secondaryCtaStyles),\n ...(disabled ? { opacity: '0.5', 'pointer-events': 'none', cursor: 'not-allowed' } : {}),\n };\n // FRONT face keeps the CTA label CLEAN (\"Add to cart\" \u2014 no price): on\n // cards, Add-to-cart opens the VARIANT PICKER (it doesn't add anything\n // yet), so a price-in-label overpromises. The PICKER's CTA is the one\n // that commits, and it carries the live price there. (An earlier\n // price-in-label variant left dead isAddToCart/morphed locals here \u2014\n // removed.) The text stays a single text node so\n // [data-product-cta=\"primary\"].textContent still contains\n // \"Add to cart\" / \"View cart\" for the behavioral tests.\n const ctaLabel = c.label;\n return html`<a\n class=\"sc-product-card__cta\"\n data-product-cta=${i === 0 ? 'primary' : 'secondary'}\n data-critical-text\n data-variant=${c.variant}\n data-disabled=${disabled ? 'true' : nothing}\n aria-disabled=${disabled ? 'true' : nothing}\n href=${c.actionId === 'add_to_cart' ? '#' : (c.href ?? '#')}\n target=${c.target}\n rel=${c.target === '_blank' ? 'noopener noreferrer' : ''}\n data-action-id=${c.actionId ?? ''}\n style=${styleMap(ctaStyles)}\n @click=${\n disabled\n ? (e: Event) => {\n e.preventDefault();\n }\n : i === 0 && onPrimaryCtaClick\n ? onPrimaryCtaClick\n : c.actionId === 'add_to_cart'\n ? (e: Event) => {\n e.preventDefault();\n }\n : undefined\n }\n >${\n // Secondary \"Product Page\" link gets a trailing chevron (panel's\n // quiet-secondary affordance) unless the author already added one.\n i !== 0 && !/[\u203A>\u2192]\\s*$/.test(ctaLabel) ? `${ctaLabel} \u203A` : ctaLabel\n }</a\n >`;\n })}\n </div>`;\n\n // \u2500\u2500 Item 2 overlay pills (COMPACT only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Rating pill (upper-left) and price pill (upper-right) render INSIDE the\n // media wrapper on compact. They carry the SAME data-attributes as their\n // in-column counterparts so behavioral hooks/tests still resolve them; the\n // price amount stays [data-critical-text] (never ellipsized \u2014 it wraps).\n const ratingOverlay =\n overlay && product.rating && isVisible('rating')\n ? html`<div\n class=\"sc-product-card__rating sc-product-card__rating--overlay\"\n data-product-rating\n data-rating-value=${product.rating.value}\n data-rating-max=${product.rating.max ?? 5}\n aria-label=${`${product.rating.value} out of ${product.rating.max ?? 5}, ${product.rating.count} reviews`}\n style=${styleMap(overlayRatingStyles)}\n >\n <span style=\"color: #f5a623;\">\u2605</span\n ><span style=\"margin-left: 0.15rem; font-weight: 600;\">${product.rating.value}</span\n ><span\n class=\"sc-product-card__rating-count\"\n style=${styleMap({ opacity: '0.8', marginLeft: '0.15rem', display: 'var(--sc-card-rating-count-display, inline)' })}\n >(${product.rating.count.toLocaleString()})</span\n >\n </div>`\n : nothing;\n const priceOverlay =\n overlay && isVisible('price') && (product.price || product.salePrice)\n ? html`<div\n class=\"sc-product-card__price sc-product-card__price--overlay\"\n data-product-price\n style=${styleMap(overlayPriceStyles)}\n >\n <span\n class=\"sc-product-card__amount\"\n data-product-price-sale=${hasSalePrice ? 'true' : nothing}\n data-critical-text\n >${activePriceText}</span\n >\n </div>`\n : nothing;\n // On compact the image is wrapped so the pills can anchor to its corners;\n // on standard the raw <img> renders directly (unchanged).\n const mediaBlock = overlay\n ? html`<div class=\"sc-product-card__media\" style=${styleMap(mediaWrapperStyles)}>\n <img\n class=\"sc-product-card__image\"\n data-product-image\n src=${imgSrc}\n alt=${product.image.alt}\n loading=\"lazy\"\n @error=${onImageError}\n style=${styleMap(overlayImgStyles)}\n />\n ${ratingOverlay}${priceOverlay}\n </div>`\n : html`<img\n class=\"sc-product-card__image\"\n data-product-image\n src=${imgSrc}\n alt=${product.image.alt}\n loading=\"lazy\"\n @error=${onImageError}\n style=${styleMap(imageStyles)}\n />`;\n\n return html`\n <div class=\"sc-product-card__grid\" data-product-card-grid style=${styleMap(rootGridStyles)}>\n ${mediaBlock}\n <div class=\"sc-product-card__content\" style=${styleMap(contentColStyles)}>\n <header\n class=\"sc-product-card__header\"\n style=${styleMap(titleColStyles)}\n >\n <div\n class=\"sc-product-card__identity-row\"\n style=${styleMap({\n display: 'flex',\n alignItems: 'baseline',\n justifyContent: 'space-between',\n gap: '0.5rem',\n minWidth: '0',\n // WRAP so a nowrap rating pill can drop to its own line when the\n // column is too narrow to hold name + rating. Without this, at very\n // narrow widths (240px) the fixed-width rating crushes the name box\n // below a single glyph, and NO wrap policy can shrink one character \u2014\n // leaving a residual identity overflow. Wrapping frees the full\n // column width for the Tier-1 name.\n flexWrap: 'wrap' as const,\n rowGap: '0.15rem',\n })}\n >\n <h3\n class=\"sc-product-card__name\"\n data-product-name\n data-critical-text\n style=${styleMap({\n margin: '0',\n // Name size is a canvas-tunable token so a tight surface (velvet's\n // narrow right column) can shrink it to keep long names on 1-2\n // lines. Default keeps the prior density sizes.\n fontSize: `var(--sc-card-name-size, ${density === 'compact' ? '0.95rem' : '1.0625rem'})`,\n fontWeight: '700',\n lineHeight: '1.2',\n minWidth: '0',\n // IDENTITY text (Tier 1): the product NAME must render IN FULL \u2014\n // an ellipsized product title is a defect. So the name WRAPS at\n // word boundaries (up to as many lines as it needs) with NO\n // ellipsis and NO clip. We deliberately do NOT use\n // -webkit-line-clamp (it re-introduces an ellipsis) and do NOT cap\n // the height: an absurdly long name pushes layout and is DETECTED\n // as a CTA escape by the clip battery, rather than silently\n // truncated. `overflow-wrap: break-word` guarantees even a single\n // unbreakable token never overflows its column horizontally.\n // `anywhere` (not `break-word`): it wraps at word boundaries\n // FIRST and only breaks mid-word as a terminal fallback, but \u2014\n // unlike break-word \u2014 it also shrinks the box's min-content so a\n // word a few px wider than a narrow (240px) column can't leave a\n // residual horizontal overflow.\n display: 'block',\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n })}\n >${\n // The NAME is a PDP link when a navigating CTA exists (same pattern\n // as the comparison's name-link) \u2014 so a canvas that hides the\n // secondary CTA pill (velvet: --sc-card-secondary-display: none, to\n // avoid mid-word clipping in the narrow right column) still keeps\n // the product page reachable.\n (() => {\n const navCta = ctas.find((c) => c.href);\n return navCta?.href\n ? html`<a\n data-product-name-link\n href=${navCta.href}\n target=${navCta.target ?? '_self'}\n rel=${navCta.target === '_blank' ? 'noopener noreferrer' : ''}\n style=\"color:inherit;text-decoration:none;cursor:pointer;display:block;min-width:0;white-space:normal;overflow-wrap:anywhere;\"\n >${product.name}</a>`\n : product.name;\n })()\n }</h3>\n ${\n // On compact the rating renders as an image OVERLAY pill (item 2),\n // so it is omitted from the header identity row here.\n !overlay && product.rating && isVisible('rating')\n ? html`<div\n class=\"sc-product-card__rating\"\n data-product-rating\n data-rating-value=${product.rating.value}\n data-rating-max=${product.rating.max ?? 5}\n aria-label=${`${product.rating.value} out of ${product.rating.max ?? 5}, ${product.rating.count} reviews`}\n style=${styleMap({\n fontSize: '0.78rem',\n whiteSpace: 'nowrap',\n flexShrink: '0',\n })}\n >\n <span class=\"sc-product-card__rating-stars\" style=\"color: #f5a623;\">\u2605</span>\n <span style=${styleMap({ marginLeft: '0.2rem', fontWeight: '600' })}>${product.rating.value}</span>\n <span class=\"sc-product-card__rating-count\" style=${styleMap({ opacity: '0.65', marginLeft: '0.2rem', display: 'var(--sc-card-rating-count-display, inline)' })}>(${product.rating.count.toLocaleString()})</span>\n </div>`\n : nothing\n }\n </div>\n ${\n (product.tagline || product.framing) && isVisible('tagline')\n ? html`<p\n class=\"sc-product-card__tagline\"\n data-product-tagline\n style=${styleMap({\n margin: '0',\n fontSize: '0.82rem',\n opacity: '0.72',\n lineHeight: '1.3',\n // The LLM-authored tagline WRAPS (up to 3 lines, then\n // ellipsis) \u2014 a clipped single line wastes the card's space\n // and hides the framing the agent wrote (2026-07-23).\n // Vendor-prefixed styleMap keys need a capital first letter:\n // lowercase `webkitLineClamp` emits `webkit-line-clamp` (no\n // leading dash) which the CSS parser DROPS, leaving the\n // element in legacy un-oriented -webkit-box layout \u2014 the\n // unwrapped-clipped-line bug this replaces.\n display: '-webkit-box',\n // The line COUNT is the host canvas's call \u2014 a height-bounded\n // band (velvet's 232-292px deck) can't afford 3 lines without\n // pushing the CTA rows to the clipped bottom edge (user\n // report 2026-07-24). Token bridge across sc-mount's shadow\n // root, same as --sc-comparison-max-fields.\n WebkitLineClamp: 'var(--sc-card-tagline-lines, 3)',\n WebkitBoxOrient: 'vertical' as const,\n overflow: 'hidden',\n // Terminal fallback (item b): a 60-char unbroken tagline token\n // has no break opportunity, so without this its min-content\n // width (as a flex child of the header) would force the header\n // wider than the card. min-width:0 + overflow-wrap:anywhere let\n // the line-clamp box shrink and the token break, so the ellipsis\n // clip stays WITHIN the card instead of blowing it out.\n minWidth: '0',\n overflowWrap: 'anywhere' as const,\n })}\n >${product.tagline || product.framing}</p>`\n : nothing\n }\n </header>\n <div\n class=\"sc-product-card__body\"\n data-product-body\n style=${styleMap({\n display: 'flex',\n flexDirection: 'column',\n gap: density === 'compact' ? '0.35rem' : '0.5rem',\n minWidth: '0',\n })}\n >\n ${factOrder.map((key) => {\n if (key === 'price') {\n // On compact the price renders as an image OVERLAY pill (item 2), so it\n // is omitted from the content body here to avoid a duplicate.\n if (overlay) return nothing;\n if (!isVisible('price') || (!product.price && !product.salePrice)) return nothing;\n return html`<div\n class=\"sc-product-card__price\"\n data-product-price\n style=${styleMap(priceBlockStyles)}\n >\n ${\n hasSalePrice && product.price\n ? html`<span\n class=\"sc-product-card__amount-original\"\n data-product-price-original\n style=${styleMap(priceOriginalStyles)}\n >${product.price.amount}</span\n >`\n : nothing\n }\n <span\n class=\"sc-product-card__amount\"\n data-product-price-sale=${hasSalePrice ? 'true' : nothing}\n data-critical-text\n style=${styleMap(priceSaleStyles)}\n >${activePriceText}</span\n >\n ${\n activeCadence\n ? html`<span\n class=\"sc-product-card__cadence\"\n style=${styleMap(priceCadenceStyles)}\n >${activeCadence}</span\n >`\n : nothing\n }\n </div>`;\n }\n if (key === 'availability') {\n if (!isVisible('availability') || !product.availability) return nothing;\n return html`<span\n class=\"sc-product-card__availability\"\n data-product-availability\n data-availability=${product.availability}\n style=${styleMap(availabilityStyles)}\n >${AVAILABILITY_LABELS[product.availability]}</span\n >`;\n }\n if (key === 'badge') {\n if (!isVisible('badge') || !product.badge) return nothing;\n return html`<span\n class=\"sc-product-card__badge\"\n data-product-badge\n data-tone=${product.badge.tone}\n style=${styleMap({\n // Canvas-ownable display: the badge often carries a category /\n // collection label (\"Running\") \u2014 a variable fact row the deck's\n // fixed band can't fit. Default shows it (chat / PDP); velvet sets\n // --sc-card-badge-display:none for the same band-budget reason as\n // availability above.\n display: 'var(--sc-card-badge-display, inline-block)' as never,\n })}\n >${product.badge.label}</span\n >`;\n }\n if (key === 'specs') {\n if (!isVisible('specs') || specGroups.length === 0) return nothing;\n // Compact density renders specs as a wrapping pill chip cloud. How MANY\n // chip rows show is a LAYOUT decision, not a data cap: the chip row's\n // max-height is token-driven (--sc-card-chip-max-h, default `none` =\n // wrap freely). A chat/PDP card shows the full cloud; velvet caps the\n // row to one line via the token + `overflow: hidden`. The adaptive\n // hands over a modest set of highlights (top 4 \u2014 enough to fill a couple\n // of wrapped rows on a roomy surface) and lets the canvas clip. Standard\n // density keeps the full structured table for surfaces where the card is\n // the destination, not a preview.\n if (density === 'compact') {\n const allRows = specGroups.flatMap((g) => g.rows);\n const highlights = allRows.slice(0, 4);\n if (highlights.length === 0) return nothing;\n // Panel-style pill chips: name + value fused (\"Lion's Mane 1000mg\"),\n // 9999px radius, hairline border, wrap freely; the container's\n // token-driven max-height clips the flow (one row under velvet).\n // NO section heading at compact \u2014 the card is a glance preview.\n return html`<div\n class=\"sc-product-card__specs\"\n data-density=\"compact\"\n data-clip-ok\n data-spec-chip-scroll\n tabindex=\"0\"\n aria-label=\"Product specifications\"\n style=${styleMap(specChipRowStyles)}\n >\n ${highlights.map(\n (r) => html`<span\n data-spec-chip\n style=${styleMap(specChipStyles)}\n ><span style=\"font-weight: 600;\">${r.name}</span\n ><span style=\"opacity: 0.72;\">${r.value}${r.unit ? r.unit : ''}</span></span\n >`\n )}\n </div>`;\n }\n // Standard density keeps the grouped, sectioned structure (a section\n // heading per spec section) but lays each row out as a panel-style pill\n // chip \u2014 the same pill vocabulary as compact, just richer (all rows,\n // wraps to multiple lines). The tiny section label uses the 11px/600\n // title-color heading from the variant panel.\n return html`<div class=\"sc-product-card__specs\" style=${styleMap(specsContainerStyles)}>\n ${specGroups.map(\n (g) => html`<section\n class=\"sc-product-card__spec-section\"\n data-spec-section=${g.section}\n style=${styleMap(specGroupStyles)}\n >\n <h4\n class=\"sc-product-card__spec-heading\"\n style=${styleMap(specHeadingStyles)}\n >${g.section}</h4>\n <ul\n class=\"sc-product-card__spec-rows\"\n data-clip-ok\n data-spec-chip-scroll\n tabindex=\"0\"\n aria-label=${`${g.section} specifications`}\n style=${styleMap(specRowsStyles)}\n >\n ${g.rows.map(\n (r) => html`<li\n data-spec-row\n data-spec-chip\n data-attribute-emphasis=${r.emphasis ? 'true' : 'false'}\n style=${styleMap({\n ...specChipStyles,\n ...(r.emphasis\n ? {\n background: 'var(--sc-color-primary, #3d8a5e)',\n color: 'var(--sc-color-on-primary, #ffffff)',\n borderColor: 'var(--sc-color-primary, #3d8a5e)',\n }\n : {}),\n })}\n >\n <span\n class=\"sc-product-card__spec-name\"\n data-spec-name\n style=\"font-weight: 600;\"\n >${r.name}</span\n >\n <span\n class=\"sc-product-card__spec-value\"\n data-spec-value\n style=${styleMap(specValueStyles)}\n >${r.value}</span\n >\n ${\n r.unit\n ? html`<span\n class=\"sc-product-card__spec-unit\"\n data-spec-unit\n style=\"opacity: 0.72;\"\n >${r.unit}</span\n >`\n : nothing\n }\n </li>`\n )}\n </ul>\n </section>`\n )}\n </div>`;\n }\n if (key === 'framing') {\n if (!isVisible('framing') || !showFraming) return nothing;\n return html`<p\n class=\"sc-product-card__framing\"\n style=${styleMap({\n margin: '0',\n // Prose terminal fallback (item b): a long unbroken hook token can't\n // overflow the content column.\n minWidth: '0',\n overflowWrap: 'anywhere' as const,\n wordBreak: 'break-word' as const,\n })}\n .innerHTML=${sanitizeHtml(product.framing ?? '')}\n ></p>`;\n }\n // tagline + rating render in the header; nothing to emit here.\n return nothing;\n })}\n </div>\n </div>\n ${ctaRow}\n </div>\n `;\n}\n\n/**\n * Standalone product card renderer \u2014 used by ProductGridLit and other contexts\n * that need a non-stateful card without the face state machine.\n * Produces the full `<article>` wrapper.\n */\nexport function renderProductCard(\n product: Product,\n density: 'compact' | 'standard',\n resolved: { image?: string; price?: string } | undefined,\n visibleFacts?: readonly VisibleFactKey[]\n): TemplateResult {\n const articleStyles = _buildArticleStyles(density);\n\n return html`\n <article\n class=\"sc-product-card\"\n data-density=${density}\n data-labels=${product.labels ? JSON.stringify(product.labels) : nothing}\n style=${styleMap(articleStyles)}\n >\n ${renderProductCardInner(product, density, resolved, visibleFacts, product.ctas, undefined)}\n </article>\n `;\n}\n\nfunction onImageError(e: Event): void {\n const img = e.currentTarget as HTMLImageElement;\n img.dataset.imageError = 'true';\n img.removeAttribute('src');\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\n\ntype DomSelectorPrice = Extract<PriceBind, { type: 'dom-selector' }>;\ntype DomSelectorImage = Extract<ImageBind, { type: 'dom-selector' }>;\n\nexport async function resolveDomSelectorPrice(\n input: DomSelectorPrice,\n _signal: AbortSignal\n): Promise<string | null> {\n const el = document.querySelector(input.selector);\n const text = el?.textContent?.trim();\n return text && text.length > 0 ? text : null;\n}\n\nexport async function resolveDomSelectorImage(\n input: DomSelectorImage,\n _signal: AbortSignal\n): Promise<string | null> {\n const el = document.querySelector(input.selector);\n if (!el) return null;\n if (el instanceof HTMLImageElement) return el.currentSrc || el.src || null;\n const src = el.getAttribute('src');\n return src && src.length > 0 ? src : null;\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\n\ntype JsonLdPriceBind = Extract<PriceBind, { type: 'jsonld' }>;\ntype JsonLdImageBind = Extract<ImageBind, { type: 'jsonld' }>;\n\ninterface Offer {\n price?: string | number;\n priceCurrency?: string;\n}\ninterface ProductNode {\n '@type'?: string | string[];\n image?: string | string[];\n offers?: Offer | Offer[];\n}\n\nfunction isProduct(node: unknown): node is ProductNode {\n if (typeof node !== 'object' || node === null) return false;\n const t = (node as { '@type'?: unknown })['@type'];\n if (typeof t === 'string') return t === 'Product';\n if (Array.isArray(t)) return t.includes('Product');\n return false;\n}\n\nfunction parseScripts(scripts: string[]): ProductNode | null {\n for (const text of scripts) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n continue;\n }\n const stack: unknown[] = [parsed];\n while (stack.length) {\n const cur = stack.pop();\n if (Array.isArray(cur)) {\n for (const c of cur) stack.push(c);\n continue;\n }\n if (isProduct(cur)) return cur;\n if (typeof cur === 'object' && cur !== null) {\n const graph = (cur as { '@graph'?: unknown })['@graph'];\n if (Array.isArray(graph)) for (const g of graph) stack.push(g);\n }\n }\n }\n return null;\n}\n\nfunction collectInlineScripts(): string[] {\n const els = document.querySelectorAll('script[type=\"application/ld+json\"]');\n return Array.from(els, (el) => el.textContent ?? '');\n}\n\nasync function collectScriptsFromUrl(url: string, signal: AbortSignal): Promise<string[]> {\n const res = await fetch(url, { signal });\n if (!res.ok) return [];\n const text = await res.text();\n const doc = new DOMParser().parseFromString(text, 'text/html');\n return Array.from(\n doc.querySelectorAll('script[type=\"application/ld+json\"]'),\n (el) => el.textContent ?? ''\n );\n}\n\nfunction pickPrice(node: ProductNode): { amount: number; currency: string } | null {\n const offer: Offer | undefined = Array.isArray(node.offers) ? node.offers[0] : node.offers;\n if (!offer) return null;\n const raw = offer.price;\n const num =\n typeof raw === 'number' ? raw : typeof raw === 'string' ? Number.parseFloat(raw) : Number.NaN;\n if (!Number.isFinite(num)) return null;\n return { amount: num, currency: offer.priceCurrency || 'USD' };\n}\n\nfunction formatPrice(amount: number, currency: string): string | null {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch (err) {\n console.warn('[adaptive-product.bind] jsonld: Intl.NumberFormat failed', { currency }, err);\n return null;\n }\n}\n\nexport async function resolveJsonLdPrice(\n input: JsonLdPriceBind,\n signal: AbortSignal\n): Promise<string | null> {\n const scripts = input.url\n ? await collectScriptsFromUrl(input.url, signal)\n : collectInlineScripts();\n const product = parseScripts(scripts);\n if (!product) return null;\n const picked = pickPrice(product);\n return picked ? formatPrice(picked.amount, picked.currency) : null;\n}\n\nexport async function resolveJsonLdImage(\n input: JsonLdImageBind,\n signal: AbortSignal\n): Promise<string | null> {\n const scripts = input.url\n ? await collectScriptsFromUrl(input.url, signal)\n : collectInlineScripts();\n const product = parseScripts(scripts);\n if (!product) return null;\n const img = Array.isArray(product.image) ? product.image[0] : product.image;\n return typeof img === 'string' && img.length > 0 ? img : null;\n}\n", "import type { ImageBind } from '../schema-primitives';\n\ntype OgBind = Extract<ImageBind, { type: 'opengraph' }>;\n\nexport async function resolveOpenGraphImage(\n input: OgBind,\n signal: AbortSignal\n): Promise<string | null> {\n const res = await fetch(input.url, { signal });\n if (!res.ok) return null;\n const text = await res.text();\n const doc = new DOMParser().parseFromString(text, 'text/html');\n const meta = doc.querySelector('meta[property=\"og:image\"]') as HTMLMetaElement | null;\n const raw = meta?.getAttribute('content');\n if (!raw) return null;\n try {\n return new URL(raw, input.url).toString();\n } catch {\n return null;\n }\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\n\ntype ShopifyAjaxPriceBind = Extract<PriceBind, { type: 'shopify-ajax' }>;\ntype ShopifyAjaxImageBind = Extract<ImageBind, { type: 'shopify-ajax' }>;\n\ninterface ShopifyProductPayload {\n price?: number; // cents (Shopify minor units)\n currency?: string; // ISO code; \"USD\" by default in Shopify's AJAX API\n images?: string[];\n}\n\nfunction originFor(base: string | undefined): string {\n if (base) return base.replace(/\\/$/, '');\n return typeof location !== 'undefined' ? location.origin : '';\n}\n\nasync function fetchProduct(\n handle: string,\n base: string | undefined,\n signal: AbortSignal\n): Promise<ShopifyProductPayload | null> {\n const url = `${originFor(base)}/products/${encodeURIComponent(handle)}.js`;\n const res = await fetch(url, { signal });\n if (!res.ok) return null;\n try {\n return (await res.json()) as ShopifyProductPayload;\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') throw err;\n console.warn('[adaptive-product.bind] shopify-ajax: json parse failed', err);\n return null;\n }\n}\n\nexport async function resolveShopifyAjaxPrice(\n input: ShopifyAjaxPriceBind,\n signal: AbortSignal\n): Promise<string | null> {\n const payload = await fetchProduct(input.handle, input.base, signal);\n if (!payload || typeof payload.price !== 'number') return null;\n const currency = payload.currency || 'USD';\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(\n payload.price / 100\n );\n } catch (err) {\n console.warn(\n '[adaptive-product.bind] shopify-ajax/price: Intl.NumberFormat failed',\n { currency },\n err\n );\n return null;\n }\n}\n\nexport async function resolveShopifyAjaxImage(\n input: ShopifyAjaxImageBind,\n signal: AbortSignal\n): Promise<string | null> {\n const payload = await fetchProduct(input.handle, input.base, signal);\n const first = payload?.images?.[0];\n return typeof first === 'string' && first.length > 0 ? first : null;\n}\n", "import type { PriceBind } from '../schema-primitives';\n\ntype WooPriceBind = Extract<PriceBind, { type: 'woocommerce-store-api' }>;\n\ninterface WooStoreProduct {\n id: number;\n slug: string;\n prices?: {\n price?: string;\n currency_code?: string;\n currency_minor_unit?: number;\n };\n}\n\nfunction originFor(base: string | undefined): string {\n if (base) return base.replace(/\\/$/, '');\n return typeof location !== 'undefined' ? location.origin : '';\n}\n\nexport async function resolveWooStoreApiPrice(\n input: WooPriceBind,\n signal: AbortSignal\n): Promise<string | null> {\n const url = `${originFor(input.base)}/wp-json/wc/store/products?slug=${encodeURIComponent(input.slug)}`;\n const res = await fetch(url, { signal });\n if (!res.ok) return null;\n let body: WooStoreProduct[];\n try {\n body = (await res.json()) as WooStoreProduct[];\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') throw err;\n console.warn('[adaptive-product.bind] woocommerce-store-api: json parse failed', err);\n return null;\n }\n const product = Array.isArray(body) ? body.find((p) => p.slug === input.slug) : undefined;\n const prices = product?.prices;\n if (!prices || typeof prices.price !== 'string') return null;\n const minor = prices.currency_minor_unit ?? 2;\n const raw = Number.parseInt(prices.price, 10);\n if (!Number.isFinite(raw)) return null;\n const amount = raw / 10 ** minor;\n const currency = prices.currency_code || 'USD';\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch (err) {\n console.warn(\n '[adaptive-product.bind] woocommerce-store-api: Intl.NumberFormat failed',\n { currency },\n err\n );\n return null;\n }\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\nimport { resolveDomSelectorImage, resolveDomSelectorPrice } from './dom-selector';\nimport { resolveJsonLdImage, resolveJsonLdPrice } from './jsonld';\nimport { resolveOpenGraphImage } from './opengraph';\nimport { resolveShopifyAjaxImage, resolveShopifyAjaxPrice } from './shopify-ajax';\nimport type { BindResult } from './types';\nimport { resolveWooStoreApiPrice } from './woocommerce-store-api';\n\nexport async function resolvePriceBind(input: PriceBind, signal: AbortSignal): Promise<BindResult> {\n try {\n switch (input.type) {\n case 'dom-selector':\n return await resolveDomSelectorPrice(input, signal);\n case 'shopify-ajax':\n return await resolveShopifyAjaxPrice(input, signal);\n case 'woocommerce-store-api':\n return await resolveWooStoreApiPrice(input, signal);\n case 'jsonld':\n return await resolveJsonLdPrice(input, signal);\n default:\n return null;\n }\n } catch (err) {\n if (!(err instanceof Error && err.name === 'AbortError')) {\n console.warn(`[adaptive-product.bind] price/${input.type}:`, err);\n }\n return null;\n }\n}\n\nexport async function resolveImageBind(input: ImageBind, signal: AbortSignal): Promise<BindResult> {\n try {\n switch (input.type) {\n case 'dom-selector':\n return await resolveDomSelectorImage(input, signal);\n case 'shopify-ajax':\n return await resolveShopifyAjaxImage(input, signal);\n case 'opengraph':\n return await resolveOpenGraphImage(input, signal);\n case 'jsonld':\n return await resolveJsonLdImage(input, signal);\n default:\n return null;\n }\n } catch (err) {\n if (!(err instanceof Error && err.name === 'AbortError')) {\n console.warn(`[adaptive-product.bind] image/${input.type}:`, err);\n }\n return null;\n }\n}\n\nexport type { BindResult, ImageResolver, PriceResolver } from './types';\n", "import type { Product } from '../schema-primitives';\nimport { resolveImageBind, resolvePriceBind } from './index';\n\nexport type BindCallback = (\n productId: string,\n kind: 'image' | 'price',\n value: string | null\n) => void;\n\nexport class BindController {\n #abort = new AbortController();\n #pending: Promise<void>[] = [];\n #destroyed = false;\n\n constructor(\n private readonly products: readonly Product[],\n private readonly callback: BindCallback\n ) {}\n\n start(): void {\n for (const product of this.products) {\n if (product.image.bind) {\n this.#pending.push(this.#resolveImage(product.id, product.image.bind));\n }\n if (product.price?.bind) {\n this.#pending.push(this.#resolvePrice(product.id, product.price.bind));\n }\n }\n }\n\n async allSettled(): Promise<void> {\n await Promise.allSettled(this.#pending);\n }\n\n destroy(): void {\n if (this.#destroyed) return;\n this.#destroyed = true;\n this.#abort.abort();\n }\n\n async #resolveImage(\n productId: string,\n bind: NonNullable<Product['image']['bind']>\n ): Promise<void> {\n const value = await resolveImageBind(bind, this.#abort.signal);\n if (this.#destroyed) return;\n this.callback(productId, 'image', value);\n }\n\n async #resolvePrice(\n productId: string,\n bind: NonNullable<NonNullable<Product['price']>['bind']>\n ): Promise<void> {\n const value = await resolvePriceBind(bind, this.#abort.signal);\n if (this.#destroyed) return;\n this.callback(productId, 'price', value);\n }\n}\n", "import type { AddToCartInput, VariantPayload } from '../platformAdapter';\n\nexport type CardProductState =\n | { status: 'empty'; id: null }\n | { status: 'invalid'; id: string | null }\n | { status: 'valid'; id: string };\n\nexport type CardVariantState =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'ready'; payload: VariantPayload }\n | { status: 'error'; message: string };\n\nexport type CardCartState =\n | { status: 'idle' }\n | { status: 'pending'; variantId: string | null }\n | { status: 'added' }\n | { status: 'error'; message: string };\n\nexport interface CardState {\n generation: number;\n connection: 'connected' | 'disconnected';\n commerce: 'available' | 'unavailable';\n product: CardProductState;\n face: 'front' | 'back';\n flip: 'flat' | 'animating';\n variants: CardVariantState;\n cart: CardCartState;\n selection: Record<string, string>;\n activation: { status: 'idle' } | { status: 'pending'; reducedMotion: boolean };\n}\n\nexport type CardEvent =\n | { type: 'props-replaced'; productId: string | null; valid: boolean }\n | { type: 'commerce-updated'; available: boolean }\n | { type: 'primary-activated'; reducedMotion?: boolean }\n | { type: 'variants-resolved'; generation: number; payload: VariantPayload | null }\n | { type: 'variants-failed'; generation: number; message: string; errorName?: string }\n | { type: 'selection-changed'; attributeKey: string; valueId: string }\n | { type: 'variant-add-activated'; variantId: string }\n | { type: 'add-succeeded'; generation: number }\n | { type: 'add-failed'; generation: number; message: string; code?: string }\n | { type: 'retry-activated' }\n | { type: 'back-activated' }\n | { type: 'animation-settled' }\n | { type: 'disconnected' }\n | { type: 'reconnected' };\n\nexport type CardEffect =\n | { type: 'lookup-variants'; generation: number; productId: string }\n | {\n type: 'add-to-cart';\n generation: number;\n input: AddToCartInput;\n }\n | { type: 'open-cart' }\n | { type: 'close-sdk' }\n | { type: 'publish-event'; name: string; props: Record<string, unknown> }\n | { type: 'capture-telemetry'; name: string; props: Record<string, unknown> }\n | { type: 'schedule-flip-settle'; delayMs: number }\n | { type: 'cancel-flip-settle' }\n | { type: 'bind-product'; productId: string }\n | { type: 'report-render-health' }\n | { type: 'cleanup' };\n\nexport interface CardTransition {\n state: CardState;\n effects: CardEffect[];\n}\n\nexport interface CardView {\n face: CardState['face'];\n flipAnimating: boolean;\n primaryCta: {\n mode: 'add' | 'adding' | 'view-cart';\n disabled: boolean;\n };\n panel: {\n status: 'hidden' | 'loading' | 'ready' | 'error';\n payload: VariantPayload | null;\n error: string | null;\n };\n addError: string | null;\n}\n\nexport function createInitialCardState(generation = 0): CardState {\n return {\n generation,\n connection: 'connected',\n commerce: 'available',\n product: { status: 'empty', id: null },\n face: 'front',\n flip: 'flat',\n variants: { status: 'idle' },\n cart: { status: 'idle' },\n selection: {},\n activation: { status: 'idle' },\n };\n}\n\nexport function selectCardView(state: CardState): CardView {\n const primaryCta =\n state.cart.status === 'added'\n ? ({\n mode: 'view-cart',\n disabled: state.connection !== 'connected' || state.commerce !== 'available',\n } as const)\n : state.cart.status === 'pending'\n ? ({ mode: 'adding', disabled: true } as const)\n : ({\n mode: 'add',\n disabled:\n state.connection !== 'connected' ||\n state.commerce !== 'available' ||\n state.product.status !== 'valid',\n } as const);\n\n let panel: CardView['panel'] = { status: 'hidden', payload: null, error: null };\n if (state.face === 'back') {\n switch (state.variants.status) {\n case 'ready':\n panel = { status: 'ready', payload: state.variants.payload, error: null };\n break;\n case 'error':\n panel = { status: 'error', payload: null, error: state.variants.message };\n break;\n case 'idle':\n case 'loading':\n panel = { status: 'loading', payload: null, error: null };\n break;\n }\n }\n\n return {\n face: state.face,\n flipAnimating: state.flip === 'animating',\n primaryCta,\n panel,\n addError: state.cart.status === 'error' ? state.cart.message : null,\n };\n}\n\nfunction unchanged(state: CardState): CardTransition {\n return { state, effects: [] };\n}\n\nfunction replaceProps(\n state: CardState,\n event: Extract<CardEvent, { type: 'props-replaced' }>\n): CardTransition {\n const sameProduct =\n event.valid &&\n event.productId !== null &&\n state.product.status === 'valid' &&\n state.product.id === event.productId;\n\n if (sameProduct && event.productId !== null) {\n const effects: CardEffect[] = [{ type: 'bind-product', productId: event.productId }];\n if (state.variants.status === 'idle') {\n effects.push({\n type: 'lookup-variants',\n generation: state.generation,\n productId: event.productId,\n });\n }\n effects.push({ type: 'report-render-health' });\n return { state, effects };\n }\n\n const generation = state.generation + 1;\n const next = { ...createInitialCardState(generation), commerce: state.commerce };\n const effects: CardEffect[] = [{ type: 'cancel-flip-settle' }];\n if (!event.valid || event.productId === null) {\n return {\n state: {\n ...next,\n product: { status: 'invalid', id: event.productId },\n },\n effects,\n };\n }\n\n effects.push(\n { type: 'bind-product', productId: event.productId },\n { type: 'lookup-variants', generation, productId: event.productId },\n { type: 'report-render-health' }\n );\n return {\n state: {\n ...next,\n product: { status: 'valid', id: event.productId },\n },\n effects,\n };\n}\n\nfunction publish(name: string, props: Record<string, unknown>): CardEffect {\n return { type: 'publish-event', name, props };\n}\n\nfunction activatePrimary(\n state: CardState,\n event: Extract<CardEvent, { type: 'primary-activated' }>\n): CardTransition {\n if (\n state.connection !== 'connected' ||\n state.commerce !== 'available' ||\n state.product.status !== 'valid' ||\n state.face !== 'front'\n ) {\n return unchanged(state);\n }\n const productId = state.product.id;\n if (state.cart.status === 'added') {\n return {\n state,\n effects: [\n publish('chatbot.product_card.view_cart_clicked', { product_id: productId }),\n { type: 'close-sdk' },\n { type: 'open-cart' },\n ],\n };\n }\n if (state.cart.status === 'pending') return unchanged(state);\n if (state.activation.status === 'pending') return unchanged(state);\n\n if (state.variants.status === 'idle' || state.variants.status === 'loading') {\n return {\n state: {\n ...state,\n variants: { status: 'loading' },\n activation: { status: 'pending', reducedMotion: event.reducedMotion ?? false },\n },\n effects:\n state.variants.status === 'idle'\n ? [{ type: 'lookup-variants', generation: state.generation, productId }]\n : [],\n };\n }\n\n const shouldOpenPanel =\n state.variants.status === 'error' || state.variants.payload.variants.length > 1;\n if (shouldOpenPanel) {\n return {\n state: {\n ...state,\n face: 'back',\n flip: event.reducedMotion ? 'flat' : 'animating',\n activation: { status: 'idle' },\n },\n effects: [\n publish('chatbot.product_card.face_changed', {\n product_id: productId,\n from: state.face,\n to: 'back',\n }),\n ...(event.reducedMotion\n ? []\n : ([{ type: 'schedule-flip-settle', delayMs: 450 }] satisfies CardEffect[])),\n ],\n };\n }\n\n return {\n state: {\n ...state,\n cart: { status: 'pending', variantId: null },\n activation: { status: 'idle' },\n },\n effects: [\n {\n type: 'add-to-cart',\n generation: state.generation,\n input: { product_id: productId, variant_id: null, quantity: 1 },\n },\n ],\n };\n}\n\nfunction serializeSelection(selection: Record<string, string>): {\n selection_keys: string;\n selection_serialized: string;\n} {\n const keys = Object.keys(selection).sort();\n return {\n selection_keys: keys.join(','),\n selection_serialized: keys.map((key) => `${key}=${selection[key]}`).join(';'),\n };\n}\n\nfunction activateVariantAdd(\n state: CardState,\n event: Extract<CardEvent, { type: 'variant-add-activated' }>\n): CardTransition {\n if (\n state.connection !== 'connected' ||\n state.commerce !== 'available' ||\n state.product.status !== 'valid' ||\n state.face !== 'back' ||\n state.variants.status !== 'ready' ||\n state.cart.status === 'pending' ||\n state.cart.status === 'added'\n ) {\n return unchanged(state);\n }\n const variant = state.variants.payload.variants.find(\n (candidate) => candidate.id === event.variantId\n );\n if (!variant?.in_stock) return unchanged(state);\n\n const props = {\n product_id: state.product.id,\n variant_id: variant.id,\n ...serializeSelection(state.selection),\n };\n return {\n state: { ...state, cart: { status: 'pending', variantId: variant.id } },\n effects: [\n publish('chatbot.product_card.add_to_cart.attempted', props),\n {\n type: 'add-to-cart',\n generation: state.generation,\n input: { product_id: state.product.id, variant_id: variant.id, quantity: 1 },\n },\n ],\n };\n}\n\nfunction applyAddResult(\n state: CardState,\n event: Extract<CardEvent, { type: 'add-succeeded' | 'add-failed' }>\n): CardTransition {\n if (state.cart.status !== 'pending' || state.product.status !== 'valid') {\n return unchanged(state);\n }\n const props = {\n product_id: state.product.id,\n variant_id: state.cart.variantId,\n ...serializeSelection(state.selection),\n };\n if (event.type === 'add-failed') {\n return {\n state: { ...state, cart: { status: 'error', message: event.message } },\n effects: [\n publish('chatbot.product_card.add_to_cart.failed', {\n ...props,\n error_code: event.code ?? 'unknown',\n error_name: event.message,\n }),\n ],\n };\n }\n return {\n state: { ...state, face: 'front', flip: 'flat', cart: { status: 'added' } },\n effects: [\n publish('chatbot.product_card.add_to_cart.succeeded', props),\n { type: 'cancel-flip-settle' },\n ],\n };\n}\n\nexport function transitionCard(state: CardState, event: CardEvent): CardTransition {\n if (event.type === 'props-replaced') return replaceProps(state, event);\n if (event.type === 'commerce-updated') {\n const commerce = event.available ? 'available' : 'unavailable';\n return commerce === state.commerce\n ? unchanged(state)\n : { state: { ...state, commerce }, effects: [] };\n }\n\n if (\n (event.type === 'variants-resolved' ||\n event.type === 'variants-failed' ||\n event.type === 'add-succeeded' ||\n event.type === 'add-failed') &&\n (event.generation !== state.generation ||\n state.connection !== 'connected' ||\n state.product.status !== 'valid')\n ) {\n return unchanged(state);\n }\n\n switch (event.type) {\n case 'primary-activated':\n return activatePrimary(state, event);\n case 'variants-resolved': {\n const next: CardState = {\n ...state,\n variants:\n event.payload === null ? { status: 'idle' } : { status: 'ready', payload: event.payload },\n };\n if (state.activation.status !== 'pending') return { state: next, effects: [] };\n return activatePrimary(\n { ...next, activation: { status: 'idle' } },\n { type: 'primary-activated', reducedMotion: state.activation.reducedMotion }\n );\n }\n case 'variants-failed': {\n const failureEffect = publish('chatbot.product_card.variant_fetch.failed', {\n product_id: state.product.id,\n error_name: event.errorName ?? 'Error',\n });\n const next: CardState = {\n ...state,\n variants: { status: 'error', message: event.message },\n };\n if (state.activation.status !== 'pending') {\n return { state: next, effects: [failureEffect] };\n }\n const resumed = activatePrimary(\n { ...next, activation: { status: 'idle' } },\n { type: 'primary-activated', reducedMotion: state.activation.reducedMotion }\n );\n return { state: resumed.state, effects: [failureEffect, ...resumed.effects] };\n }\n case 'selection-changed':\n if (\n state.commerce !== 'available' ||\n state.face !== 'back' ||\n state.variants.status !== 'ready'\n ) {\n return unchanged(state);\n }\n return {\n state: {\n ...state,\n selection: { ...state.selection, [event.attributeKey]: event.valueId },\n },\n effects:\n state.product.status === 'valid'\n ? [\n publish('chatbot.product_card.variant_selected', {\n product_id: state.product.id,\n attribute_key: event.attributeKey,\n value_id: event.valueId,\n }),\n ]\n : [],\n };\n case 'variant-add-activated':\n return activateVariantAdd(state, event);\n case 'add-succeeded':\n case 'add-failed':\n return applyAddResult(state, event);\n case 'retry-activated':\n if (\n state.product.status !== 'valid' ||\n state.commerce !== 'available' ||\n state.variants.status !== 'error'\n ) {\n return unchanged(state);\n }\n return {\n state: { ...state, variants: { status: 'loading' } },\n effects: [\n {\n type: 'lookup-variants',\n generation: state.generation,\n productId: state.product.id,\n },\n ],\n };\n case 'back-activated':\n if (state.face !== 'back') return unchanged(state);\n return {\n state: { ...state, face: 'front', flip: 'flat' },\n effects: [\n { type: 'cancel-flip-settle' },\n ...(state.product.status === 'valid'\n ? [\n publish('chatbot.product_card.face_changed', {\n product_id: state.product.id,\n from: 'back',\n to: 'front',\n }),\n ]\n : []),\n ],\n };\n case 'animation-settled':\n if (state.flip !== 'animating') return unchanged(state);\n return {\n state: { ...state, flip: 'flat' },\n effects: [{ type: 'cancel-flip-settle' }],\n };\n case 'disconnected':\n if (state.connection === 'disconnected') return unchanged(state);\n return {\n state: { ...state, connection: 'disconnected', flip: 'flat' },\n effects: [{ type: 'cleanup' }],\n };\n case 'reconnected': {\n if (state.connection === 'connected') return unchanged(state);\n const generation = state.generation + 1;\n return {\n state: { ...state, generation, connection: 'connected' },\n effects:\n state.product.status === 'valid' && state.commerce === 'available'\n ? [\n { type: 'bind-product', productId: state.product.id },\n { type: 'lookup-variants', generation, productId: state.product.id },\n { type: 'report-render-health' },\n ]\n : [],\n };\n }\n default:\n return unchanged(state);\n }\n}\n", "import type { ProductPlatformAdapter, VariantPayload } from './platformAdapter';\n\n/**\n * Module-level cache keyed by product_id. Dedupes concurrent fetches \u2014\n * multiple cards mounted for the same product, or remounts on chat-trail\n * rehydration, share one in-flight Promise. Cache entries live for the\n * lifetime of the page; rejected fetches are NOT cached so the next call\n * retries cleanly.\n */\nconst _cache = new Map<string, Promise<VariantPayload>>();\n\nexport function cachedFetchVariants(\n adapter: ProductPlatformAdapter,\n productId: string\n): Promise<VariantPayload> {\n const hit = _cache.get(productId);\n if (hit) return hit;\n\n const promise = adapter.fetchVariants(productId).catch((err) => {\n // Don't poison the cache with a rejected promise; remove the entry\n // so the next call re-attempts the fetch.\n _cache.delete(productId);\n throw err;\n });\n _cache.set(productId, promise);\n return promise;\n}\n\n/** Test helper: drop all cached entries. */\nexport function clearVariantCache(): void {\n _cache.clear();\n}\n", "import {\n getProductPlatformAdapter,\n type ProductPlatformAdapter,\n type VariantPayload,\n} from '../platformAdapter';\nimport { cachedFetchVariants } from '../variantCache';\nimport type { CardEffect, CardEvent } from './card';\n\nexport interface CardEffectPorts {\n dispatch(event: CardEvent): void;\n publishEvent(name: string, props: Record<string, unknown>): void;\n captureTelemetry(name: string, props: Record<string, unknown>): void;\n closeSdk(): void;\n bindProduct(productId: string): void;\n reportRenderHealth(): void;\n cleanup(): void;\n schedule(callback: () => void, delayMs: number): unknown;\n cancelScheduled(handle: unknown): void;\n getAdapter?(): ProductPlatformAdapter | null;\n fetchVariants?(adapter: ProductPlatformAdapter, productId: string): Promise<VariantPayload>;\n}\n\nexport interface CardEffectExecutor {\n execute(effect: CardEffect): Promise<void>;\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n return fallback;\n}\n\nexport function createCardEffectExecutor(ports: CardEffectPorts): CardEffectExecutor {\n const getAdapter = ports.getAdapter ?? getProductPlatformAdapter;\n const fetchVariants = ports.fetchVariants ?? cachedFetchVariants;\n let flipSettleHandle: unknown;\n let lifecycle = 0;\n\n const cancelFlipSettle = (): void => {\n if (flipSettleHandle === undefined) return;\n ports.cancelScheduled(flipSettleHandle);\n flipSettleHandle = undefined;\n };\n\n const execute = async (effect: CardEffect): Promise<void> => {\n try {\n switch (effect.type) {\n case 'lookup-variants': {\n const adapter = getAdapter();\n if (!adapter) return;\n const startedIn = lifecycle;\n try {\n const payload = await fetchVariants(adapter, effect.productId);\n if (startedIn !== lifecycle) return;\n ports.dispatch({\n type: 'variants-resolved',\n generation: effect.generation,\n payload,\n });\n } catch (error) {\n if (startedIn !== lifecycle) return;\n ports.dispatch({\n type: 'variants-failed',\n generation: effect.generation,\n message: errorMessage(error, 'Variant lookup failed'),\n errorName: error instanceof Error ? error.name : 'Error',\n });\n }\n return;\n }\n case 'add-to-cart': {\n const adapter = getAdapter();\n if (!adapter) {\n ports.dispatch({\n type: 'add-failed',\n generation: effect.generation,\n message: \"Couldn't add to cart. Try again.\",\n code: 'adapter_unavailable',\n });\n return;\n }\n const startedIn = lifecycle;\n try {\n const result = await adapter.addToCart(effect.input);\n if (startedIn !== lifecycle) return;\n ports.dispatch(\n result.ok\n ? { type: 'add-succeeded', generation: effect.generation }\n : {\n type: 'add-failed',\n generation: effect.generation,\n message: result.error?.message ?? \"Couldn't add to cart. Try again.\",\n code: result.error?.code ?? 'unknown',\n }\n );\n } catch (error) {\n if (startedIn !== lifecycle) return;\n ports.dispatch({\n type: 'add-failed',\n generation: effect.generation,\n message: errorMessage(error, \"Couldn't add to cart. Try again.\"),\n code: 'unknown',\n });\n }\n return;\n }\n case 'open-cart': {\n const adapter = getAdapter();\n if (adapter) await adapter.openCart();\n return;\n }\n case 'close-sdk':\n ports.closeSdk();\n return;\n case 'publish-event':\n ports.publishEvent(effect.name, effect.props);\n return;\n case 'capture-telemetry':\n ports.captureTelemetry(effect.name, effect.props);\n return;\n case 'schedule-flip-settle':\n cancelFlipSettle();\n {\n const scheduledIn = lifecycle;\n flipSettleHandle = ports.schedule(() => {\n flipSettleHandle = undefined;\n if (scheduledIn === lifecycle) ports.dispatch({ type: 'animation-settled' });\n }, effect.delayMs);\n }\n return;\n case 'cancel-flip-settle':\n cancelFlipSettle();\n return;\n case 'bind-product':\n ports.bindProduct(effect.productId);\n return;\n case 'report-render-health':\n ports.reportRenderHealth();\n return;\n case 'cleanup':\n lifecycle += 1;\n cancelFlipSettle();\n ports.cleanup();\n return;\n }\n } catch {\n // Host integrations and telemetry must not crash the widget.\n }\n };\n\n return { execute };\n}\n", "/**\n * HTML Sanitizer\n *\n * Sanitizes HTML to prevent XSS attacks.\n * Uses native Sanitizer API when available, falls back to whitelist approach.\n */\n\nconst ALLOWED_TAGS = new Set([\n 'b',\n 'strong',\n 'i',\n 'em',\n 'u',\n 'span',\n 'div',\n 'p',\n 'br',\n 'ul',\n 'ol',\n 'li',\n 'code',\n 'pre',\n 'small',\n 'sup',\n 'sub',\n 'a',\n 'button',\n // SVG elements (for inline Lucide icons in config HTML)\n 'svg',\n 'path',\n 'circle',\n 'line',\n 'polyline',\n 'polygon',\n 'rect',\n 'g',\n]);\n\nfunction normalizeUrlAttr(value: string): string {\n const withoutControlCharacters = Array.from(value, (character) => {\n const codePoint = character.codePointAt(0) ?? 0;\n return codePoint <= 0x1f || codePoint === 0x7f ? '' : character;\n }).join('');\n return withoutControlCharacters.trim().toLowerCase();\n}\n\nfunction isDangerousUrlAttr(name: string, value: string): boolean {\n if (name !== 'href' && name !== 'src' && name !== 'formaction') return false;\n const normalized = normalizeUrlAttr(value);\n return (\n normalized.startsWith('javascript:') ||\n normalized.startsWith('vbscript:') ||\n normalized.startsWith('data:')\n );\n}\n\nexport function sanitizeHtml(html: string): string {\n // Try native Sanitizer API first\n const hasNative = typeof (window as any).Sanitizer === 'function';\n if (hasNative) {\n try {\n const s = new (window as any).Sanitizer({});\n const frag = s.sanitizeToFragment(html);\n const div = document.createElement('div');\n div.append(frag);\n return div.innerHTML;\n } catch {\n // Fall through to manual sanitizer\n }\n }\n\n // Conservative fallback sanitizer\n const tpl = document.createElement('template');\n tpl.innerHTML = html;\n const root = tpl.content;\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);\n const toRemove: Element[] = [];\n\n while (walker.nextNode()) {\n const el = walker.currentNode as Element;\n const tag = el.tagName.toLowerCase();\n\n if (!ALLOWED_TAGS.has(tag)) {\n toRemove.push(el);\n continue;\n }\n\n // Remove dangerous attributes\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n const isEvent = name.startsWith('on');\n\n if (isEvent || isDangerousUrlAttr(name, attr.value)) {\n el.removeAttribute(attr.name);\n }\n }\n }\n\n // Remove disallowed elements but keep their children\n for (const el of toRemove) {\n while (el.firstChild) {\n el.parentNode?.insertBefore(el.firstChild, el);\n }\n el.remove();\n }\n\n return tpl.innerHTML;\n}\n", "/**\n * Inject a `<style>` into the element's OWN root node \u2014 the shadow root that\n * contains it when the widget is mounted inside another element's shadow tree\n * (e.g. a canvas's `<sc-mount>`), or `document.head` when it lives in the light\n * DOM (chat trail, a host PDP page).\n *\n * Why this exists: these product widgets render in light DOM (`createRenderRoot`\n * returns `this`) and style themselves with an injected stylesheet. When a\n * canvas mounts the widget inside `<sc-mount>`, the widget subtree lands inside\n * `sc-mount`'s SHADOW root. A stylesheet appended to `document.head` does NOT\n * cross that shadow boundary, so the widget rendered with default browser\n * styling and none of its own CSS. Injecting into `getRootNode()` puts the\n * stylesheet in the same tree as the element, so it applies on every surface.\n * Only CSS custom-property VALUES cross shadow boundaries \u2014 which is why the\n * host canvas themes the widget via `--sc-*` tokens while the layout/appearance\n * CSS must travel with the widget itself.\n *\n * Idempotent per root: guarded by the style id so repeated mounts into the same\n * root inject once; distinct roots (multiple mounts) each get their own copy.\n */\nexport function ensureStylesInRoot(el: Element, id: string, css: string): void {\n if (typeof document === 'undefined') return;\n const root = el.getRootNode();\n const target: ParentNode = root instanceof ShadowRoot ? root : document.head;\n if (target.querySelector(`#${id}`)) return;\n const style = document.createElement('style');\n style.id = id;\n style.textContent = css;\n (target as ParentNode & Node).appendChild(style);\n}\n", "/**\n * renderHealth \u2014 runtime self-detection of clipping and blank render.\n *\n * A product card can look correct in tests yet clip text or paint blank on a\n * real host (a narrow theme column, a font that measures wider, a broken image\n * CDN). The battery walks the playground at build time; this module is its\n * RUNTIME counterpart \u2014 after the card paints, it re-measures itself against the\n * SAME policy and emits a PostHog signal when it finds an undeclared clip or a\n * blank card. Field-RUM for \"the card broke on a customer's page\".\n *\n * NON-PII discipline: only element tag/class PATHS and pixel numbers ever leave\n * the widget \u2014 never text content, never customer copy. The paths are the\n * structural coordinates a dashboard needs to locate the offending node.\n *\n * Pattern-setter: this lives in a small shared module (not inline in\n * ProductCardLit) so sibling widgets (comparison, grid, hero) can adopt the same\n * self-detection with one import.\n */\n\n/** A single undeclared-clip finding inside a card. */\nexport interface ClipFinding {\n /** Structural path to the offending node, e.g. `div.sc-product-card__specs>span`. NON-PII. */\n path: string;\n /** How many px the node overflows its clip box (text scrollWidth overflow, or\n * child escaping the card's own clip box). Rounded. */\n over_px: number;\n /** Why this counts as a clip. Absent for ordinary (Tier-3) undeclared clips.\n * `identity-truncated` marks a Tier-1 CRITICAL text node (product name,\n * price, CTA label, picker title \u2014 `[data-critical-text]`) that truncated\n * even with a declared ellipsis / clip-ok. Identity text must render in full,\n * so ANY horizontal overflow of a critical node is a defect. NON-PII. */\n reason?: 'identity-truncated';\n}\n\n/** Result of a render-health measurement pass. */\nexport interface RenderHealth {\n /** Undeclared clips found inside the card (empty = clean). */\n clips: ClipFinding[];\n /** True when the card occupies real area but painted no text AND no complete\n * image (a blank card). */\n blank: boolean;\n}\n\n/** Minimum card area (px\u00B2) below which \"blank\" is not meaningful (a collapsed /\n * offscreen node isn't a blank-card bug). */\nconst BLANK_MIN_AREA = 60 * 60;\n\n/**\n * Build a short, NON-PII structural path for an element: up to 3 ancestors of\n * `tag.class` (first class only), joined by `>`. Never includes text content.\n */\nfunction elementPath(el: Element, stopAt: Element): string {\n const seg = (e: Element): string => {\n const tag = e.tagName.toLowerCase();\n const cls =\n typeof e.className === 'string' && e.className.trim()\n ? `.${e.className.trim().split(/\\s+/)[0]}`\n : '';\n return `${tag}${cls}`;\n };\n const parts: string[] = [seg(el)];\n let cur: Element | null = el.parentElement;\n let hops = 0;\n while (cur && cur !== stopAt && hops < 3) {\n parts.unshift(seg(cur));\n cur = cur.parentElement;\n hops++;\n }\n return parts.join('>');\n}\n\n/** True if `el` (or an ancestor up to `root`) declares the clip is intentional. */\nfunction hasClipOkAncestor(el: Element, root: Element): boolean {\n let cur: Element | null = el;\n while (cur && cur !== root.parentElement) {\n if (cur.hasAttribute?.('data-clip-ok')) return true;\n cur = cur.parentElement;\n }\n return false;\n}\n\n/** A clipping ancestor exempt from escape-flagging: scrollable, or a declared\n * line-clamp ellipsis box, or explicitly clip-ok. Mirrors the battery policy. */\nfunction isDeclaredClipBox(cs: CSSStyleDeclaration): boolean {\n const scrollable =\n ['auto', 'scroll'].includes(cs.overflowX) || ['auto', 'scroll'].includes(cs.overflowY);\n if (scrollable) return true;\n const lineClamp = cs.webkitLineClamp || cs.getPropertyValue('-webkit-line-clamp');\n if (lineClamp && lineClamp !== 'none') return true;\n return false;\n}\n\nfunction isHardClipBox(cs: CSSStyleDeclaration): boolean {\n return ['hidden', 'clip'].includes(cs.overflowX) || ['hidden', 'clip'].includes(cs.overflowY);\n}\n\n/**\n * Measure the render health of a card element. Pure DOM read \u2014 no side effects,\n * no telemetry. Safe to call any time after paint. The caller (ProductCardLit)\n * decides whether/when to emit.\n *\n * Policy mirrors tmp-live/battery.mjs:\n * - identity-truncated (Tier 1): a [data-critical-text] node (name, price, CTA\n * label, picker title) whose scrollWidth exceeds clientWidth by >1px \u2014 even\n * WITH an ellipsis or under [data-clip-ok]. Identity text must render in full.\n * - text-clip: an element with its own text whose scrollWidth exceeds\n * clientWidth by >1px and has no textOverflow:ellipsis, not under [data-clip-ok].\n * - element-clip: a text/media node escaping a HARD-clip ancestor by >1px,\n * where the ancestor is not scrollable, not a line-clamp box, not clip-ok.\n * - blank: the card has real area but no painted text and no complete image.\n */\nexport function measureRenderHealth(card: Element): RenderHealth {\n const clips: ClipFinding[] = [];\n const seen = new Set<string>();\n const push = (f: ClipFinding): void => {\n const k = `${f.path}|${f.over_px}`;\n if (seen.has(k)) return;\n seen.add(k);\n clips.push(f);\n };\n\n let hasText = false;\n let hasCompleteImage = false;\n\n const els = card.querySelectorAll('*');\n for (const el of els) {\n const cs = getComputedStyle(el);\n if (cs.display === 'none' || cs.visibility === 'hidden') continue;\n const r = el.getBoundingClientRect();\n if (r.width === 0 || r.height === 0) continue;\n\n // Track painted content for the blank check.\n const ownText = Array.from(el.childNodes).some(\n (n) => n.nodeType === 3 && (n.textContent ?? '').trim().length > 0\n );\n if (ownText) hasText = true;\n if (el.tagName === 'IMG') {\n const img = el as HTMLImageElement;\n if (img.complete && img.naturalWidth > 0 && img.dataset.imageError !== 'true') {\n hasCompleteImage = true;\n }\n }\n\n // Tier 1 \u2014 CRITICAL/identity text (product name, price, CTA label, picker\n // title, marked [data-critical-text]). It MUST render in full: ANY\n // horizontal truncation is a defect, EVEN with a declared ellipsis or under\n // a data-clip-ok container. Checked before (and instead of) the Tier-3\n // text-clip rule so an ellipsized identity node is never excused.\n const isCritical =\n typeof el.closest === 'function' ? el.closest('[data-critical-text]') != null : false;\n if (ownText && isCritical && el.scrollWidth > el.clientWidth + 1) {\n push({\n path: elementPath(el, card),\n over_px: el.scrollWidth - el.clientWidth,\n reason: 'identity-truncated',\n });\n }\n\n // text-clip (Tier 3): own-text node overflowing without an ellipsis, not\n // declared, not a critical node (handled by the Tier-1 rule above).\n if (\n ownText &&\n !isCritical &&\n el.scrollWidth > el.clientWidth + 1 &&\n cs.textOverflow !== 'ellipsis' &&\n !hasClipOkAncestor(el, card)\n ) {\n push({ path: elementPath(el, card), over_px: el.scrollWidth - el.clientWidth });\n }\n\n // element-clip: escape from a HARD-clip ancestor within the card.\n let a = el.parentElement;\n while (a && a !== card.parentElement) {\n const acs = getComputedStyle(a);\n if (isDeclaredClipBox(acs)) break;\n if (isHardClipBox(acs)) {\n const ar = a.getBoundingClientRect();\n const esc = Math.max(\n 0,\n Math.round(r.right - ar.right),\n Math.round(ar.left - r.left),\n Math.round(r.bottom - ar.bottom),\n Math.round(ar.top - r.top)\n );\n if (esc > 1 && !hasClipOkAncestor(el, card) && !hasClipOkAncestor(a, card)) {\n const t = (el.textContent ?? '').trim();\n if (t || ['IMG', 'BUTTON', 'A'].includes(el.tagName)) {\n push({ path: elementPath(el, card), over_px: esc });\n }\n }\n break;\n }\n a = a.parentElement;\n }\n }\n\n const cardRect = card.getBoundingClientRect();\n const area = cardRect.width * cardRect.height;\n const blank = area >= BLANK_MIN_AREA && !hasText && !hasCompleteImage;\n\n return { clips, blank };\n}\n", "/**\n * renderHealthReporter \u2014 the shared, reusable half of runtime render-health\n * self-detection.\n *\n * `renderHealth.ts` MEASURES a rendered element (pure DOM read: undeclared clips\n * + blank paint). This module is the counterpart that REPORTS the measurement:\n * it emits `app:<category>:blank` / `app:<category>:clipped` through the ONE\n * consent-gated door to PostHog (`window.SynOS.runtime.events.emit`), and packages\n * the once-per-visual-state scheduling as a Lit ReactiveController so every tile\n * widget adopts the same self-detection with ~5 lines and no copy-paste.\n *\n * COE-051 lesson: `publish()` only notifies in-process canvas subscribers \u2014 it\n * NEVER reaches PostHog. `emit()` is the sanctioned capture path: the EventBus\n * forwards emit()'d events to its posthogCapture sink (bootstrap-runtime wires it\n * to the consent-gated `telemetry.track`). We never call posthog directly. The\n * event name MUST satisfy validateEventName (`app:{category}:{action}`, colon\n * segments, no `app:` prefix stripped) or the EventBus silently drops it \u2014 the\n * exact silent failure this telemetry exists to surface.\n *\n * NON-PII discipline: only element tag/class PATHS and pixel numbers ever leave a\n * widget \u2014 never text content, never customer copy.\n */\nimport type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { measureRenderHealth, type RenderHealth } from './renderHealth';\n\ninterface SynOSEventsWindow {\n SynOS?: {\n runtime?: {\n events?: {\n // The ONLY transport that reaches PostHog (see module doc / COE-051).\n emit?: (name: string, props: Record<string, unknown>) => unknown;\n };\n };\n };\n}\n\n/** Emit a telemetry event on the PostHog-reaching path. Never throws. */\nfunction synEmit(name: string, props: Record<string, unknown>): void {\n if (typeof window === 'undefined') return;\n try {\n const emit = (window as unknown as SynOSEventsWindow).SynOS?.runtime?.events?.emit;\n if (typeof emit === 'function') emit(name, props);\n } catch {\n // Telemetry must never crash the widget.\n }\n}\n\nexport interface RenderHealthReportOptions {\n /** Event category segment: emits `app:<category>:blank` / `:clipped`. Must be a\n * valid segment (`^[a-z][a-z0-9_]*$`), e.g. `product_comparison`. */\n category: string;\n /** `widget_kind` tag included in every payload, e.g. `adaptive-product:comparison`. */\n widgetKind: string;\n /** Extra NON-PII context merged into every payload (density, velvet, \u2026). */\n context?: Record<string, unknown>;\n}\n\nconst CLEAN: RenderHealth = { clips: [], blank: false };\n\n/**\n * Measure `root` and, if it clips or paints blank, emit the corresponding\n * self-detection event via the consent-gated emit() path. Returns the measured\n * health (so callers/tests can inspect it). Never throws \u2014 a measurement or\n * telemetry failure must never crash the widget or the host page.\n */\nexport function reportRenderHealth(root: Element, opts: RenderHealthReportOptions): RenderHealth {\n try {\n const health = measureRenderHealth(root);\n if (health.clips.length === 0 && !health.blank) return health;\n const base: Record<string, unknown> = { ...(opts.context ?? {}), widget_kind: opts.widgetKind };\n if (health.blank) {\n synEmit(`app:${opts.category}:blank`, base);\n return health;\n }\n // Clipped: NON-PII paths + the worst overflow px. A Tier-1 identity\n // truncation (name/price/CTA that didn't render in full) tags the payload so\n // a dashboard can separate \"prose clipped\" from \"identity broke\".\n const paths = health.clips.map((c) => c.path).sort();\n const over_px = health.clips.reduce((m, c) => Math.max(m, c.over_px), 0);\n const identityTruncated = health.clips.some((c) => c.reason === 'identity-truncated');\n synEmit(`app:${opts.category}:clipped`, {\n paths: paths.join(','),\n over_px,\n clip_count: health.clips.length,\n ...(identityTruncated ? { reason: 'identity-truncated' } : {}),\n ...base,\n });\n return health;\n } catch {\n // Self-detection must never crash the widget.\n return CLEAN;\n }\n}\n\n/** What a widget's probe returns for one measurement pass. */\nexport interface RenderHealthProbe extends RenderHealthReportOptions {\n /** The element to measure, or `null` to SKIP this pass. A widget returns null\n * for any UNSETTLED state (loading, error, empty, wrong face) so a transient\n * no-content frame is never mis-reported as a blank tile. */\n root: Element | null;\n /** Stable key for the current visual state; a state reports AT MOST ONCE\n * (prevents event spam across re-renders of the same visual state). */\n stateKey: string;\n}\n\n/**\n * Lit ReactiveController that runs a widget's render-health probe after each\n * paint settles (rAF then a 0ms timeout, so lazy images / web fonts land first),\n * at most once per visual state. Attach in a widget's field initializer:\n *\n * #health = new RenderHealthController(this, () => this.#healthProbe());\n *\n * The controller's `hostUpdated` fires automatically after every host update.\n */\nexport class RenderHealthController implements ReactiveController {\n #probe: () => RenderHealthProbe | null;\n #reported = new Set<string>();\n #raf: number | undefined;\n\n constructor(host: ReactiveControllerHost, probe: () => RenderHealthProbe | null) {\n this.#probe = probe;\n host.addController(this);\n }\n\n hostUpdated(): void {\n this.#schedule();\n }\n\n hostDisconnected(): void {\n this.#cancel();\n }\n\n #schedule(): void {\n if (typeof window === 'undefined') return;\n if (this.#raf !== undefined) return; // one pending pass is enough\n const raf =\n typeof window.requestAnimationFrame === 'function'\n ? window.requestAnimationFrame.bind(window)\n : (cb: FrameRequestCallback) => window.setTimeout(() => cb(0), 0);\n this.#raf = raf(() => {\n window.setTimeout(() => {\n this.#raf = undefined;\n this.#run();\n }, 0);\n }) as unknown as number;\n }\n\n #run(): void {\n try {\n const probe = this.#probe();\n if (!probe || !probe.root) return;\n if (this.#reported.has(probe.stateKey)) return;\n // Mark consumed BEFORE reporting so a clean state isn't re-measured every\n // frame and a finding never double-fires for the same visual state.\n this.#reported.add(probe.stateKey);\n // `probe` IS a RenderHealthReportOptions (superset); reportRenderHealth\n // reads only category/widgetKind/context.\n reportRenderHealth(probe.root, probe);\n } catch {\n // Self-detection must never crash the widget.\n }\n }\n\n #cancel(): void {\n if (\n this.#raf !== undefined &&\n typeof window !== 'undefined' &&\n typeof window.cancelAnimationFrame === 'function'\n ) {\n window.cancelAnimationFrame(this.#raf);\n }\n this.#raf = undefined;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\nimport type { AttributeDef, Variant, VariantPayload } from '../platformAdapter';\nimport { ensureStylesInRoot } from './ensureStylesInRoot';\nimport { RenderHealthController, type RenderHealthProbe } from './renderHealthReporter';\n\n// Neutral, token-driven baseline. The panel is light-DOM so a host canvas's\n// --sc-* tokens cascade in; every color/surface here reads a token (with a\n// LIGHT fallback for bare surfaces like chat/PDP) rather than the old\n// dark-surface translucent-white chrome that clashed on velvet's cream tile.\n// The opinionated two-column + connected-segmented layout lives in the velvet\n// canvas, which supplies token VALUES only (selectors can't cross into the\n// panel's shadow-nested root \u2014 see ensureStylesInRoot). Layout is therefore\n// PARAMETERIZED here and switched on by the canvas via tokens:\n// --sc-variant-cols grid-template-columns (default 1fr = single column;\n// velvet sets \"minmax(72px, 30%) 1fr\" for image | controls)\n// --sc-variant-image-max hero image cap (default 120px; velvet sets 100% to\n// fill its column in the two-column band)\n// The [data-image-col] (image + Back) and [data-panel-body] (title + pickers +\n// CTA) are the two grid items, so one token flips the whole panel between a\n// stacked single column and a two-column layout.\n//\n// Round-2 feedback rework (structure, ALL surfaces):\n// - There is NO top header row with a Back button + right-aligned title\n// anymore (it ate vertical space above the pickers).\n// - The TITLE is the PRODUCT NAME, shown compactly at the TOP of the body\n// (where the old header/back row was) as the panel's visible title.\n// - BACK moves OUT of the header into the BOTTOM of the IMAGE column (below\n// the CTA row, RIGHT of Add) as a small, quiet '\u2039 Back' control \u2014 freeing the vertical\n// the old header consumed for the variant pickers. It keeps emitting the\n// same `variant-panel-back` event.\nconst PANEL_CSS = `\n syntro-variant-panel {\n display: grid;\n /* minmax(0, 1fr) not 1fr: the content track must be able to shrink below its\n min-content size so a long variant label / product title can't push the\n panel past the card. The svp-image-col / svp-body children carry\n min-width:0 to complete the guarantee. */\n grid-template-columns: var(--sc-variant-cols, minmax(0, 1fr));\n /* Item 3: a single 1fr row so the body column can FILL a height-bounded\n parent (velvet's definite-height, overflow:hidden deck card) \u2014 that's\n what gives the interior scroll region a bounded height to scroll within.\n height:100% resolves to auto (a no-op) when the parent height is\n indefinite (chat / PDP), so those surfaces keep their natural,\n non-scrolling height. minmax(0,\u2026) lets the row shrink below content. */\n grid-template-rows: minmax(0, 1fr);\n height: 100%;\n column-gap: 0.75rem;\n align-items: start;\n padding: var(--sc-variant-pad, 12px); font-family: var(--sc-font-family, inherit);\n color: var(--sc-tile-text-color, var(--sc-color-text, inherit));\n box-sizing: border-box;\n /* Root overflow invariant (item e): the panel is a card face \u2014 it can shrink\n with its container and never paints past it. */\n min-width: 0; max-width: 100%; overflow: hidden;\n }\n /* Two-column mode (velvet sets --sc-variant-cols) only makes sense when BOTH\n the image column and the body are present. Loading, error, single-variant-\n note, and image-less states render a single top-level child \u2014 it must span\n all columns instead of being crammed into the narrow image column. */\n syntro-variant-panel > :only-child { grid-column: 1 / -1; }\n /* Image column: the picture with the quiet Back control tucked BELOW it. In\n the single-column (chat/PDP) default it stacks above the controls; in\n velvet's two-column band it is the left feature column. */\n .svp-image-col {\n display: flex; flex-direction: column; align-items: stretch; min-width: 0;\n margin-bottom: var(--sc-variant-row-gap, 10px);\n }\n .svp-image {\n display: block; width: 100%; max-width: var(--sc-variant-image-max, 120px);\n /* Same portrait box as the card front (token-tunable) so the flip reads\n as the same object turning over. */\n aspect-ratio: var(--sc-variant-image-aspect, 1 / 1); object-fit: cover;\n border-radius: var(--sc-radius-md, 8px);\n }\n /* Quiet Back link under the picture \u2014 small, low-emphasis, no pill chrome. */\n .svp-back-btn {\n display: inline-flex; align-items: center; gap: 4px; align-self: flex-start;\n margin-top: 0; padding: 2px 2px; flex: 0 0 auto;\n background: none; border: 0; cursor: pointer;\n font-size: 12px; font-weight: 600; line-height: 1;\n color: var(--sc-tile-title-color, inherit); opacity: 0.7;\n transition: opacity 120ms ease;\n }\n .svp-back-btn:hover { opacity: 1; }\n /* The product NAME as the panel title, at the top of the body. IDENTITY text\n (Tier 1): it must render IN FULL \u2014 it WRAPS at word boundaries rather than\n ellipsizing. The title height may grow for a long name; that's fine. */\n .svp-product-title {\n font-weight: 700; font-size: 14px; line-height: 1.2;\n color: var(--sc-tile-title-color, inherit);\n margin-bottom: var(--sc-variant-row-gap, 10px);\n min-width: 0; white-space: normal; overflow-wrap: break-word;\n }\n /* Item 3: the body is a bounded flex column \u2014 pinned title at the top, a\n flex:1 SCROLL region in the middle (the attribute rows), pinned CTA at the\n bottom. align-self:stretch + min-height:0 let it fill the 1fr row so the\n middle region has a definite height to scroll within. */\n .svp-body { min-width: 0; display: flex; flex-direction: column; align-self: stretch; min-height: 0; max-height: 100%; }\n /* The scrolling middle: many attributes SCROLL (never squish the rows below\n a readable height, never clip a row off). overscroll-behavior:contain keeps\n a flick from chaining to the host page; the bottom fade hints \"more below\".\n data-clip-ok marks this as a DECLARED, reachable scroll affordance so the\n clip battery accepts its overflow. */\n .svp-attributes-scroll {\n flex: 1 1 auto; min-height: 0;\n overflow-y: auto; overflow-x: hidden;\n overscroll-behavior: contain;\n scrollbar-width: none; -ms-overflow-style: none;\n mask-image: linear-gradient(180deg, black 0, black calc(100% - 16px), transparent 100%);\n -webkit-mask-image: linear-gradient(180deg, black 0, black calc(100% - 16px), transparent 100%);\n }\n .svp-attributes-scroll::-webkit-scrollbar { display: none; }\n /* Rows keep a minimum readable/tappable height so a squeezed band scrolls\n rather than crushing the pickers below usability. */\n .svp-attribute-row { margin-bottom: var(--sc-variant-row-gap, 10px); min-height: var(--sc-variant-row-min-h, 40px); }\n .svp-attribute-label {\n font-size: 11px; font-weight: 600; margin-bottom: 4px; letter-spacing: 0.01em;\n color: var(--sc-tile-title-color, inherit); opacity: 0.85;\n }\n .svp-pill-row { display: flex; flex-wrap: wrap; gap: 6px; }\n .svp-pill {\n border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n background: transparent;\n color: var(--sc-tile-title-color, inherit);\n padding: 6px 14px;\n border-radius: 9999px;\n cursor: pointer;\n font-size: 13px; font-weight: 600;\n transition: background-color 120ms ease, color 120ms ease, opacity 120ms ease;\n }\n .svp-pill[aria-checked=\"true\"] {\n background: var(--sc-color-primary, #3d8a5e);\n color: var(--sc-color-on-primary, #ffffff);\n border-color: var(--sc-color-primary, #3d8a5e);\n }\n .svp-pill:disabled { opacity: 0.35; cursor: not-allowed; }\n /* Connected segmented control: pills collapse into one pill-shaped bar with\n hairline dividers; the selected segment keeps the primary fill above. It\n must NOT wrap to a second row in a narrow column \u2014 the segments SHARE the\n available width and shrink (min-width:0) so 2-3 options always sit on one\n line. The container clips rather than wraps. */\n .svp-pill-row[data-segmented] {\n display: flex; flex-wrap: nowrap; gap: 0; width: 100%; max-width: 100%;\n border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n border-radius: 9999px; overflow: hidden;\n }\n .svp-pill-row[data-segmented] .svp-pill {\n border: 0; border-radius: 0;\n flex: 1 1 0; min-width: 0;\n padding: 6px 8px; font-size: 12px;\n white-space: nowrap; text-align: center;\n overflow: hidden; text-overflow: ellipsis;\n }\n .svp-pill-row[data-segmented] .svp-pill + .svp-pill {\n border-left: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n }\n .svp-swatch-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n /* \u2500\u2500 Density Tier B: one-row scroll track (9-16 options) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A wrapped row of 9+ options eats the bounded panel's height; nowrap +\n overflow-x turns the axis into a single scrollable track with an edge\n fade hinting \"more\". Same recipe as the card's compact spec-chip row.\n data-clip-ok: declared, reachable scroll affordance (clip battery). */\n .svp-pill-row[data-scroll], .svp-swatch-row[data-scroll] {\n flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;\n overscroll-behavior-x: contain; scroll-snap-type: x proximity;\n scrollbar-width: none; -ms-overflow-style: none;\n mask-image: linear-gradient(90deg, black 0, black calc(100% - 20px), transparent 100%);\n -webkit-mask-image: linear-gradient(90deg, black 0, black calc(100% - 20px), transparent 100%);\n padding-bottom: 2px;\n }\n .svp-pill-row[data-scroll]::-webkit-scrollbar,\n .svp-swatch-row[data-scroll]::-webkit-scrollbar { display: none; }\n .svp-pill-row[data-scroll] > *, .svp-swatch-row[data-scroll] > * {\n flex: 0 0 auto; scroll-snap-align: start;\n }\n /* \u2500\u2500 Density Tier C: collapsed selector (17+ options) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n The axis renders as a select-style trigger showing the CURRENT choice;\n tapping opens a bounded, scrollable listbox inside the panel (filter\n input past 24 options). Only the offending axis collapses \u2014 sibling\n axes keep their own tier. */\n .svp-collapse { min-width: 0; }\n .svp-collapse-trigger {\n display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;\n border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n background: transparent; border-radius: var(--sc-radius-md, 8px);\n padding: 8px 10px; cursor: pointer; font-size: 13px; font-weight: 600;\n color: var(--sc-tile-title-color, inherit);\n }\n .svp-collapse-trigger .svp-swatch-dot { width: 16px; height: 16px; flex: none; }\n .svp-collapse-value {\n min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .svp-collapse-caret { margin-left: auto; flex: none; opacity: 0.6; font-size: 11px; }\n .svp-collapse-list {\n margin-top: 6px; border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n border-radius: var(--sc-radius-md, 8px);\n max-height: var(--sc-variant-list-max-h, 180px);\n overflow-y: auto; overscroll-behavior: contain;\n }\n .svp-collapse-filter {\n width: 100%; box-sizing: border-box; padding: 7px 10px; border: 0;\n border-bottom: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n font-size: 13px; font-family: inherit; background: transparent;\n color: inherit; outline: none; position: sticky; top: 0;\n backdrop-filter: blur(2px);\n }\n .svp-collapse-option {\n display: flex; align-items: center; gap: 8px; width: 100%;\n padding: 7px 10px; background: transparent; border: 0; cursor: pointer;\n font-size: 13px; font-family: inherit; text-align: left;\n color: var(--sc-tile-title-color, inherit);\n }\n .svp-collapse-option .svp-swatch-dot { width: 14px; height: 14px; flex: none; }\n .svp-collapse-option[aria-selected=\"true\"] {\n background: var(--sc-color-primary, #3d8a5e);\n color: var(--sc-color-on-primary, #ffffff);\n }\n .svp-collapse-option:disabled { opacity: 0.35; cursor: not-allowed; }\n .svp-collapse-empty { padding: 8px 10px; font-size: 12px; opacity: 0.7; }\n .svp-swatch-pill {\n width: 28px; height: 28px; border-radius: 50%;\n border: 2px solid transparent;\n cursor: pointer;\n padding: 0;\n transition: border-color 120ms ease, opacity 120ms ease;\n }\n .svp-swatch-pill[aria-checked=\"true\"] { border-color: var(--sc-color-primary, #3d8a5e); }\n .svp-swatch-pill:disabled { opacity: 0.35; cursor: not-allowed; }\n .svp-swatch-dot { width: 100%; height: 100%; border-radius: 50%; border: 1px solid rgba(0,0,0,0.2); }\n .svp-swatch-label { font-size: 13px; margin-bottom: 4px; opacity: 0.85; }\n .svp-oos-microcopy { font-size: 11px; opacity: 0.6; margin-top: 4px; }\n .svp-single-variant-note { font-size: 12px; opacity: 0.75; padding: 8px 0; }\n .svp-cta-row {\n margin-top: auto; padding-top: var(--sc-variant-row-gap, 12px);\n /* Add + Back share ONE line, always (Back spilled under the CTA when the\n button kept width:100%). The CTA flexes, Back keeps its content size. */\n display: flex; align-items: center; gap: 10px; flex-wrap: nowrap; min-width: 0;\n }\n .svp-cta {\n flex: 1 1 auto; min-width: 0; padding: var(--sc-variant-cta-pad, 10px 16px); border-radius: 9999px;\n border: 1px solid var(--sc-color-primary, #3d8a5e);\n background: var(--sc-color-primary, #3d8a5e);\n color: var(--sc-color-on-primary, #ffffff); font-size: 13px; font-weight: 600;\n cursor: pointer; white-space: nowrap;\n transition: opacity 120ms ease, transform 120ms ease;\n }\n .svp-cta:hover:not(:disabled) { transform: translateY(-1px); }\n .svp-cta:disabled { opacity: 0.4; cursor: not-allowed; }\n .svp-compare { text-decoration: line-through; opacity: 0.65; margin-right: 6px; }\n .svp-skeleton {\n animation: svp-shimmer 1.4s linear infinite;\n background: linear-gradient(90deg, rgba(0,0,0,0.04), rgba(0,0,0,0.08), rgba(0,0,0,0.04));\n background-size: 200% 100%;\n height: 32px; border-radius: var(--sc-radius-md, 8px); margin-bottom: 12px;\n }\n @keyframes svp-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }\n .svp-error-block { padding: 16px; text-align: center; font-size: 13px; }\n .svp-retry-btn { margin-top: 8px; padding: 6px 14px; border-radius: 9999px; border: 1px solid currentColor; background: none; color: inherit; cursor: pointer; }\n .svp-live { position: absolute; left: -9999px; top: 0; width: 1px; height: 1px; overflow: hidden; }\n`;\n\nexport class VariantPanelLit extends LitElement {\n static override properties = {\n payload: { attribute: false },\n productTitle: { type: String },\n productImage: { attribute: false },\n state: { type: String },\n error: { type: String },\n _selection: { state: true },\n _touched: { state: true },\n _expandedAxes: { state: true },\n _axisFilter: { state: true },\n };\n\n payload: VariantPayload | null = null;\n productTitle = '';\n /** Product-level hero image, passed from the card. Shown on the back face and\n * used as the fallback when the selected variant carries no featured_image. */\n productImage: { src: string; alt?: string } | null = null;\n state: 'idle' | 'loading' | 'error' = 'idle';\n error: string | null = null;\n\n /** @internal */\n _selection: Record<string, string> = {};\n /** @internal */\n _touched: Set<string> = new Set();\n /** @internal \u2014 Tier-C axes currently showing their listbox. */\n _expandedAxes: Set<string> = new Set();\n /** @internal \u2014 Tier-C per-axis filter text. */\n _axisFilter: Record<string, string> = {};\n\n constructor() {\n super();\n // Runtime self-detection: emits app:variant_panel:blank / :clipped after\n // paint if the SETTLED panel has area but paints no content. Self-registers\n // via host.addController \u2014 no stored reference needed.\n new RenderHealthController(this, () => this.#healthProbe());\n }\n\n /** Measure only the SETTLED panel (idle state with a payload). The loading\n * skeleton and the error block are unsettled \u2014 a transient no-content frame\n * must never be mis-reported as a blank tile. NON-PII stateKey. */\n #healthProbe(): RenderHealthProbe | null {\n if (this.state !== 'idle' || !this.payload) return null;\n return {\n root: this,\n stateKey: `variant|${this.payload.variants.length}|${this.payload.attributes.length}`,\n category: 'variant_panel',\n widgetKind: 'adaptive-product:variant',\n };\n }\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Inject into the panel's OWN root (shadow root under a canvas's <sc-mount>,\n // or document.head in the light DOM) so the stylesheet actually reaches it.\n ensureStylesInRoot(this, 'syntro-variant-panel-style', PANEL_CSS);\n this.addEventListener('keydown', this._onKeydown);\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener('keydown', this._onKeydown);\n }\n\n private _onKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') {\n this.dispatchEvent(new CustomEvent('variant-panel-back', { bubbles: true, composed: true }));\n }\n };\n\n private _findMatchingVariant(): Variant | null {\n if (!this.payload) return null;\n return (\n this.payload.variants.find((v) =>\n Object.entries(v.selection).every(([k, val]) => this._selection[k] === val)\n ) ?? null\n );\n }\n\n private _isValueDisabled(attrKey: string, valueId: string): boolean {\n if (!this.payload) return false;\n // OOS: a value is disabled if NO in-stock variant exists that uses this value\n // GIVEN the visitor's current selection for OTHER attributes.\n const otherSelections = Object.entries(this._selection).filter(([k]) => k !== attrKey);\n const candidateVariants = this.payload.variants.filter((v) => {\n if (v.selection[attrKey] !== valueId) return false;\n for (const [k, val] of otherSelections) {\n if (v.selection[k] !== val) return false;\n }\n return true;\n });\n return candidateVariants.length === 0 || candidateVariants.every((v) => !v.in_stock);\n }\n\n private _hasAnyOOSForAttribute(attr: AttributeDef): boolean {\n return attr.values.some((v) => this._isValueDisabled(attr.key, v.id));\n }\n\n private _onPillClick(attrKey: string, valueId: string): void {\n if (this._isValueDisabled(attrKey, valueId)) return;\n this._selection = { ...this._selection, [attrKey]: valueId };\n this._touched = new Set([...this._touched, attrKey]);\n this.dispatchEvent(\n new CustomEvent('variant-panel-selection-change', {\n detail: { attribute_key: attrKey, value_id: valueId },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _onBack(): void {\n this.dispatchEvent(new CustomEvent('variant-panel-back', { bubbles: true, composed: true }));\n }\n\n private _onAdd(): void {\n const variant = this._findMatchingVariant();\n if (!variant) return;\n this.dispatchEvent(\n new CustomEvent('variant-panel-add', {\n detail: { variant, selection: { ...this._selection } },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _onRetry(): void {\n this.dispatchEvent(new CustomEvent('variant-panel-retry', { bubbles: true, composed: true }));\n }\n\n /** The hero image URL to show: the matched variant's featured_image when the\n * current selection resolves to a variant that has one, else the product\n * image. Null when neither exists. */\n private _heroSrc(): string | null {\n return this._findMatchingVariant()?.featured_image ?? this.productImage?.src ?? null;\n }\n\n private _ctaEnabled(): boolean {\n if (!this.payload) return false;\n if (this.payload.attributes.some((a) => !this._touched.has(a.key))) return false;\n const m = this._findMatchingVariant();\n return m?.in_stock === true;\n }\n\n override render() {\n if (this.state === 'error') {\n return html`\n <div class=\"svp-error-block\" data-error-block>\n <div>Couldn't load options. ${this.error ?? ''}</div>\n <button class=\"svp-retry-btn\" data-retry-button @click=${this._onRetry}>Try again</button>\n </div>\n `;\n }\n if (this.state === 'loading' || !this.payload) {\n return html`\n <div data-loading-skeleton>\n <div class=\"svp-skeleton\"></div>\n <div class=\"svp-skeleton\"></div>\n <div class=\"svp-skeleton\"></div>\n </div>\n `;\n }\n\n const matching = this._findMatchingVariant();\n const ctaPrice = matching?.price ?? null;\n const compareAt = matching?.compare_at_price ?? null;\n\n // Single-SKU products (no variant matrix) come back with zero attributes and\n // one default variant \u2014 e.g. a storefront synthesizes this for products with\n // no options. Rendering the empty attribute list would leave a bare,\n // headerless grid that looks broken, so show a one-line note instead. The\n // CTA is enabled immediately (no selection required) and adds the default.\n const singleVariant = this.payload.attributes.length === 0;\n const heroSrc = this._heroSrc();\n\n // Structure: an [data-image-col] wrapper (the hero image + a quiet Back\n // control below it) and a [data-panel-body] wrapper are SIBLINGS so the\n // velvet canvas can place them into a two-column (image | controls) grid\n // that uses horizontal space in its short vertical band. On a bare surface\n // (chat/PDP) they stack \u2014 image column on top, controls below \u2014 a fine\n // single-column default.\n //\n // The Back control lives at the BOTTOM of the image column (below the\n // picture); the product NAME is the panel title at the TOP of the body.\n // There is no separate top header row (round-2 feedback: free the vertical\n // for the pickers). When there is no hero image we still render the image\n // column so Back stays reachable \u2014 just without the <img>.\n const backBtn = html`<button\n class=\"svp-back-btn\"\n data-back-button\n @click=${this._onBack}\n aria-label=\"Back\"\n >\u2039 Back</button>`;\n return html`\n <div class=\"svp-image-col\" data-image-col>\n ${\n heroSrc\n ? html`<img\n class=\"svp-image\"\n data-variant-image\n src=${heroSrc}\n alt=${this.productImage?.alt ?? ''}\n loading=\"lazy\"\n />`\n : nothing\n }\n </div>\n <div class=\"svp-body\" data-panel-body>\n ${\n this.productTitle\n ? html`<div class=\"svp-product-title\" data-product-title data-critical-text>${this.productTitle}</div>`\n : nothing\n }\n <div class=\"svp-attributes-scroll\" data-attributes-scroll data-clip-ok>\n ${\n singleVariant\n ? html`<div class=\"svp-single-variant-note\" data-single-variant-note>\n Just one option \u2014 ready to add.\n </div>`\n : this.payload.attributes.map((attr) => this._renderAttributeRow(attr))\n }\n </div>\n <div class=\"svp-cta-row\">\n <button\n class=\"svp-cta\"\n data-add-to-cart\n data-critical-text\n ?disabled=${!this._ctaEnabled()}\n @click=${this._onAdd}\n >\n ${\n // Short label: \"Add \u00B7 $64\" (the price commits HERE \u2014 the card's\n // front face carries no price; round-3 feedback shortened this\n // so Back fits beside it on the same row).\n ctaPrice != null\n ? html`Add ${compareAt ? html`<span class=\"svp-compare\">$${compareAt.toFixed(2)}</span>` : nothing}\u00B7 $${ctaPrice.toFixed(2)}`\n : html`Add to cart`\n }\n </button>\n ${backBtn}\n </div>\n <div class=\"svp-live\" data-live data-clip-ok aria-live=\"polite\">\n ${this.productTitle ? `Showing options for ${this.productTitle}` : nothing}\n </div>\n </div>\n `;\n }\n\n private _renderAttributeRow(attr: AttributeDef) {\n const oosVisible = this._hasAnyOOSForAttribute(attr);\n return html`\n <div class=\"svp-attribute-row\" data-attribute-row data-attribute-key=${attr.key}>\n <div class=\"svp-attribute-label\" data-attribute-label id=${`attrlabel-${attr.key}`}>${attr.label}</div>\n ${\n attr.values.length >= VariantPanelLit.COLLAPSE_MIN_OPTIONS\n ? this._renderCollapsedAxis(attr)\n : attr.kind === 'swatch'\n ? this._renderSwatchRow(attr)\n : this._renderPillRow(attr)\n }\n ${oosVisible ? html`<div class=\"svp-oos-microcopy\" data-out-of-stock-microcopy>Out of stock for one or more options</div>` : nothing}\n </div>\n `;\n }\n\n // The connected segmented bar's own CSS contract is a 2-3 option envelope:\n // segments split the row width (flex:1, min-width:0, nowrap) and CLIP rather\n // than wrap. Past the envelope every segment ellipsizes into an unreadable\n // sliver (live report: 6 sizes squished illegible, 2026-07-23). Beyond the\n // cap, fall back to the wrapping pill row \u2014 readable pills across rows.\n static readonly SEGMENTED_MAX_OPTIONS = 3;\n /** Density Tier B: a wrap row past this many options becomes a one-row\n * horizontal scroll track (edge-faded, snap) instead of eating the bounded\n * panel's height with wrapped rows. */\n static readonly SCROLL_MIN_OPTIONS = 9;\n /** Density Tier C: past this many options the axis collapses into a\n * select-style trigger + scrollable listbox. ONLY the offending axis\n * collapses \u2014 sibling axes keep their own tier (per Parker, 2026-07-23). */\n static readonly COLLAPSE_MIN_OPTIONS = 17;\n /** Inside a Tier-C listbox, a type-ahead filter appears past this count. */\n static readonly FILTER_MIN_OPTIONS = 25;\n\n private _isScrollTier(attr: AttributeDef): boolean {\n return (\n attr.values.length >= VariantPanelLit.SCROLL_MIN_OPTIONS &&\n attr.values.length < VariantPanelLit.COLLAPSE_MIN_OPTIONS\n );\n }\n\n private _renderPillRow(attr: AttributeDef) {\n const segmented = attr.values.length <= VariantPanelLit.SEGMENTED_MAX_OPTIONS;\n return html`\n <div class=\"svp-pill-row\" data-segmented=${segmented ? '' : nothing} data-scroll=${this._isScrollTier(attr) ? '' : nothing} role=\"radiogroup\" aria-labelledby=${`attrlabel-${attr.key}`}>\n ${attr.values.map((v) => {\n const disabled = this._isValueDisabled(attr.key, v.id);\n const checked = this._selection[attr.key] === v.id;\n return html`<button\n class=\"svp-pill\"\n data-pill\n data-attribute-key=${attr.key}\n data-value-id=${v.id}\n role=\"radio\"\n aria-checked=${checked ? 'true' : 'false'}\n aria-disabled=${disabled ? 'true' : 'false'}\n ?disabled=${disabled}\n @click=${() => this._onPillClick(attr.key, v.id)}\n >${v.label}</button>`;\n })}\n </div>\n `;\n }\n\n private _renderSwatchRow(attr: AttributeDef) {\n const selectedValueId = this._selection[attr.key];\n const selectedLabel = attr.values.find((v) => v.id === selectedValueId)?.label ?? '';\n return html`\n <div class=\"svp-swatch-label\">${selectedLabel}</div>\n <div class=\"svp-swatch-row\" data-scroll=${this._isScrollTier(attr) ? '' : nothing} role=\"radiogroup\" aria-labelledby=${`attrlabel-${attr.key}`}>\n ${attr.values.map((v) => {\n const disabled = this._isValueDisabled(attr.key, v.id);\n const checked = this._selection[attr.key] === v.id;\n const color = v.swatch?.kind === 'color' ? v.swatch.value : 'transparent';\n return html`<button\n class=\"svp-swatch-pill\"\n data-pill\n data-swatch-dot\n data-attribute-key=${attr.key}\n data-value-id=${v.id}\n role=\"radio\"\n aria-checked=${checked ? 'true' : 'false'}\n aria-disabled=${disabled ? 'true' : 'false'}\n aria-label=${v.label}\n ?disabled=${disabled}\n @click=${() => this._onPillClick(attr.key, v.id)}\n ><span class=\"svp-swatch-dot\" style=\"background-color: ${color}\"></span></button>`;\n })}\n </div>\n `;\n }\n\n private _toggleAxis(key: string): void {\n const next = new Set(this._expandedAxes);\n if (next.has(key)) {\n next.delete(key);\n // Stale-filter trap (adversarial review): a filter left behind makes the\n // NEXT open show 3 of 50 options with no visible cause. Closing clears it.\n const { [key]: _dropped, ...rest } = this._axisFilter;\n this._axisFilter = rest;\n } else {\n next.add(key);\n }\n this._expandedAxes = next;\n }\n\n private _onCollapsedSelect(attrKey: string, valueId: string): void {\n this._onPillClick(attrKey, valueId);\n const next = new Set(this._expandedAxes);\n next.delete(attrKey);\n this._expandedAxes = next;\n const { [attrKey]: _dropped, ...rest } = this._axisFilter;\n this._axisFilter = rest;\n void this.updateComplete.then(() => {\n this.querySelector<HTMLElement>(\n `[data-collapse-trigger][data-attribute-key=\"${attrKey}\"]`\n )?.focus();\n });\n }\n\n private _onAxisFilterInput(attrKey: string, e: Event): void {\n this._axisFilter = { ...this._axisFilter, [attrKey]: (e.target as HTMLInputElement).value };\n }\n\n /** Density Tier C \u2014 select-style trigger + bounded scrollable listbox.\n * ONLY this axis collapses; siblings keep their own tier. Filter appears\n * past FILTER_MIN_OPTIONS. Out-of-stock options stay visible, disabled. */\n private _renderCollapsedAxis(attr: AttributeDef) {\n const expanded = this._expandedAxes.has(attr.key);\n const selected = attr.values.find((v) => v.id === this._selection[attr.key]) ?? null;\n const listId = `svp-collapse-list-${attr.key}`;\n const filter = (this._axisFilter[attr.key] ?? '').trim().toLowerCase();\n const filtered = filter\n ? attr.values.filter((v) => v.label.toLowerCase().includes(filter))\n : attr.values;\n const showFilter = attr.values.length >= VariantPanelLit.FILTER_MIN_OPTIONS;\n const dot = (v: { swatch?: { kind: string; value: string } | null } | null) =>\n v?.swatch?.kind === 'color'\n ? html`<span class=\"svp-swatch-dot\" style=\"background-color: ${v.swatch.value}\"></span>`\n : nothing;\n return html`\n <div\n class=\"svp-collapse\"\n data-collapsed-axis\n data-attribute-key=${attr.key}\n @keydown=${(e: KeyboardEvent) => {\n if (e.key === 'Escape' && expanded) {\n e.stopPropagation();\n this._toggleAxis(attr.key);\n void this.updateComplete.then(() => {\n this.querySelector<HTMLElement>(\n `[data-collapse-trigger][data-attribute-key=\"${attr.key}\"]`\n )?.focus();\n });\n }\n }}\n >\n <button\n class=\"svp-collapse-trigger\"\n data-collapse-trigger\n data-attribute-key=${attr.key}\n aria-haspopup=\"listbox\"\n aria-expanded=${expanded ? 'true' : 'false'}\n aria-controls=${listId}\n @click=${() => this._toggleAxis(attr.key)}\n >\n ${dot(selected)}\n <span class=\"svp-collapse-value\">${selected ? selected.label : `Choose ${attr.label.toLowerCase()}`}</span>\n <span class=\"svp-collapse-caret\">${expanded ? '\\u25b4' : '\\u25be'}</span>\n </button>\n ${\n expanded\n ? html`\n <div\n class=\"svp-collapse-list\"\n data-clip-ok\n id=${listId}\n role=\"listbox\"\n aria-labelledby=${`attrlabel-${attr.key}`}\n >\n ${\n showFilter\n ? html`<input\n class=\"svp-collapse-filter\"\n data-collapse-filter\n type=\"text\"\n placeholder=${`Filter ${attr.label.toLowerCase()}\\u2026`}\n .value=${this._axisFilter[attr.key] ?? ''}\n @input=${(e: Event) => this._onAxisFilterInput(attr.key, e)}\n />`\n : nothing\n }\n ${filtered.map((v) => {\n const disabled = this._isValueDisabled(attr.key, v.id);\n const isSelected = this._selection[attr.key] === v.id;\n return html`<button\n class=\"svp-collapse-option\"\n data-collapse-option\n data-value-id=${v.id}\n role=\"option\"\n aria-selected=${isSelected ? 'true' : 'false'}\n aria-disabled=${disabled ? 'true' : 'false'}\n ?disabled=${disabled}\n @click=${() => this._onCollapsedSelect(attr.key, v.id)}\n >${dot(v)}<span class=\"svp-collapse-value\">${v.label}</span></button>`;\n })}\n ${filtered.length === 0 ? html`<div class=\"svp-collapse-empty\">No matches</div>` : nothing}\n </div>`\n : nothing\n }\n </div>\n `;\n }\n}\n\ncustomElements.define('syntro-variant-panel', VariantPanelLit);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-variant-panel': VariantPanelLit;\n }\n}\n", "import { dispatchDiveDeeper } from '@syntrologie/sdk-contracts';\nimport { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { BindController } from '../bind/controller';\nimport { type ComparisonProps, comparisonSchema } from '../schema';\nimport type { Product } from '../schema-primitives';\nimport { RenderHealthController, type RenderHealthProbe } from './renderHealthReporter';\n\ntype ResolvedMap = Record<string, { image?: string; price?: string }>;\n\nexport class ProductComparisonLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n scrollFade: { state: true },\n };\n\n props: ComparisonProps | undefined = undefined;\n resolved: ResolvedMap = {};\n validationError = false;\n /** True when the matrix body overflows its band and is scrollable \u2014 drives\n * the bottom fade mask so the cut-off row reads as \"more below\". */\n scrollFade = false;\n\n #controller: BindController | undefined;\n #parsed: ComparisonProps | undefined;\n\n constructor() {\n super();\n // Runtime self-detection: emits app:product_comparison:blank / :clipped after\n // paint if the mounted matrix has area but paints no content. Self-registers\n // via host.addController \u2014 no stored reference needed.\n new RenderHealthController(this, () => this.#healthProbe());\n }\n\n /** Measure only the SETTLED matrix (valid, parsed props). An invalid/empty\n * comparison renders nothing and is never \"blank\". NON-PII stateKey. */\n #healthProbe(): RenderHealthProbe | null {\n if (this.validationError || !this.#parsed) return null;\n const root = this.querySelector('.sc-product-comparison');\n if (!root) return null;\n return {\n root,\n stateKey: `cmp|${this.#parsed.products.map((p) => p.id).join(',')}`,\n category: 'product_comparison',\n widgetKind: 'adaptive-product:comparison',\n };\n }\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Anchor `color` on the host so descendants inherit from this\n // widget's theme token, not the host's `body { color }`.\n // See `ProductCardLit.connectedCallback` for the full rationale \u2014\n // canvas tile widgets need this; PDP widgets do not.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n // Make the host a shrinkable flex column so it fills \u2014 and can shrink to \u2014\n // a height-bounded band (velvet's deck-card is a bounded flex column with\n // `overflow:hidden`). `min-height:0` lets flex shrink the host below its\n // content height; the section then scrolls INSIDE that bound instead of\n // being clipped by the card. In a roomy (unbounded) canvas the host takes\n // its content height and nothing scrolls \u2014 same as before.\n this.style.display = 'flex';\n this.style.flexDirection = 'column';\n this.style.minHeight = '0';\n this.style.maxHeight = '100%';\n // On RECONNECT the rendered DOM is preserved and Lit does NOT re-run\n // updated(), so the scroll listener (bound in updated(), torn down in\n // disconnectedCallback) would stay unbound and the cue would freeze. Force\n // one update so updated() rebinds it. Harmless on first connect (an update\n // is already pending).\n this.requestUpdate();\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#startBind();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#scrollListenerEl?.removeEventListener('scroll', this.#onBodyScroll);\n this.#scrollListenerEl = null;\n this.#controller?.destroy();\n }\n\n /** The scroll region we've bound the live scroll listener to (re-bind if the\n * body node is recreated across renders). */\n #scrollListenerEl: HTMLElement | null = null;\n #onBodyScroll = (): void => this.#syncScrollCue();\n\n /**\n * True when there is content BELOW the current scroll position \u2014 drives BOTH\n * the bottom fade mask and the bouncing \"scroll for more\" chevron. Position-\n * aware (not just \"overflows\"), with a few-px slack so a sub-pixel remainder at\n * the true bottom doesn't keep the cue on: the arrow must vanish once you've\n * reached the end, exactly like the velvet deck's own scroll chevron.\n */\n #syncScrollCue(): void {\n const body = this.querySelector('[data-comparison-scroll]') as HTMLElement | null;\n if (!body) return;\n const moreBelow = body.scrollHeight - body.scrollTop - body.clientHeight > 8;\n if (moreBelow !== this.scrollFade) this.scrollFade = moreBelow;\n }\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n this.#parsed = undefined;\n return;\n }\n const result = comparisonSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n this.#parsed = undefined;\n console.warn('[adaptive-product] invalid comparison props:', result.error.issues);\n return;\n }\n this.validationError = false;\n this.#parsed = result.data;\n this.#controller = new BindController(result.data.products, (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.#parsed) return html``;\n const parsed = this.#parsed;\n // The tile body is a flex column: the heading is fixed and the matrix lives\n // in a vertically-SCROLLABLE region. When a height-bounded canvas (velvet's\n // deck) constrains the band and the rows exceed it, the body scrolls\n // (`overflow-y:auto`) instead of clipping \u2014 never a cut-off, unreachable\n // row. `data-clip-ok` tells the clip battery this container's own overflow\n // is intentional (a scroll region, not a hidden-content bug); the matrix\n // still guarantees the HORIZONTAL fit (min-width:0 columns). A bottom fade\n // mask (only while overflowing) signals there is more below.\n const bodyStyle =\n // touch-action:pan-y is REQUIRED for the deck swipe-to-cycle to work over a\n // comparison card. This body is a NESTED scroll container (overflow-y:auto)\n // inside the deck card's sc-mount; a scroll container arbitrates touch\n // gestures by its OWN touch-action, and the default `auto` lets the browser\n // claim horizontal pans here \u2014 so a horizontal swipe fires pointercancel and\n // the deck never cycles (the exact BUG-1784140378 failure, one level deeper:\n // that fix put pan-y on the sc-mount HOST, but this inner region overrides\n // it for gestures that start inside it). pan-y keeps vertical scroll of the\n // rows and hands horizontal drags back to the deck's swipe handler.\n 'flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;touch-action:pan-y;' +\n 'scrollbar-width:none;-ms-overflow-style:none;overscroll-behavior:contain;' +\n (this.scrollFade\n ? 'mask-image:linear-gradient(to bottom,#000 calc(100% - 18px),transparent);' +\n '-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 18px),transparent);'\n : '');\n return html`\n <section\n class=\"sc-product-comparison\"\n style=\"display:flex;flex-direction:column;flex:1 1 auto;padding:2px var(--sc-comparison-inset,12px) var(--sc-comparison-inset,12px);box-sizing:border-box;max-width:100%;min-width:0;min-height:0;max-height:100%;overflow:hidden;\"\n >\n <style>\n .sc-product-comparison__scroll-cue {\n position: absolute;\n left: 50%;\n bottom: 5px;\n width: 7px;\n height: 7px;\n transform: translateX(-50%) rotate(45deg);\n border-right: 2px solid currentColor;\n border-bottom: 2px solid currentColor;\n border-bottom-right-radius: 1px;\n opacity: 0.5;\n pointer-events: none;\n animation: sc-cmp-scroll-cue 1.5s ease-in-out infinite;\n }\n @keyframes sc-cmp-scroll-cue {\n 0%,\n 100% {\n transform: translateX(-50%) translateY(0) rotate(45deg);\n }\n 50% {\n transform: translateX(-50%) translateY(3px) rotate(45deg);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n .sc-product-comparison__scroll-cue {\n animation: none;\n }\n }\n </style>\n ${\n parsed.heading\n ? html`<h2\n class=\"sc-product-comparison__heading\"\n data-comparison-heading\n style=\"flex:0 0 auto;margin:0 0 4px;font:600 14px/1.3 var(--sc-font-family,inherit);color:var(--sc-tile-title-color,inherit);min-width:0;overflow-wrap:break-word;\"\n >${parsed.heading}</h2>`\n : nothing\n }\n <div\n class=\"sc-product-comparison__scroll-wrap\"\n style=\"position:relative;display:flex;flex-direction:column;flex:1 1 auto;min-height:0;\"\n >\n <div\n class=\"sc-product-comparison__body\"\n data-comparison-scroll\n data-clip-ok\n style=${bodyStyle}\n >\n ${this.#renderMatrix(parsed.products, this.#visibleRows(parsed))}\n </div>\n ${\n // A bouncing down-chevron pinned to the bottom of the scroll region \u2014\n // the same \"scroll for more\" cue as the velvet deck card, but for the\n // comparison card's OWN inner scroll (the deck-level chevron never\n // fires here because the widget scrolls inside itself, so the card\n // never overflows the deck). currentColor = the card's text token, so\n // it's in-theme; pointer-events:none so it never blocks scroll/taps;\n // gated on scrollFade (content below the current scroll) so it hides\n // at the end. Bounce is dropped under prefers-reduced-motion (style).\n this.scrollFade\n ? html`<span class=\"sc-product-comparison__scroll-cue\" aria-hidden=\"true\"></span>`\n : nothing\n }\n </div>\n ${this.#renderDiveDeeper(parsed)}\n </section>\n `;\n }\n\n /**\n * \"Dive deeper\" affordance (items 4/5). A comparison tile shows a capped,\n * short-blip matrix; rather than expecting the user to scroll a cramped tile\n * for depth, the chip hands the comparison's context back to the CHAT as a\n * deep-dive turn. Pinned bottom-right (flex:0 0 auto, below the scroll body)\n * so it reads as \"in a deck: beside the pager / inline: bottom-right of the\n * tile\". Canvas-agnostic: it dispatches the neutral DIVE_DEEPER_EVENT; the\n * chat-bar mountable turns it into a user turn.\n */\n #renderDiveDeeper(parsed: ComparisonProps): TemplateResult | typeof nothing {\n if (parsed.products.length < 2) return nothing;\n const chipStyle =\n 'flex:0 0 auto;align-self:flex-end;margin-top:6px;padding:4px 10px;border-radius:9999px;' +\n 'border:1px solid var(--sc-color-border,rgba(0,0,0,0.14));background:transparent;cursor:pointer;' +\n 'font:600 12px/1.2 var(--sc-font-family,inherit);color:var(--sc-color-primary,#3d8a5e);' +\n 'white-space:nowrap;';\n return html`<button\n type=\"button\"\n class=\"sc-product-comparison__dive-deeper\"\n data-dive-deeper\n style=${chipStyle}\n @click=${() => this.#onDiveDeeper(parsed)}\n >Dive deeper \u203A</button>`;\n }\n\n #onDiveDeeper(parsed: ComparisonProps): void {\n const names = parsed.products.map((p) => p.name);\n const dims = this.#visibleRows(parsed).map((r) => r.label);\n const title = names.join(' vs ');\n // State an UNMET NEED \u2014 never ask a question (BUG-1784153527).\n //\n // This used to be `Tell me more \u2014 how do ${title} compare?`: a direct\n // imperative in the visitor's own voice. The agent dutifully answered it\n // and never asked what was actually missing \u2014 which is catastrophic when\n // the comparison itself is the problem (live: a \"VelaLift Sports Bra vs\n // RunCloud Quarter Sock Pack\" card, where the agent had to talk its way\n // out with \"the real question isn't which is better...\").\n //\n // A question demands an answer; a complaint invites a question. The tap\n // means \"this tile didn't land\", so that is what the turn says. The\n // product names ride in `title` + the turn-origin envelope \u2014 putting them\n // in the visitor's mouth is what made the turn sound answerable.\n const prompt = \"I'd like to dive deeper on this comparison, I didn't see what I needed.\";\n dispatchDiveDeeper({\n prompt,\n title,\n context: dims.length > 0 ? `Compared on: ${dims.join(', ')}` : undefined,\n });\n }\n\n override updated(changed: Map<string, unknown>): void {\n super.updated(changed);\n // Bind a live scroll listener so the cue (fade + arrow) tracks the scroll\n // position \u2014 hides the moment you reach the bottom, reappears on scroll up.\n // The body div is stable across most re-renders, but rebind if Lit recreates\n // it. Runs after every render (band height is a canvas/layout decision that\n // can change without a `props` change).\n const body = this.querySelector('[data-comparison-scroll]') as HTMLElement | null;\n if (!body) return;\n if (this.#scrollListenerEl !== body) {\n this.#scrollListenerEl?.removeEventListener('scroll', this.#onBodyScroll);\n body.addEventListener('scroll', this.#onBodyScroll, { passive: true });\n this.#scrollListenerEl = body;\n }\n this.#syncScrollCue();\n }\n\n /**\n * The comparison rows to show. The adaptive owns the DATA (all rows, authored or\n * synthesized from attributes); the CANVAS owns the LAYOUT \u2014 including how many\n * rows fit. A vertically-tight canvas (velvet's height-bounded deck) caps the\n * count by setting `--sc-comparison-max-fields`; unset = show every row (roomy\n * canvases / PDP). We render the first N so the assistant's most important\n * dimensions (authored/attribute order) survive the cut.\n */\n #visibleRows(parsed: ComparisonProps): {\n label: string;\n valueA: string;\n valueB: string;\n valueC?: string;\n valueD?: string;\n }[] {\n // Row-integrity guard (mainpeptides filler-cell incident): an AUTHORED row\n // must carry a non-blank value for every MOUNTED product column \u2014 a blank\n // cell is a visitor-facing \"we don't know\" in a tile that exists to answer\n // exactly that question, so the whole row is dropped. Cells beyond\n // products.length (valueC/valueD on a 2-product mount) are never rendered\n // and never gate the row. Synthesized rows keep their explicit \"\u2014\"\n // alignment \u2014 they are catalog-derived, not LLM-authored.\n const all =\n parsed.rows && parsed.rows.length > 0\n ? parsed.rows.filter((row) => this.#rowComplete(row, parsed.products.length))\n : this.#synthesizeRows(parsed.products);\n const max = this.#maxFields();\n return max == null ? all : all.slice(0, max);\n }\n\n /** True when the row has a non-empty (post-trim) cell for every mounted\n * product column \u2014 the first `productCount` of valueA..valueD. */\n #rowComplete(\n row: { valueA: string; valueB: string; valueC?: string; valueD?: string },\n productCount: number\n ): boolean {\n const cells = [row.valueA, row.valueB, row.valueC, row.valueD].slice(0, productCount);\n return cells.every((cell) => (cell ?? '').trim().length > 0);\n }\n\n /** Canvas-set cap on comparison rows (`--sc-comparison-max-fields`), or null for\n * no cap. Read from computed style so it's the canvas's layout decision, not a\n * data/LLM field. */\n #maxFields(): number | null {\n const raw = getComputedStyle(this).getPropertyValue('--sc-comparison-max-fields').trim();\n if (!raw) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) && n > 0 ? n : null;\n }\n\n /**\n * Build comparison rows from the products' own `attributes` when the assistant\n * didn't author explicit `rows`. A comparison is ALWAYS a side-by-side matrix \u2014\n * the visitor asked to compare, so the comparison (not stacked product cards)\n * is what shows. We take the ordered union of attribute labels across products\n * and align each product's value under it (missing \u2192 \"\u2014\").\n */\n #synthesizeRows(products: readonly Product[]): {\n label: string;\n valueA: string;\n valueB: string;\n valueC?: string;\n valueD?: string;\n }[] {\n const labels: string[] = [];\n for (const p of products) {\n for (const a of p.attributes ?? []) {\n if (!labels.includes(a.label)) labels.push(a.label);\n }\n }\n return labels.map((label) => {\n const [a, b, c, d] = products.map(\n (p) => (p.attributes ?? []).find((x) => x.label === label)?.value ?? '\u2014'\n );\n return { label, valueA: a ?? '\u2014', valueB: b ?? '\u2014', valueC: c, valueD: d };\n });\n }\n\n /**\n * Side-by-side matrix: products are columns, `rows` are dimensions. Used when\n * the assistant authored `rows` to answer the visitor's question. `values[i]`\n * is the cell for `products[i]`; a missing/absent value renders as \"\u2014\".\n */\n #renderMatrix(\n products: readonly Product[],\n rows: readonly {\n label: string;\n valueA: string;\n valueB: string;\n valueC?: string;\n valueD?: string;\n }[]\n ): TemplateResult {\n // `minmax(0, 1fr)` \u2014 NOT `1fr` \u2014 so product columns can shrink below their\n // content width and share the card evenly. A bare `1fr` resolves to\n // `minmax(auto, 1fr)`, whose `auto` min is the content's min-content size, so\n // 3\u20134 columns force the grid wider than the card and it spills off the edge.\n // `min-width:0` + `overflow-wrap:anywhere` on cells lets long values wrap\n // rather than push the column out. The label column stays narrow.\n const cols = `minmax(62px, 0.55fr) repeat(${products.length}, minmax(0, 1fr))`;\n // No external stylesheet targets these classes (the widget is inline-styled),\n // so the matrix carries its own layout. Theme colors come through inherited\n // `--sc-tile-*` vars / currentColor; structure is inline here.\n // Column header (round-3 feedback): ALWAYS a HORIZONTAL mini-row \u2014 rounded\n // image on the LEFT, name + price stacked to its right \u2014 at EVERY product\n // count. The tile has horizontal space, not vertical, so the old \u22653-product\n // \"stacked\" fallback (image ABOVE the name) is gone. At 3+ products each\n // column is narrow, so the header goes COMPACT: smaller thumb (20px) and\n // smaller name type (0.7rem); names still wrap at word boundaries \u2014 never\n // ellipsized (Tier-1 identity text) \u2014 and min-width:0 everywhere preserves\n // the matrix HORIZONTAL fit guarantee (columns shrink, text wraps inside).\n const compactHeaders = products.length >= 3;\n // RECO-CARD column header (user-approved direction, 2026-07-08): the\n // header IS a large image card \u2014 the reco carousel's visual vocabulary\n // (full-bleed image, bottom gradient scrim, text overlaid on the scrim)\n // plus the product card's corner pills. Replaces the 20-24px thumb\n // mini-row and the intermediate banner/split iterations.\n const colHeadStyle =\n 'display:block;padding:6px 3px;text-align:left;font-weight:600;min-width:0;';\n const labelStyle =\n 'padding:8px 4px 8px 2px;font-weight:600;font-size:0.75rem;opacity:0.75;border-top:1px solid var(--sc-tile-border-color,rgba(0,0,0,0.08));align-self:stretch;min-width:0;overflow-wrap:break-word;hyphens:auto;';\n const cellStyle =\n 'padding:8px 6px;border-top:1px solid var(--sc-tile-border-color,rgba(0,0,0,0.08));align-self:stretch;min-width:0;overflow-wrap:anywhere;';\n return html`\n <div\n class=\"sc-product-comparison__matrix\"\n data-comparison-matrix\n role=\"table\"\n style=${`display:grid;grid-template-columns:${cols};column-gap:4px;font-size:0.8rem;line-height:1.3;max-width:100%;`}\n >\n <div role=\"row\" style=\"display:contents;\">\n <!-- The top-left corner cell is intentionally BLANK visually \u2014 the row\n labels below make it self-evident these are product attributes, so\n the header just spent vertical space restating the obvious (user\n feedback). Kept as a grid cell so the product columns still align,\n and the accessible name is preserved via aria-label for the a11y\n table semantics (composed-accessibility work, 2026-07-12). -->\n <span\n class=\"sc-product-comparison__matrix-corner\"\n role=\"columnheader\"\n aria-label=\"Product attributes\"\n style=\"padding:6px 3px;font-weight:600;font-size:0.7rem;line-height:1.2;overflow-wrap:anywhere;\"\n ><span\n style=\"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;\"\n >Product attributes</span\n ></span\n >\n ${products.map((p) => {\n const resolved = this.resolved[p.id];\n const imgSrc = resolved?.image ?? p.image.src;\n const priceText = resolved?.price ?? p.price?.amount;\n // The product NAME is the link to its personalized PDP \u2014 there is no\n // separate \"View\" CTA (it cost a whole row of vertical space, which\n // canvases like velvet can't spare). The target is the first CTA that\n // NAVIGATES (carries an href) \u2014 NOT `ctas[0]`, which in the standard\n // product CTA set is \"Add to cart\" (an action with no href); the PDP\n // link is a later CTA (e.g. \"Product Page\"). If none navigates, the\n // name is plain text rather than a dead link.\n const navCta = p.ctas.find((c) => c.href);\n const pdpHref = navCta?.href;\n // RECO-CARD header: corner pills (rating upper-left at 2 products;\n // price upper-right always) + NAME overlaid along the bottom on the\n // reco gradient scrim (white + text-shadow, wraps, never\n // ellipsized). The whole card is the PDP link when one exists \u2014\n // same tap semantics as a reco carousel item.\n const pillBase =\n 'position:absolute;top:4px;display:inline-flex;align-items:center;' +\n 'background:rgba(17,17,17,0.66);color:#fff;border-radius:9999px;' +\n `padding:0.12rem 0.36rem;font-size:${compactHeaders ? '0.56rem' : '0.64rem'};` +\n 'line-height:1.2;font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,0.25);' +\n 'max-width:calc(100% - 8px);min-width:0;box-sizing:border-box;' +\n 'white-space:normal;overflow-wrap:anywhere;';\n const showRatingPill = !compactHeaders && p.rating;\n const nameOverlayStyle = compactHeaders\n ? 'display:block;font-weight:600;font-size:0.66rem;line-height:1.2;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.55);white-space:normal;overflow-wrap:break-word;min-width:0;'\n : 'display:block;font-weight:600;font-size:0.76rem;line-height:1.25;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.55);white-space:normal;overflow-wrap:break-word;min-width:0;';\n // 19/10 (\u22481.9) default = ~30% shorter than the original 4/3 card at\n // the same width. The 8/3 interim (2026-07-09) halved the height,\n // which read as too aggressive; ~30% balances the header image\n // against the spec matrix (2026-07-12 user direction). Still\n // overridable per-workspace via the --sc-comparison-image-aspect token.\n const mediaStyle =\n 'position:relative;display:block;width:100%;aspect-ratio:var(--sc-comparison-image-aspect, 19 / 10);' +\n 'border-radius:10px;overflow:hidden;background:#e5e7eb;text-decoration:none;';\n const cardInner = html`\n <img\n class=\"sc-product-comparison__thumb\"\n src=${imgSrc}\n alt=${p.image.alt}\n loading=\"lazy\"\n style=\"position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\"\n />\n <span aria-hidden=\"true\" style=\"position:absolute;inset:auto 0 0 0;height:62%;background:linear-gradient(transparent,rgba(0,0,0,.62));\"></span>\n ${\n showRatingPill && p.rating\n ? html`<span\n class=\"sc-product-comparison__rating-pill\"\n data-product-rating\n data-rating-value=${p.rating.value}\n aria-label=${`${p.rating.value} out of ${p.rating.max ?? 5}, ${p.rating.count} reviews`}\n style=${`${pillBase}left:4px;`}\n ><span style=\"color:#f5a623;\">\u2605</span><span style=\"margin-left:0.12rem;\">${p.rating.value}</span></span\n >`\n : nothing\n }\n ${\n priceText\n ? html`<span\n class=\"sc-product-comparison__price\"\n data-critical-text\n style=${`${pillBase}right:4px;`}\n >${priceText}</span\n >`\n : nothing\n }\n <span\n class=\"sc-product-comparison__name\"\n data-critical-text\n style=${`position:absolute;left:8px;right:8px;bottom:6px;${nameOverlayStyle}`}\n >${p.name}</span\n >`;\n return html`<div\n class=\"sc-product-comparison__matrix-col\"\n role=\"columnheader\"\n data-product-id=${p.id}\n style=${colHeadStyle}\n >\n ${\n pdpHref\n ? html`<a\n class=\"sc-product-comparison__media\"\n data-comparison-media\n data-product-cta=\"primary\"\n data-product-name-link\n href=${pdpHref}\n target=${navCta?.target ?? '_self'}\n rel=${navCta?.target === '_blank' ? 'noopener noreferrer' : ''}\n data-action-id=${navCta?.actionId ?? ''}\n data-variant=${navCta?.variant ?? ''}\n style=${mediaStyle}\n >${cardInner}</a>`\n : html`<div class=\"sc-product-comparison__media\" data-comparison-media style=${mediaStyle}>${cardInner}</div>`\n }\n </div>`;\n })}\n </div>\n ${rows.map((row) => {\n const cells = [row.valueA, row.valueB, row.valueC, row.valueD];\n return html`\n <div role=\"row\" style=\"display:contents;\">\n <span class=\"sc-product-comparison__matrix-label\" role=\"rowheader\" style=${labelStyle}\n >${row.label}</span\n >\n ${products.map(\n (_p, i) =>\n html`<span class=\"sc-product-comparison__matrix-cell\" role=\"cell\" style=${cellStyle}\n >${cells[i] ?? '\u2014'}</span\n >`\n )}\n </div>\n `;\n })}\n </div>\n `;\n }\n}\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { BindController } from '../bind/controller';\nimport { type GridProps, gridSchema } from '../schema';\nimport { renderProductCard } from './ProductCardLit';\n\nexport class ProductGridLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n };\n\n props: GridProps | undefined = undefined;\n resolved: Record<string, { image?: string; price?: string }> = {};\n validationError = false;\n\n #controller: BindController | undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Anchor `color` on the host so descendants inherit from this\n // widget's theme token, not the host's `body { color }`.\n // See `ProductCardLit.connectedCallback` for the full rationale \u2014\n // canvas tile widgets need this; PDP widgets do not.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#startBind();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#controller?.destroy();\n }\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n return;\n }\n const result = gridSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n console.warn('[adaptive-product] invalid grid props:', result.error.issues);\n return;\n }\n this.validationError = false;\n this.#controller = new BindController(result.data.products, (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.props) return html``;\n const parsed = gridSchema.safeParse(this.props);\n if (!parsed.success) return html``;\n const { products, desktopColumns, heading } = parsed.data;\n return html`\n <section class=\"sc-product-grid\">\n ${\n heading\n ? html`<h2 data-grid-heading class=\"sc-product-grid__heading\">${heading}</h2>`\n : nothing\n }\n <div\n data-grid-root\n class=\"sc-product-grid__cells\"\n style=${`--sc-product-grid-cols:${desktopColumns}`}\n >\n ${products.map(\n (p) => html`<div data-grid-cell class=\"sc-product-grid__cell\">\n ${renderProductCard(p, 'standard', this.resolved[p.id])}\n </div>`\n )}\n </div>\n </section>\n `;\n }\n}\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { BindController } from '../bind/controller';\nimport { sanitizeHtml } from '../sanitizer';\nimport { type HeroProps, heroSchema } from '../schema';\n\nexport class ProductHeroLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n };\n\n props: HeroProps | undefined = undefined;\n resolved: Record<string, { image?: string; price?: string }> = {};\n validationError = false;\n\n #controller: BindController | undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Anchor `color` on the host so descendants inherit from this\n // widget's theme token, not the host's `body { color }`.\n // See `ProductCardLit.connectedCallback` for the full rationale \u2014\n // canvas tile widgets need this; PDP widgets do not.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#startBind();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#controller?.destroy();\n }\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n return;\n }\n const result = heroSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n console.warn('[adaptive-product] invalid hero props:', result.error.issues);\n return;\n }\n this.validationError = false;\n this.#controller = new BindController([result.data.product], (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.props) return html``;\n const parsed = heroSchema.safeParse(this.props);\n if (!parsed.success) return html``;\n const { product, layout, longCopy } = parsed.data;\n const resolved = this.resolved[product.id];\n const imgSrc = resolved?.image ?? product.image.src;\n const priceText = resolved?.price ?? product.price?.amount;\n // Constrain image to thumbnail size \u2014 hero is \"marketing-scale\" by\n // longCopy + framing weight, not by image size. Default thumb sits\n // beside the content (image-left / image-right) or as a slim banner\n // strip on top (image-top).\n const isTopLayout = layout === 'image-top';\n const sectionStyles = isTopLayout\n ? 'display:block;'\n : `display:flex;flex-direction:${layout === 'image-right' ? 'row-reverse' : 'row'};gap:1rem;align-items:flex-start;`;\n const imageStyles = isTopLayout\n ? 'width:100%;max-height:160px;object-fit:cover;border-radius:8px;display:block;margin-bottom:0.75rem;'\n : 'width:128px;height:128px;object-fit:cover;border-radius:8px;flex-shrink:0;';\n const contentStyles = isTopLayout ? '' : 'flex:1;min-width:0;';\n return html`\n <section class=\"sc-product-hero\" data-hero-layout=${layout} style=${sectionStyles}>\n <img\n class=\"sc-product-hero__image\"\n src=${imgSrc}\n alt=${product.image.alt}\n loading=\"lazy\"\n style=${imageStyles}\n />\n <div class=\"sc-product-hero__content\" style=${contentStyles}>\n <h2 class=\"sc-product-hero__name\">${product.name}</h2>\n ${\n product.tagline\n ? html`<p class=\"sc-product-hero__tagline\">${product.tagline}</p>`\n : nothing\n }\n ${\n product.price\n ? html`<p class=\"sc-product-hero__price\">\n ${priceText}${\n product.price.cadence ? html` <small>${product.price.cadence}</small>` : nothing\n }\n </p>`\n : nothing\n }\n ${\n longCopy\n ? html`<div\n data-hero-longcopy\n class=\"sc-product-hero__longcopy\"\n .innerHTML=${sanitizeHtml(longCopy)}\n ></div>`\n : nothing\n }\n <div class=\"sc-product-hero__ctas\">\n ${product.ctas.map(\n (c, i) => html`<a\n class=\"sc-product-hero__cta\"\n data-product-cta=${i === 0 ? 'primary' : 'secondary'}\n data-variant=${c.variant}\n href=${c.href ?? '#'}\n target=${c.target}\n rel=${c.target === '_blank' ? 'noopener noreferrer' : ''}\n >${c.label}</a\n >`\n )}\n </div>\n </div>\n </section>\n `;\n }\n}\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport { type RecoProps, recoSchema } from '../schema';\nimport { RenderHealthController, type RenderHealthProbe } from './renderHealthReporter';\n\nexport class ProductRecoCarouselLit extends LitElement {\n static override properties = { props: { attribute: false }, _parsed: { state: true } };\n static readonly _testing = {\n healthProbe: (host: ProductRecoCarouselLit): RenderHealthProbe | null => host.#healthProbe(),\n };\n declare props: RecoProps | undefined;\n private declare _parsed: RecoProps | undefined;\n constructor() {\n super();\n this.props = undefined;\n this._parsed = undefined;\n // Runtime self-detection: emits app:product_reco:blank / :clipped after paint\n // if the mounted carousel has area but paints no content (the reco-blank bug).\n // Self-registers via host.addController \u2014 no stored reference needed.\n new RenderHealthController(this, () => this.#healthProbe());\n }\n\n /** Measure only the SETTLED carousel (items rendered). An empty carousel\n * renders nothing and is never \"blank\". NON-PII stateKey. */\n #healthProbe(): RenderHealthProbe | null {\n const items = this._parsed?.items ?? [];\n if (items.length === 0) return null;\n const root = this.querySelector('.sc-reco');\n if (!root) return null;\n return {\n root,\n stateKey: `reco|${items.length}|${items[0]?.pdp_href ?? ''}`,\n category: 'product_reco',\n widgetKind: 'adaptive-product:reco',\n };\n }\n override createRenderRoot() {\n return this;\n }\n override willUpdate(changed: Map<string, unknown>) {\n if (changed.has('props')) {\n const r = this.props ? recoSchema.safeParse(this.props) : undefined;\n this._parsed = r?.success ? r.data : undefined;\n }\n }\n override render(): TemplateResult | typeof nothing {\n const items = this._parsed?.items ?? [];\n if (items.length === 0) return html``;\n return html`\n <section class=\"sc-reco\" style=\"box-sizing:border-box;display:flex;flex-direction:column;height:100%;padding:2px var(--sc-reco-inset,12px) var(--sc-reco-inset,12px);\">\n <div class=\"sc-reco__carousel\" role=\"list\" style=\"display:flex;align-items:stretch;gap:8px;flex:1 1 auto;min-height:0;overflow-x:auto;scroll-snap-type:x mandatory;\">\n ${items.map(\n (it) => html`\n <div role=\"listitem\" style=\"display:contents;\">\n <a data-reco-item data-product-cta=\"primary\" href=${it.pdp_href}\n style=${styleMap({ position: 'relative', flex: '0 0 auto', aspectRatio: '4 / 5', minHeight: 'var(--sc-reco-item-min-h, 162px)', maxHeight: 'var(--sc-reco-item-max-h, 340px)', borderRadius: '12px', overflow: 'hidden', scrollSnapAlign: 'start', textDecoration: 'none', display: 'block', background: '#e5e7eb' })}>\n ${it.image_url ? html`<img src=${it.image_url} alt=${it.hook ?? 'Recommended product'} loading=\"lazy\" style=\"position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\">` : nothing}\n <span style=\"position:absolute;inset:auto 0 0 0;height:60%;background:linear-gradient(transparent,rgba(0,0,0,.62));\"></span>\n <span style=\"position:absolute;left:8px;right:8px;bottom:8px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);\">\n ${it.price ? html`<span style=\"display:block;font-weight:700;font-size:0.9rem;\">${it.price}</span>` : nothing}\n ${it.hook ? html`<span style=\"display:block;font-size:0.72rem;line-height:1.2;opacity:.95;\">${it.hook}</span>` : nothing}\n </span>\n </a>\n </div>`\n )}\n </div>\n </section>`;\n }\n}\nif (!customElements.get('syntro-product-reco-carousel'))\n customElements.define('syntro-product-reco-carousel', ProductRecoCarouselLit);\n", "import { html, LitElement } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\ninterface TrendingNewsItem {\n headline: string;\n source: string;\n published_relative: string;\n blurb: string;\n /** Optional click-through URL to the source article. When present the\n * widget renders the headline as a link opening in a new tab with\n * `rel=\"noopener noreferrer\"`. Sub-agent-generated content omits this\n * field; curated seed content includes a real URL. */\n url?: string | null;\n}\n\ninterface TrendingNewsData {\n items: TrendingNewsItem[];\n rationale: string;\n}\n\n/**\n * Inject the trending-news widget's stylesheet once per document.\n * Idempotent \u2014 safe to call from connectedCallback on every instance.\n */\nfunction ensureTrendingNewsStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-trending-news-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-trending-news-style';\n style.textContent = TRENDING_NEWS_CSS;\n document.head.appendChild(style);\n}\n\nconst TRENDING_NEWS_CSS = `\n.pdp-trending-news {\n --tn-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --tn-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --tn-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --tn-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --tn-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --tn-accent: var(--syntro-pdp-accent, hsl(95 18% 48%));\n --tn-display: var(--syntro-pdp-display, inherit);\n --tn-sans: var(--syntro-pdp-sans, inherit);\n\n display: grid;\n grid-template-columns: 1fr;\n gap: 18px;\n font-family: var(--tn-sans);\n color: var(--tn-fg);\n}\n\n@media (min-width: 900px) {\n .pdp-trending-news {\n grid-template-columns: 1.4fr 1fr;\n gap: 24px;\n grid-auto-rows: minmax(0, auto);\n }\n}\n\n/* Featured card \u2014 first item, spans both rows on desktop */\n.pdp-tn-card {\n position: relative;\n /* Card surface derives from the theme card-bg + primary tokens so it tracks\n the host theme (dark on healthmaxxer, light on vela) instead of a\n hardcoded dark base that rendered dark-on-dark under a light-host fg. */\n background:\n radial-gradient(\n 120% 80% at 0% 0%,\n color-mix(in srgb, var(--tn-primary) 7%, transparent) 0%,\n transparent 55%\n ),\n linear-gradient(180deg, color-mix(in srgb, var(--tn-bg) 94%, white), var(--tn-bg));\n border: 1px solid var(--tn-border);\n border-radius: 18px;\n padding: 22px 22px 22px 24px;\n overflow: hidden;\n transition:\n transform 200ms ease,\n border-color 200ms ease,\n box-shadow 200ms ease;\n /* Resting state VISIBLE (GPU/compositing hazard fix): cards default to\n opacity 1; the stagger is re-armed only under .pdp-trending-news.is-entering\n so a suppressed animation can never leave the trending grid blank. */\n opacity: 1;\n}\n.pdp-trending-news.is-entering .pdp-tn-card {\n opacity: 0;\n transform: translateY(18px);\n animation: pdp-tn-fade-up 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n}\n.pdp-tn-card:hover {\n border-color: color-mix(in srgb, var(--tn-primary) 55%, transparent);\n box-shadow: 0 12px 50px -16px color-mix(in srgb, var(--tn-primary) 25%, transparent);\n transform: translateY(-2px);\n}\n\n/* stagger arrival */\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(1) {\n animation-delay: 60ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(2) {\n animation-delay: 160ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(3) {\n animation-delay: 260ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(4) {\n animation-delay: 360ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(5) {\n animation-delay: 460ms;\n}\n\n.pdp-tn-card.is-featured {\n grid-row: span 2;\n padding: 28px 28px 28px 30px;\n background:\n radial-gradient(\n 130% 100% at 0% 0%,\n color-mix(in srgb, var(--tn-primary) 13%, transparent) 0%,\n transparent 55%\n ),\n radial-gradient(\n 90% 70% at 100% 100%,\n color-mix(in srgb, var(--tn-accent) 10%, transparent) 0%,\n transparent 60%\n ),\n linear-gradient(180deg, color-mix(in srgb, var(--tn-bg) 92%, white), var(--tn-bg));\n}\n\n.pdp-tn-meta {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 11px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n}\n.pdp-tn-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--tn-primary);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--tn-primary) 18%, transparent);\n animation: pdp-tn-pulse 2400ms ease-in-out infinite;\n flex: 0 0 auto;\n}\n.pdp-tn-meta-source {\n color: var(--tn-fg);\n font-weight: 600;\n letter-spacing: 0.08em;\n}\n.pdp-tn-meta-sep {\n opacity: 0.4;\n}\n\n.pdp-tn-headline {\n font-family: var(--tn-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 60;\n margin: 14px 0 10px;\n line-height: 1.12;\n letter-spacing: -0.012em;\n color: var(--tn-fg);\n font-size: clamp(19px, 2.6vw, 22px);\n font-feature-settings: 'liga' 1, 'ss01' 1;\n /* Avoid mid-word breaks at narrow widths. */\n overflow-wrap: break-word;\n hyphens: none;\n}\n.pdp-tn-card.is-featured .pdp-tn-headline {\n font-size: clamp(24px, 4.4vw, 34px);\n font-weight: 400;\n margin: 18px 0 14px;\n font-variation-settings: 'SOFT' 90, 'WONK' 1;\n}\n\n.pdp-tn-blurb {\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--tn-fg-muted);\n margin: 0;\n max-width: 56ch;\n /* Clamp to 4 lines with ellipsis when the blurb is longer than the\n card can comfortably hold. Schema allows up to ~280 chars; this\n keeps narrow viewports + the 5-card grid visually balanced\n regardless of source content length. */\n display: -webkit-box;\n -webkit-line-clamp: 4;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.pdp-tn-card.is-featured .pdp-tn-blurb {\n font-size: 16px;\n line-height: 1.55;\n max-width: 50ch;\n /* Featured card has more vertical room \u2014 allow one more line. */\n -webkit-line-clamp: 5;\n}\n\n@media (max-width: 640px) {\n .pdp-tn-card,\n .pdp-tn-card.is-featured {\n padding: 18px 18px 20px;\n border-radius: 16px;\n }\n .pdp-tn-meta {\n font-size: 10px;\n }\n .pdp-tn-blurb {\n font-size: 13.5px;\n }\n .pdp-tn-card.is-featured .pdp-tn-blurb {\n font-size: 14.5px;\n }\n}\n\n/* Click-through link wrapper on the headline. We use a bare <a>\n inheriting type styles so the headline visual stays identical when\n a URL is or isn't present \u2014 the only affordance is a subtle hover\n underline + \\\\2197 (north-east arrow) glyph indicating an external destination. */\n.pdp-tn-headline-link {\n color: inherit;\n text-decoration: none;\n background-image: linear-gradient(currentColor, currentColor);\n background-position: 0 100%;\n background-repeat: no-repeat;\n background-size: 0 1px;\n transition: background-size 220ms ease;\n}\n.pdp-tn-headline-link:hover,\n.pdp-tn-headline-link:focus-visible {\n background-size: 100% 1px;\n outline: none;\n}\n.pdp-tn-headline-link::after {\n content: ' \\\\2197';\n font-family: var(--tn-sans);\n font-size: 0.65em;\n letter-spacing: 0;\n color: var(--tn-primary);\n opacity: 0.55;\n vertical-align: 0.18em;\n margin-left: 0.2em;\n transition: opacity 220ms ease;\n}\n.pdp-tn-headline-link:hover::after,\n.pdp-tn-headline-link:focus-visible::after {\n opacity: 1;\n}\n\n.pdp-tn-corner-mark {\n position: absolute;\n top: 14px;\n right: 16px;\n font-family: var(--tn-display);\n font-style: italic;\n font-size: 11px;\n letter-spacing: 0.06em;\n color: color-mix(in srgb, var(--tn-primary) 70%, transparent);\n padding: 3px 9px 3px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--tn-primary) 12%, transparent);\n pointer-events: none;\n}\n.pdp-tn-card.is-featured .pdp-tn-corner-mark::before {\n /* \\\\2726 = four-pointed star, CSS-escaped like \\\\2197 above \u2014 the raw\n glyph shipped double-encoded (garbled \"featured\" badge on the live modal). */\n content: '\\\\2726 ';\n font-style: normal;\n}\n\n@media (max-width: 640px) {\n .pdp-tn-corner-mark {\n /* On narrow viewports the meta + corner mark fight for the same row.\n Lift the mark above the meta so neither truncates. */\n position: static;\n display: inline-block;\n margin-bottom: 10px;\n align-self: flex-start;\n }\n .pdp-tn-card.is-featured {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n }\n}\n\n/* skeleton */\n.pdp-trending-news-skeleton {\n display: grid;\n gap: 14px;\n}\n.pdp-skeleton-bar {\n height: 16px;\n border-radius: 6px;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--tn-bg) 92%, white) 0%,\n color-mix(in srgb, var(--tn-bg) 78%, white) 50%,\n color-mix(in srgb, var(--tn-bg) 92%, white) 100%\n );\n background-size: 200% 100%;\n animation: pdp-tn-shimmer 1.6s ease-in-out infinite;\n}\n\n@keyframes pdp-tn-fade-up {\n from {\n opacity: 0;\n transform: translateY(18px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@keyframes pdp-tn-pulse {\n 0%,\n 100% {\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--tn-primary) 18%, transparent);\n }\n 50% {\n box-shadow: 0 0 0 6px color-mix(in srgb, var(--tn-primary) 28%, transparent);\n }\n}\n@keyframes pdp-tn-shimmer {\n 0% {\n background-position: -200% 0;\n }\n 100% {\n background-position: 200% 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-trending-news.is-entering .pdp-tn-card {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n`;\n\nexport class PdpTrendingNewsLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: TrendingNewsData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n // Light DOM so the parent's CSS variables and tailwind utilities flow through.\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureTrendingNewsStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-trending-news');\n if (!container) return;\n this._armed = true;\n // staggered cards: last delay 460ms + 700ms duration.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1400\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-trending-news-skeleton\">\n <div class=\"pdp-skeleton-bar\" style=\"width:60%\"></div>\n <div class=\"pdp-skeleton-bar\" style=\"width:90%\"></div>\n <div class=\"pdp-skeleton-bar\" style=\"width:80%\"></div>\n <div class=\"pdp-skeleton-bar\" style=\"width:70%\"></div>\n </div>`;\n }\n\n const items = this.data.items;\n const featured = items[0];\n const rest = items.slice(1);\n\n return html`\n <section\n class=\"${this._entering ? 'pdp-trending-news is-entering' : 'pdp-trending-news'}\"\n aria-label=\"Trending news\"\n >\n <article class=\"pdp-tn-card is-featured\">\n <div class=\"pdp-tn-meta\">\n <span class=\"pdp-tn-dot\" aria-hidden=\"true\"></span>\n <span class=\"pdp-tn-meta-source\">${featured.source}</span>\n <span class=\"pdp-tn-meta-sep\">\u00B7</span>\n <span>${featured.published_relative}</span>\n </div>\n <span class=\"pdp-tn-corner-mark\">featured</span>\n <h3 class=\"pdp-tn-headline\">${renderHeadlineLink(featured)}</h3>\n <p class=\"pdp-tn-blurb\">${featured.blurb}</p>\n </article>\n ${rest.map(\n (item) => html`\n <article class=\"pdp-tn-card\">\n <div class=\"pdp-tn-meta\">\n <span class=\"pdp-tn-dot\" aria-hidden=\"true\"></span>\n <span class=\"pdp-tn-meta-source\">${item.source}</span>\n <span class=\"pdp-tn-meta-sep\">\u00B7</span>\n <span>${item.published_relative}</span>\n </div>\n <h3 class=\"pdp-tn-headline\">${renderHeadlineLink(item)}</h3>\n <p class=\"pdp-tn-blurb\">${item.blurb}</p>\n </article>\n `\n )}\n </section>\n `;\n }\n}\n\n/** Render the headline as a click-through link when ``url`` is present;\n * fall back to bare text when it isn't. Sub-agent-generated content\n * omits the URL (no hallucinated links); curated seed content\n * includes a real URL so visitors can read the full article. */\nfunction renderHeadlineLink(item: TrendingNewsItem) {\n if (!item.url) return html`${item.headline}`;\n return html`<a\n class=\"pdp-tn-headline-link\"\n href=${item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >${item.headline}</a\n >`;\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-trending-news')) {\n customElements.define('syntro-pdp-trending-news', PdpTrendingNewsLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-trending-news': PdpTrendingNewsLit;\n }\n}\n", "/**\n * Entrance-animation settle helper \u2014 GPU/compositing hazard fix.\n *\n * The PDP widgets play a fade/slide entrance on mount. The old authoring\n * rested the animated element at `opacity: 0` and relied on\n * `animation: \u2026 forwards` to bring it to `opacity: 1`. When the browser\n * DROPS or SUPPRESSES that animation \u2014 battery-saver mode, an over-budget\n * compositor, a backgrounded tab that never runs the keyframes \u2014 the element\n * stays at `opacity: 0` forever: a permanently blank PDP section. (Same class\n * of bug as the ProductCard/FAQ flip resting at rotateY(180).)\n *\n * The fix keeps the entrance PIXEL-IDENTICAL but inverts the resting state:\n * - base rule: `opacity: 1` (content is always visible at rest);\n * - a transient `.is-entering` class (applied on the container at mount)\n * re-introduces `opacity: 0` + the exact same keyframes with `both` fill\n * so the pre-delay invisibility is preserved;\n * - `@media (prefers-reduced-motion: reduce)` neutralises `.is-entering`\n * (content immediately visible, no animation).\n *\n * This helper arms the REMOVAL of `.is-entering` once the entrance has run:\n * - fast path: the container's OWN `animationend` (only when `fastPath` is\n * set \u2014 i.e. the container's own keyframes are the last to finish; widgets\n * with longer-running staggered children leave this off and lean on the\n * belt so the class is never pulled mid-stagger);\n * - belt: a `setTimeout` sized to the longest keyframe\n * (delay + duration + margin) that ALWAYS fires, even if `animationend`\n * never does.\n *\n * Removing the class is a visual no-op: the base state is `opacity: 1` and the\n * `both` fill holds the final keyframe until the moment the class drops.\n *\n * @returns a disposer that clears the belt timer + listener (call on disconnect).\n */\nexport function armEntranceSettle(\n onSettle: () => void,\n container: HTMLElement | null,\n settleMs: number,\n fastPath = false\n): () => void {\n let done = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const cleanup = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n container?.removeEventListener('animationend', onAnimationEnd);\n };\n\n const settle = (): void => {\n if (done) return;\n done = true;\n cleanup();\n onSettle();\n };\n\n const onAnimationEnd = (event: Event): void => {\n // Only settle from the container's OWN entrance keyframes. Staggered child\n // animations bubble here too; ignoring them keeps the belt authoritative so\n // the class is never removed before the last child finishes.\n if (fastPath && event.target === container) settle();\n };\n\n container?.addEventListener('animationend', onAnimationEnd);\n timer = setTimeout(settle, settleMs);\n\n return cleanup;\n}\n", "import { html, LitElement } from 'lit';\n\ninterface TrendingNewsItem {\n headline: string;\n source: string;\n published_relative: string;\n blurb: string;\n /** Optional click-through URL \u2014 see PdpTrendingNewsLit.ts for the\n * curated-seed-vs-sub-agent rationale. */\n url?: string | null;\n}\n\ninterface TrendingNewsData {\n items: TrendingNewsItem[];\n rationale: string;\n}\n\n/**\n * Magazine-Column variant for the trending-news module.\n *\n * Typographic-first single-column reading column. Lead item gets a\n * drop-cap on its blurb; subsequent items use a tight meta-line +\n * headline pair separated by hairline rules. No card chrome, no\n * gradients \u2014 the editorial density comes from type scale and\n * whitespace discipline.\n *\n * HIGH typographic density. Best between a LOW-density module\n * (pull-quote, full-bleed) and another LOW-density module.\n */\nfunction ensureMagazineStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-trending-news-magazine-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-trending-news-magazine-style';\n style.textContent = MAGAZINE_CSS;\n document.head.appendChild(style);\n}\n\nconst MAGAZINE_CSS = `\n.pdp-tn-mag {\n --tn-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --tn-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --tn-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --tn-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --tn-display: var(--syntro-pdp-display, inherit);\n --tn-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--tn-sans);\n color: var(--tn-fg);\n max-width: 640px;\n margin: 0 auto;\n padding: 8px 4px;\n}\n\n.pdp-tn-mag__kicker {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--tn-accent);\n font-weight: 600;\n margin-bottom: 18px;\n}\n.pdp-tn-mag__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--tn-accent);\n opacity: 0.8;\n}\n\n.pdp-tn-mag__lead {\n border-bottom: 1px solid var(--tn-rule);\n padding-bottom: 22px;\n margin-bottom: 22px;\n}\n\n.pdp-tn-mag__meta {\n font-size: 11px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n margin-bottom: 10px;\n}\n.pdp-tn-mag__meta-source {\n color: var(--tn-fg);\n font-weight: 600;\n}\n\n.pdp-tn-mag__headline {\n font-family: var(--tn-display);\n font-weight: 450;\n font-variation-settings: 'SOFT' 80;\n line-height: 1.08;\n letter-spacing: -0.018em;\n margin: 0 0 14px;\n font-size: clamp(26px, 5.2vw, 34px);\n font-feature-settings: 'liga' 1, 'ss01' 1;\n}\n\n.pdp-tn-mag__blurb {\n font-family: var(--tn-display);\n font-weight: 380;\n font-size: 16.5px;\n line-height: 1.55;\n color: hsl(30 8% 78%);\n margin: 0;\n}\n/* NB: lead blurb is deliberately NOT clamped. The lead carries a\n ::first-letter drop-cap (float: left, 3.3em) \u2014 'float' doesn't\n participate in '-webkit-box' line counting, so combining the two\n can cause the drop-cap to overlap clamped text or misplace the\n ellipsis in WebKit/Blink. The lead is one blurb per card and the\n schema's 280c cap keeps it bounded; letting it grow is the safer\n call than risking a broken drop-cap. */\n/* Drop-cap on the lead blurb. ::first-letter is enough \u2014 no JS,\n no extra DOM, falls back gracefully on unsupported engines. */\n.pdp-tn-mag__lead .pdp-tn-mag__blurb::first-letter {\n font-family: var(--tn-display);\n font-weight: 500;\n font-size: 3.3em;\n line-height: 0.85;\n float: left;\n padding: 4px 10px 0 0;\n color: var(--tn-accent);\n font-variation-settings: 'SOFT' 100;\n}\n\n.pdp-tn-mag__item {\n display: grid;\n grid-template-columns: 1fr;\n gap: 6px;\n padding: 16px 0;\n border-bottom: 1px solid var(--tn-rule);\n}\n.pdp-tn-mag__item:last-child {\n border-bottom: 0;\n padding-bottom: 4px;\n}\n\n.pdp-tn-mag__item .pdp-tn-mag__headline {\n font-size: clamp(17px, 3.4vw, 19px);\n font-weight: 500;\n line-height: 1.2;\n margin: 0;\n}\n.pdp-tn-mag__item .pdp-tn-mag__blurb {\n font-family: var(--tn-sans);\n font-size: 13.5px;\n line-height: 1.5;\n margin: 0;\n color: hsl(30 8% 70%);\n /* Clamp non-lead item blurbs to 3 lines (no drop-cap on these so\n the line-clamp triplet plays nicely). Full triplet needed because\n the base .pdp-tn-mag__blurb intentionally has no display:-webkit-box. */\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Headline click-through link (inherits type styles; subtle hover\n underline + \u2197 glyph to signal external destination). */\n.pdp-tn-mag__headline-link {\n color: inherit;\n text-decoration: none;\n background-image: linear-gradient(currentColor, currentColor);\n background-position: 0 100%;\n background-repeat: no-repeat;\n background-size: 0 1px;\n transition: background-size 220ms ease;\n}\n.pdp-tn-mag__headline-link:hover,\n.pdp-tn-mag__headline-link:focus-visible {\n background-size: 100% 1px;\n outline: none;\n}\n.pdp-tn-mag__headline-link::after {\n content: ' \\\\2197';\n font-family: var(--tn-sans);\n font-size: 0.6em;\n letter-spacing: 0;\n color: var(--tn-accent);\n opacity: 0.55;\n vertical-align: 0.2em;\n margin-left: 0.18em;\n transition: opacity 220ms ease;\n}\n.pdp-tn-mag__headline-link:hover::after,\n.pdp-tn-mag__headline-link:focus-visible::after {\n opacity: 1;\n}\n\n@media (max-width: 480px) {\n .pdp-tn-mag {\n padding: 4px 0;\n }\n .pdp-tn-mag__lead {\n padding-bottom: 18px;\n margin-bottom: 18px;\n }\n .pdp-tn-mag__lead .pdp-tn-mag__blurb::first-letter {\n font-size: 3em;\n }\n}\n`;\n\nexport class PdpTrendingNewsMagazineLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: TrendingNewsData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureMagazineStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-tn-mag\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n const items = this.data.items;\n const [lead, ...rest] = items;\n return html`\n <section class=\"pdp-tn-mag\" aria-label=\"Trending news\">\n ${\n lead\n ? html`<article class=\"pdp-tn-mag__lead\">\n <div class=\"pdp-tn-mag__meta\">\n <span class=\"pdp-tn-mag__meta-source\">${lead.source}</span>\n \u00B7 ${lead.published_relative}\n </div>\n <h3 class=\"pdp-tn-mag__headline\">${renderHeadlineLink(lead)}</h3>\n <p class=\"pdp-tn-mag__blurb\">${lead.blurb}</p>\n </article>`\n : ''\n }\n ${rest.map(\n (it) => html`<article class=\"pdp-tn-mag__item\">\n <div class=\"pdp-tn-mag__meta\">\n <span class=\"pdp-tn-mag__meta-source\">${it.source}</span>\n \u00B7 ${it.published_relative}\n </div>\n <h4 class=\"pdp-tn-mag__headline\">${renderHeadlineLink(it)}</h4>\n <p class=\"pdp-tn-mag__blurb\">${it.blurb}</p>\n </article>`\n )}\n </section>\n `;\n }\n}\n\nfunction renderHeadlineLink(item: TrendingNewsItem) {\n if (!item.url) return html`${item.headline}`;\n return html`<a\n class=\"pdp-tn-mag__headline-link\"\n href=${item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >${item.headline}</a\n >`;\n}\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('syntro-pdp-trending-news-magazine')\n) {\n customElements.define('syntro-pdp-trending-news-magazine', PdpTrendingNewsMagazineLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-trending-news-magazine': PdpTrendingNewsMagazineLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface TrendingNewsItem {\n headline: string;\n source: string;\n published_relative: string;\n blurb: string;\n /** Optional click-through URL \u2014 see PdpTrendingNewsLit.ts for the\n * curated-seed-vs-sub-agent rationale. */\n url?: string | null;\n}\n\ninterface TrendingNewsData {\n items: TrendingNewsItem[];\n rationale: string;\n}\n\n/**\n * Hero-Quote variant for the trending-news module.\n *\n * Lead item rendered as a full-bleed pull-quote panel (the blurb is\n * treated as the editorial quote, the source becomes the citation\n * footer). Follow-on items collapse to a tight 1-line-per-item list.\n *\n * LOW visual density \u2014 functions as a rhythm-break beat between\n * heavier modules. Best for lead items that read as a single\n * declarative finding with a recognizable source. Avoid for\n * procedural / how-to news items.\n */\nfunction ensureHeroStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-trending-news-hero-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-trending-news-hero-style';\n style.textContent = HERO_CSS;\n document.head.appendChild(style);\n}\n\nconst HERO_CSS = `\n.pdp-tn-hero {\n --tn-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --tn-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --tn-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --tn-accent-soft: var(--syntro-pdp-primary-soft, hsl(150 32% 38% / 0.18));\n --tn-display: var(--syntro-pdp-display, inherit);\n --tn-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--tn-sans);\n color: var(--tn-fg);\n /* Full-bleed within its slot \u2014 escape the column gutter the parent\n PDP sets so this section reads as a rhythm-break. */\n margin: 0 calc(-1 * var(--syntro-pdp-bleed, 0px));\n}\n\n.pdp-tn-hero__panel {\n position: relative;\n padding: 36px 28px 32px;\n border-radius: 22px;\n /* Soft duotone gradient \u2014 accent at top-left, deep at bottom. */\n background:\n radial-gradient(120% 90% at 0% 0%, var(--tn-accent-soft) 0%, transparent 60%),\n linear-gradient(180deg, hsl(150 24% 12%) 0%, hsl(25 10% 9%) 100%);\n border: 1px solid hsl(30 6% 18%);\n overflow: hidden;\n}\n\n/* Big SVG quote-mark in the corner \u2014 pure CSS, no internet image dep. */\n.pdp-tn-hero__panel::before {\n content: '\u201C';\n position: absolute;\n top: 4px;\n left: 16px;\n font-family: var(--tn-display);\n font-style: normal;\n font-weight: 500;\n font-size: 140px;\n line-height: 1;\n color: var(--tn-accent);\n opacity: 0.18;\n pointer-events: none;\n}\n\n.pdp-tn-hero__quote {\n position: relative;\n font-family: var(--tn-display);\n font-style: italic;\n font-weight: 400;\n font-variation-settings: 'SOFT' 90, 'WONK' 1;\n line-height: 1.18;\n letter-spacing: -0.015em;\n margin: 0 0 22px;\n font-size: clamp(22px, 4.6vw, 30px);\n color: var(--tn-fg);\n max-width: 32ch;\n /* Pull-quote shouldn't run past 5 lines or it stops reading as a\n quote and starts reading as a paragraph. Schema permits ~280c\n which can blow past 5 lines at narrow viewports. */\n display: -webkit-box;\n -webkit-line-clamp: 5;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.pdp-tn-hero__cite {\n display: flex;\n align-items: baseline;\n gap: 12px;\n font-size: 12px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n}\n.pdp-tn-hero__cite-rule {\n flex: 0 0 24px;\n height: 1px;\n background: var(--tn-accent);\n align-self: center;\n opacity: 0.7;\n}\n.pdp-tn-hero__cite-source {\n color: var(--tn-fg);\n font-weight: 600;\n}\n\n.pdp-tn-hero__headline {\n margin: 22px 0 0;\n font-family: var(--tn-sans);\n font-weight: 600;\n font-size: 14.5px;\n color: hsl(30 8% 78%);\n line-height: 1.4;\n max-width: 48ch;\n border-top: 1px solid hsl(30 6% 20%);\n padding-top: 18px;\n}\n\n/* Follow-on items: tight, one row each, headline left, source right */\n.pdp-tn-hero__followon {\n margin-top: 18px;\n padding: 0 4px;\n}\n.pdp-tn-hero__row {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 16px;\n align-items: baseline;\n padding: 12px 0;\n border-bottom: 1px solid hsl(30 6% 18%);\n}\n.pdp-tn-hero__row:last-child {\n border-bottom: 0;\n}\n.pdp-tn-hero__row-headline {\n font-size: 14.5px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--tn-fg);\n font-family: var(--tn-sans);\n margin: 0;\n /* clamp to single line + ellipsis at narrow widths */\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.pdp-tn-hero__row-meta {\n font-size: 10.5px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* Headline click-through (used on the lead headline below the quote\n and on each follow-on row headline). Inherits type styles; subtle\n hover underline + \u2197 glyph signals external destination. */\n.pdp-tn-hero__headline-link {\n color: inherit;\n text-decoration: none;\n background-image: linear-gradient(currentColor, currentColor);\n background-position: 0 100%;\n background-repeat: no-repeat;\n background-size: 0 1px;\n transition: background-size 220ms ease;\n}\n.pdp-tn-hero__headline-link:hover,\n.pdp-tn-hero__headline-link:focus-visible {\n background-size: 100% 1px;\n outline: none;\n}\n.pdp-tn-hero__headline-link::after {\n content: ' \\\\2197';\n font-family: var(--tn-sans);\n font-size: 0.7em;\n letter-spacing: 0;\n color: var(--tn-accent);\n opacity: 0.55;\n vertical-align: 0.18em;\n margin-left: 0.2em;\n transition: opacity 220ms ease;\n}\n.pdp-tn-hero__headline-link:hover::after,\n.pdp-tn-hero__headline-link:focus-visible::after {\n opacity: 1;\n}\n\n@media (max-width: 480px) {\n .pdp-tn-hero__panel {\n padding: 28px 22px 26px;\n border-radius: 18px;\n }\n .pdp-tn-hero__panel::before {\n font-size: 110px;\n top: 0;\n left: 10px;\n }\n}\n`;\n\nexport class PdpTrendingNewsHeroLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: TrendingNewsData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureHeroStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-tn-hero\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n const [lead, ...rest] = this.data.items;\n if (!lead) return html``;\n return html`\n <section class=\"pdp-tn-hero\" aria-label=\"Trending news\">\n <article class=\"pdp-tn-hero__panel\">\n <blockquote class=\"pdp-tn-hero__quote\">${lead.blurb}</blockquote>\n <footer class=\"pdp-tn-hero__cite\">\n <span class=\"pdp-tn-hero__cite-rule\" aria-hidden=\"true\"></span>\n <span class=\"pdp-tn-hero__cite-source\">${lead.source}</span>\n <span>${lead.published_relative}</span>\n </footer>\n <p class=\"pdp-tn-hero__headline\">${renderHeadlineLink(lead)}</p>\n </article>\n ${\n rest.length\n ? html`<div class=\"pdp-tn-hero__followon\">\n ${rest.map(\n (it) => html`<div class=\"pdp-tn-hero__row\">\n <p class=\"pdp-tn-hero__row-headline\">${renderHeadlineLink(it)}</p>\n <span class=\"pdp-tn-hero__row-meta\">${it.source}</span>\n </div>`\n )}\n </div>`\n : ''\n }\n </section>\n `;\n }\n}\n\nfunction renderHeadlineLink(item: TrendingNewsItem) {\n if (!item.url) return html`${item.headline}`;\n return html`<a\n class=\"pdp-tn-hero__headline-link\"\n href=${item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >${item.headline}</a\n >`;\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-trending-news-hero')) {\n customElements.define('syntro-pdp-trending-news-hero', PdpTrendingNewsHeroLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-trending-news-hero': PdpTrendingNewsHeroLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface PeerPost {\n initials: string;\n persona: string;\n quote: string;\n similarity_reason: string;\n time_ago: string;\n}\n\ninterface PeerFeedData {\n peers: PeerPost[];\n rationale: string;\n}\n\n/**\n * Masonry-Quote variant for the peer-feed module.\n *\n * Two-column staggered-height grid using CSS columns. Each card has a\n * CSS-rendered initial avatar (colored circle, persona-hashed hue), a\n * bold quote, and a small persona footer. Card heights vary with quote\n * length, which is where the editorial rhythm comes from.\n *\n * Intentionally image-free. The peer-feed module avoids photography\n * entirely (see PeerFeedVariant docstring) \u2014 variety comes from layout\n * and typography, not faces.\n */\nfunction ensureMasonryStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-peer-feed-masonry-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-peer-feed-masonry-style';\n style.textContent = MASONRY_CSS;\n document.head.appendChild(style);\n}\n\nconst MASONRY_CSS = `\n.pdp-pf-mas {\n --pf-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pf-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --pf-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --pf-bg: var(--syntro-pdp-card-bg, hsl(25 10% 12%));\n --pf-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pf-display: var(--syntro-pdp-display, inherit);\n --pf-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--pf-sans);\n color: var(--pf-fg);\n padding: 4px 0;\n}\n\n.pdp-pf-mas__kicker {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--pf-accent);\n font-weight: 600;\n margin-bottom: 18px;\n}\n.pdp-pf-mas__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--pf-accent);\n opacity: 0.8;\n}\n\n/* CSS-columns gives a real masonry shape without JS layout\n (grid masonry is still behind a flag in most engines). On narrow\n mobile we collapse to a single column. */\n.pdp-pf-mas__grid {\n column-count: 2;\n column-gap: 14px;\n column-fill: balance;\n}\n@media (max-width: 540px) {\n .pdp-pf-mas__grid {\n column-count: 1;\n }\n}\n\n.pdp-pf-mas__card {\n /* Cards must NOT break across columns. */\n break-inside: avoid;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 18px 16px 16px;\n border-radius: 14px;\n background: var(--pf-bg);\n border: 1px solid var(--pf-rule);\n margin: 0 0 14px;\n}\n\n.pdp-pf-mas__header {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.pdp-pf-mas__avatar {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n flex: 0 0 36px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: var(--pf-sans);\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: hsl(35 15% 95%);\n /* Hue is set per-card via inline style. */\n background: hsl(var(--pf-avatar-hue, 150) 28% 32%);\n border: 1px solid hsl(var(--pf-avatar-hue, 150) 28% 42% / 0.7);\n}\n.pdp-pf-mas__persona {\n font-size: 12.5px;\n line-height: 1.3;\n color: var(--pf-fg);\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n}\n.pdp-pf-mas__persona-sub {\n color: var(--pf-fg-muted);\n font-weight: 400;\n font-size: 11.5px;\n margin-top: 2px;\n display: block;\n}\n\n.pdp-pf-mas__quote {\n font-family: var(--pf-display);\n font-weight: 400;\n font-style: italic;\n font-size: 15.5px;\n line-height: 1.4;\n margin: 0;\n color: var(--pf-fg);\n font-variation-settings: 'SOFT' 80;\n}\n.pdp-pf-mas__quote::before {\n content: '\"';\n font-size: 22px;\n line-height: 0;\n position: relative;\n top: 7px;\n margin-right: 2px;\n color: var(--pf-accent);\n opacity: 0.7;\n}\n\n.pdp-pf-mas__footer {\n font-size: 10.5px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--pf-fg-muted);\n font-weight: 500;\n border-top: 1px solid var(--pf-rule);\n padding-top: 10px;\n}\n`;\n\n// Stable per-string hue 0-360. Deterministic so the same persona\n// always renders the same color across page loads.\nfunction personaHue(s: string): number {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n return Math.abs(h) % 360;\n}\n\nexport class PdpPeerFeedMasonryLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: PeerFeedData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureMasonryStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-pf-mas\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n return html`\n <section class=\"pdp-pf-mas\" aria-label=\"Peer voices\">\n <div class=\"pdp-pf-mas__grid\">\n ${this.data.peers.map(\n (p) => html`<article\n class=\"pdp-pf-mas__card\"\n style=${`--pf-avatar-hue: ${personaHue(p.persona)};`}\n >\n <header class=\"pdp-pf-mas__header\">\n <span class=\"pdp-pf-mas__avatar\" aria-hidden=\"true\">${p.initials}</span>\n <span class=\"pdp-pf-mas__persona\"\n >${p.persona}\n <span class=\"pdp-pf-mas__persona-sub\">${p.similarity_reason}</span>\n </span>\n </header>\n <blockquote class=\"pdp-pf-mas__quote\">${p.quote}</blockquote>\n <footer class=\"pdp-pf-mas__footer\">${p.time_ago}</footer>\n </article>`\n )}\n </div>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-peer-feed-masonry')) {\n customElements.define('syntro-pdp-peer-feed-masonry', PdpPeerFeedMasonryLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-peer-feed-masonry': PdpPeerFeedMasonryLit;\n }\n}\n", "import { html, LitElement, svg } from 'lit';\n\ninterface PeerPost {\n initials: string;\n persona: string;\n quote: string;\n similarity_reason: string;\n time_ago: string;\n}\n\ninterface PeerFeedData {\n peers: PeerPost[];\n rationale: string;\n}\n\n/**\n * Voice-Carousel variant for the peer-feed module.\n *\n * Horizontal-scroll row of vertical voice cards. Each card has a\n * distinctive geometric SVG glyph chosen by hashing the persona string\n * (waveform / gauge / leaf / spark). Same persona \u2192 same glyph across\n * loads. Reads as \"voices we follow\" \u2014 variety by typology, not faces.\n *\n * Intentionally image-free. See PeerFeedVariant docstring for the\n * \"no UGC, no fake UGC either\" reasoning.\n */\nfunction ensureCarouselStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-peer-feed-carousel-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-peer-feed-carousel-style';\n style.textContent = CAROUSEL_CSS;\n document.head.appendChild(style);\n}\n\nconst CAROUSEL_CSS = `\n/* The custom-element tag itself + its slot-mount parent must declare\n display:block AND min-width:0 so they participate in the grid cell's\n width constraint instead of expanding to intrinsic content. Without\n this, the inner flex track's content sets the cell's auto-size and\n busts past the PDP container, forcing whole-page horizontal scroll. */\nsyntro-pdp-peer-feed-carousel {\n display: block;\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n.syntro-pdp-slot-mount:has(> syntro-pdp-peer-feed-carousel) {\n min-width: 0;\n overflow: hidden;\n}\n.pdp-pf-car {\n --pf-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pf-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --pf-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --pf-bg: var(--syntro-pdp-card-bg, hsl(25 10% 12%));\n --pf-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pf-display: var(--syntro-pdp-display, inherit);\n --pf-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--pf-sans);\n color: var(--pf-fg);\n padding: 4px 0;\n /* Critical \u2014 the carousel is a horizontal-scroll surface. Without an\n explicit width constraint, the inner flex track expands to its\n intrinsic content width and pushes the whole page into horizontal\n scroll. min-width:0 + width:100% lets the section shrink inside\n a grid/flex parent so the track's overflow:auto can do its job. */\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.pdp-pf-car__kicker {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--pf-accent);\n font-weight: 600;\n margin-bottom: 18px;\n}\n.pdp-pf-car__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--pf-accent);\n opacity: 0.8;\n}\n\n.pdp-pf-car__track {\n /* Flex (NOT grid) so the track honors its parent's width with\n overflow:auto. Grid containers size to their intrinsic content\n even when overflow:auto is set, which forced the whole page to\n scroll horizontally. */\n display: flex;\n flex-wrap: nowrap;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n width: 100%;\n min-width: 0;\n scroll-snap-type: x mandatory;\n scrollbar-width: thin;\n scrollbar-color: var(--pf-rule) transparent;\n padding-bottom: 8px;\n /* Edge fade \u2014 hint that there's more to scroll. */\n mask-image: linear-gradient(90deg, transparent 0, black 14px, black calc(100% - 24px), transparent 100%);\n -webkit-mask-image: linear-gradient(90deg, transparent 0, black 14px, black calc(100% - 24px), transparent 100%);\n}\n.pdp-pf-car__track::-webkit-scrollbar {\n height: 4px;\n}\n\n.pdp-pf-car__card {\n /* Per-card width \u2014 explicit because flex children of overflow-x:auto\n need a width to render off-screen. 78% of viewport with min/max\n so the card always fits-ish on phones AND never crowds on wide. */\n flex: 0 0 78%;\n max-width: 320px;\n min-width: 230px;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 20px 18px 18px;\n border-radius: 14px;\n background: var(--pf-bg);\n border: 1px solid var(--pf-rule);\n min-height: 220px;\n position: relative;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n/* Glyph block \u2014 sits top-right behind text as a tinted decoration. */\n.pdp-pf-car__glyph {\n position: absolute;\n top: 16px;\n right: 14px;\n width: 60px;\n height: 60px;\n color: hsl(var(--pf-glyph-hue, 150) 32% 50%);\n opacity: 0.38;\n pointer-events: none;\n}\n\n.pdp-pf-car__persona {\n font-size: 11px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--pf-accent);\n font-weight: 600;\n max-width: 60%;\n}\n\n.pdp-pf-car__quote {\n font-family: var(--pf-display);\n font-weight: 400;\n font-style: italic;\n font-size: 15.5px;\n line-height: 1.4;\n margin: 0;\n color: var(--pf-fg);\n font-variation-settings: 'SOFT' 80;\n flex: 1 1 auto;\n}\n\n.pdp-pf-car__footer {\n font-size: 10.5px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--pf-fg-muted);\n font-weight: 500;\n border-top: 1px solid var(--pf-rule);\n padding-top: 10px;\n display: flex;\n justify-content: space-between;\n gap: 8px;\n}\n.pdp-pf-car__footer-sim {\n color: var(--pf-fg-muted);\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n font-style: italic;\n text-align: right;\n flex: 1 1 auto;\n /* Truncate similarity reason if it overflows the row. */\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n`;\n\n// 4 geometric glyphs that read as \"kinds of human.\" Pure SVG so no\n// internet image deps. Pick by hashing the persona \u2014 same persona \u2192\n// same glyph across loads.\ntype GlyphKind = 'waveform' | 'gauge' | 'leaf' | 'spark';\nconst GLYPHS: GlyphKind[] = ['waveform', 'gauge', 'leaf', 'spark'];\n\nfunction glyphFor(persona: string): GlyphKind {\n let h = 0;\n for (let i = 0; i < persona.length; i++) h = (h * 31 + persona.charCodeAt(i)) | 0;\n return GLYPHS[Math.abs(h) % GLYPHS.length];\n}\n\nfunction glyphHue(persona: string): number {\n // Distinct from the glyph hash so the color isn't perfectly correlated\n // with the kind \u2014 same shape across people, different tints.\n let h = 0;\n for (let i = 0; i < persona.length; i++) h = (h * 17 + persona.charCodeAt(i)) | 0;\n return Math.abs(h) % 360;\n}\n\nfunction renderGlyph(kind: GlyphKind) {\n const common = svg`<g stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></g>`;\n if (kind === 'waveform') {\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n ${common}\n <path d=\"M4 30 Q12 12 18 30 T32 30 T46 30 T58 30\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/>\n <path d=\"M4 38 Q12 24 18 38 T32 38 T46 38 T58 38\" stroke=\"currentColor\" stroke-width=\"1.4\" fill=\"none\" stroke-linecap=\"round\" opacity=\"0.55\"/>\n </svg>`;\n }\n if (kind === 'gauge') {\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n <path d=\"M8 42 A22 22 0 0 1 52 42\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/>\n <path d=\"M8 42 A22 22 0 0 1 30 20\" stroke=\"currentColor\" stroke-width=\"3.2\" fill=\"none\" stroke-linecap=\"round\"/>\n <line x1=\"30\" y1=\"42\" x2=\"38\" y2=\"26\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"/>\n <circle cx=\"30\" cy=\"42\" r=\"2.6\" fill=\"currentColor\"/>\n </svg>`;\n }\n if (kind === 'leaf') {\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n <path d=\"M14 46 C14 22 30 8 50 12 C46 34 32 50 14 46 Z\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linejoin=\"round\"/>\n <path d=\"M14 46 L42 18\" stroke=\"currentColor\" stroke-width=\"1.4\" fill=\"none\"/>\n </svg>`;\n }\n // spark\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n <path d=\"M30 6 L34 26 L54 30 L34 34 L30 54 L26 34 L6 30 L26 26 Z\" stroke=\"currentColor\" stroke-width=\"1.6\" fill=\"none\" stroke-linejoin=\"round\"/>\n </svg>`;\n}\n\nexport class PdpPeerFeedCarouselLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: PeerFeedData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureCarouselStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-pf-car\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n return html`\n <section class=\"pdp-pf-car\" aria-label=\"Peer voices\">\n <div class=\"pdp-pf-car__track\">\n ${this.data.peers.map((p) => {\n const kind = glyphFor(p.persona);\n const hue = glyphHue(p.persona);\n return html`<article class=\"pdp-pf-car__card\" style=${`--pf-glyph-hue: ${hue};`}>\n <span class=\"pdp-pf-car__glyph\" aria-hidden=\"true\">${renderGlyph(kind)}</span>\n <span class=\"pdp-pf-car__persona\">${p.persona}</span>\n <blockquote class=\"pdp-pf-car__quote\">${p.quote}</blockquote>\n <footer class=\"pdp-pf-car__footer\">\n <span>${p.time_ago}</span>\n <span class=\"pdp-pf-car__footer-sim\" title=${p.similarity_reason}\n >${p.similarity_reason}</span\n >\n </footer>\n </article>`;\n })}\n </div>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-peer-feed-carousel')) {\n customElements.define('syntro-pdp-peer-feed-carousel', PdpPeerFeedCarouselLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-peer-feed-carousel': PdpPeerFeedCarouselLit;\n }\n}\n", "import { html, LitElement, nothing, svg } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * Backend ChartPayload shape from the PDP pipeline's ChartPayload Pydantic\n * model. The widget renders one of three layouts:\n *\n * - bar: in-house CSS bars (no dependency); animated grow-in; per-series\n * color palette; mobile-friendly label rotation.\n * - line: in-house SVG polyline; stroke-dasharray reveal animation;\n * tap-area markers on each data point.\n * - table: stylized comparison table built from a pivoted matrix.\n *\n * adapt-viz/vega is deliberately NOT used \u2014 at the PDP-tile scale our data\n * is always 3\u201310 points \u00D7 \u22644 series, and vega's bundle weight + label\n * layout heuristics work poorly at narrow widths.\n */\n\ninterface ChartPoint {\n x: string;\n y: number;\n}\n\ninterface ChartSeries {\n name: string;\n points: ChartPoint[];\n}\n\nexport interface ChartData {\n title: string;\n viz_kind: 'bar' | 'line' | 'table';\n series: ChartSeries[];\n rationale: string;\n}\n\n// \u2500\u2500\u2500 data utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Format a numeric value \u2014 drop trailing zeros, use thousands separator. */\nfunction formatNum(v: number | undefined): string {\n if (v === undefined || !Number.isFinite(v)) return '\u2014';\n if (Number.isInteger(v)) return v.toLocaleString('en-US');\n const trimmed = v.toFixed(2).replace(/\\.?0+$/, '');\n const [intPart, decPart] = trimmed.split('.');\n const formatted = Number(intPart).toLocaleString('en-US');\n return decPart ? `${formatted}.${decPart}` : formatted;\n}\n\n/** Comparison table matrix \u2014 same shape used by table layout. */\ninterface ComparisonMatrix {\n rowLabels: string[];\n colLabels: string[];\n cells: (number | undefined)[][];\n winningRow: number | null;\n}\n\nfunction buildComparisonMatrix(data: ChartData): ComparisonMatrix {\n const colLabels = data.series.map((s) => s.name);\n const seenRows = new Map<string, number>();\n const rowLabels: string[] = [];\n for (const series of data.series) {\n for (const p of series.points) {\n if (!seenRows.has(p.x)) {\n seenRows.set(p.x, rowLabels.length);\n rowLabels.push(p.x);\n }\n }\n }\n const cells: (number | undefined)[][] = rowLabels.map(() =>\n new Array<number | undefined>(colLabels.length).fill(undefined)\n );\n data.series.forEach((series, colIdx) => {\n for (const p of series.points) {\n const rowIdx = seenRows.get(p.x);\n // Every point populated seenRows and cells in the first pass above.\n const row = cells[rowIdx as number] as (number | undefined)[];\n row[colIdx] = p.y;\n }\n });\n let winningRow: number | null = null;\n let bestSum = Number.NEGATIVE_INFINITY;\n rowLabels.forEach((_, idx) => {\n const row = cells[idx] as (number | undefined)[];\n const sum = row.reduce<number>((acc, v) => acc + (typeof v === 'number' ? v : 0), 0);\n if (Number.isFinite(sum) && sum > bestSum) {\n bestSum = sum;\n winningRow = idx;\n }\n });\n return { rowLabels, colLabels, cells, winningRow };\n}\n\n/**\n * Group bars by x-value buckets for a grouped bar chart.\n * Each bucket carries one bar per series that has a point at that x.\n */\ninterface BarBucket {\n label: string;\n bars: { value: number; seriesIdx: number; seriesName: string }[];\n}\n\nfunction buildBarBuckets(data: ChartData): { buckets: BarBucket[]; yMax: number } {\n const seenX = new Map<string, number>();\n const buckets: BarBucket[] = [];\n for (const series of data.series) {\n for (const p of series.points) {\n let idx = seenX.get(p.x);\n if (idx === undefined) {\n idx = buckets.length;\n seenX.set(p.x, idx);\n buckets.push({ label: p.x, bars: [] });\n }\n }\n }\n data.series.forEach((series, sIdx) => {\n for (const p of series.points) {\n const idx = seenX.get(p.x);\n // Buckets and seenX are built from these same points in the first pass.\n const bucket = buckets[idx as number] as BarBucket;\n bucket.bars.push({ value: p.y, seriesIdx: sIdx, seriesName: series.name });\n }\n });\n let yMax = 0;\n for (const b of buckets) {\n for (const bar of b.bars) {\n if (bar.value > yMax) yMax = bar.value;\n }\n }\n if (yMax === 0) yMax = 1;\n return { buckets, yMax };\n}\n\n/** y-axis tick values \u2014 at most 4 ticks, \"nice\" rounded numbers. */\nfunction niceTicks(yMax: number): number[] {\n if (yMax <= 0) return [0];\n const targetCount = 4;\n const rawStep = yMax / targetCount;\n const mag = 10 ** Math.floor(Math.log10(rawStep));\n const niceSteps = [1, 2, 2.5, 5, 10];\n // 10 * mag always bounds rawStep because mag is rawStep's base-10 magnitude.\n const step = niceSteps.find((s) => s * mag >= rawStep) as number;\n const niceStep = step * mag;\n const ticks: number[] = [];\n for (let v = 0; v <= yMax + niceStep * 0.001; v += niceStep) {\n ticks.push(Number(v.toFixed(6)));\n }\n return ticks;\n}\n\n/**\n * For the line chart: project all series into a shared SVG coord space\n * (0 \u2192 100 on x, 0 \u2192 60 on y, origin at top-left).\n */\ninterface LinePathSeries {\n name: string;\n seriesIdx: number;\n points: { cx: number; cy: number; rawY: number; label: string }[];\n}\n\nfunction buildLinePaths(data: ChartData): {\n series: LinePathSeries[];\n xLabels: string[];\n yMax: number;\n} {\n const seenX = new Map<string, number>();\n const xLabels: string[] = [];\n for (const s of data.series) {\n for (const p of s.points) {\n if (!seenX.has(p.x)) {\n seenX.set(p.x, xLabels.length);\n xLabels.push(p.x);\n }\n }\n }\n const denom = Math.max(1, xLabels.length - 1);\n let yMax = 0;\n for (const s of data.series) {\n for (const p of s.points) {\n if (p.y > yMax) yMax = p.y;\n }\n }\n if (yMax === 0) yMax = 1;\n const seriesOut: LinePathSeries[] = data.series.map((s, sIdx) => ({\n name: s.name,\n seriesIdx: sIdx,\n points: s.points.map((p) => {\n // xLabels and seenX are built from these same points immediately above.\n const xIdx = seenX.get(p.x) as number;\n return {\n cx: (xIdx / denom) * 100,\n cy: 60 - (p.y / yMax) * 56 - 2,\n rawY: p.y,\n label: p.x,\n };\n }),\n }));\n return { series: seriesOut, xLabels, yMax };\n}\n\n// \u2500\u2500\u2500 styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction ensureChartStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-chart-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-chart-style';\n style.textContent = CHART_CSS;\n document.head.appendChild(style);\n}\n\nconst CHART_CSS = `\n.pdp-chart-frame {\n --pc-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pc-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --pc-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pc-primary-soft: color-mix(in srgb, var(--pc-primary) 10%, transparent);\n --pc-accent: var(--syntro-pdp-accent, hsl(95 18% 48%));\n --pc-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --pc-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --pc-display: var(--syntro-pdp-display, inherit);\n --pc-sans: var(--syntro-pdp-sans, inherit);\n\n /* 4-step brand palette for grouped bars / multi-series lines. Each step\n blends primary \u2192 accent and ramps opacity down so a single series owns\n the boldest swatch and additional series read as supporting context. */\n --pc-color-0: var(--pc-primary);\n --pc-color-1: color-mix(in srgb, var(--pc-accent) 70%, var(--pc-primary));\n --pc-color-2: color-mix(in srgb, var(--pc-primary) 55%, transparent);\n --pc-color-3: color-mix(in srgb, var(--pc-accent) 50%, transparent);\n\n position: relative;\n border: 1px solid var(--pc-border);\n border-radius: 22px;\n padding: 28px 26px 30px;\n background:\n radial-gradient(80% 60% at 12% 0%, color-mix(in srgb, var(--pc-primary) 10%, transparent) 0%, transparent 60%),\n radial-gradient(60% 50% at 100% 100%, color-mix(in srgb, var(--pc-accent) 7%, transparent) 0%, transparent 65%),\n linear-gradient(180deg, hsl(25 10% 12%), hsl(25 10% 9%));\n overflow: hidden;\n color: var(--pc-fg);\n font-family: var(--pc-sans);\n /* Resting state VISIBLE (GPU/compositing hazard fix): the whole chart frame\n defaults to opacity 1 so a dropped/suppressed entrance can never leave the\n entire chart blank. The entrance (and every child reveal) is re-armed only\n under .pdp-chart-frame.is-entering. */\n opacity: 1;\n}\n.pdp-chart-frame.is-entering {\n opacity: 0;\n transform: translateY(20px);\n animation: pdp-chart-enter 700ms cubic-bezier(0.2, 0.7, 0.2, 1) 120ms both;\n}\n\n.pdp-chart-frame::before {\n content: '';\n position: absolute;\n inset: 0 0 auto 0;\n height: 1px;\n background: linear-gradient(\n 90deg,\n transparent,\n color-mix(in srgb, var(--pc-primary) 60%, transparent),\n color-mix(in srgb, var(--pc-accent) 40%, transparent),\n transparent\n );\n opacity: 0.7;\n}\n\n.pdp-chart-eyebrow {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 11px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--pc-fg-muted);\n font-weight: 500;\n margin-bottom: 6px;\n}\n.pdp-chart-eyebrow-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--pc-primary);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--pc-primary) 16%, transparent);\n}\n\n.pdp-chart-title {\n font-family: var(--pc-display);\n font-weight: 400;\n font-size: clamp(26px, 2.6vw, 38px);\n letter-spacing: -0.015em;\n line-height: 1.1;\n margin: 0 0 4px;\n font-variation-settings: 'SOFT' 70, 'WONK' 1;\n color: var(--pc-fg);\n}\n\n.pdp-chart-rationale {\n margin: 18px 0 0;\n font-style: italic;\n color: var(--pc-fg-muted);\n font-size: 14.5px;\n line-height: 1.55;\n max-width: 64ch;\n padding-left: 14px;\n border-left: 2px solid color-mix(in srgb, var(--pc-primary) 40%, transparent);\n}\n\n/* \u2500\u2500\u2500 legend (multi-series only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-legend {\n display: flex;\n flex-wrap: wrap;\n gap: 14px;\n margin-top: 14px;\n font-size: 12px;\n color: var(--pc-fg-muted);\n}\n.pdp-chart-legend-item {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n letter-spacing: 0.02em;\n}\n.pdp-chart-legend-swatch {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n flex: 0 0 auto;\n}\n\n/* \u2500\u2500\u2500 bar chart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-bars {\n --bars-grid-h: 220px;\n margin-top: 18px;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 0;\n align-items: stretch;\n}\n\n.pdp-chart-yaxis {\n display: grid;\n align-content: stretch;\n position: relative;\n height: var(--bars-grid-h);\n padding-right: 10px;\n}\n.pdp-chart-yaxis-tick {\n position: absolute;\n right: 12px;\n font-size: 10.5px;\n color: var(--pc-fg-muted);\n font-feature-settings: 'tnum' 1, 'lnum' 1;\n letter-spacing: 0.02em;\n transform: translateY(-50%);\n text-align: right;\n white-space: nowrap;\n}\n\n.pdp-chart-bars-area {\n position: relative;\n height: var(--bars-grid-h);\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: minmax(0, 1fr);\n gap: 14px;\n align-items: end;\n padding: 0 4px;\n border-left: 1px solid color-mix(in srgb, var(--pc-fg-muted) 18%, transparent);\n}\n.pdp-chart-bars-gridline {\n position: absolute;\n left: 0;\n right: 0;\n height: 1px;\n background: color-mix(in srgb, var(--pc-fg-muted) 12%, transparent);\n pointer-events: none;\n}\n.pdp-chart-bars-baseline {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n background: color-mix(in srgb, var(--pc-fg-muted) 26%, transparent);\n}\n\n.pdp-chart-bars-bucket {\n display: grid;\n grid-template-rows: 1fr auto;\n gap: 8px;\n height: 100%;\n align-items: end;\n min-width: 0;\n}\n.pdp-chart-bars-stack {\n display: flex;\n align-items: end;\n gap: 4px;\n height: 100%;\n}\n.pdp-chart-bar {\n flex: 1 1 0;\n min-width: 0;\n height: var(--h);\n border-radius: 6px 6px 2px 2px;\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--bar-color) 88%, white 8%) 0%,\n var(--bar-color) 100%\n );\n position: relative;\n box-shadow:\n inset 0 1px 0 color-mix(in srgb, white 20%, transparent),\n 0 0 0 1px color-mix(in srgb, var(--bar-color) 24%, transparent);\n transform-origin: bottom;\n}\n.pdp-chart-frame.is-entering .pdp-chart-bar {\n animation: pdp-chart-bar-grow 800ms cubic-bezier(0.2, 0.7, 0.2, 1) backwards;\n animation-delay: var(--delay, 0ms);\n}\n.pdp-chart-bar-value {\n position: absolute;\n top: -22px;\n left: 50%;\n transform: translateX(-50%);\n font-size: 11px;\n font-weight: 500;\n color: var(--pc-fg);\n background: color-mix(in srgb, var(--pc-bg) 80%, transparent);\n padding: 1px 5px;\n border-radius: 6px;\n opacity: 1;\n font-feature-settings: 'tnum' 1, 'lnum' 1;\n white-space: nowrap;\n}\n.pdp-chart-frame.is-entering .pdp-chart-bar-value {\n opacity: 0;\n animation: pdp-chart-bar-value-in 400ms ease-out both;\n animation-delay: calc(var(--delay, 0ms) + 600ms);\n}\n\n.pdp-chart-bars-label {\n font-size: 11.5px;\n text-align: center;\n color: var(--pc-fg-muted);\n letter-spacing: 0.01em;\n line-height: 1.2;\n padding-top: 10px;\n overflow-wrap: break-word;\n max-width: 100%;\n}\n\n/* \u2500\u2500\u2500 line chart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-line {\n margin-top: 22px;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 8px;\n align-items: stretch;\n}\n.pdp-chart-line-yaxis {\n position: relative;\n height: 220px;\n padding-right: 10px;\n}\n.pdp-chart-line-area {\n position: relative;\n height: 220px;\n border-left: 1px solid color-mix(in srgb, var(--pc-fg-muted) 18%, transparent);\n}\n.pdp-chart-line-svg {\n width: 100%;\n height: 100%;\n display: block;\n overflow: visible;\n}\n.pdp-chart-line-stroke {\n fill: none;\n stroke: var(--line-color);\n stroke-width: 1.6;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 240 240;\n /* Resting state VISIBLE: the line is fully drawn at rest (offset 0). The\n draw-on reveal is re-armed only under .is-entering so a suppressed\n animation can never strand the line hidden (offset 240). */\n stroke-dashoffset: 0;\n filter: drop-shadow(0 4px 8px color-mix(in srgb, var(--line-color) 25%, transparent));\n}\n.pdp-chart-frame.is-entering .pdp-chart-line-stroke {\n stroke-dashoffset: 240;\n animation: pdp-chart-line-draw 1100ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n animation-delay: var(--delay, 0ms);\n}\n.pdp-chart-line-dot {\n fill: var(--line-color);\n stroke: hsl(25 10% 8%);\n stroke-width: 0.4;\n opacity: 1;\n}\n.pdp-chart-frame.is-entering .pdp-chart-line-dot {\n opacity: 0;\n animation: pdp-chart-line-dot-in 360ms ease-out both;\n animation-delay: calc(var(--delay, 0ms) + var(--dot-delay, 0ms));\n}\n.pdp-chart-line-gridline {\n stroke: color-mix(in srgb, var(--pc-fg-muted) 12%, transparent);\n stroke-width: 0.3;\n}\n.pdp-chart-line-labels {\n display: grid;\n grid-template-columns: repeat(var(--col-count, 1), 1fr);\n gap: 0;\n margin-top: 10px;\n margin-left: calc(var(--yaxis-w, 32px) + 1px);\n font-size: 11.5px;\n color: var(--pc-fg-muted);\n letter-spacing: 0.01em;\n}\n.pdp-chart-line-labels > span {\n text-align: center;\n line-height: 1.2;\n overflow-wrap: break-word;\n}\n\n/* \u2500\u2500\u2500 stylized comparison table for viz_kind=table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-table-wrap {\n margin-top: 18px;\n border-radius: 14px;\n overflow: hidden;\n border: 1px solid hsl(30 6% 22%);\n background: linear-gradient(180deg, hsl(25 10% 12%), hsl(25 10% 10%));\n}\n.pdp-chart-table {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n font-family: var(--pc-sans);\n color: var(--pc-fg);\n}\n.pdp-chart-table thead th {\n text-align: left;\n font-weight: 500;\n font-size: 10.5px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--pc-fg-muted);\n padding: 14px 18px;\n border-bottom: 1px solid hsl(30 6% 22%);\n background: hsl(25 10% 11%);\n white-space: nowrap;\n}\n.pdp-chart-table tbody td {\n padding: 14px 18px;\n border-bottom: 1px solid hsl(30 6% 17%);\n font-size: 14.5px;\n color: hsl(35 15% 86%);\n vertical-align: middle;\n}\n.pdp-chart-table tbody tr:last-child td {\n border-bottom: none;\n}\n.pdp-chart-table tbody td:first-child {\n font-family: var(--pc-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 50;\n font-size: 16px;\n letter-spacing: -0.005em;\n color: var(--pc-fg);\n white-space: nowrap;\n}\n.pdp-chart-table tbody tr.is-winning {\n background: linear-gradient(90deg, color-mix(in srgb, var(--pc-primary) 12%, transparent), transparent 80%);\n}\n.pdp-chart-table tbody tr.is-winning td:first-child {\n color: color-mix(in srgb, var(--pc-primary) 70%, white 30%);\n}\n.pdp-chart-table tbody tr.is-winning td:first-child::before {\n content: '\\\\2726 ';\n font-size: 11px;\n color: color-mix(in srgb, var(--pc-primary) 60%, white 40%);\n vertical-align: 1px;\n margin-right: 2px;\n}\n.pdp-chart-numeric-cell {\n font-feature-settings: 'tnum' 1, 'lnum' 1;\n color: hsl(35 15% 88%);\n}\n\n/* skeleton */\n.pdp-chart-skeleton {\n display: grid;\n gap: 14px;\n padding: 28px;\n border-radius: 22px;\n border: 1px solid hsl(30 6% 18%);\n background: hsl(25 10% 10%);\n}\n.pdp-chart-skeleton .pdp-skeleton-bar {\n height: 18px;\n border-radius: 6px;\n background: linear-gradient(90deg, hsl(25 10% 14%) 0%, hsl(25 10% 19%) 50%, hsl(25 10% 14%) 100%);\n background-size: 200% 100%;\n animation: pdp-chart-shimmer 1.6s ease-in-out infinite;\n}\n.pdp-chart-skeleton .pdp-skeleton-rect {\n border-radius: 12px;\n background: linear-gradient(90deg, hsl(25 10% 14%) 0%, hsl(25 10% 19%) 50%, hsl(25 10% 14%) 100%);\n background-size: 200% 100%;\n animation: pdp-chart-shimmer 1.6s ease-in-out infinite;\n}\n\n/* \u2500\u2500\u2500 mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n@media (max-width: 640px) {\n .pdp-chart-frame {\n padding: 22px 16px 24px;\n border-radius: 18px;\n }\n .pdp-chart-title {\n font-size: clamp(22px, 6.5vw, 30px);\n }\n .pdp-chart-rationale {\n font-size: 13.5px;\n padding-left: 12px;\n }\n .pdp-chart-table thead th,\n .pdp-chart-table tbody td {\n padding: 12px 12px;\n }\n .pdp-chart-table tbody td:first-child {\n font-size: 14.5px;\n }\n .pdp-chart-table tbody td {\n font-size: 13px;\n }\n .pdp-chart-bars,\n .pdp-chart-line {\n --bars-grid-h: 180px;\n }\n .pdp-chart-bars-area {\n gap: 8px;\n }\n .pdp-chart-bars-label {\n font-size: 10.5px;\n line-height: 1.15;\n }\n .pdp-chart-bar-value {\n font-size: 10px;\n top: -18px;\n }\n .pdp-chart-line-yaxis,\n .pdp-chart-line-area {\n height: 180px;\n }\n .pdp-chart-line-labels {\n font-size: 10.5px;\n }\n}\n\n/* \u2500\u2500\u2500 keyframes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n@keyframes pdp-chart-enter {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes pdp-chart-bar-grow {\n from { transform: scaleY(0); }\n to { transform: scaleY(1); }\n}\n@keyframes pdp-chart-bar-value-in {\n from { opacity: 0; transform: translate(-50%, 4px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n@keyframes pdp-chart-line-draw {\n from { stroke-dashoffset: 240; }\n to { stroke-dashoffset: 0; }\n}\n@keyframes pdp-chart-line-dot-in {\n from { opacity: 0; transform: scale(0.6); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes pdp-chart-shimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-chart-frame.is-entering,\n .pdp-chart-frame.is-entering .pdp-chart-bar,\n .pdp-chart-frame.is-entering .pdp-chart-bar-value,\n .pdp-chart-frame.is-entering .pdp-chart-line-stroke,\n .pdp-chart-frame.is-entering .pdp-chart-line-dot {\n opacity: 1;\n transform: none;\n stroke-dashoffset: 0;\n animation: none;\n }\n}\n`;\n\n// \u2500\u2500\u2500 widget \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Number of bar/line color slots in the CSS palette. */\nconst PALETTE_SIZE = 4;\n\nfunction colorVar(seriesIdx: number): string {\n return `var(--pc-color-${seriesIdx % PALETTE_SIZE})`;\n}\n\nexport class PdpChartLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: ChartData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureChartStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-chart-frame');\n if (!container) return;\n this._armed = true;\n // frame enter 700ms+120ms; per-point line/bar reveals run out to ~1.7s once\n // their runtime --delay stacks. Belt oversized so the class is never pulled\n // mid-reveal; base state is already the settled (visible) frame.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 2500\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-chart-skeleton\">\n <div class=\"pdp-skeleton-bar\" style=\"width:50%\"></div>\n <div class=\"pdp-skeleton-rect\" style=\"height:200px\"></div>\n </div>`;\n }\n\n const isTable = this.data.viz_kind === 'table';\n const allSeriesOnePoint = this.data.series.every((s) => s.points.length <= 1);\n // Degrade line \u2192 bar when there is only one point per series; a single\n // point can't define a line and would otherwise render as an empty axis.\n const effectiveKind: 'bar' | 'line' | 'table' = isTable\n ? 'table'\n : this.data.viz_kind === 'line' && !allSeriesOnePoint\n ? 'line'\n : 'bar';\n\n const isMultiSeries = this.data.series.length > 1;\n\n return html`\n <section\n class=\"${this._entering ? 'pdp-chart pdp-chart-frame is-entering' : 'pdp-chart pdp-chart-frame'}\"\n aria-label=\"${this.data.title}\"\n >\n <div class=\"pdp-chart-eyebrow\">\n <span class=\"pdp-chart-eyebrow-dot\" aria-hidden=\"true\"></span>\n <span>Computed comparison</span>\n </div>\n <h3 class=\"pdp-chart-title\">${this.data.title}</h3>\n\n ${effectiveKind === 'table' ? this.#renderTable() : nothing}\n ${effectiveKind === 'bar' ? this.#renderBars() : nothing}\n ${effectiveKind === 'line' ? this.#renderLine() : nothing}\n ${effectiveKind !== 'table' && isMultiSeries ? this.#renderLegend() : nothing}\n ${\n this.data.rationale\n ? html`<p class=\"pdp-chart-rationale\">${this.data.rationale}</p>`\n : nothing\n }\n </section>\n `;\n }\n\n #renderLegend() {\n // render() invokes helpers only after its data guard.\n const data = this.data as ChartData;\n return html`\n <ol class=\"pdp-chart-legend\" aria-label=\"Legend\">\n ${data.series.map(\n (s, idx) => html`\n <li class=\"pdp-chart-legend-item\">\n <span\n class=\"pdp-chart-legend-swatch\"\n style=\"background:${colorVar(idx)}\"\n aria-hidden=\"true\"\n ></span>\n <span>${s.name}</span>\n </li>\n `\n )}\n </ol>\n `;\n }\n\n #renderTable() {\n const matrix = buildComparisonMatrix(this.data as ChartData);\n return html`\n <div class=\"pdp-chart-table-wrap\">\n <table class=\"pdp-chart-table\">\n <thead>\n <tr>\n <th></th>\n ${matrix.colLabels.map((label) => html`<th>${label}</th>`)}\n </tr>\n </thead>\n <tbody>\n ${matrix.rowLabels.map(\n (row, rowIdx) => html`\n <tr class=${rowIdx === matrix.winningRow ? 'is-winning' : ''}>\n <td>${row}</td>\n ${(matrix.cells[rowIdx] as (number | undefined)[]).map(\n (cell) => html`<td class=\"pdp-chart-numeric-cell\">${formatNum(cell)}</td>`\n )}\n </tr>\n `\n )}\n </tbody>\n </table>\n </div>\n `;\n }\n\n #renderBars() {\n const data = this.data as ChartData;\n const { buckets, yMax } = buildBarBuckets(data);\n const ticks = niceTicks(yMax);\n // Use the max tick (which is \u2265 yMax) for height scaling so bar tops sit at\n // or below the highest gridline rather than overflowing it.\n const scaleMax = Math.max(yMax, ticks[ticks.length - 1] as number);\n return html`\n <div\n class=\"pdp-chart-bars\"\n role=\"figure\"\n aria-label=\"${data.title}\"\n data-bar-chart=\"\"\n >\n <div class=\"pdp-chart-yaxis\" aria-hidden=\"true\">\n ${ticks.map(\n (t) => html`\n <span\n class=\"pdp-chart-yaxis-tick\"\n style=\"bottom:${(t / scaleMax) * 100}%\"\n >\n ${formatNum(t)}\n </span>\n `\n )}\n </div>\n <div class=\"pdp-chart-bars-area\">\n ${ticks.map(\n (t) => html`\n <div\n class=\"pdp-chart-bars-gridline\"\n style=\"bottom:${(t / scaleMax) * 100}%\"\n aria-hidden=\"true\"\n ></div>\n `\n )}\n <div class=\"pdp-chart-bars-baseline\" aria-hidden=\"true\"></div>\n ${buckets.map(\n (bucket, bIdx) => html`\n <div class=\"pdp-chart-bars-bucket\">\n <div class=\"pdp-chart-bars-stack\">\n ${bucket.bars.map(\n (bar, barIdxInBucket) => html`\n <div\n class=\"pdp-chart-bar\"\n style=\"\n --h:${(Math.max(0, bar.value) / scaleMax) * 100}%;\n --bar-color:${colorVar(bar.seriesIdx)};\n --delay:${bIdx * 90 + barIdxInBucket * 30}ms;\n \"\n role=\"img\"\n aria-label=\"${bucket.label} \u2014 ${bar.seriesName}: ${formatNum(bar.value)}\"\n >\n ${\n bucket.bars.length === 1\n ? html`<span class=\"pdp-chart-bar-value\"\n >${formatNum(bar.value)}</span\n >`\n : nothing\n }\n </div>\n `\n )}\n </div>\n <div class=\"pdp-chart-bars-label\">${bucket.label}</div>\n </div>\n `\n )}\n </div>\n </div>\n `;\n }\n\n #renderLine() {\n const data = this.data as ChartData;\n const { series, xLabels, yMax } = buildLinePaths(data);\n const ticks = niceTicks(yMax);\n const scaleMax = Math.max(yMax, ticks[ticks.length - 1] as number);\n return html`\n <div\n class=\"pdp-chart-line\"\n role=\"figure\"\n aria-label=\"${data.title}\"\n style=\"--col-count:${xLabels.length}\"\n data-line-chart=\"\"\n >\n <div class=\"pdp-chart-line-yaxis\" aria-hidden=\"true\">\n ${ticks.map(\n (t) => html`\n <span\n class=\"pdp-chart-yaxis-tick\"\n style=\"bottom:${(t / scaleMax) * 100}%\"\n >\n ${formatNum(t)}\n </span>\n `\n )}\n </div>\n <div class=\"pdp-chart-line-area\">\n <svg\n class=\"pdp-chart-line-svg\"\n viewBox=\"0 0 100 60\"\n preserveAspectRatio=\"none\"\n aria-hidden=\"true\"\n >\n ${ticks.map((t) => {\n const y = 60 - (t / scaleMax) * 56 - 2;\n return svg`<line class=\"pdp-chart-line-gridline\" x1=\"0\" y1=${y} x2=\"100\" y2=${y} />`;\n })}\n ${series.map(\n (s) => svg`\n <polyline\n class=\"pdp-chart-line-stroke\"\n style=${`--line-color:${colorVar(s.seriesIdx)}; --delay:${s.seriesIdx * 200}ms`}\n points=${s.points.map((p) => `${p.cx},${p.cy}`).join(' ')}\n />\n ${s.points.map(\n (p, idx) => svg`\n <circle\n class=\"pdp-chart-line-dot\"\n cx=${p.cx}\n cy=${p.cy}\n r=\"1.4\"\n style=${`--line-color:${colorVar(s.seriesIdx)}; --delay:${s.seriesIdx * 200}ms; --dot-delay:${800 + idx * 60}ms`}\n />\n `\n )}\n `\n )}\n </svg>\n </div>\n </div>\n <div\n class=\"pdp-chart-line-labels\"\n style=\"--col-count:${xLabels.length}\"\n aria-hidden=\"true\"\n >\n ${xLabels.map((label) => html`<span>${label}</span>`)}\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-chart')) {\n customElements.define('syntro-pdp-chart', PdpChartLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-chart': PdpChartLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface ChartPoint {\n x: string;\n y: number;\n}\ninterface ChartSeries {\n name: string;\n points: ChartPoint[];\n}\ninterface ChartData {\n title: string;\n viz_kind: 'bar' | 'line' | 'table';\n series: ChartSeries[];\n rationale: string;\n}\n\n/**\n * Bar-Stacked variant for the chart module.\n *\n * Renders the series as a horizontal bar chart entirely in CSS \u2014 no\n * <canvas>, no internet image deps. Each row is a category (x value)\n * with parallel bars per series, each bar scaled to the per-row max.\n * Reads as a classic data block: HIGH typographic density, lots of\n * labels, clear comparison across x.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-chart-bar-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-chart-bar-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-chart-bar {\n --c-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --c-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --c-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --c-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --c-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--c-sans);\n color: var(--c-fg);\n padding: 20px 22px 18px;\n border: 1px solid var(--c-rule);\n border-radius: 14px;\n background: hsl(25 10% 11%);\n}\n.pdp-chart-bar__title {\n font-size: 14px;\n font-weight: 600;\n margin: 0 0 16px;\n}\n.pdp-chart-bar__legend {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 18px;\n margin-bottom: 14px;\n font-size: 11px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--c-fg-muted);\n}\n.pdp-chart-bar__legend-dot {\n display: inline-block;\n width: 9px;\n height: 9px;\n border-radius: 2px;\n margin-right: 6px;\n vertical-align: middle;\n}\n.pdp-chart-bar__row {\n display: grid;\n grid-template-columns: 1fr;\n gap: 4px;\n margin-bottom: 12px;\n}\n.pdp-chart-bar__row-label {\n font-size: 12.5px;\n color: var(--c-fg);\n font-weight: 500;\n}\n.pdp-chart-bar__row-bars {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.pdp-chart-bar__bar-track {\n position: relative;\n background: hsl(25 10% 16%);\n border-radius: 3px;\n height: 18px;\n overflow: hidden;\n}\n.pdp-chart-bar__bar-fill {\n position: absolute;\n inset: 0 auto 0 0;\n border-radius: 3px;\n transition: width 600ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n.pdp-chart-bar__bar-value {\n position: absolute;\n right: 6px;\n top: 50%;\n transform: translateY(-50%);\n font-size: 11px;\n color: hsl(30 8% 80%);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n`;\n\n// Stable palette indexed by series order. Greens, golds, deep blues \u2014\n// safe across most themes. Wraps if more than the palette length.\nconst PALETTE = ['hsl(150 32% 50%)', 'hsl(40 75% 60%)', 'hsl(208 50% 55%)', 'hsl(320 38% 60%)'];\n\nexport class PdpChartBarStackedLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: ChartData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-chart-bar\" style=\"opacity:0.5;\">\u2026</div>`;\n const { title, series } = this.data;\n // Build a unique row order from the first series; fall back to\n // union across series if a later one introduces a new x.\n const allX = Array.from(new Set(series.flatMap((s) => s.points.map((p) => p.x))));\n // Global max for consistent scaling across rows. Otherwise a small\n // row would visually rival a tall row.\n const max = Math.max(...series.flatMap((s) => s.points.map((p) => p.y)), 1);\n return html`\n <section class=\"pdp-chart-bar\" aria-label=${title}>\n <h3 class=\"pdp-chart-bar__title\">${title}</h3>\n ${\n series.length > 1\n ? html`<div class=\"pdp-chart-bar__legend\">\n ${series.map(\n (s, i) => html`<span\n ><span\n class=\"pdp-chart-bar__legend-dot\"\n style=${`background: ${PALETTE[i % PALETTE.length]};`}\n ></span\n >${s.name}</span\n >`\n )}\n </div>`\n : ''\n }\n ${allX.map(\n (x) => html`<div class=\"pdp-chart-bar__row\">\n <div class=\"pdp-chart-bar__row-label\">${x}</div>\n <div class=\"pdp-chart-bar__row-bars\">\n ${series.map((s, i) => {\n const point = s.points.find((p) => p.x === x);\n if (!point) return '';\n const pct = (point.y / max) * 100;\n return html`<div class=\"pdp-chart-bar__bar-track\">\n <div\n class=\"pdp-chart-bar__bar-fill\"\n style=${`width: ${pct.toFixed(1)}%; background: ${PALETTE[i % PALETTE.length]};`}\n ></div>\n <span class=\"pdp-chart-bar__bar-value\">${point.y}</span>\n </div>`;\n })}\n </div>\n </div>`\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-chart-bar')) {\n customElements.define('syntro-pdp-chart-bar', PdpChartBarStackedLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-chart-bar': PdpChartBarStackedLit;\n }\n}\n", "import { html, LitElement, svg } from 'lit';\n\ninterface ChartPoint {\n x: string;\n y: number;\n}\ninterface ChartSeries {\n name: string;\n points: ChartPoint[];\n}\ninterface ChartData {\n title: string;\n viz_kind: 'bar' | 'line' | 'table';\n series: ChartSeries[];\n rationale: string;\n}\n\n/**\n * Metric-Pull variant for the chart module.\n *\n * Pulls the maximum data point from the first series as the headline\n * metric \u2014 rendered as a giant typographic number. The remaining\n * points render as a tiny SVG sparkline below. Delta vs the mean is\n * shown as a small +/- pill. LOW density rhythm-break beat between\n * heavier modules.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-chart-metric-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-chart-metric-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-chart-met {\n --c-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --c-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --c-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --c-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --c-display: var(--syntro-pdp-display, inherit);\n --c-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--c-sans);\n color: var(--c-fg);\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 22px 28px;\n padding: 24px 26px;\n border: 1px solid var(--c-rule);\n border-radius: 16px;\n background:\n radial-gradient(120% 80% at 0% 0%, hsl(150 32% 38% / 0.10) 0%, transparent 60%),\n linear-gradient(180deg, hsl(25 10% 12%) 0%, hsl(25 10% 9%) 100%);\n align-items: start;\n}\n.pdp-chart-met__metric {\n display: flex;\n flex-direction: column;\n gap: 6px;\n align-self: center;\n}\n.pdp-chart-met__big {\n font-family: var(--c-display);\n font-weight: 380;\n font-variation-settings: 'SOFT' 100;\n font-size: clamp(48px, 12vw, 72px);\n line-height: 0.95;\n letter-spacing: -0.04em;\n color: var(--c-fg);\n font-variant-numeric: tabular-nums;\n}\n.pdp-chart-met__delta {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 9px;\n border-radius: 999px;\n font-size: 11px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n font-weight: 600;\n background: hsl(150 32% 32% / 0.25);\n color: hsl(150 35% 75%);\n width: max-content;\n}\n.pdp-chart-met__side {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.pdp-chart-met__title {\n font-size: 11px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--c-fg-muted);\n font-weight: 600;\n}\n.pdp-chart-met__label {\n font-size: 14px;\n font-weight: 500;\n color: var(--c-fg);\n line-height: 1.3;\n}\n.pdp-chart-met__spark {\n width: 100%;\n height: 38px;\n margin-top: 6px;\n}\n.pdp-chart-met__spark path.line {\n fill: none;\n stroke: var(--c-accent);\n stroke-width: 2;\n stroke-linejoin: round;\n stroke-linecap: round;\n}\n.pdp-chart-met__spark path.area {\n fill: var(--c-accent);\n opacity: 0.18;\n}\n.pdp-chart-met__spark circle.peak {\n fill: var(--c-accent);\n}\n`;\n\ntype NonEmptyPoints = [ChartPoint, ...ChartPoint[]];\n\nfunction buildSparkline(points: NonEmptyPoints): {\n linePath: string;\n areaPath: string;\n peakIndex: number;\n} {\n const w = 200;\n const h = 38;\n const xs = points.map((_, i) => (i / Math.max(points.length - 1, 1)) * (w - 4) + 2);\n const min = Math.min(...points.map((p) => p.y));\n const max = Math.max(...points.map((p) => p.y));\n const range = max - min || 1;\n const ys = points.map((p) => h - 4 - ((p.y - min) / range) * (h - 8));\n const line = xs\n .map((x, i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${ys[i].toFixed(1)}`)\n .join(' ');\n const area = `${line} L ${xs[xs.length - 1].toFixed(1)} ${h} L ${xs[0].toFixed(1)} ${h} Z`;\n const peakIndex = points.reduce((best, p, i) => (p.y > points[best].y ? i : best), 0);\n return { linePath: line, areaPath: area, peakIndex };\n}\n\nexport class PdpChartMetricPullLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: ChartData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-chart-met\" style=\"opacity:0.5;\">\u2026</div>`;\n const { title, series } = this.data;\n const first = series[0];\n if (!first || first.points.length === 0) return html``;\n // The guard above establishes the non-empty contract used by the peak and\n // sparkline calculations below.\n const points = first.points as NonEmptyPoints;\n const peak = points.reduce((b, p) => (p.y > b.y ? p : b), points[0]);\n const mean = points.reduce((s, p) => s + p.y, 0) / points.length;\n const delta = ((peak.y - mean) / Math.max(mean, 1)) * 100;\n const spark = buildSparkline(points);\n\n const w = 200;\n const h = 38;\n // A maximum of a finite, non-empty series is always >= that series' mean,\n // and buildSparkline always returns an index in the same non-empty series.\n const px = (spark.peakIndex / Math.max(points.length - 1, 1)) * (w - 4) + 2;\n const min = Math.min(...points.map((p) => p.y));\n const max = Math.max(...points.map((p) => p.y));\n const range = max - min || 1;\n const py = h - 4 - ((peak.y - min) / range) * (h - 8);\n\n return html`\n <section class=\"pdp-chart-met\" aria-label=${title}>\n <div class=\"pdp-chart-met__metric\">\n <span class=\"pdp-chart-met__big\">${peak.y}</span>\n <span class=\"pdp-chart-met__delta\">+${delta.toFixed(0)}% vs avg</span>\n </div>\n <div class=\"pdp-chart-met__side\">\n <span class=\"pdp-chart-met__title\">${title}</span>\n <span class=\"pdp-chart-met__label\">Peak at ${peak.x} (${first.name})</span>\n <svg\n class=\"pdp-chart-met__spark\"\n viewBox=${`0 0 ${w} ${h}`}\n preserveAspectRatio=\"none\"\n aria-hidden=\"true\"\n >\n <path class=\"area\" d=${spark.areaPath}></path>\n <path class=\"line\" d=${spark.linePath}></path>\n ${svg`<circle class=\"peak\" cx=${px} cy=${py} r=\"3\"></circle>`}\n </svg>\n </div>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-chart-metric')) {\n customElements.define('syntro-pdp-chart-metric', PdpChartMetricPullLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-chart-metric': PdpChartMetricPullLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface QaItem {\n question: string;\n answer: string;\n}\ninterface QaData {\n items: QaItem[];\n rationale: string;\n}\n\n/**\n * Accordion-Multi variant for the Q&A module.\n *\n * Single column, multi-expand accordion (any number of items can be\n * open at once). Airy LOW density block. Tap a question to reveal its\n * answer; the chevron rotates. Pure CSS / details disclosure \u2014 no JS\n * state machine.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-qa-accordion-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-qa-accordion-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-qa-acc {\n --qa-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --qa-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --qa-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --qa-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --qa-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--qa-sans);\n color: var(--qa-fg);\n max-width: 720px;\n}\n.pdp-qa-acc__kicker {\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--qa-accent);\n font-weight: 600;\n margin: 0 0 20px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.pdp-qa-acc__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--qa-accent);\n opacity: 0.8;\n}\n.pdp-qa-acc__item {\n border-bottom: 1px solid var(--qa-rule);\n}\n.pdp-qa-acc__item[open] {\n background: linear-gradient(180deg, hsl(150 24% 14% / 0.18) 0%, transparent 100%);\n}\n.pdp-qa-acc__item summary {\n list-style: none;\n cursor: pointer;\n display: flex;\n gap: 14px;\n align-items: flex-start;\n padding: 18px 4px;\n font-size: 15.5px;\n font-weight: 500;\n line-height: 1.35;\n color: var(--qa-fg);\n}\n.pdp-qa-acc__item summary::-webkit-details-marker {\n display: none;\n}\n.pdp-qa-acc__chev {\n flex: 0 0 14px;\n width: 14px;\n height: 14px;\n margin-top: 4px;\n border-right: 2px solid var(--qa-accent);\n border-bottom: 2px solid var(--qa-accent);\n transform: rotate(-45deg);\n transition: transform 200ms ease;\n}\n.pdp-qa-acc__item[open] .pdp-qa-acc__chev {\n transform: rotate(45deg);\n}\n.pdp-qa-acc__a {\n padding: 0 4px 20px 28px;\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--qa-fg-muted);\n margin: 0;\n max-width: 56ch;\n}\n`;\n\nexport class PdpQaAccordionLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: QaData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-qa-acc\" style=\"opacity:0.5;\">\u2026</div>`;\n return html`\n <section class=\"pdp-qa-acc\" aria-label=\"Frequently asked questions\">\n ${this.data.items.map(\n (it) => html`<details class=\"pdp-qa-acc__item\">\n <summary>\n <span class=\"pdp-qa-acc__chev\" aria-hidden=\"true\"></span>\n <span>${it.question}</span>\n </summary>\n <p class=\"pdp-qa-acc__a\">${it.answer}</p>\n </details>`\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-qa-accordion')) {\n customElements.define('syntro-pdp-qa-accordion', PdpQaAccordionLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-qa-accordion': PdpQaAccordionLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface QaItem {\n question: string;\n answer: string;\n}\ninterface QaData {\n items: QaItem[];\n rationale: string;\n}\n\n/**\n * Side-by-Side variant for the Q&A module.\n *\n * Two-column reading block. Question on the left (bold, narrow),\n * answer on the right (regular, wider). Hairline rules between pairs.\n * MED density \u2014 the reader is meant to absorb each pair, not skim.\n * Collapses to a stacked layout on narrow viewports.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-qa-side-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-qa-side-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-qa-side {\n --qa-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --qa-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --qa-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --qa-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --qa-display: var(--syntro-pdp-display, inherit);\n --qa-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--qa-sans);\n color: var(--qa-fg);\n}\n.pdp-qa-side__kicker {\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--qa-accent);\n font-weight: 600;\n margin: 0 0 22px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.pdp-qa-side__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--qa-accent);\n opacity: 0.8;\n}\n.pdp-qa-side__item {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 26px;\n padding: 22px 0;\n border-bottom: 1px solid var(--qa-rule);\n align-items: baseline;\n}\n.pdp-qa-side__item:last-child {\n border-bottom: 0;\n}\n@media (max-width: 600px) {\n .pdp-qa-side__item {\n grid-template-columns: 1fr;\n gap: 8px;\n padding: 18px 0;\n }\n}\n.pdp-qa-side__q {\n font-family: var(--qa-display);\n font-weight: 460;\n font-variation-settings: 'SOFT' 80;\n font-size: 18px;\n line-height: 1.22;\n letter-spacing: -0.012em;\n color: var(--qa-fg);\n margin: 0;\n}\n.pdp-qa-side__a {\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--qa-fg-muted);\n margin: 0;\n}\n`;\n\nexport class PdpQaSideBySideLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: QaData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-qa-side\" style=\"opacity:0.5;\">\u2026</div>`;\n return html`\n <section class=\"pdp-qa-side\" aria-label=\"Q&A\">\n ${this.data.items.map(\n (it) => html`<div class=\"pdp-qa-side__item\">\n <h4 class=\"pdp-qa-side__q\">${it.question}</h4>\n <p class=\"pdp-qa-side__a\">${it.answer}</p>\n </div>`\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-qa-side')) {\n customElements.define('syntro-pdp-qa-side', PdpQaSideBySideLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-qa-side': PdpQaSideBySideLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface RegionalData {\n region_label: string;\n temporal_label: string;\n headline: string;\n body: string;\n why_this_matters: string;\n rationale: string;\n}\n\n/**\n * Full-Bleed variant for the regional module.\n *\n * Edge-to-edge colored band with a locale SVG glyph (compass-style),\n * the temporal/regional kicker, a large display headline, supporting\n * body, and the \"why this matters\" tagline. LOW density rhythm-break\n * beat between heavier modules.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-regional-fullbleed-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-regional-fullbleed-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-reg-fb {\n --r-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --r-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --r-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --r-display: var(--syntro-pdp-display, inherit);\n --r-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--r-sans);\n color: var(--r-fg);\n margin: 0 calc(-1 * var(--syntro-pdp-bleed, 0px));\n}\n.pdp-reg-fb__panel {\n position: relative;\n border-radius: 22px;\n padding: 40px 28px 36px;\n background:\n radial-gradient(120% 80% at 100% 0%, hsl(150 32% 38% / 0.18) 0%, transparent 60%),\n linear-gradient(180deg, hsl(150 22% 14%) 0%, hsl(25 10% 9%) 100%);\n border: 1px solid hsl(30 6% 18%);\n overflow: hidden;\n}\n.pdp-reg-fb__glyph {\n position: absolute;\n top: 22px;\n right: 22px;\n width: 56px;\n height: 56px;\n color: var(--r-accent);\n opacity: 0.42;\n pointer-events: none;\n}\n.pdp-reg-fb__kicker {\n display: flex;\n gap: 14px;\n font-size: 10.5px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--r-fg-muted);\n font-weight: 600;\n margin: 0 0 16px;\n}\n.pdp-reg-fb__kicker-region {\n color: var(--r-accent);\n}\n.pdp-reg-fb__headline {\n font-family: var(--r-display);\n font-weight: 440;\n font-variation-settings: 'SOFT' 85;\n margin: 0 0 16px;\n font-size: clamp(24px, 5vw, 32px);\n line-height: 1.1;\n letter-spacing: -0.018em;\n color: var(--r-fg);\n max-width: 18ch;\n}\n.pdp-reg-fb__body {\n font-size: 14.5px;\n line-height: 1.55;\n color: hsl(30 8% 80%);\n margin: 0 0 22px;\n max-width: 50ch;\n}\n.pdp-reg-fb__why {\n font-size: 12px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--r-fg-muted);\n font-weight: 500;\n padding-top: 16px;\n border-top: 1px solid hsl(30 6% 20%);\n}\n.pdp-reg-fb__why-body {\n text-transform: none;\n letter-spacing: 0;\n color: var(--r-fg);\n font-weight: 400;\n font-size: 13.5px;\n margin-left: 10px;\n font-style: italic;\n}\n`;\n\nexport class PdpRegionalFullBleedLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: RegionalData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-reg-fb\" style=\"opacity:0.5;\">\u2026</div>`;\n const d = this.data;\n return html`\n <section class=\"pdp-reg-fb\" aria-label=\"Where you are\">\n <article class=\"pdp-reg-fb__panel\">\n <svg class=\"pdp-reg-fb__glyph\" viewBox=\"0 0 56 56\" aria-hidden=\"true\">\n <circle\n cx=\"28\"\n cy=\"28\"\n r=\"22\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n />\n <path\n d=\"M28 12 L34 28 L28 44 L22 28 Z\"\n fill=\"currentColor\"\n opacity=\"0.5\"\n />\n <path d=\"M28 8 L28 14 M28 42 L28 48 M8 28 L14 28 M42 28 L48 28\"\n stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </svg>\n <div class=\"pdp-reg-fb__kicker\">\n <span class=\"pdp-reg-fb__kicker-region\">${d.region_label}</span>\n <span>${d.temporal_label}</span>\n </div>\n <h3 class=\"pdp-reg-fb__headline\">${d.headline}</h3>\n <p class=\"pdp-reg-fb__body\">${d.body}</p>\n <div class=\"pdp-reg-fb__why\">\n <span>Why this matters</span>\n <span class=\"pdp-reg-fb__why-body\">${d.why_this_matters}</span>\n </div>\n </article>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-regional-fullbleed')) {\n customElements.define('syntro-pdp-regional-fullbleed', PdpRegionalFullBleedLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-regional-fullbleed': PdpRegionalFullBleedLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface RegionalData {\n region_label: string;\n temporal_label: string;\n headline: string;\n body: string;\n why_this_matters: string;\n rationale: string;\n}\n\n/**\n * Inline-Note variant for the regional module.\n *\n * Compact italic callout with icon-left. Smallest footprint \u2014\n * informational rather than promotional. Headline is the bold lead;\n * body stays small; \"why this matters\" is suppressed in this variant\n * to keep the footprint genuinely tiny.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-regional-inline-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-regional-inline-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-reg-in {\n --r-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --r-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --r-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --r-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --r-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--r-sans);\n color: var(--r-fg);\n display: grid;\n grid-template-columns: 28px 1fr;\n gap: 14px;\n align-items: start;\n padding: 14px 16px;\n border-left: 2px solid var(--r-accent);\n background: hsl(150 16% 12% / 0.5);\n border-radius: 0 12px 12px 0;\n}\n.pdp-reg-in__icon {\n width: 22px;\n height: 22px;\n color: var(--r-accent);\n margin-top: 2px;\n}\n.pdp-reg-in__meta {\n font-size: 10.5px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--r-fg-muted);\n font-weight: 600;\n margin-bottom: 4px;\n}\n.pdp-reg-in__meta-region {\n color: var(--r-accent);\n margin-right: 10px;\n}\n.pdp-reg-in__headline {\n font-size: 14px;\n font-weight: 600;\n color: var(--r-fg);\n line-height: 1.35;\n margin: 0 0 4px;\n}\n.pdp-reg-in__body {\n font-size: 13px;\n line-height: 1.5;\n color: var(--r-fg-muted);\n margin: 0;\n font-style: italic;\n}\n`;\n\nexport class PdpRegionalInlineNoteLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: RegionalData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-reg-in\" style=\"opacity:0.5;\">\u2026</div>`;\n const d = this.data;\n return html`\n <aside class=\"pdp-reg-in\" aria-label=\"Regional note\">\n <svg class=\"pdp-reg-in__icon\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"/>\n <circle cx=\"11\" cy=\"11\" r=\"2.4\" fill=\"currentColor\"/>\n <path d=\"M11 2 L11 5 M11 17 L11 20 M2 11 L5 11 M17 11 L20 11\"\n stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </svg>\n <div>\n <div class=\"pdp-reg-in__meta\">\n <span class=\"pdp-reg-in__meta-region\">${d.region_label}</span>\n <span>${d.temporal_label}</span>\n </div>\n <p class=\"pdp-reg-in__headline\">${d.headline}</p>\n <p class=\"pdp-reg-in__body\">${d.body}</p>\n </div>\n </aside>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-regional-inline')) {\n customElements.define('syntro-pdp-regional-inline', PdpRegionalInlineNoteLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-regional-inline': PdpRegionalInlineNoteLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\n/**\n * Neutral placeholder for PDP slots that don't have a live sub-agent in v1.\n * Shows a soft \"more personalized content coming soon\" message rather than\n * a misleading skeleton that implies a load is in flight.\n */\nexport class PdpSlotPlaceholderLit extends LitElement {\n static override properties = {\n label: { type: String },\n };\n\n label = 'More personalized content coming soon';\n\n override createRenderRoot() {\n return this;\n }\n\n override render() {\n return html`\n <div class=\"pdp-slot-placeholder\" data-placeholder>\n <p>${this.label}</p>\n <small>Ask in chat to explore this product further.</small>\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-slot-placeholder')) {\n customElements.define('syntro-pdp-slot-placeholder', PdpSlotPlaceholderLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-slot-placeholder': PdpSlotPlaceholderLit;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-ribbon> \u2014 the slim, italic \"personalized for you\" pill that\n * sits between a PDP hero and the first agent-authored section.\n *\n * Props:\n * - `topic` (string, required) \u2014 the phrase the agent personalized around\n * (\"absorption\", \"stimulant-free focus\", etc.).\n * Rendered in primary color, non-italic.\n * - `subtitle` (string, optional) \u2014 trailing fade-out text. Default: a copy\n * deck appropriate for v1 (\"Sections below\n * were authored just for you.\").\n *\n * Brand customization:\n * The widget reads three CSS variables from its host context; each has a\n * fallback so it just works in the dark / sage healthmaxxer-ish default.\n * Host pages can override:\n * --syntro-pdp-primary primary accent color\n * --syntro-pdp-fg body foreground color\n * --syntro-pdp-card-bg card background color\n * --syntro-pdp-display display font override (inherits host font if unset)\n */\n\nfunction ensureRibbonStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-ribbon-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-ribbon-style';\n s.textContent = RIBBON_CSS;\n document.head.appendChild(s);\n}\n\nconst RIBBON_CSS = `\n.syntro-pdp-ribbon {\n --rb-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --rb-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --rb-muted: var(--syntro-pdp-muted, hsl(30 8% 55%));\n --rb-bg-a: var(--syntro-pdp-ribbon-bg-a, hsl(25 10% 11%));\n --rb-bg-b: var(--syntro-pdp-ribbon-bg-b, hsl(25 10% 13%));\n --rb-display: var(--syntro-pdp-display, inherit);\n\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 22px;\n border-radius: 999px;\n border: 1px solid color-mix(in srgb, var(--rb-primary) 30%, transparent);\n background: linear-gradient(90deg, var(--rb-bg-a) 0%, var(--rb-bg-b) 50%, var(--rb-bg-a) 100%);\n overflow: hidden;\n /* Resting state is VISIBLE (GPU/compositing hazard fix): the entrance is\n gated behind the transient .is-entering class so a dropped/suppressed\n animation can never leave the ribbon permanently at opacity 0. */\n opacity: 1;\n color: var(--rb-fg);\n font-family: var(--syntro-pdp-sans, inherit);\n}\n.syntro-pdp-ribbon.is-entering {\n opacity: 0;\n animation: syntro-pdp-fade-up 700ms cubic-bezier(0.2, 0.7, 0.2, 1) 340ms both;\n}\n.syntro-pdp-ribbon::before {\n content: '';\n position: absolute;\n inset: 0;\n background: radial-gradient(\n 80% 120% at 50% 0%,\n color-mix(in srgb, var(--rb-primary) 10%, transparent) 0%,\n transparent 60%\n );\n pointer-events: none;\n}\n.syntro-pdp-ribbon-sweep {\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n transparent 0%,\n color-mix(in srgb, var(--rb-primary) 18%, transparent) 50%,\n transparent 100%\n );\n transform: translateX(-110%);\n animation: syntro-pdp-sweep 2200ms cubic-bezier(0.4, 0, 0.2, 1) 700ms forwards;\n pointer-events: none;\n}\n.syntro-pdp-ribbon-spark {\n flex: 0 0 auto;\n width: 16px;\n height: 16px;\n display: inline-block;\n color: var(--rb-primary);\n}\n.syntro-pdp-ribbon-copy {\n font-family: var(--rb-display);\n font-style: italic;\n font-size: 16px;\n line-height: 1.4;\n letter-spacing: -0.005em;\n font-variation-settings: 'SOFT' 60;\n margin: 0;\n position: relative;\n z-index: 1;\n}\n.syntro-pdp-ribbon-topic {\n font-style: normal;\n font-family: var(--syntro-pdp-sans, inherit);\n font-weight: 500;\n color: var(--rb-primary);\n font-size: 14.5px;\n letter-spacing: 0;\n}\n.syntro-pdp-ribbon-trail {\n color: var(--rb-muted);\n}\n\n@media (max-width: 640px) {\n .syntro-pdp-ribbon {\n border-radius: 22px;\n padding: 12px 16px;\n align-items: flex-start;\n }\n .syntro-pdp-ribbon-spark {\n margin-top: 2px;\n }\n .syntro-pdp-ribbon-copy {\n font-size: 14px;\n line-height: 1.45;\n }\n .syntro-pdp-ribbon-topic {\n font-size: 13.5px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .syntro-pdp-ribbon.is-entering {\n opacity: 1;\n animation: none;\n }\n .syntro-pdp-ribbon-sweep {\n animation: none;\n }\n}\n\n@keyframes syntro-pdp-fade-up {\n from { opacity: 0; transform: translateY(18px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes syntro-pdp-sweep {\n 0% { transform: translateX(-110%); }\n 100% { transform: translateX(110%); }\n}\n`;\n\nexport class PdpRibbonLit extends LitElement {\n static override properties = {\n topic: { type: String },\n subtitle: { type: String },\n _entering: { state: true },\n };\n\n topic = '';\n subtitle = 'Sections below were authored just for you.';\n\n /** Transient entrance flag \u2014 true until the mount animation settles. */\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureRibbonStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.syntro-pdp-ribbon');\n if (!container) return;\n this._armed = true;\n // fade-up: 700ms + 340ms delay; belt margin covers a late frame.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1300,\n true\n );\n }\n\n override render() {\n const cls = this._entering ? 'syntro-pdp-ribbon is-entering' : 'syntro-pdp-ribbon';\n return html`\n <div class=\"${cls}\" role=\"status\" aria-live=\"polite\">\n <span class=\"syntro-pdp-ribbon-sweep\" aria-hidden=\"true\"></span>\n <svg\n class=\"syntro-pdp-ribbon-spark\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5z\"\n />\n <path d=\"M19 4l.7 2L21 6.7l-1.3.7L19 9l-.7-1.6L17 6.7l1.3-.7z\" />\n </svg>\n <p class=\"syntro-pdp-ribbon-copy\">\n Tailored to your conversation about\n ${\n this.topic\n ? html` <span class=\"syntro-pdp-ribbon-topic\">${this.topic}</span>.`\n : nothing\n }\n <span class=\"syntro-pdp-ribbon-trail\"> ${this.subtitle}</span>\n </p>\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-ribbon')) {\n customElements.define('syntro-pdp-ribbon', PdpRibbonLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-ribbon': PdpRibbonLit;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-qa> \u2014 agent-authored Q&A section for a PDP slot.\n *\n * Each item is a `<details>` element so the open/closed UX is native (works\n * without JS, screen-reader-correct, keyboard-accessible). The default\n * disclosure triangle is hidden in favor of an animated plus/minus glyph\n * that matches the rest of the PDP visual vocabulary.\n *\n * Props:\n * - `data` (QaData): `{ items: [{question, answer}], rationale }`.\n *\n * Brand customization via the standard `--syntro-pdp-*` token namespace.\n */\n\ninterface QaItem {\n question: string;\n answer: string;\n}\n\ninterface QaData {\n items: QaItem[];\n rationale: string;\n}\n\nfunction ensureQaStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-qa-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-qa-style';\n s.textContent = QA_CSS;\n document.head.appendChild(s);\n}\n\nconst QA_CSS = `\n.pdp-qa {\n --qa-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --qa-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --qa-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --qa-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --qa-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --qa-display: var(--syntro-pdp-display, inherit);\n --qa-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--qa-sans);\n color: var(--qa-fg);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.pdp-qa-item {\n border: 1px solid var(--qa-border);\n border-radius: 16px;\n /* Surface tracks the theme card-bg token (dark host \u2192 dark, light host \u2192\n light) so the question text \u2014 which reads the theme fg token \u2014 never\n renders same-on-same. */\n background: linear-gradient(180deg, color-mix(in srgb, var(--qa-bg) 94%, white), var(--qa-bg));\n overflow: hidden;\n transition: border-color 200ms ease, background 200ms ease;\n /* Resting state VISIBLE (GPU/compositing hazard fix): items default to\n opacity 1; the stagger is re-armed only under .pdp-qa.is-entering so a\n suppressed animation can never leave the FAQ list blank. */\n opacity: 1;\n}\n.pdp-qa.is-entering .pdp-qa-item {\n opacity: 0;\n transform: translateY(12px);\n animation: pdp-qa-fade-up 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n}\n.pdp-qa.is-entering .pdp-qa-item:nth-child(1) { animation-delay: 60ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(2) { animation-delay: 140ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(3) { animation-delay: 220ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(4) { animation-delay: 300ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(5) { animation-delay: 380ms; }\n.pdp-qa-item:hover {\n border-color: color-mix(in srgb, var(--qa-primary) 35%, var(--qa-border));\n}\n.pdp-qa-item[open] {\n border-color: color-mix(in srgb, var(--qa-primary) 55%, var(--qa-border));\n background: linear-gradient(180deg, color-mix(in srgb, var(--qa-bg) 90%, white), var(--qa-bg));\n}\n\n.pdp-qa-question {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 18px 22px;\n cursor: pointer;\n list-style: none;\n user-select: none;\n font-family: var(--qa-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 60;\n font-size: clamp(16px, 2vw, 19px);\n line-height: 1.3;\n letter-spacing: -0.005em;\n color: var(--qa-fg);\n}\n.pdp-qa-question::-webkit-details-marker {\n display: none;\n}\n.pdp-qa-question::marker {\n display: none;\n}\n\n.pdp-qa-toggle {\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--qa-primary) 14%, transparent);\n display: grid;\n place-items: center;\n color: color-mix(in srgb, var(--qa-primary) 80%, white 20%);\n transition: background 200ms ease, transform 200ms ease;\n}\n.pdp-qa-item[open] .pdp-qa-toggle {\n background: var(--qa-primary);\n color: var(--qa-bg);\n transform: rotate(45deg);\n}\n.pdp-qa-toggle svg {\n width: 12px;\n height: 12px;\n}\n\n.pdp-qa-answer-wrap {\n padding: 0 22px 20px;\n color: var(--qa-fg-muted);\n font-size: 14.5px;\n line-height: 1.6;\n border-top: 1px solid color-mix(in srgb, var(--qa-primary) 20%, transparent);\n padding-top: 16px;\n margin-top: 0;\n}\n\n@media (max-width: 640px) {\n .pdp-qa-item {\n border-radius: 14px;\n }\n .pdp-qa-question {\n padding: 16px 18px;\n font-size: 15.5px;\n }\n .pdp-qa-answer-wrap {\n padding: 14px 18px 18px;\n font-size: 14px;\n }\n}\n\n/* skeleton */\n.pdp-qa-skeleton {\n display: grid;\n gap: 10px;\n}\n.pdp-qa-skeleton-row {\n height: 60px;\n border-radius: 14px;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--qa-bg) 92%, white) 0%,\n color-mix(in srgb, var(--qa-bg) 78%, white) 50%,\n color-mix(in srgb, var(--qa-bg) 92%, white) 100%\n );\n background-size: 200% 100%;\n animation: pdp-qa-shimmer 1.6s ease-in-out infinite;\n}\n\n@keyframes pdp-qa-fade-up {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes pdp-qa-shimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-qa.is-entering .pdp-qa-item {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n`;\n\nexport class PdpQaLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: QaData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureQaStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-qa');\n if (!container) return;\n this._armed = true;\n // staggered items: last delay 380ms + 600ms duration.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1200\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-qa-skeleton\">\n <div class=\"pdp-qa-skeleton-row\"></div>\n <div class=\"pdp-qa-skeleton-row\"></div>\n <div class=\"pdp-qa-skeleton-row\"></div>\n </div>`;\n }\n return html`\n <section\n class=\"${this._entering ? 'pdp-qa is-entering' : 'pdp-qa'}\"\n aria-label=\"Frequently asked questions\"\n >\n ${this.data.items.map(\n (item, idx) => html`\n <details class=\"pdp-qa-item\" ?open=${idx === 0}>\n <summary class=\"pdp-qa-question\">\n <span>${item.question}</span>\n <span class=\"pdp-qa-toggle\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.5\" stroke-linecap=\"round\">\n <path d=\"M12 5v14M5 12h14\" />\n </svg>\n </span>\n </summary>\n <div class=\"pdp-qa-answer-wrap\">${item.answer}</div>\n </details>\n `\n )}\n ${nothing}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-qa')) {\n customElements.define('syntro-pdp-qa', PdpQaLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-qa': PdpQaLit;\n }\n}\n", "import { html, LitElement } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-regional> \u2014 region- and temporal-context personalization tile.\n *\n * The agent surfaces a \"what's happening near you / right now that matters\n * for this product\" framing. The visual treatment is a single rich\n * full-width card with a quietly-textured background (suggests place /\n * season), a region \u00B7 time pill, a serif headline, body copy, and a\n * \"why this matters for you\" footer line tinted in brand color.\n *\n * Props:\n * - `data` (RegionalData): `{ region_label, temporal_label, headline,\n * body, why_this_matters, rationale }`.\n */\n\ninterface RegionalData {\n region_label: string; // \"Pacific Northwest\"\n temporal_label: string; // \"October 2026\"\n headline: string;\n body: string;\n why_this_matters: string;\n rationale: string;\n}\n\nfunction ensureRegionalStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-regional-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-regional-style';\n s.textContent = REGIONAL_CSS;\n document.head.appendChild(s);\n}\n\nconst REGIONAL_CSS = `\n.pdp-regional {\n --rg-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --rg-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 65%));\n --rg-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --rg-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --rg-accent: var(--syntro-pdp-accent, hsl(95 18% 48%));\n --rg-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --rg-display: var(--syntro-pdp-display, inherit);\n --rg-sans: var(--syntro-pdp-sans, inherit);\n\n position: relative;\n border: 1px solid var(--rg-border);\n border-radius: 24px;\n padding: 36px 36px 32px;\n overflow: hidden;\n font-family: var(--rg-sans);\n color: var(--rg-fg);\n background:\n radial-gradient(\n 90% 90% at 100% 100%,\n color-mix(in srgb, var(--rg-accent) 14%, transparent) 0%,\n transparent 60%\n ),\n radial-gradient(\n 70% 80% at 0% 0%,\n color-mix(in srgb, var(--rg-primary) 12%, transparent) 0%,\n transparent 65%\n ),\n linear-gradient(180deg, color-mix(in srgb, var(--rg-bg) 94%, white), var(--rg-bg));\n /* Resting state VISIBLE (GPU/compositing hazard fix) \u2014 entrance gated on\n the transient .is-entering class so a suppressed animation cannot strand\n the card at opacity 0. */\n opacity: 1;\n}\n.pdp-regional.is-entering {\n opacity: 0;\n transform: translateY(18px);\n animation: pdp-regional-enter 700ms cubic-bezier(0.2, 0.7, 0.2, 1) 120ms both;\n}\n\n/* Soft geographic-grid texture in the background \u2014 a personalization\n atmospheric, not a real map. */\n.pdp-regional::before {\n content: '';\n position: absolute;\n inset: 0;\n background-image:\n linear-gradient(\n to right,\n color-mix(in srgb, var(--rg-primary) 6%, transparent) 1px,\n transparent 1px\n ),\n linear-gradient(\n to bottom,\n color-mix(in srgb, var(--rg-primary) 6%, transparent) 1px,\n transparent 1px\n );\n background-size: 36px 36px;\n mask-image: radial-gradient(80% 80% at 50% 50%, black, transparent 75%);\n -webkit-mask-image: radial-gradient(80% 80% at 50% 50%, black, transparent 75%);\n pointer-events: none;\n opacity: 0.6;\n}\n\n.pdp-regional-pill {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 6px 12px 6px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--rg-primary) 12%, transparent);\n border: 1px solid color-mix(in srgb, var(--rg-primary) 30%, transparent);\n font-size: 11px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--rg-primary) 75%, white 25%);\n font-weight: 500;\n margin-bottom: 22px;\n}\n.pdp-regional-pill svg {\n width: 12px;\n height: 12px;\n flex: 0 0 auto;\n}\n.pdp-regional-pill-sep {\n width: 3px;\n height: 3px;\n border-radius: 50%;\n background: currentColor;\n opacity: 0.6;\n}\n\n.pdp-regional-headline {\n position: relative;\n font-family: var(--rg-display);\n font-weight: 400;\n font-size: clamp(26px, 3.4vw, 40px);\n line-height: 1.08;\n letter-spacing: -0.02em;\n color: var(--rg-fg);\n font-variation-settings: 'SOFT' 80, 'WONK' 1;\n margin: 0 0 16px;\n max-width: 22ch;\n}\n.pdp-regional-headline em {\n font-style: italic;\n font-variation-settings: 'SOFT' 100, 'WONK' 1;\n color: var(--rg-primary);\n}\n\n.pdp-regional-body {\n position: relative;\n font-size: 15.5px;\n line-height: 1.55;\n color: var(--rg-fg-muted);\n margin: 0;\n max-width: 56ch;\n}\n\n.pdp-regional-footer {\n position: relative;\n margin-top: 24px;\n padding-top: 18px;\n border-top: 1px dashed color-mix(in srgb, var(--rg-primary) 30%, transparent);\n display: flex;\n align-items: flex-start;\n gap: 12px;\n font-style: italic;\n font-family: var(--rg-display);\n font-size: 14.5px;\n font-variation-settings: 'SOFT' 70;\n color: color-mix(in srgb, var(--rg-primary) 65%, white 35%);\n line-height: 1.5;\n}\n.pdp-regional-footer-spark {\n flex: 0 0 auto;\n width: 16px;\n height: 16px;\n margin-top: 4px;\n}\n\n@media (max-width: 640px) {\n .pdp-regional {\n padding: 26px 22px 24px;\n border-radius: 20px;\n }\n .pdp-regional-pill {\n margin-bottom: 16px;\n font-size: 10px;\n }\n .pdp-regional-headline {\n font-size: clamp(22px, 7vw, 28px);\n max-width: 16ch;\n }\n .pdp-regional-body {\n font-size: 14px;\n }\n .pdp-regional-footer {\n margin-top: 18px;\n padding-top: 14px;\n font-size: 13px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-regional.is-entering {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n\n@keyframes pdp-regional-enter {\n from { opacity: 0; transform: translateY(18px); }\n to { opacity: 1; transform: translateY(0); }\n}\n`;\n\nexport class PdpRegionalLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: RegionalData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureRegionalStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-regional');\n if (!container) return;\n this._armed = true;\n // enter: 700ms + 120ms delay.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1100,\n true\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-qa-skeleton\" style=\"height:200px;\">\n <div class=\"pdp-qa-skeleton-row\" style=\"height:200px;border-radius:24px;\"></div>\n </div>`;\n }\n return html`\n <section\n class=\"${this._entering ? 'pdp-regional is-entering' : 'pdp-regional'}\"\n aria-label=\"Regional and temporal context\"\n >\n <div class=\"pdp-regional-pill\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 21s-7-6.5-7-12a7 7 0 1 1 14 0c0 5.5-7 12-7 12z\" />\n <circle cx=\"12\" cy=\"9\" r=\"2.5\" />\n </svg>\n <span>${this.data.region_label}</span>\n <span class=\"pdp-regional-pill-sep\" aria-hidden=\"true\"></span>\n <span>${this.data.temporal_label}</span>\n </div>\n <h3 class=\"pdp-regional-headline\">${this.data.headline}</h3>\n <p class=\"pdp-regional-body\">${this.data.body}</p>\n <p class=\"pdp-regional-footer\">\n <svg\n class=\"pdp-regional-footer-spark\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5z\" />\n </svg>\n ${this.data.why_this_matters}\n </p>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-regional')) {\n customElements.define('syntro-pdp-regional', PdpRegionalLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-regional': PdpRegionalLit;\n }\n}\n", "import { html, LitElement } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-peer-feed> \u2014 \"users like you\" social-proof feed.\n *\n * Renders 3\u20134 short usage snippets from users the agent considers similar\n * to the viewer (by goals, demographic, stack, etc.). Each card surfaces a\n * persona label, a verbatim-style quote, a similarity reason, and a\n * relative timestamp. The visual treatment is a stacked feed (think\n * intentionally-quiet timeline) rather than a uniform grid \u2014 so a long\n * quote can breathe.\n *\n * Props:\n * - `data` (PeerData): `{ peers: PeerPost[], rationale }`.\n */\n\ninterface PeerPost {\n initials: string; // \"M.K.\" \u2014 1\u20134 chars, displayed in an avatar circle\n persona: string; // \"Software engineer, 32, focus-oriented goals\"\n quote: string;\n similarity_reason: string; // \"shares your nootropic focus goal\"\n time_ago: string; // \"3 days ago\"\n}\n\ninterface PeerData {\n peers: PeerPost[];\n rationale: string;\n}\n\nfunction ensurePeerFeedStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-peer-feed-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-peer-feed-style';\n s.textContent = PEER_FEED_CSS;\n document.head.appendChild(s);\n}\n\nconst PEER_FEED_CSS = `\n.pdp-peer-feed {\n --pf-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pf-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 62%));\n --pf-bg-a: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --pf-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --pf-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pf-display: var(--syntro-pdp-display, inherit);\n --pf-sans: var(--syntro-pdp-sans, inherit);\n\n display: grid;\n gap: 14px;\n font-family: var(--pf-sans);\n color: var(--pf-fg);\n}\n\n.pdp-peer-post {\n position: relative;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 16px;\n padding: 20px 22px;\n border: 1px solid var(--pf-border);\n border-radius: 18px;\n /* Card surface derives from the theme card-bg token so it tracks the host:\n dark on a dark host (healthmaxxer), light on a light host (vela). A\n hardcoded dark surface here rendered dark-on-dark once the host set a\n dark --syntro-pdp-fg for its own light page. */\n background: linear-gradient(180deg, color-mix(in srgb, var(--pf-bg-a) 94%, white), var(--pf-bg-a));\n /* Resting state VISIBLE (GPU/compositing hazard fix): posts default to\n opacity 1 so a dropped/suppressed stagger can never leave the whole feed\n blank. The entrance is re-armed only under the container's .is-entering. */\n opacity: 1;\n transition: border-color 200ms ease;\n}\n.pdp-peer-feed.is-entering .pdp-peer-post {\n opacity: 0;\n transform: translateY(14px);\n animation: pdp-pf-fade-up 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n}\n.pdp-peer-post:hover {\n border-color: color-mix(in srgb, var(--pf-primary) 35%, var(--pf-border));\n}\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(1) { animation-delay: 60ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(2) { animation-delay: 140ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(3) { animation-delay: 220ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(4) { animation-delay: 300ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(5) { animation-delay: 380ms; }\n\n.pdp-peer-avatar {\n flex: 0 0 auto;\n width: 44px;\n height: 44px;\n border-radius: 50%;\n background: linear-gradient(\n 135deg,\n color-mix(in srgb, var(--pf-primary) 55%, white 8%),\n color-mix(in srgb, var(--pf-primary) 95%, black 10%)\n );\n /* Initials sit on the saturated primary chip; a near-white reads on any\n brand primary (green, maroon, \u2026), where a hardcoded near-black failed\n once the primary was a dark brand ink. */\n color: color-mix(in srgb, white 90%, var(--pf-primary));\n display: grid;\n place-items: center;\n font-family: var(--pf-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 70;\n font-size: 15px;\n letter-spacing: 0.01em;\n border: 1px solid color-mix(in srgb, var(--pf-primary) 60%, transparent);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--pf-primary) 8%, transparent);\n}\n\n.pdp-peer-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.pdp-peer-meta {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--pf-fg-muted);\n font-weight: 500;\n}\n.pdp-peer-persona {\n color: var(--pf-fg-muted);\n}\n.pdp-peer-meta-sep {\n opacity: 0.4;\n}\n.pdp-peer-time {\n color: var(--pf-fg-muted);\n}\n\n.pdp-peer-quote {\n font-family: var(--pf-display);\n font-style: italic;\n font-weight: 400;\n font-size: clamp(15px, 1.8vw, 17.5px);\n line-height: 1.5;\n font-variation-settings: 'SOFT' 70;\n color: var(--pf-fg);\n margin: 0;\n letter-spacing: -0.005em;\n}\n.pdp-peer-quote::before {\n content: '\u201C ';\n color: color-mix(in srgb, var(--pf-primary) 60%, transparent);\n font-size: 1.4em;\n vertical-align: -0.2em;\n margin-right: -2px;\n}\n.pdp-peer-quote::after {\n content: ' \u201D';\n color: color-mix(in srgb, var(--pf-primary) 60%, transparent);\n font-size: 1.4em;\n vertical-align: -0.2em;\n margin-left: -2px;\n}\n\n.pdp-peer-similarity {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n font-size: 12.5px;\n color: color-mix(in srgb, var(--pf-primary) 75%, white 25%);\n font-style: italic;\n font-family: var(--pf-display);\n margin-top: 2px;\n}\n.pdp-peer-similarity-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--pf-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-primary) 14%, transparent);\n}\n\n@media (max-width: 640px) {\n .pdp-peer-post {\n padding: 16px 16px;\n border-radius: 16px;\n gap: 12px;\n }\n .pdp-peer-avatar {\n width: 38px;\n height: 38px;\n font-size: 13px;\n }\n .pdp-peer-meta {\n font-size: 10px;\n }\n .pdp-peer-quote {\n font-size: 15px;\n }\n .pdp-peer-similarity {\n font-size: 12px;\n }\n}\n\n/* skeleton */\n.pdp-peer-skeleton {\n display: grid;\n gap: 12px;\n}\n.pdp-peer-skeleton-row {\n height: 100px;\n border-radius: 18px;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--pf-bg-a) 92%, white) 0%,\n color-mix(in srgb, var(--pf-bg-a) 78%, white) 50%,\n color-mix(in srgb, var(--pf-bg-a) 92%, white) 100%\n );\n background-size: 200% 100%;\n animation: pdp-pf-shimmer 1.6s ease-in-out infinite;\n}\n\n@keyframes pdp-pf-fade-up {\n from { opacity: 0; transform: translateY(14px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes pdp-pf-shimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-peer-feed.is-entering .pdp-peer-post {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n`;\n\nexport class PdpPeerFeedLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: PeerData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensurePeerFeedStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-peer-feed');\n if (!container) return;\n this._armed = true;\n // staggered posts: last delay 380ms + 600ms duration. No fast path \u2014 the\n // belt must outlast the last child so the class is never pulled mid-stagger.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1200\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-peer-skeleton\">\n <div class=\"pdp-peer-skeleton-row\"></div>\n <div class=\"pdp-peer-skeleton-row\"></div>\n <div class=\"pdp-peer-skeleton-row\"></div>\n </div>`;\n }\n return html`\n <section\n class=\"${this._entering ? 'pdp-peer-feed is-entering' : 'pdp-peer-feed'}\"\n aria-label=\"Users like you\"\n >\n ${this.data.peers.map(\n (peer) => html`\n <article class=\"pdp-peer-post\">\n <div class=\"pdp-peer-avatar\" aria-hidden=\"true\">${peer.initials}</div>\n <div class=\"pdp-peer-body\">\n <div class=\"pdp-peer-meta\">\n <span class=\"pdp-peer-persona\">${peer.persona}</span>\n <span class=\"pdp-peer-meta-sep\">\u00B7</span>\n <span class=\"pdp-peer-time\">${peer.time_ago}</span>\n </div>\n <p class=\"pdp-peer-quote\">${peer.quote}</p>\n <div class=\"pdp-peer-similarity\">\n <span class=\"pdp-peer-similarity-dot\" aria-hidden=\"true\"></span>\n <span>${peer.similarity_reason}</span>\n </div>\n </div>\n </article>\n `\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-peer-feed')) {\n customElements.define('syntro-pdp-peer-feed', PdpPeerFeedLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-peer-feed': PdpPeerFeedLit;\n }\n}\n", "/**\n * adaptive-product \u2014 Runtime manifest\n *\n * Exports the runtime descriptor consumed by the SDK's AppLoader.\n * Registers the four <syntro-product-*> custom elements as a side effect\n * of importing this module, and exposes widget mountables used by\n * SmartCanvasRuntime's WidgetRegistry.\n */\n\nimport { type MountPlumbing, stripMountPlumbing } from '@syntrologie/sdk-contracts';\nimport { startNativeSlotWatcher } from './native/nativeSlotWatcher';\nimport type {\n CardProps,\n ComparisonProps,\n GridProps,\n HeroProps,\n RecoProps,\n SyntroPdpProps,\n} from './schema';\nimport { ProductCardLit } from './widgets/ProductCardLit';\nimport { ProductComparisonLit } from './widgets/ProductComparisonLit';\nimport { ProductGridLit } from './widgets/ProductGridLit';\nimport { ProductHeroLit } from './widgets/ProductHeroLit';\nimport { ProductRecoCarouselLit } from './widgets/ProductRecoCarouselLit';\nimport './widgets/SyntroPdpLit';\nimport { SyntroPdpLit } from './widgets/SyntroPdpLit';\nimport './widgets/PdpTrendingNewsLit';\nimport './widgets/PdpTrendingNewsMagazineLit';\nimport './widgets/PdpTrendingNewsHeroLit';\nimport './widgets/PdpPeerFeedMasonryLit';\nimport './widgets/PdpPeerFeedCarouselLit';\nimport './widgets/PdpChartLit';\nimport './widgets/PdpChartBarStackedLit';\nimport './widgets/PdpChartMetricPullLit';\nimport './widgets/PdpQaAccordionLit';\nimport './widgets/PdpQaSideBySideLit';\nimport './widgets/PdpRegionalFullBleedLit';\nimport './widgets/PdpRegionalInlineNoteLit';\nimport './widgets/PdpSlotPlaceholderLit';\nimport './widgets/PdpRibbonLit';\nimport './widgets/PdpSectionHeaderLit';\nimport './widgets/PdpSlotLit';\nimport './widgets/PdpQaLit';\nimport './widgets/PdpRegionalLit';\nimport './widgets/PdpPeerFeedLit';\nimport './widgets/PdpOtherProductsCarouselLit';\nimport './widgets/PdpOtherProductsGridLit';\n\nconst TAGS = {\n card: 'syntro-product-card',\n comparison: 'syntro-product-comparison',\n hero: 'syntro-product-hero',\n grid: 'syntro-product-grid',\n pdp: 'syntro-pdp',\n reco: 'syntro-product-reco-carousel',\n} as const;\n\n/* v8 ignore next -- browser element registry guard; runtime import/idempotence is covered in jsdom. */\nif (typeof customElements !== 'undefined') {\n if (!customElements.get(TAGS.card)) customElements.define(TAGS.card, ProductCardLit);\n if (!customElements.get(TAGS.comparison))\n customElements.define(TAGS.comparison, ProductComparisonLit);\n if (!customElements.get(TAGS.hero)) customElements.define(TAGS.hero, ProductHeroLit);\n if (!customElements.get(TAGS.grid)) customElements.define(TAGS.grid, ProductGridLit);\n if (!customElements.get(TAGS.pdp)) customElements.define(TAGS.pdp, SyntroPdpLit);\n if (!customElements.get(TAGS.reco)) customElements.define(TAGS.reco, ProductRecoCarouselLit);\n}\n\n// Native-section takeover for hosts that render their own FAQ/review slots but\n// no <syntro-pdp> element \u2014 the vela SPA quick-view modal (FEAT-1784346204).\n// Runs AFTER the element registrations so the self-guard (`document.querySelector\n// ('syntro-pdp')`) sees an upgraded element on pages the element already owns and\n// stays inert there. Idempotent.\n/* v8 ignore next -- browser document guard; native watcher behavior is covered through its test hooks. */\nif (typeof document !== 'undefined') startNativeSlotWatcher();\n\n// \u2500\u2500\u2500 <syntro-pdp> runtime wirer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// The element needs a `runtimeRef` to gather signals + score the visitor\n// archetype for the config-first render. The widget mountable path\n// (runtime.widgets.mount) wires it\n// automatically when an action-plan tile mounts the element. But the\n// PRIMARY use case is customer JSX that places <syntro-pdp> directly \u2014\n// that path never calls widgets.mount, so runtimeRef would stay null\n// forever and the element would sit in skeleton state.\n//\n// Rather than have the element silently fall back to a global lookup\n// (which would obscure the contract), the runtime owns the wire-up\n// explicitly: when adaptive-product is registered against a runtime\n// instance, every <syntro-pdp> currently on the page gets `runtimeRef`\n// set, AND a MutationObserver watches for future additions so a\n// late-mounted element also gets wired. The element's only contract is\n// \"I need runtimeRef\" \u2014 provisioning is the runtime's job.\n//\n// Called once per (runtime, document) pair below in `register()`.\n\nconst _WIRED_RUNTIMES = new WeakSet<object>();\nconst _ACTIVE_OBSERVERS: MutationObserver[] = [];\n\ninterface PdpHostElement extends HTMLElement {\n runtimeRef?: unknown;\n}\n\nfunction wirePdpElements(runtime: object, doc: Document): MutationObserver {\n // Wire everything already in the DOM.\n for (const el of Array.from(doc.querySelectorAll<PdpHostElement>(TAGS.pdp))) {\n if (el.runtimeRef == null) el.runtimeRef = runtime;\n }\n // Watch for future additions.\n const observer = new MutationObserver((mutations) => {\n for (const m of mutations) {\n for (const node of Array.from(m.addedNodes)) {\n if (!(node instanceof HTMLElement)) continue;\n // Newly-added element itself\n if (node.tagName.toLowerCase() === TAGS.pdp) {\n const host = node as PdpHostElement;\n if (host.runtimeRef == null) host.runtimeRef = runtime;\n }\n // Any descendants\n for (const desc of Array.from(node.querySelectorAll<PdpHostElement>(TAGS.pdp))) {\n if (desc.runtimeRef == null) desc.runtimeRef = runtime;\n }\n }\n }\n });\n observer.observe(doc.documentElement, { childList: true, subtree: true });\n _ACTIVE_OBSERVERS.push(observer);\n return observer;\n}\n\n/**\n * Test seam. Disconnects all wirer observers and clears the\n * \"already wired\" set so subsequent tests can call `onActivate` again\n * with fresh state. NOT a production API.\n */\nexport const _testing = {\n resetWirer(): void {\n for (const obs of _ACTIVE_OBSERVERS) obs.disconnect();\n _ACTIVE_OBSERVERS.length = 0;\n // WeakSet has no clear() method; nothing held externally still\n // references the old runtimes, so they'll be collected. For the\n // pathological case of tests re-using the SAME runtime object\n // across resets, that's still no-op'd correctly because we just\n // disconnected the observer and the wire-on-existing path will\n // re-wire whatever's in the DOM on the next onActivate call.\n },\n};\n\n/**\n * AppRegistry calls this once at activation via the manifest's\n * `runtime.onActivate` hook (see cdn.ts). Wires runtimeRef onto every\n * <syntro-pdp> element on the page AND sets up a MutationObserver for\n * future additions. Idempotent per runtime instance \u2014 calling twice\n * with the same runtime is a no-op on the second call.\n *\n * Returns a teardown that disconnects the observer and removes the\n * runtime from the WeakSet. AppRegistry pushes the teardown onto its\n * cleanup chain so observers don't leak across deactivate/activate\n * cycles.\n *\n * Customer code never calls this directly.\n */\nexport function onActivate(runtime: unknown): (() => void) | void {\n if (typeof document === 'undefined') return;\n if (runtime == null || typeof runtime !== 'object') return;\n if (_WIRED_RUNTIMES.has(runtime)) return;\n _WIRED_RUNTIMES.add(runtime);\n const observer = wirePdpElements(runtime, document);\n return () => {\n observer.disconnect();\n const i = _ACTIVE_OBSERVERS.indexOf(observer);\n if (i >= 0) _ACTIVE_OBSERVERS.splice(i, 1);\n _WIRED_RUNTIMES.delete(runtime);\n };\n}\n\nfunction makeMountable<P extends object>(tag: string) {\n return {\n mount(container: HTMLElement, config?: (P & MountPlumbing) | null | undefined) {\n const widgetProps = stripMountPlumbing<P>(config ?? null);\n const el = document.createElement(tag) as HTMLElement & { props?: unknown };\n el.props = widgetProps;\n container.appendChild(el);\n return () => el.remove();\n },\n };\n}\n\nexport const ProductCardMountable = makeMountable<CardProps>(TAGS.card);\nexport const ProductComparisonMountable = makeMountable<ComparisonProps>(TAGS.comparison);\nexport const ProductHeroMountable = makeMountable<HeroProps>(TAGS.hero);\nexport const ProductGridMountable = makeMountable<GridProps>(TAGS.grid);\nexport const SyntroPdpMountable = makeMountable<SyntroPdpProps>(TAGS.pdp);\nexport const SyntroRecoMountable = makeMountable<RecoProps>(TAGS.reco);\n\nexport const runtime = {\n id: 'adaptive-product',\n version: '1.0.0',\n name: 'Product',\n description:\n 'Product surfaces: cards, side-by-side comparisons, featured hero, and grid lists. Optional bind for price/image to host-page sources (DOM, Shopify, WooCommerce, JSON-LD, OpenGraph).',\n executors: [],\n widgets: [\n {\n id: 'adaptive-product:card',\n component: ProductCardMountable,\n metadata: {\n name: 'Product Card',\n description: 'Single product card',\n icon: '\uD83D\uDECD\uFE0F',\n },\n },\n {\n id: 'adaptive-product:comparison',\n component: ProductComparisonMountable,\n metadata: {\n name: 'Product Comparison',\n description: '2\u20134 products, expandable rows',\n icon: '\u2696\uFE0F',\n },\n },\n {\n id: 'adaptive-product:hero',\n component: ProductHeroMountable,\n metadata: {\n name: 'Product Hero',\n description: 'Featured product with long copy',\n icon: '\u2B50',\n },\n },\n {\n id: 'adaptive-product:grid',\n component: ProductGridMountable,\n metadata: {\n name: 'Product Grid',\n description: '2\u201312 products in a grid',\n icon: '\uD83D\uDDC2\uFE0F',\n },\n },\n {\n id: 'adaptive-product:pdp',\n component: SyntroPdpMountable,\n metadata: {\n name: 'Dynamic PDP',\n description:\n 'Personalized product detail page surface. Config-first: scores the visitor archetype, mounts host presets, and generates live slots via /api/pdp/module + /api/pdp/dossier.',\n icon: '\uD83D\uDCC4',\n },\n },\n {\n id: 'adaptive-product:reco',\n component: SyntroRecoMountable,\n metadata: {\n name: 'Recommendation Carousel',\n description:\n 'Horizontally-scrollable carousel of recommended products with image, price, hook copy, and PDP links.',\n icon: '\uD83C\uDFA0',\n },\n },\n {\n // Alias: the reco carousel registered under its ELEMENT TAG as well as\n // its namespaced id. The `recommendations` tool is server-driven, so it\n // mounts the carousel as a tile whose `widget` is the raw tag\n // `syntro-product-reco-carousel` (frontend `SERVER_MOUNTED_TILE_WIDGETS`\n // self-map + backend `template_id=mount.widget`). On velvet that tile\n // lands in the DECK, whose `<sc-mount>` mounts THROUGH the WidgetRegistry\n // (`widgets.mount(tile.widget)`) \u2014 so the registry MUST recognise the tag\n // or the deck tile is a permanent blank card with zero render-health (the\n // widget class never constructs, so its own probe can't fire). This entry\n // makes the tag a first-class registry id, fulfilling the contract\n // `SERVER_MOUNTED_TILE_WIDGETS` documents. The inline (healthmaxxer chat\n // trail) path is unaffected \u2014 it uses `document.createElement(tag)`, which\n // never touched the registry. Same mountable as the namespaced entry.\n id: TAGS.reco,\n component: SyntroRecoMountable,\n metadata: {\n name: 'Recommendation Carousel',\n description:\n 'Horizontally-scrollable carousel of recommended products with image, price, hook copy, and PDP links.',\n icon: '\uD83C\uDFA0',\n },\n },\n ],\n};\n\nexport {\n runTakeover,\n type TakeoverOutcome,\n type TakeoverPorts,\n type TakeoverResult,\n} from './takeover/engine';\nexport { type DetachHandle, detachSection } from './takeover/sectionDetach';\n\nexport default runtime;\n"],
4
+ "sourcesContent": ["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\n\n/**\n * The Context type defines a type brand to associate a key value with the context value type\n */\nexport type Context<KeyType, ValueType> = KeyType & {__context__: ValueType};\n\n/**\n * @deprecated use Context instead\n */\nexport type ContextKey<KeyType, ValueType> = Context<KeyType, ValueType>;\n\n/**\n * A helper type which can extract a Context value type from a Context type\n */\nexport type ContextType<Key extends Context<unknown, unknown>> =\n Key extends Context<unknown, infer ValueType> ? ValueType : never;\n\n/**\n * Creates a typed Context.\n *\n * Contexts are compared with strict equality.\n *\n * If you want two separate `createContext()` calls to referer to the same\n * context, then use a key that will by equal under strict equality like a\n * string for `Symbol.for()`:\n *\n * ```ts\n * // true\n * createContext('my-context') === createContext('my-context')\n * // true\n * createContext(Symbol.for('my-context')) === createContext(Symbol.for('my-context'))\n * ```\n *\n * If you want a context to be unique so that it's guaranteed to not collide\n * with other contexts, use a key that's unique under strict equality, like\n * a `Symbol()` or object.:\n *\n * ```\n * // false\n * createContext({}) === createContext({})\n * // false\n * createContext(Symbol('my-context')) === createContext(Symbol('my-context'))\n * ```\n *\n * @param key a context key value\n * @template ValueType the type of value that can be provided by this context.\n * @returns the context key value cast to `Context<K, ValueType>`\n */\nexport function createContext<ValueType, K = unknown>(key: K) {\n return key as Context<K, ValueType>;\n}\n", "/**\n * Canvas runtime context \u2014 the shared @lit/context symbol both\n * runtime-sdk (the provider) and canvas-sdk / canvas authors (the\n * consumers) use to thread a narrow runtime handle through the canvas\n * element tree.\n *\n * Living here keeps the symbol identity stable across both packages.\n * If canvas-sdk created its own symbol with `createContext(...)`, it\n * would never match the one runtime-sdk publishes, and `<sc-mount>`\n * would silently see `undefined` instead of the widget registry.\n *\n * The shape declared here is a NARROW VIEW of `SmartCanvasRuntime`.\n * Canvas-side code reads only this subset. The runtime-sdk's\n * `SmartCanvasRuntime` type is a structural superset.\n */\nimport { createContext } from '@lit/context';\n/**\n * The @lit/context symbol. Both runtime-sdk's ContextProvider and\n * canvas-sdk's ContextConsumer must import THIS exact symbol \u2014 not a\n * symbol with the same string name \u2014 for context propagation to work.\n */\nexport const canvasRuntimeContext = createContext('syntrologie:canvas-runtime');\n", "/**\n * \"Dive deeper\" contract \u2014 the ONE canonical way a space-constrained tile\n * (FAQ, comparison, \u2026) hands its content context back to the chat conversation\n * as a deep-dive turn.\n *\n * A tile that can only show a shallow slice of its content (a comparison matrix\n * capped to a few rows, an FAQ answer clamped to a few lines) renders a \"Dive\n * deeper\" affordance. Tapping it dispatches a `window` CustomEvent carrying the\n * tile's content summary; the chat-bar mountable listens for exactly this event\n * and turns it into a user-visible chat turn so the agent can help \u2014 no\n * re-scrolling the cramped tile.\n *\n * THE PROMPT STATES AN UNMET NEED. IT NEVER ASKS A QUESTION (BUG-1784153527).\n * Emitters used to author `Tell me more \u2014 how do A vs B compare?` / `Tell me\n * more about: <question>` \u2014 direct imperatives, in the visitor's own voice. The\n * agent dutifully answered them and never asked what was actually missing,\n * which is catastrophic exactly when the tile is the problem: a live\n * \"VelaLift Sports Bra vs RunCloud Quarter Sock Pack\" comparison (two products\n * with no decision between them) got a straight-faced answer that had to\n * conclude \"the real question isn't which is better\u2026\".\n *\n * A question demands an answer; a complaint invites a question. The tap means\n * \"this tile didn't land\", so the turn says only that \u2014 the specifics (product\n * names, the question text, what's on screen) ride in `title` / `context` and\n * the turn-origin envelope, where they ground the agent WITHOUT putting an\n * answerable question in the visitor's mouth.\n *\n * This lives in the neutral `@syntrologie/sdk-contracts` (not in any single\n * adaptive) so BOTH the emitters (adaptive-product comparison, adaptive-faq)\n * and the listener (adaptive-chatbot chat bar) share one wire shape without\n * depending on each other. The event is canvas-agnostic \u2014 it never references a\n * deck, a slot, or a specific host canvas.\n */\n/** The `window` CustomEvent name carrying a {@link DiveDeeperDetail}. */\nexport const DIVE_DEEPER_EVENT = 'syntro:chat:dive-deeper';\n/**\n * Dispatch a {@link DIVE_DEEPER_EVENT} on `window`. Safe in non-DOM contexts\n * (SSR / tests without a window) \u2014 it no-ops. Emitters call this; they never\n * reach into the chat session directly, keeping the adaptive side decoupled\n * from the chat implementation.\n */\nexport function dispatchDiveDeeper(detail) {\n if (typeof window === 'undefined')\n return;\n window.dispatchEvent(new CustomEvent(DIVE_DEEPER_EVENT, { detail }));\n}\n", "/**\n * Mount contract types and helper for adaptive widget mountables.\n *\n * The `WidgetRegistry` in `@syntrologie/runtime-sdk` delivers props to each\n * mountable as `{ ...tile.props, instanceId, runtime, tileId? }` spread flat\n * (see `MountableContract.test.ts` in runtime-sdk for the end-to-end lockdown).\n *\n * Adaptives that strip plumbing manually maintain private blacklists that\n * silently drift when the contract grows (PR #2234 and #2238 documented this).\n * `stripMountPlumbing` centralizes the list so adding a new plumbing key in\n * the future is a one-line change here that every adaptive picks up automatically.\n *\n * Adaptives whose widget schemas use Zod `.strict()` MUST call this before\n * validating, or strict-mode will reject the runtime-injected keys and the\n * widget will silently render its empty/error state.\n */\nexport const MOUNT_PLUMBING_KEYS = ['instanceId', 'runtime', 'tileId'];\nexport function stripMountPlumbing(config) {\n if (!config || typeof config !== 'object') {\n return {};\n }\n const out = { ...config };\n for (const key of MOUNT_PLUMBING_KEYS) {\n delete out[key];\n }\n return out;\n}\n", "/**\n * Canonical route normalization. See `routes.md` for rules and\n * `normalize-route.cases.json` for the parity corpus shared with the\n * Python implementation in syntrologie_common/sdk/routing.py.\n *\n * Two exports \u2014 `normalizeRoute` for literal paths, `normalizeRoutePattern`\n * for activation patterns containing `*`, `**`, `:param`. Today they share\n * an implementation because the rules happen to be wildcard-safe (no\n * lowercase, unreserved-only decode, slash collapse preserves `**`).\n * The seam is preserved as separate exports so the API can diverge\n * without consumer churn if rules change.\n */\n// RFC 3986 reserved characters (gen-delims + sub-delims). When a `%XX`\n// sequence decodes to one of these bytes, we keep the percent-encoded\n// form \u2014 decoding would re-segment the path or change its meaning.\nconst RESERVED_BYTES = new Set([\n 0x21, // !\n 0x23, // #\n 0x24, // $\n 0x26, // &\n 0x27, // '\n 0x28, // (\n 0x29, // )\n 0x2a, // *\n 0x2b, // +\n 0x2c, // ,\n 0x2f, // /\n 0x3a, // :\n 0x3b, // ;\n 0x3d, // =\n 0x3f, // ?\n 0x40, // @\n 0x5b, // [\n 0x5d, // ]\n]);\nconst utf8Decoder = new TextDecoder('utf-8', { fatal: false });\n/** Decode `%XX` sequences for unreserved bytes only. Collapses\n * adjacent `%XX` runs into a UTF-8 decode so `%C3%A9` \u2192 `\u00E9`. */\nfunction decodeUnreservedOnly(input) {\n let out = '';\n let pending = [];\n const flushPending = () => {\n if (pending.length === 0)\n return;\n const bytes = new Uint8Array(pending);\n out += utf8Decoder.decode(bytes);\n pending = [];\n };\n let i = 0;\n while (i < input.length) {\n const ch = input[i];\n if (ch === '%' && i + 2 < input.length && isHex(input[i + 1]) && isHex(input[i + 2])) {\n const byte = parseInt(input.slice(i + 1, i + 3), 16);\n if (RESERVED_BYTES.has(byte)) {\n flushPending();\n // Keep raw, but normalize hex case to uppercase so the\n // canonical form is stable across input casing.\n out += `%${input.slice(i + 1, i + 3).toUpperCase()}`;\n i += 3;\n }\n else {\n pending.push(byte);\n i += 3;\n }\n }\n else {\n flushPending();\n out += ch;\n i += 1;\n }\n }\n flushPending();\n return out;\n}\nfunction isHex(c) {\n return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');\n}\n/** Strip query string and hash fragment. */\nfunction stripQueryAndHash(s) {\n const q = s.indexOf('?');\n if (q !== -1)\n s = s.slice(0, q);\n const h = s.indexOf('#');\n if (h !== -1)\n s = s.slice(0, h);\n return s;\n}\n/**\n * Normalize a literal path (e.g. `window.location.pathname`, an\n * action's `route` field, a wiki route key).\n *\n * Throws `TypeError` if the input is not an absolute path. Callers\n * that want a soft API should use {@link normalizeRouteWithChange}.\n */\nexport function normalizeRoute(path) {\n if (typeof path !== 'string' || path.length === 0) {\n throw new TypeError('normalizeRoute: input must be a non-empty string');\n }\n if (!path.startsWith('/')) {\n throw new TypeError(`normalizeRoute: input must be absolute (start with '/'); got ${JSON.stringify(path)}`);\n }\n let s = stripQueryAndHash(path);\n s = decodeUnreservedOnly(s);\n s = s.replace(/\\/+/g, '/');\n if (s.length > 1 && s.endsWith('/'))\n s = s.slice(0, -1);\n return s;\n}\n/**\n * Normalize an activation route pattern. Preserves `*`, `**`,\n * `:param` exactly. Today equivalent to {@link normalizeRoute} \u2014 kept\n * as a separate export so rules can diverge later without API churn.\n */\nexport function normalizeRoutePattern(pattern) {\n return normalizeRoute(pattern);\n}\n/**\n * Normalize a route and report whether the input was already\n * canonical. Used by authoring tools to decide whether to emit a\n * warning to the LLM.\n */\nexport function normalizeRouteWithChange(path) {\n const canonical = normalizeRoute(path);\n return { canonical, changed: canonical !== path };\n}\n/** Pattern-side counterpart of {@link normalizeRouteWithChange}. */\nexport function normalizeRoutePatternWithChange(pattern) {\n const canonical = normalizeRoutePattern(pattern);\n return { canonical, changed: canonical !== pattern };\n}\n/**\n * Case-insensitive comparison of two already-canonical paths. Use\n * this anywhere two routes are compared for equality (wiki lookups,\n * non-pattern action route gates) \u2014 preserves casing in the inputs\n * while honoring case-insensitive routing on the customer's site.\n */\nexport function routesMatch(a, b) {\n return a.toLowerCase() === b.toLowerCase();\n}\n", "/**\n * Shared Zod schemas for decision strategies, conditions, and event scoping.\n *\n * These are the canonical definitions \u2014 runtime-sdk and all adaptive packages\n * should import from here instead of duplicating.\n */\nimport { z } from 'zod';\n// =============================================================================\n// ANCHOR ID SCHEMA\n// =============================================================================\nexport const AnchorIdZ = z\n .object({\n selector: z.string(),\n route: z.union([z.string(), z.array(z.string())]),\n})\n .strict();\n// =============================================================================\n// AUTHORING FIELDS \u2014 id / title / description / validation\n//\n// Shared fields every action carries. `id` is the action identifier the\n// runtime uses to dispatch, dedupe, and drop/replace actions \u2014 it is NOT\n// stripped before serving. `title` / `description` / `validation` are\n// authoring-only metadata stripped server-side in `to_runtime_config`\n// (platform/backend/app/domains/experiments/helpers.py).\n//\n// They all appear in the JSON Schema (and therefore in the tactician's\n// prompt) because the LLM needs to know they are valid action properties \u2014\n// otherwise schema validation would reject what the prompt commands.\n//\n// Each action variant should `.extend(AuthoringFieldsZ)` alongside any\n// triggerWhen/condition extensions.\n// =============================================================================\nexport const AuthoringFieldsZ = {\n id: z.string().optional().describe('Stable action identifier (e.g. \"act_3db6a14d2ab0\").'),\n title: z\n .string()\n .max(200)\n .optional()\n .describe('Authoring-only: short label shown on the action plan dashboard. Stripped before serving to the runtime SDK.'),\n description: z\n .string()\n .max(1000)\n .optional()\n .describe('Authoring-only: one-sentence explanation of what this action does and why. Stripped before serving to the runtime SDK.'),\n validation: z\n .array(z.string().max(500))\n .max(10)\n .optional()\n .describe('Authoring-only: ordered steps a reviewer can follow to trigger this action and visually confirm it works. Each entry is one step. Stripped before serving to the runtime SDK.'),\n};\n// =============================================================================\n// TRIGGER VOCABULARY \u2014 canonical lists of valid event names, metric keys, etc.\n// These flow through to the JSON schema as enums and are used by the LLM prompt.\n// =============================================================================\n/** Events that can be counted in event_count conditions.\n *\n * Every value here must be an event the runtime actually emits \u2014 either a\n * PostHog-autocapture normalization (ui.click/scroll/input/change/submit) or\n * an event-processor detector (ui.hover/idle/scroll_thrash/focus_bounce/\n * hesitate/rage_click). Do not add aspirational names; a trigger counting an\n * event nothing emits never fires.\n */\nexport const COUNTABLE_EVENTS = [\n // User interactions (from PostHog autocapture normalization)\n 'ui.click',\n 'ui.scroll',\n 'ui.input',\n 'ui.change',\n 'ui.submit',\n // Behavioral detectors (from event-processor)\n 'ui.hover',\n 'ui.idle',\n 'ui.scroll_thrash',\n 'ui.focus_bounce',\n 'ui.hesitate',\n 'ui.rage_click',\n // Navigation\n 'nav.page_view',\n 'nav.page_leave',\n];\nexport const CountableEventZ = z\n .enum(COUNTABLE_EVENTS)\n .describe('Event name to count. ui.* = user interactions and behavioral detectors (hesitate, rage_click, scroll_thrash, focus_bounce, idle, hover); nav.* = page navigation.');\n/** Valid session metric keys. */\nexport const SESSION_METRIC_KEYS = ['time_on_page', 'page_views', 'scroll_depth'];\nexport const SessionMetricKeyZ = z\n .enum(SESSION_METRIC_KEYS)\n .describe('Session metric key. time_on_page = seconds on current page, page_views = pages visited this session, scroll_depth = 0-100 percentage.');\n/** Element chain match field prefixes for counter filters. */\nexport const ELEMENT_MATCH_FIELDS = ['tag_name', '$el_text'];\n// Note: attr__* is a dynamic prefix (attr__data-id, attr__class, attr__href, etc.)\n// and cannot be enumerated. The match key is either one of ELEMENT_MATCH_FIELDS\n// or starts with \"attr__\".\n// =============================================================================\n// CONDITION SCHEMAS\n// =============================================================================\nexport const PageUrlConditionZ = z\n .object({\n type: z.literal('page_url'),\n url: z.string().describe('URL path to match (e.g. \"/pricing\", \"/dashboard\")'),\n})\n .describe('Fires when the current page URL matches. Use for page-specific actions. ' +\n 'Example: {\"type\": \"page_url\", \"url\": \"/pricing\"}');\nexport const RouteConditionZ = z\n .object({\n type: z.literal('route'),\n routeId: z.string().describe('Named route ID from the route filter'),\n})\n .describe('Fires when the current route matches a named route ID.');\nexport const AnchorVisibleConditionZ = z\n .object({\n type: z.literal('anchor_visible'),\n anchorId: z.string().describe('CSS selector of the anchor element'),\n state: z\n .enum(['visible', 'present', 'absent'])\n .describe('\"visible\" = in viewport, \"present\" = in DOM, \"absent\" = not in DOM'),\n})\n .describe(\"Fires based on a DOM element's visibility state. \" +\n 'Example: {\"type\": \"anchor_visible\", \"anchorId\": \"#cta-button\", \"state\": \"visible\"}');\nexport const EventOccurredConditionZ = z\n .object({\n type: z.literal('event_occurred'),\n eventName: z.string().describe('Event name (e.g. \"ui.click\", \"$pageview\")'),\n withinMs: z.number().optional().describe('Time window in ms. Omit = any time this session.'),\n})\n .describe('Fires when a specific event has occurred during this session. ' +\n 'Example: {\"type\": \"event_occurred\", \"eventName\": \"ui.click\", \"withinMs\": 5000}');\nexport const StateEqualsConditionZ = z\n .object({\n type: z.literal('state_equals'),\n key: z\n .string()\n .describe('Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set().'),\n value: z.unknown().describe('Expected value to match against'),\n})\n .describe('Checks the SDK persistent state store (localStorage). ONLY for host-app state set via syntro.state.set() \u2014 ' +\n 'NOT for user attributes like region, device, or UTM params (those are handled by segment targeting). ' +\n 'Do NOT use this for targeting. If you do not know the valid state keys, do not use this condition type.');\nexport const ViewportConditionZ = z\n .object({\n type: z.literal('viewport'),\n minWidth: z.number().optional().describe('Minimum viewport width in pixels'),\n maxWidth: z.number().optional().describe('Maximum viewport width in pixels'),\n minHeight: z.number().optional().describe('Minimum viewport height in pixels'),\n maxHeight: z.number().optional().describe('Maximum viewport height in pixels'),\n})\n .describe('Fires based on viewport (screen) size. Use for responsive behavior. ' +\n 'Example: {\"type\": \"viewport\", \"minWidth\": 768} \u2014 fires on tablet and larger.');\nexport const SessionMetricConditionZ = z\n .object({\n type: z.literal('session_metric'),\n key: SessionMetricKeyZ,\n operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),\n threshold: z.number().describe('Numeric threshold to compare against'),\n})\n .describe('Fires when a session metric crosses a threshold. Valid keys: \"time_on_page\" (seconds), ' +\n '\"page_views\" (count), \"scroll_depth\" (0-100). ' +\n 'Example: {\"type\": \"session_metric\", \"key\": \"time_on_page\", \"operator\": \"gte\", \"threshold\": 30}');\nexport const DismissedConditionZ = z\n .object({\n type: z.literal('dismissed'),\n key: z.string().describe('Dismissal key (usually a tile or action ID)'),\n inverted: z\n .boolean()\n .optional()\n .describe('When true, fires if NOT dismissed (default behavior)'),\n})\n .describe('Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.');\nexport const CooldownActiveConditionZ = z\n .object({\n type: z.literal('cooldown_active'),\n key: z.string().describe('Cooldown key'),\n inverted: z.boolean().optional().describe('When true, fires if cooldown is NOT active'),\n})\n .describe('Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.');\nexport const FrequencyLimitConditionZ = z\n .object({\n type: z.literal('frequency_limit'),\n key: z.string().describe('Frequency counter key'),\n limit: z.number().describe('Maximum allowed count'),\n inverted: z.boolean().optional().describe('When true, fires if limit NOT reached'),\n})\n .describe('Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.');\nexport const MatchOpZ = z\n .object({\n equals: z.union([z.string(), z.number(), z.boolean()]).optional(),\n contains: z.string().optional(),\n})\n .refine((operator) => Number(operator.equals !== undefined) + Number(operator.contains !== undefined) === 1, {\n message: 'Exactly one of equals or contains must be specified.',\n})\n .describe('Match operator for counter filters. Exactly one of equals or contains must be specified.');\nexport const CounterDefZ = z\n .object({\n events: z\n .array(CountableEventZ)\n .min(1)\n .describe('Event names to count. Use values from the countable events enum.'),\n match: z\n .record(z.string(), MatchOpZ)\n .optional()\n .describe('Property filters. Keys are event prop names or element-chain fields ' +\n '(tag_name, $el_text, attr__*). All entries AND together.'),\n})\n .describe('Defines what events to count. Registered as an accumulator predicate at config-load time.');\nexport const EventCountConditionZ = z\n .object({\n type: z.literal('event_count'),\n key: z.string().describe('Unique key for this counter (used for accumulator registration)'),\n operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),\n count: z.number().int().min(0).describe('Target count threshold'),\n withinMs: z\n .number()\n .positive()\n .optional()\n .describe('Time window in ms. Omit = count across entire session.'),\n counter: CounterDefZ.optional().describe('Inline counter definition. Defines what events to count.'),\n})\n .describe('Fires when accumulated event count crosses a threshold. Most powerful trigger type. ' +\n 'Example: {\"type\": \"event_count\", \"key\": \"pricing-clicks\", \"operator\": \"gte\", \"count\": 3, ' +\n '\"counter\": {\"events\": [\"ui.click\"], \"match\": {\"attr__data-cta\": {\"contains\": \"pricing\"}}}}');\nexport const ConditionZ = z.discriminatedUnion('type', [\n PageUrlConditionZ,\n RouteConditionZ,\n AnchorVisibleConditionZ,\n EventOccurredConditionZ,\n StateEqualsConditionZ,\n ViewportConditionZ,\n SessionMetricConditionZ,\n DismissedConditionZ,\n CooldownActiveConditionZ,\n FrequencyLimitConditionZ,\n EventCountConditionZ,\n]);\n// =============================================================================\n// STRATEGY SCHEMAS\n// =============================================================================\nexport const RuleZ = z\n .object({\n conditions: z\n .array(ConditionZ)\n .describe('Array of conditions \u2014 ALL must match (AND logic) for this rule to fire.'),\n value: z\n .unknown()\n .describe('Value returned when all conditions match. For triggerWhen: true = fire the action.'),\n})\n .describe('A single rule. ALL conditions must match (AND logic). Rules in a strategy are evaluated ' +\n 'top-to-bottom \u2014 first rule where all conditions match wins and returns its value.');\nexport const RuleStrategyZ = z\n .object({\n type: z.literal('rules'),\n rules: z\n .array(RuleZ)\n .describe('Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins.'),\n default: z\n .unknown()\n .describe('Fallback value when no rule matches. For triggerWhen: false = do not fire by default.'),\n})\n .describe('Rule-based strategy. Evaluates rules top-to-bottom. First rule where ALL conditions match ' +\n 'returns its value. If no rule matches, returns default. ' +\n 'For triggerWhen: set value=true on matching rules, default=false.');\nexport const ScoreStrategyZ = z\n .object({\n type: z.literal('score'),\n field: z.string(),\n threshold: z.number(),\n above: z.unknown(),\n below: z.unknown(),\n})\n .describe('Score-based strategy. Compares a field value against a threshold.');\nexport const ModelStrategyZ = z\n .object({\n type: z.literal('model'),\n modelId: z.string(),\n inputs: z.array(z.string()),\n outputMapping: z.record(z.string(), z.unknown()),\n default: z.unknown(),\n})\n .describe('ML model strategy. Sends inputs to a model and maps outputs.');\nexport const ExternalStrategyZ = z\n .object({\n type: z.literal('external'),\n endpoint: z.string(),\n method: z.enum(['GET', 'POST']).optional(),\n default: z.unknown(),\n timeoutMs: z.number().optional(),\n})\n .describe('External API strategy. Calls an endpoint to determine the value.');\nexport const DecisionStrategyZ = z.discriminatedUnion('type', [\n RuleStrategyZ,\n ScoreStrategyZ,\n ModelStrategyZ,\n ExternalStrategyZ,\n]);\n/** Canonical Zod schema for the optional triggerWhen field on actions and adaptive items. */\nexport const TriggerWhenZ = DecisionStrategyZ.nullable().optional();\n// =============================================================================\n// TRIGGER DOCUMENTATION \u2014 examples and match field docs\n// Exported as constants so the schema generator can inject them into the\n// JSON schema. The Python prompt builder reads them from the schema.\n// =============================================================================\n/** Complete triggerWhen examples showing the full rules wrapper structure. */\nexport const TRIGGER_EXAMPLES = [\n {\n name: 'Click count on a specific element',\n description: 'Fire when user clicks an element with data-id=\"hero-cta\" 2+ times',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'event_count',\n key: 'cta-clicks',\n operator: 'gte',\n count: 2,\n counter: {\n events: ['ui.click'],\n match: { 'attr__data-id': { equals: 'hero-cta' } },\n },\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'Time on page threshold',\n description: 'Fire after user spends 30+ seconds on the page',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'session_metric',\n key: 'time_on_page',\n operator: 'gte',\n threshold: 30,\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'Element visible in viewport',\n description: 'Fire when a DOM element becomes visible',\n triggerWhen: {\n type: 'rules',\n rules: [\n {\n conditions: [\n {\n type: 'anchor_visible',\n anchorId: '#pricing-section',\n state: 'visible',\n },\n ],\n value: true,\n },\n ],\n default: false,\n },\n },\n {\n name: 'No trigger (fire immediately)',\n description: 'Action fires as soon as the segment matches \u2014 no in-session condition needed',\n triggerWhen: null,\n },\n];\n/** Documentation for counter.match field keys. */\nexport const MATCH_FIELD_DOCS = {\n tag_name: 'HTML tag name (e.g. \"button\", \"a\", \"input\")',\n $el_text: 'Visible text content of the element',\n 'attr__*': 'HTML attribute prefixed with attr__. Example: attr__data-id matches the data-id attribute, ' +\n 'attr__class matches the class attribute, attr__href matches the href attribute.',\n};\n// =============================================================================\n// EVENT SCOPE SCHEMA\n// =============================================================================\n/** Scopes a widget to specific events/URLs. */\nexport const EventScopeZ = z.object({\n events: z.array(z.string()),\n urlContains: z.string().optional(),\n props: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),\n});\n// =============================================================================\n// NOTIFY SCHEMA\n// =============================================================================\n/** Toast notification config for triggerWhen transitions. */\nexport const NotifyZ = z\n .object({\n title: z.string().optional(),\n body: z.string().optional(),\n icon: z.string().optional(),\n})\n .nullable()\n .optional();\n", "/**\n * PDP presets store (WI-4) \u2014 the ONE accessor for the per-product preset\n * table that `<syntro-pdp>` and the native slot watcher read.\n *\n * Two producers feed it:\n * 1. Config-driven delivery: when the SDK config carries `pdpPresetsUrl`,\n * the runtime-sdk bootstrap fetches the JSON and writes the well-known\n * window globals below (see runtime-sdk `fetchers/pdpPresetsFetcher.ts`).\n * Window is the channel BY DESIGN: adaptive-product ships as its own CDN\n * chunk, so module-scoped state here can never be set by the runtime\n * bundle.\n * 2. Legacy demo boot scripts (vela / healthmaxxer) that hand-set\n * `window.__SYNTRO_PDP_PRESETS__` before the SDK boots.\n *\n * Reads are LAZY \u2014 every call re-reads the global, so late assignment (a\n * slow bootstrap fetch, a demo script racing the element upgrade) is always\n * observed and absence is never cached. On fetch failure the store simply\n * stays empty and the PDP fails closed (renders nothing) \u2014 never a broken\n * box.\n */\n\n/** One config-set slot value: the widget kind, an optional header, its content. */\nexport interface PdpSlotPreset {\n kind: string;\n header?: unknown;\n data: unknown;\n}\n\n/** `presets[product][archetype][slot] = PdpSlotPreset` \u2014 the config's SET values. */\nexport type PdpPresetTable = Record<string, Record<string, Record<string, PdpSlotPreset>>>;\n\n/** Well-known globals \u2014 the cross-bundle contract with the runtime-sdk\n * bootstrap fetcher AND the legacy demo boot scripts. Do not rename. */\ninterface PresetsWindow {\n __SYNTRO_PDP_PRESETS__?: PdpPresetTable;\n /** Set by the bootstrap fetcher while a config-driven fetch is in flight. */\n __SYNTRO_PDP_PRESETS_PROMISE__?: Promise<unknown>;\n}\n\nfunction presetsWindow(): PresetsWindow | undefined {\n if (typeof window === 'undefined') return undefined;\n return window as unknown as PresetsWindow;\n}\n\n/** The current preset table, or undefined when none has been provided yet. */\nexport function getPdpPresets(): PdpPresetTable | undefined {\n return presetsWindow()?.__SYNTRO_PDP_PRESETS__;\n}\n\n/**\n * Store a preset table. Writes through to the window global so every bundle\n * (element chunk, native-watcher chunk, runtime bundle) sees the same table.\n */\nexport function setPdpPresets(table: PdpPresetTable): void {\n const win = presetsWindow();\n if (win) win.__SYNTRO_PDP_PRESETS__ = table;\n}\n\n/**\n * True when a config-driven presets fetch has been STARTED (its readiness\n * promise is on the window \u2014 in flight or settled). Lets synchronous-path\n * consumers skip the async gate entirely when no `pdpPresetsUrl` delivery is\n * configured, preserving their exact pre-WI-4 microtask timing.\n */\nexport function pdpPresetsFetchStarted(): boolean {\n const pending = presetsWindow()?.__SYNTRO_PDP_PRESETS_PROMISE__;\n return pending != null && typeof pending.then === 'function';\n}\n\n/**\n * Readiness gate for consumers that fire once (the element's compose, the\n * watcher's per-product decision): resolves when any in-flight config-driven\n * presets fetch has settled \u2014 immediately when none is in flight. NEVER\n * rejects; a failed fetch resolves with the store left empty (fail-closed).\n */\nexport function pdpPresetsReady(): Promise<void> {\n const pending = presetsWindow()?.__SYNTRO_PDP_PRESETS_PROMISE__;\n if (!pending || typeof pending.then !== 'function') return Promise.resolve();\n return Promise.resolve(pending).then(\n () => undefined,\n () => undefined\n );\n}\n", "/**\n * Mount-time pool selection \u2014 the client half of the two-axis model.\n *\n * Cached PDP presets carry a POOL of items, each tagged with the decision\n * archetype(s) its evidence serves (`archetypes: string[]`). At mount, the\n * scored decision archetype picks what surfaces:\n *\n * - SHARP label \u2192 items tagged with it lead, untagged (general) items fill,\n * then the rest. The module specializes without another generation call.\n * - UNKNOWN \u2192 the sprinkle: one item per distinct tag (round-robin, first-seen\n * tag order), so every blocking question gets its evidence somewhere on the\n * page, then untagged/remaining fill.\n *\n * Pure functions \u2014 no DOM, no network \u2014 so the selection rule is unit-testable\n * and identical across the qa / trending-news / peer-feed widgets.\n */\n\nexport const UNKNOWN_DECISION = 'unknown';\n\n/** Display caps per module kind \u2014 pools are bigger than what mounts. */\nexport const DISPLAY_MAX: Record<string, number> = {\n qa: 5,\n 'trending-news': 5,\n 'peer-feed': 4,\n};\n\nexport interface TaggedItem {\n archetypes?: string[];\n}\n\nfunction tagsOf(item: TaggedItem): string[] {\n return Array.isArray(item.archetypes) ? item.archetypes : [];\n}\n\n/**\n * Order a tagged pool for one visitor and cap it at `max`.\n * Never returns fewer than min(pool length, max) items \u2014 selection only\n * reorders and trims, it never empties a module.\n */\nexport function selectPoolItems<T extends TaggedItem>(\n items: T[],\n decision: string | undefined,\n max: number\n): T[] {\n if (items.length <= max) return [...items];\n\n const sharp = decision && decision !== UNKNOWN_DECISION;\n if (sharp) {\n const matched = items.filter((i) => tagsOf(i).includes(decision));\n const general = items.filter((i) => tagsOf(i).length === 0);\n const rest = items.filter((i) => !matched.includes(i) && !general.includes(i));\n return [...matched, ...general, ...rest].slice(0, max);\n }\n\n // Sprinkle: one item per distinct tag in first-seen order (an item covers\n // every tag it carries), then untagged, then whatever remains.\n const picked: T[] = [];\n const covered = new Set<string>();\n for (const item of items) {\n const tags = tagsOf(item);\n if (tags.length === 0) continue;\n if (tags.some((t) => covered.has(t))) continue;\n picked.push(item);\n for (const t of tags) covered.add(t);\n if (picked.length >= max) return picked.slice(0, max);\n }\n for (const item of items) {\n if (picked.length >= max) break;\n if (!picked.includes(item) && tagsOf(item).length === 0) picked.push(item);\n }\n for (const item of items) {\n if (picked.length >= max) break;\n if (!picked.includes(item)) picked.push(item);\n }\n return picked.slice(0, max);\n}\n\n/**\n * Apply pool selection to a cached slot preset's payload, by module kind.\n * Kinds without a pooled item list (chart / regional / other-products) and\n * malformed payloads pass through untouched.\n */\nexport function selectPresetData(\n kind: string,\n data: unknown,\n decision: string | undefined\n): unknown {\n const max = DISPLAY_MAX[kind];\n if (!max || typeof data !== 'object' || data === null) return data;\n const record = data as Record<string, unknown>;\n const listKey = kind === 'peer-feed' ? 'peers' : 'items';\n const list = record[listKey];\n if (!Array.isArray(list)) return data;\n return { ...record, [listKey]: selectPoolItems(list as TaggedItem[], decision, max) };\n}\n", "// Request/response types for the runtime-backend /api/pdp/* endpoints are\n// DERIVED from the generated contract (single source of truth) \u2014 a Pydantic\n// change on ModuleRequest/ModuleResponse/DossierResponse breaks this compile\n// instead of drifting. See docs/adr/2026-07-05-runtime-backend-generated-types.md.\nimport type {\n DossierMetric,\n DossierResponse,\n ModuleRequest,\n ModuleResponse,\n} from '@syntrologie/runtime-contracts';\nimport { html, LitElement, type PropertyValues, render } from 'lit';\nimport {\n createInitialPdpState,\n type PdpEvent,\n type PdpState,\n type PdpWhyBoxView,\n selectPdpView,\n transitionPdp,\n} from '../decision/pdp';\nimport {\n buildPdpPlan,\n createPdpComposeAdapter,\n orderPdpSlots,\n PDP_SLOT_IDS,\n type PdpPlanEnvelope,\n type PdpPlanTaskClient,\n} from '../decision/pdp-compose';\nimport { createPdpEffectExecutor, type PdpEffectRun } from '../decision/pdp-effects';\n// Native takeover (FEAT-1784346204): on pages that server-render their own FAQ /\n// review sections (the Task 3 `/pdp/dynamic` shape), the element swaps content\n// INTO those host sections instead of rendering its own duplicate ones.\nimport {\n findNativeSlot,\n NATIVE_KIND_BY_SLOT,\n type NativeSwapHandle,\n nativeFieldsFromContent,\n swapNativeSlot,\n} from '../native/nativeSlotSwap';\n// WI-4: presets come through the store accessor \u2014 populated by the\n// runtime-sdk bootstrap fetch (config `pdpPresetsUrl`) or by legacy demo\n// boot scripts; never read the window global directly here.\nimport { getPdpPresets, pdpPresetsReady } from '../presets-store';\n// Widget-props contract (unified schema `adaptive-product:pdp` entry).\nimport type { SyntroPdpProps } from '../schema';\n// The scored-template orchestration lives in the adaptive (not the runtime) \u2014 it's\n// the PDP's composition logic. The runtime stays thin (score + authedFetch).\nimport { selectPresetData } from '../scoring/pool-selection';\nimport { resolveScoredTemplate, type SlotPreset } from '../scoring/scored-template';\n// Page-takeover engine (FEAT-1785977524): when the widget props carry a\n// `takeover` config, the element becomes the takeover root and this engine \u2014\n// not the six-slot compose path \u2014 owns everything it renders.\nimport { runTakeover, type TakeoverResult } from '../takeover/engine';\nimport { createTakeoverPlanFetcher } from '../takeover/fetchPlan';\nimport { attachLifecycleDispose, createDomPorts } from '../takeover/ports';\nimport { createTakeoverModuleRenderer } from '../takeover/renderModules';\nimport type { PdpTakeoverConfig } from '../takeover/schema';\nimport './PdpSectionHeaderLit';\nimport './PdpOtherProductsCarouselLit.js';\nimport './PdpOtherProductsGridLit.js';\n\n// \u2500\u2500\u2500 visitor signal types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface VisitorAttribution {\n referrer: string;\n referring_domain: string;\n initial_url: string;\n initial_pathname: string;\n utm_source: string;\n utm_medium: string;\n utm_campaign: string;\n utm_content: string;\n utm_term: string;\n}\n\nexport interface VisitorGeo {\n country: string;\n country_code: string;\n region: string;\n city: string;\n is_eu: boolean;\n}\n\nexport interface VisitorDevice {\n user_agent: string;\n locale: string;\n viewport_w: number | null;\n viewport_h: number | null;\n screen_w: number | null;\n screen_h: number | null;\n dpr: number | null;\n is_touch: boolean;\n}\n\nexport interface VisitorSignals {\n attribution: VisitorAttribution;\n geo: VisitorGeo;\n device: VisitorDevice;\n session_metrics: Record<string, string | number | boolean>;\n extras: Record<string, string | number | boolean>;\n}\n\n// \u2500\u2500\u2500 module plan / mutation types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface MountedInstance {\n instance_id: string;\n element_type: 'tile' | 'action' | 'item';\n template_id: string;\n placement: { type: 'slot'; slot: string } | { type: string; [key: string]: unknown };\n content: Record<string, unknown>;\n state: string;\n mounted_by: { agent_id: string; turn_id: string };\n mounted_at: string;\n version: number;\n}\n\nexport interface MutationEnvelope {\n type: 'syntro.element.mutation';\n schema_version: 'v0';\n mutations: Array<{ op: 'mount'; instance: MountedInstance }>;\n}\n\nexport type {\n PdpModuleHeaderClient,\n PdpPlanEnvelope,\n PdpPlanTaskClient,\n} from '../decision/pdp-compose';\n\n// \u2500\u2500\u2500 internal helpers for reading the SDK global \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** `presets[product][archetype][slot] = SlotPreset` \u2014 the config's SET values. */\ntype PdpPresetTable = Record<string, Record<string, Record<string, SlotPreset>>>;\n\nexport interface RuntimeContext {\n page: {\n locale?: string;\n routeId?: string;\n title?: string;\n url: string;\n };\n session: {\n pageHistory?: Array<{ ts: number; url: string }>;\n sessionId?: string;\n startTs: number;\n };\n surface: {\n device: string;\n host: string;\n mode: string;\n type: string;\n };\n viewport: {\n height: number;\n width: number;\n };\n}\n\n/** Canonical product identity shape (mirrors `@syntrologie/sdk-contracts`\n * ProductIdentity \u2014 structural type so the adaptive bundle stays free of\n * runtime-sdk imports). */\nexport interface PageProductIdentity {\n productHandle: string;\n productUrl: string;\n productTitle?: string;\n}\n\nexport interface SmartCanvasRuntime {\n apps?: {\n has?(appId: string): boolean;\n };\n telemetry?: {\n track?(eventName: string, props?: Record<string, unknown>): void;\n };\n /**\n * WI-6b: page-URL product resolver, set at bootstrap when the active\n * platform adapter can parse product page URLs. The attr-less element\n * (config placement injects one static HTML string across all product\n * pages) resolves its own product through this hook.\n */\n getPageProductIdentity?: () => PageProductIdentity | null;\n /**\n * The runtime's NavigationMonitor (subscribe-only view). Absent on bare\n * test mounts \u2014 the element skips SPA re-resolution gracefully then.\n * NEVER patch history here; the monitor is the single History API patch\n * point (runtime-sdk gotcha #7).\n */\n navigation?: {\n subscribe(listener: (url: string, method: string) => void): () => void;\n };\n}\n\ninterface SynOSGlobal {\n runtime?: {\n context: { get(): RuntimeContext };\n sessionMetrics?: { getAll(): Record<string, number> };\n telemetry?: {\n getDistinctId?(): string | undefined;\n getSessionId?(): string | undefined;\n getSegmentFlags?(): Record<string, boolean> | undefined;\n getAllFeatureFlags?(): Record<string, boolean | string> | undefined;\n };\n };\n /** On-demand scoring (the runtime's thin `score` primitive). */\n score?: (spec: unknown) => Promise<string>;\n /** Batched two-axis scoring through one backend model call. */\n scoreMulti?: (specs: unknown[]) => Promise<string[]>;\n /**\n * Authenticated transport to the runtime-backend (the runtime owns auth; the\n * adaptive owns which endpoint + payload). Used for the live module endpoint.\n */\n authedFetch?: (path: string, init?: RequestInit) => Promise<Response>;\n}\n\nfunction _readSynOS(): SynOSGlobal | undefined {\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { SynOS?: SynOSGlobal }).SynOS;\n}\n\nfunction _readRuntime(): SynOSGlobal['runtime'] | undefined {\n return _readSynOS()?.runtime;\n}\n\n/** Archetype slug \u2192 description (`window.__SYNTRO_PDP_ARCHETYPES__`). The\n * description tells the live sub-agent what the bucket MEANS. */\nfunction _readPdpArchetypes(): Record<string, string> | undefined {\n /* v8 ignore next -- SSR guard for a browser global reader; hydrated behavior is covered through scoring tests. */\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { __SYNTRO_PDP_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_ARCHETYPES__;\n}\n\n/** Axis A: the question blocking the purchase. Hosts that omit this keep single-axis PDPs. */\nfunction _readPdpDecisionArchetypes(): Record<string, string> | undefined {\n /* v8 ignore next -- SSR guard for a browser global reader; hydrated behavior is covered through scoring tests. */\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { __SYNTRO_PDP_DECISION_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_DECISION_ARCHETYPES__;\n}\n\n/**\n * Generate one uncached PDP module via the runtime-backend. The adaptive owns the\n * endpoint (`/api/pdp/module`) + its request/response shape; the runtime's\n * `authedFetch` owns auth. Returns the module payload, or null on any failure\n * (the slot just stays an empty reserved box \u2014 no throw, no layout shift).\n */\nasync function composeModule(\n authedFetch: NonNullable<SynOSGlobal['authedFetch']>,\n args: {\n productName: string;\n productId: string | null;\n kind: string;\n variant: string | null;\n archetype: { slug: string; description: string };\n decisionArchetype: { slug: string; description: string } | null;\n behaviorSummary: string;\n signals: Record<string, unknown>;\n }\n): Promise<Record<string, unknown> | null> {\n try {\n // Typed against the backend ModuleRequest contract \u2014 snake_case field names\n // and value types are checked at compile time.\n const requestBody: ModuleRequest = {\n product_name: args.productName,\n product_id: args.productId,\n // `kind` flows from runtime slot-id parsing (`pdp-slot-<kind>`), so it's a\n // plain string here; the backend validates it against the PdpModuleKind\n // enum (422 on an unknown kind). The other fields ARE contract-checked.\n kind: args.kind as ModuleRequest['kind'],\n variant: args.variant,\n archetype: { slug: args.archetype.slug, description: args.archetype.description },\n decision_archetype: args.decisionArchetype,\n coverage: 'focused',\n behavior_summary: args.behaviorSummary,\n // geo/attribution/device \u2014 the regional module needs the visitor's region.\n signals: args.signals,\n };\n const resp = await authedFetch('/api/pdp/module', {\n method: 'POST',\n body: JSON.stringify(requestBody),\n });\n if (!resp.ok) return null;\n const data = (await resp.json()) as Pick<ModuleResponse, 'payload'>;\n return data.payload ?? null;\n } catch {\n return null;\n }\n}\n\n/** Build a mount envelope for a live-generated module (mirrors the cached mount). */\nfunction _buildLiveMount(\n slot: string,\n kind: string,\n payload: Record<string, unknown>\n): MutationEnvelope {\n return {\n type: 'syntro.element.mutation',\n schema_version: 'v0',\n mutations: [\n {\n op: 'mount',\n instance: {\n instance_id: `live-${slot}`,\n element_type: 'tile',\n template_id: `pdp-${kind}`,\n placement: { type: 'slot', slot },\n content: payload,\n state: 'mounted',\n mounted_by: { agent_id: 'live', turn_id: 'live' },\n mounted_at: new Date().toISOString(),\n version: 1,\n },\n },\n ],\n };\n}\n\n/**\n * Per-product PDP presets via the WI-4 store accessor (config `pdpPresetsUrl`\n * fetch OR legacy hand-set `window.__SYNTRO_PDP_PRESETS__` \u2014 the store reads\n * the global lazily, so late producers are always observed).\n */\nfunction _readPdpPresets(): PdpPresetTable | undefined {\n return getPdpPresets() as PdpPresetTable | undefined;\n}\n\n// \u2500\u2500\u2500 \"Why box\" \u2014 surfaces the two model outputs + demo metrics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface PdpExplainMetric {\n value: string;\n label: string;\n /** Forensic one-liner shown in the \"What we measured\" drawer. */\n hint?: string;\n}\ninterface PdpExplain {\n archetypeLabel: string;\n archetypeDesc: string;\n decisionLabel: string | null;\n decisionDesc: string | null;\n summary: string;\n /** Hero row \u2014 punchy, scannable. */\n metrics: PdpExplainMetric[];\n /** Full forensic dossier \u2014 revealed in the \"What we measured\" drawer. */\n dossier: PdpExplainMetric[];\n}\n\n/** Title-case a slug: \"aging-cognitive\" \u2192 \"Aging Cognitive\". */\nfunction _titleCase(slug: string): string {\n return slug\n .split(/[-_\\s]+/)\n .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : ''))\n .join(' ');\n}\n\n/** Count non-empty leaf values in the (nested) signal bag \u2014 a real number to show off. */\nfunction _countSignals(signals: Record<string, unknown>): number {\n let n = 0;\n const walk = (v: unknown): void => {\n if (v && typeof v === 'object') {\n for (const x of Object.values(v as Record<string, unknown>)) walk(x);\n } else if (v !== '' && v != null) {\n // Count `false` too: \"is_touch=false\" / \"is_eu=false\" are real measurements,\n // not absent signals \u2014 excluding them would deflate the honest count.\n n++;\n }\n };\n walk(signals);\n return n;\n}\n\n/** Count top-level signal categories carrying data (attribution, geo, device, \u2026) \u2014 real. */\nfunction _signalDimensions(signals: Record<string, unknown>): number {\n let n = 0;\n for (const v of Object.values(signals)) {\n if (v && typeof v === 'object') {\n if (Object.keys(v as Record<string, unknown>).length) n++;\n } else if (v !== '' && v != null && v !== false) {\n n++;\n }\n }\n return n;\n}\n\n/**\n * A stable hex signature of the visitor's full signal bag (FNV-1a \u2192 8 hex digits).\n * Opaque on purpose: it doesn't claim a population statistic, it shows that we\n * distilled the depth of this visitor's behavior into a unique fingerprint.\n */\nfunction _fingerprint(signals: Record<string, unknown>): string {\n const s = JSON.stringify(signals);\n let h = 2166136261;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\n/** Read one numeric value from the signal bag's nested `session_metrics`, or null. */\nfunction _sessionMetric(signals: Record<string, unknown>, key: string): number | null {\n const sm = (signals as { session_metrics?: Record<string, unknown> }).session_metrics;\n const v = sm?.[key];\n return typeof v === 'number' && Number.isFinite(v) ? v : null;\n}\n\n/**\n * Build the \"why box\" content from the two model outputs + the visitor's REAL\n * session signals. Every metric is either a true measurement (signals analyzed,\n * scroll depth, time engaged, pages, models run, latency) or an opaque signature\n * (the behavioral fingerprint). NO invented population statistics (\"similar\n * visitors matched\", \"top N%\") \u2014 those are the only refutable kind, because they\n * assert something about OTHER people we can't show. Everything here is provable\n * from this visitor's own session.\n *\n * Tier 1 (here): SDK-local real metrics. Tier 2 (backend) will fold in the\n * event-stream forensics (interaction rhythm, engagement entropy, friction\n * signals, cursor travel) computed from `recent_observations`.\n */\nfunction buildExplain(opts: {\n archetype: string;\n description: string;\n decisionArchetype: string;\n decisionDescription: string;\n summary: string;\n signals: Record<string, unknown>;\n latencyMs: number;\n}): PdpExplain {\n const {\n archetype,\n description,\n decisionArchetype,\n decisionDescription,\n summary,\n signals,\n latencyMs,\n } = opts;\n const blockingQuestion =\n /Blocked on:\\s*([\\s\\S]*?)\\s*Resolved by:/.exec(decisionDescription)?.[1] ?? decisionDescription;\n // Transparency (Parker, 2026-07-16: \"if it is unknown.. score it as unknown\"):\n // show the decision row whenever the axis was SCORED \u2014 including an explicit\n // 'unknown' decline \u2014 and only hide it when the axis was never scored at all\n // (feature off / no scoreMulti). `decisionKnown` still gates the real blocking\n // question; an 'unknown' shows \"Unknown\" with a plain not-enough-signal note.\n const decisionScored = decisionArchetype.length > 0;\n const decisionKnown = decisionScored && decisionArchetype !== 'unknown';\n const signalsCount = _countSignals(signals);\n const dims = _signalDimensions(signals);\n const fingerprint = `0x${_fingerprint(signals)}`;\n const latency = `${Math.max(1, Math.round(latencyMs))}ms`;\n const scrollDepth = _sessionMetric(signals, 'scroll_depth');\n const timeOnPage = _sessionMetric(signals, 'time_on_page');\n const pageViews = _sessionMetric(signals, 'page_views');\n\n // Build the dossier from real values; only include session metrics we actually\n // captured (they may be absent on a fresh first paint).\n const dossier: PdpExplainMetric[] = [\n {\n value: `${signalsCount}`,\n label: 'behavioral signals analyzed',\n hint: 'Distinct non-empty data points across your attribution, device, geo, and live session.',\n },\n {\n value: `${dims}`,\n label: 'signal dimensions correlated',\n hint: 'Independent categories of behavior we cross-referenced to place you.',\n },\n ];\n if (scrollDepth != null)\n dossier.push({\n value: `${Math.round(scrollDepth)}%`,\n label: 'page depth reached',\n hint: 'How far down this page you explored before we personalized it.',\n });\n if (timeOnPage != null)\n dossier.push({\n value: `${(timeOnPage / 1000).toFixed(1)}s`,\n label: 'time engaged on page',\n hint: 'Active time you spent here \u2014 a direct read on deliberation.',\n });\n if (pageViews != null)\n dossier.push({\n value: `${pageViews}`,\n label: 'pages this session',\n hint: 'The depth of your journey across the site so far.',\n });\n dossier.push(\n {\n value: '2',\n label: 'AI models run live',\n hint: 'A two-axis archetype classifier (what you do + what is blocking your decision) and a behavior-summary model, evaluated in real time on your signals.',\n },\n {\n value: latency,\n label: 'personalization latency',\n hint: 'Time to score your behavior and assemble this page.',\n },\n {\n value: fingerprint,\n label: 'behavioral fingerprint',\n hint: 'A unique signature distilled from the full depth of your session signals.',\n }\n );\n\n // Hero row: the 5 punchiest, scannable at a glance.\n const hero: PdpExplainMetric[] = [\n { value: `${signalsCount}`, label: 'behavioral signals analyzed' },\n ];\n if (timeOnPage != null)\n hero.push({ value: `${(timeOnPage / 1000).toFixed(1)}s`, label: 'time engaged' });\n if (scrollDepth != null)\n hero.push({ value: `${Math.round(scrollDepth)}%`, label: 'of the page explored' });\n hero.push(\n { value: '2', label: 'AI models run live' },\n { value: fingerprint, label: 'behavioral fingerprint' }\n );\n\n return {\n archetypeLabel: _titleCase(archetype),\n archetypeDesc: description,\n decisionLabel: decisionScored ? _titleCase(decisionArchetype) : null,\n decisionDesc: decisionKnown\n ? blockingQuestion\n : decisionScored\n ? 'Engaged, but no clear blocking question yet \u2014 still exploring.'\n : null,\n summary,\n metrics: hero,\n dossier,\n };\n}\n\n/**\n * Tier 2: fetch the backend behavioral forensics (event-stream dossier \u2014 entropy,\n * interaction rhythm, hesitation / friction / hover counts) computed from the\n * visitor's `recent_observations`. Fail-soft to [] on any error \u2014 the why box\n * still shows the Tier-1 local metrics. The adaptive owns the endpoint; the\n * runtime's authedFetch owns auth (Bearer token + X-Distinct-Id).\n */\nasync function fetchDossier(\n authedFetch: NonNullable<SynOSGlobal['authedFetch']>\n): Promise<PdpExplainMetric[]> {\n try {\n const resp = await authedFetch('/api/pdp/dossier', { method: 'POST' });\n if (!resp.ok) return [];\n const data: unknown = await resp.json();\n // The wire shape is the backend DossierResponse (metrics: DossierMetric[]);\n // still validated at runtime since the response is untrusted.\n const rows = (data as Partial<DossierResponse>).metrics;\n if (!Array.isArray(rows)) return [];\n return rows\n .filter(\n (m): m is DossierMetric => !!m && typeof m.value === 'string' && typeof m.label === 'string'\n )\n .map((m) => ({\n value: m.value,\n label: m.label,\n hint: typeof m.hint === 'string' ? m.hint : undefined,\n }));\n } catch {\n return [];\n }\n}\n\n// \u2500\u2500\u2500 signal gatherers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Each gatherer guards against SSR / missing browser globals so they can be\n// imported in Node test environments without blowing up.\n\nfunction gatherDevice(context: RuntimeContext | undefined): VisitorDevice {\n if (typeof window === 'undefined') {\n return {\n user_agent: '',\n locale: context?.page.locale ?? '',\n viewport_w: context?.viewport.width ?? null,\n viewport_h: context?.viewport.height ?? null,\n screen_w: null,\n screen_h: null,\n dpr: null,\n is_touch: false,\n };\n }\n const nav = window.navigator;\n const scr = window.screen;\n return {\n user_agent: nav?.userAgent ?? '',\n locale: context?.page.locale ?? nav?.language ?? '',\n viewport_w: context?.viewport.width ?? window.innerWidth ?? null,\n viewport_h: context?.viewport.height ?? window.innerHeight ?? null,\n screen_w: scr?.width ?? null,\n screen_h: scr?.height ?? null,\n dpr: window.devicePixelRatio || null,\n is_touch: 'ontouchstart' in window || (nav?.maxTouchPoints ?? 0) > 0,\n };\n}\n\n/**\n * UTM + referrer + initial URL \u2014 read from PostHog person properties\n * (where the SDK writes them via setPersonPropertiesOnce).\n *\n * TODO: swap to `runtime.telemetry.getProperty(name)` once that getter lands.\n */\nfunction gatherAttribution(): VisitorAttribution {\n if (typeof window === 'undefined') {\n return {\n referrer: '',\n referring_domain: '',\n initial_url: '',\n initial_pathname: '',\n utm_source: '',\n utm_medium: '',\n utm_campaign: '',\n utm_content: '',\n utm_term: '',\n };\n }\n interface PosthogLike {\n get_property?: (name: string) => unknown;\n }\n const ph = (window as unknown as { posthog?: PosthogLike }).posthog;\n const get = (key: string): string => {\n const v = ph?.get_property?.(key);\n return typeof v === 'string' ? v : '';\n };\n const refLive = document.referrer || '';\n let refDomain = get('$initial_referring_domain');\n if (!refDomain && refLive) {\n try {\n refDomain = new URL(refLive).hostname;\n } catch {\n // ignore \u2014 referrer wasn't a valid URL\n }\n }\n return {\n referrer: get('$initial_referrer') || refLive,\n referring_domain: refDomain,\n initial_url: get('$initial_url') || `${window.location.origin}${window.location.pathname}`,\n initial_pathname: get('$initial_pathname') || window.location.pathname,\n utm_source: get('$initial_utm_source'),\n utm_medium: get('$initial_utm_medium'),\n utm_campaign: get('$initial_utm_campaign'),\n utm_content: get('$initial_utm_content'),\n utm_term: get('$initial_utm_term'),\n };\n}\n\n/**\n * Geo \u2014 read from the localStorage cache the SDK writes at bootstrap time\n * (key `syntro_geo`).\n *\n * TODO: swap to `runtime.context.get().geo` once the SDK exposes that field.\n */\nfunction gatherGeo(): VisitorGeo {\n interface CachedGeo {\n country?: string;\n country_code?: string;\n region?: string;\n city?: string;\n is_eu?: boolean | string;\n }\n let cached: CachedGeo | undefined;\n try {\n if (typeof sessionStorage === 'undefined' && typeof localStorage === 'undefined') {\n cached = undefined;\n } else {\n // The SDK may write to localStorage; guard in case storage is blocked.\n const raw = typeof localStorage !== 'undefined' ? localStorage.getItem('syntro_geo') : null;\n cached = raw ? (JSON.parse(raw) as CachedGeo) : undefined;\n }\n } catch {\n cached = undefined;\n }\n return {\n country: String(cached?.country ?? ''),\n country_code: String(cached?.country_code ?? ''),\n region: String(cached?.region ?? ''),\n city: String(cached?.city ?? ''),\n is_eu: cached?.is_eu === true || cached?.is_eu === 'true',\n };\n}\n\nfunction gatherSessionMetrics(\n runtime: SynOSGlobal['runtime']\n): Record<string, string | number | boolean> {\n const metrics = runtime?.sessionMetrics?.getAll() ?? {};\n const out: Record<string, string | number | boolean> = {};\n for (const [key, value] of Object.entries(metrics)) {\n if (typeof value === 'number' && Number.isFinite(value)) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction gatherExtras(\n runtime: SynOSGlobal['runtime'],\n context: RuntimeContext | undefined\n): Record<string, string | number | boolean> {\n const out: Record<string, string | number | boolean> = {};\n\n // Segment membership flags \u2014 runtime telemetry API.\n const segmentFlags = runtime?.telemetry?.getSegmentFlags?.() ?? {};\n for (const [key, value] of Object.entries(segmentFlags)) {\n if (typeof value === 'boolean') out[`segment.${key}`] = value;\n }\n\n if (context) {\n out['surface.type'] = context.surface.type;\n out['surface.host'] = context.surface.host;\n out['surface.device'] = context.surface.device;\n out['surface.mode'] = context.surface.mode;\n out['page.url'] = context.page.url;\n if (context.page.title) out['page.title'] = context.page.title;\n if (context.page.routeId) out['page.route'] = context.page.routeId;\n if (context.session.pageHistory) {\n out['session.page_count'] = context.session.pageHistory.length;\n // The TRAIL, not just the count: which pages the visitor actually\n // walked is the persona signal the archetype classifier scores on. A\n // browse-only session (chat never opened \u2192 observer never started \u2192\n // zero server-side events) previously gave the classifier NOTHING\n // persona-shaped, so it correctly declined to \"unknown\" forever.\n const trail = _renderPageTrail(context.session.pageHistory);\n if (trail) out['session.page_trail'] = trail;\n }\n } else if (typeof window !== 'undefined') {\n out['page.url'] = window.location.href;\n out['page.title'] = document.title;\n }\n\n return out;\n}\n\n// Bound the trail so the signals bag stays well under the backend's 8000-char\n// cap even alongside a long chat excerpt.\nconst _PAGE_TRAIL_MAX_CHARS = 600;\n\n/**\n * Render the session page history as a compact \"\u2192\"-joined path trail (origin\n * stripped, query kept \u2014 `/shop?sort=new` is signal). Newest entries win: we\n * walk backwards and stop before the joined string would exceed the bound.\n */\nfunction _renderPageTrail(history: Array<{ url: string; ts: number }>): string {\n const paths: string[] = [];\n let length = 0;\n for (let i = history.length - 1; i >= 0; i--) {\n let path = history[i]!.url;\n try {\n const u = new URL(path);\n path = `${u.pathname}${u.search}`;\n } catch {\n /* relative or malformed \u2014 use as-is */\n }\n const cost = path.length + (paths.length > 0 ? 3 : 0); // ' \u2192 ' separator\n if (length + cost > _PAGE_TRAIL_MAX_CHARS) break;\n paths.unshift(path);\n length += cost;\n }\n return paths.join(' \u2192 ');\n}\n\n// The runtime PDP endpoints cap chat_excerpt at ~8000 chars. Stay below the\n// limit so the backend doesn't re-trim. Captured excerpts can be larger than\n// the limit \u2014 long multi-turn sessions easily exceed 10k chars.\n/**\n * Inject the global section-layout stylesheet ONCE into document.head,\n * guarded by element id \u2014 same pattern as PdpSlotLit.ensureSlotStyles and\n * PdpSectionHeaderLit.ensureSectionHeaderStyles. These rules use attribute\n * selectors (`[data-syntro-pdp-section]`, `[data-syntro-pdp-root]`) that\n * are global by nature, so they must live in head exactly once rather than\n * being re-emitted inside the per-instance Lit render (which would duplicate\n * the rule set for every mounted `<syntro-pdp>`).\n *\n * Per-slot accents drive a DEDICATED `--syntro-pdp-section-accent` custom\n * property (NOT `--syntro-pdp-primary`). The section-header reads it with a\n * fallback to the host's brand primary, so the SDK never silently mutates a\n * customer-set `--syntro-pdp-primary`; a host that wants brand-only eyebrows\n * can set `--syntro-pdp-section-accent: var(--syntro-pdp-primary)`.\n */\nfunction ensureSectionLayoutStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-layout-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-layout-style';\n s.textContent = SECTION_LAYOUT_CSS;\n document.head.appendChild(s);\n}\n\nconst SECTION_LAYOUT_CSS = `\n[data-syntro-pdp-root] {\n --sc-pdp-max-w: var(--syntro-pdp-max-width, 1080px);\n --sc-pdp-section-pad-y: var(--syntro-pdp-section-pad-y, clamp(40px, 6vw, 80px));\n --sc-pdp-section-pad-x: var(--syntro-pdp-section-pad-x, clamp(20px, 4vw, 40px));\n /* Portable banding: default to a tint of the host's OWN text color\n (currentColor), so the alternating band adapts to any theme \u2014 light or\n dark \u2014 instead of a hardcoded near-black that's invisible on dark hosts\n and wrong on light ones. A client can pin an exact band color (e.g. a\n brand surface) via --syntro-pdp-card-bg-b. */\n --sc-pdp-band-alt: var(--syntro-pdp-card-bg-b, color-mix(in srgb, currentColor 5%, transparent));\n /* The root spans the FULL width of wherever the host mounts <syntro-pdp>,\n so the alternating section bands read as full-horizontal slices (like a\n storefront homepage). Per-section content is re-constrained to\n --sc-pdp-max-w by the inner wrapper. Mount the element full-width (no\n constraining container) to get edge-to-edge bands. */\n}\n[data-syntro-pdp-section] {\n position: relative;\n /* Vertical rhythm only \u2014 the band is full-bleed, so horizontal insets +\n max-width live on the inner wrapper, not here. No radius (a full-width\n band shouldn't have rounded corners at the viewport edge). Asymmetric:\n a tighter top so the eyebrow sits up near the band's top edge (the color\n transition), a generous bottom for breathing room under the content. */\n padding-top: var(--sc-pdp-section-pad-top, clamp(24px, 3vw, 36px));\n padding-bottom: var(--sc-pdp-section-pad-y);\n}\n/* Even sections carry a full-width band; odd sit on the host's base bg. The\n alternating color IS the separator \u2014 contiguous slices, no gap or divider. */\n[data-syntro-pdp-section]:nth-of-type(even) {\n background: var(--sc-pdp-band-alt);\n}\n/* Feature band: one section takes a full brand surface (e.g. a cream slice,\n like a storefront homepage) when the host opts in via\n --syntro-pdp-card-bg-feature. Defaults to the regular alternating tint, so\n hosts that don't set it see no change. Flips the section's color context\n (fg/muted/currentColor) to the feature foreground so the header + the body\n widgets (which read the same tokens) stay readable on the lighter surface.\n MUST follow the :nth-of-type(even) rule to win source order. */\n[data-syntro-pdp-section]:nth-of-type(4) {\n background: var(--syntro-pdp-card-bg-feature, var(--sc-pdp-band-alt));\n color: var(--syntro-pdp-fg-feature, inherit);\n --syntro-pdp-fg: var(--syntro-pdp-fg-feature, inherit);\n --syntro-pdp-muted: var(--syntro-pdp-muted-feature, inherit);\n}\n/* The content layer: constrained + centered inside the full-bleed band.\n margin-top separates the body from the headline (which now renders one\n layer up, at the full-width band level). */\n[data-syntro-pdp-section-inner] {\n max-width: var(--sc-pdp-max-w);\n margin-inline: auto;\n padding-inline: var(--sc-pdp-section-pad-x);\n margin-top: clamp(20px, 3vw, 32px);\n}\n/* Skeleton (pre-plan) stays constrained \u2014 only streamed sections go\n full-bleed, so the loading state doesn't jump edge-to-edge then back. */\n[data-syntro-pdp-root][data-syntro-pdp-skeleton] {\n max-width: var(--sc-pdp-max-w);\n margin-inline: auto;\n padding-inline: 16px;\n}\n/* Per-slot accent \u2192 a DEDICATED variable, never --syntro-pdp-primary. */\n[data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] { --syntro-pdp-section-accent: hsl(28 70% 58%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-chart'] { --syntro-pdp-section-accent: hsl(178 45% 50%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-qa'] { --syntro-pdp-section-accent: hsl(38 75% 60%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-regional'] { --syntro-pdp-section-accent: hsl(205 50% 55%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-peer-feed'] { --syntro-pdp-section-accent: hsl(265 45% 60%); }\n[data-syntro-pdp-section][data-slot='pdp-slot-other-products'] { --syntro-pdp-section-accent: hsl(150 38% 50%); }\n\n/* Editorial 2-column layout for the trending-news section (desktop): the\n section title/eyebrow sits in a narrow left column and the articles fill a\n wider right column \u2014 a magazine masthead next to its stories. This trades\n the full-bleed band (a grid can't be both full-width AND content-centered)\n for the side-by-side reading rhythm; the section reads as a deliberate\n magazine block on the base background. Mobile keeps the stacked layout. */\n@media (min-width: 720px) {\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] {\n background: transparent;\n display: grid;\n grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr);\n gap: clamp(32px, 4vw, 64px);\n max-width: var(--sc-pdp-max-w);\n margin-inline: auto;\n padding-inline: var(--sc-pdp-section-pad-x);\n align-items: start;\n }\n /* Title column: header left-aligned (not the full-band centered eyebrow),\n and gently sticky so it holds while the stories scroll past. */\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] > syntro-pdp-section-header {\n position: sticky;\n top: clamp(24px, 8vh, 72px);\n }\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] .syntro-pdp-section-eyebrow {\n justify-content: flex-start;\n }\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] .syntro-pdp-section-headline {\n max-width: none;\n margin-inline: 0;\n padding-inline: 0;\n }\n /* Stories column: the body fills the wider right column (drop the centered\n inner constraint + the header-gap top margin \u2014 the grid gap spaces it). */\n [data-syntro-pdp-section][data-slot='pdp-slot-trending-news'] > [data-syntro-pdp-section-inner] {\n max-width: none;\n margin: 0;\n padding: 0;\n }\n}\n`;\n\nconst CHAT_EXCERPT_KEY = 'syntro.chat.lastExcerpt';\nconst CHAT_EXCERPT_MAX = 7500;\n\nfunction readChatExcerpt(): string {\n try {\n if (typeof sessionStorage === 'undefined') return '';\n const raw = sessionStorage.getItem(CHAT_EXCERPT_KEY) ?? '';\n return raw.length > CHAT_EXCERPT_MAX ? `\u2026${raw.slice(-CHAT_EXCERPT_MAX + 1)}` : raw;\n } catch {\n return '';\n }\n}\n\n/**\n * Collect every personalization signal the SDK has access to client-side.\n * Never throws \u2014 missing signals degrade to empty values.\n */\nexport function gatherVisitorSignals(): VisitorSignals {\n const runtime = _readRuntime();\n const context = runtime?.context.get();\n return {\n attribution: gatherAttribution(),\n geo: gatherGeo(),\n device: gatherDevice(context),\n session_metrics: gatherSessionMetrics(runtime),\n extras: gatherExtras(runtime, context),\n };\n}\n\n/**\n * Behavioral-signal count for the takeover engine's cold-start gate. Derived\n * from the SAME signal bag the element scores with, but counting only the\n * BEHAVIORAL subset (live session metrics + a captured chat excerpt): device,\n * geo, and attribution exist on every first paint, so counting them would\n * make `hasSignals()` a constant true and the no-curtain progressive path\n * unreachable. 0 \u21D2 first-visit cold start (spec: \"no behavioral signals\").\n */\nfunction countTakeoverSignals(signals: VisitorSignals & { chat_excerpt: string }): number {\n return _countSignals(signals.session_metrics) + (signals.chat_excerpt ? 1 : 0);\n}\n\n// \u2500\u2500\u2500 TEMPLATE_TO_TAG dispatch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Maps (template_id, optional variant) \u2192 custom element tag name.\n// Keys are `\"<kind>\"` for base variants and `\"<kind>/<variant>\"` for specific\n// variants. resolveTagForMount() tries the variant key first, then the base\n// key, then returns null for unknown templates.\n\nexport type TemplateToTagKey = string;\n\n// Variant keys MUST be the exact `<kind>/<variant>` values the planner\n// emits \u2014 i.e. the backend PdpModuleKind + the *full* variant enum value\n// (TrendingNewsVariant, ChartVariant, QaVariant, \u2026). An abbreviated key\n// (e.g. `qa/accordion` vs the emitted `qa/accordion-multi`) silently\n// fails the lookup and every module falls back to its base widget, so the\n// dedicated variant layouts never render. `test_resolveTag` pins this.\nexport const TEMPLATE_TO_TAG: Record<TemplateToTagKey, string> = {\n // trending-news (TrendingNewsVariant)\n 'trending-news': 'syntro-pdp-trending-news',\n 'trending-news/magazine-column': 'syntro-pdp-trending-news-magazine',\n 'trending-news/hero-quote': 'syntro-pdp-trending-news-hero',\n // chart (ChartVariant)\n chart: 'syntro-pdp-chart',\n 'chart/bar-stacked': 'syntro-pdp-chart-bar',\n 'chart/metric-pull': 'syntro-pdp-chart-metric',\n // qa (QaVariant)\n qa: 'syntro-pdp-qa',\n 'qa/accordion-multi': 'syntro-pdp-qa-accordion',\n 'qa/side-by-side': 'syntro-pdp-qa-side',\n // regional (RegionalVariant)\n regional: 'syntro-pdp-regional',\n 'regional/full-bleed': 'syntro-pdp-regional-fullbleed',\n 'regional/inline-note': 'syntro-pdp-regional-inline',\n // peer-feed (PeerFeedVariant)\n 'peer-feed': 'syntro-pdp-peer-feed',\n 'peer-feed/voice-carousel': 'syntro-pdp-peer-feed-carousel',\n 'peer-feed/masonry-quote': 'syntro-pdp-peer-feed-masonry',\n // other-products (OtherProductsVariant) \u2014 already canonical\n 'other-products': 'syntro-pdp-other-products-carousel',\n 'other-products/carousel': 'syntro-pdp-other-products-carousel',\n 'other-products/two-column-grid': 'syntro-pdp-other-products-grid',\n};\n\n/**\n * Resolve the custom element tag to mount for a given plan task instance.\n *\n * Resolution order:\n * 1. `\"<kind>/<variant>\"` if `variant` is non-null\n * 2. `\"<kind>\"` (base) \u2014 covers unknown-variant fallback\n * 3. `null` if the kind itself is unknown\n */\nexport function resolveTagForMount(inst: PdpPlanTaskClient): string | null {\n if (inst.variant) {\n const variantKey = `${inst.kind}/${inst.variant}`;\n if (variantKey in TEMPLATE_TO_TAG) {\n return TEMPLATE_TO_TAG[variantKey];\n }\n // Variant string didn't match \u2014 surface this as a planner-prompt\n // drift signal. The base-key fallback below silently mounts the\n // default variant; without this log it's invisible.\n if (inst.kind in TEMPLATE_TO_TAG) {\n console.warn(\n `syntro-pdp.variant_drift: planner emitted unknown variant ` +\n `\"${inst.variant}\" for kind=\"${inst.kind}\" \u2014 falling back to ` +\n `default variant via TEMPLATE_TO_TAG[\"${inst.kind}\"]. Fix the ` +\n `planner prompt or add this variant to TEMPLATE_TO_TAG.`\n );\n }\n }\n const baseKey = inst.kind;\n if (baseKey in TEMPLATE_TO_TAG) {\n return TEMPLATE_TO_TAG[baseKey];\n }\n return null;\n}\n\n/**\n * Test seam \u2014 exposes private module-scope functions for unit testing only.\n * Do NOT use outside of test files.\n */\nexport const _testing = {\n buildExplain,\n gatherAttribution,\n gatherDevice,\n gatherExtras,\n gatherGeo,\n gatherSessionMetrics,\n gatherVisitorSignals,\n readChatExcerpt,\n resolveTagForMount,\n TEMPLATE_TO_TAG,\n titleCase: _titleCase,\n};\n\ntype MountOutcome =\n | { status: 'mounted'; slot: string }\n | { status: 'missed'; slot: string }\n | { status: 'ignored'; slot: string }\n // Native takeover: content was swapped into the host's OWN native section, or\n // the host defaults were deliberately kept for a fully-unknown visitor.\n | { status: 'mounted-native'; slot: string }\n | { status: 'native-default-kept'; slot: string };\n\n/**\n * <syntro-pdp> \u2014 the dynamic personalized PDP surface.\n *\n * Config-first flow: gathers visitor signals from the SDK runtime, scores the\n * visitor's archetype, mounts the host-supplied config presets, and generates\n * the remaining live slots via the runtime container's /api/pdp/module and\n * /api/pdp/dossier endpoints. Customer code mounts this one element; everything\n * else is internal to the SDK.\n *\n * Attributes (kebab-case, set by host markup):\n * - product-id (required)\n * - product-name (required)\n * - product-blurb (optional)\n *\n * Properties (set by mountable, not host markup):\n * - runtimeRef: SmartCanvasRuntime \u2014 passed by the AppLoader through\n * the existing widget-mountable mechanism.\n */\nexport class SyntroPdpLit extends LitElement {\n static override properties = {\n productId: { type: String, attribute: 'product-id' },\n productName: { type: String, attribute: 'product-name' },\n productBlurb: { type: String, attribute: 'product-blurb' },\n runtimeRef: { attribute: false },\n takeover: { attribute: false },\n _decisionState: { state: true },\n _pageUnresolvable: { state: true },\n _presetsSettled: { state: true },\n };\n\n productId = '';\n productName = '';\n productBlurb = '';\n\n /**\n * Page-takeover anatomy (FEAT-1785977524), normally delivered through the\n * widget `props` below. Presence at CONNECT time latches the element into\n * takeover mode: the engine owns the root and the six-slot compose path\n * never starts. Setting it after a legacy connect is ignored (warned) \u2014\n * flipping modes on a live element could leave both paths' DOM behind.\n */\n takeover: PdpTakeoverConfig | undefined = undefined;\n\n private _propsValue: SyntroPdpProps | undefined = undefined;\n\n /**\n * Widget props (the `adaptive-product:pdp` mountable sets `el.props` before\n * appending). Mapped IMMEDIATELY in the setter \u2014 not in willUpdate \u2014 so\n * `takeover` is already on the element when connectedCallback latches the\n * mode. Absent fields leave the element's current values untouched.\n */\n get props(): SyntroPdpProps | undefined {\n return this._propsValue;\n }\n\n set props(value: SyntroPdpProps | undefined) {\n this._propsValue = value;\n if (!value) return;\n if (value.productId) this.productId = value.productId;\n if (value.productName) this.productName = value.productName;\n if (value.productBlurb) this.productBlurb = value.productBlurb;\n if (value.takeover) this.takeover = value.takeover;\n }\n\n /**\n * SDK runtime. Null until the mountable wires it in. We only render\n * the personalized surface once runtimeRef is present AND the\n * activation gate passes.\n */\n runtimeRef: SmartCanvasRuntime | null = null;\n\n /** Canonical decision and render state. */\n private _decisionState: PdpState = createInitialPdpState();\n\n /**\n * WI-6b self-identification state. `_selfIdentify` latches true when the\n * element connects WITHOUT a product-id attr (config-placement shape) \u2014\n * from then on the page URL, not the attr, owns identity, and SPA\n * navigations re-resolve it. `_pageUnresolvable` (reactive) makes render()\n * produce NOTHING \u2014 fail-closed, consistent with the missing-preset\n * behavior: never a broken box on a non-product page.\n */\n private _selfIdentify = false;\n private _pageUnresolvable = false;\n private _navUnsub: (() => void) | null = null;\n\n /**\n * WI-4 presets readiness. False while a config-driven `pdpPresetsUrl`\n * fetch may still land; flips true (reactive) once `pdpPresetsReady()`\n * settles \u2014 which is immediate when no fetch is in flight. Once settled,\n * an element whose product still has no preset row renders NOTHING\n * (fail-closed) instead of an eternal loading skeleton.\n */\n private _presetsSettled = false;\n private _presetsReadyWired = false;\n\n private _composeStartedAt = 0;\n private _lastArchetype = '';\n private _mountDurations = new Map<string, number>();\n\n /**\n * Native takeover state (FEAT-1784346204). `_nativeSlots` holds the slot ids\n * whose native host section exists OUTSIDE this element \u2014 the render loop\n * skips those so the element never renders a duplicate qa/peer-feed section.\n * `_nativeHandles` tracks each active swap so it can be restored (before a\n * re-swap, and on disconnect).\n */\n private _nativeSlots = new Set<string>();\n private _nativeHandles = new Map<string, NativeSwapHandle>();\n\n /**\n * Takeover mode latch \u2014 decided ONCE, at the element's FIRST connect, from\n * the `takeover` prop, and kept for the element's whole lifetime. While\n * true, Lit never renders into this element (see shouldUpdate): the engine\n * renders imperatively and its deactivateRoot() calls replaceChildren() on\n * this root, which would destroy Lit's child parts. The latch is one-way in\n * BOTH directions \u2014 an element that ever rendered legacy has live Lit parts\n * (engaging the engine would render next to them, then destroy them), and\n * an element the engine ever owned has no parts left for Lit to resume.\n */\n private _takeoverMode = false;\n private _takeoverModeLatched = false;\n\n /** The single in-flight/settled takeover run for this connected lifetime. */\n private _takeoverRun: {\n disposed: boolean;\n detach: (() => void) | null;\n result: Promise<TakeoverResult | null>;\n } | null = null;\n\n /**\n * Settles when the previous run's dispose has fully completed. A re-run\n * (reconnect, or a synchronous move between parents) chains on this so a\n * LATE dispose from the torn-down run can never deactivate the root the\n * new run is rendering into.\n */\n private _takeoverDisposal: Promise<unknown> = Promise.resolve();\n\n /** A previous run deactivated the root (display:none + emptied); a re-run\n * after reconnect must undo that before the engine renders again. */\n private _takeoverRanBefore = false;\n\n private _takeoverLateWarned = false;\n\n /**\n * bfcache re-entry (review F4). pagehide disposes the run\n * (attachLifecycleDispose) but a bfcache restore does NOT re-fire\n * connectedCallback \u2014 without this listener the restored page would sit on\n * the native fallback forever. `persisted` is true only for bfcache\n * restores; the normal-load pageshow is owned by the connect path. Arrow\n * function per Lit event-handler convention (stable identity for\n * add/removeEventListener).\n */\n private _onTakeoverPageShow = (event: Event): void => {\n if (!(event as PageTransitionEvent).persisted) return;\n if (!this._takeoverMode || !this.isConnected) return;\n const config = this.takeover;\n if (!config) return;\n // Clear the pagehide-disposed run record (engine dispose is idempotent \u2014\n // pinned by the F12 test), then re-enter through the same\n // _takeoverDisposal serialization every re-run uses.\n this._teardownTakeover();\n this._startTakeover(config);\n };\n\n /**\n * The scored axes from the current config pass \u2014 the SAME values\n * `_fireFromConfig` resolved (sport-archetype preset key + decision archetype).\n * Captured at scoring time so they are already on the element when mounts\n * apply, which is BEFORE `_fireFromConfig` returns. Both default to 'unknown'.\n */\n private _resolvedArchetype = 'unknown';\n private _resolvedDecision = 'unknown';\n\n private _effectExecutor = createPdpEffectExecutor({\n dispatch: (event) => this._dispatchDecision(event),\n startCompose: (generation, run) => this._startDecisionCompose(generation, run),\n captureTelemetry: (name, props) => this._captureDecisionTelemetry(name, props),\n warn: (code, props) => this._warnDecision(code, props),\n cleanup: () => this._cleanupDecisionEffects(),\n });\n\n private _dispatchDecision(event: PdpEvent): void {\n const transition = transitionPdp(this._decisionState, event);\n this._decisionState = transition.state;\n for (const effect of transition.effects) void this._effectExecutor.execute(effect);\n }\n\n private _onDossierToggle = (e: Event): void => {\n const open = (e.target as HTMLDetailsElement).open;\n this._dispatchDecision({ type: 'dossier-toggled', open });\n };\n\n override createRenderRoot() {\n // Light DOM so existing customer CSS (brand-token bridge, page\n // layout) cascades into the inner section frames.\n return this;\n }\n\n override connectedCallback(): void {\n // \u2500\u2500 Takeover mode (FEAT-1785977524) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Latched at FIRST connect: prop present \u21D2 the engine owns this element\n // INSTEAD OF the six-slot compose path \u2014 none of the decision-machine\n // sync below runs, so the legacy path can never render alongside a\n // takeover. Prop absent \u21D2 the takeover code is fully inert and the\n // legacy path below is byte-identical to pre-takeover behavior. Later\n // reconnects KEEP the first-connect mode (see the latch field docs);\n // switching modes requires a fresh element.\n //\n // KNOWN LIMITATION (editor-sdk live-update path): props patched AFTER\n // this latch silently no-op for engine-captured values \u2014 _startTakeover\n // snapshots productId/productName, the signal bag, and the decision\n // vocabulary when the run starts, and the takeover config itself is only\n // read here. A live config edit must re-mount the widget (fresh element)\n // to take effect; in-place patches will appear to do nothing.\n if (!this._takeoverModeLatched) {\n this._takeoverModeLatched = true;\n this._takeoverMode = Boolean(this.takeover);\n } else if (!this._takeoverMode && this.takeover && !this._takeoverLateWarned) {\n this._takeoverLateWarned = true;\n console.warn(\n '[syntro-pdp] takeover prop set on an element that already connected on the ' +\n 'standard compose path \u2014 ignored (mode is latched per element). Mount a ' +\n 'fresh element with `takeover` in its widget props.'\n );\n }\n if (this._takeoverMode) {\n super.connectedCallback();\n ensureSectionLayoutStyles();\n if (typeof window !== 'undefined') {\n // remove-then-add keeps this idempotent under re-entrant connects.\n window.removeEventListener('pageshow', this._onTakeoverPageShow);\n window.addEventListener('pageshow', this._onTakeoverPageShow);\n }\n if (this.takeover) this._startTakeover(this.takeover);\n return;\n }\n const reconnecting = this._decisionState.connection === 'disconnected';\n super.connectedCallback();\n ensureSectionLayoutStyles();\n // WI-6b: no product-id attr at connect \u2192 the element owns its identity\n // from the page URL (config placement injects one static HTML string\n // across all product pages). Latched \u2014 an attr-driven element never\n // flips into self-identify mode.\n if (!this.productId) this._selfIdentify = true;\n // On vela's server-rendered `/pdp/dynamic` pages the native sections are in\n // the initial HTML, so they exist by the time the element upgrades.\n this._refreshNativeSlots();\n this._syncDecisionRuntime(reconnecting);\n this._syncDecisionPortal();\n // WI-4: gate the one-shot fire on presets readiness. When a\n // config-driven fetch is in flight at connect, `configured` was false in\n // the sync above; re-sync once the fetch settles so a late-arriving\n // table starts the compose (startCompose only fires while the stream is\n // idle, so an already-running compose is never disturbed).\n if (!this._presetsReadyWired) {\n this._presetsReadyWired = true;\n void pdpPresetsReady().then(() => {\n this._presetsSettled = true;\n if (this.isConnected) this._syncDecisionRuntime(false);\n });\n }\n }\n\n override disconnectedCallback(): void {\n if (this._takeoverMode) {\n super.disconnectedCallback();\n if (typeof window !== 'undefined') {\n window.removeEventListener('pageshow', this._onTakeoverPageShow);\n }\n // Abort-and-restore (spec: SPA navigation / unmount mid-takeover):\n // detached host sections reattach, the root deactivates. A reconnect\n // re-evaluates takeover fresh via connectedCallback.\n this._teardownTakeover();\n return;\n }\n super.disconnectedCallback();\n // WI-6b: drop the SPA-navigation subscription (re-wired on reconnect).\n this._navUnsub?.();\n this._navUnsub = null;\n // Restore every native host section to its ORIGINAL nodes (same references \u2014\n // a React host may still hold refs to them).\n for (const handle of this._nativeHandles.values()) handle.restore();\n this._nativeHandles.clear();\n this._dispatchDecision({ type: 'disconnected' });\n }\n\n override shouldUpdate(changed: PropertyValues): boolean {\n // Takeover mode: Lit must NEVER render into this element. The engine owns\n // the root imperatively, and deactivateRoot() calls replaceChildren() \u2014\n // fatal to Lit child parts if any were ever created here.\n if (this._takeoverMode) return false;\n // A takeover prop arriving AFTER a legacy connect is refused: the compose\n // path may already have rendered, and engaging now could leave both\n // paths' DOM in the document. Reconnect with the prop set to take over.\n if (changed.has('takeover') && this.takeover && !this._takeoverLateWarned) {\n this._takeoverLateWarned = true;\n console.warn(\n '[syntro-pdp] takeover prop set after the element already connected on the ' +\n 'standard compose path \u2014 ignored. Provide `takeover` in the widget props ' +\n 'before the element connects (or re-mount it).'\n );\n }\n return true;\n }\n\n override willUpdate(changed: PropertyValues): void {\n if (changed.has('runtimeRef')) {\n this._syncDecisionRuntime(false);\n }\n // Re-probe before every render so section suppression stays correct even if\n // the host sections upgraded after the element connected (lazy hydration).\n this._refreshNativeSlots();\n this._syncDecisionPortal();\n }\n\n /**\n * Probe the document for native host sections (`data-syntro-slot=\"qa\"` /\n * `\"peer-feed\"`) that live OUTSIDE this element, and record their slot ids.\n * The render loop skips these slots; the mount path swaps into them.\n */\n private _refreshNativeSlots(): void {\n if (typeof document === 'undefined') return;\n this._nativeSlots.clear();\n for (const [slotId, kind] of Object.entries(NATIVE_KIND_BY_SLOT)) {\n const native = findNativeSlot(document, kind);\n if (native && !this.contains(native)) this._nativeSlots.add(slotId);\n }\n }\n\n /**\n * A visitor is personalized unless BOTH scored axes declined to 'unknown'.\n * A fully-unknown visitor keeps the host's default native content untouched.\n */\n private _isPersonalizedVisitor(): boolean {\n return !(this._resolvedArchetype === 'unknown' && this._resolvedDecision === 'unknown');\n }\n\n override updated(_changed: PropertyValues): void {\n // If the host page exposes an above-the-fold portal, render the page-wide\n // \"why box\" THERE \u2014 leading the page, ahead of the standard product card \u2014\n // instead of inline above the modules. render() omits the inline copy when\n // the portal exists, so the portal is the single source of truth.\n //\n const view = selectPdpView(this._decisionState);\n const portal = this._whyPortalEl();\n if (!portal) return;\n render(view.whyBox.destination === 'portal' ? this._renderWhyBox(view.whyBox) : '', portal);\n }\n\n /** The host page's above-the-fold mount for the page-wide why box, if any. */\n private _whyPortalEl(): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n return document.querySelector<HTMLElement>('[data-syntro-pdp-why-portal]');\n }\n\n // \u2500\u2500 WI-6b: page-URL self-identification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Resolve identity from the page URL when in self-identify mode. Runs at\n * every runtime sync (connect + runtimeRef arrival) \u2014 idempotent. A miss\n * marks the page unresolvable: render() then produces NOTHING (fail-closed)\n * and a warning names the URL. While runtimeRef is still null we stay\n * quiet \u2014 the mountable wires it in moments later and we re-run.\n */\n private _resolvePageProduct(): void {\n if (!this._selfIdentify || !this.runtimeRef) return;\n const identity = this.runtimeRef.getPageProductIdentity?.() ?? null;\n if (identity) {\n this._pageUnresolvable = false;\n this.productId = identity.productHandle;\n // Product name is OPTIONAL on the resolved path \u2014 a page URL carries no\n // human-readable name; preset/module payloads supply the content, and\n // composeModule tolerates an empty product_name.\n if (!this.productName && identity.productTitle) this.productName = identity.productTitle;\n return;\n }\n if (!this._pageUnresolvable) {\n this._pageUnresolvable = true;\n this.productId = '';\n const url = typeof window !== 'undefined' ? window.location.href : '';\n console.warn(\n `[syntro-pdp] no product-id attribute and the page URL did not resolve to a ` +\n `product \u2014 rendering nothing (fail-closed). url=${url}`\n );\n }\n }\n\n /**\n * Subscribe to the runtime's NavigationMonitor so SPA/PJAX product-page\n * changes re-resolve identity and re-fire (WI-6b). Self-identified elements\n * only \u2014 an attr-driven element's identity is pinned by its attr. Skips\n * gracefully when the runtime lacks `navigation` (bare test mounts).\n */\n private _wireNavigation(): void {\n if (!this._selfIdentify || this._navUnsub) return;\n const nav = this.runtimeRef?.navigation;\n if (!nav || typeof nav.subscribe !== 'function') return;\n this._navUnsub = nav.subscribe((url) => this._onPageNavigated(url));\n }\n\n /** Re-resolve on navigation; on a product change reset the decision machine\n * through its sanctioned disconnect/reconnect path and re-fire. */\n private _onPageNavigated(navUrl: string): void {\n if (!this._selfIdentify || !this.isConnected || !this.runtimeRef) return;\n const identity = this.runtimeRef.getPageProductIdentity?.() ?? null;\n const next = identity?.productHandle ?? '';\n if (next === this.productId) return; // same product (or still unresolvable)\n // Reset: 'disconnected' tears down the current generation (restores native\n // swaps' twin state via cleanup effects); the reconnect sync below starts a\n // fresh compose for the new product \u2014 or goes dark when unresolvable.\n this._dispatchDecision({ type: 'disconnected' });\n if (identity) {\n this._pageUnresolvable = false;\n this.productId = identity.productHandle;\n if (identity.productTitle) this.productName = identity.productTitle;\n } else {\n this.productId = '';\n if (!this._pageUnresolvable) {\n this._pageUnresolvable = true;\n console.warn(\n `[syntro-pdp] navigation left the product surface \u2014 rendering nothing ` +\n `(fail-closed). url=${navUrl}`\n );\n }\n }\n this._syncDecisionRuntime(true);\n }\n\n private _syncDecisionRuntime(reconnecting: boolean): void {\n this._resolvePageProduct();\n this._wireNavigation();\n let appActive = false;\n try {\n appActive = Boolean(this.runtimeRef?.apps?.has?.('adaptive-product'));\n } catch {\n // Runtime integrations remain fail-soft during the shadow cutover.\n }\n const configured = Boolean(\n _readPdpPresets()?.[this.productId] && typeof _readSynOS()?.score === 'function'\n );\n const status = {\n runtimeAvailable: this.runtimeRef !== null,\n appActive,\n configured,\n };\n this._dispatchDecision(\n reconnecting ? { type: 'reconnected', ...status } : { type: 'runtime-updated', ...status }\n );\n }\n\n private _syncDecisionPortal(): void {\n this._dispatchDecision({ type: 'portal-updated', present: this._whyPortalEl() !== null });\n }\n\n // \u2500\u2500 Takeover wiring (FEAT-1785977524) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /**\n * Run the takeover engine with real ports: this element as the root,\n * `SynOS.authedFetch` as the plan/module transport and the existing slot\n * machinery as the renderer. Activation is config delivery itself: the\n * served config either carries `takeover` or it doesn't. Runs at\n * most once per connected lifetime (`_takeoverRun` latch); disconnect tears\n * it down and a reconnect re-evaluates fresh.\n */\n private _startTakeover(config: PdpTakeoverConfig): void {\n if (this._takeoverRun) return; // double-start guard (reconnect races)\n if (!this.productId) {\n // The server contract REQUIRES product_id (min 1) \u2014 a takeover call\n // without one would 422. No silent default (`feedback_no_silent_defaults`):\n // skip the takeover entirely, loudly, and leave the native page as-is.\n this._track('takeover.missing_product_id', {});\n console.warn(\n '[syntro-pdp] takeover requires a product id (widget props / product-id attr) \u2014 ' +\n 'skipping takeover, native page untouched.'\n );\n return;\n }\n const synos = _readSynOS();\n const authedFetch = synos?.authedFetch;\n const signals = { ...gatherVisitorSignals(), chat_excerpt: readChatExcerpt() };\n const decisionDescriptions = _readPdpDecisionArchetypes() ?? {};\n const telemetry = (event: string, props?: Record<string, unknown>): void =>\n this._track(event, props);\n\n // The engine forwards only the PLAN to renderModules; the server-scored\n // decision archetype crosses from fetchPlan to the renderer through this\n // holder (preset-row selection + live-module context both need it).\n const scored = { decisionArchetype: 'unknown' };\n const fetchPlan = authedFetch\n ? createTakeoverPlanFetcher({\n authedFetch,\n config,\n productId: this.productId,\n productName: this.productName,\n decisionArchetypes: decisionDescriptions,\n // No client-side summary exists on the takeover path (the server\n // scores); the field mirrors /api/pdp/module's for the scorer.\n behaviorSummary: '',\n signals,\n })\n : null;\n\n const run: NonNullable<SyntroPdpLit['_takeoverRun']> = {\n disposed: false,\n detach: null,\n result: Promise.resolve(null),\n };\n\n const renderModules = createTakeoverModuleRenderer({\n root: this,\n getPresetRow: (decisionArchetype) => {\n const table = _readPdpPresets()?.[this.productId];\n return table?.[decisionArchetype] ?? table?.unknown;\n },\n resolveTag: (kind, variant) =>\n resolveTagForMount({\n slot: `pdp-slot-${kind}`,\n kind,\n variant,\n header: { eyebrow: '', title: '', accent: null, subtitle: null },\n } as PdpPlanTaskClient),\n requestLiveModule: async (kind, decisionArchetype) => {\n if (!authedFetch) return null;\n return composeModule(authedFetch, {\n productName: this.productName,\n // Guaranteed non-empty: _startTakeover gates on a missing product id.\n productId: this.productId,\n kind,\n variant: null,\n // The takeover round trip scores only the decision axis (the server\n // resolves the plan from it); the sport-archetype axis is unscored\n // on this path, so it is sent as an explicit 'unknown' \u2014 never a\n // silently-invented value.\n archetype: { slug: 'unknown', description: '' },\n decisionArchetype:\n decisionArchetype === 'unknown'\n ? null\n : {\n slug: decisionArchetype,\n description: decisionDescriptions[decisionArchetype] ?? '',\n },\n behaviorSummary: '',\n signals,\n });\n },\n telemetry,\n getDecisionArchetype: () => scored.decisionArchetype,\n });\n\n const ports = createDomPorts(config, {\n root: this,\n signalCount: countTakeoverSignals(signals),\n fetchPlan: fetchPlan\n ? async () => {\n const response = await fetchPlan();\n scored.decisionArchetype = response.decisionArchetype;\n return response;\n }\n : async () => {\n // No authed transport \u21D2 no plan \u21D2 the engine fails open.\n throw new Error('SynOS.authedFetch unavailable \u2014 cannot fetch takeover plan');\n },\n // Torn-down guard: the progressive path's dispose is a no-op (nothing\n // was detached), so its BACKGROUND render has no cancellation handle \u2014\n // this gate is what stops a stale run from appending into a root a\n // newer run may own by then.\n renderModules: async (plan, onModuleFailed) => {\n if (run.disposed) return;\n return renderModules(plan, onModuleFailed);\n },\n telemetry,\n });\n\n // Chain on the PREVIOUS run's disposal: a synchronous move between\n // parents tears down run N and starts run N+1 in the same task \u2014 without\n // this ordering, run N's late dispose() would deactivate (hide + empty)\n // the root while run N+1 is rendering into it.\n run.result = this._takeoverDisposal\n .then((): Promise<TakeoverResult | null> | null => {\n if (run.disposed) return null; // torn down before the engine started\n // A prior run's dispose deactivated the root \u2014 undo before rendering.\n if (this._takeoverRanBefore) {\n this.style.removeProperty('display');\n this.replaceChildren();\n }\n this._takeoverRanBefore = true;\n return runTakeover(config, ports, { commitTimeoutMs: config.commitTimeoutMs });\n })\n .then((result) => {\n if (!result) return null;\n telemetry('takeover.outcome', { outcome: result.outcome });\n if (run.disposed) {\n // Torn down (disconnect) before the engine settled \u2014 dispose NOW so\n // any committed detach is rolled back.\n result.dispose();\n return result;\n }\n run.detach = attachLifecycleDispose(result);\n return result;\n })\n .catch((err) => {\n // runTakeover is designed never to reject; belt and braces so a\n // defect can never strand a detached page.\n telemetry('takeover.run_failed', { error: String(err).slice(0, 200) });\n return null;\n });\n this._takeoverRun = run;\n }\n\n /** Dispose the current takeover run (idempotent). */\n private _teardownTakeover(): void {\n const run = this._takeoverRun;\n if (!run) return;\n this._takeoverRun = null;\n run.disposed = true;\n run.detach?.();\n run.detach = null;\n this._takeoverDisposal = run.result.then((result) => result?.dispose());\n }\n\n /**\n * Emit a telemetry event via the runtime's track API.\n * Wrapped in try/catch so a broken telemetry integration never crashes the\n * compose flow. Always includes `source: 'sdk'` in props.\n */\n private _track(eventName: string, props: Record<string, unknown> = {}): void {\n try {\n this.runtimeRef?.telemetry?.track?.(eventName, { ...props, source: 'sdk' });\n } catch {\n // Never let telemetry failures surface to callers.\n }\n }\n\n private _captureDecisionTelemetry(name: string, props: Record<string, unknown>): void {\n const enriched = { ...props };\n if (name === 'syntro_pdp_plan_received' || name === 'syntro_pdp_done') {\n enriched.duration_ms = Math.round(performance.now() - this._composeStartedAt);\n }\n if (name === 'syntro_pdp_done') enriched.archetype = this._lastArchetype;\n if (name === 'syntro_pdp_section_mounted' && typeof props.slot === 'string') {\n enriched.duration_ms = this._mountDurations.get(props.slot) ?? 0;\n }\n this._track(name, enriched);\n }\n\n private _warnDecision(code: string, props: Record<string, unknown>): void {\n if (code === 'missing-config') {\n console.warn(\n `[syntro-pdp] no PDP presets for product \"${this.productId}\" (or the ` +\n 'scored-template primitive is unavailable) \u2014 skipping personalization. ' +\n 'Deliver presets via the SDK config `pdpPresetsUrl` (or the legacy ' +\n '__SYNTRO_PDP_PRESETS__ boot global) to enable the config-first PDP.'\n );\n return;\n }\n if (code === 'mount-miss' && typeof props.slot === 'string') {\n console.warn(\n `syntro-pdp.mount_miss: no slot container for \"${props.slot}\" even after updateComplete.`\n );\n return;\n }\n console.warn(`[syntro-pdp] ${code}`, props);\n }\n\n private _clearDecisionPortal(): void {\n const portal = this._whyPortalEl();\n if (portal) render('', portal);\n }\n\n private _cleanupDecisionEffects(): void {\n this._clearDecisionPortal();\n }\n\n private async _startDecisionCompose(generation: number, run: PdpEffectRun): Promise<void> {\n const productPresets = _readPdpPresets()?.[this.productId];\n const synos = _readSynOS();\n if (!productPresets || typeof synos?.score !== 'function') {\n throw new Error('PDP configuration became unavailable');\n }\n await this._fireFromConfig(\n productPresets,\n synos.score,\n synos.authedFetch,\n generation,\n run,\n synos.scoreMulti\n );\n }\n\n /** Mount each instance into its slot. Shared by config and compose. */\n private async _applyMounts(\n env: MutationEnvelope,\n warnOnMiss = true,\n isCurrent: () => boolean = () => true\n ): Promise<MountOutcome[]> {\n const outcomes: MountOutcome[] = [];\n // The slot containers render reactively from decision state; wait\n // for that pending render so the containers exist before we mount into them.\n await this.updateComplete;\n if (!isCurrent()) return outcomes;\n for (const m of env.mutations) {\n if (!isCurrent()) return outcomes;\n if (m.op !== 'mount') continue;\n const inst = m.instance;\n if (inst.placement.type !== 'slot') continue;\n const slotId = (inst.placement as { type: 'slot'; slot: string }).slot;\n const slotStartedAt = performance.now();\n\n // \u2500\u2500 Native takeover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // If this slot has a native host section OUTSIDE the element, swap into\n // it instead of mounting the element's own widget (the section render\n // loop already skipped rendering our twin). Runs BEFORE the internal\n // container lookup because a suppressed slot has no internal container.\n const nativeKind = NATIVE_KIND_BY_SLOT[slotId];\n if (nativeKind) {\n const native =\n typeof document !== 'undefined' ? findNativeSlot(document, nativeKind) : null;\n if (native && !this.contains(native)) {\n if (!this._isPersonalizedVisitor()) {\n // Fully-unknown visitor \u2014 the host's DEFAULT content is what shows.\n // If a prior (personalized) generation swapped this slot, restore it\n // first: without this, a re-fire that declines to unknown would leave\n // the stale personalized content in the host DOM while claiming the\n // defaults were kept.\n this._nativeHandles.get(slotId)?.restore();\n this._nativeHandles.delete(slotId);\n console.info(`syntro-pdp.native_default_kept: ${slotId}`);\n outcomes.push({ status: 'native-default-kept', slot: slotId });\n continue;\n }\n const fields = nativeFieldsFromContent(nativeKind, inst.content);\n // A container can only carry one live swap \u2014 restore the previous one\n // (e.g. a re-fired generation) before swapping again.\n this._nativeHandles.get(slotId)?.restore();\n this._nativeHandles.delete(slotId);\n const handle = swapNativeSlot(native, fields);\n if (handle) this._nativeHandles.set(slotId, handle);\n outcomes.push({ status: handle ? 'mounted-native' : 'missed', slot: slotId });\n continue;\n }\n }\n\n const container = this.querySelector(`[data-syntro-pdp-slot=\"${slotId}\"]`);\n if (!container) {\n if (warnOnMiss) {\n console.warn(\n `syntro-pdp.mount_miss: no slot container for \"${slotId}\" even after updateComplete.`\n );\n }\n outcomes.push({ status: 'missed', slot: slotId });\n continue;\n }\n const task =\n this._decisionState.plan.status === 'ready'\n ? this._decisionState.plan.plan.tasks.find((candidate) => candidate.slot === slotId)\n : undefined;\n const tag = task ? resolveTagForMount(task) : null;\n if (!tag) {\n outcomes.push({ status: 'ignored', slot: slotId });\n continue;\n }\n\n container.innerHTML = '';\n const el = document.createElement(tag) as HTMLElement & { data?: unknown };\n el.data = inst.content;\n container.appendChild(el);\n\n this._mountDurations.set(slotId, Math.round(performance.now() - slotStartedAt));\n outcomes.push({ status: 'mounted', slot: slotId });\n }\n return outcomes;\n }\n\n /** Render from config: score the archetype \u2192 mount the preset set-values. */\n private async _fireFromConfig(\n productPresets: Record<string, Record<string, SlotPreset>>,\n score: NonNullable<SynOSGlobal['score']>,\n authedFetch: SynOSGlobal['authedFetch'],\n generation: number,\n run: PdpEffectRun,\n scoreMulti?: SynOSGlobal['scoreMulti']\n ): Promise<void> {\n const startedAt = performance.now();\n this._composeStartedAt = startedAt;\n this._mountDurations.clear();\n // Reset the scored axes for this pass \u2014 the wrapped scorers below set them\n // the instant the model resolves, which is BEFORE any mount applies.\n this._resolvedArchetype = 'unknown';\n this._resolvedDecision = 'unknown';\n // `unknown` is the fallback preset key. The classifier adds its own\n // decline option, so it must not appear in the declared enum values.\n const archetypeValues = Object.keys(productPresets).filter((value) => value !== 'unknown');\n const archetypeDescriptions = _readPdpArchetypes() ?? {};\n const decisionDescriptions = _readPdpDecisionArchetypes() ?? {};\n const decisionValues = Object.keys(decisionDescriptions);\n const signals = { ...gatherVisitorSignals(), chat_excerpt: readChatExcerpt() };\n\n try {\n // Set when onScored dispatched the explanation early \u2014 the post-compose\n // dispatch then skips, so the early (score-time) latency metric and\n // content are never overwritten with one that counts live-module time.\n let explanationDispatched = false;\n let scoredArgs: { archetype: string; decisionArchetype: string } | null = null;\n const composeAdapter = createPdpComposeAdapter({\n scoreSummary: () =>\n authedFetch\n ? score({ model_id: 'summary', name: 'behavior_summary', type: 'string', signals })\n : Promise.resolve('unknown'),\n requestDossier: () =>\n authedFetch ? fetchDossier(authedFetch) : Promise.resolve<PdpExplainMetric[]>([]),\n resolveTemplate: (_currentGeneration, composeUnset) =>\n resolveScoredTemplate<PdpPlanEnvelope, MutationEnvelope>({\n // Wrap the scorer so the resolved SPORT archetype lands on the\n // element the instant it scores \u2014 the native-takeover check needs it\n // before the set-slot mounts apply. In the two-axis path scoreMulti\n // runs instead (wrapped below) and this only serves the archetype.\n score: async (spec) => {\n const value = await score(spec);\n this._resolvedArchetype = value;\n return value;\n },\n scoreSpec: {\n model_id: 'llm',\n name: 'archetype',\n type: 'enum',\n values: archetypeValues,\n value_descriptions: Object.fromEntries(\n archetypeValues\n .filter((value) => archetypeDescriptions[value])\n .map((value) => [value, archetypeDescriptions[value]])\n ),\n signals,\n },\n decisionSpec:\n decisionValues.length > 0 && scoreMulti\n ? {\n model_id: 'llm',\n name: 'decision_archetype',\n type: 'enum',\n values: decisionValues,\n value_descriptions: decisionDescriptions,\n signals,\n }\n : undefined,\n // Wrap the batched scorer to capture BOTH axes (sport archetype +\n // decision archetype) at scoring time \u2014 mirrors resolveScoredTemplate's\n // own `?? 'unknown'` normalization so the two never disagree.\n scoreMulti: scoreMulti\n ? async (specs) => {\n const results = await scoreMulti(specs);\n this._resolvedArchetype = results[0] ?? 'unknown';\n this._resolvedDecision = results[1] ?? 'unknown';\n return results;\n }\n : undefined,\n transformPreset: (preset, decisionValue) => ({\n ...preset,\n data: selectPresetData(preset.kind, preset.data, decisionValue),\n }),\n presets: productPresets,\n slotIds: [...PDP_SLOT_IDS],\n orderSlots: orderPdpSlots,\n fallbackValue: 'unknown',\n buildPlan: buildPdpPlan,\n buildMount: (slot: string, preset: SlotPreset): MutationEnvelope => ({\n type: 'syntro.element.mutation',\n schema_version: 'v0',\n mutations: [\n {\n op: 'mount',\n instance: {\n instance_id: `cfg-${slot}`,\n element_type: 'tile',\n template_id: `pdp-${preset.kind}`,\n placement: { type: 'slot', slot },\n content: preset.data as Record<string, unknown>,\n state: 'mounted',\n mounted_by: { agent_id: 'config', turn_id: 'config' },\n mounted_at: new Date().toISOString(),\n version: 1,\n },\n },\n ],\n }),\n emitPlan: (plan) => {\n if (!run.isCurrent()) return;\n run.dispatch({ type: 'plan-received', generation, plan });\n },\n emitMount: async (env) => {\n if (!run.isCurrent()) return;\n const outcomes = await this._applyMounts(env, false, run.isCurrent);\n if (!run.isCurrent()) return;\n for (const outcome of outcomes) {\n if (outcome.status === 'mounted') {\n run.dispatch({ type: 'mount-succeeded', generation, slot: outcome.slot });\n } else if (outcome.status === 'missed') {\n run.dispatch({ type: 'mount-missed', generation, slot: outcome.slot });\n }\n }\n },\n composeUnset: authedFetch ? composeUnset : undefined,\n }),\n requestModule: async ({ slot, archetype, decisionArchetype, behaviorSummary }) => {\n if (!authedFetch || !run.isCurrent()) return;\n const kind = slot.replace('pdp-slot-', '');\n const payload = await composeModule(authedFetch, {\n productName: this.productName,\n productId: this.productId || null,\n kind,\n variant: null,\n archetype: {\n slug: archetype,\n description: archetypeDescriptions[archetype] ?? '',\n },\n decisionArchetype:\n decisionArchetype === 'unknown'\n ? null\n : {\n slug: decisionArchetype,\n description: decisionDescriptions[decisionArchetype] ?? '',\n },\n behaviorSummary,\n signals,\n });\n if (!run.isCurrent()) return;\n if (!payload) {\n run.dispatch({ type: 'module-failed', generation, slot });\n return;\n }\n const outcomes = await this._applyMounts(\n _buildLiveMount(slot, kind, payload),\n false,\n run.isCurrent\n );\n if (!run.isCurrent()) return;\n const outcome = outcomes.find((candidate) => candidate.slot === slot);\n if (outcome?.status === 'missed') {\n run.dispatch({ type: 'mount-missed', generation, slot });\n return;\n }\n if (\n outcome?.status !== 'mounted' &&\n // A native takeover (swap into the host section, or defaults kept) is\n // a success, not a failure \u2014 unreachable on vela today (native slots\n // are always config-SET), but keeps the live path honest if a native\n // slot is ever left unset. The follow-on reasoning dispatch is inert\n // because the native section is suppressed (never rendered).\n outcome?.status !== 'mounted-native' &&\n outcome?.status !== 'native-default-kept'\n ) {\n run.dispatch({ type: 'module-failed', generation, slot });\n return;\n }\n run.dispatch({ type: 'module-resolved', generation, slot });\n const rationale =\n typeof payload.rationale === 'string' && payload.rationale ? payload.rationale : '';\n const text =\n rationale ||\n `Generated live by the ${kind} agent for your \u201C${_titleCase(archetype)}\u201D archetype.`;\n run.dispatch({ type: 'live-reasoning-resolved', generation, slot, text });\n },\n // Why-box early exit (BUG-1783990580): the explanation needs only the\n // scored axes \u2014 surface it the INSTANT the score exists; the behavior\n // summary patches in when it lands (onSummary below), and the live\n // modules never gate it (24\u201385s trailing observed live).\n onScored: ({ archetype, decisionArchetype }) => {\n if (!run.isCurrent() || explanationDispatched) return;\n explanationDispatched = true;\n scoredArgs = { archetype, decisionArchetype };\n const durationMs = performance.now() - startedAt;\n const early = buildExplain({\n archetype,\n description: archetypeDescriptions[archetype] ?? '',\n decisionArchetype,\n decisionDescription: decisionDescriptions[decisionArchetype] ?? '',\n summary: '',\n signals,\n latencyMs: durationMs,\n });\n run.dispatch({\n type: 'explanation-resolved',\n generation,\n explanation: { ...early },\n durationMs,\n });\n run.dispatch({ type: 'dossier-resolved', generation, metrics: early.dossier });\n },\n // The summary line folds into the visible explanation when it lands \u2014\n // same pattern as the Tier-2 dossier fold below.\n onSummary: ({ behaviorSummary }) => {\n if (!run.isCurrent() || !scoredArgs || !behaviorSummary) return;\n const patched = buildExplain({\n archetype: scoredArgs.archetype,\n description: archetypeDescriptions[scoredArgs.archetype] ?? '',\n decisionArchetype: scoredArgs.decisionArchetype,\n decisionDescription: decisionDescriptions[scoredArgs.decisionArchetype] ?? '',\n summary: behaviorSummary,\n signals,\n latencyMs: performance.now() - startedAt,\n });\n run.dispatch({ type: 'explanation-resolved', generation, explanation: { ...patched } });\n },\n });\n\n const composed = await composeAdapter.compose(generation);\n if (!run.isCurrent()) return;\n const result = composed.template;\n this._lastArchetype = result.value;\n if (!explanationDispatched) {\n const explanation = buildExplain({\n archetype: result.value,\n description: archetypeDescriptions[result.value] ?? '',\n decisionArchetype: result.decisionValue,\n decisionDescription: decisionDescriptions[result.decisionValue] ?? '',\n summary: composed.summary === 'unknown' ? '' : composed.summary,\n signals,\n latencyMs: performance.now() - startedAt,\n });\n run.dispatch({\n type: 'explanation-resolved',\n generation,\n explanation: { ...explanation },\n durationMs: performance.now() - startedAt,\n });\n run.dispatch({ type: 'dossier-resolved', generation, metrics: explanation.dossier });\n }\n\n void composed.dossier.then((rows) => {\n if (!rows.length || !run.isCurrent()) return;\n run.dispatch({ type: 'dossier-resolved', generation, metrics: rows });\n });\n\n const archLabel = _titleCase(result.value);\n const decisionLabel =\n result.decisionValue === 'unknown' ? null : _titleCase(result.decisionValue);\n for (const slot of result.setSlots) {\n const text = decisionLabel\n ? `Chosen from the content library \u2014 matched to your \u201C${archLabel}\u201D archetype, showing the items that answer your \u201C${decisionLabel}\u201D question first.`\n : `Chosen from the content library \u2014 matched to your \u201C${archLabel}\u201D archetype.`;\n run.dispatch({ type: 'cached-reasoning-resolved', generation, slot, text });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200);\n throw new Error(message);\n }\n }\n\n /**\n * The demo \"why box\": surfaces the archetype + behavior-summary model outputs\n * and a row of impressive personalization metrics at the very top of the PDP\n * (below the host banner).\n *\n * The NARRATIVE MUST LEAD THE PAGE: a placeholder renders the instant the\n * compose flow starts streaming (before the archetype/summary land), then\n * fills IN PLACE when the decision explanation arrives. Previously it rendered nothing until\n * both models scored, so on a live load it popped in AFTER modules had already\n * mounted \u2014 the reasoning banner trailed the content instead of leading it.\n * Renders nothing only before streaming begins (pre-plan) or once done with no\n * explanation (nothing to say).\n */\n private _renderWhyBox(view: PdpWhyBoxView) {\n if (view.status === 'hidden') return '';\n if (view.status === 'loading') return this._renderWhyBoxLoading();\n const e = view.explanation;\n return html`\n <style>\n /* Token bridge: every color resolves from a --syntro-pdp-* var the\n * host (vela pages + SPA) publishes, falling back to a NEUTRAL host\n * palette elsewhere. No standalone navy/sky billboard \u2014 closed, this\n * reads as a subordinate line of the demo band above it. */\n [data-syntro-pdp-why] {\n margin: 0; padding: 4px 0;\n font-family: var(--syntro-pdp-sans, system-ui, -apple-system, 'Segoe UI', sans-serif);\n color: var(--syntro-pdp-fg, #1a1a1a);\n }\n /* Closed: one quiet muted line \u2014 no card chrome (no bg/border/shadow). */\n /* Collapsed: reads as a LINK, not fine print \u2014 accent color, dotted\n underline (solid on hover), a visible chevron, and a fit-content hit\n area (holistic-alignment pass, 2026-07-18). */\n [data-syntro-pdp-why] > summary {\n cursor: pointer; list-style: none; user-select: none;\n display: inline-flex; align-items: center; gap: 6px; width: fit-content;\n font-size: 12.5px; font-weight: 600;\n color: var(--syntro-pdp-accent, #555);\n text-decoration: underline dotted 1px;\n text-underline-offset: 3px;\n }\n [data-syntro-pdp-why] > summary:hover,\n [data-syntro-pdp-why] > summary:focus-visible {\n text-decoration: underline solid 1px; outline: none;\n }\n [data-syntro-pdp-why] > summary::-webkit-details-marker { display: none; }\n [data-syntro-pdp-why] > summary::after {\n content: '\u25BE'; font-size: 13px; line-height: 1;\n transition: transform 150ms ease;\n }\n [data-syntro-pdp-why][open] > summary::after { transform: rotate(180deg); }\n /* Open: NOTHING moves. The summary line stays exactly where it was\n (no card padding displacing it); the content unfolds BELOW it as a\n quiet text block in the same column \u2014 same left edge, no box, no\n second screen (holistic pass round 2, 2026-07-18). */\n [data-syntro-pdp-why][open] { margin: 0; padding: 0; background: none; border: none; }\n [data-syntro-pdp-why][open] > summary { margin-bottom: 6px; }\n [data-syntro-pdp-why-note] { font-size: 11px; color: var(--syntro-pdp-muted, #666); margin: 0 0 4px; }\n [data-syntro-pdp-why-archetype] { font-size: 13px; margin: 3px 0; color: var(--syntro-pdp-fg, #1a1a1a); }\n [data-syntro-pdp-why-archetype] strong { color: var(--syntro-pdp-primary, #1a1a1a); }\n [data-syntro-pdp-why-summary] {\n font-size: 12.5px; font-style: italic; color: var(--syntro-pdp-muted, #666); margin: 6px 0 8px;\n border-left: 2px solid var(--syntro-pdp-accent, #555); padding-left: 10px;\n }\n /* Metrics: a compact inline row \u2014 value\u00B7label pairs \u2014 not dashboard\n tiles. */\n [data-syntro-pdp-why-metrics] {\n display: flex; flex-wrap: wrap; gap: 4px 20px; margin-top: 6px;\n }\n [data-syntro-pdp-why-metric] { display: inline-flex; align-items: baseline; gap: 6px; }\n [data-syntro-pdp-why-metric-value] {\n font-size: 14px; font-weight: 700; color: var(--syntro-pdp-fg, #1a1a1a); line-height: 1.2;\n font-variant-numeric: tabular-nums;\n }\n [data-syntro-pdp-why-metric-label] {\n font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;\n color: var(--syntro-pdp-muted, #666);\n }\n [data-syntro-pdp-why-dossier] { margin-top: 8px; }\n [data-syntro-pdp-why-dossier] > summary {\n cursor: pointer; list-style: none; user-select: none; width: fit-content;\n font-size: 11px; font-weight: 600; color: var(--syntro-pdp-accent, #555);\n text-decoration: underline dotted 1px; text-underline-offset: 3px;\n }\n [data-syntro-pdp-why-dossier] > summary:hover { text-decoration: underline solid 1px; }\n [data-syntro-pdp-why-dossier] > summary::-webkit-details-marker { display: none; }\n [data-syntro-pdp-why-dossier] > summary::before { content: ''; }\n [data-syntro-pdp-why-dossier] > summary::after { content: ' \u25BE'; font-size: 11px; }\n [data-syntro-pdp-why-dossier][open] > summary::after { content: ' \u25B4'; }\n [data-syntro-pdp-why-dossier-list] { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }\n [data-syntro-pdp-why-dossier-row] { display: flex; gap: 12px; align-items: baseline; }\n [data-syntro-pdp-why-dossier-value] {\n flex: 0 0 auto; min-width: 56px; font-size: 13px; font-weight: 700; color: var(--syntro-pdp-fg, #1a1a1a);\n font-variant-numeric: tabular-nums;\n }\n [data-syntro-pdp-why-dossier-text] { display: flex; flex-direction: column; }\n [data-syntro-pdp-why-dossier-label] { font-size: 12px; font-weight: 600; color: var(--syntro-pdp-fg, #1a1a1a); }\n [data-syntro-pdp-why-dossier-hint] { font-size: 11.5px; color: var(--syntro-pdp-muted, #666); line-height: 1.4; }\n </style>\n <details data-syntro-pdp-why>\n <summary>How Syntrologie decided</summary>\n <div data-syntro-pdp-why-note>\n Shown in demo mode only \u2014 a peek under the hood at the live personalization.\n </div>\n <div data-syntro-pdp-why-archetype>\n Shopping as: <strong>${e.archetypeLabel}</strong>${\n e.archetypeDesc ? html` \u2014 ${e.archetypeDesc}` : ''\n }\n </div>\n ${\n e.decisionLabel\n ? html`<div data-syntro-pdp-why-archetype>\n What they're weighing: <strong>${e.decisionLabel}</strong>${\n e.decisionDesc ? html` \u2014 \u201C${e.decisionDesc}\u201D` : ''\n }\n </div>`\n : ''\n }\n ${e.summary ? html`<div data-syntro-pdp-why-summary>\"${e.summary}\"</div>` : ''}\n <div data-syntro-pdp-why-metrics>\n ${e.metrics.map(\n (m) => html`<div data-syntro-pdp-why-metric>\n <span data-syntro-pdp-why-metric-value>${m.value}</span>\n <span data-syntro-pdp-why-metric-label>${m.label}</span>\n </div>`\n )}\n </div>\n ${\n view.dossier.length\n ? html`<details data-syntro-pdp-why-dossier ?open=${view.dossierOpen} @toggle=${this._onDossierToggle}>\n <summary>What was measured</summary>\n <div data-syntro-pdp-why-dossier-list>\n ${view.dossier.map(\n (m) => html`<div data-syntro-pdp-why-dossier-row>\n <span data-syntro-pdp-why-dossier-value>${m.value}</span>\n <div data-syntro-pdp-why-dossier-text>\n <span data-syntro-pdp-why-dossier-label>${m.label}</span>\n ${m.hint ? html`<span data-syntro-pdp-why-dossier-hint>${m.hint}</span>` : ''}\n </div>\n </div>`\n )}\n </div>\n </details>`\n : ''\n }\n </details>\n `;\n }\n\n /**\n * The narrative placeholder: the same quiet \"How Syntrologie decided\" line as\n * the real why box (so it reads as \"the reasoning is loading\", not a different\n * element), with a shimmer bar where the archetype + summary will land.\n * Carries the same `data-syntro-pdp-why` marker plus `data-syntro-pdp-why-loading`,\n * so the swap to the filled box is a state change, not a first appearance.\n */\n private _renderWhyBoxLoading() {\n return html`\n <style>\n [data-syntro-pdp-why] {\n margin: 0; padding: 4px 0;\n font-family: var(--syntro-pdp-sans, system-ui, -apple-system, 'Segoe UI', sans-serif);\n color: var(--syntro-pdp-fg, #1a1a1a);\n }\n /* Same quiet collapsed line as the filled box \u2014 the fill-in is a state\n * change, not a first appearance. The \u25B8 is decorative here (nothing to\n * expand yet). */\n [data-syntro-pdp-why-loading-line] {\n display: inline-flex; align-items: center; gap: 6px; width: fit-content;\n font-size: 12.5px; font-weight: 600;\n color: var(--syntro-pdp-accent, #555);\n text-decoration: underline dotted 1px;\n text-underline-offset: 3px;\n opacity: 0.7;\n }\n [data-syntro-pdp-why-loading-line]::after {\n content: '\u25BE'; font-size: 13px; line-height: 1;\n }\n [data-syntro-pdp-why-shimmer] {\n height: 10px; border-radius: 5px; margin: 8px 0 0;\n background: linear-gradient(90deg, rgba(0,0,0,0.06) 25%, rgba(0,0,0,0.12) 37%, rgba(0,0,0,0.06) 63%);\n background-size: 400% 100%;\n animation: syntro-pdp-why-shimmer 1.4s ease infinite;\n }\n [data-syntro-pdp-why-shimmer][data-w='60'] { width: 60%; }\n @keyframes syntro-pdp-why-shimmer {\n 0% { background-position: 100% 0; }\n 100% { background-position: 0 0; }\n }\n </style>\n <div data-syntro-pdp-why data-syntro-pdp-why-loading aria-busy=\"true\">\n <span data-syntro-pdp-why-loading-line>How Syntrologie decided</span>\n <div data-syntro-pdp-why-shimmer data-w=\"60\"></div>\n </div>\n `;\n }\n\n override render() {\n // WI-6b fail-closed: a self-identifying element on a page that resolves\n // to no product renders NOTHING \u2014 no root, no skeleton, no broken box.\n if (this._pageUnresolvable) return html``;\n\n // WI-4 fail-closed: presets delivery has SETTLED (config fetch done or\n // none configured) and this product still has no preset row \u2014 nothing\n // will ever compose, so render NOTHING instead of an eternal skeleton.\n // Lazy store read: a late legacy global re-enters through the reactive\n // sync path (runtimeRef arrival / reconnect), which starts the compose\n // and moves the stream out of idle before this guard matters.\n if (\n this._presetsSettled &&\n this._decisionState.stream.status === 'idle' &&\n this._decisionState.plan.status === 'empty' &&\n !_readPdpPresets()?.[this.productId]\n ) {\n return html``;\n }\n\n // Inline-styled skeleton so the SDK ships its own loading affordance\n // without depending on host-page CSS. Light DOM is fine here \u2014 the\n // selectors are namespaced to `data-syntro-pdp-skeleton` so they\n // can't collide with customer styles.\n const skeletonStyles = SyntroPdpLit._skeletonStyles;\n const view = selectPdpView(this._decisionState);\n\n if (view.layout === 'skeleton') {\n // Pre-plan: render 3 generic section skeletons so the page reads\n // as \"loading personalized content\" instead of \"blank below the\n // fold.\" Counts/heights are tuned to roughly match the live\n // sections we expect (header + content frame).\n return html`\n ${skeletonStyles}\n <div data-syntro-pdp-root data-state=\"pending\" data-syntro-pdp-skeleton>\n ${[0, 1, 2].map(\n (i) => html`\n <section\n data-syntro-pdp-skeleton-section\n style=\"animation-delay: ${i * 0.15}s\"\n >\n <div data-syntro-pdp-skeleton-header>\n <div data-syntro-pdp-skeleton-eyebrow></div>\n <div data-syntro-pdp-skeleton-title></div>\n <div data-syntro-pdp-skeleton-subtitle></div>\n </div>\n <div data-syntro-pdp-skeleton-body></div>\n </section>\n `\n )}\n </div>\n `;\n }\n\n return html`\n ${skeletonStyles}\n <div data-syntro-pdp-root data-state=${view.rootState}>\n ${view.whyBox.destination === 'inline' ? this._renderWhyBox(view.whyBox) : ''}\n ${view.slots.map((slot) => {\n if (!slot.visible) return '';\n const task = slot.task;\n // Native takeover: the host renders its OWN qa/peer-feed section, so\n // never render a duplicate \u2014 the mount path swaps into the host one.\n if (this._nativeSlots.has(task.slot)) return '';\n return html`\n <section data-syntro-pdp-section data-slot=${task.slot}>\n <syntro-pdp-section-header\n .eyebrow=${task.header.eyebrow}\n .title=${task.header.title}\n .accent=${task.header.accent ?? ''}\n .subtitle=${task.header.subtitle ?? ''}\n ></syntro-pdp-section-header>\n <div data-syntro-pdp-section-inner>\n <div\n data-syntro-pdp-slot=${task.slot}\n data-slot-state=${slot.status}\n ></div>\n ${\n /*\n * Per-slot shimmer rendered as a SIBLING of the slot\n * container, NOT a child. `onMutation` clears the slot\n * via `container.innerHTML = ''` and `appendChild` to\n * mount the real module \u2014 that imperative DOM op would\n * collide with Lit's child-part markers if the shimmer\n * lived inside the slot. As a sibling, Lit owns the\n * shimmer's full lifecycle (mount / unmount on state\n * change) without ever touching the imperatively-managed\n * slot subtree. See adversarial-review failure mode #2.\n */\n slot.showLoading\n ? html`<div data-syntro-pdp-slot-shimmer>\n <div data-syntro-pdp-skeleton-body></div>\n </div>`\n : ''\n }\n </div>\n </section>\n `;\n })}\n </div>\n `;\n }\n\n /**\n * Self-contained skeleton (shimmer) CSS. Rendered inside the component\n * during the pre-plan / streaming phase. The global section-layout +\n * per-slot accent rules live in the head-injected `SECTION_LAYOUT_CSS`\n * (see `ensureSectionLayoutStyles`), not here, so they're emitted once\n * per document rather than once per `<syntro-pdp>` instance.\n */\n private static readonly _skeletonStyles = html`\n <style>\n [data-syntro-pdp-skeleton] {\n display: flex;\n flex-direction: column;\n gap: var(--sc-pdp-skeleton-gap, 32px);\n padding: var(--sc-pdp-skeleton-pad, 16px 0);\n }\n [data-syntro-pdp-skeleton-section] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n opacity: 0;\n animation: sc-pdp-skel-fade 0.4s ease forwards;\n }\n [data-syntro-pdp-skeleton-header] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n [data-syntro-pdp-skeleton-eyebrow],\n [data-syntro-pdp-skeleton-title],\n [data-syntro-pdp-skeleton-subtitle],\n [data-syntro-pdp-skeleton-body] {\n border-radius: var(--sc-pdp-skeleton-radius, 8px);\n background: linear-gradient(\n 90deg,\n var(--sc-pdp-skeleton-base, rgba(0, 0, 0, 0.06)) 0%,\n var(--sc-pdp-skeleton-shine, rgba(0, 0, 0, 0.11)) 50%,\n var(--sc-pdp-skeleton-base, rgba(0, 0, 0, 0.06)) 100%\n );\n background-size: 200% 100%;\n animation: sc-pdp-skel-shimmer 1.4s ease-in-out infinite;\n }\n [data-syntro-pdp-skeleton-eyebrow] {\n height: 10px;\n width: 35%;\n }\n [data-syntro-pdp-skeleton-title] {\n height: 22px;\n width: 75%;\n }\n [data-syntro-pdp-skeleton-subtitle] {\n height: 12px;\n width: 55%;\n }\n [data-syntro-pdp-skeleton-body] {\n height: var(--sc-pdp-skeleton-body-h, 140px);\n width: 100%;\n margin-top: 4px;\n }\n @keyframes sc-pdp-skel-shimmer {\n 0% {\n background-position: 100% 0;\n }\n 100% {\n background-position: -100% 0;\n }\n }\n @keyframes sc-pdp-skel-fade {\n to {\n opacity: 1;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n [data-syntro-pdp-skeleton-eyebrow],\n [data-syntro-pdp-skeleton-title],\n [data-syntro-pdp-skeleton-subtitle],\n [data-syntro-pdp-skeleton-body],\n [data-syntro-pdp-skeleton-section] {\n animation: none;\n opacity: 1;\n }\n }\n </style>\n `;\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp')) {\n customElements.define('syntro-pdp', SyntroPdpLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp': SyntroPdpLit;\n }\n}\n", "export type PdpConnection = 'connected' | 'disconnected';\n\nexport type PdpActivationState =\n | { status: 'waiting-runtime' }\n | { status: 'waiting-app' }\n | { status: 'active' };\n\nexport type PdpStreamState =\n | { status: 'idle' }\n | { status: 'streaming' }\n | { status: 'done' }\n | { status: 'error'; message: string };\n\nexport interface PdpModuleHeader {\n eyebrow: string;\n title: string;\n accent: string | null;\n subtitle: string | null;\n}\n\nexport interface PdpPlanTask {\n slot: string;\n kind: 'trending-news' | 'chart' | 'qa' | 'regional' | 'peer-feed' | 'other-products';\n variant: string | null;\n header: PdpModuleHeader;\n}\n\nexport interface PdpPlan {\n rationale: string;\n tasks: PdpPlanTask[];\n}\n\nexport type PdpPlanState = { status: 'empty' } | { status: 'ready'; plan: PdpPlan };\n\nexport type PdpSlotStatus = 'pending' | 'mounted' | 'failed';\nexport type PdpSlotState = Record<string, PdpSlotStatus>;\n\nexport interface PdpExplainMetric {\n value: string;\n label: string;\n hint?: string;\n}\n\nexport interface PdpExplanation {\n archetypeLabel: string;\n archetypeDesc: string;\n decisionLabel: string | null;\n decisionDesc: string | null;\n summary: string;\n metrics: PdpExplainMetric[];\n}\n\nexport type PdpExplanationState =\n | { status: 'empty' }\n | { status: 'loading' }\n | { status: 'ready'; explanation: PdpExplanation };\n\nexport interface PdpSlotReasoning {\n source: 'cached' | 'live';\n text: string;\n}\n\nexport type PdpReasoningState = Record<string, PdpSlotReasoning>;\n\nexport type PdpDossierState =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'ready'; metrics: PdpExplainMetric[] }\n | { status: 'error'; message: string };\n\nexport interface PdpState {\n generation: number;\n connection: PdpConnection;\n activation: PdpActivationState;\n stream: PdpStreamState;\n plan: PdpPlanState;\n slots: PdpSlotState;\n explanation: PdpExplanationState;\n reasoning: PdpReasoningState;\n dossier: PdpDossierState;\n dossierOpen: boolean;\n portalPresent: boolean;\n}\n\nexport type PdpEvent =\n | {\n type: 'runtime-updated';\n runtimeAvailable: boolean;\n appActive: boolean;\n configured: boolean;\n }\n | { type: 'plan-received'; generation: number; plan: PdpPlan }\n | { type: 'mount-succeeded'; generation: number; slot: string }\n | { type: 'mount-missed'; generation: number; slot: string }\n | { type: 'module-resolved'; generation: number; slot: string }\n | { type: 'module-failed'; generation: number; slot: string }\n | { type: 'cached-reasoning-resolved'; generation: number; slot: string; text: string }\n | { type: 'live-reasoning-resolved'; generation: number; slot: string; text: string }\n | {\n type: 'explanation-resolved';\n generation: number;\n explanation: PdpExplanation;\n durationMs?: number;\n }\n | { type: 'dossier-resolved'; generation: number; metrics: PdpExplainMetric[] }\n | { type: 'compose-succeeded'; generation: number }\n | { type: 'compose-failed'; generation: number; message: string }\n | { type: 'portal-updated'; present: boolean }\n | { type: 'dossier-toggled'; open: boolean }\n | { type: 'disconnected' }\n | {\n type: 'reconnected';\n runtimeAvailable: boolean;\n appActive: boolean;\n configured: boolean;\n };\n\nexport type PdpEffect =\n | { type: 'start-compose'; generation: number }\n | { type: 'capture-telemetry'; name: string; props: Record<string, unknown> }\n | { type: 'warn'; code: string; props: Record<string, unknown> }\n | { type: 'cleanup' };\n\nexport interface PdpTransition {\n state: PdpState;\n effects: PdpEffect[];\n}\n\nexport interface PdpSlotView {\n task: PdpPlanTask;\n status: PdpSlotStatus;\n visible: boolean;\n showLoading: boolean;\n reasoning: PdpSlotReasoning | null;\n}\n\nexport type PdpWhyBoxView =\n | { status: 'hidden'; destination: null }\n | { status: 'loading'; destination: 'inline' | 'portal' }\n | {\n status: 'ready';\n destination: 'inline' | 'portal';\n explanation: PdpExplanation;\n dossier: PdpExplainMetric[];\n dossierOpen: boolean;\n };\n\nexport type PdpTerminalView =\n | { status: 'none' }\n | { status: 'done' }\n | { status: 'error'; message: string };\n\nexport interface PdpView {\n layout: 'skeleton' | 'content';\n rootState: 'pending' | PdpStreamState['status'];\n slots: PdpSlotView[];\n whyBox: PdpWhyBoxView;\n terminal: PdpTerminalView;\n}\n\nexport function createInitialPdpState(generation = 0): PdpState {\n return {\n generation,\n connection: 'connected',\n activation: { status: 'waiting-runtime' },\n stream: { status: 'idle' },\n plan: { status: 'empty' },\n slots: {},\n explanation: { status: 'empty' },\n reasoning: {},\n dossier: { status: 'idle' },\n dossierOpen: true,\n portalPresent: false,\n };\n}\n\nexport function selectPdpView(state: PdpState): PdpView {\n const terminal: PdpTerminalView =\n state.stream.status === 'done'\n ? { status: 'done' }\n : state.stream.status === 'error'\n ? { status: 'error', message: state.stream.message }\n : { status: 'none' };\n\n const destination = state.portalPresent ? 'portal' : 'inline';\n const canRenderWhyBox = state.plan.status === 'ready' || state.portalPresent;\n const whyBox: PdpWhyBoxView =\n canRenderWhyBox && state.explanation.status === 'ready'\n ? {\n status: 'ready',\n destination,\n explanation: state.explanation.explanation,\n dossier: state.dossier.status === 'ready' ? state.dossier.metrics : [],\n dossierOpen: state.dossierOpen,\n }\n : canRenderWhyBox && state.stream.status === 'streaming'\n ? { status: 'loading', destination }\n : { status: 'hidden', destination: null };\n\n if (state.plan.status === 'empty') {\n return {\n layout: 'skeleton',\n rootState: 'pending',\n slots: [],\n whyBox,\n terminal,\n };\n }\n\n return {\n layout: 'content',\n rootState: state.stream.status,\n slots: state.plan.plan.tasks.map((task) => {\n const status = state.slots[task.slot] ?? 'pending';\n return {\n task,\n status,\n visible: status !== 'failed',\n showLoading: status === 'pending',\n reasoning: state.reasoning[task.slot] ?? null,\n };\n }),\n whyBox,\n terminal,\n };\n}\n\nfunction unchanged(state: PdpState): PdpTransition {\n return { state, effects: [] };\n}\n\nfunction telemetry(name: string, props: Record<string, unknown>): PdpEffect {\n return { type: 'capture-telemetry', name, props };\n}\n\nfunction withActivation(state: PdpState, status: PdpActivationState['status']): PdpTransition {\n return state.activation.status === status\n ? unchanged(state)\n : { state: { ...state, activation: { status } }, effects: [] };\n}\n\nfunction startCompose(state: PdpState, generation: number): PdpTransition {\n return {\n state: {\n ...createInitialPdpState(generation),\n activation: { status: 'active' },\n stream: { status: 'streaming' },\n dossierOpen: state.dossierOpen,\n portalPresent: state.portalPresent,\n },\n effects: [{ type: 'start-compose', generation }],\n };\n}\n\nfunction applyRuntimeStatus(\n state: PdpState,\n event: Extract<PdpEvent, { type: 'runtime-updated' | 'reconnected' }>,\n generation: number\n): PdpTransition {\n if (state.stream.status !== 'idle') return unchanged(state);\n if (!event.runtimeAvailable) return withActivation(state, 'waiting-runtime');\n if (!event.appActive) return withActivation(state, 'waiting-app');\n if (!event.configured) {\n if (state.activation.status === 'active' && state.stream.status === 'idle') {\n return unchanged(state);\n }\n return {\n state: { ...state, activation: { status: 'active' } },\n effects: [{ type: 'warn', code: 'missing-config', props: {} }],\n };\n }\n return startCompose(state, generation);\n}\n\nfunction taskForSlot(state: PdpState, slot: string): PdpPlanTask | undefined {\n return state.plan.status === 'ready'\n ? state.plan.plan.tasks.find((task) => task.slot === slot)\n : undefined;\n}\n\nfunction markSlotMounted(state: PdpState, slot: string): PdpTransition {\n if (state.slots[slot] !== 'pending') return unchanged(state);\n const task = taskForSlot(state, slot);\n if (!task) return unchanged(state);\n return {\n state: { ...state, slots: { ...state.slots, [slot]: 'mounted' } },\n effects: [\n telemetry('syntro_pdp_section_mounted', {\n slot,\n kind: task.kind,\n }),\n ],\n };\n}\n\nfunction setSlotReasoning(\n state: PdpState,\n slot: string,\n source: PdpSlotReasoning['source'],\n text: string\n): PdpTransition {\n if (state.slots[slot] !== 'mounted' || state.reasoning[slot] || text.length === 0) {\n return unchanged(state);\n }\n return {\n state: {\n ...state,\n reasoning: { ...state.reasoning, [slot]: { source, text } },\n },\n effects: [],\n };\n}\n\nexport function transitionPdp(state: PdpState, event: PdpEvent): PdpTransition {\n if (event.type === 'runtime-updated') {\n if (state.connection !== 'connected') return unchanged(state);\n return applyRuntimeStatus(state, event, state.generation + 1);\n }\n\n if (event.type === 'disconnected') {\n if (state.connection === 'disconnected') return unchanged(state);\n const generation = state.generation + 1;\n return {\n state: {\n ...createInitialPdpState(generation),\n connection: 'disconnected',\n dossierOpen: state.dossierOpen,\n portalPresent: state.portalPresent,\n },\n effects: [{ type: 'cleanup' }],\n };\n }\n\n if (event.type === 'reconnected') {\n if (state.connection === 'connected') return unchanged(state);\n const connected = { ...state, connection: 'connected' as const };\n return applyRuntimeStatus(connected, event, state.generation);\n }\n\n if (event.type === 'portal-updated') {\n return state.portalPresent === event.present\n ? unchanged(state)\n : { state: { ...state, portalPresent: event.present }, effects: [] };\n }\n\n if (event.type === 'dossier-toggled') {\n return state.dossierOpen === event.open\n ? unchanged(state)\n : { state: { ...state, dossierOpen: event.open }, effects: [] };\n }\n\n if (event.generation !== state.generation || state.connection !== 'connected') {\n return unchanged(state);\n }\n const acceptsDoneDossier = event.type === 'dossier-resolved' && state.stream.status === 'done';\n if (state.stream.status !== 'streaming' && !acceptsDoneDossier) return unchanged(state);\n\n switch (event.type) {\n case 'plan-received': {\n if (state.plan.status === 'ready') return unchanged(state);\n const slots: PdpSlotState = {};\n for (const task of event.plan.tasks) slots[task.slot] = 'pending';\n return {\n state: { ...state, plan: { status: 'ready', plan: event.plan }, slots },\n effects: [telemetry('syntro_pdp_plan_received', { task_count: event.plan.tasks.length })],\n };\n }\n case 'mount-succeeded':\n case 'module-resolved':\n return markSlotMounted(state, event.slot);\n case 'mount-missed':\n return state.slots[event.slot] === 'pending'\n ? {\n state,\n effects: [{ type: 'warn', code: 'mount-miss', props: { slot: event.slot } }],\n }\n : unchanged(state);\n case 'module-failed':\n return state.slots[event.slot] === 'pending'\n ? {\n state: { ...state, slots: { ...state.slots, [event.slot]: 'failed' } },\n effects: [],\n }\n : unchanged(state);\n case 'cached-reasoning-resolved':\n return setSlotReasoning(state, event.slot, 'cached', event.text);\n case 'live-reasoning-resolved':\n return setSlotReasoning(state, event.slot, 'live', event.text);\n case 'explanation-resolved': {\n // The summary patch re-dispatches this event; only the FIRST resolution\n // is the user-visible fill worth instrumenting (why-box latency audit\n // 2026-07-14).\n const firstResolution = state.explanation.status !== 'ready';\n return {\n state: {\n ...state,\n explanation: { status: 'ready', explanation: event.explanation },\n },\n effects:\n firstResolution && event.durationMs !== undefined\n ? [\n telemetry('syntro_pdp_explanation_resolved', {\n duration_ms: Math.round(event.durationMs),\n }),\n ]\n : [],\n };\n }\n case 'dossier-resolved':\n if (event.metrics.length === 0) return unchanged(state);\n return {\n state: {\n ...state,\n dossier: {\n status: 'ready',\n metrics:\n state.dossier.status === 'ready'\n ? [...state.dossier.metrics, ...event.metrics]\n : event.metrics,\n },\n },\n effects: [],\n };\n case 'compose-succeeded':\n return {\n state: { ...state, stream: { status: 'done' } },\n effects: [telemetry('syntro_pdp_done', { source: 'config' })],\n };\n case 'compose-failed':\n return {\n state: { ...state, stream: { status: 'error', message: event.message } },\n effects: [telemetry('syntro_pdp_error', { message: event.message, source: 'config' })],\n };\n }\n}\n", "import type { ScoredTemplateResult, SlotPreset } from '../scoring/scored-template';\nimport type { PdpExplainMetric } from './pdp';\n\nexport interface PdpModuleHeaderClient {\n eyebrow: string;\n title: string;\n accent: string | null;\n subtitle: string | null;\n}\n\nexport interface PdpPlanTaskClient {\n slot: string;\n kind: 'trending-news' | 'chart' | 'qa' | 'regional' | 'peer-feed' | 'other-products';\n variant: string | null;\n header: PdpModuleHeaderClient;\n}\n\nexport interface PdpPlanEnvelope {\n rationale: string;\n tasks: PdpPlanTaskClient[];\n}\n\n/**\n * Fixed PDP slot set and canonical live-slot order. Configured slots lead in\n * this order; remaining live slots keep stable page positions regardless of\n * response latency, so heavy modules cannot leapfrog lighter content.\n */\nexport const PDP_SLOT_IDS = [\n 'pdp-slot-qa',\n 'pdp-slot-trending-news',\n 'pdp-slot-peer-feed',\n 'pdp-slot-chart',\n 'pdp-slot-regional',\n 'pdp-slot-other-products',\n] as const;\n\nconst LIVE_SLOT_ORDER: readonly string[] = PDP_SLOT_IDS;\n\nconst LIVE_SLOT_HEADERS: Record<string, { eyebrow: string; title: string }> = {\n qa: { eyebrow: 'Answers', title: 'Questions like yours' },\n 'trending-news': { eyebrow: 'In the news', title: 'Recent research' },\n 'peer-feed': { eyebrow: 'From people like you', title: 'What others say' },\n chart: { eyebrow: 'Formulation', title: \"What's inside\" },\n regional: { eyebrow: 'In your area', title: 'Regional trend' },\n 'other-products': { eyebrow: 'Complete your stack', title: 'Others also take' },\n};\n\nconst FALLBACK_HEADER = { eyebrow: '', title: '' };\n\nexport function orderPdpSlots(slotIds: string[], isSet: (slot: string) => boolean): string[] {\n const configured = slotIds.filter((slot) => isSet(slot));\n const live = slotIds\n .filter((slot) => !isSet(slot))\n .sort((a, b) => LIVE_SLOT_ORDER.indexOf(a) - LIVE_SLOT_ORDER.indexOf(b));\n return [...configured, ...live];\n}\n\nexport function buildPdpPlan(\n slots: Array<{ slot: string; preset: SlotPreset | undefined }>\n): PdpPlanEnvelope {\n return {\n rationale: '',\n tasks: slots.map(({ slot, preset }) => {\n const kind = preset ? preset.kind : slot.replace('pdp-slot-', '');\n const header = preset ? preset.header : (LIVE_SLOT_HEADERS[kind] ?? FALLBACK_HEADER);\n return {\n slot,\n kind: kind as PdpPlanTaskClient['kind'],\n variant: preset?.variant ?? null,\n header: header as PdpModuleHeaderClient,\n };\n }),\n };\n}\n\nexport interface PdpComposeModuleRequest {\n generation: number;\n slot: string;\n archetype: string;\n decisionArchetype: string;\n behaviorSummary: string;\n}\n\nexport type PdpComposeUnset = (\n unsetSlots: string[],\n archetype: string,\n decisionArchetype?: string\n) => Promise<void>;\n\nexport interface PdpComposePorts {\n scoreSummary(generation: number): Promise<string>;\n requestDossier(generation: number): Promise<PdpExplainMetric[]>;\n resolveTemplate(generation: number, composeUnset: PdpComposeUnset): Promise<ScoredTemplateResult>;\n requestModule(request: PdpComposeModuleRequest): Promise<void> | void;\n /** Early exit for the scored axes \u2014 fired the INSTANT the score exists,\n * before the behavior summary resolves and before the live modules are\n * awaited, so the why-box never trails either (BUG-1783990580; the\n * summary alone was worth ~0.5s). Only invoked when live slots exist;\n * the all-preset path resolves compose() fast anyway. */\n onScored?(args: { generation: number; archetype: string; decisionArchetype: string }): void;\n\n /** Follow-up to onScored: the behavior summary, when it lands. Consumers\n * patch it into the already-visible explanation (dossier-fold pattern). */\n onSummary?(args: { generation: number; behaviorSummary: string }): void;\n}\n\nexport interface PdpComposeResult {\n template: ScoredTemplateResult;\n summary: string;\n dossier: Promise<PdpExplainMetric[]>;\n}\n\nexport interface PdpComposeAdapter {\n compose(generation: number): Promise<PdpComposeResult>;\n}\n\nfunction startFailSoft<T>(operation: () => Promise<T> | T, fallback: T): Promise<T> {\n try {\n return Promise.resolve(operation()).catch(() => fallback);\n } catch {\n return Promise.resolve(fallback);\n }\n}\n\nexport function createPdpComposeAdapter(ports: PdpComposePorts): PdpComposeAdapter {\n const compose = async (generation: number): Promise<PdpComposeResult> => {\n const summaryPromise = startFailSoft(() => ports.scoreSummary(generation), 'unknown');\n const dossier = startFailSoft(() => ports.requestDossier(generation), []);\n\n const template = await ports.resolveTemplate(\n generation,\n async (unsetSlots, archetype, decisionArchetype = 'unknown') => {\n try {\n ports.onScored?.({ generation, archetype, decisionArchetype });\n } catch {\n // The early explanation is best-effort; never break composition.\n }\n const summary = await summaryPromise;\n const behaviorSummary = summary === 'unknown' ? '' : summary;\n try {\n ports.onSummary?.({ generation, behaviorSummary });\n } catch {\n // Summary patch is best-effort; never break composition.\n }\n await Promise.all(\n unsetSlots.map((slot) =>\n startFailSoft(\n () =>\n ports.requestModule({\n generation,\n slot,\n archetype,\n decisionArchetype,\n behaviorSummary,\n }),\n undefined\n )\n )\n );\n }\n );\n\n return {\n template,\n summary: await summaryPromise,\n dossier,\n };\n };\n\n return { compose };\n}\n", "import type { PdpEffect, PdpEvent } from './pdp';\n\nexport interface PdpEffectRun {\n dispatch(event: PdpEvent): boolean;\n isCurrent(): boolean;\n}\n\nexport interface PdpEffectPorts {\n dispatch(event: PdpEvent): void;\n startCompose(generation: number, run: PdpEffectRun): Promise<void> | void;\n captureTelemetry(name: string, props: Record<string, unknown>): Promise<void> | void;\n warn(code: string, props: Record<string, unknown>): Promise<void> | void;\n cleanup(): Promise<void> | void;\n}\n\nexport interface PdpEffectExecutor {\n execute(effect: PdpEffect): Promise<void>;\n}\n\nexport function createPdpEffectExecutor(ports: PdpEffectPorts): PdpEffectExecutor {\n let lifecycle = 0;\n\n const execute = async (effect: PdpEffect): Promise<void> => {\n try {\n switch (effect.type) {\n case 'start-compose': {\n lifecycle += 1;\n const startedIn = lifecycle;\n const run: PdpEffectRun = {\n isCurrent: () => startedIn === lifecycle,\n dispatch: (event) => {\n if (startedIn !== lifecycle) return false;\n try {\n ports.dispatch(event);\n return true;\n } catch {\n return false;\n }\n },\n };\n try {\n await ports.startCompose(effect.generation, run);\n } catch (error) {\n run.dispatch({\n type: 'compose-failed',\n generation: effect.generation,\n message:\n error instanceof Error && error.message\n ? error.message\n : typeof error === 'string' && error\n ? error\n : 'PDP compose failed',\n });\n return;\n }\n run.dispatch({ type: 'compose-succeeded', generation: effect.generation });\n return;\n }\n case 'capture-telemetry':\n await ports.captureTelemetry(effect.name, effect.props);\n return;\n case 'warn':\n await ports.warn(effect.code, effect.props);\n return;\n case 'cleanup':\n lifecycle += 1;\n await ports.cleanup();\n return;\n }\n } catch {\n // Host integrations must not crash the widget orchestration path.\n }\n };\n\n return { execute };\n}\n", "/**\n * Native slot swap \u2014 the design-agnostic content-swap engine (FEAT-1784346204).\n *\n * A host page renders its OWN FAQ/review sections and marks them with the\n * data-attribute contract below. Personalization never re-renders the design:\n * we clone one of the host's existing items as the template, fill its\n * `[data-syntro-field]` text nodes, and replace the item set. The swapped\n * section is literally the host's DOM shape. `restore()` reattaches the\n * ORIGINAL nodes (same references \u2014 a React host may hold refs to them).\n */\n\nexport const NATIVE_SLOT_ATTR = 'data-syntro-slot';\nexport const NATIVE_ITEM_ATTR = 'data-syntro-item';\nexport const NATIVE_FIELD_ATTR = 'data-syntro-field';\n/** Same string as adaptive-reviews' PRODUCT_ID_ATTRIBUTE \u2014 kept as an\n * independent constant so adaptive-product gains no cross-package dep. */\nexport const NATIVE_PRODUCT_ATTR = 'data-syntro-product-id';\n\nexport type NativeSlotKind = 'qa' | 'peer-feed';\nexport type NativeItemFields = Record<string, string>;\n\nexport interface NativeSwapHandle {\n container: HTMLElement;\n restore(): void;\n}\n\n/** Track actively-swapped containers to prevent double-swap corruption. */\nconst activeSwaps = new WeakSet<HTMLElement>();\n\nexport function findNativeSlot(root: ParentNode, kind: NativeSlotKind): HTMLElement | null {\n return root.querySelector<HTMLElement>(`[${NATIVE_SLOT_ATTR}=\"${kind}\"]`);\n}\n\nexport function nativeProductId(container: HTMLElement): string | null {\n return (\n container.closest<HTMLElement>(`[${NATIVE_PRODUCT_ATTR}]`)?.getAttribute(NATIVE_PRODUCT_ATTR) ??\n null\n );\n}\n\n/**\n * PDP slot id \u2192 native-section kind. Only these two slots have a native (host\n * server-rendered) counterpart; every other slot renders as usual. Shared by\n * `SyntroPdpLit` (page-owning element) and `nativeSlotWatcher` (SPA modal).\n */\nexport const NATIVE_KIND_BY_SLOT: Record<string, NativeSlotKind> = {\n 'pdp-slot-qa': 'qa',\n 'pdp-slot-peer-feed': 'peer-feed',\n};\n\n/**\n * Map a mounted instance's `content` payload to the flat `[field \u2192 text]` items\n * the native swap engine fills. qa \u2192 {question, answer}; peer-feed maps the\n * review shape onto the host's field names (quote\u2192text, persona\u2192reviewer,\n * time_ago\u2192meta). Malformed/absent lists degrade to an empty item set (the swap\n * engine then no-ops and leaves the host defaults).\n */\nexport function nativeFieldsFromContent(\n kind: NativeSlotKind,\n content: Record<string, unknown>\n): NativeItemFields[] {\n if (kind === 'qa') {\n const items = Array.isArray(content.items)\n ? (content.items as Array<{ question?: unknown; answer?: unknown }>)\n : [];\n return items.map((item) => ({\n question: String(item.question ?? ''),\n answer: String(item.answer ?? ''),\n }));\n }\n const peers = Array.isArray(content.peers)\n ? (content.peers as Array<{ quote?: unknown; persona?: unknown; time_ago?: unknown }>)\n : [];\n return peers.map((peer) => ({\n text: String(peer.quote ?? ''),\n reviewer: String(peer.persona ?? ''),\n meta: String(peer.time_ago ?? ''),\n }));\n}\n\nexport function swapNativeSlot(\n container: HTMLElement,\n items: NativeItemFields[]\n): NativeSwapHandle | null {\n if (activeSwaps.has(container)) {\n console.warn('syntro-pdp.native_swap_skipped: container already swapped \u2014 restore() first');\n return null;\n }\n const originals = [...container.querySelectorAll<HTMLElement>(`[${NATIVE_ITEM_ATTR}]`)];\n if (originals.length === 0 || items.length === 0) {\n console.warn(\n `syntro-pdp.native_swap_skipped: ${originals.length === 0 ? 'no template item in container' : 'no items to swap in'}`\n );\n return null;\n }\n const template = originals[0];\n const anchor = document.createComment('syntro-native-slot');\n template.before(anchor);\n for (const original of originals) original.remove();\n\n const clones = items.map((fields) => {\n const clone = template.cloneNode(true) as HTMLElement;\n for (const [name, value] of Object.entries(fields)) {\n const target = clone.querySelector<HTMLElement>(`[${NATIVE_FIELD_ATTR}=\"${name}\"]`);\n if (target) target.textContent = value;\n }\n return clone;\n });\n anchor.after(...clones);\n\n activeSwaps.add(container);\n\n return {\n container,\n restore: () => {\n for (const clone of clones) clone.remove();\n anchor.after(...originals);\n anchor.remove();\n activeSwaps.delete(container);\n },\n };\n}\n", "/**\n * Scored-template orchestration for the PDP \u2014 score \u2192 resolve presets \u2192 reserve\n * every slot box \u2192 fill the cached (set) slots from config \u2192 stream the rest.\n *\n * Lives in the adaptive (not runtime-sdk): it's the PDP's composition logic, used\n * by exactly one adaptive, and pure. The runtime stays thin (`score` +\n * `authedFetch`). It was previously parked in runtime-sdk and handed over via\n * `window.SynOS` ONLY to dodge the adaptive\u2192runtime import cycle \u2014 but the logic\n * is adaptive-domain (presets, slots, plan, mounts), so it belongs here.\n */\n\n// DERIVED from the runtime-backend contract (single source of truth), same as\n// runtime-sdk's score-client \u2014 so the two ScoreSpecs can't drift. A Pydantic\n// change to ScoreRequest/ScoreOutputSpec breaks this compile.\n// See docs/adr/2026-07-05-runtime-backend-generated-types.md.\nimport type {\n ScoreOutputSpec as BackendScoreOutputSpec,\n ScoreRequest as BackendScoreRequest,\n} from '@syntrologie/runtime-contracts';\n\n/** What an element declares it wants scored (the runtime `score` primitive's input). */\nexport interface ScoreSpec {\n model_id: BackendScoreRequest['model_id'];\n name: BackendScoreOutputSpec['name'];\n type: BackendScoreOutputSpec['type'];\n values?: BackendScoreOutputSpec['values'];\n /**\n * Per-enum-value descriptions passed to the classifier (\"label \u2014 description\").\n * Feeds the model what each archetype MEANS so it doesn't classify opaque slugs\n * and give up to `unknown`. Forwarded verbatim to the `/api/score` output block.\n */\n value_descriptions?: BackendScoreOutputSpec['value_descriptions'];\n signals?: BackendScoreRequest['signals'];\n}\n\n/** One config-set slot value: the widget kind, an optional header, and its content. */\nexport interface SlotPreset {\n kind: string;\n /** Optional variant layout key (e.g. `accordion-multi`) \u2014 selects the\n * dedicated variant widget via `TEMPLATE_TO_TAG` (`<kind>/<variant>`).\n * Absent/null mounts the base widget. */\n variant?: string | null;\n header?: unknown;\n data: unknown;\n}\n\n/** `presets[scoredValue][slotId] = SlotPreset` \u2014 the config's SET values. */\nexport type PresetTable = Record<string, Record<string, SlotPreset>>;\n\nexport interface ScoredTemplateOptions<Plan, Mount> {\n /** Score the variable (e.g. `SynOS.score`). */\n score: (spec: ScoreSpec) => Promise<string>;\n /** What to score: model_id, name, enum values, and the visitor signals. */\n scoreSpec: ScoreSpec;\n /**\n * Axis A \u2014 the decision archetype spec (\"what is blocking the purchase\").\n * When provided together with `scoreMulti`, both axes are scored in ONE\n * backend model call; the decision value drives `transformPreset` (pool\n * selection) and is passed to `composeUnset` for the live slots. Absent \u2192\n * exactly the single-axis behavior.\n */\n decisionSpec?: ScoreSpec;\n /** Batched scorer (e.g. `SynOS.scoreMulti`) \u2014 several specs, one model call. */\n scoreMulti?: (specs: ScoreSpec[]) => Promise<string[]>;\n /**\n * Transform a SET slot's preset before mounting \u2014 the pool-selection hook\n * (pick which tagged items surface for this visitor's decision archetype).\n */\n transformPreset?: (preset: SlotPreset, decisionValue: string) => SlotPreset;\n /** Per-(scored value) per-slot SET values. */\n presets: PresetTable;\n /** The fixed slot set this template renders. */\n slotIds: string[];\n /** Value to fall back to when the scored value has no preset row (default 'unknown'). */\n fallbackValue?: string;\n /**\n * Build the plan from EVERY slot. `preset` is `undefined` for unset slots,\n * which still reserve a skeleton box so live/streamed arrivals fill a reserved\n * box instead of reflowing the page (no layout shift).\n */\n buildPlan: (slots: Array<{ slot: string; preset: SlotPreset | undefined }>) => Plan;\n /** Build one mount from a slot preset (set slots only). */\n buildMount: (slot: string, preset: SlotPreset) => Mount;\n /** Apply the plan (reserve slot skeletons / headers) before mounting. */\n emitPlan: (plan: Plan) => void;\n /** Apply one mount. */\n emitMount: (mount: Mount) => Promise<void> | void;\n /**\n * Stream the slots the presets didn't set (live compose). Receives the scored\n * value (and the decision value when a decisionSpec was scored) so the caller\n * can personalize the live request. Optional.\n */\n composeUnset?: (unsetSlots: string[], value: string, decisionValue?: string) => Promise<void>;\n /**\n * Reorder the slots for the PLAN \u2014 i.e. the PAGE order. Called after scoring\n * with the full slot list and set-membership, so instant (preset) slots can\n * float to the top and slow live slots can sink to the bottom (sections fill\n * top-down as responses land instead of a slow section blocking the fold).\n * Purely presentational: does not change which slots mount or compose.\n * Default: the given slotIds order.\n */\n orderSlots?: (slotIds: string[], isSet: (slot: string) => boolean) => string[];\n}\n\nexport interface ScoredTemplateResult {\n /** The scored value used to resolve presets. */\n value: string;\n /** The scored decision-archetype value ('unknown' when no decisionSpec). */\n decisionValue: string;\n /** Slots filled from config. */\n setSlots: string[];\n /** Slots left to the live stream (compose). */\n unsetSlots: string[];\n}\n\n/**\n * Score \u2192 resolve the scored value's preset row \u2192 reserve a box for EVERY slot \u2192\n * fill the SET slots from config immediately \u2192 stream only the UNSET slots.\n */\nexport async function resolveScoredTemplate<Plan, Mount>(\n opts: ScoredTemplateOptions<Plan, Mount>\n): Promise<ScoredTemplateResult> {\n let value: string;\n let decisionValue = 'unknown';\n if (opts.decisionSpec && opts.scoreMulti) {\n // Both axes from ONE backend model call. A short result must not clobber\n // the 'unknown' default with undefined.\n const scored = await opts.scoreMulti([opts.scoreSpec, opts.decisionSpec]);\n value = scored[0] ?? 'unknown';\n decisionValue = scored[1] ?? 'unknown';\n } else {\n value = await opts.score(opts.scoreSpec);\n }\n const variant = opts.presets[value] ?? opts.presets[opts.fallbackValue ?? 'unknown'] ?? {};\n\n const setSlots = opts.slotIds.filter((s) => s in variant);\n const unsetSlots = opts.slotIds.filter((s) => !(s in variant));\n\n // Reserve a box for EVERY slot up front (set AND unset) \u2014 streamed/live\n // arrivals then fill a reserved skeleton instead of reflowing (no layout shift).\n // Page order comes from orderSlots (presentational only) when provided.\n const pageOrder = opts.orderSlots\n ? opts.orderSlots([...opts.slotIds], (slot) => slot in variant)\n : opts.slotIds;\n if (pageOrder.length > 0) {\n opts.emitPlan(opts.buildPlan(pageOrder.map((slot) => ({ slot, preset: variant[slot] }))));\n }\n // Fill the cached (set) slots immediately from config \u2014 no model in the render\n // path. transformPreset (pool selection) picks which tagged items surface for\n // this visitor's decision archetype before the mount is built.\n for (const slot of setSlots) {\n const preset = opts.transformPreset\n ? opts.transformPreset(variant[slot], decisionValue)\n : variant[slot];\n await opts.emitMount(opts.buildMount(slot, preset));\n }\n // Stream the unset slots (live compose).\n if (unsetSlots.length > 0 && opts.composeUnset) {\n await opts.composeUnset(unsetSlots, value, decisionValue);\n }\n\n return { value, decisionValue, setSlots, unsetSlots };\n}\n", "/**\n * Watches the takeover region for host JS (React reconciliation, WooCommerce\n * AJAX fragments, theme scripts) re-rendering a section we detached. The\n * engine DISCARDS resurrected clones (they are fresh host re-renders, not\n * the original nodes we hold for restore). Budget mirrors adaptive-content's\n * reconciliation-guard: 3 retries, 50ms debounce; exhausted => the engine\n * aborts the takeover and restores the originals \u2014 never duplicate host\n * content next to its Syntro replacement.\n */\n\nexport interface ResurrectionGuardOptions {\n region: HTMLElement;\n /** Nodes the takeover itself rendered \u2014 never treated as resurrections,\n * even when a host anchor selector happens to match them (review F3). */\n ignore?: (el: HTMLElement) => boolean;\n anchors: Map<string, string>;\n onResurrected(sectionId: string, el: HTMLElement): void;\n /** Receives the still-attached resurrected element so the caller can\n * discard it before restoring originals \u2014 never leave a duplicate. */\n onBudgetExhausted(sectionId: string, el: HTMLElement): void;\n maxRetries?: number;\n debounceMs?: number;\n}\n\nexport interface ResurrectionGuardHandle {\n stop(): void;\n unwatch(sectionId: string): void;\n}\n\nexport function startResurrectionGuard(opts: ResurrectionGuardOptions): ResurrectionGuardHandle {\n const maxRetries = opts.maxRetries ?? 3;\n const debounceMs = opts.debounceMs ?? 50;\n const watched = new Map(opts.anchors);\n const counts = new Map<string, number>();\n let timer: ReturnType<typeof setTimeout> | null = null;\n let stopped = false;\n\n const sweep = (): void => {\n if (stopped) return;\n // Collect ALL present clones first so a budget exhaustion can discard\n // every one of them \u2014 the abort path restores originals, and a surviving\n // sibling clone would sit next to its restored original (duplicate).\n const present: Array<{ sectionId: string; el: HTMLElement; n: number }> = [];\n for (const [sectionId, selector] of watched) {\n let matches: HTMLElement[] = [];\n try {\n matches = Array.from(opts.region.querySelectorAll<HTMLElement>(selector));\n } catch {\n continue; // malformed selector \u2014 nothing to watch\n }\n const el = matches.find((m) => !(opts.ignore?.(m) ?? false));\n if (!el) continue;\n present.push({ sectionId, el, n: (counts.get(sectionId) ?? 0) + 1 });\n }\n const exhausted = present.find((p) => p.n > maxRetries);\n if (exhausted) {\n for (const p of present) p.el.remove();\n opts.onBudgetExhausted(exhausted.sectionId, exhausted.el);\n return; // engine aborts; it calls stop()\n }\n for (const { sectionId, el, n } of present) {\n counts.set(sectionId, n);\n opts.onResurrected(sectionId, el);\n }\n };\n\n const observer = new MutationObserver(() => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(sweep, debounceMs);\n });\n observer.observe(opts.region, { childList: true, subtree: true });\n\n return {\n stop() {\n stopped = true;\n if (timer) clearTimeout(timer);\n observer.disconnect();\n },\n unwatch(sectionId) {\n watched.delete(sectionId);\n counts.delete(sectionId);\n },\n };\n}\n", "/**\n * Section detach/reattach for PDP takeover \u2014 nativeSlotSwap's restore\n * semantics (comment anchor + ORIGINAL node references, React-ref safe)\n * without the clone-and-fill step. Never hostPatcher setStyle: its revert\n * strips untouched inline styles/attrs (spec rev 2, adversarial finding 1).\n */\n\nexport interface DetachHandle {\n readonly section: HTMLElement;\n readonly restored: boolean;\n restore(): void;\n}\n\n/** Sections currently detached by us \u2014 prevents double-detach corruption. */\nconst activeDetaches = new WeakSet<HTMLElement>();\n\nexport function detachSection(section: HTMLElement): DetachHandle | null {\n if (activeDetaches.has(section)) return null;\n if (!section.parentNode) return null;\n activeDetaches.add(section);\n\n const anchor = document.createComment('syntro-takeover-section');\n section.before(anchor);\n section.remove();\n\n let restored = false;\n return {\n section,\n get restored() {\n return restored;\n },\n restore() {\n if (restored) return;\n restored = true;\n if (anchor.parentNode) anchor.after(section);\n anchor.remove();\n activeDetaches.delete(section);\n },\n };\n}\n", "/**\n * PDP takeover orchestration \u2014 staged with an explicit COMMIT POINT.\n * Nothing detaches until score + plan are ready; every failure before the\n * commit point ends with the native page untouched by construction (spec\n * rev 2 \"The takeover mechanics\"). Ports pattern mirrors\n * decision/pdp-compose.ts: the engine is pure orchestration; DOM effects\n * arrive injected. The takeover root is the already-mounted <syntro-pdp>\n * element (config mounts it via core:mountWidget \u2014 no parallel mechanism).\n *\n * Resurrected sections are host RE-RENDERS (fresh nodes), not our originals:\n * they are discarded. Only original sections \u2014 held as DetachHandles \u2014 are\n * ever restored.\n */\n\nimport { type ResurrectionGuardHandle, startResurrectionGuard } from './resurrectionGuard';\nimport type { PdpTakeoverConfig, TakeoverPlan, TakeoverPlanResponse } from './schema';\nimport { type DetachHandle, detachSection } from './sectionDetach';\n\nexport interface TakeoverPorts {\n hasSignals(): boolean;\n findSection(anchor: string): HTMLElement | null;\n measureSection(el: HTMLElement): number;\n reserve(minHeightPx: number): void;\n releaseReservation(): void;\n region(): HTMLElement;\n /** True for nodes the takeover itself rendered \u2014 the resurrection guard\n * must never treat our own sections as host resurrections (review F3). */\n ownsNode(el: HTMLElement): boolean;\n /** Curtains the takeover surface. The ports decide the targets (root +\n * to-be-suppressed host sections \u2014 NEVER a shared ancestor, which would\n * blank the buy box / LCP element; review F1). Returns lift(). */\n curtain(): () => void;\n deactivateRoot(): void;\n /** One server round trip: scores the visitor AND returns that archetype's\n * plan, resolved centrally. Ordering intelligence lives server-side so a\n * model improvement propagates fleet-wide without config re-publishes. */\n fetchPlan(): Promise<TakeoverPlanResponse>;\n renderModules(\n plan: TakeoverPlan,\n onModuleFailed: (suppressedSectionId: string | null) => void\n ): Promise<void>;\n telemetry(event: string, props?: Record<string, unknown>): void;\n}\n\nexport type TakeoverOutcome = 'progressive' | 'committed' | 'aborted-precommit';\n\nexport interface TakeoverResult {\n outcome: TakeoverOutcome;\n dispose(): void;\n}\n\n/**\n * Native module kinds that SUPERSEDE a host-section role: rendering the kind\n * next to an attached host section of that role duplicates content (the\n * two-Q&As bug, 2026-08-09). Committed path: the server suppresses the role\n * (mirror map server-side, plan_rules.py). Progressive path (no curtain, no\n * suppression possible): the KIND is dropped instead \u2014 the host keeps its\n * section, we keep our non-overlapping modules.\n */\nconst KIND_SUPERSEDES_ROLE: Record<string, string> = {\n qa: 'faq',\n 'peer-feed': 'social-proof',\n};\n\nconst DEFAULT_COMMIT_TIMEOUT_MS = 500;\nconst NOOP_DISPOSE = (): void => {};\n\nexport async function runTakeover(\n config: PdpTakeoverConfig,\n ports: TakeoverPorts,\n opts: { commitTimeoutMs?: number } = {}\n): Promise<TakeoverResult> {\n // No separate kill switch: rollout AND kill live at CONFIG DELIVERY (the\n // GrowthBook flag that serves the config variant, fetched per page load).\n // If runTakeover was called, the served config carries `takeover` \u2014 that IS\n // the gate. A second runtime flag would be a parallel control plane\n // (feedback_sdk_config_single_source_of_truth; Parker, 2026-08-07).\n\n // Cold-start fast path: no signals worth scoring => no curtain, and the\n // server plan (it resolves 'unknown' server-side) renders progressively\n // whenever it arrives. Suppression is STRIPPED on this path \u2014 with no\n // curtain there is no safe window to detach host content, so the risk\n // stays zero by construction.\n if (!ports.hasSignals()) {\n const declaredRoles = new Set(config.hostSections.map((s) => s.role));\n void Promise.resolve()\n .then(() => ports.fetchPlan())\n .then((response) =>\n ports.renderModules(\n {\n // Dropped on this path (nothing may detach, so the host's own\n // sections all stay attached \u2014 rendering an overlap duplicates\n // content, the exact class the spec forbids):\n // 1. host-derived entries (kind:sectionId) \u2014 review F2;\n // 2. native kinds superseding a DECLARED host role (two-Q&As\n // bug: our qa next to their attached FAQ).\n order: response.plan.order.filter((entry) => {\n if (entry.includes(':')) return false;\n const superseded = KIND_SUPERSEDES_ROLE[entry];\n return !(superseded && declaredRoles.has(superseded));\n }),\n suppress: [],\n },\n () => {}\n )\n )\n .catch(() => ports.telemetry('takeover.render_failed', { path: 'progressive' }));\n return { outcome: 'progressive', dispose: NOOP_DISPOSE };\n }\n\n const commitTimeoutMs = opts.commitTimeoutMs ?? DEFAULT_COMMIT_TIMEOUT_MS;\n const region = ports.region();\n const lift = ports.curtain();\n\n // Race the plan fetch against the curtain budget. Timeout OR rejection\n // before commit => the visitor sees the untouched native page (nothing\n // has been detached). Distinct telemetry per cause: ops must be able to\n // tell backend-down from slow-backend (review F5).\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timedOut: unique symbol = Symbol('timeout');\n const fetchFailed: unique symbol = Symbol('fetch-failed');\n const response = await Promise.race([\n ports.fetchPlan().catch((): typeof fetchFailed => fetchFailed),\n new Promise<typeof timedOut>((r) => {\n timeoutId = setTimeout(() => r(timedOut), commitTimeoutMs);\n }),\n ]).finally(() => clearTimeout(timeoutId));\n\n if (response === timedOut || response === fetchFailed) {\n ports.deactivateRoot();\n lift();\n if (response === timedOut) {\n ports.telemetry('takeover.precommit_timeout', { budgetMs: commitTimeoutMs });\n } else {\n ports.telemetry('takeover.plan_fetch_failed', { budgetMs: commitTimeoutMs });\n }\n return { outcome: 'aborted-precommit', dispose: NOOP_DISPOSE };\n }\n\n // \u2500\u2500 COMMIT POINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const plan = response.plan;\n const sectionsById = new Map(config.hostSections.map((s) => [s.id, s]));\n\n // Measure + reserve BEFORE detaching so surrounding content doesn't jump.\n const toSuppress: Array<{ id: string; el: HTMLElement }> = [];\n let reservedPx = 0;\n for (const id of plan.suppress) {\n const section = sectionsById.get(id);\n if (!section) {\n // Server/config drift defense: the plan may only suppress sections the\n // config DECLARES (with a selector). An undeclared id is skipped, never\n // guessed at.\n ports.telemetry('takeover.suppress_undeclared', { sectionId: id });\n continue;\n }\n let el: HTMLElement | null = null;\n try {\n el = ports.findSection(section.anchor);\n } catch {\n // Malformed selector (querySelector throws SyntaxError) \u2014 same\n // treatment as an anchor that matched nothing.\n }\n if (!el) {\n ports.telemetry('takeover.section_anchor_missing', { sectionId: id });\n continue;\n }\n reservedPx += ports.measureSection(el);\n toSuppress.push({ id, el });\n }\n ports.reserve(reservedPx);\n\n const handles = new Map<string, DetachHandle>(); // ORIGINAL sections only\n for (const { id, el } of toSuppress) {\n const handle = detachSection(el);\n if (handle) handles.set(id, handle);\n }\n\n let disposed = false;\n let guard: ResurrectionGuardHandle | undefined;\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n guard?.stop();\n for (const handle of handles.values()) handle.restore();\n handles.clear();\n ports.deactivateRoot();\n lift();\n };\n\n guard = startResurrectionGuard({\n region,\n ignore: (el) => ports.ownsNode(el),\n anchors: new Map(toSuppress.map(({ id }) => [id, sectionsById.get(id)!.anchor] as const)),\n onResurrected: (sectionId, el) => {\n // Host re-render \u2014 a fresh clone, not our original. Discard it.\n ports.telemetry('takeover.section_resurrected', { sectionId });\n el.remove();\n },\n onBudgetExhausted: (sectionId, el) => {\n ports.telemetry('takeover.resurrection_budget_exhausted', { sectionId });\n el.remove(); // discard the final clone BEFORE restoring the original\n dispose();\n },\n });\n\n const onModuleFailed = (suppressedSectionId: string | null): void => {\n if (!suppressedSectionId) return;\n const handle = handles.get(suppressedSectionId);\n if (!handle) return;\n // Reattaching the original must not count as a resurrection.\n guard?.unwatch(suppressedSectionId);\n handle.restore();\n handles.delete(suppressedSectionId);\n ports.telemetry('takeover.module_failed_section_restored', {\n sectionId: suppressedSectionId,\n });\n };\n\n // Curtain lifts NOW \u2014 baked payloads render synchronously and live slots\n // show skeletons, both presentable. Holding until every module settles\n // would quietly exceed the 500ms budget (plan rev 2, finding 13).\n // A WHOLESALE renderer rejection (not a per-module failure) must not\n // strand detached sections: dispose restores everything, fail-open.\n void Promise.resolve()\n .then(() => ports.renderModules(plan, onModuleFailed))\n .catch(() => {\n ports.telemetry('takeover.render_failed', { path: 'committed' });\n dispose();\n })\n .finally(() => ports.releaseReservation());\n lift();\n\n return { outcome: 'committed', dispose };\n}\n", "/**\n * Real `fetchPlan` for the takeover engine \u2014 ONE POST to the runtime-backend's\n * `/api/pdp/plan` through the runtime's `authedFetch` (the same authenticated\n * transport the element's `/api/pdp/module` calls use: Bearer token +\n * X-Distinct-Id, BUG-1784152746 \u2014 the transport owns headers, this module owns\n * endpoint + payload). The server scores the visitor's decision archetype AND\n * returns that archetype's plan in the same round trip (spec rev 3).\n *\n * The response is untrusted: it is validated against the frozen\n * `TakeoverPlanResponse` wire shape (zod, strict) before it is returned \u2014 an\n * invalid payload REJECTS, which the engine treats as backend-down and fails\n * open to the native page. No silent defaults, no partial acceptance.\n */\n\nimport { z } from 'zod';\nimport type { TakeoverPorts } from './engine';\nimport { type PdpTakeoverConfig, type TakeoverPlanResponse, takeoverPlanSchema } from './schema';\n\n/** Wire shape of the `/api/pdp/plan` response \u2014 `TakeoverPlanResponse`. */\nexport const takeoverPlanResponseSchema = z\n .object({\n decisionArchetype: z.string().min(1),\n plan: takeoverPlanSchema,\n })\n .strict();\n\n// Compile-time proof the zod shape and the frozen contract interface agree.\ntype _AssertResponseShape =\n z.infer<typeof takeoverPlanResponseSchema> extends TakeoverPlanResponse\n ? TakeoverPlanResponse extends z.infer<typeof takeoverPlanResponseSchema>\n ? true\n : never\n : never;\nconst _responseShapeMatchesContract: _AssertResponseShape = true;\nvoid _responseShapeMatchesContract;\n\nexport interface TakeoverPlanFetcherDeps {\n /** `window.SynOS.authedFetch` \u2014 the runtime-owned authenticated transport. */\n authedFetch: (path: string, init?: RequestInit) => Promise<Response>;\n config: PdpTakeoverConfig;\n /** REQUIRED server-side (min 1) \u2014 the caller gates: no product id, no\n * takeover call at all (`takeover.missing_product_id`). Never defaulted. */\n productId: string;\n productName: string;\n /** Decision-archetype vocabulary (slug \u2192 description) \u2014 the same axis-A\n * context the element's scoring path sends, so the server can score. */\n decisionArchetypes: Record<string, string>;\n /** Client-side behavior summary when one exists; '' otherwise. Context for\n * the server-side scorer, mirroring the `/api/pdp/module` field. */\n behaviorSummary: string;\n /** The full visitor signal bag the element already gathers for scoring. */\n signals: Record<string, unknown>;\n}\n\nexport function createTakeoverPlanFetcher(\n deps: TakeoverPlanFetcherDeps\n): TakeoverPorts['fetchPlan'] {\n return async () => {\n const resp = await deps.authedFetch('/api/pdp/plan', {\n method: 'POST',\n // FROZEN SEAM SHAPE \u2014 field set AND order are pinned byte-for-byte by\n // `src/__fixtures__/takeover-plan-request.json`; the runtime-backend PR\n // commits the same fixture. Change both sides together or not at all.\n // Deliberately NO client-scored archetype/decision slugs: the server\n // scores (spec rev 3) \u2014 the client sends vocabulary + signals only.\n body: JSON.stringify({\n product_id: deps.productId,\n product_name: deps.productName,\n // Page anatomy: ids + roles only. Anchors are host DOM selectors \u2014\n // client-side facts the server has no business receiving.\n host_sections: deps.config.hostSections.map(({ id, role }) => ({ id, role })),\n decision_archetypes: deps.decisionArchetypes,\n behavior_summary: deps.behaviorSummary,\n signals: deps.signals,\n ingest_snapshot: deps.config.ingestSnapshot,\n // The client's configurable wait (commitTimeoutMs, default 500):\n // the server may classify inline within it (bounded inline classify).\n budget_ms: deps.config.commitTimeoutMs ?? 500,\n }),\n });\n if (!resp.ok) {\n throw new Error(`takeover plan fetch failed: HTTP ${resp.status}`);\n }\n const data: unknown = await resp.json();\n return takeoverPlanResponseSchema.parse(data);\n };\n}\n", "/**\n * Region-scoped anti-flicker curtain (spec rev 2 \"Curtain the region\").\n * Same pattern as runtime-sdk's initAntiFlicker but applied to an arbitrary\n * region element and owned by this adaptive: adaptive-product deliberately\n * has no runtime-sdk dependency (see nativeSlotSwap.ts's cross-package-dep\n * note), and runtime-sdk's schema generation consumes THIS package's dist,\n * so importing runtime-sdk here would create a build cycle.\n * The failsafe timeout guarantees the region always becomes visible.\n */\n\nconst CLASS = 'syntro-takeover-curtain';\nconst STYLE_ATTR = 'data-syntro-takeover-curtain';\n\n/** Ref-count for the shared style element: concurrent curtains (multiple\n * targets, or two takeover tiles) must not have the first lift() strip the\n * style the others still depend on. Removed only when the LAST lifts. */\nlet styleRefs = 0;\n\nfunction acquireStyle(): void {\n styleRefs += 1;\n if (document.head.querySelector(`style[${STYLE_ATTR}]`)) return;\n const styleEl = document.createElement('style');\n styleEl.setAttribute(STYLE_ATTR, '');\n styleEl.textContent = `.${CLASS} { opacity: 0 !important; }`;\n document.head.appendChild(styleEl);\n}\n\nfunction releaseStyle(): void {\n styleRefs = Math.max(0, styleRefs - 1);\n if (styleRefs === 0) {\n document.head.querySelector(`style[${STYLE_ATTR}]`)?.remove();\n }\n}\n\nexport function applyRegionCurtain(\n region: HTMLElement,\n opts: { timeoutMs?: number } = {}\n): () => void {\n const timeoutMs = opts.timeoutMs ?? 3_000;\n acquireStyle();\n region.classList.add(CLASS);\n\n let lifted = false;\n const lift = (): void => {\n if (lifted) return;\n lifted = true;\n clearTimeout(timeoutId);\n region.classList.remove(CLASS);\n releaseStyle();\n };\n const timeoutId = setTimeout(lift, timeoutMs);\n return lift;\n}\n\n/** Curtain several elements as one unit; the returned lift releases all. */\nexport function applyCurtainToTargets(\n targets: HTMLElement[],\n opts: { timeoutMs?: number } = {}\n): () => void {\n const lifts = targets.map((t) => applyRegionCurtain(t, opts));\n return () => {\n for (const lift of lifts) lift();\n };\n}\n", "/**\n * DOM/runtime adapters for the takeover engine. The engine stays pure; this\n * module owns every real-world effect. Activation is gated by CONFIG\n * DELIVERY (the served config either carries `takeover` or it doesn't) \u2014\n * there is deliberately no separate runtime flag.\n * The root is the config-mounted <syntro-pdp> element \u2014 the runtime owns its\n * mount lifecycle; deactivateRoot only empties/collapses it, never removes it.\n */\nimport type { TakeoverPorts, TakeoverResult } from './engine';\nimport { applyCurtainToTargets } from './regionCurtain';\nimport type { PdpTakeoverConfig } from './schema';\n\nexport interface DomPortDeps {\n root: HTMLElement;\n /** One round trip to the runtime-backend: scores the visitor and returns\n * the server-resolved plan. Stage 2 wires the real HTTP call. */\n fetchPlan: TakeoverPorts['fetchPlan'];\n signalCount: number;\n renderModules: TakeoverPorts['renderModules'];\n telemetry: TakeoverPorts['telemetry'];\n}\n\nexport function createDomPorts(config: PdpTakeoverConfig, deps: DomPortDeps): TakeoverPorts {\n return {\n hasSignals: () => deps.signalCount > 0,\n findSection(anchor) {\n try {\n return document.querySelector<HTMLElement>(anchor);\n } catch {\n return null; // malformed selector \u2014 engine treats as anchor missing\n }\n },\n measureSection: (el) => el.getBoundingClientRect().height,\n reserve(minHeightPx) {\n if (minHeightPx > 0) deps.root.style.minHeight = `${Math.round(minHeightPx)}px`;\n },\n releaseReservation() {\n deps.root.style.removeProperty('min-height');\n },\n region: () => deps.root.parentElement ?? deps.root,\n ownsNode: (el) => deps.root.contains(el),\n curtain() {\n // Curtain ONLY the takeover surface: the root plus each declared host\n // section. Never a shared ancestor \u2014 on a real PDP the root's parent\n // wraps the whole product page, and blanking it hides the buy box and\n // the LCP element for the plan-fetch window (review F1).\n const targets: HTMLElement[] = [deps.root];\n for (const section of config.hostSections) {\n try {\n const el = document.querySelector<HTMLElement>(section.anchor);\n if (el) targets.push(el);\n } catch {\n // malformed selector \u2014 engine handles it at suppress time\n }\n }\n // Failsafe must outlast the configurable wait: budget + 1s headroom,\n // never below the 3s default.\n const failsafe = Math.max(3_000, (config.commitTimeoutMs ?? 500) + 1_000);\n return applyCurtainToTargets(targets, { timeoutMs: failsafe });\n },\n deactivateRoot() {\n deps.root.replaceChildren();\n deps.root.style.display = 'none';\n },\n fetchPlan: deps.fetchPlan,\n renderModules: deps.renderModules,\n telemetry: deps.telemetry,\n };\n}\n\n/**\n * SPA-navigation / bfcache safety: dispose the takeover when the page hides.\n * NOTE: pagehide-dispose alone leaves a bfcache-restored page on the native\n * fallback (connectedCallback does not re-fire on restore). The takeover\n * OWNER must also listen for `pageshow` with `event.persisted` and re-enter\n * takeover \u2014 that wiring lives at the call site, not here.\n */\nexport function attachLifecycleDispose(result: TakeoverResult): () => void {\n const onHide = (): void => result.dispose();\n window.addEventListener('pagehide', onHide, { once: true });\n return () => window.removeEventListener('pagehide', onHide);\n}\n", "import { html, LitElement, nothing } from 'lit';\n\n/**\n * <syntro-pdp-section-header> \u2014 the eyebrow + title + italic subtitle\n * pattern that frames every agent-authored PDP section. Fonts inherit the\n * host page by default (override via --syntro-pdp-sans / --syntro-pdp-display)\n * so the section reads as native typography, not an imposed type system.\n *\n * Props:\n * - `eyebrow` (string, required) \u2014 small-caps label above the title\n * (e.g. \"Signals \u00B7 this week\", \"Compared\")\n * - `title` (string, required) \u2014 main display title\n * - `accent` (string, optional) \u2014 substring inside `title` rendered\n * italic in primary color. Matched\n * case-insensitively, first occurrence\n * only. If omitted, the whole title\n * renders plain.\n * - `subtitle` (string, optional) \u2014 italic subtitle / LLM rationale.\n *\n * Brand customization:\n * --syntro-pdp-primary primary accent color\n * --syntro-pdp-fg heading foreground\n * --syntro-pdp-muted subtitle / eyebrow color\n * --syntro-pdp-display display font override (inherits host font if unset)\n */\n\nfunction ensureSectionHeaderStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-section-header-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-section-header-style';\n s.textContent = SECTION_HEADER_CSS;\n document.head.appendChild(s);\n}\n\nconst SECTION_HEADER_CSS = `\n/* The header renders at the FULL-WIDTH section (band) level so the eyebrow\n can center across the whole horizontal slice, while the headline below\n re-constrains to the content column and left-aligns with the body. */\nsyntro-pdp-section-header { display: block; }\n.syntro-pdp-section-header {\n --sh-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n /* The eyebrow dot + title accent use the per-slot section accent when\n SyntroPdpLit sets one; otherwise they fall back to the host's brand\n primary. The SDK never overwrites --syntro-pdp-primary itself, so a\n customer's brand token stays intact everywhere it's read. */\n --sh-accent: var(--syntro-pdp-section-accent, var(--sh-primary));\n --sh-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --sh-muted: var(--syntro-pdp-muted, hsl(30 8% 55%));\n --sh-display: var(--syntro-pdp-display, inherit);\n font-family: var(--syntro-pdp-sans, inherit);\n}\n\n/* Eyebrow: centered across the full-width band (the kicker). */\n.syntro-pdp-section-eyebrow {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 10px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--sh-muted);\n /* Sit up near the band's top edge with a clear gap down to the title \u2014\n the eyebrow reads as a kicker on the color transition, not a title label. */\n margin-bottom: clamp(28px, 4vw, 44px);\n font-weight: 500;\n}\n/* Headline: re-constrain to the content column + left-align so the title and\n subtitle line up on the left with the body, while the band stays full-width.\n Same max-width + horizontal inset as the body's inner wrapper (inherited\n --sc-pdp-* tokens) so their left edges match. */\n.syntro-pdp-section-headline {\n max-width: var(--sc-pdp-max-w, 1080px);\n margin-inline: auto;\n padding-inline: var(--sc-pdp-section-pad-x, clamp(20px, 4vw, 40px));\n text-align: left;\n}\n.syntro-pdp-section-eyebrow-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sh-accent);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--sh-accent) 16%, transparent);\n animation: syntro-pdp-pulse-dot 2200ms ease-in-out infinite;\n}\n\n.syntro-pdp-section-title {\n font-family: var(--sh-display);\n font-weight: 400;\n /* Smaller upper bound + wider wrap target so 30-45 char titles fit a\n single line at the typical demo content width. The 22ch / 56px\n pair from v1 produced ubiquitous 2-line headers \u2014 every editorial\n title in the live planner output was wrapping. */\n font-size: clamp(28px, 2.6vw, 38px);\n line-height: 1.1;\n letter-spacing: -0.015em;\n color: var(--sh-fg);\n font-variation-settings: 'SOFT' 80, 'WONK' 1;\n margin: 0;\n max-width: 46ch;\n text-align: left;\n}\n.syntro-pdp-section-title em {\n font-style: italic;\n font-variation-settings: 'SOFT' 100, 'WONK' 1;\n color: var(--sh-accent);\n}\n\n.syntro-pdp-section-subtitle {\n margin: 14px 0 0;\n font-style: italic;\n font-family: var(--sh-display);\n font-size: 16.5px;\n line-height: 1.55;\n color: var(--sh-muted);\n max-width: 60ch;\n text-align: left;\n}\n\n@media (max-width: 640px) {\n .syntro-pdp-section-title {\n font-size: clamp(24px, 6.5vw, 30px);\n max-width: 30ch;\n }\n .syntro-pdp-section-subtitle {\n font-size: 14.5px;\n margin-top: 10px;\n }\n}\n\n@keyframes syntro-pdp-pulse-dot {\n 0%, 100% { opacity: 0.6; transform: scale(1); }\n 50% { opacity: 1; transform: scale(1.15); }\n}\n`;\n\n/**\n * Find the first case-insensitive occurrence of `needle` inside `haystack`\n * and return a 3-tuple of [before, match, after]. Returns null if not found\n * so the caller can render the title plain.\n */\nfunction splitTitleAt(haystack: string, needle: string): [string, string, string] | null {\n if (!needle) return null;\n const idx = haystack.toLowerCase().indexOf(needle.toLowerCase());\n if (idx < 0) return null;\n return [\n haystack.slice(0, idx),\n haystack.slice(idx, idx + needle.length),\n haystack.slice(idx + needle.length),\n ];\n}\n\nexport class PdpSectionHeaderLit extends LitElement {\n static override properties = {\n eyebrow: { type: String },\n title: { type: String },\n accent: { type: String },\n subtitle: { type: String },\n };\n\n eyebrow = '';\n title = '';\n accent = '';\n subtitle = '';\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureSectionHeaderStyles();\n }\n\n override render() {\n const split = this.accent ? splitTitleAt(this.title, this.accent) : null;\n return html`\n <header class=\"syntro-pdp-section-header\">\n <div class=\"syntro-pdp-section-eyebrow\">\n <span class=\"syntro-pdp-section-eyebrow-dot\" aria-hidden=\"true\"></span>\n <span>${this.eyebrow}</span>\n </div>\n <div class=\"syntro-pdp-section-headline\">\n <h2 class=\"syntro-pdp-section-title\">\n ${split ? html`${split[0]}<em>${split[1]}</em>${split[2]}` : this.title}\n </h2>\n ${\n this.subtitle\n ? html`<p class=\"syntro-pdp-section-subtitle\">${this.subtitle}</p>`\n : nothing\n }\n </div>\n </header>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-section-header')) {\n customElements.define('syntro-pdp-section-header', PdpSectionHeaderLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-section-header': PdpSectionHeaderLit;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\n\n/**\n * <syntro-pdp-slot> \u2014 wraps a single PDP slot container with its skeleton /\n * pending / failed state. The host renders this element with a `slot-id` and\n * `kind`; the runtime imperatively mounts the LLM-authored custom element\n * INSIDE the inner mount container via `el.data = ...; mount.appendChild(el)`.\n *\n * The skeleton animates as a sibling of the mount container (grid-stacked)\n * so it can be hidden via the `state` attribute without React/Lit reconciling\n * the imperatively-appended child out of existence.\n *\n * Props:\n * - `slot-id` (string, required) \u2014 the DOM id the runtime targets to mount\n * (also exposed on the inner mount div for\n * backwards compatibility with the\n * existing wire contract).\n * - `kind` ('trending-news' | 'chart' | string) \u2014 chooses the skeleton\n * copy and accent.\n * - `state` ('pending' | 'mounted' | 'failed') \u2014 controls which layer\n * is visible. Default: 'pending'.\n *\n * Brand customization:\n * --syntro-pdp-primary primary accent color\n * --syntro-pdp-fg body foreground\n * --syntro-pdp-muted muted text\n * --syntro-pdp-border slot border\n * --syntro-pdp-card-bg slot background top\n * --syntro-pdp-card-bg-b slot background bottom\n * --syntro-pdp-display display font override (inherits host font if unset)\n */\n\nfunction ensureSlotStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-slot-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-slot-style';\n s.textContent = SLOT_CSS;\n document.head.appendChild(s);\n}\n\nconst SLOT_CSS = `\n.syntro-pdp-slot {\n --sl-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --sl-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --sl-muted: var(--syntro-pdp-muted, hsl(30 8% 55%));\n --sl-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --sl-bg-a: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --sl-bg-b: var(--syntro-pdp-card-bg-b, hsl(25 10% 9%));\n --sl-display: var(--syntro-pdp-display, inherit);\n\n position: relative;\n display: grid;\n grid-template-areas: 'stack';\n font-family: var(--syntro-pdp-sans, inherit);\n}\n.syntro-pdp-slot > .syntro-pdp-slot-mount,\n.syntro-pdp-slot > .syntro-pdp-slot-pending,\n.syntro-pdp-slot > .syntro-pdp-slot-failed {\n grid-area: stack;\n}\n\n.syntro-pdp-slot-mount {\n min-height: 220px;\n}\n\n/* When mutation arrived, hide the pending layer */\n.syntro-pdp-slot[data-state='mounted'] .syntro-pdp-slot-pending,\n.syntro-pdp-slot[data-state='failed'] .syntro-pdp-slot-pending {\n display: none;\n}\n.syntro-pdp-slot[data-state='mounted'] .syntro-pdp-slot-failed,\n.syntro-pdp-slot[data-state='pending'] .syntro-pdp-slot-failed {\n display: none;\n}\n\n/* Pending \u2014 a quietly-animated waiting state. */\n.syntro-pdp-slot-pending {\n position: relative;\n border: 1px dashed var(--sl-border);\n border-radius: 22px;\n min-height: 220px;\n padding: 36px 28px;\n background: linear-gradient(180deg, var(--sl-bg-a), var(--sl-bg-b));\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n}\n.syntro-pdp-slot-pending-shimmer {\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n transparent,\n color-mix(in srgb, var(--sl-primary) 7%, transparent),\n transparent\n );\n transform: translateX(-100%);\n animation: syntro-pdp-slot-sweep 1800ms ease-in-out infinite;\n}\n.syntro-pdp-slot-pending-row {\n position: relative;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.syntro-pdp-slot-pending-dot {\n flex: 0 0 auto;\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--sl-primary);\n box-shadow: 0 0 0 6px color-mix(in srgb, var(--sl-primary) 14%, transparent);\n animation: syntro-pdp-slot-pulse 1600ms ease-in-out infinite;\n}\n.syntro-pdp-slot-pending-label {\n font-family: var(--sl-display);\n font-style: italic;\n font-size: 18px;\n font-variation-settings: 'SOFT' 70;\n color: var(--sl-muted);\n margin: 0;\n line-height: 1.4;\n}\n\n/* Failed \u2014 the planner or sub-agent failed; the host page still works. */\n.syntro-pdp-slot-failed {\n border: 1px solid var(--sl-border);\n border-radius: 22px;\n min-height: 180px;\n padding: 24px 28px;\n background: linear-gradient(180deg, var(--sl-bg-a), var(--sl-bg-b));\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 6px;\n}\n.syntro-pdp-slot-failed-title {\n font-family: var(--sl-display);\n font-style: italic;\n font-size: 17px;\n color: var(--sl-fg);\n margin: 0;\n font-variation-settings: 'SOFT' 70;\n}\n.syntro-pdp-slot-failed-body {\n color: var(--sl-muted);\n font-size: 13.5px;\n margin: 0;\n line-height: 1.5;\n}\n\n@media (max-width: 640px) {\n .syntro-pdp-slot-pending,\n .syntro-pdp-slot-failed {\n border-radius: 18px;\n padding: 24px 18px;\n min-height: 180px;\n }\n .syntro-pdp-slot-pending-label {\n font-size: 15.5px;\n }\n}\n\n@keyframes syntro-pdp-slot-sweep {\n 0% { transform: translateX(-100%); }\n 100% { transform: translateX(100%); }\n}\n@keyframes syntro-pdp-slot-pulse {\n 0%, 100% {\n opacity: 0.6;\n box-shadow: 0 0 0 6px color-mix(in srgb, var(--sl-primary) 14%, transparent);\n }\n 50% {\n opacity: 1;\n box-shadow: 0 0 0 8px color-mix(in srgb, var(--sl-primary) 26%, transparent);\n }\n}\n`;\n\nconst PENDING_COPY: Record<string, string> = {\n 'trending-news': 'Composing this section\u2026',\n chart: 'Drawing the comparison\u2026',\n qa: 'Picking the questions worth answering\u2026',\n regional: 'Locating you in time and place\u2026',\n 'peer-feed': 'Finding people like you\u2026',\n};\n\nconst PENDING_FALLBACK = 'Composing\u2026';\n\nexport class PdpSlotLit extends LitElement {\n static override properties = {\n slotId: { type: String, attribute: 'slot-id' },\n kind: { type: String },\n state: { type: String, reflect: true },\n };\n\n /** DOM id the host's mutation runtime targets when appending the widget. */\n slotId = '';\n kind = '';\n state: 'pending' | 'mounted' | 'failed' = 'pending';\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureSlotStyles();\n }\n\n override willUpdate(): void {\n // Reflect state via a data attribute so the CSS rules above can drive\n // the layer visibility without us having to conditionally render mounts.\n // We always render the mount div so the runtime's appendChild lands.\n this.dataset.state = this.state;\n }\n\n override render() {\n const pendingLabel = PENDING_COPY[this.kind] ?? PENDING_FALLBACK;\n return html`\n <div class=\"syntro-pdp-slot\" data-state=${this.state} data-kind=${this.kind}>\n <div\n class=\"syntro-pdp-slot-mount\"\n id=${this.slotId || nothing}\n data-pdp-slot=${this.kind}\n ></div>\n <div class=\"syntro-pdp-slot-pending\" aria-hidden=${this.state !== 'pending'}>\n <div class=\"syntro-pdp-slot-pending-shimmer\" aria-hidden=\"true\"></div>\n <div class=\"syntro-pdp-slot-pending-row\">\n <span class=\"syntro-pdp-slot-pending-dot\" aria-hidden=\"true\"></span>\n <p class=\"syntro-pdp-slot-pending-label\">${pendingLabel}</p>\n </div>\n </div>\n <div class=\"syntro-pdp-slot-failed\" aria-hidden=${this.state !== 'failed'}>\n <p class=\"syntro-pdp-slot-failed-title\">\n Personalized view couldn't load.\n </p>\n <p class=\"syntro-pdp-slot-failed-body\">\n The standard page still has everything you need.\n </p>\n </div>\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-slot')) {\n customElements.define('syntro-pdp-slot', PdpSlotLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-slot': PdpSlotLit;\n }\n}\n", "/**\n * Real `renderModules` for the takeover engine \u2014 renders the server plan's\n * `order` into the takeover root using the EXISTING slot machinery: the\n * `<syntro-pdp-slot>` wrapper (skeleton/failed states), the\n * `<syntro-pdp-section-header>` element, the section frame attributes the\n * head-injected SECTION_LAYOUT_CSS styles, and the existing preset/live\n * content split (baked payloads from the presets store mount immediately;\n * live kinds keep the `/api/pdp/module` streaming path behind a skeleton).\n * ZERO new placement mechanisms (see `sdk-widget-placement`).\n *\n * Plan entries are `<kind>` (Syntro-native) or `<kind>:<hostSectionId>`\n * (a baked module derived from a detached host section). An entry whose\n * widget kind is unknown (Stage 2b kinds: evidence-table, metric-grid,\n * stepper, timeline) is reported through `onModuleFailed(hostSectionId)` so\n * the engine reattaches that host section \u2014 never a blank box, never a\n * dropped section.\n *\n * Failure containment: every per-module failure is contained (slot flips to\n * 'failed', or the section is removed when it replaced host content). The\n * returned promise only rejects on a wholesale renderer defect \u2014 which the\n * engine answers by disposing the whole takeover (fail open).\n */\n\nimport { buildPdpPlan } from '../decision/pdp-compose';\nimport { selectPresetData } from '../scoring/pool-selection';\nimport type { SlotPreset } from '../scoring/scored-template';\n// Side-effect imports: register the slot + section-header custom elements.\n// Neither module imports takeover code, so no cycle.\nimport '../widgets/PdpSectionHeaderLit';\nimport '../widgets/PdpSlotLit';\nimport type { TakeoverPorts } from './engine';\n\nexport interface TakeoverModuleRendererDeps {\n /** The takeover root \u2014 the `<syntro-pdp>` element itself. */\n root: HTMLElement;\n /** Preset row (slot id \u2192 SlotPreset) for a decision archetype; the caller\n * owns the presets-store read and the `unknown`-row fallback. */\n getPresetRow(decisionArchetype: string): Record<string, SlotPreset> | undefined;\n /** TEMPLATE_TO_TAG resolution \u2014 null means the kind has no widget yet. */\n resolveTag(kind: string, variant: string | null): string | null;\n /** Live compose via `/api/pdp/module`; resolves null on any failure. */\n requestLiveModule(\n kind: string,\n decisionArchetype: string\n ): Promise<Record<string, unknown> | null>;\n telemetry(event: string, props?: Record<string, unknown>): void;\n /** The server-scored decision archetype \u2014 captured from the fetchPlan\n * response by the caller (the engine forwards only the plan). */\n getDecisionArchetype(): string;\n}\n\n/** `\"<kind>\"` or `\"<kind>:<hostSectionId>\"` \u2192 its parts. */\nexport function parsePlanEntry(entry: string): { kind: string; hostSectionId: string | null } {\n const i = entry.indexOf(':');\n if (i === -1) return { kind: entry, hostSectionId: null };\n return { kind: entry.slice(0, i), hostSectionId: entry.slice(i + 1) || null };\n}\n\ninterface SlotElementLike extends HTMLElement {\n slotId: string;\n kind: string;\n state: 'pending' | 'mounted' | 'failed';\n updateComplete: Promise<unknown>;\n}\n\ninterface SectionHeaderLike extends HTMLElement {\n eyebrow: string;\n title: string;\n accent: string;\n subtitle: string;\n}\n\nexport function createTakeoverModuleRenderer(\n deps: TakeoverModuleRendererDeps\n): TakeoverPorts['renderModules'] {\n return async (plan, onModuleFailed) => {\n const decisionArchetype = deps.getDecisionArchetype();\n const row = deps.getPresetRow(decisionArchetype) ?? {};\n /** Work that must settle before the renderer resolves: scaffolds + baked\n * mounts. Live fetches stream in AFTER the curtain lifts by design. */\n const bakedWork: Array<Promise<void>> = [];\n\n plan.order.forEach((entry, index) => {\n const { kind, hostSectionId } = parsePlanEntry(entry);\n const slotId = `pdp-slot-${kind}`;\n const preset = row[slotId];\n const tag = deps.resolveTag(kind, preset?.variant ?? null);\n if (!tag) {\n // Stage 2b kind (or plan drift): no widget exists for this entry.\n deps.telemetry('takeover.module_kind_unknown', { entry, kind });\n onModuleFailed(hostSectionId);\n return;\n }\n\n // Section scaffold \u2014 same frame the six-slot render produces, so the\n // head-injected section layout CSS and per-slot accents apply as-is.\n const envelope = buildPdpPlan([{ slot: slotId, preset }]);\n const task = envelope.tasks[0];\n const section = document.createElement('section');\n section.toggleAttribute('data-syntro-pdp-section', true);\n section.setAttribute('data-slot', slotId);\n section.setAttribute('data-syntro-takeover-entry', entry);\n const header = document.createElement('syntro-pdp-section-header') as SectionHeaderLike;\n header.eyebrow = task.header.eyebrow ?? '';\n header.title = task.header.title ?? '';\n header.accent = task.header.accent ?? '';\n header.subtitle = task.header.subtitle ?? '';\n const inner = document.createElement('div');\n inner.toggleAttribute('data-syntro-pdp-section-inner', true);\n const slotEl = document.createElement('syntro-pdp-slot') as SlotElementLike;\n // Unique per entry: duplicate kinds (two host-derived sections of the\n // same kind) must not produce duplicate mount ids.\n slotEl.slotId = `syntro-takeover-${index}-${slotId}`;\n slotEl.kind = kind;\n slotEl.state = 'pending';\n inner.appendChild(slotEl);\n section.append(header, inner);\n deps.root.appendChild(section);\n\n const fail = (): void => {\n if (hostSectionId) {\n // The module replaced detached host content and the engine is about\n // to reattach it \u2014 remove our section so the restored original never\n // sits next to a failed Syntro box.\n section.remove();\n } else {\n slotEl.state = 'failed';\n }\n deps.telemetry('takeover.module_failed', { entry, kind });\n onModuleFailed(hostSectionId);\n };\n\n const mountPayload = async (payload: Record<string, unknown>): Promise<void> => {\n await slotEl.updateComplete;\n const mount = slotEl.querySelector<HTMLElement>('.syntro-pdp-slot-mount');\n if (!mount) {\n fail();\n return;\n }\n mount.replaceChildren();\n const widget = document.createElement(tag) as HTMLElement & { data?: unknown };\n widget.data = payload;\n mount.appendChild(widget);\n slotEl.state = 'mounted';\n };\n\n if (preset) {\n // Baked payload \u2014 pool-selected for the scored decision archetype,\n // exactly like the six-slot path's transformPreset hook.\n const data = selectPresetData(preset.kind, preset.data, decisionArchetype) as Record<\n string,\n unknown\n >;\n bakedWork.push(mountPayload(data).catch(fail));\n } else {\n // Live kind \u2014 skeleton now, module streams in whenever it lands.\n // Deliberately NOT awaited by the renderer: the engine lifts the\n // curtain on skeletons; failures are contained via fail().\n void deps\n .requestLiveModule(kind, decisionArchetype)\n .then((payload) => (payload ? mountPayload(payload) : fail()))\n .catch(fail);\n }\n });\n\n await Promise.all(bakedWork);\n };\n}\n", "import { html, LitElement } from 'lit';\n\nimport './OtherProductTileLit.js';\nimport type { OtherProductTileData } from './OtherProductTileLit.js';\n\n/**\n * <pdp-other-products-carousel> \u2014 horizontal scroll carousel of cross-sell tiles.\n *\n * Each tile is an <other-product-tile>. Scroll-snap on the x-axis, edge-fade\n * mask to hint at overflow. Collapses to a wider card on narrow viewports.\n *\n * Props:\n * - `data` (OtherProductsPayload): `{ tiles, rationale }`.\n */\n\nexport interface OtherProductsPayload {\n tiles: OtherProductTileData[];\n rationale: string;\n}\n\nfunction ensureCarouselStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-other-products-carousel-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-other-products-carousel-style';\n s.textContent = CAROUSEL_CSS;\n document.head.appendChild(s);\n}\n\nconst CAROUSEL_CSS = `\n/* Host containment \u2014 must not bust the PDP container width. */\nsyntro-pdp-other-products-carousel {\n display: block;\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.opc-carousel {\n --opc-sans: var(--syntro-pdp-sans, inherit);\n font-family: var(--opc-sans);\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n/* Flex track (not grid) \u2014 grid auto-sizes to content even with overflow:auto,\n which would cause whole-page horizontal scroll. */\n.carousel {\n display: flex;\n flex-wrap: nowrap;\n gap: 0.75rem;\n overflow-x: auto;\n overflow-y: hidden;\n scroll-snap-type: x mandatory;\n scrollbar-width: thin;\n scrollbar-color: var(--syntro-pdp-border, hsl(30 6% 20%)) transparent;\n padding: 0.5rem 0.25rem 0.75rem;\n width: 100%;\n min-width: 0;\n /* Edge fade to hint at overflow content */\n mask-image: linear-gradient(90deg, transparent 0, black 12px, black calc(100% - 20px), transparent 100%);\n -webkit-mask-image: linear-gradient(90deg, transparent 0, black 12px, black calc(100% - 20px), transparent 100%);\n}\n.carousel::-webkit-scrollbar {\n height: 4px;\n}\n\n.opc-item {\n flex: 0 0 calc(25% - 0.6rem);\n scroll-snap-align: start;\n min-width: 220px;\n max-width: 300px;\n box-sizing: border-box;\n}\n\n@media (max-width: 719px) {\n .opc-item {\n flex: 0 0 80%;\n }\n}\n`;\n\nexport class PdpOtherProductsCarouselLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: OtherProductsPayload | null = null;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureCarouselStyles();\n }\n\n override render() {\n if (!this.data) return html``;\n return html`\n <div class=\"opc-carousel\">\n <div class=\"carousel\" role=\"list\">\n ${this.data.tiles.map(\n (tile) => html`\n <div class=\"opc-item\" role=\"listitem\">\n <syntro-pdp-other-product-tile .tile=${tile}></syntro-pdp-other-product-tile>\n </div>\n `\n )}\n </div>\n </div>\n `;\n }\n}\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('syntro-pdp-other-products-carousel')\n) {\n customElements.define('syntro-pdp-other-products-carousel', PdpOtherProductsCarouselLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-other-products-carousel': PdpOtherProductsCarouselLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\n/**\n * <other-product-tile> \u2014 a single cross-sell / upsell product tile.\n *\n * Renders a compact card with: optional image, relationship badge,\n * product name + tagline, an AI-authored why_blurb, evidence chips\n * (social-proof signals), and a price + CTA footer.\n *\n * Designed as the leaf node for <pdp-other-products-carousel> and\n * <pdp-other-products-grid>. It is also usable standalone.\n *\n * Props:\n * - `tile` (OtherProductTileData): full tile descriptor from the backend.\n */\n\nexport interface OtherProductTileEvidenceChip {\n kind: string;\n display: string;\n}\n\nexport interface OtherProductTileData {\n recommended_product_id: string;\n name: string;\n tagline: string;\n image_url: string | null;\n price_cents: number | null;\n badges: string[];\n relationship: 'discovery' | 'complement' | 'alternative';\n why_blurb: string;\n evidence_chips: OtherProductTileEvidenceChip[];\n}\n\nconst RELATIONSHIP_LABELS: Record<OtherProductTileData['relationship'], string> = {\n discovery: 'Recommended for you',\n complement: 'Pairs with this',\n alternative: 'Alternative',\n};\n\nfunction ensureOtherProductTileStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-other-product-tile-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-other-product-tile-style';\n s.textContent = OTHER_PRODUCT_TILE_CSS;\n document.head.appendChild(s);\n}\n\nconst OTHER_PRODUCT_TILE_CSS = `\n.opt-card {\n --opt-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --opt-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --opt-bg: var(--syntro-pdp-card-bg, hsl(25 10% 12%));\n --opt-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --opt-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --opt-chip-bg: var(--syntro-pdp-chip-bg, hsl(150 20% 16%));\n --opt-chip-fg: var(--syntro-pdp-chip-fg, hsl(150 32% 72%));\n --opt-sans: var(--syntro-pdp-sans, inherit);\n --opt-display: var(--syntro-pdp-display, inherit);\n\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 0.875rem;\n border: 1px solid var(--opt-border);\n border-radius: 14px;\n background: var(--opt-bg);\n font-family: var(--opt-sans);\n color: var(--opt-fg);\n transition: border-color 180ms ease;\n}\n.opt-card:hover {\n border-color: color-mix(in srgb, var(--opt-primary) 30%, var(--opt-border));\n}\n\n.opt-image {\n aspect-ratio: 1 / 1;\n border-radius: 8px;\n overflow: hidden;\n background: hsl(25 10% 15%);\n}\n.opt-image img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.opt-relationship {\n font-size: 10px;\n text-transform: uppercase;\n letter-spacing: 0.14em;\n color: var(--opt-primary);\n font-weight: 600;\n display: flex;\n align-items: center;\n gap: 0.3em;\n}\n/* The raw relationship key (.opt-relationship-type) is always in the DOM for\n machine-readable queries and test assertions. It is visually hidden \u2014 the\n human-readable label from RELATIONSHIP_LABELS is the visible text. */\n.opt-relationship-type {\n font-size: 0;\n width: 0;\n overflow: hidden;\n position: absolute;\n}\n\n.opt-name {\n font-weight: 700;\n font-size: 1rem;\n margin: 0;\n line-height: 1.2;\n color: var(--opt-fg);\n}\n\n.opt-tagline {\n font-size: 0.82rem;\n color: var(--opt-fg-muted);\n margin: 0;\n line-height: 1.3;\n}\n\n.opt-why {\n font-family: var(--opt-display);\n font-style: italic;\n font-size: 13.5px;\n line-height: 1.45;\n margin: 0;\n color: var(--opt-fg);\n font-variation-settings: 'SOFT' 70;\n}\n\n.opt-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 0.3rem;\n}\n.opt-chip {\n font-size: 0.68rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n background: var(--opt-chip-bg);\n color: var(--opt-chip-fg);\n font-weight: 500;\n letter-spacing: 0.02em;\n}\n\n.opt-footer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 0.25rem;\n gap: 0.5rem;\n}\n.opt-price {\n font-weight: 700;\n font-size: 0.95rem;\n font-variant-numeric: tabular-nums;\n}\n.opt-cta {\n border: 1px solid var(--opt-border);\n background: transparent;\n color: var(--opt-fg);\n padding: 0.35rem 0.75rem;\n border-radius: 8px;\n cursor: pointer;\n font-size: 0.8rem;\n font-family: var(--opt-sans);\n font-weight: 500;\n transition: border-color 150ms ease, background 150ms ease;\n}\n.opt-cta:hover {\n border-color: var(--opt-primary);\n background: color-mix(in srgb, var(--opt-primary) 10%, transparent);\n}\n`;\n\nexport class OtherProductTileLit extends LitElement {\n static override properties = {\n tile: { attribute: false },\n };\n\n tile: OtherProductTileData | null = null;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureOtherProductTileStyles();\n }\n\n override render() {\n const t = this.tile;\n if (!t) {\n return html``;\n }\n const relationshipLabel = RELATIONSHIP_LABELS[t.relationship];\n const price = t.price_cents == null ? '' : `$${(t.price_cents / 100).toFixed(0)}`;\n return html`\n <article class=\"opt-card\" data-relationship=\"${t.relationship}\">\n ${\n t.image_url\n ? html`<div class=\"opt-image\">\n <img src=\"${t.image_url}\" alt=\"${t.name}\" loading=\"lazy\" />\n </div>`\n : null\n }\n <div class=\"opt-relationship\" data-relationship=\"${t.relationship}\">\n ${relationshipLabel ? html`${relationshipLabel}` : null}\n <span class=\"opt-relationship-type\">${t.relationship}</span>\n </div>\n <h3 class=\"opt-name\">${t.name}</h3>\n ${t.tagline ? html`<p class=\"opt-tagline\">${t.tagline}</p>` : null}\n ${t.why_blurb ? html`<p class=\"opt-why\">${t.why_blurb}</p>` : null}\n ${\n t.evidence_chips.length\n ? html`<div class=\"opt-chips\">\n ${t.evidence_chips.map((c) => html`<span class=\"opt-chip\">${c.display}</span>`)}\n </div>`\n : null\n }\n <div class=\"opt-footer\">\n <span class=\"opt-price\">${price}</span>\n <button type=\"button\" class=\"opt-cta\" data-action=\"view-product\">View</button>\n </div>\n </article>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-other-product-tile')) {\n customElements.define('syntro-pdp-other-product-tile', OtherProductTileLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-other-product-tile': OtherProductTileLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\nimport './OtherProductTileLit.js';\nimport type { OtherProductsPayload } from './PdpOtherProductsCarouselLit.js';\n\n/**\n * <pdp-other-products-grid> \u2014 2-column grid of cross-sell tiles.\n *\n * Collapses to a single column on narrow viewports.\n *\n * Props:\n * - `data` (OtherProductsPayload): `{ tiles, rationale }`.\n */\n\nfunction ensureGridStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-other-products-grid-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-other-products-grid-style';\n s.textContent = GRID_CSS;\n document.head.appendChild(s);\n}\n\nconst GRID_CSS = `\nsyntro-pdp-other-products-grid {\n display: block;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n}\n\n.grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 0.75rem;\n}\n\n@media (max-width: 719px) {\n .grid {\n grid-template-columns: 1fr;\n }\n}\n`;\n\nexport class PdpOtherProductsGridLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: OtherProductsPayload | null = null;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureGridStyles();\n }\n\n override render() {\n if (!this.data) return html``;\n return html`\n <div class=\"grid\" role=\"list\">\n ${this.data.tiles.map(\n (tile) => html`\n <syntro-pdp-other-product-tile role=\"listitem\" .tile=${tile}></syntro-pdp-other-product-tile>\n `\n )}\n </div>\n `;\n }\n}\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('syntro-pdp-other-products-grid')\n) {\n customElements.define('syntro-pdp-other-products-grid', PdpOtherProductsGridLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-other-products-grid': PdpOtherProductsGridLit;\n }\n}\n", "/**\n * Native slot watcher \u2014 the quick-view-modal half of the native-takeover\n * feature (FEAT-1784346204).\n *\n * On the vela SPA there is NO `<syntro-pdp>` element: the quick-view modal\n * server-renders its OWN native FAQ / review sections (the Task 4 shape). This\n * watcher observes those sections appearing, scores the visitor ONCE per product,\n * swaps the personalized preset content into the host sections (via the Task 1\n * `swapNativeSlot` engine), mounts a compact trending-news \"extras\" section, and\n * speaks a small DOM-event protocol that the demo toggle (Task 7) drives:\n *\n * listens (document): `syntro:demo-toggle` \u2014 CustomEvent<{ on: boolean }>\n * emits (document): `syntro:pdp-native-swap` \u2014 CustomEvent<{\n * state: 'swapped' | 'restored' | 'default-kept' | 'unavailable';\n * productId: string;\n * summary: string; // \"Tailored for: runner \u00B7 fit_risk\"; '' when not swapped\n * canToggle: boolean; // true whenever flipping the toggle can visibly change\n * // anything \u2014 a personalized overlap swap OR the cold\n * // generic extras. false for `unavailable` and for a\n * // cold visitor whose product has no `unknown`-row\n * // trending-news to mount.\n * extrasMounted: boolean; // whether the compact trending-news extras section is\n * // currently in the DOM. A cold-with-extras visitor\n * // emits `default-kept` + extrasMounted:true.\n * archetype: string; // raw scorer sport slug ('' unless swapped) \u2014 the\n * // band maps it to HUMAN words and never prints it.\n * decision: string; // raw scorer decision slug ('' unless swapped).\n * }>\n *\n * The four states are unchanged. A fully-unknown visitor on a product that HAS an\n * `unknown` preset row with trending-news still keeps the overlap sections\n * (qa/peer-feed) at their defaults, but mounts the generic extras \u2014 parity with\n * `<syntro-pdp>`, which renders the unknown preset's extra sections. That case\n * emits `default-kept` + canToggle:true + extrasMounted:true.\n *\n * It is INERT on pages owned by `<syntro-pdp>` (that element does its own native\n * takeover): if one exists in the document when the watcher starts, it never\n * observes. Single watcher per page; module-scoped state; never throws.\n */\n\n// WI-4: presets come through the store accessor \u2014 populated by the\n// runtime-sdk bootstrap fetch (config `pdpPresetsUrl`) or by legacy demo\n// boot scripts; never read the window global directly here.\nimport { getPdpPresets, pdpPresetsFetchStarted, pdpPresetsReady } from '../presets-store';\nimport { DISPLAY_MAX, selectPoolItems, type TaggedItem } from '../scoring/pool-selection';\nimport { gatherVisitorSignals } from '../widgets/SyntroPdpLit';\nimport {\n NATIVE_KIND_BY_SLOT,\n NATIVE_PRODUCT_ATTR,\n NATIVE_SLOT_ATTR,\n type NativeItemFields,\n type NativeSlotKind,\n type NativeSwapHandle,\n nativeFieldsFromContent,\n nativeProductId,\n swapNativeSlot,\n} from './nativeSlotSwap';\n\n// \u2500\u2500\u2500 global reads (mirror SyntroPdpLit's access patterns) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** One config-set slot value: the widget kind, an optional header, and its content. */\ninterface SlotPreset {\n kind: string;\n header?: unknown;\n data: unknown;\n}\n/** `presets[product][archetype][slot] = SlotPreset` \u2014 the config's SET values. */\ntype PdpPresetTable = Record<string, Record<string, Record<string, SlotPreset>>>;\n\ninterface ScoreSpec {\n model_id: string;\n name: string;\n type: string;\n values?: string[];\n value_descriptions?: Record<string, string>;\n signals?: Record<string, unknown>;\n}\ninterface SynOSGlobal {\n score?: (spec: ScoreSpec) => Promise<string>;\n scoreMulti?: (specs: ScoreSpec[]) => Promise<string[]>;\n}\n\nfunction readSynOS(): SynOSGlobal | undefined {\n if (typeof window === 'undefined') return undefined;\n return (window as unknown as { SynOS?: SynOSGlobal }).SynOS;\n}\n/** Presets via the WI-4 store accessor (config `pdpPresetsUrl` fetch OR the\n * legacy hand-set window global \u2014 read lazily, absence never cached). */\nfunction readPresets(): PdpPresetTable | undefined {\n return getPdpPresets() as PdpPresetTable | undefined;\n}\nfunction readArchetypeDescriptions(): Record<string, string> {\n if (typeof window === 'undefined') return {};\n return (\n (window as unknown as { __SYNTRO_PDP_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_ARCHETYPES__ ?? {}\n );\n}\nfunction readDecisionDescriptions(): Record<string, string> {\n if (typeof window === 'undefined') return {};\n return (\n (window as unknown as { __SYNTRO_PDP_DECISION_ARCHETYPES__?: Record<string, string> })\n .__SYNTRO_PDP_DECISION_ARCHETYPES__ ?? {}\n );\n}\n\n// The runtime PDP endpoints cap chat_excerpt; the element folds it into signals,\n// so mirror that here (best-effort \u2014 a missing excerpt degrades to '').\nconst CHAT_EXCERPT_KEY = 'syntro.chat.lastExcerpt';\nconst CHAT_EXCERPT_MAX = 7500;\nfunction readChatExcerpt(): string {\n try {\n if (typeof sessionStorage === 'undefined') return '';\n const raw = sessionStorage.getItem(CHAT_EXCERPT_KEY) ?? '';\n return raw.length > CHAT_EXCERPT_MAX ? `\u2026${raw.slice(-CHAT_EXCERPT_MAX + 1)}` : raw;\n } catch {\n return '';\n }\n}\n\nconst TRENDING_NEWS_TAG = 'syntro-pdp-trending-news';\nconst SECTION_HEADER_TAG = 'syntro-pdp-section-header';\nconst TRENDING_NEWS_SLOT = 'pdp-slot-trending-news';\nconst EXTRAS_ATTR = 'data-syntro-native-extras';\nconst UNKNOWN = 'unknown';\n\n// \u2500\u2500\u2500 decision (computed once per product) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface ScorePair {\n archetype: string;\n decision: string;\n}\n\ntype Decision =\n | { kind: 'unavailable' }\n | {\n kind: 'default-kept';\n /** the generic (`unknown`-row) trending-news payload for a cold visitor's\n * extras section, or null when there is nothing to mount. When non-null the\n * toggle is meaningful: it mounts/removes these extras. */\n extrasData: { payload: Record<string, unknown>; header: ExtrasHeader | null } | null;\n }\n | {\n kind: 'personalized';\n summary: string;\n /** Raw scorer slugs \u2014 carried through so the demo band can map them to\n * HUMAN words (never printed as slugs). */\n archetype: string;\n decision: string;\n /** slot id \u2192 the flat host-field items to fill (native qa / peer-feed). */\n fieldsBySlot: Map<string, NativeItemFields[]>;\n /** the pool-selected trending-news payload for the extras section, or null. */\n extrasData: { payload: Record<string, unknown>; header: ExtrasHeader | null } | null;\n };\n\n// \u2500\u2500\u2500 module-scoped state (single watcher per page) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Per-product score, computed at most once per pageload. */\nconst scoreCache = new Map<string, Promise<ScorePair>>();\n\ninterface Session {\n productId: string;\n decision: Decision;\n /** slot id \u2192 live swap handle (for restore on toggle-off / product switch). */\n handles: Map<string, NativeSwapHandle>;\n extras: HTMLElement | null;\n swapped: boolean;\n /** last emitted state \u2014 avoids re-emitting an unchanged state on spurious scans. */\n lastState: string | null;\n /** For NON-personalized (default-kept/unavailable) sessions: the native\n * container that anchored the last emit. When it detaches (modal closed) the\n * reconcile clears `lastState` so a re-appearing container re-emits. */\n anchor: HTMLElement | null;\n}\nlet session: Session | null = null;\n\n/** Monotonic request counter \u2014 only the latest-requested product may install a\n * session after its (possibly slow) score resolves. Guards the compute race. */\nlet requestSeq = 0;\n\n/** Personalization defaults ON (the demo's \"wow\"); the toggle turns it off/on. */\nlet toggleOn = true;\n\nlet observer: MutationObserver | null = null;\n/** True once a `<syntro-pdp>` was found at start \u2014 the watcher then stays inert. */\nlet disabled = false;\nlet currentStop: (() => void) | null = null;\n\nconst pending = new Set<string>();\nlet flushScheduled = false;\n\n// \u2500\u2500\u2500 scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function doScore(\n productPresets: Record<string, Record<string, SlotPreset>>\n): Promise<ScorePair> {\n const synos = readSynOS();\n const score = synos?.score;\n if (typeof score !== 'function') return { archetype: UNKNOWN, decision: UNKNOWN };\n const scoreMulti = synos?.scoreMulti;\n const signals = { ...gatherVisitorSignals(), chat_excerpt: readChatExcerpt() };\n const archetypeValues = Object.keys(productPresets).filter((v) => v !== UNKNOWN);\n const archetypeDescriptions = readArchetypeDescriptions();\n const decisionDescriptions = readDecisionDescriptions();\n const decisionValues = Object.keys(decisionDescriptions);\n\n const archetypeSpec: ScoreSpec = {\n model_id: 'llm',\n name: 'archetype',\n type: 'enum',\n values: archetypeValues,\n value_descriptions: Object.fromEntries(\n archetypeValues\n .filter((v) => archetypeDescriptions[v])\n .map((v) => [v, archetypeDescriptions[v]])\n ),\n signals,\n };\n\n // Two-axis scoring (sport archetype + decision archetype) through one model\n // call when both a decision taxonomy and the batched scorer are available \u2014\n // exactly the SyntroPdpLit path.\n if (decisionValues.length > 0 && scoreMulti) {\n const decisionSpec: ScoreSpec = {\n model_id: 'llm',\n name: 'decision_archetype',\n type: 'enum',\n values: decisionValues,\n value_descriptions: decisionDescriptions,\n signals,\n };\n const results = await scoreMulti([archetypeSpec, decisionSpec]);\n return { archetype: results[0] ?? UNKNOWN, decision: results[1] ?? UNKNOWN };\n }\n const archetype = await score(archetypeSpec);\n return { archetype: archetype ?? UNKNOWN, decision: UNKNOWN };\n}\n\nfunction scoreOnce(\n productId: string,\n productPresets: Record<string, Record<string, SlotPreset>>\n): Promise<ScorePair> {\n let p = scoreCache.get(productId);\n if (!p) {\n p = doScore(productPresets);\n scoreCache.set(productId, p);\n }\n return p;\n}\n\n// \u2500\u2500\u2500 field / extras selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction poolOf(preset: SlotPreset, kind: NativeSlotKind): TaggedItem[] {\n const data = preset.data as Record<string, unknown> | null;\n const list = kind === 'peer-feed' ? data?.peers : data?.items;\n return Array.isArray(list) ? (list as TaggedItem[]) : [];\n}\n\n/** Build the flat host-field items for one native slot from its scored preset. */\nfunction fieldsForSlot(\n row: Record<string, SlotPreset>,\n slotId: string,\n kind: NativeSlotKind,\n decision: string\n): NativeItemFields[] {\n const preset = row[slotId];\n if (!preset) return [];\n const selected = selectPoolItems(poolOf(preset, kind), decision, DISPLAY_MAX[kind] ?? Infinity);\n const content = kind === 'peer-feed' ? { peers: selected } : { items: selected };\n return nativeFieldsFromContent(kind, content);\n}\n\n/** The preset slot's section-header shape (eyebrow/title/accent/subtitle). */\ninterface ExtrasHeader {\n eyebrow?: string | null;\n title?: string | null;\n accent?: string | null;\n subtitle?: string | null;\n}\n\n/** Pool-selected trending-news payload + the slot's section header, so the\n * modal's extras carry the same heading the page's section wrapper renders\n * (a header-less article pile reads as a rendering glitch \u2014 Parker,\n * 2026-07-18). */\nfunction extrasDataFor(\n row: Record<string, SlotPreset>,\n decision: string\n): { payload: Record<string, unknown>; header: ExtrasHeader | null } | null {\n const preset = row[TRENDING_NEWS_SLOT];\n if (!preset || typeof preset.data !== 'object' || preset.data === null) return null;\n const data = preset.data as Record<string, unknown>;\n const items = Array.isArray(data.items) ? (data.items as TaggedItem[]) : [];\n const selected = selectPoolItems(items, decision, DISPLAY_MAX['trending-news'] ?? Infinity);\n const header =\n typeof preset.header === 'object' && preset.header !== null\n ? (preset.header as ExtrasHeader)\n : null;\n return { payload: { ...data, items: selected }, header };\n}\n\n/** A visitor is personalized unless BOTH scored axes declined to 'unknown'. */\nfunction isPersonalized(pair: ScorePair): boolean {\n return !(pair.archetype === UNKNOWN && pair.decision === UNKNOWN);\n}\n\nfunction summaryFor(pair: ScorePair): string {\n return pair.decision !== UNKNOWN\n ? `Tailored for: ${pair.archetype} \u00B7 ${pair.decision}`\n : `Tailored for: ${pair.archetype}`;\n}\n\nasync function computeDecision(productId: string): Promise<Decision> {\n // WI-4 ready gate: an in-flight config-driven presets fetch must land\n // before we snapshot the table \u2014 otherwise a modal opening during boot\n // would settle a (cached) 'unavailable' decision from an empty store.\n // Conditional so the no-fetch path keeps its exact synchronous timing\n // (the latest-request-wins race guard depends on score being called\n // within scanNow's first microtask). Never rejects.\n if (pdpPresetsFetchStarted()) await pdpPresetsReady();\n const productPresets = readPresets()?.[productId];\n const score = readSynOS()?.score;\n if (!productPresets || typeof score !== 'function') {\n console.warn('syntro-pdp.native_watch_unavailable', {\n productId,\n reason: !productPresets ? 'no-presets' : 'no-score',\n });\n return { kind: 'unavailable' };\n }\n try {\n const pair = await scoreOnce(productId, productPresets);\n if (!isPersonalized(pair)) {\n // Cold visitor: the overlap sections (qa/peer-feed) stay at their defaults,\n // but the generic extras still mount \u2014 the `unknown` sport-archetype row's\n // trending-news pool, sprinkle-selected (decision 'unknown'). This is the\n // same content `<syntro-pdp>` shows an unknown visitor. When the product has\n // no `unknown` row (or it carries no trending-news) there is nothing to\n // mount and the toggle stays inert.\n const unknownRow = productPresets[UNKNOWN];\n const extrasData = unknownRow ? extrasDataFor(unknownRow, UNKNOWN) : null;\n return { kind: 'default-kept', extrasData };\n }\n // Row for the scored archetype; fall back to the 'unknown' row when the\n // archetype has no dedicated preset row (keep-defaults otherwise).\n const row = productPresets[pair.archetype] ?? productPresets[UNKNOWN];\n if (!row) return { kind: 'default-kept', extrasData: null };\n const fieldsBySlot = new Map<string, NativeItemFields[]>();\n for (const [slotId, kind] of Object.entries(NATIVE_KIND_BY_SLOT)) {\n fieldsBySlot.set(slotId, fieldsForSlot(row, slotId, kind, pair.decision));\n }\n return {\n kind: 'personalized',\n summary: summaryFor(pair),\n archetype: pair.archetype,\n decision: pair.decision,\n fieldsBySlot,\n extrasData: extrasDataFor(row, pair.decision),\n };\n } catch (err) {\n console.warn('syntro-pdp.native_watch_unavailable', { productId, error: String(err) });\n return { kind: 'unavailable' };\n }\n}\n\n// \u2500\u2500\u2500 DOM helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** The native slot container for a product, ignoring any inside a `<syntro-pdp>`. */\nfunction slotContainer(productId: string, kind: NativeSlotKind): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n for (const el of document.querySelectorAll<HTMLElement>(`[${NATIVE_SLOT_ATTR}=\"${kind}\"]`)) {\n if (el.closest('syntro-pdp')) continue;\n if (nativeProductId(el) === productId) return el;\n }\n return null;\n}\n\ntype SwapEventState = 'swapped' | 'restored' | 'default-kept' | 'unavailable';\n\nfunction emit(\n state: SwapEventState,\n productId: string,\n summary: string,\n canToggle: boolean,\n extrasMounted: boolean,\n archetype = '',\n decision = ''\n): void {\n if (typeof document === 'undefined') return;\n document.dispatchEvent(\n new CustomEvent('syntro:pdp-native-swap', {\n detail: { state, productId, summary, canToggle, extrasMounted, archetype, decision },\n })\n );\n}\n\nfunction removeExtras(): void {\n if (!session) return;\n session.extras?.remove();\n session.extras = null;\n}\n\nfunction restoreHandles(): void {\n if (!session) return;\n for (const handle of session.handles.values()) {\n // Only restore containers still in the tree; a closed modal already removed\n // them, so restore() would reattach originals to a detached anchor.\n if (handle.container.isConnected) handle.restore();\n }\n session.handles.clear();\n}\n\nfunction tearDownSwap(): void {\n restoreHandles();\n removeExtras();\n if (session) session.swapped = false;\n}\n\n/**\n * Mount the compact trending-news extras section for a product, appended after the\n * peer-feed section's PARENT block (falling back to the qa section). Shared by the\n * personalized swap and the cold generic-extras path. Returns true when it mounted\n * (a resolvable container existed) so the caller can decide whether anything\n * visibly changed. Records the node on `session.extras`.\n */\nfunction mountExtras(\n productId: string,\n extrasData: { payload: Record<string, unknown>; header: ExtrasHeader | null }\n): boolean {\n if (!session || typeof document === 'undefined') return false;\n const anchor = slotContainer(productId, 'peer-feed') ?? slotContainer(productId, 'qa');\n if (!anchor) return false;\n const wrap = document.createElement('div');\n wrap.setAttribute(EXTRAS_ATTR, '');\n // Same section-header component the page's <syntro-pdp> wrapper renders \u2014\n // fed from the preset slot's own header (\"In the news / Recent research\").\n const header = document.createElement(SECTION_HEADER_TAG) as HTMLElement & {\n eyebrow?: string;\n title?: string;\n accent?: string;\n subtitle?: string;\n };\n header.eyebrow = extrasData.header?.eyebrow ?? 'In the news';\n header.title = extrasData.header?.title ?? '';\n header.accent = extrasData.header?.accent ?? '';\n header.subtitle = extrasData.header?.subtitle ?? '';\n wrap.appendChild(header);\n const news = document.createElement(TRENDING_NEWS_TAG) as HTMLElement & { data?: unknown };\n news.data = extrasData.payload;\n wrap.appendChild(news);\n // Insert after the native section's PARENT block \u2014 on the real modal DOM the\n // <section data-syntro-slot=\"peer-feed\"> is nested inside a Reviews card, so\n // anchoring on the section itself would cram the extras INSIDE that card.\n // Guard: if the parent IS the product-id panel host (flat markup) or is\n // missing, fall back to the container itself, so the extras can never escape\n // the modal panel.\n const panelHost = anchor.closest(`[${NATIVE_PRODUCT_ATTR}]`);\n const parent = anchor.parentElement;\n const insertAfter = parent && parent !== panelHost ? parent : anchor;\n insertAfter.after(wrap);\n session.extras = wrap;\n return true;\n}\n\n// \u2500\u2500\u2500 render (apply desired DOM for the current session + toggle) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Apply the desired swap for a personalized session. Returns the number of\n * native slots successfully swapped \u2014 the caller must NOT report/emit 'swapped'\n * when this is 0 (e.g. the product's containers are not in the DOM yet, or were\n * already detached by a modal close), or the pill would claim a swap that never\n * touched the page.\n */\nfunction applySwap(\n productId: string,\n decision: Extract<Decision, { kind: 'personalized' }>\n): number {\n if (!session) return 0;\n // Swap each native slot that is present with a resolvable container.\n for (const [slotId, kind] of Object.entries(NATIVE_KIND_BY_SLOT)) {\n const container = slotContainer(productId, kind);\n if (!container) continue;\n const fields = decision.fieldsBySlot.get(slotId) ?? [];\n // A container carries at most one live swap \u2014 restore a stale one first.\n session.handles.get(slotId)?.restore();\n session.handles.delete(slotId);\n const handle = swapNativeSlot(container, fields);\n if (handle) session.handles.set(slotId, handle);\n }\n // Extras: a compact trending-news section. Skip when there is no trending-news\n // preset for this archetype.\n if (decision.extrasData) mountExtras(productId, decision.extrasData);\n const swappedCount = session.handles.size;\n // Only mark the session swapped when at least one container was actually\n // rewritten. Leaving `swapped` false lets a later render retry once the\n // containers appear.\n if (swappedCount > 0) session.swapped = true;\n return swappedCount;\n}\n\n/** The native container currently anchoring a non-personalized session (used to\n * detect a modal close \u2192 reopen so the pill can re-emit). */\nfunction anchorContainer(productId: string): HTMLElement | null {\n return slotContainer(productId, 'peer-feed') ?? slotContainer(productId, 'qa');\n}\n\n/**\n * Render a cold (fully-unknown) session. The overlap sections are left at their\n * defaults; the ONLY thing the toggle drives is the generic extras section (when\n * the product's `unknown` row carries trending-news). `canToggle` is true exactly\n * when there ARE such extras to mount/remove.\n */\nfunction renderColdSession(\n productId: string,\n decision: Extract<Decision, { kind: 'default-kept' }>\n): void {\n if (!session) return;\n const canToggle = decision.extrasData !== null;\n\n // Reconcile detachment: if the anchoring container detached (modal closed) OR\n // the mounted extras detached, reset so a re-appearing modal re-mounts and\n // re-emits \u2014 the pill must resolve correctly on every surface, incl. re-open.\n const anchorDetached = session.anchor !== null && !session.anchor.isConnected;\n const extrasDetached = session.extras !== null && !session.extras.isConnected;\n if (anchorDetached || extrasDetached) {\n session.anchor = null;\n if (extrasDetached) session.extras = null;\n session.lastState = null;\n }\n\n // Desired extras presence follows the toggle (only meaningful when extras exist).\n const wantExtras = canToggle && toggleOn;\n let changed = false;\n if (wantExtras && session.extras === null && decision.extrasData) {\n if (mountExtras(productId, decision.extrasData)) changed = true;\n } else if (!wantExtras && session.extras !== null) {\n removeExtras();\n changed = true;\n }\n\n const extrasMounted = session.extras !== null;\n // Emit on first appearance of the state, or whenever the extras presence flips.\n if (session.lastState !== 'default-kept' || changed) {\n session.lastState = 'default-kept';\n session.anchor = anchorContainer(productId);\n emit('default-kept', productId, '', canToggle, extrasMounted);\n }\n}\n\nfunction renderSession(): void {\n if (!session) return;\n const { productId, decision } = session;\n\n if (decision.kind === 'unavailable') {\n // Reconcile the anchoring container: if the one that anchored the last emit\n // has detached (modal closed), clear lastState so a re-appearing container\n // re-emits \u2014 the pill must resolve on every surface, including on re-open.\n if (session.anchor && !session.anchor.isConnected) {\n session.anchor = null;\n session.lastState = null;\n }\n if (session.lastState !== 'unavailable') {\n session.lastState = 'unavailable';\n session.anchor = anchorContainer(productId);\n emit('unavailable', productId, '', false, false);\n }\n return;\n }\n\n if (decision.kind === 'default-kept') {\n renderColdSession(productId, decision);\n return;\n }\n\n // Personalized: reconcile a stale swap (modal re-opened \u2192 old nodes detached).\n if (session.swapped) {\n const stale =\n [...session.handles.values()].some((h) => !h.container.isConnected) ||\n (session.extras !== null && !session.extras.isConnected);\n if (stale) {\n session.handles.clear();\n session.extras = null;\n session.swapped = false;\n session.lastState = null;\n }\n }\n\n if (toggleOn && !session.swapped) {\n // Only report 'swapped' when a container was actually rewritten \u2014 a swap of\n // zero containers (product's slots absent, e.g. superseded/closed) must not\n // claim a swap.\n const swappedCount = applySwap(productId, decision);\n if (swappedCount > 0) {\n session.lastState = 'swapped';\n emit(\n 'swapped',\n productId,\n decision.summary,\n true,\n session.extras !== null,\n decision.archetype,\n decision.decision\n );\n }\n } else if (!toggleOn && session.swapped) {\n tearDownSwap();\n session.lastState = 'restored';\n emit('restored', productId, '', true, false);\n }\n}\n\n// \u2500\u2500\u2500 flush (batched appearing-product processing) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function onProductAppeared(productId: string): Promise<void> {\n if (session && session.productId !== productId) {\n // Product switch: the previous product's containers are gone (or being\n // replaced) \u2014 drop its swap without emitting for the old product.\n tearDownSwap();\n session = null;\n }\n if (!session) {\n // Capture a request token BEFORE the await. Only the latest-requested\n // product may install the session once its score resolves \u2014 otherwise a\n // slow compute for a product the visitor already left could resolve FIRST\n // and lock the session to the wrong product (its containers are gone),\n // falsely claiming a swap while the current product keeps defaults.\n const token = ++requestSeq;\n const decision = await computeDecision(productId);\n // A late-arriving decision for a product that has since been superseded is\n // discarded \u2014 either another product already took the session, or a newer\n // request has been issued since this one started.\n if (session || token !== requestSeq) return;\n session = {\n productId,\n decision,\n handles: new Map(),\n extras: null,\n swapped: false,\n lastState: null,\n anchor: null,\n };\n }\n renderSession();\n}\n\nasync function flush(): Promise<void> {\n flushScheduled = false;\n if (disabled) {\n pending.clear();\n return;\n }\n // A <syntro-pdp> can mount AFTER the watcher started \u2014 the config-driven\n // boundary mount, including the page-takeover root (Stage 2). The start-time\n // self-guard can't see a late mount, so re-check per flush: a page owned by\n // the element (it does its own native takeover / page takeover) must never\n // also be touched by the watcher.\n if (typeof document !== 'undefined' && document.querySelector('syntro-pdp')) {\n pending.clear();\n return;\n }\n const ids = [...pending];\n pending.clear();\n for (const id of ids) await onProductAppeared(id);\n}\n\nfunction scheduleFlush(): void {\n if (flushScheduled || disabled) return;\n flushScheduled = true;\n queueMicrotask(() => void flush());\n}\n\n/** Scan one added subtree for native slot containers with a resolvable product id. */\nfunction collectFromNode(node: Node): void {\n if (!(node instanceof HTMLElement)) return;\n const containers: HTMLElement[] = [];\n if (node.matches(`[${NATIVE_SLOT_ATTR}]`)) containers.push(node);\n containers.push(...node.querySelectorAll<HTMLElement>(`[${NATIVE_SLOT_ATTR}]`));\n for (const c of containers) {\n if (c.closest('syntro-pdp')) continue;\n const pid = nativeProductId(c);\n if (pid) pending.add(pid);\n }\n}\n\n// \u2500\u2500\u2500 event handlers (arrow functions \u2014 project convention) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst onDemoToggle = (e: Event): void => {\n const detail = (e as CustomEvent<{ on?: boolean }>).detail;\n if (!detail || typeof detail.on !== 'boolean') return;\n toggleOn = detail.on;\n renderSession();\n};\n\nconst onMutations: MutationCallback = (records) => {\n for (const record of records) {\n for (const node of record.addedNodes) collectFromNode(node);\n }\n scheduleFlush();\n};\n\n/** Attach the body observer. Deferred to DOMContentLoaded when `document.body`\n * isn't parsed yet at start (module-eval) time. */\nconst onDomReady = (): void => {\n if (disabled || observer || typeof document === 'undefined' || !document.body) return;\n observer = new MutationObserver(onMutations);\n observer.observe(document.body, { childList: true, subtree: true });\n};\n\n// \u2500\u2500\u2500 public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Start the native slot watcher. Idempotent: a second call is a no-op that\n * returns the SAME stop function (single watcher per page).\n *\n * No-ops (never observes) when a `<syntro-pdp>` element already owns the page \u2014\n * that element performs its own native takeover.\n */\nexport function startNativeSlotWatcher(): () => void {\n if (currentStop) return currentStop;\n\n if (typeof document !== 'undefined' && document.querySelector('syntro-pdp')) {\n // Page owned by <syntro-pdp>: stay inert. Still return a stop so callers can\n // treat activation uniformly.\n disabled = true;\n currentStop = () => {\n disabled = false;\n currentStop = null;\n };\n return currentStop;\n }\n\n disabled = false;\n if (typeof document !== 'undefined') {\n document.addEventListener('syntro:demo-toggle', onDemoToggle);\n if (document.body) {\n observer = new MutationObserver(onMutations);\n observer.observe(document.body, { childList: true, subtree: true });\n } else {\n // Module-eval before <body> is parsed (runtime.ts side-effect): observing a\n // null body would THROW and abort the rest of runtime.ts. Defer to DOM ready.\n document.addEventListener('DOMContentLoaded', onDomReady, { once: true });\n }\n }\n\n currentStop = () => {\n observer?.disconnect();\n observer = null;\n if (typeof document !== 'undefined') {\n document.removeEventListener('syntro:demo-toggle', onDemoToggle);\n document.removeEventListener('DOMContentLoaded', onDomReady);\n }\n tearDownSwap();\n session = null;\n requestSeq = 0;\n scoreCache.clear();\n pending.clear();\n flushScheduled = false;\n toggleOn = true;\n currentStop = null;\n };\n return currentStop;\n}\n\n/**\n * Test seam. Fully resets module state between tests (production code never calls\n * this). `scanNow` synchronously scans the whole document for native containers\n * and drives the SAME flush logic the observer uses \u2014 a deterministic stand-in\n * for jsdom's async MutationObserver.\n */\nexport const _testing = {\n async scanNow(): Promise<void> {\n if (typeof document !== 'undefined') collectFromNode(document.body);\n await flush();\n },\n readSynOSForTest: readSynOS,\n readPresetsForTest: readPresets,\n readArchetypeDescriptionsForTest: readArchetypeDescriptions,\n readDecisionDescriptionsForTest: readDecisionDescriptions,\n readChatExcerptForTest: readChatExcerpt,\n doScoreForTest: doScore,\n fieldsForSlotForTest: fieldsForSlot,\n extrasDataForTest: extrasDataFor,\n slotContainerForTest: slotContainer,\n mountExtrasForTest: mountExtras,\n emitForTest: emit,\n tearDownSwapForTest: tearDownSwap,\n reset(): void {\n observer?.disconnect();\n observer = null;\n if (typeof document !== 'undefined') {\n document.removeEventListener('syntro:demo-toggle', onDemoToggle);\n document.removeEventListener('DOMContentLoaded', onDomReady);\n }\n session = null;\n requestSeq = 0;\n scoreCache.clear();\n pending.clear();\n flushScheduled = false;\n toggleOn = true;\n disabled = false;\n currentStop = null;\n },\n};\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport { BindController } from '../bind/controller';\nimport {\n type CardEvent,\n type CardState,\n createInitialCardState,\n selectCardView,\n transitionCard,\n} from '../decision/card';\nimport { createCardEffectExecutor } from '../decision/card-effects';\nimport { getProductPlatformAdapter, type Variant } from '../platformAdapter';\nimport { sanitizeHtml } from '../sanitizer';\nimport { type CardProps, cardSchema, VISIBLE_FACT_KEYS, type VisibleFactKey } from '../schema';\nimport type { Product } from '../schema-primitives';\nimport { ensureStylesInRoot } from './ensureStylesInRoot';\nimport { reportRenderHealth } from './renderHealthReporter';\nimport './VariantPanelLit.js';\n\n/**\n * Typed interface for the runtime SDK's public window surface.\n * Catches symbol typos at compile time (e.g. __SYNTRO__ vs SynOS).\n * The runtime SDK sets `window.SynOS.handle` at api.ts:277/347.\n */\ninterface SynOSWindow {\n SynOS?: {\n handle?: { close?: () => void };\n runtime?: {\n events?: {\n publish?: (name: string, props: Record<string, unknown>) => void;\n // The ONLY transport that reaches PostHog. The EventBus forwards\n // emit()'d events to its posthogCapture sink (bootstrap-runtime wires\n // it to telemetry.track) after a consent gate; publish() does NOT \u2014\n // it only notifies in-process canvas subscribers. emit() enforces\n // validateEventName: a custom event MUST be `app:{category}:{action}`\n // (colon segments, no `app:` prefix \u2192 silently dropped).\n emit?: (name: string, props: Record<string, unknown>) => unknown;\n };\n context?: {\n get?(): { surface?: { device?: string } };\n subscribe?(cb: (ctx: { surface?: { device?: string } }) => void): () => void;\n };\n };\n };\n}\n\n/** Typed accessor \u2014 reads `window` on each call so tests can stub it at runtime. */\nfunction synWindow(): SynOSWindow {\n /* v8 ignore next -- ProductCard is a browser custom element; event/no-event paths cover this accessor in jsdom. */\n return (typeof window !== 'undefined' ? window : {}) as SynOSWindow;\n}\n\n/** Publish a named event through the runtime event bus (window.SynOS.runtime.events). */\nfunction _publishEvent(name: string, props: Record<string, unknown>): void {\n /* v8 ignore next -- SSR telemetry guard; telemetry publish and missing-publish behavior are covered in jsdom. */\n if (typeof window === 'undefined') return;\n try {\n const publish = synWindow().SynOS?.runtime?.events?.publish;\n if (typeof publish === 'function') {\n publish(name, props);\n }\n } catch {\n // Telemetry must never crash the widget.\n }\n}\n\n/**\n * Capture a telemetry event on the PostHog-reaching path.\n *\n * `publish()` only notifies in-process canvas subscribers \u2014 it never forwards\n * to PostHog. `emit()` is the sanctioned capture path: the EventBus forwards\n * emit()'d events to its posthogCapture sink (wired in bootstrap-runtime to the\n * consent-gated `telemetry.track`). We never call posthog directly \u2014 emit() is\n * the SDK's one consent-handled door to PostHog.\n *\n * `name` MUST be a valid custom-event name (`app:{category}:{action}` with\n * colon segments). A dotted name or a name missing the `app:` prefix is\n * silently dropped by the EventBus's validateEventName, which would re-create\n * the exact silent-failure this telemetry exists to surface.\n */\nfunction _captureTelemetry(name: string, props: Record<string, unknown>): void {\n /* v8 ignore next -- SSR telemetry guard; emit and missing-emit behavior are covered in jsdom. */\n if (typeof window === 'undefined') return;\n try {\n const emit = synWindow().SynOS?.runtime?.events?.emit;\n if (typeof emit === 'function') {\n emit(name, props);\n }\n } catch {\n // Telemetry must never crash the widget.\n }\n}\n\n/**\n * Derive a sorted, de-duplicated list of dotted field paths from Zod issues\n * (e.g. `['product.ctas', 'product.image.src']`). Used as the NON-PII context\n * for the `render_failed` telemetry event so the dashboard can see WHICH\n * required field made the card render blank. Only the structural path is\n * emitted \u2014 never the offending value, which could carry customer content.\n */\nfunction _invalidPathsFromIssues(\n issues: ReadonlyArray<{ path: ReadonlyArray<PropertyKey> }>\n): string[] {\n const paths = new Set<string>();\n for (const issue of issues) {\n // A non-empty path joins to a dotted field; an empty path means the root\n // object failed (e.g. `product` missing entirely) \u2014 record it as 'product'\n // so the signal is never blank.\n const dotted = issue.path.map((seg) => String(seg)).join('.');\n paths.add(dotted.length > 0 ? dotted : 'product');\n }\n return Array.from(paths).sort();\n}\n\n/**\n * The authoritative \"did not render\" telemetry event. Routed through emit()\n * (the PostHog-reaching path), so it MUST satisfy validateEventName:\n * `app:{category}:{action}` with colon segments. A dotted name would be\n * silently dropped \u2014 the very failure this signal exists to catch.\n */\nconst RENDER_FAILED_EVENT = 'app:product_card:render_failed';\n\n// Runtime self-detection (clipped / blank) is emitted through the shared\n// `reportRenderHealth` reporter \u2014 the same field-RUM counterpart every sibling\n// tile widget now uses (`app:product_card:clipped` / `:blank`). NON-PII: only\n// element paths + pixel numbers ever leave the widget.\n\ntype DeviceClass = 'mobile' | 'tablet' | 'desktop';\n\n/** The runtime's device class (surface.device), or 'desktop' when unavailable. */\nfunction _readDevice(): DeviceClass {\n /* v8 ignore next -- SSR device fallback for a browser element; runtime context branches are covered in jsdom. */\n if (typeof window === 'undefined') return 'desktop';\n try {\n const d = synWindow().SynOS?.runtime?.context?.get?.()?.surface?.device;\n return d === 'mobile' || d === 'tablet' ? d : 'desktop';\n } catch {\n return 'desktop';\n }\n}\n\n// \u2500\u2500 Card front-face LAYOUT tokens (canvas-owned, like the variant panel) \u2500\u2500\u2500\u2500\n//\n// The card owns the design VOCABULARY (pill spec chips, price-in-CTA-label,\n// panel color/radius tokens, compact rating, one-line tagline) \u2014 that's the\n// canvas-agnostic design system, so it lives inline here. But LAYOUT belongs\n// to the host canvas: a chat drawer / PDP wants a clean STACKED card, while\n// velvet's short-and-wide deck band wants a HORIZONTAL two-column card. Rather\n// than baking velvet's layout inline (which forced every surface into it),\n// layout is PARAMETERIZED via CSS custom properties with content-neutral\n// defaults. Only token VALUES cross the shadow boundary; no selectors, no\n// baked layout \u2014 the same discipline the VariantPanelLit --sc-variant-* tokens\n// use (and velvet sets both on the SAME deck-card rule).\n//\n// --sc-card-cols grid-template-columns of the front face's\n// image | content grid.\n// Default: density thumb + content \u2014 `72px 1fr`\n// (compact) / `110px 1fr` (standard), i.e. a small\n// top-aligned header-row thumbnail beside the content\n// (a clean STACKED card).\n// Velvet sets `minmax(72px, 30%) 1fr` \u2014 a real\n// two-column split where the image fills its column.\n//\n// --sc-card-image-max image size cap. Default a modest thumb\n// (`72px` compact / `110px` standard). Velvet sets\n// `100%` so the image fills (column-width) its column.\n//\n// (Which rows the image SPANS is decided by the named\n// `image` grid area \u2014 see --sc-card-areas \u2014 not a\n// separate row token; a grid-row override would unset\n// the area's row span.)\n//\n// --sc-card-image-h image height. Default `auto` (a square-ish thumb\n// sized by --sc-card-image-max). Velvet sets `100%`\n// so the full-height panel fills the spanned rows.\n//\n// --sc-card-image-aspect image aspect-ratio. Default `1 / 1` (a square\n// thumb). Velvet sets `auto` so height:100% +\n// object-fit:cover win and the picture fills the tall\n// column instead of forcing a square row.\n//\n// --sc-card-image-align image align-self within its grid area. Default\n// `start` (top-aligned thumb). Velvet sets `stretch`\n// so the image paints the whole full-height area.\n//\n// --sc-card-chip-max-h spec-chip-row cap. Chips wrap freely by default\n// (`none` = no cap, multi-row wrap). Velvet sets a\n// one-row height (`1.9rem`) with the row's\n// `overflow: hidden` clipping to a single row \u2014 the\n// card is a glance preview in the bounded band. A\n// max-height + overflow:hidden is the cleanest CSS\n// mechanism: it needs no JS row-counting and degrades\n// to \"show as many chips as fit\" instead of a hard\n// count.\n//\n// --sc-card-col-gap gap between the image and content columns.\n// Default density-based; velvet may tighten it.\n//\n// --sc-card-areas grid-template-areas of the front face. This is the\n// LAYOUT-STRUCTURE token that a plain column/row token\n// can't express: WHERE the CTA sits relative to the\n// image and content. Three named areas: `image`,\n// `content`, `cta`.\n// Default (stacked/neutral):\n// \"image content\"\n// \"cta cta\"\n// \u2014 a top-aligned thumb beside the content, then a\n// FULL-WIDTH CTA row spanning below both (the clean\n// chat/PDP card). Velvet sets:\n// \"image content\"\n// \"image cta\"\n// so the image becomes a full-height LEFT feature\n// column (spans both rows) and the CTA sits in the\n// RIGHT column at the bottom \u2014 mirroring where the\n// variant picker's CTA lives. Grid-area NAMES are\n// referenced by var() on each child (image-area/\n// content-area/cta-area tokens below), so the areas\n// string and the child placements stay in sync.\n//\n// --sc-card-rows grid-template-rows of the front face. Default\n// `auto auto` (content row + CTA row). Velvet keeps\n// `1fr auto` so the content row absorbs the slack and\n// the CTA hugs the bottom of the tall band.\n//\n// --sc-card-cta-justify justify-self of the CTA row within its grid area.\n// Default `stretch` (full-width row spans the card).\n// Velvet sets `stretch` too but the area is only the\n// right column, so the CTA fills just that column.\n// Exposed as a token so a canvas can right-align a\n// compact CTA if it wants.\n//\n// Scoped flip mechanics owned by the card itself (the FAQ-widget precedent).\n// The prior both-faces approach leaked the variant panel onto non-velvet\n// surfaces because hiding depended on velvet's stylesheet; this stylesheet\n// travels with the widget, so the 3D context + backface hiding are correct on\n// every surface (velvet, chat, PDP). Faces are grid-stacked in one cell so the\n// container self-sizes to the tallest face \u2014 no fixed height needed inside\n// velvet's clipped deck band. Timing reads velvet's --vc-transition-* tokens\n// with a sane fallback so the flip works uncustomized.\n// Right-edge fade mask for the spec-chip scroll tracks. A chip row that\n// overflows the card width becomes a horizontally-scrollable track; the mask\n// fades the trailing edge so overflow reads as \"more, scrollable\" rather than a\n// hard slice. Mirrors PdpOtherProductsCarouselLit's carousel mask. The left\n// edge is left solid (chips start flush) \u2014 only the RIGHT edge fades since chip\n// rows only ever overflow to the right.\nconst SPEC_CHIP_FADE_MASK =\n 'linear-gradient(90deg, black 0, black calc(100% - 18px), transparent 100%)';\n\nconst CARD_FLIP_CSS = `\n /* Hide the scrollbar chrome on the declared, scrollable spec-chip tracks\n (data-clip-ok scroll containers) while keeping them scrollable. The inline\n styles set scrollbar-width:none (Firefox); WebKit needs this pseudo. */\n .sc-product-card [data-spec-chip-scroll]::-webkit-scrollbar { display: none; }\n\n /* Perspective ONLY while the flip container exists \u2014 a static perspective on\n the RESTING card promotes all its text onto composited GPU layers, which\n rasterizes titles soft/fuzzy at fractional positions (user-visible blur).\n :has() is supported across our targets; when absent (very old engines) the\n flip simply loses depth, never correctness. */\n .sc-product-card:has([data-card-flip]) { perspective: 1200px; }\n .sc-product-card [data-card-flip] {\n display: grid;\n transform-style: preserve-3d;\n transform: rotateY(180deg);\n animation: sc-card-flip-in var(--vc-transition-duration, 320ms)\n var(--vc-transition-easing, cubic-bezier(0.22, 1, 0.36, 1));\n }\n .sc-product-card [data-card-flip] > [data-face] {\n grid-area: 1 / 1;\n backface-visibility: hidden;\n -webkit-backface-visibility: hidden;\n }\n .sc-product-card [data-card-flip] > [data-face=\"back\"] { transform: rotateY(180deg); }\n @keyframes sc-card-flip-in {\n from { transform: rotateY(0deg); }\n to { transform: rotateY(180deg); }\n }\n @media (prefers-reduced-motion: reduce) {\n .sc-product-card [data-card-flip] { animation: none; }\n }\n`;\n\nexport class ProductCardLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n _device: { state: true },\n };\n\n static readonly _testing = {\n captureTelemetry: _captureTelemetry,\n publishEvent: _publishEvent,\n readDevice: _readDevice,\n synWindow,\n };\n\n props: CardProps | undefined = undefined;\n resolved: Record<string, { image?: string; price?: string }> = {};\n validationError = false;\n /** Device class \u2014 drives the effective density (mobile is forced compact). */\n _device: DeviceClass = 'desktop';\n\n _decisionState: CardState = createInitialCardState();\n\n #effectExecutor = createCardEffectExecutor({\n dispatch: (event) => this.#dispatchCard(event),\n publishEvent: _publishEvent,\n captureTelemetry: _captureTelemetry,\n closeSdk: () => synWindow().SynOS?.handle?.close?.(),\n bindProduct: () => this.#startBind(),\n reportRenderHealth: () => this.#scheduleHealthCheck(),\n cleanup: () => this.#cleanupHostResources(),\n schedule: (callback, delayMs) => setTimeout(callback, delayMs),\n cancelScheduled: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),\n });\n\n #controller: BindController | undefined;\n #unsubDevice: (() => void) | undefined;\n /** Fingerprint of the last reported schema-validation failure (sorted\n * invalid paths). Guards `render_failed` telemetry against re-render spam:\n * the same bad props re-rendering must not re-emit. Reset on a successful\n * parse so a later DIFFERENT failure still reports. */\n #lastFailureFingerprint: string | null = null;\n /** Set of render-health \"states\" already measured, so a self-detection event\n * fires AT MOST ONCE per instance per state (density \u00D7 face \u00D7 velvet \u00D7\n * product). Prevents event spam across re-renders of the same visual state. */\n #reportedHealthStates = new Set<string>();\n /** Pending rAF/timeout handle for the render-health measurement, cleared on\n * the next scheduled pass or on disconnect. */\n #healthRaf: number | undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n this.#dispatchCard({ type: 'reconnected' });\n this.#dispatchCard({\n type: 'commerce-updated',\n available: getProductPlatformAdapter() !== null,\n });\n ensureStylesInRoot(this, 'syntro-product-card-flip-style', CARD_FLIP_CSS);\n // Anchor `color` on the host so descendants inherit from this\n // widget's own theme token, not from the customer's\n // `body { color: ... }` rule. Light DOM is intentional (so the\n // host's brand fonts, layout, and spacing tokens cascade in \u2014\n // see the matching comment on `SyntroPdpLit.createRenderRoot`),\n // but foreground color is the WIDGET's responsibility, not the\n // host's: the card renders on its own `--sc-tile-background`\n // and the host body color was set for a totally different\n // background pair. Without this anchor, hosts that ship a dark\n // theme (`body { color: #eeebe8 }` on healthmaxxer) cascade pale\n // cream text into every descendant of this card \u2014 the host's\n // `--sc-tile-text-color` token was correct in spec but\n // never reached the text because the article's local `color`\n // rule didn't anchor to the host element. Setting it here\n // re-roots inheritance for every text node below.\n //\n // NOTE: Only the product canvas-tile widgets (Card, Comparison,\n // Grid, Hero) need this. The PDP widgets (PdpRibbon, PdpQa,\n // PdpPeerFeed, etc.) render on the host's PDP page where the\n // host's body color IS the intended contrast pair \u2014 leave those\n // alone.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n\n // Device-aware density: a mobile card surfaces LESS (compact) so it keeps a\n // smaller vertical profile and can't bully the chat; desktop keeps the config\n // density (room to breathe). Reactive \u2014 re-densifies if the surface changes.\n this._device = _readDevice();\n const ctx = synWindow().SynOS?.runtime?.context;\n this.#unsubDevice = ctx?.subscribe?.((c) => {\n const d = c?.surface?.device;\n const next: DeviceClass = d === 'mobile' || d === 'tablet' ? d : 'desktop';\n if (next !== this._device) this._device = next;\n });\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#dispatchCard({\n type: 'commerce-updated',\n available: getProductPlatformAdapter() !== null,\n });\n const observation = this.#readProductObservation();\n this.#dispatchCard({ type: 'props-replaced', ...observation });\n\n if (!observation.valid) this.#startBind();\n }\n }\n\n override updated(): void {\n // Schedule a runtime render-health measurement after the paint settles.\n // Only meaningful on the FRONT face with valid props (the resting card the\n // visitor sees); a validation error already fires render_failed.\n if (this.validationError || !this.props || this._decisionState.face !== 'front') return;\n this.#scheduleHealthCheck();\n }\n\n /** Schedule the self-detection pass on the next frame (+ a small timeout so\n * lazy images and web fonts have a chance to settle). rAF-then-timeout is the\n * cheapest way to run AFTER the browser has laid out and painted. */\n #scheduleHealthCheck(): void {\n if (typeof window === 'undefined') return;\n if (this.#healthRaf !== undefined) return; // one pending pass is enough\n const raf =\n typeof window.requestAnimationFrame === 'function'\n ? window.requestAnimationFrame.bind(window)\n : (cb: FrameRequestCallback) => window.setTimeout(() => cb(0), 0);\n this.#healthRaf = raf(() => {\n window.setTimeout(() => {\n this.#healthRaf = undefined;\n this.#measureAndReportHealth();\n }, 0);\n }) as unknown as number;\n }\n\n /**\n * Measure this card's render health and emit a self-detection event at most\n * ONCE per instance per visual state. Wrapped end-to-end in try/catch \u2014 a\n * measurement failure must NEVER crash the widget or the host page.\n */\n #measureAndReportHealth(): void {\n try {\n const card = this.querySelector('.sc-product-card');\n if (!card) return;\n const parsed = this.props ? cardSchema.safeParse(this.props) : null;\n if (!parsed?.success) return;\n const density = this._device === 'mobile' ? 'compact' : parsed.data.density;\n // Velvet sets the card layout tokens; detect it structurally from a token\n // the canvas sets (--sc-card-secondary-display:none) so we don't couple to\n // a canvas import. Best-effort NON-PII context only.\n const velvet =\n getComputedStyle(card).getPropertyValue('--sc-card-secondary-display').trim() === 'none';\n // De-dupe key: one report per (product \u00D7 density \u00D7 velvet) visual state.\n const stateKey = `${parsed.data.product.id}|${density}|${velvet ? 'v' : 'n'}`;\n if (this.#reportedHealthStates.has(stateKey)) return;\n // Mark consumed BEFORE reporting so a clean render isn't re-measured every\n // frame and a finding never double-fires for the same visual state.\n this.#reportedHealthStates.add(stateKey);\n // Measure + emit through the shared reporter: `app:product_card:blank` /\n // `:clipped` with { density, velvet, widget_kind } context (and, on a\n // Tier-1 identity truncation, reason:'identity-truncated').\n reportRenderHealth(card, {\n category: 'product_card',\n widgetKind: 'adaptive-product:card',\n context: { density, velvet },\n });\n } catch {\n // Self-detection must never crash the widget.\n }\n }\n\n override disconnectedCallback(): void {\n this.#dispatchCard({ type: 'disconnected' });\n super.disconnectedCallback();\n }\n\n #cleanupHostResources(): void {\n this.#controller?.destroy();\n this.#unsubDevice?.();\n this.#unsubDevice = undefined;\n if (this.#healthRaf !== undefined && typeof window !== 'undefined') {\n if (typeof window.cancelAnimationFrame === 'function') {\n window.cancelAnimationFrame(this.#healthRaf);\n }\n this.#healthRaf = undefined;\n }\n }\n\n #dispatchCard(event: CardEvent): void {\n const previousState = this._decisionState;\n const transition = transitionCard(this._decisionState, event);\n this._decisionState = transition.state;\n if (this.isConnected && transition.state !== previousState) this.requestUpdate();\n for (const effect of transition.effects) void this.#effectExecutor.execute(effect);\n }\n\n /** animationend on the flip container \u2192 the turn is done, render flat. */\n #onFlipAnimationEnd = (e: AnimationEvent): void => {\n if (e.animationName === 'sc-card-flip-in') this.#dispatchCard({ type: 'animation-settled' });\n };\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n return;\n }\n const result = cardSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n console.warn('[adaptive-product] invalid card props:', result.error.issues);\n // The card is about to render BLANK (render() returns html`` on\n // validationError). Without a signal that's an invisible failure \u2014 the\n // visitor sees an empty space and nothing downstream knows. Emit the\n // authoritative \"did not render\" event through emit() \u2014 the ONLY\n // consent-handled transport that actually reaches PostHog (publish()\n // only notifies in-process subscribers). Fire once per distinct failure\n // so a re-render with the same bad props doesn't spam the pipeline.\n const invalidPaths = _invalidPathsFromIssues(result.error.issues);\n const fingerprint = invalidPaths.join('|');\n if (fingerprint !== this.#lastFailureFingerprint) {\n this.#lastFailureFingerprint = fingerprint;\n _captureTelemetry(RENDER_FAILED_EVENT, {\n invalid_paths: invalidPaths.join(','),\n // Best-effort non-PII context \u2014 only set if the bad props happen to\n // carry an id; never any free-text/customer content.\n product_id:\n typeof (this.props as { product?: { id?: unknown } })?.product?.id === 'string'\n ? (this.props as { product: { id: string } }).product.id\n : null,\n widget_kind: 'adaptive-product:card',\n });\n }\n return;\n }\n this.#lastFailureFingerprint = null;\n this.validationError = false;\n const parsed = result.data;\n this.#controller = new BindController([parsed.product], (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n private _onVariantBack = (): void => {\n this.#syncDecisionInputsBeforeIntent();\n this.#dispatchCard({ type: 'back-activated' });\n };\n\n private _onVariantSelectionChange = (e: CustomEvent): void => {\n this.#syncDecisionInputsBeforeIntent();\n const detail = e.detail as { attribute_key: string; value_id: string };\n this.#dispatchCard({\n type: 'selection-changed',\n attributeKey: detail.attribute_key,\n valueId: detail.value_id,\n });\n };\n\n private _onVariantAdd = (e: CustomEvent): void => {\n this.#syncDecisionInputsBeforeIntent();\n const detail = e.detail as { variant: Variant };\n this.#dispatchCard({ type: 'variant-add-activated', variantId: detail.variant.id });\n };\n\n private _onVariantRetry = (): void => {\n this.#syncDecisionInputsBeforeIntent();\n this.#dispatchCard({ type: 'retry-activated' });\n };\n\n #readProductObservation(): {\n productId: string | null;\n valid: boolean;\n } {\n const parsed = this.props ? cardSchema.safeParse(this.props) : null;\n const rawProductId = (this.props as { product?: { id?: unknown } } | undefined)?.product?.id;\n return {\n productId: parsed?.success\n ? parsed.data.product.id\n : typeof rawProductId === 'string'\n ? rawProductId\n : null,\n valid: parsed?.success === true,\n };\n }\n\n #syncDecisionInputsBeforeIntent(): void {\n this.#dispatchCard({\n type: 'commerce-updated',\n available: getProductPlatformAdapter() !== null,\n });\n const observation = this.#readProductObservation();\n const product = this._decisionState.product;\n const isCurrent =\n observation.valid === (product.status === 'valid') && observation.productId === product.id;\n if (!isCurrent) this.#dispatchCard({ type: 'props-replaced', ...observation });\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.props) return html``;\n const parsed = cardSchema.safeParse(this.props);\n if (!parsed.success) return html``;\n // Mobile forces compact (smaller vertical profile, less content); desktop/tablet\n // keep the config-authored density.\n const density = this._device === 'mobile' ? 'compact' : parsed.data.density;\n const { product, visibleFacts } = parsed.data;\n const resolved = this.resolved[product.id];\n const view = selectCardView(this._decisionState);\n\n const articleStyles = _buildArticleStyles(density, view.face);\n\n // The decision core resolves view-cart, direct-add, and variant-picker\n // behavior from the current state; this shell only supplies user intent.\n const onPrimaryCtaClick = (e: Event): void => {\n e.preventDefault();\n this.#syncDecisionInputsBeforeIntent();\n const reducedMotion =\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n this.#dispatchCard({ type: 'primary-activated', reducedMotion });\n };\n\n // Build a version of ctas with the primary CTA label morphed when addedToCart\n const ctasWithMorphedLabel = product.ctas.map((c, i) => {\n if (i === 0 && view.primaryCta.mode === 'view-cart') {\n return { ...c, label: 'View cart \u2192' };\n }\n return c;\n });\n\n const frontContent = renderProductCardInner(\n product,\n density,\n resolved,\n visibleFacts,\n ctasWithMorphedLabel,\n onPrimaryCtaClick,\n !view.primaryCta.disabled\n );\n\n // The back-face variant picker markup, shared by the animating and settled\n // back states (only the wrapping container differs).\n const backFace = html`<syntro-variant-panel\n .payload=${view.panel.payload}\n .productTitle=${product.name ?? ''}\n .productImage=${product.image ? { src: product.image.src, alt: product.image.alt } : null}\n .state=${\n view.panel.status === 'error'\n ? 'error'\n : view.panel.status === 'ready'\n ? 'idle'\n : 'loading'\n }\n .error=${view.panel.error}\n @variant-panel-back=${this._onVariantBack}\n @variant-panel-add=${this._onVariantAdd}\n @variant-panel-retry=${this._onVariantRetry}\n @variant-panel-selection-change=${this._onVariantSelectionChange}\n ></syntro-variant-panel>\n ${view.addError ? html`<div data-add-to-cart-error>${view.addError}</div>` : nothing}`;\n\n // Render ONLY the active face. The back face (variant picker) must never be\n // present on the resting card \u2014 it appears solely after Add to cart flips to\n // 'back'. Rendering both faces and relying on a host canvas stylesheet to\n // hide the back (velvet's `[data-active-face]` rotate rules) leaked the\n // variant panel onto the resting card in every non-velvet surface (chat,\n // PDP). A single-face swap is self-contained and correct everywhere.\n //\n // The 3D flip container (perspective + rotateY, a composited layer) exists\n // ONLY while `view.flipAnimating` \u2014 i.e. for the duration of the turn. Once the\n // flip settles it is torn down and the back face renders FLAT: no\n // perspective (the `.sc-product-card:has([data-card-flip])` rule no longer\n // matches), no rotateY. A face left on a transformed/composited layer\n // rasterizes its text with grayscale AA (soft/fuzzy); the settled-flat face\n // renders subpixel-sharp.\n const backView =\n view.face === 'back' && view.flipAnimating\n ? // Mid-flip: both faces coexist inside a preserve-3d container. The\n // front turns away (inert + aria-hidden) while the back turns in.\n html`<div data-card-flip @animationend=${this.#onFlipAnimationEnd}>\n <div data-face=\"front\" aria-hidden=\"true\" inert>${frontContent}</div>\n <div data-face=\"back\">${backFace}</div>\n </div>`\n : view.face === 'back'\n ? // Settled back: flat, untransformed \u2014 no flip container, no perspective.\n html`<div data-face=\"back\">${backFace}</div>`\n : html`<div data-face=\"front\">\n ${frontContent}\n ${view.addError ? html`<div data-add-to-cart-error>${view.addError}</div>` : nothing}\n </div>`;\n\n return html`\n <article\n class=\"sc-product-card\"\n data-active-face=${view.face}\n data-density=${density}\n data-labels=${product.labels ? JSON.stringify(product.labels) : nothing}\n style=${styleMap(articleStyles)}\n >\n ${backView}\n </article>\n `;\n }\n}\n\nconst AVAILABILITY_LABELS: Record<NonNullable<Product['availability']>, string> = {\n in_stock: 'In stock',\n preorder: 'Preorder',\n backorder: 'Backorder',\n out_of_stock: 'Out of stock',\n};\n\ninterface SpecGroup {\n section: string;\n rows: Array<{ name: string; value: string; unit?: string; emphasis: boolean }>;\n}\n\n// Merge v2 specs[] with legacy v1 attributes[] (shimmed as \"About\" section).\n// Order: specs sections in author order, then \"About\" appended for any\n// legacy attributes present. New configs author specs[] directly; old\n// configs keep working until migrated.\nfunction buildSpecGroups(product: Product): SpecGroup[] {\n const groups = new Map<string, SpecGroup>();\n for (const spec of product.specs ?? []) {\n let group = groups.get(spec.section);\n if (!group) {\n group = { section: spec.section, rows: [] };\n groups.set(spec.section, group);\n }\n group.rows.push({\n name: spec.name,\n value: spec.value,\n unit: spec.unit,\n emphasis: spec.emphasis ?? false,\n });\n }\n if (product.attributes && product.attributes.length > 0) {\n const about: SpecGroup = groups.get('About') ?? { section: 'About', rows: [] };\n for (const attr of product.attributes) {\n about.rows.push({\n name: attr.label,\n value: attr.value,\n emphasis: attr.emphasis ?? false,\n });\n }\n if (!groups.has('About')) groups.set('About', about);\n }\n return Array.from(groups.values());\n}\n\n/** Build the article-level inline styles (shared by class render and standalone renderProductCard). */\nfunction _buildArticleStyles(density: 'compact' | 'standard', face: 'front' | 'back' = 'front') {\n return {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: density === 'compact' ? '0.5rem' : '0.625rem',\n // Inner padding so the card content doesn't hug the tile chrome.\n // When mounted chromeless (e.g. velvet deck), the article IS the\n // visible card; without this it reads as flush-edge. Standard\n // density gets slightly more breathing room.\n //\n // On the BACK face the variant panel supplies its own padding\n // (--sc-variant-pad); keeping the article padding too would double it and\n // eat the short deck band's vertical budget (risking a clipped CTA). Zero\n // it on the back so the panel owns the full band.\n padding: face === 'back' ? '0' : density === 'compact' ? '0.875rem' : '1rem',\n color: 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))',\n // \u2500\u2500 Root overflow invariant (item c) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The article IS the visible card. These three make it structurally\n // impossible for ANY descendant to paint past the card edge, no matter\n // what LLM-authored content lands inside:\n // box-sizing \u2192 padding counts INSIDE the width, so the card never\n // exceeds its allotted track by its own padding.\n // max-width \u2192 the card can never be wider than its container (the\n // deck-card / chat column / grid cell it's mounted in).\n // min-width:0 \u2192 as a flex/grid child, the card can SHRINK below its\n // content's intrinsic min size instead of forcing its\n // parent wider.\n // overflow \u2192 last-resort clip. Combined with the per-region\n // min-width:0 + terminal wrap fallbacks below, content\n // ADAPTS (wraps/shrinks) before this ever engages \u2014 but\n // if some future content still escapes, it's contained,\n // not spilled across the host page. Safe with the 3D\n // flip: the faces Y-rotate, so their horizontal footprint\n // never exceeds the card box at any rotation angle.\n boxSizing: 'border-box' as const,\n maxWidth: '100%',\n minWidth: '0',\n overflow: 'hidden',\n };\n}\n\n/**\n * Render the inner content of a product card (header row + body + CTAs) without\n * the article wrapper. Used by ProductCardLit.render() for the front face.\n */\nfunction renderProductCardInner(\n product: Product,\n density: 'compact' | 'standard',\n resolved: { image?: string; price?: string } | undefined,\n visibleFacts: readonly VisibleFactKey[] | undefined,\n ctas: Product['ctas'],\n onPrimaryCtaClick?: ((e: Event) => void | Promise<void>) | undefined,\n // When no platform commerce adapter is available (e.g. unknown platform / lite\n // bundle), the add-to-cart CTA renders disabled rather than silently no-opping.\n commerceAvailable = true\n): TemplateResult {\n const imgSrc = resolved?.image ?? product.image.src;\n const showFraming = density === 'standard' && product.framing;\n const specGroups = buildSpecGroups(product);\n // When visibleFacts is omitted, every populated block renders in the\n // default author order (back-compat). When set, only listed keys\n // render and the list's order is the render order.\n // CORE facts are the card's contract (user-drawn spec: name+rating /\n // tagline / price+availability / category / CTAs) \u2014 they render whenever\n // the DATA exists, regardless of the LLM's visibleFacts curation. Live\n // failure this prevents: a mount with visibleFacts:[\"tagline\",\"specs\"]\n // stripped rating+price and shipped a bare name-and-buttons card.\n // visibleFacts curates only the OPTIONAL extras (specs, framing).\n const CORE_FACTS: readonly VisibleFactKey[] = [\n 'rating',\n 'tagline',\n 'price',\n 'availability',\n 'badge',\n ] as VisibleFactKey[];\n const isVisible = (key: VisibleFactKey): boolean =>\n CORE_FACTS.includes(key) || (visibleFacts === undefined ? true : visibleFacts.includes(key));\n const factOrder: readonly VisibleFactKey[] = Array.from(\n new Set([...CORE_FACTS, ...(visibleFacts ?? VISIBLE_FACT_KEYS)])\n );\n\n // When salePrice is set, it becomes the active amount and `price` strikes through.\n // Bind-resolved price always wins for the active amount (live discount feeds).\n const hasSalePrice = product.salePrice !== undefined;\n const activePriceText = resolved?.price ?? product.salePrice?.amount ?? product.price?.amount;\n const activeCadence = hasSalePrice ? product.salePrice?.cadence : product.price?.cadence;\n\n // LAYOUT is token-driven (see the --sc-card-* comment block at the top of\n // this file). The template ALWAYS renders an image | content grid; the tokens\n // decide whether that reads as a clean STACKED card (neutral default: small\n // top-aligned header-row thumb beside the content) or velvet's HORIZONTAL\n // two-column card (image fills a tall left column). The card ships neutral\n // defaults; velvet sets the horizontal values on its deck-card rule.\n //\n // Neutral defaults reproduce the pre-redesign stacked structure: a density\n // thumb (72px compact / 110px standard) beside the content, CTAs in a\n // full-width row below (via the default --sc-card-areas). Velvet overrides\n // --sc-card-cols to a wider split, --sc-card-image-max/-h to 100%, and flips\n // --sc-card-areas so the image area spans both rows (a full-height left\n // feature column) and the CTA sits in the right column bottom.\n // Item 2 \u2014 COMPACT (mobile / velvet-deck) cards use an OVERLAY layout: the\n // \u2605rating and price move OUT of the content column and INTO the image's upper\n // corners as small pills, and the CTAs STACK vertically. That frees the\n // content column's bottom row on the space-constrained mobile card. Standard\n // density keeps the classic stacked card (rating in header, price in body,\n // horizontal CTA row). Velvet forces compact, so its deck card gets the\n // overlay treatment through this same path \u2014 the 132px image column + 3/4\n // aspect (\u2192 132\u00D7176) already come from velvet's --sc-card-* tokens; the\n // overlays position within whatever image box the tokens produce.\n const overlay = density === 'compact';\n // The compact image is a 132px feature column (132\u00D7176 at the default 3/4\n // aspect) so the overlay pills have a legible canvas. Standard keeps a 110px\n // thumbnail. Velvet overrides --sc-card-cols / -image-* regardless.\n const thumbPx = density === 'compact' ? 132 : 110;\n // `minmax(0, 1fr)` NOT `1fr`: a bare `1fr` track has an implicit `auto`\n // minimum, so a wide/unbreakable child (long name, 60-char token) inflates\n // the track past the card and the grid overflows. `minmax(0, \u2026)` lets the\n // content track shrink to zero, so it can never blow the card out. The image\n // track keeps its fixed thumb width (or velvet's token).\n const defaultCols = `${thumbPx}px minmax(0, 1fr)`;\n const defaultColGap = density === 'compact' ? '0.75rem' : '1rem';\n // Named grid areas make the CTA placement a LAYOUT-STRUCTURE token (see the\n // --sc-card-areas comment block). Neutral: CTA spans a full-width row below\n // image+content. Velvet: image spans both rows (feature column), CTA sits in\n // the right column bottom. The image/content/cta children reference the area\n // NAMES via var()-with-fallback so a canvas can override placement + areas\n // atomically.\n const defaultAreas = `\"image content\" \"cta cta\"`;\n const rootGridStyles = {\n display: 'grid',\n // Neutral: `72px 1fr` / `110px 1fr` (thumb | content). Velvet: `minmax(72px,30%) 1fr`.\n gridTemplateColumns: `var(--sc-card-cols, ${defaultCols})`,\n // Neutral: content row then CTA row (both auto). Velvet keeps `1fr auto` so\n // the content row absorbs slack and the CTA hugs the band bottom.\n gridTemplateRows: 'var(--sc-card-rows, auto auto)',\n gridTemplateAreas: `var(--sc-card-areas, ${defaultAreas})`,\n columnGap: `var(--sc-card-col-gap, ${defaultColGap})`,\n rowGap: density === 'compact' ? '0.5rem' : '0.625rem',\n // `start` keeps the neutral thumb top-aligned; velvet's full-height image\n // reads as `stretch` because it sets height:100% + grid-row 1/-1 itself.\n alignItems: 'start' as const,\n // The grid is itself a child of the (flex-column) face wrapper \u2014 keep it\n // shrinkable and capped so a velvet token override (`--sc-card-cols`) with\n // a bare `1fr` still can't push the grid past the card.\n minWidth: '0',\n maxWidth: '100%',\n };\n const contentColStyles = {\n gridArea: 'content',\n display: 'flex',\n flexDirection: 'column' as const,\n gap: density === 'compact' ? '0.4rem' : '0.45rem',\n minWidth: '0',\n };\n const titleColStyles = {\n display: 'flex',\n flexDirection: 'column' as const,\n gap: '0.2rem',\n minWidth: '0',\n };\n // The image is a rounded thumbnail by default and a full-height panel under\n // velvet. Every dimension that differs between the two layouts reads a token:\n // --sc-card-image-max width cap (default thumbPx; velvet 100%)\n // --sc-card-image-h height (default auto square-ish thumb; velvet 100%)\n // --sc-card-image-aspect aspect-ratio (default 1/1; velvet auto)\n // --sc-card-image-align align-self (default start; velvet stretch)\n // Which rows the image SPANS is the named `image` grid area (--sc-card-areas),\n // not a row token. object-fit:cover keeps the framing tidy in both modes.\n const imageStyles = {\n // The image lives in the `image` grid area (placement comes ENTIRELY from\n // the named area \u2014 no separate grid-row override, which would unset the\n // area's row span and collapse the image back into row 1). Neutral: the area\n // is row 1 left (`\"image content\" \"cta cta\"`), a top-aligned thumb. Velvet:\n // the area spans both rows (`\"image content\" \"image cta\"`), so the image is\n // a full-height feature column.\n gridArea: 'image',\n width: '100%',\n maxWidth: `var(--sc-card-image-max, ${thumbPx}px)`,\n // Neutral: `auto` \u2192 a square-ish thumb sized by max-width + aspect-ratio.\n // Velvet: `100%` \u2192 fill the spanned rows' height.\n height: 'var(--sc-card-image-h, auto)',\n // Neutral keeps a 1/1 thumb; velvet sets `auto` so height:100% + object-fit\n // cover win and the picture fills the tall column instead of forcing a\n // square row.\n aspectRatio: 'var(--sc-card-image-aspect, 1 / 1)',\n // A definite floor so `align-self: stretch` + `height:100%` resolve to the\n // grid area's full block size (velvet's full-height feature panel) rather\n // than the image's intrinsic size.\n minHeight: '0',\n // Grid child: allow the image column to shrink below its intrinsic size so\n // it never inflates its track and pushes the content column off-card.\n minWidth: '0',\n objectFit: 'cover' as const,\n borderRadius: 'var(--sc-radius-md, 10px)',\n display: 'block',\n // Neutral: top-aligned thumb (`start`). Velvet: `stretch` fills the area.\n alignSelf: 'var(--sc-card-image-align, start)' as const,\n };\n // \u2500\u2500 Item 2 overlay media wrapper + pills (COMPACT only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // The image lives inside a position:relative wrapper so the rating/price\n // pills can anchor to its corners. The wrapper carries the `image` grid area\n // + sizing tokens; the image fills it. On standard density the raw <img>\n // renders directly (no wrapper) so nothing changes there.\n const mediaWrapperStyles = {\n gridArea: 'image',\n position: 'relative' as const,\n width: '100%',\n maxWidth: `var(--sc-card-image-max, ${thumbPx}px)`,\n height: 'var(--sc-card-image-h, auto)',\n // 132 \u00D7 (4/3) = 176 \u2192 the spec's 132\u00D7176 portrait. Velvet's token wins.\n aspectRatio: 'var(--sc-card-image-aspect, 3 / 4)',\n alignSelf: 'var(--sc-card-image-align, start)',\n minWidth: '0',\n minHeight: '0',\n };\n const overlayImgStyles = {\n width: '100%',\n height: '100%',\n objectFit: 'cover' as const,\n borderRadius: 'var(--sc-radius-md, 10px)',\n display: 'block',\n minWidth: '0',\n minHeight: '0',\n };\n // Small legible pill over the image. Solid translucent fill (NO\n // backdrop-filter \u2014 the compositing contract bans filter/backdrop-filter on\n // text-bearing surfaces). white-space:normal + overflow-wrap:anywhere +\n // a bounded max-width mean the pill WRAPS within the image rather than\n // overflowing the card, so even a pathological price never escapes the\n // article (keeps the clip battery green).\n const overlayPillBase = {\n position: 'absolute' as const,\n top: '8px',\n display: 'inline-flex',\n alignItems: 'center',\n background: 'rgba(17, 17, 17, 0.66)',\n color: '#ffffff',\n borderRadius: '9999px',\n padding: '0.18rem 0.5rem',\n fontSize: '0.72rem',\n lineHeight: '1.25',\n fontWeight: '600',\n boxShadow: '0 1px 3px rgba(0, 0, 0, 0.25)',\n maxWidth: 'calc(100% - 16px)',\n minWidth: '0',\n boxSizing: 'border-box' as const,\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n };\n const overlayRatingStyles = { ...overlayPillBase, left: '8px' };\n const overlayPriceStyles = { ...overlayPillBase, right: '8px' };\n // Inline-styled price block \u2014 strikethrough on original, larger sale amount,\n // small cadence suffix. Keeps the card readable without host CSS.\n const priceBlockStyles = {\n display: 'flex',\n flexDirection: 'row',\n alignItems: 'baseline',\n flexWrap: 'wrap' as const,\n gap: '0.4rem',\n minWidth: '0',\n };\n // Opacity dim was tuned for solid-white hosts. Against a translucent\n // host (velvet's cream backdrop-filter), 0.6 reads as illegible \u2014 bumped\n // to 0.78. Same for every other opacity-based dim in this file: each\n // value's floor is \"still legible on a 0.85-opacity surface.\"\n // Terminal wrap fallback (item b): a long, comma-joined price token\n // (\"$1,299.99\") has no break opportunity, so `overflow-wrap: anywhere` is the\n // last-resort policy that lets it break mid-token rather than overflow the\n // (possibly very narrow) content column. The sale amount is Tier-1 identity\n // text \u2014 it must never be ellipsized; wrapping keeps it fully readable.\n const priceOriginalStyles = {\n textDecoration: 'line-through',\n opacity: '0.78',\n fontSize: '0.9em',\n overflowWrap: 'anywhere' as const,\n };\n const priceSaleStyles = {\n fontWeight: '600',\n fontSize: '1.05em',\n color: 'var(--sc-tile-title-color, inherit)',\n minWidth: '0',\n overflowWrap: 'anywhere' as const,\n };\n const priceCadenceStyles = {\n fontSize: '0.82em',\n opacity: '0.82',\n overflowWrap: 'anywhere' as const,\n };\n // \u2500\u2500 Pill vocabulary (from VariantPanelLit) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Spec chips fuse name + value into one pill (e.g. \"Lion's Mane 1000mg\").\n // 9999px radius, 1px --sc-color-border hairline, 13px/600, tight 6px 14px\n // paddings at standard; slightly tighter at compact to fit the deck band.\n // Chip-row flow is token-driven. By default chips WRAP FREELY \u2014 a chat/PDP\n // card has room for a multi-row chip cloud. A tight canvas (velvet) sets\n // --sc-card-chip-wrap: nowrap for a SINGLE line.\n //\n // A single nowrap line WILL overflow the card width for long chips\n // (\"Bacopa320mg\"). A hard `overflow: hidden` clip was BANNED \u2014 a chip sliced\n // mid-word reads as breakage, and the content becomes unreachable. Instead the\n // nowrap row is a horizontally SCROLLABLE track (overflow-x: auto) with hidden\n // scrollbars and a right-edge FADE MASK (same pattern as\n // PdpOtherProductsCarouselLit): the overflow is declared, reachable by scroll,\n // and visually hinted by the fade. The container carries `data-clip-ok`\n // because its clipping is a *declared, scrollable* affordance \u2014 not a silent\n // hard-clip. On the WRAP default (chat/PDP roomy card) there is no single-row\n // overflow, so the mask/scroll are inert and it reads as a plain chip cloud.\n const specChipRowStyles = {\n display: 'flex',\n flexWrap: 'var(--sc-card-chip-wrap, wrap)' as never,\n gap: '0.35rem',\n // Scrollable when nowrap overflows; wrap mode never overflows so this is\n // inert there. `hidden` on the cross axis keeps a wrapped cloud from\n // scrolling vertically.\n overflowX: 'auto' as const,\n overflowY: 'hidden' as const,\n // Flex child: min-width:0 so the (scrollable) track shrinks with the card\n // instead of forcing it wider \u2014 the scroll/fade is the affordance, but the\n // ROW must never dictate the card width.\n minWidth: '0',\n // Hide the scrollbar chrome (Firefox + WebKit handled via the shared CSS\n // rule injected in connectedCallback for [data-clip-ok-scroll]).\n scrollbarWidth: 'none' as const,\n // Right-edge fade to hint at more chips beyond the card edge.\n maskImage: SPEC_CHIP_FADE_MASK,\n WebkitMaskImage: SPEC_CHIP_FADE_MASK,\n };\n const specChipStyles = {\n display: 'inline-flex',\n alignItems: 'baseline',\n gap: density === 'compact' ? '0.28rem' : '0.22rem',\n border: '1px solid var(--sc-color-border, rgba(0,0,0,0.14))',\n background: 'var(--sc-color-surface, transparent)',\n color: 'var(--sc-tile-title-color, inherit)',\n borderRadius: '9999px',\n padding: density === 'compact' ? '0.2rem 0.6rem' : '0.18rem 0.55rem',\n fontSize: density === 'compact' ? '0.72rem' : '0.7rem',\n lineHeight: '1.3',\n whiteSpace: 'nowrap' as const,\n flexShrink: '0',\n };\n // Tiny section label (11px/600, letter-spacing 0.01em, title color @0.85).\n const specHeadingStyles = {\n fontSize: '11px',\n letterSpacing: '0.01em',\n color: 'var(--sc-tile-title-color, inherit)',\n opacity: '0.85',\n margin: '0 0 0.3rem',\n fontWeight: '600',\n };\n // Standard-density spec table keeps a structured layout, but the row values\n // read in the pill language's palette (title color for names/values).\n const specGroupStyles = {\n display: 'flex',\n flexDirection: 'column',\n gap: '0.25rem',\n minWidth: '0',\n };\n // Standard chips lay out in a 2-column grid so N specs collapse to\n // ceil(N/2) rows \u2014 the two-rows-of-chips the horizontal-first design calls\n // for. A flex-wrap row would stack one-per-line when a chip is wider than\n // half the content column, making a 3-spec card too tall for the bounded\n // deck band. The image is narrow (30%) at standard specifically so each grid\n // cell is wide enough to hold a full \"Lion's Mane 1000mg\" chip WITHOUT\n // ellipsis. justify-items:start keeps chips their natural width (left-packed),\n // not stretched edge-to-edge.\n const specRowsStyles = {\n listStyle: 'none',\n margin: '0',\n padding: '0',\n display: 'grid',\n // Content-sized columns: each column is exactly as wide as its widest chip,\n // so chip text is NEVER clipped mid-word. Two chips per row keeps the card\n // short (ceil(N/2) rows) for the bounded deck band.\n gridTemplateColumns: 'max-content max-content',\n justifyContent: 'start' as const,\n gap: '0.3rem 0.35rem',\n // A `max-content` grid can grow wider than the content column (\"Alpha-GPC\n // 300 mg\" chips), and a hard `overflow: hidden` was slicing those chips\n // mid-word (the neutral standard card's spec rows escaped 8-94px). Replace\n // the hard clip with a declared, scrollable, fade-masked track \u2014 reachable\n // and hinted, never sliced. When the rows fit (roomy content column) the\n // scroll/mask are inert.\n overflowX: 'auto' as const,\n overflowY: 'hidden' as const,\n // Flex/grid child: shrinkable so the max-content chip grid never inflates\n // the content column past the card.\n minWidth: '0',\n scrollbarWidth: 'none' as const,\n maskImage: SPEC_CHIP_FADE_MASK,\n WebkitMaskImage: SPEC_CHIP_FADE_MASK,\n };\n const specValueStyles = {\n fontVariantNumeric: 'tabular-nums' as const,\n };\n const specsContainerStyles = {\n display: 'flex',\n flexDirection: 'column',\n gap: '0.5rem',\n minWidth: '0',\n };\n const availabilityStyles = {\n // Canvas-ownable display: a height-bounded band (velvet's deck card) can't\n // afford a variable fact row without clipping the CTA. Default keeps the\n // row visible on roomy surfaces (chat / PDP); velvet sets\n // --sc-card-availability-display:none so the fixed deck budget stays\n // reserved for identity/tagline/price/chips/CTA.\n display: 'var(--sc-card-availability-display, inline-block)' as never,\n fontSize: '0.8rem',\n opacity: '0.85',\n };\n // CTAs live in the `cta` grid area. Neutral: that area is a FULL-WIDTH row\n // below image+content, so \"Add to cart \u00B7 $49\" and \"Product Page \u203A\" sit on one\n // line spanning the card. Velvet: the area is the RIGHT column bottom (image\n // owns the full-height left), so the CTA row sits under the content \u2014 exactly\n // where the variant picker's CTA sits. flex-wrap lets the secondary link drop\n // below the primary pill when the (narrower) velvet right column can't hold\n // both on one line, instead of clipping.\n const ctasStyles = {\n gridArea: 'cta',\n justifySelf: 'var(--sc-card-cta-justify, stretch)',\n alignSelf: 'end' as const,\n display: 'flex',\n // Direction is canvas-tunable. Item 2: COMPACT (mobile) cards STACK the\n // CTAs vertically (Add to cart above Product Page \u203A) so the freed content\n // row isn't re-cramped by a horizontal button pair; standard keeps them on\n // one row. Velvet's --sc-card-cta-direction token still wins if set.\n flexDirection: `var(--sc-card-cta-direction, ${overlay ? 'column' : 'row'})` as never,\n // Stacked CTAs stretch to a consistent width; the row layout centers.\n alignItems: `var(--sc-card-cta-align, ${overlay ? 'stretch' : 'center'})` as never,\n flexWrap: 'var(--sc-card-cta-wrap, nowrap)' as never,\n justifyContent: 'space-between',\n gap: 'var(--sc-card-cta-gap, 0.5rem)',\n marginTop: '0',\n minWidth: '0',\n };\n // Primary CTA = panel-green pill with the LIVE price in the label\n // (Add to cart \u00B7 $49), mirroring the variant panel's price-in-CTA. Secondary\n // = a quiet ghost text link (\"Product Page \u203A\"). The reading order matches the\n // visual order: do the thing, or dig into details.\n const primaryCtaStyles = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n textAlign: 'center' as const,\n padding: density === 'compact' ? '0.5rem 1rem' : '0.55rem 1.15rem',\n borderRadius: '9999px',\n border: '1px solid var(--sc-color-primary, #3d8a5e)',\n background: 'var(--sc-color-primary, #3d8a5e)',\n color: 'var(--sc-color-on-primary, #ffffff)',\n fontWeight: '600',\n fontSize: '0.85rem',\n textDecoration: 'none',\n lineHeight: '1.15',\n // Item (b): the CTA label is Tier-1 identity text \u2014 ellipsis is BANNED.\n // A long label (\"Add to cart \u00B7 $1,299.99 per unit bundle\") must never\n // overflow its row. Instead of a nowrap pill that spills off the card, the\n // PILL itself shrinks (min-width:0, flex-shrink:1, max-width:100%) and its\n // label WRAPS to a second line (`white-space: normal` + `overflow-wrap:\n // anywhere`). The pill grows taller, never wider than the card.\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n minWidth: '0',\n maxWidth: '100%',\n flexShrink: '1',\n };\n const secondaryCtaStyles = {\n // Canvas-hideable: velvet's narrow right column can't fit the secondary\n // pill beside the primary without a mid-word clip, so it sets\n // --sc-card-secondary-display: none \u2014 the product page stays reachable via\n // the NAME link (see data-product-name-link).\n display: 'var(--sc-card-secondary-display, inline-flex)' as never,\n alignItems: 'center',\n justifyContent: 'center',\n gap: '0.15rem',\n padding: '0.4rem 0.5rem',\n borderRadius: '9999px',\n border: '0',\n background: 'transparent',\n color: 'var(--sc-color-primary, #3d8a5e)',\n fontWeight: '600',\n fontSize: '0.82rem',\n textDecoration: 'none',\n textAlign: 'center' as const,\n lineHeight: '1.15',\n // Same terminal policy as the primary pill: shrink + wrap, never overflow.\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n minWidth: '0',\n maxWidth: '100%',\n flexShrink: '1',\n };\n\n // Full-width CTA row (rendered below the image|content grid). Kept as its own\n // template so it can live OUTSIDE the two-column grid and span the whole card.\n const ctaRow = html`<div class=\"sc-product-card__ctas\" style=${styleMap(ctasStyles)}>\n ${ctas.map((c, i) => {\n // The add-to-cart CTA is disabled when no commerce adapter is present.\n const disabled = c.actionId === 'add_to_cart' && !commerceAvailable;\n const ctaStyles = {\n ...(i === 0 ? primaryCtaStyles : secondaryCtaStyles),\n ...(disabled ? { opacity: '0.5', 'pointer-events': 'none', cursor: 'not-allowed' } : {}),\n };\n // FRONT face keeps the CTA label CLEAN (\"Add to cart\" \u2014 no price): on\n // cards, Add-to-cart opens the VARIANT PICKER (it doesn't add anything\n // yet), so a price-in-label overpromises. The PICKER's CTA is the one\n // that commits, and it carries the live price there. (An earlier\n // price-in-label variant left dead isAddToCart/morphed locals here \u2014\n // removed.) The text stays a single text node so\n // [data-product-cta=\"primary\"].textContent still contains\n // \"Add to cart\" / \"View cart\" for the behavioral tests.\n const ctaLabel = c.label;\n return html`<a\n class=\"sc-product-card__cta\"\n data-product-cta=${i === 0 ? 'primary' : 'secondary'}\n data-critical-text\n data-variant=${c.variant}\n data-disabled=${disabled ? 'true' : nothing}\n aria-disabled=${disabled ? 'true' : nothing}\n href=${c.actionId === 'add_to_cart' ? '#' : (c.href ?? '#')}\n target=${c.target}\n rel=${c.target === '_blank' ? 'noopener noreferrer' : ''}\n data-action-id=${c.actionId ?? ''}\n style=${styleMap(ctaStyles)}\n @click=${\n disabled\n ? (e: Event) => {\n e.preventDefault();\n }\n : i === 0 && onPrimaryCtaClick\n ? onPrimaryCtaClick\n : c.actionId === 'add_to_cart'\n ? (e: Event) => {\n e.preventDefault();\n }\n : undefined\n }\n >${\n // Secondary \"Product Page\" link gets a trailing chevron (panel's\n // quiet-secondary affordance) unless the author already added one.\n i !== 0 && !/[\u203A>\u2192]\\s*$/.test(ctaLabel) ? `${ctaLabel} \u203A` : ctaLabel\n }</a\n >`;\n })}\n </div>`;\n\n // \u2500\u2500 Item 2 overlay pills (COMPACT only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Rating pill (upper-left) and price pill (upper-right) render INSIDE the\n // media wrapper on compact. They carry the SAME data-attributes as their\n // in-column counterparts so behavioral hooks/tests still resolve them; the\n // price amount stays [data-critical-text] (never ellipsized \u2014 it wraps).\n const ratingOverlay =\n overlay && product.rating && isVisible('rating')\n ? html`<div\n class=\"sc-product-card__rating sc-product-card__rating--overlay\"\n data-product-rating\n data-rating-value=${product.rating.value}\n data-rating-max=${product.rating.max ?? 5}\n aria-label=${`${product.rating.value} out of ${product.rating.max ?? 5}, ${product.rating.count} reviews`}\n style=${styleMap(overlayRatingStyles)}\n >\n <span style=\"color: #f5a623;\">\u2605</span\n ><span style=\"margin-left: 0.15rem; font-weight: 600;\">${product.rating.value}</span\n ><span\n class=\"sc-product-card__rating-count\"\n style=${styleMap({ opacity: '0.8', marginLeft: '0.15rem', display: 'var(--sc-card-rating-count-display, inline)' })}\n >(${product.rating.count.toLocaleString()})</span\n >\n </div>`\n : nothing;\n const priceOverlay =\n overlay && isVisible('price') && (product.price || product.salePrice)\n ? html`<div\n class=\"sc-product-card__price sc-product-card__price--overlay\"\n data-product-price\n style=${styleMap(overlayPriceStyles)}\n >\n <span\n class=\"sc-product-card__amount\"\n data-product-price-sale=${hasSalePrice ? 'true' : nothing}\n data-critical-text\n >${activePriceText}</span\n >\n </div>`\n : nothing;\n // On compact the image is wrapped so the pills can anchor to its corners;\n // on standard the raw <img> renders directly (unchanged).\n const mediaBlock = overlay\n ? html`<div class=\"sc-product-card__media\" style=${styleMap(mediaWrapperStyles)}>\n <img\n class=\"sc-product-card__image\"\n data-product-image\n src=${imgSrc}\n alt=${product.image.alt}\n loading=\"lazy\"\n @error=${onImageError}\n style=${styleMap(overlayImgStyles)}\n />\n ${ratingOverlay}${priceOverlay}\n </div>`\n : html`<img\n class=\"sc-product-card__image\"\n data-product-image\n src=${imgSrc}\n alt=${product.image.alt}\n loading=\"lazy\"\n @error=${onImageError}\n style=${styleMap(imageStyles)}\n />`;\n\n return html`\n <div class=\"sc-product-card__grid\" data-product-card-grid style=${styleMap(rootGridStyles)}>\n ${mediaBlock}\n <div class=\"sc-product-card__content\" style=${styleMap(contentColStyles)}>\n <header\n class=\"sc-product-card__header\"\n style=${styleMap(titleColStyles)}\n >\n <div\n class=\"sc-product-card__identity-row\"\n style=${styleMap({\n display: 'flex',\n alignItems: 'baseline',\n justifyContent: 'space-between',\n gap: '0.5rem',\n minWidth: '0',\n // WRAP so a nowrap rating pill can drop to its own line when the\n // column is too narrow to hold name + rating. Without this, at very\n // narrow widths (240px) the fixed-width rating crushes the name box\n // below a single glyph, and NO wrap policy can shrink one character \u2014\n // leaving a residual identity overflow. Wrapping frees the full\n // column width for the Tier-1 name.\n flexWrap: 'wrap' as const,\n rowGap: '0.15rem',\n })}\n >\n <h3\n class=\"sc-product-card__name\"\n data-product-name\n data-critical-text\n style=${styleMap({\n margin: '0',\n // Name size is a canvas-tunable token so a tight surface (velvet's\n // narrow right column) can shrink it to keep long names on 1-2\n // lines. Default keeps the prior density sizes.\n fontSize: `var(--sc-card-name-size, ${density === 'compact' ? '0.95rem' : '1.0625rem'})`,\n fontWeight: '700',\n lineHeight: '1.2',\n minWidth: '0',\n // IDENTITY text (Tier 1): the product NAME must render IN FULL \u2014\n // an ellipsized product title is a defect. So the name WRAPS at\n // word boundaries (up to as many lines as it needs) with NO\n // ellipsis and NO clip. We deliberately do NOT use\n // -webkit-line-clamp (it re-introduces an ellipsis) and do NOT cap\n // the height: an absurdly long name pushes layout and is DETECTED\n // as a CTA escape by the clip battery, rather than silently\n // truncated. `overflow-wrap: break-word` guarantees even a single\n // unbreakable token never overflows its column horizontally.\n // `anywhere` (not `break-word`): it wraps at word boundaries\n // FIRST and only breaks mid-word as a terminal fallback, but \u2014\n // unlike break-word \u2014 it also shrinks the box's min-content so a\n // word a few px wider than a narrow (240px) column can't leave a\n // residual horizontal overflow.\n display: 'block',\n whiteSpace: 'normal' as const,\n overflowWrap: 'anywhere' as const,\n })}\n >${\n // The NAME is a PDP link when a navigating CTA exists (same pattern\n // as the comparison's name-link) \u2014 so a canvas that hides the\n // secondary CTA pill (velvet: --sc-card-secondary-display: none, to\n // avoid mid-word clipping in the narrow right column) still keeps\n // the product page reachable.\n (() => {\n const navCta = ctas.find((c) => c.href);\n return navCta?.href\n ? html`<a\n data-product-name-link\n href=${navCta.href}\n target=${navCta.target ?? '_self'}\n rel=${navCta.target === '_blank' ? 'noopener noreferrer' : ''}\n style=\"color:inherit;text-decoration:none;cursor:pointer;display:block;min-width:0;white-space:normal;overflow-wrap:anywhere;\"\n >${product.name}</a>`\n : product.name;\n })()\n }</h3>\n ${\n // On compact the rating renders as an image OVERLAY pill (item 2),\n // so it is omitted from the header identity row here.\n !overlay && product.rating && isVisible('rating')\n ? html`<div\n class=\"sc-product-card__rating\"\n data-product-rating\n data-rating-value=${product.rating.value}\n data-rating-max=${product.rating.max ?? 5}\n aria-label=${`${product.rating.value} out of ${product.rating.max ?? 5}, ${product.rating.count} reviews`}\n style=${styleMap({\n fontSize: '0.78rem',\n whiteSpace: 'nowrap',\n flexShrink: '0',\n })}\n >\n <span class=\"sc-product-card__rating-stars\" style=\"color: #f5a623;\">\u2605</span>\n <span style=${styleMap({ marginLeft: '0.2rem', fontWeight: '600' })}>${product.rating.value}</span>\n <span class=\"sc-product-card__rating-count\" style=${styleMap({ opacity: '0.65', marginLeft: '0.2rem', display: 'var(--sc-card-rating-count-display, inline)' })}>(${product.rating.count.toLocaleString()})</span>\n </div>`\n : nothing\n }\n </div>\n ${\n (product.tagline || product.framing) && isVisible('tagline')\n ? html`<p\n class=\"sc-product-card__tagline\"\n data-product-tagline\n style=${styleMap({\n margin: '0',\n fontSize: '0.82rem',\n opacity: '0.72',\n lineHeight: '1.3',\n // The LLM-authored tagline WRAPS (up to 3 lines, then\n // ellipsis) \u2014 a clipped single line wastes the card's space\n // and hides the framing the agent wrote (2026-07-23).\n // Vendor-prefixed styleMap keys need a capital first letter:\n // lowercase `webkitLineClamp` emits `webkit-line-clamp` (no\n // leading dash) which the CSS parser DROPS, leaving the\n // element in legacy un-oriented -webkit-box layout \u2014 the\n // unwrapped-clipped-line bug this replaces.\n display: '-webkit-box',\n // The line COUNT is the host canvas's call \u2014 a height-bounded\n // band (velvet's 232-292px deck) can't afford 3 lines without\n // pushing the CTA rows to the clipped bottom edge (user\n // report 2026-07-24). Token bridge across sc-mount's shadow\n // root, same as --sc-comparison-max-fields.\n WebkitLineClamp: 'var(--sc-card-tagline-lines, 3)',\n WebkitBoxOrient: 'vertical' as const,\n overflow: 'hidden',\n // Terminal fallback (item b): a 60-char unbroken tagline token\n // has no break opportunity, so without this its min-content\n // width (as a flex child of the header) would force the header\n // wider than the card. min-width:0 + overflow-wrap:anywhere let\n // the line-clamp box shrink and the token break, so the ellipsis\n // clip stays WITHIN the card instead of blowing it out.\n minWidth: '0',\n overflowWrap: 'anywhere' as const,\n })}\n >${product.tagline || product.framing}</p>`\n : nothing\n }\n </header>\n <div\n class=\"sc-product-card__body\"\n data-product-body\n style=${styleMap({\n display: 'flex',\n flexDirection: 'column',\n gap: density === 'compact' ? '0.35rem' : '0.5rem',\n minWidth: '0',\n })}\n >\n ${factOrder.map((key) => {\n if (key === 'price') {\n // On compact the price renders as an image OVERLAY pill (item 2), so it\n // is omitted from the content body here to avoid a duplicate.\n if (overlay) return nothing;\n if (!isVisible('price') || (!product.price && !product.salePrice)) return nothing;\n return html`<div\n class=\"sc-product-card__price\"\n data-product-price\n style=${styleMap(priceBlockStyles)}\n >\n ${\n hasSalePrice && product.price\n ? html`<span\n class=\"sc-product-card__amount-original\"\n data-product-price-original\n style=${styleMap(priceOriginalStyles)}\n >${product.price.amount}</span\n >`\n : nothing\n }\n <span\n class=\"sc-product-card__amount\"\n data-product-price-sale=${hasSalePrice ? 'true' : nothing}\n data-critical-text\n style=${styleMap(priceSaleStyles)}\n >${activePriceText}</span\n >\n ${\n activeCadence\n ? html`<span\n class=\"sc-product-card__cadence\"\n style=${styleMap(priceCadenceStyles)}\n >${activeCadence}</span\n >`\n : nothing\n }\n </div>`;\n }\n if (key === 'availability') {\n if (!isVisible('availability') || !product.availability) return nothing;\n return html`<span\n class=\"sc-product-card__availability\"\n data-product-availability\n data-availability=${product.availability}\n style=${styleMap(availabilityStyles)}\n >${AVAILABILITY_LABELS[product.availability]}</span\n >`;\n }\n if (key === 'badge') {\n if (!isVisible('badge') || !product.badge) return nothing;\n return html`<span\n class=\"sc-product-card__badge\"\n data-product-badge\n data-tone=${product.badge.tone}\n style=${styleMap({\n // Canvas-ownable display: the badge often carries a category /\n // collection label (\"Running\") \u2014 a variable fact row the deck's\n // fixed band can't fit. Default shows it (chat / PDP); velvet sets\n // --sc-card-badge-display:none for the same band-budget reason as\n // availability above.\n display: 'var(--sc-card-badge-display, inline-block)' as never,\n })}\n >${product.badge.label}</span\n >`;\n }\n if (key === 'specs') {\n if (!isVisible('specs') || specGroups.length === 0) return nothing;\n // Compact density renders specs as a wrapping pill chip cloud. How MANY\n // chip rows show is a LAYOUT decision, not a data cap: the chip row's\n // max-height is token-driven (--sc-card-chip-max-h, default `none` =\n // wrap freely). A chat/PDP card shows the full cloud; velvet caps the\n // row to one line via the token + `overflow: hidden`. The adaptive\n // hands over a modest set of highlights (top 4 \u2014 enough to fill a couple\n // of wrapped rows on a roomy surface) and lets the canvas clip. Standard\n // density keeps the full structured table for surfaces where the card is\n // the destination, not a preview.\n if (density === 'compact') {\n const allRows = specGroups.flatMap((g) => g.rows);\n const highlights = allRows.slice(0, 4);\n if (highlights.length === 0) return nothing;\n // Panel-style pill chips: name + value fused (\"Lion's Mane 1000mg\"),\n // 9999px radius, hairline border, wrap freely; the container's\n // token-driven max-height clips the flow (one row under velvet).\n // NO section heading at compact \u2014 the card is a glance preview.\n return html`<div\n class=\"sc-product-card__specs\"\n data-density=\"compact\"\n data-clip-ok\n data-spec-chip-scroll\n tabindex=\"0\"\n aria-label=\"Product specifications\"\n style=${styleMap(specChipRowStyles)}\n >\n ${highlights.map(\n (r) => html`<span\n data-spec-chip\n style=${styleMap(specChipStyles)}\n ><span style=\"font-weight: 600;\">${r.name}</span\n ><span style=\"opacity: 0.72;\">${r.value}${r.unit ? r.unit : ''}</span></span\n >`\n )}\n </div>`;\n }\n // Standard density keeps the grouped, sectioned structure (a section\n // heading per spec section) but lays each row out as a panel-style pill\n // chip \u2014 the same pill vocabulary as compact, just richer (all rows,\n // wraps to multiple lines). The tiny section label uses the 11px/600\n // title-color heading from the variant panel.\n return html`<div class=\"sc-product-card__specs\" style=${styleMap(specsContainerStyles)}>\n ${specGroups.map(\n (g) => html`<section\n class=\"sc-product-card__spec-section\"\n data-spec-section=${g.section}\n style=${styleMap(specGroupStyles)}\n >\n <h4\n class=\"sc-product-card__spec-heading\"\n style=${styleMap(specHeadingStyles)}\n >${g.section}</h4>\n <ul\n class=\"sc-product-card__spec-rows\"\n data-clip-ok\n data-spec-chip-scroll\n tabindex=\"0\"\n aria-label=${`${g.section} specifications`}\n style=${styleMap(specRowsStyles)}\n >\n ${g.rows.map(\n (r) => html`<li\n data-spec-row\n data-spec-chip\n data-attribute-emphasis=${r.emphasis ? 'true' : 'false'}\n style=${styleMap({\n ...specChipStyles,\n ...(r.emphasis\n ? {\n background: 'var(--sc-color-primary, #3d8a5e)',\n color: 'var(--sc-color-on-primary, #ffffff)',\n borderColor: 'var(--sc-color-primary, #3d8a5e)',\n }\n : {}),\n })}\n >\n <span\n class=\"sc-product-card__spec-name\"\n data-spec-name\n style=\"font-weight: 600;\"\n >${r.name}</span\n >\n <span\n class=\"sc-product-card__spec-value\"\n data-spec-value\n style=${styleMap(specValueStyles)}\n >${r.value}</span\n >\n ${\n r.unit\n ? html`<span\n class=\"sc-product-card__spec-unit\"\n data-spec-unit\n style=\"opacity: 0.72;\"\n >${r.unit}</span\n >`\n : nothing\n }\n </li>`\n )}\n </ul>\n </section>`\n )}\n </div>`;\n }\n if (key === 'framing') {\n if (!isVisible('framing') || !showFraming) return nothing;\n return html`<p\n class=\"sc-product-card__framing\"\n style=${styleMap({\n margin: '0',\n // Prose terminal fallback (item b): a long unbroken hook token can't\n // overflow the content column.\n minWidth: '0',\n overflowWrap: 'anywhere' as const,\n wordBreak: 'break-word' as const,\n })}\n .innerHTML=${sanitizeHtml(product.framing ?? '')}\n ></p>`;\n }\n // tagline + rating render in the header; nothing to emit here.\n return nothing;\n })}\n </div>\n </div>\n ${ctaRow}\n </div>\n `;\n}\n\n/**\n * Standalone product card renderer \u2014 used by ProductGridLit and other contexts\n * that need a non-stateful card without the face state machine.\n * Produces the full `<article>` wrapper.\n */\nexport function renderProductCard(\n product: Product,\n density: 'compact' | 'standard',\n resolved: { image?: string; price?: string } | undefined,\n visibleFacts?: readonly VisibleFactKey[]\n): TemplateResult {\n const articleStyles = _buildArticleStyles(density);\n\n return html`\n <article\n class=\"sc-product-card\"\n data-density=${density}\n data-labels=${product.labels ? JSON.stringify(product.labels) : nothing}\n style=${styleMap(articleStyles)}\n >\n ${renderProductCardInner(product, density, resolved, visibleFacts, product.ctas, undefined)}\n </article>\n `;\n}\n\nfunction onImageError(e: Event): void {\n const img = e.currentTarget as HTMLImageElement;\n img.dataset.imageError = 'true';\n img.removeAttribute('src');\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\n\ntype DomSelectorPrice = Extract<PriceBind, { type: 'dom-selector' }>;\ntype DomSelectorImage = Extract<ImageBind, { type: 'dom-selector' }>;\n\nexport async function resolveDomSelectorPrice(\n input: DomSelectorPrice,\n _signal: AbortSignal\n): Promise<string | null> {\n const el = document.querySelector(input.selector);\n const text = el?.textContent?.trim();\n return text && text.length > 0 ? text : null;\n}\n\nexport async function resolveDomSelectorImage(\n input: DomSelectorImage,\n _signal: AbortSignal\n): Promise<string | null> {\n const el = document.querySelector(input.selector);\n if (!el) return null;\n if (el instanceof HTMLImageElement) return el.currentSrc || el.src || null;\n const src = el.getAttribute('src');\n return src && src.length > 0 ? src : null;\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\n\ntype JsonLdPriceBind = Extract<PriceBind, { type: 'jsonld' }>;\ntype JsonLdImageBind = Extract<ImageBind, { type: 'jsonld' }>;\n\ninterface Offer {\n price?: string | number;\n priceCurrency?: string;\n}\ninterface ProductNode {\n '@type'?: string | string[];\n image?: string | string[];\n offers?: Offer | Offer[];\n}\n\nfunction isProduct(node: unknown): node is ProductNode {\n if (typeof node !== 'object' || node === null) return false;\n const t = (node as { '@type'?: unknown })['@type'];\n if (typeof t === 'string') return t === 'Product';\n if (Array.isArray(t)) return t.includes('Product');\n return false;\n}\n\nfunction parseScripts(scripts: string[]): ProductNode | null {\n for (const text of scripts) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n continue;\n }\n const stack: unknown[] = [parsed];\n while (stack.length) {\n const cur = stack.pop();\n if (Array.isArray(cur)) {\n for (const c of cur) stack.push(c);\n continue;\n }\n if (isProduct(cur)) return cur;\n if (typeof cur === 'object' && cur !== null) {\n const graph = (cur as { '@graph'?: unknown })['@graph'];\n if (Array.isArray(graph)) for (const g of graph) stack.push(g);\n }\n }\n }\n return null;\n}\n\nfunction collectInlineScripts(): string[] {\n const els = document.querySelectorAll('script[type=\"application/ld+json\"]');\n return Array.from(els, (el) => el.textContent ?? '');\n}\n\nasync function collectScriptsFromUrl(url: string, signal: AbortSignal): Promise<string[]> {\n const res = await fetch(url, { signal });\n if (!res.ok) return [];\n const text = await res.text();\n const doc = new DOMParser().parseFromString(text, 'text/html');\n return Array.from(\n doc.querySelectorAll('script[type=\"application/ld+json\"]'),\n (el) => el.textContent ?? ''\n );\n}\n\nfunction pickPrice(node: ProductNode): { amount: number; currency: string } | null {\n const offer: Offer | undefined = Array.isArray(node.offers) ? node.offers[0] : node.offers;\n if (!offer) return null;\n const raw = offer.price;\n const num =\n typeof raw === 'number' ? raw : typeof raw === 'string' ? Number.parseFloat(raw) : Number.NaN;\n if (!Number.isFinite(num)) return null;\n return { amount: num, currency: offer.priceCurrency || 'USD' };\n}\n\nfunction formatPrice(amount: number, currency: string): string | null {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch (err) {\n console.warn('[adaptive-product.bind] jsonld: Intl.NumberFormat failed', { currency }, err);\n return null;\n }\n}\n\nexport async function resolveJsonLdPrice(\n input: JsonLdPriceBind,\n signal: AbortSignal\n): Promise<string | null> {\n const scripts = input.url\n ? await collectScriptsFromUrl(input.url, signal)\n : collectInlineScripts();\n const product = parseScripts(scripts);\n if (!product) return null;\n const picked = pickPrice(product);\n return picked ? formatPrice(picked.amount, picked.currency) : null;\n}\n\nexport async function resolveJsonLdImage(\n input: JsonLdImageBind,\n signal: AbortSignal\n): Promise<string | null> {\n const scripts = input.url\n ? await collectScriptsFromUrl(input.url, signal)\n : collectInlineScripts();\n const product = parseScripts(scripts);\n if (!product) return null;\n const img = Array.isArray(product.image) ? product.image[0] : product.image;\n return typeof img === 'string' && img.length > 0 ? img : null;\n}\n", "import type { ImageBind } from '../schema-primitives';\n\ntype OgBind = Extract<ImageBind, { type: 'opengraph' }>;\n\nexport async function resolveOpenGraphImage(\n input: OgBind,\n signal: AbortSignal\n): Promise<string | null> {\n const res = await fetch(input.url, { signal });\n if (!res.ok) return null;\n const text = await res.text();\n const doc = new DOMParser().parseFromString(text, 'text/html');\n const meta = doc.querySelector('meta[property=\"og:image\"]') as HTMLMetaElement | null;\n const raw = meta?.getAttribute('content');\n if (!raw) return null;\n try {\n return new URL(raw, input.url).toString();\n } catch {\n return null;\n }\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\n\ntype ShopifyAjaxPriceBind = Extract<PriceBind, { type: 'shopify-ajax' }>;\ntype ShopifyAjaxImageBind = Extract<ImageBind, { type: 'shopify-ajax' }>;\n\ninterface ShopifyProductPayload {\n price?: number; // cents (Shopify minor units)\n currency?: string; // ISO code; \"USD\" by default in Shopify's AJAX API\n images?: string[];\n}\n\nfunction originFor(base: string | undefined): string {\n if (base) return base.replace(/\\/$/, '');\n return typeof location !== 'undefined' ? location.origin : '';\n}\n\nasync function fetchProduct(\n handle: string,\n base: string | undefined,\n signal: AbortSignal\n): Promise<ShopifyProductPayload | null> {\n const url = `${originFor(base)}/products/${encodeURIComponent(handle)}.js`;\n const res = await fetch(url, { signal });\n if (!res.ok) return null;\n try {\n return (await res.json()) as ShopifyProductPayload;\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') throw err;\n console.warn('[adaptive-product.bind] shopify-ajax: json parse failed', err);\n return null;\n }\n}\n\nexport async function resolveShopifyAjaxPrice(\n input: ShopifyAjaxPriceBind,\n signal: AbortSignal\n): Promise<string | null> {\n const payload = await fetchProduct(input.handle, input.base, signal);\n if (!payload || typeof payload.price !== 'number') return null;\n const currency = payload.currency || 'USD';\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(\n payload.price / 100\n );\n } catch (err) {\n console.warn(\n '[adaptive-product.bind] shopify-ajax/price: Intl.NumberFormat failed',\n { currency },\n err\n );\n return null;\n }\n}\n\nexport async function resolveShopifyAjaxImage(\n input: ShopifyAjaxImageBind,\n signal: AbortSignal\n): Promise<string | null> {\n const payload = await fetchProduct(input.handle, input.base, signal);\n const first = payload?.images?.[0];\n return typeof first === 'string' && first.length > 0 ? first : null;\n}\n", "import type { PriceBind } from '../schema-primitives';\n\ntype WooPriceBind = Extract<PriceBind, { type: 'woocommerce-store-api' }>;\n\ninterface WooStoreProduct {\n id: number;\n slug: string;\n prices?: {\n price?: string;\n currency_code?: string;\n currency_minor_unit?: number;\n };\n}\n\nfunction originFor(base: string | undefined): string {\n if (base) return base.replace(/\\/$/, '');\n return typeof location !== 'undefined' ? location.origin : '';\n}\n\nexport async function resolveWooStoreApiPrice(\n input: WooPriceBind,\n signal: AbortSignal\n): Promise<string | null> {\n const url = `${originFor(input.base)}/wp-json/wc/store/products?slug=${encodeURIComponent(input.slug)}`;\n const res = await fetch(url, { signal });\n if (!res.ok) return null;\n let body: WooStoreProduct[];\n try {\n body = (await res.json()) as WooStoreProduct[];\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') throw err;\n console.warn('[adaptive-product.bind] woocommerce-store-api: json parse failed', err);\n return null;\n }\n const product = Array.isArray(body) ? body.find((p) => p.slug === input.slug) : undefined;\n const prices = product?.prices;\n if (!prices || typeof prices.price !== 'string') return null;\n const minor = prices.currency_minor_unit ?? 2;\n const raw = Number.parseInt(prices.price, 10);\n if (!Number.isFinite(raw)) return null;\n const amount = raw / 10 ** minor;\n const currency = prices.currency_code || 'USD';\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);\n } catch (err) {\n console.warn(\n '[adaptive-product.bind] woocommerce-store-api: Intl.NumberFormat failed',\n { currency },\n err\n );\n return null;\n }\n}\n", "import type { ImageBind, PriceBind } from '../schema-primitives';\nimport { resolveDomSelectorImage, resolveDomSelectorPrice } from './dom-selector';\nimport { resolveJsonLdImage, resolveJsonLdPrice } from './jsonld';\nimport { resolveOpenGraphImage } from './opengraph';\nimport { resolveShopifyAjaxImage, resolveShopifyAjaxPrice } from './shopify-ajax';\nimport type { BindResult } from './types';\nimport { resolveWooStoreApiPrice } from './woocommerce-store-api';\n\nexport async function resolvePriceBind(input: PriceBind, signal: AbortSignal): Promise<BindResult> {\n try {\n switch (input.type) {\n case 'dom-selector':\n return await resolveDomSelectorPrice(input, signal);\n case 'shopify-ajax':\n return await resolveShopifyAjaxPrice(input, signal);\n case 'woocommerce-store-api':\n return await resolveWooStoreApiPrice(input, signal);\n case 'jsonld':\n return await resolveJsonLdPrice(input, signal);\n default:\n return null;\n }\n } catch (err) {\n if (!(err instanceof Error && err.name === 'AbortError')) {\n console.warn(`[adaptive-product.bind] price/${input.type}:`, err);\n }\n return null;\n }\n}\n\nexport async function resolveImageBind(input: ImageBind, signal: AbortSignal): Promise<BindResult> {\n try {\n switch (input.type) {\n case 'dom-selector':\n return await resolveDomSelectorImage(input, signal);\n case 'shopify-ajax':\n return await resolveShopifyAjaxImage(input, signal);\n case 'opengraph':\n return await resolveOpenGraphImage(input, signal);\n case 'jsonld':\n return await resolveJsonLdImage(input, signal);\n default:\n return null;\n }\n } catch (err) {\n if (!(err instanceof Error && err.name === 'AbortError')) {\n console.warn(`[adaptive-product.bind] image/${input.type}:`, err);\n }\n return null;\n }\n}\n\nexport type { BindResult, ImageResolver, PriceResolver } from './types';\n", "import type { Product } from '../schema-primitives';\nimport { resolveImageBind, resolvePriceBind } from './index';\n\nexport type BindCallback = (\n productId: string,\n kind: 'image' | 'price',\n value: string | null\n) => void;\n\nexport class BindController {\n #abort = new AbortController();\n #pending: Promise<void>[] = [];\n #destroyed = false;\n\n constructor(\n private readonly products: readonly Product[],\n private readonly callback: BindCallback\n ) {}\n\n start(): void {\n for (const product of this.products) {\n if (product.image.bind) {\n this.#pending.push(this.#resolveImage(product.id, product.image.bind));\n }\n if (product.price?.bind) {\n this.#pending.push(this.#resolvePrice(product.id, product.price.bind));\n }\n }\n }\n\n async allSettled(): Promise<void> {\n await Promise.allSettled(this.#pending);\n }\n\n destroy(): void {\n if (this.#destroyed) return;\n this.#destroyed = true;\n this.#abort.abort();\n }\n\n async #resolveImage(\n productId: string,\n bind: NonNullable<Product['image']['bind']>\n ): Promise<void> {\n const value = await resolveImageBind(bind, this.#abort.signal);\n if (this.#destroyed) return;\n this.callback(productId, 'image', value);\n }\n\n async #resolvePrice(\n productId: string,\n bind: NonNullable<NonNullable<Product['price']>['bind']>\n ): Promise<void> {\n const value = await resolvePriceBind(bind, this.#abort.signal);\n if (this.#destroyed) return;\n this.callback(productId, 'price', value);\n }\n}\n", "import type { AddToCartInput, VariantPayload } from '../platformAdapter';\n\nexport type CardProductState =\n | { status: 'empty'; id: null }\n | { status: 'invalid'; id: string | null }\n | { status: 'valid'; id: string };\n\nexport type CardVariantState =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'ready'; payload: VariantPayload }\n | { status: 'error'; message: string };\n\nexport type CardCartState =\n | { status: 'idle' }\n | { status: 'pending'; variantId: string | null }\n | { status: 'added' }\n | { status: 'error'; message: string };\n\nexport interface CardState {\n generation: number;\n connection: 'connected' | 'disconnected';\n commerce: 'available' | 'unavailable';\n product: CardProductState;\n face: 'front' | 'back';\n flip: 'flat' | 'animating';\n variants: CardVariantState;\n cart: CardCartState;\n selection: Record<string, string>;\n activation: { status: 'idle' } | { status: 'pending'; reducedMotion: boolean };\n}\n\nexport type CardEvent =\n | { type: 'props-replaced'; productId: string | null; valid: boolean }\n | { type: 'commerce-updated'; available: boolean }\n | { type: 'primary-activated'; reducedMotion?: boolean }\n | { type: 'variants-resolved'; generation: number; payload: VariantPayload | null }\n | { type: 'variants-failed'; generation: number; message: string; errorName?: string }\n | { type: 'selection-changed'; attributeKey: string; valueId: string }\n | { type: 'variant-add-activated'; variantId: string }\n | { type: 'add-succeeded'; generation: number }\n | { type: 'add-failed'; generation: number; message: string; code?: string }\n | { type: 'retry-activated' }\n | { type: 'back-activated' }\n | { type: 'animation-settled' }\n | { type: 'disconnected' }\n | { type: 'reconnected' };\n\nexport type CardEffect =\n | { type: 'lookup-variants'; generation: number; productId: string }\n | {\n type: 'add-to-cart';\n generation: number;\n input: AddToCartInput;\n }\n | { type: 'open-cart' }\n | { type: 'close-sdk' }\n | { type: 'publish-event'; name: string; props: Record<string, unknown> }\n | { type: 'capture-telemetry'; name: string; props: Record<string, unknown> }\n | { type: 'schedule-flip-settle'; delayMs: number }\n | { type: 'cancel-flip-settle' }\n | { type: 'bind-product'; productId: string }\n | { type: 'report-render-health' }\n | { type: 'cleanup' };\n\nexport interface CardTransition {\n state: CardState;\n effects: CardEffect[];\n}\n\nexport interface CardView {\n face: CardState['face'];\n flipAnimating: boolean;\n primaryCta: {\n mode: 'add' | 'adding' | 'view-cart';\n disabled: boolean;\n };\n panel: {\n status: 'hidden' | 'loading' | 'ready' | 'error';\n payload: VariantPayload | null;\n error: string | null;\n };\n addError: string | null;\n}\n\nexport function createInitialCardState(generation = 0): CardState {\n return {\n generation,\n connection: 'connected',\n commerce: 'available',\n product: { status: 'empty', id: null },\n face: 'front',\n flip: 'flat',\n variants: { status: 'idle' },\n cart: { status: 'idle' },\n selection: {},\n activation: { status: 'idle' },\n };\n}\n\nexport function selectCardView(state: CardState): CardView {\n const primaryCta =\n state.cart.status === 'added'\n ? ({\n mode: 'view-cart',\n disabled: state.connection !== 'connected' || state.commerce !== 'available',\n } as const)\n : state.cart.status === 'pending'\n ? ({ mode: 'adding', disabled: true } as const)\n : ({\n mode: 'add',\n disabled:\n state.connection !== 'connected' ||\n state.commerce !== 'available' ||\n state.product.status !== 'valid',\n } as const);\n\n let panel: CardView['panel'] = { status: 'hidden', payload: null, error: null };\n if (state.face === 'back') {\n switch (state.variants.status) {\n case 'ready':\n panel = { status: 'ready', payload: state.variants.payload, error: null };\n break;\n case 'error':\n panel = { status: 'error', payload: null, error: state.variants.message };\n break;\n case 'idle':\n case 'loading':\n panel = { status: 'loading', payload: null, error: null };\n break;\n }\n }\n\n return {\n face: state.face,\n flipAnimating: state.flip === 'animating',\n primaryCta,\n panel,\n addError: state.cart.status === 'error' ? state.cart.message : null,\n };\n}\n\nfunction unchanged(state: CardState): CardTransition {\n return { state, effects: [] };\n}\n\nfunction replaceProps(\n state: CardState,\n event: Extract<CardEvent, { type: 'props-replaced' }>\n): CardTransition {\n const sameProduct =\n event.valid &&\n event.productId !== null &&\n state.product.status === 'valid' &&\n state.product.id === event.productId;\n\n if (sameProduct && event.productId !== null) {\n const effects: CardEffect[] = [{ type: 'bind-product', productId: event.productId }];\n if (state.variants.status === 'idle') {\n effects.push({\n type: 'lookup-variants',\n generation: state.generation,\n productId: event.productId,\n });\n }\n effects.push({ type: 'report-render-health' });\n return { state, effects };\n }\n\n const generation = state.generation + 1;\n const next = { ...createInitialCardState(generation), commerce: state.commerce };\n const effects: CardEffect[] = [{ type: 'cancel-flip-settle' }];\n if (!event.valid || event.productId === null) {\n return {\n state: {\n ...next,\n product: { status: 'invalid', id: event.productId },\n },\n effects,\n };\n }\n\n effects.push(\n { type: 'bind-product', productId: event.productId },\n { type: 'lookup-variants', generation, productId: event.productId },\n { type: 'report-render-health' }\n );\n return {\n state: {\n ...next,\n product: { status: 'valid', id: event.productId },\n },\n effects,\n };\n}\n\nfunction publish(name: string, props: Record<string, unknown>): CardEffect {\n return { type: 'publish-event', name, props };\n}\n\nfunction activatePrimary(\n state: CardState,\n event: Extract<CardEvent, { type: 'primary-activated' }>\n): CardTransition {\n if (\n state.connection !== 'connected' ||\n state.commerce !== 'available' ||\n state.product.status !== 'valid' ||\n state.face !== 'front'\n ) {\n return unchanged(state);\n }\n const productId = state.product.id;\n if (state.cart.status === 'added') {\n return {\n state,\n effects: [\n publish('chatbot.product_card.view_cart_clicked', { product_id: productId }),\n { type: 'close-sdk' },\n { type: 'open-cart' },\n ],\n };\n }\n if (state.cart.status === 'pending') return unchanged(state);\n if (state.activation.status === 'pending') return unchanged(state);\n\n if (state.variants.status === 'idle' || state.variants.status === 'loading') {\n return {\n state: {\n ...state,\n variants: { status: 'loading' },\n activation: { status: 'pending', reducedMotion: event.reducedMotion ?? false },\n },\n effects:\n state.variants.status === 'idle'\n ? [{ type: 'lookup-variants', generation: state.generation, productId }]\n : [],\n };\n }\n\n const shouldOpenPanel =\n state.variants.status === 'error' || state.variants.payload.variants.length > 1;\n if (shouldOpenPanel) {\n return {\n state: {\n ...state,\n face: 'back',\n flip: event.reducedMotion ? 'flat' : 'animating',\n activation: { status: 'idle' },\n },\n effects: [\n publish('chatbot.product_card.face_changed', {\n product_id: productId,\n from: state.face,\n to: 'back',\n }),\n ...(event.reducedMotion\n ? []\n : ([{ type: 'schedule-flip-settle', delayMs: 450 }] satisfies CardEffect[])),\n ],\n };\n }\n\n return {\n state: {\n ...state,\n cart: { status: 'pending', variantId: null },\n activation: { status: 'idle' },\n },\n effects: [\n {\n type: 'add-to-cart',\n generation: state.generation,\n input: { product_id: productId, variant_id: null, quantity: 1 },\n },\n ],\n };\n}\n\nfunction serializeSelection(selection: Record<string, string>): {\n selection_keys: string;\n selection_serialized: string;\n} {\n const keys = Object.keys(selection).sort();\n return {\n selection_keys: keys.join(','),\n selection_serialized: keys.map((key) => `${key}=${selection[key]}`).join(';'),\n };\n}\n\nfunction activateVariantAdd(\n state: CardState,\n event: Extract<CardEvent, { type: 'variant-add-activated' }>\n): CardTransition {\n if (\n state.connection !== 'connected' ||\n state.commerce !== 'available' ||\n state.product.status !== 'valid' ||\n state.face !== 'back' ||\n state.variants.status !== 'ready' ||\n state.cart.status === 'pending' ||\n state.cart.status === 'added'\n ) {\n return unchanged(state);\n }\n const variant = state.variants.payload.variants.find(\n (candidate) => candidate.id === event.variantId\n );\n if (!variant?.in_stock) return unchanged(state);\n\n const props = {\n product_id: state.product.id,\n variant_id: variant.id,\n ...serializeSelection(state.selection),\n };\n return {\n state: { ...state, cart: { status: 'pending', variantId: variant.id } },\n effects: [\n publish('chatbot.product_card.add_to_cart.attempted', props),\n {\n type: 'add-to-cart',\n generation: state.generation,\n input: { product_id: state.product.id, variant_id: variant.id, quantity: 1 },\n },\n ],\n };\n}\n\nfunction applyAddResult(\n state: CardState,\n event: Extract<CardEvent, { type: 'add-succeeded' | 'add-failed' }>\n): CardTransition {\n if (state.cart.status !== 'pending' || state.product.status !== 'valid') {\n return unchanged(state);\n }\n const props = {\n product_id: state.product.id,\n variant_id: state.cart.variantId,\n ...serializeSelection(state.selection),\n };\n if (event.type === 'add-failed') {\n return {\n state: { ...state, cart: { status: 'error', message: event.message } },\n effects: [\n publish('chatbot.product_card.add_to_cart.failed', {\n ...props,\n error_code: event.code ?? 'unknown',\n error_name: event.message,\n }),\n ],\n };\n }\n return {\n state: { ...state, face: 'front', flip: 'flat', cart: { status: 'added' } },\n effects: [\n publish('chatbot.product_card.add_to_cart.succeeded', props),\n { type: 'cancel-flip-settle' },\n ],\n };\n}\n\nexport function transitionCard(state: CardState, event: CardEvent): CardTransition {\n if (event.type === 'props-replaced') return replaceProps(state, event);\n if (event.type === 'commerce-updated') {\n const commerce = event.available ? 'available' : 'unavailable';\n return commerce === state.commerce\n ? unchanged(state)\n : { state: { ...state, commerce }, effects: [] };\n }\n\n if (\n (event.type === 'variants-resolved' ||\n event.type === 'variants-failed' ||\n event.type === 'add-succeeded' ||\n event.type === 'add-failed') &&\n (event.generation !== state.generation ||\n state.connection !== 'connected' ||\n state.product.status !== 'valid')\n ) {\n return unchanged(state);\n }\n\n switch (event.type) {\n case 'primary-activated':\n return activatePrimary(state, event);\n case 'variants-resolved': {\n const next: CardState = {\n ...state,\n variants:\n event.payload === null ? { status: 'idle' } : { status: 'ready', payload: event.payload },\n };\n if (state.activation.status !== 'pending') return { state: next, effects: [] };\n return activatePrimary(\n { ...next, activation: { status: 'idle' } },\n { type: 'primary-activated', reducedMotion: state.activation.reducedMotion }\n );\n }\n case 'variants-failed': {\n const failureEffect = publish('chatbot.product_card.variant_fetch.failed', {\n product_id: state.product.id,\n error_name: event.errorName ?? 'Error',\n });\n const next: CardState = {\n ...state,\n variants: { status: 'error', message: event.message },\n };\n if (state.activation.status !== 'pending') {\n return { state: next, effects: [failureEffect] };\n }\n const resumed = activatePrimary(\n { ...next, activation: { status: 'idle' } },\n { type: 'primary-activated', reducedMotion: state.activation.reducedMotion }\n );\n return { state: resumed.state, effects: [failureEffect, ...resumed.effects] };\n }\n case 'selection-changed':\n if (\n state.commerce !== 'available' ||\n state.face !== 'back' ||\n state.variants.status !== 'ready'\n ) {\n return unchanged(state);\n }\n return {\n state: {\n ...state,\n selection: { ...state.selection, [event.attributeKey]: event.valueId },\n },\n effects:\n state.product.status === 'valid'\n ? [\n publish('chatbot.product_card.variant_selected', {\n product_id: state.product.id,\n attribute_key: event.attributeKey,\n value_id: event.valueId,\n }),\n ]\n : [],\n };\n case 'variant-add-activated':\n return activateVariantAdd(state, event);\n case 'add-succeeded':\n case 'add-failed':\n return applyAddResult(state, event);\n case 'retry-activated':\n if (\n state.product.status !== 'valid' ||\n state.commerce !== 'available' ||\n state.variants.status !== 'error'\n ) {\n return unchanged(state);\n }\n return {\n state: { ...state, variants: { status: 'loading' } },\n effects: [\n {\n type: 'lookup-variants',\n generation: state.generation,\n productId: state.product.id,\n },\n ],\n };\n case 'back-activated':\n if (state.face !== 'back') return unchanged(state);\n return {\n state: { ...state, face: 'front', flip: 'flat' },\n effects: [\n { type: 'cancel-flip-settle' },\n ...(state.product.status === 'valid'\n ? [\n publish('chatbot.product_card.face_changed', {\n product_id: state.product.id,\n from: 'back',\n to: 'front',\n }),\n ]\n : []),\n ],\n };\n case 'animation-settled':\n if (state.flip !== 'animating') return unchanged(state);\n return {\n state: { ...state, flip: 'flat' },\n effects: [{ type: 'cancel-flip-settle' }],\n };\n case 'disconnected':\n if (state.connection === 'disconnected') return unchanged(state);\n return {\n state: { ...state, connection: 'disconnected', flip: 'flat' },\n effects: [{ type: 'cleanup' }],\n };\n case 'reconnected': {\n if (state.connection === 'connected') return unchanged(state);\n const generation = state.generation + 1;\n return {\n state: { ...state, generation, connection: 'connected' },\n effects:\n state.product.status === 'valid' && state.commerce === 'available'\n ? [\n { type: 'bind-product', productId: state.product.id },\n { type: 'lookup-variants', generation, productId: state.product.id },\n { type: 'report-render-health' },\n ]\n : [],\n };\n }\n default:\n return unchanged(state);\n }\n}\n", "import type { ProductPlatformAdapter, VariantPayload } from './platformAdapter';\n\n/**\n * Module-level cache keyed by product_id. Dedupes concurrent fetches \u2014\n * multiple cards mounted for the same product, or remounts on chat-trail\n * rehydration, share one in-flight Promise. Cache entries live for the\n * lifetime of the page; rejected fetches are NOT cached so the next call\n * retries cleanly.\n */\nconst _cache = new Map<string, Promise<VariantPayload>>();\n\nexport function cachedFetchVariants(\n adapter: ProductPlatformAdapter,\n productId: string\n): Promise<VariantPayload> {\n const hit = _cache.get(productId);\n if (hit) return hit;\n\n const promise = adapter.fetchVariants(productId).catch((err) => {\n // Don't poison the cache with a rejected promise; remove the entry\n // so the next call re-attempts the fetch.\n _cache.delete(productId);\n throw err;\n });\n _cache.set(productId, promise);\n return promise;\n}\n\n/** Test helper: drop all cached entries. */\nexport function clearVariantCache(): void {\n _cache.clear();\n}\n", "import {\n getProductPlatformAdapter,\n type ProductPlatformAdapter,\n type VariantPayload,\n} from '../platformAdapter';\nimport { cachedFetchVariants } from '../variantCache';\nimport type { CardEffect, CardEvent } from './card';\n\nexport interface CardEffectPorts {\n dispatch(event: CardEvent): void;\n publishEvent(name: string, props: Record<string, unknown>): void;\n captureTelemetry(name: string, props: Record<string, unknown>): void;\n closeSdk(): void;\n bindProduct(productId: string): void;\n reportRenderHealth(): void;\n cleanup(): void;\n schedule(callback: () => void, delayMs: number): unknown;\n cancelScheduled(handle: unknown): void;\n getAdapter?(): ProductPlatformAdapter | null;\n fetchVariants?(adapter: ProductPlatformAdapter, productId: string): Promise<VariantPayload>;\n}\n\nexport interface CardEffectExecutor {\n execute(effect: CardEffect): Promise<void>;\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n if (error instanceof Error && error.message) return error.message;\n if (typeof error === 'string' && error) return error;\n return fallback;\n}\n\nexport function createCardEffectExecutor(ports: CardEffectPorts): CardEffectExecutor {\n const getAdapter = ports.getAdapter ?? getProductPlatformAdapter;\n const fetchVariants = ports.fetchVariants ?? cachedFetchVariants;\n let flipSettleHandle: unknown;\n let lifecycle = 0;\n\n const cancelFlipSettle = (): void => {\n if (flipSettleHandle === undefined) return;\n ports.cancelScheduled(flipSettleHandle);\n flipSettleHandle = undefined;\n };\n\n const execute = async (effect: CardEffect): Promise<void> => {\n try {\n switch (effect.type) {\n case 'lookup-variants': {\n const adapter = getAdapter();\n if (!adapter) return;\n const startedIn = lifecycle;\n try {\n const payload = await fetchVariants(adapter, effect.productId);\n if (startedIn !== lifecycle) return;\n ports.dispatch({\n type: 'variants-resolved',\n generation: effect.generation,\n payload,\n });\n } catch (error) {\n if (startedIn !== lifecycle) return;\n ports.dispatch({\n type: 'variants-failed',\n generation: effect.generation,\n message: errorMessage(error, 'Variant lookup failed'),\n errorName: error instanceof Error ? error.name : 'Error',\n });\n }\n return;\n }\n case 'add-to-cart': {\n const adapter = getAdapter();\n if (!adapter) {\n ports.dispatch({\n type: 'add-failed',\n generation: effect.generation,\n message: \"Couldn't add to cart. Try again.\",\n code: 'adapter_unavailable',\n });\n return;\n }\n const startedIn = lifecycle;\n try {\n const result = await adapter.addToCart(effect.input);\n if (startedIn !== lifecycle) return;\n ports.dispatch(\n result.ok\n ? { type: 'add-succeeded', generation: effect.generation }\n : {\n type: 'add-failed',\n generation: effect.generation,\n message: result.error?.message ?? \"Couldn't add to cart. Try again.\",\n code: result.error?.code ?? 'unknown',\n }\n );\n } catch (error) {\n if (startedIn !== lifecycle) return;\n ports.dispatch({\n type: 'add-failed',\n generation: effect.generation,\n message: errorMessage(error, \"Couldn't add to cart. Try again.\"),\n code: 'unknown',\n });\n }\n return;\n }\n case 'open-cart': {\n const adapter = getAdapter();\n if (adapter) await adapter.openCart();\n return;\n }\n case 'close-sdk':\n ports.closeSdk();\n return;\n case 'publish-event':\n ports.publishEvent(effect.name, effect.props);\n return;\n case 'capture-telemetry':\n ports.captureTelemetry(effect.name, effect.props);\n return;\n case 'schedule-flip-settle':\n cancelFlipSettle();\n {\n const scheduledIn = lifecycle;\n flipSettleHandle = ports.schedule(() => {\n flipSettleHandle = undefined;\n if (scheduledIn === lifecycle) ports.dispatch({ type: 'animation-settled' });\n }, effect.delayMs);\n }\n return;\n case 'cancel-flip-settle':\n cancelFlipSettle();\n return;\n case 'bind-product':\n ports.bindProduct(effect.productId);\n return;\n case 'report-render-health':\n ports.reportRenderHealth();\n return;\n case 'cleanup':\n lifecycle += 1;\n cancelFlipSettle();\n ports.cleanup();\n return;\n }\n } catch {\n // Host integrations and telemetry must not crash the widget.\n }\n };\n\n return { execute };\n}\n", "/**\n * HTML Sanitizer\n *\n * Sanitizes HTML to prevent XSS attacks.\n * Uses native Sanitizer API when available, falls back to whitelist approach.\n */\n\nconst ALLOWED_TAGS = new Set([\n 'b',\n 'strong',\n 'i',\n 'em',\n 'u',\n 'span',\n 'div',\n 'p',\n 'br',\n 'ul',\n 'ol',\n 'li',\n 'code',\n 'pre',\n 'small',\n 'sup',\n 'sub',\n 'a',\n 'button',\n // SVG elements (for inline Lucide icons in config HTML)\n 'svg',\n 'path',\n 'circle',\n 'line',\n 'polyline',\n 'polygon',\n 'rect',\n 'g',\n]);\n\nfunction normalizeUrlAttr(value: string): string {\n const withoutControlCharacters = Array.from(value, (character) => {\n const codePoint = character.codePointAt(0) ?? 0;\n return codePoint <= 0x1f || codePoint === 0x7f ? '' : character;\n }).join('');\n return withoutControlCharacters.trim().toLowerCase();\n}\n\nfunction isDangerousUrlAttr(name: string, value: string): boolean {\n if (name !== 'href' && name !== 'src' && name !== 'formaction') return false;\n const normalized = normalizeUrlAttr(value);\n return (\n normalized.startsWith('javascript:') ||\n normalized.startsWith('vbscript:') ||\n normalized.startsWith('data:')\n );\n}\n\nexport function sanitizeHtml(html: string): string {\n // Try native Sanitizer API first\n const hasNative = typeof (window as any).Sanitizer === 'function';\n if (hasNative) {\n try {\n const s = new (window as any).Sanitizer({});\n const frag = s.sanitizeToFragment(html);\n const div = document.createElement('div');\n div.append(frag);\n return div.innerHTML;\n } catch {\n // Fall through to manual sanitizer\n }\n }\n\n // Conservative fallback sanitizer\n const tpl = document.createElement('template');\n tpl.innerHTML = html;\n const root = tpl.content;\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);\n const toRemove: Element[] = [];\n\n while (walker.nextNode()) {\n const el = walker.currentNode as Element;\n const tag = el.tagName.toLowerCase();\n\n if (!ALLOWED_TAGS.has(tag)) {\n toRemove.push(el);\n continue;\n }\n\n // Remove dangerous attributes\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n const isEvent = name.startsWith('on');\n\n if (isEvent || isDangerousUrlAttr(name, attr.value)) {\n el.removeAttribute(attr.name);\n }\n }\n }\n\n // Remove disallowed elements but keep their children\n for (const el of toRemove) {\n while (el.firstChild) {\n el.parentNode?.insertBefore(el.firstChild, el);\n }\n el.remove();\n }\n\n return tpl.innerHTML;\n}\n", "/**\n * Inject a `<style>` into the element's OWN root node \u2014 the shadow root that\n * contains it when the widget is mounted inside another element's shadow tree\n * (e.g. a canvas's `<sc-mount>`), or `document.head` when it lives in the light\n * DOM (chat trail, a host PDP page).\n *\n * Why this exists: these product widgets render in light DOM (`createRenderRoot`\n * returns `this`) and style themselves with an injected stylesheet. When a\n * canvas mounts the widget inside `<sc-mount>`, the widget subtree lands inside\n * `sc-mount`'s SHADOW root. A stylesheet appended to `document.head` does NOT\n * cross that shadow boundary, so the widget rendered with default browser\n * styling and none of its own CSS. Injecting into `getRootNode()` puts the\n * stylesheet in the same tree as the element, so it applies on every surface.\n * Only CSS custom-property VALUES cross shadow boundaries \u2014 which is why the\n * host canvas themes the widget via `--sc-*` tokens while the layout/appearance\n * CSS must travel with the widget itself.\n *\n * Idempotent per root: guarded by the style id so repeated mounts into the same\n * root inject once; distinct roots (multiple mounts) each get their own copy.\n */\nexport function ensureStylesInRoot(el: Element, id: string, css: string): void {\n if (typeof document === 'undefined') return;\n const root = el.getRootNode();\n const target: ParentNode = root instanceof ShadowRoot ? root : document.head;\n if (target.querySelector(`#${id}`)) return;\n const style = document.createElement('style');\n style.id = id;\n style.textContent = css;\n (target as ParentNode & Node).appendChild(style);\n}\n", "/**\n * renderHealth \u2014 runtime self-detection of clipping and blank render.\n *\n * A product card can look correct in tests yet clip text or paint blank on a\n * real host (a narrow theme column, a font that measures wider, a broken image\n * CDN). The battery walks the playground at build time; this module is its\n * RUNTIME counterpart \u2014 after the card paints, it re-measures itself against the\n * SAME policy and emits a PostHog signal when it finds an undeclared clip or a\n * blank card. Field-RUM for \"the card broke on a customer's page\".\n *\n * NON-PII discipline: only element tag/class PATHS and pixel numbers ever leave\n * the widget \u2014 never text content, never customer copy. The paths are the\n * structural coordinates a dashboard needs to locate the offending node.\n *\n * Pattern-setter: this lives in a small shared module (not inline in\n * ProductCardLit) so sibling widgets (comparison, grid, hero) can adopt the same\n * self-detection with one import.\n */\n\n/** A single undeclared-clip finding inside a card. */\nexport interface ClipFinding {\n /** Structural path to the offending node, e.g. `div.sc-product-card__specs>span`. NON-PII. */\n path: string;\n /** How many px the node overflows its clip box (text scrollWidth overflow, or\n * child escaping the card's own clip box). Rounded. */\n over_px: number;\n /** Why this counts as a clip. Absent for ordinary (Tier-3) undeclared clips.\n * `identity-truncated` marks a Tier-1 CRITICAL text node (product name,\n * price, CTA label, picker title \u2014 `[data-critical-text]`) that truncated\n * even with a declared ellipsis / clip-ok. Identity text must render in full,\n * so ANY horizontal overflow of a critical node is a defect. NON-PII. */\n reason?: 'identity-truncated';\n}\n\n/** Result of a render-health measurement pass. */\nexport interface RenderHealth {\n /** Undeclared clips found inside the card (empty = clean). */\n clips: ClipFinding[];\n /** True when the card occupies real area but painted no text AND no complete\n * image (a blank card). */\n blank: boolean;\n}\n\n/** Minimum card area (px\u00B2) below which \"blank\" is not meaningful (a collapsed /\n * offscreen node isn't a blank-card bug). */\nconst BLANK_MIN_AREA = 60 * 60;\n\n/**\n * Build a short, NON-PII structural path for an element: up to 3 ancestors of\n * `tag.class` (first class only), joined by `>`. Never includes text content.\n */\nfunction elementPath(el: Element, stopAt: Element): string {\n const seg = (e: Element): string => {\n const tag = e.tagName.toLowerCase();\n const cls =\n typeof e.className === 'string' && e.className.trim()\n ? `.${e.className.trim().split(/\\s+/)[0]}`\n : '';\n return `${tag}${cls}`;\n };\n const parts: string[] = [seg(el)];\n let cur: Element | null = el.parentElement;\n let hops = 0;\n while (cur && cur !== stopAt && hops < 3) {\n parts.unshift(seg(cur));\n cur = cur.parentElement;\n hops++;\n }\n return parts.join('>');\n}\n\n/** True if `el` (or an ancestor up to `root`) declares the clip is intentional. */\nfunction hasClipOkAncestor(el: Element, root: Element): boolean {\n let cur: Element | null = el;\n while (cur && cur !== root.parentElement) {\n if (cur.hasAttribute?.('data-clip-ok')) return true;\n cur = cur.parentElement;\n }\n return false;\n}\n\n/** A clipping ancestor exempt from escape-flagging: scrollable, or a declared\n * line-clamp ellipsis box, or explicitly clip-ok. Mirrors the battery policy. */\nfunction isDeclaredClipBox(cs: CSSStyleDeclaration): boolean {\n const scrollable =\n ['auto', 'scroll'].includes(cs.overflowX) || ['auto', 'scroll'].includes(cs.overflowY);\n if (scrollable) return true;\n const lineClamp = cs.webkitLineClamp || cs.getPropertyValue('-webkit-line-clamp');\n if (lineClamp && lineClamp !== 'none') return true;\n return false;\n}\n\nfunction isHardClipBox(cs: CSSStyleDeclaration): boolean {\n return ['hidden', 'clip'].includes(cs.overflowX) || ['hidden', 'clip'].includes(cs.overflowY);\n}\n\n/**\n * Measure the render health of a card element. Pure DOM read \u2014 no side effects,\n * no telemetry. Safe to call any time after paint. The caller (ProductCardLit)\n * decides whether/when to emit.\n *\n * Policy mirrors tmp-live/battery.mjs:\n * - identity-truncated (Tier 1): a [data-critical-text] node (name, price, CTA\n * label, picker title) whose scrollWidth exceeds clientWidth by >1px \u2014 even\n * WITH an ellipsis or under [data-clip-ok]. Identity text must render in full.\n * - text-clip: an element with its own text whose scrollWidth exceeds\n * clientWidth by >1px and has no textOverflow:ellipsis, not under [data-clip-ok].\n * - element-clip: a text/media node escaping a HARD-clip ancestor by >1px,\n * where the ancestor is not scrollable, not a line-clamp box, not clip-ok.\n * - blank: the card has real area but no painted text and no complete image.\n */\nexport function measureRenderHealth(card: Element): RenderHealth {\n const clips: ClipFinding[] = [];\n const seen = new Set<string>();\n const push = (f: ClipFinding): void => {\n const k = `${f.path}|${f.over_px}`;\n if (seen.has(k)) return;\n seen.add(k);\n clips.push(f);\n };\n\n let hasText = false;\n let hasCompleteImage = false;\n\n const els = card.querySelectorAll('*');\n for (const el of els) {\n const cs = getComputedStyle(el);\n if (cs.display === 'none' || cs.visibility === 'hidden') continue;\n const r = el.getBoundingClientRect();\n if (r.width === 0 || r.height === 0) continue;\n\n // Track painted content for the blank check.\n const ownText = Array.from(el.childNodes).some(\n (n) => n.nodeType === 3 && (n.textContent ?? '').trim().length > 0\n );\n if (ownText) hasText = true;\n if (el.tagName === 'IMG') {\n const img = el as HTMLImageElement;\n if (img.complete && img.naturalWidth > 0 && img.dataset.imageError !== 'true') {\n hasCompleteImage = true;\n }\n }\n\n // Tier 1 \u2014 CRITICAL/identity text (product name, price, CTA label, picker\n // title, marked [data-critical-text]). It MUST render in full: ANY\n // horizontal truncation is a defect, EVEN with a declared ellipsis or under\n // a data-clip-ok container. Checked before (and instead of) the Tier-3\n // text-clip rule so an ellipsized identity node is never excused.\n const isCritical =\n typeof el.closest === 'function' ? el.closest('[data-critical-text]') != null : false;\n if (ownText && isCritical && el.scrollWidth > el.clientWidth + 1) {\n push({\n path: elementPath(el, card),\n over_px: el.scrollWidth - el.clientWidth,\n reason: 'identity-truncated',\n });\n }\n\n // text-clip (Tier 3): own-text node overflowing without an ellipsis, not\n // declared, not a critical node (handled by the Tier-1 rule above).\n if (\n ownText &&\n !isCritical &&\n el.scrollWidth > el.clientWidth + 1 &&\n cs.textOverflow !== 'ellipsis' &&\n !hasClipOkAncestor(el, card)\n ) {\n push({ path: elementPath(el, card), over_px: el.scrollWidth - el.clientWidth });\n }\n\n // element-clip: escape from a HARD-clip ancestor within the card.\n let a = el.parentElement;\n while (a && a !== card.parentElement) {\n const acs = getComputedStyle(a);\n if (isDeclaredClipBox(acs)) break;\n if (isHardClipBox(acs)) {\n const ar = a.getBoundingClientRect();\n const esc = Math.max(\n 0,\n Math.round(r.right - ar.right),\n Math.round(ar.left - r.left),\n Math.round(r.bottom - ar.bottom),\n Math.round(ar.top - r.top)\n );\n if (esc > 1 && !hasClipOkAncestor(el, card) && !hasClipOkAncestor(a, card)) {\n const t = (el.textContent ?? '').trim();\n if (t || ['IMG', 'BUTTON', 'A'].includes(el.tagName)) {\n push({ path: elementPath(el, card), over_px: esc });\n }\n }\n break;\n }\n a = a.parentElement;\n }\n }\n\n const cardRect = card.getBoundingClientRect();\n const area = cardRect.width * cardRect.height;\n const blank = area >= BLANK_MIN_AREA && !hasText && !hasCompleteImage;\n\n return { clips, blank };\n}\n", "/**\n * renderHealthReporter \u2014 the shared, reusable half of runtime render-health\n * self-detection.\n *\n * `renderHealth.ts` MEASURES a rendered element (pure DOM read: undeclared clips\n * + blank paint). This module is the counterpart that REPORTS the measurement:\n * it emits `app:<category>:blank` / `app:<category>:clipped` through the ONE\n * consent-gated door to PostHog (`window.SynOS.runtime.events.emit`), and packages\n * the once-per-visual-state scheduling as a Lit ReactiveController so every tile\n * widget adopts the same self-detection with ~5 lines and no copy-paste.\n *\n * COE-051 lesson: `publish()` only notifies in-process canvas subscribers \u2014 it\n * NEVER reaches PostHog. `emit()` is the sanctioned capture path: the EventBus\n * forwards emit()'d events to its posthogCapture sink (bootstrap-runtime wires it\n * to the consent-gated `telemetry.track`). We never call posthog directly. The\n * event name MUST satisfy validateEventName (`app:{category}:{action}`, colon\n * segments, no `app:` prefix stripped) or the EventBus silently drops it \u2014 the\n * exact silent failure this telemetry exists to surface.\n *\n * NON-PII discipline: only element tag/class PATHS and pixel numbers ever leave a\n * widget \u2014 never text content, never customer copy.\n */\nimport type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { measureRenderHealth, type RenderHealth } from './renderHealth';\n\ninterface SynOSEventsWindow {\n SynOS?: {\n runtime?: {\n events?: {\n // The ONLY transport that reaches PostHog (see module doc / COE-051).\n emit?: (name: string, props: Record<string, unknown>) => unknown;\n };\n };\n };\n}\n\n/** Emit a telemetry event on the PostHog-reaching path. Never throws. */\nfunction synEmit(name: string, props: Record<string, unknown>): void {\n if (typeof window === 'undefined') return;\n try {\n const emit = (window as unknown as SynOSEventsWindow).SynOS?.runtime?.events?.emit;\n if (typeof emit === 'function') emit(name, props);\n } catch {\n // Telemetry must never crash the widget.\n }\n}\n\nexport interface RenderHealthReportOptions {\n /** Event category segment: emits `app:<category>:blank` / `:clipped`. Must be a\n * valid segment (`^[a-z][a-z0-9_]*$`), e.g. `product_comparison`. */\n category: string;\n /** `widget_kind` tag included in every payload, e.g. `adaptive-product:comparison`. */\n widgetKind: string;\n /** Extra NON-PII context merged into every payload (density, velvet, \u2026). */\n context?: Record<string, unknown>;\n}\n\nconst CLEAN: RenderHealth = { clips: [], blank: false };\n\n/**\n * Measure `root` and, if it clips or paints blank, emit the corresponding\n * self-detection event via the consent-gated emit() path. Returns the measured\n * health (so callers/tests can inspect it). Never throws \u2014 a measurement or\n * telemetry failure must never crash the widget or the host page.\n */\nexport function reportRenderHealth(root: Element, opts: RenderHealthReportOptions): RenderHealth {\n try {\n const health = measureRenderHealth(root);\n if (health.clips.length === 0 && !health.blank) return health;\n const base: Record<string, unknown> = { ...(opts.context ?? {}), widget_kind: opts.widgetKind };\n if (health.blank) {\n synEmit(`app:${opts.category}:blank`, base);\n return health;\n }\n // Clipped: NON-PII paths + the worst overflow px. A Tier-1 identity\n // truncation (name/price/CTA that didn't render in full) tags the payload so\n // a dashboard can separate \"prose clipped\" from \"identity broke\".\n const paths = health.clips.map((c) => c.path).sort();\n const over_px = health.clips.reduce((m, c) => Math.max(m, c.over_px), 0);\n const identityTruncated = health.clips.some((c) => c.reason === 'identity-truncated');\n synEmit(`app:${opts.category}:clipped`, {\n paths: paths.join(','),\n over_px,\n clip_count: health.clips.length,\n ...(identityTruncated ? { reason: 'identity-truncated' } : {}),\n ...base,\n });\n return health;\n } catch {\n // Self-detection must never crash the widget.\n return CLEAN;\n }\n}\n\n/** What a widget's probe returns for one measurement pass. */\nexport interface RenderHealthProbe extends RenderHealthReportOptions {\n /** The element to measure, or `null` to SKIP this pass. A widget returns null\n * for any UNSETTLED state (loading, error, empty, wrong face) so a transient\n * no-content frame is never mis-reported as a blank tile. */\n root: Element | null;\n /** Stable key for the current visual state; a state reports AT MOST ONCE\n * (prevents event spam across re-renders of the same visual state). */\n stateKey: string;\n}\n\n/**\n * Lit ReactiveController that runs a widget's render-health probe after each\n * paint settles (rAF then a 0ms timeout, so lazy images / web fonts land first),\n * at most once per visual state. Attach in a widget's field initializer:\n *\n * #health = new RenderHealthController(this, () => this.#healthProbe());\n *\n * The controller's `hostUpdated` fires automatically after every host update.\n */\nexport class RenderHealthController implements ReactiveController {\n #probe: () => RenderHealthProbe | null;\n #reported = new Set<string>();\n #raf: number | undefined;\n\n constructor(host: ReactiveControllerHost, probe: () => RenderHealthProbe | null) {\n this.#probe = probe;\n host.addController(this);\n }\n\n hostUpdated(): void {\n this.#schedule();\n }\n\n hostDisconnected(): void {\n this.#cancel();\n }\n\n #schedule(): void {\n if (typeof window === 'undefined') return;\n if (this.#raf !== undefined) return; // one pending pass is enough\n const raf =\n typeof window.requestAnimationFrame === 'function'\n ? window.requestAnimationFrame.bind(window)\n : (cb: FrameRequestCallback) => window.setTimeout(() => cb(0), 0);\n this.#raf = raf(() => {\n window.setTimeout(() => {\n this.#raf = undefined;\n this.#run();\n }, 0);\n }) as unknown as number;\n }\n\n #run(): void {\n try {\n const probe = this.#probe();\n if (!probe || !probe.root) return;\n if (this.#reported.has(probe.stateKey)) return;\n // Mark consumed BEFORE reporting so a clean state isn't re-measured every\n // frame and a finding never double-fires for the same visual state.\n this.#reported.add(probe.stateKey);\n // `probe` IS a RenderHealthReportOptions (superset); reportRenderHealth\n // reads only category/widgetKind/context.\n reportRenderHealth(probe.root, probe);\n } catch {\n // Self-detection must never crash the widget.\n }\n }\n\n #cancel(): void {\n if (\n this.#raf !== undefined &&\n typeof window !== 'undefined' &&\n typeof window.cancelAnimationFrame === 'function'\n ) {\n window.cancelAnimationFrame(this.#raf);\n }\n this.#raf = undefined;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\nimport type { AttributeDef, Variant, VariantPayload } from '../platformAdapter';\nimport { ensureStylesInRoot } from './ensureStylesInRoot';\nimport { RenderHealthController, type RenderHealthProbe } from './renderHealthReporter';\n\n// Neutral, token-driven baseline. The panel is light-DOM so a host canvas's\n// --sc-* tokens cascade in; every color/surface here reads a token (with a\n// LIGHT fallback for bare surfaces like chat/PDP) rather than the old\n// dark-surface translucent-white chrome that clashed on velvet's cream tile.\n// The opinionated two-column + connected-segmented layout lives in the velvet\n// canvas, which supplies token VALUES only (selectors can't cross into the\n// panel's shadow-nested root \u2014 see ensureStylesInRoot). Layout is therefore\n// PARAMETERIZED here and switched on by the canvas via tokens:\n// --sc-variant-cols grid-template-columns (default 1fr = single column;\n// velvet sets \"minmax(72px, 30%) 1fr\" for image | controls)\n// --sc-variant-image-max hero image cap (default 120px; velvet sets 100% to\n// fill its column in the two-column band)\n// The [data-image-col] (image + Back) and [data-panel-body] (title + pickers +\n// CTA) are the two grid items, so one token flips the whole panel between a\n// stacked single column and a two-column layout.\n//\n// Round-2 feedback rework (structure, ALL surfaces):\n// - There is NO top header row with a Back button + right-aligned title\n// anymore (it ate vertical space above the pickers).\n// - The TITLE is the PRODUCT NAME, shown compactly at the TOP of the body\n// (where the old header/back row was) as the panel's visible title.\n// - BACK moves OUT of the header into the BOTTOM of the IMAGE column (below\n// the CTA row, RIGHT of Add) as a small, quiet '\u2039 Back' control \u2014 freeing the vertical\n// the old header consumed for the variant pickers. It keeps emitting the\n// same `variant-panel-back` event.\nconst PANEL_CSS = `\n syntro-variant-panel {\n display: grid;\n /* minmax(0, 1fr) not 1fr: the content track must be able to shrink below its\n min-content size so a long variant label / product title can't push the\n panel past the card. The svp-image-col / svp-body children carry\n min-width:0 to complete the guarantee. */\n grid-template-columns: var(--sc-variant-cols, minmax(0, 1fr));\n /* Item 3: a single 1fr row so the body column can FILL a height-bounded\n parent (velvet's definite-height, overflow:hidden deck card) \u2014 that's\n what gives the interior scroll region a bounded height to scroll within.\n height:100% resolves to auto (a no-op) when the parent height is\n indefinite (chat / PDP), so those surfaces keep their natural,\n non-scrolling height. minmax(0,\u2026) lets the row shrink below content. */\n grid-template-rows: minmax(0, 1fr);\n height: 100%;\n column-gap: 0.75rem;\n align-items: start;\n padding: var(--sc-variant-pad, 12px); font-family: var(--sc-font-family, inherit);\n color: var(--sc-tile-text-color, var(--sc-color-text, inherit));\n box-sizing: border-box;\n /* Root overflow invariant (item e): the panel is a card face \u2014 it can shrink\n with its container and never paints past it. */\n min-width: 0; max-width: 100%; overflow: hidden;\n }\n /* Two-column mode (velvet sets --sc-variant-cols) only makes sense when BOTH\n the image column and the body are present. Loading, error, single-variant-\n note, and image-less states render a single top-level child \u2014 it must span\n all columns instead of being crammed into the narrow image column. */\n syntro-variant-panel > :only-child { grid-column: 1 / -1; }\n /* Image column: the picture with the quiet Back control tucked BELOW it. In\n the single-column (chat/PDP) default it stacks above the controls; in\n velvet's two-column band it is the left feature column. */\n .svp-image-col {\n display: flex; flex-direction: column; align-items: stretch; min-width: 0;\n margin-bottom: var(--sc-variant-row-gap, 10px);\n }\n .svp-image {\n display: block; width: 100%; max-width: var(--sc-variant-image-max, 120px);\n /* Same portrait box as the card front (token-tunable) so the flip reads\n as the same object turning over. */\n aspect-ratio: var(--sc-variant-image-aspect, 1 / 1); object-fit: cover;\n border-radius: var(--sc-radius-md, 8px);\n }\n /* Quiet Back link under the picture \u2014 small, low-emphasis, no pill chrome. */\n .svp-back-btn {\n display: inline-flex; align-items: center; gap: 4px; align-self: flex-start;\n margin-top: 0; padding: 2px 2px; flex: 0 0 auto;\n background: none; border: 0; cursor: pointer;\n font-size: 12px; font-weight: 600; line-height: 1;\n color: var(--sc-tile-title-color, inherit); opacity: 0.7;\n transition: opacity 120ms ease;\n }\n .svp-back-btn:hover { opacity: 1; }\n /* The product NAME as the panel title, at the top of the body. IDENTITY text\n (Tier 1): it must render IN FULL \u2014 it WRAPS at word boundaries rather than\n ellipsizing. The title height may grow for a long name; that's fine. */\n .svp-product-title {\n font-weight: 700; font-size: 14px; line-height: 1.2;\n color: var(--sc-tile-title-color, inherit);\n margin-bottom: var(--sc-variant-row-gap, 10px);\n min-width: 0; white-space: normal; overflow-wrap: break-word;\n }\n /* Item 3: the body is a bounded flex column \u2014 pinned title at the top, a\n flex:1 SCROLL region in the middle (the attribute rows), pinned CTA at the\n bottom. align-self:stretch + min-height:0 let it fill the 1fr row so the\n middle region has a definite height to scroll within. */\n .svp-body { min-width: 0; display: flex; flex-direction: column; align-self: stretch; min-height: 0; max-height: 100%; }\n /* The scrolling middle: many attributes SCROLL (never squish the rows below\n a readable height, never clip a row off). overscroll-behavior:contain keeps\n a flick from chaining to the host page; the bottom fade hints \"more below\".\n data-clip-ok marks this as a DECLARED, reachable scroll affordance so the\n clip battery accepts its overflow. */\n .svp-attributes-scroll {\n flex: 1 1 auto; min-height: 0;\n overflow-y: auto; overflow-x: hidden;\n overscroll-behavior: contain;\n scrollbar-width: none; -ms-overflow-style: none;\n mask-image: linear-gradient(180deg, black 0, black calc(100% - 16px), transparent 100%);\n -webkit-mask-image: linear-gradient(180deg, black 0, black calc(100% - 16px), transparent 100%);\n }\n .svp-attributes-scroll::-webkit-scrollbar { display: none; }\n /* Rows keep a minimum readable/tappable height so a squeezed band scrolls\n rather than crushing the pickers below usability. */\n .svp-attribute-row { margin-bottom: var(--sc-variant-row-gap, 10px); min-height: var(--sc-variant-row-min-h, 40px); }\n .svp-attribute-label {\n font-size: 11px; font-weight: 600; margin-bottom: 4px; letter-spacing: 0.01em;\n color: var(--sc-tile-title-color, inherit); opacity: 0.85;\n }\n .svp-pill-row { display: flex; flex-wrap: wrap; gap: 6px; }\n .svp-pill {\n border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n background: transparent;\n color: var(--sc-tile-title-color, inherit);\n padding: 6px 14px;\n border-radius: 9999px;\n cursor: pointer;\n font-size: 13px; font-weight: 600;\n transition: background-color 120ms ease, color 120ms ease, opacity 120ms ease;\n }\n .svp-pill[aria-checked=\"true\"] {\n background: var(--sc-color-primary, #3d8a5e);\n color: var(--sc-color-on-primary, #ffffff);\n border-color: var(--sc-color-primary, #3d8a5e);\n }\n .svp-pill:disabled { opacity: 0.35; cursor: not-allowed; }\n /* Connected segmented control: pills collapse into one pill-shaped bar with\n hairline dividers; the selected segment keeps the primary fill above. It\n must NOT wrap to a second row in a narrow column \u2014 the segments SHARE the\n available width and shrink (min-width:0) so 2-3 options always sit on one\n line. The container clips rather than wraps. */\n .svp-pill-row[data-segmented] {\n display: flex; flex-wrap: nowrap; gap: 0; width: 100%; max-width: 100%;\n border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n border-radius: 9999px; overflow: hidden;\n }\n .svp-pill-row[data-segmented] .svp-pill {\n border: 0; border-radius: 0;\n flex: 1 1 0; min-width: 0;\n padding: 6px 8px; font-size: 12px;\n white-space: nowrap; text-align: center;\n overflow: hidden; text-overflow: ellipsis;\n }\n .svp-pill-row[data-segmented] .svp-pill + .svp-pill {\n border-left: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n }\n .svp-swatch-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n /* \u2500\u2500 Density Tier B: one-row scroll track (9-16 options) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A wrapped row of 9+ options eats the bounded panel's height; nowrap +\n overflow-x turns the axis into a single scrollable track with an edge\n fade hinting \"more\". Same recipe as the card's compact spec-chip row.\n data-clip-ok: declared, reachable scroll affordance (clip battery). */\n .svp-pill-row[data-scroll], .svp-swatch-row[data-scroll] {\n flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden;\n overscroll-behavior-x: contain; scroll-snap-type: x proximity;\n scrollbar-width: none; -ms-overflow-style: none;\n mask-image: linear-gradient(90deg, black 0, black calc(100% - 20px), transparent 100%);\n -webkit-mask-image: linear-gradient(90deg, black 0, black calc(100% - 20px), transparent 100%);\n padding-bottom: 2px;\n }\n .svp-pill-row[data-scroll]::-webkit-scrollbar,\n .svp-swatch-row[data-scroll]::-webkit-scrollbar { display: none; }\n .svp-pill-row[data-scroll] > *, .svp-swatch-row[data-scroll] > * {\n flex: 0 0 auto; scroll-snap-align: start;\n }\n /* \u2500\u2500 Density Tier C: collapsed selector (17+ options) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n The axis renders as a select-style trigger showing the CURRENT choice;\n tapping opens a bounded, scrollable listbox inside the panel (filter\n input past 24 options). Only the offending axis collapses \u2014 sibling\n axes keep their own tier. */\n .svp-collapse { min-width: 0; }\n .svp-collapse-trigger {\n display: flex; align-items: center; gap: 8px; width: 100%; min-width: 0;\n border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n background: transparent; border-radius: var(--sc-radius-md, 8px);\n padding: 8px 10px; cursor: pointer; font-size: 13px; font-weight: 600;\n color: var(--sc-tile-title-color, inherit);\n }\n .svp-collapse-trigger .svp-swatch-dot { width: 16px; height: 16px; flex: none; }\n .svp-collapse-value {\n min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n .svp-collapse-caret { margin-left: auto; flex: none; opacity: 0.6; font-size: 11px; }\n .svp-collapse-list {\n margin-top: 6px; border: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n border-radius: var(--sc-radius-md, 8px);\n max-height: var(--sc-variant-list-max-h, 180px);\n overflow-y: auto; overscroll-behavior: contain;\n }\n .svp-collapse-filter {\n width: 100%; box-sizing: border-box; padding: 7px 10px; border: 0;\n border-bottom: 1px solid var(--sc-color-border, rgba(0,0,0,0.14));\n font-size: 13px; font-family: inherit; background: transparent;\n color: inherit; outline: none; position: sticky; top: 0;\n backdrop-filter: blur(2px);\n }\n .svp-collapse-option {\n display: flex; align-items: center; gap: 8px; width: 100%;\n padding: 7px 10px; background: transparent; border: 0; cursor: pointer;\n font-size: 13px; font-family: inherit; text-align: left;\n color: var(--sc-tile-title-color, inherit);\n }\n .svp-collapse-option .svp-swatch-dot { width: 14px; height: 14px; flex: none; }\n .svp-collapse-option[aria-selected=\"true\"] {\n background: var(--sc-color-primary, #3d8a5e);\n color: var(--sc-color-on-primary, #ffffff);\n }\n .svp-collapse-option:disabled { opacity: 0.35; cursor: not-allowed; }\n .svp-collapse-empty { padding: 8px 10px; font-size: 12px; opacity: 0.7; }\n .svp-swatch-pill {\n width: 28px; height: 28px; border-radius: 50%;\n border: 2px solid transparent;\n cursor: pointer;\n padding: 0;\n transition: border-color 120ms ease, opacity 120ms ease;\n }\n .svp-swatch-pill[aria-checked=\"true\"] { border-color: var(--sc-color-primary, #3d8a5e); }\n .svp-swatch-pill:disabled { opacity: 0.35; cursor: not-allowed; }\n .svp-swatch-dot { width: 100%; height: 100%; border-radius: 50%; border: 1px solid rgba(0,0,0,0.2); }\n .svp-swatch-label { font-size: 13px; margin-bottom: 4px; opacity: 0.85; }\n .svp-oos-microcopy { font-size: 11px; opacity: 0.6; margin-top: 4px; }\n .svp-single-variant-note { font-size: 12px; opacity: 0.75; padding: 8px 0; }\n .svp-cta-row {\n margin-top: auto; padding-top: var(--sc-variant-row-gap, 12px);\n /* Add + Back share ONE line, always (Back spilled under the CTA when the\n button kept width:100%). The CTA flexes, Back keeps its content size. */\n display: flex; align-items: center; gap: 10px; flex-wrap: nowrap; min-width: 0;\n }\n .svp-cta {\n flex: 1 1 auto; min-width: 0; padding: var(--sc-variant-cta-pad, 10px 16px); border-radius: 9999px;\n border: 1px solid var(--sc-color-primary, #3d8a5e);\n background: var(--sc-color-primary, #3d8a5e);\n color: var(--sc-color-on-primary, #ffffff); font-size: 13px; font-weight: 600;\n cursor: pointer; white-space: nowrap;\n transition: opacity 120ms ease, transform 120ms ease;\n }\n .svp-cta:hover:not(:disabled) { transform: translateY(-1px); }\n .svp-cta:disabled { opacity: 0.4; cursor: not-allowed; }\n .svp-compare { text-decoration: line-through; opacity: 0.65; margin-right: 6px; }\n .svp-skeleton {\n animation: svp-shimmer 1.4s linear infinite;\n background: linear-gradient(90deg, rgba(0,0,0,0.04), rgba(0,0,0,0.08), rgba(0,0,0,0.04));\n background-size: 200% 100%;\n height: 32px; border-radius: var(--sc-radius-md, 8px); margin-bottom: 12px;\n }\n @keyframes svp-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }\n .svp-error-block { padding: 16px; text-align: center; font-size: 13px; }\n .svp-retry-btn { margin-top: 8px; padding: 6px 14px; border-radius: 9999px; border: 1px solid currentColor; background: none; color: inherit; cursor: pointer; }\n .svp-live { position: absolute; left: -9999px; top: 0; width: 1px; height: 1px; overflow: hidden; }\n`;\n\nexport class VariantPanelLit extends LitElement {\n static override properties = {\n payload: { attribute: false },\n productTitle: { type: String },\n productImage: { attribute: false },\n state: { type: String },\n error: { type: String },\n _selection: { state: true },\n _touched: { state: true },\n _expandedAxes: { state: true },\n _axisFilter: { state: true },\n };\n\n payload: VariantPayload | null = null;\n productTitle = '';\n /** Product-level hero image, passed from the card. Shown on the back face and\n * used as the fallback when the selected variant carries no featured_image. */\n productImage: { src: string; alt?: string } | null = null;\n state: 'idle' | 'loading' | 'error' = 'idle';\n error: string | null = null;\n\n /** @internal */\n _selection: Record<string, string> = {};\n /** @internal */\n _touched: Set<string> = new Set();\n /** @internal \u2014 Tier-C axes currently showing their listbox. */\n _expandedAxes: Set<string> = new Set();\n /** @internal \u2014 Tier-C per-axis filter text. */\n _axisFilter: Record<string, string> = {};\n\n constructor() {\n super();\n // Runtime self-detection: emits app:variant_panel:blank / :clipped after\n // paint if the SETTLED panel has area but paints no content. Self-registers\n // via host.addController \u2014 no stored reference needed.\n new RenderHealthController(this, () => this.#healthProbe());\n }\n\n /** Measure only the SETTLED panel (idle state with a payload). The loading\n * skeleton and the error block are unsettled \u2014 a transient no-content frame\n * must never be mis-reported as a blank tile. NON-PII stateKey. */\n #healthProbe(): RenderHealthProbe | null {\n if (this.state !== 'idle' || !this.payload) return null;\n return {\n root: this,\n stateKey: `variant|${this.payload.variants.length}|${this.payload.attributes.length}`,\n category: 'variant_panel',\n widgetKind: 'adaptive-product:variant',\n };\n }\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Inject into the panel's OWN root (shadow root under a canvas's <sc-mount>,\n // or document.head in the light DOM) so the stylesheet actually reaches it.\n ensureStylesInRoot(this, 'syntro-variant-panel-style', PANEL_CSS);\n this.addEventListener('keydown', this._onKeydown);\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener('keydown', this._onKeydown);\n }\n\n private _onKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') {\n this.dispatchEvent(new CustomEvent('variant-panel-back', { bubbles: true, composed: true }));\n }\n };\n\n private _findMatchingVariant(): Variant | null {\n if (!this.payload) return null;\n return (\n this.payload.variants.find((v) =>\n Object.entries(v.selection).every(([k, val]) => this._selection[k] === val)\n ) ?? null\n );\n }\n\n private _isValueDisabled(attrKey: string, valueId: string): boolean {\n if (!this.payload) return false;\n // OOS: a value is disabled if NO in-stock variant exists that uses this value\n // GIVEN the visitor's current selection for OTHER attributes.\n const otherSelections = Object.entries(this._selection).filter(([k]) => k !== attrKey);\n const candidateVariants = this.payload.variants.filter((v) => {\n if (v.selection[attrKey] !== valueId) return false;\n for (const [k, val] of otherSelections) {\n if (v.selection[k] !== val) return false;\n }\n return true;\n });\n return candidateVariants.length === 0 || candidateVariants.every((v) => !v.in_stock);\n }\n\n private _hasAnyOOSForAttribute(attr: AttributeDef): boolean {\n return attr.values.some((v) => this._isValueDisabled(attr.key, v.id));\n }\n\n private _onPillClick(attrKey: string, valueId: string): void {\n if (this._isValueDisabled(attrKey, valueId)) return;\n this._selection = { ...this._selection, [attrKey]: valueId };\n this._touched = new Set([...this._touched, attrKey]);\n this.dispatchEvent(\n new CustomEvent('variant-panel-selection-change', {\n detail: { attribute_key: attrKey, value_id: valueId },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _onBack(): void {\n this.dispatchEvent(new CustomEvent('variant-panel-back', { bubbles: true, composed: true }));\n }\n\n private _onAdd(): void {\n const variant = this._findMatchingVariant();\n if (!variant) return;\n this.dispatchEvent(\n new CustomEvent('variant-panel-add', {\n detail: { variant, selection: { ...this._selection } },\n bubbles: true,\n composed: true,\n })\n );\n }\n\n private _onRetry(): void {\n this.dispatchEvent(new CustomEvent('variant-panel-retry', { bubbles: true, composed: true }));\n }\n\n /** The hero image URL to show: the matched variant's featured_image when the\n * current selection resolves to a variant that has one, else the product\n * image. Null when neither exists. */\n private _heroSrc(): string | null {\n return this._findMatchingVariant()?.featured_image ?? this.productImage?.src ?? null;\n }\n\n private _ctaEnabled(): boolean {\n if (!this.payload) return false;\n if (this.payload.attributes.some((a) => !this._touched.has(a.key))) return false;\n const m = this._findMatchingVariant();\n return m?.in_stock === true;\n }\n\n override render() {\n if (this.state === 'error') {\n return html`\n <div class=\"svp-error-block\" data-error-block>\n <div>Couldn't load options. ${this.error ?? ''}</div>\n <button class=\"svp-retry-btn\" data-retry-button @click=${this._onRetry}>Try again</button>\n </div>\n `;\n }\n if (this.state === 'loading' || !this.payload) {\n return html`\n <div data-loading-skeleton>\n <div class=\"svp-skeleton\"></div>\n <div class=\"svp-skeleton\"></div>\n <div class=\"svp-skeleton\"></div>\n </div>\n `;\n }\n\n const matching = this._findMatchingVariant();\n const ctaPrice = matching?.price ?? null;\n const compareAt = matching?.compare_at_price ?? null;\n\n // Single-SKU products (no variant matrix) come back with zero attributes and\n // one default variant \u2014 e.g. a storefront synthesizes this for products with\n // no options. Rendering the empty attribute list would leave a bare,\n // headerless grid that looks broken, so show a one-line note instead. The\n // CTA is enabled immediately (no selection required) and adds the default.\n const singleVariant = this.payload.attributes.length === 0;\n const heroSrc = this._heroSrc();\n\n // Structure: an [data-image-col] wrapper (the hero image + a quiet Back\n // control below it) and a [data-panel-body] wrapper are SIBLINGS so the\n // velvet canvas can place them into a two-column (image | controls) grid\n // that uses horizontal space in its short vertical band. On a bare surface\n // (chat/PDP) they stack \u2014 image column on top, controls below \u2014 a fine\n // single-column default.\n //\n // The Back control lives at the BOTTOM of the image column (below the\n // picture); the product NAME is the panel title at the TOP of the body.\n // There is no separate top header row (round-2 feedback: free the vertical\n // for the pickers). When there is no hero image we still render the image\n // column so Back stays reachable \u2014 just without the <img>.\n const backBtn = html`<button\n class=\"svp-back-btn\"\n data-back-button\n @click=${this._onBack}\n aria-label=\"Back\"\n >\u2039 Back</button>`;\n return html`\n <div class=\"svp-image-col\" data-image-col>\n ${\n heroSrc\n ? html`<img\n class=\"svp-image\"\n data-variant-image\n src=${heroSrc}\n alt=${this.productImage?.alt ?? ''}\n loading=\"lazy\"\n />`\n : nothing\n }\n </div>\n <div class=\"svp-body\" data-panel-body>\n ${\n this.productTitle\n ? html`<div class=\"svp-product-title\" data-product-title data-critical-text>${this.productTitle}</div>`\n : nothing\n }\n <div class=\"svp-attributes-scroll\" data-attributes-scroll data-clip-ok>\n ${\n singleVariant\n ? html`<div class=\"svp-single-variant-note\" data-single-variant-note>\n Just one option \u2014 ready to add.\n </div>`\n : this.payload.attributes.map((attr) => this._renderAttributeRow(attr))\n }\n </div>\n <div class=\"svp-cta-row\">\n <button\n class=\"svp-cta\"\n data-add-to-cart\n data-critical-text\n ?disabled=${!this._ctaEnabled()}\n @click=${this._onAdd}\n >\n ${\n // Short label: \"Add \u00B7 $64\" (the price commits HERE \u2014 the card's\n // front face carries no price; round-3 feedback shortened this\n // so Back fits beside it on the same row).\n ctaPrice != null\n ? html`Add ${compareAt ? html`<span class=\"svp-compare\">$${compareAt.toFixed(2)}</span>` : nothing}\u00B7 $${ctaPrice.toFixed(2)}`\n : html`Add to cart`\n }\n </button>\n ${backBtn}\n </div>\n <div class=\"svp-live\" data-live data-clip-ok aria-live=\"polite\">\n ${this.productTitle ? `Showing options for ${this.productTitle}` : nothing}\n </div>\n </div>\n `;\n }\n\n private _renderAttributeRow(attr: AttributeDef) {\n const oosVisible = this._hasAnyOOSForAttribute(attr);\n return html`\n <div class=\"svp-attribute-row\" data-attribute-row data-attribute-key=${attr.key}>\n <div class=\"svp-attribute-label\" data-attribute-label id=${`attrlabel-${attr.key}`}>${attr.label}</div>\n ${\n attr.values.length >= VariantPanelLit.COLLAPSE_MIN_OPTIONS\n ? this._renderCollapsedAxis(attr)\n : attr.kind === 'swatch'\n ? this._renderSwatchRow(attr)\n : this._renderPillRow(attr)\n }\n ${oosVisible ? html`<div class=\"svp-oos-microcopy\" data-out-of-stock-microcopy>Out of stock for one or more options</div>` : nothing}\n </div>\n `;\n }\n\n // The connected segmented bar's own CSS contract is a 2-3 option envelope:\n // segments split the row width (flex:1, min-width:0, nowrap) and CLIP rather\n // than wrap. Past the envelope every segment ellipsizes into an unreadable\n // sliver (live report: 6 sizes squished illegible, 2026-07-23). Beyond the\n // cap, fall back to the wrapping pill row \u2014 readable pills across rows.\n static readonly SEGMENTED_MAX_OPTIONS = 3;\n /** Density Tier B: a wrap row past this many options becomes a one-row\n * horizontal scroll track (edge-faded, snap) instead of eating the bounded\n * panel's height with wrapped rows. */\n static readonly SCROLL_MIN_OPTIONS = 9;\n /** Density Tier C: past this many options the axis collapses into a\n * select-style trigger + scrollable listbox. ONLY the offending axis\n * collapses \u2014 sibling axes keep their own tier (per Parker, 2026-07-23). */\n static readonly COLLAPSE_MIN_OPTIONS = 17;\n /** Inside a Tier-C listbox, a type-ahead filter appears past this count. */\n static readonly FILTER_MIN_OPTIONS = 25;\n\n private _isScrollTier(attr: AttributeDef): boolean {\n return (\n attr.values.length >= VariantPanelLit.SCROLL_MIN_OPTIONS &&\n attr.values.length < VariantPanelLit.COLLAPSE_MIN_OPTIONS\n );\n }\n\n private _renderPillRow(attr: AttributeDef) {\n const segmented = attr.values.length <= VariantPanelLit.SEGMENTED_MAX_OPTIONS;\n return html`\n <div class=\"svp-pill-row\" data-segmented=${segmented ? '' : nothing} data-scroll=${this._isScrollTier(attr) ? '' : nothing} role=\"radiogroup\" aria-labelledby=${`attrlabel-${attr.key}`}>\n ${attr.values.map((v) => {\n const disabled = this._isValueDisabled(attr.key, v.id);\n const checked = this._selection[attr.key] === v.id;\n return html`<button\n class=\"svp-pill\"\n data-pill\n data-attribute-key=${attr.key}\n data-value-id=${v.id}\n role=\"radio\"\n aria-checked=${checked ? 'true' : 'false'}\n aria-disabled=${disabled ? 'true' : 'false'}\n ?disabled=${disabled}\n @click=${() => this._onPillClick(attr.key, v.id)}\n >${v.label}</button>`;\n })}\n </div>\n `;\n }\n\n private _renderSwatchRow(attr: AttributeDef) {\n const selectedValueId = this._selection[attr.key];\n const selectedLabel = attr.values.find((v) => v.id === selectedValueId)?.label ?? '';\n return html`\n <div class=\"svp-swatch-label\">${selectedLabel}</div>\n <div class=\"svp-swatch-row\" data-scroll=${this._isScrollTier(attr) ? '' : nothing} role=\"radiogroup\" aria-labelledby=${`attrlabel-${attr.key}`}>\n ${attr.values.map((v) => {\n const disabled = this._isValueDisabled(attr.key, v.id);\n const checked = this._selection[attr.key] === v.id;\n const color = v.swatch?.kind === 'color' ? v.swatch.value : 'transparent';\n return html`<button\n class=\"svp-swatch-pill\"\n data-pill\n data-swatch-dot\n data-attribute-key=${attr.key}\n data-value-id=${v.id}\n role=\"radio\"\n aria-checked=${checked ? 'true' : 'false'}\n aria-disabled=${disabled ? 'true' : 'false'}\n aria-label=${v.label}\n ?disabled=${disabled}\n @click=${() => this._onPillClick(attr.key, v.id)}\n ><span class=\"svp-swatch-dot\" style=\"background-color: ${color}\"></span></button>`;\n })}\n </div>\n `;\n }\n\n private _toggleAxis(key: string): void {\n const next = new Set(this._expandedAxes);\n if (next.has(key)) {\n next.delete(key);\n // Stale-filter trap (adversarial review): a filter left behind makes the\n // NEXT open show 3 of 50 options with no visible cause. Closing clears it.\n const { [key]: _dropped, ...rest } = this._axisFilter;\n this._axisFilter = rest;\n } else {\n next.add(key);\n }\n this._expandedAxes = next;\n }\n\n private _onCollapsedSelect(attrKey: string, valueId: string): void {\n this._onPillClick(attrKey, valueId);\n const next = new Set(this._expandedAxes);\n next.delete(attrKey);\n this._expandedAxes = next;\n const { [attrKey]: _dropped, ...rest } = this._axisFilter;\n this._axisFilter = rest;\n void this.updateComplete.then(() => {\n this.querySelector<HTMLElement>(\n `[data-collapse-trigger][data-attribute-key=\"${attrKey}\"]`\n )?.focus();\n });\n }\n\n private _onAxisFilterInput(attrKey: string, e: Event): void {\n this._axisFilter = { ...this._axisFilter, [attrKey]: (e.target as HTMLInputElement).value };\n }\n\n /** Density Tier C \u2014 select-style trigger + bounded scrollable listbox.\n * ONLY this axis collapses; siblings keep their own tier. Filter appears\n * past FILTER_MIN_OPTIONS. Out-of-stock options stay visible, disabled. */\n private _renderCollapsedAxis(attr: AttributeDef) {\n const expanded = this._expandedAxes.has(attr.key);\n const selected = attr.values.find((v) => v.id === this._selection[attr.key]) ?? null;\n const listId = `svp-collapse-list-${attr.key}`;\n const filter = (this._axisFilter[attr.key] ?? '').trim().toLowerCase();\n const filtered = filter\n ? attr.values.filter((v) => v.label.toLowerCase().includes(filter))\n : attr.values;\n const showFilter = attr.values.length >= VariantPanelLit.FILTER_MIN_OPTIONS;\n const dot = (v: { swatch?: { kind: string; value: string } | null } | null) =>\n v?.swatch?.kind === 'color'\n ? html`<span class=\"svp-swatch-dot\" style=\"background-color: ${v.swatch.value}\"></span>`\n : nothing;\n return html`\n <div\n class=\"svp-collapse\"\n data-collapsed-axis\n data-attribute-key=${attr.key}\n @keydown=${(e: KeyboardEvent) => {\n if (e.key === 'Escape' && expanded) {\n e.stopPropagation();\n this._toggleAxis(attr.key);\n void this.updateComplete.then(() => {\n this.querySelector<HTMLElement>(\n `[data-collapse-trigger][data-attribute-key=\"${attr.key}\"]`\n )?.focus();\n });\n }\n }}\n >\n <button\n class=\"svp-collapse-trigger\"\n data-collapse-trigger\n data-attribute-key=${attr.key}\n aria-haspopup=\"listbox\"\n aria-expanded=${expanded ? 'true' : 'false'}\n aria-controls=${listId}\n @click=${() => this._toggleAxis(attr.key)}\n >\n ${dot(selected)}\n <span class=\"svp-collapse-value\">${selected ? selected.label : `Choose ${attr.label.toLowerCase()}`}</span>\n <span class=\"svp-collapse-caret\">${expanded ? '\\u25b4' : '\\u25be'}</span>\n </button>\n ${\n expanded\n ? html`\n <div\n class=\"svp-collapse-list\"\n data-clip-ok\n id=${listId}\n role=\"listbox\"\n aria-labelledby=${`attrlabel-${attr.key}`}\n >\n ${\n showFilter\n ? html`<input\n class=\"svp-collapse-filter\"\n data-collapse-filter\n type=\"text\"\n placeholder=${`Filter ${attr.label.toLowerCase()}\\u2026`}\n .value=${this._axisFilter[attr.key] ?? ''}\n @input=${(e: Event) => this._onAxisFilterInput(attr.key, e)}\n />`\n : nothing\n }\n ${filtered.map((v) => {\n const disabled = this._isValueDisabled(attr.key, v.id);\n const isSelected = this._selection[attr.key] === v.id;\n return html`<button\n class=\"svp-collapse-option\"\n data-collapse-option\n data-value-id=${v.id}\n role=\"option\"\n aria-selected=${isSelected ? 'true' : 'false'}\n aria-disabled=${disabled ? 'true' : 'false'}\n ?disabled=${disabled}\n @click=${() => this._onCollapsedSelect(attr.key, v.id)}\n >${dot(v)}<span class=\"svp-collapse-value\">${v.label}</span></button>`;\n })}\n ${filtered.length === 0 ? html`<div class=\"svp-collapse-empty\">No matches</div>` : nothing}\n </div>`\n : nothing\n }\n </div>\n `;\n }\n}\n\ncustomElements.define('syntro-variant-panel', VariantPanelLit);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-variant-panel': VariantPanelLit;\n }\n}\n", "import { dispatchDiveDeeper } from '@syntrologie/sdk-contracts';\nimport { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { BindController } from '../bind/controller';\nimport { type ComparisonProps, comparisonSchema } from '../schema';\nimport type { Product } from '../schema-primitives';\nimport { RenderHealthController, type RenderHealthProbe } from './renderHealthReporter';\n\ntype ResolvedMap = Record<string, { image?: string; price?: string }>;\n\nexport class ProductComparisonLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n scrollFade: { state: true },\n };\n\n props: ComparisonProps | undefined = undefined;\n resolved: ResolvedMap = {};\n validationError = false;\n /** True when the matrix body overflows its band and is scrollable \u2014 drives\n * the bottom fade mask so the cut-off row reads as \"more below\". */\n scrollFade = false;\n\n #controller: BindController | undefined;\n #parsed: ComparisonProps | undefined;\n\n constructor() {\n super();\n // Runtime self-detection: emits app:product_comparison:blank / :clipped after\n // paint if the mounted matrix has area but paints no content. Self-registers\n // via host.addController \u2014 no stored reference needed.\n new RenderHealthController(this, () => this.#healthProbe());\n }\n\n /** Measure only the SETTLED matrix (valid, parsed props). An invalid/empty\n * comparison renders nothing and is never \"blank\". NON-PII stateKey. */\n #healthProbe(): RenderHealthProbe | null {\n if (this.validationError || !this.#parsed) return null;\n const root = this.querySelector('.sc-product-comparison');\n if (!root) return null;\n return {\n root,\n stateKey: `cmp|${this.#parsed.products.map((p) => p.id).join(',')}`,\n category: 'product_comparison',\n widgetKind: 'adaptive-product:comparison',\n };\n }\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Anchor `color` on the host so descendants inherit from this\n // widget's theme token, not the host's `body { color }`.\n // See `ProductCardLit.connectedCallback` for the full rationale \u2014\n // canvas tile widgets need this; PDP widgets do not.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n // Make the host a shrinkable flex column so it fills \u2014 and can shrink to \u2014\n // a height-bounded band (velvet's deck-card is a bounded flex column with\n // `overflow:hidden`). `min-height:0` lets flex shrink the host below its\n // content height; the section then scrolls INSIDE that bound instead of\n // being clipped by the card. In a roomy (unbounded) canvas the host takes\n // its content height and nothing scrolls \u2014 same as before.\n this.style.display = 'flex';\n this.style.flexDirection = 'column';\n this.style.minHeight = '0';\n this.style.maxHeight = '100%';\n // On RECONNECT the rendered DOM is preserved and Lit does NOT re-run\n // updated(), so the scroll listener (bound in updated(), torn down in\n // disconnectedCallback) would stay unbound and the cue would freeze. Force\n // one update so updated() rebinds it. Harmless on first connect (an update\n // is already pending).\n this.requestUpdate();\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#startBind();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#scrollListenerEl?.removeEventListener('scroll', this.#onBodyScroll);\n this.#scrollListenerEl = null;\n this.#controller?.destroy();\n }\n\n /** The scroll region we've bound the live scroll listener to (re-bind if the\n * body node is recreated across renders). */\n #scrollListenerEl: HTMLElement | null = null;\n #onBodyScroll = (): void => this.#syncScrollCue();\n\n /**\n * True when there is content BELOW the current scroll position \u2014 drives BOTH\n * the bottom fade mask and the bouncing \"scroll for more\" chevron. Position-\n * aware (not just \"overflows\"), with a few-px slack so a sub-pixel remainder at\n * the true bottom doesn't keep the cue on: the arrow must vanish once you've\n * reached the end, exactly like the velvet deck's own scroll chevron.\n */\n #syncScrollCue(): void {\n const body = this.querySelector('[data-comparison-scroll]') as HTMLElement | null;\n if (!body) return;\n const moreBelow = body.scrollHeight - body.scrollTop - body.clientHeight > 8;\n if (moreBelow !== this.scrollFade) this.scrollFade = moreBelow;\n }\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n this.#parsed = undefined;\n return;\n }\n const result = comparisonSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n this.#parsed = undefined;\n console.warn('[adaptive-product] invalid comparison props:', result.error.issues);\n return;\n }\n this.validationError = false;\n this.#parsed = result.data;\n this.#controller = new BindController(result.data.products, (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.#parsed) return html``;\n const parsed = this.#parsed;\n // The tile body is a flex column: the heading is fixed and the matrix lives\n // in a vertically-SCROLLABLE region. When a height-bounded canvas (velvet's\n // deck) constrains the band and the rows exceed it, the body scrolls\n // (`overflow-y:auto`) instead of clipping \u2014 never a cut-off, unreachable\n // row. `data-clip-ok` tells the clip battery this container's own overflow\n // is intentional (a scroll region, not a hidden-content bug); the matrix\n // still guarantees the HORIZONTAL fit (min-width:0 columns). A bottom fade\n // mask (only while overflowing) signals there is more below.\n const bodyStyle =\n // touch-action:pan-y is REQUIRED for the deck swipe-to-cycle to work over a\n // comparison card. This body is a NESTED scroll container (overflow-y:auto)\n // inside the deck card's sc-mount; a scroll container arbitrates touch\n // gestures by its OWN touch-action, and the default `auto` lets the browser\n // claim horizontal pans here \u2014 so a horizontal swipe fires pointercancel and\n // the deck never cycles (the exact BUG-1784140378 failure, one level deeper:\n // that fix put pan-y on the sc-mount HOST, but this inner region overrides\n // it for gestures that start inside it). pan-y keeps vertical scroll of the\n // rows and hands horizontal drags back to the deck's swipe handler.\n 'flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;touch-action:pan-y;' +\n 'scrollbar-width:none;-ms-overflow-style:none;overscroll-behavior:contain;' +\n (this.scrollFade\n ? 'mask-image:linear-gradient(to bottom,#000 calc(100% - 18px),transparent);' +\n '-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 18px),transparent);'\n : '');\n return html`\n <section\n class=\"sc-product-comparison\"\n style=\"display:flex;flex-direction:column;flex:1 1 auto;padding:2px var(--sc-comparison-inset,12px) var(--sc-comparison-inset,12px);box-sizing:border-box;max-width:100%;min-width:0;min-height:0;max-height:100%;overflow:hidden;\"\n >\n <style>\n .sc-product-comparison__scroll-cue {\n position: absolute;\n left: 50%;\n bottom: 5px;\n width: 7px;\n height: 7px;\n transform: translateX(-50%) rotate(45deg);\n border-right: 2px solid currentColor;\n border-bottom: 2px solid currentColor;\n border-bottom-right-radius: 1px;\n opacity: 0.5;\n pointer-events: none;\n animation: sc-cmp-scroll-cue 1.5s ease-in-out infinite;\n }\n @keyframes sc-cmp-scroll-cue {\n 0%,\n 100% {\n transform: translateX(-50%) translateY(0) rotate(45deg);\n }\n 50% {\n transform: translateX(-50%) translateY(3px) rotate(45deg);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n .sc-product-comparison__scroll-cue {\n animation: none;\n }\n }\n </style>\n ${\n parsed.heading\n ? html`<h2\n class=\"sc-product-comparison__heading\"\n data-comparison-heading\n style=\"flex:0 0 auto;margin:0 0 4px;font:600 14px/1.3 var(--sc-font-family,inherit);color:var(--sc-tile-title-color,inherit);min-width:0;overflow-wrap:break-word;\"\n >${parsed.heading}</h2>`\n : nothing\n }\n <div\n class=\"sc-product-comparison__scroll-wrap\"\n style=\"position:relative;display:flex;flex-direction:column;flex:1 1 auto;min-height:0;\"\n >\n <div\n class=\"sc-product-comparison__body\"\n data-comparison-scroll\n data-clip-ok\n style=${bodyStyle}\n >\n ${this.#renderMatrix(parsed.products, this.#visibleRows(parsed))}\n </div>\n ${\n // A bouncing down-chevron pinned to the bottom of the scroll region \u2014\n // the same \"scroll for more\" cue as the velvet deck card, but for the\n // comparison card's OWN inner scroll (the deck-level chevron never\n // fires here because the widget scrolls inside itself, so the card\n // never overflows the deck). currentColor = the card's text token, so\n // it's in-theme; pointer-events:none so it never blocks scroll/taps;\n // gated on scrollFade (content below the current scroll) so it hides\n // at the end. Bounce is dropped under prefers-reduced-motion (style).\n this.scrollFade\n ? html`<span class=\"sc-product-comparison__scroll-cue\" aria-hidden=\"true\"></span>`\n : nothing\n }\n </div>\n ${this.#renderDiveDeeper(parsed)}\n </section>\n `;\n }\n\n /**\n * \"Dive deeper\" affordance (items 4/5). A comparison tile shows a capped,\n * short-blip matrix; rather than expecting the user to scroll a cramped tile\n * for depth, the chip hands the comparison's context back to the CHAT as a\n * deep-dive turn. Pinned bottom-right (flex:0 0 auto, below the scroll body)\n * so it reads as \"in a deck: beside the pager / inline: bottom-right of the\n * tile\". Canvas-agnostic: it dispatches the neutral DIVE_DEEPER_EVENT; the\n * chat-bar mountable turns it into a user turn.\n */\n #renderDiveDeeper(parsed: ComparisonProps): TemplateResult | typeof nothing {\n if (parsed.products.length < 2) return nothing;\n const chipStyle =\n 'flex:0 0 auto;align-self:flex-end;margin-top:6px;padding:4px 10px;border-radius:9999px;' +\n 'border:1px solid var(--sc-color-border,rgba(0,0,0,0.14));background:transparent;cursor:pointer;' +\n 'font:600 12px/1.2 var(--sc-font-family,inherit);color:var(--sc-color-primary,#3d8a5e);' +\n 'white-space:nowrap;';\n return html`<button\n type=\"button\"\n class=\"sc-product-comparison__dive-deeper\"\n data-dive-deeper\n style=${chipStyle}\n @click=${() => this.#onDiveDeeper(parsed)}\n >Dive deeper \u203A</button>`;\n }\n\n #onDiveDeeper(parsed: ComparisonProps): void {\n const names = parsed.products.map((p) => p.name);\n const dims = this.#visibleRows(parsed).map((r) => r.label);\n const title = names.join(' vs ');\n // State an UNMET NEED \u2014 never ask a question (BUG-1784153527).\n //\n // This used to be `Tell me more \u2014 how do ${title} compare?`: a direct\n // imperative in the visitor's own voice. The agent dutifully answered it\n // and never asked what was actually missing \u2014 which is catastrophic when\n // the comparison itself is the problem (live: a \"VelaLift Sports Bra vs\n // RunCloud Quarter Sock Pack\" card, where the agent had to talk its way\n // out with \"the real question isn't which is better...\").\n //\n // A question demands an answer; a complaint invites a question. The tap\n // means \"this tile didn't land\", so that is what the turn says. The\n // product names ride in `title` + the turn-origin envelope \u2014 putting them\n // in the visitor's mouth is what made the turn sound answerable.\n const prompt = \"I'd like to dive deeper on this comparison, I didn't see what I needed.\";\n dispatchDiveDeeper({\n prompt,\n title,\n context: dims.length > 0 ? `Compared on: ${dims.join(', ')}` : undefined,\n });\n }\n\n override updated(changed: Map<string, unknown>): void {\n super.updated(changed);\n // Bind a live scroll listener so the cue (fade + arrow) tracks the scroll\n // position \u2014 hides the moment you reach the bottom, reappears on scroll up.\n // The body div is stable across most re-renders, but rebind if Lit recreates\n // it. Runs after every render (band height is a canvas/layout decision that\n // can change without a `props` change).\n const body = this.querySelector('[data-comparison-scroll]') as HTMLElement | null;\n if (!body) return;\n if (this.#scrollListenerEl !== body) {\n this.#scrollListenerEl?.removeEventListener('scroll', this.#onBodyScroll);\n body.addEventListener('scroll', this.#onBodyScroll, { passive: true });\n this.#scrollListenerEl = body;\n }\n this.#syncScrollCue();\n }\n\n /**\n * The comparison rows to show. The adaptive owns the DATA (all rows, authored or\n * synthesized from attributes); the CANVAS owns the LAYOUT \u2014 including how many\n * rows fit. A vertically-tight canvas (velvet's height-bounded deck) caps the\n * count by setting `--sc-comparison-max-fields`; unset = show every row (roomy\n * canvases / PDP). We render the first N so the assistant's most important\n * dimensions (authored/attribute order) survive the cut.\n */\n #visibleRows(parsed: ComparisonProps): {\n label: string;\n valueA: string;\n valueB: string;\n valueC?: string;\n valueD?: string;\n }[] {\n // Row-integrity guard (mainpeptides filler-cell incident): an AUTHORED row\n // must carry a non-blank value for every MOUNTED product column \u2014 a blank\n // cell is a visitor-facing \"we don't know\" in a tile that exists to answer\n // exactly that question, so the whole row is dropped. Cells beyond\n // products.length (valueC/valueD on a 2-product mount) are never rendered\n // and never gate the row. Synthesized rows keep their explicit \"\u2014\"\n // alignment \u2014 they are catalog-derived, not LLM-authored.\n const all =\n parsed.rows && parsed.rows.length > 0\n ? parsed.rows.filter((row) => this.#rowComplete(row, parsed.products.length))\n : this.#synthesizeRows(parsed.products);\n const max = this.#maxFields();\n return max == null ? all : all.slice(0, max);\n }\n\n /** True when the row has a non-empty (post-trim) cell for every mounted\n * product column \u2014 the first `productCount` of valueA..valueD. */\n #rowComplete(\n row: { valueA: string; valueB: string; valueC?: string; valueD?: string },\n productCount: number\n ): boolean {\n const cells = [row.valueA, row.valueB, row.valueC, row.valueD].slice(0, productCount);\n return cells.every((cell) => (cell ?? '').trim().length > 0);\n }\n\n /** Canvas-set cap on comparison rows (`--sc-comparison-max-fields`), or null for\n * no cap. Read from computed style so it's the canvas's layout decision, not a\n * data/LLM field. */\n #maxFields(): number | null {\n const raw = getComputedStyle(this).getPropertyValue('--sc-comparison-max-fields').trim();\n if (!raw) return null;\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) && n > 0 ? n : null;\n }\n\n /**\n * Build comparison rows from the products' own `attributes` when the assistant\n * didn't author explicit `rows`. A comparison is ALWAYS a side-by-side matrix \u2014\n * the visitor asked to compare, so the comparison (not stacked product cards)\n * is what shows. We take the ordered union of attribute labels across products\n * and align each product's value under it (missing \u2192 \"\u2014\").\n */\n #synthesizeRows(products: readonly Product[]): {\n label: string;\n valueA: string;\n valueB: string;\n valueC?: string;\n valueD?: string;\n }[] {\n const labels: string[] = [];\n for (const p of products) {\n for (const a of p.attributes ?? []) {\n if (!labels.includes(a.label)) labels.push(a.label);\n }\n }\n return labels.map((label) => {\n const [a, b, c, d] = products.map(\n (p) => (p.attributes ?? []).find((x) => x.label === label)?.value ?? '\u2014'\n );\n return { label, valueA: a ?? '\u2014', valueB: b ?? '\u2014', valueC: c, valueD: d };\n });\n }\n\n /**\n * Side-by-side matrix: products are columns, `rows` are dimensions. Used when\n * the assistant authored `rows` to answer the visitor's question. `values[i]`\n * is the cell for `products[i]`; a missing/absent value renders as \"\u2014\".\n */\n #renderMatrix(\n products: readonly Product[],\n rows: readonly {\n label: string;\n valueA: string;\n valueB: string;\n valueC?: string;\n valueD?: string;\n }[]\n ): TemplateResult {\n // `minmax(0, 1fr)` \u2014 NOT `1fr` \u2014 so product columns can shrink below their\n // content width and share the card evenly. A bare `1fr` resolves to\n // `minmax(auto, 1fr)`, whose `auto` min is the content's min-content size, so\n // 3\u20134 columns force the grid wider than the card and it spills off the edge.\n // `min-width:0` + `overflow-wrap:anywhere` on cells lets long values wrap\n // rather than push the column out. The label column stays narrow.\n const cols = `minmax(62px, 0.55fr) repeat(${products.length}, minmax(0, 1fr))`;\n // No external stylesheet targets these classes (the widget is inline-styled),\n // so the matrix carries its own layout. Theme colors come through inherited\n // `--sc-tile-*` vars / currentColor; structure is inline here.\n // Column header (round-3 feedback): ALWAYS a HORIZONTAL mini-row \u2014 rounded\n // image on the LEFT, name + price stacked to its right \u2014 at EVERY product\n // count. The tile has horizontal space, not vertical, so the old \u22653-product\n // \"stacked\" fallback (image ABOVE the name) is gone. At 3+ products each\n // column is narrow, so the header goes COMPACT: smaller thumb (20px) and\n // smaller name type (0.7rem); names still wrap at word boundaries \u2014 never\n // ellipsized (Tier-1 identity text) \u2014 and min-width:0 everywhere preserves\n // the matrix HORIZONTAL fit guarantee (columns shrink, text wraps inside).\n const compactHeaders = products.length >= 3;\n // RECO-CARD column header (user-approved direction, 2026-07-08): the\n // header IS a large image card \u2014 the reco carousel's visual vocabulary\n // (full-bleed image, bottom gradient scrim, text overlaid on the scrim)\n // plus the product card's corner pills. Replaces the 20-24px thumb\n // mini-row and the intermediate banner/split iterations.\n const colHeadStyle =\n 'display:block;padding:6px 3px;text-align:left;font-weight:600;min-width:0;';\n const labelStyle =\n 'padding:8px 4px 8px 2px;font-weight:600;font-size:0.75rem;opacity:0.75;border-top:1px solid var(--sc-tile-border-color,rgba(0,0,0,0.08));align-self:stretch;min-width:0;overflow-wrap:break-word;hyphens:auto;';\n const cellStyle =\n 'padding:8px 6px;border-top:1px solid var(--sc-tile-border-color,rgba(0,0,0,0.08));align-self:stretch;min-width:0;overflow-wrap:anywhere;';\n return html`\n <div\n class=\"sc-product-comparison__matrix\"\n data-comparison-matrix\n role=\"table\"\n style=${`display:grid;grid-template-columns:${cols};column-gap:4px;font-size:0.8rem;line-height:1.3;max-width:100%;`}\n >\n <div role=\"row\" style=\"display:contents;\">\n <!-- The top-left corner cell is intentionally BLANK visually \u2014 the row\n labels below make it self-evident these are product attributes, so\n the header just spent vertical space restating the obvious (user\n feedback). Kept as a grid cell so the product columns still align,\n and the accessible name is preserved via aria-label for the a11y\n table semantics (composed-accessibility work, 2026-07-12). -->\n <span\n class=\"sc-product-comparison__matrix-corner\"\n role=\"columnheader\"\n aria-label=\"Product attributes\"\n style=\"padding:6px 3px;font-weight:600;font-size:0.7rem;line-height:1.2;overflow-wrap:anywhere;\"\n ><span\n style=\"position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;\"\n >Product attributes</span\n ></span\n >\n ${products.map((p) => {\n const resolved = this.resolved[p.id];\n const imgSrc = resolved?.image ?? p.image.src;\n const priceText = resolved?.price ?? p.price?.amount;\n // The product NAME is the link to its personalized PDP \u2014 there is no\n // separate \"View\" CTA (it cost a whole row of vertical space, which\n // canvases like velvet can't spare). The target is the first CTA that\n // NAVIGATES (carries an href) \u2014 NOT `ctas[0]`, which in the standard\n // product CTA set is \"Add to cart\" (an action with no href); the PDP\n // link is a later CTA (e.g. \"Product Page\"). If none navigates, the\n // name is plain text rather than a dead link.\n const navCta = p.ctas.find((c) => c.href);\n const pdpHref = navCta?.href;\n // RECO-CARD header: corner pills (rating upper-left at 2 products;\n // price upper-right always) + NAME overlaid along the bottom on the\n // reco gradient scrim (white + text-shadow, wraps, never\n // ellipsized). The whole card is the PDP link when one exists \u2014\n // same tap semantics as a reco carousel item.\n const pillBase =\n 'position:absolute;top:4px;display:inline-flex;align-items:center;' +\n 'background:rgba(17,17,17,0.66);color:#fff;border-radius:9999px;' +\n `padding:0.12rem 0.36rem;font-size:${compactHeaders ? '0.56rem' : '0.64rem'};` +\n 'line-height:1.2;font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,0.25);' +\n 'max-width:calc(100% - 8px);min-width:0;box-sizing:border-box;' +\n 'white-space:normal;overflow-wrap:anywhere;';\n const showRatingPill = !compactHeaders && p.rating;\n const nameOverlayStyle = compactHeaders\n ? 'display:block;font-weight:600;font-size:0.66rem;line-height:1.2;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.55);white-space:normal;overflow-wrap:break-word;min-width:0;'\n : 'display:block;font-weight:600;font-size:0.76rem;line-height:1.25;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.55);white-space:normal;overflow-wrap:break-word;min-width:0;';\n // 19/10 (\u22481.9) default = ~30% shorter than the original 4/3 card at\n // the same width. The 8/3 interim (2026-07-09) halved the height,\n // which read as too aggressive; ~30% balances the header image\n // against the spec matrix (2026-07-12 user direction). Still\n // overridable per-workspace via the --sc-comparison-image-aspect token.\n const mediaStyle =\n 'position:relative;display:block;width:100%;aspect-ratio:var(--sc-comparison-image-aspect, 19 / 10);' +\n 'border-radius:10px;overflow:hidden;background:#e5e7eb;text-decoration:none;';\n const cardInner = html`\n <img\n class=\"sc-product-comparison__thumb\"\n src=${imgSrc}\n alt=${p.image.alt}\n loading=\"lazy\"\n style=\"position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\"\n />\n <span aria-hidden=\"true\" style=\"position:absolute;inset:auto 0 0 0;height:62%;background:linear-gradient(transparent,rgba(0,0,0,.62));\"></span>\n ${\n showRatingPill && p.rating\n ? html`<span\n class=\"sc-product-comparison__rating-pill\"\n data-product-rating\n data-rating-value=${p.rating.value}\n aria-label=${`${p.rating.value} out of ${p.rating.max ?? 5}, ${p.rating.count} reviews`}\n style=${`${pillBase}left:4px;`}\n ><span style=\"color:#f5a623;\">\u2605</span><span style=\"margin-left:0.12rem;\">${p.rating.value}</span></span\n >`\n : nothing\n }\n ${\n priceText\n ? html`<span\n class=\"sc-product-comparison__price\"\n data-critical-text\n style=${`${pillBase}right:4px;`}\n >${priceText}</span\n >`\n : nothing\n }\n <span\n class=\"sc-product-comparison__name\"\n data-critical-text\n style=${`position:absolute;left:8px;right:8px;bottom:6px;${nameOverlayStyle}`}\n >${p.name}</span\n >`;\n return html`<div\n class=\"sc-product-comparison__matrix-col\"\n role=\"columnheader\"\n data-product-id=${p.id}\n style=${colHeadStyle}\n >\n ${\n pdpHref\n ? html`<a\n class=\"sc-product-comparison__media\"\n data-comparison-media\n data-product-cta=\"primary\"\n data-product-name-link\n href=${pdpHref}\n target=${navCta?.target ?? '_self'}\n rel=${navCta?.target === '_blank' ? 'noopener noreferrer' : ''}\n data-action-id=${navCta?.actionId ?? ''}\n data-variant=${navCta?.variant ?? ''}\n style=${mediaStyle}\n >${cardInner}</a>`\n : html`<div class=\"sc-product-comparison__media\" data-comparison-media style=${mediaStyle}>${cardInner}</div>`\n }\n </div>`;\n })}\n </div>\n ${rows.map((row) => {\n const cells = [row.valueA, row.valueB, row.valueC, row.valueD];\n return html`\n <div role=\"row\" style=\"display:contents;\">\n <span class=\"sc-product-comparison__matrix-label\" role=\"rowheader\" style=${labelStyle}\n >${row.label}</span\n >\n ${products.map(\n (_p, i) =>\n html`<span class=\"sc-product-comparison__matrix-cell\" role=\"cell\" style=${cellStyle}\n >${cells[i] ?? '\u2014'}</span\n >`\n )}\n </div>\n `;\n })}\n </div>\n `;\n }\n}\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { BindController } from '../bind/controller';\nimport { type GridProps, gridSchema } from '../schema';\nimport { renderProductCard } from './ProductCardLit';\n\nexport class ProductGridLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n };\n\n props: GridProps | undefined = undefined;\n resolved: Record<string, { image?: string; price?: string }> = {};\n validationError = false;\n\n #controller: BindController | undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Anchor `color` on the host so descendants inherit from this\n // widget's theme token, not the host's `body { color }`.\n // See `ProductCardLit.connectedCallback` for the full rationale \u2014\n // canvas tile widgets need this; PDP widgets do not.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#startBind();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#controller?.destroy();\n }\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n return;\n }\n const result = gridSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n console.warn('[adaptive-product] invalid grid props:', result.error.issues);\n return;\n }\n this.validationError = false;\n this.#controller = new BindController(result.data.products, (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.props) return html``;\n const parsed = gridSchema.safeParse(this.props);\n if (!parsed.success) return html``;\n const { products, desktopColumns, heading } = parsed.data;\n return html`\n <section class=\"sc-product-grid\">\n ${\n heading\n ? html`<h2 data-grid-heading class=\"sc-product-grid__heading\">${heading}</h2>`\n : nothing\n }\n <div\n data-grid-root\n class=\"sc-product-grid__cells\"\n style=${`--sc-product-grid-cols:${desktopColumns}`}\n >\n ${products.map(\n (p) => html`<div data-grid-cell class=\"sc-product-grid__cell\">\n ${renderProductCard(p, 'standard', this.resolved[p.id])}\n </div>`\n )}\n </div>\n </section>\n `;\n }\n}\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { BindController } from '../bind/controller';\nimport { sanitizeHtml } from '../sanitizer';\nimport { type HeroProps, heroSchema } from '../schema';\n\nexport class ProductHeroLit extends LitElement {\n static override properties = {\n props: { attribute: false },\n resolved: { state: true },\n validationError: { state: true },\n };\n\n props: HeroProps | undefined = undefined;\n resolved: Record<string, { image?: string; price?: string }> = {};\n validationError = false;\n\n #controller: BindController | undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n // Anchor `color` on the host so descendants inherit from this\n // widget's theme token, not the host's `body { color }`.\n // See `ProductCardLit.connectedCallback` for the full rationale \u2014\n // canvas tile widgets need this; PDP widgets do not.\n this.style.color = 'var(--sc-tile-text-color, rgba(20, 20, 20, 0.92))';\n }\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (changed.has('props')) {\n this.#startBind();\n }\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#controller?.destroy();\n }\n\n #startBind(): void {\n this.#controller?.destroy();\n this.resolved = {};\n if (!this.props) {\n this.validationError = false;\n return;\n }\n const result = heroSchema.safeParse(this.props);\n if (!result.success) {\n this.validationError = true;\n console.warn('[adaptive-product] invalid hero props:', result.error.issues);\n return;\n }\n this.validationError = false;\n this.#controller = new BindController([result.data.product], (productId, kind, value) => {\n if (value == null) return;\n this.resolved = {\n ...this.resolved,\n [productId]: { ...this.resolved[productId], [kind]: value },\n };\n });\n this.#controller.start();\n }\n\n override render(): TemplateResult | typeof nothing {\n if (this.validationError || !this.props) return html``;\n const parsed = heroSchema.safeParse(this.props);\n if (!parsed.success) return html``;\n const { product, layout, longCopy } = parsed.data;\n const resolved = this.resolved[product.id];\n const imgSrc = resolved?.image ?? product.image.src;\n const priceText = resolved?.price ?? product.price?.amount;\n // Constrain image to thumbnail size \u2014 hero is \"marketing-scale\" by\n // longCopy + framing weight, not by image size. Default thumb sits\n // beside the content (image-left / image-right) or as a slim banner\n // strip on top (image-top).\n const isTopLayout = layout === 'image-top';\n const sectionStyles = isTopLayout\n ? 'display:block;'\n : `display:flex;flex-direction:${layout === 'image-right' ? 'row-reverse' : 'row'};gap:1rem;align-items:flex-start;`;\n const imageStyles = isTopLayout\n ? 'width:100%;max-height:160px;object-fit:cover;border-radius:8px;display:block;margin-bottom:0.75rem;'\n : 'width:128px;height:128px;object-fit:cover;border-radius:8px;flex-shrink:0;';\n const contentStyles = isTopLayout ? '' : 'flex:1;min-width:0;';\n return html`\n <section class=\"sc-product-hero\" data-hero-layout=${layout} style=${sectionStyles}>\n <img\n class=\"sc-product-hero__image\"\n src=${imgSrc}\n alt=${product.image.alt}\n loading=\"lazy\"\n style=${imageStyles}\n />\n <div class=\"sc-product-hero__content\" style=${contentStyles}>\n <h2 class=\"sc-product-hero__name\">${product.name}</h2>\n ${\n product.tagline\n ? html`<p class=\"sc-product-hero__tagline\">${product.tagline}</p>`\n : nothing\n }\n ${\n product.price\n ? html`<p class=\"sc-product-hero__price\">\n ${priceText}${\n product.price.cadence ? html` <small>${product.price.cadence}</small>` : nothing\n }\n </p>`\n : nothing\n }\n ${\n longCopy\n ? html`<div\n data-hero-longcopy\n class=\"sc-product-hero__longcopy\"\n .innerHTML=${sanitizeHtml(longCopy)}\n ></div>`\n : nothing\n }\n <div class=\"sc-product-hero__ctas\">\n ${product.ctas.map(\n (c, i) => html`<a\n class=\"sc-product-hero__cta\"\n data-product-cta=${i === 0 ? 'primary' : 'secondary'}\n data-variant=${c.variant}\n href=${c.href ?? '#'}\n target=${c.target}\n rel=${c.target === '_blank' ? 'noopener noreferrer' : ''}\n >${c.label}</a\n >`\n )}\n </div>\n </div>\n </section>\n `;\n }\n}\n", "import { html, LitElement, nothing, type TemplateResult } from 'lit';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport { type RecoProps, recoSchema } from '../schema';\nimport { RenderHealthController, type RenderHealthProbe } from './renderHealthReporter';\n\nexport class ProductRecoCarouselLit extends LitElement {\n static override properties = { props: { attribute: false }, _parsed: { state: true } };\n static readonly _testing = {\n healthProbe: (host: ProductRecoCarouselLit): RenderHealthProbe | null => host.#healthProbe(),\n };\n declare props: RecoProps | undefined;\n private declare _parsed: RecoProps | undefined;\n constructor() {\n super();\n this.props = undefined;\n this._parsed = undefined;\n // Runtime self-detection: emits app:product_reco:blank / :clipped after paint\n // if the mounted carousel has area but paints no content (the reco-blank bug).\n // Self-registers via host.addController \u2014 no stored reference needed.\n new RenderHealthController(this, () => this.#healthProbe());\n }\n\n /** Measure only the SETTLED carousel (items rendered). An empty carousel\n * renders nothing and is never \"blank\". NON-PII stateKey. */\n #healthProbe(): RenderHealthProbe | null {\n const items = this._parsed?.items ?? [];\n if (items.length === 0) return null;\n const root = this.querySelector('.sc-reco');\n if (!root) return null;\n return {\n root,\n stateKey: `reco|${items.length}|${items[0]?.pdp_href ?? ''}`,\n category: 'product_reco',\n widgetKind: 'adaptive-product:reco',\n };\n }\n override createRenderRoot() {\n return this;\n }\n override willUpdate(changed: Map<string, unknown>) {\n if (changed.has('props')) {\n const r = this.props ? recoSchema.safeParse(this.props) : undefined;\n this._parsed = r?.success ? r.data : undefined;\n }\n }\n override render(): TemplateResult | typeof nothing {\n const items = this._parsed?.items ?? [];\n if (items.length === 0) return html``;\n return html`\n <section class=\"sc-reco\" style=\"box-sizing:border-box;display:flex;flex-direction:column;height:100%;padding:2px var(--sc-reco-inset,12px) var(--sc-reco-inset,12px);\">\n <div class=\"sc-reco__carousel\" role=\"list\" style=\"display:flex;align-items:stretch;gap:8px;flex:1 1 auto;min-height:0;overflow-x:auto;scroll-snap-type:x mandatory;\">\n ${items.map(\n (it) => html`\n <div role=\"listitem\" style=\"display:contents;\">\n <a data-reco-item data-product-cta=\"primary\" href=${it.pdp_href}\n style=${styleMap({ position: 'relative', flex: '0 0 auto', aspectRatio: '4 / 5', minHeight: 'var(--sc-reco-item-min-h, 162px)', maxHeight: 'var(--sc-reco-item-max-h, 340px)', borderRadius: '12px', overflow: 'hidden', scrollSnapAlign: 'start', textDecoration: 'none', display: 'block', background: '#e5e7eb' })}>\n ${it.image_url ? html`<img src=${it.image_url} alt=${it.hook ?? 'Recommended product'} loading=\"lazy\" style=\"position:absolute;inset:0;width:100%;height:100%;object-fit:cover;\">` : nothing}\n <span style=\"position:absolute;inset:auto 0 0 0;height:60%;background:linear-gradient(transparent,rgba(0,0,0,.62));\"></span>\n <span style=\"position:absolute;left:8px;right:8px;bottom:8px;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.5);\">\n ${it.price ? html`<span style=\"display:block;font-weight:700;font-size:0.9rem;\">${it.price}</span>` : nothing}\n ${it.hook ? html`<span style=\"display:block;font-size:0.72rem;line-height:1.2;opacity:.95;\">${it.hook}</span>` : nothing}\n </span>\n </a>\n </div>`\n )}\n </div>\n </section>`;\n }\n}\nif (!customElements.get('syntro-product-reco-carousel'))\n customElements.define('syntro-product-reco-carousel', ProductRecoCarouselLit);\n", "import { html, LitElement } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\ninterface TrendingNewsItem {\n headline: string;\n source: string;\n published_relative: string;\n blurb: string;\n /** Optional click-through URL to the source article. When present the\n * widget renders the headline as a link opening in a new tab with\n * `rel=\"noopener noreferrer\"`. Sub-agent-generated content omits this\n * field; curated seed content includes a real URL. */\n url?: string | null;\n}\n\ninterface TrendingNewsData {\n items: TrendingNewsItem[];\n rationale: string;\n}\n\n/**\n * Inject the trending-news widget's stylesheet once per document.\n * Idempotent \u2014 safe to call from connectedCallback on every instance.\n */\nfunction ensureTrendingNewsStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-trending-news-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-trending-news-style';\n style.textContent = TRENDING_NEWS_CSS;\n document.head.appendChild(style);\n}\n\nconst TRENDING_NEWS_CSS = `\n.pdp-trending-news {\n --tn-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --tn-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --tn-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --tn-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --tn-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --tn-accent: var(--syntro-pdp-accent, hsl(95 18% 48%));\n --tn-display: var(--syntro-pdp-display, inherit);\n --tn-sans: var(--syntro-pdp-sans, inherit);\n\n display: grid;\n grid-template-columns: 1fr;\n gap: 18px;\n font-family: var(--tn-sans);\n color: var(--tn-fg);\n}\n\n@media (min-width: 900px) {\n .pdp-trending-news {\n grid-template-columns: 1.4fr 1fr;\n gap: 24px;\n grid-auto-rows: minmax(0, auto);\n }\n}\n\n/* Featured card \u2014 first item, spans both rows on desktop */\n.pdp-tn-card {\n position: relative;\n /* Card surface derives from the theme card-bg + primary tokens so it tracks\n the host theme (dark on healthmaxxer, light on vela) instead of a\n hardcoded dark base that rendered dark-on-dark under a light-host fg. */\n background:\n radial-gradient(\n 120% 80% at 0% 0%,\n color-mix(in srgb, var(--tn-primary) 7%, transparent) 0%,\n transparent 55%\n ),\n linear-gradient(180deg, color-mix(in srgb, var(--tn-bg) 94%, white), var(--tn-bg));\n border: 1px solid var(--tn-border);\n border-radius: 18px;\n padding: 22px 22px 22px 24px;\n overflow: hidden;\n transition:\n transform 200ms ease,\n border-color 200ms ease,\n box-shadow 200ms ease;\n /* Resting state VISIBLE (GPU/compositing hazard fix): cards default to\n opacity 1; the stagger is re-armed only under .pdp-trending-news.is-entering\n so a suppressed animation can never leave the trending grid blank. */\n opacity: 1;\n}\n.pdp-trending-news.is-entering .pdp-tn-card {\n opacity: 0;\n transform: translateY(18px);\n animation: pdp-tn-fade-up 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n}\n.pdp-tn-card:hover {\n border-color: color-mix(in srgb, var(--tn-primary) 55%, transparent);\n box-shadow: 0 12px 50px -16px color-mix(in srgb, var(--tn-primary) 25%, transparent);\n transform: translateY(-2px);\n}\n\n/* stagger arrival */\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(1) {\n animation-delay: 60ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(2) {\n animation-delay: 160ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(3) {\n animation-delay: 260ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(4) {\n animation-delay: 360ms;\n}\n.pdp-trending-news.is-entering .pdp-tn-card:nth-child(5) {\n animation-delay: 460ms;\n}\n\n.pdp-tn-card.is-featured {\n grid-row: span 2;\n padding: 28px 28px 28px 30px;\n background:\n radial-gradient(\n 130% 100% at 0% 0%,\n color-mix(in srgb, var(--tn-primary) 13%, transparent) 0%,\n transparent 55%\n ),\n radial-gradient(\n 90% 70% at 100% 100%,\n color-mix(in srgb, var(--tn-accent) 10%, transparent) 0%,\n transparent 60%\n ),\n linear-gradient(180deg, color-mix(in srgb, var(--tn-bg) 92%, white), var(--tn-bg));\n}\n\n.pdp-tn-meta {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 11px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n}\n.pdp-tn-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--tn-primary);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--tn-primary) 18%, transparent);\n animation: pdp-tn-pulse 2400ms ease-in-out infinite;\n flex: 0 0 auto;\n}\n.pdp-tn-meta-source {\n color: var(--tn-fg);\n font-weight: 600;\n letter-spacing: 0.08em;\n}\n.pdp-tn-meta-sep {\n opacity: 0.4;\n}\n\n.pdp-tn-headline {\n font-family: var(--tn-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 60;\n margin: 14px 0 10px;\n line-height: 1.12;\n letter-spacing: -0.012em;\n color: var(--tn-fg);\n font-size: clamp(19px, 2.6vw, 22px);\n font-feature-settings: 'liga' 1, 'ss01' 1;\n /* Avoid mid-word breaks at narrow widths. */\n overflow-wrap: break-word;\n hyphens: none;\n}\n.pdp-tn-card.is-featured .pdp-tn-headline {\n font-size: clamp(24px, 4.4vw, 34px);\n font-weight: 400;\n margin: 18px 0 14px;\n font-variation-settings: 'SOFT' 90, 'WONK' 1;\n}\n\n.pdp-tn-blurb {\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--tn-fg-muted);\n margin: 0;\n max-width: 56ch;\n /* Clamp to 4 lines with ellipsis when the blurb is longer than the\n card can comfortably hold. Schema allows up to ~280 chars; this\n keeps narrow viewports + the 5-card grid visually balanced\n regardless of source content length. */\n display: -webkit-box;\n -webkit-line-clamp: 4;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.pdp-tn-card.is-featured .pdp-tn-blurb {\n font-size: 16px;\n line-height: 1.55;\n max-width: 50ch;\n /* Featured card has more vertical room \u2014 allow one more line. */\n -webkit-line-clamp: 5;\n}\n\n@media (max-width: 640px) {\n .pdp-tn-card,\n .pdp-tn-card.is-featured {\n padding: 18px 18px 20px;\n border-radius: 16px;\n }\n .pdp-tn-meta {\n font-size: 10px;\n }\n .pdp-tn-blurb {\n font-size: 13.5px;\n }\n .pdp-tn-card.is-featured .pdp-tn-blurb {\n font-size: 14.5px;\n }\n}\n\n/* Click-through link wrapper on the headline. We use a bare <a>\n inheriting type styles so the headline visual stays identical when\n a URL is or isn't present \u2014 the only affordance is a subtle hover\n underline + \\\\2197 (north-east arrow) glyph indicating an external destination. */\n.pdp-tn-headline-link {\n color: inherit;\n text-decoration: none;\n background-image: linear-gradient(currentColor, currentColor);\n background-position: 0 100%;\n background-repeat: no-repeat;\n background-size: 0 1px;\n transition: background-size 220ms ease;\n}\n.pdp-tn-headline-link:hover,\n.pdp-tn-headline-link:focus-visible {\n background-size: 100% 1px;\n outline: none;\n}\n.pdp-tn-headline-link::after {\n content: ' \\\\2197';\n font-family: var(--tn-sans);\n font-size: 0.65em;\n letter-spacing: 0;\n color: var(--tn-primary);\n opacity: 0.55;\n vertical-align: 0.18em;\n margin-left: 0.2em;\n transition: opacity 220ms ease;\n}\n.pdp-tn-headline-link:hover::after,\n.pdp-tn-headline-link:focus-visible::after {\n opacity: 1;\n}\n\n.pdp-tn-corner-mark {\n position: absolute;\n top: 14px;\n right: 16px;\n font-family: var(--tn-display);\n font-style: italic;\n font-size: 11px;\n letter-spacing: 0.06em;\n color: color-mix(in srgb, var(--tn-primary) 70%, transparent);\n padding: 3px 9px 3px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--tn-primary) 12%, transparent);\n pointer-events: none;\n}\n.pdp-tn-card.is-featured .pdp-tn-corner-mark::before {\n /* \\\\2726 = four-pointed star, CSS-escaped like \\\\2197 above \u2014 the raw\n glyph shipped double-encoded (garbled \"featured\" badge on the live modal). */\n content: '\\\\2726 ';\n font-style: normal;\n}\n\n@media (max-width: 640px) {\n .pdp-tn-corner-mark {\n /* On narrow viewports the meta + corner mark fight for the same row.\n Lift the mark above the meta so neither truncates. */\n position: static;\n display: inline-block;\n margin-bottom: 10px;\n align-self: flex-start;\n }\n .pdp-tn-card.is-featured {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n }\n}\n\n/* skeleton */\n.pdp-trending-news-skeleton {\n display: grid;\n gap: 14px;\n}\n.pdp-skeleton-bar {\n height: 16px;\n border-radius: 6px;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--tn-bg) 92%, white) 0%,\n color-mix(in srgb, var(--tn-bg) 78%, white) 50%,\n color-mix(in srgb, var(--tn-bg) 92%, white) 100%\n );\n background-size: 200% 100%;\n animation: pdp-tn-shimmer 1.6s ease-in-out infinite;\n}\n\n@keyframes pdp-tn-fade-up {\n from {\n opacity: 0;\n transform: translateY(18px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n@keyframes pdp-tn-pulse {\n 0%,\n 100% {\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--tn-primary) 18%, transparent);\n }\n 50% {\n box-shadow: 0 0 0 6px color-mix(in srgb, var(--tn-primary) 28%, transparent);\n }\n}\n@keyframes pdp-tn-shimmer {\n 0% {\n background-position: -200% 0;\n }\n 100% {\n background-position: 200% 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-trending-news.is-entering .pdp-tn-card {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n`;\n\nexport class PdpTrendingNewsLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: TrendingNewsData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n // Light DOM so the parent's CSS variables and tailwind utilities flow through.\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureTrendingNewsStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-trending-news');\n if (!container) return;\n this._armed = true;\n // staggered cards: last delay 460ms + 700ms duration.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1400\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-trending-news-skeleton\">\n <div class=\"pdp-skeleton-bar\" style=\"width:60%\"></div>\n <div class=\"pdp-skeleton-bar\" style=\"width:90%\"></div>\n <div class=\"pdp-skeleton-bar\" style=\"width:80%\"></div>\n <div class=\"pdp-skeleton-bar\" style=\"width:70%\"></div>\n </div>`;\n }\n\n const items = this.data.items;\n const featured = items[0];\n const rest = items.slice(1);\n\n return html`\n <section\n class=\"${this._entering ? 'pdp-trending-news is-entering' : 'pdp-trending-news'}\"\n aria-label=\"Trending news\"\n >\n <article class=\"pdp-tn-card is-featured\">\n <div class=\"pdp-tn-meta\">\n <span class=\"pdp-tn-dot\" aria-hidden=\"true\"></span>\n <span class=\"pdp-tn-meta-source\">${featured.source}</span>\n <span class=\"pdp-tn-meta-sep\">\u00B7</span>\n <span>${featured.published_relative}</span>\n </div>\n <span class=\"pdp-tn-corner-mark\">featured</span>\n <h3 class=\"pdp-tn-headline\">${renderHeadlineLink(featured)}</h3>\n <p class=\"pdp-tn-blurb\">${featured.blurb}</p>\n </article>\n ${rest.map(\n (item) => html`\n <article class=\"pdp-tn-card\">\n <div class=\"pdp-tn-meta\">\n <span class=\"pdp-tn-dot\" aria-hidden=\"true\"></span>\n <span class=\"pdp-tn-meta-source\">${item.source}</span>\n <span class=\"pdp-tn-meta-sep\">\u00B7</span>\n <span>${item.published_relative}</span>\n </div>\n <h3 class=\"pdp-tn-headline\">${renderHeadlineLink(item)}</h3>\n <p class=\"pdp-tn-blurb\">${item.blurb}</p>\n </article>\n `\n )}\n </section>\n `;\n }\n}\n\n/** Render the headline as a click-through link when ``url`` is present;\n * fall back to bare text when it isn't. Sub-agent-generated content\n * omits the URL (no hallucinated links); curated seed content\n * includes a real URL so visitors can read the full article. */\nfunction renderHeadlineLink(item: TrendingNewsItem) {\n if (!item.url) return html`${item.headline}`;\n return html`<a\n class=\"pdp-tn-headline-link\"\n href=${item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >${item.headline}</a\n >`;\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-trending-news')) {\n customElements.define('syntro-pdp-trending-news', PdpTrendingNewsLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-trending-news': PdpTrendingNewsLit;\n }\n}\n", "/**\n * Entrance-animation settle helper \u2014 GPU/compositing hazard fix.\n *\n * The PDP widgets play a fade/slide entrance on mount. The old authoring\n * rested the animated element at `opacity: 0` and relied on\n * `animation: \u2026 forwards` to bring it to `opacity: 1`. When the browser\n * DROPS or SUPPRESSES that animation \u2014 battery-saver mode, an over-budget\n * compositor, a backgrounded tab that never runs the keyframes \u2014 the element\n * stays at `opacity: 0` forever: a permanently blank PDP section. (Same class\n * of bug as the ProductCard/FAQ flip resting at rotateY(180).)\n *\n * The fix keeps the entrance PIXEL-IDENTICAL but inverts the resting state:\n * - base rule: `opacity: 1` (content is always visible at rest);\n * - a transient `.is-entering` class (applied on the container at mount)\n * re-introduces `opacity: 0` + the exact same keyframes with `both` fill\n * so the pre-delay invisibility is preserved;\n * - `@media (prefers-reduced-motion: reduce)` neutralises `.is-entering`\n * (content immediately visible, no animation).\n *\n * This helper arms the REMOVAL of `.is-entering` once the entrance has run:\n * - fast path: the container's OWN `animationend` (only when `fastPath` is\n * set \u2014 i.e. the container's own keyframes are the last to finish; widgets\n * with longer-running staggered children leave this off and lean on the\n * belt so the class is never pulled mid-stagger);\n * - belt: a `setTimeout` sized to the longest keyframe\n * (delay + duration + margin) that ALWAYS fires, even if `animationend`\n * never does.\n *\n * Removing the class is a visual no-op: the base state is `opacity: 1` and the\n * `both` fill holds the final keyframe until the moment the class drops.\n *\n * @returns a disposer that clears the belt timer + listener (call on disconnect).\n */\nexport function armEntranceSettle(\n onSettle: () => void,\n container: HTMLElement | null,\n settleMs: number,\n fastPath = false\n): () => void {\n let done = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const cleanup = (): void => {\n if (timer !== undefined) {\n clearTimeout(timer);\n timer = undefined;\n }\n container?.removeEventListener('animationend', onAnimationEnd);\n };\n\n const settle = (): void => {\n if (done) return;\n done = true;\n cleanup();\n onSettle();\n };\n\n const onAnimationEnd = (event: Event): void => {\n // Only settle from the container's OWN entrance keyframes. Staggered child\n // animations bubble here too; ignoring them keeps the belt authoritative so\n // the class is never removed before the last child finishes.\n if (fastPath && event.target === container) settle();\n };\n\n container?.addEventListener('animationend', onAnimationEnd);\n timer = setTimeout(settle, settleMs);\n\n return cleanup;\n}\n", "import { html, LitElement } from 'lit';\n\ninterface TrendingNewsItem {\n headline: string;\n source: string;\n published_relative: string;\n blurb: string;\n /** Optional click-through URL \u2014 see PdpTrendingNewsLit.ts for the\n * curated-seed-vs-sub-agent rationale. */\n url?: string | null;\n}\n\ninterface TrendingNewsData {\n items: TrendingNewsItem[];\n rationale: string;\n}\n\n/**\n * Magazine-Column variant for the trending-news module.\n *\n * Typographic-first single-column reading column. Lead item gets a\n * drop-cap on its blurb; subsequent items use a tight meta-line +\n * headline pair separated by hairline rules. No card chrome, no\n * gradients \u2014 the editorial density comes from type scale and\n * whitespace discipline.\n *\n * HIGH typographic density. Best between a LOW-density module\n * (pull-quote, full-bleed) and another LOW-density module.\n */\nfunction ensureMagazineStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-trending-news-magazine-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-trending-news-magazine-style';\n style.textContent = MAGAZINE_CSS;\n document.head.appendChild(style);\n}\n\nconst MAGAZINE_CSS = `\n.pdp-tn-mag {\n --tn-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --tn-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --tn-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --tn-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --tn-display: var(--syntro-pdp-display, inherit);\n --tn-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--tn-sans);\n color: var(--tn-fg);\n max-width: 640px;\n margin: 0 auto;\n padding: 8px 4px;\n}\n\n.pdp-tn-mag__kicker {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--tn-accent);\n font-weight: 600;\n margin-bottom: 18px;\n}\n.pdp-tn-mag__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--tn-accent);\n opacity: 0.8;\n}\n\n.pdp-tn-mag__lead {\n border-bottom: 1px solid var(--tn-rule);\n padding-bottom: 22px;\n margin-bottom: 22px;\n}\n\n.pdp-tn-mag__meta {\n font-size: 11px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n margin-bottom: 10px;\n}\n.pdp-tn-mag__meta-source {\n color: var(--tn-fg);\n font-weight: 600;\n}\n\n.pdp-tn-mag__headline {\n font-family: var(--tn-display);\n font-weight: 450;\n font-variation-settings: 'SOFT' 80;\n line-height: 1.08;\n letter-spacing: -0.018em;\n margin: 0 0 14px;\n font-size: clamp(26px, 5.2vw, 34px);\n font-feature-settings: 'liga' 1, 'ss01' 1;\n}\n\n.pdp-tn-mag__blurb {\n font-family: var(--tn-display);\n font-weight: 380;\n font-size: 16.5px;\n line-height: 1.55;\n color: hsl(30 8% 78%);\n margin: 0;\n}\n/* NB: lead blurb is deliberately NOT clamped. The lead carries a\n ::first-letter drop-cap (float: left, 3.3em) \u2014 'float' doesn't\n participate in '-webkit-box' line counting, so combining the two\n can cause the drop-cap to overlap clamped text or misplace the\n ellipsis in WebKit/Blink. The lead is one blurb per card and the\n schema's 280c cap keeps it bounded; letting it grow is the safer\n call than risking a broken drop-cap. */\n/* Drop-cap on the lead blurb. ::first-letter is enough \u2014 no JS,\n no extra DOM, falls back gracefully on unsupported engines. */\n.pdp-tn-mag__lead .pdp-tn-mag__blurb::first-letter {\n font-family: var(--tn-display);\n font-weight: 500;\n font-size: 3.3em;\n line-height: 0.85;\n float: left;\n padding: 4px 10px 0 0;\n color: var(--tn-accent);\n font-variation-settings: 'SOFT' 100;\n}\n\n.pdp-tn-mag__item {\n display: grid;\n grid-template-columns: 1fr;\n gap: 6px;\n padding: 16px 0;\n border-bottom: 1px solid var(--tn-rule);\n}\n.pdp-tn-mag__item:last-child {\n border-bottom: 0;\n padding-bottom: 4px;\n}\n\n.pdp-tn-mag__item .pdp-tn-mag__headline {\n font-size: clamp(17px, 3.4vw, 19px);\n font-weight: 500;\n line-height: 1.2;\n margin: 0;\n}\n.pdp-tn-mag__item .pdp-tn-mag__blurb {\n font-family: var(--tn-sans);\n font-size: 13.5px;\n line-height: 1.5;\n margin: 0;\n color: hsl(30 8% 70%);\n /* Clamp non-lead item blurbs to 3 lines (no drop-cap on these so\n the line-clamp triplet plays nicely). Full triplet needed because\n the base .pdp-tn-mag__blurb intentionally has no display:-webkit-box. */\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Headline click-through link (inherits type styles; subtle hover\n underline + \u2197 glyph to signal external destination). */\n.pdp-tn-mag__headline-link {\n color: inherit;\n text-decoration: none;\n background-image: linear-gradient(currentColor, currentColor);\n background-position: 0 100%;\n background-repeat: no-repeat;\n background-size: 0 1px;\n transition: background-size 220ms ease;\n}\n.pdp-tn-mag__headline-link:hover,\n.pdp-tn-mag__headline-link:focus-visible {\n background-size: 100% 1px;\n outline: none;\n}\n.pdp-tn-mag__headline-link::after {\n content: ' \\\\2197';\n font-family: var(--tn-sans);\n font-size: 0.6em;\n letter-spacing: 0;\n color: var(--tn-accent);\n opacity: 0.55;\n vertical-align: 0.2em;\n margin-left: 0.18em;\n transition: opacity 220ms ease;\n}\n.pdp-tn-mag__headline-link:hover::after,\n.pdp-tn-mag__headline-link:focus-visible::after {\n opacity: 1;\n}\n\n@media (max-width: 480px) {\n .pdp-tn-mag {\n padding: 4px 0;\n }\n .pdp-tn-mag__lead {\n padding-bottom: 18px;\n margin-bottom: 18px;\n }\n .pdp-tn-mag__lead .pdp-tn-mag__blurb::first-letter {\n font-size: 3em;\n }\n}\n`;\n\nexport class PdpTrendingNewsMagazineLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: TrendingNewsData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureMagazineStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-tn-mag\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n const items = this.data.items;\n const [lead, ...rest] = items;\n return html`\n <section class=\"pdp-tn-mag\" aria-label=\"Trending news\">\n ${\n lead\n ? html`<article class=\"pdp-tn-mag__lead\">\n <div class=\"pdp-tn-mag__meta\">\n <span class=\"pdp-tn-mag__meta-source\">${lead.source}</span>\n \u00B7 ${lead.published_relative}\n </div>\n <h3 class=\"pdp-tn-mag__headline\">${renderHeadlineLink(lead)}</h3>\n <p class=\"pdp-tn-mag__blurb\">${lead.blurb}</p>\n </article>`\n : ''\n }\n ${rest.map(\n (it) => html`<article class=\"pdp-tn-mag__item\">\n <div class=\"pdp-tn-mag__meta\">\n <span class=\"pdp-tn-mag__meta-source\">${it.source}</span>\n \u00B7 ${it.published_relative}\n </div>\n <h4 class=\"pdp-tn-mag__headline\">${renderHeadlineLink(it)}</h4>\n <p class=\"pdp-tn-mag__blurb\">${it.blurb}</p>\n </article>`\n )}\n </section>\n `;\n }\n}\n\nfunction renderHeadlineLink(item: TrendingNewsItem) {\n if (!item.url) return html`${item.headline}`;\n return html`<a\n class=\"pdp-tn-mag__headline-link\"\n href=${item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >${item.headline}</a\n >`;\n}\n\nif (\n typeof customElements !== 'undefined' &&\n !customElements.get('syntro-pdp-trending-news-magazine')\n) {\n customElements.define('syntro-pdp-trending-news-magazine', PdpTrendingNewsMagazineLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-trending-news-magazine': PdpTrendingNewsMagazineLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface TrendingNewsItem {\n headline: string;\n source: string;\n published_relative: string;\n blurb: string;\n /** Optional click-through URL \u2014 see PdpTrendingNewsLit.ts for the\n * curated-seed-vs-sub-agent rationale. */\n url?: string | null;\n}\n\ninterface TrendingNewsData {\n items: TrendingNewsItem[];\n rationale: string;\n}\n\n/**\n * Hero-Quote variant for the trending-news module.\n *\n * Lead item rendered as a full-bleed pull-quote panel (the blurb is\n * treated as the editorial quote, the source becomes the citation\n * footer). Follow-on items collapse to a tight 1-line-per-item list.\n *\n * LOW visual density \u2014 functions as a rhythm-break beat between\n * heavier modules. Best for lead items that read as a single\n * declarative finding with a recognizable source. Avoid for\n * procedural / how-to news items.\n */\nfunction ensureHeroStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-trending-news-hero-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-trending-news-hero-style';\n style.textContent = HERO_CSS;\n document.head.appendChild(style);\n}\n\nconst HERO_CSS = `\n.pdp-tn-hero {\n --tn-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --tn-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --tn-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --tn-accent-soft: var(--syntro-pdp-primary-soft, hsl(150 32% 38% / 0.18));\n --tn-display: var(--syntro-pdp-display, inherit);\n --tn-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--tn-sans);\n color: var(--tn-fg);\n /* Full-bleed within its slot \u2014 escape the column gutter the parent\n PDP sets so this section reads as a rhythm-break. */\n margin: 0 calc(-1 * var(--syntro-pdp-bleed, 0px));\n}\n\n.pdp-tn-hero__panel {\n position: relative;\n padding: 36px 28px 32px;\n border-radius: 22px;\n /* Soft duotone gradient \u2014 accent at top-left, deep at bottom. */\n background:\n radial-gradient(120% 90% at 0% 0%, var(--tn-accent-soft) 0%, transparent 60%),\n linear-gradient(180deg, hsl(150 24% 12%) 0%, hsl(25 10% 9%) 100%);\n border: 1px solid hsl(30 6% 18%);\n overflow: hidden;\n}\n\n/* Big SVG quote-mark in the corner \u2014 pure CSS, no internet image dep. */\n.pdp-tn-hero__panel::before {\n content: '\u201C';\n position: absolute;\n top: 4px;\n left: 16px;\n font-family: var(--tn-display);\n font-style: normal;\n font-weight: 500;\n font-size: 140px;\n line-height: 1;\n color: var(--tn-accent);\n opacity: 0.18;\n pointer-events: none;\n}\n\n.pdp-tn-hero__quote {\n position: relative;\n font-family: var(--tn-display);\n font-style: italic;\n font-weight: 400;\n font-variation-settings: 'SOFT' 90, 'WONK' 1;\n line-height: 1.18;\n letter-spacing: -0.015em;\n margin: 0 0 22px;\n font-size: clamp(22px, 4.6vw, 30px);\n color: var(--tn-fg);\n max-width: 32ch;\n /* Pull-quote shouldn't run past 5 lines or it stops reading as a\n quote and starts reading as a paragraph. Schema permits ~280c\n which can blow past 5 lines at narrow viewports. */\n display: -webkit-box;\n -webkit-line-clamp: 5;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.pdp-tn-hero__cite {\n display: flex;\n align-items: baseline;\n gap: 12px;\n font-size: 12px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n}\n.pdp-tn-hero__cite-rule {\n flex: 0 0 24px;\n height: 1px;\n background: var(--tn-accent);\n align-self: center;\n opacity: 0.7;\n}\n.pdp-tn-hero__cite-source {\n color: var(--tn-fg);\n font-weight: 600;\n}\n\n.pdp-tn-hero__headline {\n margin: 22px 0 0;\n font-family: var(--tn-sans);\n font-weight: 600;\n font-size: 14.5px;\n color: hsl(30 8% 78%);\n line-height: 1.4;\n max-width: 48ch;\n border-top: 1px solid hsl(30 6% 20%);\n padding-top: 18px;\n}\n\n/* Follow-on items: tight, one row each, headline left, source right */\n.pdp-tn-hero__followon {\n margin-top: 18px;\n padding: 0 4px;\n}\n.pdp-tn-hero__row {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 16px;\n align-items: baseline;\n padding: 12px 0;\n border-bottom: 1px solid hsl(30 6% 18%);\n}\n.pdp-tn-hero__row:last-child {\n border-bottom: 0;\n}\n.pdp-tn-hero__row-headline {\n font-size: 14.5px;\n font-weight: 500;\n line-height: 1.3;\n color: var(--tn-fg);\n font-family: var(--tn-sans);\n margin: 0;\n /* clamp to single line + ellipsis at narrow widths */\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.pdp-tn-hero__row-meta {\n font-size: 10.5px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--tn-fg-muted);\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* Headline click-through (used on the lead headline below the quote\n and on each follow-on row headline). Inherits type styles; subtle\n hover underline + \u2197 glyph signals external destination. */\n.pdp-tn-hero__headline-link {\n color: inherit;\n text-decoration: none;\n background-image: linear-gradient(currentColor, currentColor);\n background-position: 0 100%;\n background-repeat: no-repeat;\n background-size: 0 1px;\n transition: background-size 220ms ease;\n}\n.pdp-tn-hero__headline-link:hover,\n.pdp-tn-hero__headline-link:focus-visible {\n background-size: 100% 1px;\n outline: none;\n}\n.pdp-tn-hero__headline-link::after {\n content: ' \\\\2197';\n font-family: var(--tn-sans);\n font-size: 0.7em;\n letter-spacing: 0;\n color: var(--tn-accent);\n opacity: 0.55;\n vertical-align: 0.18em;\n margin-left: 0.2em;\n transition: opacity 220ms ease;\n}\n.pdp-tn-hero__headline-link:hover::after,\n.pdp-tn-hero__headline-link:focus-visible::after {\n opacity: 1;\n}\n\n@media (max-width: 480px) {\n .pdp-tn-hero__panel {\n padding: 28px 22px 26px;\n border-radius: 18px;\n }\n .pdp-tn-hero__panel::before {\n font-size: 110px;\n top: 0;\n left: 10px;\n }\n}\n`;\n\nexport class PdpTrendingNewsHeroLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: TrendingNewsData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureHeroStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-tn-hero\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n const [lead, ...rest] = this.data.items;\n if (!lead) return html``;\n return html`\n <section class=\"pdp-tn-hero\" aria-label=\"Trending news\">\n <article class=\"pdp-tn-hero__panel\">\n <blockquote class=\"pdp-tn-hero__quote\">${lead.blurb}</blockquote>\n <footer class=\"pdp-tn-hero__cite\">\n <span class=\"pdp-tn-hero__cite-rule\" aria-hidden=\"true\"></span>\n <span class=\"pdp-tn-hero__cite-source\">${lead.source}</span>\n <span>${lead.published_relative}</span>\n </footer>\n <p class=\"pdp-tn-hero__headline\">${renderHeadlineLink(lead)}</p>\n </article>\n ${\n rest.length\n ? html`<div class=\"pdp-tn-hero__followon\">\n ${rest.map(\n (it) => html`<div class=\"pdp-tn-hero__row\">\n <p class=\"pdp-tn-hero__row-headline\">${renderHeadlineLink(it)}</p>\n <span class=\"pdp-tn-hero__row-meta\">${it.source}</span>\n </div>`\n )}\n </div>`\n : ''\n }\n </section>\n `;\n }\n}\n\nfunction renderHeadlineLink(item: TrendingNewsItem) {\n if (!item.url) return html`${item.headline}`;\n return html`<a\n class=\"pdp-tn-hero__headline-link\"\n href=${item.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >${item.headline}</a\n >`;\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-trending-news-hero')) {\n customElements.define('syntro-pdp-trending-news-hero', PdpTrendingNewsHeroLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-trending-news-hero': PdpTrendingNewsHeroLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface PeerPost {\n initials: string;\n persona: string;\n quote: string;\n similarity_reason: string;\n time_ago: string;\n}\n\ninterface PeerFeedData {\n peers: PeerPost[];\n rationale: string;\n}\n\n/**\n * Masonry-Quote variant for the peer-feed module.\n *\n * Two-column staggered-height grid using CSS columns. Each card has a\n * CSS-rendered initial avatar (colored circle, persona-hashed hue), a\n * bold quote, and a small persona footer. Card heights vary with quote\n * length, which is where the editorial rhythm comes from.\n *\n * Intentionally image-free. The peer-feed module avoids photography\n * entirely (see PeerFeedVariant docstring) \u2014 variety comes from layout\n * and typography, not faces.\n */\nfunction ensureMasonryStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-peer-feed-masonry-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-peer-feed-masonry-style';\n style.textContent = MASONRY_CSS;\n document.head.appendChild(style);\n}\n\nconst MASONRY_CSS = `\n.pdp-pf-mas {\n --pf-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pf-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --pf-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --pf-bg: var(--syntro-pdp-card-bg, hsl(25 10% 12%));\n --pf-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pf-display: var(--syntro-pdp-display, inherit);\n --pf-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--pf-sans);\n color: var(--pf-fg);\n padding: 4px 0;\n}\n\n.pdp-pf-mas__kicker {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--pf-accent);\n font-weight: 600;\n margin-bottom: 18px;\n}\n.pdp-pf-mas__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--pf-accent);\n opacity: 0.8;\n}\n\n/* CSS-columns gives a real masonry shape without JS layout\n (grid masonry is still behind a flag in most engines). On narrow\n mobile we collapse to a single column. */\n.pdp-pf-mas__grid {\n column-count: 2;\n column-gap: 14px;\n column-fill: balance;\n}\n@media (max-width: 540px) {\n .pdp-pf-mas__grid {\n column-count: 1;\n }\n}\n\n.pdp-pf-mas__card {\n /* Cards must NOT break across columns. */\n break-inside: avoid;\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 18px 16px 16px;\n border-radius: 14px;\n background: var(--pf-bg);\n border: 1px solid var(--pf-rule);\n margin: 0 0 14px;\n}\n\n.pdp-pf-mas__header {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.pdp-pf-mas__avatar {\n width: 36px;\n height: 36px;\n border-radius: 50%;\n flex: 0 0 36px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-family: var(--pf-sans);\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: hsl(35 15% 95%);\n /* Hue is set per-card via inline style. */\n background: hsl(var(--pf-avatar-hue, 150) 28% 32%);\n border: 1px solid hsl(var(--pf-avatar-hue, 150) 28% 42% / 0.7);\n}\n.pdp-pf-mas__persona {\n font-size: 12.5px;\n line-height: 1.3;\n color: var(--pf-fg);\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n}\n.pdp-pf-mas__persona-sub {\n color: var(--pf-fg-muted);\n font-weight: 400;\n font-size: 11.5px;\n margin-top: 2px;\n display: block;\n}\n\n.pdp-pf-mas__quote {\n font-family: var(--pf-display);\n font-weight: 400;\n font-style: italic;\n font-size: 15.5px;\n line-height: 1.4;\n margin: 0;\n color: var(--pf-fg);\n font-variation-settings: 'SOFT' 80;\n}\n.pdp-pf-mas__quote::before {\n content: '\"';\n font-size: 22px;\n line-height: 0;\n position: relative;\n top: 7px;\n margin-right: 2px;\n color: var(--pf-accent);\n opacity: 0.7;\n}\n\n.pdp-pf-mas__footer {\n font-size: 10.5px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--pf-fg-muted);\n font-weight: 500;\n border-top: 1px solid var(--pf-rule);\n padding-top: 10px;\n}\n`;\n\n// Stable per-string hue 0-360. Deterministic so the same persona\n// always renders the same color across page loads.\nfunction personaHue(s: string): number {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;\n return Math.abs(h) % 360;\n}\n\nexport class PdpPeerFeedMasonryLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: PeerFeedData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureMasonryStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-pf-mas\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n return html`\n <section class=\"pdp-pf-mas\" aria-label=\"Peer voices\">\n <div class=\"pdp-pf-mas__grid\">\n ${this.data.peers.map(\n (p) => html`<article\n class=\"pdp-pf-mas__card\"\n style=${`--pf-avatar-hue: ${personaHue(p.persona)};`}\n >\n <header class=\"pdp-pf-mas__header\">\n <span class=\"pdp-pf-mas__avatar\" aria-hidden=\"true\">${p.initials}</span>\n <span class=\"pdp-pf-mas__persona\"\n >${p.persona}\n <span class=\"pdp-pf-mas__persona-sub\">${p.similarity_reason}</span>\n </span>\n </header>\n <blockquote class=\"pdp-pf-mas__quote\">${p.quote}</blockquote>\n <footer class=\"pdp-pf-mas__footer\">${p.time_ago}</footer>\n </article>`\n )}\n </div>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-peer-feed-masonry')) {\n customElements.define('syntro-pdp-peer-feed-masonry', PdpPeerFeedMasonryLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-peer-feed-masonry': PdpPeerFeedMasonryLit;\n }\n}\n", "import { html, LitElement, svg } from 'lit';\n\ninterface PeerPost {\n initials: string;\n persona: string;\n quote: string;\n similarity_reason: string;\n time_ago: string;\n}\n\ninterface PeerFeedData {\n peers: PeerPost[];\n rationale: string;\n}\n\n/**\n * Voice-Carousel variant for the peer-feed module.\n *\n * Horizontal-scroll row of vertical voice cards. Each card has a\n * distinctive geometric SVG glyph chosen by hashing the persona string\n * (waveform / gauge / leaf / spark). Same persona \u2192 same glyph across\n * loads. Reads as \"voices we follow\" \u2014 variety by typology, not faces.\n *\n * Intentionally image-free. See PeerFeedVariant docstring for the\n * \"no UGC, no fake UGC either\" reasoning.\n */\nfunction ensureCarouselStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-peer-feed-carousel-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-peer-feed-carousel-style';\n style.textContent = CAROUSEL_CSS;\n document.head.appendChild(style);\n}\n\nconst CAROUSEL_CSS = `\n/* The custom-element tag itself + its slot-mount parent must declare\n display:block AND min-width:0 so they participate in the grid cell's\n width constraint instead of expanding to intrinsic content. Without\n this, the inner flex track's content sets the cell's auto-size and\n busts past the PDP container, forcing whole-page horizontal scroll. */\nsyntro-pdp-peer-feed-carousel {\n display: block;\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n.syntro-pdp-slot-mount:has(> syntro-pdp-peer-feed-carousel) {\n min-width: 0;\n overflow: hidden;\n}\n.pdp-pf-car {\n --pf-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pf-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --pf-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --pf-bg: var(--syntro-pdp-card-bg, hsl(25 10% 12%));\n --pf-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pf-display: var(--syntro-pdp-display, inherit);\n --pf-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--pf-sans);\n color: var(--pf-fg);\n padding: 4px 0;\n /* Critical \u2014 the carousel is a horizontal-scroll surface. Without an\n explicit width constraint, the inner flex track expands to its\n intrinsic content width and pushes the whole page into horizontal\n scroll. min-width:0 + width:100% lets the section shrink inside\n a grid/flex parent so the track's overflow:auto can do its job. */\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.pdp-pf-car__kicker {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--pf-accent);\n font-weight: 600;\n margin-bottom: 18px;\n}\n.pdp-pf-car__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--pf-accent);\n opacity: 0.8;\n}\n\n.pdp-pf-car__track {\n /* Flex (NOT grid) so the track honors its parent's width with\n overflow:auto. Grid containers size to their intrinsic content\n even when overflow:auto is set, which forced the whole page to\n scroll horizontally. */\n display: flex;\n flex-wrap: nowrap;\n gap: 12px;\n overflow-x: auto;\n overflow-y: hidden;\n width: 100%;\n min-width: 0;\n scroll-snap-type: x mandatory;\n scrollbar-width: thin;\n scrollbar-color: var(--pf-rule) transparent;\n padding-bottom: 8px;\n /* Edge fade \u2014 hint that there's more to scroll. */\n mask-image: linear-gradient(90deg, transparent 0, black 14px, black calc(100% - 24px), transparent 100%);\n -webkit-mask-image: linear-gradient(90deg, transparent 0, black 14px, black calc(100% - 24px), transparent 100%);\n}\n.pdp-pf-car__track::-webkit-scrollbar {\n height: 4px;\n}\n\n.pdp-pf-car__card {\n /* Per-card width \u2014 explicit because flex children of overflow-x:auto\n need a width to render off-screen. 78% of viewport with min/max\n so the card always fits-ish on phones AND never crowds on wide. */\n flex: 0 0 78%;\n max-width: 320px;\n min-width: 230px;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 14px;\n padding: 20px 18px 18px;\n border-radius: 14px;\n background: var(--pf-bg);\n border: 1px solid var(--pf-rule);\n min-height: 220px;\n position: relative;\n overflow: hidden;\n box-sizing: border-box;\n}\n\n/* Glyph block \u2014 sits top-right behind text as a tinted decoration. */\n.pdp-pf-car__glyph {\n position: absolute;\n top: 16px;\n right: 14px;\n width: 60px;\n height: 60px;\n color: hsl(var(--pf-glyph-hue, 150) 32% 50%);\n opacity: 0.38;\n pointer-events: none;\n}\n\n.pdp-pf-car__persona {\n font-size: 11px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--pf-accent);\n font-weight: 600;\n max-width: 60%;\n}\n\n.pdp-pf-car__quote {\n font-family: var(--pf-display);\n font-weight: 400;\n font-style: italic;\n font-size: 15.5px;\n line-height: 1.4;\n margin: 0;\n color: var(--pf-fg);\n font-variation-settings: 'SOFT' 80;\n flex: 1 1 auto;\n}\n\n.pdp-pf-car__footer {\n font-size: 10.5px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--pf-fg-muted);\n font-weight: 500;\n border-top: 1px solid var(--pf-rule);\n padding-top: 10px;\n display: flex;\n justify-content: space-between;\n gap: 8px;\n}\n.pdp-pf-car__footer-sim {\n color: var(--pf-fg-muted);\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n font-style: italic;\n text-align: right;\n flex: 1 1 auto;\n /* Truncate similarity reason if it overflows the row. */\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n`;\n\n// 4 geometric glyphs that read as \"kinds of human.\" Pure SVG so no\n// internet image deps. Pick by hashing the persona \u2014 same persona \u2192\n// same glyph across loads.\ntype GlyphKind = 'waveform' | 'gauge' | 'leaf' | 'spark';\nconst GLYPHS: GlyphKind[] = ['waveform', 'gauge', 'leaf', 'spark'];\n\nfunction glyphFor(persona: string): GlyphKind {\n let h = 0;\n for (let i = 0; i < persona.length; i++) h = (h * 31 + persona.charCodeAt(i)) | 0;\n return GLYPHS[Math.abs(h) % GLYPHS.length];\n}\n\nfunction glyphHue(persona: string): number {\n // Distinct from the glyph hash so the color isn't perfectly correlated\n // with the kind \u2014 same shape across people, different tints.\n let h = 0;\n for (let i = 0; i < persona.length; i++) h = (h * 17 + persona.charCodeAt(i)) | 0;\n return Math.abs(h) % 360;\n}\n\nfunction renderGlyph(kind: GlyphKind) {\n const common = svg`<g stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></g>`;\n if (kind === 'waveform') {\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n ${common}\n <path d=\"M4 30 Q12 12 18 30 T32 30 T46 30 T58 30\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/>\n <path d=\"M4 38 Q12 24 18 38 T32 38 T46 38 T58 38\" stroke=\"currentColor\" stroke-width=\"1.4\" fill=\"none\" stroke-linecap=\"round\" opacity=\"0.55\"/>\n </svg>`;\n }\n if (kind === 'gauge') {\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n <path d=\"M8 42 A22 22 0 0 1 52 42\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linecap=\"round\"/>\n <path d=\"M8 42 A22 22 0 0 1 30 20\" stroke=\"currentColor\" stroke-width=\"3.2\" fill=\"none\" stroke-linecap=\"round\"/>\n <line x1=\"30\" y1=\"42\" x2=\"38\" y2=\"26\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\"/>\n <circle cx=\"30\" cy=\"42\" r=\"2.6\" fill=\"currentColor\"/>\n </svg>`;\n }\n if (kind === 'leaf') {\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n <path d=\"M14 46 C14 22 30 8 50 12 C46 34 32 50 14 46 Z\" stroke=\"currentColor\" stroke-width=\"2\" fill=\"none\" stroke-linejoin=\"round\"/>\n <path d=\"M14 46 L42 18\" stroke=\"currentColor\" stroke-width=\"1.4\" fill=\"none\"/>\n </svg>`;\n }\n // spark\n return svg`\n <svg viewBox=\"0 0 60 60\" aria-hidden=\"true\">\n <path d=\"M30 6 L34 26 L54 30 L34 34 L30 54 L26 34 L6 30 L26 26 Z\" stroke=\"currentColor\" stroke-width=\"1.6\" fill=\"none\" stroke-linejoin=\"round\"/>\n </svg>`;\n}\n\nexport class PdpPeerFeedCarouselLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n\n data: PeerFeedData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureCarouselStyles();\n }\n\n override render() {\n if (!this.data) {\n return html`<div aria-busy=\"true\" class=\"pdp-pf-car\" style=\"opacity:0.5;\">\u2026</div>`;\n }\n return html`\n <section class=\"pdp-pf-car\" aria-label=\"Peer voices\">\n <div class=\"pdp-pf-car__track\">\n ${this.data.peers.map((p) => {\n const kind = glyphFor(p.persona);\n const hue = glyphHue(p.persona);\n return html`<article class=\"pdp-pf-car__card\" style=${`--pf-glyph-hue: ${hue};`}>\n <span class=\"pdp-pf-car__glyph\" aria-hidden=\"true\">${renderGlyph(kind)}</span>\n <span class=\"pdp-pf-car__persona\">${p.persona}</span>\n <blockquote class=\"pdp-pf-car__quote\">${p.quote}</blockquote>\n <footer class=\"pdp-pf-car__footer\">\n <span>${p.time_ago}</span>\n <span class=\"pdp-pf-car__footer-sim\" title=${p.similarity_reason}\n >${p.similarity_reason}</span\n >\n </footer>\n </article>`;\n })}\n </div>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-peer-feed-carousel')) {\n customElements.define('syntro-pdp-peer-feed-carousel', PdpPeerFeedCarouselLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-peer-feed-carousel': PdpPeerFeedCarouselLit;\n }\n}\n", "import { html, LitElement, nothing, svg } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * Backend ChartPayload shape from the PDP pipeline's ChartPayload Pydantic\n * model. The widget renders one of three layouts:\n *\n * - bar: in-house CSS bars (no dependency); animated grow-in; per-series\n * color palette; mobile-friendly label rotation.\n * - line: in-house SVG polyline; stroke-dasharray reveal animation;\n * tap-area markers on each data point.\n * - table: stylized comparison table built from a pivoted matrix.\n *\n * adapt-viz/vega is deliberately NOT used \u2014 at the PDP-tile scale our data\n * is always 3\u201310 points \u00D7 \u22644 series, and vega's bundle weight + label\n * layout heuristics work poorly at narrow widths.\n */\n\ninterface ChartPoint {\n x: string;\n y: number;\n}\n\ninterface ChartSeries {\n name: string;\n points: ChartPoint[];\n}\n\nexport interface ChartData {\n title: string;\n viz_kind: 'bar' | 'line' | 'table';\n series: ChartSeries[];\n rationale: string;\n}\n\n// \u2500\u2500\u2500 data utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Format a numeric value \u2014 drop trailing zeros, use thousands separator. */\nfunction formatNum(v: number | undefined): string {\n if (v === undefined || !Number.isFinite(v)) return '\u2014';\n if (Number.isInteger(v)) return v.toLocaleString('en-US');\n const trimmed = v.toFixed(2).replace(/\\.?0+$/, '');\n const [intPart, decPart] = trimmed.split('.');\n const formatted = Number(intPart).toLocaleString('en-US');\n return decPart ? `${formatted}.${decPart}` : formatted;\n}\n\n/** Comparison table matrix \u2014 same shape used by table layout. */\ninterface ComparisonMatrix {\n rowLabels: string[];\n colLabels: string[];\n cells: (number | undefined)[][];\n winningRow: number | null;\n}\n\nfunction buildComparisonMatrix(data: ChartData): ComparisonMatrix {\n const colLabels = data.series.map((s) => s.name);\n const seenRows = new Map<string, number>();\n const rowLabels: string[] = [];\n for (const series of data.series) {\n for (const p of series.points) {\n if (!seenRows.has(p.x)) {\n seenRows.set(p.x, rowLabels.length);\n rowLabels.push(p.x);\n }\n }\n }\n const cells: (number | undefined)[][] = rowLabels.map(() =>\n new Array<number | undefined>(colLabels.length).fill(undefined)\n );\n data.series.forEach((series, colIdx) => {\n for (const p of series.points) {\n const rowIdx = seenRows.get(p.x);\n // Every point populated seenRows and cells in the first pass above.\n const row = cells[rowIdx as number] as (number | undefined)[];\n row[colIdx] = p.y;\n }\n });\n let winningRow: number | null = null;\n let bestSum = Number.NEGATIVE_INFINITY;\n rowLabels.forEach((_, idx) => {\n const row = cells[idx] as (number | undefined)[];\n const sum = row.reduce<number>((acc, v) => acc + (typeof v === 'number' ? v : 0), 0);\n if (Number.isFinite(sum) && sum > bestSum) {\n bestSum = sum;\n winningRow = idx;\n }\n });\n return { rowLabels, colLabels, cells, winningRow };\n}\n\n/**\n * Group bars by x-value buckets for a grouped bar chart.\n * Each bucket carries one bar per series that has a point at that x.\n */\ninterface BarBucket {\n label: string;\n bars: { value: number; seriesIdx: number; seriesName: string }[];\n}\n\nfunction buildBarBuckets(data: ChartData): { buckets: BarBucket[]; yMax: number } {\n const seenX = new Map<string, number>();\n const buckets: BarBucket[] = [];\n for (const series of data.series) {\n for (const p of series.points) {\n let idx = seenX.get(p.x);\n if (idx === undefined) {\n idx = buckets.length;\n seenX.set(p.x, idx);\n buckets.push({ label: p.x, bars: [] });\n }\n }\n }\n data.series.forEach((series, sIdx) => {\n for (const p of series.points) {\n const idx = seenX.get(p.x);\n // Buckets and seenX are built from these same points in the first pass.\n const bucket = buckets[idx as number] as BarBucket;\n bucket.bars.push({ value: p.y, seriesIdx: sIdx, seriesName: series.name });\n }\n });\n let yMax = 0;\n for (const b of buckets) {\n for (const bar of b.bars) {\n if (bar.value > yMax) yMax = bar.value;\n }\n }\n if (yMax === 0) yMax = 1;\n return { buckets, yMax };\n}\n\n/** y-axis tick values \u2014 at most 4 ticks, \"nice\" rounded numbers. */\nfunction niceTicks(yMax: number): number[] {\n if (yMax <= 0) return [0];\n const targetCount = 4;\n const rawStep = yMax / targetCount;\n const mag = 10 ** Math.floor(Math.log10(rawStep));\n const niceSteps = [1, 2, 2.5, 5, 10];\n // 10 * mag always bounds rawStep because mag is rawStep's base-10 magnitude.\n const step = niceSteps.find((s) => s * mag >= rawStep) as number;\n const niceStep = step * mag;\n const ticks: number[] = [];\n for (let v = 0; v <= yMax + niceStep * 0.001; v += niceStep) {\n ticks.push(Number(v.toFixed(6)));\n }\n return ticks;\n}\n\n/**\n * For the line chart: project all series into a shared SVG coord space\n * (0 \u2192 100 on x, 0 \u2192 60 on y, origin at top-left).\n */\ninterface LinePathSeries {\n name: string;\n seriesIdx: number;\n points: { cx: number; cy: number; rawY: number; label: string }[];\n}\n\nfunction buildLinePaths(data: ChartData): {\n series: LinePathSeries[];\n xLabels: string[];\n yMax: number;\n} {\n const seenX = new Map<string, number>();\n const xLabels: string[] = [];\n for (const s of data.series) {\n for (const p of s.points) {\n if (!seenX.has(p.x)) {\n seenX.set(p.x, xLabels.length);\n xLabels.push(p.x);\n }\n }\n }\n const denom = Math.max(1, xLabels.length - 1);\n let yMax = 0;\n for (const s of data.series) {\n for (const p of s.points) {\n if (p.y > yMax) yMax = p.y;\n }\n }\n if (yMax === 0) yMax = 1;\n const seriesOut: LinePathSeries[] = data.series.map((s, sIdx) => ({\n name: s.name,\n seriesIdx: sIdx,\n points: s.points.map((p) => {\n // xLabels and seenX are built from these same points immediately above.\n const xIdx = seenX.get(p.x) as number;\n return {\n cx: (xIdx / denom) * 100,\n cy: 60 - (p.y / yMax) * 56 - 2,\n rawY: p.y,\n label: p.x,\n };\n }),\n }));\n return { series: seriesOut, xLabels, yMax };\n}\n\n// \u2500\u2500\u2500 styles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction ensureChartStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-chart-style')) return;\n const style = document.createElement('style');\n style.id = 'syntro-pdp-chart-style';\n style.textContent = CHART_CSS;\n document.head.appendChild(style);\n}\n\nconst CHART_CSS = `\n.pdp-chart-frame {\n --pc-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pc-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --pc-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pc-primary-soft: color-mix(in srgb, var(--pc-primary) 10%, transparent);\n --pc-accent: var(--syntro-pdp-accent, hsl(95 18% 48%));\n --pc-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --pc-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --pc-display: var(--syntro-pdp-display, inherit);\n --pc-sans: var(--syntro-pdp-sans, inherit);\n\n /* 4-step brand palette for grouped bars / multi-series lines. Each step\n blends primary \u2192 accent and ramps opacity down so a single series owns\n the boldest swatch and additional series read as supporting context. */\n --pc-color-0: var(--pc-primary);\n --pc-color-1: color-mix(in srgb, var(--pc-accent) 70%, var(--pc-primary));\n --pc-color-2: color-mix(in srgb, var(--pc-primary) 55%, transparent);\n --pc-color-3: color-mix(in srgb, var(--pc-accent) 50%, transparent);\n\n position: relative;\n border: 1px solid var(--pc-border);\n border-radius: 22px;\n padding: 28px 26px 30px;\n background:\n radial-gradient(80% 60% at 12% 0%, color-mix(in srgb, var(--pc-primary) 10%, transparent) 0%, transparent 60%),\n radial-gradient(60% 50% at 100% 100%, color-mix(in srgb, var(--pc-accent) 7%, transparent) 0%, transparent 65%),\n linear-gradient(180deg, hsl(25 10% 12%), hsl(25 10% 9%));\n overflow: hidden;\n color: var(--pc-fg);\n font-family: var(--pc-sans);\n /* Resting state VISIBLE (GPU/compositing hazard fix): the whole chart frame\n defaults to opacity 1 so a dropped/suppressed entrance can never leave the\n entire chart blank. The entrance (and every child reveal) is re-armed only\n under .pdp-chart-frame.is-entering. */\n opacity: 1;\n}\n.pdp-chart-frame.is-entering {\n opacity: 0;\n transform: translateY(20px);\n animation: pdp-chart-enter 700ms cubic-bezier(0.2, 0.7, 0.2, 1) 120ms both;\n}\n\n.pdp-chart-frame::before {\n content: '';\n position: absolute;\n inset: 0 0 auto 0;\n height: 1px;\n background: linear-gradient(\n 90deg,\n transparent,\n color-mix(in srgb, var(--pc-primary) 60%, transparent),\n color-mix(in srgb, var(--pc-accent) 40%, transparent),\n transparent\n );\n opacity: 0.7;\n}\n\n.pdp-chart-eyebrow {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 11px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--pc-fg-muted);\n font-weight: 500;\n margin-bottom: 6px;\n}\n.pdp-chart-eyebrow-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--pc-primary);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--pc-primary) 16%, transparent);\n}\n\n.pdp-chart-title {\n font-family: var(--pc-display);\n font-weight: 400;\n font-size: clamp(26px, 2.6vw, 38px);\n letter-spacing: -0.015em;\n line-height: 1.1;\n margin: 0 0 4px;\n font-variation-settings: 'SOFT' 70, 'WONK' 1;\n color: var(--pc-fg);\n}\n\n.pdp-chart-rationale {\n margin: 18px 0 0;\n font-style: italic;\n color: var(--pc-fg-muted);\n font-size: 14.5px;\n line-height: 1.55;\n max-width: 64ch;\n padding-left: 14px;\n border-left: 2px solid color-mix(in srgb, var(--pc-primary) 40%, transparent);\n}\n\n/* \u2500\u2500\u2500 legend (multi-series only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-legend {\n display: flex;\n flex-wrap: wrap;\n gap: 14px;\n margin-top: 14px;\n font-size: 12px;\n color: var(--pc-fg-muted);\n}\n.pdp-chart-legend-item {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n letter-spacing: 0.02em;\n}\n.pdp-chart-legend-swatch {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n flex: 0 0 auto;\n}\n\n/* \u2500\u2500\u2500 bar chart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-bars {\n --bars-grid-h: 220px;\n margin-top: 18px;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 0;\n align-items: stretch;\n}\n\n.pdp-chart-yaxis {\n display: grid;\n align-content: stretch;\n position: relative;\n height: var(--bars-grid-h);\n padding-right: 10px;\n}\n.pdp-chart-yaxis-tick {\n position: absolute;\n right: 12px;\n font-size: 10.5px;\n color: var(--pc-fg-muted);\n font-feature-settings: 'tnum' 1, 'lnum' 1;\n letter-spacing: 0.02em;\n transform: translateY(-50%);\n text-align: right;\n white-space: nowrap;\n}\n\n.pdp-chart-bars-area {\n position: relative;\n height: var(--bars-grid-h);\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: minmax(0, 1fr);\n gap: 14px;\n align-items: end;\n padding: 0 4px;\n border-left: 1px solid color-mix(in srgb, var(--pc-fg-muted) 18%, transparent);\n}\n.pdp-chart-bars-gridline {\n position: absolute;\n left: 0;\n right: 0;\n height: 1px;\n background: color-mix(in srgb, var(--pc-fg-muted) 12%, transparent);\n pointer-events: none;\n}\n.pdp-chart-bars-baseline {\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 1px;\n background: color-mix(in srgb, var(--pc-fg-muted) 26%, transparent);\n}\n\n.pdp-chart-bars-bucket {\n display: grid;\n grid-template-rows: 1fr auto;\n gap: 8px;\n height: 100%;\n align-items: end;\n min-width: 0;\n}\n.pdp-chart-bars-stack {\n display: flex;\n align-items: end;\n gap: 4px;\n height: 100%;\n}\n.pdp-chart-bar {\n flex: 1 1 0;\n min-width: 0;\n height: var(--h);\n border-radius: 6px 6px 2px 2px;\n background: linear-gradient(\n 180deg,\n color-mix(in srgb, var(--bar-color) 88%, white 8%) 0%,\n var(--bar-color) 100%\n );\n position: relative;\n box-shadow:\n inset 0 1px 0 color-mix(in srgb, white 20%, transparent),\n 0 0 0 1px color-mix(in srgb, var(--bar-color) 24%, transparent);\n transform-origin: bottom;\n}\n.pdp-chart-frame.is-entering .pdp-chart-bar {\n animation: pdp-chart-bar-grow 800ms cubic-bezier(0.2, 0.7, 0.2, 1) backwards;\n animation-delay: var(--delay, 0ms);\n}\n.pdp-chart-bar-value {\n position: absolute;\n top: -22px;\n left: 50%;\n transform: translateX(-50%);\n font-size: 11px;\n font-weight: 500;\n color: var(--pc-fg);\n background: color-mix(in srgb, var(--pc-bg) 80%, transparent);\n padding: 1px 5px;\n border-radius: 6px;\n opacity: 1;\n font-feature-settings: 'tnum' 1, 'lnum' 1;\n white-space: nowrap;\n}\n.pdp-chart-frame.is-entering .pdp-chart-bar-value {\n opacity: 0;\n animation: pdp-chart-bar-value-in 400ms ease-out both;\n animation-delay: calc(var(--delay, 0ms) + 600ms);\n}\n\n.pdp-chart-bars-label {\n font-size: 11.5px;\n text-align: center;\n color: var(--pc-fg-muted);\n letter-spacing: 0.01em;\n line-height: 1.2;\n padding-top: 10px;\n overflow-wrap: break-word;\n max-width: 100%;\n}\n\n/* \u2500\u2500\u2500 line chart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-line {\n margin-top: 22px;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 8px;\n align-items: stretch;\n}\n.pdp-chart-line-yaxis {\n position: relative;\n height: 220px;\n padding-right: 10px;\n}\n.pdp-chart-line-area {\n position: relative;\n height: 220px;\n border-left: 1px solid color-mix(in srgb, var(--pc-fg-muted) 18%, transparent);\n}\n.pdp-chart-line-svg {\n width: 100%;\n height: 100%;\n display: block;\n overflow: visible;\n}\n.pdp-chart-line-stroke {\n fill: none;\n stroke: var(--line-color);\n stroke-width: 1.6;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 240 240;\n /* Resting state VISIBLE: the line is fully drawn at rest (offset 0). The\n draw-on reveal is re-armed only under .is-entering so a suppressed\n animation can never strand the line hidden (offset 240). */\n stroke-dashoffset: 0;\n filter: drop-shadow(0 4px 8px color-mix(in srgb, var(--line-color) 25%, transparent));\n}\n.pdp-chart-frame.is-entering .pdp-chart-line-stroke {\n stroke-dashoffset: 240;\n animation: pdp-chart-line-draw 1100ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n animation-delay: var(--delay, 0ms);\n}\n.pdp-chart-line-dot {\n fill: var(--line-color);\n stroke: hsl(25 10% 8%);\n stroke-width: 0.4;\n opacity: 1;\n}\n.pdp-chart-frame.is-entering .pdp-chart-line-dot {\n opacity: 0;\n animation: pdp-chart-line-dot-in 360ms ease-out both;\n animation-delay: calc(var(--delay, 0ms) + var(--dot-delay, 0ms));\n}\n.pdp-chart-line-gridline {\n stroke: color-mix(in srgb, var(--pc-fg-muted) 12%, transparent);\n stroke-width: 0.3;\n}\n.pdp-chart-line-labels {\n display: grid;\n grid-template-columns: repeat(var(--col-count, 1), 1fr);\n gap: 0;\n margin-top: 10px;\n margin-left: calc(var(--yaxis-w, 32px) + 1px);\n font-size: 11.5px;\n color: var(--pc-fg-muted);\n letter-spacing: 0.01em;\n}\n.pdp-chart-line-labels > span {\n text-align: center;\n line-height: 1.2;\n overflow-wrap: break-word;\n}\n\n/* \u2500\u2500\u2500 stylized comparison table for viz_kind=table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.pdp-chart-table-wrap {\n margin-top: 18px;\n border-radius: 14px;\n overflow: hidden;\n border: 1px solid hsl(30 6% 22%);\n background: linear-gradient(180deg, hsl(25 10% 12%), hsl(25 10% 10%));\n}\n.pdp-chart-table {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n font-family: var(--pc-sans);\n color: var(--pc-fg);\n}\n.pdp-chart-table thead th {\n text-align: left;\n font-weight: 500;\n font-size: 10.5px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--pc-fg-muted);\n padding: 14px 18px;\n border-bottom: 1px solid hsl(30 6% 22%);\n background: hsl(25 10% 11%);\n white-space: nowrap;\n}\n.pdp-chart-table tbody td {\n padding: 14px 18px;\n border-bottom: 1px solid hsl(30 6% 17%);\n font-size: 14.5px;\n color: hsl(35 15% 86%);\n vertical-align: middle;\n}\n.pdp-chart-table tbody tr:last-child td {\n border-bottom: none;\n}\n.pdp-chart-table tbody td:first-child {\n font-family: var(--pc-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 50;\n font-size: 16px;\n letter-spacing: -0.005em;\n color: var(--pc-fg);\n white-space: nowrap;\n}\n.pdp-chart-table tbody tr.is-winning {\n background: linear-gradient(90deg, color-mix(in srgb, var(--pc-primary) 12%, transparent), transparent 80%);\n}\n.pdp-chart-table tbody tr.is-winning td:first-child {\n color: color-mix(in srgb, var(--pc-primary) 70%, white 30%);\n}\n.pdp-chart-table tbody tr.is-winning td:first-child::before {\n content: '\\\\2726 ';\n font-size: 11px;\n color: color-mix(in srgb, var(--pc-primary) 60%, white 40%);\n vertical-align: 1px;\n margin-right: 2px;\n}\n.pdp-chart-numeric-cell {\n font-feature-settings: 'tnum' 1, 'lnum' 1;\n color: hsl(35 15% 88%);\n}\n\n/* skeleton */\n.pdp-chart-skeleton {\n display: grid;\n gap: 14px;\n padding: 28px;\n border-radius: 22px;\n border: 1px solid hsl(30 6% 18%);\n background: hsl(25 10% 10%);\n}\n.pdp-chart-skeleton .pdp-skeleton-bar {\n height: 18px;\n border-radius: 6px;\n background: linear-gradient(90deg, hsl(25 10% 14%) 0%, hsl(25 10% 19%) 50%, hsl(25 10% 14%) 100%);\n background-size: 200% 100%;\n animation: pdp-chart-shimmer 1.6s ease-in-out infinite;\n}\n.pdp-chart-skeleton .pdp-skeleton-rect {\n border-radius: 12px;\n background: linear-gradient(90deg, hsl(25 10% 14%) 0%, hsl(25 10% 19%) 50%, hsl(25 10% 14%) 100%);\n background-size: 200% 100%;\n animation: pdp-chart-shimmer 1.6s ease-in-out infinite;\n}\n\n/* \u2500\u2500\u2500 mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n@media (max-width: 640px) {\n .pdp-chart-frame {\n padding: 22px 16px 24px;\n border-radius: 18px;\n }\n .pdp-chart-title {\n font-size: clamp(22px, 6.5vw, 30px);\n }\n .pdp-chart-rationale {\n font-size: 13.5px;\n padding-left: 12px;\n }\n .pdp-chart-table thead th,\n .pdp-chart-table tbody td {\n padding: 12px 12px;\n }\n .pdp-chart-table tbody td:first-child {\n font-size: 14.5px;\n }\n .pdp-chart-table tbody td {\n font-size: 13px;\n }\n .pdp-chart-bars,\n .pdp-chart-line {\n --bars-grid-h: 180px;\n }\n .pdp-chart-bars-area {\n gap: 8px;\n }\n .pdp-chart-bars-label {\n font-size: 10.5px;\n line-height: 1.15;\n }\n .pdp-chart-bar-value {\n font-size: 10px;\n top: -18px;\n }\n .pdp-chart-line-yaxis,\n .pdp-chart-line-area {\n height: 180px;\n }\n .pdp-chart-line-labels {\n font-size: 10.5px;\n }\n}\n\n/* \u2500\u2500\u2500 keyframes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n@keyframes pdp-chart-enter {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes pdp-chart-bar-grow {\n from { transform: scaleY(0); }\n to { transform: scaleY(1); }\n}\n@keyframes pdp-chart-bar-value-in {\n from { opacity: 0; transform: translate(-50%, 4px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n@keyframes pdp-chart-line-draw {\n from { stroke-dashoffset: 240; }\n to { stroke-dashoffset: 0; }\n}\n@keyframes pdp-chart-line-dot-in {\n from { opacity: 0; transform: scale(0.6); }\n to { opacity: 1; transform: scale(1); }\n}\n@keyframes pdp-chart-shimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-chart-frame.is-entering,\n .pdp-chart-frame.is-entering .pdp-chart-bar,\n .pdp-chart-frame.is-entering .pdp-chart-bar-value,\n .pdp-chart-frame.is-entering .pdp-chart-line-stroke,\n .pdp-chart-frame.is-entering .pdp-chart-line-dot {\n opacity: 1;\n transform: none;\n stroke-dashoffset: 0;\n animation: none;\n }\n}\n`;\n\n// \u2500\u2500\u2500 widget \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Number of bar/line color slots in the CSS palette. */\nconst PALETTE_SIZE = 4;\n\nfunction colorVar(seriesIdx: number): string {\n return `var(--pc-color-${seriesIdx % PALETTE_SIZE})`;\n}\n\nexport class PdpChartLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: ChartData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureChartStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-chart-frame');\n if (!container) return;\n this._armed = true;\n // frame enter 700ms+120ms; per-point line/bar reveals run out to ~1.7s once\n // their runtime --delay stacks. Belt oversized so the class is never pulled\n // mid-reveal; base state is already the settled (visible) frame.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 2500\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-chart-skeleton\">\n <div class=\"pdp-skeleton-bar\" style=\"width:50%\"></div>\n <div class=\"pdp-skeleton-rect\" style=\"height:200px\"></div>\n </div>`;\n }\n\n const isTable = this.data.viz_kind === 'table';\n const allSeriesOnePoint = this.data.series.every((s) => s.points.length <= 1);\n // Degrade line \u2192 bar when there is only one point per series; a single\n // point can't define a line and would otherwise render as an empty axis.\n const effectiveKind: 'bar' | 'line' | 'table' = isTable\n ? 'table'\n : this.data.viz_kind === 'line' && !allSeriesOnePoint\n ? 'line'\n : 'bar';\n\n const isMultiSeries = this.data.series.length > 1;\n\n return html`\n <section\n class=\"${this._entering ? 'pdp-chart pdp-chart-frame is-entering' : 'pdp-chart pdp-chart-frame'}\"\n aria-label=\"${this.data.title}\"\n >\n <div class=\"pdp-chart-eyebrow\">\n <span class=\"pdp-chart-eyebrow-dot\" aria-hidden=\"true\"></span>\n <span>Computed comparison</span>\n </div>\n <h3 class=\"pdp-chart-title\">${this.data.title}</h3>\n\n ${effectiveKind === 'table' ? this.#renderTable() : nothing}\n ${effectiveKind === 'bar' ? this.#renderBars() : nothing}\n ${effectiveKind === 'line' ? this.#renderLine() : nothing}\n ${effectiveKind !== 'table' && isMultiSeries ? this.#renderLegend() : nothing}\n ${\n this.data.rationale\n ? html`<p class=\"pdp-chart-rationale\">${this.data.rationale}</p>`\n : nothing\n }\n </section>\n `;\n }\n\n #renderLegend() {\n // render() invokes helpers only after its data guard.\n const data = this.data as ChartData;\n return html`\n <ol class=\"pdp-chart-legend\" aria-label=\"Legend\">\n ${data.series.map(\n (s, idx) => html`\n <li class=\"pdp-chart-legend-item\">\n <span\n class=\"pdp-chart-legend-swatch\"\n style=\"background:${colorVar(idx)}\"\n aria-hidden=\"true\"\n ></span>\n <span>${s.name}</span>\n </li>\n `\n )}\n </ol>\n `;\n }\n\n #renderTable() {\n const matrix = buildComparisonMatrix(this.data as ChartData);\n return html`\n <div class=\"pdp-chart-table-wrap\">\n <table class=\"pdp-chart-table\">\n <thead>\n <tr>\n <th></th>\n ${matrix.colLabels.map((label) => html`<th>${label}</th>`)}\n </tr>\n </thead>\n <tbody>\n ${matrix.rowLabels.map(\n (row, rowIdx) => html`\n <tr class=${rowIdx === matrix.winningRow ? 'is-winning' : ''}>\n <td>${row}</td>\n ${(matrix.cells[rowIdx] as (number | undefined)[]).map(\n (cell) => html`<td class=\"pdp-chart-numeric-cell\">${formatNum(cell)}</td>`\n )}\n </tr>\n `\n )}\n </tbody>\n </table>\n </div>\n `;\n }\n\n #renderBars() {\n const data = this.data as ChartData;\n const { buckets, yMax } = buildBarBuckets(data);\n const ticks = niceTicks(yMax);\n // Use the max tick (which is \u2265 yMax) for height scaling so bar tops sit at\n // or below the highest gridline rather than overflowing it.\n const scaleMax = Math.max(yMax, ticks[ticks.length - 1] as number);\n return html`\n <div\n class=\"pdp-chart-bars\"\n role=\"figure\"\n aria-label=\"${data.title}\"\n data-bar-chart=\"\"\n >\n <div class=\"pdp-chart-yaxis\" aria-hidden=\"true\">\n ${ticks.map(\n (t) => html`\n <span\n class=\"pdp-chart-yaxis-tick\"\n style=\"bottom:${(t / scaleMax) * 100}%\"\n >\n ${formatNum(t)}\n </span>\n `\n )}\n </div>\n <div class=\"pdp-chart-bars-area\">\n ${ticks.map(\n (t) => html`\n <div\n class=\"pdp-chart-bars-gridline\"\n style=\"bottom:${(t / scaleMax) * 100}%\"\n aria-hidden=\"true\"\n ></div>\n `\n )}\n <div class=\"pdp-chart-bars-baseline\" aria-hidden=\"true\"></div>\n ${buckets.map(\n (bucket, bIdx) => html`\n <div class=\"pdp-chart-bars-bucket\">\n <div class=\"pdp-chart-bars-stack\">\n ${bucket.bars.map(\n (bar, barIdxInBucket) => html`\n <div\n class=\"pdp-chart-bar\"\n style=\"\n --h:${(Math.max(0, bar.value) / scaleMax) * 100}%;\n --bar-color:${colorVar(bar.seriesIdx)};\n --delay:${bIdx * 90 + barIdxInBucket * 30}ms;\n \"\n role=\"img\"\n aria-label=\"${bucket.label} \u2014 ${bar.seriesName}: ${formatNum(bar.value)}\"\n >\n ${\n bucket.bars.length === 1\n ? html`<span class=\"pdp-chart-bar-value\"\n >${formatNum(bar.value)}</span\n >`\n : nothing\n }\n </div>\n `\n )}\n </div>\n <div class=\"pdp-chart-bars-label\">${bucket.label}</div>\n </div>\n `\n )}\n </div>\n </div>\n `;\n }\n\n #renderLine() {\n const data = this.data as ChartData;\n const { series, xLabels, yMax } = buildLinePaths(data);\n const ticks = niceTicks(yMax);\n const scaleMax = Math.max(yMax, ticks[ticks.length - 1] as number);\n return html`\n <div\n class=\"pdp-chart-line\"\n role=\"figure\"\n aria-label=\"${data.title}\"\n style=\"--col-count:${xLabels.length}\"\n data-line-chart=\"\"\n >\n <div class=\"pdp-chart-line-yaxis\" aria-hidden=\"true\">\n ${ticks.map(\n (t) => html`\n <span\n class=\"pdp-chart-yaxis-tick\"\n style=\"bottom:${(t / scaleMax) * 100}%\"\n >\n ${formatNum(t)}\n </span>\n `\n )}\n </div>\n <div class=\"pdp-chart-line-area\">\n <svg\n class=\"pdp-chart-line-svg\"\n viewBox=\"0 0 100 60\"\n preserveAspectRatio=\"none\"\n aria-hidden=\"true\"\n >\n ${ticks.map((t) => {\n const y = 60 - (t / scaleMax) * 56 - 2;\n return svg`<line class=\"pdp-chart-line-gridline\" x1=\"0\" y1=${y} x2=\"100\" y2=${y} />`;\n })}\n ${series.map(\n (s) => svg`\n <polyline\n class=\"pdp-chart-line-stroke\"\n style=${`--line-color:${colorVar(s.seriesIdx)}; --delay:${s.seriesIdx * 200}ms`}\n points=${s.points.map((p) => `${p.cx},${p.cy}`).join(' ')}\n />\n ${s.points.map(\n (p, idx) => svg`\n <circle\n class=\"pdp-chart-line-dot\"\n cx=${p.cx}\n cy=${p.cy}\n r=\"1.4\"\n style=${`--line-color:${colorVar(s.seriesIdx)}; --delay:${s.seriesIdx * 200}ms; --dot-delay:${800 + idx * 60}ms`}\n />\n `\n )}\n `\n )}\n </svg>\n </div>\n </div>\n <div\n class=\"pdp-chart-line-labels\"\n style=\"--col-count:${xLabels.length}\"\n aria-hidden=\"true\"\n >\n ${xLabels.map((label) => html`<span>${label}</span>`)}\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-chart')) {\n customElements.define('syntro-pdp-chart', PdpChartLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-chart': PdpChartLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface ChartPoint {\n x: string;\n y: number;\n}\ninterface ChartSeries {\n name: string;\n points: ChartPoint[];\n}\ninterface ChartData {\n title: string;\n viz_kind: 'bar' | 'line' | 'table';\n series: ChartSeries[];\n rationale: string;\n}\n\n/**\n * Bar-Stacked variant for the chart module.\n *\n * Renders the series as a horizontal bar chart entirely in CSS \u2014 no\n * <canvas>, no internet image deps. Each row is a category (x value)\n * with parallel bars per series, each bar scaled to the per-row max.\n * Reads as a classic data block: HIGH typographic density, lots of\n * labels, clear comparison across x.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-chart-bar-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-chart-bar-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-chart-bar {\n --c-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --c-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --c-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --c-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --c-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--c-sans);\n color: var(--c-fg);\n padding: 20px 22px 18px;\n border: 1px solid var(--c-rule);\n border-radius: 14px;\n background: hsl(25 10% 11%);\n}\n.pdp-chart-bar__title {\n font-size: 14px;\n font-weight: 600;\n margin: 0 0 16px;\n}\n.pdp-chart-bar__legend {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 18px;\n margin-bottom: 14px;\n font-size: 11px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--c-fg-muted);\n}\n.pdp-chart-bar__legend-dot {\n display: inline-block;\n width: 9px;\n height: 9px;\n border-radius: 2px;\n margin-right: 6px;\n vertical-align: middle;\n}\n.pdp-chart-bar__row {\n display: grid;\n grid-template-columns: 1fr;\n gap: 4px;\n margin-bottom: 12px;\n}\n.pdp-chart-bar__row-label {\n font-size: 12.5px;\n color: var(--c-fg);\n font-weight: 500;\n}\n.pdp-chart-bar__row-bars {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.pdp-chart-bar__bar-track {\n position: relative;\n background: hsl(25 10% 16%);\n border-radius: 3px;\n height: 18px;\n overflow: hidden;\n}\n.pdp-chart-bar__bar-fill {\n position: absolute;\n inset: 0 auto 0 0;\n border-radius: 3px;\n transition: width 600ms cubic-bezier(0.2, 0.7, 0.2, 1);\n}\n.pdp-chart-bar__bar-value {\n position: absolute;\n right: 6px;\n top: 50%;\n transform: translateY(-50%);\n font-size: 11px;\n color: hsl(30 8% 80%);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n}\n`;\n\n// Stable palette indexed by series order. Greens, golds, deep blues \u2014\n// safe across most themes. Wraps if more than the palette length.\nconst PALETTE = ['hsl(150 32% 50%)', 'hsl(40 75% 60%)', 'hsl(208 50% 55%)', 'hsl(320 38% 60%)'];\n\nexport class PdpChartBarStackedLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: ChartData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-chart-bar\" style=\"opacity:0.5;\">\u2026</div>`;\n const { title, series } = this.data;\n // Build a unique row order from the first series; fall back to\n // union across series if a later one introduces a new x.\n const allX = Array.from(new Set(series.flatMap((s) => s.points.map((p) => p.x))));\n // Global max for consistent scaling across rows. Otherwise a small\n // row would visually rival a tall row.\n const max = Math.max(...series.flatMap((s) => s.points.map((p) => p.y)), 1);\n return html`\n <section class=\"pdp-chart-bar\" aria-label=${title}>\n <h3 class=\"pdp-chart-bar__title\">${title}</h3>\n ${\n series.length > 1\n ? html`<div class=\"pdp-chart-bar__legend\">\n ${series.map(\n (s, i) => html`<span\n ><span\n class=\"pdp-chart-bar__legend-dot\"\n style=${`background: ${PALETTE[i % PALETTE.length]};`}\n ></span\n >${s.name}</span\n >`\n )}\n </div>`\n : ''\n }\n ${allX.map(\n (x) => html`<div class=\"pdp-chart-bar__row\">\n <div class=\"pdp-chart-bar__row-label\">${x}</div>\n <div class=\"pdp-chart-bar__row-bars\">\n ${series.map((s, i) => {\n const point = s.points.find((p) => p.x === x);\n if (!point) return '';\n const pct = (point.y / max) * 100;\n return html`<div class=\"pdp-chart-bar__bar-track\">\n <div\n class=\"pdp-chart-bar__bar-fill\"\n style=${`width: ${pct.toFixed(1)}%; background: ${PALETTE[i % PALETTE.length]};`}\n ></div>\n <span class=\"pdp-chart-bar__bar-value\">${point.y}</span>\n </div>`;\n })}\n </div>\n </div>`\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-chart-bar')) {\n customElements.define('syntro-pdp-chart-bar', PdpChartBarStackedLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-chart-bar': PdpChartBarStackedLit;\n }\n}\n", "import { html, LitElement, svg } from 'lit';\n\ninterface ChartPoint {\n x: string;\n y: number;\n}\ninterface ChartSeries {\n name: string;\n points: ChartPoint[];\n}\ninterface ChartData {\n title: string;\n viz_kind: 'bar' | 'line' | 'table';\n series: ChartSeries[];\n rationale: string;\n}\n\n/**\n * Metric-Pull variant for the chart module.\n *\n * Pulls the maximum data point from the first series as the headline\n * metric \u2014 rendered as a giant typographic number. The remaining\n * points render as a tiny SVG sparkline below. Delta vs the mean is\n * shown as a small +/- pill. LOW density rhythm-break beat between\n * heavier modules.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-chart-metric-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-chart-metric-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-chart-met {\n --c-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --c-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --c-rule: var(--syntro-pdp-border, hsl(30 6% 20%));\n --c-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --c-display: var(--syntro-pdp-display, inherit);\n --c-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--c-sans);\n color: var(--c-fg);\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 22px 28px;\n padding: 24px 26px;\n border: 1px solid var(--c-rule);\n border-radius: 16px;\n background:\n radial-gradient(120% 80% at 0% 0%, hsl(150 32% 38% / 0.10) 0%, transparent 60%),\n linear-gradient(180deg, hsl(25 10% 12%) 0%, hsl(25 10% 9%) 100%);\n align-items: start;\n}\n.pdp-chart-met__metric {\n display: flex;\n flex-direction: column;\n gap: 6px;\n align-self: center;\n}\n.pdp-chart-met__big {\n font-family: var(--c-display);\n font-weight: 380;\n font-variation-settings: 'SOFT' 100;\n font-size: clamp(48px, 12vw, 72px);\n line-height: 0.95;\n letter-spacing: -0.04em;\n color: var(--c-fg);\n font-variant-numeric: tabular-nums;\n}\n.pdp-chart-met__delta {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 9px;\n border-radius: 999px;\n font-size: 11px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n font-weight: 600;\n background: hsl(150 32% 32% / 0.25);\n color: hsl(150 35% 75%);\n width: max-content;\n}\n.pdp-chart-met__side {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.pdp-chart-met__title {\n font-size: 11px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--c-fg-muted);\n font-weight: 600;\n}\n.pdp-chart-met__label {\n font-size: 14px;\n font-weight: 500;\n color: var(--c-fg);\n line-height: 1.3;\n}\n.pdp-chart-met__spark {\n width: 100%;\n height: 38px;\n margin-top: 6px;\n}\n.pdp-chart-met__spark path.line {\n fill: none;\n stroke: var(--c-accent);\n stroke-width: 2;\n stroke-linejoin: round;\n stroke-linecap: round;\n}\n.pdp-chart-met__spark path.area {\n fill: var(--c-accent);\n opacity: 0.18;\n}\n.pdp-chart-met__spark circle.peak {\n fill: var(--c-accent);\n}\n`;\n\ntype NonEmptyPoints = [ChartPoint, ...ChartPoint[]];\n\nfunction buildSparkline(points: NonEmptyPoints): {\n linePath: string;\n areaPath: string;\n peakIndex: number;\n} {\n const w = 200;\n const h = 38;\n const xs = points.map((_, i) => (i / Math.max(points.length - 1, 1)) * (w - 4) + 2);\n const min = Math.min(...points.map((p) => p.y));\n const max = Math.max(...points.map((p) => p.y));\n const range = max - min || 1;\n const ys = points.map((p) => h - 4 - ((p.y - min) / range) * (h - 8));\n const line = xs\n .map((x, i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${ys[i].toFixed(1)}`)\n .join(' ');\n const area = `${line} L ${xs[xs.length - 1].toFixed(1)} ${h} L ${xs[0].toFixed(1)} ${h} Z`;\n const peakIndex = points.reduce((best, p, i) => (p.y > points[best].y ? i : best), 0);\n return { linePath: line, areaPath: area, peakIndex };\n}\n\nexport class PdpChartMetricPullLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: ChartData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-chart-met\" style=\"opacity:0.5;\">\u2026</div>`;\n const { title, series } = this.data;\n const first = series[0];\n if (!first || first.points.length === 0) return html``;\n // The guard above establishes the non-empty contract used by the peak and\n // sparkline calculations below.\n const points = first.points as NonEmptyPoints;\n const peak = points.reduce((b, p) => (p.y > b.y ? p : b), points[0]);\n const mean = points.reduce((s, p) => s + p.y, 0) / points.length;\n const delta = ((peak.y - mean) / Math.max(mean, 1)) * 100;\n const spark = buildSparkline(points);\n\n const w = 200;\n const h = 38;\n // A maximum of a finite, non-empty series is always >= that series' mean,\n // and buildSparkline always returns an index in the same non-empty series.\n const px = (spark.peakIndex / Math.max(points.length - 1, 1)) * (w - 4) + 2;\n const min = Math.min(...points.map((p) => p.y));\n const max = Math.max(...points.map((p) => p.y));\n const range = max - min || 1;\n const py = h - 4 - ((peak.y - min) / range) * (h - 8);\n\n return html`\n <section class=\"pdp-chart-met\" aria-label=${title}>\n <div class=\"pdp-chart-met__metric\">\n <span class=\"pdp-chart-met__big\">${peak.y}</span>\n <span class=\"pdp-chart-met__delta\">+${delta.toFixed(0)}% vs avg</span>\n </div>\n <div class=\"pdp-chart-met__side\">\n <span class=\"pdp-chart-met__title\">${title}</span>\n <span class=\"pdp-chart-met__label\">Peak at ${peak.x} (${first.name})</span>\n <svg\n class=\"pdp-chart-met__spark\"\n viewBox=${`0 0 ${w} ${h}`}\n preserveAspectRatio=\"none\"\n aria-hidden=\"true\"\n >\n <path class=\"area\" d=${spark.areaPath}></path>\n <path class=\"line\" d=${spark.linePath}></path>\n ${svg`<circle class=\"peak\" cx=${px} cy=${py} r=\"3\"></circle>`}\n </svg>\n </div>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-chart-metric')) {\n customElements.define('syntro-pdp-chart-metric', PdpChartMetricPullLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-chart-metric': PdpChartMetricPullLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface QaItem {\n question: string;\n answer: string;\n}\ninterface QaData {\n items: QaItem[];\n rationale: string;\n}\n\n/**\n * Accordion-Multi variant for the Q&A module.\n *\n * Single column, multi-expand accordion (any number of items can be\n * open at once). Airy LOW density block. Tap a question to reveal its\n * answer; the chevron rotates. Pure CSS / details disclosure \u2014 no JS\n * state machine.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-qa-accordion-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-qa-accordion-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-qa-acc {\n --qa-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --qa-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --qa-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --qa-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --qa-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--qa-sans);\n color: var(--qa-fg);\n max-width: 720px;\n}\n.pdp-qa-acc__kicker {\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--qa-accent);\n font-weight: 600;\n margin: 0 0 20px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.pdp-qa-acc__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--qa-accent);\n opacity: 0.8;\n}\n.pdp-qa-acc__item {\n border-bottom: 1px solid var(--qa-rule);\n}\n.pdp-qa-acc__item[open] {\n background: linear-gradient(180deg, hsl(150 24% 14% / 0.18) 0%, transparent 100%);\n}\n.pdp-qa-acc__item summary {\n list-style: none;\n cursor: pointer;\n display: flex;\n gap: 14px;\n align-items: flex-start;\n padding: 18px 4px;\n font-size: 15.5px;\n font-weight: 500;\n line-height: 1.35;\n color: var(--qa-fg);\n}\n.pdp-qa-acc__item summary::-webkit-details-marker {\n display: none;\n}\n.pdp-qa-acc__chev {\n flex: 0 0 14px;\n width: 14px;\n height: 14px;\n margin-top: 4px;\n border-right: 2px solid var(--qa-accent);\n border-bottom: 2px solid var(--qa-accent);\n transform: rotate(-45deg);\n transition: transform 200ms ease;\n}\n.pdp-qa-acc__item[open] .pdp-qa-acc__chev {\n transform: rotate(45deg);\n}\n.pdp-qa-acc__a {\n padding: 0 4px 20px 28px;\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--qa-fg-muted);\n margin: 0;\n max-width: 56ch;\n}\n`;\n\nexport class PdpQaAccordionLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: QaData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-qa-acc\" style=\"opacity:0.5;\">\u2026</div>`;\n return html`\n <section class=\"pdp-qa-acc\" aria-label=\"Frequently asked questions\">\n ${this.data.items.map(\n (it) => html`<details class=\"pdp-qa-acc__item\">\n <summary>\n <span class=\"pdp-qa-acc__chev\" aria-hidden=\"true\"></span>\n <span>${it.question}</span>\n </summary>\n <p class=\"pdp-qa-acc__a\">${it.answer}</p>\n </details>`\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-qa-accordion')) {\n customElements.define('syntro-pdp-qa-accordion', PdpQaAccordionLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-qa-accordion': PdpQaAccordionLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface QaItem {\n question: string;\n answer: string;\n}\ninterface QaData {\n items: QaItem[];\n rationale: string;\n}\n\n/**\n * Side-by-Side variant for the Q&A module.\n *\n * Two-column reading block. Question on the left (bold, narrow),\n * answer on the right (regular, wider). Hairline rules between pairs.\n * MED density \u2014 the reader is meant to absorb each pair, not skim.\n * Collapses to a stacked layout on narrow viewports.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-qa-side-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-qa-side-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-qa-side {\n --qa-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --qa-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --qa-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --qa-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --qa-display: var(--syntro-pdp-display, inherit);\n --qa-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--qa-sans);\n color: var(--qa-fg);\n}\n.pdp-qa-side__kicker {\n font-size: 10.5px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--qa-accent);\n font-weight: 600;\n margin: 0 0 22px;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.pdp-qa-side__kicker::before {\n content: '';\n width: 18px;\n height: 1px;\n background: var(--qa-accent);\n opacity: 0.8;\n}\n.pdp-qa-side__item {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 26px;\n padding: 22px 0;\n border-bottom: 1px solid var(--qa-rule);\n align-items: baseline;\n}\n.pdp-qa-side__item:last-child {\n border-bottom: 0;\n}\n@media (max-width: 600px) {\n .pdp-qa-side__item {\n grid-template-columns: 1fr;\n gap: 8px;\n padding: 18px 0;\n }\n}\n.pdp-qa-side__q {\n font-family: var(--qa-display);\n font-weight: 460;\n font-variation-settings: 'SOFT' 80;\n font-size: 18px;\n line-height: 1.22;\n letter-spacing: -0.012em;\n color: var(--qa-fg);\n margin: 0;\n}\n.pdp-qa-side__a {\n font-size: 14.5px;\n line-height: 1.55;\n color: var(--qa-fg-muted);\n margin: 0;\n}\n`;\n\nexport class PdpQaSideBySideLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: QaData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-qa-side\" style=\"opacity:0.5;\">\u2026</div>`;\n return html`\n <section class=\"pdp-qa-side\" aria-label=\"Q&A\">\n ${this.data.items.map(\n (it) => html`<div class=\"pdp-qa-side__item\">\n <h4 class=\"pdp-qa-side__q\">${it.question}</h4>\n <p class=\"pdp-qa-side__a\">${it.answer}</p>\n </div>`\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-qa-side')) {\n customElements.define('syntro-pdp-qa-side', PdpQaSideBySideLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-qa-side': PdpQaSideBySideLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface RegionalData {\n region_label: string;\n temporal_label: string;\n headline: string;\n body: string;\n why_this_matters: string;\n rationale: string;\n}\n\n/**\n * Full-Bleed variant for the regional module.\n *\n * Edge-to-edge colored band with a locale SVG glyph (compass-style),\n * the temporal/regional kicker, a large display headline, supporting\n * body, and the \"why this matters\" tagline. LOW density rhythm-break\n * beat between heavier modules.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-regional-fullbleed-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-regional-fullbleed-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-reg-fb {\n --r-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --r-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --r-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --r-display: var(--syntro-pdp-display, inherit);\n --r-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--r-sans);\n color: var(--r-fg);\n margin: 0 calc(-1 * var(--syntro-pdp-bleed, 0px));\n}\n.pdp-reg-fb__panel {\n position: relative;\n border-radius: 22px;\n padding: 40px 28px 36px;\n background:\n radial-gradient(120% 80% at 100% 0%, hsl(150 32% 38% / 0.18) 0%, transparent 60%),\n linear-gradient(180deg, hsl(150 22% 14%) 0%, hsl(25 10% 9%) 100%);\n border: 1px solid hsl(30 6% 18%);\n overflow: hidden;\n}\n.pdp-reg-fb__glyph {\n position: absolute;\n top: 22px;\n right: 22px;\n width: 56px;\n height: 56px;\n color: var(--r-accent);\n opacity: 0.42;\n pointer-events: none;\n}\n.pdp-reg-fb__kicker {\n display: flex;\n gap: 14px;\n font-size: 10.5px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--r-fg-muted);\n font-weight: 600;\n margin: 0 0 16px;\n}\n.pdp-reg-fb__kicker-region {\n color: var(--r-accent);\n}\n.pdp-reg-fb__headline {\n font-family: var(--r-display);\n font-weight: 440;\n font-variation-settings: 'SOFT' 85;\n margin: 0 0 16px;\n font-size: clamp(24px, 5vw, 32px);\n line-height: 1.1;\n letter-spacing: -0.018em;\n color: var(--r-fg);\n max-width: 18ch;\n}\n.pdp-reg-fb__body {\n font-size: 14.5px;\n line-height: 1.55;\n color: hsl(30 8% 80%);\n margin: 0 0 22px;\n max-width: 50ch;\n}\n.pdp-reg-fb__why {\n font-size: 12px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--r-fg-muted);\n font-weight: 500;\n padding-top: 16px;\n border-top: 1px solid hsl(30 6% 20%);\n}\n.pdp-reg-fb__why-body {\n text-transform: none;\n letter-spacing: 0;\n color: var(--r-fg);\n font-weight: 400;\n font-size: 13.5px;\n margin-left: 10px;\n font-style: italic;\n}\n`;\n\nexport class PdpRegionalFullBleedLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: RegionalData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-reg-fb\" style=\"opacity:0.5;\">\u2026</div>`;\n const d = this.data;\n return html`\n <section class=\"pdp-reg-fb\" aria-label=\"Where you are\">\n <article class=\"pdp-reg-fb__panel\">\n <svg class=\"pdp-reg-fb__glyph\" viewBox=\"0 0 56 56\" aria-hidden=\"true\">\n <circle\n cx=\"28\"\n cy=\"28\"\n r=\"22\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n />\n <path\n d=\"M28 12 L34 28 L28 44 L22 28 Z\"\n fill=\"currentColor\"\n opacity=\"0.5\"\n />\n <path d=\"M28 8 L28 14 M28 42 L28 48 M8 28 L14 28 M42 28 L48 28\"\n stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </svg>\n <div class=\"pdp-reg-fb__kicker\">\n <span class=\"pdp-reg-fb__kicker-region\">${d.region_label}</span>\n <span>${d.temporal_label}</span>\n </div>\n <h3 class=\"pdp-reg-fb__headline\">${d.headline}</h3>\n <p class=\"pdp-reg-fb__body\">${d.body}</p>\n <div class=\"pdp-reg-fb__why\">\n <span>Why this matters</span>\n <span class=\"pdp-reg-fb__why-body\">${d.why_this_matters}</span>\n </div>\n </article>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-regional-fullbleed')) {\n customElements.define('syntro-pdp-regional-fullbleed', PdpRegionalFullBleedLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-regional-fullbleed': PdpRegionalFullBleedLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\ninterface RegionalData {\n region_label: string;\n temporal_label: string;\n headline: string;\n body: string;\n why_this_matters: string;\n rationale: string;\n}\n\n/**\n * Inline-Note variant for the regional module.\n *\n * Compact italic callout with icon-left. Smallest footprint \u2014\n * informational rather than promotional. Headline is the bold lead;\n * body stays small; \"why this matters\" is suppressed in this variant\n * to keep the footprint genuinely tiny.\n */\nfunction ensureStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-regional-inline-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-regional-inline-style';\n s.textContent = CSS;\n document.head.appendChild(s);\n}\n\nconst CSS = `\n.pdp-reg-in {\n --r-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --r-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 68%));\n --r-rule: var(--syntro-pdp-border, hsl(30 6% 22%));\n --r-accent: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --r-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--r-sans);\n color: var(--r-fg);\n display: grid;\n grid-template-columns: 28px 1fr;\n gap: 14px;\n align-items: start;\n padding: 14px 16px;\n border-left: 2px solid var(--r-accent);\n background: hsl(150 16% 12% / 0.5);\n border-radius: 0 12px 12px 0;\n}\n.pdp-reg-in__icon {\n width: 22px;\n height: 22px;\n color: var(--r-accent);\n margin-top: 2px;\n}\n.pdp-reg-in__meta {\n font-size: 10.5px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--r-fg-muted);\n font-weight: 600;\n margin-bottom: 4px;\n}\n.pdp-reg-in__meta-region {\n color: var(--r-accent);\n margin-right: 10px;\n}\n.pdp-reg-in__headline {\n font-size: 14px;\n font-weight: 600;\n color: var(--r-fg);\n line-height: 1.35;\n margin: 0 0 4px;\n}\n.pdp-reg-in__body {\n font-size: 13px;\n line-height: 1.5;\n color: var(--r-fg-muted);\n margin: 0;\n font-style: italic;\n}\n`;\n\nexport class PdpRegionalInlineNoteLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n };\n data: RegionalData | undefined = undefined;\n\n override createRenderRoot() {\n return this;\n }\n override connectedCallback(): void {\n super.connectedCallback();\n ensureStyles();\n }\n\n override render() {\n if (!this.data) return html`<div class=\"pdp-reg-in\" style=\"opacity:0.5;\">\u2026</div>`;\n const d = this.data;\n return html`\n <aside class=\"pdp-reg-in\" aria-label=\"Regional note\">\n <svg class=\"pdp-reg-in__icon\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"/>\n <circle cx=\"11\" cy=\"11\" r=\"2.4\" fill=\"currentColor\"/>\n <path d=\"M11 2 L11 5 M11 17 L11 20 M2 11 L5 11 M17 11 L20 11\"\n stroke=\"currentColor\" stroke-width=\"1.2\" stroke-linecap=\"round\"/>\n </svg>\n <div>\n <div class=\"pdp-reg-in__meta\">\n <span class=\"pdp-reg-in__meta-region\">${d.region_label}</span>\n <span>${d.temporal_label}</span>\n </div>\n <p class=\"pdp-reg-in__headline\">${d.headline}</p>\n <p class=\"pdp-reg-in__body\">${d.body}</p>\n </div>\n </aside>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-regional-inline')) {\n customElements.define('syntro-pdp-regional-inline', PdpRegionalInlineNoteLit);\n}\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-regional-inline': PdpRegionalInlineNoteLit;\n }\n}\n", "import { html, LitElement } from 'lit';\n\n/**\n * Neutral placeholder for PDP slots that don't have a live sub-agent in v1.\n * Shows a soft \"more personalized content coming soon\" message rather than\n * a misleading skeleton that implies a load is in flight.\n */\nexport class PdpSlotPlaceholderLit extends LitElement {\n static override properties = {\n label: { type: String },\n };\n\n label = 'More personalized content coming soon';\n\n override createRenderRoot() {\n return this;\n }\n\n override render() {\n return html`\n <div class=\"pdp-slot-placeholder\" data-placeholder>\n <p>${this.label}</p>\n <small>Ask in chat to explore this product further.</small>\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-slot-placeholder')) {\n customElements.define('syntro-pdp-slot-placeholder', PdpSlotPlaceholderLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-slot-placeholder': PdpSlotPlaceholderLit;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-ribbon> \u2014 the slim, italic \"personalized for you\" pill that\n * sits between a PDP hero and the first agent-authored section.\n *\n * Props:\n * - `topic` (string, required) \u2014 the phrase the agent personalized around\n * (\"absorption\", \"stimulant-free focus\", etc.).\n * Rendered in primary color, non-italic.\n * - `subtitle` (string, optional) \u2014 trailing fade-out text. Default: a copy\n * deck appropriate for v1 (\"Sections below\n * were authored just for you.\").\n *\n * Brand customization:\n * The widget reads three CSS variables from its host context; each has a\n * fallback so it just works in the dark / sage healthmaxxer-ish default.\n * Host pages can override:\n * --syntro-pdp-primary primary accent color\n * --syntro-pdp-fg body foreground color\n * --syntro-pdp-card-bg card background color\n * --syntro-pdp-display display font override (inherits host font if unset)\n */\n\nfunction ensureRibbonStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-ribbon-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-ribbon-style';\n s.textContent = RIBBON_CSS;\n document.head.appendChild(s);\n}\n\nconst RIBBON_CSS = `\n.syntro-pdp-ribbon {\n --rb-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --rb-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --rb-muted: var(--syntro-pdp-muted, hsl(30 8% 55%));\n --rb-bg-a: var(--syntro-pdp-ribbon-bg-a, hsl(25 10% 11%));\n --rb-bg-b: var(--syntro-pdp-ribbon-bg-b, hsl(25 10% 13%));\n --rb-display: var(--syntro-pdp-display, inherit);\n\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 22px;\n border-radius: 999px;\n border: 1px solid color-mix(in srgb, var(--rb-primary) 30%, transparent);\n background: linear-gradient(90deg, var(--rb-bg-a) 0%, var(--rb-bg-b) 50%, var(--rb-bg-a) 100%);\n overflow: hidden;\n /* Resting state is VISIBLE (GPU/compositing hazard fix): the entrance is\n gated behind the transient .is-entering class so a dropped/suppressed\n animation can never leave the ribbon permanently at opacity 0. */\n opacity: 1;\n color: var(--rb-fg);\n font-family: var(--syntro-pdp-sans, inherit);\n}\n.syntro-pdp-ribbon.is-entering {\n opacity: 0;\n animation: syntro-pdp-fade-up 700ms cubic-bezier(0.2, 0.7, 0.2, 1) 340ms both;\n}\n.syntro-pdp-ribbon::before {\n content: '';\n position: absolute;\n inset: 0;\n background: radial-gradient(\n 80% 120% at 50% 0%,\n color-mix(in srgb, var(--rb-primary) 10%, transparent) 0%,\n transparent 60%\n );\n pointer-events: none;\n}\n.syntro-pdp-ribbon-sweep {\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 90deg,\n transparent 0%,\n color-mix(in srgb, var(--rb-primary) 18%, transparent) 50%,\n transparent 100%\n );\n transform: translateX(-110%);\n animation: syntro-pdp-sweep 2200ms cubic-bezier(0.4, 0, 0.2, 1) 700ms forwards;\n pointer-events: none;\n}\n.syntro-pdp-ribbon-spark {\n flex: 0 0 auto;\n width: 16px;\n height: 16px;\n display: inline-block;\n color: var(--rb-primary);\n}\n.syntro-pdp-ribbon-copy {\n font-family: var(--rb-display);\n font-style: italic;\n font-size: 16px;\n line-height: 1.4;\n letter-spacing: -0.005em;\n font-variation-settings: 'SOFT' 60;\n margin: 0;\n position: relative;\n z-index: 1;\n}\n.syntro-pdp-ribbon-topic {\n font-style: normal;\n font-family: var(--syntro-pdp-sans, inherit);\n font-weight: 500;\n color: var(--rb-primary);\n font-size: 14.5px;\n letter-spacing: 0;\n}\n.syntro-pdp-ribbon-trail {\n color: var(--rb-muted);\n}\n\n@media (max-width: 640px) {\n .syntro-pdp-ribbon {\n border-radius: 22px;\n padding: 12px 16px;\n align-items: flex-start;\n }\n .syntro-pdp-ribbon-spark {\n margin-top: 2px;\n }\n .syntro-pdp-ribbon-copy {\n font-size: 14px;\n line-height: 1.45;\n }\n .syntro-pdp-ribbon-topic {\n font-size: 13.5px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .syntro-pdp-ribbon.is-entering {\n opacity: 1;\n animation: none;\n }\n .syntro-pdp-ribbon-sweep {\n animation: none;\n }\n}\n\n@keyframes syntro-pdp-fade-up {\n from { opacity: 0; transform: translateY(18px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes syntro-pdp-sweep {\n 0% { transform: translateX(-110%); }\n 100% { transform: translateX(110%); }\n}\n`;\n\nexport class PdpRibbonLit extends LitElement {\n static override properties = {\n topic: { type: String },\n subtitle: { type: String },\n _entering: { state: true },\n };\n\n topic = '';\n subtitle = 'Sections below were authored just for you.';\n\n /** Transient entrance flag \u2014 true until the mount animation settles. */\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureRibbonStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.syntro-pdp-ribbon');\n if (!container) return;\n this._armed = true;\n // fade-up: 700ms + 340ms delay; belt margin covers a late frame.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1300,\n true\n );\n }\n\n override render() {\n const cls = this._entering ? 'syntro-pdp-ribbon is-entering' : 'syntro-pdp-ribbon';\n return html`\n <div class=\"${cls}\" role=\"status\" aria-live=\"polite\">\n <span class=\"syntro-pdp-ribbon-sweep\" aria-hidden=\"true\"></span>\n <svg\n class=\"syntro-pdp-ribbon-spark\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5z\"\n />\n <path d=\"M19 4l.7 2L21 6.7l-1.3.7L19 9l-.7-1.6L17 6.7l1.3-.7z\" />\n </svg>\n <p class=\"syntro-pdp-ribbon-copy\">\n Tailored to your conversation about\n ${\n this.topic\n ? html` <span class=\"syntro-pdp-ribbon-topic\">${this.topic}</span>.`\n : nothing\n }\n <span class=\"syntro-pdp-ribbon-trail\"> ${this.subtitle}</span>\n </p>\n </div>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-ribbon')) {\n customElements.define('syntro-pdp-ribbon', PdpRibbonLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-ribbon': PdpRibbonLit;\n }\n}\n", "import { html, LitElement, nothing } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-qa> \u2014 agent-authored Q&A section for a PDP slot.\n *\n * Each item is a `<details>` element so the open/closed UX is native (works\n * without JS, screen-reader-correct, keyboard-accessible). The default\n * disclosure triangle is hidden in favor of an animated plus/minus glyph\n * that matches the rest of the PDP visual vocabulary.\n *\n * Props:\n * - `data` (QaData): `{ items: [{question, answer}], rationale }`.\n *\n * Brand customization via the standard `--syntro-pdp-*` token namespace.\n */\n\ninterface QaItem {\n question: string;\n answer: string;\n}\n\ninterface QaData {\n items: QaItem[];\n rationale: string;\n}\n\nfunction ensureQaStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-qa-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-qa-style';\n s.textContent = QA_CSS;\n document.head.appendChild(s);\n}\n\nconst QA_CSS = `\n.pdp-qa {\n --qa-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --qa-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 60%));\n --qa-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --qa-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --qa-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --qa-display: var(--syntro-pdp-display, inherit);\n --qa-sans: var(--syntro-pdp-sans, inherit);\n\n font-family: var(--qa-sans);\n color: var(--qa-fg);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.pdp-qa-item {\n border: 1px solid var(--qa-border);\n border-radius: 16px;\n /* Surface tracks the theme card-bg token (dark host \u2192 dark, light host \u2192\n light) so the question text \u2014 which reads the theme fg token \u2014 never\n renders same-on-same. */\n background: linear-gradient(180deg, color-mix(in srgb, var(--qa-bg) 94%, white), var(--qa-bg));\n overflow: hidden;\n transition: border-color 200ms ease, background 200ms ease;\n /* Resting state VISIBLE (GPU/compositing hazard fix): items default to\n opacity 1; the stagger is re-armed only under .pdp-qa.is-entering so a\n suppressed animation can never leave the FAQ list blank. */\n opacity: 1;\n}\n.pdp-qa.is-entering .pdp-qa-item {\n opacity: 0;\n transform: translateY(12px);\n animation: pdp-qa-fade-up 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n}\n.pdp-qa.is-entering .pdp-qa-item:nth-child(1) { animation-delay: 60ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(2) { animation-delay: 140ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(3) { animation-delay: 220ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(4) { animation-delay: 300ms; }\n.pdp-qa.is-entering .pdp-qa-item:nth-child(5) { animation-delay: 380ms; }\n.pdp-qa-item:hover {\n border-color: color-mix(in srgb, var(--qa-primary) 35%, var(--qa-border));\n}\n.pdp-qa-item[open] {\n border-color: color-mix(in srgb, var(--qa-primary) 55%, var(--qa-border));\n background: linear-gradient(180deg, color-mix(in srgb, var(--qa-bg) 90%, white), var(--qa-bg));\n}\n\n.pdp-qa-question {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 18px 22px;\n cursor: pointer;\n list-style: none;\n user-select: none;\n font-family: var(--qa-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 60;\n font-size: clamp(16px, 2vw, 19px);\n line-height: 1.3;\n letter-spacing: -0.005em;\n color: var(--qa-fg);\n}\n.pdp-qa-question::-webkit-details-marker {\n display: none;\n}\n.pdp-qa-question::marker {\n display: none;\n}\n\n.pdp-qa-toggle {\n flex: 0 0 auto;\n width: 26px;\n height: 26px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--qa-primary) 14%, transparent);\n display: grid;\n place-items: center;\n color: color-mix(in srgb, var(--qa-primary) 80%, white 20%);\n transition: background 200ms ease, transform 200ms ease;\n}\n.pdp-qa-item[open] .pdp-qa-toggle {\n background: var(--qa-primary);\n color: var(--qa-bg);\n transform: rotate(45deg);\n}\n.pdp-qa-toggle svg {\n width: 12px;\n height: 12px;\n}\n\n.pdp-qa-answer-wrap {\n padding: 0 22px 20px;\n color: var(--qa-fg-muted);\n font-size: 14.5px;\n line-height: 1.6;\n border-top: 1px solid color-mix(in srgb, var(--qa-primary) 20%, transparent);\n padding-top: 16px;\n margin-top: 0;\n}\n\n@media (max-width: 640px) {\n .pdp-qa-item {\n border-radius: 14px;\n }\n .pdp-qa-question {\n padding: 16px 18px;\n font-size: 15.5px;\n }\n .pdp-qa-answer-wrap {\n padding: 14px 18px 18px;\n font-size: 14px;\n }\n}\n\n/* skeleton */\n.pdp-qa-skeleton {\n display: grid;\n gap: 10px;\n}\n.pdp-qa-skeleton-row {\n height: 60px;\n border-radius: 14px;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--qa-bg) 92%, white) 0%,\n color-mix(in srgb, var(--qa-bg) 78%, white) 50%,\n color-mix(in srgb, var(--qa-bg) 92%, white) 100%\n );\n background-size: 200% 100%;\n animation: pdp-qa-shimmer 1.6s ease-in-out infinite;\n}\n\n@keyframes pdp-qa-fade-up {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes pdp-qa-shimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-qa.is-entering .pdp-qa-item {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n`;\n\nexport class PdpQaLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: QaData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureQaStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-qa');\n if (!container) return;\n this._armed = true;\n // staggered items: last delay 380ms + 600ms duration.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1200\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-qa-skeleton\">\n <div class=\"pdp-qa-skeleton-row\"></div>\n <div class=\"pdp-qa-skeleton-row\"></div>\n <div class=\"pdp-qa-skeleton-row\"></div>\n </div>`;\n }\n return html`\n <section\n class=\"${this._entering ? 'pdp-qa is-entering' : 'pdp-qa'}\"\n aria-label=\"Frequently asked questions\"\n >\n ${this.data.items.map(\n (item, idx) => html`\n <details class=\"pdp-qa-item\" ?open=${idx === 0}>\n <summary class=\"pdp-qa-question\">\n <span>${item.question}</span>\n <span class=\"pdp-qa-toggle\" aria-hidden=\"true\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.5\" stroke-linecap=\"round\">\n <path d=\"M12 5v14M5 12h14\" />\n </svg>\n </span>\n </summary>\n <div class=\"pdp-qa-answer-wrap\">${item.answer}</div>\n </details>\n `\n )}\n ${nothing}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-qa')) {\n customElements.define('syntro-pdp-qa', PdpQaLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-qa': PdpQaLit;\n }\n}\n", "import { html, LitElement } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-regional> \u2014 region- and temporal-context personalization tile.\n *\n * The agent surfaces a \"what's happening near you / right now that matters\n * for this product\" framing. The visual treatment is a single rich\n * full-width card with a quietly-textured background (suggests place /\n * season), a region \u00B7 time pill, a serif headline, body copy, and a\n * \"why this matters for you\" footer line tinted in brand color.\n *\n * Props:\n * - `data` (RegionalData): `{ region_label, temporal_label, headline,\n * body, why_this_matters, rationale }`.\n */\n\ninterface RegionalData {\n region_label: string; // \"Pacific Northwest\"\n temporal_label: string; // \"October 2026\"\n headline: string;\n body: string;\n why_this_matters: string;\n rationale: string;\n}\n\nfunction ensureRegionalStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-regional-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-regional-style';\n s.textContent = REGIONAL_CSS;\n document.head.appendChild(s);\n}\n\nconst REGIONAL_CSS = `\n.pdp-regional {\n --rg-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --rg-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 65%));\n --rg-bg: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --rg-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --rg-accent: var(--syntro-pdp-accent, hsl(95 18% 48%));\n --rg-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --rg-display: var(--syntro-pdp-display, inherit);\n --rg-sans: var(--syntro-pdp-sans, inherit);\n\n position: relative;\n border: 1px solid var(--rg-border);\n border-radius: 24px;\n padding: 36px 36px 32px;\n overflow: hidden;\n font-family: var(--rg-sans);\n color: var(--rg-fg);\n background:\n radial-gradient(\n 90% 90% at 100% 100%,\n color-mix(in srgb, var(--rg-accent) 14%, transparent) 0%,\n transparent 60%\n ),\n radial-gradient(\n 70% 80% at 0% 0%,\n color-mix(in srgb, var(--rg-primary) 12%, transparent) 0%,\n transparent 65%\n ),\n linear-gradient(180deg, color-mix(in srgb, var(--rg-bg) 94%, white), var(--rg-bg));\n /* Resting state VISIBLE (GPU/compositing hazard fix) \u2014 entrance gated on\n the transient .is-entering class so a suppressed animation cannot strand\n the card at opacity 0. */\n opacity: 1;\n}\n.pdp-regional.is-entering {\n opacity: 0;\n transform: translateY(18px);\n animation: pdp-regional-enter 700ms cubic-bezier(0.2, 0.7, 0.2, 1) 120ms both;\n}\n\n/* Soft geographic-grid texture in the background \u2014 a personalization\n atmospheric, not a real map. */\n.pdp-regional::before {\n content: '';\n position: absolute;\n inset: 0;\n background-image:\n linear-gradient(\n to right,\n color-mix(in srgb, var(--rg-primary) 6%, transparent) 1px,\n transparent 1px\n ),\n linear-gradient(\n to bottom,\n color-mix(in srgb, var(--rg-primary) 6%, transparent) 1px,\n transparent 1px\n );\n background-size: 36px 36px;\n mask-image: radial-gradient(80% 80% at 50% 50%, black, transparent 75%);\n -webkit-mask-image: radial-gradient(80% 80% at 50% 50%, black, transparent 75%);\n pointer-events: none;\n opacity: 0.6;\n}\n\n.pdp-regional-pill {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n padding: 6px 12px 6px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--rg-primary) 12%, transparent);\n border: 1px solid color-mix(in srgb, var(--rg-primary) 30%, transparent);\n font-size: 11px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--rg-primary) 75%, white 25%);\n font-weight: 500;\n margin-bottom: 22px;\n}\n.pdp-regional-pill svg {\n width: 12px;\n height: 12px;\n flex: 0 0 auto;\n}\n.pdp-regional-pill-sep {\n width: 3px;\n height: 3px;\n border-radius: 50%;\n background: currentColor;\n opacity: 0.6;\n}\n\n.pdp-regional-headline {\n position: relative;\n font-family: var(--rg-display);\n font-weight: 400;\n font-size: clamp(26px, 3.4vw, 40px);\n line-height: 1.08;\n letter-spacing: -0.02em;\n color: var(--rg-fg);\n font-variation-settings: 'SOFT' 80, 'WONK' 1;\n margin: 0 0 16px;\n max-width: 22ch;\n}\n.pdp-regional-headline em {\n font-style: italic;\n font-variation-settings: 'SOFT' 100, 'WONK' 1;\n color: var(--rg-primary);\n}\n\n.pdp-regional-body {\n position: relative;\n font-size: 15.5px;\n line-height: 1.55;\n color: var(--rg-fg-muted);\n margin: 0;\n max-width: 56ch;\n}\n\n.pdp-regional-footer {\n position: relative;\n margin-top: 24px;\n padding-top: 18px;\n border-top: 1px dashed color-mix(in srgb, var(--rg-primary) 30%, transparent);\n display: flex;\n align-items: flex-start;\n gap: 12px;\n font-style: italic;\n font-family: var(--rg-display);\n font-size: 14.5px;\n font-variation-settings: 'SOFT' 70;\n color: color-mix(in srgb, var(--rg-primary) 65%, white 35%);\n line-height: 1.5;\n}\n.pdp-regional-footer-spark {\n flex: 0 0 auto;\n width: 16px;\n height: 16px;\n margin-top: 4px;\n}\n\n@media (max-width: 640px) {\n .pdp-regional {\n padding: 26px 22px 24px;\n border-radius: 20px;\n }\n .pdp-regional-pill {\n margin-bottom: 16px;\n font-size: 10px;\n }\n .pdp-regional-headline {\n font-size: clamp(22px, 7vw, 28px);\n max-width: 16ch;\n }\n .pdp-regional-body {\n font-size: 14px;\n }\n .pdp-regional-footer {\n margin-top: 18px;\n padding-top: 14px;\n font-size: 13px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-regional.is-entering {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n\n@keyframes pdp-regional-enter {\n from { opacity: 0; transform: translateY(18px); }\n to { opacity: 1; transform: translateY(0); }\n}\n`;\n\nexport class PdpRegionalLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: RegionalData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensureRegionalStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-regional');\n if (!container) return;\n this._armed = true;\n // enter: 700ms + 120ms delay.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1100,\n true\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-qa-skeleton\" style=\"height:200px;\">\n <div class=\"pdp-qa-skeleton-row\" style=\"height:200px;border-radius:24px;\"></div>\n </div>`;\n }\n return html`\n <section\n class=\"${this._entering ? 'pdp-regional is-entering' : 'pdp-regional'}\"\n aria-label=\"Regional and temporal context\"\n >\n <div class=\"pdp-regional-pill\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M12 21s-7-6.5-7-12a7 7 0 1 1 14 0c0 5.5-7 12-7 12z\" />\n <circle cx=\"12\" cy=\"9\" r=\"2.5\" />\n </svg>\n <span>${this.data.region_label}</span>\n <span class=\"pdp-regional-pill-sep\" aria-hidden=\"true\"></span>\n <span>${this.data.temporal_label}</span>\n </div>\n <h3 class=\"pdp-regional-headline\">${this.data.headline}</h3>\n <p class=\"pdp-regional-body\">${this.data.body}</p>\n <p class=\"pdp-regional-footer\">\n <svg\n class=\"pdp-regional-footer-spark\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5z\" />\n </svg>\n ${this.data.why_this_matters}\n </p>\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-regional')) {\n customElements.define('syntro-pdp-regional', PdpRegionalLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-regional': PdpRegionalLit;\n }\n}\n", "import { html, LitElement } from 'lit';\nimport { armEntranceSettle } from './entrance';\n\n/**\n * <syntro-pdp-peer-feed> \u2014 \"users like you\" social-proof feed.\n *\n * Renders 3\u20134 short usage snippets from users the agent considers similar\n * to the viewer (by goals, demographic, stack, etc.). Each card surfaces a\n * persona label, a verbatim-style quote, a similarity reason, and a\n * relative timestamp. The visual treatment is a stacked feed (think\n * intentionally-quiet timeline) rather than a uniform grid \u2014 so a long\n * quote can breathe.\n *\n * Props:\n * - `data` (PeerData): `{ peers: PeerPost[], rationale }`.\n */\n\ninterface PeerPost {\n initials: string; // \"M.K.\" \u2014 1\u20134 chars, displayed in an avatar circle\n persona: string; // \"Software engineer, 32, focus-oriented goals\"\n quote: string;\n similarity_reason: string; // \"shares your nootropic focus goal\"\n time_ago: string; // \"3 days ago\"\n}\n\ninterface PeerData {\n peers: PeerPost[];\n rationale: string;\n}\n\nfunction ensurePeerFeedStyles(): void {\n if (typeof document === 'undefined') return;\n if (document.getElementById('syntro-pdp-peer-feed-style')) return;\n const s = document.createElement('style');\n s.id = 'syntro-pdp-peer-feed-style';\n s.textContent = PEER_FEED_CSS;\n document.head.appendChild(s);\n}\n\nconst PEER_FEED_CSS = `\n.pdp-peer-feed {\n --pf-fg: var(--syntro-pdp-fg, hsl(35 15% 92%));\n --pf-fg-muted: var(--syntro-pdp-muted, hsl(30 8% 62%));\n --pf-bg-a: var(--syntro-pdp-card-bg, hsl(25 10% 11%));\n --pf-border: var(--syntro-pdp-border, hsl(30 6% 18%));\n --pf-primary: var(--syntro-pdp-primary, hsl(150 32% 38%));\n --pf-display: var(--syntro-pdp-display, inherit);\n --pf-sans: var(--syntro-pdp-sans, inherit);\n\n display: grid;\n gap: 14px;\n font-family: var(--pf-sans);\n color: var(--pf-fg);\n}\n\n.pdp-peer-post {\n position: relative;\n display: grid;\n grid-template-columns: auto 1fr;\n gap: 16px;\n padding: 20px 22px;\n border: 1px solid var(--pf-border);\n border-radius: 18px;\n /* Card surface derives from the theme card-bg token so it tracks the host:\n dark on a dark host (healthmaxxer), light on a light host (vela). A\n hardcoded dark surface here rendered dark-on-dark once the host set a\n dark --syntro-pdp-fg for its own light page. */\n background: linear-gradient(180deg, color-mix(in srgb, var(--pf-bg-a) 94%, white), var(--pf-bg-a));\n /* Resting state VISIBLE (GPU/compositing hazard fix): posts default to\n opacity 1 so a dropped/suppressed stagger can never leave the whole feed\n blank. The entrance is re-armed only under the container's .is-entering. */\n opacity: 1;\n transition: border-color 200ms ease;\n}\n.pdp-peer-feed.is-entering .pdp-peer-post {\n opacity: 0;\n transform: translateY(14px);\n animation: pdp-pf-fade-up 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;\n}\n.pdp-peer-post:hover {\n border-color: color-mix(in srgb, var(--pf-primary) 35%, var(--pf-border));\n}\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(1) { animation-delay: 60ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(2) { animation-delay: 140ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(3) { animation-delay: 220ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(4) { animation-delay: 300ms; }\n.pdp-peer-feed.is-entering .pdp-peer-post:nth-child(5) { animation-delay: 380ms; }\n\n.pdp-peer-avatar {\n flex: 0 0 auto;\n width: 44px;\n height: 44px;\n border-radius: 50%;\n background: linear-gradient(\n 135deg,\n color-mix(in srgb, var(--pf-primary) 55%, white 8%),\n color-mix(in srgb, var(--pf-primary) 95%, black 10%)\n );\n /* Initials sit on the saturated primary chip; a near-white reads on any\n brand primary (green, maroon, \u2026), where a hardcoded near-black failed\n once the primary was a dark brand ink. */\n color: color-mix(in srgb, white 90%, var(--pf-primary));\n display: grid;\n place-items: center;\n font-family: var(--pf-display);\n font-weight: 500;\n font-variation-settings: 'SOFT' 70;\n font-size: 15px;\n letter-spacing: 0.01em;\n border: 1px solid color-mix(in srgb, var(--pf-primary) 60%, transparent);\n box-shadow: 0 0 0 4px color-mix(in srgb, var(--pf-primary) 8%, transparent);\n}\n\n.pdp-peer-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.pdp-peer-meta {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--pf-fg-muted);\n font-weight: 500;\n}\n.pdp-peer-persona {\n color: var(--pf-fg-muted);\n}\n.pdp-peer-meta-sep {\n opacity: 0.4;\n}\n.pdp-peer-time {\n color: var(--pf-fg-muted);\n}\n\n.pdp-peer-quote {\n font-family: var(--pf-display);\n font-style: italic;\n font-weight: 400;\n font-size: clamp(15px, 1.8vw, 17.5px);\n line-height: 1.5;\n font-variation-settings: 'SOFT' 70;\n color: var(--pf-fg);\n margin: 0;\n letter-spacing: -0.005em;\n}\n.pdp-peer-quote::before {\n content: '\u201C ';\n color: color-mix(in srgb, var(--pf-primary) 60%, transparent);\n font-size: 1.4em;\n vertical-align: -0.2em;\n margin-right: -2px;\n}\n.pdp-peer-quote::after {\n content: ' \u201D';\n color: color-mix(in srgb, var(--pf-primary) 60%, transparent);\n font-size: 1.4em;\n vertical-align: -0.2em;\n margin-left: -2px;\n}\n\n.pdp-peer-similarity {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n font-size: 12.5px;\n color: color-mix(in srgb, var(--pf-primary) 75%, white 25%);\n font-style: italic;\n font-family: var(--pf-display);\n margin-top: 2px;\n}\n.pdp-peer-similarity-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--pf-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--pf-primary) 14%, transparent);\n}\n\n@media (max-width: 640px) {\n .pdp-peer-post {\n padding: 16px 16px;\n border-radius: 16px;\n gap: 12px;\n }\n .pdp-peer-avatar {\n width: 38px;\n height: 38px;\n font-size: 13px;\n }\n .pdp-peer-meta {\n font-size: 10px;\n }\n .pdp-peer-quote {\n font-size: 15px;\n }\n .pdp-peer-similarity {\n font-size: 12px;\n }\n}\n\n/* skeleton */\n.pdp-peer-skeleton {\n display: grid;\n gap: 12px;\n}\n.pdp-peer-skeleton-row {\n height: 100px;\n border-radius: 18px;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--pf-bg-a) 92%, white) 0%,\n color-mix(in srgb, var(--pf-bg-a) 78%, white) 50%,\n color-mix(in srgb, var(--pf-bg-a) 92%, white) 100%\n );\n background-size: 200% 100%;\n animation: pdp-pf-shimmer 1.6s ease-in-out infinite;\n}\n\n@keyframes pdp-pf-fade-up {\n from { opacity: 0; transform: translateY(14px); }\n to { opacity: 1; transform: translateY(0); }\n}\n@keyframes pdp-pf-shimmer {\n 0% { background-position: -200% 0; }\n 100% { background-position: 200% 0; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pdp-peer-feed.is-entering .pdp-peer-post {\n opacity: 1;\n transform: none;\n animation: none;\n }\n}\n`;\n\nexport class PdpPeerFeedLit extends LitElement {\n static override properties = {\n data: { attribute: false },\n _entering: { state: true },\n };\n\n data: PeerData | undefined = undefined;\n _entering = true;\n private _armed = false;\n private _disposeEntrance?: () => void;\n\n override createRenderRoot() {\n return this;\n }\n\n override connectedCallback(): void {\n super.connectedCallback();\n ensurePeerFeedStyles();\n // Reconnect does not schedule a Lit update; nudge one so updated() re-arms\n // the entrance belt when a DOM move interrupted the entrance.\n if (this._entering) this.requestUpdate();\n }\n\n override disconnectedCallback(): void {\n this._disposeEntrance?.();\n // Re-arm on reconnect: a DOM move (deck reorder) disconnects mid-entrance;\n // without this reset the belt never re-arms and .is-entering is stranded\n // (blank widget when the animation is suppressed \u2014 THE hazard).\n this._armed = false;\n super.disconnectedCallback();\n }\n\n override updated(): void {\n if (this._armed || !this._entering) return;\n const container = this.querySelector<HTMLElement>('.pdp-peer-feed');\n if (!container) return;\n this._armed = true;\n // staggered posts: last delay 380ms + 600ms duration. No fast path \u2014 the\n // belt must outlast the last child so the class is never pulled mid-stagger.\n this._disposeEntrance = armEntranceSettle(\n () => {\n this._entering = false;\n },\n container,\n 1200\n );\n }\n\n override render() {\n if (!this.data) {\n return html`<div data-skeleton class=\"pdp-peer-skeleton\">\n <div class=\"pdp-peer-skeleton-row\"></div>\n <div class=\"pdp-peer-skeleton-row\"></div>\n <div class=\"pdp-peer-skeleton-row\"></div>\n </div>`;\n }\n return html`\n <section\n class=\"${this._entering ? 'pdp-peer-feed is-entering' : 'pdp-peer-feed'}\"\n aria-label=\"Users like you\"\n >\n ${this.data.peers.map(\n (peer) => html`\n <article class=\"pdp-peer-post\">\n <div class=\"pdp-peer-avatar\" aria-hidden=\"true\">${peer.initials}</div>\n <div class=\"pdp-peer-body\">\n <div class=\"pdp-peer-meta\">\n <span class=\"pdp-peer-persona\">${peer.persona}</span>\n <span class=\"pdp-peer-meta-sep\">\u00B7</span>\n <span class=\"pdp-peer-time\">${peer.time_ago}</span>\n </div>\n <p class=\"pdp-peer-quote\">${peer.quote}</p>\n <div class=\"pdp-peer-similarity\">\n <span class=\"pdp-peer-similarity-dot\" aria-hidden=\"true\"></span>\n <span>${peer.similarity_reason}</span>\n </div>\n </div>\n </article>\n `\n )}\n </section>\n `;\n }\n}\n\nif (typeof customElements !== 'undefined' && !customElements.get('syntro-pdp-peer-feed')) {\n customElements.define('syntro-pdp-peer-feed', PdpPeerFeedLit);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'syntro-pdp-peer-feed': PdpPeerFeedLit;\n }\n}\n", "/**\n * adaptive-product \u2014 Runtime manifest\n *\n * Exports the runtime descriptor consumed by the SDK's AppLoader.\n * Registers the four <syntro-product-*> custom elements as a side effect\n * of importing this module, and exposes widget mountables used by\n * SmartCanvasRuntime's WidgetRegistry.\n */\n\nimport { type MountPlumbing, stripMountPlumbing } from '@syntrologie/sdk-contracts';\nimport { startNativeSlotWatcher } from './native/nativeSlotWatcher';\nimport type {\n CardProps,\n ComparisonProps,\n GridProps,\n HeroProps,\n RecoProps,\n SyntroPdpProps,\n} from './schema';\nimport { ProductCardLit } from './widgets/ProductCardLit';\nimport { ProductComparisonLit } from './widgets/ProductComparisonLit';\nimport { ProductGridLit } from './widgets/ProductGridLit';\nimport { ProductHeroLit } from './widgets/ProductHeroLit';\nimport { ProductRecoCarouselLit } from './widgets/ProductRecoCarouselLit';\nimport './widgets/SyntroPdpLit';\nimport { SyntroPdpLit } from './widgets/SyntroPdpLit';\nimport './widgets/PdpTrendingNewsLit';\nimport './widgets/PdpTrendingNewsMagazineLit';\nimport './widgets/PdpTrendingNewsHeroLit';\nimport './widgets/PdpPeerFeedMasonryLit';\nimport './widgets/PdpPeerFeedCarouselLit';\nimport './widgets/PdpChartLit';\nimport './widgets/PdpChartBarStackedLit';\nimport './widgets/PdpChartMetricPullLit';\nimport './widgets/PdpQaAccordionLit';\nimport './widgets/PdpQaSideBySideLit';\nimport './widgets/PdpRegionalFullBleedLit';\nimport './widgets/PdpRegionalInlineNoteLit';\nimport './widgets/PdpSlotPlaceholderLit';\nimport './widgets/PdpRibbonLit';\nimport './widgets/PdpSectionHeaderLit';\nimport './widgets/PdpSlotLit';\nimport './widgets/PdpQaLit';\nimport './widgets/PdpRegionalLit';\nimport './widgets/PdpPeerFeedLit';\nimport './widgets/PdpOtherProductsCarouselLit';\nimport './widgets/PdpOtherProductsGridLit';\n\nconst TAGS = {\n card: 'syntro-product-card',\n comparison: 'syntro-product-comparison',\n hero: 'syntro-product-hero',\n grid: 'syntro-product-grid',\n pdp: 'syntro-pdp',\n reco: 'syntro-product-reco-carousel',\n} as const;\n\n/* v8 ignore next -- browser element registry guard; runtime import/idempotence is covered in jsdom. */\nif (typeof customElements !== 'undefined') {\n if (!customElements.get(TAGS.card)) customElements.define(TAGS.card, ProductCardLit);\n if (!customElements.get(TAGS.comparison))\n customElements.define(TAGS.comparison, ProductComparisonLit);\n if (!customElements.get(TAGS.hero)) customElements.define(TAGS.hero, ProductHeroLit);\n if (!customElements.get(TAGS.grid)) customElements.define(TAGS.grid, ProductGridLit);\n if (!customElements.get(TAGS.pdp)) customElements.define(TAGS.pdp, SyntroPdpLit);\n if (!customElements.get(TAGS.reco)) customElements.define(TAGS.reco, ProductRecoCarouselLit);\n}\n\n// Native-section takeover for hosts that render their own FAQ/review slots but\n// no <syntro-pdp> element \u2014 the vela SPA quick-view modal (FEAT-1784346204).\n// Runs AFTER the element registrations so the self-guard (`document.querySelector\n// ('syntro-pdp')`) sees an upgraded element on pages the element already owns and\n// stays inert there. Idempotent.\n/* v8 ignore next -- browser document guard; native watcher behavior is covered through its test hooks. */\nif (typeof document !== 'undefined') startNativeSlotWatcher();\n\n// \u2500\u2500\u2500 <syntro-pdp> runtime wirer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// The element needs a `runtimeRef` to gather signals + score the visitor\n// archetype for the config-first render. The widget mountable path\n// (runtime.widgets.mount) wires it\n// automatically when an action-plan tile mounts the element. But the\n// PRIMARY use case is customer JSX that places <syntro-pdp> directly \u2014\n// that path never calls widgets.mount, so runtimeRef would stay null\n// forever and the element would sit in skeleton state.\n//\n// Rather than have the element silently fall back to a global lookup\n// (which would obscure the contract), the runtime owns the wire-up\n// explicitly: when adaptive-product is registered against a runtime\n// instance, every <syntro-pdp> currently on the page gets `runtimeRef`\n// set, AND a MutationObserver watches for future additions so a\n// late-mounted element also gets wired. The element's only contract is\n// \"I need runtimeRef\" \u2014 provisioning is the runtime's job.\n//\n// Called once per (runtime, document) pair below in `register()`.\n\nconst _WIRED_RUNTIMES = new WeakSet<object>();\nconst _ACTIVE_OBSERVERS: MutationObserver[] = [];\n\ninterface PdpHostElement extends HTMLElement {\n runtimeRef?: unknown;\n}\n\nfunction wirePdpElements(runtime: object, doc: Document): MutationObserver {\n // Wire everything already in the DOM.\n for (const el of Array.from(doc.querySelectorAll<PdpHostElement>(TAGS.pdp))) {\n if (el.runtimeRef == null) el.runtimeRef = runtime;\n }\n // Watch for future additions.\n const observer = new MutationObserver((mutations) => {\n for (const m of mutations) {\n for (const node of Array.from(m.addedNodes)) {\n if (!(node instanceof HTMLElement)) continue;\n // Newly-added element itself\n if (node.tagName.toLowerCase() === TAGS.pdp) {\n const host = node as PdpHostElement;\n if (host.runtimeRef == null) host.runtimeRef = runtime;\n }\n // Any descendants\n for (const desc of Array.from(node.querySelectorAll<PdpHostElement>(TAGS.pdp))) {\n if (desc.runtimeRef == null) desc.runtimeRef = runtime;\n }\n }\n }\n });\n observer.observe(doc.documentElement, { childList: true, subtree: true });\n _ACTIVE_OBSERVERS.push(observer);\n return observer;\n}\n\n/**\n * Test seam. Disconnects all wirer observers and clears the\n * \"already wired\" set so subsequent tests can call `onActivate` again\n * with fresh state. NOT a production API.\n */\nexport const _testing = {\n resetWirer(): void {\n for (const obs of _ACTIVE_OBSERVERS) obs.disconnect();\n _ACTIVE_OBSERVERS.length = 0;\n // WeakSet has no clear() method; nothing held externally still\n // references the old runtimes, so they'll be collected. For the\n // pathological case of tests re-using the SAME runtime object\n // across resets, that's still no-op'd correctly because we just\n // disconnected the observer and the wire-on-existing path will\n // re-wire whatever's in the DOM on the next onActivate call.\n },\n};\n\n/**\n * AppRegistry calls this once at activation via the manifest's\n * `runtime.onActivate` hook (see cdn.ts). Wires runtimeRef onto every\n * <syntro-pdp> element on the page AND sets up a MutationObserver for\n * future additions. Idempotent per runtime instance \u2014 calling twice\n * with the same runtime is a no-op on the second call.\n *\n * Returns a teardown that disconnects the observer and removes the\n * runtime from the WeakSet. AppRegistry pushes the teardown onto its\n * cleanup chain so observers don't leak across deactivate/activate\n * cycles.\n *\n * Customer code never calls this directly.\n */\nexport function onActivate(runtime: unknown): (() => void) | void {\n if (typeof document === 'undefined') return;\n if (runtime == null || typeof runtime !== 'object') return;\n if (_WIRED_RUNTIMES.has(runtime)) return;\n _WIRED_RUNTIMES.add(runtime);\n const observer = wirePdpElements(runtime, document);\n return () => {\n observer.disconnect();\n const i = _ACTIVE_OBSERVERS.indexOf(observer);\n if (i >= 0) _ACTIVE_OBSERVERS.splice(i, 1);\n _WIRED_RUNTIMES.delete(runtime);\n };\n}\n\nfunction makeMountable<P extends object>(tag: string) {\n return {\n mount(container: HTMLElement, config?: (P & MountPlumbing) | null | undefined) {\n const widgetProps = stripMountPlumbing<P>(config ?? null);\n const el = document.createElement(tag) as HTMLElement & { props?: unknown };\n el.props = widgetProps;\n container.appendChild(el);\n return () => el.remove();\n },\n };\n}\n\nexport const ProductCardMountable = makeMountable<CardProps>(TAGS.card);\nexport const ProductComparisonMountable = makeMountable<ComparisonProps>(TAGS.comparison);\nexport const ProductHeroMountable = makeMountable<HeroProps>(TAGS.hero);\nexport const ProductGridMountable = makeMountable<GridProps>(TAGS.grid);\nexport const SyntroPdpMountable = makeMountable<SyntroPdpProps>(TAGS.pdp);\nexport const SyntroRecoMountable = makeMountable<RecoProps>(TAGS.reco);\n\nexport const runtime = {\n id: 'adaptive-product',\n version: '1.0.0',\n name: 'Product',\n description:\n 'Product surfaces: cards, side-by-side comparisons, featured hero, and grid lists. Optional bind for price/image to host-page sources (DOM, Shopify, WooCommerce, JSON-LD, OpenGraph).',\n executors: [],\n widgets: [\n {\n id: 'adaptive-product:card',\n component: ProductCardMountable,\n metadata: {\n name: 'Product Card',\n description: 'Single product card',\n icon: '\uD83D\uDECD\uFE0F',\n },\n },\n {\n id: 'adaptive-product:comparison',\n component: ProductComparisonMountable,\n metadata: {\n name: 'Product Comparison',\n description: '2\u20134 products, expandable rows',\n icon: '\u2696\uFE0F',\n },\n },\n {\n id: 'adaptive-product:hero',\n component: ProductHeroMountable,\n metadata: {\n name: 'Product Hero',\n description: 'Featured product with long copy',\n icon: '\u2B50',\n },\n },\n {\n id: 'adaptive-product:grid',\n component: ProductGridMountable,\n metadata: {\n name: 'Product Grid',\n description: '2\u201312 products in a grid',\n icon: '\uD83D\uDDC2\uFE0F',\n },\n },\n {\n id: 'adaptive-product:pdp',\n component: SyntroPdpMountable,\n metadata: {\n name: 'Dynamic PDP',\n description:\n 'Personalized product detail page surface. Config-first: scores the visitor archetype, mounts host presets, and generates live slots via /api/pdp/module + /api/pdp/dossier.',\n icon: '\uD83D\uDCC4',\n },\n },\n {\n id: 'adaptive-product:reco',\n component: SyntroRecoMountable,\n metadata: {\n name: 'Recommendation Carousel',\n description:\n 'Horizontally-scrollable carousel of recommended products with image, price, hook copy, and PDP links.',\n icon: '\uD83C\uDFA0',\n },\n },\n {\n // Alias: the reco carousel registered under its ELEMENT TAG as well as\n // its namespaced id. The `recommendations` tool is server-driven, so it\n // mounts the carousel as a tile whose `widget` is the raw tag\n // `syntro-product-reco-carousel` (frontend `SERVER_MOUNTED_TILE_WIDGETS`\n // self-map + backend `template_id=mount.widget`). On velvet that tile\n // lands in the DECK, whose `<sc-mount>` mounts THROUGH the WidgetRegistry\n // (`widgets.mount(tile.widget)`) \u2014 so the registry MUST recognise the tag\n // or the deck tile is a permanent blank card with zero render-health (the\n // widget class never constructs, so its own probe can't fire). This entry\n // makes the tag a first-class registry id, fulfilling the contract\n // `SERVER_MOUNTED_TILE_WIDGETS` documents. The inline (healthmaxxer chat\n // trail) path is unaffected \u2014 it uses `document.createElement(tag)`, which\n // never touched the registry. Same mountable as the namespaced entry.\n id: TAGS.reco,\n component: SyntroRecoMountable,\n metadata: {\n name: 'Recommendation Carousel',\n description:\n 'Horizontally-scrollable carousel of recommended products with image, price, hook copy, and PDP links.',\n icon: '\uD83C\uDFA0',\n },\n },\n ],\n};\n\nexport {\n runTakeover,\n type TakeoverOutcome,\n type TakeoverPorts,\n type TakeoverResult,\n} from './takeover/engine';\nexport { type DetachHandle, detachSection } from './takeover/sectionDetach';\n\nexport default runtime;\n"],
5
5
  "mappings": "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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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",
6
6
  "names": ["createContext", "key", "pending", "i", "t", "html", "LitElement", "s", "n", "observer", "s", "response", "z", "z", "t", "s", "html", "LitElement", "nothing", "s", "i", "html", "LitElement", "html", "LitElement", "s", "t", "c", "s", "LitElement", "html", "html", "LitElement", "s", "LitElement", "html", "n", "s", "i", "runtime", "LitElement", "e", "telemetry", "html", "i", "CHAT_EXCERPT_KEY", "CHAT_EXCERPT_MAX", "readChatExcerpt", "c", "e", "html", "LitElement", "nothing", "t", "c", "originFor", "unchanged", "effects", "html", "s", "e", "n", "t", "emit", "c", "html", "LitElement", "nothing", "LitElement", "e", "html", "nothing", "disabled", "publish", "emit", "LitElement", "e", "c", "html", "i", "nothing", "disabled", "html", "LitElement", "nothing", "_controller", "healthProbe_fn", "startBind_fn", "LitElement", "html", "nothing", "n", "c", "i", "html", "LitElement", "nothing", "_controller", "startBind_fn", "LitElement", "html", "nothing", "html", "LitElement", "nothing", "_controller", "startBind_fn", "LitElement", "html", "nothing", "c", "i", "html", "LitElement", "nothing", "styleMap", "healthProbe_fn", "LitElement", "html", "styleMap", "nothing", "html", "LitElement", "LitElement", "html", "html", "LitElement", "renderHeadlineLink", "html", "LitElement", "renderHeadlineLink", "html", "LitElement", "s", "i", "html", "LitElement", "ensureCarouselStyles", "CAROUSEL_CSS", "i", "html", "LitElement", "nothing", "svg", "s", "LitElement", "html", "nothing", "t", "svg", "html", "LitElement", "s", "i", "html", "LitElement", "svg", "ensureStyles", "s", "CSS", "i", "html", "LitElement", "ensureStyles", "s", "CSS", "html", "LitElement", "ensureStyles", "s", "CSS", "html", "LitElement", "ensureStyles", "s", "CSS", "html", "LitElement", "ensureStyles", "s", "CSS", "html", "LitElement", "html", "LitElement", "nothing", "s", "LitElement", "html", "nothing", "html", "LitElement", "nothing", "s", "LitElement", "html", "nothing", "html", "LitElement", "s", "LitElement", "html", "html", "LitElement", "s", "LitElement", "html", "runtime", "observer", "i"]
7
7
  }