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();
})();