NYTimes Strands Drawing Overlay

Adds a togglable drawing canvas overlay to the NYTimes.com Strands game.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name       NYTimes Strands Drawing Overlay
// @namespace  http://mathemaniac.org/
// @version    1.1.0
// @description  Adds a togglable drawing canvas overlay to the NYTimes.com Strands game.
// @match      https://www.nytimes.com/games/strands
// @copyright  2025-2026, Sebastian Paaske Tørholm
// @grant      none
// @license    MIT
// ==/UserScript==

// Changelog:
// 1.1.0 - Moved to vector-based layers that can be deleted/hidden and so on.
// 1.0.0 - Initial release

(function () {
    'use strict';

    // --- Configuration ---
    const DEFAULT_BRUSH_SIZE = 30;
    const MIN_BRUSH_SIZE = 1;
    const MAX_BRUSH_SIZE = 60;
    const COLORS = ['#FF0000', '#0000FF', '#00FF00', '#FFFF00', '#000000']; // Red, Blue, Green, Yellow, Black
    const DEFAULT_COLOR = COLORS[0]; // Red
    const OVERLAY_OPACITY = 0.4; // 40%
    const CANVAS_BACKGROUND_COLOR = 'rgba(200, 200, 200, 0.3)'; // Slight gray tinge

    // --- Utility Functions ---
    const log = (...args) => console.log('[StrandsDrawingOverlay]', ...args);

    // --- State Management ---
    let isDrawing = false;
    let lastX = 0;
    let lastY = 0;
    let currentColor = DEFAULT_COLOR;
    let currentBrushSize = DEFAULT_BRUSH_SIZE;
    let isOverlayVisible = false;
    let dpr = window.devicePixelRatio || 1; // Store Device Pixel Ratio

    // --- Layers (vector) ---
    /** @type {{id:string,name:string,visible:boolean,createdAt:number,strokes:Array<{color:string,size:number,points:Array<{nx:number,ny:number}>,letters?:string[]}>}[]} */
    let layers = [];
    /** @type {null|{color:string,size:number,points:Array<{nx:number,ny:number}>,letters:string[]}} */
    let activeStroke = null;
    let selectedLayerIds = new Set();
    let letterGridCache = []; // [{letter, rect:{left,top,right,bottom}}...]

    // --- DOM Elements ---
    let gameBoardContainer = null;
    let canvasOverlay = null;
    let ctx = null;
    let controlsContainer = null;
    let toggleButton = null;
    let colorButtons = [];
    let cursorPreview = null;

    // --- Layers UI elements ---
    let layersPanel = null;
    let layersListEl = null;
    let mergeSelectedButton = null;

    // --- Storage ---
    const getGameDate = () => {
        const dateElement = document.getElementById('portal-game-date');
        return dateElement ? dateElement.textContent.trim() : new Date().toDateString();
    };

    const getStorageKey = (suffix) => `strands_drawing_${getGameDate()}_${suffix}`;

    const safeJsonParse = (s, fallback) => {
        try { return JSON.parse(s); } catch { return fallback; }
    };

    const saveCanvasState = () => {
        // Now saves vector layers (not a single raster image)
        try {
            const payload = {
                v: 1,
                layers: layers.map(l => ({
                    id: l.id,
                    name: l.name,
                    visible: !!l.visible,
                    createdAt: l.createdAt,
                    strokes: (l.strokes || []).map(st => ({
                        color: st.color,
                        size: st.size,
                        points: (st.points || []).map(p => ({ nx: p.nx, ny: p.ny })),
                        letters: Array.isArray(st.letters) ? st.letters : undefined
                    }))
                }))
            };
            localStorage.setItem(getStorageKey('layers'), JSON.stringify(payload));
            log('Layers state saved.');
        } catch (e) {
            console.error('Failed to save layers state:', e);
        }
    };

    const loadCanvasState = () => {
        // Now loads vector layers (not a single raster image)
        const raw = localStorage.getItem(getStorageKey('layers'));
        if (!raw) return;

        const parsed = safeJsonParse(raw, null);
        if (!parsed || !Array.isArray(parsed.layers)) return;

        layers = parsed.layers
            .filter(l => l && Array.isArray(l.strokes))
            .map(l => ({
                id: String(l.id || crypto.randomUUID()),
                name: String(l.name || 'Layer'),
                visible: l.visible !== false,
                createdAt: Number(l.createdAt || Date.now()),
                strokes: l.strokes.map(st => ({
                    color: String(st.color || DEFAULT_COLOR),
                    size: Number(st.size || DEFAULT_BRUSH_SIZE),
                    points: Array.isArray(st.points) ? st.points.map(p => ({
                        nx: Math.max(0, Math.min(1, Number(p.nx))),
                        ny: Math.max(0, Math.min(1, Number(p.ny)))
                    })) : [],
                    letters: Array.isArray(st.letters) ? st.letters.map(String) : undefined
                }))
            }));

        log(`Layers state loaded (${layers.length} layers).`);
        renderAllLayers();
        rebuildLayersUI();
    };

    const saveSettings = () => {
        localStorage.setItem(getStorageKey('color'), currentColor);
        localStorage.setItem(getStorageKey('brushSize'), currentBrushSize.toString());
        log('Settings saved.');
    };

    const loadSettings = () => {
        const storedColor = localStorage.getItem(getStorageKey('color'));
        const storedBrushSize = localStorage.getItem(getStorageKey('brushSize'));

        if (storedColor && COLORS.includes(storedColor)) {
            currentColor = storedColor;
        }
        if (storedBrushSize) {
            const size = parseInt(storedBrushSize, 10);
            if (!isNaN(size) && size >= MIN_BRUSH_SIZE && size <= MAX_BRUSH_SIZE) {
                currentBrushSize = size;
            }
        }
        log('Settings loaded.');
    };

    // --- Letter grid cache (for auto layer naming) ---
    const updateLetterGridCache = () => {
        if (!gameBoardContainer) return;
        const buttons = Array.from(gameBoardContainer.querySelectorAll("button[id^='button-']"));
        letterGridCache = buttons
            .map(btn => {
                const letter = (btn.textContent || '').trim();
                if (!letter) return null;
                const r = btn.getBoundingClientRect();
                return { letter, rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom } };
            })
            .filter(Boolean);
        log(`Letter grid cached: ${letterGridCache.length} cells.`);
    };

    const hitTestLetterAtClientPoint = (clientX, clientY) => {
        for (let i = 0; i < letterGridCache.length; i++) {
            const cell = letterGridCache[i];
            const r = cell.rect;
            if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) {
                return { letter: cell.letter, cellIndex: i };
            }
        }
        return null;
    };

    const makeUniqueLayerName = (base) => {
        const clean = String(base || '').trim() || 'Layer';
        const existing = new Set(layers.map(l => l.name));
        if (!existing.has(clean)) return clean;
        let i = 2;
        while (existing.has(`${clean} (${i})`)) i++;
        return `${clean} (${i})`;
    };

    // --- Rendering (composite visible layers onto single canvas) ---
    const clearCanvas = () => {
        if (!ctx || !canvasOverlay) return;
        ctx.clearRect(0, 0, canvasOverlay.width, canvasOverlay.height);
    };

    const drawStrokeToCtx = (stroke, containerRect) => {
        if (!ctx || !canvasOverlay) return;
        const pts = stroke.points || [];
        if (pts.length < 2) return;

        ctx.globalCompositeOperation = 'source-over';
        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';
        ctx.strokeStyle = stroke.color;
        ctx.lineWidth = (stroke.size || DEFAULT_BRUSH_SIZE) * dpr;
        ctx.globalAlpha = 1;

        ctx.beginPath();
        const first = pts[0];
        ctx.moveTo(first.nx * containerRect.width * dpr, first.ny * containerRect.height * dpr);
        for (let i = 1; i < pts.length; i++) {
            const p = pts[i];
            ctx.lineTo(p.nx * containerRect.width * dpr, p.ny * containerRect.height * dpr);
        }
        ctx.stroke();
    };

    const renderAllLayers = () => {
        if (!ctx || !canvasOverlay || !gameBoardContainer) return;
        const containerRect = gameBoardContainer.getBoundingClientRect();
        clearCanvas();
        for (const layer of layers) {
            if (!layer.visible) continue;
            for (const stroke of (layer.strokes || [])) drawStrokeToCtx(stroke, containerRect);
        }
    };

    // --- Canvas Logic ---
    const startDrawing = (e) => {
        isDrawing = true;

        const [cssX, cssY] = getCanvasCoordinates(e);
        const rect = canvasOverlay.getBoundingClientRect();
        const nx = rect.width ? (cssX / rect.width) : 0;
        const ny = rect.height ? (cssY / rect.height) : 0;

        activeStroke = {
            color: currentColor,
            size: currentBrushSize,
            points: [{ nx, ny }],
            letters: [],
            visitedCells: new Set()
        };

        // seed letter (if any)
        const hitResult = hitTestLetterAtClientPoint(
            e.type.includes('touch') ? e.touches[0].clientX : e.clientX,
            e.type.includes('touch') ? e.touches[0].clientY : e.clientY
        );
        if (hitResult) {
            activeStroke.letters.push(hitResult.letter);
            activeStroke.visitedCells.add(hitResult.cellIndex);
        }

        lastX = cssX * dpr;
        lastY = cssY * dpr;
    };

    const draw = (e) => {
        if (!isDrawing || !isOverlayVisible || !ctx || !canvasOverlay || !activeStroke) return;

        const [cssX, cssY] = getCanvasCoordinates(e);
        const rect = canvasOverlay.getBoundingClientRect();
        const nx = rect.width ? (cssX / rect.width) : 0;
        const ny = rect.height ? (cssY / rect.height) : 0;

        activeStroke.points.push({ nx, ny });

        // auto-name sampling: collect unique letters in order from unique cells
        const clientX = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
        const clientY = e.type.includes('touch') ? e.touches[0].clientY : e.clientY;
        const hitResult = hitTestLetterAtClientPoint(clientX, clientY);
        if (hitResult && !activeStroke.visitedCells.has(hitResult.cellIndex)) {
            activeStroke.letters.push(hitResult.letter);
            activeStroke.visitedCells.add(hitResult.cellIndex);
        }

        // live draw (only the incremental segment) for responsiveness
        ctx.globalCompositeOperation = 'source-over';
        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';
        ctx.strokeStyle = currentColor;
        ctx.lineWidth = currentBrushSize * dpr;
        ctx.globalAlpha = 1;

        const currentX = cssX * dpr;
        const currentY = cssY * dpr;

        ctx.beginPath();
        ctx.moveTo(lastX, lastY);
        ctx.lineTo(currentX, currentY);
        ctx.stroke();

        lastX = currentX;
        lastY = currentY;
    };

    const stopDrawing = () => {
        if (!isDrawing) return;
        isDrawing = false;

        const stroke = activeStroke;
        activeStroke = null;

        if (!stroke || !stroke.points || stroke.points.length < 2) return;

        const baseName = (stroke.letters && stroke.letters.length)
            ? stroke.letters.join('')
            : `Layer ${layers.length + 1}`;

        layers.push({
            id: crypto.randomUUID(),
            name: makeUniqueLayerName(baseName),
            visible: true,
            createdAt: Date.now(),
            strokes: [{ color: stroke.color, size: stroke.size, points: stroke.points, letters: stroke.letters }]
        });

        renderAllLayers();
        saveCanvasState();
        rebuildLayersUI();
    };

    // --- Coordinate Functions ---
    const getCanvasCoordinates = (e) => {
        const rect = canvasOverlay.getBoundingClientRect();
        let clientX, clientY;
        if (e.type.includes('touch')) {
            clientX = e.touches[0].clientX;
            clientY = e.touches[0].clientY;
        } else {
            clientX = e.clientX;
            clientY = e.clientY;
        }
        // Return coordinates relative to the canvas top-left in CSS pixels
        return [(clientX - rect.left), (clientY - rect.top)];
    };

    // --- UI: Layers panel ---
    const toggleLayerVisibility = (layerId) => {
        const layer = layers.find(l => l.id === layerId);
        if (!layer) return;
        layer.visible = !layer.visible;
        renderAllLayers();
        saveCanvasState();
        rebuildLayersUI();
    };

    const changeLayerColor = (layerId, newColor) => {
        const layer = layers.find(l => l.id === layerId);
        if (!layer) return;
        // Change color of all strokes in this layer
        for (const stroke of (layer.strokes || [])) {
            stroke.color = newColor;
        }
        renderAllLayers();
        saveCanvasState();
        rebuildLayersUI();
    };

    const deleteLayer = (layerId) => {
        layers = layers.filter(l => l.id !== layerId);
        selectedLayerIds.delete(layerId);
        renderAllLayers();
        saveCanvasState();
        rebuildLayersUI();
    };

    const mergeSelectedLayers = () => {
        const ids = Array.from(selectedLayerIds);
        if (ids.length < 2) return;

        const selected = layers.filter(l => selectedLayerIds.has(l.id));
        if (selected.length < 2) return;

        const mergedStrokes = selected.flatMap(l => l.strokes || []);
        const mergedNameBase = selected.map(l => l.name).join(' + ').slice(0, 60) || 'Merged';
        const mergedLayer = {
            id: crypto.randomUUID(),
            name: makeUniqueLayerName(`Merged: ${mergedNameBase}`),
            visible: true,
            createdAt: Date.now(),
            strokes: mergedStrokes
        };

        // keep relative order: insert merged where first selected was
        const firstIdx = layers.findIndex(l => l.id === ids[0]);
        layers = layers.filter(l => !selectedLayerIds.has(l.id));
        layers.splice(Math.max(0, firstIdx), 0, mergedLayer);

        selectedLayerIds.clear();
        renderAllLayers();
        saveCanvasState();
        rebuildLayersUI();
    };

    const rebuildLayersUI = () => {
        if (!layersListEl || !mergeSelectedButton) return;

        layersListEl.innerHTML = '';

        if (!layers.length) {
            const empty = document.createElement('div');
            empty.textContent = 'No layers yet';
            empty.style.fontSize = '12px';
            empty.style.opacity = '0.7';
            empty.style.padding = '4px';
            layersListEl.appendChild(empty);
        } else {
            // newest first
            const ordered = [...layers].sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
            for (const layer of ordered) {
                const row = document.createElement('div');
                row.style.display = 'grid';
                row.style.gridTemplateColumns = '18px 20px 1fr 28px 28px';
                row.style.alignItems = 'center';
                row.style.gap = '6px';
                row.style.padding = '4px 2px';
                row.style.borderRadius = '3px';
                row.style.transition = 'background-color 0.15s';
                row.addEventListener('mouseenter', () => {
                    row.style.backgroundColor = '#f5f5f5';
                });
                row.addEventListener('mouseleave', () => {
                    row.style.backgroundColor = 'transparent';
                });

                const sel = document.createElement('input');
                sel.type = 'checkbox';
                sel.checked = selectedLayerIds.has(layer.id);
                sel.title = 'Select for merge';
                sel.style.cursor = 'pointer';
                sel.addEventListener('change', (ev) => {
                    if (sel.checked) selectedLayerIds.add(layer.id);
                    else selectedLayerIds.delete(layer.id);
                    mergeSelectedButton.disabled = selectedLayerIds.size < 2;
                });
                sel.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                });

                const swatch = document.createElement('div');
                const uColors = [...new Set((layer.strokes || []).map(s => s.color))];
                swatch.style.width = '18px';
                swatch.style.height = '18px';
                swatch.style.borderRadius = '3px';
                swatch.style.border = '1px solid #ccc';
                swatch.style.boxSizing = 'border-box';
                swatch.style.cursor = 'pointer';
                swatch.title = 'Click to change layer color';
                if (uColors.length === 1) {
                    swatch.style.backgroundColor = uColors[0];
                } else if (uColors.length > 1) {
                    swatch.style.background = `linear-gradient(135deg, ${uColors[0]}, ${uColors[1]})`;
                }
                
                // Color picker dropdown
                swatch.addEventListener('click', (ev) => {
                    ev.preventDefault();
                    ev.stopPropagation();
                    
                    // Create color picker popup
                    const popup = document.createElement('div');
                    popup.style.position = 'absolute';
                    popup.style.backgroundColor = 'white';
                    popup.style.border = '1px solid #ccc';
                    popup.style.borderRadius = '4px';
                    popup.style.padding = '6px';
                    popup.style.display = 'flex';
                    popup.style.gap = '4px';
                    popup.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
                    popup.style.zIndex = '10000';
                    
                    const rect = swatch.getBoundingClientRect();
                    popup.style.left = `${rect.right + 5}px`;
                    popup.style.top = `${rect.top}px`;
                    
                    COLORS.forEach(color => {
                        const colorBtn = document.createElement('div');
                        colorBtn.style.width = '20px';
                        colorBtn.style.height = '20px';
                        colorBtn.style.backgroundColor = color;
                        colorBtn.style.border = '1px solid #999';
                        colorBtn.style.borderRadius = '3px';
                        colorBtn.style.cursor = 'pointer';
                        colorBtn.title = color;
                        
                        colorBtn.addEventListener('click', (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            changeLayerColor(layer.id, color);
                            document.body.removeChild(popup);
                        });
                        
                        popup.appendChild(colorBtn);
                    });
                    
                    document.body.appendChild(popup);
                    
                    const closePopup = (e) => {
                        if (!popup.contains(e.target)) {
                            document.body.removeChild(popup);
                            document.removeEventListener('click', closePopup);
                        }
                    };
                    setTimeout(() => document.addEventListener('click', closePopup), 0);
                });

                const name = document.createElement('div');
                name.textContent = layer.name;
                name.title = layer.name;
                name.style.fontSize = '12px';
                name.style.whiteSpace = 'nowrap';
                name.style.overflow = 'hidden';
                name.style.textOverflow = 'ellipsis';
                name.style.opacity = layer.visible ? '1' : '0.5';

                const vis = document.createElement('button');
                vis.type = 'button';
                vis.innerHTML = layer.visible ? '👁️' : '👁️‍🗨️';
                vis.title = layer.visible ? 'Hide layer' : 'Show layer';
                vis.style.fontSize = '16px';
                vis.style.padding = '2px';
                vis.style.border = 'none';
                vis.style.background = 'transparent';
                vis.style.cursor = 'pointer';
                vis.style.opacity = layer.visible ? '1' : '0.5';
                vis.addEventListener('click', (ev) => {
                    ev.preventDefault(); 
                    ev.stopPropagation();
                    toggleLayerVisibility(layer.id);
                }, true);

                const del = document.createElement('button');
                del.type = 'button';
                del.textContent = '🗑️';
                del.title = 'Delete layer';
                del.style.fontSize = '14px';
                del.style.padding = '2px';
                del.style.border = 'none';
                del.style.background = 'transparent';
                del.style.cursor = 'pointer';
                del.addEventListener('click', (ev) => {
                    ev.preventDefault(); 
                    ev.stopPropagation();
                    deleteLayer(layer.id);
                }, true);

                row.appendChild(sel);
                row.appendChild(swatch);
                row.appendChild(name);
                row.appendChild(vis);
                row.appendChild(del);
                layersListEl.appendChild(row);
            }
        }

        mergeSelectedButton.disabled = selectedLayerIds.size < 2;
    };

    // --- UI Creation ---
    const createCanvasOverlay = () => {
        if (canvasOverlay) return;

        canvasOverlay = document.createElement('canvas');
        canvasOverlay.id = 'strands-drawing-overlay';
        canvasOverlay.style.position = 'absolute';
        canvasOverlay.style.top = '0';
        canvasOverlay.style.left = '0';
        canvasOverlay.style.width = '100%';
        canvasOverlay.style.height = '100%';
        canvasOverlay.style.pointerEvents = 'none';
        canvasOverlay.style.opacity = OVERLAY_OPACITY.toString();
        canvasOverlay.style.zIndex = '1000';
        canvasOverlay.style.backgroundColor = 'transparent';
        canvasOverlay.style.display = 'none';
        canvasOverlay.style.boxSizing = 'border-box';
        canvasOverlay.style.cursor = 'none';

        ctx = canvasOverlay.getContext('2d');
        if (!ctx) {
            console.error('Could not get 2D context for the drawing canvas.');
            canvasOverlay = null;
            return;
        }

        // Event listeners for drawing
        const handleEvent = (handler) => (e) => {
            if (isOverlayVisible) {
                handler(e);
                // Aggressively prevent default and stop propagation for drawing events
                e.preventDefault();
                e.stopPropagation();
            }
        };

        canvasOverlay.addEventListener('mousedown', handleEvent(startDrawing));
        canvasOverlay.addEventListener('mousemove', handleEvent(draw));
        canvasOverlay.addEventListener('mouseup', handleEvent(stopDrawing));
        canvasOverlay.addEventListener('mouseout', handleEvent(stopDrawing));
        canvasOverlay.addEventListener('touchstart', handleEvent(startDrawing), { passive: false });
        canvasOverlay.addEventListener('touchmove', handleEvent(draw), { passive: false });
        canvasOverlay.addEventListener('touchend', handleEvent(stopDrawing), { passive: false });
        canvasOverlay.addEventListener('touchcancel', handleEvent(stopDrawing), { passive: false });

        gameBoardContainer.style.position = 'relative';
        gameBoardContainer.appendChild(canvasOverlay);
        log('Canvas overlay created.');
    };

    const createCursorPreview = () => {
        if (cursorPreview) return;

        cursorPreview = document.createElement('div');
        cursorPreview.id = 'strands-drawing-cursor-preview';
        cursorPreview.style.position = 'absolute';
        cursorPreview.style.pointerEvents = 'none';
        cursorPreview.style.zIndex = '1001'; // Above canvas, below controls
        cursorPreview.style.borderRadius = '50%';
        cursorPreview.style.transform = 'translate(-50%, -50%)';
        cursorPreview.style.display = 'none';
        cursorPreview.style.boxSizing = 'border-box';

        updateCursorPreviewStyle();
        gameBoardContainer.appendChild(cursorPreview);

        const handleMove = (e) => {
            if (isOverlayVisible) {
                const [x, y] = getCanvasCoordinates(e);
                const rect = gameBoardContainer.getBoundingClientRect();
                const isInBounds = x >= 0 && x <= rect.width && y >= 0 && y <= rect.height;

                if (isInBounds) {
                    cursorPreview.style.display = 'block';
                    cursorPreview.style.left = `${x}px`;
                    cursorPreview.style.top = `${y}px`;
                } else {
                    cursorPreview.style.display = 'none';
                }
            }
        };
        
        canvasOverlay.addEventListener('mousemove', handleMove);
        canvasOverlay.addEventListener('touchmove', (e) => { handleMove(e); e.preventDefault(); }, { passive: false });
        canvasOverlay.addEventListener('mouseleave', () => {
             if (isOverlayVisible) cursorPreview.style.display = 'none';
        });
        canvasOverlay.addEventListener('touchcancel', () => {
             if (isOverlayVisible) cursorPreview.style.display = 'none';
        });
        canvasOverlay.addEventListener('touchend', () => {
             if (isOverlayVisible) cursorPreview.style.display = 'none';
        });
    };

    const updateCursorPreviewStyle = () => {
        if (!cursorPreview) return;
        cursorPreview.style.width = `${currentBrushSize}px`;
        cursorPreview.style.height = `${currentBrushSize}px`;
        cursorPreview.style.border = `2px solid ${currentColor}`;
        cursorPreview.style.backgroundColor = `${currentColor}`;
        cursorPreview.style.opacity = '0.7';
    };

    const createControls = () => {
        if (controlsContainer) return;

        controlsContainer = document.createElement('div');
        controlsContainer.id = 'strands-drawing-controls';
        controlsContainer.style.position = 'absolute';
        controlsContainer.style.top = '10px';
        controlsContainer.style.right = '-240px';
        controlsContainer.style.zIndex = '1002';
        controlsContainer.style.display = 'none';
        controlsContainer.style.fontFamily = 'Arial, sans-serif';
        controlsContainer.style.fontSize = '14px';
        controlsContainer.style.minWidth = '220px';
        controlsContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.97)';
        controlsContainer.style.border = '1px solid #bbb';
        controlsContainer.style.borderRadius = '6px';
        controlsContainer.style.padding = '12px';
        controlsContainer.style.boxShadow = '0 3px 12px rgba(0,0,0,0.25)';

        // Close button in top-right corner
        const closeButton = document.createElement('button');
        closeButton.type = 'button';
        closeButton.textContent = '✕';
        closeButton.title = 'Hide overlay';
        closeButton.style.position = 'absolute';
        closeButton.style.top = '4px';
        closeButton.style.right = '4px';
        closeButton.style.padding = '4px 7px';
        closeButton.style.fontSize = '14px';
        closeButton.style.cursor = 'pointer';
        closeButton.style.border = 'none';
        closeButton.style.borderRadius = '3px';
        closeButton.style.backgroundColor = 'transparent';
        closeButton.style.color = '#666';
        closeButton.addEventListener('mouseenter', () => {
            closeButton.style.backgroundColor = '#f0f0f0';
            closeButton.style.color = '#000';
        });
        closeButton.addEventListener('mouseleave', () => {
            closeButton.style.backgroundColor = 'transparent';
            closeButton.style.color = '#666';
        });
        closeButton.addEventListener('click', (event) => {
             event.preventDefault();
             event.stopPropagation();
             event.stopImmediatePropagation();
             log("Hiding overlay...");
             toggleOverlay();
        }, true);
        closeButton.addEventListener('mousedown', (event) => {
             event.stopPropagation();
        }, true);
        closeButton.addEventListener('mouseup', (event) => {
             event.stopPropagation();
        }, true);

        // Title
        const title = document.createElement('div');
        title.textContent = 'Drawing Tools';
        title.style.fontSize = '14px';
        title.style.fontWeight = '700';
        title.style.marginBottom = '12px';
        title.style.color = '#333';
        title.style.paddingRight = '20px';

        // Color Section
        const colorSection = document.createElement('div');
        colorSection.style.marginBottom = '12px';

        const colorLabel = document.createElement('div');
        colorLabel.textContent = 'Color';
        colorLabel.style.fontSize = '12px';
        colorLabel.style.fontWeight = '600';
        colorLabel.style.marginBottom = '6px';
        colorLabel.style.color = '#555';

        const colorPickerContainer = document.createElement('div');
        colorPickerContainer.id = 'drawing-color-picker';
        colorPickerContainer.style.display = 'flex';
        colorPickerContainer.style.gap = '6px';
        colorPickerContainer.style.flexWrap = 'wrap';

        colorButtons = [];

        COLORS.forEach(color => {
            const colorButton = document.createElement('button');
            colorButton.type = 'button';
            colorButton.className = 'drawing-color-button';
            colorButton.setAttribute('data-color', color);
            colorButton.style.width = '32px';
            colorButton.style.height = '32px';
            colorButton.style.border = '2px solid #ddd';
            colorButton.style.borderRadius = '4px';
            colorButton.style.backgroundColor = color;
            colorButton.style.cursor = 'pointer';
            colorButton.style.padding = '0';
            colorButton.style.boxSizing = 'border-box';
            colorButton.style.transition = 'all 0.15s';

            if (color === currentColor) {
                colorButton.style.border = '3px solid #333';
                colorButton.style.transform = 'scale(1.1)';
            }

            colorButton.addEventListener('mouseenter', () => {
                if (color !== currentColor) {
                    colorButton.style.transform = 'scale(1.05)';
                }
            });
            colorButton.addEventListener('mouseleave', () => {
                if (color !== currentColor) {
                    colorButton.style.transform = 'scale(1)';
                }
            });

            colorButton.addEventListener('click', (event) => {
                event.preventDefault();
                event.stopPropagation();
                currentColor = color;
                colorButtons.forEach(btn => {
                    if (btn.getAttribute('data-color') === color) {
                        btn.style.border = '3px solid #333';
                        btn.style.transform = 'scale(1.1)';
                    } else {
                        btn.style.border = '2px solid #ddd';
                        btn.style.transform = 'scale(1)';
                    }
                });
                saveSettings();
                updateCursorPreviewStyle();
            });

            colorButtons.push(colorButton);
            colorPickerContainer.appendChild(colorButton);
        });

        colorSection.appendChild(colorLabel);
        colorSection.appendChild(colorPickerContainer);

        // Brush Size Section
        const sizeSection = document.createElement('div');
        sizeSection.style.marginBottom = '12px';

        const sizeLabel = document.createElement('div');
        sizeLabel.textContent = 'Brush Size';
        sizeLabel.style.fontSize = '12px';
        sizeLabel.style.fontWeight = '600';
        sizeLabel.style.marginBottom = '6px';
        sizeLabel.style.color = '#555';

        const sizeSliderContainer = document.createElement('div');
        sizeSliderContainer.style.display = 'flex';
        sizeSliderContainer.style.alignItems = 'center';
        sizeSliderContainer.style.gap = '8px';

        const sizeSlider = document.createElement('input');
        sizeSlider.type = 'range';
        sizeSlider.id = 'drawing-brush-size';
        sizeSlider.min = MIN_BRUSH_SIZE;
        sizeSlider.max = MAX_BRUSH_SIZE;
        sizeSlider.value = currentBrushSize;
        sizeSlider.style.flex = '1';
        sizeSlider.style.cursor = 'pointer';

        const sizeValue = document.createElement('span');
        sizeValue.id = 'drawing-brush-size-value';
        sizeValue.textContent = currentBrushSize;
        sizeValue.style.fontSize = '12px';
        sizeValue.style.fontWeight = '600';
        sizeValue.style.minWidth = '25px';
        sizeValue.style.textAlign = 'right';
        sizeValue.style.color = '#333';

        sizeSlider.addEventListener('input', () => {
            currentBrushSize = parseInt(sizeSlider.value, 10);
            sizeValue.textContent = currentBrushSize;
            saveSettings();
            updateCursorPreviewStyle();
        });

        sizeSliderContainer.appendChild(sizeSlider);
        sizeSliderContainer.appendChild(sizeValue);

        sizeSection.appendChild(sizeLabel);
        sizeSection.appendChild(sizeSliderContainer);

        // Clear Button
        const clearButton = document.createElement('button');
        clearButton.type = 'button';
        clearButton.textContent = '🗑️ Clear All Layers';
        clearButton.title = 'Clear all layers';
        clearButton.style.width = '100%';
        clearButton.style.marginBottom = '12px';
        clearButton.style.padding = '8px';
        clearButton.style.fontSize = '13px';
        clearButton.style.fontWeight = '600';
        clearButton.style.cursor = 'pointer';
        clearButton.style.border = '1px solid #ddd';
        clearButton.style.borderRadius = '4px';
        clearButton.style.backgroundColor = '#fff';
        clearButton.style.color = '#d32f2f';
        clearButton.style.transition = 'all 0.15s';
        clearButton.addEventListener('mouseenter', () => {
            clearButton.style.backgroundColor = '#ffebee';
            clearButton.style.borderColor = '#d32f2f';
        });
        clearButton.addEventListener('mouseleave', () => {
            clearButton.style.backgroundColor = '#fff';
            clearButton.style.borderColor = '#ddd';
        });
        clearButton.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            if (event.target !== clearButton) return;
            if (!confirm('Clear all layers? This cannot be undone.')) return;
            log('Clearing all layers...');
            layers = [];
            selectedLayerIds.clear();
            renderAllLayers();
            saveCanvasState();
            rebuildLayersUI();
        }, true);

        // --- Layers panel ---
        layersPanel = document.createElement('div');
        layersPanel.style.paddingTop = '12px';
        layersPanel.style.borderTop = '1px solid #e0e0e0';

        const layersTitle = document.createElement('div');
        layersTitle.textContent = 'Layers';
        layersTitle.style.fontSize = '12px';
        layersTitle.style.fontWeight = '600';
        layersTitle.style.marginBottom = '8px';
        layersTitle.style.color = '#555';

        layersListEl = document.createElement('div');
        layersListEl.id = 'drawing-layers-list';
        layersListEl.style.display = 'block';
        layersListEl.style.maxHeight = '180px';
        layersListEl.style.overflow = 'auto';
        layersListEl.style.border = '1px solid #e0e0e0';
        layersListEl.style.borderRadius = '4px';
        layersListEl.style.padding = '4px';
        layersListEl.style.background = '#fafafa';
        layersListEl.style.marginBottom = '8px';

        mergeSelectedButton = document.createElement('button');
        mergeSelectedButton.type = 'button';
        mergeSelectedButton.textContent = '⚙️ Merge Selected';
        mergeSelectedButton.title = 'Merge selected layers into one';
        mergeSelectedButton.style.width = '100%';
        mergeSelectedButton.style.padding = '6px 8px';
        mergeSelectedButton.style.fontSize = '12px';
        mergeSelectedButton.style.fontWeight = '600';
        mergeSelectedButton.style.cursor = 'pointer';
        mergeSelectedButton.style.border = '1px solid #ddd';
        mergeSelectedButton.style.borderRadius = '4px';
        mergeSelectedButton.style.backgroundColor = '#fff';
        mergeSelectedButton.style.transition = 'all 0.15s';
        mergeSelectedButton.disabled = true;
        mergeSelectedButton.addEventListener('mouseenter', () => {
            if (!mergeSelectedButton.disabled) {
                mergeSelectedButton.style.backgroundColor = '#f0f0f0';
            }
        });
        mergeSelectedButton.addEventListener('mouseleave', () => {
            mergeSelectedButton.style.backgroundColor = '#fff';
        });
        mergeSelectedButton.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            mergeSelectedLayers();
        }, true);

        layersPanel.appendChild(layersTitle);
        layersPanel.appendChild(layersListEl);
        layersPanel.appendChild(mergeSelectedButton);

        controlsContainer.appendChild(closeButton);
        controlsContainer.appendChild(title);
        controlsContainer.appendChild(colorSection);
        controlsContainer.appendChild(sizeSection);
        controlsContainer.appendChild(clearButton);
        controlsContainer.appendChild(layersPanel);

        gameBoardContainer.appendChild(controlsContainer);
        log('Controls created.');
        rebuildLayersUI();
    };

    // --- Canvas Size Update ---
    const updateCanvasSize = () => {
        if (!canvasOverlay || !gameBoardContainer) return;

        const containerRect = gameBoardContainer.getBoundingClientRect();
        dpr = window.devicePixelRatio || 1;

        canvasOverlay.width = Math.floor(containerRect.width * dpr);
        canvasOverlay.height = Math.floor(containerRect.height * dpr);

        canvasOverlay.style.width = `${containerRect.width}px`;
        canvasOverlay.style.height = `${containerRect.height}px`;

        ctx.setTransform(1, 0, 0, 1, 0, 0);

        updateLetterGridCache();
        renderAllLayers();

        log(`Canvas updated: Display(${containerRect.width}x${containerRect.height}), Internal(${canvasOverlay.width}x${canvasOverlay.height}), DPR(${dpr})`);
    };

    // --- Main Logic ---
    const toggleOverlay = () => {
        if (!canvasOverlay || !controlsContainer || !cursorPreview) return;

        isOverlayVisible = !isOverlayVisible;

        if (isOverlayVisible) {
            canvasOverlay.style.pointerEvents = 'auto';
            canvasOverlay.style.backgroundColor = CANVAS_BACKGROUND_COLOR;
            canvasOverlay.style.display = 'block';
            controlsContainer.style.display = 'block';
            toggleButton.setAttribute('aria-label', 'Hide drawing notes overlay');
            cursorPreview.style.display = 'none'; // Start hidden, shown on move
        } else {
            canvasOverlay.style.pointerEvents = 'none';
            canvasOverlay.style.backgroundColor = 'transparent';
            canvasOverlay.style.display = 'none';
            controlsContainer.style.display = 'none';
            toggleButton.setAttribute('aria-label', 'Show drawing notes overlay');
            cursorPreview.style.display = 'none';
        }
        log(`Overlay ${isOverlayVisible ? 'shown' : 'hidden'}.`);
    };

    const createToggleButton = () => {
        if (toggleButton) return;

        const toolbar = document.querySelector('.pz-module.pz-flex-row.pz-game-toolbar-content');
        if (!toolbar) {
            log('Toolbar not found, retrying...');
            setTimeout(createToggleButton, 1000);
            return;
        }

        toggleButton = document.createElement('button');
        toggleButton.type = 'button';
        toggleButton.className = 'ToolbarItem-module_toolbar_item__xrBr_ ToolbarItem-module_toolbar_itemDesktop__jFTZJ';
        toggleButton.id = 'drawing-overlay-toggle';
        toggleButton.setAttribute('aria-label', 'Show drawing notes overlay');

        const iconWrapper = document.createElement('div');
        iconWrapper.className = 'Icon-module_iconWrapper__ZfKPm';

        const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svgIcon.setAttribute('aria-hidden', 'true');
        svgIcon.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        svgIcon.setAttribute('height', '24');
        svgIcon.setAttribute('viewBox', '0 0 24 24');
        svgIcon.setAttribute('width', '24');
        svgIcon.setAttribute('class', 'game-icon');
        svgIcon.setAttribute('data-testid', 'icon-drawing-overlay');
        svgIcon.innerHTML = `
            <path fill="var(--text)" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 5.63l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83a.9959.9959 0 0 0 0-1.41z"/>
            <path fill="none" d="M0 0h24v24H0z"/>
        `;

        iconWrapper.appendChild(svgIcon);
        toggleButton.appendChild(iconWrapper);

        // --- Robust click handler for toggle button ---
        toggleButton.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            log("Toggle button clicked.");
            toggleOverlay();
        }, true); // Use capture phase

        const helpButton = document.getElementById('help-button');
        if (helpButton && helpButton.parentNode) {
            helpButton.parentNode.parentNode.insertBefore(toggleButton, helpButton.parentNode);
        } else {
            const toolbarSection = toolbar.querySelector('section');
            if (toolbarSection) {
                toolbarSection.appendChild(toggleButton);
            } else {
                toolbar.appendChild(toggleButton);
            }
        }
        log('Toggle button created.');
    };

    const init = () => {
        log('Initializing...');

        gameBoardContainer = document.querySelector("form[data-testid='strands-board'] > div:first-child > div:first-child");
        if (!gameBoardContainer) {
            log('Game board container not found, retrying...');
            setTimeout(init, 1000);
            return;
        }

        loadSettings();
        createCanvasOverlay();
        createControls();
        createToggleButton();
        createCursorPreview();

        if (canvasOverlay) {
            setTimeout(() => {
                updateCanvasSize();
                loadCanvasState(); // now loads layers + renders + UI
                // ...existing colorButtons highlight code...
                updateCursorPreviewStyle();

                window.addEventListener('resize', () => {
                    clearTimeout(window.strandsResizeTimeout);
                    window.strandsResizeTimeout = setTimeout(updateCanvasSize, 100);
                });
                const resizeObserver = new ResizeObserver(() => {
                    clearTimeout(window.strandsResizeObserverTimeout);
                    window.strandsResizeObserverTimeout = setTimeout(updateCanvasSize, 100);
                });
                resizeObserver.observe(gameBoardContainer);
            }, 100);
        }
    };

    // --- Keyboard Shortcut ---
    document.addEventListener('keydown', (e) => {
        if (e.key === 't' && e.ctrlKey === false && e.altKey === false && e.shiftKey === false) {
             if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
                 e.preventDefault();
                 log("T key pressed, toggling overlay.");
                 toggleOverlay();
             }
        }
    });

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();