WME Candy Shop

WME Candy Shop: master-layer shapes, pop/revert, brush/eraser (geodesic, hole→slit per part), shape–place links, FPS, workflow UX.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         WME Candy Shop
// @namespace       https://github.com/horizon911
// @version         2026.04.12.15
// @description  WME Candy Shop: master-layer shapes, pop/revert, brush/eraser (geodesic, hole→slit per part), shape–place links, FPS, workflow UX.
// @match        https://*.waze.com/*/editor*
// @match        https://*.waze.com/editor*
// @icon            https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Lollipop/Flat/lollipop_flat.svg
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jsts.min.js
// @require      https://cdn.jsdelivr.net/gh/WazeSpace/wme-sdk-plus@f09f4dfbcfa3a09d127cc6b85e964c5b54374ae0/dist/wme-sdk-plus.js
// @connect      unpkg.com
// @connect      cdnjs.cloudflare.com
// @license         GPLv3
// @supportURL      https://github.com/horizon911/WME_Candy-Paint/issues
// ==/UserScript==

//WME SDK Plus GitHub commits page: https://github.com/WazeSpace/wme-sdk-plus/commits/main

(function() {
    'use strict';

    /** Set localStorage.wmeCandyPaintDebug=1 to mirror log lines to the browser console. */
    const DEBUG = (function() {
        try { return localStorage.getItem('wmeCandyPaintDebug') === '1'; } catch (e) { return false; }
    })();

    const LS_AUTO_INGEST = 'wmeCandyPaintAutoIngest';
    function getAutoIngest() {
        try { return localStorage.getItem(LS_AUTO_INGEST) === '1'; } catch (e) { return false; }
    }
    function setAutoIngest(on) {
        try { localStorage.setItem(LS_AUTO_INGEST, on ? '1' : '0'); } catch (e) { /* ignore */ }
    }

    const LS_COMMIT_MODE = 'wmeCandyPaintCommitMode';
    /** Boolean commit modes for shape tools; also used for undo snapshot validation. */
    const COMMIT_MODE_IDS = new Set(['replace', 'union', 'difference', 'intersection', 'xor']);
    function getPersistedCommitMode() {
        try {
            const raw = localStorage.getItem(LS_COMMIT_MODE);
            if (raw && COMMIT_MODE_IDS.has(raw)) return raw;
        } catch (e) { /* ignore */ }
        return null;
    }
    function setPersistedCommitMode(mode) {
        if (!COMMIT_MODE_IDS.has(mode)) return;
        try { localStorage.setItem(LS_COMMIT_MODE, mode); } catch (e) { /* ignore */ }
    }

    const LS_EXPERIMENTAL_UNDO_KEYS = 'wmeCandyPaintExperimentalUndoKeys';
    function getExperimentalUndoKeys() {
        try { return localStorage.getItem(LS_EXPERIMENTAL_UNDO_KEYS) === '1'; } catch (e) { return false; }
    }
    function setExperimentalUndoKeys(on) {
        try { localStorage.setItem(LS_EXPERIMENTAL_UNDO_KEYS, on ? '1' : '0'); } catch (e) { /* ignore */ }
    }

    /** Tools where switching away after Ingest would interrupt an in-progress draw. */
    const DRAWING_STYLE_TOOLS = new Set(['rectangle', 'ellipse', 'lasso', 'polygon', 'brush', 'eraser', 'wand']);

    let autoIngestTimer = null;
    let selectionChangeDebounceTimer = null;
    const SELECTION_CHANGE_DEBOUNCE_MS = 120;

    // WME enables Trusted Types in report-only mode: assigning plain strings to innerHTML / script.src
    // logs "TrustedHTML" / "TrustedScriptURL" warnings. They do not block execution until enforcement.
    // They are an artifact of Waze's CSP policies and Tampermonkey scripts bypassing them using pure DOM injection.

    const BOOTSTRAP_INTERVAL_MS = 800;
    const BOOTSTRAP_MAX_ATTEMPTS = 75;

    let wmeSDK;
    let canvasElement, ctx;
    let renderFrameRequested = false;
    /** Coalesce tracker HUD updates — one RAF per frame max (avoids N HUD callbacks stacking per mousemove burst). */
    let liveMouseHudFrameRequested = false;

    // FPS Tracking State
    let fpsFrames = 0;
    let fpsLastTime = performance.now();

    // Core Geometry States
    /**
     * jsts MultiPolygon: each entry is one independent shape for Apply / linking.
     * Geometry policy:
     * - Each masterPolygons[i] should end as one closed ring (one continuous boundary).
     * - If an eraser punches a hole (inner ring), replace that topology with a narrow cut from the hole to the
     *   closest point on this part’s exterior boundary — the same single outline, not a second disconnected loop.
     * - Multiple indices = multiple shapes, each with its own boundary; do not add cuts between different entries.
     */
    let masterPolygons = []; // Target: one ring per entry. Transient: [ outerRing, innerRing?, ... ] before slit fix.
    let tempVertices = [];
    let hoveredTempVertexIndex = -1;
    let draggingVertexIndex = -1;

    // Draft Mode States
    let draftVertices = [];
    /** Inner rings (holes) while editing; outer boundary is always draftVertices. Pop-to-draft copies from master part[1..]. */
    let draftInteriorRings = [];
    let hoveredDraftVertexIndex = -1;
    let draggingDraftVertexIndex = -1;
    let hoveredDraftMidpointIndex = -1;
    let draftAction = 'none';
    let dragStartMouse = null;
    let dragStartGlobalCoords = null;
    /** @type {null | { x: number, y: number }[][]} Per-ring screen pixels at start of move/rotate/resize (includes holes). */
    let dragStartDraftRingsPx = null;
    let dragStartBBox = null;
    let currentMousePixel = null;
    let isDrawingShape = false;
    /** Synced from document keydown/keyup (capture) for commitDraft / shape drag modifiers. */
    let isShiftDown = false;
    let isAltDown = false;
    /** Space-hold temporary pan (P10); restored on keyup or window blur. */
    let spacePanActive = false;
    let spacePanRestoreTool = 'pan';

    // Advanced Tool States
    let dragStartMasterPixels = null;
    let hoveredMasterPolyIndex = -1;
    let revertOriginalPolygon = null;
    /** @type {null | object} Link entry spliced with revert pop; restored on draft cancel (full schema via cloneLinkEntry). */
    let revertOriginalLink = null;
    /** Link tool: WME venue id to bind on next hovered shape (flexible link order). */
    let pendingLinkVenueId = null;
    /** masterPolygons index shape was popped from; survives changeTool (hover index does not). */
    let draftPopInsertMasterIndex = -1;
    /** Parallel to masterPolygons[i]: null = new place on Apply; { venueId } = updateVenue. */
    let shapePlaceLinks = [];

    /** User layers: bottom (index 0) → top; drawing targets the active layer. Places preview is separate. */
    const PLACES_LAYER_ID = '__candy_places_preview__';
    const DEFAULT_SHAPE_LAYER_NAME = 'Default layer';
    const LS_DOCUMENT_KEY = 'wmeCandyPaintDocument';
    const DOCUMENT_STATE_VERSION = 2;
    let userLayers = [];
    let activeLayerId = null;
    let documentSaveTimer = null;
    const placesPreviewLayer = {
        id: PLACES_LAYER_ID,
        isPlaces: true,
        visible: true,
        name: 'Places',
        polygons: [],
        links: [],
        shapeIds: []
    };

    /** CS Layers: selection keys `layerId:shapeId` */
    const csLayersSelection = new Set();
    let csLayersLastAnchorLayerId = null;
    let csLayersLastAnchorShapeIndex = -1;
    let csLayersMapHoverShapeKey = null;
    let csLayersListHoverShapeKey = null;
    let csLayersNameFilter = '';
    let csLayersCollapsed = Object.create(null);
    let csLayersPanelEventsBound = false;
    let shapeInspectorRefreshScheduled = false;
    let csLayersMapHoverRaf = null;
    let csLayersDragLayerIndex = -1;
    let csLayersFocusedKind = 'none';
    let csLayersFocusedLayerId = null;
    let csLayersFocusedShapeKey = null;
    const LS_CS_LAYERS_POS = 'wmeCandyPaintCsLayersPos';
    const LS_CS_LAYERS_OPEN = 'wmeCandyPaintCsLayersOpen';
    const LS_CS_HISTORY_POS = 'wmeCandyPaintCsHistoryPos';
    const LS_CS_HISTORY_OPEN = 'wmeCandyPaintCsHistoryOpen';
    let csHistoryRefreshScheduled = false;

    const BRUSH_METERS_MIN = 1;
    const BRUSH_METERS_MAX = 250;
    /** Limit geodesic subdivisions per mousemove (large jumps else insert hundreds of points in one frame). */
    const BRUSH_APPEND_MAX_STEPS_PER_EVENT = 64;
    /** Soft cap on stroke vertices; decimate by half until under cap (keeps endpoints). */
    const BRUSH_STROKE_SAMPLES_MAX = 12000;
    /** Long eraser strokes: subtract in sub-strokes (overlap 1 vertex) to avoid jsts hangs. */
    const BRUSH_ERASER_DIFF_CHUNK_VERTS = 28;
    const BRUSH_ERASER_DIFF_CHUNK_OVERLAP = 1;
    /** Long brush strokes: build stamp per sub-path then union chunks (avoids jsts OOM / throw on 3k+ verts). */
    /** Larger chunks ⇒ fewer boolean unions (jsts can throw on many outline unions). */
    const BRUSH_STAMP_UNION_MAX_VERTS = 512;
    const BRUSH_STAMP_UNION_CHUNK_OVERLAP = 3;
    /** Max multipolygon parts unioned in one balanced/sequential batch before folding (avoids stack overflow in jsts). */
    const BRUSH_STAMP_UNION_BATCH_SIZE = 16;
    /** Centerline longer than this (after pre-stroke RDP): build brush stamp from overlapping bufferPath chunks then union. */
    const BRUSH_STAMP_BUFFER_CHUNK_VERTS = 200;
    /** Post-eraser coalesce: max merge waves (stagnation exits earlier when unions stop reducing part count). */
    const BRUSH_COALESCE_MAX_PASSES = 48;
    /** Partial brush loss: union/simplify kept valid multipolygon but dropped area vs pre-stroke (deg² proxy). */
    const BRUSH_REGRESSION_AREA_RATIO = 0.72;
    const BRUSH_COMPLEX_VERT_THRESHOLD = 200;
    const BRUSH_SHARP_TURN_MIN_RAD = 2.0;
    const BRUSH_SHARP_TURN_COUNT_THRESHOLD = 8;
    /** Canvas preview: at most this many vertices (stride subsample; full path kept for mouse-up commit). */
    const BRUSH_PREVIEW_MAX_VERTICES = 480;
    /** While dragging brush/eraser: draw each ring with at most this many verts (full data unchanged). */
    const BRUSH_LIVE_RENDER_MAX_RING_VERTS = 1400;
    /** While dragging brush/eraser: cursor ring uses at most this many sides (commit uses full N from sagitta). */
    const BRUSH_LIVE_CURSOR_MAX_SIDES = 24;
    /** Max sagitta δ (m): inscribed regular N-gon stays within δ of true geodesic circle (see brushCircleSidesForRadiusMeters). */
    const BRUSH_CIRCLE_SAGITTA_MIN_M = 0.2;
    const BRUSH_CIRCLE_SAGITTA_MAX_M = 3.0;
    const BRUSH_CIRCLE_SAGITTA_DEFAULT_M = 0.5;
    const BRUSH_CIRCLE_SIDES_MIN = 8;
    const BRUSH_CIRCLE_SIDES_MAX = 64;
    /** Chaikin passes on outer ring after brush (smooths disk-union scallops). */
    const BRUSH_OUTER_CHAIKIN_ITER = 1;
    /** Full-viewport paint canvas: below WME side/top/bottom chrome (typically z-index in low thousands); above base map. Increase if stroke hides under the map. */
    const CANVAS_OVERLAY_Z_INDEX = 500;
    /** Pixels outward for draft transform handles vs vertex nodes (hit + render). */
    const DRAFT_BBOX_PAD_PX = 12;
    /** 10px hit radius squared (vertex / midpoint stubs). */
    const VERTEX_HIT_RADIUS_SQ = 100;
    const LS_BRUSH_VERTEX_MIN_M = 'wmeCandyPaintBrushVertexMinM';
    const LS_BRUSH_MIN_TURN_DEG = 'wmeCandyPaintBrushMinTurnDeg';
    /** Brush/eraser geodesic radius (meters); slider writes here on change. */
    const LS_BRUSH_SIZE_M = 'wmeCandyPaintBrushRadiusM';
    /** Brush circle sagitta δ (m); optional slider; same policy drives geodesic disks and JSTS buffer quadrant segments. */
    const LS_BRUSH_CIRCLE_SAGITTA_M = 'wmeCandyPaintBrushCircleSagittaM';
    /** freehand | line */
    const LS_BRUSH_STYLE = 'wmeCandyPaintBrushStrokeStyle';
    /** circle | square | diamond | hexagon | rounded_square */
    const LS_BRUSH_PROFILE = 'wmeCandyPaintBrushProfile';
    /** map | stroke */
    const LS_BRUSH_ALIGN = 'wmeCandyPaintBrushAlign';
    /** Corner radius as fraction of R for rounded square */
    const LS_BRUSH_CORNER_RATIO = 'wmeCandyPaintBrushCornerRatio';

    const BRUSH_PROFILE_IDS = ['circle', 'square', 'diamond', 'hexagon', 'rounded_square'];
    const BRUSH_ALIGN_IDS = ['map', 'stroke'];
    const BRUSH_CORNER_RATIO_MIN = 0.06;
    const BRUSH_CORNER_RATIO_MAX = 0.45;
    const BRUSH_CORNER_RATIO_DEFAULT = 0.18;
    /** Densify stroke so stamp centers are at most this factor × R apart (overlap). */
    const BRUSH_POLY_STAMP_SPACING_FACTOR = 0.75;
    /** Segments per quarter-circle on rounded-square local outline. */
    const BRUSH_ROUNDED_SQUARE_ARC_SEGS = 5;

    function readPersistedBrushSizeM() {
        try {
            const raw = localStorage.getItem(LS_BRUSH_SIZE_M);
            if (raw == null || raw === '') return null;
            const n = Number(raw);
            if (!Number.isFinite(n)) return null;
            return Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Math.round(n)));
        } catch (e) {
            return null;
        }
    }

    function writePersistedBrushSizeM(meters) {
        try {
            const v = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Math.round(Number(meters))));
            localStorage.setItem(LS_BRUSH_SIZE_M, String(v));
        } catch (e) { /* ignore */ }
    }

    function readPersistedBrushCircleSagittaM() {
        try {
            const raw = localStorage.getItem(LS_BRUSH_CIRCLE_SAGITTA_M);
            if (raw == null || raw === '') return null;
            const n = Number(raw);
            if (!Number.isFinite(n)) return null;
            const v = Math.round(n * 20) / 20;
            return Math.max(BRUSH_CIRCLE_SAGITTA_MIN_M, Math.min(BRUSH_CIRCLE_SAGITTA_MAX_M, v));
        } catch (e) {
            return null;
        }
    }

    function writePersistedBrushCircleSagittaM(deltaM) {
        try {
            const v = Math.round(Number(deltaM) * 20) / 20;
            const c = Math.max(BRUSH_CIRCLE_SAGITTA_MIN_M, Math.min(BRUSH_CIRCLE_SAGITTA_MAX_M, v));
            localStorage.setItem(LS_BRUSH_CIRCLE_SAGITTA_M, String(c));
        } catch (e) { /* ignore */ }
    }

    function readPersistedBrushStyle() {
        try {
            const raw = localStorage.getItem(LS_BRUSH_STYLE);
            if (raw === 'line' || raw === 'freehand') return raw;
        } catch (e) { /* ignore */ }
        return null;
    }

    function writePersistedBrushStyle(style) {
        try {
            if (style === 'line' || style === 'freehand') localStorage.setItem(LS_BRUSH_STYLE, style);
        } catch (e) { /* ignore */ }
    }

    function readPersistedBrushProfile() {
        try {
            const raw = localStorage.getItem(LS_BRUSH_PROFILE);
            if (raw && BRUSH_PROFILE_IDS.includes(raw)) return raw;
        } catch (e) { /* ignore */ }
        return null;
    }

    function writePersistedBrushProfile(id) {
        try {
            if (BRUSH_PROFILE_IDS.includes(id)) localStorage.setItem(LS_BRUSH_PROFILE, id);
        } catch (e) { /* ignore */ }
    }

    function readPersistedBrushAlign() {
        try {
            const raw = localStorage.getItem(LS_BRUSH_ALIGN);
            if (raw && BRUSH_ALIGN_IDS.includes(raw)) return raw;
        } catch (e) { /* ignore */ }
        return null;
    }

    function writePersistedBrushAlign(id) {
        try {
            if (BRUSH_ALIGN_IDS.includes(id)) localStorage.setItem(LS_BRUSH_ALIGN, id);
        } catch (e) { /* ignore */ }
    }

    function readPersistedBrushCornerRatio() {
        try {
            const raw = localStorage.getItem(LS_BRUSH_CORNER_RATIO);
            if (raw == null || raw === '') return null;
            const n = Number(raw);
            if (!Number.isFinite(n)) return null;
            return Math.max(BRUSH_CORNER_RATIO_MIN, Math.min(BRUSH_CORNER_RATIO_MAX, Math.round(n * 1000) / 1000));
        } catch (e) {
            return null;
        }
    }

    function writePersistedBrushCornerRatio(ratio) {
        try {
            const v = Math.max(BRUSH_CORNER_RATIO_MIN, Math.min(BRUSH_CORNER_RATIO_MAX, Number(ratio)));
            localStorage.setItem(LS_BRUSH_CORNER_RATIO, String(Math.round(v * 1000) / 1000));
        } catch (e) { /* ignore */ }
    }

    let brushPainting = false;
    let brushIsEraser = false;
    let brushStrokeSamples = [];
    /** Skip venue link validation every frame while painting (expensive on large maps). */
    let applyHudInvalidLinksCache = false;
    // Brush tool writes into the shared masterPolygons layer.

    // --- CENTRALIZED STATE MACHINE ---
    const appState = {
        tool: 'pan',
        mode: getPersistedCommitMode() || 'replace',
        brushSize: readPersistedBrushSizeM() ?? 12,
        brushCircleSagittaM: readPersistedBrushCircleSagittaM() ?? BRUSH_CIRCLE_SAGITTA_DEFAULT_M,
        wandTolerance: 50,
        brushStyle: readPersistedBrushStyle() ?? 'freehand',
        brushProfile: readPersistedBrushProfile() ?? 'circle',
        brushAlign: readPersistedBrushAlign() ?? 'map',
        brushCornerRatio: readPersistedBrushCornerRatio() ?? BRUSH_CORNER_RATIO_DEFAULT,
        targetVenueObj: null,
        targetVenueId: null,
        isDraftActive: false,
        highlightedLinkIndex: -1
    };

    /**
     * Same primary URL as @require, plus CDN fallbacks if injection is blocked. Bridge userscripts that inline this
     * file (or load it via file://) do not get @require, so we inject when window.jsts is missing.
     */
    const JSTS_SCRIPT_URLS = [
        'https://cdn.jsdelivr.net/npm/[email protected]/dist/jsts.min.js',
        'https://unpkg.com/[email protected]/dist/jsts.min.js',
        'https://cdnjs.cloudflare.com/ajax/libs/jsts/2.12.1/jsts.min.js',
    ];
    let geometryEngineLoadCallbacks = null;
    /** True when dynamic script injection of jsts failed (e.g. blocked CDN). */
    let geometryEngineLoadFailed = false;

    // Guard against repeated bootstrap retries stacking listeners and/or double init.
    let wmeReadyListenerAdded = false;
    let initStarted = false;
    let bootstrapAttempts = 0;

    function initScriptOnce() {
        if (initStarted) return;
        initStarted = true;
        initScript();
    }

    function onWmeReady() {
        ensureGeometryEngine(initScriptOnce);
    }

    const _jstsWrapperCache = {
        reader: null,
        writer: null,
        getReader() {
            if (!this.reader) this.reader = new window.jsts.io.GeoJSONReader();
            return this.reader;
        },
        getWriter() {
            if (!this.writer) this.writer = new window.jsts.io.GeoJSONWriter();
            return this.writer;
        },
        getDepth(arr) {
            let depth = 0;
            let current = arr;
            while (Array.isArray(current)) {
                depth++;
                current = current[0];
            }
            return depth;
        },
        toJSTS(coords) {
            if (!coords || !Array.isArray(coords) || coords.length === 0) return null;
            const depth = this.getDepth(coords);
            let geojson = null;

            if (depth === 2) {
                geojson = { type: 'Polygon', coordinates: [coords] };
            } else if (depth === 3) {
                geojson = { type: 'Polygon', coordinates: coords };
            } else if (depth === 4) {
                geojson = { type: 'MultiPolygon', coordinates: coords };
            } else if (depth >= 5) {
                let resultGeom = null;
                for (const item of coords) {
                    const g = this.toJSTS(item);
                    if (!g) continue;
                    if (!resultGeom) resultGeom = g;
                    else {
                        try {
                            resultGeom = resultGeom.buffer(0).union(g.buffer(0));
                        } catch(e) {
                            console.error('[Candy Paint] JSTS Union Error during read:', e);
                        }
                    }
                }
                return resultGeom;
            }

            if (!geojson) return null;

            try {
                return this.getReader().read(geojson);
            } catch (e) {
                console.error('[Candy Paint] JSTS Read Error:', e, geojson);
                return null;
            }
        },
        fromJSTS(geom) {
            if (!geom || geom.isEmpty()) return [];
            const geojson = this.getWriter().write(geom);
            if (geojson.type === 'MultiPolygon') {
                return geojson.coordinates;
            } else if (geojson.type === 'Polygon') {
                return [geojson.coordinates];
            } else if (geojson.type === 'GeometryCollection') {
                const out = [];
                for (const g of geojson.geometries) {
                    if (g.type === 'MultiPolygon') out.push(...g.coordinates);
                    else if (g.type === 'Polygon') out.push(g.coordinates);
                }
                return out;
            }
            return [];
        },
        /**
         * Single closed shell (possibly self-intersecting) → planar faces as separate Candy parts.
         * Not used when the part has interior rings (real holes) — those use buffer(0) only.
         */
        polygonizeClosedShellLonLatToParts(shellLonLat) {
            const j = window.jsts;
            if (!j || !shellLonLat || shellLonLat.length < 4) return null;
            const Polygonizer = j.operation && j.operation.polygonize && j.operation.polygonize.Polygonizer;
            const UnaryUnionOp = j.operation && j.operation.union && j.operation.union.UnaryUnionOp;
            const Coordinate = j.geom && j.geom.Coordinate;
            if (!Polygonizer || !UnaryUnionOp || !Coordinate) return null;
            try {
                const reader = this.getReader();
                const anchor = reader.read({ type: 'Point', coordinates: [shellLonLat[0][0], shellLonLat[0][1]] });
                const factory = anchor.getFactory();
                const coords = [];
                for (let k = 0; k < shellLonLat.length; k++) {
                    const p = shellLonLat[k];
                    coords.push(new Coordinate(p[0], p[1]));
                }
                if (coords.length > 1 && coords[0].equals2D(coords[coords.length - 1])) coords.pop();
                if (coords.length < 3) return null;
                const segs = [];
                for (let i = 0; i < coords.length; i++) {
                    const jn = (i + 1) % coords.length;
                    segs.push(factory.createLineString([coords[i], coords[jn]]));
                }
                const ml = factory.createMultiLineString(segs);
                const noded = UnaryUnionOp.union(ml);
                if (!noded || noded.isEmpty()) return null;
                const writer = this.getWriter();
                const minAbsArea = 1e-22;
                const extract = (checkRings) => {
                    const pol = new Polygonizer();
                    pol.setCheckRingsValid(checkRings);
                    pol.add(noded);
                    const polyList = pol.getPolygons();
                    if (!polyList || polyList.size() === 0) return [];
                    const parts = [];
                    for (let pi = 0; pi < polyList.size(); pi++) {
                        const pg = polyList.get(pi);
                        if (!pg || pg.isEmpty()) continue;
                        let a = 0;
                        try { a = Math.abs(pg.getArea()); } catch (e) { a = 1; }
                        if (a < minAbsArea) continue;
                        const gj = writer.write(pg);
                        if (gj.type === 'Polygon' && gj.coordinates && gj.coordinates.length) {
                            parts.push(gj.coordinates.map(ring => ring.map(c => [c[0], c[1]])));
                        }
                    }
                    return parts;
                };
                let parts = extract(true);
                if (parts.length === 0) parts = extract(false);
                return parts.length ? parts : null;
            } catch (e) {
                if (DEBUG) console.warn('[WME Candy Shop][polygonize] closed shell:', e);
                return null;
            }
        },
        combine(geoms, opName) {
            const valid = geoms.filter(g => g !== null && !g.isEmpty());
            if (valid.length === 0) return null;
            let result = valid[0];
            for (let i = 1; i < valid.length; i++) {
                try {
                    // Geometry precision issues can cause non-noded intersection topology exceptions.
                    // A tiny buffer(0) operation often snaps coordinates properly before applying the operation.
                    const a = result.buffer(0);
                    const b = valid[i].buffer(0);
                    result = a[opName](b);
                } catch (e) {
                    console.error(`[Candy Paint] JSTS ${opName} error:`, e);
                }
            }
            return result;
        },
        applyOp(opName, args) {
            if (args.length === 0) return [];
            const geoms = [];
            for (const arg of args) {
                if (arg == null) continue;
                const geom = this.toJSTS(arg);
                if (geom) geoms.push(geom);
            }

            if (geoms.length === 0) return [];
            if (geoms.length === 1 && (opName === 'union' || opName === 'symDifference')) {
                return this.fromJSTS(geoms[0]);
            }

            const resultGeom = this.combine(geoms, opName);
            return this.fromJSTS(resultGeom);
        }
    };

    const jstsWrapper = {
        union: (...args) => _jstsWrapperCache.applyOp('union', args),
        difference: (...args) => _jstsWrapperCache.applyOp('difference', args),
        intersection: (...args) => _jstsWrapperCache.applyOp('intersection', args),
        xor: (...args) => _jstsWrapperCache.applyOp('symDifference', args),
        /** True if JSTS geometries intersect with non-empty area (P10 commit vs intersecting parts). */
        geometriesIntersect(geomCoordsA, geomCoordsB) {
            try {
                const ga = _jstsWrapperCache.toJSTS(geomCoordsA);
                const gb = _jstsWrapperCache.toJSTS(geomCoordsB);
                if (!ga || !gb || ga.isEmpty() || gb.isEmpty()) return false;
                const inter = ga.buffer(0).intersection(gb.buffer(0));
                return inter != null && !inter.isEmpty();
            } catch (e) {
                return false;
            }
        },
        /** JSTS buffer(0) per Candy part (array of rings); MultiPolygon → multiple parts. */
        sanitizePartToParts(part) {
            if (!part || !Array.isArray(part) || part.length === 0) return [];
            try {
                if (part.length === 1 && part[0] && part[0].length >= 4) {
                    const polyParts = _jstsWrapperCache.polygonizeClosedShellLonLatToParts(part[0]);
                    if (polyParts && polyParts.length > 0) return polyParts;
                }
                const g = _jstsWrapperCache.toJSTS(part);
                if (!g || g.isEmpty()) return [];
                const fixed = g.buffer(0);
                if (!fixed || fixed.isEmpty()) return [];
                return _jstsWrapperCache.fromJSTS(fixed);
            } catch (e) {
                console.error('[Candy Paint] JSTS sanitizePartToParts error:', e);
                return [];
            }
        },
        bufferPath: (lonLatArray, radiusM, capSegments = 8) => {
            if (!lonLatArray || lonLatArray.length === 0) return [];
            const nRaw = lonLatArray.length;
            const refLon = lonLatArray.reduce((s, p) => s + p[0], 0) / nRaw;
            const refLat = lonLatArray.reduce((s, p) => s + p[1], 0) / nRaw;

            const localMeters = lonLatArray.map(p => lonLatToLocalMeters(p[0], p[1], refLon, refLat));
            
            const deduplicated = [];
            for (let i = 0; i < localMeters.length; i++) {
                if (i === 0 || Math.hypot(localMeters[i].x - deduplicated[deduplicated.length - 1][0], localMeters[i].y - deduplicated[deduplicated.length - 1][1]) > 1e-4) {
                    deduplicated.push([localMeters[i].x, localMeters[i].y]);
                }
            }
            if (deduplicated.length === 0) return [];
            
            let geojson;
            if (deduplicated.length === 1) {
                geojson = { type: 'Point', coordinates: deduplicated[0] };
            } else {
                geojson = { type: 'LineString', coordinates: deduplicated };
            }

            try {
                const geom = _jstsWrapperCache.getReader().read(geojson);
                const bufferedGeom = geom.buffer(radiusM, capSegments);
                const outputGeojson = _jstsWrapperCache.getWriter().write(bufferedGeom);
                
                const convertCoordinates = (coords, depth) => {
                    if (depth === 0) {
                        const lonLat = localMetersToLonLat(coords[0], coords[1], refLon, refLat);
                        return [lonLat[0], lonLat[1]];
                    }
                    return coords.map(c => convertCoordinates(c, depth - 1));
                };

                let result = [];
                if (outputGeojson.type === 'Polygon') {
                    result = [convertCoordinates(outputGeojson.coordinates, 2)];
                } else if (outputGeojson.type === 'MultiPolygon') {
                    result = convertCoordinates(outputGeojson.coordinates, 3);
                } else if (outputGeojson.type === 'GeometryCollection') {
                    for (const g of outputGeojson.geometries) {
                        if (g.type === 'MultiPolygon') {
                            result.push(...convertCoordinates(g.coordinates, 3));
                        } else if (g.type === 'Polygon') {
                            result.push(convertCoordinates(g.coordinates, 2));
                        }
                    }
                }
                return result;
            } catch (e) {
                console.error('[Candy Paint] JSTS bufferPath error:', e);
                return [];
            }
        }
    };

    function getGeometryEngine() {
        return window.jsts ? jstsWrapper : null;
    }

    function ensureGeometryEngine(done) {
        if (window.jsts) {
            done();
            return;
        }
        if (geometryEngineLoadCallbacks) {
            geometryEngineLoadCallbacks.push(done);
            return;
        }
        geometryEngineLoadCallbacks = [done];
        try {
            logToUI('Loading geometry engine (JSTS)…');
        } catch (e) { /* ignore */ }

        function drainSuccess() {
            geometryEngineLoadFailed = false;
            const cbs = geometryEngineLoadCallbacks;
            geometryEngineLoadCallbacks = null;
            if (cbs) cbs.forEach(fn => fn());
        }

        function tryLoad(urlIndex) {
            if (window.jsts) {
                drainSuccess();
                return;
            }
            if (urlIndex >= JSTS_SCRIPT_URLS.length) {
                geometryEngineLoadFailed = true;
                geometryEngineLoadCallbacks = null;
                console.error('[WME Candy Shop] Failed to load jsts from all sources:', JSTS_SCRIPT_URLS);
                try {
                    logToUI('Geometry engine failed to load (JSTS). Tools are disabled.', true);
                } catch (e) { /* ignore */ }
                initScriptOnce();
                return;
            }
            // WME Trusted Types workaround for `.src` / `.innerHTML` warnings (report-only)
            const s = document.createElement('script');
            const url = JSTS_SCRIPT_URLS[urlIndex];
            s.setAttribute('src', url);
            s.onload = drainSuccess;
            s.onerror = () => {
                if (s.parentNode) s.parentNode.removeChild(s);
                tryLoad(urlIndex + 1);
            };
            (document.head || document.documentElement).appendChild(s);
        }

        tryLoad(0);
    }

    /**
     * Host map adapter: WME OpenLayers map (`window.W.map`).
     * - Use this (not raw `W.map`) for resolution, projection, and map event registration so call sites stay in one place.
     * - `wmeSDK.Map` (wme-sdk-plus) provides high-level helpers such as `getMapCenter()`; it does not replace OL
     *   `getResolution` / `getProjectionObject` on the pinned build — those still come from `getHostMap()`.
     * - If `W.map` is missing, returns null; callers must handle null (fail-soft).
     */
    function getHostMap() {
        return window.W?.map ?? null;
    }

    function getMapResolution() {
        const map = getHostMap();
        return typeof map?.getResolution === 'function' ? map.getResolution() : null;
    }

    function getMapProjection() {
        const map = getHostMap();
        return typeof map?.getProjectionObject === 'function' ? map.getProjectionObject() : null;
    }

    function isImperialUnits() {
        return !!window.W?.prefs?.attributes?.isImperial;
    }

    function getHoveredLayerLabel() {
        const hi = window.W?.selectionManager?.hoveredItem;
        if (!hi) return 'None';
        const m = hi.model || hi;
        return m.attributes?.name || m.type || 'None';
    }

    function registerMapViewChange(handler) {
        const map = getHostMap();
        if (map?.events?.register) {
            map.events.register('move', null, handler);
            map.events.register('zoomend', null, handler);
        }
    }

    /** @returns {number|null} geodesic length in meters (WME OpenLayers), or null if unavailable */
    function hostGeodesicLineLengthMeters(lon1, lat1, lon2, lat2) {
        const OL = window.OpenLayers;
        const proj = getMapProjection();
        if (!OL?.Geometry?.Point || !OL?.Projection || !proj) return null;
        try {
            const p4326 = new OL.Projection('EPSG:4326');
            let a = new OL.Geometry.Point(lon1, lat1);
            let b = new OL.Geometry.Point(lon2, lat2);
            a.transform(p4326, proj);
            b.transform(p4326, proj);
            const line = new OL.Geometry.LineString([a, b]);
            return line.getGeodesicLength(proj);
        } catch (e) {
            return null;
        }
    }

    /**
     * @param {Array<{lon:number,lat:number}>} verticesLonLat closed or open ring in WGS84
     * @returns {{ area: number, perimeter: number, olPoly: object }|null}
     */
    function hostGeodesicPolygonAreaAndPerimeter(verticesLonLat) {
        const OL = window.OpenLayers;
        const proj = getMapProjection();
        if (!OL?.Geometry?.Point || !OL?.Projection || !proj || !verticesLonLat?.length) return null;
        try {
            const p4326 = new OL.Projection('EPSG:4326');
            const olPts = verticesLonLat.map(v => {
                const pt = new OL.Geometry.Point(v.lon, v.lat);
                pt.transform(p4326, proj);
                return pt;
            });
            if (olPts[0].x !== olPts[olPts.length - 1].x || olPts[0].y !== olPts[olPts.length - 1].y) {
                olPts.push(olPts[0].clone());
            }
            const ring = new OL.Geometry.LinearRing(olPts);
            const poly = new OL.Geometry.Polygon([ring]);
            return {
                area: poly.getGeodesicArea(proj),
                perimeter: ring.getGeodesicLength(proj),
                olPoly: poly
            };
        } catch (e) {
            return null;
        }
    }

    function isGeometryLibraryReady() {
        return !!getGeometryEngine() && !geometryEngineLoadFailed;
    }

    /** Visible notice when WME never reaches a ready state (complements console.error for users without DevTools). */
    function showBootstrapMaxAttemptsFailure() {
        if (document.getElementById('wme-candy-shop-bootstrap-fail')) return;
        const mount = () => {
            if (document.getElementById('wme-candy-shop-bootstrap-fail')) return;
            if (!document.body) {
                setTimeout(mount, 100);
                return;
            }
            const el = document.createElement('div');
            el.id = 'wme-candy-shop-bootstrap-fail';
            el.setAttribute('role', 'alert');
            el.textContent = 'WME Candy Shop: the Waze editor did not become ready in time. Reload the page or open the browser console for details.';
            el.style.cssText = [
                'position:fixed', 'bottom:12px', 'left:12px', 'max-width:min(420px,calc(100vw - 24px))',
                'z-index:2147483646', 'padding:10px 12px', 'font:12px/1.35 "Helvetica Neue",Helvetica,Arial,sans-serif',
                'color:#fff', 'background:#c0392b', 'border-radius:6px', 'box-shadow:0 2px 12px rgba(0,0,0,0.35)'
            ].join(';');
            document.body.appendChild(el);
        };
        mount();
    }

    // --- 1. BOOTSTRAP & WME SDK INITIALIZATION ---
    function bootstrap() {
        if (window.W && window.W.loginManager && window.W.loginManager.user && getHostMap()) {
            ensureGeometryEngine(initScriptOnce);
            return;
        }
        if (!wmeReadyListenerAdded) {
            wmeReadyListenerAdded = true;
            document.addEventListener('wme-ready', onWmeReady, { once: true });
        }
        bootstrapAttempts++;
        if (bootstrapAttempts > BOOTSTRAP_MAX_ATTEMPTS) {
            console.error('[WME Candy Shop] WME not ready after maximum bootstrap attempts.');
            showBootstrapMaxAttemptsFailure();
            return;
        }
        setTimeout(bootstrap, BOOTSTRAP_INTERVAL_MS);
    }

    function initScript() {
        wmeSDK = window.getWmeSdk({ scriptId: 'wme-candy-shop', scriptName: 'WME Candy Shop' });

        initPaintDocument();

        injectCanvasOverlay();
        createRefactoredUI();
        createTrackerHUD();

        wmeSDK.Events.on({ eventName: 'wme-selection-changed', eventHandler: debouncedHandleSelectionChange });
        /* Selection: wme-sdk-plus Events only; avoid duplicate W.selectionManager (same updates, double DOM work). */
        registerPaintHistorySdkLifecycleClear();

        registerMapViewChange(updateCanvasOnMapChange);

        const map = getHostMap();
        if (map && typeof map.updateSize === 'function') {
            setTimeout(() => {
                map.updateSize();
                window.dispatchEvent(new Event('resize'));
            }, 800);
        }

        requestAnimationFrame(trackFPS);
        logToUI('WME Candy Shop online.');
        if (!isGeometryLibraryReady()) {
            if (geometryEngineLoadFailed) {
                logToUI('Geometry library failed to load — Ingest/Apply and shape boolean tools are disabled.', true);
            } else {
                logToUI('Geometry library not available — Ingest/Apply and shape boolean tools are disabled.', true);
            }
        }
        applyGeometryDependencyToUI();
        handleSelectionChange();
        document.addEventListener('keydown', onDocumentModifierAndExperimentalKeydown, true);
        document.addEventListener('keyup', onDocumentModifierKeyup, true);
    }

    function trackFPS(now) {
        fpsFrames++;
        if (now - fpsLastTime >= 1000) {
            const fpsEl = document.getElementById('hud-fps-val');
            if (fpsEl) {
                fpsEl.innerText = fpsFrames;
                if (fpsFrames >= 45) fpsEl.style.color = '#26CC9A';
                else if (fpsFrames >= 25) fpsEl.style.color = '#FFC000';
                else fpsEl.style.color = '#FF5B5B';
            }
            fpsFrames = 0;
            fpsLastTime = now;
        }
        requestAnimationFrame(trackFPS);
    }

    // --- 2. STATE MACHINE MANAGERS ---
    /**
     * @param {string} newTool
     * @param {{ quiet?: boolean, preserveDrawingState?: boolean }} [opts] If quiet, skip default "Tool: …" log.
     *   preserveDrawingState: keep tempVertices / isDrawingShape (undo/redo restore with polygon in progress).
     */
    function changeTool(newTool, opts) {
        endBrushPainting();
        appState.tool = newTool;
        if (newTool !== 'link') pendingLinkVenueId = null;

        if (newTool === 'pan') {
            canvasElement.style.pointerEvents = appState.isDraftActive ? 'auto' : 'none';
            canvasElement.style.cursor = '';
        } else {
            canvasElement.style.pointerEvents = 'auto';
            if (newTool === 'brush' || newTool === 'eraser') {
                canvasElement.style.cursor = 'crosshair';
            } else {
                canvasElement.style.cursor = '';
            }
        }

        const preserveDrawing = opts && opts.preserveDrawingState;
        if (!preserveDrawing) {
            isDrawingShape = false;
            tempVertices = [];
        }
        hoveredTempVertexIndex = -1;
        draggingVertexIndex = -1;
        hoveredMasterPolyIndex = -1;

        document.querySelectorAll('.wpo-btn-tool').forEach(btn => {
            btn.classList.toggle('active', btn.getAttribute('data-tool') === newTool);
        });

        renderOptionsRow();
        requestRender();
        updateTrackerHUD();
        if (!opts || !opts.quiet) logToUI(`Tool: ${newTool}`);
    }

    function changeMode(newMode, opts) {
        if (!COMMIT_MODE_IDS.has(newMode)) return;
        appState.mode = newMode;
        document.querySelectorAll('.wpo-btn-mode').forEach(btn => {
            btn.classList.toggle('active', btn.getAttribute('data-val') === newMode);
        });
        requestRender();
        if (opts && opts.persistCommitMode) setPersistedCommitMode(newMode);
    }

    function setDraftActive(state) {
        appState.isDraftActive = state;
        if (!state) {
            draftAction = 'none';
            hoveredDraftVertexIndex = -1;
            draggingDraftVertexIndex = -1;
            hoveredDraftMidpointIndex = -1;
            draftVertices = [];
            draftInteriorRings = [];
            dragStartDraftRingsPx = null;
            draftPopInsertMasterIndex = -1;
            if (canvasElement && appState.tool === 'pan') canvasElement.style.pointerEvents = 'none';
        } else if (canvasElement) {
            canvasElement.style.pointerEvents = 'auto';
        }
        renderOptionsRow();
    }

    function applyGeometryDependencyToUI() {
        const ready = isGeometryLibraryReady();
        updateApplyButtonState();
        document.querySelectorAll('.wpo-btn-tool').forEach(btn => {
            const tool = btn.getAttribute('data-tool');
            const allowWithoutPolyClip = tool === 'pan' || tool === 'measure';
            if (!allowWithoutPolyClip) {
                btn.disabled = !ready;
                btn.style.opacity = !ready ? '0.4' : '';
                btn.style.pointerEvents = !ready ? 'none' : '';
            } else {
                btn.disabled = false;
                btn.style.opacity = '';
                btn.style.pointerEvents = '';
            }
        });
        if (!ready && appState.tool !== 'pan' && appState.tool !== 'measure') {
            changeTool('pan');
        }
    }

    function handleSelectionChange() {
        const models = getSelectedAreaVenues();
        const ingestBtn = document.getElementById('wpo-btn-ingest');
        const targetText = document.getElementById('wpo-target-text');
        const geomReady = isGeometryLibraryReady();

        if (appState.tool === 'link') {
            if (models.length === 1 && models[0].id != null && models[0].id !== '') {
                pendingLinkVenueId = normalizeVenueIdForSdk(models[0].id);
            } else {
                pendingLinkVenueId = null;
            }
        }

        appState.highlightedLinkIndex = -1;
        if (models.length === 1 && models[0].id != null && models[0].id !== '' && masterPolygons.length) {
            const sid = normalizeVenueIdForSdk(models[0].id);
            let hit = -1;
            for (let i = 0; i < shapePlaceLinks.length; i++) {
                const L = shapePlaceLinks[i];
                if (L && venueIdsMatch(L.venueId, sid) && isActiveNonBrokenLink(L)) {
                    hit = i;
                    break;
                }
            }
            if (hit < 0) {
                for (let i = 0; i < shapePlaceLinks.length; i++) {
                    const L = shapePlaceLinks[i];
                    if (L && venueIdsMatch(L.venueId, sid)) {
                        hit = i;
                        break;
                    }
                }
            }
            appState.highlightedLinkIndex = hit;
        }

        if (models.length > 0) {
            appState.targetVenueObj = models[0];
            const rawId = models[0].id;
            appState.targetVenueId = rawId != null && rawId !== '' ? normalizeVenueIdForSdk(rawId) : null;
            const label = appState.targetVenueId != null ? String(appState.targetVenueId) : '(no venue id)';
            targetText.innerHTML = `Target: <span style="color:#00A1F1; font-weight:bold;">${label}</span>`;
            ingestBtn.disabled = !geomReady;
            ingestBtn.style.opacity = geomReady ? '1' : '0.5';
            ingestBtn.style.cursor = geomReady ? 'pointer' : 'not-allowed';
        } else {
            appState.targetVenueObj = null;
            appState.targetVenueId = null;
            targetText.innerHTML = `Target: <em>None</em>`;
            ingestBtn.disabled = true;
            ingestBtn.style.opacity = '0.5';
            ingestBtn.style.cursor = 'not-allowed';
        }
        updateApplyButtonState();
        updateTrackerHUD();
        requestRender();
        scheduleAutoIngest();
    }

    function debouncedHandleSelectionChange() {
        if (selectionChangeDebounceTimer) clearTimeout(selectionChangeDebounceTimer);
        selectionChangeDebounceTimer = setTimeout(() => {
            selectionChangeDebounceTimer = null;
            handleSelectionChange();
        }, SELECTION_CHANGE_DEBOUNCE_MS);
    }

    // --- 3. UI GENERATION (HTML/CSS INJECTION) ---
    function injectStyles() {
        if(document.getElementById('wpo-styles')) return;
        const style = document.createElement('style');
        style.id = 'wpo-styles';
        style.innerHTML = `
            #wme-paint-palette { position: fixed; top: 100px; right: 50px; width: auto; min-width: 310px; z-index: 9999999; background: #ffffff; border: 1px solid #c2c9d1; border-radius: 6px; box-shadow: 0 4px 15px rgba(0,0,0,0.15); font-family: "Helvetica Neue", Helvetica, "Boing", Arial, sans-serif; display: flex; flex-direction: column; overflow: hidden; color: #333; white-space: nowrap; }
            .wpo-header { background: #00A1F1; color: #fff; padding: 5px 8px; font-size: 13px; font-weight: bold; cursor: move; user-select: none; display: flex; justify-content: space-between; align-items: center; }
            .wpo-hdr-btn { background: transparent; border: none; color: #fff; cursor: pointer; opacity: 0.7; margin-left: 6px; font-size: 13px; padding: 0 3px; }
            .wpo-hdr-btn:hover { opacity: 1; }
            .wpo-body { padding: 6px; display: flex; flex-direction: column; gap: 5px; }
            .wpo-row { display: flex; align-items: center; width: 100%; }
            .wpo-target-row { justify-content: space-between; border-bottom: 1px solid #e0e4e8; padding-bottom: 5px; }
            .wpo-tool-row { background: #f0f2f5; border-radius: 4px; padding: 3px; flex-wrap: nowrap; gap: 2px; border: 1px solid #e0e4e8; }
            .wpo-options-row { background: #f0f2f5; border-radius: 4px; padding: 3px 6px; min-height: 28px; border: 1px solid #e0e4e8; justify-content: space-between; transition: all 0.2s; white-space: normal; }
            .wpo-actions-row { justify-content: flex-end; gap: 3px; }
            .wpo-tool-group { display: flex; gap: 1px; border-right: 1px solid #c2c9d1; padding-right: 3px; margin-right: 2px; }
            .wpo-tool-group:last-child { border-right: none; padding-right: 0; margin-right: 0; }

            .wpo-btn { border: 1px solid transparent; background: transparent; padding: 4px 6px; cursor: pointer; border-radius: 4px; color: #495057; font-size: 13px; transition: all 0.15s ease-in-out; display: flex; align-items: center; justify-content: center; }
            .wpo-btn:hover:not(:disabled) { background: #e0e4e8; }
            .wpo-btn:disabled { opacity: 0.5; cursor: not-allowed; }
            .wpo-btn.active { background: #00A1F1; color: white; box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); border-color: #0084C9; }

            .wpo-icon-only { background: transparent; border: none; cursor: pointer; font-size: 16px; opacity: 0.7; transition: all 0.2s; display: flex; align-items: center; justify-content: center; padding: 2px 6px; }
            .wpo-icon-only:hover:not(:disabled) { opacity: 1; transform: scale(1.1); }
            .wpo-icon-only:disabled { opacity: 0.3 !important; cursor: not-allowed !important; color: #999 !important; transform: none !important; filter: grayscale(100%); }
            .wpo-icon-success { color: #26CC9A; }
            .wpo-icon-danger { color: #FF5B5B; }

            .wpo-btn-action { font-size: 11px; font-weight: bold; padding: 4px 8px; gap: 4px; border: 1px solid #c2c9d1; background: #fff; border-radius: 4px; }
            .wpo-btn-action:hover:not(:disabled) { background: #f4f6f8; }
            .wpo-btn-success { background: #26CC9A; color: white; border-color: #1FB386; }
            .wpo-btn-success:hover:not(:disabled) { background: #1FB386; }

            .wpo-slider-container { display: flex; align-items: center; gap: 5px; font-size: 11px; color: #555; font-weight: bold; min-width: 0; }
            .wpo-slider { width: 70px; margin: 0; flex: 0 0 auto; }
            .wpo-slider.wpo-slider--brush { width: 88px; min-width: 48px; flex: 1 1 72px; max-width: 140px; }
            .wpo-brush-options { display: flex; flex-flow: row nowrap; align-items: center; gap: 6px; margin-left: auto; max-width: 100%; justify-content: flex-end; overflow-x: auto; }
            .wpo-brush-sliders { display: flex; flex-flow: row nowrap; align-items: center; gap: 6px; flex: 1 1 auto; min-width: 0; }
            .wpo-brush-verts { font-size: 10px; color: #6c757d; font-weight: normal; line-height: 1.2; white-space: nowrap; flex-shrink: 0; }
            .wpo-brush-style-btn { padding: 3px 7px; min-width: 28px; }
            .wpo-brush-profile-btn { padding: 3px 5px; min-width: 24px; font-size: 12px; }
            .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
            #wme-paint-log { height: 60px; overflow-y: auto; background: #212529; color:#fff; border-radius: 3px; padding: 4px; font-family: monospace; font-size: 10px; margin-top: 2px; border: 1px inset #555; }

            @keyframes wpo-pulse { 0% { opacity: 1; } 50% { opacity: 0.2; } 100% { opacity: 1; } }
            .wpo-blinking-dot { display: inline-block; width: 8px; height: 8px; background-color: #FF5B5B; border-radius: 50%; animation: wpo-pulse 1.5s infinite; }
            .wpo-meta-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 6px; font-size: 10px; border-bottom: 1px solid #e8eaed; padding-bottom: 5px; margin-bottom: 2px; }
            #wpo-mode-chip { display: none; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: bold; border: 1px solid #c2c9d1; background: #f8f9fa; white-space: nowrap; }
            .wpo-auto-ingest-label { display: flex; align-items: center; gap: 4px; color: #495057; cursor: pointer; user-select: none; }
            .wpo-layer-strip { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 11px; border-bottom: 1px solid #e0e4e8; padding-bottom: 6px; margin-bottom: 4px; }
            .wpo-layer-strip select { flex: 1; min-width: 120px; max-width: 220px; font-size: 11px; }
            .wpo-places-block { margin-top: 2px; border: 1px solid #e0e4e8; border-radius: 4px; padding: 4px; max-height: 140px; overflow-y: auto; background: #fafbfc; }
            .wpo-places-block-h { font-size: 10px; font-weight: bold; color: #495057; margin-bottom: 4px; }
            .wpo-places-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 2px 0; border-bottom: 1px solid #eef0f2; }
            .wpo-places-row:last-child { border-bottom: none; }
            .wpo-places-row-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 10px; }
            .wpo-places-empty { font-size: 10px; color: #6c757d; padding: 4px; }

            #wme-cs-history-panel { position: fixed; top: 120px; left: 360px; width: 260px; max-height: min(56vh, 420px); z-index: 9999998;
                display: flex; flex-direction: column; background: #fff; border: 1px solid #cfd8dc; border-radius: 6px;
                box-shadow: 0 4px 16px rgba(0,0,0,0.12); font-family: sans-serif; font-size: 11px; }
            #wme-cs-history-panel.csh-hidden { display: none !important; }
            .csh-header { background: #5e35b1; color: #fff; padding: 6px 8px; font-weight: bold; cursor: move; user-select: none;
                display: flex; align-items: center; justify-content: space-between; border-radius: 5px 5px 0 0; }
            .csh-header-btns { display: flex; gap: 4px; }
            .csh-hdr-btn { background: transparent; border: none; color: #fff; cursor: pointer; opacity: 0.85; padding: 2px 6px; font-size: 12px; border-radius: 3px; }
            .csh-hdr-btn:hover { opacity: 1; background: rgba(255,255,255,0.12); }
            .csh-body { display: flex; flex-direction: column; min-height: 0; flex: 1; }
            .csh-hint { margin: 0; padding: 6px 8px; font-size: 9px; color: #6c757d; line-height: 1.35; border-bottom: 1px solid #e8eaed; }
            .csh-scroll { flex: 1; min-height: 60px; overflow-y: auto; padding: 4px 6px; }
            .csh-row { padding: 5px 8px; border-radius: 4px; margin-bottom: 2px; cursor: default; font-size: 10px; }
            .csh-row.csh-current { background: rgba(94, 53, 177, 0.12); font-weight: 600; color: #4527a0; border: 1px solid rgba(94, 53, 177, 0.25); }
            .csh-row.csh-past { cursor: pointer; border: 1px solid transparent; }
            .csh-row.csh-past:hover { background: #f0f2f5; border-color: #dee2e6; }
            .csh-lab { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            #wme-cs-layers-panel { position: fixed; top: 120px; left: 24px; width: 320px; max-height: min(72vh, 560px); z-index: 9999998;
                background: #fff; border: 1px solid #c2c9d1; border-radius: 6px; box-shadow: 0 4px 15px rgba(0,0,0,0.12);
                font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; display: flex; flex-direction: column; color: #333; font-size: 11px; }
            #wme-cs-layers-panel.csl-hidden { display: none !important; }
            .csl-header { background: #00838f; color: #fff; padding: 6px 8px; font-weight: bold; cursor: move; user-select: none;
                display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; border-radius: 5px 5px 0 0; }
            .csl-header-btns { display: flex; gap: 4px; }
            .csl-hdr-btn { background: transparent; border: none; color: #fff; cursor: pointer; opacity: 0.85; padding: 2px 6px; font-size: 12px; border-radius: 3px; }
            .csl-hdr-btn:hover { opacity: 1; background: rgba(255,255,255,0.12); }
            .csl-toolbar { display: flex; gap: 6px; align-items: center; padding: 6px 8px; border-bottom: 1px solid #e0e4e8; flex-wrap: wrap; }
            .csl-filter { flex: 1; min-width: 100px; font-size: 11px; padding: 3px 6px; border: 1px solid #c2c9d1; border-radius: 4px; }
            .csl-scroll { flex: 1; min-height: 80px; overflow-y: auto; padding: 4px 6px; }
            .csl-layer-block { border: 1px solid #e4e8ec; border-radius: 4px; margin-bottom: 6px; background: #fafbfc; }
            .csl-layer-head { display: flex; align-items: center; gap: 4px; padding: 4px 6px; background: #eef2f5; border-radius: 4px 4px 0 0; flex-wrap: wrap; }
            .csl-layer-head.csl-active-layer { box-shadow: inset 0 -2px 0 #00A1F1; }
            .csl-drag-h { cursor: grab; color: #6c757d; padding: 0 2px; user-select: none; }
            .csl-name { flex: 1; min-width: 60px; font-weight: 600; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .csl-name-input { flex: 1; min-width: 60px; font-size: 11px; padding: 2px 4px; }
            .csl-badge { font-size: 9px; color: #495057; background: #e9ecef; padding: 1px 4px; border-radius: 3px; max-width: 88px; overflow: hidden; text-overflow: ellipsis; }
            .csl-icon-btn { border: 1px solid #c2c9d1; background: #fff; border-radius: 3px; padding: 1px 5px; cursor: pointer; font-size: 10px; line-height: 1.3; color: #495057; }
            .csl-icon-btn:hover:not(:disabled) { background: #f0f2f5; }
            .csl-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
            .csl-shape-row { display: flex; align-items: center; gap: 4px; padding: 3px 6px 3px 10px; border-top: 1px solid #eef0f2; cursor: pointer; outline: none; }
            .csl-shape-row:focus-visible { box-shadow: inset 0 0 0 2px #00A1F1; }
            .csl-shape-row.csl-sel { background: rgba(0, 188, 212, 0.12); }
            .csl-shape-lab { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
            .csl-link-pill { font-size: 9px; padding: 1px 4px; border-radius: 3px; background: #dee2e6; flex-shrink: 0; }
            .csl-places-section { margin-top: 8px; padding-top: 6px; border-top: 2px solid #cfd8dc; }
            .csl-places-h { font-size: 10px; font-weight: bold; color: #495057; margin-bottom: 4px; }
            .csl-footer { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px 8px; border-top: 1px solid #e0e4e8; background: #f8f9fa; border-radius: 0 0 5px 5px; }
            .csl-footer .csl-icon-btn { font-size: 10px; }
            .csl-move-select { font-size: 10px; max-width: 92px; padding: 1px; border-radius: 3px; border: 1px solid #c2c9d1; }
            .csl-overflow-backdrop { position: fixed; inset: 0; z-index: 9999997; background: transparent; display: none; }
            .csl-overflow-backdrop.csl-show { display: block; }
            .csl-overflow-menu { position: absolute; z-index: 9999999; background: #fff; border: 1px solid #c2c9d1; border-radius: 4px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.15); min-width: 180px; display: none; padding: 4px 0; }
            .csl-overflow-menu.csl-show { display: block; }
            .csl-overflow-item { padding: 6px 12px; cursor: pointer; font-size: 11px; }
            .csl-overflow-item:hover { background: #f0f2f5; }
            .wpo-active-layer-readout { font-size: 10px; color: #495057; padding: 2px 0 4px; border-bottom: 1px solid #e8eaed; }
        `;
        document.head.appendChild(style);
    }

    function createRefactoredUI() {
        if(document.getElementById('wme-paint-palette')) return;
        injectStyles();

        const palette = document.createElement('div');
        palette.id = 'wme-paint-palette';

        palette.innerHTML = `
            <div id="wpo-header" class="wpo-header">
                <div><i class="fa fa-paint-brush"></i> WME Candy Shop</div>
                <div>
                    <button id="wpo-toggle-cs-history" type="button" class="wpo-hdr-btn" title="Toggle CS History (undo timeline)"><i class="fa fa-clock-o"></i></button>
                    <button id="wpo-toggle-cs-layers" type="button" class="wpo-hdr-btn" title="Toggle CS Layers panel (layers, shapes, Places)"><i class="fa fa-list"></i></button>
                    <button id="wpo-toggle-tracker" class="wpo-hdr-btn" title="Toggle Tracker"><i class="fa fa-bug"></i></button>
                    <button id="wpo-toggle-log" class="wpo-hdr-btn" title="Toggle Console"><i class="fa fa-terminal"></i></button>
                </div>
            </div>
            <div class="wpo-body">
                <div class="wpo-row wpo-target-row">
                    <div style="display:flex; gap: 4px; align-items:center;">
                        <button id="wpo-btn-undo" type="button" class="wpo-btn wpo-icon-only" disabled title="Undo"><i class="fa fa-undo"></i></button>
                        <button id="wpo-btn-redo" type="button" class="wpo-btn wpo-icon-only" disabled title="Redo"><i class="fa fa-repeat"></i></button>
                        <button id="wpo-btn-ingest" class="wpo-btn wpo-btn-action" title="Ingest selected WME Venue"><i class="fa fa-download"></i> Ingest</button>
                        <button id="wpo-btn-apply" class="wpo-btn wpo-btn-action wpo-btn-success" title="Apply shapes to WME"><i class="fa fa-check"></i> Apply</button>
                        <button id="wpo-btn-link-hover" type="button" class="wpo-btn wpo-icon-only" title="Link hovered shape to selected place, or use Link tool: select place first (remembered), then hover + click here"><i class="fa fa-link"></i></button>
                        <button id="wpo-btn-unlink-hover" type="button" class="wpo-btn wpo-icon-only" title="Unlink hovered shape"><i class="fa fa-chain-broken"></i></button>
                        <button id="wpo-btn-activate-link-hover" type="button" class="wpo-btn wpo-icon-only" title="Activate link: hovered shape becomes the active link for its venue (others inactive)"><i class="fa fa-star"></i></button>
                    </div>
                    <div id="wpo-target-text" style="font-size: 11px; color: #6c757d;">Target: <em>None</em></div>
                </div>
                <div class="wpo-meta-row">
                    <span id="wpo-mode-chip" title="How the next committed draft combines with the master layer"></span>
                    <div style="display:flex; flex-wrap:wrap; gap:10px; align-items:center; justify-content:flex-end;">
                        <label class="wpo-auto-ingest-label" title="When the active Candy layer has no shapes, ingest the selected polygon place automatically">
                            <input type="checkbox" id="wpo-auto-ingest" />
                            Auto-ingest
                        </label>
                        <label class="wpo-auto-ingest-label" title="Ctrl+Z / Ctrl+Y; V/P/M/L tools; Space hold=pan (restore on release). Off while typing in fields. May conflict with WME when on.">
                            <input type="checkbox" id="wpo-experimental-undo-keys" />
                            Hotkeys
                        </label>
                    </div>
                </div>

                <div class="wpo-active-layer-readout" id="wpo-active-layer-readout" title="WME map layers are separate from Candy Shop user layers. Open CS Layers to manage them.">
                    Active layer: <strong id="wpo-active-layer-name">—</strong>
                </div>

                <div class="wpo-row wpo-tool-row">
                    <div class="wpo-tool-group">
                        <button class="wpo-btn wpo-btn-tool active" data-tool="pan" title="Pan Map"><i class="fa fa-hand-paper-o"></i></button>
                    </div>
                    <div class="wpo-tool-group">
                        <button class="wpo-btn wpo-btn-tool" data-tool="measure" title="Measure Geodesic Distance"><i class="fa fa-arrows-h"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="move" title="Move Master Layer"><i class="fa fa-arrows"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="revert" title="Shape Shifter (Pop to Draft)"><i class="fa fa-history"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="link" title="Link hovered part to selected place (Shift+click: unlink)"><i class="fa fa-link"></i></button>
                    </div>
                    <div class="wpo-tool-group">
                        <button class="wpo-btn wpo-btn-tool" data-tool="rectangle" title="Rectangle"><i class="fa fa-square-o"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="ellipse" title="Ellipse"><i class="fa fa-circle-o"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="lasso" title="Lasso"><i class="fa fa-pencil"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="polygon" title="Polygon"><i class="fa fa-star-o"></i></button>
                    </div>
                    <div class="wpo-tool-group">
                        <button class="wpo-btn wpo-btn-tool" data-tool="brush" title="Paintbrush"><i class="fa fa-paint-brush"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="eraser" title="Eraser"><i class="fa fa-eraser"></i></button>
                        <button class="wpo-btn wpo-btn-tool" data-tool="wand" title="Magic Wand"><i class="fa fa-magic"></i></button>
                    </div>
                </div>

                <div id="wpo-options-row" class="wpo-row wpo-options-row"></div>

                <div class="wpo-row wpo-actions-row">
                    <button id="wpo-btn-clear" class="wpo-btn wpo-btn-action" title="Wipe entire canvas"><i class="fa fa-trash"></i> Clear</button>
                </div>
                <div id="wme-paint-log"></div>
            </div>
        `;
        document.body.appendChild(palette);

        const header = document.getElementById('wpo-header');
        let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
        header.addEventListener("mousedown", e => { if(e.target === header || e.target.parentNode === header) { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; isDragging = true; } });
        document.addEventListener("mouseup", () => { initialX = currentX; initialY = currentY; isDragging = false; });
        document.addEventListener("mousemove", e => {
            if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; palette.style.transform = `translate(${currentX}px, ${currentY}px)`; }
        });

        document.getElementById('wpo-toggle-tracker').onclick = () => {
            const trk = document.getElementById('wme-paint-hud');
            if(trk) trk.style.display = (trk.style.display === 'none') ? 'block' : 'none';
        };
        document.getElementById('wpo-toggle-log').onclick = () => {
            const lg = document.getElementById('wme-paint-log');
            if(lg) lg.style.display = (lg.style.display === 'none') ? 'block' : 'none';
        };

        document.getElementById('wpo-btn-undo').onclick = () => undo();
        document.getElementById('wpo-btn-redo').onclick = () => redo();
        document.getElementById('wpo-btn-ingest').onclick = ingestWmeSelection;
        document.getElementById('wpo-btn-apply').onclick = injectToWaze;
        document.getElementById('wpo-btn-clear').onclick = clearCanvasState;
        document.getElementById('wpo-btn-link-hover').onclick = () => tryLinkHoveredToSelection();
        document.getElementById('wpo-btn-activate-link-hover').onclick = () => tryActivateHoveredShapeLink();
        document.getElementById('wpo-btn-unlink-hover').onclick = () => tryUnlinkHovered();

        const autoIngestCb = document.getElementById('wpo-auto-ingest');
        if (autoIngestCb) {
            autoIngestCb.checked = getAutoIngest();
            autoIngestCb.onchange = () => setAutoIngest(autoIngestCb.checked);
        }
        const expUndoCb = document.getElementById('wpo-experimental-undo-keys');
        if (expUndoCb) {
            expUndoCb.checked = getExperimentalUndoKeys();
            expUndoCb.onchange = () => setExperimentalUndoKeys(expUndoCb.checked);
        }

        const tHist = document.getElementById('wpo-toggle-cs-history');
        if (tHist) tHist.onclick = () => toggleCsHistoryPanel();
        const tCs = document.getElementById('wpo-toggle-cs-layers');
        if (tCs) tCs.onclick = () => toggleCsLayersPanel();
        ensureCsHistoryPanel();
        ensureCsLayersPanel();
        rebuildLayerSelectDom();
        refreshPlacesPreviewFromMap();
        rebuildPlacesListDom();

        document.querySelectorAll('.wpo-btn-tool').forEach(btn => {
            btn.addEventListener('click', (e) => changeTool(e.currentTarget.getAttribute('data-tool')));
        });

        renderOptionsRow();
        updateUndoRedoButtons();
        scheduleCsHistoryRefresh();
    }

    function renderOptionsRow() {
        const container = document.getElementById('wpo-options-row');
        let optionsHtml = '';
        const shapeTools = ['rectangle', 'ellipse', 'polygon', 'lasso', 'move', 'revert'];
        const draftCapableTools = ['revert', 'rectangle', 'ellipse', 'lasso', 'polygon', 'wand'];

        if (appState.tool === 'pan') {
            optionsHtml = `<span style="font-size: 10px; color:#6c757d; text-align:right; flex:1;">Tip: Ingest a place, edit with Shape Shifter or draw tools, then Apply. Pan mode active.</span>`;
        } else if (appState.tool === 'measure') {
            optionsHtml = `<span style="font-size: 10px; color:#6c757d; text-align:right; flex:1;">Tip: Ingest → edit → Apply. Click and drag to measure distance.</span>`;
        } else if (appState.tool === 'move') {
            optionsHtml = `<span style="font-size: 10px; color:#6c757d; text-align:right; flex:1;">Click and drag to move the master layer.</span>`;
        } else if (appState.tool === 'revert' && !appState.isDraftActive) {
            optionsHtml = `<span style="font-size: 10px; color:#6c757d; text-align:right; flex:1;">Click a shape to pop it into Draft mode.</span>`;
        } else if (appState.tool === 'link' && !appState.isDraftActive) {
            optionsHtml = `<span style="font-size: 10px; color:#6c757d; text-align:right; flex:1;">Hover a part, then click to link the selected WME place. Shift+click or Alt+click to unlink.</span>`;
        } else if (shapeTools.includes(appState.tool)) {
            optionsHtml = `
                <div style="display:flex; gap: 1px; margin-left: auto;">
                    <button class="wpo-btn wpo-btn-mode ${appState.mode==='replace'?'active':''}" data-val="replace" title="Replace"><i class="fa fa-file-o"></i></button>
                    <button class="wpo-btn wpo-btn-mode ${appState.mode==='union'?'active':''}" data-val="union" title="Add / Union"><i class="fa fa-plus"></i></button>
                    <button class="wpo-btn wpo-btn-mode ${appState.mode==='difference'?'active':''}" data-val="difference" title="Subtract"><i class="fa fa-minus"></i></button>
                    <button class="wpo-btn wpo-btn-mode ${appState.mode==='intersection'?'active':''}" data-val="intersection" title="Intersect"><i class="fa fa-pie-chart"></i></button>
                    <button class="wpo-btn wpo-btn-mode ${appState.mode==='xor'?'active':''}" data-val="xor" title="XOR"><i class="fa fa-exchange"></i></button>
                </div>
            `;
        } else if (appState.tool === 'brush' || appState.tool === 'eraser') {
            const sagVal = appState.brushCircleSagittaM;
            const sidesD = brushCircleSidesRawAndClamped(appState.brushSize);
            const vertsLabel = brushVertexReadoutText(sidesD);
            const fhActive = appState.brushStyle === 'freehand';
            const lnActive = appState.brushStyle === 'line';
            const prof = appState.brushProfile;
            const mapA = appState.brushAlign === 'map';
            const strokeA = appState.brushAlign === 'stroke';
            const showAlign = brushProfileUsesAlignControl();
            const showCorner = prof === 'rounded_square';
            const showCurve = brushProfileShowsCurveSlider();
            const corVal = appState.brushCornerRatio;
            const pCircle = prof === 'circle' ? 'active' : '';
            const pSquare = prof === 'square' ? 'active' : '';
            const pDiamond = prof === 'diamond' ? 'active' : '';
            const pHex = prof === 'hexagon' ? 'active' : '';
            const pRound = prof === 'rounded_square' ? 'active' : '';
            optionsHtml = `
                <div class="wpo-brush-options">
                    <div role="group" aria-label="Brush stroke shape" style="display:flex; gap:2px; flex-shrink:0;">
                        <button type="button" class="wpo-btn wpo-brush-style-btn ${fhActive ? 'active' : ''}" id="wpo-brush-style-freehand"
                            title="Freehand: drag to paint along the pointer path" aria-pressed="${fhActive ? 'true' : 'false'}"><i class="fa fa-pencil" aria-hidden="true"></i><span class="sr-only"> Freehand</span></button>
                        <button type="button" class="wpo-btn wpo-brush-style-btn ${lnActive ? 'active' : ''}" id="wpo-brush-style-line"
                            title="Straight line: click and drag one segment" aria-pressed="${lnActive ? 'true' : 'false'}"><i class="fa fa-minus" aria-hidden="true"></i><span class="sr-only"> Straight line</span></button>
                    </div>
                    <div role="group" aria-label="Brush footprint shape" style="display:flex; gap:1px; flex-shrink:0;">
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${pCircle}" data-profile="circle" title="Circle: round stamp; JSTS buffer along stroke" aria-pressed="${prof === 'circle' ? 'true' : 'false'}"><i class="fa fa-circle-o"></i></button>
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${pSquare}" data-profile="square" title="Square: flat sides, map north/east or along stroke" aria-pressed="${prof === 'square' ? 'true' : 'false'}"><i class="fa fa-square-o"></i></button>
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${pDiamond}" data-profile="diamond" title="Diamond (square on point)" aria-pressed="${prof === 'diamond' ? 'true' : 'false'}"><i class="fa fa-certificate"></i></button>
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${pHex}" data-profile="hexagon" title="Regular hexagon (apothem = R)" aria-pressed="${prof === 'hexagon' ? 'true' : 'false'}"><span style="font-size:11px;line-height:1;">⬡</span></button>
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${pRound}" data-profile="rounded_square" title="Rounded square (local flat-earth corners; R = half-width)" aria-pressed="${prof === 'rounded_square' ? 'true' : 'false'}"><i class="fa fa-square"></i></button>
                    </div>
                    <div id="wpo-brush-align-wrap" role="group" aria-label="Square alignment" style="display:${showAlign ? 'flex' : 'none'}; gap:1px; flex-shrink:0;">
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${mapA ? 'active' : ''}" id="wpo-brush-align-map" title="Map: square edges along north / east" aria-pressed="${mapA ? 'true' : 'false'}"><i class="fa fa-compass"></i></button>
                        <button type="button" class="wpo-btn wpo-brush-profile-btn ${strokeA ? 'active' : ''}" id="wpo-brush-align-stroke" title="Along stroke: rotate with path direction" aria-pressed="${strokeA ? 'true' : 'false'}"><i class="fa fa-long-arrow-right"></i></button>
                    </div>
                    <div class="wpo-brush-sliders">
                        <div id="wpo-brush-corner-wrap" class="wpo-slider-container" style="display:${showCorner ? 'flex' : 'none'};" title="Corner radius as fraction of R (rounded square). Saved as ${LS_BRUSH_CORNER_RATIO}.">
                            <span title="Corner">Corner</span>
                            <input type="range" class="wpo-slider wpo-slider--brush" id="wpo-brush-corner" min="${BRUSH_CORNER_RATIO_MIN}" max="${BRUSH_CORNER_RATIO_MAX}" step="0.01" value="${corVal}">
                            <span id="wpo-brush-corner-val" style="width:36px; text-align:right; flex-shrink:0;">${Number(corVal).toFixed(2)}</span>
                        </div>
                        <div class="wpo-slider-container" title="Geodesic brush size R in meters: circle radius; square/hex apothem (center to flat edge). Saved as ${LS_BRUSH_SIZE_M}.">
                            <span title="Brush size">R</span>
                            <input type="range" class="wpo-slider wpo-slider--brush" id="wpo-brush-size" min="${BRUSH_METERS_MIN}" max="${BRUSH_METERS_MAX}" step="1" value="${appState.brushSize}">
                            <span id="wpo-brush-size-val" style="width:40px; text-align:right; flex-shrink:0;">${appState.brushSize} m</span>
                        </div>
                        <div id="wpo-brush-curve-wrap" class="wpo-slider-container" style="display:${showCurve ? 'flex' : 'none'};" title="Curve δ (m): sagitta for circles and rounded-square corner arcs. Saved as ${LS_BRUSH_CIRCLE_SAGITTA_M}.">
                            <span title="Curve">δ</span>
                            <input type="range" class="wpo-slider wpo-slider--brush" id="wpo-brush-sagitta" min="${BRUSH_CIRCLE_SAGITTA_MIN_M}" max="${BRUSH_CIRCLE_SAGITTA_MAX_M}" step="0.05" value="${sagVal}">
                            <span id="wpo-brush-sagitta-val" style="width:44px; text-align:right; flex-shrink:0;">${Number(sagVal).toFixed(2)} m</span>
                        </div>
                        <div class="wpo-brush-verts" id="wpo-brush-verts">${vertsLabel}</div>
                    </div>
                </div>
            `;
        } else if (appState.tool === 'wand') {
            optionsHtml = `
                <div class="wpo-slider-container" style="margin-left:auto;" title="Edge capture: max distance in screen pixels to polygon outline when not inside a fill (0 = inside only; 100 ≈ 40 px).">
                    <span>Tol:</span>
                    <input type="range" class="wpo-slider" id="wpo-wand-tol" min="0" max="100" value="${appState.wandTolerance}">
                    <span id="wpo-wand-tol-val" style="width:18px; text-align:right;">${appState.wandTolerance}</span>
                </div>
            `;
        }

        if (draftCapableTools.includes(appState.tool)) {
            let disabledAttr = appState.isDraftActive ? '' : 'disabled';
            let draftButtons = `
                <div style="display:flex; gap: 6px; border-left: 1px solid #c2c9d1; padding-left: 8px; margin-left: auto;">
                    <button class="wpo-icon-only wpo-icon-success" id="wpo-opt-finish" title="Commit Draft (Enter)" ${disabledAttr}><i class="fa fa-check"></i></button>
                    <button class="wpo-icon-only wpo-icon-danger" id="wpo-opt-cancel" title="Discard Draft (Esc)" ${disabledAttr}><i class="fa fa-times"></i></button>
                </div>
            `;
            container.innerHTML = `<div style="display:flex; align-items:center; width:100%; gap:6px; flex-wrap:wrap;">` + optionsHtml + draftButtons + `</div>`;

            document.getElementById('wpo-opt-finish').onclick = () => {
                if (appState.isDraftActive) { commitDraft(); requestRender(); updateTrackerHUD(); }
            };
            document.getElementById('wpo-opt-cancel').onclick = () => {
                if (appState.isDraftActive || revertOriginalPolygon || isDrawingShape || tempVertices.length > 0) {
                    cancelDraft(); requestRender(); updateTrackerHUD();
                }
            };
        } else {
            container.innerHTML = `<div style="display:flex; align-items:center; width:100%; gap:6px; flex-wrap:wrap;">${optionsHtml}</div>`;
        }

        container.querySelectorAll('.wpo-btn-mode').forEach(btn => {
            btn.onclick = (e) => {
                const v = e.currentTarget.getAttribute('data-val');
                changeMode(v, { persistCommitMode: true });
            };
        });
        if (document.getElementById('wpo-brush-size')) {
            const bs = document.getElementById('wpo-brush-size');
            const bsv = document.getElementById('wpo-brush-size-val');
            const syncBrush = () => {
                const v = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(bs.value) || BRUSH_METERS_MIN));
                appState.brushSize = v;
                writePersistedBrushSizeM(v);
                bs.value = String(v);
                bsv.innerText = v + ' m';
                updateModeChip();
                updateBrushVertexReadout();
                requestRender();
            };
            bs.oninput = syncBrush;
            bs.onchange = syncBrush;
            const syncBrushStyleButtons = () => {
                const fh = document.getElementById('wpo-brush-style-freehand');
                const ln = document.getElementById('wpo-brush-style-line');
                if (fh && ln) {
                    const isFh = appState.brushStyle === 'freehand';
                    fh.classList.toggle('active', isFh);
                    ln.classList.toggle('active', !isFh);
                    fh.setAttribute('aria-pressed', isFh ? 'true' : 'false');
                    ln.setAttribute('aria-pressed', !isFh ? 'true' : 'false');
                }
            };
            const setBrushStyle = (style) => {
                if (style !== 'freehand' && style !== 'line') return;
                appState.brushStyle = style;
                writePersistedBrushStyle(style);
                syncBrushStyleButtons();
            };
            const fhBtn = document.getElementById('wpo-brush-style-freehand');
            const lnBtn = document.getElementById('wpo-brush-style-line');
            if (fhBtn) {
                fhBtn.onclick = () => setBrushStyle('freehand');
                fhBtn.onkeydown = (e) => {
                    if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setBrushStyle('freehand'); }
                };
            }
            if (lnBtn) {
                lnBtn.onclick = () => setBrushStyle('line');
                lnBtn.onkeydown = (e) => {
                    if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setBrushStyle('line'); }
                };
            }
            syncBrushStyleButtons();
            updateBrushVertexReadout();

            document.querySelectorAll('.wpo-brush-profile-btn[data-profile]').forEach(btn => {
                btn.onclick = () => {
                    const id = btn.getAttribute('data-profile');
                    if (!BRUSH_PROFILE_IDS.includes(id)) return;
                    appState.brushProfile = id;
                    writePersistedBrushProfile(id);
                    renderOptionsRow();
                    updateModeChip();
                    requestRender();
                };
                btn.onkeydown = (e) => {
                    if (e.key === 'Enter' || e.key === ' ') {
                        e.preventDefault();
                        btn.click();
                    }
                };
            });
            const mapAlignBtn = document.getElementById('wpo-brush-align-map');
            const strokeAlignBtn = document.getElementById('wpo-brush-align-stroke');
            if (mapAlignBtn) {
                mapAlignBtn.onclick = () => {
                    appState.brushAlign = 'map';
                    writePersistedBrushAlign('map');
                    renderOptionsRow();
                    updateModeChip();
                    requestRender();
                };
            }
            if (strokeAlignBtn) {
                strokeAlignBtn.onclick = () => {
                    appState.brushAlign = 'stroke';
                    writePersistedBrushAlign('stroke');
                    renderOptionsRow();
                    updateModeChip();
                    requestRender();
                };
            }
            const cr = document.getElementById('wpo-brush-corner');
            const crv = document.getElementById('wpo-brush-corner-val');
            if (cr && crv) {
                const syncCorner = () => {
                    let v = Number(cr.value);
                    if (!Number.isFinite(v)) v = BRUSH_CORNER_RATIO_DEFAULT;
                    v = Math.max(BRUSH_CORNER_RATIO_MIN, Math.min(BRUSH_CORNER_RATIO_MAX, Math.round(v * 1000) / 1000));
                    appState.brushCornerRatio = v;
                    writePersistedBrushCornerRatio(v);
                    cr.value = String(v);
                    crv.innerText = v.toFixed(2);
                    updateBrushVertexReadout();
                    requestRender();
                };
                cr.oninput = syncCorner;
                cr.onchange = syncCorner;
            }
        }
        if (document.getElementById('wpo-brush-sagitta')) {
            const sg = document.getElementById('wpo-brush-sagitta');
            const sgv = document.getElementById('wpo-brush-sagitta-val');
            const syncSagitta = () => {
                let v = Number(sg.value);
                if (!Number.isFinite(v)) v = BRUSH_CIRCLE_SAGITTA_DEFAULT_M;
                v = Math.round(v * 20) / 20;
                v = Math.max(BRUSH_CIRCLE_SAGITTA_MIN_M, Math.min(BRUSH_CIRCLE_SAGITTA_MAX_M, v));
                appState.brushCircleSagittaM = v;
                writePersistedBrushCircleSagittaM(v);
                sg.value = String(v);
                sgv.innerText = v.toFixed(2) + ' m';
                updateBrushVertexReadout();
                requestRender();
            };
            sg.oninput = syncSagitta;
            sg.onchange = syncSagitta;
        }
        if (document.getElementById('wpo-wand-tol')) {
            document.getElementById('wpo-wand-tol').oninput = (e) => { appState.wandTolerance = e.target.value; document.getElementById('wpo-wand-tol-val').innerText = appState.wandTolerance; };
        }
    }

    function createTrackerHUD() {
        if(document.getElementById('wme-paint-hud')) return;
        const hud = document.createElement('div');
        hud.id = 'wme-paint-hud';
        hud.style.cssText = 'position:fixed; bottom:30px; right:350px; background:rgba(0,0,0,0.85); color:#00A1F1; padding:8px; font-family:monospace; font-size:11px; z-index:9999999; border-radius:4px; border:1px solid #00A1F1; min-width:210px; pointer-events:auto; display:block;';

        hud.innerHTML = `
            <div id="wme-paint-hud-header" style="font-weight:bold; margin-bottom:4px; color:#fff; font-size:11px; cursor:move; padding-bottom:3px; border-bottom:1px solid #555; display:flex; align-items:center;">
                <i class="fa fa-crosshairs" style="margin-right:4px;"></i> Geo-Math Tracker
                <div style="margin-left:auto; display:flex; align-items:center; gap:6px;">
                    <span style="font-size:9px; color:#888; font-weight:normal;">FPS: <span id="hud-fps-val" style="color:#26CC9A; font-weight:bold;">0</span></span>
                    <span class="wpo-blinking-dot"></span>
                </div>
            </div>
            <div style="color: #26CC9A; font-weight:bold; margin-top:4px;">-- LIVE DATA --</div>
            <div>Px: [<span id="hud-px" style="color:#fff;">0, 0</span>]</div>
            <div>Geo: [<span id="hud-gps" style="color:#fff;">0.00000, 0.00000</span>]</div>
            <div style="margin-top:2px;">Layer: <span id="hud-layer" style="color:#FFC000;">None</span></div>
            <div style="margin-top: 6px; border-top: 1px solid #00A1F1; padding-top: 4px;">
                <span style="color:#26CC9A; font-weight:bold;">-- GEOMETRY --</span>
                <div id="hud-geo-math" style="color: #FFC000; margin-top: 2px;"><em>No shape active</em></div>
            </div>
        `;
        document.body.appendChild(hud);

        const hudHeader = document.getElementById('wme-paint-hud-header');
        let isDraggingHud = false, curHudX, curHudY, initHudX, initHudY, xHudOffset = 0, yHudOffset = 0;
        hudHeader.addEventListener("mousedown", e => { initHudX = e.clientX - xHudOffset; initHudY = e.clientY - yHudOffset; isDraggingHud = true; });
        document.addEventListener("mouseup", () => { initHudX = curHudX; initHudY = curHudY; isDraggingHud = false; });
        document.addEventListener("mousemove", e => {
            if (isDraggingHud) { e.preventDefault(); curHudX = e.clientX - initHudX; curHudY = e.clientY - initHudY; xHudOffset = curHudX; yHudOffset = curHudY; hud.style.transform = `translate(${curHudX}px, ${curHudY}px)`; }
        });
    }

    // --- 4. CANVAS INJECTION & EVENTS ---
    function injectCanvasOverlay() {
        if (document.getElementById('wme-paint-overlay')) return;

        const canvasContainer = document.createElement('div');
        canvasContainer.id = 'wme-paint-overlay';
        canvasContainer.style.cssText = 'position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:' + CANVAS_OVERLAY_Z_INDEX + '; pointer-events:none;';

        canvasElement = document.createElement('canvas');
        canvasElement.id = 'native-paint-canvas';
        canvasElement.style.cssText = 'width:100%; height:100%; pointer-events:none;';

        canvasContainer.appendChild(canvasElement);
        document.body.appendChild(canvasContainer);
        ctx = canvasElement.getContext('2d');

        syncCanvasSize();
        setupDrawingEvents();
        window.addEventListener('resize', syncCanvasSize);
    }

    function syncCanvasSize() {
        if (!canvasElement) return;
        canvasElement.width = window.innerWidth;
        canvasElement.height = window.innerHeight;
        requestRender();
    }

    function updateCanvasOnMapChange() {
        refreshPlacesPreviewFromMap();
        rebuildPlacesListDom();
        requestRender();
        updateTrackerHUD();
    }

    function requestRender() {
        if (!renderFrameRequested) {
            renderFrameRequested = true;
            requestAnimationFrame(() => {
                renderCanvas();
                renderFrameRequested = false;
            });
        }
    }

    function scheduleLiveMouseHudUpdate() {
        const hud = document.getElementById('wme-paint-hud');
        if (!hud || hud.style.display === 'none') return;
        if (liveMouseHudFrameRequested) return;
        liveMouseHudFrameRequested = true;
        requestAnimationFrame(() => {
            liveMouseHudFrameRequested = false;
            updateLiveMouseHUD(currentMousePixel);
        });
    }

    function setupDrawingEvents() {
        // If mouseup happens outside the overlay canvas, the stroke/draw can get "stuck"
        // (canvas keeps capturing events and WME feels hung). Finalize globally.
        let lastGlobalPointerUpAt = 0;
        let lastGlobalBrushMoveAt = 0;

        const onGlobalBrushMove = (e) => {
            if (!brushPainting) return;
            if (appState.isDraftActive) return;
            if (!(appState.tool === 'brush' || appState.tool === 'eraser')) return;
            if (!e) return;

            const now = Date.now();
            // Limit cross-document sampling overhead; canvas mousemove still renders preview.
            if (now - lastGlobalBrushMoveAt < 12) return;
            lastGlobalBrushMoveAt = now;

            const x = e.clientX;
            const y = e.clientY;
            if (x == null || y == null) return;
            currentMousePixel = { x, y };
            scheduleLiveMouseHudUpdate();

            const g = globalPixelToGps(x, y);
            if (!g) return;
            const radiusM = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
            if (appState.brushStyle === 'line') {
                if (brushStrokeSamples.length < 2) brushStrokeSamples.push([g.lon, g.lat]);
                else brushStrokeSamples[1] = [g.lon, g.lat];
            } else {
                appendBrushSamplesAlongGeodesic(brushStrokeSamples, g.lon, g.lat, radiusM);
            }
        };
        const onGlobalPointerUp = (e) => {
            if (appState.tool === 'pan' && !appState.isDraftActive && draftAction === 'none' && draggingVertexIndex === -1) return;
            const now = Date.now();
            if (now - lastGlobalPointerUpAt < 40) return;
            lastGlobalPointerUpAt = now;

            // Avoid double-finalizing when the canvas handler will run.
            if (canvasElement && (e?.target === canvasElement)) return;

            if ((appState.tool === 'brush' || appState.tool === 'eraser') && brushPainting) {
                endBrushPainting();
                return;
            }

            if (draftAction !== 'none') {
                draftAction = 'none';
                draggingDraftVertexIndex = -1;
                dragStartDraftRingsPx = null;
                requestRender();
                updateTrackerHUD();
                return;
            }

            if (draggingVertexIndex !== -1) {
                draggingVertexIndex = -1;
                requestRender();
                updateTrackerHUD();
                return;
            }

            if (appState.tool === 'move') {
                isDrawingShape = false;
                dragStartMasterPixels = null;
                requestRender();
                updateTrackerHUD();
                return;
            }

            if (appState.tool === 'measure') {
                isDrawingShape = false;
                requestRender();
                updateTrackerHUD();
                return;
            }

            if (isDrawingShape && appState.tool !== 'polygon' && appState.tool !== 'revert') {
                isDrawingShape = false;
                if (tempVertices.length > 2) {
                    draftInteriorRings = [];
                    draftVertices = [...tempVertices];
                    setDraftActive(true);
                    logToUI("Draft ready.");
                }
                tempVertices = [];
                requestRender();
                updateTrackerHUD();
            }
        };

        document.addEventListener('mouseup', onGlobalPointerUp, true);
        document.addEventListener('pointerup', onGlobalPointerUp, true);
        document.addEventListener('mousemove', onGlobalBrushMove, true);
        document.addEventListener('pointermove', onGlobalBrushMove, true);
        window.addEventListener('blur', () => {
            if (brushPainting) endBrushPainting();
            if (spacePanActive) {
                spacePanActive = false;
                changeTool(spacePanRestoreTool, { quiet: true });
            }
        }, true);

        document.addEventListener('mousedown', function(e) {
            if (appState.tool !== 'pan') return;
            if (e.button != null && e.button !== 0) return;
            if (isCandyPaintUiRoot(e.target)) return;
            if (isTextEditingTarget(e.target)) return;
            if (tryPopShapeToDraftAt(e.clientX, e.clientY, e)) {
                e.preventDefault();
                e.stopPropagation();
            }
        }, true);

        canvasElement.addEventListener('wheel', function(e) {
            if (appState.tool === 'pan') return;
            e.preventDefault();
            canvasElement.style.pointerEvents = 'none';
            let target = document.elementFromPoint(e.clientX, e.clientY);
            let wheelEvent = new WheelEvent('wheel', { clientX: e.clientX, clientY: e.clientY, deltaY: e.deltaY, deltaX: e.deltaX, deltaMode: e.deltaMode, bubbles: true, cancelable: true });
            if (target) target.dispatchEvent(wheelEvent);
            canvasElement.style.pointerEvents = 'auto';
        }, { passive: false });

        // Pointer events + capture: fixes "first stroke is single stamp" and ensures we never miss drags.
        // Prevents mouse-compat events from duplicating strokes.
        canvasElement.style.touchAction = 'none';
        let pointerCaptureId = null;

        const pointerToGps = (e) => globalPixelToGps(e.clientX, e.clientY);

        canvasElement.addEventListener('pointerdown', function(e) {
            if (appState.tool === 'pan') return;
            if (e.button != null && e.button !== 0) return;
            const gps = pointerToGps(e);
            if (!gps) return;

            if ((appState.tool === 'brush' || appState.tool === 'eraser') && !appState.isDraftActive) {
                if (!isGeometryLibraryReady()) {
                    logToUI('Brush/eraser need jsts.', true);
                    return;
                }
                if (appState.tool === 'eraser' && masterPolygons.length === 0) return;
                brushPainting = true;
                brushIsEraser = appState.tool === 'eraser';
                brushStrokeSamples = [[gps.lon, gps.lat]];
                if (appState.brushStyle === 'line') brushStrokeSamples.push([gps.lon, gps.lat]);

                try {
                    pointerCaptureId = e.pointerId;
                    canvasElement.setPointerCapture(pointerCaptureId);
                } catch (err) { /* ignore */ }

                e.preventDefault();
                e.stopPropagation();
                requestRender();
                updateTrackerHUD();
                return;
            }
        }, true);

        canvasElement.addEventListener('pointermove', function(e) {
            currentMousePixel = { x: e.clientX, y: e.clientY };
            scheduleLiveMouseHudUpdate();
            if (appState.tool === 'pan') return;

            if ((appState.tool === 'brush' || appState.tool === 'eraser') && brushPainting && !appState.isDraftActive) {
                const g = pointerToGps(e);
                if (g) {
                    const radiusM = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
                    if (appState.brushStyle === 'line') {
                        if (brushStrokeSamples.length < 2) brushStrokeSamples.push([g.lon, g.lat]);
                        else brushStrokeSamples[1] = [g.lon, g.lat];
                    } else {
                        appendBrushSamplesAlongGeodesic(brushStrokeSamples, g.lon, g.lat, radiusM);
                    }
                }
                e.preventDefault();
                e.stopPropagation();
                requestRender();
                updateTrackerHUD();
                return;
            }
        }, true);

        canvasElement.addEventListener('pointerup', function(e) {
            if (appState.tool === 'pan') return;
            if ((appState.tool === 'brush' || appState.tool === 'eraser') && brushPainting) {
                endBrushPainting();
                try {
                    if (pointerCaptureId != null) canvasElement.releasePointerCapture(pointerCaptureId);
                } catch (err) { /* ignore */ }
                pointerCaptureId = null;
                e.preventDefault();
                e.stopPropagation();
                return;
            }
        }, true);

        canvasElement.addEventListener('mousedown', function(e) {
            // If pointer events are in play, avoid duplicate strokes from mouse-compat events.
            if (pointerCaptureId != null) return;

            if (appState.tool === 'pan' && tryPopShapeToDraftAt(e.clientX, e.clientY, e)) return;

            const gps = globalPixelToGps(e.clientX, e.clientY);
            if (!gps) return;
            if (appState.tool === 'pan' && !appState.isDraftActive) return;

            // Handle Active Draft Manipulations
            if (appState.isDraftActive) {
                let hit = getDraftHitRegion(e.clientX, e.clientY);
                if (hit) {
                    draftAction = hit;
                    if (hit === 'vertex') {
                        draggingDraftVertexIndex = hoveredDraftVertexIndex;
                    } else if (hit === 'midpoint') {
                        const insertIdx = hoveredDraftMidpointIndex + 1;
                        draftVertices.splice(insertIdx, 0, gps);
                        draggingDraftVertexIndex = insertIdx;
                        draftAction = 'vertex';
                    }
                    dragStartMouse = { x: e.clientX, y: e.clientY };
                    dragStartGlobalCoords = draftVertices.map(v => gpsToGlobalPixel(v.lon, v.lat));
                    dragStartDraftRingsPx = (hit === 'vertex' || hit === 'midpoint') ? null : captureDraftRingsPixelCoords();
                    dragStartBBox = getDraftBBox();
                    return;
                } else if (appState.tool !== 'revert' && appState.tool !== 'link') {
                    // Commit only: do not fall through — otherwise tempVertices/isDrawingShape
                    // stay set and replace-mode rendering hides the master layer (near-transparent).
                    commitDraft();
                    requestRender();
                    updateTrackerHUD();
                    return;
                } else {
                    return;
                }
            }

            // Move Tool Logic
            if (appState.tool === 'move') {
                if (masterPolygons.length > 0) {
                    commitBeforeChange('move');
                    isDrawingShape = true;
                    dragStartMouse = { x: e.clientX, y: e.clientY };
                    dragStartMasterPixels = masterPolygons.map(poly =>
                        poly.map(ring =>
                            ring.map(pt => gpsToGlobalPixel(pt[0], pt[1]))
                        )
                    );
                }
            }
            // Shape Shifter (Revert) Logic
            else if (appState.tool === 'revert' && !appState.isDraftActive) {
                if (hoveredMasterPolyIndex !== -1 && masterPolygons[hoveredMasterPolyIndex]) {
                    commitBeforeChange('pop_draft');
                    alignShapeLinksToMaster();
                    draftPopInsertMasterIndex = hoveredMasterPolyIndex;
                    revertOriginalLink = shapePlaceLinks.splice(hoveredMasterPolyIndex, 1)[0] ?? null;
                    const poppedPoly = masterPolygons.splice(hoveredMasterPolyIndex, 1)[0];
                    revertOriginalPolygon = [poppedPoly];
                    assignDraftFromPoppedMasterPart(poppedPoly);
                    setDraftActive(true);
                    logToUI('Change to Transform Tool.');
                }
            } else if (appState.tool === 'link' && !appState.isDraftActive) {
                if (hoveredMasterPolyIndex >= 0 && masterPolygons[hoveredMasterPolyIndex]) {
                    if (e.shiftKey || e.altKey) {
                        tryUnlinkHovered();
                    } else {
                        tryLinkHoveredToSelection();
                    }
                }
            } else if (appState.tool === 'wand' && !appState.isDraftActive) {
                if (!isGeometryLibraryReady()) {
                    logToUI('Magic wand needs the geometry library (JSTS).', true);
                    return;
                }
                const tolPx = wandToleranceToPixels();
                const widx = pickMasterPolygonAtWithTolerance(e.clientX, e.clientY, tolPx);
                if (widx < 0) {
                    logToUI('Magic wand: no shape near the pointer (increase tolerance to snap to edges).', false);
                    return;
                }
                const Lw = getActiveUserLayer();
                if (!Lw) return;
                ensureShapeIdsForLayer(Lw);
                const sidW = Lw.shapeIds[widx];
                csLayersSelection.clear();
                csLayersSelection.add(csShapeKey(Lw.id, sidW));
                popActiveLayerShapeToDraftFromIndex(widx);
                return;
            } else if ((appState.tool === 'brush' || appState.tool === 'eraser') && !appState.isDraftActive) {
                if (!isGeometryLibraryReady()) {
                    logToUI('Brush/eraser need jsts.', true);
                    return;
                }
                if (appState.tool === 'eraser' && masterPolygons.length === 0) return;
                brushPainting = true;
                brushIsEraser = appState.tool === 'eraser';
                brushStrokeSamples = [[gps.lon, gps.lat]];
                if (appState.brushStyle === 'line') brushStrokeSamples.push([gps.lon, gps.lat]);
                requestRender();
                updateTrackerHUD();
                return;
            }
            // Standard Tools
            else if (appState.tool === 'polygon') {
                if (hoveredTempVertexIndex !== -1) {
                    draggingVertexIndex = hoveredTempVertexIndex;
                } else {
                    tempVertices.push(gps);
                }
            } else if (appState.tool === 'measure') {
                tempVertices = [gps, gps];
                isDrawingShape = true;
            } else if (appState.tool !== 'brush' && appState.tool !== 'eraser' && appState.tool !== 'wand') {
                isDrawingShape = true;
                tempVertices = [gps];
                dragStartMouse = { x: e.clientX, y: e.clientY };
            }
            requestRender();
            updateTrackerHUD();
        });

        canvasElement.addEventListener('mousemove', function(e) {
            if (pointerCaptureId != null) return;
            currentMousePixel = { x: e.clientX, y: e.clientY };
            scheduleLiveMouseHudUpdate();

            if (appState.tool === 'pan' && !appState.isDraftActive) return;

            if ((appState.tool === 'brush' || appState.tool === 'eraser') && !appState.isDraftActive && draftAction === 'none') {
                canvasElement.style.cursor = 'crosshair';
            }

            if (draftAction !== 'none') {
                executeDraftTransform(e.clientX, e.clientY);
            } else if ((appState.tool === 'brush' || appState.tool === 'eraser') && brushPainting && !appState.isDraftActive) {
                canvasElement.style.cursor = 'crosshair';
                const g = globalPixelToGps(e.clientX, e.clientY);
                if (g) {
                    const radiusM = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
                    if (appState.brushStyle === 'line') {
                        if (brushStrokeSamples.length < 2) brushStrokeSamples.push([g.lon, g.lat]);
                        else brushStrokeSamples[1] = [g.lon, g.lat];
                    } else {
                        appendBrushSamplesAlongGeodesic(brushStrokeSamples, g.lon, g.lat, radiusM);
                    }
                }
            } else if (appState.tool === 'polygon' && !appState.isDraftActive && tempVertices.length > 0) {
                const g = globalPixelToGps(e.clientX, e.clientY);
                if (draggingVertexIndex !== -1 && g) {
                    tempVertices[draggingVertexIndex] = g;
                    canvasElement.style.cursor = 'grabbing';
                } else {
                    hoveredTempVertexIndex = -1;
                    for (let i = 0; i < tempVertices.length; i++) {
                        const px = gpsToGlobalPixel(tempVertices[i].lon, tempVertices[i].lat);
                        if (px) {
                            const dx = e.clientX - px.x;
                            const dy = e.clientY - px.y;
                            if (dx * dx + dy * dy <= VERTEX_HIT_RADIUS_SQ) {
                                hoveredTempVertexIndex = i;
                                break;
                            }
                        }
                    }
                    canvasElement.style.cursor = hoveredTempVertexIndex !== -1 ? 'grab' : 'crosshair';
                }
            } else if (appState.isDraftActive) {
                let hit = getDraftHitRegion(e.clientX, e.clientY);
                canvasElement.style.cursor = getCursorForHit(hit);
            } else if (appState.tool === 'move' && isDrawingShape && dragStartMasterPixels) {
                let dx = e.clientX - dragStartMouse.x;
                let dy = e.clientY - dragStartMouse.y;
                masterPolygons = dragStartMasterPixels.map(poly =>
                    poly.map(ring =>
                        ring.map(px => {
                            if(!px) return [0,0];
                            let newGps = globalPixelToGps(px.x + dx, px.y + dy);
                            return newGps ? [newGps.lon, newGps.lat] : [0,0];
                        })
                    )
                );
                flushBrushGeometryToActiveLayer();
            } else if (appState.tool === 'revert' && !appState.isDraftActive) {
                hoveredMasterPolyIndex = pickMasterPolygonAt(currentMousePixel.x, currentMousePixel.y);
                canvasElement.style.cursor = hoveredMasterPolyIndex >= 0 ? 'pointer' : 'crosshair';
            } else if (appState.tool === 'link' && !appState.isDraftActive) {
                hoveredMasterPolyIndex = pickMasterPolygonAt(currentMousePixel.x, currentMousePixel.y);
                canvasElement.style.cursor = hoveredMasterPolyIndex >= 0 ? 'pointer' : 'crosshair';
            } else if (isDrawingShape) {
                const gps = globalPixelToGps(e.clientX, e.clientY);
                const startGps = globalPixelToGps(dragStartMouse ? dragStartMouse.x : e.clientX, dragStartMouse ? dragStartMouse.y : e.clientY);

                if (gps && appState.tool === 'measure') {
                    tempVertices[1] = gps;
                } else if (gps && startGps) {
                    const shiftM = !!(e.shiftKey || isShiftDown);
                    const altM = !!(e.altKey || isAltDown);
                    if (appState.tool === 'rectangle') {
                        tempVertices = rectangleVerticesFromDrag(startGps, gps, { square: shiftM, fromCenter: altM });
                    } else if (appState.tool === 'ellipse') {
                        tempVertices = ellipseVerticesFromDrag(startGps, gps, { square: shiftM, fromCenter: !altM });
                    }
                    else if (appState.tool === 'lasso') {
                        let last = tempVertices[tempVertices.length-1];
                        if (Math.abs(gps.lon - last.lon) > 0.00005 || Math.abs(gps.lat - last.lat) > 0.00005) {
                            tempVertices.push(gps);
                        }
                    }
                }
            }
            requestRender();
            if (isDrawingShape || draftAction !== 'none') updateTrackerHUD();
        });

        canvasElement.addEventListener('mouseup', function(e) {
            if (appState.tool === 'pan' && !appState.isDraftActive) return;
            if (pointerCaptureId != null) return;

            if ((appState.tool === 'brush' || appState.tool === 'eraser') && brushPainting) {
                endBrushPainting();
                updateTrackerHUD();
                return;
            }
            if (draftAction !== 'none') {
                draftAction = 'none';
                draggingDraftVertexIndex = -1;
                dragStartDraftRingsPx = null;
            } else if (draggingVertexIndex !== -1) {
                draggingVertexIndex = -1;
            } else if (appState.tool === 'move') {
                isDrawingShape = false;
                dragStartMasterPixels = null;
            } else if (appState.tool === 'measure') {
                isDrawingShape = false;
            } else if (isDrawingShape && appState.tool !== 'polygon' && appState.tool !== 'revert') {
                isDrawingShape = false;
                if (tempVertices.length > 2) {
                    draftInteriorRings = [];
                    draftVertices = [...tempVertices];
                    setDraftActive(true);
                    logToUI("Draft ready.");
                }
                tempVertices = [];
            }
            requestRender();
            updateTrackerHUD();
        });

        canvasElement.addEventListener('dblclick', function(e) {
            if (appState.tool === 'pan' && !appState.isDraftActive) return;
            if (appState.tool === 'polygon' && tempVertices.length > 2) {
                draftInteriorRings = [];
                draftVertices = [...tempVertices];
                setDraftActive(true);
                tempVertices = [];
                logToUI("Polygon drafted.");
                requestRender();
                updateTrackerHUD();
            }
        });

        window.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                cancelDraft(); requestRender(); updateTrackerHUD();
            } else if (e.key === 'Enter' && appState.isDraftActive) {
                const tEnter = e.target;
                if (tEnter && (tEnter.tagName === 'INPUT' || tEnter.tagName === 'TEXTAREA' || tEnter.tagName === 'SELECT' || tEnter.isContentEditable)) return;
                e.preventDefault();
                e.stopPropagation();
                commitDraft(); requestRender(); updateTrackerHUD();
            } else if ((e.key === 'd' || e.key === 'D') && appState.isDraftActive && hoveredDraftVertexIndex !== -1) {
                const t = e.target;
                if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return;
                e.preventDefault();
                if (draftVertices.length > 3) {
                    draftVertices.splice(hoveredDraftVertexIndex, 1);
                    hoveredDraftVertexIndex = -1;
                    hoveredDraftMidpointIndex = -1;
                    requestRender();
                    updateTrackerHUD();
                } else {
                    e.preventDefault();
                    logToUI("Cannot delete vertex: polygon must have at least 3 points.", true);
                }
            }
        });
    }

    // --- 5. LOGIC HELPERS & PURE MATH ---
    function logToUI(msg, isError = false) {
        const logBox = document.getElementById('wme-paint-log');
        if (logBox) {
            const time = new Date().toLocaleTimeString([], { hour12: false });
            const color = isError ? '#FF5B5B' : '#26CC9A';
            const row = document.createElement('div');
            row.style.color = color;
            const ts = document.createElement('span');
            ts.style.color = '#888';
            ts.textContent = `[${time}] `;
            row.appendChild(ts);
            row.appendChild(document.createTextNode(String(msg)));
            logBox.appendChild(row);
            logBox.scrollTop = logBox.scrollHeight;
        }
        if (DEBUG) console.log(`[WME Candy Shop] ${msg}`);
    }

    function cancelDraft() {
        endBrushPainting();
        hoveredTempVertexIndex = -1;
        draggingVertexIndex = -1;
        draftAction = 'none';
        hoveredDraftVertexIndex = -1;
        draggingDraftVertexIndex = -1;
        hoveredDraftMidpointIndex = -1;
        dragStartDraftRingsPx = null;
        if (isDrawingShape || tempVertices.length > 0) {
            isDrawingShape = false; tempVertices = [];
        } else if (appState.isDraftActive) {
            if (revertOriginalPolygon) {
                commitBeforeChange('draft_cancel_restore');
                let insertIdx = draftPopInsertMasterIndex >= 0 && draftPopInsertMasterIndex <= masterPolygons.length
                    ? draftPopInsertMasterIndex
                    : masterPolygons.length;
                    
                masterPolygons.splice(insertIdx, 0, ...revertOriginalPolygon);
                if (revertOriginalPolygon.length) {
                    const L = cloneLinkEntry(revertOriginalLink);
                    shapePlaceLinks.splice(insertIdx, 0, ...revertOriginalPolygon.map((_, i) => i === 0 ? L : null));
                }
                logToUI("Shape restored.");
            } else {
                logToUI("Draft cancelled.");
            }
            revertOriginalPolygon = null;
            revertOriginalLink = null;
            setDraftActive(false);
        } else if (appState.tool !== 'pan') {
            changeTool('pan');
        }
    }

    function clearCanvasState() {
        endBrushPainting();
        commitBeforeChange('clear');
        csLayersSelection.clear();
        userLayers = [];
        activeLayerId = createUserLayer(DEFAULT_SHAPE_LAYER_NAME).id;
        syncActiveLayerAliases();
        tempVertices = []; draftVertices = []; draftInteriorRings = [];
        hoveredTempVertexIndex = -1;
        draggingVertexIndex = -1;
        hoveredDraftVertexIndex = -1;
        draggingDraftVertexIndex = -1;
        hoveredDraftMidpointIndex = -1;
        revertOriginalPolygon = null;
        revertOriginalLink = null;
        draftPopInsertMasterIndex = -1;
        setDraftActive(false); isDrawingShape = false;
        rebuildLayerSelectDom();
        refreshPlacesPreviewFromMap();
        rebuildPlacesListDom();
        scheduleDocumentSave();
        requestRender(); updateTrackerHUD();
        logToUI('Canvas cleared (one default layer).');
    }

    function formatLength(meters, isImperial) {
        if (isImperial) {
            let ft = meters * 3.28084;
            return (ft > 5280) ? (ft / 5280).toFixed(2) + ' mi' : ft.toFixed(1) + ' ft';
        } else {
            return (meters > 1000) ? (meters / 1000).toFixed(2) + ' km' : meters.toFixed(1) + ' m';
        }
    }

    function formatArea(sqMeters, isImperial) {
        if (isImperial) return Math.round(sqMeters * 10.7639).toLocaleString() + ' ft²';
        return Math.round(sqMeters).toLocaleString() + ' m²';
    }

    function getPixelArea(pts) {
        let area = 0;
        for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
            area += (pts[j].x + pts[i].x) * (pts[j].y - pts[i].y);
        }
        return Math.abs(area / 2);
    }

    function wgs84ToMercator(lon, lat) {
        const x = (lon * 20037508.34) / 180;
        let y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180);
        return { x: x, y: (y * 20037508.34) / 180 };
    }

    function mercatorToWgs84(x, y) {
        const lon = (x / 20037508.34) * 180;
        const y_deg = (y / 20037508.34) * 180;
        return { lon: lon, lat: (180 / Math.PI) * (2 * Math.atan(Math.exp(y_deg * Math.PI / 180)) - Math.PI / 2) };
    }

    /**
     * Screen ↔ WGS84 for the visible map viewport (overlay canvas uses document pixels).
     * Assumptions: WME keeps the editor map in `#map`; center matches `wmeSDK.Map.getMapCenter()`; scale matches
     * `getHostMap().getResolution()` for that same view. Uses spherical Web Mercator math (wgs84ToMercator) aligned
     * with WME’s projection — if any of map div / SDK / OL map / resolution is missing, returns null.
     */
    function globalPixelToGps(globalX, globalY) {
        const mapDiv = document.getElementById('map');
        if (!mapDiv || !getHostMap() || !wmeSDK) return null;

        const centerGps = wmeSDK.Map.getMapCenter();
        const res = getMapResolution();
        if (!centerGps || !res) return null;

        const mapRect = mapDiv.getBoundingClientRect();
        const localX = globalX - mapRect.left;
        const localY = globalY - mapRect.top;

        const centerMerc = wgs84ToMercator(centerGps.lon, centerGps.lat);
        const mapW = mapDiv.clientWidth;
        const mapH = mapDiv.clientHeight;

        const targetMercX = centerMerc.x + ((localX - (mapW / 2)) * res);
        const targetMercY = centerMerc.y - ((localY - (mapH / 2)) * res);

        return mercatorToWgs84(targetMercX, targetMercY);
    }

    /** Inverse of globalPixelToGps; same `#map` + center + resolution coupling. Returns null if inputs unavailable. */
    function gpsToGlobalPixel(lon, lat) {
        const mapDiv = document.getElementById('map');
        if (!mapDiv || !getHostMap() || !wmeSDK) return null;

        const centerGps = wmeSDK.Map.getMapCenter();
        const res = getMapResolution();
        if (!centerGps || !res) return null;

        const targetMerc = wgs84ToMercator(lon, lat);
        const centerMerc = wgs84ToMercator(centerGps.lon, centerGps.lat);

        const mapW = mapDiv.clientWidth;
        const mapH = mapDiv.clientHeight;

        const localX = (mapW / 2) + ((targetMerc.x - centerMerc.x) / res);
        const localY = (mapH / 2) - ((targetMerc.y - centerMerc.y) / res);

        const mapRect = mapDiv.getBoundingClientRect();
        return { x: localX + mapRect.left, y: localY + mapRect.top };
    }

    /** Viewport of `#map` in document pixels — clip canvas drawing so paint never covers WME chrome. */
    function getMapClipRect() {
        const el = document.getElementById('map');
        if (!el) return null;
        const r = el.getBoundingClientRect();
        if (r.width < 4 || r.height < 4) return null;
        return { x: r.left, y: r.top, w: r.width, h: r.height };
    }

    const EARTH_RADIUS_M = 6371008.8;

    function haversineMeters(lon1, lat1, lon2, lat2) {
        const r = Math.PI / 180;
        const dLat = (lat2 - lat1) * r;
        const dLon = (lon2 - lon1) * r;
        const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * r) * Math.cos(lat2 * r) * Math.sin(dLon / 2) ** 2;
        return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(a)));
    }

    function geoBearingRad(lon1, lat1, lon2, lat2) {
        const r = Math.PI / 180;
        const y = Math.sin((lon2 - lon1) * r) * Math.cos(lat2 * r);
        const x = Math.cos(lat1 * r) * Math.sin(lat2 * r) - Math.sin(lat1 * r) * Math.cos(lat2 * r) * Math.cos((lon2 - lon1) * r);
        return Math.atan2(y, x);
    }

    function destinationPointMeters(lon, lat, bearingRad, distM) {
        const r = Math.PI / 180;
        const lat1 = lat * r;
        const lon1 = lon * r;
        const ang = distM / EARTH_RADIUS_M;
        const lat2 = Math.asin(Math.sin(lat1) * Math.cos(ang) + Math.cos(lat1) * Math.sin(ang) * Math.cos(bearingRad));
        const lon2 = lon1 + Math.atan2(Math.sin(bearingRad) * Math.sin(ang) * Math.cos(lat1), Math.cos(ang) - Math.sin(lat1) * Math.sin(lat2));
        let lonDeg = lon2 * 180 / Math.PI;
        lonDeg = ((lonDeg + 540) % 360) - 180;
        return { lon: lonDeg, lat: lat2 * 180 / Math.PI };
    }

    function interpolateGeodesicLonLat(lon1, lat1, lon2, lat2, t) {
        const d = haversineMeters(lon1, lat1, lon2, lat2);
        if (d < 1e-6) return [lon2, lat2];
        const br = geoBearingRad(lon1, lat1, lon2, lat2);
        const p = destinationPointMeters(lon1, lat1, br, d * t);
        return [p.lon, p.lat];
    }

    /**
     * Inscribed regular N-gon on geodesic circle radius R; max distance from true circle to edge midpoints ≤ δ (sagitta).
     * @returns {{ n: number, nRaw: number }} n clamped to [BRUSH_CIRCLE_SIDES_MIN, BRUSH_CIRCLE_SIDES_MAX]; nRaw before clamp.
     */
    function brushCircleSidesRawAndClamped(radiusM) {
        const R = Math.max(1e-9, Number(radiusM) || 0);
        const delta = Math.max(
            BRUSH_CIRCLE_SAGITTA_MIN_M,
            Math.min(BRUSH_CIRCLE_SAGITTA_MAX_M, Number(appState.brushCircleSagittaM) || BRUSH_CIRCLE_SAGITTA_DEFAULT_M)
        );
        const x = 1 - delta / R;
        const clamped = Math.max(-1, Math.min(1, x));
        const ang = Math.acos(clamped);
        let nRaw = BRUSH_CIRCLE_SIDES_MAX;
        if (Number.isFinite(ang) && ang >= 1e-12) {
            nRaw = Math.ceil(Math.PI / ang);
        }
        const n = Math.max(BRUSH_CIRCLE_SIDES_MIN, Math.min(BRUSH_CIRCLE_SIDES_MAX, nRaw));
        return { n, nRaw };
    }

    function brushCircleSidesForRadiusMeters(radiusM) {
        return brushCircleSidesRawAndClamped(radiusM).n;
    }

    function brushVertexReadoutText(sides) {
        const rm = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || BRUSH_METERS_MIN));
        if (appState.brushProfile === 'circle') {
            const s = sides || brushCircleSidesRawAndClamped(rm);
            let extra = '';
            if (s.nRaw > BRUSH_CIRCLE_SIDES_MAX) extra = ' (max)';
            else if (s.nRaw < BRUSH_CIRCLE_SIDES_MIN) extra = ' (min)';
            return `~${s.n} verts / stamp${extra}`;
        }
        const n = brushStampRingVertexCount(rm);
        return `${n} verts / stamp`;
    }

    function brushVertexReadoutTitle(sides) {
        const rm = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || BRUSH_METERS_MIN));
        if (appState.brushProfile === 'circle') {
            const s = sides || brushCircleSidesRawAndClamped(rm);
            return `Approximate vertices on one stamp (curve δ; clamped ${BRUSH_CIRCLE_SIDES_MIN}–${BRUSH_CIRCLE_SIDES_MAX}). Ideal N: ${s.nRaw}. Strokes union many stamps.`;
        }
        if (appState.brushProfile === 'rounded_square') {
            return 'Vertices on one rounded-square stamp (flat-earth corners). Quarter circles use δ for segment count. Strokes union stamps along the path.';
        }
        return `Vertices on one ${appState.brushProfile} stamp. Strokes union overlapping stamps along the path.`;
    }

    function updateBrushVertexReadout() {
        const el = document.getElementById('wpo-brush-verts');
        if (!el) return;
        const d = brushCircleSidesRawAndClamped(appState.brushSize);
        el.textContent = brushVertexReadoutText(d);
        el.title = brushVertexReadoutTitle(d);
    }

    /**
     * JSTS buffer(distance, quadrantSegments): ~4× segments per full circle. Match geodesic disk angular resolution.
     */
    function bufferPathQuadrantSegmentsForRadius(radiusM) {
        const n = brushCircleSidesForRadiusMeters(radiusM);
        const q = Math.round(n / 4);
        return Math.max(2, Math.min(16, q));
    }

    function geodesicDiskRing(lon, lat, radiusM, sideCountOverride) {
        const nFull = brushCircleSidesForRadiusMeters(radiusM);
        const n = sideCountOverride != null
            ? Math.max(BRUSH_CIRCLE_SIDES_MIN, Math.min(Math.round(sideCountOverride), nFull))
            : nFull;
        const ring = [];
        for (let i = 0; i < n; i++) {
            const br = (i / n) * Math.PI * 2;
            const p = destinationPointMeters(lon, lat, br, radiusM);
            ring.push([p.lon, p.lat]);
        }
        if (ring.length && (ring[0][0] !== ring[ring.length - 1][0] || ring[0][1] !== ring[ring.length - 1][1]))
            ring.push([ring[0][0], ring[0][1]]);
        return ring;
    }

    function brushUsesCircularBuffer() {
        return appState.brushProfile === 'circle';
    }

    function brushProfileUsesAlignControl() {
        const p = appState.brushProfile;
        return p === 'square' || p === 'diamond' || p === 'rounded_square';
    }

    function brushProfileShowsCurveSlider() {
        const p = appState.brushProfile;
        return p === 'circle' || p === 'rounded_square';
    }

    function brushProfileChipLabel(profile) {
        switch (profile) {
            case 'circle': return 'Circle';
            case 'square': return 'Square';
            case 'diamond': return 'Diamond';
            case 'hexagon': return 'Hex';
            case 'rounded_square': return 'Round sq';
            default: return '';
        }
    }

    function brushStrokeBearingRad(path, index) {
        if (!path || path.length < 2) return 0;
        const n = path.length;
        if (index <= 0) return geoBearingRad(path[0][0], path[0][1], path[1][0], path[1][1]);
        if (index >= n - 1) return geoBearingRad(path[n - 2][0], path[n - 2][1], path[n - 1][0], path[n - 1][1]);
        return geoBearingRad(path[index - 1][0], path[index - 1][1], path[index][0], path[index][1]);
    }

    function brushBaseBearingRad(path, index) {
        if (appState.brushAlign !== 'stroke' || !brushProfileUsesAlignControl()) return 0;
        return brushStrokeBearingRad(path, index);
    }

    /** Rotate local shape (x east, y north) by bearing b from geographic north. */
    function rotateLocalEastNorthMeters(x, y, bearingRad) {
        const c = Math.cos(bearingRad);
        const s = Math.sin(bearingRad);
        return {
            east: x * c - y * s,
            north: x * s + y * c
        };
    }

    /** Closed meter-space polyline for rounded square; half-size R, corner radius r. */
    function roundedSquareMeterRingClosed(R, rCorner) {
        const Rm = Math.max(1e-6, R);
        let r = Math.min(Math.max(rCorner, 0), Rm * 0.45);
        const pts = [];
        const arc = (cx, cy, a0, a1) => {
            const n = BRUSH_ROUNDED_SQUARE_ARC_SEGS;
            for (let i = 1; i <= n; i++) {
                const t = i / n;
                const ang = a0 + (a1 - a0) * t;
                pts.push([cx + r * Math.cos(ang), cy + r * Math.sin(ang)]);
            }
        };
        pts.push([-Rm + r, -Rm]);
        pts.push([Rm - r, -Rm]);
        arc(Rm - r, -Rm + r, -Math.PI / 2, 0);
        pts.push([Rm, Rm - r]);
        arc(Rm - r, Rm - r, 0, Math.PI / 2);
        pts.push([-Rm + r, Rm]);
        arc(-Rm + r, Rm - r, Math.PI / 2, Math.PI);
        pts.push([-Rm, -Rm + r]);
        arc(-Rm + r, -Rm + r, Math.PI, 1.5 * Math.PI);
        pts.push([pts[0][0], pts[0][1]]);
        return pts;
    }

    function meterRingToLonLatRing(centerLon, centerLat, refLat, xyPairs, baseBearingRad) {
        const ring = [];
        for (let i = 0; i < xyPairs.length; i++) {
            const [x, y] = xyPairs[i];
            const { east, north } = rotateLocalEastNorthMeters(x, y, baseBearingRad);
            ring.push(localMetersToLonLat(east, north, centerLon, centerLat));
        }
        return ring;
    }

    /**
     * Closed geodesic-ish ring for current brush profile (R = radius / apothem per tooltips).
     * @param {number|null} circleSideOverride only for circle live preview
     */
    function geodesicBrushProfileRing(lon, lat, radiusM, refLat, path, pathIndex, circleSideOverride) {
        const R = Math.max(1e-6, Number(radiusM) || 0);
        const ref = refLat != null ? refLat : lat;
        const prof = appState.brushProfile;
        const base = brushBaseBearingRad(path, pathIndex);

        if (prof === 'circle') {
            return geodesicDiskRing(lon, lat, R, circleSideOverride);
        }

        if (prof === 'square') {
            const d = R * Math.SQRT2;
            const ring = [];
            for (let k = 0; k < 4; k++) {
                const br = base + Math.PI / 4 + k * (Math.PI / 2);
                const p = destinationPointMeters(lon, lat, br, d);
                ring.push([p.lon, p.lat]);
            }
            ring.push([ring[0][0], ring[0][1]]);
            return ring;
        }

        if (prof === 'diamond') {
            const ring = [];
            for (let k = 0; k < 4; k++) {
                const br = base + k * (Math.PI / 2);
                const p = destinationPointMeters(lon, lat, br, R);
                ring.push([p.lon, p.lat]);
            }
            ring.push([ring[0][0], ring[0][1]]);
            return ring;
        }

        if (prof === 'hexagon') {
            const rv = R / Math.cos(Math.PI / 6);
            const ring = [];
            for (let k = 0; k < 6; k++) {
                const br = base + Math.PI / 6 + k * (Math.PI / 3);
                const p = destinationPointMeters(lon, lat, br, rv);
                ring.push([p.lon, p.lat]);
            }
            ring.push([ring[0][0], ring[0][1]]);
            return ring;
        }

        if (prof === 'rounded_square') {
            const ratio = Math.max(BRUSH_CORNER_RATIO_MIN, Math.min(BRUSH_CORNER_RATIO_MAX, Number(appState.brushCornerRatio) || BRUSH_CORNER_RATIO_DEFAULT));
            const rCorner = ratio * R;
            const xy = roundedSquareMeterRingClosed(R, rCorner);
            return meterRingToLonLatRing(lon, lat, ref, xy, base);
        }

        return geodesicDiskRing(lon, lat, R, circleSideOverride);
    }

    function brushStampRingVertexCount(radiusM) {
        const R = Math.max(1e-6, Number(radiusM) || 0);
        const prof = appState.brushProfile;
        if (prof === 'circle') return brushCircleSidesForRadiusMeters(R);
        if (prof === 'square' || prof === 'diamond') return 4;
        if (prof === 'hexagon') return 6;
        if (prof === 'rounded_square') {
            const ratio = Math.max(BRUSH_CORNER_RATIO_MIN, Math.min(BRUSH_CORNER_RATIO_MAX, Number(appState.brushCornerRatio) || BRUSH_CORNER_RATIO_DEFAULT));
            const rCorner = ratio * R;
            const xy = roundedSquareMeterRingClosed(R, rCorner);
            return Math.max(4, xy.length - 1);
        }
        return 0;
    }

    function densifyOpenPolylineForBrushStamps(path, maxSegM) {
        if (!path || path.length < 2) return path ? path.map(p => p.slice()) : [];
        const maxStep = Math.max(1.5, maxSegM);
        const out = [[path[0][0], path[0][1]]];
        for (let i = 0; i < path.length - 1; i++) {
            const a = path[i];
            const b = path[i + 1];
            const d = haversineMeters(a[0], a[1], b[0], b[1]);
            const steps = Math.max(1, Math.ceil(d / maxStep));
            for (let k = 1; k <= steps; k++) {
                const t = k / steps;
                out.push(interpolateGeodesicLonLat(a[0], a[1], b[0], b[1], t));
            }
        }
        return out;
    }

    function buildPolylineStampUnionMultipolygon(pc, openPath, radiusM, refLat, logCtx, circleSideOverride) {
        if (!openPath || !openPath.length) return null;
        const R = Math.max(BRUSH_METERS_MIN, Number(radiusM) || 0);
        const ref = refLat != null ? refLat : openPath[0][1];
        if (openPath.length < 2) {
            const p = openPath[0];
            const ring = geodesicBrushProfileRing(p[0], p[1], R, ref, null, 0, circleSideOverride);
            return [[ring]];
        }
        const maxSeg = Math.max(2, R * BRUSH_POLY_STAMP_SPACING_FACTOR);
        const dense = densifyOpenPolylineForBrushStamps(openPath, maxSeg);
        if (!dense.length) return null;
        const parts = [];
        for (let i = 0; i < dense.length; i++) {
            const q = dense[i];
            const ring = geodesicBrushProfileRing(q[0], q[1], R, ref, dense, i, circleSideOverride);
            parts.push([[ring]]);
        }
        if (!parts.length) return null;
        if (parts.length === 1) return parts[0];
        return unionMultipolysWithFallback(pc, parts, Object.assign({ reason: 'polyline_stamp_union', stampCount: parts.length }, logCtx || {}));
    }

    function subsampleClosedRingForPreview(ring, maxVerts) {
        if (!ring || ring.length <= 2 || maxVerts == null || ring.length <= maxVerts) return ring;
        const inner = ring[ring.length - 1][0] === ring[0][0] && ring[ring.length - 1][1] === ring[0][1]
            ? ring.slice(0, -1)
            : ring.slice();
        const n = inner.length;
        if (n <= maxVerts) return ring;
        const out = [];
        const step = n / maxVerts;
        for (let i = 0; i < maxVerts; i++) {
            const idx = Math.min(n - 1, Math.floor(i * step));
            out.push(inner[idx]);
        }
        out.push([out[0][0], out[0][1]]);
        return out;
    }

    function trimBrushStrokeSamplesIfNeeded(samples) {
        while (samples.length > BRUSH_STROKE_SAMPLES_MAX) {
            const last = samples[samples.length - 1];
            const next = [];
            for (let i = 0; i < samples.length; i += 2) next.push(samples[i]);
            const L = next[next.length - 1];
            if (!L || L[0] !== last[0] || L[1] !== last[1]) next.push(last);
            samples.length = 0;
            next.forEach(p => samples.push(p));
        }
    }

    /** Keep stamp centers close enough that disk unions overlap (handles fast mouse jumps). */
    function appendBrushSamplesAlongGeodesic(samples, gLon, gLat, radiusM) {
        if (!samples.length) {
            samples.push([gLon, gLat]);
            return;
        }
        const last = samples[samples.length - 1];
        const d = haversineMeters(last[0], last[1], gLon, gLat);
        if (d < 0.06) return;
        const maxSeg = Math.max(2, radiusM * 1.5);
        if (d <= maxSeg) {
            samples.push([gLon, gLat]);
            trimBrushStrokeSamplesIfNeeded(samples);
            return;
        }
        let steps = Math.max(1, Math.ceil(d / maxSeg));
        steps = Math.min(steps, BRUSH_APPEND_MAX_STEPS_PER_EVENT);
        for (let k = 1; k <= steps; k++) {
            const t = k / steps;
            samples.push(interpolateGeodesicLonLat(last[0], last[1], gLon, gLat, t));
        }
        trimBrushStrokeSamplesIfNeeded(samples);
    }

    /** Union many multipolygon parts in a balanced tree (avoids deep linear pc.union chains). */
    function balancedUnionMultipolys(pc, parts) {
        if (!parts || !parts.length) return null;
        let layer = parts.filter(p => p != null);
        while (layer.length > 1) {
            const next = [];
            for (let i = 0; i < layer.length; i += 2) {
                if (i + 1 < layer.length) next.push(pc.union(layer[i], layer[i + 1]));
                else next.push(layer[i]);
            }
            layer = next;
        }
        return layer[0];
    }

    /** Linear union; sometimes succeeds when balanced tree hits jsts bugs. */
    function sequentialUnionMultipolys(pc, parts) {
        if (!parts || !parts.length) return null;
        const layer = parts.filter(p => p != null);
        if (!layer.length) return null;
        let acc = layer[0];
        for (let i = 1; i < layer.length; i++) acc = pc.union(acc, layer[i]);
        return acc;
    }

    /** Union one batch: balanced → sequential → binary split (disk+capsule can be hundreds of parts). */
    function unionOneBatchWithFallback(pc, batch) {
        if (!batch || !batch.length) return null;
        if (batch.length === 1) return batch[0];
        try {
            return balancedUnionMultipolys(pc, batch);
        } catch (e1) {
            try {
                return sequentialUnionMultipolys(pc, batch);
            } catch (e2) {
                if (batch.length <= 2) throw e2;
                const mid = Math.ceil(batch.length / 2);
                const left = unionOneBatchWithFallback(pc, batch.slice(0, mid));
                const right = unionOneBatchWithFallback(pc, batch.slice(mid));
                return pc.union(left, right);
            }
        }
    }

    /** Fold many parts in waves of at most BRUSH_STAMP_UNION_BATCH_SIZE per union group. */
    function batchedFoldUnionMultipolys(pc, parts) {
        let layer = parts.filter(p => p != null);
        if (!layer.length) return null;
        const B = BRUSH_STAMP_UNION_BATCH_SIZE;
        while (layer.length > 1) {
            const next = [];
            for (let i = 0; i < layer.length; i += B) {
                const batch = layer.slice(i, i + B);
                if (batch.length === 1) next.push(batch[0]);
                else next.push(unionOneBatchWithFallback(pc, batch));
            }
            layer = next;
        }
        return layer[0];
    }

    function batchedFoldUnionSequentialOnly(pc, parts) {
        let layer = parts.filter(p => p != null);
        if (!layer.length) return null;
        const B = BRUSH_STAMP_UNION_BATCH_SIZE;
        while (layer.length > 1) {
            const next = [];
            for (let i = 0; i < layer.length; i += B) {
                const batch = layer.slice(i, i + B);
                if (batch.length === 1) next.push(batch[0]);
                else next.push(sequentialUnionMultipolys(pc, batch));
            }
            layer = next;
        }
        return layer[0];
    }

    /** Try balanced union first, then sequential (chunk outline unions can throw on complex geometry). */
    function unionMultipolysWithFallback(pc, parts, logCtx) {
        if (!parts || !parts.length) return null;
        const layer = parts.filter(p => p != null);
        if (!layer.length) return null;
        if (layer.length === 1) return layer[0];
        const tryPrimary = () => (layer.length <= BRUSH_STAMP_UNION_BATCH_SIZE
            ? balancedUnionMultipolys(pc, layer)
            : batchedFoldUnionMultipolys(pc, layer));
        const trySecondary = () => (layer.length <= BRUSH_STAMP_UNION_BATCH_SIZE
            ? sequentialUnionMultipolys(pc, layer)
            : batchedFoldUnionSequentialOnly(pc, layer));
        try {
            return tryPrimary();
        } catch (e1) {
            logBrushGeom('warn', 'stamp_union_balanced_throw', Object.assign({
                message: e1 && e1.message ? String(e1.message) : String(e1),
                partCount: layer.length
            }, logCtx || {}));
            try {
                return trySecondary();
            } catch (e2) {
                logBrushGeom('error', 'stamp_union_sequential_throw', Object.assign({
                    message: e2 && e2.message ? String(e2.message) : String(e2),
                    partCount: layer.length
                }, logCtx || {}));
                return null;
            }
        }
    }

    /** Normalize angle to (-π, π]. */
    function normalizeAngleRad(a) {
        let x = a;
        while (x <= -Math.PI) x += 2 * Math.PI;
        while (x > Math.PI) x -= 2 * Math.PI;
        return x;
    }

    /** Local equirectangular meters from ref (good for single-stroke brush footprints). */
    function lonLatToLocalMeters(lon, lat, refLon, refLat) {
        const cosLat = Math.cos(refLat * Math.PI / 180);
        const x = (lon - refLon) * (Math.PI / 180) * EARTH_RADIUS_M * cosLat;
        const y = (lat - refLat) * (Math.PI / 180) * EARTH_RADIUS_M;
        return { x, y };
    }

    function localMetersToLonLat(x, y, refLon, refLat) {
        const cosLat = Math.cos(refLat * Math.PI / 180);
        const lon = refLon + x / (EARTH_RADIUS_M * cosLat * Math.PI / 180);
        const lat = refLat + y / (EARTH_RADIUS_M * Math.PI / 180);
        return [lon, lat];
    }

    function vec2Normalize(vx, vy) {
        const L = Math.hypot(vx, vy);
        if (L < 1e-18) return { x: 1, y: 0 };
        return { x: vx / L, y: vy / L };
    }

    /** Left perpendicular (CCW 90° in x-east, y-north plane). */
    function perpLeftXY(vx, vy) {
        return vec2Normalize(-vy, vx);
    }


    function ringAreaAbsDeg(ring) {
        let a = 0;
        const n = ring.length;
        for (let i = 0, j = n - 1; i < n; j = i++) {
            a += ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];
        }
        return Math.abs(a);
    }

    /** Sum outer-ring areas (deg² proxy) for relative before/after checks. */
    function sumOuterRingAreaDeg(mp) {
        if (!mp || !mp.length) return 0;
        let s = 0;
        for (let i = 0; i < mp.length; i++) {
            const part = mp[i];
            if (part && part[0] && part[0].length >= 3) s += ringAreaAbsDeg(part[0]);
        }
        return s;
    }

    /** Total rings (outer + holes) across multipolygon parts. */
    function totalRingCount(mp) {
        if (!mp || !mp.length) return 0;
        let n = 0;
        for (let i = 0; i < mp.length; i++) {
            if (mp[i]) n += mp[i].length;
        }
        return n;
    }

    /** Count sharp turn vertices (figure-8 / crossings tend to have large bearing deltas). */
    function countStrokeSharpTurns(strokeLonLat, minDeltaRad) {
        if (!strokeLonLat || strokeLonLat.length < 3) return 0;
        let c = 0;
        for (let i = 1; i < strokeLonLat.length - 1; i++) {
            const a = strokeLonLat[i - 1], b = strokeLonLat[i], d = strokeLonLat[i + 1];
            const br1 = geoBearingRad(a[0], a[1], b[0], b[1]);
            const br2 = geoBearingRad(b[0], b[1], d[0], d[1]);
            let delta = Math.abs(br2 - br1);
            if (delta > Math.PI) delta = 2 * Math.PI - delta;
            if (delta >= minDeltaRad) c++;
        }
        return c;
    }

    function ringCentroidLonLat(ring) {
        let sx = 0, sy = 0;
        const n = ring.length - (ring.length > 1 && ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1] ? 1 : 0);
        const lim = Math.max(0, n);
        for (let i = 0; i < lim; i++) {
            sx += ring[i][0];
            sy += ring[i][1];
        }
        return lim ? [sx / lim, sy / lim] : [0, 0];
    }

    function closestPointOnRingEdgeToPoint(ring, lon, lat, edgeInterior) {
        let best = null;
        let bestD = Infinity;
        const closed = ring.length > 1 && ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1];
        const m = closed ? ring.length - 1 : ring.length;
        const trim = edgeInterior ? 0.1 : 0;
        for (let i = 0; i < m; i++) {
            const a = ring[i], b = ring[(i + 1) % m];
            const segM = haversineMeters(a[0], a[1], b[0], b[1]);
            const steps = Math.max(6, Math.ceil(segM / 1.2));
            const lo = Math.max(0, Math.floor(trim * steps));
            const hi = Math.min(steps, Math.ceil((1 - trim) * steps));
            for (let s = lo; s <= hi; s++) {
                const t = s / steps;
                const ll = interpolateGeodesicLonLat(a[0], a[1], b[0], b[1], t);
                const d = haversineMeters(lon, lat, ll[0], ll[1]);
                if (d < bestD) {
                    bestD = d;
                    best = ll;
                }
            }
        }
        return best || [ring[0][0], ring[0][1]];
    }

    function geodesicCorridorRing(lon1, lat1, lon2, lat2, halfWidthM, steps) {
        const pc = getGeometryEngine();
        if (pc && pc.bufferPath) {
            try {
                const buff = pc.bufferPath([[lon1, lat1], [lon2, lat2]], halfWidthM, steps);
                if (buff && buff.length > 0 && buff[0] && buff[0].length > 0) {
                    return buff[0][0];
                }
            } catch (e) {
                // fallback to minimal ring on failure
            }
        }
        return [[lon1, lat1], [lon2, lat2], [lon1, lat1]];
    }

    function pickLargestPartByArea(multipoly) {
        let best = null;
        let bestA = -1;
        if (!multipoly) return null;
        for (let p = 0; p < multipoly.length; p++) {
            const part = multipoly[p];
            if (!part || !part[0]) continue;
            const a = ringAreaAbsDeg(part[0]);
            if (a > bestA) {
                bestA = a;
                best = part;
            }
        }
        return best;
    }

    function bridgePartHoles(pc, part) {
        if (!part || !part[0] || part.length < 2) return part;
        let rings = part.map(r => r.map(p => [p[0], p[1]]));
        let safety = 0;
        while (rings.length > 1 && safety++ < 40) {
            const outer = rings[0];
            const inner = rings[1];
            const hc = ringCentroidLonLat(inner);
            const pOut = closestPointOnRingEdgeToPoint(outer, hc[0], hc[1], true);
            const pIn = closestPointOnRingEdgeToPoint(inner, pOut[0], pOut[1], true);
            const dist = haversineMeters(pIn[0], pIn[1], pOut[0], pOut[1]);
            let halfW = Math.max(0.35, Math.min(2.5, dist * 0.07));
            const stepN = Math.max(10, Math.min(56, Math.ceil(dist / 2.5)));
            let slit = geodesicCorridorRing(pIn[0], pIn[1], pOut[0], pOut[1], halfW, stepN);
            let merged;
            try {
                merged = pc.union([rings], [[slit]]);
            } catch (e) {
                break;
            }
            let next = pickLargestPartByArea(merged);
            if (next && next.length < rings.length) {
                rings = next.map(r => r.map(p => [p[0], p[1]]));
                continue;
            }
            halfW = Math.min(4, halfW * 2.2);
            slit = geodesicCorridorRing(pIn[0], pIn[1], pOut[0], pOut[1], halfW, stepN);
            try {
                merged = pc.union([rings], [[slit]]);
            } catch (e2) {
                break;
            }
            next = pickLargestPartByArea(merged);
            if (next && next.length < rings.length) {
                rings = next.map(r => r.map(p => [p[0], p[1]]));
                continue;
            }
            break;
        }
        return rings;
    }

    function bridgeHolesAfterBrushOp(pc) {
        const mapped = masterPolygons.map(part => bridgePartHoles(pc, part)).filter(p => p && p[0] && p[0].length > 2);
        const L = getActiveUserLayer();
        if (!L) return;
        L.polygons = mapped;
        masterPolygons = L.polygons;
        alignShapeLinksToMaster();
    }

    function flattenSanitizedDrawnParts(drawnParts, pc) {
        if (!drawnParts || !drawnParts.length || !pc || typeof pc.sanitizePartToParts !== 'function') return [];
        const out = [];
        for (const p of drawnParts) {
            const expanded = pc.sanitizePartToParts(p);
            if (expanded && expanded.length) {
                for (let k = 0; k < expanded.length; k++) out.push(expanded[k]);
            }
        }
        return out;
    }

    /**
     * buffer(0) each part; drops empty parts.
     * Active non-broken link + multi-part split → splitGroupId on siblings; j>0 get inactive+broken for UI/Apply (§1.9).
     * sourceIndices[k] = original input row index for output part k (for competing-link cleanup on Apply).
     */
    function expandSanitizedPolygonsAndLinks(pc, polygonsIn, linksIn) {
        if (!polygonsIn.length) {
            return { polygons: [], links: [], sourceIndices: [] };
        }
        if (!pc || typeof pc.sanitizePartToParts !== 'function') {
            return { polygons: polygonsIn, links: linksIn, sourceIndices: polygonsIn.map((_, i) => i) };
        }
        const links = linksIn.slice();
        while (links.length < polygonsIn.length) links.push(null);
        links.length = polygonsIn.length;
        const newPolys = [];
        const newLinks = [];
        const newSourceIndices = [];
        for (let i = 0; i < polygonsIn.length; i++) {
            const link = normalizeShapeLink(links[i]);
            const expanded = pc.sanitizePartToParts(polygonsIn[i]);
            if (!expanded || expanded.length === 0) continue;
            const hasActiveSplit = link && link.venueId != null && link.active !== false && link.broken !== true;
            let splitGroupId = null;
            if (expanded.length > 1 && hasActiveSplit) {
                splitGroupId = 'sg_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
            }
            for (let j = 0; j < expanded.length; j++) {
                newPolys.push(expanded[j]);
                newSourceIndices.push(i);
                if (!link) {
                    newLinks.push(null);
                } else if (expanded.length === 1) {
                    newLinks.push(cloneLinkEntry(link));
                } else if (!hasActiveSplit) {
                    newLinks.push(j === 0 ? cloneLinkEntry(link) : null);
                } else {
                    const base = cloneLinkEntry(link);
                    base.splitGroupId = splitGroupId;
                    if (j === 0) {
                        base.active = true;
                        base.broken = false;
                    } else {
                        base.active = false;
                        base.broken = true;
                    }
                    newLinks.push(base);
                }
            }
        }
        return { polygons: newPolys, links: newLinks, sourceIndices: newSourceIndices };
    }

    /** buffer(0) each master part on the active layer; first split keeps shapePlaceLinks[i], rest null. */
    function expandSanitizedMasterPolygons(pc) {
        if (!masterPolygons.length) return;
        alignShapeLinksToMaster();
        const ex = expandSanitizedPolygonsAndLinks(pc, masterPolygons, shapePlaceLinks);
        const L = getActiveUserLayer();
        if (!L) return;
        L.polygons = ex.polygons;
        L.links = ex.links;
        masterPolygons = L.polygons;
        shapePlaceLinks = L.links;
        ensureShapeIdsForLayer(L);
    }

    function perpendicularDistanceXY(p, p1, p2) {
        let x = p1[0], y = p1[1];
        let dx = p2[0] - x, dy = p2[1] - y;
        if (dx !== 0 || dy !== 0) {
            let t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);
            if (t > 1) { x = p2[0]; y = p2[1]; }
            else if (t > 0) { x += dx * t; y += dy * t; }
        }
        dx = p[0] - x; dy = p[1] - y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    function rdpXY(points, epsilon) {
        if (points.length < 3) return points;
        let dmax = 0;
        let index = 0;
        const end = points.length - 1;
        for (let i = 1; i < end; i++) {
            let d = perpendicularDistanceXY(points[i], points[0], points[end]);
            if (d > dmax) {
                index = i;
                dmax = d;
            }
        }
        if (dmax > epsilon) {
            let recResults1 = rdpXY(points.slice(0, index + 1), epsilon);
            let recResults2 = rdpXY(points.slice(index), epsilon);
            return recResults1.slice(0, recResults1.length - 1).concat(recResults2);
        } else {
            return [points[0], points[end]];
        }
    }

    /**
     * Douglas–Peucker on an open lon/lat polyline (brush centerline). Preserves endpoints.
     * Tightens epsilon up to 12 passes until vertex count is at most maxVerts (when maxVerts is finite).
     */
    function simplifyOpenPolylineLonLatRDP(strokeLonLat, epsilonMeters, refLat, maxVerts) {
        if (!strokeLonLat || strokeLonLat.length < 3) return strokeLonLat ? strokeLonLat.map(p => [p[0], p[1]]) : [];
        const latRad = (refLat * Math.PI) / 180;
        const mPerDegLon = 111320 * Math.max(0.2, Math.cos(latRad));
        const mPerDegLat = 110540;
        const toXY = p => [p[0] * mPerDegLon, p[1] * mPerDegLat];
        const toLL = xy => [xy[0] / mPerDegLon, xy[1] / mPerDegLat];
        let eps = epsilonMeters;
        const cap = maxVerts != null && Number.isFinite(maxVerts) ? maxVerts : Infinity;
        const a0 = strokeLonLat[0];
        const a1 = strokeLonLat[strokeLonLat.length - 1];
        for (let iter = 0; iter < 12; iter++) {
            const xy = strokeLonLat.map(toXY);
            const simplifiedXY = rdpXY(xy, eps);
            let ll = simplifiedXY.map(toLL);
            if (ll.length && (ll[0][0] !== a0[0] || ll[0][1] !== a0[1])) ll = [[a0[0], a0[1]]].concat(ll.slice(1));
            if (ll.length && (ll[ll.length - 1][0] !== a1[0] || ll[ll.length - 1][1] !== a1[1]))
                ll = ll.slice(0, -1).concat([[a1[0], a1[1]]]);
            if (ll.length < 2) return strokeLonLat.map(p => [p[0], p[1]]);
            if (ll.length <= cap || iter >= 11) return ll;
            eps *= 1.35;
        }
        return strokeLonLat.map(p => [p[0], p[1]]);
    }

    /** Unique vertices in a closed ring (drops duplicate closing point). */
    function ringOpenVertices(ring) {
        if (!ring || ring.length < 2) return null;
        const closed = ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1];
        return closed ? ring.slice(0, -1) : ring.slice();
    }

    function simplifyRingRDP(ring, epsilonMeters, refLat) {
        if (!ring || ring.length < 4) return ring;

        const minVerts = 8; // safety floor (unique vertices along boundary)
        const open = ringOpenVertices(ring);
        if (!open || open.length < 3) return ring;

        const latRad = (refLat * Math.PI) / 180;
        const mPerDegLon = 111320 * Math.max(0.2, Math.cos(latRad));
        const mPerDegLat = 110540;
        const toXY = p => [p[0] * mPerDegLon, p[1] * mPerDegLat];
        const toLL = xy => [xy[0] / mPerDegLon, xy[1] / mPerDegLat];

        const simplifyOnce = (eps) => {
            const xy = open.map(toXY);
            // IMPORTANT: run RDP on an OPEN polyline v0..v_{n-1}. Do NOT duplicate v0 at the end:
            // xy.concat([xy[0]]) makes first==last for RDP and collapses complex loops (figure-8, multi-loop).
            const simplifiedXY = rdpXY(xy, eps);
            if (!simplifiedXY || simplifiedXY.length < 2) return null;
            let ll = simplifiedXY.map(toLL);
            const first = ll[0];
            const last = ll[ll.length - 1];
            if (first[0] !== last[0] || first[1] !== last[1]) ll = ll.concat([[first[0], first[1]]]);
            return ll;
        };

        const openVertCount = (closedRing) => {
            const o = ringOpenVertices(closedRing);
            return o ? o.length : 0;
        };

        let simplified = simplifyOnce(epsilonMeters);
        if (simplified == null || openVertCount(simplified) < minVerts)
            simplified = simplifyOnce(Math.max(0.05, epsilonMeters * 0.5));
        if (simplified == null || openVertCount(simplified) < minVerts) simplified = open.slice();

        const first = simplified[0];
        const last = simplified[simplified.length - 1];
        if (first[0] !== last[0] || first[1] !== last[1]) simplified = simplified.concat([[first[0], first[1]]]);
        return simplified;
    }

    function cloneMasterPolygons(src) {
        if (!src || !src.length) return [];
        return src.map(part => part.map(ring => ring.map(p => [p[0], p[1]])));
    }

    /** True if multipolygon is missing or has no valid outer ring. */
    function isMultipolyEmptyOrInvalid(mp) {
        if (!mp || !mp.length) return true;
        for (let p = 0; p < mp.length; p++) {
            const part = mp[p];
            if (!part || !part[0] || part[0].length < 4) return true;
        }
        return false;
    }

    /** Paint brush: valid geometry but suspicious loss vs pre-stroke (area or holes). */
    function brushGeometryRegression(before, after, beforeWasEmpty) {
        if (!after || isMultipolyEmptyOrInvalid(after)) return true;
        if (beforeWasEmpty) return false;
        const sumB = sumOuterRingAreaDeg(before);
        const sumA = sumOuterRingAreaDeg(after);
        if (sumB > 1e-12 && sumA < sumB * BRUSH_REGRESSION_AREA_RATIO) return true;
        if (before.some(p => p && p.length > 1) && totalRingCount(after) < totalRingCount(before)) return true;
        return false;
    }

    /** Compact metrics for brush/eraser diagnostics (deg² proxy, not true geodesic area). */
    function brushGeomSnapshot(mp) {
        return {
            parts: mp && mp.length ? mp.length : 0,
            sumOuterDeg: sumOuterRingAreaDeg(mp),
            rings: totalRingCount(mp),
            invalid: isMultipolyEmptyOrInvalid(mp)
        };
    }

    /**
     * @param {'trace'|'warn'|'error'} level
     * @param {string} event
     * @param {object} [data]
     */
    function logBrushGeom(level, event, data) {
        if (!DEBUG) return;
        const payload = Object.assign({ event }, data || {});
        if (level === 'error') console.error('[CandyPaint][brushGeom]', payload);
        else if (level === 'warn') console.warn('[CandyPaint][brushGeom]', payload);
        else console.info('[CandyPaint][brushGeom]', payload);
    }

    function readBrushSimplifyParams(refLat) {
        let vertexMinM = 5;
        let minTurnDeg = 12;
        try {
            const a = localStorage.getItem(LS_BRUSH_VERTEX_MIN_M);
            const b = localStorage.getItem(LS_BRUSH_MIN_TURN_DEG);
            if (a != null && a !== '') vertexMinM = Math.max(0.25, Number(a) || vertexMinM);
            if (b != null && b !== '') minTurnDeg = Math.max(0, Math.min(180, Number(b) || minTurnDeg));
        } catch (e) { /* ignore */ }
        const latRad = ((refLat != null ? refLat : 50) * Math.PI) / 180;
        const mPerDegLon = 111320 * Math.max(0.2, Math.cos(latRad));
        const mPerDegLat = 110540;
        return { vertexMinM, minTurnDeg, mPerDegLon, mPerDegLat };
    }

    function simplifyMultipolygonRDP(multipoly, epsilonMeters, refLat) {
        if (!multipoly) return null;
        return multipoly.map(part => {
            if (!part) return part;
            return part.map(ring => simplifyRingRDP(ring, epsilonMeters, refLat));
        });
    }

    function reconcileLinksAfterPartCountChange(prevLinks, prevCount) {
        const nowCount = masterPolygons.length;
        const prevNonNull = prevLinks.filter(l => l && l.venueId != null).length;
        alignShapeLinksToMaster();
        if (prevNonNull === 0) return;
        if (prevNonNull === 1 && nowCount > prevCount) {
            let best = -1;
            let bestA = -1;
            for (let i = 0; i < nowCount; i++) {
                const a = ringAreaAbsDeg(masterPolygons[i][0]);
                if (a > bestA) {
                    bestA = a;
                    best = i;
                }
            }
            const found = prevLinks.find(l => l && l.venueId != null);
            if (found && best >= 0) {
                shapePlaceLinks.fill(null);
                shapePlaceLinks[best] = normalizeShapeLink(found);
                rebalanceVenueLinkActives({ layer: getActiveUserLayer(), shapeIndex: best });
                logToUI('Link kept on largest fragment after erase.', false);
            }
            return;
        }
        shapePlaceLinks.fill(null);
        logToUI('Shape–place links cleared (part count changed).', false);
    }

    /** Overlapping windows along an open lon/lat polyline; each slice has at least two points when possible. */
    function sliceOpenPolylineIntoChunks(path, chunkSize, overlap) {
        const chunks = [];
        if (!path || path.length < 2) return chunks;
        const stride = Math.max(1, chunkSize - overlap);
        for (let start = 0; start < path.length; start += stride) {
            const end = Math.min(path.length, start + chunkSize);
            const slice = path.slice(start, end);
            if (slice.length >= 2) chunks.push(slice);
            if (end >= path.length) break;
        }
        return chunks;
    }

    /** One-part multipolygon: single brush stamp (profile from appState), tap / zero-length stroke. */
    function diskStampMultipolygon(lon, lat, radiusM, refLat) {
        const ref = refLat != null ? refLat : lat;
        const ring = geodesicBrushProfileRing(lon, lat, radiusM, ref, null, 0, undefined);
        return [[ring]];
    }

    /**
     * Brush stamp: one bufferPath or chunked bufferPaths unioned via unionMultipolysWithFallback.
     * Stamps are not RDP-simplified here — quadrant segments follow brushCircleSidesForRadiusMeters (sagitta δ); RDP was coarsening results.
     * @param {object} logCtx passed to unionMultipolysWithFallback
     * @param {boolean} [_skipSimplify] unused (API compat for callers)
     */
    function buildBrushStampMultipolygonFromPath(pc, strokePathForStamp, radiusM, _epsilonMeters, refLat, logCtx, _skipSimplify) {
        if (!strokePathForStamp || strokePathForStamp.length === 0) return null;
        const quadSeg = bufferPathQuadrantSegmentsForRadius(radiusM);

        if (strokePathForStamp.length < 2) {
            const p = strokePathForStamp[0];
            return diskStampMultipolygon(p[0], p[1], radiusM, refLat);
        }

        const chunkV = Math.min(BRUSH_STAMP_UNION_MAX_VERTS, BRUSH_STAMP_BUFFER_CHUNK_VERTS);
        const overlap = BRUSH_STAMP_UNION_CHUNK_OVERLAP;
        const slices = strokePathForStamp.length <= chunkV
            ? [strokePathForStamp]
            : sliceOpenPolylineIntoChunks(strokePathForStamp, chunkV, overlap);
        if (!slices.length) return null;
        const parts = [];
        for (let s = 0; s < slices.length; s++) {
            let stamp = pc.bufferPath(slices[s], radiusM, quadSeg);
            if (!stamp || !stamp.length) continue;
            parts.push(stamp);
        }
        if (!parts.length) return null;
        if (parts.length === 1) return parts[0];
        const merged = unionMultipolysWithFallback(pc, parts, Object.assign({ chunkSlices: slices.length }, logCtx || {}));
        return merged;
    }

    /** Circle: JSTS buffer along path; other profiles: union overlapping geodesic stamps. */
    function buildAnyBrushStrokeStampMultipolygon(pc, openPath, radiusM, refLat, logCtx) {
        if (brushUsesCircularBuffer()) {
            return buildBrushStampMultipolygonFromPath(pc, openPath, radiusM, 0, refLat, logCtx, false);
        }
        return buildPolylineStampUnionMultipolygon(pc, openPath, radiusM, refLat, logCtx, undefined);
    }

    function applyBrushGeometryOnce(centerLineLonLat, radiusM, isEraser, doPostSimplifyOuter) {
        const pc = getGeometryEngine();
        if (!pc || !centerLineLonLat.length) {
            logBrushGeom('warn', 'early_exit', {
                reason: !pc ? 'no_polygon_clipping' : 'empty_path',
                isEraser,
                pathLen: centerLineLonLat ? centerLineLonLat.length : 0
            });
            return;
        }

        const brushTargetLayer = getActiveUserLayer();
        if (!brushTargetLayer) {
            logBrushGeom('warn', 'early_exit', { reason: 'no_active_layer', isEraser });
            return;
        }
        syncActiveLayerAliases();

        // Dynamic tolerance based on brush size R (reduced aggressiveness by 50%)
        const epsilonMeters = Math.max(0.2, 0.5 * (radiusM > 40 ? radiusM / 6 : radiusM / 4));
        const refLat = centerLineLonLat[0][1];

        const preStrokeEpsilon = Math.max(0.1, Math.min(epsilonMeters * 0.52, radiusM * 0.14));
        const vertBudget = Math.max(200, Math.floor(BRUSH_STAMP_UNION_MAX_VERTS * 0.92));
        const strokePathForStamp = simplifyOpenPolylineLonLatRDP(centerLineLonLat, preStrokeEpsilon, refLat, vertBudget);

        let stampWorkPath = strokePathForStamp;
        if (stampWorkPath.length >= 2) {
            const dSpan = haversineMeters(
                stampWorkPath[0][0], stampWorkPath[0][1],
                stampWorkPath[stampWorkPath.length - 1][0], stampWorkPath[stampWorkPath.length - 1][1]);
            if (dSpan < Math.max(0.12, radiusM * 0.00035)) stampWorkPath = [stampWorkPath[0]];
        }

        try {
        const prevCount = masterPolygons.length;
        const prevLinks = cloneShapePlaceLinks(shapePlaceLinks);
        const masterBeforeBrush = !isEraser ? cloneMasterPolygons(masterPolygons) : null;

        const sharpTurns = !isEraser ? countStrokeSharpTurns(centerLineLonLat, BRUSH_SHARP_TURN_MIN_RAD) : 0;
        const complexStroke = !isEraser && (
            centerLineLonLat.length >= BRUSH_COMPLEX_VERT_THRESHOLD ||
            sharpTurns >= BRUSH_SHARP_TURN_COUNT_THRESHOLD
        );

        logBrushGeom('trace', 'stroke_start', {
            isEraser,
            radiusM,
            pathLen: centerLineLonLat.length,
            pathLenStamp: strokePathForStamp.length,
            pathLenStampWork: stampWorkPath.length,
            preStrokeEpsilonMeters: preStrokeEpsilon,
            epsilonMeters,
            masterWideSimplify: false,
            sharpTurns,
            complexStroke,
            masterPartsBefore: masterPolygons.length,
            sumOuterBefore: sumOuterRingAreaDeg(masterPolygons)
        });

        try {
            if (isEraser) {
                if (!masterPolygons.length) {
                    logBrushGeom('warn', 'eraser_skip_no_master', {});
                    return;
                }

                const quadSeg = bufferPathQuadrantSegmentsForRadius(radiusM);

                if (stampWorkPath.length > BRUSH_ERASER_DIFF_CHUNK_VERTS) {
                    const slices = sliceOpenPolylineIntoChunks(
                        stampWorkPath,
                        BRUSH_ERASER_DIFF_CHUNK_VERTS,
                        BRUSH_ERASER_DIFF_CHUNK_OVERLAP
                    );
                    logBrushGeom('trace', 'eraser_chunked', { chunks: slices.length, pathLen: stampWorkPath.length });
                    if (!slices.length) {
                        logBrushGeom('warn', 'eraser_chunked_empty_slices', { pathLen: stampWorkPath.length });
                        return;
                    }
                    for (let si = 0; si < slices.length; si++) {
                        let stampPoly = slices[si].length >= 2
                            ? (brushUsesCircularBuffer()
                                ? pc.bufferPath(slices[si], radiusM, quadSeg)
                                : buildPolylineStampUnionMultipolygon(pc, slices[si], radiusM, refLat, { reason: 'eraser_chunk', chunkIndex: si }))
                            : diskStampMultipolygon(slices[si][0][0], slices[si][0][1], radiusM, refLat);
                        if (!stampPoly || !stampPoly.length) {
                            logBrushGeom('warn', 'eraser_stamp_buffer_null', {
                                chunkIndex: si,
                                pathLen: slices[si].length,
                                radiusM
                            });
                            continue;
                        }
                        masterPolygons = pc.difference(masterPolygons, stampPoly);
                        if (!masterPolygons.length) break;
                    }
                } else {
                    let stampPoly = stampWorkPath.length >= 2
                        ? (brushUsesCircularBuffer()
                            ? pc.bufferPath(stampWorkPath, radiusM, quadSeg)
                            : buildPolylineStampUnionMultipolygon(pc, stampWorkPath, radiusM, refLat, { reason: 'eraser_main' }))
                        : diskStampMultipolygon(stampWorkPath[0][0], stampWorkPath[0][1], radiusM, refLat);
                    if (!stampPoly || !stampPoly.length) {
                        logBrushGeom('warn', 'eraser_stamp_buffer_null', { pathLen: centerLineLonLat.length, radiusM });
                        return;
                    }
                    masterPolygons = pc.difference(masterPolygons, stampPoly);
                }
            } else {
                let stampPoly = buildAnyBrushStrokeStampMultipolygon(pc, stampWorkPath, radiusM, refLat, { reason: 'brush_main' });
                if (!stampPoly || !stampPoly.length) {
                    logBrushGeom('warn', 'brush_stamp_buffer_null', {
                        pathLen: centerLineLonLat.length,
                        radiusM,
                        reason: 'bufferPath_returned_null_or_empty'
                    });
                    logToUI('Brush: stamp build failed. Set localStorage.wmeCandyPaintDebug=1 for [CandyPaint][brushGeom] details.', true);
                    return;
                }
                logBrushGeom('trace', 'brush_stamp_built', { stamp: brushGeomSnapshot(stampPoly) });
                logBrushGeom('trace', 'brush_stamp_simplified', { stamp: brushGeomSnapshot(stampPoly) });
                const masterSnapBeforeBool = brushGeomSnapshot(masterPolygons.length ? masterPolygons : null);
                masterPolygons = masterPolygons.length ? pc.union(masterPolygons, stampPoly) : stampPoly;
                logBrushGeom('trace', 'after_boolean_union', {
                    masterBeforeUnion: masterSnapBeforeBool,
                    masterAfterUnion: brushGeomSnapshot(masterPolygons)
                });
            }
        } catch (e) {
            logBrushGeom('error', 'boolean_op_threw', {
                message: e && e.message ? String(e.message) : String(e),
                stack: e && e.stack ? String(e.stack) : undefined,
                isEraser
            });
            logToUI('Brush geometry error. Set localStorage.wmeCandyPaintDebug=1 for [CandyPaint][brushGeom] boolean_op_threw.', true);
            return;
        }

        // Do not RDP the full master after union/difference: epsilonMeters scales with brush R (meters) and would
        // collapse thin pre-existing strokes and round buffer output into coarse polygons.

        // Brush: closed-ring RDP / union can collapse multi-loop strokes; union could return empty. Retry with minimal simplification.
        if (!isEraser && isMultipolyEmptyOrInvalid(masterPolygons) && masterBeforeBrush != null) {
            logBrushGeom('warn', 'brush_empty_invalid_after_main_path', {
                master: brushGeomSnapshot(masterPolygons),
                masterBefore: brushGeomSnapshot(masterBeforeBrush)
            });
            masterPolygons = cloneMasterPolygons(masterBeforeBrush);
            try {
                const stampPoly = buildAnyBrushStrokeStampMultipolygon(
                    pc,
                    centerLineLonLat,
                    radiusM,
                    refLat,
                    { reason: 'brush_empty_fallback' }
                );
                if (stampPoly && stampPoly.length) {
                    masterPolygons = masterBeforeBrush.length ? pc.union(masterBeforeBrush, stampPoly) : stampPoly;
                } else {
                    logBrushGeom('warn', 'brush_fallback_stamp_null', {});
                }
            } catch (e2) {
                logBrushGeom('error', 'brush_empty_fallback_caught', { message: e2 && e2.message ? String(e2.message) : String(e2) });
            }
            if (isMultipolyEmptyOrInvalid(masterPolygons)) {
                masterPolygons = cloneMasterPolygons(masterBeforeBrush);
                logBrushGeom('warn', 'brush_still_invalid_after_empty_fallback', { master: brushGeomSnapshot(masterPolygons) });
                logToUI('Brush stroke did not merge (empty result). Try smaller radius or fewer crossings per stroke.', true);
            } else {
                logBrushGeom('trace', 'brush_recovered_after_empty_fallback', { master: brushGeomSnapshot(masterPolygons) });
                logToUI('Brush: used fallback simplification for a complex loop.', false);
            }
        }

        // Brush: valid multipolygon but partial loss (merged loops / lost holes) — no log until now.
        if (!isEraser && masterBeforeBrush != null && !isMultipolyEmptyOrInvalid(masterPolygons)) {
            const beforeWasEmpty = masterBeforeBrush.length === 0;
            const sumBefore = sumOuterRingAreaDeg(masterBeforeBrush);
            const sumAfter = sumOuterRingAreaDeg(masterPolygons);
            const ringsBefore = totalRingCount(masterBeforeBrush);
            const ringsAfter = totalRingCount(masterPolygons);
            if (brushGeometryRegression(masterBeforeBrush, masterPolygons, beforeWasEmpty)) {
                logBrushGeom('warn', 'brush_regression_detected', {
                    sumBefore,
                    sumAfter,
                    ringsBefore,
                    ringsAfter,
                    partsBefore: masterBeforeBrush.length,
                    partsAfter: masterPolygons.length,
                    sharpTurns,
                    complexStroke,
                    ratio: sumBefore > 1e-12 ? sumAfter / sumBefore : null
                });
                logToUI('Brush: complex stroke — reduced simplification to preserve loops.', false);
                masterPolygons = cloneMasterPolygons(masterBeforeBrush);
                try {
                    const stampPoly = buildAnyBrushStrokeStampMultipolygon(
                        pc,
                        centerLineLonLat,
                        radiusM,
                        refLat,
                        { reason: 'brush_regression_retry' }
                    );
                    if (stampPoly && stampPoly.length) {
                        masterPolygons = masterBeforeBrush.length ? pc.union(masterBeforeBrush, stampPoly) : stampPoly;
                    } else {
                        logBrushGeom('warn', 'brush_regression_retry_stamp_null', {});
                    }
                } catch (e3) {
                    logBrushGeom('error', 'brush_regression_retry_caught', { message: e3 && e3.message ? String(e3.message) : String(e3) });
                }
                if (brushGeometryRegression(masterBeforeBrush, masterPolygons, beforeWasEmpty)) {
                    masterPolygons = cloneMasterPolygons(masterBeforeBrush);
                    try {
                        const stampPoly = buildAnyBrushStrokeStampMultipolygon(
                            pc,
                            centerLineLonLat,
                            radiusM,
                            refLat,
                            { reason: 'brush_regression_final' }
                        );
                        if (stampPoly && stampPoly.length)
                            masterPolygons = masterBeforeBrush.length ? pc.union(masterBeforeBrush, stampPoly) : stampPoly;
                    } catch (e4) {
                        logBrushGeom('error', 'brush_regression_final_caught', { message: e4 && e4.message ? String(e4.message) : String(e4) });
                    }
                    if (isMultipolyEmptyOrInvalid(masterPolygons)) {
                        masterPolygons = cloneMasterPolygons(masterBeforeBrush);
                        logBrushGeom('warn', 'brush_regression_final_invalid', { master: brushGeomSnapshot(masterPolygons) });
                        logToUI('Brush could not preserve geometry after retry.', true);
                    } else {
                        logBrushGeom('trace', 'brush_regression_recovered_unsimplified', { master: brushGeomSnapshot(masterPolygons) });
                    }
                }
            }
        }

        if (!isEraser) {
            const fin = brushGeomSnapshot(masterPolygons);
            logBrushGeom(fin.invalid && centerLineLonLat.length >= 2 ? 'warn' : 'trace', 'stroke_end', { fin });
            if (fin.invalid && centerLineLonLat.length >= 2) {
                logToUI('Brush: no valid polygon after stroke. Set localStorage.wmeCandyPaintDebug=1 for [CandyPaint][brushGeom] stroke_end.', true);
            }
        } else {
            logBrushGeom('trace', 'stroke_end', { isEraser: true, fin: brushGeomSnapshot(masterPolygons) });
        }

        const afterBool = masterPolygons.length;
        alignShapeLinksToMaster();
        if (afterBool === prevCount) {
            for (let i = 0; i < prevCount; i++) {
                if (prevLinks[i]) shapePlaceLinks[i] = prevLinks[i];
            }
        } else {
            reconcileLinksAfterPartCountChange(prevLinks, prevCount);
        }
        } finally {
            flushBrushGeometryToActiveLayer();
        }
    }

    /** Merge touching parts after a stroke; pairwise unions avoid one huge linear union (stack depth). */
    function coalesceMasterPartsAfterBrushStroke(pc) {
        if (masterPolygons.length <= 1) return;
        const linkedIdx = [];
        for (let i = 0; i < shapePlaceLinks.length; i++) {
            if (shapePlaceLinks[i] && shapePlaceLinks[i].venueId != null) linkedIdx.push(i);
        }
        if (linkedIdx.length > 1) return;
        const savedLink = linkedIdx.length === 1 ? cloneLinkEntry(shapePlaceLinks[linkedIdx[0]]) : null;
        if (masterPolygons.length > 32) return;
        try {
            let layer = masterPolygons.slice();
            let pass = 0;
            while (layer.length > 1) {
                if (pass >= BRUSH_COALESCE_MAX_PASSES) {
                    logBrushGeom('warn', 'coalesce_max_passes', { pass, partCount: layer.length });
                    break;
                }
                pass++;
                const partCountBefore = layer.length;
                const next = [];
                for (let i = 0; i < layer.length; i += 2) {
                    if (i + 1 < layer.length) {
                        const u = pc.union([layer[i]], [layer[i + 1]]);
                        for (let p = 0; p < u.length; p++) next.push(u[p]);
                    } else {
                        next.push(layer[i]);
                    }
                }
                layer = next;
                if (layer.length >= partCountBefore) {
                    logBrushGeom('trace', 'coalesce_stagnation_exit', { pass, partCount: layer.length });
                    break;
                }
            }
            masterPolygons = layer;
            alignShapeLinksToMaster();
            if (savedLink && masterPolygons.length === 1) {
                shapePlaceLinks[0] = cloneLinkEntry(savedLink);
                rebalanceVenueLinkActives({ layer: getActiveUserLayer(), shapeIndex: 0 });
            }
        } catch (e) { /* ignore */ }
        flushBrushGeometryToActiveLayer();
    }

    /** Commits the full stroke with high-quality disk unions + boolean ops (runs once on mouse-up). */
    function flushBrushStroke() {
        const pc = getGeometryEngine();
        if (!brushStrokeSamples.length) return;
        if (!pc) {
            brushStrokeSamples = [];
            return;
        }
        const radiusM = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
        const path = brushStrokeSamples.slice();
        commitBeforeChange(brushIsEraser ? 'eraser' : 'brush');
        applyBrushGeometryOnce(path, radiusM, brushIsEraser, !brushIsEraser);
        // Coalescing unions parts back together, which will "round" crisp shapes like rectangles.
        // Only do this after erasing where fragments are expected.
        if (brushIsEraser) coalesceMasterPartsAfterBrushStroke(pc);
        brushStrokeSamples = [];
    }

    function endBrushPainting() {
        if (!brushPainting) return;
        brushPainting = false;
        flushBrushStroke();
        requestRender();
        updateTrackerHUD();
    }

    /** GeoJSON Polygon (lon/lat) → one Multipolygon part for jsts: one polygon with one or more rings. */
    function sdkPolygonToClippingPart(poly) {
        if (!poly || poly.type !== "Polygon" || !poly.coordinates || !poly.coordinates.length) return null;
        return [poly.coordinates.map(ring => ring.map(([lon, lat]) => [lon, lat]))];
    }

    /** SDK Polygon or MultiPolygon → array of Candy parts (each part = array of rings). */
    function sdkGeometryToClippingParts(geometry) {
        if (!geometry) return [];
        if (geometry.type === 'Polygon' && geometry.coordinates && geometry.coordinates.length) {
            const one = sdkPolygonToClippingPart(geometry);
            return one ? [one] : [];
        }
        if (geometry.type === 'MultiPolygon' && geometry.coordinates && geometry.coordinates.length) {
            const out = [];
            for (let p = 0; p < geometry.coordinates.length; p++) {
                const polyCoords = geometry.coordinates[p];
                if (!polyCoords || !polyCoords.length) continue;
                out.push(polyCoords.map(ring => ring.map(([lon, lat]) => [lon, lat])));
            }
            return out;
        }
        return [];
    }

    /** One polygon part from masterPolygons (array of rings) → SDK Polygon GeoJSON. */
    function masterPolyPartToSdkPolygon(polyPart) {
        const coordinates = polyPart.map(ring => {
            let r = ring.map(([lon, lat]) => [lon, lat]);
            if (r.length && (r[0][0] !== r[r.length - 1][0] || r[0][1] !== r[r.length - 1][1]))
                r = [...r, r[0]];
            return r;
        });
        return { type: "Polygon", coordinates };
    }

    function getSelectedAreaVenues() {
        const selected = [];
        if (!wmeSDK || !wmeSDK.Editing || !wmeSDK.DataModel || !wmeSDK.DataModel.Venues) return selected;
        const sel = wmeSDK.Editing.getSelection();
        if (!sel || sel.objectType !== "venue" || !sel.ids || !sel.ids.length) return selected;

        sel.ids.forEach(id => {
            const venue = wmeSDK.DataModel.Venues.getById({ venueId: id });
            if (!venue) return;
            const g = venue.geometry;
            if (!g) return;
            if (g.type === "Point") return;
            if (g.type === "Polygon" && g.coordinates && g.coordinates.length) selected.push(venue);
            else if (g.type === "MultiPolygon" && g.coordinates && g.coordinates.length) selected.push(venue);
        });
        return selected;
    }

    function normalizeVenueIdForSdk(id) {
        if (typeof id === 'number' && Number.isFinite(id)) return id;
        if (typeof id === 'string' && id.trim() !== '') {
            const n = Number(id);
            if (Number.isFinite(n) && String(n) === id.trim()) return n;
        }
        return id;
    }

    function venueIdsMatch(a, b) {
        if (a == null || b == null) return false;
        return String(a) === String(b);
    }

    /**
     * Shape–place link: { venueId, active?, broken?, isInvalid?, splitGroupId? }.
     * Legacy { venueId } ⇒ active true, not broken.
     */
    function normalizeShapeLink(raw) {
        if (!raw || raw.venueId == null) return null;
        const venueId = normalizeVenueIdForSdk(raw.venueId);
        const broken = raw.broken === true;
        const active = raw.active === false ? false : true;
        const o = { venueId, active, broken };
        if (raw.splitGroupId) o.splitGroupId = raw.splitGroupId;
        if (raw.isInvalid) o.isInvalid = true;
        return o;
    }

    function cloneLinkEntry(l) {
        if (!l || l.venueId == null) return null;
        const o = {
            venueId: normalizeVenueIdForSdk(l.venueId),
            active: l.active === false ? false : true,
            broken: l.broken === true
        };
        if (l.splitGroupId) o.splitGroupId = l.splitGroupId;
        if (l.isInvalid) o.isInvalid = true;
        return o;
    }

    /** True if Apply should use updateVenue for this link (not addVenue). */
    function isActiveNonBrokenLink(link) {
        const L = normalizeShapeLink(link);
        return !!(L && L.venueId != null && L.active !== false && L.broken !== true);
    }

    /** Apply blocks only on active, non-broken link problems (§1.9). */
    function linkBlocksApplyValidation(link) {
        return isActiveNonBrokenLink(link);
    }

    /**
     * One active (non-broken) link per venue globally. Optional preferred { layer, shapeIndex } wins for that venue.
     */
    function rebalanceVenueLinkActives(preferred) {
        const prefLayer = preferred && preferred.layer;
        const prefIdx = preferred && typeof preferred.shapeIndex === 'number' ? preferred.shapeIndex : -1;
        const byVenue = new Map();
        for (let li = 0; li < userLayers.length; li++) {
            const layer = userLayers[li];
            if (layer.isPlaces) continue;
            const links = layer.links;
            for (let i = 0; i < links.length; i++) {
                const L = normalizeShapeLink(links[i]);
                if (!L || L.venueId == null || L.broken === true) continue;
                const k = String(normalizeVenueIdForSdk(L.venueId));
                if (!byVenue.has(k)) byVenue.set(k, []);
                byVenue.get(k).push({ layer, i });
            }
        }
        for (const [, entries] of byVenue) {
            if (!entries.length) continue;
            let winner = entries.find(e => prefLayer && e.layer === prefLayer && e.i === prefIdx) || entries[0];
            for (let e = 0; e < entries.length; e++) {
                const ent = entries[e];
                const L = normalizeShapeLink(ent.layer.links[ent.i]);
                const isWin = ent.layer === winner.layer && ent.i === winner.i;
                ent.layer.links[ent.i] = { ...L, active: isWin, broken: false };
            }
        }
        syncActiveLayerAliases();
    }

    /**
     * After updateVenue(V): other shapes with active non-broken link to V → broken (winner keeps link).
     * @param {*} updatedVenueId
     * @param {object|null} winnerLayer user layer object (not clone)
     * @param {number} winnerShapeIndex index in layer.polygons / layer.links
     */
    function breakCompetingVenueLinksForVenue(updatedVenueId, winnerLayer, winnerShapeIndex) {
        const norm = normalizeVenueIdForSdk(updatedVenueId);
        for (let li = 0; li < userLayers.length; li++) {
            const layer = userLayers[li];
            if (layer.isPlaces) continue;
            for (let i = 0; i < layer.links.length; i++) {
                if (winnerLayer && layer === winnerLayer && i === winnerShapeIndex) continue;
                const L = normalizeShapeLink(layer.links[i]);
                if (!L || L.venueId == null) continue;
                if (!venueIdsMatch(L.venueId, norm)) continue;
                if (L.broken === true) continue;
                if (L.active === false) continue;
                layer.links[i] = { ...L, active: false, broken: true };
            }
        }
        syncActiveLayerAliases();
    }

    function alignShapeLinksToMaster() {
        while (shapePlaceLinks.length < masterPolygons.length) shapePlaceLinks.push(null);
        shapePlaceLinks.length = masterPolygons.length;
    }

    function generateLayerId() {
        return 'L' + Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
    }
    function generateShapeId() {
        return 'S' + Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
    }
    function migrateLegacyLayerName(name) {
        if (name === 'Master' || name === 'master') return DEFAULT_SHAPE_LAYER_NAME;
        return name || DEFAULT_SHAPE_LAYER_NAME;
    }
    function getUserLayerById(id) {
        return userLayers.find(l => l.id === id) || null;
    }
    function getActiveUserLayer() {
        return activeLayerId ? getUserLayerById(activeLayerId) : null;
    }
    function getActiveUserLayerShapeCount() {
        const L = getActiveUserLayer();
        return L && L.polygons ? L.polygons.length : 0;
    }
    function hasAnyUserLayerPolygons() {
        for (let i = 0; i < userLayers.length; i++) {
            if (userLayers[i].polygons && userLayers[i].polygons.length) return true;
        }
        return false;
    }
    function syncActiveLayerAliases() {
        const L = getActiveUserLayer();
        if (!L) {
            masterPolygons = [];
            shapePlaceLinks = [];
            return;
        }
        masterPolygons = L.polygons;
        shapePlaceLinks = L.links;
    }
    function ensureShapeIdsForLayer(layer) {
        if (!layer.shapeIds) layer.shapeIds = [];
        while (layer.shapeIds.length < layer.polygons.length) layer.shapeIds.push(generateShapeId());
        layer.shapeIds.length = layer.polygons.length;
    }
    function createUserLayer(nameOpt) {
        const layer = {
            id: generateLayerId(),
            isPlaces: false,
            visible: true,
            name: migrateLegacyLayerName(nameOpt != null ? nameOpt : DEFAULT_SHAPE_LAYER_NAME),
            polygons: [],
            links: [],
            shapeIds: []
        };
        userLayers.push(layer);
        ensureShapeIdsForLayer(layer);
        return layer;
    }
    function validateActiveLayerId() {
        if (!activeLayerId || !getUserLayerById(activeLayerId)) {
            activeLayerId = userLayers.length ? userLayers[0].id : null;
        }
    }
    function flushBrushGeometryToActiveLayer() {
        const L = getActiveUserLayer();
        if (!L) return;
        L.polygons = masterPolygons;
        L.links = shapePlaceLinks;
        masterPolygons = L.polygons;
        shapePlaceLinks = L.links;
        ensureShapeIdsForLayer(L);
        scheduleDocumentSave();
    }
    function getActiveLayerDisplayName() {
        const L = getActiveUserLayer();
        return L ? L.name : DEFAULT_SHAPE_LAYER_NAME;
    }

    const PAINT_HISTORY_CAP = 200;
    let paintUndoStack = [];
    let paintRedoStack = [];
    let paintHistoryApplying = false;

    const HISTORY_TAG_TO_LABEL = {
        brush: 'Brush stroke',
        eraser: 'Eraser',
        ingest: 'Ingest',
        clear: 'Clear canvas',
        layer_merge: 'Layer merge',
        layer_add: 'Add layer',
        layer_del: 'Delete layer',
        layer_dup: 'Duplicate layer',
        layer_reorder: 'Reorder layer',
        layer_rename: 'Rename layer',
        layer_active: 'Active layer',
        layer_visibility: 'Layer visibility',
        shape_dup: 'Duplicate shape',
        shape_del: 'Delete shapes',
        shape_move: 'Move shape',
        shape_reorder: 'Reorder shapes',
        unlink: 'Unlink',
        link: 'Link',
        link_activate: 'Activate link',
        pop_draft: 'Pop to draft',
        draft_commit: 'Draft commit',
        draft_cancel_restore: 'Restore shape',
        move: 'Move shapes',
        edit: 'Edit'
    };

    function historyLabelForTag(tag, explicitLabel) {
        if (explicitLabel) return explicitLabel;
        if (tag && HISTORY_TAG_TO_LABEL[tag]) return HISTORY_TAG_TO_LABEL[tag];
        return HISTORY_TAG_TO_LABEL.edit;
    }

    function paintHistoryEntrySnapshot(entry) {
        if (!entry) return null;
        if (entry.snapshot) return entry.snapshot;
        return entry;
    }

    function trimUndoStackToCap() {
        while (paintUndoStack.length > PAINT_HISTORY_CAP) paintUndoStack.shift();
    }

    function cloneShapePlaceLinks(src) {
        if (!src || !src.length) return [];
        return src.map(l => cloneLinkEntry(l));
    }

    function saveDocumentStateNow() {
        try {
            localStorage.setItem(LS_DOCUMENT_KEY, JSON.stringify({
                v: DOCUMENT_STATE_VERSION,
                activeLayerId,
                userLayers: userLayers.map(l => ({
                    id: l.id,
                    name: l.name,
                    visible: l.visible !== false,
                    polygons: cloneMasterPolygons(l.polygons),
                    links: cloneShapePlaceLinks(l.links),
                    shapeIds: (l.shapeIds || []).slice()
                }))
            }));
        } catch (e) { /* ignore quota / privacy mode */ }
    }

    function scheduleDocumentSave() {
        if (documentSaveTimer) clearTimeout(documentSaveTimer);
        documentSaveTimer = setTimeout(() => {
            documentSaveTimer = null;
            saveDocumentStateNow();
        }, 320);
    }

    function loadDocumentState() {
        try {
            const raw = localStorage.getItem(LS_DOCUMENT_KEY);
            if (!raw) return false;
            const data = JSON.parse(raw);
            if ((data.v !== DOCUMENT_STATE_VERSION && data.v !== 1) || !Array.isArray(data.userLayers) || !data.userLayers.length) return false;
            userLayers = data.userLayers.map(l => ({
                id: l.id || generateLayerId(),
                isPlaces: false,
                visible: l.visible !== false,
                name: migrateLegacyLayerName(l.name || DEFAULT_SHAPE_LAYER_NAME),
                polygons: cloneMasterPolygons(l.polygons),
                links: (l.links || []).map(x => normalizeShapeLink(x)),
                shapeIds: Array.isArray(l.shapeIds) ? l.shapeIds.slice() : []
            }));
            userLayers.forEach(ensureShapeIdsForLayer);
            activeLayerId = data.activeLayerId;
            validateActiveLayerId();
            return true;
        } catch (e) {
            return false;
        }
    }

    function initPaintDocument() {
        placesPreviewLayer.polygons = [];
        placesPreviewLayer.links = [];
        placesPreviewLayer.shapeIds = [];
        if (!loadDocumentState()) {
            userLayers = [];
            activeLayerId = createUserLayer(DEFAULT_SHAPE_LAYER_NAME).id;
        }
        syncActiveLayerAliases();
    }

    /** Cached polygon places in viewport (same source as Places preview + list UI). */
    let viewportPlacesVenuesCache = [];

    function getMapExtentLonLat() {
        const map = getHostMap();
        if (!map || typeof map.getExtent !== 'function') return null;
        const ext = map.getExtent();
        const proj = getMapProjection();
        const OL = window.OpenLayers;
        if (!ext || !proj || !OL?.Geometry?.Point || !OL?.Projection) return null;
        try {
            const p4326 = new OL.Projection('EPSG:4326');
            const pts = [
                new OL.Geometry.Point(ext.left, ext.bottom),
                new OL.Geometry.Point(ext.right, ext.bottom),
                new OL.Geometry.Point(ext.right, ext.top),
                new OL.Geometry.Point(ext.left, ext.top)
            ];
            let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
            for (let i = 0; i < pts.length; i++) {
                pts[i].transform(proj, p4326);
                const x = pts[i].x, y = pts[i].y;
                minLon = Math.min(minLon, x); maxLon = Math.max(maxLon, x);
                minLat = Math.min(minLat, y); maxLat = Math.max(maxLat, y);
            }
            return { minLon, minLat, maxLon, maxLat };
        } catch (e) {
            return null;
        }
    }

    function ringLonLatBounds(ring) {
        let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
        if (!ring || !ring.length) return { minLon, minLat, maxLon, maxLat };
        for (let i = 0; i < ring.length; i++) {
            const lon = ring[i][0], lat = ring[i][1];
            minLon = Math.min(minLon, lon); maxLon = Math.max(maxLon, lon);
            minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat);
        }
        return { minLon, minLat, maxLon, maxLat };
    }

    function bboxesOverlap(a, b) {
        return a.minLon <= b.maxLon && a.maxLon >= b.minLon && a.minLat <= b.maxLat && a.maxLat >= b.minLat;
    }

    /** Union bbox of MultiPolygon outer rings (lon/lat). */
    function multiPolygonLonLatBounds(g) {
        if (!g || g.type !== 'MultiPolygon' || !g.coordinates || !g.coordinates.length) return null;
        let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
        for (let p = 0; p < g.coordinates.length; p++) {
            const poly = g.coordinates[p];
            if (!poly || !poly[0]) continue;
            const rb = ringLonLatBounds(poly[0]);
            minLon = Math.min(minLon, rb.minLon); maxLon = Math.max(maxLon, rb.maxLon);
            minLat = Math.min(minLat, rb.minLat); maxLat = Math.max(maxLat, rb.maxLat);
        }
        if (!Number.isFinite(minLon)) return null;
        return { minLon, minLat, maxLon, maxLat };
    }

    /** Polygon area venues whose bounds intersect the current map view (WME native model). */
    function getViewportPolygonVenues() {
        const bbox = getMapExtentLonLat();
        if (!bbox) return [];
        const objs = window.W?.model?.venues?.objects;
        if (!objs) return [];
        const out = [];
        for (const k in objs) {
            if (!Object.prototype.hasOwnProperty.call(objs, k)) continue;
            const v = objs[k];
            const g = v?.geometry;
            if (!g || !g.coordinates?.length) continue;
            let rb = null;
            if (g.type === 'Polygon') rb = ringLonLatBounds(g.coordinates[0]);
            else if (g.type === 'MultiPolygon') rb = multiPolygonLonLatBounds(g);
            if (!rb || !bboxesOverlap(rb, bbox)) continue;
            const id = v.attributes?.id != null ? v.attributes.id : (v.id != null ? v.id : k);
            const name = v.attributes?.name || v.name || String(id);
            out.push({ id, geometry: g, name });
        }
        return out;
    }

    function rebuildPlacesPreviewPolygonsFromVenues(venues) {
        const flat = [];
        for (let i = 0; i < venues.length; i++) {
            const parts = sdkGeometryToClippingParts(venues[i].geometry);
            for (let j = 0; j < parts.length; j++) flat.push(parts[j]);
        }
        placesPreviewLayer.polygons = flat;
        placesPreviewLayer.links = new Array(flat.length).fill(null);
        ensureShapeIdsForLayer(placesPreviewLayer);
    }

    function refreshPlacesPreviewFromMap() {
        viewportPlacesVenuesCache = getViewportPolygonVenues();
        rebuildPlacesPreviewPolygonsFromVenues(viewportPlacesVenuesCache);
    }

    function csShapeKey(layerId, shapeId) {
        return String(layerId) + ':' + String(shapeId);
    }
    function parseShapeKey(key) {
        const i = key.indexOf(':');
        if (i <= 0) return null;
        return { layerId: key.slice(0, i), shapeId: key.slice(i + 1) };
    }
    function escapeHtmlCs(s) {
        return String(s)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }
    function isCandyPaintUiRoot(node) {
        return !!(node && node.closest && node.closest('#wme-paint-palette,#wme-cs-layers-panel,#wme-cs-history-panel,#wme-paint-hud,#wme-paint-log,.csl-overflow-backdrop'));
    }
    function isCsLayersPanelOpen() {
        const p = document.getElementById('wme-cs-layers-panel');
        return !!(p && !p.classList.contains('csl-hidden'));
    }
    function demoteLinkForDuplicate(link) {
        const L = normalizeShapeLink(link);
        if (!L || L.venueId == null) return null;
        return normalizeShapeLink(Object.assign({}, L, { active: false, broken: false }));
    }
    function clonePolygonPart(part) {
        if (!part) return null;
        return part.map(ring => ring.map(pt => [pt[0], pt[1]]));
    }
    function layerLinkBadgeText(layer) {
        let n = 0, b = 0;
        const links = layer.links || [];
        for (let i = 0; i < links.length; i++) {
            const L = normalizeShapeLink(links[i]);
            if (!L || L.venueId == null) continue;
            n++;
            if (L.broken === true) b++;
        }
        if (!n) return '';
        return b ? `${n} linked · ${b} broken` : `${n} linked`;
    }
    function mergeLonLatBboxes(a, b) {
        if (!a) return b;
        if (!b) return a;
        return {
            minLon: Math.min(a.minLon, b.minLon),
            maxLon: Math.max(a.maxLon, b.maxLon),
            minLat: Math.min(a.minLat, b.minLat),
            maxLat: Math.max(a.maxLat, b.maxLat)
        };
    }
    function bboxForPartLonLat(part) {
        let acc = null;
        if (!part) return null;
        for (let r = 0; r < part.length; r++) {
            const ring = part[r];
            if (!ring || !ring.length) continue;
            acc = mergeLonLatBboxes(acc, ringLonLatBounds(ring));
        }
        return acc;
    }
    function zoomMapToLonLatBBox(bbox) {
        if (!bbox) return;
        const map = getHostMap();
        const OL = window.OpenLayers;
        const proj = getMapProjection();
        if (!map || !OL || !proj || typeof map.zoomToExtent !== 'function') return;
        try {
            const p4326 = new OL.Projection('EPSG:4326');
            const sw = new OL.Geometry.Point(bbox.minLon, bbox.minLat);
            const ne = new OL.Geometry.Point(bbox.maxLon, bbox.maxLat);
            sw.transform(p4326, proj);
            ne.transform(p4326, proj);
            const minX = Math.min(sw.x, ne.x);
            const maxX = Math.max(sw.x, ne.x);
            const minY = Math.min(sw.y, ne.y);
            const maxY = Math.max(sw.y, ne.y);
            const res = typeof map.getResolution === 'function' ? map.getResolution() : 1;
            const padX = Math.max((maxX - minX) * 0.12, 64 * res);
            const padY = Math.max((maxY - minY) * 0.12, 64 * res);
            const bounds = new OL.Bounds(minX - padX, minY - padY, maxX + padX, maxY + padY);
            map.zoomToExtent(bounds);
        } catch (e) { /* ignore */ }
    }
    function pruneShapeInspectorSelection() {
        for (const key of [...csLayersSelection]) {
            const p = parseShapeKey(key);
            if (!p) {
                csLayersSelection.delete(key);
                continue;
            }
            const L = getUserLayerById(p.layerId);
            if (!L || L.isPlaces) {
                csLayersSelection.delete(key);
                continue;
            }
            ensureShapeIdsForLayer(L);
            const ix = (L.shapeIds || []).indexOf(p.shapeId);
            if (ix < 0) csLayersSelection.delete(key);
        }
    }
    function scheduleShapeInspectorRefresh() {
        if (shapeInspectorRefreshScheduled) return;
        shapeInspectorRefreshScheduled = true;
        requestAnimationFrame(() => {
            shapeInspectorRefreshScheduled = false;
            renderShapeInspectorList();
            updateActiveLayerReadout();
        });
    }
    function updateActiveLayerReadout() {
        const el = document.getElementById('wpo-active-layer-name');
        if (el) el.textContent = getActiveLayerDisplayName();
    }
    function setActiveLayerById(id, opts) {
        if (!id || id === activeLayerId) return;
        if (!opts || !opts.skipHistory) commitBeforeChange('layer_active');
        activeLayerId = id;
        validateActiveLayerId();
        syncActiveLayerAliases();
        scheduleDocumentSave();
        requestRender();
        updateTrackerHUD();
        handleSelectionChange();
        scheduleShapeInspectorRefresh();
    }
    function swapUserLayersAt(i, j) {
        if (i < 0 || j < 0 || i >= userLayers.length || j >= userLayers.length || i === j) return;
        const t = userLayers[i];
        userLayers[i] = userLayers[j];
        userLayers[j] = t;
    }
    function mergeLayerDownAtIndex(topIdx) {
        if (topIdx <= 0 || topIdx >= userLayers.length) return;
        commitBeforeChange('layer_merge');
        const lower = userLayers[topIdx - 1];
        const upper = userLayers[topIdx];
        for (let i = 0; i < upper.polygons.length; i++) {
            lower.polygons.push(clonePolygonPart(upper.polygons[i]));
            lower.links.push(cloneLinkEntry(upper.links[i]));
            lower.shapeIds.push(generateShapeId());
        }
        ensureShapeIdsForLayer(lower);
        const removedId = upper.id;
        userLayers.splice(topIdx, 1);
        if (activeLayerId === removedId) activeLayerId = lower.id;
        validateActiveLayerId();
        rebalanceVenueLinkActives();
        syncActiveLayerAliases();
        alignShapeLinksToMaster();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
        updateTrackerHUD();
        logToUI(`Merged layer into "${lower.name}".`, false);
    }
    function duplicateUserLayerAtIndex(vi) {
        if (vi < 0 || vi >= userLayers.length) return;
        const src = userLayers[vi];
        commitBeforeChange('layer_dup');
        const copy = createUserLayer((src.name || 'Layer') + ' copy');
        for (let i = 0; i < src.polygons.length; i++) {
            copy.polygons.push(clonePolygonPart(src.polygons[i]));
            copy.links.push(demoteLinkForDuplicate(src.links[i]));
            copy.shapeIds.push(generateShapeId());
        }
        ensureShapeIdsForLayer(copy);
        userLayers.pop();
        userLayers.splice(vi + 1, 0, copy);
        activeLayerId = copy.id;
        syncActiveLayerAliases();
        alignShapeLinksToMaster();
        rebalanceVenueLinkActives();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
        updateTrackerHUD();
        logToUI(`Duplicated layer "${src.name}".`, false);
    }
    function duplicateShapeOnLayer(layer, shapeIndex) {
        if (!layer || layer.isPlaces || shapeIndex < 0 || shapeIndex >= layer.polygons.length) return;
        commitBeforeChange('shape_dup');
        layer.polygons.splice(shapeIndex + 1, 0, clonePolygonPart(layer.polygons[shapeIndex]));
        layer.links.splice(shapeIndex + 1, 0, demoteLinkForDuplicate(layer.links[shapeIndex]));
        layer.shapeIds.splice(shapeIndex + 1, 0, generateShapeId());
        ensureShapeIdsForLayer(layer);
        if (layer.id === activeLayerId) syncActiveLayerAliases();
        rebalanceVenueLinkActives();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
    }
    function deleteShapesBySelectionKeys() {
        const byLayer = new Map();
        for (const key of csLayersSelection) {
            const p = parseShapeKey(key);
            if (!p) continue;
            const L = getUserLayerById(p.layerId);
            if (!L || L.isPlaces) continue;
            ensureShapeIdsForLayer(L);
            const ix = L.shapeIds.indexOf(p.shapeId);
            if (ix < 0) continue;
            if (!byLayer.has(L.id)) byLayer.set(L.id, []);
            byLayer.get(L.id).push(ix);
        }
        if (!byLayer.size) return;
        commitBeforeChange('shape_del');
        const layerIds = [...byLayer.keys()];
        for (let li = 0; li < layerIds.length; li++) {
            const lid = layerIds[li];
            const L = getUserLayerById(lid);
            if (!L) continue;
            const indices = byLayer.get(lid).sort((a, b) => b - a);
            for (let k = 0; k < indices.length; k++) {
                const si = indices[k];
                L.polygons.splice(si, 1);
                L.links.splice(si, 1);
                L.shapeIds.splice(si, 1);
            }
            ensureShapeIdsForLayer(L);
        }
        csLayersSelection.clear();
        syncActiveLayerAliases();
        alignShapeLinksToMaster();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
        updateTrackerHUD();
    }
    function unlinkShapesBySelectionKeys() {
        let willTouch = false;
        for (const key of [...csLayersSelection]) {
            const p = parseShapeKey(key);
            if (!p) continue;
            const L = getUserLayerById(p.layerId);
            if (!L || L.isPlaces) continue;
            const ix = (L.shapeIds || []).indexOf(p.shapeId);
            if (ix < 0 || !L.links[ix]) continue;
            willTouch = true;
            break;
        }
        if (!willTouch) return;
        commitBeforeChange('unlink');
        let touched = false;
        for (const key of [...csLayersSelection]) {
            const p = parseShapeKey(key);
            if (!p) continue;
            const L = getUserLayerById(p.layerId);
            if (!L || L.isPlaces) continue;
            const ix = (L.shapeIds || []).indexOf(p.shapeId);
            if (ix < 0) continue;
            if (L.links[ix]) {
                L.links[ix] = null;
                touched = true;
            }
        }
        if (!touched) return;
        syncActiveLayerAliases();
        alignShapeLinksToMaster();
        rebalanceVenueLinkActives();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
    }
    function zoomToSelectionKeysBbox() {
        let acc = null;
        for (const key of csLayersSelection) {
            const p = parseShapeKey(key);
            if (!p) continue;
            const L = getUserLayerById(p.layerId);
            if (!L || L.isPlaces) continue;
            const ix = (L.shapeIds || []).indexOf(p.shapeId);
            if (ix < 0 || !L.polygons[ix]) continue;
            acc = mergeLonLatBboxes(acc, bboxForPartLonLat(L.polygons[ix]));
        }
        if (acc) zoomMapToLonLatBBox(acc);
    }
    function moveShapeToLayer(fromLayer, shapeIndex, targetLayerId) {
        if (!fromLayer || fromLayer.isPlaces || shapeIndex < 0 || shapeIndex >= fromLayer.polygons.length) return;
        const tgt = getUserLayerById(targetLayerId);
        if (!tgt || tgt.isPlaces || tgt.id === fromLayer.id) return;
        commitBeforeChange('shape_move');
        const part = fromLayer.polygons.splice(shapeIndex, 1)[0];
        const link = fromLayer.links.splice(shapeIndex, 1)[0];
        fromLayer.shapeIds.splice(shapeIndex, 1);
        tgt.polygons.push(part);
        tgt.links.push(cloneLinkEntry(link));
        tgt.shapeIds.push(generateShapeId());
        ensureShapeIdsForLayer(fromLayer);
        ensureShapeIdsForLayer(tgt);
        syncActiveLayerAliases();
        alignShapeLinksToMaster();
        rebalanceVenueLinkActives();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
    }
    function swapShapesInLayer(layer, i, j) {
        if (!layer || i === j || i < 0 || j < 0 || i >= layer.polygons.length || j >= layer.polygons.length) return;
        commitBeforeChange('shape_reorder');
        const tp = layer.polygons[i];
        layer.polygons[i] = layer.polygons[j];
        layer.polygons[j] = tp;
        const tl = layer.links[i];
        layer.links[i] = layer.links[j];
        layer.links[j] = tl;
        const ts = layer.shapeIds[i];
        layer.shapeIds[i] = layer.shapeIds[j];
        layer.shapeIds[j] = ts;
        if (layer.id === activeLayerId) syncActiveLayerAliases();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
    }
    function tryActivateShapeLinkOnLayer(layer, shapeIndex) {
        if (!layer || layer.isPlaces || shapeIndex < 0 || shapeIndex >= layer.polygons.length) return;
        const cur = normalizeShapeLink(layer.links[shapeIndex]);
        if (!cur || cur.venueId == null) {
            logToUI('Shape has no place link to activate.', true);
            return;
        }
        commitBeforeChange('link_activate');
        layer.links[shapeIndex] = Object.assign({}, cur, { active: true, broken: false });
        rebalanceVenueLinkActives({ layer, shapeIndex });
        scheduleDocumentSave();
        requestRender();
        handleSelectionChange();
        scheduleShapeInspectorRefresh();
        logToUI(`Active link set on shape in "${layer.name}".`, false);
    }
    function deleteUserLayerById(layerId, skipConfirm) {
        const vi = userLayers.findIndex(l => l.id === layerId);
        if (vi < 0) return;
        if (userLayers.length <= 1) {
            logToUI('Cannot remove the last user layer.', true);
            return;
        }
        const L = userLayers[vi];
        if (L.polygons && L.polygons.length && !skipConfirm) {
            if (!window.confirm(`Delete layer "${L.name}" with ${L.polygons.length} shape(s)? You can undo from the toolbar.`)) return;
        }
        commitBeforeChange('layer_del');
        userLayers.splice(vi, 1);
        if (activeLayerId === layerId) {
            activeLayerId = userLayers[Math.max(0, vi - 1)].id;
        }
        validateActiveLayerId();
        syncActiveLayerAliases();
        alignShapeLinksToMaster();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
        updateTrackerHUD();
        logToUI('Layer deleted.', false);
    }
    function renderShapeInspectorList() {
        const root = document.getElementById('wpo-cs-layers-list-root');
        if (!root) return;
        const filterRaw = (document.getElementById('csl-filter-input') && document.getElementById('csl-filter-input').value) || csLayersNameFilter || '';
        const fl = filterRaw.trim().toLowerCase();
        let html = '';
        const n = userLayers.length;
        for (let vi = n - 1; vi >= 0; vi--) {
            const layer = userLayers[vi];
            if (fl && !(String(layer.name || '').toLowerCase().includes(fl))) continue;
            const isActive = layer.id === activeLayerId;
            const collapsed = !!csLayersCollapsed[layer.id];
            const badge = layerLinkBadgeText(layer);
            const headCls = 'csl-layer-head' + (isActive ? ' csl-active-layer' : '');
            html += `<div class="csl-layer-block" data-layer-id="${escapeHtmlCs(layer.id)}">`;
            html += `<div class="${headCls}" tabindex="0" data-csl-focus="layer" data-csl-drop-layer="${vi}" data-layer-id="${escapeHtmlCs(layer.id)}">`;
            html += `<span class="csl-drag-h" draggable="true" data-csl-drag-layer="${vi}" title="Drag to reorder" aria-label="Drag layer row">⠿</span>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-eye="${vi}" aria-label="${layer.visible === false ? 'Show layer' : 'Hide layer'}">${layer.visible === false ? '○' : '●'}</button>`;
            html += `<span class="csl-name" data-csl-rename="${vi}" title="Double-click to rename">${escapeHtmlCs(layer.name || 'Layer')}</span>`;
            if (badge) html += `<span class="csl-badge" title="Link summary">${escapeHtmlCs(badge)}</span>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-layer-up="${vi}" ${vi >= n - 1 ? 'disabled' : ''} aria-label="Move layer up">↑</button>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-layer-dn="${vi}" ${vi <= 0 ? 'disabled' : ''} aria-label="Move layer down">↓</button>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-dup-layer="${vi}" aria-label="Duplicate layer">Dup</button>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-merge="${vi}" ${vi <= 0 ? 'disabled' : ''} title="Merge into layer below (toward map background)" aria-label="Merge down">M↓</button>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-del-layer="${vi}" aria-label="Delete layer">🗑</button>`;
            html += `<button type="button" class="csl-icon-btn" data-csl-collapse="${vi}" aria-label="${collapsed ? 'Expand' : 'Collapse'}">${collapsed ? '▶' : '▼'}</button>`;
            html += `</div>`;
            if (!collapsed) {
                const polys = layer.polygons || [];
                ensureShapeIdsForLayer(layer);
                for (let si = 0; si < polys.length; si++) {
                    const sid = layer.shapeIds[si];
                    const key = csShapeKey(layer.id, sid);
                    const sel = csLayersSelection.has(key);
                    const Ln = normalizeShapeLink(layer.links[si]);
                    let pill = '—';
                    if (Ln && Ln.venueId != null) {
                        if (Ln.isInvalid && isActiveNonBrokenLink(Ln)) pill = '! ' + String(Ln.venueId).slice(-4);
                        else if (Ln.broken === true) pill = 'B·' + String(Ln.venueId).slice(-4);
                        else if (Ln.active === false) pill = 'I·' + String(Ln.venueId).slice(-4);
                        else pill = 'U·' + String(Ln.venueId).slice(-4);
                    }
                    html += `<div class="csl-shape-row${sel ? ' csl-sel' : ''}" tabindex="0" data-csl-focus="shape" data-shape-key="${escapeHtmlCs(key)}" data-layer-id="${escapeHtmlCs(layer.id)}" data-shape-idx="${si}">`;
                    html += `<span class="csl-shape-lab">Shape ${si + 1}</span>`;
                    html += `<span class="csl-link-pill">${escapeHtmlCs(pill)}</span>`;
                    html += `<button type="button" class="csl-icon-btn" data-csl-zoom-shape="${escapeHtmlCs(key)}" aria-label="Zoom to shape">⌖</button>`;
                    html += `<button type="button" class="csl-icon-btn" data-csl-dup-shape="${escapeHtmlCs(key)}" aria-label="Duplicate shape">Dup</button>`;
                    html += `<button type="button" class="csl-icon-btn" data-csl-shape-up="${escapeHtmlCs(key)}" ${si <= 0 ? 'disabled' : ''} aria-label="Raise shape">↑</button>`;
                    html += `<button type="button" class="csl-icon-btn" data-csl-shape-dn="${escapeHtmlCs(key)}" ${si >= polys.length - 1 ? 'disabled' : ''} aria-label="Lower shape">↓</button>`;
                    html += `<button type="button" class="csl-icon-btn" data-csl-act-shape="${escapeHtmlCs(key)}" aria-label="Activate link">Act</button>`;
                    html += `<button type="button" class="csl-icon-btn" data-csl-del-shape="${escapeHtmlCs(key)}" aria-label="Delete shape">×</button>`;
                    html += `</div>`;
                }
                const moveOpts = '<option value="">— layer —</option>' + userLayers.filter(u => !u.isPlaces && u.id !== layer.id)
                    .map(u => `<option value="${escapeHtmlCs(u.id)}">${escapeHtmlCs(u.name)}</option>`).join('');
                if (polys.length && moveOpts) {
                    html += `<div style="padding:2px 8px 6px;font-size:9px;color:#6c757d;">Move selected shape:</div>`;
                    for (let si = 0; si < polys.length; si++) {
                        const sid = layer.shapeIds[si];
                        const key = csShapeKey(layer.id, sid);
                        html += `<div style="display:flex;align-items:center;gap:4px;padding:0 8px 4px 14px;">`;
                        html += `<span style="font-size:9px;">#${si + 1}</span>`;
                        html += `<select class="csl-move-select" data-csl-move="${escapeHtmlCs(key)}" aria-label="Move shape to layer">${moveOpts}</select>`;
                        html += `</div>`;
                    }
                }
            }
            html += `</div>`;
        }
        html += `<div class="csl-places-section">`;
        html += `<div class="csl-places-h">Places in view (preview, not applied)</div>`;
        const venues = viewportPlacesVenuesCache || [];
        if (!venues.length) {
            html += `<div class="wpo-places-empty">No polygon or multipolygon places in view</div>`;
        } else {
            for (let i = 0; i < venues.length; i++) {
                const v = venues[i];
                const labelText = String(v.name != null ? v.name : v.id);
                const vid = v.id;
                html += `<div class="wpo-places-row">`;
                html += `<span class="wpo-places-row-label" title="${escapeHtmlCs(labelText)}">${escapeHtmlCs(labelText)}</span>`;
                html += `<button type="button" class="wpo-btn wpo-btn-action" data-csl-ingest="${escapeHtmlCs(String(vid))}">Ingest</button>`;
                html += `</div>`;
            }
        }
        html += `</div>`;
        root.innerHTML = html;
    }
    function persistCsLayersPanelPos(panel) {
        try {
            const r = panel.getBoundingClientRect();
            localStorage.setItem(LS_CS_LAYERS_POS, JSON.stringify({ left: r.left, top: r.top }));
        } catch (e) { /* ignore */ }
    }
    function loadCsLayersPanelPos(panel) {
        try {
            const raw = localStorage.getItem(LS_CS_LAYERS_POS);
            if (!raw) return;
            const o = JSON.parse(raw);
            if (typeof o.left === 'number' && typeof o.top === 'number') {
                panel.style.left = Math.max(0, o.left) + 'px';
                panel.style.top = Math.max(0, o.top) + 'px';
                panel.style.right = 'auto';
            }
        } catch (e) { /* ignore */ }
    }
    function toggleCsLayersPanel() {
        const p = document.getElementById('wme-cs-layers-panel');
        if (!p) return;
        p.classList.toggle('csl-hidden');
        const open = !p.classList.contains('csl-hidden');
        try { localStorage.setItem(LS_CS_LAYERS_OPEN, open ? '1' : '0'); } catch (e2) { /* ignore */ }
        if (open) scheduleShapeInspectorRefresh();
    }
    function bindCsLayersPanelOnce() {
        if (csLayersPanelEventsBound) return;
        csLayersPanelEventsBound = true;
        document.addEventListener('pointermove', onDocumentCsLayersMapHover, true);
        document.addEventListener('keydown', onDocumentCsLayersDelKeydown, true);
        document.addEventListener('dragover', (e) => {
            if (e.target && e.target.closest && e.target.closest('[data-csl-drop-layer]')) e.preventDefault();
        }, true);
        document.addEventListener('dragenter', (e) => {
            if (e.target && e.target.closest && e.target.closest('[data-csl-drop-layer]')) e.preventDefault();
        }, true);
        document.addEventListener('drop', onCsLayersLayerDrop, true);
    }
    let csLayersPendingHoverX = 0, csLayersPendingHoverY = 0;
    function onDocumentCsLayersMapHover(e) {
        if (!isCsLayersPanelOpen() || appState.tool !== 'pan' || appState.isDraftActive) {
            if (csLayersMapHoverShapeKey) {
                csLayersMapHoverShapeKey = null;
                requestRender();
            }
            return;
        }
        if (isCandyPaintUiRoot(e.target)) return;
        csLayersPendingHoverX = e.clientX;
        csLayersPendingHoverY = e.clientY;
        if (csLayersMapHoverRaf != null) return;
        csLayersMapHoverRaf = requestAnimationFrame(() => {
            csLayersMapHoverRaf = null;
            const L = getActiveUserLayer();
            if (!L || !ctx) {
                csLayersMapHoverShapeKey = null;
                requestRender();
                return;
            }
            const idx = pickMasterPolygonAt(csLayersPendingHoverX, csLayersPendingHoverY);
            if (idx < 0) {
                csLayersMapHoverShapeKey = null;
            } else {
                ensureShapeIdsForLayer(L);
                csLayersMapHoverShapeKey = csShapeKey(L.id, L.shapeIds[idx]);
            }
            requestRender();
        });
    }
    function onDocumentCsLayersDelKeydown(e) {
        if (e.key !== 'Delete' && e.key !== 'Backspace') return;
        const t = e.target;
        if (isTextEditingTarget(t)) return;
        if (!isCsLayersPanelOpen()) return;
        if (!t || !t.closest || !t.closest('#wme-cs-layers-panel')) return;
        if (csLayersFocusedKind === 'shape' && csLayersFocusedShapeKey) {
            e.preventDefault();
            e.stopPropagation();
            csLayersSelection.clear();
            csLayersSelection.add(csLayersFocusedShapeKey);
            deleteShapesBySelectionKeys();
            return;
        }
        if (csLayersFocusedKind === 'layer' && csLayersFocusedLayerId) {
            e.preventDefault();
            e.stopPropagation();
            deleteUserLayerById(csLayersFocusedLayerId, false);
        }
    }
    function isTextEditingTarget(el) {
        if (!el || el.nodeType !== 1) return false;
        const tag = el.tagName;
        if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
        return !!el.isContentEditable;
    }
    function onCsLayersLayerDrop(e) {
        const dropEl = e.target && e.target.closest ? e.target.closest('[data-csl-drop-layer]') : null;
        if (!dropEl) return;
        e.preventDefault();
        const toVi = parseInt(dropEl.getAttribute('data-csl-drop-layer'), 10);
        const fromVi = csLayersDragLayerIndex;
        csLayersDragLayerIndex = -1;
        if (!Number.isFinite(fromVi) || !Number.isFinite(toVi) || fromVi === toVi) return;
        commitBeforeChange('layer_reorder');
        const moved = userLayers.splice(fromVi, 1)[0];
        let insertAt = toVi;
        if (fromVi < toVi) insertAt = toVi - 1;
        userLayers.splice(insertAt, 0, moved);
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        scheduleDocumentSave();
        requestRender();
    }
    function wireCsLayersListRootDelegation() {
        const root = document.getElementById('wpo-cs-layers-list-root');
        if (!root || root.dataset.cslWired === '1') return;
        root.dataset.cslWired = '1';
        root.addEventListener('focusin', (e) => {
            const row = e.target && e.target.closest ? e.target.closest('[data-csl-focus]') : null;
            if (!row) return;
            const k = row.getAttribute('data-csl-focus');
            if (k === 'shape') {
                csLayersFocusedKind = 'shape';
                csLayersFocusedShapeKey = row.getAttribute('data-shape-key');
                csLayersFocusedLayerId = null;
            } else if (k === 'layer') {
                csLayersFocusedKind = 'layer';
                csLayersFocusedLayerId = row.getAttribute('data-layer-id');
                csLayersFocusedShapeKey = null;
            }
        });
        root.addEventListener('click', (e) => {
            const t = e.target;
            if (t.closest('select') || t.closest('input')) return;
            const rename = t.closest && t.closest('[data-csl-rename]');
            if (rename && e.detail === 2) {
                const vi = parseInt(rename.getAttribute('data-csl-rename'), 10);
                const L = userLayers[vi];
                if (!L) return;
                const inp = document.createElement('input');
                inp.type = 'text';
                inp.className = 'csl-name-input';
                inp.value = L.name || '';
                rename.replaceWith(inp);
                inp.focus();
                inp.select();
                const done = () => {
                    const v = inp.value.trim() || DEFAULT_SHAPE_LAYER_NAME;
                    commitBeforeChange('layer_rename');
                    L.name = v;
                    scheduleDocumentSave();
                    scheduleShapeInspectorRefresh();
                };
                inp.onblur = () => done();
                inp.onkeydown = (ev) => {
                    if (ev.key === 'Enter') { ev.preventDefault(); inp.blur(); }
                    if (ev.key === 'Escape') { inp.value = L.name; inp.blur(); }
                };
                return;
            }
            if (t.closest('[data-csl-drag-layer]')) return;
            const shapeRow = t.closest && t.closest('.csl-shape-row');
            if (shapeRow && !t.closest('button') && !t.closest('select')) {
                const key = shapeRow.getAttribute('data-shape-key');
                const layerId = shapeRow.getAttribute('data-layer-id');
                const si = parseInt(shapeRow.getAttribute('data-shape-idx'), 10);
                if (!key) return;
                if (e.shiftKey) {
                    if (csLayersLastAnchorLayerId !== layerId) {
                        csLayersLastAnchorLayerId = layerId;
                        csLayersLastAnchorShapeIndex = si;
                        csLayersSelection.clear();
                        csLayersSelection.add(key);
                    } else {
                        const L = getUserLayerById(layerId);
                        if (!L) return;
                        const a = Math.min(csLayersLastAnchorShapeIndex, si);
                        const b = Math.max(csLayersLastAnchorShapeIndex, si);
                        csLayersSelection.clear();
                        ensureShapeIdsForLayer(L);
                        for (let j = a; j <= b; j++) csLayersSelection.add(csShapeKey(layerId, L.shapeIds[j]));
                    }
                } else if (e.ctrlKey || e.metaKey) {
                    csLayersLastAnchorLayerId = layerId;
                    csLayersLastAnchorShapeIndex = si;
                    if (csLayersSelection.has(key)) csLayersSelection.delete(key);
                    else csLayersSelection.add(key);
                } else {
                    csLayersLastAnchorLayerId = layerId;
                    csLayersLastAnchorShapeIndex = si;
                    csLayersSelection.clear();
                    csLayersSelection.add(key);
                }
                scheduleShapeInspectorRefresh();
                requestRender();
                return;
            }
            const head = t.closest && t.closest('.csl-layer-head');
            if (head && !t.closest('button') && !t.closest('.csl-drag-h')) {
                const lid = head.getAttribute('data-layer-id');
                if (lid) setActiveLayerById(lid);
                return;
            }
            const eye = t.closest && t.closest('[data-csl-eye]');
            if (eye) {
                const vi = parseInt(eye.getAttribute('data-csl-eye'), 10);
                const L = userLayers[vi];
                if (!L) return;
                commitBeforeChange('layer_visibility');
                L.visible = L.visible === false ? true : false;
                syncActiveLayerAliases();
                scheduleShapeInspectorRefresh();
                scheduleDocumentSave();
                requestRender();
                return;
            }
            const lu = t.closest && t.closest('[data-csl-layer-up]');
            if (lu && !lu.disabled) {
                const vi = parseInt(lu.getAttribute('data-csl-layer-up'), 10);
                if (vi < userLayers.length - 1) {
                    commitBeforeChange('layer_reorder');
                    swapUserLayersAt(vi, vi + 1);
                    pruneShapeInspectorSelection();
                    scheduleShapeInspectorRefresh();
                    scheduleDocumentSave();
                    requestRender();
                }
                return;
            }
            const ld = t.closest && t.closest('[data-csl-layer-dn]');
            if (ld && !ld.disabled) {
                const vi = parseInt(ld.getAttribute('data-csl-layer-dn'), 10);
                if (vi > 0) {
                    commitBeforeChange('layer_reorder');
                    swapUserLayersAt(vi, vi - 1);
                    pruneShapeInspectorSelection();
                    scheduleShapeInspectorRefresh();
                    scheduleDocumentSave();
                    requestRender();
                }
                return;
            }
            const dupL = t.closest && t.closest('[data-csl-dup-layer]');
            if (dupL) {
                duplicateUserLayerAtIndex(parseInt(dupL.getAttribute('data-csl-dup-layer'), 10));
                return;
            }
            const mergeB = t.closest && t.closest('[data-csl-merge]');
            if (mergeB && !mergeB.disabled) {
                mergeLayerDownAtIndex(parseInt(mergeB.getAttribute('data-csl-merge'), 10));
                return;
            }
            const delL = t.closest && t.closest('[data-csl-del-layer]');
            if (delL) {
                const vi = parseInt(delL.getAttribute('data-csl-del-layer'), 10);
                const L = userLayers[vi];
                if (L) deleteUserLayerById(L.id, false);
                return;
            }
            const col = t.closest && t.closest('[data-csl-collapse]');
            if (col) {
                const vi = parseInt(col.getAttribute('data-csl-collapse'), 10);
                const L = userLayers[vi];
                if (L) {
                    csLayersCollapsed[L.id] = !csLayersCollapsed[L.id];
                    scheduleShapeInspectorRefresh();
                }
                return;
            }
            const z = t.closest && t.closest('[data-csl-zoom-shape]');
            if (z) {
                const key = z.getAttribute('data-csl-zoom-shape');
                const p = parseShapeKey(key);
                if (!p) return;
                const L = getUserLayerById(p.layerId);
                const ix = L ? (L.shapeIds || []).indexOf(p.shapeId) : -1;
                if (L && ix >= 0 && L.polygons[ix]) zoomMapToLonLatBBox(bboxForPartLonLat(L.polygons[ix]));
                return;
            }
            const dps = t.closest && t.closest('[data-csl-dup-shape]');
            if (dps) {
                const key = dps.getAttribute('data-csl-dup-shape');
                const p = parseShapeKey(key);
                if (!p) return;
                const L = getUserLayerById(p.layerId);
                const ix = L ? (L.shapeIds || []).indexOf(p.shapeId) : -1;
                if (L && ix >= 0) duplicateShapeOnLayer(L, ix);
                return;
            }
            const su = t.closest && t.closest('[data-csl-shape-up]');
            if (su && !su.disabled) {
                const key = su.getAttribute('data-csl-shape-up');
                const p = parseShapeKey(key);
                const L = p ? getUserLayerById(p.layerId) : null;
                const ix = L && p ? L.shapeIds.indexOf(p.shapeId) : -1;
                if (L && ix > 0) swapShapesInLayer(L, ix, ix - 1);
                return;
            }
            const sd = t.closest && t.closest('[data-csl-shape-dn]');
            if (sd && !sd.disabled) {
                const key = sd.getAttribute('data-csl-shape-dn');
                const p = parseShapeKey(key);
                const L = p ? getUserLayerById(p.layerId) : null;
                const ix = L && p ? L.shapeIds.indexOf(p.shapeId) : -1;
                if (L && ix >= 0 && ix < L.polygons.length - 1) swapShapesInLayer(L, ix, ix + 1);
                return;
            }
            const act = t.closest && t.closest('[data-csl-act-shape]');
            if (act) {
                const key = act.getAttribute('data-csl-act-shape');
                const p = parseShapeKey(key);
                const L = p ? getUserLayerById(p.layerId) : null;
                const ix = L && p ? L.shapeIds.indexOf(p.shapeId) : -1;
                if (L && ix >= 0) tryActivateShapeLinkOnLayer(L, ix);
                return;
            }
            const dels = t.closest && t.closest('[data-csl-del-shape]');
            if (dels) {
                const key = dels.getAttribute('data-csl-del-shape');
                csLayersSelection.clear();
                csLayersSelection.add(key);
                deleteShapesBySelectionKeys();
                return;
            }
            const ing = t.closest && t.closest('[data-csl-ingest]');
            if (ing) {
                const vid = ing.getAttribute('data-csl-ingest');
                const vm = venueModelForIngestById(vid);
                if (!vm) logToUI('Place not found for ingest.', true);
                else ingestVenuesIntoNewLayer([vm]);
                return;
            }
        });
        root.addEventListener('change', (e) => {
            const sel = e.target && e.target.closest ? e.target.closest('[data-csl-move]') : null;
            if (!sel || sel.tagName !== 'SELECT') return;
            const key = sel.getAttribute('data-csl-move');
            const tgtId = sel.value;
            const p = parseShapeKey(key);
            if (!p || tgtId === '' || !tgtId) return;
            const L = getUserLayerById(p.layerId);
            const ix = L ? L.shapeIds.indexOf(p.shapeId) : -1;
            if (L && ix >= 0) moveShapeToLayer(L, ix, tgtId);
            sel.selectedIndex = 0;
        });
        root.addEventListener('mouseover', (e) => {
            const row = e.target && e.target.closest ? e.target.closest('.csl-shape-row') : null;
            if (!row || !root.contains(row)) return;
            const k = row.getAttribute('data-shape-key');
            if (k && k !== csLayersListHoverShapeKey) {
                csLayersListHoverShapeKey = k;
                requestRender();
            }
        });
        root.addEventListener('mouseout', (e) => {
            const row = e.target && e.target.closest ? e.target.closest('.csl-shape-row') : null;
            if (!row || !root.contains(row)) return;
            const rel = e.relatedTarget;
            if (rel && row.contains(rel)) return;
            csLayersListHoverShapeKey = null;
            requestRender();
        });
        root.addEventListener('dragstart', (e) => {
            const h = e.target && e.target.closest ? e.target.closest('[data-csl-drag-layer]') : null;
            if (!h) return;
            csLayersDragLayerIndex = parseInt(h.getAttribute('data-csl-drag-layer'), 10);
            try { e.dataTransfer.setData('text/plain', String(csLayersDragLayerIndex)); } catch (e2) { /* ignore */ }
            e.dataTransfer.effectAllowed = 'move';
        });
    }
    function createCsLayersPanel() {
        if (document.getElementById('wme-cs-layers-panel')) return;
        const panel = document.createElement('div');
        panel.id = 'wme-cs-layers-panel';
        panel.innerHTML = `
            <div class="csl-header" id="csl-header-drag">
                <span>CS Layers</span>
                <div class="csl-header-btns">
                    <button type="button" class="csl-hdr-btn" id="csl-btn-min" title="Minimize">−</button>
                    <button type="button" class="csl-hdr-btn" id="csl-btn-close" title="Hide panel">×</button>
                </div>
            </div>
            <div class="csl-body" id="csl-body">
                <div class="csl-toolbar">
                    <input type="text" class="csl-filter" id="csl-filter-input" placeholder="Filter layers…" aria-label="Filter layers by name" />
                    <button type="button" class="csl-icon-btn" id="csl-overflow-btn" title="More">⋯</button>
                </div>
                <div id="csl-overflow-backdrop" class="csl-overflow-backdrop"></div>
                <div id="csl-overflow-menu" class="csl-overflow-menu" style="display:none;">
                    <div class="csl-overflow-item" data-csl-expand-all>Expand all layers</div>
                    <div class="csl-overflow-item" data-csl-collapse-all>Collapse all layers</div>
                </div>
                <div class="csl-scroll"><div id="wpo-cs-layers-list-root"></div></div>
                <div class="csl-footer">
                    <button type="button" class="csl-icon-btn" id="csl-sel-none" title="Clear selection on active layer">Sel ∅</button>
                    <button type="button" class="csl-icon-btn" id="csl-sel-all" title="Select all shapes on active layer">Sel all</button>
                    <button type="button" class="csl-icon-btn" id="csl-del-sel" title="Delete selected shapes">Del</button>
                    <button type="button" class="csl-icon-btn" id="csl-unlink-sel" title="Unlink selected">Unlink</button>
                    <button type="button" class="csl-icon-btn" id="csl-zoom-sel" title="Zoom to selection">Zoom</button>
                    <button type="button" class="wpo-btn wpo-btn-action" id="csl-btn-add-layer" title="Add empty user layer">+Layer</button>
                </div>
            </div>`;
        panel.style.position = 'fixed';
        panel.style.left = '24px';
        panel.style.top = '120px';
        panel.style.right = 'auto';
        document.body.appendChild(panel);
        loadCsLayersPanelPos(panel);
        try {
            if (localStorage.getItem(LS_CS_LAYERS_OPEN) === '0') panel.classList.add('csl-hidden');
        } catch (e) { /* ignore */ }
        const hdr = document.getElementById('csl-header-drag');
        let drag = false, sx = 0, sy = 0, sl = 0, st = 0;
        hdr.addEventListener('mousedown', (e) => {
            if (e.target.closest('.csl-header-btns')) return;
            drag = true;
            const r = panel.getBoundingClientRect();
            sx = e.clientX;
            sy = e.clientY;
            sl = r.left;
            st = r.top;
            e.preventDefault();
        });
        document.addEventListener('mousemove', (e) => {
            if (!drag) return;
            const nl = sl + (e.clientX - sx);
            const nt = st + (e.clientY - sy);
            panel.style.left = Math.max(0, nl) + 'px';
            panel.style.top = Math.max(0, nt) + 'px';
            panel.style.right = 'auto';
        });
        document.addEventListener('mouseup', () => {
            if (drag) persistCsLayersPanelPos(panel);
            drag = false;
        });
        document.getElementById('csl-btn-close').onclick = () => toggleCsLayersPanel();
        document.getElementById('csl-btn-min').onclick = () => {
            const b = document.getElementById('csl-body');
            if (b) b.style.display = b.style.display === 'none' ? '' : 'none';
        };
        const finp = document.getElementById('csl-filter-input');
        if (finp) {
            finp.addEventListener('input', () => {
                csLayersNameFilter = finp.value;
                scheduleShapeInspectorRefresh();
            });
        }
        const ob = document.getElementById('csl-overflow-btn');
        const om = document.getElementById('csl-overflow-menu');
        const obd = document.getElementById('csl-overflow-backdrop');
        const placeMenu = () => {
            if (!ob || !om) return;
            const r = ob.getBoundingClientRect();
            om.style.left = r.left + 'px';
            om.style.top = r.bottom + 2 + 'px';
            om.style.display = 'block';
            om.classList.add('csl-show');
            if (obd) {
                obd.classList.add('csl-show');
                obd.style.display = 'block';
            }
        };
        const hideMenu = () => {
            if (om) { om.style.display = 'none'; om.classList.remove('csl-show'); }
            if (obd) { obd.classList.remove('csl-show'); obd.style.display = 'none'; }
        };
        if (ob) ob.onclick = (ev) => {
            ev.stopPropagation();
            if (om && om.classList.contains('csl-show')) hideMenu();
            else placeMenu();
        };
        if (obd) obd.onclick = () => hideMenu();
        if (om) {
            om.onclick = (ev) => {
                const it = ev.target.closest && ev.target.closest('.csl-overflow-item');
                if (!it) return;
                hideMenu();
                if (it.hasAttribute('data-csl-expand-all')) {
                    userLayers.forEach(L => { csLayersCollapsed[L.id] = false; });
                }
                if (it.hasAttribute('data-csl-collapse-all')) {
                    userLayers.forEach(L => { csLayersCollapsed[L.id] = true; });
                }
                scheduleShapeInspectorRefresh();
            };
        }
        document.getElementById('csl-btn-add-layer').onclick = () => onAddUserLayerClick();
        document.getElementById('csl-sel-none').onclick = () => {
            const L = getActiveUserLayer();
            if (!L) return;
            ensureShapeIdsForLayer(L);
            for (let i = 0; i < L.shapeIds.length; i++) {
                csLayersSelection.delete(csShapeKey(L.id, L.shapeIds[i]));
            }
            scheduleShapeInspectorRefresh();
            requestRender();
        };
        document.getElementById('csl-sel-all').onclick = () => {
            const L = getActiveUserLayer();
            if (!L) return;
            ensureShapeIdsForLayer(L);
            for (let i = 0; i < L.shapeIds.length; i++) {
                csLayersSelection.add(csShapeKey(L.id, L.shapeIds[i]));
            }
            scheduleShapeInspectorRefresh();
            requestRender();
        };
        document.getElementById('csl-del-sel').onclick = () => deleteShapesBySelectionKeys();
        document.getElementById('csl-unlink-sel').onclick = () => unlinkShapesBySelectionKeys();
        document.getElementById('csl-zoom-sel').onclick = () => zoomToSelectionKeysBbox();
        wireCsLayersListRootDelegation();
        bindCsLayersPanelOnce();
        renderShapeInspectorList();
        updateActiveLayerReadout();
    }
    function ensureCsLayersPanel() {
        if (!document.getElementById('wme-cs-layers-panel')) createCsLayersPanel();
        else wireCsLayersListRootDelegation();
    }

    function rebuildLayerSelectDom() {
        scheduleShapeInspectorRefresh();
        updateActiveLayerReadout();
    }

    function venueModelForIngestById(venueIdRaw) {
        const norm = normalizeVenueIdForSdk(venueIdRaw);
        if (wmeSDK?.DataModel?.Venues?.getById) {
            try {
                const v = wmeSDK.DataModel.Venues.getById({ venueId: norm });
                const gt = v && v.geometry && v.geometry.type;
                if (v && (gt === 'Polygon' || gt === 'MultiPolygon')) return v;
            } catch (e) { /* ignore */ }
        }
        const objs = window.W?.model?.venues?.objects;
        if (!objs) return null;
        for (const k in objs) {
            if (!Object.prototype.hasOwnProperty.call(objs, k)) continue;
            const o = objs[k];
            const oid = o.attributes?.id != null ? o.attributes.id : (o.id != null ? o.id : k);
            if (venueIdsMatch(oid, venueIdRaw) || venueIdsMatch(normalizeVenueIdForSdk(oid), norm)) {
                const gt = o.geometry?.type;
                if (gt === 'Polygon' || gt === 'MultiPolygon') {
                    return { id: oid, geometry: o.geometry, name: o.attributes?.name || o.name };
                }
            }
        }
        return null;
    }

    function rebuildPlacesListDom() {
        scheduleShapeInspectorRefresh();
    }

    function deriveIngestLayerName(models) {
        if (models.length === 1) {
            const m = models[0];
            const n = m.name || (m.attributes && m.attributes.name);
            if (n) return 'Ingest: ' + String(n).slice(0, 48);
            return 'Ingest: ' + String(m.id);
        }
        return `Ingest (${models.length} places)`;
    }

    function ingestVenuesIntoNewLayer(models) {
        if (!isGeometryLibraryReady()) {
            logToUI('Ingest disabled: jsts is not available.', true);
            return;
        }
        if (!models.length) return;
        const newPolys = [];
        const perModelPartCount = [];
        for (let mi = 0; mi < models.length; mi++) {
            const parts = sdkGeometryToClippingParts(models[mi].geometry);
            perModelPartCount.push(parts.length);
            for (let p = 0; p < parts.length; p++) newPolys.push(parts[p]);
        }
        if (!newPolys.length) return;
        if (!getGeometryEngine()) {
            logToUI('Geometry library (jsts) is still loading. Wait a moment and try Ingest again.', true);
            return;
        }
        commitBeforeChange('ingest');
        try {
            const newLayer = createUserLayer(deriveIngestLayerName(models));
            activeLayerId = newLayer.id;
            newLayer.polygons = newPolys;
            newLayer.links = new Array(newPolys.length).fill(null);
            let shapeIdx = 0;
            for (let mi = 0; mi < models.length; mi++) {
                const id = models[mi].id;
                const nParts = perModelPartCount[mi];
                if (id != null && id !== '') {
                    const vid = normalizeVenueIdForSdk(id);
                    for (let p = 0; p < nParts; p++) {
                        newLayer.links[shapeIdx + p] = normalizeShapeLink({ venueId: vid, active: true, broken: false });
                    }
                }
                shapeIdx += nParts;
            }
            rebalanceVenueLinkActives();
            ensureShapeIdsForLayer(newLayer);
            syncActiveLayerAliases();
            alignShapeLinksToMaster();
            logToUI(`Ingested ${models.length} venue(s) into new layer "${newLayer.name}".`);
            changeMode('replace', { persistCommitMode: true });
            logToUI('Commit mode set to Replace after ingest.', false);
            if (!DRAWING_STYLE_TOOLS.has(appState.tool)) {
                changeTool('polygon');
            } else {
                requestRender();
                updateTrackerHUD();
            }
            handleSelectionChange();
            rebuildLayerSelectDom();
            scheduleDocumentSave();
        } catch (e) {
            logToUI('Error ingesting.', true);
            console.error(e);
        }
    }

    function onAddUserLayerClick() {
        commitBeforeChange('layer_add');
        const L = createUserLayer(DEFAULT_SHAPE_LAYER_NAME + ' ' + (userLayers.length + 1));
        activeLayerId = L.id;
        syncActiveLayerAliases();
        rebuildLayerSelectDom();
        scheduleDocumentSave();
        requestRender();
        updateTrackerHUD();
        logToUI(`Added layer: ${L.name}`);
    }

    function onLayerSelectChange(ev) {
        const id = ev.target && ev.target.value;
        if (id) setActiveLayerById(id);
    }

    function cloneUserLayersForSnapshot(layers) {
        return layers.map(l => ({
            id: l.id,
            isPlaces: !!l.isPlaces,
            visible: l.visible !== false,
            name: l.name,
            polygons: cloneMasterPolygons(l.polygons),
            links: cloneShapePlaceLinks(l.links),
            shapeIds: (l.shapeIds || []).slice()
        }));
    }

    function cloneGpsVertices(verts) {
        if (!verts || !verts.length) return [];
        return verts.map(v => (v && typeof v.lon === 'number' && typeof v.lat === 'number' ? { lon: v.lon, lat: v.lat } : { lon: Number(v.lon), lat: Number(v.lat) }));
    }

    function capturePaintDocumentSnapshot() {
        return {
            userLayers: cloneUserLayersForSnapshot(userLayers),
            activeLayerId,
            masterPolygons: cloneMasterPolygons(masterPolygons),
            shapePlaceLinks: cloneShapePlaceLinks(shapePlaceLinks),
            draft: {
                isDraftActive: !!appState.isDraftActive,
                draftVertices: cloneGpsVertices(draftVertices),
                draftInteriorRings: (draftInteriorRings || []).map(ring => cloneGpsVertices(ring)),
                tempVertices: cloneGpsVertices(tempVertices),
                revertOriginalPolygon: revertOriginalPolygon ? cloneMasterPolygons(revertOriginalPolygon) : null,
                revertOriginalLink: revertOriginalLink != null ? cloneLinkEntry(revertOriginalLink) : null,
                draftPopInsertMasterIndex: draftPopInsertMasterIndex,
                isDrawingShape: !!isDrawingShape
            },
            csLayersSelectionKeys: Array.from(csLayersSelection).sort(),
            tool: appState.tool,
            mode: appState.mode
        };
    }

    const HISTORY_SNAPSHOT_TOOLS = new Set(['pan', 'measure', 'move', 'revert', 'link', 'rectangle', 'ellipse', 'lasso', 'polygon', 'brush', 'eraser', 'wand']);

    /** Restore toolbar boolean mode + tool after history apply (older snapshots omit tool/mode). */
    function applySnapshotToolAndMode(snapshot) {
        if (!snapshot) return;
        const m = snapshot.mode;
        const t = snapshot.tool;
        if (typeof m === 'string' && COMMIT_MODE_IDS.has(m)) changeMode(m);
        if (typeof t === 'string' && HISTORY_SNAPSHOT_TOOLS.has(t)) changeTool(t, { preserveDrawingState: true, quiet: true });
    }

    function applySnapshotDraftAndSelection(snapshot) {
        csLayersSelection.clear();
        const keys = snapshot && snapshot.csLayersSelectionKeys;
        if (Array.isArray(keys)) {
            for (let i = 0; i < keys.length; i++) csLayersSelection.add(keys[i]);
        }
        const draft = snapshot && snapshot.draft;
        if (!draft) {
            tempVertices = [];
            isDrawingShape = false;
            revertOriginalPolygon = null;
            revertOriginalLink = null;
            draftPopInsertMasterIndex = -1;
            setDraftActive(false);
            return;
        }
        tempVertices = cloneGpsVertices(draft.tempVertices || []);
        isDrawingShape = !!draft.isDrawingShape;
        if (!draft.isDraftActive) {
            revertOriginalPolygon = null;
            revertOriginalLink = null;
            draftPopInsertMasterIndex = -1;
            setDraftActive(false);
            return;
        }
        draftVertices = cloneGpsVertices(draft.draftVertices || []);
        draftInteriorRings = (draft.draftInteriorRings || []).map(ring => cloneGpsVertices(ring));
        revertOriginalPolygon = draft.revertOriginalPolygon ? cloneMasterPolygons(draft.revertOriginalPolygon) : null;
        revertOriginalLink = draft.revertOriginalLink != null ? cloneLinkEntry(draft.revertOriginalLink) : null;
        draftPopInsertMasterIndex = typeof draft.draftPopInsertMasterIndex === 'number' ? draft.draftPopInsertMasterIndex : -1;
        setDraftActive(true);
    }

    function applyRestorePaintDocument(snapshot) {
        if (!snapshot) return;
        if (snapshot.userLayers && snapshot.userLayers.length) {
            userLayers = snapshot.userLayers.map(l => ({
                id: l.id || generateLayerId(),
                isPlaces: false,
                visible: l.visible !== false,
                name: migrateLegacyLayerName(l.name || DEFAULT_SHAPE_LAYER_NAME),
                polygons: cloneMasterPolygons(l.polygons),
                links: cloneShapePlaceLinks(l.links),
                shapeIds: Array.isArray(l.shapeIds) ? l.shapeIds.slice() : []
            }));
            userLayers.forEach(ensureShapeIdsForLayer);
            activeLayerId = snapshot.activeLayerId;
            validateActiveLayerId();
            syncActiveLayerAliases();
            refreshPlacesPreviewFromMap();
            rebuildLayerSelectDom();
            rebuildPlacesListDom();
            applySnapshotDraftAndSelection(snapshot);
            applySnapshotToolAndMode(snapshot);
            pruneShapeInspectorSelection();
            scheduleDocumentSave();
            return;
        }
        if (Array.isArray(snapshot.userLayers) && snapshot.userLayers.length === 0 && snapshot.masterPolygons) {
            if (DEBUG) console.warn('[WME Candy Shop] History snapshot had empty userLayers; rebuilt one layer from masterPolygons.');
            const newId = snapshot.activeLayerId || generateLayerId();
            userLayers = [{
                id: newId,
                isPlaces: false,
                visible: true,
                name: migrateLegacyLayerName(DEFAULT_SHAPE_LAYER_NAME),
                polygons: cloneMasterPolygons(snapshot.masterPolygons),
                links: cloneShapePlaceLinks(snapshot.shapePlaceLinks || []),
                shapeIds: []
            }];
            userLayers.forEach(ensureShapeIdsForLayer);
            activeLayerId = newId;
            validateActiveLayerId();
            syncActiveLayerAliases();
            refreshPlacesPreviewFromMap();
            rebuildLayerSelectDom();
            rebuildPlacesListDom();
            applySnapshotDraftAndSelection(snapshot);
            applySnapshotToolAndMode(snapshot);
            pruneShapeInspectorSelection();
            scheduleDocumentSave();
            return;
        }
        masterPolygons = cloneMasterPolygons(snapshot.masterPolygons);
        shapePlaceLinks = cloneShapePlaceLinks(snapshot.shapePlaceLinks);
        alignShapeLinksToMaster();
        const L = getActiveUserLayer();
        if (L) {
            L.polygons = masterPolygons;
            L.links = shapePlaceLinks;
            masterPolygons = L.polygons;
            shapePlaceLinks = L.links;
            ensureShapeIdsForLayer(L);
        }
        rebuildLayerSelectDom();
        refreshPlacesPreviewFromMap();
        rebuildPlacesListDom();
        applySnapshotDraftAndSelection(snapshot);
        applySnapshotToolAndMode(snapshot);
        pruneShapeInspectorSelection();
        scheduleDocumentSave();
    }

    function commitBeforeChange(reasonTag, explicitLabel) {
        if (paintHistoryApplying) return;
        const label = historyLabelForTag(reasonTag, explicitLabel);
        paintUndoStack.push({ snapshot: capturePaintDocumentSnapshot(), label });
        trimUndoStackToCap();
        paintRedoStack = [];
        updateUndoRedoButtons();
        scheduleCsHistoryRefresh();
    }

    function clearHistory() {
        paintUndoStack = [];
        paintRedoStack = [];
        updateUndoRedoButtons();
        scheduleCsHistoryRefresh();
    }

    function canUndo() {
        return paintUndoStack.length > 0;
    }

    function canRedo() {
        return paintRedoStack.length > 0;
    }

    function resetTransientInteractionStateAfterHistoryRestore() {
        brushPainting = false;
        brushStrokeSamples = [];
        hoveredTempVertexIndex = -1;
        draggingVertexIndex = -1;
        hoveredDraftVertexIndex = -1;
        draggingDraftVertexIndex = -1;
        hoveredDraftMidpointIndex = -1;
        draftAction = 'none';
        dragStartMouse = null;
        dragStartGlobalCoords = null;
        dragStartDraftRingsPx = null;
        dragStartBBox = null;
        dragStartMasterPixels = null;
        hoveredMasterPolyIndex = -1;
        pendingLinkVenueId = null;
        appState.highlightedLinkIndex = -1;
    }

    function resetEphemeralPaintStateAfterHistoryRestore() {
        resetTransientInteractionStateAfterHistoryRestore();
        requestRender();
        updateTrackerHUD();
        handleSelectionChange();
        updateUndoRedoButtons();
        rebuildLayerSelectDom();
        rebuildPlacesListDom();
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
        renderOptionsRow();
        scheduleCsHistoryRefresh();
    }

    function undo() {
        if (!canUndo()) return;
        const current = capturePaintDocumentSnapshot();
        const prevEntry = paintUndoStack.pop();
        const prevSnap = paintHistoryEntrySnapshot(prevEntry);
        paintRedoStack.push({ snapshot: current, label: historyLabelForTag('edit') });
        paintHistoryApplying = true;
        try {
            applyRestorePaintDocument(prevSnap);
        } finally {
            paintHistoryApplying = false;
        }
        resetEphemeralPaintStateAfterHistoryRestore();
    }

    function redo() {
        if (!canRedo()) return;
        const current = capturePaintDocumentSnapshot();
        const nextEntry = paintRedoStack.pop();
        const nextSnap = paintHistoryEntrySnapshot(nextEntry);
        paintUndoStack.push({ snapshot: current, label: historyLabelForTag('edit') });
        trimUndoStackToCap();
        paintHistoryApplying = true;
        try {
            applyRestorePaintDocument(nextSnap);
        } finally {
            paintHistoryApplying = false;
        }
        resetEphemeralPaintStateAfterHistoryRestore();
    }

    function jumpToUndoDepth(depth) {
        if (paintHistoryApplying) return;
        if (!Number.isFinite(depth) || depth < 1 || depth > paintUndoStack.length) return;
        const targetIx = paintUndoStack.length - depth;
        const targetEntry = paintUndoStack[targetIx];
        const targetSnap = paintHistoryEntrySnapshot(targetEntry);
        paintUndoStack.splice(paintUndoStack.length - depth, depth);
        paintRedoStack = [];
        paintHistoryApplying = true;
        try {
            applyRestorePaintDocument(targetSnap);
        } finally {
            paintHistoryApplying = false;
        }
        resetEphemeralPaintStateAfterHistoryRestore();
    }

    function updateUndoRedoButtons() {
        const u = document.getElementById('wpo-btn-undo');
        const r = document.getElementById('wpo-btn-redo');
        if (u) {
            u.disabled = !canUndo();
            u.style.opacity = canUndo() ? '1' : '0.5';
            u.style.cursor = canUndo() ? 'pointer' : 'not-allowed';
        }
        if (r) {
            r.disabled = !canRedo();
            r.style.opacity = canRedo() ? '1' : '0.5';
            r.style.cursor = canRedo() ? 'pointer' : 'not-allowed';
        }
    }

    function scheduleCsHistoryRefresh() {
        if (csHistoryRefreshScheduled) return;
        csHistoryRefreshScheduled = true;
        requestAnimationFrame(() => {
            csHistoryRefreshScheduled = false;
            renderCsHistoryPanel();
        });
    }

    function renderCsHistoryPanel() {
        const root = document.getElementById('csh-list-root');
        if (!root) return;
        let html = '<div class="csh-row csh-current"><span class="csh-lab">Current</span></div>';
        for (let i = paintUndoStack.length - 1; i >= 0; i--) {
            const ent = paintUndoStack[i];
            const lab = ent && typeof ent === 'object' && ent.label ? ent.label : 'Edit';
            const depth = paintUndoStack.length - i;
            html += `<div class="csh-row csh-past" tabindex="0" role="button" data-csh-depth="${depth}"><span class="csh-lab">${escapeHtmlCs(lab)}</span></div>`;
        }
        root.innerHTML = html;
        root.querySelectorAll('.csh-past').forEach(row => {
            row.onclick = () => {
                const d = parseInt(row.getAttribute('data-csh-depth'), 10);
                if (Number.isFinite(d)) jumpToUndoDepth(d);
            };
        });
    }

    function persistCsHistoryPanelPos(panel) {
        try {
            const r = panel.getBoundingClientRect();
            localStorage.setItem(LS_CS_HISTORY_POS, JSON.stringify({ left: r.left, top: r.top }));
        } catch (e) { /* ignore */ }
    }

    function loadCsHistoryPanelPos(panel) {
        try {
            const raw = localStorage.getItem(LS_CS_HISTORY_POS);
            if (!raw) return;
            const o = JSON.parse(raw);
            if (typeof o.left === 'number' && typeof o.top === 'number') {
                panel.style.left = Math.max(0, o.left) + 'px';
                panel.style.top = Math.max(0, o.top) + 'px';
                panel.style.right = 'auto';
            }
        } catch (e) { /* ignore */ }
    }

    function toggleCsHistoryPanel() {
        const p = document.getElementById('wme-cs-history-panel');
        if (!p) return;
        p.classList.toggle('csh-hidden');
        const open = !p.classList.contains('csh-hidden');
        try { localStorage.setItem(LS_CS_HISTORY_OPEN, open ? '1' : '0'); } catch (e2) { /* ignore */ }
        if (open) scheduleCsHistoryRefresh();
    }

    function createCsHistoryPanel() {
        if (document.getElementById('wme-cs-history-panel')) return;
        const panel = document.createElement('div');
        panel.id = 'wme-cs-history-panel';
        panel.className = 'csh-panel';
        panel.innerHTML = `
            <div class="csh-header" id="csh-header-drag">
                <span>CS History</span>
                <div class="csh-header-btns">
                    <button type="button" class="csh-hdr-btn" id="csh-btn-min" title="Minimize">−</button>
                    <button type="button" class="csh-hdr-btn" id="csh-btn-close" title="Hide panel">×</button>
                </div>
            </div>
            <div class="csh-body" id="csh-body">
                <p class="csh-hint" title="Each row is one saved state. Choosing an older row restores it and drops newer undo steps.">Each row is one saved state; picking an older row restores it and removes newer undo steps.</p>
                <p class="csh-hint csh-memory-hint" title="Up to 200 full snapshots are kept; long editing sessions use more browser memory.">Tip: many undo steps use more memory in long sessions.</p>
                <div class="csh-scroll" id="csh-list-root"></div>
            </div>`;
        panel.style.position = 'fixed';
        panel.style.left = '360px';
        panel.style.top = '120px';
        panel.style.right = 'auto';
        document.body.appendChild(panel);
        loadCsHistoryPanelPos(panel);
        try {
            if (localStorage.getItem(LS_CS_HISTORY_OPEN) === '0') panel.classList.add('csh-hidden');
        } catch (e) { /* ignore */ }
        const hdr = document.getElementById('csh-header-drag');
        let drag = false, sx = 0, sy = 0, sl = 0, st = 0;
        if (hdr) {
            hdr.addEventListener('mousedown', (e) => {
                if (e.target.closest('.csh-header-btns')) return;
                drag = true;
                const r = panel.getBoundingClientRect();
                sx = e.clientX;
                sy = e.clientY;
                sl = r.left;
                st = r.top;
                e.preventDefault();
            });
        }
        document.addEventListener('mousemove', (e) => {
            if (!drag) return;
            const nl = sl + (e.clientX - sx);
            const nt = st + (e.clientY - sy);
            panel.style.left = Math.max(0, nl) + 'px';
            panel.style.top = Math.max(0, nt) + 'px';
            panel.style.right = 'auto';
        });
        document.addEventListener('mouseup', () => {
            if (drag) persistCsHistoryPanelPos(panel);
            drag = false;
        });
        const cbtn = document.getElementById('csh-btn-close');
        if (cbtn) cbtn.onclick = () => toggleCsHistoryPanel();
        const mbtn = document.getElementById('csh-btn-min');
        if (mbtn) mbtn.onclick = () => {
            const b = document.getElementById('csh-body');
            if (b) b.style.display = b.style.display === 'none' ? '' : 'none';
        };
        renderCsHistoryPanel();
    }

    function ensureCsHistoryPanel() {
        if (!document.getElementById('wme-cs-history-panel')) createCsHistoryPanel();
        else scheduleCsHistoryRefresh();
    }

    function candyPaintUndoHotkeysLikelyConflict(e) {
        const t = e.target;
        if (!t) return false;
        const tag = t.tagName;
        if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
        if (t.isContentEditable) return true;
        return false;
    }

    function candyPaintUndoHotkeyEditingContext() {
        if (appState.isDraftActive) return true;
        if (isDrawingShape || brushPainting) return true;
        if (tempVertices.length > 0 || draftVertices.length > 0) return true;
        if (appState.tool !== 'pan' && appState.tool !== 'measure') return true;
        if (hasAnyUserLayerPolygons()) return true;
        return false;
    }

    /** Block V/P/M/L and Space→pan while mid drag-stroke (P10). */
    function candyPaintToolHotkeyBlocksToolSwitch() {
        return brushPainting || isDrawingShape;
    }

    function syncKeyboardModifiersFromEvent(e) {
        isShiftDown = !!e.shiftKey;
        isAltDown = !!e.altKey;
    }

    function onDocumentModifierAndExperimentalKeydown(e) {
        syncKeyboardModifiersFromEvent(e);
        if (!getExperimentalUndoKeys()) return;
        onDocumentExperimentalHotkeysKeydown(e);
    }

    function onDocumentModifierKeyup(e) {
        syncKeyboardModifiersFromEvent(e);
        if (!getExperimentalUndoKeys()) return;
        onDocumentExperimentalHotkeysKeyup(e);
    }

    function onDocumentExperimentalHotkeysKeydown(e) {
        if (candyPaintUndoHotkeysLikelyConflict(e)) return;

        if ((e.ctrlKey || e.metaKey) && candyPaintUndoHotkeyEditingContext()) {
            if (e.code === 'KeyZ') {
                if (e.shiftKey) {
                    if (!canRedo()) return;
                    e.preventDefault();
                    e.stopPropagation();
                    redo();
                    return;
                }
                if (!canUndo()) return;
                e.preventDefault();
                e.stopPropagation();
                undo();
                return;
            }
            if (e.code === 'KeyY' && !e.shiftKey) {
                if (!canRedo()) return;
                e.preventDefault();
                e.stopPropagation();
                redo();
                return;
            }
        }

        if (e.ctrlKey || e.metaKey) return;
        if (candyPaintToolHotkeyBlocksToolSwitch()) return;

        if (e.code === 'KeyV') {
            e.preventDefault();
            e.stopPropagation();
            changeTool('move');
            return;
        }
        if (e.code === 'KeyP') {
            e.preventDefault();
            e.stopPropagation();
            changeTool('pan');
            return;
        }
        if (e.code === 'KeyM') {
            e.preventDefault();
            e.stopPropagation();
            changeTool('measure');
            return;
        }
        if (e.code === 'KeyL') {
            e.preventDefault();
            e.stopPropagation();
            changeTool('link');
            return;
        }

        if (e.code === 'Space') {
            if (spacePanActive) {
                e.preventDefault();
                e.stopPropagation();
                return;
            }
            if (!e.repeat) {
                spacePanRestoreTool = appState.tool;
                spacePanActive = true;
                changeTool('pan', { quiet: true });
                e.preventDefault();
                e.stopPropagation();
            }
        }
    }

    function onDocumentExperimentalHotkeysKeyup(e) {
        if (candyPaintUndoHotkeysLikelyConflict(e)) return;
        if (e.code === 'Space' && spacePanActive) {
            spacePanActive = false;
            changeTool(spacePanRestoreTool, { quiet: true });
            e.preventDefault();
            e.stopPropagation();
        }
    }

    /** Event names from WME SDK SdkEvents (production typings, e.g. web-assets.waze.com wme_sdk_docs). */
    function registerPaintHistorySdkLifecycleClear() {
        if (!wmeSDK || !wmeSDK.Events || typeof wmeSDK.Events.on !== 'function') return;
        wmeSDK.Events.on({
            eventName: 'wme-logged-out',
            eventHandler: () => clearHistory()
        });
    }

    // (No brush mask: brush unions into the shared layer.)

    /** @returns {number} index in active layer polygons or -1 */
    function pickMasterPolygonAt(clientX, clientY) {
        const L = getActiveUserLayer();
        if (!ctx || !L || L.visible === false || !L.polygons.length) return -1;
        const polys = L.polygons;
        for (let i = polys.length - 1; i >= 0; i--) {
            if (!polys[i]) continue;
            ctx.beginPath();
            polys[i].forEach(ring => {
                ring.forEach((pt, idx) => {
                    const px = gpsToGlobalPixel(pt[0], pt[1]);
                    if (px) idx === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                });
            });
            if (ctx.isPointInPath(clientX, clientY, 'evenodd')) return i;
        }
        return -1;
    }

    function wandToleranceToPixels() {
        const t = Number(appState.wandTolerance);
        if (!Number.isFinite(t)) return 0;
        return Math.max(0, Math.min(40, t * 0.4));
    }

    function distancePointToSegmentPx(px, py, x1, y1, x2, y2) {
        const dx = x2 - x1, dy = y2 - y1;
        const len2 = dx * dx + dy * dy;
        if (len2 < 1e-10) return Math.hypot(px - x1, py - y1);
        let t = ((px - x1) * dx + (py - y1) * dy) / len2;
        t = Math.max(0, Math.min(1, t));
        const nx = x1 + t * dx, ny = y1 + t * dy;
        return Math.hypot(px - nx, py - ny);
    }

    /** Min screen distance from point to outer ring edges of one Candy part (P10 magic wand). */
    function minDistancePointToPolygonOuterRingPx(clientX, clientY, poly) {
        if (!poly || !poly[0] || poly[0].length < 2) return Infinity;
        const ring = poly[0];
        let minD = Infinity;
        for (let i = 0; i < ring.length; i++) {
            const j = (i + 1) % ring.length;
            const p1 = gpsToGlobalPixel(ring[i][0], ring[i][1]);
            const p2 = gpsToGlobalPixel(ring[j][0], ring[j][1]);
            if (!p1 || !p2) continue;
            minD = Math.min(minD, distancePointToSegmentPx(clientX, clientY, p1.x, p1.y, p2.x, p2.y));
        }
        return minD;
    }

    /** Like pickMasterPolygonAt but allows edge hit within tolPx (0 = inside-only). */
    function pickMasterPolygonAtWithTolerance(clientX, clientY, tolPx) {
        const inside = pickMasterPolygonAt(clientX, clientY);
        if (inside >= 0) return inside;
        if (!(tolPx > 0)) return -1;
        const L = getActiveUserLayer();
        if (!ctx || !L || L.visible === false || !L.polygons.length) return -1;
        const polys = L.polygons;
        let best = -1;
        let bestD = tolPx + 1;
        for (let i = polys.length - 1; i >= 0; i--) {
            if (!polys[i]) continue;
            const d = minDistancePointToPolygonOuterRingPx(clientX, clientY, polys[i]);
            if (d <= tolPx && d < bestD) {
                bestD = d;
                best = i;
            }
        }
        return best;
    }

    /** Shared pop-to-draft splice (pan click-to-draft + magic wand). */
    function popActiveLayerShapeToDraftFromIndex(shapeIndex) {
        if (shapeIndex < 0 || shapeIndex >= masterPolygons.length || !masterPolygons[shapeIndex]) return;
        commitBeforeChange('pop_draft');
        alignShapeLinksToMaster();
        revertOriginalLink = shapePlaceLinks.splice(shapeIndex, 1)[0] ?? null;
        const poppedPoly = masterPolygons.splice(shapeIndex, 1)[0];
        revertOriginalPolygon = [poppedPoly];
        draftPopInsertMasterIndex = shapeIndex;
        assignDraftFromPoppedMasterPart(poppedPoly);
        setDraftActive(true);
        changeTool('revert', { quiet: true });
        logToUI('Change to Transform Tool.');
        scheduleShapeInspectorRefresh();
        requestRender();
        updateTrackerHUD();
    }

    /** Top→bottom visible user layers; topmost shape wins (for future CS Layers map sync). */
    function pickTopVisibleShapeAt(clientX, clientY) {
        if (!ctx) return null;
        for (let li = userLayers.length - 1; li >= 0; li--) {
            const layer = userLayers[li];
            if (layer.visible === false) continue;
            const polys = layer.polygons;
            for (let i = polys.length - 1; i >= 0; i--) {
                if (!polys[i]) continue;
                ctx.beginPath();
                polys[i].forEach(ring => {
                    ring.forEach((pt, idx) => {
                        const px = gpsToGlobalPixel(pt[0], pt[1]);
                        if (px) idx === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                    });
                });
                if (ctx.isPointInPath(clientX, clientY, 'evenodd')) return { layerId: layer.id, shapeIndex: i };
            }
        }
        return null;
    }

    /** Pop hit active-layer shape into draft (click-to-draft). Returns true if handled (incl. Ctrl multiselect). */
    function tryPopShapeToDraftAt(clientX, clientY, ev) {
        if (appState.tool !== 'pan') return false;
        if (isDrawingShape || tempVertices.length > 0 || brushPainting) return false;
        if (appState.tool === 'revert' || appState.tool === 'link') return false;
        if (appState.tool === 'brush' || appState.tool === 'eraser') return false;
        const idx = pickMasterPolygonAt(clientX, clientY);
        if (idx === -1 || !masterPolygons[idx]) return false;
        const Lact = getActiveUserLayer();
        if (!Lact) return false;
        ensureShapeIdsForLayer(Lact);
        const sid = Lact.shapeIds[idx];
        const key = csShapeKey(Lact.id, sid);
        const mod = ev && (ev.ctrlKey || ev.metaKey);
        if (mod) {
            if (csLayersSelection.has(key)) csLayersSelection.delete(key);
            else csLayersSelection.add(key);
            scheduleShapeInspectorRefresh();
            requestRender();
            return true;
        }
        csLayersSelection.clear();
        csLayersSelection.add(key);
        if (appState.isDraftActive) {
            cancelDraft();
            syncActiveLayerAliases();
        }
        const idx2 = pickMasterPolygonAt(clientX, clientY);
        if (idx2 === -1 || !masterPolygons[idx2]) {
            scheduleShapeInspectorRefresh();
            requestRender();
            return true;
        }
        popActiveLayerShapeToDraftFromIndex(idx2);
        return true;
    }

    function findNativeVenueObject(venueId) {
        const objs = window.W?.model?.venues?.objects;
        if (!objs || venueId == null) return null;
        let nv = objs[venueId];
        if (nv) return nv;
        nv = objs[String(venueId)];
        return nv || null;
    }

    function validateLinkState() {
        let hasInvalidLinks = false;
        if (!window.W || !window.W.model || !window.W.model.venues || !window.W.model.venues.objects) return false;

        for (let li = 0; li < userLayers.length; li++) {
            const layer = userLayers[li];
            if (layer.isPlaces) continue;
            const links = layer.links;
            for (let i = 0; i < links.length; i++) {
                const link = normalizeShapeLink(links[i]);
                if (!link || link.venueId == null) continue;
                if (!linkBlocksApplyValidation(link)) {
                    const L = normalizeShapeLink(links[i]);
                    if (!L) continue;
                    const cleared = { ...L };
                    delete cleared.isInvalid;
                    links[i] = normalizeShapeLink(cleared);
                    continue;
                }
                const nativeVenue = findNativeVenueObject(link.venueId);
                if (!nativeVenue) {
                    links[i] = { ...link, isInvalid: true };
                    hasInvalidLinks = true;
                } else if (typeof nativeVenue.isGeometryEditable === 'function' && !nativeVenue.isGeometryEditable()) {
                    links[i] = { ...link, isInvalid: true };
                    hasInvalidLinks = true;
                } else {
                    const ok = { ...link };
                    delete ok.isInvalid;
                    links[i] = normalizeShapeLink(ok);
                }
            }
        }
        syncActiveLayerAliases();
        return hasInvalidLinks;
    }

    function modeChipLabel(mode) {
        const map = { replace: 'Replace', union: 'Union', difference: 'Subtract', intersection: 'Intersect', xor: 'XOR' };
        return map[mode] || String(mode);
    }

    function modeChipTextColor(mode) {
        if (mode === 'union') return '#1a8f6a';
        if (mode === 'difference') return '#c43d3d';
        if (mode === 'intersection') return '#b38600';
        if (mode === 'xor') return '#6b2fb8';
        return '#334155';
    }

    function updateModeChip() {
        const el = document.getElementById('wpo-mode-chip');
        if (!el) return;
        if (appState.tool === 'brush' || appState.tool === 'eraser') {
            el.style.display = 'inline-flex';
            el.style.color = appState.tool === 'eraser' ? '#c43d3d' : '#1a8f6a';
            const r = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
            const pl = brushProfileChipLabel(appState.brushProfile);
            el.textContent = (appState.tool === 'eraser' ? 'Eraser' : 'Brush') + ' · R ' + r + ' m · ' + pl + ' (ignores commit mode)';
            el.title = 'Brush and eraser always union / subtract; commit mode buttons do not apply.';
            return;
        }
        el.style.display = 'inline-flex';
        el.style.color = modeChipTextColor(appState.mode);
        el.textContent = 'Commit mode: ' + modeChipLabel(appState.mode);
        el.title = hasAnyUserLayerPolygons()
            ? 'How the next committed draft combines with shapes on user layers'
            : 'Applies when you commit a drawn or draft shape';
    }

    function scheduleAutoIngest() {
        if (autoIngestTimer) clearTimeout(autoIngestTimer);
        if (!getAutoIngest()) return;
        autoIngestTimer = setTimeout(() => {
            autoIngestTimer = null;
            if (!getAutoIngest()) return;
            if (!isGeometryLibraryReady()) return;
            if (getActiveUserLayerShapeCount() > 0) return;
            const models = getSelectedAreaVenues();
            if (models.length !== 1) return;
            ingestWmeSelection();
        }, 280);
    }

    function updateApplyButtonState() {
        const applyBtn = document.getElementById('wpo-btn-apply');
        if (!applyBtn) return;

        const ready = isGeometryLibraryReady();
        let hasInvalidLinks;
        if (brushPainting) {
            hasInvalidLinks = applyHudInvalidLinksCache;
        } else {
            hasInvalidLinks = validateLinkState();
            applyHudInvalidLinksCache = hasInvalidLinks;
        }

        if (!ready) {
            applyBtn.disabled = true;
            applyBtn.style.opacity = '0.5';
            applyBtn.style.cursor = 'not-allowed';
            applyBtn.title = 'Geometry library not ready';
        } else if (hasInvalidLinks) {
            applyBtn.disabled = true;
            applyBtn.style.opacity = '0.5';
            applyBtn.style.cursor = 'not-allowed';
            applyBtn.title = 'Blocked: An active (non-broken) linked venue is missing or not geometry-editable';
            
            const targetText = document.getElementById('wpo-target-text');
            if (targetText && !targetText.innerHTML.includes('Invalid links')) {
                targetText.innerHTML += ` <span style="color:#FF5B5B; font-weight:bold;">(Invalid links)</span>`;
            }
        } else {
            applyBtn.disabled = false;
            applyBtn.style.opacity = '1';
            applyBtn.style.cursor = 'pointer';
            applyBtn.title = 'Apply shapes to WME';
        }
    }

    function tryLinkHoveredToSelection() {
        if (hoveredMasterPolyIndex < 0) {
            logToUI('Hover a master-layer shape first.', true);
            return;
        }
        const models = getSelectedAreaVenues();
        let id = null;
        if (models.length === 1 && models[0].id != null && models[0].id !== '') {
            id = normalizeVenueIdForSdk(models[0].id);
        } else if (pendingLinkVenueId != null) {
            id = normalizeVenueIdForSdk(pendingLinkVenueId);
        }
        if (id == null || id === '') {
            logToUI('Select exactly one polygon/multipolygon place in WME (Link tool remembers the target while you hover).', true);
            return;
        }

        const nativeVenue = findNativeVenueObject(id);
        if (nativeVenue && typeof nativeVenue.isGeometryEditable === 'function' && !nativeVenue.isGeometryEditable()) {
            logToUI('Cannot link: Selected place is locked or un-editable.', true);
            return;
        }

        commitBeforeChange('link');
        alignShapeLinksToMaster();
        shapePlaceLinks[hoveredMasterPolyIndex] = normalizeShapeLink({ venueId: id, active: true, broken: false });
        const L = getActiveUserLayer();
        rebalanceVenueLinkActives(L ? { layer: L, shapeIndex: hoveredMasterPolyIndex } : null);
        pendingLinkVenueId = null;
        logToUI(`Linked shape ${hoveredMasterPolyIndex + 1} to venue ${shapePlaceLinks[hoveredMasterPolyIndex].venueId}.`, false);
        scheduleDocumentSave();
        requestRender();
        handleSelectionChange();
    }

    /** CS Layers “Activate” (palette): hovered shape becomes the active link for its venue. */
    function tryActivateHoveredShapeLink() {
        if (hoveredMasterPolyIndex < 0) {
            logToUI('Hover a shape on the active layer first.', true);
            return;
        }
        alignShapeLinksToMaster();
        const cur = normalizeShapeLink(shapePlaceLinks[hoveredMasterPolyIndex]);
        if (!cur || cur.venueId == null) {
            logToUI('Hovered shape has no place link to activate.', true);
            return;
        }
        commitBeforeChange('link_activate');
        const L = getActiveUserLayer();
        shapePlaceLinks[hoveredMasterPolyIndex] = { ...cur, active: true, broken: false };
        rebalanceVenueLinkActives(L ? { layer: L, shapeIndex: hoveredMasterPolyIndex } : null);
        logToUI(`Active link: shape ${hoveredMasterPolyIndex + 1} for venue ${cur.venueId}.`, false);
        scheduleDocumentSave();
        requestRender();
        handleSelectionChange();
    }

    function tryUnlinkHovered() {
        if (hoveredMasterPolyIndex < 0) {
            logToUI('Hover a master-layer shape first.', true);
            return;
        }
        alignShapeLinksToMaster();
        if (!shapePlaceLinks[hoveredMasterPolyIndex]) return;
        commitBeforeChange('unlink');
        shapePlaceLinks[hoveredMasterPolyIndex] = null;
        logToUI(`Unlinked shape ${hoveredMasterPolyIndex + 1}.`, false);
        scheduleDocumentSave();
        requestRender();
        handleSelectionChange();
    }

    function assignDraftFromPoppedMasterPart(part) {
        draftInteriorRings = [];
        if (!part || !part.length || !part[0] || part[0].length < 3) {
            draftVertices = [];
            return;
        }
        draftVertices = part[0].map(pt => ({ lon: pt[0], lat: pt[1] }));
        for (let r = 1; r < part.length; r++) {
            const ring = part[r];
            if (!ring || ring.length < 3) continue;
            draftInteriorRings.push(ring.map(pt => ({ lon: pt[0], lat: pt[1] })));
        }
    }

    function captureDraftRingsPixelCoords() {
        const rings = [draftVertices, ...draftInteriorRings];
        const out = [];
        for (const ring of rings) {
            const row = [];
            for (const v of ring) {
                const p = gpsToGlobalPixel(v.lon, v.lat);
                if (p) row.push({ x: p.x, y: p.y });
            }
            out.push(row);
        }
        return out;
    }

    function mapDraftRingsFromPx(mapPoint) {
        const pxRings = dragStartDraftRingsPx;
        if (!pxRings || !pxRings.length) return;
        const nv0 = pxRings[0].map(mapPoint).filter(Boolean);
        if (nv0.length >= 3) draftVertices = nv0;
        for (let i = 1; i < pxRings.length; i++) {
            const nv = pxRings[i].map(mapPoint).filter(Boolean);
            if (i - 1 < draftInteriorRings.length && nv.length >= 3) draftInteriorRings[i - 1] = nv;
        }
    }

    function ringLonLatClosedFromDraftVerts(verts) {
        let pts = verts.map(v => [v.lon, v.lat]);
        if (pts.length < 3) return null;
        if (pts[0][0] !== pts[pts.length - 1][0] || pts[0][1] !== pts[pts.length - 1][1])
            pts = pts.concat([[pts[0][0], pts[0][1]]]);
        return pts;
    }

    function buildClosedDraftPartLonLat() {
        const outer = ringLonLatClosedFromDraftVerts(draftVertices);
        if (!outer) return null;
        const holes = [];
        for (const hole of draftInteriorRings) {
            const h = ringLonLatClosedFromDraftVerts(hole);
            if (h && h.length >= 4) holes.push(h);
        }
        return [outer, ...holes];
    }

    function addAllDraftRingsToCanvasPath(targetCtx) {
        const rings = [draftVertices, ...draftInteriorRings];
        for (const ring of rings) {
            if (!ring || ring.length < 2) continue;
            let started = false;
            for (const v of ring) {
                const px = gpsToGlobalPixel(v.lon, v.lat);
                if (!px) continue;
                if (!started) {
                    targetCtx.moveTo(px.x, px.y);
                    started = true;
                } else targetCtx.lineTo(px.x, px.y);
            }
            if (started) targetCtx.closePath();
        }
    }

    /** P10: axis-aligned rectangle in Web Mercator; shift = square, alt = first point is center. */
    function rectangleVerticesFromDrag(p1, p2, opts) {
        const square = !!(opts && opts.square);
        const fromCenter = !!(opts && opts.fromCenter);
        const ax = wgs84ToMercator(p1.lon, p1.lat);
        const bx = wgs84ToMercator(p2.lon, p2.lat);
        let x0, y0, x1, y1;
        if (fromCenter) {
            const cx = ax.x, cy = ax.y;
            let vx = bx.x - cx;
            let vy = bx.y - cy;
            if (square) {
                const s = Math.max(Math.abs(vx), Math.abs(vy));
                vx = Math.sign(vx || 1) * s;
                vy = Math.sign(vy || 1) * s;
            }
            x0 = cx - Math.abs(vx);
            x1 = cx + Math.abs(vx);
            y0 = cy - Math.abs(vy);
            y1 = cy + Math.abs(vy);
        } else {
            let vx = bx.x - ax.x;
            let vy = bx.y - ax.y;
            if (square) {
                const s = Math.max(Math.abs(vx), Math.abs(vy));
                vx = Math.sign(vx || 1) * s;
                vy = Math.sign(vy || 1) * s;
            }
            const xB = ax.x + vx;
            const yB = ax.y + vy;
            x0 = Math.min(ax.x, xB);
            x1 = Math.max(ax.x, xB);
            y0 = Math.min(ax.y, yB);
            y1 = Math.max(ax.y, yB);
        }
        const c1 = mercatorToWgs84(x0, y0);
        const c2 = mercatorToWgs84(x1, y0);
        const c3 = mercatorToWgs84(x1, y1);
        const c4 = mercatorToWgs84(x0, y1);
        return [{ lon: c1.lon, lat: c1.lat }, { lon: c2.lon, lat: c2.lat }, { lon: c3.lon, lat: c3.lat }, { lon: c4.lon, lat: c4.lat }];
    }

    /** P10: shift = circle (uniform radius); default fromCenter = legacy (first point = center). Alt = diameter by two clicks. */
    function ellipseVerticesFromDrag(p1, p2, opts) {
        const square = !!(opts && opts.square);
        const fromCenter = !!(opts && opts.fromCenter);
        const a = wgs84ToMercator(p1.lon, p1.lat);
        const b = wgs84ToMercator(p2.lon, p2.lat);
        let cx, cy, r;
        if (fromCenter) {
            cx = a.x;
            cy = a.y;
            let vx = b.x - a.x;
            let vy = b.y - a.y;
            r = Math.hypot(vx, vy);
            if (square) r = Math.max(Math.abs(vx), Math.abs(vy));
        } else {
            cx = (a.x + b.x) / 2;
            cy = (a.y + b.y) / 2;
            r = Math.hypot(b.x - a.x, b.y - a.y) / 2;
            if (square) r = Math.max(Math.abs(b.x - a.x), Math.abs(b.y - a.y)) / 2;
        }
        const pts = [];
        for (let i = 0; i < 36; i++) {
            const ang = (i / 36) * Math.PI * 2;
            pts.push(mercatorToWgs84(cx + r * Math.cos(ang), cy + r * Math.sin(ang)));
        }
        return pts;
    }

    function generateRectangle(p1, p2) {
        return rectangleVerticesFromDrag(p1, p2, { square: false, fromCenter: false });
    }
    function generateEllipse(center, edge) {
        return ellipseVerticesFromDrag(center, edge, { square: false, fromCenter: true });
    }
    function getDraftBBox() {
        const pxs = [];
        const pushRing = (ring) => {
            for (const v of ring) {
                const p = gpsToGlobalPixel(v.lon, v.lat);
                if (p) pxs.push(p);
            }
        };
        pushRing(draftVertices);
        for (const h of draftInteriorRings) pushRing(h);
        if (!pxs.length) return null;
        const xs = pxs.map(p => p.x), ys = pxs.map(p => p.y);
        return { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
    }
    function getDraftHitRegion(x, y) {
        hoveredDraftVertexIndex = -1;
        hoveredDraftMidpointIndex = -1;
        for (let i = 0; i < draftVertices.length; i++) {
            const px = gpsToGlobalPixel(draftVertices[i].lon, draftVertices[i].lat);
            if (px) {
                const dx = x - px.x;
                const dy = y - px.y;
                if (dx * dx + dy * dy <= VERTEX_HIT_RADIUS_SQ) {
                    hoveredDraftVertexIndex = i;
                    return 'vertex';
                }
            }
        }
        for (let i = 0; i < draftVertices.length; i++) {
            const nextIdx = (i + 1) % draftVertices.length;
            const px1 = gpsToGlobalPixel(draftVertices[i].lon, draftVertices[i].lat);
            const px2 = gpsToGlobalPixel(draftVertices[nextIdx].lon, draftVertices[nextIdx].lat);
            if (px1 && px2) {
                const midX = (px1.x + px2.x) / 2;
                const midY = (px1.y + px2.y) / 2;
                const dx = x - midX;
                const dy = y - midY;
                if (dx * dx + dy * dy <= VERTEX_HIT_RADIUS_SQ) {
                    hoveredDraftMidpointIndex = i;
                    return 'midpoint';
                }
            }
        }

        let b = getDraftBBox(); if (!b) return null;
        const pad = DRAFT_BBOX_PAD_PX;
        const pb = { minX: b.minX - pad, maxX: b.maxX + pad, minY: b.minY - pad, maxY: b.maxY + pad };
        let cx = (pb.minX + pb.maxX) / 2, cy = (pb.minY + pb.maxY) / 2, H = 6;
        if (Math.hypot(x - cx, y - (pb.minY - 25)) <= H * 1.5) return 'rotate';
        if (Math.abs(x - pb.minX) <= H && Math.abs(y - pb.minY) <= H) return 'resize-nw';
        if (Math.abs(x - pb.maxX) <= H && Math.abs(y - pb.minY) <= H) return 'resize-ne';
        if (Math.abs(x - pb.minX) <= H && Math.abs(y - pb.maxY) <= H) return 'resize-sw';
        if (Math.abs(x - pb.maxX) <= H && Math.abs(y - pb.maxY) <= H) return 'resize-se';
        ctx.beginPath();
        addAllDraftRingsToCanvasPath(ctx);
        if (ctx.isPointInPath(x, y, 'evenodd')) return 'move';
        return null;
    }
    function getCursorForHit(hit) {
        if (hit === 'vertex' || hit === 'midpoint') return draggingDraftVertexIndex !== -1 ? 'grabbing' : 'grab';
        if (hit === 'rotate') return 'crosshair'; if (hit === 'move') return 'move';
        if (hit === 'resize-nw' || hit === 'resize-se') return 'nwse-resize'; if (hit === 'resize-ne' || hit === 'resize-sw') return 'nesw-resize';
        return 'crosshair';
    }
    function executeDraftTransform(curX, curY) {
        if (draftAction === 'vertex' && draggingDraftVertexIndex !== -1) {
            const newGps = globalPixelToGps(curX, curY);
            if (newGps) draftVertices[draggingDraftVertexIndex] = newGps;
            return;
        }
        let b = dragStartBBox;
        if (!b) return;
        const pad = DRAFT_BBOX_PAD_PX;
        const pb = { minX: b.minX - pad, maxX: b.maxX + pad, minY: b.minY - pad, maxY: b.maxY + pad };
        let cx = (pb.minX + pb.maxX) / 2, cy = (pb.minY + pb.maxY) / 2;
        if (!dragStartDraftRingsPx || dragStartDraftRingsPx.length === 0) return;
        if (draftAction === 'move') {
            const dx = curX - dragStartMouse.x, dy = curY - dragStartMouse.y;
            mapDraftRingsFromPx((p) => globalPixelToGps(p.x + dx, p.y + dy));
        } else if (draftAction === 'rotate') {
            const da = Math.atan2(curY - cy, curX - cx) - Math.atan2(dragStartMouse.y - cy, dragStartMouse.x - cx);
            mapDraftRingsFromPx((p) => globalPixelToGps(
                cx + (p.x - cx) * Math.cos(da) - (p.y - cy) * Math.sin(da),
                cy + (p.x - cx) * Math.sin(da) + (p.y - cy) * Math.cos(da)
            ));
        } else if (draftAction.startsWith('resize-')) {
            const h = draftAction.split('-')[1];
            let origin = { x: cx, y: cy };
            if (h.includes('n')) origin.y = pb.maxY; if (h.includes('s')) origin.y = pb.minY;
            if (h.includes('w')) origin.x = pb.maxX; if (h.includes('e')) origin.x = pb.minX;
            const scaleX = Math.abs(curX - origin.x) / (Math.abs(dragStartMouse.x - origin.x) || 1);
            const scaleY = Math.abs(curY - origin.y) / (Math.abs(dragStartMouse.y - origin.y) || 1);
            const signX = Math.sign(curX - origin.x) === Math.sign(dragStartMouse.x - origin.x) ? 1 : -1;
            const signY = Math.sign(curY - origin.y) === Math.sign(dragStartMouse.y - origin.y) ? 1 : -1;
            mapDraftRingsFromPx((p) => globalPixelToGps(
                origin.x + (p.x - origin.x) * (scaleX * signX),
                origin.y + (p.y - origin.y) * (scaleY * signY)
            ));
        }
    }

    /**
     * P10: Shift = union draft with parts that intersect drawnPoly; Alt (without Shift) = difference those parts minus draft.
     * Non-intersecting parts + links are preserved in order. New geometry from the op gets null links.
     * @returns {boolean} true if applied (caller skips default boolean path).
     */
    function applyCommitDraftIntersectingModifiers(pc, drawnPoly, prevParts, prevLinks, shiftWins) {
        const n = prevParts.length;
        const hitSet = new Set();
        for (let i = 0; i < n; i++) {
            if (prevParts[i] && pc.geometriesIntersect(prevParts[i], drawnPoly)) hitSet.add(i);
        }
        const missParts = [];
        const missLinks = [];
        const hitParts = [];
        for (let i = 0; i < n; i++) {
            if (hitSet.has(i)) hitParts.push(prevParts[i]);
            else {
                missParts.push(prevParts[i]);
                missLinks.push(prevLinks[i]);
            }
        }
        let mergedTail;
        if (hitParts.length === 0) {
            mergedTail = drawnPoly;
        } else {
            const gHit = hitParts.length === 1 ? hitParts[0] : pc.union(...hitParts);
            mergedTail = shiftWins ? pc.union(gHit, drawnPoly) : pc.difference(gHit, drawnPoly);
        }
        if (!mergedTail || !mergedTail.length) {
            masterPolygons = missParts.slice();
            shapePlaceLinks = missLinks.slice();
            logToUI('P10 commit: boolean result empty on intersecting parts; non-intersecting shapes kept.', false);
            return true;
        }
        masterPolygons = missParts.concat(mergedTail);
        shapePlaceLinks = missLinks.concat(new Array(mergedTail.length).fill(null));
        logToUI(shiftWins
            ? 'Committed (Shift): union with intersecting shapes on active layer.'
            : 'Committed (Alt): subtract draft from intersecting shapes on active layer.', false);
        return true;
    }

    function commitDraft() {
        if (!appState.isDraftActive || draftVertices.length < 3) return;
        const pc = getGeometryEngine();
        if (!pc) {
            logToUI("Geometry library (jsts) is still loading. Wait a moment and try again.", true);
            return;
        }
        const closedPart = buildClosedDraftPartLonLat();
        if (!closedPart || !closedPart[0] || closedPart[0].length < 4) return;
        let drawnPoly = pc.union([closedPart]);
        drawnPoly = flattenSanitizedDrawnParts(drawnPoly, pc);
        if (!drawnPoly.length) {
            logToUI('Draft geometry collapsed after cleanup; nothing saved.', true);
            return;
        }
        commitBeforeChange('draft_commit');
        syncActiveLayerAliases();
        const prevParts = masterPolygons.slice();
        const prevLinks = cloneShapePlaceLinks(shapePlaceLinks);
        try {
            if (revertOriginalPolygon) {
                masterPolygons = [...prevParts];
                shapePlaceLinks = [...prevLinks];

                const insertIdx = draftPopInsertMasterIndex >= 0 && draftPopInsertMasterIndex <= prevParts.length
                    ? draftPopInsertMasterIndex
                    : prevParts.length;

                if (appState.mode === 'replace') {
                    // Re-insert the original at its slot, then swap with committed draft (same index + links).
                    masterPolygons.splice(insertIdx, 0, ...revertOriginalPolygon);
                    const ro = cloneLinkEntry(revertOriginalLink);
                    const linksToInsert = revertOriginalPolygon.map((_, i) => (i === 0 ? ro : null));
                    shapePlaceLinks.splice(insertIdx, 0, ...linksToInsert);
                    masterPolygons.splice(insertIdx, revertOriginalPolygon.length, ...drawnPoly);
                    const rb = cloneLinkEntry(revertOriginalLink);
                    let newLinks;
                    if (!rb) {
                        newLinks = drawnPoly.map(() => null);
                    } else if (drawnPoly.length <= 1) {
                        newLinks = [cloneLinkEntry(rb)];
                    } else {
                        const splitGroupId = 'sg_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
                        newLinks = drawnPoly.map((_, i) => ({
                            ...cloneLinkEntry(rb),
                            splitGroupId,
                            active: i === 0,
                            broken: i !== 0
                        }));
                    }
                    shapePlaceLinks.splice(insertIdx, revertOriginalPolygon.length, ...newLinks);
                    logToUI('Committed shape edit.');
                } else {
                    // Boolean modes: the popped geometry is already absent from masterPolygons. Do not re-insert it
                    // before union/XOR/etc. — that left a copy at the old position plus the moved draft.
                    const preBoolN = masterPolygons.length;
                    const preBoolLinks = cloneShapePlaceLinks(shapePlaceLinks);

                    if (preBoolN === 0) {
                        masterPolygons = drawnPoly;
                        const rb = cloneLinkEntry(revertOriginalLink);
                        if (!rb) {
                            shapePlaceLinks = drawnPoly.map(() => null);
                        } else if (drawnPoly.length <= 1) {
                            shapePlaceLinks = [cloneLinkEntry(rb)];
                        } else {
                            const splitGroupId = 'sg_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
                            shapePlaceLinks = drawnPoly.map((_, i) => ({
                                ...cloneLinkEntry(rb),
                                splitGroupId,
                                active: i === 0,
                                broken: i !== 0
                            }));
                        }
                        logToUI(`Committed shape edit (${appState.mode}).`);
                    } else {
                        if (appState.mode === 'union') {
                            masterPolygons = pc.union(masterPolygons, drawnPoly);
                        } else if (appState.mode === 'difference') {
                            masterPolygons = pc.difference(masterPolygons, drawnPoly);
                        } else if (appState.mode === 'intersection') {
                            masterPolygons = pc.intersection(masterPolygons, drawnPoly);
                        } else if (appState.mode === 'xor') {
                            masterPolygons = pc.xor(masterPolygons, drawnPoly);
                        }

                        const newCount = masterPolygons.length;
                        if (newCount === preBoolN + drawnPoly.length) {
                            const rb = cloneLinkEntry(revertOriginalLink);
                            const tail = !rb ? drawnPoly.map(() => null) : drawnPoly.length <= 1
                                ? [cloneLinkEntry(rb)]
                                : (() => {
                                    const splitGroupId = 'sg_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);
                                    return drawnPoly.map((_, i) => ({
                                        ...cloneLinkEntry(rb),
                                        splitGroupId,
                                        active: i === 0,
                                        broken: i !== 0
                                    }));
                                })();
                            shapePlaceLinks = preBoolLinks.concat(tail);
                        } else if (newCount === preBoolN) {
                            shapePlaceLinks = preBoolLinks.slice(0, newCount);
                        } else {
                            shapePlaceLinks = new Array(newCount).fill(null);
                            logToUI('Shape–place links cleared (geometry part count changed).', false);
                        }
                        logToUI(`Committed edit via ${appState.mode.toUpperCase()}`);
                    }
                }
            } else {
                const shiftWins = isShiftDown; // both modifiers: shift wins (union) — P10
                const useP10Intersect = prevParts.length > 0 && appState.mode !== 'replace' && (isShiftDown || isAltDown);
                let p10Handled = false;
                if (useP10Intersect) {
                    p10Handled = applyCommitDraftIntersectingModifiers(pc, drawnPoly, prevParts, prevLinks, shiftWins);
                }
                if (!p10Handled) {
                    if (appState.mode === 'replace' || masterPolygons.length === 0) {
                        masterPolygons = drawnPoly;
                        // For brand new replace, wipe everything and initialize links
                        shapePlaceLinks = new Array(masterPolygons.length).fill(null);

                        // Edge case: they popped a shape, then switched to union/intersection, but canvas was empty so it fell back to drawnPoly
                        if (revertOriginalPolygon && prevParts.length === 0 && drawnPoly.length === 1) {
                            const cl = cloneLinkEntry(revertOriginalLink);
                            shapePlaceLinks = [cl || null];
                        }
                    } else if (appState.mode === 'union') {
                        masterPolygons = pc.union(masterPolygons, drawnPoly);
                    } else if (appState.mode === 'difference') {
                        masterPolygons = pc.difference(masterPolygons, drawnPoly);
                    } else if (appState.mode === 'intersection') {
                        masterPolygons = pc.intersection(masterPolygons, drawnPoly);
                    } else if (appState.mode === 'xor') {
                        masterPolygons = pc.xor(masterPolygons, drawnPoly);
                    }

                    if (!revertOriginalPolygon || appState.mode !== 'replace') {
                        logToUI(`Committed via ${appState.mode.toUpperCase()}`);
                    }

                    const newCount = masterPolygons.length;
                    if (appState.mode !== 'replace' && masterPolygons.length > 0) {
                        if (newCount !== prevParts.length) {
                            shapePlaceLinks = new Array(newCount).fill(null);
                            logToUI('Shape–place links cleared (geometry part count changed).', false);
                        } else {
                            shapePlaceLinks = prevLinks.slice(0, newCount);
                        }
                    }
                }
            }
            expandSanitizedMasterPolygons(pc);
            alignShapeLinksToMaster();
            flushBrushGeometryToActiveLayer();
        } catch (e) { logToUI("Math Error. Crossed lines?", true); }

        revertOriginalPolygon = null;
        revertOriginalLink = null;
        setDraftActive(false);
        pruneShapeInspectorSelection();
        scheduleShapeInspectorRefresh();
    }

    // --- 6. RENDERING & TRACKER MATH ---
    /** Mean map-pixel radius of a geodesic disk (for canvas preview line width). */
    function brushMeanRadiusPixels(lon, lat, radiusM) {
        const c = gpsToGlobalPixel(lon, lat);
        if (!c) return 6;
        const pN = destinationPointMeters(lon, lat, 0, radiusM);
        const pE = destinationPointMeters(lon, lat, Math.PI / 2, radiusM);
        const pxN = gpsToGlobalPixel(pN.lon, pN.lat);
        const pxE = gpsToGlobalPixel(pE.lon, pE.lat);
        if (!pxN || !pxE) return 6;
        return Math.max(2, (Math.hypot(pxN.x - c.x, pxN.y - c.y) + Math.hypot(pxE.x - c.x, pxE.y - c.y)) / 2);
    }

    /**
     * While dragging: fast Canvas2D “sausage” (round stroke) along sampled GPS path.
     * Committed geometry is still built once on mouse-up via jsts.
     */
    function subsampleStrokeLonLatForPreview(pts) {
        if (pts.length <= BRUSH_PREVIEW_MAX_VERTICES) return pts;
        const stride = Math.ceil(pts.length / BRUSH_PREVIEW_MAX_VERTICES);
        const out = [];
        for (let i = 0; i < pts.length - 1; i += stride) out.push(pts[i]);
        out.push(pts[pts.length - 1]);
        return out;
    }

    /** Stride-subsample a closed lon/lat ring for faster Canvas draws (preview only). */
    function decimateLonLatRingForCanvas(ring, maxVerts) {
        if (!ring || ring.length <= maxVerts) return ring;
        const closed = ring.length > 1 && ring[0][0] === ring[ring.length - 1][0] && ring[0][1] === ring[ring.length - 1][1];
        const open = closed ? ring.slice(0, -1) : ring.slice();
        if (open.length <= maxVerts) return ring;
        const stride = Math.ceil(open.length / maxVerts);
        const out = [];
        for (let i = 0; i < open.length; i += stride) out.push(open[i]);
        if (out.length < 3) return ring;
        out.push([out[0][0], out[0][1]]);
        return out;
    }

    function masterPartRingsForCanvasDraw(part) {
        if (!brushPainting || !part || !part.length) return part;
        return part.map(r => decimateLonLatRingForCanvas(r, BRUSH_LIVE_RENDER_MAX_RING_VERTS));
    }

    function renderBrushLivePreview(ctx) {
        if (!brushPainting || brushStrokeSamples.length === 0) return;
        const previewPts = subsampleStrokeLonLatForPreview(brushStrokeSamples);
        const rm = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
        const mid = previewPts[Math.floor(previewPts.length / 2)];
        const rPx = brushMeanRadiusPixels(mid[0], mid[1], rm);
        const lineW = Math.max(4, rPx * 2);

        ctx.save();
        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';
        ctx.lineWidth = lineW;
        ctx.globalAlpha = brushIsEraser ? 0.36 : 0.4;
        ctx.strokeStyle = brushIsEraser ? 'rgba(196,61,61,1)' : 'rgba(0,161,241,1)';
        ctx.beginPath();
        if (previewPts.length === 1) {
            const px = gpsToGlobalPixel(previewPts[0][0], previewPts[0][1]);
            if (px) {
                ctx.arc(px.x, px.y, rPx, 0, Math.PI * 2);
                ctx.stroke();
            }
            ctx.restore();
            return;
        }
        let started = false;
        previewPts.forEach(pt => {
            const px = gpsToGlobalPixel(pt[0], pt[1]);
            if (!px) return;
            if (!started) {
                ctx.moveTo(px.x, px.y);
                started = true;
            } else {
                ctx.lineTo(px.x, px.y);
            }
        });
        if (started) ctx.stroke();
        ctx.restore();
    }

    function renderCanvas() {
        if (!ctx || !canvasElement) return;
        updateApplyButtonState();
        updateModeChip();
        const cw = canvasElement.width;
        const ch = canvasElement.height;
        ctx.clearRect(0, 0, cw, ch);

        const mapClip = getMapClipRect();
        ctx.save();
        if (mapClip) {
            ctx.beginPath();
            ctx.rect(mapClip.x, mapClip.y, mapClip.w, mapClip.h);
            ctx.clip();
        }

        if (appState.tool !== 'pan') {
            ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
            if (mapClip) ctx.fillRect(mapClip.x, mapClip.y, mapClip.w, mapClip.h);
            else ctx.fillRect(0, 0, cw, ch);
        }

        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';

        if (appState.tool === 'measure' && tempVertices.length === 2) {
            let p1 = gpsToGlobalPixel(tempVertices[0].lon, tempVertices[0].lat);
            let p2 = gpsToGlobalPixel(tempVertices[1].lon, tempVertices[1].lat);
            if(p1 && p2) {
                let pxDist = Math.hypot(p2.x - p1.x, p2.y - p1.y);
                const realDist = hostGeodesicLineLengthMeters(
                    tempVertices[0].lon, tempVertices[0].lat,
                    tempVertices[1].lon, tempVertices[1].lat
                );
                const isImp = isImperialUnits();
                let realStr = realDist != null ? formatLength(realDist, isImp) : '—';

                ctx.strokeStyle = '#FF5B5B'; ctx.lineWidth = 2; ctx.beginPath();
                ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();

                ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(p2.x + 10, p2.y + 10, 130, 40);
                ctx.fillStyle = '#fff'; ctx.font = '11px monospace';
                ctx.fillText(`Px: ${Math.round(pxDist)}`, p2.x + 15, p2.y + 25);
                ctx.fillText(`Geo: ${realStr}`, p2.x + 15, p2.y + 40);
            }
        }

        const isReplacing = (appState.mode === 'replace') && (isDrawingShape || appState.isDraftActive || tempVertices.length > 0) && !brushPainting;

        if (placesPreviewLayer.visible && placesPreviewLayer.polygons.length > 0) {
            ctx.fillStyle = 'rgba(90, 120, 100, 0.06)';
            ctx.strokeStyle = 'rgba(70, 100, 85, 0.42)';
            ctx.lineWidth = 1;
            ctx.setLineDash([5, 4]);
            for (let p = 0; p < placesPreviewLayer.polygons.length; p++) {
                const drawPart = masterPartRingsForCanvasDraw(placesPreviewLayer.polygons[p]);
                ctx.beginPath();
                for (let r = 0; r < drawPart.length; r++) {
                    const pts = drawPart[r];
                    for (let i = 0; i < pts.length; i++) {
                        const px = gpsToGlobalPixel(pts[i][0], pts[i][1]);
                        if (px) i === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                    }
                    ctx.closePath();
                }
                ctx.fill('evenodd');
                ctx.stroke();
            }
            ctx.setLineDash([]);
        }

        for (let li = 0; li < userLayers.length; li++) {
            const uLayer = userLayers[li];
            if (uLayer.visible === false) continue;
            const polys = uLayer.polygons;
            const links = uLayer.links;
            const isActiveLayer = uLayer.id === activeLayerId;
            if (!polys.length) continue;

            ctx.fillStyle = isReplacing ? 'rgba(0, 161, 241, 0.1)' : 'rgba(0, 161, 241, 0.4)';
            ctx.strokeStyle = isReplacing ? 'rgba(0, 161, 241, 0.2)' : '#00A1F1';
            ctx.lineWidth = 2;

            for (let p = 0; p < polys.length; p++) {
                const drawPart = masterPartRingsForCanvasDraw(polys[p]);
                ctx.beginPath();
                for (let r = 0; r < drawPart.length; r++) {
                    let pts = drawPart[r];
                    for (let i = 0; i < pts.length; i++) {
                        let px = gpsToGlobalPixel(pts[i][0], pts[i][1]);
                        if (px) i === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                    }
                    ctx.closePath();
                }
                ctx.fill('evenodd'); ctx.stroke();

                if (isActiveLayer && appState.highlightedLinkIndex === p) {
                    ctx.strokeStyle = '#FFC000';
                    ctx.lineWidth = 4;
                    ctx.beginPath();
                    for (let r = 0; r < drawPart.length; r++) {
                        let pts = drawPart[r];
                        for (let i = 0; i < pts.length; i++) {
                            let px = gpsToGlobalPixel(pts[i][0], pts[i][1]);
                            if (px) i === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                        }
                        ctx.closePath();
                    }
                    ctx.stroke();
                    ctx.lineWidth = 2;
                    ctx.strokeStyle = isReplacing ? 'rgba(0, 161, 241, 0.2)' : '#00A1F1';
                }

                const ext = polys[p][0];
                if (!brushPainting && ext && ext.length && isActiveLayer) {
                    let sx = 0, sy = 0, n = 0;
                    ext.forEach(pt => {
                        const px = gpsToGlobalPixel(pt[0], pt[1]);
                        if (px) { sx += px.x; sy += px.y; n++; }
                    });
                    if (n) {
                        const bx = sx / n + 8;
                        const by = sy / n - 8;
                        const link = links[p];
                        const Ln = normalizeShapeLink(link);
                        let prefix = '+';
                        let isError = false;
                        if (Ln && Ln.venueId != null) {
                            if (Ln.isInvalid && isActiveNonBrokenLink(Ln)) {
                                prefix = '!·';
                                isError = true;
                            } else if (Ln.broken === true) {
                                prefix = 'B·';
                            } else if (Ln.active === false) {
                                prefix = 'I·';
                            } else {
                                prefix = 'U·';
                            }
                        }
                        const tail = Ln && Ln.venueId != null
                            ? (prefix + String(Ln.venueId).replace(/\D/g, '').slice(-4) || String(Ln.venueId))
                            : '+';
                        const text = (p + 1) + ' ' + tail;
                        ctx.save();
                        ctx.font = 'bold 10px "Helvetica Neue", Helvetica, Arial, sans-serif';
                        const padX = 5;
                        const padY = 3;
                        const tw = ctx.measureText(text).width;
                        const bw = tw + padX * 2;
                        const bh = 11 + padY * 2;
                        const hx = bx - bw / 2;
                        const hy = by - bh / 2;
                        const hoverHere = (appState.tool === 'revert' || appState.tool === 'link') && hoveredMasterPolyIndex === p;

                        if (isError) {
                            ctx.fillStyle = '#FF5B5B';
                        } else {
                            ctx.fillStyle = hoverHere ? 'rgba(0,0,0,0.82)' : 'rgba(0,0,0,0.68)';
                        }

                        ctx.strokeStyle = appState.highlightedLinkIndex === p ? '#FFC000' : (hoverHere ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.35)');
                        ctx.lineWidth = appState.highlightedLinkIndex === p ? 2.2 : (hoverHere ? 1.6 : 1);
                        ctx.beginPath();
                        if (typeof ctx.roundRect === 'function') ctx.roundRect(hx, hy, bw, bh, 4);
                        else ctx.rect(hx, hy, bw, bh);
                        ctx.fill();
                        ctx.stroke();
                        ctx.fillStyle = '#fff';
                        ctx.textBaseline = 'middle';
                        ctx.fillText(text, hx + padX, hy + bh / 2);
                        ctx.restore();
                        ctx.strokeStyle = isReplacing ? 'rgba(0, 161, 241, 0.2)' : '#00A1F1';
                        ctx.lineWidth = 2;
                    }
                }
            }
        }

        for (let li = 0; li < userLayers.length; li++) {
            const uLayer = userLayers[li];
            if (uLayer.visible === false) continue;
            ensureShapeIdsForLayer(uLayer);
            const polys = uLayer.polygons;
            for (let p = 0; p < polys.length; p++) {
                const csk = csShapeKey(uLayer.id, uLayer.shapeIds[p]);
                const drawPart = masterPartRingsForCanvasDraw(polys[p]);
                const hl = csk === csLayersListHoverShapeKey || csk === csLayersMapHoverShapeKey;
                const sel = csLayersSelection.has(csk);
                if (!hl && !sel) continue;
                const trace = () => {
                    ctx.beginPath();
                    for (let r = 0; r < drawPart.length; r++) {
                        const pts = drawPart[r];
                        for (let i = 0; i < pts.length; i++) {
                            const px = gpsToGlobalPixel(pts[i][0], pts[i][1]);
                            if (px) i === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                        }
                        ctx.closePath();
                    }
                };
                if (hl) {
                    trace();
                    ctx.fillStyle = 'rgba(255, 235, 59, 0.3)';
                    ctx.fill('evenodd');
                    ctx.strokeStyle = '#f9a825';
                    ctx.lineWidth = 2;
                    ctx.stroke();
                }
                if (sel) {
                    trace();
                    ctx.strokeStyle = 'rgba(0, 229, 255, 0.95)';
                    ctx.lineWidth = 3;
                    ctx.stroke();
                }
            }
        }

        renderBrushLivePreview(ctx);

        // Shape Shifter / link tool hover highlight
        if ((appState.tool === 'revert' || appState.tool === 'link') && hoveredMasterPolyIndex !== -1 && !appState.isDraftActive) {
            const hoveredPoly = masterPolygons[hoveredMasterPolyIndex];
            if (hoveredPoly && Array.isArray(hoveredPoly)) {
                ctx.fillStyle = 'rgba(255, 200, 0, 0.5)';
                ctx.strokeStyle = '#FFC000';
                ctx.lineWidth = 3;
                ctx.beginPath();
                hoveredPoly.forEach(ring => {
                    ring.forEach((pt, idx) => {
                        let px = gpsToGlobalPixel(pt[0], pt[1]);
                        if (px) idx === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                    });
                });
                ctx.fill("evenodd"); ctx.stroke();
            }
        }

        let modeColor = appState.mode==='union'?'#26CC9A':appState.mode==='difference'?'#FF5B5B':appState.mode==='intersection'?'#FFC000':appState.mode==='xor'?'#a451fa':'#000';

        if (appState.isDraftActive && draftVertices.length > 0) {
            ctx.fillStyle = modeColor; ctx.globalAlpha = 0.3; ctx.beginPath();
            addAllDraftRingsToCanvasPath(ctx);
            ctx.fill('evenodd'); ctx.globalAlpha = 1.0; ctx.strokeStyle = modeColor; ctx.stroke();

            for (let i = 0; i < draftVertices.length; i++) {
                let px = gpsToGlobalPixel(draftVertices[i].lon, draftVertices[i].lat);
                if (px) {
                    ctx.beginPath();
                    let isH = (i === hoveredDraftVertexIndex || i === draggingDraftVertexIndex);
                    ctx.arc(px.x, px.y, isH ? 6 : 4, 0, Math.PI * 2);
                    ctx.fillStyle = isH ? modeColor : '#fff';
                    ctx.strokeStyle = modeColor;
                    ctx.lineWidth = 2;
                    ctx.fill();
                    ctx.stroke();
                }
            }
            for (let i = 0; i < draftVertices.length; i++) {
                const nextIdx = (i + 1) % draftVertices.length;
                const px1 = gpsToGlobalPixel(draftVertices[i].lon, draftVertices[i].lat);
                const px2 = gpsToGlobalPixel(draftVertices[nextIdx].lon, draftVertices[nextIdx].lat);
                if (px1 && px2) {
                    const midX = (px1.x + px2.x) / 2;
                    const midY = (px1.y + px2.y) / 2;
                    ctx.beginPath();
                    let isH = (i === hoveredDraftMidpointIndex);
                    ctx.arc(midX, midY, isH ? 5 : 3, 0, Math.PI * 2);
                    ctx.fillStyle = isH ? modeColor : 'rgba(255, 255, 255, 0.6)';
                    ctx.strokeStyle = isH ? modeColor : 'rgba(0, 0, 0, 0.3)';
                    ctx.lineWidth = 1;
                    ctx.fill();
                    ctx.stroke();
                }
            }

            let b = getDraftBBox();
            if (b) {
                const pad = DRAFT_BBOX_PAD_PX;
                const pb = { minX: b.minX - pad, maxX: b.maxX + pad, minY: b.minY - pad, maxY: b.maxY + pad };
                ctx.setLineDash([4, 4]); ctx.strokeRect(pb.minX, pb.minY, pb.maxX - pb.minX, pb.maxY - pb.minY); ctx.setLineDash([]);
                ctx.fillStyle = '#fff'; ctx.strokeStyle = '#000';
                let cx = (pb.minX + pb.maxX) / 2, cy = (pb.minY + pb.maxY) / 2, H = 4;
                ctx.beginPath(); ctx.moveTo(cx, pb.minY); ctx.lineTo(cx, pb.minY - 25); ctx.stroke();
                ctx.beginPath(); ctx.arc(cx, pb.minY - 25, 4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
                [[pb.minX, pb.minY], [cx, pb.minY], [pb.maxX, pb.minY], [pb.maxX, cy], [pb.maxX, pb.maxY], [cx, pb.maxY], [pb.minX, pb.maxY], [pb.minX, cy]].forEach(p => { ctx.fillRect(p[0] - H, p[1] - H, H * 2, H * 2); ctx.strokeRect(p[0] - H, p[1] - H, H * 2, H * 2); });
            }
        }

        if (tempVertices.length > 0 && appState.tool !== 'measure') {
            ctx.strokeStyle = modeColor; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); ctx.beginPath();
            for (let i = 0; i < tempVertices.length; i++) { let px = gpsToGlobalPixel(tempVertices[i].lon, tempVertices[i].lat); if (px) i === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y); }
            if (appState.tool === 'polygon' && currentMousePixel) ctx.lineTo(currentMousePixel.x, currentMousePixel.y); else if (tempVertices.length > 2) ctx.closePath();
            ctx.stroke(); ctx.setLineDash([]);
            if (appState.tool === 'polygon') {
                for (let i = 0; i < tempVertices.length; i++) {
                    let px = gpsToGlobalPixel(tempVertices[i].lon, tempVertices[i].lat);
                    if (px) {
                        ctx.beginPath();
                        let isH = (i === hoveredTempVertexIndex || i === draggingVertexIndex);
                        ctx.arc(px.x, px.y, isH ? 6 : 4, 0, Math.PI * 2);
                        ctx.fillStyle = isH ? modeColor : '#fff';
                        ctx.strokeStyle = modeColor;
                        ctx.lineWidth = 2;
                        ctx.fill();
                        ctx.stroke();
                    }
                }
            }
        }

        if ((appState.tool === 'brush' || appState.tool === 'eraser') && currentMousePixel) {
            const g = globalPixelToGps(currentMousePixel.x, currentMousePixel.y);
            if (g) {
                const rm = Math.max(BRUSH_METERS_MIN, Math.min(BRUSH_METERS_MAX, Number(appState.brushSize) || 12));
                const nFull = brushCircleSidesForRadiusMeters(rm);
                const circCap = brushPainting ? Math.min(BRUSH_LIVE_CURSOR_MAX_SIDES, nFull) : undefined;
                let ring = geodesicBrushProfileRing(g.lon, g.lat, rm, g.lat, null, 0, circCap);
                if (appState.brushProfile !== 'circle' && brushPainting) {
                    ring = subsampleClosedRingForPreview(ring, BRUSH_LIVE_CURSOR_MAX_SIDES);
                }
                ctx.beginPath();
                ring.forEach((pt, idx) => {
                    const px = gpsToGlobalPixel(pt[0], pt[1]);
                    if (px) idx === 0 ? ctx.moveTo(px.x, px.y) : ctx.lineTo(px.x, px.y);
                });
                ctx.closePath();
                ctx.strokeStyle = appState.tool === 'eraser' ? 'rgba(196,61,61,0.9)' : 'rgba(26,143,106,0.9)';
                ctx.setLineDash([5, 5]);
                ctx.lineWidth = 2;
                ctx.stroke();
                ctx.setLineDash([]);
            }
        }

        ctx.restore();
    }

    function updateLiveMouseHUD(globalPixel) {
        const hud = document.getElementById('wme-paint-hud');
        if (!hud || !globalPixel || hud.style.display === 'none') return;
        document.getElementById('hud-px').innerText = `${Math.round(globalPixel.x)}, ${Math.round(globalPixel.y)}`;
        const liveGps = globalPixelToGps(globalPixel.x, globalPixel.y);
        if (liveGps) {
            document.getElementById('hud-gps').innerText = `${liveGps.lon.toFixed(5)}, ${liveGps.lat.toFixed(5)}`;
            document.getElementById('hud-layer').innerText = `${getActiveLayerDisplayName()} · WME: ${getHoveredLayerLabel()}`;
        }
    }

    function updateTrackerHUD() {
        const hud = document.getElementById('wme-paint-hud');
        if (!hud || hud.style.display === 'none') return;

        let displayVertices = [];
        let isCircle = false;

        if (appState.isDraftActive) {
            displayVertices = draftVertices;
        } else if (isDrawingShape && tempVertices.length > 0) {
            displayVertices = tempVertices;
            if (appState.tool === 'ellipse') isCircle = true;
        } else if (masterPolygons.length > 0 && masterPolygons[0][0]) {
            displayVertices = masterPolygons[0][0].map(pt => ({lon: pt[0], lat: pt[1]}));
        }

        const geoContainer = document.getElementById('hud-geo-math');

        if (displayVertices.length < 3) {
            if (appState.targetVenueObj) {
                geoContainer.innerHTML = '<em>No canvas polygon yet — use Ingest to load the place geometry from WME.</em>';
            } else {
                geoContainer.innerHTML = '<em>No shape active</em>';
            }
            return;
        }

        try {
            const isImp = isImperialUnits();
            let pxList = displayVertices.map(v => gpsToGlobalPixel(v.lon, v.lat)).filter(p=>p);
            let pxArea = getPixelArea(pxList);

            const geo = hostGeodesicPolygonAreaAndPerimeter(displayVertices);
            if (!geo) {
                geoContainer.innerHTML = '<em style="color:#FF5B5B;">Calc Error</em>';
                return;
            }
            const geoArea = geo.area;
            const geoPerimeter = geo.perimeter;

            if (isCircle) {
                let b = geo.olPoly.getBounds();
                let geoRadius = (b.right - b.left) / 2;
                let pxRadiusText = "N/A";
                if(pxList.length > 0) {
                    let xs = pxList.map(p=>p.x); let w = Math.max(...xs) - Math.min(...xs);
                    pxRadiusText = `${Math.round(w/2)} px`;
                }

                geoContainer.innerHTML = `
                    <div>Radius: ${formatLength(geoRadius, isImp)} (${pxRadiusText})</div>
                    <div>Area: ${formatArea(geoArea, isImp)}</div>
                    <div>Px Area: ${Math.round(pxArea).toLocaleString()} px²</div>
                `;
            } else {
                geoContainer.innerHTML = `
                    <div>Area: ${formatArea(geoArea, isImp)}</div>
                    <div>Perimeter: ${formatLength(geoPerimeter, isImp)}</div>
                    <div>Px Area: ${Math.round(pxArea).toLocaleString()} px²</div>
                `;
            }
        } catch (e) {
            geoContainer.innerHTML = '<em style="color:#FF5B5B;">Calc Error</em>';
        }
    }

    // --- 7. WME I/O ---
    function ingestWmeSelection() {
        ingestVenuesIntoNewLayer(getSelectedAreaVenues());
    }

    /** Expand + bridge holes; keep links/sourceIndices aligned with surviving parts (Apply path). */
    function sanitizeAndBridgeLayerForApply(pc, polysIn, linksIn) {
        const ex = expandSanitizedPolygonsAndLinks(pc, polysIn, linksIn);
        const polys = [];
        const links = [];
        const sourceIndices = [];
        for (let i = 0; i < ex.polygons.length; i++) {
            let bridged;
            try {
                bridged = bridgePartHoles(pc, ex.polygons[i]);
            } catch (e) {
                bridged = null;
            }
            if (bridged && bridged[0] && bridged[0].length > 2) {
                polys.push(bridged);
                links.push(ex.links[i] || null);
                sourceIndices.push(ex.sourceIndices[i]);
            }
        }
        return { polys, links, sourceIndices };
    }

    /** Geodesic area preferred; fallback deg² shoelace on outer ring (same as pickLargestPartByArea). */
    function outerRingAreaMetricForApply(polyPart) {
        if (!polyPart || !polyPart[0] || polyPart[0].length < 3) return -1;
        try {
            const geo = hostGeodesicPolygonAreaAndPerimeter(polyPart[0].map(([lon, lat]) => ({ lon, lat })));
            if (geo && Number.isFinite(geo.area) && geo.area > 0) return geo.area;
        } catch (e) { /* ignore */ }
        return ringAreaAbsDeg(polyPart[0]);
    }

    function getVenueDisplayNameForSdkApply(venueId) {
        try {
            const v = wmeSDK.DataModel.Venues.getById({ venueId: normalizeVenueIdForSdk(venueId) });
            if (!v) return null;
            return v.name || v.attributes?.name || null;
        } catch (e) {
            return null;
        }
    }

    function addVenueFromApply(sdkPolygon, optionalName) {
        const base = { category: 'OTHER', geometry: sdkPolygon };
        if (optionalName) base.name = optionalName;
        try {
            const r = wmeSDK.DataModel.Venues.addVenue(base);
            if (r == null) return null;
            if (r.venueId != null) return normalizeVenueIdForSdk(r.venueId);
            if (typeof r === 'number' || typeof r === 'string') return normalizeVenueIdForSdk(r);
            if (r.id != null) return normalizeVenueIdForSdk(r.id);
        } catch (e1) {
            try {
                const r2 = wmeSDK.DataModel.Venues.addVenue({ category: 'OTHER', geometry: sdkPolygon });
                if (r2 && r2.venueId != null) return normalizeVenueIdForSdk(r2.venueId);
            } catch (e2) { /* ignore */ }
        }
        return null;
    }

    function injectToWaze() {
        endBrushPainting();
        if (appState.isDraftActive) commitDraft();
        if (!isGeometryLibraryReady()) {
            logToUI('Apply disabled: jsts is not available.', true);
            return;
        }
        if (!hasAnyUserLayerPolygons()) { logToUI('Nothing to inject.', true); return; }
        if (!wmeSDK || !wmeSDK.DataModel || !wmeSDK.DataModel.Venues) { logToUI('Injection Failed: SDK not ready.', true); return; }

        const pc = getGeometryEngine();
        if (!pc) {
            logToUI('Apply disabled: geometry engine unavailable.', true);
            return;
        }

        const missing = [];
        for (let li = 0; li < userLayers.length; li++) {
            const layer = userLayers[li];
            if (layer.isPlaces || layer.visible === false) continue;
            let polys = cloneMasterPolygons(layer.polygons);
            let links = cloneShapePlaceLinks(layer.links);
            if (!polys.length) continue;
            let sanitized;
            try {
                sanitized = sanitizeAndBridgeLayerForApply(pc, polys, links);
            } catch (e) {
                console.error('Apply: layer cleanup failed', layer.name, e);
                continue;
            }
            polys = sanitized.polys;
            links = sanitized.links;
            for (let i = 0; i < links.length; i++) {
                const link = normalizeShapeLink(links[i]);
                if (!link || link.venueId == null || !linkBlocksApplyValidation(link)) continue;
                const v = wmeSDK.DataModel.Venues.getById({ venueId: link.venueId });
                if (!v) missing.push({ layer: layer.name, i, id: link.venueId });
            }
        }
        if (missing.length) {
            logToUI('Apply blocked: active linked venue(s) not found: ' + missing.map(x => `${x.layer} part ${x.i + 1} → ${x.id}`).join(', '), true);
            return;
        }

        try {
            let updateCounter = 0;
            for (let li = 0; li < userLayers.length; li++) {
                const layer = userLayers[li];
                if (layer.isPlaces || layer.visible === false) continue;
                let polys = cloneMasterPolygons(layer.polygons);
                let links = cloneShapePlaceLinks(layer.links);
                if (!polys.length) continue;
                let sanitized;
                try {
                    sanitized = sanitizeAndBridgeLayerForApply(pc, polys, links);
                } catch (e) {
                    console.error('Apply: layer cleanup failed', layer.name, e);
                    continue;
                }
                polys = sanitized.polys;
                links = sanitized.links;
                const sourceIndices = sanitized.sourceIndices;
                if (!polys.length) continue;

                const splitGroups = new Map();
                for (let i = 0; i < links.length; i++) {
                    const lk = links[i];
                    if (lk && lk.splitGroupId) {
                        if (!splitGroups.has(lk.splitGroupId)) splitGroups.set(lk.splitGroupId, []);
                        splitGroups.get(lk.splitGroupId).push(i);
                    }
                }
                const handled = new Set();

                for (let i = 0; i < polys.length; i++) {
                    if (handled.has(i)) continue;
                    const linkRaw = links[i];
                    const link = normalizeShapeLink(linkRaw);
                    const sg = link && link.splitGroupId;
                    if (sg && splitGroups.has(sg)) {
                        const idxs = splitGroups.get(sg);
                        let bestIdx = idxs[0];
                        let bestA = -1;
                        for (let j = 0; j < idxs.length; j++) {
                            const ii = idxs[j];
                            const a = outerRingAreaMetricForApply(polys[ii]);
                            if (a > bestA) {
                                bestA = a;
                                bestIdx = ii;
                            }
                        }
                        const srcVenueId = normalizeVenueIdForSdk(links[bestIdx].venueId);
                        const winnerShapeIdx = sourceIndices[bestIdx];
                        breakCompetingVenueLinksForVenue(srcVenueId, layer, winnerShapeIdx);
                        wmeSDK.DataModel.Venues.updateVenue({
                            venueId: srcVenueId,
                            geometry: masterPolyPartToSdkPolygon(polys[bestIdx])
                        });
                        updateCounter++;

                        const baseName = getVenueDisplayNameForSdkApply(srcVenueId) || 'Place';
                        const losers = idxs.filter(ii => ii !== bestIdx).sort((a, b) =>
                            outerRingAreaMetricForApply(polys[b]) - outerRingAreaMetricForApply(polys[a]));
                        const usedShapeIdxForSplit = new Set([winnerShapeIdx]);
                        for (let ord = 0; ord < losers.length; ord++) {
                            const ii = losers[ord];
                            const suffix = ord + 2;
                            const cloneName = `${baseName} (${suffix})`;
                            const newId = addVenueFromApply(masterPolyPartToSdkPolygon(polys[ii]), cloneName);
                            if (newId != null) {
                                const sh = sourceIndices[ii];
                                if (sh >= 0 && !usedShapeIdxForSplit.has(sh) && sh < layer.links.length) {
                                    usedShapeIdxForSplit.add(sh);
                                    layer.links[sh] = normalizeShapeLink({ venueId: newId, active: true, broken: false });
                                } else {
                                    const partCopy = cloneMasterPolygons([polys[ii]])[0];
                                    if (partCopy) {
                                        layer.polygons.push(partCopy);
                                        layer.links.push(normalizeShapeLink({ venueId: newId, active: true, broken: false }));
                                    }
                                }
                            }
                            updateCounter++;
                        }
                        for (let j = 0; j < idxs.length; j++) handled.add(idxs[j]);
                        continue;
                    }

                    const sdkPolygon = masterPolyPartToSdkPolygon(polys[i]);
                    const sh = sourceIndices[i];
                    if (isActiveNonBrokenLink(link)) {
                        breakCompetingVenueLinksForVenue(link.venueId, layer, sh);
                        wmeSDK.DataModel.Venues.updateVenue({ venueId: link.venueId, geometry: sdkPolygon });
                    } else if (link && link.venueId != null) {
                        const baseName = getVenueDisplayNameForSdkApply(link.venueId);
                        const nm = link.broken === true && baseName ? `${baseName} (split)` : baseName;
                        const newId = addVenueFromApply(sdkPolygon, nm || undefined);
                        if (newId != null && sh >= 0 && sh < layer.links.length) {
                            layer.links[sh] = normalizeShapeLink({ venueId: newId, active: true, broken: false });
                        }
                    } else {
                        addVenueFromApply(sdkPolygon);
                    }
                    updateCounter++;
                }
                ensureShapeIdsForLayer(layer);
            }
            if (!updateCounter) {
                logToUI('Apply: no valid geometry after cleanup.', true);
                return;
            }
            rebalanceVenueLinkActives();
            syncActiveLayerAliases();
            scheduleDocumentSave();
            logToUI(`Injected ${updateCounter} venue(s).`);
            clearHistory();
            changeTool('pan');
        } catch (e) {
            let detail = 'Injection failed.';
            const m = e && e.message ? String(e.message) : '';
            if (e && e.name === 'InvalidStateError' || m.includes('Not allowed to update geometry')) {
                detail = 'WME blocked geometry update (venue locked, not selected for edit, or editing disabled). Select an editable venue and try Apply again.';
            }
            logToUI(detail, true);
            console.error(e);
        }
    }

    bootstrap();
})();