@broberg/bodymap 0.7.1 → 0.9.0

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.
package/dist/three.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/react.tsx","../src/three.tsx"],"names":["useRef","useState","useEffect","jsxs","jsx"],"mappings":"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","file":"three.js","sourcesContent":["// @broberg/bodymap — headless core (F052.1).\n//\n// Framework-neutral: the region taxonomy + the PainReport data model (zod) + a\n// selection engine + per-app region config. NO React/Preact/DOM import — the 2D\n// (SVG) and 3D (Three.js) renderers, and all three FD apps, share this one\n// contract. Output is a structured PainReport, never a bare image.\n\nimport { z } from \"zod\";\n\nexport type Side = \"left\" | \"right\";\n\nexport interface BodyRegion {\n /** Stable, unique identifier (snake_case) — the key used in a PainReport. */\n key: string;\n /** Human label (Danish). */\n label: string;\n /** Clinical short code (unique). */\n code: string;\n /** Body side, when the region is paired. */\n side?: Side;\n}\n\n/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy\n * (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS\n * clinical codes + a separate `side` field (L/R on limbs, C=center on axis\n * regions). The `key` is a unique per-side identifier; `code` is the side-less\n * clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —\n * ~30 named surface regions for a pain-map. The 2D front renderer draws the\n * front-visible subset; the 3D body (F052.6) drives them all. */\nexport const REGIONS: readonly BodyRegion[] = [\n // axis / centre-line (serialised side \"center\")\n { key: \"head\", label: \"Hoved\", code: \"HEAD\" },\n { key: \"neck\", label: \"Nakke\", code: \"NECK\" },\n { key: \"chest\", label: \"Bryst\", code: \"CHEST\" },\n { key: \"thora\", label: \"Øvre ryg (thorakal)\", code: \"THORA\" },\n { key: \"lumbar\", label: \"Lænd (lumbal)\", code: \"LUMBAR\" },\n { key: \"groin\", label: \"Lyske\", code: \"GROIN\" },\n // paired limbs / sides (L / R)\n { key: \"shoulder_left\", label: \"Skulder, venstre\", code: \"SHOULDER\", side: \"left\" },\n { key: \"shoulder_right\", label: \"Skulder, højre\", code: \"SHOULDER\", side: \"right\" },\n { key: \"uarm_left\", label: \"Overarm, venstre\", code: \"UARM\", side: \"left\" },\n { key: \"uarm_right\", label: \"Overarm, højre\", code: \"UARM\", side: \"right\" },\n { key: \"elbow_left\", label: \"Albue, venstre\", code: \"ELBOW\", side: \"left\" },\n { key: \"elbow_right\", label: \"Albue, højre\", code: \"ELBOW\", side: \"right\" },\n { key: \"farm_left\", label: \"Underarm, venstre\", code: \"FARM\", side: \"left\" },\n { key: \"farm_right\", label: \"Underarm, højre\", code: \"FARM\", side: \"right\" },\n { key: \"wrist_left\", label: \"Håndled, venstre\", code: \"WRIST\", side: \"left\" },\n { key: \"wrist_right\", label: \"Håndled, højre\", code: \"WRIST\", side: \"right\" },\n { key: \"hand_left\", label: \"Hånd, venstre\", code: \"HAND\", side: \"left\" },\n { key: \"hand_right\", label: \"Hånd, højre\", code: \"HAND\", side: \"right\" },\n { key: \"hip_left\", label: \"Hofte, venstre\", code: \"HIP\", side: \"left\" },\n { key: \"hip_right\", label: \"Hofte, højre\", code: \"HIP\", side: \"right\" },\n { key: \"thigh_left\", label: \"Lår, venstre\", code: \"THIGH\", side: \"left\" },\n { key: \"thigh_right\", label: \"Lår, højre\", code: \"THIGH\", side: \"right\" },\n { key: \"knee_left\", label: \"Knæ, venstre\", code: \"KNEE\", side: \"left\" },\n { key: \"knee_right\", label: \"Knæ, højre\", code: \"KNEE\", side: \"right\" },\n { key: \"lowleg_left\", label: \"Underben, venstre\", code: \"LOWLEG\", side: \"left\" },\n { key: \"lowleg_right\", label: \"Underben, højre\", code: \"LOWLEG\", side: \"right\" },\n { key: \"ankle_left\", label: \"Ankel, venstre\", code: \"ANKLE\", side: \"left\" },\n { key: \"ankle_right\", label: \"Ankel, højre\", code: \"ANKLE\", side: \"right\" },\n { key: \"foot_left\", label: \"Fod, venstre\", code: \"FOOT\", side: \"left\" },\n { key: \"foot_right\", label: \"Fod, højre\", code: \"FOOT\", side: \"right\" },\n];\n\nconst REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));\nexport const REGION_KEYS: readonly string[] = REGIONS.map((r) => r.key);\n\n/** Look up a region by its key. */\nexport function getRegion(key: string): BodyRegion | undefined {\n return REGIONS.find((r) => r.key === key);\n}\n\n// ---- PainReport model ---------------------------------------------------\n\nexport const PAIN_TYPES = [\"stikkende\", \"dump\", \"konstant\", \"jagende\"] as const;\nexport type PainType = (typeof PAIN_TYPES)[number];\n\n/** One marked pain point. `region` MUST be a known region key; `intensity` is a\n * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */\nexport const painPointSchema = z.object({\n region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: \"unknown region\" }),\n intensity: z.number().int().min(0).max(10),\n type: z.enum(PAIN_TYPES).optional(),\n timestamp: z.string(),\n});\nexport type PainPoint = z.infer<typeof painPointSchema>;\n\nexport const painReportSchema = z.array(painPointSchema);\nexport type PainReport = PainPoint[];\n\n// ---- Selection engine (framework-agnostic) ------------------------------\n\nexport interface PainSelection {\n /** Mark (or update) pain on a region. One point per region — latest wins. */\n set(region: string, intensity: number, type?: PainType): PainPoint;\n remove(region: string): boolean;\n get(region: string): PainPoint | undefined;\n has(region: string): boolean;\n clear(): void;\n /** The current, validated PainReport. */\n getReport(): PainReport;\n}\n\nexport interface PainSelectionOptions {\n /** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */\n now?: () => string;\n}\n\n/** Create a selection engine seeded with an optional report. Pure state — no\n * DOM, no framework, no network. One point per region. */\nexport function createPainSelection(\n initial: PainReport = [],\n opts: PainSelectionOptions = {},\n): PainSelection {\n const now = opts.now ?? (() => new Date().toISOString());\n const map = new Map<string, PainPoint>();\n for (const p of initial) {\n const v = painPointSchema.parse(p);\n map.set(v.region, v);\n }\n return {\n set(region, intensity, type) {\n const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });\n map.set(point.region, point);\n return point;\n },\n remove: (region) => map.delete(region),\n get: (region) => map.get(region),\n has: (region) => map.has(region),\n clear: () => map.clear(),\n getReport: () => painReportSchema.parse(Array.from(map.values())),\n };\n}\n\n// ---- Per-app region config (the toggle) ---------------------------------\n\nexport interface RegionSetting {\n /** Render this region at all. Default true. */\n visible?: boolean;\n /** Allow marking pain on this region. Default true. */\n selectable?: boolean;\n}\n\n/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */\nexport type RegionConfig = Record<string, RegionSetting>;\n\n/** The regions an app should render, honouring `visible` (default true). */\nexport function resolveRegions(config: RegionConfig = {}): BodyRegion[] {\n return REGIONS.filter((r) => config[r.key]?.visible ?? true);\n}\n\n/**\n * What a pick on a region should DO (F052.20).\n *\n * Lives in the core because the 2D and 3D renderers share no click code, and a\n * rule written twice is a rule that drifts. This repo measured the cost of that\n * twice on 2026-08-28 alone: a fix applied to one half of a pair, and a sibling\n * branch that carried the same defect with no test on it.\n *\n * \"clear\" the region is already marked → picking it again removes the mark\n * \"select\" unmarked → open it for marking\n * \"ignore\" not selectable (read-only or config) → nothing happens\n *\n * Three outcomes, not a boolean: \"nothing happened because it is locked\" and\n * \"nothing happened because we removed the mark\" must never look alike to a\n * caller.\n */\nexport type PickOutcome = \"clear\" | \"select\" | \"ignore\";\n\nexport function decidePick(\n region: string,\n report: PainReport,\n config: RegionConfig = {},\n): PickOutcome {\n if (!isSelectable(region, config)) return \"ignore\";\n return report.some((p) => p.region === region) ? \"clear\" : \"select\";\n}\n\n/** Whether a region may be marked. A hidden region is never selectable. */\nexport function isSelectable(key: string, config: RegionConfig = {}): boolean {\n const s = config[key];\n if (s?.visible === false) return false;\n return s?.selectable ?? true;\n}\n\n// ---- feedback signal: sound + haptics (F052.22) -----------------------------\n\n/**\n * What a pick did, handed to the consuming app so it can make a sound or a buzz.\n *\n * The outcome is the one `decidePick` ACTUALLY returned, never the intent to tap\n * — so a tap on a locked region can not announce itself as a removal, and a tap\n * the pan/pinch guard swallowed emits nothing at all (it never gets here).\n *\n * It deliberately reuses `PickOutcome` rather than introducing a second\n * three-word vocabulary. Two enums meaning the same thing is a drift bug waiting\n * for the first person who adds a fourth outcome to only one of them.\n */\nexport interface FeedbackSignal {\n outcome: PickOutcome;\n /** The region key that was picked. */\n region: string;\n}\n\nexport type FeedbackFn = (signal: FeedbackSignal) => void;\n\n/**\n * What the BROWSER did with a vibration request.\n *\n * ⚠️ `requested` does NOT mean the phone buzzed. `navigator.vibrate()` returns\n * true for a request it accepted, and all of these accept it and then produce\n * nothing: silent / do-not-disturb mode, a device with no vibration motor (most\n * laptops), a page that has not yet had a qualifying user gesture.\n *\n * Same lesson `@broberg/webpush` 0.3.1 recorded — a push that provably ARRIVED\n * on a device that never SHOWED it. \"Accepted\" and \"happened\" are two claims,\n * and only one of them is observable from here.\n */\nexport type VibrateOutcome = \"unsupported\" | \"skipped\" | \"declined\" | \"requested\";\n\n/**\n * The buzz for each outcome.\n *\n * `ignore` is empty ON PURPOSE: a tap that changed nothing must not feel like it\n * changed something. That is the whole reason this is keyed by outcome and not\n * fired from the tap handler.\n */\nexport const VIBRATION_PATTERNS: Record<PickOutcome, readonly number[]> = {\n select: [12],\n clear: [8, 40, 8],\n ignore: [],\n};\n\ninterface Vibrator {\n vibrate?: (pattern: number | number[]) => boolean;\n}\n\n/**\n * Ask the browser to vibrate. Never throws, never claims delivery.\n *\n * `nav` is injectable so a test can supply all four cases; it defaults to the\n * real `navigator` and is `unsupported` when there is not one (SSR, and every\n * browser on iPhone — WebKit has no `vibrate` at all).\n */\nexport function requestVibration(\n pattern: readonly number[],\n nav: Vibrator | undefined = (globalThis as { navigator?: Vibrator }).navigator,\n): VibrateOutcome {\n if (pattern.length === 0) return \"skipped\";\n if (typeof nav?.vibrate !== \"function\") return \"unsupported\";\n try {\n return nav.vibrate([...pattern]) ? \"requested\" : \"declined\";\n } catch {\n // Embedded webviews and cross-origin iframes throw rather than return false.\n // A refused buzz must never take the pain report down with it.\n return \"declined\";\n }\n}\n\nexport interface FeedbackOptions {\n onFeedback?: FeedbackFn;\n /** Web vibration on select/clear. Default true; inert where the API is absent. */\n haptics?: boolean;\n /** Injectable for tests. */\n nav?: unknown;\n}\n\n/**\n * Emit one pick's feedback: always the signal, optionally the buzz.\n *\n * Lives in the core because the 2D and 3D renderers share no click code, and a\n * rule written twice is a rule that drifts — the same reason `decidePick` is\n * here (F052.20).\n *\n * Sound is NOT here and never will be: `@broberg/soundkit` already exists, and\n * pulling Web Audio into a component that is often rendered read-only (a\n * journal, a PDF, a clinician view) is a cost every consumer would pay for a\n * feature most will not switch on. Wire `onFeedback` to it in four lines.\n */\nexport function emitFeedback(\n outcome: PickOutcome,\n region: string,\n opts: FeedbackOptions = {},\n): VibrateOutcome {\n opts.onFeedback?.({ outcome, region });\n if (opts.haptics === false) return \"skipped\";\n // `undefined` falls through to requestVibration's own default (the real\n // navigator) — so an omitted `nav` and a passed-in one take the same path.\n return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav as Vibrator | undefined);\n}\n\n// ---- palette (consumer-defined colours — shared by the 2D + 3D renderers) ---\n\n/** Colour control for the body renderers. Consumers pass a palette to theme the\n * body base colour, the hover + selected highlights, the pain-heat colours, and\n * optional per-region base colours. All values are CSS/hex colour strings. */\nexport interface BodymapPalette {\n /** Base body colour (an unmarked region). */\n body: string;\n /** Region highlight on hover (before click). */\n hover: string;\n /** A region selected (clicked) but not yet given an intensity. */\n selected: string;\n /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */\n heat: { low: string; mid: string; high: string };\n /** Optional per-region base-colour overrides (region key → colour). */\n regions?: Record<string, string>;\n /**\n * Optional panel-chrome colours (the selection panel, labels, hint box) —\n * NOT the body itself. All optional; anything omitted falls back to\n * {@link defaultUi}. A palette that only themed the body was half a palette:\n * a consumer passing brand colours still got hardcoded chrome. (F052.19)\n */\n ui?: BodymapUiColors;\n}\n\n/** Panel-chrome colours. Every default is WCAG-AA (>=4.5:1) on its own background. */\nexport interface BodymapUiColors {\n /** Primary text (headings, values). */\n text?: string;\n /** Secondary text — section labels, the empty-state hint. */\n mutedText?: string;\n /** Panel background. */\n panelBg?: string;\n /** Panel + control borders. */\n border?: string;\n /** Background behind the region-code badge. */\n badgeBg?: string;\n /** The destructive action (remove a marked region). */\n danger?: string;\n}\n\n/**\n * Default panel chrome. Contrast against `panelBg` (#fff), asserted by\n * `test/contrast.test.ts`:\n * text #1e293b 14.8:1\n * mutedText #475569 7.6:1 (was #94a3b8 at 2.56:1 — WCAG AA failure)\n * danger #dc2626 4.8:1 (was #ef4444 at 3.76:1 — WCAG AA failure)\n * `mutedText` on `badgeBg` (#f1f5f9) is 6.9:1 (was #64748b at 4.34:1).\n */\nexport const defaultUi: Required<BodymapUiColors> = {\n text: \"#1e293b\",\n mutedText: \"#475569\",\n panelBg: \"#fff\",\n border: \"#e2e8f0\",\n badgeBg: \"#f1f5f9\",\n danger: \"#dc2626\",\n};\n\n/** Resolve a palette's chrome colours, filling every gap from {@link defaultUi}. */\nexport function uiColors(palette?: BodymapPalette): Required<BodymapUiColors> {\n return { ...defaultUi, ...(palette?.ui ?? {}) };\n}\n\n/** The fleet default palette. Override any field per consumer. */\nexport const defaultPalette: BodymapPalette = {\n body: \"#d2d7de\",\n hover: \"#8fd0cd\",\n selected: \"#5cc4b7\",\n heat: { low: \"#fcd34d\", mid: \"#fb923c\", high: \"#ef4444\" },\n};\n\n/** The heat colour for a pain intensity, honouring the palette. */\nexport function heatFor(intensity: number, palette: BodymapPalette = defaultPalette): string {\n return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;\n}\n\n/** The base colour for a region (a per-region override, else the body colour). */\nexport function baseColorFor(regionKey: string, palette: BodymapPalette = defaultPalette): string {\n return palette.regions?.[regionKey] ?? palette.body;\n}\n\n// ---- bodymap/v1 serialization (the shared cross-app / native wire format) ---\n//\n// The shape every consumer + the native mobile apps read (aligned with\n// fd-sundhed's bodymap/v1: region CODE + side + intensity + quality + view).\n// The internal PainReport keys on the region KEY; this maps key -> clinical CODE\n// so the report is portable and human-readable on the wire.\n\nexport type BodyView = \"front\" | \"back\" | \"left\" | \"right\";\n/** Side in the serialized report — a midline region (no side) becomes \"center\". */\nexport type SerializedSide = \"left\" | \"right\" | \"center\";\n\nexport interface SerializedPainPoint {\n /** Clinical region CODE (e.g. \"LUMB\"). */\n region: string;\n side: SerializedSide;\n intensity: number;\n quality?: PainType;\n}\n\nexport interface BodymapReportV1 {\n schema: \"bodymap/v1\";\n view: BodyView;\n points: SerializedPainPoint[];\n}\n\nexport const bodymapReportV1Schema = z.object({\n schema: z.literal(\"bodymap/v1\"),\n view: z.enum([\"front\", \"back\", \"left\", \"right\"]),\n points: z.array(\n z.object({\n region: z.string(),\n side: z.enum([\"left\", \"right\", \"center\"]),\n intensity: z.number().int().min(0).max(10),\n quality: z.enum(PAIN_TYPES).optional(),\n }),\n ),\n});\n\n/** Serialize a PainReport to the shared `bodymap/v1` wire format. */\nexport function serializeReport(\n report: PainReport,\n opts: { view?: BodyView } = {},\n): BodymapReportV1 {\n return {\n schema: \"bodymap/v1\",\n view: opts.view ?? \"front\",\n points: report.map((p) => {\n const r = getRegion(p.region);\n return {\n region: r?.code ?? p.region,\n side: (r?.side ?? \"center\") as SerializedSide,\n intensity: p.intensity,\n quality: p.type,\n };\n }),\n };\n}\n\n/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →\n * key; a point whose code is unknown to this taxonomy is dropped. */\nexport function deserializeReport(\n env: unknown,\n now: () => string = () => new Date().toISOString(),\n): PainReport {\n const parsed = bodymapReportV1Schema.parse(env);\n // code is side-less, so a point is identified by code + side.\n const byCodeSide = new Map(\n REGIONS.map((r) => [`${r.code}|${r.side ?? \"center\"}`, r.key] as const),\n );\n const out: PainReport = [];\n for (const sp of parsed.points) {\n const key = byCodeSide.get(`${sp.region}|${sp.side}`);\n if (!key) continue;\n out.push(\n painPointSchema.parse({\n region: key,\n intensity: sp.intensity,\n type: sp.quality,\n timestamp: now(),\n }),\n );\n }\n return out;\n}\n","// @broberg/bodymap/react — the 2D SVG renderer (F052.2).\n//\n// <BodyMap> is the shippable MVP: a genderless, colour-blocked body a patient\n// clicks to mark where it hurts. Selecting a region opens a custom intensity\n// (0-10) + quality control (NO native inputs) that upserts a validated\n// PainReport and fires onChange. Honours the per-app RegionConfig. The 3D\n// <BodyMap3D> (F052.6) will render the SAME core on the SAME onChange contract.\n//\n// F052.8 touch-first + F052.11 a11y: the body is PRIMARILY a mobile surface, so\n// every region has a >=44px transparent hit-area, the body pinch-zooms + pans\n// (with a tap-vs-drag guard so a gesture never mis-marks), controls are\n// thumb-sized and the layout stacks on a narrow viewport. Everything is also\n// fully keyboard-operable with live-state ARIA labels + visible focus.\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n REGIONS,\n PAIN_TYPES,\n painPointSchema,\n resolveRegions,\n isSelectable,\n decidePick,\n emitFeedback,\n type FeedbackFn,\n getRegion,\n heatFor,\n baseColorFor,\n type BodyRegion,\n type BodymapPalette,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\n\n// ---- geometry: each region → an SVG shape (a neutral colour-blockout) --------\n\ntype Shape =\n | { el: \"circle\"; cx: number; cy: number; r: number }\n | { el: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n | { el: \"rect\"; x: number; y: number; width: number; height: number; rx: number };\n\n// Front-view geometry (viewBox 260×470). The 2D MVP renders the front-visible\n// regions; back-only regions (thora, lumbar) + hip are driven by the 3D body /\n// a later back view. \"left\"/\"right\" are placed self-view (viewer's left = key\n// left) for the MVP — the facing-the-patient mirror is a later config option.\nconst SHAPES: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n chest: { el: \"rect\", x: 100, y: 84, width: 60, height: 104, rx: 16 },\n groin: { el: \"rect\", x: 106, y: 190, width: 48, height: 30, rx: 13 },\n // left arm (viewer-left)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n // right arm (viewer-right)\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL: Record<string, string> = {\n head: \"#cbb7ec\", neck: \"#b8a3e6\", chest: \"#e26d6d\", groin: \"#e88a52\",\n shoulder_left: \"#f6c667\", shoulder_right: \"#f6c667\",\n uarm_left: \"#7ececb\", uarm_right: \"#7ececb\",\n elbow_left: \"#86cfcc\", elbow_right: \"#86cfcc\",\n farm_left: \"#8fd0cd\", farm_right: \"#8fd0cd\",\n wrist_left: \"#9ad6d3\", wrist_right: \"#9ad6d3\",\n hand_left: \"#a7ddda\", hand_right: \"#a7ddda\",\n thigh_left: \"#7fb2e8\", thigh_right: \"#7fb2e8\",\n knee_left: \"#c79be0\", knee_right: \"#c79be0\",\n lowleg_left: \"#e39ec6\", lowleg_right: \"#e39ec6\",\n ankle_left: \"#eeaab9\", ankle_right: \"#eeaab9\",\n foot_left: \"#f0a5a5\", foot_right: \"#f0a5a5\",\n};\n\n// Back-view geometry (same viewBox 260×470 + same limb/head geometry as the\n// front, so a marked limb sits at the same spot in both views). The torso swaps\n// front regions (chest, groin) for the posterior ones — THORA (upper back),\n// LUMBAR (lower back) and HIP (buttocks, paired) — the pilot's blocker regions.\n// Same self-view L/R convention as the front (patient-left = viewer-left).\nconst SHAPES_BACK: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n thora: { el: \"rect\", x: 100, y: 84, width: 60, height: 58, rx: 15 },\n lumbar: { el: \"rect\", x: 104, y: 144, width: 52, height: 44, rx: 13 },\n hip_left: { el: \"rect\", x: 100, y: 190, width: 27, height: 32, rx: 13 },\n hip_right: { el: \"rect\", x: 133, y: 190, width: 27, height: 32, rx: 13 },\n // arms — shared with the front (back of the arm sits at the same x/y)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs — back (thigh→calf→heel, same x/y as the front)\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL_BACK: Record<string, string> = {\n ...FILL,\n thora: \"#e2896d\",\n lumbar: \"#d97fa0\",\n hip_left: \"#c99bd6\", hip_right: \"#c99bd6\",\n};\n\nexport type BodyView2D = \"front\" | \"back\";\n\n// ---- i18n (F052.12) — every UI string is overridable; da + en ship ----------\n// Region CODE + keys + the bodymap/v1 wire are NEVER touched — labels are\n// display-only, so a report is byte-identical in any language.\n\nexport interface BodyMapLabels {\n /** Region key → display name. */\n regions: Record<string, string>;\n /** PainType → display word (e.g. \"stikkende\" → \"Stabbing\"). */\n qualities: Record<string, string>;\n intensity: string;\n quality: string;\n remove: string;\n /** Close the picker without marking anything (F052.26). */\n close: string;\n front: string;\n back: string;\n empty: string;\n /** Compare view (F052.13). */\n before: string;\n after: string;\n noChange: string;\n change: { new: string; resolved: string; improved: string; worse: string };\n /** Accessible name for a marked region. */\n ariaMarked: (name: string, intensity: number, quality?: string) => string;\n /** Accessible name for an unmarked, selectable region. */\n ariaUnmarked: (name: string) => string;\n svgLabel: string;\n viewLabel: string;\n zoomLabel: string;\n zoomIn: string;\n zoomOut: string;\n zoomReset: string;\n}\n\nconst daRegions: Record<string, string> = Object.fromEntries(REGIONS.map((r) => [r.key, r.label]));\nconst EN_CODE: Record<string, string> = {\n HEAD: \"Head\", NECK: \"Neck\", CHEST: \"Chest\", THORA: \"Upper back\", LUMBAR: \"Lower back\", GROIN: \"Groin\",\n SHOULDER: \"Shoulder\", UARM: \"Upper arm\", ELBOW: \"Elbow\", FARM: \"Forearm\", WRIST: \"Wrist\", HAND: \"Hand\",\n HIP: \"Hip\", THIGH: \"Thigh\", KNEE: \"Knee\", LOWLEG: \"Lower leg\", ANKLE: \"Ankle\", FOOT: \"Foot\",\n};\nconst enRegions: Record<string, string> = Object.fromEntries(\n REGIONS.map((r) => [r.key, EN_CODE[r.code] + (r.side ? (r.side === \"left\" ? \", left\" : \", right\") : \"\")]),\n);\n\nexport const LABELS_DA: BodyMapLabels = {\n regions: daRegions,\n qualities: { stikkende: \"stikkende\", dump: \"dump\", konstant: \"konstant\", jagende: \"jagende\" },\n intensity: \"Intensitet (0-10)\",\n quality: \"Kvalitet\",\n remove: \"Fjern punkt\",\n close: \"Luk\",\n front: \"Forfra\",\n back: \"Bagfra\",\n empty: \"Vælg en kropsdel for at markere smerte.\",\n before: \"Før\",\n after: \"Efter\",\n noChange: \"Ingen ændring\",\n change: { new: \"nyt\", resolved: \"forsvundet\", improved: \"bedre\", worse: \"værre\" },\n ariaMarked: (n, i, q) => `${n}, smerte ${i} af 10${q ? \", \" + q : \"\"}`,\n ariaUnmarked: (n) => `${n}, ikke markeret. Aktivér for at markere smerte.`,\n svgLabel: \"Kropskort — vælg hvor det gør ondt\",\n viewLabel: \"Visning\",\n zoomLabel: \"Zoom\",\n zoomIn: \"Zoom ind\",\n zoomOut: \"Zoom ud\",\n zoomReset: \"Nulstil zoom\",\n};\n\nexport const LABELS_EN: BodyMapLabels = {\n regions: enRegions,\n qualities: { stikkende: \"stabbing\", dump: \"dull\", konstant: \"constant\", jagende: \"shooting\" },\n intensity: \"Intensity (0-10)\",\n quality: \"Quality\",\n remove: \"Remove point\",\n close: \"Close\",\n front: \"Front\",\n back: \"Back\",\n empty: \"Pick a body part to mark pain.\",\n before: \"Before\",\n after: \"After\",\n noChange: \"No change\",\n change: { new: \"new\", resolved: \"resolved\", improved: \"improved\", worse: \"worse\" },\n ariaMarked: (n, i, q) => `${n}, pain ${i} of 10${q ? \", \" + q : \"\"}`,\n ariaUnmarked: (n) => `${n}, not marked. Activate to mark pain.`,\n svgLabel: \"Body map — pick where it hurts\",\n viewLabel: \"View\",\n zoomLabel: \"Zoom\",\n zoomIn: \"Zoom in\",\n zoomOut: \"Zoom out\",\n zoomReset: \"Reset zoom\",\n};\n\nexport type BodyMapLocale = \"da\" | \"en\";\n\nfunction resolveLabels(locale: BodyMapLocale, overrides?: Partial<BodyMapLabels>): BodyMapLabels {\n const base = locale === \"en\" ? LABELS_EN : LABELS_DA;\n if (!overrides) return base;\n return {\n ...base,\n ...overrides,\n regions: { ...base.regions, ...overrides.regions },\n qualities: { ...base.qualities, ...overrides.qualities },\n };\n}\n\nconst VBW = 260;\nconst VBH = 470;\n// The SVG renders ~320px wide → ~1.23 px per viewBox unit. A hit-area needs\n// ~36 units to clear the 44px touch minimum; small regions get an invisible\n// padded hit-shape at least this big (zoom handles genuinely dense clusters).\nconst MIN_HIT = 18; // half-extent (radius) in viewBox units\n\nfunction center(s: Shape): { x: number; y: number } {\n if (s.el === \"rect\") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };\n return { x: s.cx, y: s.cy };\n}\n// An enlarged, invisible hit target so small regions clear the 44px touch min.\nfunction hitShape(s: Shape): Shape {\n if (s.el === \"circle\") return { el: \"circle\", cx: s.cx, cy: s.cy, r: Math.max(s.r, MIN_HIT) };\n if (s.el === \"ellipse\")\n return { el: \"ellipse\", cx: s.cx, cy: s.cy, rx: Math.max(s.rx, MIN_HIT), ry: Math.max(s.ry, MIN_HIT) };\n const c = center(s);\n const w = Math.max(s.width, MIN_HIT * 2);\n const h = Math.max(s.height, MIN_HIT * 2);\n return { el: \"rect\", x: c.x - w / 2, y: c.y - h / 2, width: w, height: h, rx: s.rx };\n}\nfunction heat(v: number): string {\n return v >= 7 ? \"#ef4444\" : v >= 4 ? \"#fb923c\" : \"#fcd34d\";\n}\nfunction heatTier(v: number): \"low\" | \"mid\" | \"high\" {\n return v >= 7 ? \"high\" : v >= 4 ? \"mid\" : \"low\";\n}\n// F052.10 palette: a `palette` prop wins (direct colour); with no prop, a CSS-var\n// chain (--bmap-region-<key> → --bmap-body) themes with the default rainbow as\n// the fallback. Marked regions likewise: palette.heat or --bmap-heat-<tier>.\nfunction baseFill(key: string, rainbow: string, palette?: BodymapPalette): string {\n return palette ? baseColorFor(key, palette) : `var(--bmap-region-${key}, var(--bmap-body, ${rainbow}))`;\n}\nfunction markedFill(v: number, palette?: BodymapPalette): string {\n return palette ? heatFor(v, palette) : `var(--bmap-heat-${heatTier(v)}, ${heat(v)})`;\n}\nfunction shapeEl(s: Shape, props: Record<string, unknown>) {\n if (s.el === \"circle\") return <circle cx={s.cx} cy={s.cy} r={s.r} {...props} />;\n if (s.el === \"ellipse\") return <ellipse cx={s.cx} cy={s.cy} rx={s.rx} ry={s.ry} {...props} />;\n return <rect x={s.x} y={s.y} width={s.width} height={s.height} rx={s.rx} {...props} />;\n}\n\n// ---- one-time style injection (no external CSS; theme via CSS variables) -----\n\n/**\n * Map a palette onto the root element's CSS custom properties, so a consumer's\n * colours reach the panel chrome and not just the body. Only keys the consumer\n * actually set are emitted — everything else falls through to the stylesheet's\n * (AA-safe) defaults. (F052.19)\n */\nfunction chromeStyle(palette?: BodymapPalette): React.CSSProperties | undefined {\n if (!palette) return undefined;\n const ui = palette.ui ?? {};\n const vars: Record<string, string> = { \"--bmap-accent\": palette.selected };\n if (ui.text) vars[\"--bmap-ink\"] = ui.text;\n if (ui.mutedText) vars[\"--bmap-muted\"] = ui.mutedText;\n if (ui.panelBg) vars[\"--bmap-panel\"] = ui.panelBg;\n if (ui.border) vars[\"--bmap-line\"] = ui.border;\n if (ui.badgeBg) vars[\"--bmap-badge-bg\"] = ui.badgeBg;\n if (ui.danger) vars[\"--bmap-danger\"] = ui.danger;\n return vars as React.CSSProperties;\n}\n\nconst STYLE_ID = \"broberg-bodymap-styles\";\nconst STYLE = `\n.bmap{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#475569;--bmap-badge-bg:#f1f5f9;--bmap-danger:#dc2626;\n display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}\n.bmap__stage{display:flex;flex-direction:column;gap:10px;flex:0 1 340px;min-width:0;max-width:360px}\n.bmap__bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n.bmap__viewtoggle{display:inline-flex;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}\n.bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:#556274;background:none;border:0;border-radius:7px;padding:8px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}\n.bmap__vbtn:hover{color:var(--bmap-ink)}\n.bmap__vbtn:active{transform:scale(.97)}\n.bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}\n.bmap__zoom{display:inline-flex;gap:4px;margin-left:auto}\n.bmap__zoom button{font:inherit;font-weight:700;font-size:16px;line-height:1;width:36px;height:36px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);border-radius:8px;cursor:pointer;transition:border-color .12s,transform .1s;display:flex;align-items:center;justify-content:center}\n.bmap__zoom button:hover{border-color:var(--bmap-accent)}\n.bmap__zoom button:active{transform:scale(.9)}\n.bmap__zoom button:disabled{opacity:.4;cursor:default}\n.bmap__svg{width:100%;height:auto;display:block;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;background:transparent;border-radius:14px}\n.bmap__vis{stroke:#fff;stroke-width:2.4;pointer-events:none;transition:filter .12s,stroke .12s,stroke-width .12s}\n.bmap__vis--sel{stroke:var(--bmap-accent);stroke-width:3.6}\n.bmap__hit{fill:transparent;cursor:pointer;outline:none}\n.bmap__hit--locked{cursor:default}\n.bmap__hit:hover + .bmap__vis{filter:brightness(1.08) saturate(1.15)}\n.bmap__hit:focus-visible + .bmap__vis{stroke:var(--bmap-accent);stroke-width:4;stroke-dasharray:4 2.5}\n.bmap__heatn{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}\n.bmap__panel{flex:1 1 240px;min-width:220px;border:1px solid var(--bmap-line);border-radius:14px;padding:16px 18px;background:var(--bmap-panel)}\n.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}\n.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}\n.bmap__ph b{font-size:16px}\n.bmap__close{margin-left:auto;font:inherit;font-size:20px;line-height:1;cursor:pointer;background:none;border:1px solid transparent;border-radius:8px;width:32px;height:32px;color:var(--bmap-muted,#475569)}\n.bmap__close:hover{background:var(--bmap-badge-bg,#f1f5f9)}\n.bmap__close:active{transform:scale(.94)}\n.bmap__close:focus-visible{outline:2px solid var(--bmap-accent,#0e8f8a);outline-offset:2px}\n.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:var(--bmap-badge-bg);border-radius:6px;padding:2px 7px}\n.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}\n.bmap__scale{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}\n.bmap__i{font:inherit;font-size:14px;font-weight:600;width:38px;height:38px;border-radius:9px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);cursor:pointer;transition:border-color .1s,background .1s,transform .1s}\n.bmap__i:hover{border-color:var(--bmap-accent)}\n.bmap__i:active{transform:scale(.9)}\n.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}\n.bmap__chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}\n.bmap__chip{font:inherit;font-size:13px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-muted);border-radius:999px;padding:9px 15px;cursor:pointer;transition:.1s}\n.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}\n.bmap__chip:active{transform:scale(.96)}\n.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}\n.bmap__rm{font:inherit;font-size:14px;font-weight:600;border:1px solid #f6c9c9;background:#fff;color:var(--bmap-danger);border-radius:9px;padding:10px 15px;cursor:pointer;transition:.1s}\n.bmap__rm:hover{background:#fef2f2}\n.bmap__rm:active{transform:scale(.97)}\n.bmap__i:focus-visible,.bmap__chip:focus-visible,.bmap__vbtn:focus-visible,.bmap__rm:focus-visible,.bmap__zoom button:focus-visible{outline:2px solid var(--bmap-accent);outline-offset:2px}\n.bmapc{--bmap-accent:var(--primary,#0e8f8a);--bmap-line:#e2e8f0;--bmap-ink:#1e293b;--bmap-muted:#475569;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}\n.bmapc__bodies{display:flex;gap:14px;flex-wrap:wrap}\n.bmapc__fig{flex:1 1 150px;min-width:130px;max-width:220px;margin:0;text-align:center}\n.bmapc__cap{font-size:12px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#556274;margin-bottom:6px}\n.bmapc__svg{width:100%;height:auto;display:block}\n.bmapc__vis{stroke:#fff;stroke-width:2.4}\n.bmapc__n{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}\n.bmapc__delta{margin-top:16px;display:flex;flex-direction:column;gap:6px;max-width:460px}\n.bmapc__row{display:flex;align-items:center;gap:10px;font-size:13.5px;border:1px solid var(--bmap-line);border-radius:9px;padding:7px 11px}\n.bmapc__rname{font-weight:600;flex:1 1 auto}\n.bmapc__rval{font:12px ui-monospace,monospace;color:#556274;white-space:nowrap}\n.bmapc__badge{font-size:11px;font-weight:700;border-radius:6px;padding:2px 8px;white-space:nowrap}\n.bmapc__badge--improved{background:#dcfce7;color:#166534}\n.bmapc__badge--worse{background:#fee2e2;color:#991b1b}\n.bmapc__badge--new{background:#fef3c7;color:#92400e}\n.bmapc__badge--resolved{background:#e2e8f0;color:#475569}\n.bmapc__empty{color:#556274;font-size:13.5px;margin-top:14px}\n@media (max-width:560px){\n .bmap{flex-direction:column;gap:14px}\n .bmap__stage{max-width:100%;width:100%;flex-basis:auto}\n .bmap__panel{width:100%}\n .bmap__i{width:46px;height:46px;font-size:16px}\n .bmap__chip{padding:11px 17px;font-size:15px}\n .bmap__vbtn{padding:10px 18px;font-size:15px}\n}\n@media (prefers-reduced-motion:reduce){.bmap *{transition:none !important}}\n`;\n\nfunction ensureStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// ---- component ---------------------------------------------------------------\n\nexport interface BodyMapProps {\n /** Controlled report. Omit for an uncontrolled component (use defaultValue). */\n value?: PainReport;\n /** Initial report when uncontrolled. */\n defaultValue?: PainReport;\n /** Fired with the full validated PainReport on every change. */\n onChange?: (report: PainReport) => void;\n /** Per-app region config (visible / selectable). */\n config?: RegionConfig;\n /** Initial body view (front / back). Default \"front\". */\n defaultView?: BodyView2D;\n /** Fired when the user toggles the front/back view — pass it to\n * serializeReport({ view }) so the wire report carries the active view. */\n onViewChange?: (view: BodyView2D) => void;\n /** Colour theme (F052.10). Overrides the default rainbow: body/hover/selected/\n * heat + optional per-region base colours. Without it, the rainbow is used\n * (themeable via --bmap-body / --bmap-region-<key> / --bmap-heat-* CSS vars). */\n palette?: BodymapPalette;\n /** UI language (F052.12). \"da\" (default) or \"en\". */\n locale?: BodyMapLocale;\n /** Override individual UI strings (region names, headings, quality words, …).\n * Merged over the locale set — region CODE + wire format are never affected. */\n labels?: Partial<BodyMapLabels>;\n /** Display-only mode (F052.9): render a saved report coloured by intensity\n * with NO picker and no interactivity. For journals, PDFs, clinician views. */\n readOnly?: boolean;\n /** Fired after every pick with what ACTUALLY happened (F052.22): \"select\",\n * \"clear\" or \"ignore\". Wire it to a sound (`@broberg/soundkit`) or to native\n * haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an\n * iPhone, where web vibration does not exist). A pick swallowed by the\n * pan/pinch guard fires nothing at all. */\n onFeedback?: FeedbackFn;\n /** Web vibration on select/clear. Default true; silently inert where\n * `navigator.vibrate` is absent (every browser on iPhone, most desktops).\n * Set false to keep the signal but drop the buzz. */\n haptics?: boolean;\n className?: string;\n}\n\nconst ZMIN = 1;\nconst ZMAX = 4;\nconst clampPan = (p: { x: number; y: number }, z: number) => ({\n x: Math.min(0, Math.max(VBW * (1 - z), p.x)),\n y: Math.min(0, Math.max(VBH * (1 - z), p.y)),\n});\n\n/** 2D SVG body pain-map. Click/tap a region → set intensity + quality → PainReport. */\nexport function BodyMap({\n value, defaultValue, onChange, config = {}, defaultView = \"front\", onViewChange,\n palette, locale = \"da\", labels, readOnly = false, onFeedback, haptics, className,\n}: BodyMapProps) {\n useEffect(ensureStyles, []);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const [selected, setSelected] = useState<string | null>(null);\n const [view, setView] = useState<BodyView2D>(defaultView);\n const [zoom, setZoom] = useState(1);\n const [pan, setPan] = useState({ x: 0, y: 0 });\n const report = value ?? internal;\n const shapes = view === \"back\" ? SHAPES_BACK : SHAPES;\n const fills = view === \"back\" ? FILL_BACK : FILL;\n const L = resolveLabels(locale, labels);\n const nameOf = (key: string) => L.regions[key] ?? getRegion(key)?.label ?? key;\n\n const svgRef = useRef<SVGSVGElement>(null);\n const zoomRef = useRef(1);\n const panRef = useRef({ x: 0, y: 0 });\n const pointers = useRef(new Map<number, { x: number; y: number }>());\n const gesture = useRef({ startDist: 0, startZoom: 1, midVB: { x: 0, y: 0 }, start: { x: 0, y: 0 }, moved: false });\n const suppressClick = useRef(false);\n\n const setZP = (z: number, p: { x: number; y: number }) => {\n const cz = Math.max(ZMIN, Math.min(ZMAX, z));\n const cp = clampPan(p, cz);\n zoomRef.current = cz; panRef.current = cp;\n setZoom(cz); setPan(cp);\n };\n const screenToVB = (cx: number, cy: number) => {\n const el = svgRef.current;\n if (!el) return { x: VBW / 2, y: VBH / 2 };\n const r = el.getBoundingClientRect();\n if (!r.width || !r.height) return { x: VBW / 2, y: VBH / 2 };\n return { x: ((cx - r.left) / r.width) * VBW, y: ((cy - r.top) / r.height) * VBH };\n };\n // Zoom keeping the given viewBox focal point under the cursor/fingers.\n const zoomTo = (nz: number, focalVB: { x: number; y: number }) => {\n const z = Math.max(ZMIN, Math.min(ZMAX, nz));\n const p = panRef.current;\n setZP(z, { x: focalVB.x - (focalVB.x - p.x) * (z / zoomRef.current), y: focalVB.y - (focalVB.y - p.y) * (z / zoomRef.current) });\n };\n const zoomBtn = (factor: number) => zoomTo(zoomRef.current * factor, { x: VBW / 2, y: VBH / 2 });\n const resetZoom = () => setZP(1, { x: 0, y: 0 });\n\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n const pts = [...pointers.current.values()];\n if (pts.length === 2) {\n gesture.current.startDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);\n gesture.current.startZoom = zoomRef.current;\n gesture.current.midVB = screenToVB((pts[0].x + pts[1].x) / 2, (pts[0].y + pts[1].y) / 2);\n gesture.current.moved = true;\n suppressClick.current = true;\n } else {\n gesture.current.start = { x: e.clientX, y: e.clientY };\n gesture.current.moved = false;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n const pts = [...pointers.current.values()];\n if (pts.length >= 2) {\n const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);\n if (gesture.current.startDist > 0) zoomTo(gesture.current.startZoom * (dist / gesture.current.startDist), gesture.current.midVB);\n return;\n }\n const el = svgRef.current;\n const rect = el?.getBoundingClientRect();\n if (Math.hypot(e.clientX - gesture.current.start.x, e.clientY - gesture.current.start.y) > 6) {\n gesture.current.moved = true;\n suppressClick.current = true;\n }\n if (zoomRef.current > 1 && rect && rect.width) {\n const dxVB = ((e.clientX - prev.x) / rect.width) * VBW;\n const dyVB = ((e.clientY - prev.y) / rect.height) * VBH;\n setZP(zoomRef.current, { x: panRef.current.x + dxVB, y: panRef.current.y + dyVB });\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size === 0) window.setTimeout(() => { suppressClick.current = false; }, 60);\n };\n\n const changeView = (v: BodyView2D) => {\n if (v === view) return;\n setView(v);\n const sh = v === \"back\" ? SHAPES_BACK : SHAPES;\n if (selected && !sh[selected]) setSelected(null); // selected region absent in the new view\n onViewChange?.(v);\n };\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n const pointOf = (key: string) => report.find((p) => p.region === key);\n const setPain = (key: string, intensity: number, type?: PainType) => {\n const point = painPointSchema.parse({ region: key, intensity, type, timestamp: new Date().toISOString() });\n commit([...report.filter((p) => p.region !== key), point]);\n };\n const removePain = (key: string) => {\n commit(report.filter((p) => p.region !== key));\n setSelected(null);\n };\n /**\n * F052.20 — ONE decision, two entry points (tap and keyboard).\n *\n * A region that is ALREADY marked is CLEARED by picking it again. Christian's\n * call, 2026-08-28, on fd-sundhed's user asking «hvorfor kan jeg ikke bare\n * trykke på punktet igen og det forsvinder?» — taken with the cost stated:\n * re-picking a marked region used to be the only way to change its intensity,\n * so that now means clear it and mark it again.\n *\n * The «Fjern» button stays. It is the panel path right after marking a fresh\n * point, and it is the labelled control a screen-reader user reaches.\n */\n const applyPick = (key: string) => {\n const what = decidePick(key, report, config);\n // The signal carries the outcome that HAPPENED — emitted for all three,\n // including \"ignore\", so an app can tell \"locked\" from \"nothing to do\".\n // The buzz is chosen by outcome (VIBRATION_PATTERNS.ignore is empty), so a\n // tap that changed nothing never feels like it did.\n emitFeedback(what, key, { onFeedback, haptics });\n if (what === \"ignore\") return;\n if (what === \"clear\") { removePain(key); return; }\n setSelected(key);\n };\n const pickRegion = (key: string) => {\n // A pan/pinch just ended — not a tap. This guard used to protect against\n // opening a panel by accident; it now protects against DELETING a mark.\n if (suppressClick.current) { suppressClick.current = false; return; }\n applyPick(key);\n };\n\n const regions = resolveRegions(config);\n const region: BodyRegion | undefined = selected ? REGIONS.find((r) => r.key === selected) : undefined;\n const current = selected ? pointOf(selected) : undefined;\n const rootCls = [\"bmap\", className].filter(Boolean).join(\" \");\n // palette.selected drives the focus/selected stroke via --bmap-accent;\n // palette.ui drives the panel chrome (F052.19).\n const rootStyle = chromeStyle(palette);\n const ariaFor = (r: BodyRegion, marked?: PainReport[number], interactive = true): string => {\n if (marked) return L.ariaMarked(nameOf(r.key), marked.intensity, marked.type ? L.qualities[marked.type] : undefined);\n return interactive ? L.ariaUnmarked(nameOf(r.key)) : nameOf(r.key);\n };\n\n return (\n <div className={rootCls} data-testid=\"bodymap-root\" style={rootStyle}>\n <div className=\"bmap__stage\">\n <div className=\"bmap__bar\">\n <div className=\"bmap__viewtoggle\" role=\"group\" aria-label={L.viewLabel}>\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"front\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-front\"\n aria-pressed={view === \"front\"}\n onClick={() => changeView(\"front\")}\n >\n {L.front}\n </button>\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"back\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-back\"\n aria-pressed={view === \"back\"}\n onClick={() => changeView(\"back\")}\n >\n {L.back}\n </button>\n </div>\n <div className=\"bmap__zoom\" role=\"group\" aria-label={L.zoomLabel}>\n <button type=\"button\" data-testid=\"bodymap-zoom-out\" aria-label={L.zoomOut} disabled={zoom <= ZMIN} onClick={() => zoomBtn(1 / 1.4)}>−</button>\n <button type=\"button\" data-testid=\"bodymap-zoom-reset\" aria-label={L.zoomReset} disabled={zoom === 1 && pan.x === 0 && pan.y === 0} onClick={resetZoom}>⤢</button>\n <button type=\"button\" data-testid=\"bodymap-zoom-in\" aria-label={L.zoomIn} disabled={zoom >= ZMAX} onClick={() => zoomBtn(1.4)}>+</button>\n </div>\n </div>\n <svg\n ref={svgRef}\n className=\"bmap__svg\"\n data-testid=\"bodymap-svg\"\n viewBox=\"0 0 260 470\"\n role=\"group\"\n aria-label={L.svgLabel}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n onWheel={(e) => zoomTo(zoomRef.current * (e.deltaY < 0 ? 1.12 : 1 / 1.12), screenToVB(e.clientX, e.clientY))}\n >\n <g transform={`translate(${pan.x} ${pan.y}) scale(${zoom})`}>\n {regions.map((r) => {\n const s = shapes[r.key];\n if (!s) return null;\n const marked = pointOf(r.key);\n const interactive = isSelectable(r.key, config) && !readOnly;\n const visCls = [\"bmap__vis\", selected === r.key && \"bmap__vis--sel\"].filter(Boolean).join(\" \");\n const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? \"#cbd5e1\", palette);\n const hs = hitShape(s);\n return (\n <g key={r.key}>\n {shapeEl(hs, {\n className: \"bmap__hit\" + (interactive ? \"\" : \" bmap__hit--locked\"),\n \"data-testid\": `bodymap-region-${r.key}`,\n role: interactive ? \"button\" : \"img\",\n tabIndex: interactive ? 0 : -1,\n \"aria-label\": ariaFor(r, marked, interactive),\n \"aria-pressed\": interactive ? selected === r.key : undefined,\n // Click is attached whenever the map is EDITABLE, even on a\n // locked region — the pick returns \"ignore\", changes nothing\n // and does not buzz, but it reaches onFeedback so an app can\n // say WHY nothing happened. Found by measurement (F052.22):\n // the 3D renderer raycasts every mesh and already reported\n // `ignore`, so gating this on `interactive` made the two\n // renderers answer the same tap differently. ARIA is\n // deliberately NOT widened — a locked region must not become\n // a focusable button that does nothing.\n onClick: readOnly ? undefined : () => pickRegion(r.key),\n onKeyDown: interactive\n ? (e: React.KeyboardEvent) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); suppressClick.current = false; applyPick(r.key); } }\n : undefined,\n })}\n {shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })}\n </g>\n );\n })}\n {/* intensity numbers on marked regions visible in the current view */}\n {report.map((p) => {\n const s = shapes[p.region];\n if (!s || config[p.region]?.visible === false) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmap__heatn\">\n {p.intensity}\n </text>\n );\n })}\n </g>\n </svg>\n </div>\n\n {/* picker panel — hidden entirely in readOnly (display-only) mode */}\n {readOnly ? null : region ? (\n <div className=\"bmap__panel\" data-testid=\"bodymap-panel\">\n <div className=\"bmap__ph\">\n <b>{nameOf(region.key)}</b>\n <span className=\"bmap__code\">{region.code}</span>\n {/* F052.26 — the way OUT. Opening a region writes nothing, so there\n was no exit that did not write something: mark it, or open a\n different region you did not mean either. Always available, not\n only when nothing is marked — closing is \"stop editing this\n region\", which is a different act from removing the mark. */}\n <button\n type=\"button\"\n className=\"bmap__close\"\n data-testid=\"bodymap-close\"\n aria-label={L.close}\n onClick={() => setSelected(null)}\n >×</button>\n </div>\n <p className=\"bmap__lbl\" id=\"bmap-intensity-lbl\">{L.intensity}</p>\n <div className=\"bmap__scale\" role=\"group\" aria-labelledby=\"bmap-intensity-lbl\">\n {Array.from({ length: 11 }, (_, n) => (\n <button\n key={n}\n type=\"button\"\n className={\"bmap__i\" + (current?.intensity === n ? \" bmap__i--on\" : \"\")}\n data-testid={`bodymap-intensity-${n}`}\n aria-pressed={current?.intensity === n}\n onClick={() => setPain(region.key, n, current?.type)}\n >\n {n}\n </button>\n ))}\n </div>\n <p className=\"bmap__lbl\" id=\"bmap-quality-lbl\">{L.quality}</p>\n <div className=\"bmap__chips\" role=\"group\" aria-labelledby=\"bmap-quality-lbl\">\n {PAIN_TYPES.map((t) => (\n <button\n key={t}\n type=\"button\"\n className={\"bmap__chip\" + (current?.type === t ? \" bmap__chip--on\" : \"\")}\n data-testid={`bodymap-type-${t}`}\n aria-pressed={current?.type === t}\n onClick={() => setPain(region.key, current?.intensity ?? 5, t)}\n >\n {L.qualities[t]}\n </button>\n ))}\n </div>\n {current && (\n <button type=\"button\" className=\"bmap__rm\" data-testid=\"bodymap-remove\" onClick={() => removePain(region.key)}>\n {L.remove}\n </button>\n )}\n </div>\n ) : (\n <div className=\"bmap__panel bmap__panel--empty\" data-testid=\"bodymap-panel\">\n {L.empty}\n </div>\n )}\n </div>\n );\n}\n\n// ---- <BodyMapCompare> — before/after progress (F052.13) ---------------------\n// Two read-only bodies side by side + a per-region change list (improved /\n// worse / new / resolved). Pure UI over two PainReports — no network. NB: the\n// \"nudge the user to update their report over time\" is the CONSUMER app's job\n// (fd-sundhed's mobile app via @broberg/webpush), NOT this component.\n\ntype RegionChange = \"new\" | \"resolved\" | \"improved\" | \"worse\" | \"same\";\nfunction changeOf(b?: number, a?: number): RegionChange {\n if (b == null && a != null) return \"new\";\n if (b != null && a == null) return \"resolved\";\n if (b == null || a == null) return \"same\";\n return a < b ? \"improved\" : a > b ? \"worse\" : \"same\";\n}\n\nfunction CompareFigure({ report, view, palette, caption, testid }: {\n report: PainReport; view: BodyView2D; palette?: BodymapPalette; caption: string; testid: string;\n}) {\n const shapes = view === \"back\" ? SHAPES_BACK : SHAPES;\n const fills = view === \"back\" ? FILL_BACK : FILL;\n return (\n <figure className=\"bmapc__fig\" data-testid={testid}>\n <figcaption className=\"bmapc__cap\">{caption}</figcaption>\n <svg className=\"bmapc__svg\" viewBox=\"0 0 260 470\" role=\"img\" aria-label={caption}>\n {REGIONS.map((r) => {\n const s = shapes[r.key];\n if (!s) return null;\n const m = report.find((p) => p.region === r.key);\n const fill = m ? markedFill(m.intensity, palette) : baseFill(r.key, fills[r.key] ?? \"#cbd5e1\", palette);\n return <g key={r.key}>{shapeEl(s, { className: \"bmapc__vis\", fill })}</g>;\n })}\n {report.map((p) => {\n const s = shapes[p.region];\n if (!s) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmapc__n\">{p.intensity}</text>\n );\n })}\n </svg>\n </figure>\n );\n}\n\nexport interface BodyMapCompareProps {\n /** The earlier report. */\n before: PainReport;\n /** The later report. */\n after: PainReport;\n palette?: BodymapPalette;\n locale?: BodyMapLocale;\n labels?: Partial<BodyMapLabels>;\n defaultView?: BodyView2D;\n className?: string;\n}\n\n/** Read-only before/after view: two bodies side by side + a per-region change list. */\nexport function BodyMapCompare({\n before, after, palette, locale = \"da\", labels, defaultView = \"front\", className,\n}: BodyMapCompareProps) {\n useEffect(ensureStyles, []);\n const [view, setView] = useState<BodyView2D>(defaultView);\n const L = resolveLabels(locale, labels);\n const nameOf = (k: string) => L.regions[k] ?? getRegion(k)?.label ?? k;\n const rootStyle = chromeStyle(palette);\n\n const keys = Array.from(new Set([...before, ...after].map((p) => p.region)));\n const changes = keys\n .map((k) => ({\n key: k,\n b: before.find((p) => p.region === k)?.intensity,\n a: after.find((p) => p.region === k)?.intensity,\n status: changeOf(before.find((p) => p.region === k)?.intensity, after.find((p) => p.region === k)?.intensity),\n }))\n .filter((c) => c.status !== \"same\")\n .sort((x, y) => (x.key < y.key ? -1 : 1));\n\n return (\n <div className={[\"bmapc\", className].filter(Boolean).join(\" \")} data-testid=\"bodymap-compare\" style={rootStyle}>\n <div className=\"bmap__viewtoggle\" role=\"group\" aria-label={L.viewLabel} style={{ marginBottom: 12 }}>\n <button type=\"button\" className={\"bmap__vbtn\" + (view === \"front\" ? \" bmap__vbtn--on\" : \"\")} data-testid=\"bodymap-compare-view-front\" aria-pressed={view === \"front\"} onClick={() => setView(\"front\")}>{L.front}</button>\n <button type=\"button\" className={\"bmap__vbtn\" + (view === \"back\" ? \" bmap__vbtn--on\" : \"\")} data-testid=\"bodymap-compare-view-back\" aria-pressed={view === \"back\"} onClick={() => setView(\"back\")}>{L.back}</button>\n </div>\n <div className=\"bmapc__bodies\">\n <CompareFigure report={before} view={view} palette={palette} caption={L.before} testid=\"bodymap-compare-before\" />\n <CompareFigure report={after} view={view} palette={palette} caption={L.after} testid=\"bodymap-compare-after\" />\n </div>\n {changes.length ? (\n <div className=\"bmapc__delta\" data-testid=\"bodymap-compare-delta\">\n {changes.map((c) => (\n <div key={c.key} className=\"bmapc__row\" data-testid={`bodymap-compare-delta-${c.key}`}>\n <span className=\"bmapc__rname\">{nameOf(c.key)}</span>\n <span className=\"bmapc__rval\">{c.b ?? \"–\"} → {c.a ?? \"–\"}</span>\n <span className={`bmapc__badge bmapc__badge--${c.status}`}>{L.change[c.status as Exclude<RegionChange, \"same\">]}</span>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"bmapc__empty\" data-testid=\"bodymap-compare-delta\">{L.noChange}</div>\n )}\n </div>\n );\n}\n","/**\n * @broberg/bodymap/three — a rotatable 3D body pain-map on the SAME core.\n *\n * `<BodyMap3D>` wraps a VANILLA three.js scene (NOT react-three-fiber, so it\n * runs in React, Preact and a Capacitor webview alike): a realistic Blender\n * Studio human base mesh (CC0), rotatable (drag) + zoomable (scroll), with\n * hover-highlight and click-to-mark — a click colours the body part by pain\n * intensity and produces the same structured PainReport (bodymap/v1) the 2D\n * renderer does. Region hit-testing is anchor-based: every mesh vertex is\n * assigned to its nearest region anchor (true per-zone mesh segmentation is a\n * later refinement — the wire + interaction are identical either way).\n *\n * `three` is an OPTIONAL peer — only consumers importing this subpath pull it\n * in. The body models are NOT bundled: pass `models={{ male, female }}` as URLs\n * you host (the package ships reference GLBs under `@broberg/bodymap/models/`).\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { GLTFLoader } from \"three/examples/jsm/loaders/GLTFLoader.js\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport {\n REGIONS,\n PAIN_TYPES,\n getRegion,\n isSelectable,\n decidePick,\n type PickOutcome,\n emitFeedback,\n type FeedbackFn,\n serializeReport,\n heatFor,\n baseColorFor,\n defaultPalette,\n uiColors,\n type BodymapPalette,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\nimport {\n LABELS_DA,\n LABELS_EN,\n type BodyMapLabels,\n type BodyMapLocale,\n} from \"./react.js\";\n\nexport type BodyMap3DSex = \"male\" | \"female\";\n\n/** URLs of the body GLB models the consumer hosts (reference GLBs ship under `@broberg/bodymap/models/`). */\nexport interface BodyMap3DModels {\n male: string;\n female: string;\n}\n\n/** The 3D-only control strings (the shared labels come from `locale`/`labels`). */\nexport interface BodyMap3DUiLabels {\n /** Label for the built-in fullscreen control (F052.24). */\n expand?: string;\n /** Label for the same control while fullscreen. */\n collapse?: string;\n male: string;\n female: string;\n hoverHint: string;\n}\n\nconst UI_DA: BodyMap3DUiLabels = { male: \"Mand\", female: \"Kvinde\", hoverHint: \"Hover for at fremhæve · klik en kropsdel for at markere smerte.\", expand: \"Vis stor\", collapse: \"Luk stor visning\" };\nconst UI_EN: BodyMap3DUiLabels = { male: \"Male\", female: \"Female\", hoverHint: \"Hover to highlight · tap a body part to mark pain.\", expand: \"Expand\", collapse: \"Close\" };\n\n// Region anchors in normalised body space (height ~1.9, feet y=0, front +z),\n// then x-flipped so the patient's own left maps to \"venstre\" (self-view).\nconst ANCHORS: Record<string, [number, number, number]> = {\n head: [0, 1.79, 0.02], neck: [0, 1.57, 0.0],\n chest: [0, 1.42, 0.11], thora: [0, 1.42, -0.12], lumbar: [0, 1.13, -0.13], groin: [0, 0.92, 0.09],\n shoulder_left: [-0.2, 1.5, 0], shoulder_right: [0.2, 1.5, 0],\n uarm_left: [-0.27, 1.3, 0.0], uarm_right: [0.27, 1.3, 0.0],\n elbow_left: [-0.31, 1.08, 0], elbow_right: [0.31, 1.08, 0],\n farm_left: [-0.34, 0.93, 0.02], farm_right: [0.34, 0.93, 0.02],\n wrist_left: [-0.36, 0.79, 0.02], wrist_right: [0.36, 0.79, 0.02],\n hand_left: [-0.37, 0.68, 0.03], hand_right: [0.37, 0.68, 0.03],\n hip_left: [-0.14, 1.02, -0.03], hip_right: [0.14, 1.02, -0.03],\n thigh_left: [-0.1, 0.68, 0.05], thigh_right: [0.1, 0.68, 0.05],\n knee_left: [-0.1, 0.4, 0.06], knee_right: [0.1, 0.4, 0.06],\n lowleg_left: [-0.1, 0.22, 0.04], lowleg_right: [0.1, 0.22, 0.04],\n ankle_left: [-0.1, 0.05, 0.02], ankle_right: [0.1, 0.05, 0.02],\n foot_left: [-0.1, 0.02, 0.11], foot_right: [0.1, 0.02, 0.11],\n};\n/**\n * How much of the RUNNER-UP region bleeds into this vertex (F052.21).\n *\n * Takes SQUARED distances (what the assignment loop already has). Returns 0 at\n * an anchor and 0.5 where two anchors are equidistant — never above 0.5, so the\n * nearest region always dominates and a vertex can never be painted mostly as\n * its neighbour.\n *\n * `SEAM` decides how wide the fade is. Raised toward 1 the whole body turns to\n * mush and a marked region stops reading as a region; at 0 you get the old hard\n * plane back. It is a look, not a contract — hit-testing never sees this number.\n */\n/** How wide the fade is, in ratio units below the seam. 0 = the old hard plane. */\nconst SEAM = 0.18;\n\n/**\n * How much of the RUNNER-UP region bleeds into this vertex (F052.21).\n *\n * Takes SQUARED distances — what the assignment loop already has.\n *\n * ratio = d1 / (d1 + d2) → 0 at the anchor · 0.5 where two anchors are equidistant\n *\n * Returns 0 well inside a region and rises to 0.5 exactly at the seam, so the\n * nearest region ALWAYS dominates and a vertex can never be painted mostly as\n * its neighbour. Smoothstepped, so the fade has no visible start line of its own.\n *\n * This is a look, not a contract: hit-testing never sees this number, and the\n * PainReport is unchanged.\n */\nexport function seamBlend(nearestSq: number, secondSq: number): number {\n const d1 = Math.sqrt(nearestSq), d2 = Math.sqrt(secondSq);\n const sum = d1 + d2;\n if (!(sum > 0)) return 0;\n const ratio = d1 / sum;\n const t = (ratio - (0.5 - SEAM)) / SEAM;\n if (t <= 0) return 0;\n if (t >= 1) return 0.5;\n return t * t * (3 - 2 * t) * 0.5; // smoothstep\n}\n\nconst ANCHOR_KEYS = Object.keys(ANCHORS);\nfor (const k of ANCHOR_KEYS) ANCHORS[k][0] = -ANCHORS[k][0];\n\nfunction mergeLabels(locale: BodyMapLocale, overrides?: Partial<BodyMapLabels>): BodyMapLabels {\n const base = locale === \"en\" ? LABELS_EN : LABELS_DA;\n if (!overrides) return base;\n return {\n ...base,\n ...overrides,\n regions: { ...base.regions, ...overrides.regions },\n qualities: { ...base.qualities, ...overrides.qualities },\n };\n}\n\nfunction webglAvailable(): boolean {\n try {\n const c = document.createElement(\"canvas\");\n return !!(c.getContext(\"webgl\") || c.getContext(\"experimental-webgl\"));\n } catch {\n return false;\n }\n}\n\nexport interface BodyMap3DProps {\n /** URLs of the male/female body GLBs (you host them; reference GLBs ship under `@broberg/bodymap/models/`). */\n models: BodyMap3DModels;\n value?: PainReport;\n defaultValue?: PainReport;\n onChange?: (report: PainReport) => void;\n /** Per-app region config — a non-selectable region is not pickable in 3D either. */\n config?: RegionConfig;\n palette?: BodymapPalette;\n locale?: BodyMapLocale;\n labels?: Partial<BodyMapLabels>;\n /** Override the 3D-only control strings (male/female/hoverHint). */\n ui?: Partial<BodyMap3DUiLabels>;\n defaultSex?: BodyMap3DSex;\n onSexChange?: (sex: BodyMap3DSex) => void;\n /** Controlled body type — when set, the parent owns it (e.g. from the user's profile); `onSexChange` still fires. */\n sex?: BodyMap3DSex;\n /** Show the Male/Female toggle (default true). Set false when `sex` comes from a profile and the picker would just be noise. */\n showSexToggle?: boolean;\n /** Slowly auto-rotate until the user interacts (default true). */\n autoRotate?: boolean;\n /** Height of the 3D canvas (default '60vh'). A bare number is treated as px. Set a\n * shorter value on narrow viewports so the picker panel below the body stays visible\n * the moment a region is selected (no full-screen scroll). */\n canvasHeight?: string | number;\n /** Show the clinical region-code badge (e.g. \"HIP · right\") in the panel header\n * (default true). Set false for a patient/employee-facing flow where the code is\n * internal jargon and the readable region name is enough. */\n showRegionCode?: boolean;\n /** Fill the viewport with the body (F052.24). Controlled: pass it and you own\n * it; `onFullscreenChange` still fires so your own button can drive it.\n *\n * This is a VIEWPORT FILL, not the browser Fullscreen API. iOS Safari does not\n * support fullscreen on an arbitrary element at all, and the iPhone is this\n * component's primary surface — so the mechanism that works everywhere is the\n * one that ships, and it behaves identically on every platform. */\n fullscreen?: boolean;\n /** Fired when the built-in control or the Escape key changes it. */\n onFullscreenChange?: (fullscreen: boolean) => void;\n /** Show the built-in expand/collapse control (default true). Set false and\n * drive `fullscreen` from your own button. */\n showFullscreenButton?: boolean;\n /** Soften the colour transition between neighbouring regions (F052.21).\n *\n * DEFAULT FALSE, and the default is the accessible one (F052.23). The blend\n * mixes the BODY colour into a marked region near its boundary — which on a\n * SMALL mark is most of the mark. Measured on a phone with a consumer's\n * palette and one knee marked: the mark's average contrast against the body\n * fell from 4.71:1 to 3.59:1, i.e. below the WCAG AA threshold of 4.5:1, on a\n * surface a public authority is legally required to meet.\n *\n * At the single strongest pixel the cost is small (5.12 → 4.78) — which is\n * why a spot-check misses it and the AVERAGE is what a person sees.\n *\n * Turn it on when the marked areas are LARGE and you have no accessibility\n * duty: the soft seam genuinely looks better there, and that is why it is\n * still here rather than deleted. */\n seam?: boolean;\n /** Fired after every pick with what ACTUALLY happened (F052.22): \"select\",\n * \"clear\" or \"ignore\". Wire it to a sound (`@broberg/soundkit`) or to native\n * haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an\n * iPhone, where web vibration does not exist). */\n onFeedback?: FeedbackFn;\n /** Web vibration on select/clear. Default true; silently inert where\n * `navigator.vibrate` is absent (every browser on iPhone, most desktops).\n * Set false to keep the signal but drop the buzz. */\n haptics?: boolean;\n className?: string;\n}\n\nconst btn: React.CSSProperties = { font: \"inherit\", cursor: \"pointer\", borderRadius: 8, border: \"1px solid #e2e8f0\", background: \"#fff\", padding: \"6px 9px\" };\nconst seg = (on: boolean): React.CSSProperties => ({ ...btn, background: on ? \"#0e8f8a\" : \"#fff\", color: on ? \"#fff\" : \"#1e293b\", fontWeight: 600 });\n\nexport function BodyMap3D(props: BodyMap3DProps) {\n const {\n models, value, defaultValue, onChange, config, palette = defaultPalette,\n locale = \"da\", labels, ui, defaultSex = \"male\", sex: sexProp, showSexToggle = true, onSexChange,\n autoRotate = true, canvasHeight = \"60vh\", showRegionCode = true,\n onFeedback, haptics, seam = false,\n fullscreen: fullscreenProp, onFullscreenChange, showFullscreenButton = true,\n className,\n } = props;\n\n // Panel chrome — consumer palette wins, AA-safe defaults fill every gap. (F052.19)\n const chrome = uiColors(palette);\n\n const L = mergeLabels(locale, labels);\n const UI = { ...(locale === \"en\" ? UI_EN : UI_DA), ...ui };\n const nameOf = (key: string) => L.regions[key] ?? getRegion(key)?.label ?? key;\n const canvasH = typeof canvasHeight === \"number\" ? `${canvasHeight}px` : canvasHeight;\n\n const mountRef = useRef<HTMLDivElement>(null);\n const loadedRef = useRef<HTMLSpanElement>(null);\n const [unsupported, setUnsupported] = useState(false);\n const [ready, setReady] = useState(false);\n const [internalSex, setInternalSex] = useState<BodyMap3DSex>(defaultSex);\n const sex = sexProp ?? internalSex; // controlled when `sex` is passed, else internal\n const [selected, setSelected] = useState<string | null>(null);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const report = value ?? internal;\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n\n // Latest state visible to the once-mounted imperative scene.\n const reportRef = useRef(report); reportRef.current = report;\n const selectedRef = useRef(selected); selectedRef.current = selected;\n const paletteRef = useRef(palette); paletteRef.current = palette;\n const configRef = useRef(config); configRef.current = config;\n const modelsRef = useRef(models); modelsRef.current = models;\n const sexRef = useRef(sex); sexRef.current = sex;\n const setSelectedRef = useRef(setSelected); setSelectedRef.current = setSelected;\n // F052.20 — the pick DECISION, reachable from the once-mounted raycast handler.\n // Assigned below, after removePain exists.\n const pickRef = useRef<(key: string) => PickOutcome>(() => \"ignore\");\n const setReadyRef = useRef(setReady); setReadyRef.current = setReady;\n // The scene closure reads this at PAINT time, so toggling `seam` recolours\n // without rebuilding the model (F052.23).\n const seamRef = useRef(seam); seamRef.current = seam;\n // Controlled when `fullscreen` is passed, internal otherwise — same shape as\n // `sex` (F052.14), so a consumer can drive it from their own button.\n const [internalFs, setInternalFs] = useState(false);\n const isFullscreen = fullscreenProp ?? internalFs;\n const setFullscreen = (v: boolean) => {\n if (fullscreenProp === undefined) setInternalFs(v);\n onFullscreenChange?.(v);\n };\n const apiRef = useRef<{ setSex: (s: BodyMap3DSex) => void; refresh: () => void } | null>(null);\n\n useEffect(() => {\n if (!webglAvailable()) { setUnsupported(true); return; }\n const el = mountRef.current;\n if (!el) return;\n let W = el.clientWidth || 520, H = el.clientHeight || 600;\n\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(0x0e1424);\n const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 100);\n camera.position.set(0, 1.05, 4.4);\n let renderer: THREE.WebGLRenderer;\n try {\n renderer = new THREE.WebGLRenderer({ antialias: true });\n } catch {\n setUnsupported(true);\n return;\n }\n renderer.setSize(W, H);\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n el.appendChild(renderer.domElement);\n scene.add(new THREE.HemisphereLight(0xffffff, 0x223044, 1.05));\n const kl = new THREE.DirectionalLight(0xffffff, 1.5); kl.position.set(3, 5, 4); scene.add(kl);\n const rl = new THREE.DirectionalLight(0x88aaff, 0.7); rl.position.set(-4, 2, -3); scene.add(rl);\n\n const controls = new OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true; controls.dampingFactor = 0.08;\n controls.autoRotate = autoRotate; controls.autoRotateSpeed = 1.1;\n controls.minDistance = 2.2; controls.maxDistance = 8; controls.enablePan = false;\n controls.target.set(0, 0.95, 0);\n controls.addEventListener(\"start\", () => { controls.autoRotate = false; });\n\n // On-demand rendering: render only while auto-rotating or while damping is\n // still settling, then STOP — so the page goes idle. That saves battery on\n // mobile (bodymap is primarily a mobile surface) AND lets a headless\n // Lens/Playwright run actually land clicks instead of starving on a\n // never-quiet WebGL loop (fd-sundhed's F052.6-preview finding). A control\n // 'change' / interaction / state change kicks the pump back to life.\n let raf = 0;\n let pumping = false;\n const renderFrame = () => renderer.render(scene, camera);\n const pump = () => {\n const moved = controls.update();\n renderFrame();\n if (moved || controls.autoRotate) { raf = requestAnimationFrame(pump); }\n else { pumping = false; raf = 0; }\n };\n const kick = () => { if (!pumping && !document.hidden) { pumping = true; raf = requestAnimationFrame(pump); } };\n controls.addEventListener(\"change\", kick);\n\n let modelRoot: THREE.Object3D | null = null;\n let bodyMesh: THREE.Mesh | null = null;\n // F052.21 — the hard assignment stays (it is what HIT-TESTING answers with);\n // `vertexNeighbour` + `vertexBlend` exist only so the COLOUR can fade at a\n // seam instead of stopping at a plane. A soft LOOK must never become a soft\n // ANSWER in a clinical record.\n let vertexRegion: string[] = [];\n let vertexNeighbour: string[] = [];\n let vertexBlend: Float32Array = new Float32Array(0);\n let colorAttr: THREE.BufferAttribute | null = null;\n let hovered: string | null = null;\n const loader = new GLTFLoader();\n const anchorVecs = ANCHOR_KEYS.map((k) => new THREE.Vector3(...ANCHORS[k]));\n const tmp = new THREE.Color();\n\n const restingHex = (key: string): string => {\n const pt = reportRef.current.find((p) => p.region === key);\n if (pt) return heatFor(pt.intensity, paletteRef.current);\n if (selectedRef.current === key) return paletteRef.current.selected;\n return baseColorFor(key, paletteRef.current);\n };\n const tmp2 = new THREE.Color();\n const colourOf = (key: string): string => (hovered === key ? paletteRef.current.hover : restingHex(key));\n\n /**\n * ONE pass over the vertices, blending the two nearest regions.\n *\n * The old paint walked every vertex once PER REGION (20x) and set a flat\n * colour, so a boundary was a hard plane through the mesh — the \"sharp\n * transitions\" Christian saw. It was never the model: 21,160 triangles with\n * smooth normals, and the body renders smooth in every screenshot.\n */\n const paint = () => {\n if (!colorAttr) return;\n const cache = new Map<string, THREE.Color>();\n const col = (k: string) => {\n let c = cache.get(k);\n if (!c) { c = new THREE.Color(colourOf(k)); cache.set(k, c); }\n return c;\n };\n for (let i = 0; i < vertexRegion.length; i++) {\n tmp.copy(col(vertexRegion[i]!));\n // Off by default: a blended mark is a LOWER-CONTRAST mark, and the\n // shipped default has to be the one that passes AA. (F052.23)\n const w = seamRef.current ? vertexBlend[i]! : 0;\n if (w > 0) { tmp2.copy(col(vertexNeighbour[i]!)); tmp.lerp(tmp2, w); }\n colorAttr.setXYZ(i, tmp.r, tmp.g, tmp.b);\n }\n colorAttr.needsUpdate = true;\n };\n const refresh = () => { paint(); renderFrame(); };\n\n const loadModel = (which: BodyMap3DSex) => {\n const url = which === \"female\" ? modelsRef.current.female : modelsRef.current.male;\n loader.load(url, (gltf) => {\n if (modelRoot) scene.remove(modelRoot);\n const model = gltf.scene;\n const box = new THREE.Box3().setFromObject(model);\n const size = new THREE.Vector3(); box.getSize(size);\n const center = new THREE.Vector3(); box.getCenter(center);\n const scale = 1.9 / size.y;\n model.scale.setScalar(scale);\n model.position.set(-center.x * scale, -box.min.y * scale, -center.z * scale);\n model.updateMatrixWorld(true);\n bodyMesh = null;\n model.traverse((o) => { const m = o as THREE.Mesh; if (m.isMesh && !bodyMesh) bodyMesh = m; });\n if (bodyMesh) {\n const geo = (bodyMesh as THREE.Mesh).geometry as THREE.BufferGeometry;\n // F052.25 — DO NOT recompute normals when the file ships them.\n //\n // This one line was the \"lines on the body, so it looks assembled\"\n // Christian reported. computeVertexNormals() averages face normals PER\n // VERTEX INDEX, and this mesh has split vertices along its UV seams —\n // two copies at the same position, each averaging a different set of\n // faces. The result is a lighting discontinuity exactly along those\n // seams: a vertical line down the torso, horizontals at chest, waist,\n // knees, elbows, ankles. The GLB ships correct smooth normals; we were\n // throwing them away and reconstructing worse ones.\n //\n // Measured side by side, same page, same props, same viewport: with\n // the call, every one of those lines is visible; without it, none are.\n //\n // Kept as a FALLBACK only: a model with no normal attribute would\n // otherwise render flat/black, so compute them when — and only when —\n // the file did not provide any.\n if (!geo.getAttribute(\"normal\")) geo.computeVertexNormals();\n const pos = geo.getAttribute(\"position\") as THREE.BufferAttribute;\n const n = pos.count;\n const cols = new Float32Array(n * 3);\n vertexRegion = new Array(n);\n vertexNeighbour = new Array(n);\n vertexBlend = new Float32Array(n);\n const v = new THREE.Vector3();\n for (let i = 0; i < n; i++) {\n v.fromBufferAttribute(pos, i).applyMatrix4((bodyMesh as THREE.Mesh).matrixWorld);\n // nearest AND runner-up, in one pass\n let best = 0, second = 0, bd = Infinity, sd = Infinity;\n for (let a = 0; a < anchorVecs.length; a++) {\n const d = v.distanceToSquared(anchorVecs[a]);\n if (d < bd) { sd = bd; second = best; bd = d; best = a; }\n else if (d < sd) { sd = d; second = a; }\n }\n vertexRegion[i] = ANCHOR_KEYS[best]!;\n vertexNeighbour[i] = ANCHOR_KEYS[second]!;\n vertexBlend[i] = seamBlend(bd, sd);\n }\n colorAttr = new THREE.BufferAttribute(cols, 3);\n geo.setAttribute(\"color\", colorAttr);\n (bodyMesh as THREE.Mesh).material = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.72, metalness: 0.04 });\n refresh();\n }\n modelRoot = model;\n scene.add(model);\n renderFrame();\n kick();\n setReadyRef.current(true);\n if (loadedRef.current) { loadedRef.current.setAttribute(\"data-loaded\", \"true\"); loadedRef.current.setAttribute(\"data-model\", which); }\n });\n };\n loadModel(sexRef.current);\n apiRef.current = {\n setSex: (s) => { loadedRef.current?.removeAttribute(\"data-loaded\"); setReadyRef.current(false); loadModel(s); },\n refresh,\n };\n\n const raycaster = new THREE.Raycaster();\n const ndc = new THREE.Vector2();\n const pick = (clientX: number, clientY: number): string | null => {\n if (!bodyMesh) return null;\n const rect = renderer.domElement.getBoundingClientRect();\n ndc.set(((clientX - rect.left) / rect.width) * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);\n raycaster.setFromCamera(ndc, camera);\n const hits = raycaster.intersectObject(bodyMesh, true);\n if (!hits.length) return null;\n const p = hits[0].point;\n // Nearest SELECTABLE anchor — a region hidden/locked by the per-app config\n // is skipped, so it never highlights or gets picked (2D-parity).\n let best: string | null = null, bd = Infinity;\n for (const k of ANCHOR_KEYS) {\n if (!isSelectable(k, configRef.current ?? {})) continue;\n const a = ANCHORS[k];\n const d = (p.x - a[0]) ** 2 + (p.y - a[1]) ** 2 + (p.z - a[2]) ** 2;\n if (d < bd) { bd = d; best = k; }\n }\n return best;\n };\n\n let downX = 0, downY = 0, downT = 0;\n const canvas = renderer.domElement;\n const onDown = (e: PointerEvent) => { downX = e.clientX; downY = e.clientY; downT = Date.now(); };\n const onMove = (e: PointerEvent) => {\n if ((e.buttons || 0) !== 0) return;\n const k = pick(e.clientX, e.clientY);\n if (k === hovered) return;\n hovered = k;\n // One pass over the vertices repaints everything, including both seams the\n // hover just moved across — a per-region repaint cannot, now that a vertex\n // can carry colour from TWO regions.\n paint();\n canvas.style.cursor = k ? \"pointer\" : \"default\";\n renderFrame();\n };\n const onUp = (e: PointerEvent) => {\n if (Math.hypot(e.clientX - downX, e.clientY - downY) > 6 || Date.now() - downT > 450) return;\n const k = pick(e.clientX, e.clientY);\n if (!k) return;\n const outcome = pickRef.current(k);\n // A CLEARED REGION MUST NOT KEEP ITS HIGHLIGHT (F052.20).\n //\n // `refresh()` deliberately preserves the hover colour on `hovered`, so\n // after a removal the region stayed highlighted and looked exactly like it\n // was still marked. Measured on an iPhone viewport: report correctly\n // `points: []`, panel closed, chest still turquoise. A finger leaves no\n // cursor behind, so nothing ever moved away to clear it — the user would\n // tap again and mark it a second time.\n //\n // Dropped unconditionally rather than only for touch: showing a highlight\n // on something the user just deleted is wrong feedback in every input\n // mode, and on a mouse the very next pointermove puts it straight back.\n if (outcome === \"clear\") {\n hovered = null;\n canvas.style.cursor = \"default\";\n }\n };\n canvas.addEventListener(\"pointerdown\", onDown);\n canvas.addEventListener(\"pointermove\", onMove);\n canvas.addEventListener(\"pointerup\", onUp);\n\n renderFrame();\n kick(); // settle initial damping / start auto-rotate if enabled\n const onVis = () => { if (document.hidden) { cancelAnimationFrame(raf); pumping = false; raf = 0; } else kick(); };\n document.addEventListener(\"visibilitychange\", onVis);\n const onResize = () => { W = el.clientWidth || 520; H = el.clientHeight || 600; camera.aspect = W / H; camera.updateProjectionMatrix(); renderer.setSize(W, H); renderFrame(); };\n window.addEventListener(\"resize\", onResize);\n /**\n * F052.24 — the ELEMENT can change size without the WINDOW doing so, and\n * before this the canvas kept whatever size it was measured at on mount.\n * Every way that happens is ordinary, not exotic:\n * · entering/leaving fullscreen (this card's whole feature)\n * · a consumer switching `canvasHeight` at a breakpoint\n * · a container animating open, or a step/wizard revealing the panel\n * · a phone's URL bar collapsing, which changes what `vh` means\n * A stale size shows as a body that is small, off-centre, or clipped — with\n * nothing to see in the code, because the camera maths is correct and the\n * numbers it was given are old.\n */\n const ro = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(() => onResize()) : null;\n ro?.observe(el);\n\n return () => {\n cancelAnimationFrame(raf);\n ro?.disconnect();\n window.removeEventListener(\"resize\", onResize);\n document.removeEventListener(\"visibilitychange\", onVis);\n controls.removeEventListener(\"change\", kick);\n canvas.removeEventListener(\"pointerdown\", onDown);\n canvas.removeEventListener(\"pointermove\", onMove);\n canvas.removeEventListener(\"pointerup\", onUp);\n controls.dispose();\n renderer.dispose();\n if (canvas.parentNode) canvas.parentNode.removeChild(canvas);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Escape leaves fullscreen — a viewport-filling overlay with no keyboard way\n // out is a trap, and F052.11 made this component's accessibility a legal duty.\n useEffect(() => {\n if (!isFullscreen && !selected) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n // Innermost first: Escape closes the open region before it leaves\n // fullscreen, so one press never does two things at once. (F052.26)\n if (selected) setSelected(null);\n else setFullscreen(false);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n });\n\n // React → scene: recolour on selection / report / palette change; swap model on sex change.\n useEffect(() => { apiRef.current?.refresh(); }, [selected, report, palette, seam]);\n // Swap the model on sex change — but skip the first run: the mount effect\n // already loaded sexRef.current, so this avoids a redundant reload (matters on mobile).\n const sexInited = useRef(false);\n useEffect(() => {\n if (!sexInited.current) { sexInited.current = true; return; }\n apiRef.current?.setSex(sex);\n }, [sex]);\n\n const pointOf = (k: string) => report.find((p) => p.region === k);\n const setPain = (k: string, intensity: number, type?: PainType) => {\n commit([...report.filter((p) => p.region !== k), { region: k, intensity, type, timestamp: new Date().toISOString() }]);\n };\n const removePain = (k: string) => { commit(report.filter((p) => p.region !== k)); setSelected(null); };\n /**\n * F052.20 — a region that is ALREADY marked is CLEARED by picking it again.\n * The 2D renderer has the identical rule in `applyPick`; they share no click\n * code, which is exactly why both are asserted separately.\n */\n pickRef.current = (k: string): PickOutcome => {\n const what = decidePick(k, reportRef.current, configRef.current ?? {});\n // Same call as the 2D renderer, from the same decision — the signal cannot\n // drift between the two bodies because neither of them decides it. (F052.22)\n emitFeedback(what, k, { onFeedback, haptics });\n if (what === \"clear\") removePain(k);\n else if (what === \"select\") setSelected(k);\n return what;\n };\n const changeSex = (s: BodyMap3DSex) => { if (sexProp === undefined) setInternalSex(s); onSexChange?.(s); };\n\n const region = selected ? REGIONS.find((r) => r.key === selected) : null;\n const current = selected ? pointOf(selected) : undefined;\n // Smart default: when the config locks EVERYTHING (a report/display view), the\n // \"tap a body part to mark pain\" hint is misleading — suppress it unless the\n // consumer explicitly set a display text via ui.hoverHint.\n const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));\n const showEmptyHint = anySelectable || ui?.hoverHint !== undefined;\n\n return (\n <div\n data-testid=\"bodymap3d-root\"\n className={className}\n data-fullscreen={isFullscreen ? \"true\" : undefined}\n style={{\n fontFamily: \"system-ui, sans-serif\",\n color: \"#1e293b\",\n // A viewport fill, not the Fullscreen API — see the `fullscreen` prop.\n // `fixed`+`inset:0` behaves the same on iOS Safari, where element\n // fullscreen does not exist at all, as it does everywhere else.\n ...(isFullscreen\n ? {\n position: \"fixed\" as const,\n inset: 0,\n zIndex: 2147483000,\n background: chrome.panelBg,\n // F052.27 — a fixed inset:0 surface in a standalone/Capacitor\n // webview covers the STATUS BAR too, so a control laid out at the\n // top lands on the clock and the battery. Measured by fd-sundhed\n // on a real iPhone: the owner could not read the button or get\n // out, and Escape does not exist on a phone. Every side gets its\n // safe-area inset, bottom included — the home indicator covers\n // content just as effectively.\n paddingTop: \"calc(12px + env(safe-area-inset-top))\",\n paddingBottom: \"calc(12px + env(safe-area-inset-bottom))\",\n paddingLeft: \"calc(12px + env(safe-area-inset-left))\",\n paddingRight: \"calc(12px + env(safe-area-inset-right))\",\n display: \"flex\",\n flexDirection: \"column\" as const,\n overflow: \"auto\" as const,\n }\n : null),\n }}\n >\n {showSexToggle && (\n <div style={{ display: \"flex\", gap: 16, flexWrap: \"wrap\", marginBottom: 12, fontSize: 13 }}>\n <div style={{ display: \"flex\", gap: 6, alignItems: \"center\" }}>\n <button data-testid=\"bodymap3d-sex-male\" onClick={() => changeSex(\"male\")} style={seg(sex === \"male\")}>{UI.male}</button>\n <button data-testid=\"bodymap3d-sex-female\" onClick={() => changeSex(\"female\")} style={seg(sex === \"female\")}>{UI.female}</button>\n </div>\n </div>\n )}\n <span ref={loadedRef} data-testid=\"bodymap3d-loaded\" style={{ display: \"none\" }} />\n {ready && <span data-testid=\"bodymap3d-ready\" style={{ position: \"absolute\", width: 1, height: 1, opacity: 0, pointerEvents: \"none\" }} />}\n {showFullscreenButton && !unsupported && (\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", marginBottom: 8 }}>\n <button\n type=\"button\"\n data-testid=\"bodymap3d-fullscreen\"\n aria-pressed={isFullscreen}\n aria-label={isFullscreen ? (UI.collapse ?? \"Close\") : (UI.expand ?? \"Expand\")}\n onClick={() => setFullscreen(!isFullscreen)}\n style={{\n ...btn,\n color: chrome.text,\n borderColor: chrome.border,\n background: chrome.panelBg,\n ...(isFullscreen ? { width: 40, height: 40, padding: 0, fontSize: 22, lineHeight: 1 } : null),\n }}\n >\n {/* A COMPACT × once we are the whole screen. The text pill is\n right when the component sits in a page — it is discoverable —\n and wrong at the top of a phone screen, where it is wide enough\n to cover half the status bar. The label stays on aria-label, so\n nothing is lost for a screen reader. (F052.27) */}\n {isFullscreen ? \"×\" : (UI.expand ?? \"Expand\")}\n </button>\n </div>\n )}\n <div style={{ display: \"flex\", gap: 18, alignItems: \"flex-start\", flexWrap: \"wrap\", ...(isFullscreen ? { flex: \"1 1 auto\", minHeight: 0 } : null) }}>\n {unsupported ? (\n <div data-testid=\"bodymap3d-unsupported\" style={{ flex: \"1 1 520px\", minWidth: 320, height: canvasH, borderRadius: 16, background: \"#0e1424\", color: \"#cbd5e1\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", padding: 24, textAlign: \"center\", fontSize: 14 }}>\n 3D kræver WebGL, som ikke er tilgængeligt her.\n </div>\n ) : (\n <div\n ref={mountRef}\n data-testid=\"bodymap3d-canvas\"\n style={{\n flex: \"1 1 520px\",\n // In fullscreen the box takes the height it is GIVEN rather than a\n // fixed `canvasHeight`; the ResizeObserver above is what makes the\n // picture follow it.\n height: isFullscreen ? \"100%\" : canvasH,\n minWidth: 320,\n minHeight: isFullscreen ? 0 : undefined,\n borderRadius: 16,\n overflow: \"hidden\",\n background: \"#0e1424\",\n touchAction: \"none\",\n }}\n />\n )}\n <div style={{ flex: \"1 1 300px\", minWidth: 260 }}>\n {region ? (\n <div style={{ border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", marginBottom: 12 }}>\n <b style={{ fontSize: 16 }}>{nameOf(region.key)}</b>\n {showRegionCode && (\n <span data-testid=\"bodymap3d-region-code\" style={{ font: \"11px ui-monospace, monospace\", color: chrome.mutedText, background: chrome.badgeBg, borderRadius: 6, padding: \"2px 7px\" }}>{region.code}{region.side ? \" · \" + region.side : \"\"}</span>\n )}\n {/* F052.26 — the way OUT. Opening a region writes nothing, so\n until now there was no exit that did not write something:\n mark it, or open a different region you did not mean either.\n Always shown, not only when nothing is marked — closing is\n \"stop editing this region\", a different act from removing\n the mark. Both renderers get it: this package has been\n bitten three times today by fixing one half of a pair. */}\n <button\n type=\"button\"\n data-testid=\"bodymap3d-close\"\n aria-label={L.close}\n onClick={() => setSelected(null)}\n style={{ ...btn, width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg }}\n >×</button>\n </div>\n <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: \".06em\", textTransform: \"uppercase\", color: chrome.mutedText, marginBottom: 7 }}>{L.intensity}</div>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4, marginBottom: 12 }}>\n {Array.from({ length: 11 }, (_, i) => (\n <button key={i} data-testid={`bodymap3d-intensity-${i}`} onClick={() => setPain(region.key, i, current?.type)} style={{ ...btn, display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ? \"#0e8f8a\" : \"#fff\", color: current?.intensity === i ? \"#fff\" : \"#1e293b\" }}>{i}</button>\n ))}\n </div>\n <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: \".06em\", textTransform: \"uppercase\", color: chrome.mutedText, marginBottom: 7 }}>{L.quality}</div>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6, marginBottom: 14 }}>\n {PAIN_TYPES.map((t) => (\n <button key={t} data-testid={`bodymap3d-type-${t}`} onClick={() => setPain(region.key, current?.intensity ?? 5, t)} style={{ ...btn, borderRadius: 999, padding: \"6px 12px\", background: current?.type === t ? \"#1e293b\" : \"#fff\", color: current?.type === t ? \"#fff\" : \"#64748b\" }}>{L.qualities[t] ?? t}</button>\n ))}\n </div>\n {current && <button data-testid=\"bodymap3d-remove\" onClick={() => removePain(region.key)} style={{ ...btn, color: chrome.danger, borderColor: \"#f6c9c9\" }}>{L.remove}</button>}\n </div>\n ) : showEmptyHint ? (\n <div data-testid=\"bodymap3d-empty\" style={{ border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg, color: chrome.mutedText, fontSize: 13.5 }}>{UI.hoverHint}</div>\n ) : null}\n </div>\n </div>\n </div>\n );\n}\n\n/** Re-export for convenience so a 3D-only consumer can serialize without a second import. */\nexport { serializeReport };\n"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/react.tsx","../src/three.tsx"],"names":["useRef","useState","useEffect","jsxs","jsx"],"mappings":"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","file":"three.js","sourcesContent":["// @broberg/bodymap — headless core (F052.1).\n//\n// Framework-neutral: the region taxonomy + the PainReport data model (zod) + a\n// selection engine + per-app region config. NO React/Preact/DOM import — the 2D\n// (SVG) and 3D (Three.js) renderers, and all three FD apps, share this one\n// contract. Output is a structured PainReport, never a bare image.\n\nimport { z } from \"zod\";\n\nexport type Side = \"left\" | \"right\";\n\nexport interface BodyRegion {\n /** Stable, unique identifier (snake_case) — the key used in a PainReport. */\n key: string;\n /** Human label (Danish). */\n label: string;\n /** Clinical short code (unique). */\n code: string;\n /** Body side, when the region is paired. */\n side?: Side;\n}\n\n/** The canonical body regions — the AUTHORITATIVE fd-sundhed clinical taxonomy\n * (docs/BODYMAP-TAKSONOMI.md, broberg-ai/fd-sundhed @360842f): 18 SIDE-LESS\n * clinical codes + a separate `side` field (L/R on limbs, C=center on axis\n * regions). The `key` is a unique per-side identifier; `code` is the side-less\n * clinical code that goes on the bodymap/v1 wire. NOT an anatomical atlas —\n * ~30 named surface regions for a pain-map. The 2D front renderer draws the\n * front-visible subset; the 3D body (F052.6) drives them all. */\nexport const REGIONS: readonly BodyRegion[] = [\n // axis / centre-line (serialised side \"center\")\n { key: \"head\", label: \"Hoved\", code: \"HEAD\" },\n { key: \"neck\", label: \"Nakke\", code: \"NECK\" },\n { key: \"chest\", label: \"Bryst\", code: \"CHEST\" },\n { key: \"thora\", label: \"Øvre ryg (thorakal)\", code: \"THORA\" },\n { key: \"lumbar\", label: \"Lænd (lumbal)\", code: \"LUMBAR\" },\n { key: \"groin\", label: \"Lyske\", code: \"GROIN\" },\n // paired limbs / sides (L / R)\n { key: \"shoulder_left\", label: \"Skulder, venstre\", code: \"SHOULDER\", side: \"left\" },\n { key: \"shoulder_right\", label: \"Skulder, højre\", code: \"SHOULDER\", side: \"right\" },\n { key: \"uarm_left\", label: \"Overarm, venstre\", code: \"UARM\", side: \"left\" },\n { key: \"uarm_right\", label: \"Overarm, højre\", code: \"UARM\", side: \"right\" },\n { key: \"elbow_left\", label: \"Albue, venstre\", code: \"ELBOW\", side: \"left\" },\n { key: \"elbow_right\", label: \"Albue, højre\", code: \"ELBOW\", side: \"right\" },\n { key: \"farm_left\", label: \"Underarm, venstre\", code: \"FARM\", side: \"left\" },\n { key: \"farm_right\", label: \"Underarm, højre\", code: \"FARM\", side: \"right\" },\n { key: \"wrist_left\", label: \"Håndled, venstre\", code: \"WRIST\", side: \"left\" },\n { key: \"wrist_right\", label: \"Håndled, højre\", code: \"WRIST\", side: \"right\" },\n { key: \"hand_left\", label: \"Hånd, venstre\", code: \"HAND\", side: \"left\" },\n { key: \"hand_right\", label: \"Hånd, højre\", code: \"HAND\", side: \"right\" },\n { key: \"hip_left\", label: \"Hofte, venstre\", code: \"HIP\", side: \"left\" },\n { key: \"hip_right\", label: \"Hofte, højre\", code: \"HIP\", side: \"right\" },\n { key: \"thigh_left\", label: \"Lår, venstre\", code: \"THIGH\", side: \"left\" },\n { key: \"thigh_right\", label: \"Lår, højre\", code: \"THIGH\", side: \"right\" },\n { key: \"knee_left\", label: \"Knæ, venstre\", code: \"KNEE\", side: \"left\" },\n { key: \"knee_right\", label: \"Knæ, højre\", code: \"KNEE\", side: \"right\" },\n { key: \"lowleg_left\", label: \"Underben, venstre\", code: \"LOWLEG\", side: \"left\" },\n { key: \"lowleg_right\", label: \"Underben, højre\", code: \"LOWLEG\", side: \"right\" },\n { key: \"ankle_left\", label: \"Ankel, venstre\", code: \"ANKLE\", side: \"left\" },\n { key: \"ankle_right\", label: \"Ankel, højre\", code: \"ANKLE\", side: \"right\" },\n { key: \"foot_left\", label: \"Fod, venstre\", code: \"FOOT\", side: \"left\" },\n { key: \"foot_right\", label: \"Fod, højre\", code: \"FOOT\", side: \"right\" },\n];\n\nconst REGION_KEY_SET = new Set(REGIONS.map((r) => r.key));\nexport const REGION_KEYS: readonly string[] = REGIONS.map((r) => r.key);\n\n/** Look up a region by its key. */\nexport function getRegion(key: string): BodyRegion | undefined {\n return REGIONS.find((r) => r.key === key);\n}\n\n// ---- PainReport model ---------------------------------------------------\n\nexport const PAIN_TYPES = [\"stikkende\", \"dump\", \"konstant\", \"jagende\"] as const;\nexport type PainType = (typeof PAIN_TYPES)[number];\n\n/** One marked pain point. `region` MUST be a known region key; `intensity` is a\n * 0-10 integer; `type` is optional but constrained; `timestamp` is an ISO string. */\nexport const painPointSchema = z.object({\n region: z.string().refine((k) => REGION_KEY_SET.has(k), { message: \"unknown region\" }),\n intensity: z.number().int().min(0).max(10),\n type: z.enum(PAIN_TYPES).optional(),\n timestamp: z.string(),\n});\nexport type PainPoint = z.infer<typeof painPointSchema>;\n\nexport const painReportSchema = z.array(painPointSchema);\nexport type PainReport = PainPoint[];\n\n// ---- Selection engine (framework-agnostic) ------------------------------\n\nexport interface PainSelection {\n /** Mark (or update) pain on a region. One point per region — latest wins. */\n set(region: string, intensity: number, type?: PainType): PainPoint;\n remove(region: string): boolean;\n get(region: string): PainPoint | undefined;\n has(region: string): boolean;\n clear(): void;\n /** The current, validated PainReport. */\n getReport(): PainReport;\n}\n\nexport interface PainSelectionOptions {\n /** Injectable clock (ISO string) — defaults to `new Date().toISOString()`. */\n now?: () => string;\n}\n\n/** Create a selection engine seeded with an optional report. Pure state — no\n * DOM, no framework, no network. One point per region. */\nexport function createPainSelection(\n initial: PainReport = [],\n opts: PainSelectionOptions = {},\n): PainSelection {\n const now = opts.now ?? (() => new Date().toISOString());\n const map = new Map<string, PainPoint>();\n for (const p of initial) {\n const v = painPointSchema.parse(p);\n map.set(v.region, v);\n }\n return {\n set(region, intensity, type) {\n const point = painPointSchema.parse({ region, intensity, type, timestamp: now() });\n map.set(point.region, point);\n return point;\n },\n remove: (region) => map.delete(region),\n get: (region) => map.get(region),\n has: (region) => map.has(region),\n clear: () => map.clear(),\n getReport: () => painReportSchema.parse(Array.from(map.values())),\n };\n}\n\n// ---- Per-app region config (the toggle) ---------------------------------\n\nexport interface RegionSetting {\n /** Render this region at all. Default true. */\n visible?: boolean;\n /** Allow marking pain on this region. Default true. */\n selectable?: boolean;\n}\n\n/** Per-app config keyed by region key. An absent key ⇒ visible + selectable. */\nexport type RegionConfig = Record<string, RegionSetting>;\n\n/** The regions an app should render, honouring `visible` (default true). */\nexport function resolveRegions(config: RegionConfig = {}): BodyRegion[] {\n return REGIONS.filter((r) => config[r.key]?.visible ?? true);\n}\n\n/**\n * What a pick on a region should DO (F052.20).\n *\n * Lives in the core because the 2D and 3D renderers share no click code, and a\n * rule written twice is a rule that drifts. This repo measured the cost of that\n * twice on 2026-08-28 alone: a fix applied to one half of a pair, and a sibling\n * branch that carried the same defect with no test on it.\n *\n * \"clear\" the region is already marked → picking it again removes the mark\n * \"select\" unmarked → open it for marking\n * \"ignore\" not selectable (read-only or config) → nothing happens\n *\n * Three outcomes, not a boolean: \"nothing happened because it is locked\" and\n * \"nothing happened because we removed the mark\" must never look alike to a\n * caller.\n */\nexport type PickOutcome = \"clear\" | \"select\" | \"ignore\";\n\nexport function decidePick(\n region: string,\n report: PainReport,\n config: RegionConfig = {},\n): PickOutcome {\n if (!isSelectable(region, config)) return \"ignore\";\n return report.some((p) => p.region === region) ? \"clear\" : \"select\";\n}\n\n/** Whether a region may be marked. A hidden region is never selectable. */\nexport function isSelectable(key: string, config: RegionConfig = {}): boolean {\n const s = config[key];\n if (s?.visible === false) return false;\n return s?.selectable ?? true;\n}\n\n// ---- feedback signal: sound + haptics (F052.22) -----------------------------\n\n/**\n * What a pick did, handed to the consuming app so it can make a sound or a buzz.\n *\n * The outcome is the one `decidePick` ACTUALLY returned, never the intent to tap\n * — so a tap on a locked region can not announce itself as a removal, and a tap\n * the pan/pinch guard swallowed emits nothing at all (it never gets here).\n *\n * It deliberately reuses `PickOutcome` rather than introducing a second\n * three-word vocabulary. Two enums meaning the same thing is a drift bug waiting\n * for the first person who adds a fourth outcome to only one of them.\n */\nexport interface FeedbackSignal {\n outcome: PickOutcome;\n /** The region key that was picked. */\n region: string;\n}\n\nexport type FeedbackFn = (signal: FeedbackSignal) => void;\n\n/**\n * What the BROWSER did with a vibration request.\n *\n * ⚠️ `requested` does NOT mean the phone buzzed. `navigator.vibrate()` returns\n * true for a request it accepted, and all of these accept it and then produce\n * nothing: silent / do-not-disturb mode, a device with no vibration motor (most\n * laptops), a page that has not yet had a qualifying user gesture.\n *\n * Same lesson `@broberg/webpush` 0.3.1 recorded — a push that provably ARRIVED\n * on a device that never SHOWED it. \"Accepted\" and \"happened\" are two claims,\n * and only one of them is observable from here.\n */\nexport type VibrateOutcome = \"unsupported\" | \"skipped\" | \"declined\" | \"requested\";\n\n/**\n * The buzz for each outcome.\n *\n * `ignore` is empty ON PURPOSE: a tap that changed nothing must not feel like it\n * changed something. That is the whole reason this is keyed by outcome and not\n * fired from the tap handler.\n */\nexport const VIBRATION_PATTERNS: Record<PickOutcome, readonly number[]> = {\n select: [12],\n clear: [8, 40, 8],\n ignore: [],\n};\n\ninterface Vibrator {\n vibrate?: (pattern: number | number[]) => boolean;\n}\n\n/**\n * Ask the browser to vibrate. Never throws, never claims delivery.\n *\n * `nav` is injectable so a test can supply all four cases; it defaults to the\n * real `navigator` and is `unsupported` when there is not one (SSR, and every\n * browser on iPhone — WebKit has no `vibrate` at all).\n */\nexport function requestVibration(\n pattern: readonly number[],\n nav: Vibrator | undefined = (globalThis as { navigator?: Vibrator }).navigator,\n): VibrateOutcome {\n if (pattern.length === 0) return \"skipped\";\n if (typeof nav?.vibrate !== \"function\") return \"unsupported\";\n try {\n return nav.vibrate([...pattern]) ? \"requested\" : \"declined\";\n } catch {\n // Embedded webviews and cross-origin iframes throw rather than return false.\n // A refused buzz must never take the pain report down with it.\n return \"declined\";\n }\n}\n\nexport interface FeedbackOptions {\n onFeedback?: FeedbackFn;\n /** Web vibration on select/clear. Default true; inert where the API is absent. */\n haptics?: boolean;\n /** Injectable for tests. */\n nav?: unknown;\n}\n\n/**\n * Emit one pick's feedback: always the signal, optionally the buzz.\n *\n * Lives in the core because the 2D and 3D renderers share no click code, and a\n * rule written twice is a rule that drifts — the same reason `decidePick` is\n * here (F052.20).\n *\n * Sound is NOT here and never will be: `@broberg/soundkit` already exists, and\n * pulling Web Audio into a component that is often rendered read-only (a\n * journal, a PDF, a clinician view) is a cost every consumer would pay for a\n * feature most will not switch on. Wire `onFeedback` to it in four lines.\n */\nexport function emitFeedback(\n outcome: PickOutcome,\n region: string,\n opts: FeedbackOptions = {},\n): VibrateOutcome {\n opts.onFeedback?.({ outcome, region });\n if (opts.haptics === false) return \"skipped\";\n // `undefined` falls through to requestVibration's own default (the real\n // navigator) — so an omitted `nav` and a passed-in one take the same path.\n return requestVibration(VIBRATION_PATTERNS[outcome], opts.nav as Vibrator | undefined);\n}\n\n// ---- palette (consumer-defined colours — shared by the 2D + 3D renderers) ---\n\n/** Colour control for the body renderers. Consumers pass a palette to theme the\n * body base colour, the hover + selected highlights, the pain-heat colours, and\n * optional per-region base colours. All values are CSS/hex colour strings. */\nexport interface BodymapPalette {\n /** Base body colour (an unmarked region). */\n body: string;\n /** Region highlight on hover (before click). */\n hover: string;\n /** A region selected (clicked) but not yet given an intensity. */\n selected: string;\n /** Pain-intensity heat colours: low (0-3), mid (4-6), high (7-10). */\n heat: { low: string; mid: string; high: string };\n /** Optional per-region base-colour overrides (region key → colour). */\n regions?: Record<string, string>;\n /**\n * Optional panel-chrome colours (the selection panel, labels, hint box) —\n * NOT the body itself. All optional; anything omitted falls back to\n * {@link defaultUi}. A palette that only themed the body was half a palette:\n * a consumer passing brand colours still got hardcoded chrome. (F052.19)\n */\n ui?: BodymapUiColors;\n}\n\n/** Panel-chrome colours. Every default is WCAG-AA (>=4.5:1) on its own background. */\nexport interface BodymapUiColors {\n /** Primary text (headings, values). */\n text?: string;\n /** Secondary text — section labels, the empty-state hint. */\n mutedText?: string;\n /** Panel background — the card, its buttons, the empty hint. */\n panelBg?: string;\n /**\n * THE STAGE: the fullscreen backdrop, the canvas frame, and the three.js\n * scene behind the body. A different surface from `panelBg` with an opposite\n * need — see F052.29, where one field paints both and no value is right for\n * either.\n *\n * 3D only. The 2D renderer has no canvas and no fullscreen, so it ignores\n * this; it is declared here because a stage is a shared idea, not because\n * every renderer has one.\n *\n * Defaults to {@link STAGE_BG}. Pass `'#fff'` for the pre-0.8.0 white\n * fullscreen backdrop.\n */\n stageBg?: string;\n /** Panel + control borders. */\n border?: string;\n /** Background behind the region-code badge. */\n badgeBg?: string;\n /** The destructive action (remove a marked region). */\n danger?: string;\n /** Border on the destructive control. Its TEXT was already themeable and its\n * border was not — half a control. (F052.30) */\n dangerBorder?: string;\n /**\n * THE ACTIVE CONTROL colour — the chosen intensity, the active sex toggle.\n *\n * NOT `palette.selected`. That is the colour of a MARK ON SKIN; this is the\n * colour of an active button. Collapsing them would stop a consumer having a\n * teal mark and a navy button, which is a normal thing to want. (F052.30)\n */\n accent?: string;\n /** Text ON the accent. Check it against your accent — see the README. */\n accentText?: string;\n}\n\n/**\n * Default panel chrome. Contrast against `panelBg` (#fff), asserted by\n * `test/contrast.test.ts`:\n * text #1e293b 14.8:1\n * mutedText #475569 7.6:1 (was #94a3b8 at 2.56:1 — WCAG AA failure)\n * danger #dc2626 4.8:1 (was #ef4444 at 3.76:1 — WCAG AA failure)\n * `mutedText` on `badgeBg` (#f1f5f9) is 6.9:1 (was #64748b at 4.34:1).\n */\n/**\n * The stage colour — the ONE place this value lives.\n *\n * It used to be a literal in three places in `three.tsx` (the three.js scene,\n * the canvas frame, the unsupported placeholder), which meant a consumer could\n * recolour the backdrop and still get OUR navy inside it. That seam is\n * invisible on a desktop and obvious on a phone. (F052.29)\n */\nexport const STAGE_BG = \"#0e1424\";\n\nexport const defaultUi: Required<BodymapUiColors> = {\n text: \"#1e293b\",\n mutedText: \"#475569\",\n panelBg: \"#fff\",\n stageBg: STAGE_BG,\n border: \"#e2e8f0\",\n badgeBg: \"#f1f5f9\",\n danger: \"#dc2626\",\n dangerBorder: \"#f6c9c9\",\n // F052.30 — was #0e8f8a, which measured 3.95:1 against white. AA needs 4.5,\n // and this is the SELECTED intensity button on a surface where AA is a legal\n // duty. It shipped from 0.2.0 and no check could see it: the colour was an\n // inline literal, and the F052.19 contrast sweep only read this object.\n // #0c7d77 is the same teal a shade deeper — 4.98:1, with headroom rather than\n // sitting on the 4.5 boundary where any later tweak reopens it.\n accent: \"#0c7d77\",\n accentText: \"#fff\",\n};\n\n/** Resolve a palette's chrome colours, filling every gap from {@link defaultUi}. */\nexport function uiColors(palette?: BodymapPalette): Required<BodymapUiColors> {\n return { ...defaultUi, ...(palette?.ui ?? {}) };\n}\n\n/** The fleet default palette. Override any field per consumer. */\nexport const defaultPalette: BodymapPalette = {\n body: \"#d2d7de\",\n hover: \"#8fd0cd\",\n selected: \"#5cc4b7\",\n heat: { low: \"#fcd34d\", mid: \"#fb923c\", high: \"#ef4444\" },\n};\n\n/** The heat colour for a pain intensity, honouring the palette. */\nexport function heatFor(intensity: number, palette: BodymapPalette = defaultPalette): string {\n return intensity >= 7 ? palette.heat.high : intensity >= 4 ? palette.heat.mid : palette.heat.low;\n}\n\n/** The base colour for a region (a per-region override, else the body colour). */\nexport function baseColorFor(regionKey: string, palette: BodymapPalette = defaultPalette): string {\n return palette.regions?.[regionKey] ?? palette.body;\n}\n\n// ---- bodymap/v1 serialization (the shared cross-app / native wire format) ---\n//\n// The shape every consumer + the native mobile apps read (aligned with\n// fd-sundhed's bodymap/v1: region CODE + side + intensity + quality + view).\n// The internal PainReport keys on the region KEY; this maps key -> clinical CODE\n// so the report is portable and human-readable on the wire.\n\nexport type BodyView = \"front\" | \"back\" | \"left\" | \"right\";\n/** Side in the serialized report — a midline region (no side) becomes \"center\". */\nexport type SerializedSide = \"left\" | \"right\" | \"center\";\n\nexport interface SerializedPainPoint {\n /** Clinical region CODE (e.g. \"LUMB\"). */\n region: string;\n side: SerializedSide;\n intensity: number;\n quality?: PainType;\n}\n\nexport interface BodymapReportV1 {\n schema: \"bodymap/v1\";\n view: BodyView;\n points: SerializedPainPoint[];\n}\n\nexport const bodymapReportV1Schema = z.object({\n schema: z.literal(\"bodymap/v1\"),\n view: z.enum([\"front\", \"back\", \"left\", \"right\"]),\n points: z.array(\n z.object({\n region: z.string(),\n side: z.enum([\"left\", \"right\", \"center\"]),\n intensity: z.number().int().min(0).max(10),\n quality: z.enum(PAIN_TYPES).optional(),\n }),\n ),\n});\n\n/** Serialize a PainReport to the shared `bodymap/v1` wire format. */\nexport function serializeReport(\n report: PainReport,\n opts: { view?: BodyView } = {},\n): BodymapReportV1 {\n return {\n schema: \"bodymap/v1\",\n view: opts.view ?? \"front\",\n points: report.map((p) => {\n const r = getRegion(p.region);\n return {\n region: r?.code ?? p.region,\n side: (r?.side ?? \"center\") as SerializedSide,\n intensity: p.intensity,\n quality: p.type,\n };\n }),\n };\n}\n\n/** Parse a `bodymap/v1` report back into an internal PainReport. Region CODE →\n * key; a point whose code is unknown to this taxonomy is dropped. */\nexport function deserializeReport(\n env: unknown,\n now: () => string = () => new Date().toISOString(),\n): PainReport {\n const parsed = bodymapReportV1Schema.parse(env);\n // code is side-less, so a point is identified by code + side.\n const byCodeSide = new Map(\n REGIONS.map((r) => [`${r.code}|${r.side ?? \"center\"}`, r.key] as const),\n );\n const out: PainReport = [];\n for (const sp of parsed.points) {\n const key = byCodeSide.get(`${sp.region}|${sp.side}`);\n if (!key) continue;\n out.push(\n painPointSchema.parse({\n region: key,\n intensity: sp.intensity,\n type: sp.quality,\n timestamp: now(),\n }),\n );\n }\n return out;\n}\n","// @broberg/bodymap/react — the 2D SVG renderer (F052.2).\n//\n// <BodyMap> is the shippable MVP: a genderless, colour-blocked body a patient\n// clicks to mark where it hurts. Selecting a region opens a custom intensity\n// (0-10) + quality control (NO native inputs) that upserts a validated\n// PainReport and fires onChange. Honours the per-app RegionConfig. The 3D\n// <BodyMap3D> (F052.6) will render the SAME core on the SAME onChange contract.\n//\n// F052.8 touch-first + F052.11 a11y: the body is PRIMARILY a mobile surface, so\n// every region has a >=44px transparent hit-area, the body pinch-zooms + pans\n// (with a tap-vs-drag guard so a gesture never mis-marks), controls are\n// thumb-sized and the layout stacks on a narrow viewport. Everything is also\n// fully keyboard-operable with live-state ARIA labels + visible focus.\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n REGIONS,\n PAIN_TYPES,\n painPointSchema,\n resolveRegions,\n isSelectable,\n decidePick,\n emitFeedback,\n type FeedbackFn,\n getRegion,\n heatFor,\n baseColorFor,\n type BodyRegion,\n type BodymapPalette,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\n\n// ---- geometry: each region → an SVG shape (a neutral colour-blockout) --------\n\ntype Shape =\n | { el: \"circle\"; cx: number; cy: number; r: number }\n | { el: \"ellipse\"; cx: number; cy: number; rx: number; ry: number }\n | { el: \"rect\"; x: number; y: number; width: number; height: number; rx: number };\n\n// Front-view geometry (viewBox 260×470). The 2D MVP renders the front-visible\n// regions; back-only regions (thora, lumbar) + hip are driven by the 3D body /\n// a later back view. \"left\"/\"right\" are placed self-view (viewer's left = key\n// left) for the MVP — the facing-the-patient mirror is a later config option.\nconst SHAPES: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n chest: { el: \"rect\", x: 100, y: 84, width: 60, height: 104, rx: 16 },\n groin: { el: \"rect\", x: 106, y: 190, width: 48, height: 30, rx: 13 },\n // left arm (viewer-left)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n // right arm (viewer-right)\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL: Record<string, string> = {\n head: \"#cbb7ec\", neck: \"#b8a3e6\", chest: \"#e26d6d\", groin: \"#e88a52\",\n shoulder_left: \"#f6c667\", shoulder_right: \"#f6c667\",\n uarm_left: \"#7ececb\", uarm_right: \"#7ececb\",\n elbow_left: \"#86cfcc\", elbow_right: \"#86cfcc\",\n farm_left: \"#8fd0cd\", farm_right: \"#8fd0cd\",\n wrist_left: \"#9ad6d3\", wrist_right: \"#9ad6d3\",\n hand_left: \"#a7ddda\", hand_right: \"#a7ddda\",\n thigh_left: \"#7fb2e8\", thigh_right: \"#7fb2e8\",\n knee_left: \"#c79be0\", knee_right: \"#c79be0\",\n lowleg_left: \"#e39ec6\", lowleg_right: \"#e39ec6\",\n ankle_left: \"#eeaab9\", ankle_right: \"#eeaab9\",\n foot_left: \"#f0a5a5\", foot_right: \"#f0a5a5\",\n};\n\n// Back-view geometry (same viewBox 260×470 + same limb/head geometry as the\n// front, so a marked limb sits at the same spot in both views). The torso swaps\n// front regions (chest, groin) for the posterior ones — THORA (upper back),\n// LUMBAR (lower back) and HIP (buttocks, paired) — the pilot's blocker regions.\n// Same self-view L/R convention as the front (patient-left = viewer-left).\nconst SHAPES_BACK: Record<string, Shape> = {\n head: { el: \"circle\", cx: 130, cy: 40, r: 26 },\n neck: { el: \"rect\", x: 120, y: 62, width: 20, height: 16, rx: 6 },\n thora: { el: \"rect\", x: 100, y: 84, width: 60, height: 58, rx: 15 },\n lumbar: { el: \"rect\", x: 104, y: 144, width: 52, height: 44, rx: 13 },\n hip_left: { el: \"rect\", x: 100, y: 190, width: 27, height: 32, rx: 13 },\n hip_right: { el: \"rect\", x: 133, y: 190, width: 27, height: 32, rx: 13 },\n // arms — shared with the front (back of the arm sits at the same x/y)\n shoulder_left: { el: \"circle\", cx: 88, cy: 98, r: 14 },\n uarm_left: { el: \"rect\", x: 66, y: 104, width: 18, height: 54, rx: 9 },\n elbow_left: { el: \"circle\", cx: 73, cy: 164, r: 10 },\n farm_left: { el: \"rect\", x: 62, y: 174, width: 16, height: 50, rx: 8 },\n wrist_left: { el: \"circle\", cx: 68, cy: 230, r: 8 },\n hand_left: { el: \"ellipse\", cx: 67, cy: 250, rx: 11, ry: 13 },\n shoulder_right: { el: \"circle\", cx: 172, cy: 98, r: 14 },\n uarm_right: { el: \"rect\", x: 176, y: 104, width: 18, height: 54, rx: 9 },\n elbow_right: { el: \"circle\", cx: 187, cy: 164, r: 10 },\n farm_right: { el: \"rect\", x: 182, y: 174, width: 16, height: 50, rx: 8 },\n wrist_right: { el: \"circle\", cx: 192, cy: 230, r: 8 },\n hand_right: { el: \"ellipse\", cx: 193, cy: 250, rx: 11, ry: 13 },\n // legs — back (thigh→calf→heel, same x/y as the front)\n thigh_left: { el: \"rect\", x: 104, y: 224, width: 22, height: 80, rx: 11 },\n thigh_right: { el: \"rect\", x: 134, y: 224, width: 22, height: 80, rx: 11 },\n knee_left: { el: \"circle\", cx: 115, cy: 312, r: 13 },\n knee_right: { el: \"circle\", cx: 145, cy: 312, r: 13 },\n lowleg_left: { el: \"rect\", x: 106, y: 322, width: 18, height: 74, rx: 9 },\n lowleg_right: { el: \"rect\", x: 136, y: 322, width: 18, height: 74, rx: 9 },\n ankle_left: { el: \"circle\", cx: 115, cy: 404, r: 9 },\n ankle_right: { el: \"circle\", cx: 145, cy: 404, r: 9 },\n foot_left: { el: \"ellipse\", cx: 115, cy: 424, rx: 14, ry: 10 },\n foot_right: { el: \"ellipse\", cx: 145, cy: 424, rx: 14, ry: 10 },\n};\n\nconst FILL_BACK: Record<string, string> = {\n ...FILL,\n thora: \"#e2896d\",\n lumbar: \"#d97fa0\",\n hip_left: \"#c99bd6\", hip_right: \"#c99bd6\",\n};\n\nexport type BodyView2D = \"front\" | \"back\";\n\n// ---- i18n (F052.12) — every UI string is overridable; da + en ship ----------\n// Region CODE + keys + the bodymap/v1 wire are NEVER touched — labels are\n// display-only, so a report is byte-identical in any language.\n\nexport interface BodyMapLabels {\n /** Region key → display name. */\n regions: Record<string, string>;\n /** PainType → display word (e.g. \"stikkende\" → \"Stabbing\"). */\n qualities: Record<string, string>;\n intensity: string;\n quality: string;\n remove: string;\n /** Close the picker without marking anything (F052.26). */\n close: string;\n front: string;\n back: string;\n empty: string;\n /** Compare view (F052.13). */\n before: string;\n after: string;\n noChange: string;\n change: { new: string; resolved: string; improved: string; worse: string };\n /** Accessible name for a marked region. */\n ariaMarked: (name: string, intensity: number, quality?: string) => string;\n /** Accessible name for an unmarked, selectable region. */\n ariaUnmarked: (name: string) => string;\n svgLabel: string;\n viewLabel: string;\n zoomLabel: string;\n zoomIn: string;\n zoomOut: string;\n zoomReset: string;\n}\n\nconst daRegions: Record<string, string> = Object.fromEntries(REGIONS.map((r) => [r.key, r.label]));\nconst EN_CODE: Record<string, string> = {\n HEAD: \"Head\", NECK: \"Neck\", CHEST: \"Chest\", THORA: \"Upper back\", LUMBAR: \"Lower back\", GROIN: \"Groin\",\n SHOULDER: \"Shoulder\", UARM: \"Upper arm\", ELBOW: \"Elbow\", FARM: \"Forearm\", WRIST: \"Wrist\", HAND: \"Hand\",\n HIP: \"Hip\", THIGH: \"Thigh\", KNEE: \"Knee\", LOWLEG: \"Lower leg\", ANKLE: \"Ankle\", FOOT: \"Foot\",\n};\nconst enRegions: Record<string, string> = Object.fromEntries(\n REGIONS.map((r) => [r.key, EN_CODE[r.code] + (r.side ? (r.side === \"left\" ? \", left\" : \", right\") : \"\")]),\n);\n\nexport const LABELS_DA: BodyMapLabels = {\n regions: daRegions,\n qualities: { stikkende: \"stikkende\", dump: \"dump\", konstant: \"konstant\", jagende: \"jagende\" },\n intensity: \"Intensitet (0-10)\",\n quality: \"Kvalitet\",\n remove: \"Fjern punkt\",\n close: \"Luk\",\n front: \"Forfra\",\n back: \"Bagfra\",\n empty: \"Vælg en kropsdel for at markere smerte.\",\n before: \"Før\",\n after: \"Efter\",\n noChange: \"Ingen ændring\",\n change: { new: \"nyt\", resolved: \"forsvundet\", improved: \"bedre\", worse: \"værre\" },\n ariaMarked: (n, i, q) => `${n}, smerte ${i} af 10${q ? \", \" + q : \"\"}`,\n ariaUnmarked: (n) => `${n}, ikke markeret. Aktivér for at markere smerte.`,\n svgLabel: \"Kropskort — vælg hvor det gør ondt\",\n viewLabel: \"Visning\",\n zoomLabel: \"Zoom\",\n zoomIn: \"Zoom ind\",\n zoomOut: \"Zoom ud\",\n zoomReset: \"Nulstil zoom\",\n};\n\nexport const LABELS_EN: BodyMapLabels = {\n regions: enRegions,\n qualities: { stikkende: \"stabbing\", dump: \"dull\", konstant: \"constant\", jagende: \"shooting\" },\n intensity: \"Intensity (0-10)\",\n quality: \"Quality\",\n remove: \"Remove point\",\n close: \"Close\",\n front: \"Front\",\n back: \"Back\",\n empty: \"Pick a body part to mark pain.\",\n before: \"Before\",\n after: \"After\",\n noChange: \"No change\",\n change: { new: \"new\", resolved: \"resolved\", improved: \"improved\", worse: \"worse\" },\n ariaMarked: (n, i, q) => `${n}, pain ${i} of 10${q ? \", \" + q : \"\"}`,\n ariaUnmarked: (n) => `${n}, not marked. Activate to mark pain.`,\n svgLabel: \"Body map — pick where it hurts\",\n viewLabel: \"View\",\n zoomLabel: \"Zoom\",\n zoomIn: \"Zoom in\",\n zoomOut: \"Zoom out\",\n zoomReset: \"Reset zoom\",\n};\n\nexport type BodyMapLocale = \"da\" | \"en\";\n\nfunction resolveLabels(locale: BodyMapLocale, overrides?: Partial<BodyMapLabels>): BodyMapLabels {\n const base = locale === \"en\" ? LABELS_EN : LABELS_DA;\n if (!overrides) return base;\n return {\n ...base,\n ...overrides,\n regions: { ...base.regions, ...overrides.regions },\n qualities: { ...base.qualities, ...overrides.qualities },\n };\n}\n\nconst VBW = 260;\nconst VBH = 470;\n// The SVG renders ~320px wide → ~1.23 px per viewBox unit. A hit-area needs\n// ~36 units to clear the 44px touch minimum; small regions get an invisible\n// padded hit-shape at least this big (zoom handles genuinely dense clusters).\nconst MIN_HIT = 18; // half-extent (radius) in viewBox units\n\nfunction center(s: Shape): { x: number; y: number } {\n if (s.el === \"rect\") return { x: s.x + s.width / 2, y: s.y + s.height / 2 };\n return { x: s.cx, y: s.cy };\n}\n// An enlarged, invisible hit target so small regions clear the 44px touch min.\nfunction hitShape(s: Shape): Shape {\n if (s.el === \"circle\") return { el: \"circle\", cx: s.cx, cy: s.cy, r: Math.max(s.r, MIN_HIT) };\n if (s.el === \"ellipse\")\n return { el: \"ellipse\", cx: s.cx, cy: s.cy, rx: Math.max(s.rx, MIN_HIT), ry: Math.max(s.ry, MIN_HIT) };\n const c = center(s);\n const w = Math.max(s.width, MIN_HIT * 2);\n const h = Math.max(s.height, MIN_HIT * 2);\n return { el: \"rect\", x: c.x - w / 2, y: c.y - h / 2, width: w, height: h, rx: s.rx };\n}\nfunction heat(v: number): string {\n return v >= 7 ? \"#ef4444\" : v >= 4 ? \"#fb923c\" : \"#fcd34d\";\n}\nfunction heatTier(v: number): \"low\" | \"mid\" | \"high\" {\n return v >= 7 ? \"high\" : v >= 4 ? \"mid\" : \"low\";\n}\n// F052.10 palette: a `palette` prop wins (direct colour); with no prop, a CSS-var\n// chain (--bmap-region-<key> → --bmap-body) themes with the default rainbow as\n// the fallback. Marked regions likewise: palette.heat or --bmap-heat-<tier>.\nfunction baseFill(key: string, rainbow: string, palette?: BodymapPalette): string {\n return palette ? baseColorFor(key, palette) : `var(--bmap-region-${key}, var(--bmap-body, ${rainbow}))`;\n}\nfunction markedFill(v: number, palette?: BodymapPalette): string {\n return palette ? heatFor(v, palette) : `var(--bmap-heat-${heatTier(v)}, ${heat(v)})`;\n}\nfunction shapeEl(s: Shape, props: Record<string, unknown>) {\n if (s.el === \"circle\") return <circle cx={s.cx} cy={s.cy} r={s.r} {...props} />;\n if (s.el === \"ellipse\") return <ellipse cx={s.cx} cy={s.cy} rx={s.rx} ry={s.ry} {...props} />;\n return <rect x={s.x} y={s.y} width={s.width} height={s.height} rx={s.rx} {...props} />;\n}\n\n// ---- one-time style injection (no external CSS; theme via CSS variables) -----\n\n/**\n * Map a palette onto the root element's CSS custom properties, so a consumer's\n * colours reach the panel chrome and not just the body. Only keys the consumer\n * actually set are emitted — everything else falls through to the stylesheet's\n * (AA-safe) defaults. (F052.19)\n */\nfunction chromeStyle(palette?: BodymapPalette): React.CSSProperties | undefined {\n if (!palette) return undefined;\n const ui = palette.ui ?? {};\n const vars: Record<string, string> = { \"--bmap-accent\": palette.selected };\n if (ui.text) vars[\"--bmap-ink\"] = ui.text;\n if (ui.mutedText) vars[\"--bmap-muted\"] = ui.mutedText;\n if (ui.panelBg) vars[\"--bmap-panel\"] = ui.panelBg;\n if (ui.border) vars[\"--bmap-line\"] = ui.border;\n if (ui.badgeBg) vars[\"--bmap-badge-bg\"] = ui.badgeBg;\n if (ui.danger) vars[\"--bmap-danger\"] = ui.danger;\n return vars as React.CSSProperties;\n}\n\nconst STYLE_ID = \"broberg-bodymap-styles\";\nconst STYLE = `\n.bmap{--bmap-accent:var(--primary,#0c7d77);--bmap-line:#e2e8f0;--bmap-panel:#fff;--bmap-ink:#1e293b;--bmap-muted:#475569;--bmap-badge-bg:#f1f5f9;--bmap-danger:#dc2626;\n display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}\n.bmap__stage{display:flex;flex-direction:column;gap:10px;flex:0 1 340px;min-width:0;max-width:360px}\n.bmap__bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n.bmap__viewtoggle{display:inline-flex;background:#f1f5f9;border:1px solid var(--bmap-line);border-radius:10px;padding:3px;gap:2px}\n.bmap__vbtn{font:inherit;font-size:13px;font-weight:600;color:#556274;background:none;border:0;border-radius:7px;padding:8px 16px;cursor:pointer;transition:color .12s,background .12s,transform .1s}\n.bmap__vbtn:hover{color:var(--bmap-ink)}\n.bmap__vbtn:active{transform:scale(.97)}\n.bmap__vbtn--on{background:#fff;color:var(--bmap-ink);box-shadow:0 1px 2px rgba(15,23,42,.14)}\n.bmap__zoom{display:inline-flex;gap:4px;margin-left:auto}\n.bmap__zoom button{font:inherit;font-weight:700;font-size:16px;line-height:1;width:36px;height:36px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);border-radius:8px;cursor:pointer;transition:border-color .12s,transform .1s;display:flex;align-items:center;justify-content:center}\n.bmap__zoom button:hover{border-color:var(--bmap-accent)}\n.bmap__zoom button:active{transform:scale(.9)}\n.bmap__zoom button:disabled{opacity:.4;cursor:default}\n.bmap__svg{width:100%;height:auto;display:block;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;background:transparent;border-radius:14px}\n.bmap__vis{stroke:#fff;stroke-width:2.4;pointer-events:none;transition:filter .12s,stroke .12s,stroke-width .12s}\n.bmap__vis--sel{stroke:var(--bmap-accent);stroke-width:3.6}\n.bmap__hit{fill:transparent;cursor:pointer;outline:none}\n.bmap__hit--locked{cursor:default}\n.bmap__hit:hover + .bmap__vis{filter:brightness(1.08) saturate(1.15)}\n.bmap__hit:focus-visible + .bmap__vis{stroke:var(--bmap-accent);stroke-width:4;stroke-dasharray:4 2.5}\n.bmap__heatn{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}\n.bmap__panel{flex:1 1 240px;min-width:220px;border:1px solid var(--bmap-line);border-radius:14px;padding:16px 18px;background:var(--bmap-panel)}\n.bmap__panel--empty{color:var(--bmap-muted);font-size:13.5px}\n.bmap__ph{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}\n.bmap__ph b{font-size:16px}\n.bmap__close{margin-left:auto;font:inherit;font-size:20px;line-height:1;cursor:pointer;background:none;border:1px solid transparent;border-radius:8px;width:32px;height:32px;color:var(--bmap-muted,#475569)}\n.bmap__close:hover{background:var(--bmap-badge-bg,#f1f5f9)}\n.bmap__close:active{transform:scale(.94)}\n.bmap__close:focus-visible{outline:2px solid var(--bmap-accent,#0c7d77);outline-offset:2px}\n.bmap__code{font:11px ui-monospace,monospace;color:var(--bmap-muted);background:var(--bmap-badge-bg);border-radius:6px;padding:2px 7px}\n.bmap__lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--bmap-muted);margin:0 0 7px}\n.bmap__scale{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px}\n.bmap__i{font:inherit;font-size:14px;font-weight:600;width:38px;height:38px;border-radius:9px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-ink);cursor:pointer;transition:border-color .1s,background .1s,transform .1s}\n.bmap__i:hover{border-color:var(--bmap-accent)}\n.bmap__i:active{transform:scale(.9)}\n.bmap__i--on{background:var(--bmap-accent);border-color:var(--bmap-accent);color:#fff}\n.bmap__chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:14px}\n.bmap__chip{font:inherit;font-size:13px;border:1px solid var(--bmap-line);background:#fff;color:var(--bmap-muted);border-radius:999px;padding:9px 15px;cursor:pointer;transition:.1s}\n.bmap__chip:hover{border-color:var(--bmap-accent);color:var(--bmap-accent)}\n.bmap__chip:active{transform:scale(.96)}\n.bmap__chip--on{background:var(--bmap-ink);border-color:var(--bmap-ink);color:#fff}\n.bmap__rm{font:inherit;font-size:14px;font-weight:600;border:1px solid #f6c9c9;background:#fff;color:var(--bmap-danger);border-radius:9px;padding:10px 15px;cursor:pointer;transition:.1s}\n.bmap__rm:hover{background:#fef2f2}\n.bmap__rm:active{transform:scale(.97)}\n.bmap__i:focus-visible,.bmap__chip:focus-visible,.bmap__vbtn:focus-visible,.bmap__rm:focus-visible,.bmap__zoom button:focus-visible{outline:2px solid var(--bmap-accent);outline-offset:2px}\n.bmapc{--bmap-accent:var(--primary,#0c7d77);--bmap-line:#e2e8f0;--bmap-ink:#1e293b;--bmap-muted:#475569;font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif;color:var(--bmap-ink)}\n.bmapc__bodies{display:flex;gap:14px;flex-wrap:wrap}\n.bmapc__fig{flex:1 1 150px;min-width:130px;max-width:220px;margin:0;text-align:center}\n.bmapc__cap{font-size:12px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#556274;margin-bottom:6px}\n.bmapc__svg{width:100%;height:auto;display:block}\n.bmapc__vis{stroke:#fff;stroke-width:2.4}\n.bmapc__n{font:700 12px ui-sans-serif;fill:#fff;pointer-events:none}\n.bmapc__delta{margin-top:16px;display:flex;flex-direction:column;gap:6px;max-width:460px}\n.bmapc__row{display:flex;align-items:center;gap:10px;font-size:13.5px;border:1px solid var(--bmap-line);border-radius:9px;padding:7px 11px}\n.bmapc__rname{font-weight:600;flex:1 1 auto}\n.bmapc__rval{font:12px ui-monospace,monospace;color:#556274;white-space:nowrap}\n.bmapc__badge{font-size:11px;font-weight:700;border-radius:6px;padding:2px 8px;white-space:nowrap}\n.bmapc__badge--improved{background:#dcfce7;color:#166534}\n.bmapc__badge--worse{background:#fee2e2;color:#991b1b}\n.bmapc__badge--new{background:#fef3c7;color:#92400e}\n.bmapc__badge--resolved{background:#e2e8f0;color:#475569}\n.bmapc__empty{color:#556274;font-size:13.5px;margin-top:14px}\n@media (max-width:560px){\n .bmap{flex-direction:column;gap:14px}\n .bmap__stage{max-width:100%;width:100%;flex-basis:auto}\n .bmap__panel{width:100%}\n .bmap__i{width:46px;height:46px;font-size:16px}\n .bmap__chip{padding:11px 17px;font-size:15px}\n .bmap__vbtn{padding:10px 18px;font-size:15px}\n}\n@media (prefers-reduced-motion:reduce){.bmap *{transition:none !important}}\n`;\n\nfunction ensureStyles(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// ---- component ---------------------------------------------------------------\n\nexport interface BodyMapProps {\n /** Controlled report. Omit for an uncontrolled component (use defaultValue). */\n value?: PainReport;\n /** Initial report when uncontrolled. */\n defaultValue?: PainReport;\n /** Fired with the full validated PainReport on every change. */\n onChange?: (report: PainReport) => void;\n /** Per-app region config (visible / selectable). */\n config?: RegionConfig;\n /** Initial body view (front / back). Default \"front\". */\n defaultView?: BodyView2D;\n /** Fired when the user toggles the front/back view — pass it to\n * serializeReport({ view }) so the wire report carries the active view. */\n onViewChange?: (view: BodyView2D) => void;\n /** Colour theme (F052.10). Overrides the default rainbow: body/hover/selected/\n * heat + optional per-region base colours. Without it, the rainbow is used\n * (themeable via --bmap-body / --bmap-region-<key> / --bmap-heat-* CSS vars). */\n palette?: BodymapPalette;\n /** UI language (F052.12). \"da\" (default) or \"en\". */\n locale?: BodyMapLocale;\n /** Override individual UI strings (region names, headings, quality words, …).\n * Merged over the locale set — region CODE + wire format are never affected. */\n labels?: Partial<BodyMapLabels>;\n /** Display-only mode (F052.9): render a saved report coloured by intensity\n * with NO picker and no interactivity. For journals, PDFs, clinician views. */\n readOnly?: boolean;\n /** Fired after every pick with what ACTUALLY happened (F052.22): \"select\",\n * \"clear\" or \"ignore\". Wire it to a sound (`@broberg/soundkit`) or to native\n * haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an\n * iPhone, where web vibration does not exist). A pick swallowed by the\n * pan/pinch guard fires nothing at all. */\n onFeedback?: FeedbackFn;\n /** Web vibration on select/clear. Default true; silently inert where\n * `navigator.vibrate` is absent (every browser on iPhone, most desktops).\n * Set false to keep the signal but drop the buzz. */\n haptics?: boolean;\n className?: string;\n}\n\nconst ZMIN = 1;\nconst ZMAX = 4;\nconst clampPan = (p: { x: number; y: number }, z: number) => ({\n x: Math.min(0, Math.max(VBW * (1 - z), p.x)),\n y: Math.min(0, Math.max(VBH * (1 - z), p.y)),\n});\n\n/** 2D SVG body pain-map. Click/tap a region → set intensity + quality → PainReport. */\nexport function BodyMap({\n value, defaultValue, onChange, config = {}, defaultView = \"front\", onViewChange,\n palette, locale = \"da\", labels, readOnly = false, onFeedback, haptics, className,\n}: BodyMapProps) {\n useEffect(ensureStyles, []);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const [selected, setSelected] = useState<string | null>(null);\n const [view, setView] = useState<BodyView2D>(defaultView);\n const [zoom, setZoom] = useState(1);\n const [pan, setPan] = useState({ x: 0, y: 0 });\n const report = value ?? internal;\n const shapes = view === \"back\" ? SHAPES_BACK : SHAPES;\n const fills = view === \"back\" ? FILL_BACK : FILL;\n const L = resolveLabels(locale, labels);\n const nameOf = (key: string) => L.regions[key] ?? getRegion(key)?.label ?? key;\n\n const svgRef = useRef<SVGSVGElement>(null);\n const zoomRef = useRef(1);\n const panRef = useRef({ x: 0, y: 0 });\n const pointers = useRef(new Map<number, { x: number; y: number }>());\n const gesture = useRef({ startDist: 0, startZoom: 1, midVB: { x: 0, y: 0 }, start: { x: 0, y: 0 }, moved: false });\n const suppressClick = useRef(false);\n\n const setZP = (z: number, p: { x: number; y: number }) => {\n const cz = Math.max(ZMIN, Math.min(ZMAX, z));\n const cp = clampPan(p, cz);\n zoomRef.current = cz; panRef.current = cp;\n setZoom(cz); setPan(cp);\n };\n const screenToVB = (cx: number, cy: number) => {\n const el = svgRef.current;\n if (!el) return { x: VBW / 2, y: VBH / 2 };\n const r = el.getBoundingClientRect();\n if (!r.width || !r.height) return { x: VBW / 2, y: VBH / 2 };\n return { x: ((cx - r.left) / r.width) * VBW, y: ((cy - r.top) / r.height) * VBH };\n };\n // Zoom keeping the given viewBox focal point under the cursor/fingers.\n const zoomTo = (nz: number, focalVB: { x: number; y: number }) => {\n const z = Math.max(ZMIN, Math.min(ZMAX, nz));\n const p = panRef.current;\n setZP(z, { x: focalVB.x - (focalVB.x - p.x) * (z / zoomRef.current), y: focalVB.y - (focalVB.y - p.y) * (z / zoomRef.current) });\n };\n const zoomBtn = (factor: number) => zoomTo(zoomRef.current * factor, { x: VBW / 2, y: VBH / 2 });\n const resetZoom = () => setZP(1, { x: 0, y: 0 });\n\n const onPointerDown = (e: React.PointerEvent) => {\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n const pts = [...pointers.current.values()];\n if (pts.length === 2) {\n gesture.current.startDist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);\n gesture.current.startZoom = zoomRef.current;\n gesture.current.midVB = screenToVB((pts[0].x + pts[1].x) / 2, (pts[0].y + pts[1].y) / 2);\n gesture.current.moved = true;\n suppressClick.current = true;\n } else {\n gesture.current.start = { x: e.clientX, y: e.clientY };\n gesture.current.moved = false;\n }\n };\n const onPointerMove = (e: React.PointerEvent) => {\n const prev = pointers.current.get(e.pointerId);\n if (!prev) return;\n pointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });\n const pts = [...pointers.current.values()];\n if (pts.length >= 2) {\n const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);\n if (gesture.current.startDist > 0) zoomTo(gesture.current.startZoom * (dist / gesture.current.startDist), gesture.current.midVB);\n return;\n }\n const el = svgRef.current;\n const rect = el?.getBoundingClientRect();\n if (Math.hypot(e.clientX - gesture.current.start.x, e.clientY - gesture.current.start.y) > 6) {\n gesture.current.moved = true;\n suppressClick.current = true;\n }\n if (zoomRef.current > 1 && rect && rect.width) {\n const dxVB = ((e.clientX - prev.x) / rect.width) * VBW;\n const dyVB = ((e.clientY - prev.y) / rect.height) * VBH;\n setZP(zoomRef.current, { x: panRef.current.x + dxVB, y: panRef.current.y + dyVB });\n }\n };\n const onPointerUp = (e: React.PointerEvent) => {\n pointers.current.delete(e.pointerId);\n if (pointers.current.size === 0) window.setTimeout(() => { suppressClick.current = false; }, 60);\n };\n\n const changeView = (v: BodyView2D) => {\n if (v === view) return;\n setView(v);\n const sh = v === \"back\" ? SHAPES_BACK : SHAPES;\n if (selected && !sh[selected]) setSelected(null); // selected region absent in the new view\n onViewChange?.(v);\n };\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n const pointOf = (key: string) => report.find((p) => p.region === key);\n const setPain = (key: string, intensity: number, type?: PainType) => {\n const point = painPointSchema.parse({ region: key, intensity, type, timestamp: new Date().toISOString() });\n commit([...report.filter((p) => p.region !== key), point]);\n };\n const removePain = (key: string) => {\n commit(report.filter((p) => p.region !== key));\n setSelected(null);\n };\n /**\n * F052.20 — ONE decision, two entry points (tap and keyboard).\n *\n * A region that is ALREADY marked is CLEARED by picking it again. Christian's\n * call, 2026-08-28, on fd-sundhed's user asking «hvorfor kan jeg ikke bare\n * trykke på punktet igen og det forsvinder?» — taken with the cost stated:\n * re-picking a marked region used to be the only way to change its intensity,\n * so that now means clear it and mark it again.\n *\n * The «Fjern» button stays. It is the panel path right after marking a fresh\n * point, and it is the labelled control a screen-reader user reaches.\n */\n const applyPick = (key: string) => {\n const what = decidePick(key, report, config);\n // The signal carries the outcome that HAPPENED — emitted for all three,\n // including \"ignore\", so an app can tell \"locked\" from \"nothing to do\".\n // The buzz is chosen by outcome (VIBRATION_PATTERNS.ignore is empty), so a\n // tap that changed nothing never feels like it did.\n emitFeedback(what, key, { onFeedback, haptics });\n if (what === \"ignore\") return;\n if (what === \"clear\") { removePain(key); return; }\n setSelected(key);\n };\n const pickRegion = (key: string) => {\n // A pan/pinch just ended — not a tap. This guard used to protect against\n // opening a panel by accident; it now protects against DELETING a mark.\n if (suppressClick.current) { suppressClick.current = false; return; }\n applyPick(key);\n };\n\n const regions = resolveRegions(config);\n const region: BodyRegion | undefined = selected ? REGIONS.find((r) => r.key === selected) : undefined;\n const current = selected ? pointOf(selected) : undefined;\n const rootCls = [\"bmap\", className].filter(Boolean).join(\" \");\n // palette.selected drives the focus/selected stroke via --bmap-accent;\n // palette.ui drives the panel chrome (F052.19).\n const rootStyle = chromeStyle(palette);\n const ariaFor = (r: BodyRegion, marked?: PainReport[number], interactive = true): string => {\n if (marked) return L.ariaMarked(nameOf(r.key), marked.intensity, marked.type ? L.qualities[marked.type] : undefined);\n return interactive ? L.ariaUnmarked(nameOf(r.key)) : nameOf(r.key);\n };\n\n return (\n <div className={rootCls} data-testid=\"bodymap-root\" style={rootStyle}>\n <div className=\"bmap__stage\">\n <div className=\"bmap__bar\">\n <div className=\"bmap__viewtoggle\" role=\"group\" aria-label={L.viewLabel}>\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"front\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-front\"\n aria-pressed={view === \"front\"}\n onClick={() => changeView(\"front\")}\n >\n {L.front}\n </button>\n <button\n type=\"button\"\n className={\"bmap__vbtn\" + (view === \"back\" ? \" bmap__vbtn--on\" : \"\")}\n data-testid=\"bodymap-view-back\"\n aria-pressed={view === \"back\"}\n onClick={() => changeView(\"back\")}\n >\n {L.back}\n </button>\n </div>\n <div className=\"bmap__zoom\" role=\"group\" aria-label={L.zoomLabel}>\n <button type=\"button\" data-testid=\"bodymap-zoom-out\" aria-label={L.zoomOut} disabled={zoom <= ZMIN} onClick={() => zoomBtn(1 / 1.4)}>−</button>\n <button type=\"button\" data-testid=\"bodymap-zoom-reset\" aria-label={L.zoomReset} disabled={zoom === 1 && pan.x === 0 && pan.y === 0} onClick={resetZoom}>⤢</button>\n <button type=\"button\" data-testid=\"bodymap-zoom-in\" aria-label={L.zoomIn} disabled={zoom >= ZMAX} onClick={() => zoomBtn(1.4)}>+</button>\n </div>\n </div>\n <svg\n ref={svgRef}\n className=\"bmap__svg\"\n data-testid=\"bodymap-svg\"\n viewBox=\"0 0 260 470\"\n role=\"group\"\n aria-label={L.svgLabel}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n onWheel={(e) => zoomTo(zoomRef.current * (e.deltaY < 0 ? 1.12 : 1 / 1.12), screenToVB(e.clientX, e.clientY))}\n >\n <g transform={`translate(${pan.x} ${pan.y}) scale(${zoom})`}>\n {regions.map((r) => {\n const s = shapes[r.key];\n if (!s) return null;\n const marked = pointOf(r.key);\n const interactive = isSelectable(r.key, config) && !readOnly;\n const visCls = [\"bmap__vis\", selected === r.key && \"bmap__vis--sel\"].filter(Boolean).join(\" \");\n const fill = marked ? markedFill(marked.intensity, palette) : baseFill(r.key, fills[r.key] ?? \"#cbd5e1\", palette);\n const hs = hitShape(s);\n return (\n <g key={r.key}>\n {shapeEl(hs, {\n className: \"bmap__hit\" + (interactive ? \"\" : \" bmap__hit--locked\"),\n \"data-testid\": `bodymap-region-${r.key}`,\n role: interactive ? \"button\" : \"img\",\n tabIndex: interactive ? 0 : -1,\n \"aria-label\": ariaFor(r, marked, interactive),\n \"aria-pressed\": interactive ? selected === r.key : undefined,\n // Click is attached whenever the map is EDITABLE, even on a\n // locked region — the pick returns \"ignore\", changes nothing\n // and does not buzz, but it reaches onFeedback so an app can\n // say WHY nothing happened. Found by measurement (F052.22):\n // the 3D renderer raycasts every mesh and already reported\n // `ignore`, so gating this on `interactive` made the two\n // renderers answer the same tap differently. ARIA is\n // deliberately NOT widened — a locked region must not become\n // a focusable button that does nothing.\n onClick: readOnly ? undefined : () => pickRegion(r.key),\n onKeyDown: interactive\n ? (e: React.KeyboardEvent) => { if (e.key === \"Enter\" || e.key === \" \") { e.preventDefault(); suppressClick.current = false; applyPick(r.key); } }\n : undefined,\n })}\n {shapeEl(s, { className: visCls, fill, opacity: !readOnly && !isSelectable(r.key, config) ? 0.4 : 1 })}\n </g>\n );\n })}\n {/* intensity numbers on marked regions visible in the current view */}\n {report.map((p) => {\n const s = shapes[p.region];\n if (!s || config[p.region]?.visible === false) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmap__heatn\">\n {p.intensity}\n </text>\n );\n })}\n </g>\n </svg>\n </div>\n\n {/* picker panel — hidden entirely in readOnly (display-only) mode */}\n {readOnly ? null : region ? (\n <div className=\"bmap__panel\" data-testid=\"bodymap-panel\">\n <div className=\"bmap__ph\">\n <b>{nameOf(region.key)}</b>\n <span className=\"bmap__code\">{region.code}</span>\n {/* F052.26 — the way OUT. Opening a region writes nothing, so there\n was no exit that did not write something: mark it, or open a\n different region you did not mean either. Always available, not\n only when nothing is marked — closing is \"stop editing this\n region\", which is a different act from removing the mark. */}\n <button\n type=\"button\"\n className=\"bmap__close\"\n data-testid=\"bodymap-close\"\n aria-label={L.close}\n onClick={() => setSelected(null)}\n >×</button>\n </div>\n <p className=\"bmap__lbl\" id=\"bmap-intensity-lbl\">{L.intensity}</p>\n <div className=\"bmap__scale\" role=\"group\" aria-labelledby=\"bmap-intensity-lbl\">\n {Array.from({ length: 11 }, (_, n) => (\n <button\n key={n}\n type=\"button\"\n className={\"bmap__i\" + (current?.intensity === n ? \" bmap__i--on\" : \"\")}\n data-testid={`bodymap-intensity-${n}`}\n aria-pressed={current?.intensity === n}\n onClick={() => setPain(region.key, n, current?.type)}\n >\n {n}\n </button>\n ))}\n </div>\n <p className=\"bmap__lbl\" id=\"bmap-quality-lbl\">{L.quality}</p>\n <div className=\"bmap__chips\" role=\"group\" aria-labelledby=\"bmap-quality-lbl\">\n {PAIN_TYPES.map((t) => (\n <button\n key={t}\n type=\"button\"\n className={\"bmap__chip\" + (current?.type === t ? \" bmap__chip--on\" : \"\")}\n data-testid={`bodymap-type-${t}`}\n aria-pressed={current?.type === t}\n onClick={() => setPain(region.key, current?.intensity ?? 5, t)}\n >\n {L.qualities[t]}\n </button>\n ))}\n </div>\n {current && (\n <button type=\"button\" className=\"bmap__rm\" data-testid=\"bodymap-remove\" onClick={() => removePain(region.key)}>\n {L.remove}\n </button>\n )}\n </div>\n ) : (\n <div className=\"bmap__panel bmap__panel--empty\" data-testid=\"bodymap-panel\">\n {L.empty}\n </div>\n )}\n </div>\n );\n}\n\n// ---- <BodyMapCompare> — before/after progress (F052.13) ---------------------\n// Two read-only bodies side by side + a per-region change list (improved /\n// worse / new / resolved). Pure UI over two PainReports — no network. NB: the\n// \"nudge the user to update their report over time\" is the CONSUMER app's job\n// (fd-sundhed's mobile app via @broberg/webpush), NOT this component.\n\ntype RegionChange = \"new\" | \"resolved\" | \"improved\" | \"worse\" | \"same\";\nfunction changeOf(b?: number, a?: number): RegionChange {\n if (b == null && a != null) return \"new\";\n if (b != null && a == null) return \"resolved\";\n if (b == null || a == null) return \"same\";\n return a < b ? \"improved\" : a > b ? \"worse\" : \"same\";\n}\n\nfunction CompareFigure({ report, view, palette, caption, testid }: {\n report: PainReport; view: BodyView2D; palette?: BodymapPalette; caption: string; testid: string;\n}) {\n const shapes = view === \"back\" ? SHAPES_BACK : SHAPES;\n const fills = view === \"back\" ? FILL_BACK : FILL;\n return (\n <figure className=\"bmapc__fig\" data-testid={testid}>\n <figcaption className=\"bmapc__cap\">{caption}</figcaption>\n <svg className=\"bmapc__svg\" viewBox=\"0 0 260 470\" role=\"img\" aria-label={caption}>\n {REGIONS.map((r) => {\n const s = shapes[r.key];\n if (!s) return null;\n const m = report.find((p) => p.region === r.key);\n const fill = m ? markedFill(m.intensity, palette) : baseFill(r.key, fills[r.key] ?? \"#cbd5e1\", palette);\n return <g key={r.key}>{shapeEl(s, { className: \"bmapc__vis\", fill })}</g>;\n })}\n {report.map((p) => {\n const s = shapes[p.region];\n if (!s) return null;\n const c = center(s);\n return (\n <text key={p.region} x={c.x} y={c.y + 4} textAnchor=\"middle\" className=\"bmapc__n\">{p.intensity}</text>\n );\n })}\n </svg>\n </figure>\n );\n}\n\nexport interface BodyMapCompareProps {\n /** The earlier report. */\n before: PainReport;\n /** The later report. */\n after: PainReport;\n palette?: BodymapPalette;\n locale?: BodyMapLocale;\n labels?: Partial<BodyMapLabels>;\n defaultView?: BodyView2D;\n className?: string;\n}\n\n/** Read-only before/after view: two bodies side by side + a per-region change list. */\nexport function BodyMapCompare({\n before, after, palette, locale = \"da\", labels, defaultView = \"front\", className,\n}: BodyMapCompareProps) {\n useEffect(ensureStyles, []);\n const [view, setView] = useState<BodyView2D>(defaultView);\n const L = resolveLabels(locale, labels);\n const nameOf = (k: string) => L.regions[k] ?? getRegion(k)?.label ?? k;\n const rootStyle = chromeStyle(palette);\n\n const keys = Array.from(new Set([...before, ...after].map((p) => p.region)));\n const changes = keys\n .map((k) => ({\n key: k,\n b: before.find((p) => p.region === k)?.intensity,\n a: after.find((p) => p.region === k)?.intensity,\n status: changeOf(before.find((p) => p.region === k)?.intensity, after.find((p) => p.region === k)?.intensity),\n }))\n .filter((c) => c.status !== \"same\")\n .sort((x, y) => (x.key < y.key ? -1 : 1));\n\n return (\n <div className={[\"bmapc\", className].filter(Boolean).join(\" \")} data-testid=\"bodymap-compare\" style={rootStyle}>\n <div className=\"bmap__viewtoggle\" role=\"group\" aria-label={L.viewLabel} style={{ marginBottom: 12 }}>\n <button type=\"button\" className={\"bmap__vbtn\" + (view === \"front\" ? \" bmap__vbtn--on\" : \"\")} data-testid=\"bodymap-compare-view-front\" aria-pressed={view === \"front\"} onClick={() => setView(\"front\")}>{L.front}</button>\n <button type=\"button\" className={\"bmap__vbtn\" + (view === \"back\" ? \" bmap__vbtn--on\" : \"\")} data-testid=\"bodymap-compare-view-back\" aria-pressed={view === \"back\"} onClick={() => setView(\"back\")}>{L.back}</button>\n </div>\n <div className=\"bmapc__bodies\">\n <CompareFigure report={before} view={view} palette={palette} caption={L.before} testid=\"bodymap-compare-before\" />\n <CompareFigure report={after} view={view} palette={palette} caption={L.after} testid=\"bodymap-compare-after\" />\n </div>\n {changes.length ? (\n <div className=\"bmapc__delta\" data-testid=\"bodymap-compare-delta\">\n {changes.map((c) => (\n <div key={c.key} className=\"bmapc__row\" data-testid={`bodymap-compare-delta-${c.key}`}>\n <span className=\"bmapc__rname\">{nameOf(c.key)}</span>\n <span className=\"bmapc__rval\">{c.b ?? \"–\"} → {c.a ?? \"–\"}</span>\n <span className={`bmapc__badge bmapc__badge--${c.status}`}>{L.change[c.status as Exclude<RegionChange, \"same\">]}</span>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"bmapc__empty\" data-testid=\"bodymap-compare-delta\">{L.noChange}</div>\n )}\n </div>\n );\n}\n","/**\n * @broberg/bodymap/three — a rotatable 3D body pain-map on the SAME core.\n *\n * `<BodyMap3D>` wraps a VANILLA three.js scene (NOT react-three-fiber, so it\n * runs in React, Preact and a Capacitor webview alike): a realistic Blender\n * Studio human base mesh (CC0), rotatable (drag) + zoomable (scroll), with\n * hover-highlight and click-to-mark — a click colours the body part by pain\n * intensity and produces the same structured PainReport (bodymap/v1) the 2D\n * renderer does. Region hit-testing is anchor-based: every mesh vertex is\n * assigned to its nearest region anchor (true per-zone mesh segmentation is a\n * later refinement — the wire + interaction are identical either way).\n *\n * `three` is an OPTIONAL peer — only consumers importing this subpath pull it\n * in. The body models are NOT bundled: pass `models={{ male, female }}` as URLs\n * you host (the package ships reference GLBs under `@broberg/bodymap/models/`).\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport * as THREE from \"three\";\nimport { GLTFLoader } from \"three/examples/jsm/loaders/GLTFLoader.js\";\nimport { OrbitControls } from \"three/examples/jsm/controls/OrbitControls.js\";\nimport {\n REGIONS,\n PAIN_TYPES,\n getRegion,\n isSelectable,\n decidePick,\n type PickOutcome,\n emitFeedback,\n type FeedbackFn,\n serializeReport,\n heatFor,\n baseColorFor,\n defaultPalette,\n uiColors,\n type BodymapUiColors,\n type BodymapPalette,\n type PainReport,\n type PainType,\n type RegionConfig,\n} from \"./index.js\";\nimport {\n LABELS_DA,\n LABELS_EN,\n type BodyMapLabels,\n type BodyMapLocale,\n} from \"./react.js\";\n\nexport type BodyMap3DSex = \"male\" | \"female\";\n\n/** URLs of the body GLB models the consumer hosts (reference GLBs ship under `@broberg/bodymap/models/`). */\nexport interface BodyMap3DModels {\n male: string;\n female: string;\n}\n\n/** The 3D-only control strings (the shared labels come from `locale`/`labels`). */\nexport interface BodyMap3DUiLabels {\n /** Label for the built-in fullscreen control (F052.24). */\n expand?: string;\n /** Label for the same control while fullscreen. */\n collapse?: string;\n male: string;\n female: string;\n hoverHint: string;\n}\n\nconst UI_DA: BodyMap3DUiLabels = { male: \"Mand\", female: \"Kvinde\", hoverHint: \"Hover for at fremhæve · klik en kropsdel for at markere smerte.\", expand: \"Vis stor\", collapse: \"Luk stor visning\" };\nconst UI_EN: BodyMap3DUiLabels = { male: \"Male\", female: \"Female\", hoverHint: \"Hover to highlight · tap a body part to mark pain.\", expand: \"Expand\", collapse: \"Close\" };\n\n// Region anchors in normalised body space (height ~1.9, feet y=0, front +z),\n// then x-flipped so the patient's own left maps to \"venstre\" (self-view).\nconst ANCHORS: Record<string, [number, number, number]> = {\n head: [0, 1.79, 0.02], neck: [0, 1.57, 0.0],\n chest: [0, 1.42, 0.11], thora: [0, 1.42, -0.12], lumbar: [0, 1.13, -0.13], groin: [0, 0.92, 0.09],\n shoulder_left: [-0.2, 1.5, 0], shoulder_right: [0.2, 1.5, 0],\n uarm_left: [-0.27, 1.3, 0.0], uarm_right: [0.27, 1.3, 0.0],\n elbow_left: [-0.31, 1.08, 0], elbow_right: [0.31, 1.08, 0],\n farm_left: [-0.34, 0.93, 0.02], farm_right: [0.34, 0.93, 0.02],\n wrist_left: [-0.36, 0.79, 0.02], wrist_right: [0.36, 0.79, 0.02],\n hand_left: [-0.37, 0.68, 0.03], hand_right: [0.37, 0.68, 0.03],\n hip_left: [-0.14, 1.02, -0.03], hip_right: [0.14, 1.02, -0.03],\n thigh_left: [-0.1, 0.68, 0.05], thigh_right: [0.1, 0.68, 0.05],\n knee_left: [-0.1, 0.4, 0.06], knee_right: [0.1, 0.4, 0.06],\n lowleg_left: [-0.1, 0.22, 0.04], lowleg_right: [0.1, 0.22, 0.04],\n ankle_left: [-0.1, 0.05, 0.02], ankle_right: [0.1, 0.05, 0.02],\n foot_left: [-0.1, 0.02, 0.11], foot_right: [0.1, 0.02, 0.11],\n};\n/**\n * How much of the RUNNER-UP region bleeds into this vertex (F052.21).\n *\n * Takes SQUARED distances (what the assignment loop already has). Returns 0 at\n * an anchor and 0.5 where two anchors are equidistant — never above 0.5, so the\n * nearest region always dominates and a vertex can never be painted mostly as\n * its neighbour.\n *\n * `SEAM` decides how wide the fade is. Raised toward 1 the whole body turns to\n * mush and a marked region stops reading as a region; at 0 you get the old hard\n * plane back. It is a look, not a contract — hit-testing never sees this number.\n */\n/** How wide the fade is, in ratio units below the seam. 0 = the old hard plane. */\nconst SEAM = 0.18;\n\n/**\n * How much of the RUNNER-UP region bleeds into this vertex (F052.21).\n *\n * Takes SQUARED distances — what the assignment loop already has.\n *\n * ratio = d1 / (d1 + d2) → 0 at the anchor · 0.5 where two anchors are equidistant\n *\n * Returns 0 well inside a region and rises to 0.5 exactly at the seam, so the\n * nearest region ALWAYS dominates and a vertex can never be painted mostly as\n * its neighbour. Smoothstepped, so the fade has no visible start line of its own.\n *\n * This is a look, not a contract: hit-testing never sees this number, and the\n * PainReport is unchanged.\n */\nexport function seamBlend(nearestSq: number, secondSq: number): number {\n const d1 = Math.sqrt(nearestSq), d2 = Math.sqrt(secondSq);\n const sum = d1 + d2;\n if (!(sum > 0)) return 0;\n const ratio = d1 / sum;\n const t = (ratio - (0.5 - SEAM)) / SEAM;\n if (t <= 0) return 0;\n if (t >= 1) return 0.5;\n return t * t * (3 - 2 * t) * 0.5; // smoothstep\n}\n\n/**\n * F052.23 — how much of the NEIGHBOUR region's colour a vertex takes.\n *\n * Extracted from the paint loop for one reason: happy-dom cannot run WebGL, so\n * the fix that put the seam blend behind an opt-in had no automated guard at\n * all. A regression here is a WCAG failure on a public-health surface (measured:\n * 4.71:1 without the blend, 3.59:1 with it, averaged over a small mark), which\n * is not the kind of thing that should rely on someone re-measuring by hand.\n *\n * The invariant worth asserting is not \"off returns 0 for this blend value\" —\n * it is that off returns 0 for EVERY blend value, so the shipped default can\n * never mix body colour into a mark.\n */\nexport function seamWeight(seamEnabled: boolean, blend: number): number {\n return seamEnabled ? blend : 0;\n}\n\n/**\n * F052.25 — recompute vertex normals ONLY when the file shipped none.\n *\n * Same reason as above: this single condition WAS the \"lines on the body\" bug,\n * and it lived on the one code path the unit suite cannot execute. Takes a\n * structural slice rather than a THREE.BufferGeometry so it is testable without\n * a GL context.\n *\n * It performs the call as well as the check, deliberately. A predicate alone\n * would let a test pass while the call site did the opposite with it — and what\n * the acceptance criterion asks for is the EFFECT: a model that ships normals\n * must come out the other side with those same normals, untouched.\n *\n * Returns true when it computed (the model had none, and would otherwise render\n * flat), false when it left the authored normals alone.\n */\nexport function ensureNormals(geo: {\n getAttribute(name: string): unknown;\n computeVertexNormals(): void;\n}): boolean {\n if (geo.getAttribute(\"normal\")) return false;\n geo.computeVertexNormals();\n return true;\n}\n\nconst ANCHOR_KEYS = Object.keys(ANCHORS);\nfor (const k of ANCHOR_KEYS) ANCHORS[k][0] = -ANCHORS[k][0];\n\nfunction mergeLabels(locale: BodyMapLocale, overrides?: Partial<BodyMapLabels>): BodyMapLabels {\n const base = locale === \"en\" ? LABELS_EN : LABELS_DA;\n if (!overrides) return base;\n return {\n ...base,\n ...overrides,\n regions: { ...base.regions, ...overrides.regions },\n qualities: { ...base.qualities, ...overrides.qualities },\n };\n}\n\nfunction webglAvailable(): boolean {\n try {\n const c = document.createElement(\"canvas\");\n return !!(c.getContext(\"webgl\") || c.getContext(\"experimental-webgl\"));\n } catch {\n return false;\n }\n}\n\nexport interface BodyMap3DProps {\n /** URLs of the male/female body GLBs (you host them; reference GLBs ship under `@broberg/bodymap/models/`). */\n models: BodyMap3DModels;\n value?: PainReport;\n defaultValue?: PainReport;\n onChange?: (report: PainReport) => void;\n /** Per-app region config — a non-selectable region is not pickable in 3D either. */\n config?: RegionConfig;\n palette?: BodymapPalette;\n locale?: BodyMapLocale;\n labels?: Partial<BodyMapLabels>;\n /** Override the 3D-only control strings (male/female/hoverHint). */\n ui?: Partial<BodyMap3DUiLabels>;\n defaultSex?: BodyMap3DSex;\n onSexChange?: (sex: BodyMap3DSex) => void;\n /** Controlled body type — when set, the parent owns it (e.g. from the user's profile); `onSexChange` still fires. */\n sex?: BodyMap3DSex;\n /** Show the Male/Female toggle (default true). Set false when `sex` comes from a profile and the picker would just be noise. */\n showSexToggle?: boolean;\n /** Slowly auto-rotate until the user interacts (default true). */\n autoRotate?: boolean;\n /** Height of the 3D canvas (default '60vh'). A bare number is treated as px. Set a\n * shorter value on narrow viewports so the picker panel below the body stays visible\n * the moment a region is selected (no full-screen scroll). */\n canvasHeight?: string | number;\n /** Show the clinical region-code badge (e.g. \"HIP · right\") in the panel header\n * (default true). Set false for a patient/employee-facing flow where the code is\n * internal jargon and the readable region name is enough. */\n showRegionCode?: boolean;\n /** Fill the viewport with the body (F052.24). Controlled: pass it and you own\n * it; `onFullscreenChange` still fires so your own button can drive it.\n *\n * This is a VIEWPORT FILL, not the browser Fullscreen API. iOS Safari does not\n * support fullscreen on an arbitrary element at all, and the iPhone is this\n * component's primary surface — so the mechanism that works everywhere is the\n * one that ships, and it behaves identically on every platform. */\n fullscreen?: boolean;\n /** Fired when the built-in control or the Escape key changes it. */\n onFullscreenChange?: (fullscreen: boolean) => void;\n /** Show the built-in expand/collapse control (default true). Set false and\n * drive `fullscreen` from your own button. */\n showFullscreenButton?: boolean;\n /** Soften the colour transition between neighbouring regions (F052.21).\n *\n * DEFAULT FALSE, and the default is the accessible one (F052.23). The blend\n * mixes the BODY colour into a marked region near its boundary — which on a\n * SMALL mark is most of the mark. Measured on a phone with a consumer's\n * palette and one knee marked: the mark's average contrast against the body\n * fell from 4.71:1 to 3.59:1, i.e. below the WCAG AA threshold of 4.5:1, on a\n * surface a public authority is legally required to meet.\n *\n * At the single strongest pixel the cost is small (5.12 → 4.78) — which is\n * why a spot-check misses it and the AVERAGE is what a person sees.\n *\n * Turn it on when the marked areas are LARGE and you have no accessibility\n * duty: the soft seam genuinely looks better there, and that is why it is\n * still here rather than deleted. */\n seam?: boolean;\n /** Fired after every pick with what ACTUALLY happened (F052.22): \"select\",\n * \"clear\" or \"ignore\". Wire it to a sound (`@broberg/soundkit`) or to native\n * haptics (Capacitor `Haptics.impact()` — the only route to a real buzz on an\n * iPhone, where web vibration does not exist). */\n onFeedback?: FeedbackFn;\n /** Web vibration on select/clear. Default true; silently inert where\n * `navigator.vibrate` is absent (every browser on iPhone, most desktops).\n * Set false to keep the signal but drop the buzz. */\n haptics?: boolean;\n className?: string;\n}\n\n/**\n * F052.30 — the controls now READ THE PALETTE.\n *\n * These were module-level constants with the colours written in: #e2e8f0 border,\n * #fff surface, #0e8f8a accent. Three of those already existed as `chrome`\n * fields and were simply not used, so a consumer who set `ui.border` saw it on\n * the card and NOT on the buttons inside the card — in a component whose whole\n * selling point is that the consuming app themes it.\n *\n * They take `chrome` now, which is why they are functions rather than\n * constants: there is no correct value to compute before the palette is known.\n */\nconst btn = (c: Required<BodymapUiColors>): React.CSSProperties => ({\n font: \"inherit\", cursor: \"pointer\", borderRadius: 8,\n border: `1px solid ${c.border}`, background: c.panelBg, padding: \"6px 9px\",\n});\nconst seg = (c: Required<BodymapUiColors>, on: boolean): React.CSSProperties => ({\n ...btn(c),\n background: on ? c.accent : c.panelBg,\n color: on ? c.accentText : c.text,\n fontWeight: 600,\n});\n\nexport function BodyMap3D(props: BodyMap3DProps) {\n const {\n models, value, defaultValue, onChange, config, palette = defaultPalette,\n locale = \"da\", labels, ui, defaultSex = \"male\", sex: sexProp, showSexToggle = true, onSexChange,\n autoRotate = true, canvasHeight = \"60vh\", showRegionCode = true,\n onFeedback, haptics, seam = false,\n fullscreen: fullscreenProp, onFullscreenChange, showFullscreenButton = true,\n className,\n } = props;\n\n // Panel chrome — consumer palette wins, AA-safe defaults fill every gap. (F052.19)\n const chrome = uiColors(palette);\n\n const L = mergeLabels(locale, labels);\n const UI = { ...(locale === \"en\" ? UI_EN : UI_DA), ...ui };\n const nameOf = (key: string) => L.regions[key] ?? getRegion(key)?.label ?? key;\n const canvasH = typeof canvasHeight === \"number\" ? `${canvasHeight}px` : canvasHeight;\n\n const mountRef = useRef<HTMLDivElement>(null);\n const loadedRef = useRef<HTMLSpanElement>(null);\n const [unsupported, setUnsupported] = useState(false);\n const [ready, setReady] = useState(false);\n const [internalSex, setInternalSex] = useState<BodyMap3DSex>(defaultSex);\n const sex = sexProp ?? internalSex; // controlled when `sex` is passed, else internal\n const [selected, setSelected] = useState<string | null>(null);\n const [internal, setInternal] = useState<PainReport>(defaultValue ?? []);\n const report = value ?? internal;\n\n const commit = (next: PainReport) => {\n if (value === undefined) setInternal(next);\n onChange?.(next);\n };\n\n // Latest state visible to the once-mounted imperative scene.\n const reportRef = useRef(report); reportRef.current = report;\n const selectedRef = useRef(selected); selectedRef.current = selected;\n const paletteRef = useRef(palette); paletteRef.current = palette;\n const configRef = useRef(config); configRef.current = config;\n const modelsRef = useRef(models); modelsRef.current = models;\n const sexRef = useRef(sex); sexRef.current = sex;\n const setSelectedRef = useRef(setSelected); setSelectedRef.current = setSelected;\n // F052.20 — the pick DECISION, reachable from the once-mounted raycast handler.\n // Assigned below, after removePain exists.\n const pickRef = useRef<(key: string) => PickOutcome>(() => \"ignore\");\n const setReadyRef = useRef(setReady); setReadyRef.current = setReady;\n // The scene closure reads this at PAINT time, so toggling `seam` recolours\n // without rebuilding the model (F052.23).\n const seamRef = useRef(seam); seamRef.current = seam;\n // The stage colour reaches the three.js scene through a ref, because the\n // scene is built once in a mount effect while the palette can change at any\n // time. `refresh` re-applies it, so a consumer swapping palettes repaints the\n // backdrop and not just the body. (F052.29)\n const stageRef = useRef(chrome.stageBg); stageRef.current = chrome.stageBg;\n // Controlled when `fullscreen` is passed, internal otherwise — same shape as\n // `sex` (F052.14), so a consumer can drive it from their own button.\n const [internalFs, setInternalFs] = useState(false);\n const isFullscreen = fullscreenProp ?? internalFs;\n const setFullscreen = (v: boolean) => {\n if (fullscreenProp === undefined) setInternalFs(v);\n onFullscreenChange?.(v);\n };\n const apiRef = useRef<{ setSex: (s: BodyMap3DSex) => void; refresh: () => void } | null>(null);\n\n useEffect(() => {\n if (!webglAvailable()) { setUnsupported(true); return; }\n const el = mountRef.current;\n if (!el) return;\n let W = el.clientWidth || 520, H = el.clientHeight || 600;\n\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(stageRef.current);\n const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 100);\n camera.position.set(0, 1.05, 4.4);\n let renderer: THREE.WebGLRenderer;\n try {\n renderer = new THREE.WebGLRenderer({ antialias: true });\n } catch {\n setUnsupported(true);\n return;\n }\n renderer.setSize(W, H);\n renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\n el.appendChild(renderer.domElement);\n scene.add(new THREE.HemisphereLight(0xffffff, 0x223044, 1.05));\n const kl = new THREE.DirectionalLight(0xffffff, 1.5); kl.position.set(3, 5, 4); scene.add(kl);\n const rl = new THREE.DirectionalLight(0x88aaff, 0.7); rl.position.set(-4, 2, -3); scene.add(rl);\n\n const controls = new OrbitControls(camera, renderer.domElement);\n controls.enableDamping = true; controls.dampingFactor = 0.08;\n controls.autoRotate = autoRotate; controls.autoRotateSpeed = 1.1;\n controls.minDistance = 2.2; controls.maxDistance = 8; controls.enablePan = false;\n controls.target.set(0, 0.95, 0);\n controls.addEventListener(\"start\", () => { controls.autoRotate = false; });\n\n // On-demand rendering: render only while auto-rotating or while damping is\n // still settling, then STOP — so the page goes idle. That saves battery on\n // mobile (bodymap is primarily a mobile surface) AND lets a headless\n // Lens/Playwright run actually land clicks instead of starving on a\n // never-quiet WebGL loop (fd-sundhed's F052.6-preview finding). A control\n // 'change' / interaction / state change kicks the pump back to life.\n let raf = 0;\n let pumping = false;\n const renderFrame = () => renderer.render(scene, camera);\n const pump = () => {\n const moved = controls.update();\n renderFrame();\n if (moved || controls.autoRotate) { raf = requestAnimationFrame(pump); }\n else { pumping = false; raf = 0; }\n };\n const kick = () => { if (!pumping && !document.hidden) { pumping = true; raf = requestAnimationFrame(pump); } };\n controls.addEventListener(\"change\", kick);\n\n let modelRoot: THREE.Object3D | null = null;\n let bodyMesh: THREE.Mesh | null = null;\n // F052.21 — the hard assignment stays (it is what HIT-TESTING answers with);\n // `vertexNeighbour` + `vertexBlend` exist only so the COLOUR can fade at a\n // seam instead of stopping at a plane. A soft LOOK must never become a soft\n // ANSWER in a clinical record.\n let vertexRegion: string[] = [];\n let vertexNeighbour: string[] = [];\n let vertexBlend: Float32Array = new Float32Array(0);\n let colorAttr: THREE.BufferAttribute | null = null;\n let hovered: string | null = null;\n const loader = new GLTFLoader();\n const anchorVecs = ANCHOR_KEYS.map((k) => new THREE.Vector3(...ANCHORS[k]));\n const tmp = new THREE.Color();\n\n const restingHex = (key: string): string => {\n const pt = reportRef.current.find((p) => p.region === key);\n if (pt) return heatFor(pt.intensity, paletteRef.current);\n if (selectedRef.current === key) return paletteRef.current.selected;\n return baseColorFor(key, paletteRef.current);\n };\n const tmp2 = new THREE.Color();\n const colourOf = (key: string): string => (hovered === key ? paletteRef.current.hover : restingHex(key));\n\n /**\n * ONE pass over the vertices, blending the two nearest regions.\n *\n * The old paint walked every vertex once PER REGION (20x) and set a flat\n * colour, so a boundary was a hard plane through the mesh — the \"sharp\n * transitions\" Christian saw. It was never the model: 21,160 triangles with\n * smooth normals, and the body renders smooth in every screenshot.\n */\n const paint = () => {\n if (!colorAttr) return;\n const cache = new Map<string, THREE.Color>();\n const col = (k: string) => {\n let c = cache.get(k);\n if (!c) { c = new THREE.Color(colourOf(k)); cache.set(k, c); }\n return c;\n };\n for (let i = 0; i < vertexRegion.length; i++) {\n tmp.copy(col(vertexRegion[i]!));\n // Off by default: a blended mark is a LOWER-CONTRAST mark, and the\n // shipped default has to be the one that passes AA. (F052.23)\n const w = seamWeight(seamRef.current, vertexBlend[i]!);\n if (w > 0) { tmp2.copy(col(vertexNeighbour[i]!)); tmp.lerp(tmp2, w); }\n colorAttr.setXYZ(i, tmp.r, tmp.g, tmp.b);\n }\n colorAttr.needsUpdate = true;\n };\n const refresh = () => {\n scene.background = new THREE.Color(stageRef.current);\n paint();\n renderFrame();\n };\n\n const loadModel = (which: BodyMap3DSex) => {\n const url = which === \"female\" ? modelsRef.current.female : modelsRef.current.male;\n loader.load(url, (gltf) => {\n if (modelRoot) scene.remove(modelRoot);\n const model = gltf.scene;\n const box = new THREE.Box3().setFromObject(model);\n const size = new THREE.Vector3(); box.getSize(size);\n const center = new THREE.Vector3(); box.getCenter(center);\n const scale = 1.9 / size.y;\n model.scale.setScalar(scale);\n model.position.set(-center.x * scale, -box.min.y * scale, -center.z * scale);\n model.updateMatrixWorld(true);\n bodyMesh = null;\n model.traverse((o) => { const m = o as THREE.Mesh; if (m.isMesh && !bodyMesh) bodyMesh = m; });\n if (bodyMesh) {\n const geo = (bodyMesh as THREE.Mesh).geometry as THREE.BufferGeometry;\n // F052.25 — DO NOT recompute normals when the file ships them.\n //\n // This one line was the \"lines on the body, so it looks assembled\"\n // Christian reported. computeVertexNormals() averages face normals PER\n // VERTEX INDEX, and this mesh has split vertices along its UV seams —\n // two copies at the same position, each averaging a different set of\n // faces. The result is a lighting discontinuity exactly along those\n // seams: a vertical line down the torso, horizontals at chest, waist,\n // knees, elbows, ankles. The GLB ships correct smooth normals; we were\n // throwing them away and reconstructing worse ones.\n //\n // Measured side by side, same page, same props, same viewport: with\n // the call, every one of those lines is visible; without it, none are.\n //\n // Kept as a FALLBACK only: a model with no normal attribute would\n // otherwise render flat/black, so compute them when — and only when —\n // the file did not provide any.\n ensureNormals(geo);\n const pos = geo.getAttribute(\"position\") as THREE.BufferAttribute;\n const n = pos.count;\n const cols = new Float32Array(n * 3);\n vertexRegion = new Array(n);\n vertexNeighbour = new Array(n);\n vertexBlend = new Float32Array(n);\n const v = new THREE.Vector3();\n for (let i = 0; i < n; i++) {\n v.fromBufferAttribute(pos, i).applyMatrix4((bodyMesh as THREE.Mesh).matrixWorld);\n // nearest AND runner-up, in one pass\n let best = 0, second = 0, bd = Infinity, sd = Infinity;\n for (let a = 0; a < anchorVecs.length; a++) {\n const d = v.distanceToSquared(anchorVecs[a]);\n if (d < bd) { sd = bd; second = best; bd = d; best = a; }\n else if (d < sd) { sd = d; second = a; }\n }\n vertexRegion[i] = ANCHOR_KEYS[best]!;\n vertexNeighbour[i] = ANCHOR_KEYS[second]!;\n vertexBlend[i] = seamBlend(bd, sd);\n }\n colorAttr = new THREE.BufferAttribute(cols, 3);\n geo.setAttribute(\"color\", colorAttr);\n (bodyMesh as THREE.Mesh).material = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.72, metalness: 0.04 });\n refresh();\n }\n modelRoot = model;\n scene.add(model);\n renderFrame();\n kick();\n setReadyRef.current(true);\n if (loadedRef.current) { loadedRef.current.setAttribute(\"data-loaded\", \"true\"); loadedRef.current.setAttribute(\"data-model\", which); }\n });\n };\n loadModel(sexRef.current);\n apiRef.current = {\n setSex: (s) => { loadedRef.current?.removeAttribute(\"data-loaded\"); setReadyRef.current(false); loadModel(s); },\n refresh,\n };\n\n const raycaster = new THREE.Raycaster();\n const ndc = new THREE.Vector2();\n const pick = (clientX: number, clientY: number): string | null => {\n if (!bodyMesh) return null;\n const rect = renderer.domElement.getBoundingClientRect();\n ndc.set(((clientX - rect.left) / rect.width) * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);\n raycaster.setFromCamera(ndc, camera);\n const hits = raycaster.intersectObject(bodyMesh, true);\n if (!hits.length) return null;\n const p = hits[0].point;\n // Nearest SELECTABLE anchor — a region hidden/locked by the per-app config\n // is skipped, so it never highlights or gets picked (2D-parity).\n let best: string | null = null, bd = Infinity;\n for (const k of ANCHOR_KEYS) {\n if (!isSelectable(k, configRef.current ?? {})) continue;\n const a = ANCHORS[k];\n const d = (p.x - a[0]) ** 2 + (p.y - a[1]) ** 2 + (p.z - a[2]) ** 2;\n if (d < bd) { bd = d; best = k; }\n }\n return best;\n };\n\n let downX = 0, downY = 0, downT = 0;\n const canvas = renderer.domElement;\n const onDown = (e: PointerEvent) => { downX = e.clientX; downY = e.clientY; downT = Date.now(); };\n const onMove = (e: PointerEvent) => {\n if ((e.buttons || 0) !== 0) return;\n const k = pick(e.clientX, e.clientY);\n if (k === hovered) return;\n hovered = k;\n // One pass over the vertices repaints everything, including both seams the\n // hover just moved across — a per-region repaint cannot, now that a vertex\n // can carry colour from TWO regions.\n paint();\n canvas.style.cursor = k ? \"pointer\" : \"default\";\n renderFrame();\n };\n const onUp = (e: PointerEvent) => {\n if (Math.hypot(e.clientX - downX, e.clientY - downY) > 6 || Date.now() - downT > 450) return;\n const k = pick(e.clientX, e.clientY);\n if (!k) return;\n const outcome = pickRef.current(k);\n // A CLEARED REGION MUST NOT KEEP ITS HIGHLIGHT (F052.20).\n //\n // `refresh()` deliberately preserves the hover colour on `hovered`, so\n // after a removal the region stayed highlighted and looked exactly like it\n // was still marked. Measured on an iPhone viewport: report correctly\n // `points: []`, panel closed, chest still turquoise. A finger leaves no\n // cursor behind, so nothing ever moved away to clear it — the user would\n // tap again and mark it a second time.\n //\n // Dropped unconditionally rather than only for touch: showing a highlight\n // on something the user just deleted is wrong feedback in every input\n // mode, and on a mouse the very next pointermove puts it straight back.\n if (outcome === \"clear\") {\n hovered = null;\n canvas.style.cursor = \"default\";\n }\n };\n canvas.addEventListener(\"pointerdown\", onDown);\n canvas.addEventListener(\"pointermove\", onMove);\n canvas.addEventListener(\"pointerup\", onUp);\n\n renderFrame();\n kick(); // settle initial damping / start auto-rotate if enabled\n const onVis = () => { if (document.hidden) { cancelAnimationFrame(raf); pumping = false; raf = 0; } else kick(); };\n document.addEventListener(\"visibilitychange\", onVis);\n const onResize = () => { W = el.clientWidth || 520; H = el.clientHeight || 600; camera.aspect = W / H; camera.updateProjectionMatrix(); renderer.setSize(W, H); renderFrame(); };\n window.addEventListener(\"resize\", onResize);\n /**\n * F052.24 — the ELEMENT can change size without the WINDOW doing so, and\n * before this the canvas kept whatever size it was measured at on mount.\n * Every way that happens is ordinary, not exotic:\n * · entering/leaving fullscreen (this card's whole feature)\n * · a consumer switching `canvasHeight` at a breakpoint\n * · a container animating open, or a step/wizard revealing the panel\n * · a phone's URL bar collapsing, which changes what `vh` means\n * A stale size shows as a body that is small, off-centre, or clipped — with\n * nothing to see in the code, because the camera maths is correct and the\n * numbers it was given are old.\n */\n const ro = typeof ResizeObserver !== \"undefined\" ? new ResizeObserver(() => onResize()) : null;\n ro?.observe(el);\n\n return () => {\n cancelAnimationFrame(raf);\n ro?.disconnect();\n window.removeEventListener(\"resize\", onResize);\n document.removeEventListener(\"visibilitychange\", onVis);\n controls.removeEventListener(\"change\", kick);\n canvas.removeEventListener(\"pointerdown\", onDown);\n canvas.removeEventListener(\"pointermove\", onMove);\n canvas.removeEventListener(\"pointerup\", onUp);\n controls.dispose();\n renderer.dispose();\n if (canvas.parentNode) canvas.parentNode.removeChild(canvas);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Escape leaves fullscreen — a viewport-filling overlay with no keyboard way\n // out is a trap, and F052.11 made this component's accessibility a legal duty.\n useEffect(() => {\n if (!isFullscreen && !selected) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n // Innermost first: Escape closes the open region before it leaves\n // fullscreen, so one press never does two things at once. (F052.26)\n if (selected) setSelected(null);\n else setFullscreen(false);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n });\n\n // React → scene: recolour on selection / report / palette change; swap model on sex change.\n useEffect(() => { apiRef.current?.refresh(); }, [selected, report, palette, seam]);\n // Swap the model on sex change — but skip the first run: the mount effect\n // already loaded sexRef.current, so this avoids a redundant reload (matters on mobile).\n const sexInited = useRef(false);\n useEffect(() => {\n if (!sexInited.current) { sexInited.current = true; return; }\n apiRef.current?.setSex(sex);\n }, [sex]);\n\n const pointOf = (k: string) => report.find((p) => p.region === k);\n const setPain = (k: string, intensity: number, type?: PainType) => {\n commit([...report.filter((p) => p.region !== k), { region: k, intensity, type, timestamp: new Date().toISOString() }]);\n };\n const removePain = (k: string) => { commit(report.filter((p) => p.region !== k)); setSelected(null); };\n /**\n * F052.20 — a region that is ALREADY marked is CLEARED by picking it again.\n * The 2D renderer has the identical rule in `applyPick`; they share no click\n * code, which is exactly why both are asserted separately.\n */\n pickRef.current = (k: string): PickOutcome => {\n const what = decidePick(k, reportRef.current, configRef.current ?? {});\n // Same call as the 2D renderer, from the same decision — the signal cannot\n // drift between the two bodies because neither of them decides it. (F052.22)\n emitFeedback(what, k, { onFeedback, haptics });\n if (what === \"clear\") removePain(k);\n else if (what === \"select\") setSelected(k);\n return what;\n };\n const changeSex = (s: BodyMap3DSex) => { if (sexProp === undefined) setInternalSex(s); onSexChange?.(s); };\n\n const region = selected ? REGIONS.find((r) => r.key === selected) : null;\n const current = selected ? pointOf(selected) : undefined;\n // Smart default: when the config locks EVERYTHING (a report/display view), the\n // \"tap a body part to mark pain\" hint is misleading — suppress it unless the\n // consumer explicitly set a display text via ui.hoverHint.\n const anySelectable = REGIONS.some((r) => isSelectable(r.key, config ?? {}));\n const showEmptyHint = anySelectable || ui?.hoverHint !== undefined;\n\n return (\n <div\n data-testid=\"bodymap3d-root\"\n className={className}\n data-fullscreen={isFullscreen ? \"true\" : undefined}\n style={{\n fontFamily: \"system-ui, sans-serif\",\n color: chrome.text,\n // A viewport fill, not the Fullscreen API — see the `fullscreen` prop.\n // `fixed`+`inset:0` behaves the same on iOS Safari, where element\n // fullscreen does not exist at all, as it does everywhere else.\n ...(isFullscreen\n ? {\n position: \"fixed\" as const,\n inset: 0,\n zIndex: 2147483000,\n // THE STAGE, not the panel. This used to be `chrome.panelBg`,\n // the same field that paints the picker card — so a consumer with\n // a dark canvas got a white void around a dark body in fullscreen,\n // and could not fix it: darkening panelBg hid the 0-10 buttons.\n // One field, two surfaces, opposite needs. (F052.29)\n background: chrome.stageBg,\n // F052.27 — a fixed inset:0 surface in a standalone/Capacitor\n // webview covers the STATUS BAR too, so a control laid out at the\n // top lands on the clock and the battery. Measured by fd-sundhed\n // on a real iPhone: the owner could not read the button or get\n // out, and Escape does not exist on a phone. Every side gets its\n // safe-area inset, bottom included — the home indicator covers\n // content just as effectively.\n paddingTop: \"calc(12px + env(safe-area-inset-top))\",\n paddingBottom: \"calc(12px + env(safe-area-inset-bottom))\",\n paddingLeft: \"calc(12px + env(safe-area-inset-left))\",\n paddingRight: \"calc(12px + env(safe-area-inset-right))\",\n display: \"flex\",\n flexDirection: \"column\" as const,\n overflow: \"auto\" as const,\n }\n : null),\n }}\n >\n {showSexToggle && (\n <div style={{ display: \"flex\", gap: 16, flexWrap: \"wrap\", marginBottom: 12, fontSize: 13 }}>\n <div style={{ display: \"flex\", gap: 6, alignItems: \"center\" }}>\n <button data-testid=\"bodymap3d-sex-male\" onClick={() => changeSex(\"male\")} style={seg(chrome, sex === \"male\")}>{UI.male}</button>\n <button data-testid=\"bodymap3d-sex-female\" onClick={() => changeSex(\"female\")} style={seg(chrome, sex === \"female\")}>{UI.female}</button>\n </div>\n </div>\n )}\n <span ref={loadedRef} data-testid=\"bodymap3d-loaded\" style={{ display: \"none\" }} />\n {ready && <span data-testid=\"bodymap3d-ready\" style={{ position: \"absolute\", width: 1, height: 1, opacity: 0, pointerEvents: \"none\" }} />}\n {showFullscreenButton && !unsupported && (\n <div style={{ display: \"flex\", justifyContent: \"flex-end\", marginBottom: 8 }}>\n <button\n type=\"button\"\n data-testid=\"bodymap3d-fullscreen\"\n aria-pressed={isFullscreen}\n aria-label={isFullscreen ? (UI.collapse ?? \"Close\") : (UI.expand ?? \"Expand\")}\n onClick={() => setFullscreen(!isFullscreen)}\n style={{\n ...btn(chrome),\n color: chrome.text,\n borderColor: chrome.border,\n background: chrome.panelBg,\n ...(isFullscreen ? { width: 40, height: 40, padding: 0, fontSize: 22, lineHeight: 1 } : null),\n }}\n >\n {/* A COMPACT × once we are the whole screen. The text pill is\n right when the component sits in a page — it is discoverable —\n and wrong at the top of a phone screen, where it is wide enough\n to cover half the status bar. The label stays on aria-label, so\n nothing is lost for a screen reader. (F052.27) */}\n {isFullscreen ? \"×\" : (UI.expand ?? \"Expand\")}\n </button>\n </div>\n )}\n <div style={{ display: \"flex\", gap: 18, alignItems: \"flex-start\", flexWrap: \"wrap\", ...(isFullscreen ? { flex: \"1 1 auto\", minHeight: 0 } : null) }}>\n {unsupported ? (\n <div data-testid=\"bodymap3d-unsupported\" /* A message, not a stage: it takes the PANEL pair, whose contrast the\n suite already asserts (7.58:1). Following stageBg would make it\n unreadable the moment a consumer chose a light stage, and no prop\n could rescue it. (F052.29) */\n style={{ flex: \"1 1 520px\", minWidth: 320, height: canvasH, borderRadius: 16, background: chrome.panelBg, color: chrome.mutedText, border: `1px solid ${chrome.border}`, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", padding: 24, textAlign: \"center\", fontSize: 14 }}>\n 3D kræver WebGL, som ikke er tilgængeligt her.\n </div>\n ) : (\n <div\n ref={mountRef}\n data-testid=\"bodymap3d-canvas\"\n style={{\n flex: \"1 1 520px\",\n // In fullscreen the box takes the height it is GIVEN rather than a\n // fixed `canvasHeight`; the ResizeObserver above is what makes the\n // picture follow it.\n height: isFullscreen ? \"100%\" : canvasH,\n minWidth: 320,\n minHeight: isFullscreen ? 0 : undefined,\n borderRadius: 16,\n overflow: \"hidden\",\n background: chrome.stageBg,\n touchAction: \"none\",\n }}\n />\n )}\n <div style={{ flex: \"1 1 300px\", minWidth: 260 }}>\n {region ? (\n <div style={{ border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", marginBottom: 12 }}>\n <b style={{ fontSize: 16 }}>{nameOf(region.key)}</b>\n {showRegionCode && (\n <span data-testid=\"bodymap3d-region-code\" style={{ font: \"11px ui-monospace, monospace\", color: chrome.mutedText, background: chrome.badgeBg, borderRadius: 6, padding: \"2px 7px\" }}>{region.code}{region.side ? \" · \" + region.side : \"\"}</span>\n )}\n {/* F052.26 — the way OUT. Opening a region writes nothing, so\n until now there was no exit that did not write something:\n mark it, or open a different region you did not mean either.\n Always shown, not only when nothing is marked — closing is\n \"stop editing this region\", a different act from removing\n the mark. Both renderers get it: this package has been\n bitten three times today by fixing one half of a pair. */}\n <button\n type=\"button\"\n data-testid=\"bodymap3d-close\"\n aria-label={L.close}\n onClick={() => setSelected(null)}\n style={{ ...btn(chrome), width: 32, height: 32, padding: 0, fontSize: 20, lineHeight: 1, color: chrome.mutedText, borderColor: chrome.border, background: chrome.panelBg }}\n >×</button>\n </div>\n <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: \".06em\", textTransform: \"uppercase\", color: chrome.mutedText, marginBottom: 7 }}>{L.intensity}</div>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 4, marginBottom: 12 }}>\n {Array.from({ length: 11 }, (_, i) => (\n <button key={i} data-testid={`bodymap3d-intensity-${i}`} onClick={() => setPain(region.key, i, current?.type)} style={{ ...btn(chrome), display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", minWidth: 30, height: 30, padding: 0, background: current?.intensity === i ? chrome.accent : chrome.panelBg, color: current?.intensity === i ? chrome.accentText : chrome.text }}>{i}</button>\n ))}\n </div>\n <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: \".06em\", textTransform: \"uppercase\", color: chrome.mutedText, marginBottom: 7 }}>{L.quality}</div>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: 6, marginBottom: 14 }}>\n {PAIN_TYPES.map((t) => (\n <button key={t} data-testid={`bodymap3d-type-${t}`} onClick={() => setPain(region.key, current?.intensity ?? 5, t)} style={{ ...btn(chrome), borderRadius: 999, padding: \"6px 12px\", background: current?.type === t ? chrome.text : chrome.panelBg, color: current?.type === t ? chrome.panelBg : chrome.mutedText }}>{L.qualities[t] ?? t}</button>\n ))}\n </div>\n {current && <button data-testid=\"bodymap3d-remove\" onClick={() => removePain(region.key)} style={{ ...btn(chrome), color: chrome.danger, borderColor: chrome.dangerBorder }}>{L.remove}</button>}\n </div>\n ) : showEmptyHint ? (\n <div data-testid=\"bodymap3d-empty\" style={{ border: `1px solid ${chrome.border}`, borderRadius: 14, padding: 16, background: chrome.panelBg, color: chrome.mutedText, fontSize: 13.5 }}>{UI.hoverHint}</div>\n ) : null}\n </div>\n </div>\n </div>\n );\n}\n\n/** Re-export for convenience so a 3D-only consumer can serialize without a second import. */\nexport { serializeReport };\n"]}