Multi Tools Code


<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Multi Tool Hub</title>

    <style>

        :root {

            --bg-color: #1E1E2F;

            --text-color: #EAEAEA;

            --header-bg: #2B2D42;

            --accent-color: #FFD700;

            --card-bg: #3A3D5B;

            --hover-btn-color: #E6C200;

            --card-hover-bg: #FFD700;

            --card-hover-text: #1E1E2F;

            --box-shadow-color: rgba(255, 215, 0, 0.2);

            --input-bg: #2B2D42;

            --input-border: #4A4C6B;

            --error-color: #FF6B6B;

            --success-color: #76C7C0;

        }


        * {

            box-sizing: border-box;

            margin: 0;

            padding: 0;

        }


        body {

            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

            background-color: var(--bg-color);

            color: var(--text-color);

            line-height: 1.6;

            transition: background-color 0.3s, color 0.3s;

        }


        header {

            background-color: var(--header-bg);

            color: var(--text-color);

            padding: 1.5rem 0;

            text-align: center;

            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);

            margin-bottom: 2rem;

        }


        header h1 {

            font-size: 2.5rem;

            font-weight: 600;

        }


        main {

            padding: 0 1rem;

            max-width: 1200px;

            margin: 0 auto;

        }


        .tool-grid {

            display: grid;

            gap: 1.5rem;

            grid-template-columns: repeat(3, 1fr);

        }


        .tool-card {

            background-color: var(--card-bg);

            padding: 1.5rem;

            border-radius: 8px;

            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);

            transition: transform 0.3s ease, background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;

            display: flex;

            flex-direction: column;

            justify-content: space-between;

        }


        .tool-card:hover {

            transform: translateY(-5px);

            background-color: var(--card-hover-bg);

            color: var(--card-hover-text);

            box-shadow: 0 8px 25px var(--box-shadow-color);

        }


        .tool-card h2 {

            font-size: 1.5rem;

            margin-bottom: 0.75rem;

            color: var(--accent-color); /* Title color different from body text */

        }


        .tool-card:hover h2 {

            color: var(--card-hover-text);

        }


        .tool-card p {

            font-size: 0.95rem;

            margin-bottom: 1.5rem;

            flex-grow: 1;

        }


        .tool-card button,

        .modal-content button:not(.modal-close-btn),

        .tool-ui button {

            background-color: var(--accent-color);

            color: var(--bg-color);

            border: none;

            padding: 0.75rem 1.25rem;

            font-size: 1rem;

            font-weight: bold;

            border-radius: 5px;

            cursor: pointer;

            transition: background-color 0.3s ease, box-shadow 0.3s ease;

            align-self: flex-start;

        }


        .tool-card button:hover,

        .modal-content button:not(.modal-close-btn):hover,

        .tool-ui button:hover {

            background-color: var(--hover-btn-color);

            box-shadow: 0 4px 10px var(--box-shadow-color);

        }


        /* Modal Styles */

        .modal {

            display: none;

            position: fixed;

            z-index: 1000;

            left: 0;

            top: 0;

            width: 100%;

            height: 100%;

            overflow: auto;

            background-color: rgba(0, 0, 0, 0.7);

            animation: fadeIn 0.3s ease-out;

        }


        @keyframes fadeIn {

            from { opacity: 0; }

            to { opacity: 1; }

        }


        .modal-content {

            background-color: var(--header-bg);

            margin: 5% auto;

            padding: 2rem;

            border-radius: 8px;

            width: 90%;

            max-width: 700px;

            box-shadow: 0 5px 20px rgba(0,0,0,0.5);

            position: relative;

            animation: slideIn 0.3s ease-out;

            max-height: 90vh; /* Limit height and enable scroll */

            display: flex;

            flex-direction: column;

        }



        @keyframes slideIn {

            from { transform: translateY(-30px); opacity: 0; }

            to { transform: translateY(0); opacity: 1; }

        }


        .modal-header {

            display: flex;

            justify-content: space-between;

            align-items: center;

            border-bottom: 1px solid var(--card-bg);

            padding-bottom: 1rem;

            margin-bottom: 1rem;

        }


        .modal-header h2 {

            font-size: 1.8rem;

            color: var(--accent-color);

        }


        .modal-close-btn {

            color: var(--text-color);

            font-size: 2rem;

            font-weight: bold;

            background: none;

            border: none;

            cursor: pointer;

            padding: 0.5rem;

            line-height: 1;

        }


        .modal-close-btn:hover {

            color: var(--accent-color);

        }


        .modal-tool-content {

            margin-bottom: 1.5rem;

            flex-grow: 1; /* Allows content to take available space */

            overflow-y: auto; /* Scroll for content if it overflows */

        }


        .modal-tool-output {

            background-color: var(--bg-color);

            padding: 1rem;

            border-radius: 5px;

            margin-top: 1rem;

            font-family: 'Courier New', Courier, monospace;

            white-space: pre-wrap;

            word-wrap: break-word;

            max-height: 200px;

            overflow-y: auto;

            border-left: 3px solid transparent;

        }


        .modal-tool-output.error {

            color: var(--error-color);

            border-left-color: var(--error-color);

        }


        .modal-tool-output.success {

            color: var(--success-color);

            border-left-color: var(--success-color);

        }


        /* Tool-specific UI elements */

        .tool-ui {

            display: flex;

            flex-direction: column;

            gap: 1rem;

        }


        .tool-ui label {

            font-weight: bold;

            margin-bottom: 0.25rem;

            display: block;

        }


        .tool-ui input[type="text"],

        .tool-ui input[type="number"],

        .tool-ui input[type="date"],

        .tool-ui input[type="file"],

        .tool-ui textarea,

        .tool-ui select {

            width: 100%;

            padding: 0.75rem;

            background-color: var(--input-bg);

            color: var(--text-color);

            border: 1px solid var(--input-border);

            border-radius: 5px;

            font-size: 1rem;

        }


        .tool-ui input[type="file"] {

            padding: 0.5rem; /* Specific padding for file input */

        }



        .tool-ui textarea {

            min-height: 100px;

            resize: vertical;

        }


        .tool-ui .options-group {

            display: flex;

            flex-wrap: wrap;

            gap: 0.75rem;

            align-items: center;

        }

        .tool-ui .options-group label {

            font-weight: normal;

            margin-bottom: 0;

            display: flex;

            align-items: center;

        }

        .tool-ui .options-group input[type="checkbox"] {

            margin-right: 0.5rem;

            width: auto;

            accent-color: var(--accent-color);

        }

        .tool-ui input[type="range"] {

            width: 100%;

            accent-color: var(--accent-color);

        }



        .tool-ui .output-area {

            background-color: var(--bg-color);

            padding: 0.75rem;

            border-radius: 5px;

            min-height: 40px;

            border: 1px solid var(--input-border);

            word-wrap: break-word;

        }


        .tool-ui img, .tool-ui canvas, .tool-ui video, .tool-ui audio {

            max-width: 100%;

            border-radius: 5px;

            margin-top: 0.5rem;

        }


        /* QR Code Canvas (if used) */

        #qrCodeCanvas {

            border: 1px solid var(--accent-color);

            display: block;

            margin-top: 1rem;

            background-color: white; /* QR codes are typically on white */

        }


        /* Image cropper specific */

        #imageCropperCanvasContainer {

            position: relative;

            max-width: 100%;

            max-height: 400px; /* Limit height */

            overflow: hidden; /* Important for canvas sizing */

            border: 1px solid var(--accent-color);

            cursor: crosshair;

        }

        #imageCropperCanvas, #imageCropperPreviewCanvas {

            display: block; /* remove extra space below canvas */

            max-width: 100%;

            max-height: 400px;

        }


        /* Processing Indicator */

        .processing-indicator {

            position: fixed;

            top: 20px;

            left: 50%;

            transform: translateX(-50%);

            background-color: var(--accent-color);

            color: var(--bg-color);

            padding: 10px 20px;

            border-radius: 5px;

            box-shadow: 0 2px 10px rgba(0,0,0,0.3);

            z-index: 2000;

            font-weight: bold;

        }



        /* Responsive Design */

        @media (max-width: 992px) { /* Tablet */

            .tool-grid {

                grid-template-columns: repeat(2, 1fr);

            }

            header h1 {

                font-size: 2rem;

            }

            .modal-content {

                width: 85%;

                margin: 10% auto;

            }

        }


        @media (max-width: 600px) { /* Mobile */

            .tool-grid {

                grid-template-columns: 1fr;

            }

            header h1 {

                font-size: 1.8rem;

            }

            .modal-content {

                width: 95%;

                margin: 15% auto 5% auto; /* More top margin, less bottom */

                padding: 1.5rem;

                max-height: 85vh;

            }

            .modal-header h2 {

                font-size: 1.5rem;

            }

            .tool-card button, .modal-content button:not(.modal-close-btn), .tool-ui button {

                width: 100%; /* Full width buttons on mobile */

                padding: 0.9rem;

            }

        }


        /* For fade-in animation on scroll (optional with IntersectionObserver) */

        .tool-card.fade-in {

            opacity: 0;

            transform: translateY(20px);

            transition: opacity 0.5s ease-out, transform 0.5s ease-out;

        }


        .tool-card.visible {

            opacity: 1;

            transform: translateY(0);

        }

    </style>

</head>

<body>

    <header>

        <h1>Multi Tool Hub</h1>

    </header>


    <main>

        <div id="tool-grid" class="tool-grid">

            <!-- Tool cards will be injected here by JavaScript -->

        </div>

    </main>


    <div id="modal" class="modal">

        <div class="modal-content">

            <div class="modal-header">

                <h2 id="modal-title">Tool Title</h2>

                <button id="modal-close-btn" class="modal-close-btn">&times;</button>

            </div>

            <div id="modal-tool-content" class="modal-tool-content">

                <!-- Tool-specific UI will be injected here -->

            </div>

            <div id="modal-tool-output" class="modal-tool-output">

                <!-- Tool-specific output/results -->

            </div>

        </div>

    </div>


    <div id="processing-indicator" class="processing-indicator" style="display: none;">

        Processing...

    </div>


    <script>

    document.addEventListener('DOMContentLoaded', () => {

        const toolGrid = document.getElementById('tool-grid');

        const modal = document.getElementById('modal');

        const modalTitle = document.getElementById('modal-title');

        const modalToolContent = document.getElementById('modal-tool-content');

        const modalToolOutput = document.getElementById('modal-tool-output');

        const modalCloseBtn = document.getElementById('modal-close-btn');

        const processingIndicator = document.getElementById('processing-indicator');


        let currentToolCleanup = null; // Function to clean up resources for the current tool

        let activeAudioContext = null; // Global audio context for tools that need it


        function getAudioContext() {

            if (!activeAudioContext || activeAudioContext.state === 'closed') {

                activeAudioContext = new (window.AudioContext || window.webkitAudioContext)();

            }

            return activeAudioContext;

        }


        // --- Utility Functions ---

        function showProcessing(message = "Processing...") {

            processingIndicator.textContent = message;

            processingIndicator.style.display = 'block';

        }


        function hideProcessing() {

            processingIndicator.style.display = 'none';

        }


        function showOutput(message, type = 'info') { // type can be 'info', 'success', 'error'

            modalToolOutput.innerHTML = ''; // Clear previous

            const p = document.createElement('p');

            p.textContent = message;

            modalToolOutput.appendChild(p);

            modalToolOutput.className = 'modal-tool-output'; // Reset classes

            if (type === 'error') {

                modalToolOutput.classList.add('error');

            } else if (type === 'success') {

                modalToolOutput.classList.add('success');

            }

            modalToolOutput.style.display = 'block';

        }

        

        function clearOutput() {

            modalToolOutput.innerHTML = '';

            modalToolOutput.style.display = 'none';

        }


        function createDownloadLink(blob, filename, linkText = 'Download File') {

            const url = URL.createObjectURL(blob);

            const a = document.createElement('a');

            a.href = url;

            a.download = filename;

            a.textContent = linkText;

            a.className = 'tool-download-link'; 

            a.style.display = 'block'; // Make it block for better spacing

            a.style.marginTop = '10px';

            a.style.padding = '8px 12px';

            a.style.backgroundColor = 'var(--accent-color)';

            a.style.color = 'var(--bg-color)';

            a.style.textDecoration = 'none';

            a.style.borderRadius = '5px';

            a.style.fontWeight = 'bold';

            a.target = '_blank'; // Good practice for downloads

            

            // Check if modalToolOutput already has content, append to it

            if(modalToolOutput.style.display !== 'block' || modalToolOutput.innerHTML === ''){

                 modalToolOutput.style.display = 'block'; // Ensure it's visible

                 modalToolOutput.innerHTML = ''; // Clear any previous non-download message

            }

            

            const container = document.createElement('div');

            container.appendChild(a);

            modalToolOutput.appendChild(container);


            // Consider revoking URL on modal close or after some time

            // For now, it relies on browser's default blob URL lifetime or manual cleanup.

        }


        // --- Tool Definitions ---

        const tools = [

            { id: 'imageConverter', title: 'Image Converter', description: 'Convert between JPG, PNG, and WEBP formats using canvas.', init: initImageConverter, ui: createImageConverterUI },

            { id: 'imageCompressor', title: 'Image Compressor', description: 'Compress image file size using canvas and quality settings.', init: initImageCompressor, ui: createImageCompressorUI },

            { id: 'imageCropper', title: 'Image Cropper', description: 'Upload and crop image with preview and export.', init: initImageCropper, ui: createImageCropperUI },

            { id: 'videoConverter', title: 'Video Converter', description: 'Re-encode video to WebM using MediaRecorder (limited browser capabilities).', init: initVideoConverter, ui: createVideoConverterUI },

            { id: 'audioConverter', title: 'Audio to WAV Converter', description: 'Convert various audio formats (MP3, etc.) to WAV using Web Audio API.', init: initAudioConverter, ui: createAudioConverterUI },

            { id: 'audioTrimmer', title: 'Audio Trimmer', description: 'Upload, trim audio based on start/end time, and export trimmed clip as WAV.', init: initAudioTrimmer, ui: createAudioTrimmerUI },

            { id: 'ageCalculator', title: 'Age Calculator', description: 'Input date of birth ? output age in years, months, and days.', init: initAgeCalculator, ui: createAgeCalculatorUI },

            { id: 'emiCalculator', title: 'EMI Calculator', description: 'Input loan amount, interest rate, and duration ? show monthly EMI and total interest.', init: initEMICalculator, ui: createEMICalculatorUI },

            { id: 'sipCalculator', title: 'SIP Calculator', description: 'Input monthly investment, interest rate, duration ? output future value.', init: initSIPCalculator, ui: createSIPCalculatorUI },

            { id: 'qrCodeGenerator', title: 'QR Code Generator', description: 'Enter text or URL ? generate downloadable QR image (using canvas).', init: initQRCodeGenerator, ui: createQRCodeGeneratorUI },

            { id: 'passwordGenerator', title: 'Password Generator', description: 'Generate secure password with length, symbols, numbers options.', init: initPasswordGenerator, ui: createPasswordGeneratorUI },

            { id: 'wordCounter', title: 'Word Counter', description: 'Live count of words, characters, spaces, and reading time.', init: initWordCounter, ui: createWordCounterUI },

            { id: 'base64EncoderDecoder', title: 'Base64 Encoder/Decoder', description: 'Convert plain text to base64 and vice versa.', init: initBase64EncoderDecoder, ui: createBase64EncoderDecoderUI },

            { id: 'colorPicker', title: 'Color Picker Tool', description: 'Pick a color and display HEX, RGB, and HSL values.', init: initColorPicker, ui: createColorPickerUI },

            { id: 'textToSpeech', title: 'Text to Speech', description: 'Enter text and listen to it using SpeechSynthesis API.', init: initTextToSpeech, ui: createTextToSpeechUI },

            { id: 'speechToText', title: 'Speech to Text', description: 'Use microphone to convert voice into text using Web Speech API.', init: initSpeechToText, ui: createSpeechToTextUI },

            { id: 'jsonFormatter', title: 'JSON Formatter', description: 'Paste JSON ? auto-format and validate with error handling.', init: initJSONFormatter, ui: createJSONFormatterUI },

            { id: 'unitConverter', title: 'Unit Converter', description: 'Convert values between units (length, weight, temperature, etc.).', init: initUnitConverter, ui: createUnitConverterUI },

            { id: 'bmiCalculator', title: 'BMI Calculator', description: 'Input weight and height ? show BMI category and value.', init: initBMICalculator, ui: createBMICalculatorUI },

            { id: 'timerStopwatch', title: 'Timer / Stopwatch', description: 'Simple timer and stopwatch with start, stop, reset functionality.', init: initTimerStopwatch, ui: createTimerStopwatchUI },

        ];


        // --- Populate Tool Grid ---

        tools.forEach(tool => {

            const card = document.createElement('div');

            card.className = 'tool-card fade-in'; // Add fade-in class for IntersectionObserver

            card.innerHTML = `

                <h2>${tool.title}</h2>

                <p>${tool.description}</p>

                <button data-toolid="${tool.id}">Open Tool</button>

            `;

            card.querySelector('button').addEventListener('click', () => openModal(tool));

            toolGrid.appendChild(card);

        });


        // --- Modal Handling ---

        function openModal(tool) {

            if (currentToolCleanup) {

                currentToolCleanup(); 

                currentToolCleanup = null;

            }


            modalTitle.textContent = tool.title;

            modalToolContent.innerHTML = tool.ui(); // Create UI elements

            clearOutput();

            tool.init(); // Initialize tool-specific JS after UI is in DOM

            modal.style.display = 'block';

            document.body.style.overflow = 'hidden'; 


            if (typeof tool.cleanup === 'function') { // Check if cleanup is defined by the tool

                currentToolCleanup = tool.cleanup;

            }

        }


        function closeModal() {

            if (currentToolCleanup) {

                currentToolCleanup();

                currentToolCleanup = null;

            }

            modal.style.display = 'none';

            document.body.style.overflow = 'auto';

            modalToolContent.innerHTML = ''; 

            clearOutput();

            // Optionally close global audio context if no longer needed by any tool

            // if (activeAudioContext && activeAudioContext.state !== 'closed') {

            //     activeAudioContext.close().then(() => activeAudioContext = null);

            // }

        }


        modalCloseBtn.addEventListener('click', closeModal);

        modal.addEventListener('click', (event) => {

            if (event.target === modal) { 

                closeModal();

            }

        });

        document.addEventListener('keydown', (event) => {

            if (event.key === 'Escape' && modal.style.display === 'block') {

                closeModal();

            }

        });


        // --- Intersection Observer for fade-in animation ---

        if ('IntersectionObserver' in window) {

            const cards = document.querySelectorAll('.tool-card.fade-in');

            const cardObserver = new IntersectionObserver((entries) => {

                entries.forEach(entry => {

                    if (entry.isIntersecting) {

                        entry.target.classList.add('visible');

                        cardObserver.unobserve(entry.target); 

                    }

                });

            }, { threshold: 0.1 });


            cards.forEach(card => {

                cardObserver.observe(card);

            });

        } else { // Fallback for older browsers

            document.querySelectorAll('.tool-card.fade-in').forEach(card => {

                card.classList.add('visible');

            });

        }


        // --- Tool Implementations ---


        // 1. Image Converter

        function createImageConverterUI() {

            return `

                <div class="tool-ui">

                    <label for="imgConvFile">Upload Image:</label>

                    <input type="file" id="imgConvFile" accept="image/jpeg,image/png,image/webp,image/gif">

                    <label for="imgConvFormat">Convert to:</label>

                    <select id="imgConvFormat">

                        <option value="image/png">PNG</option>

                        <option value="image/jpeg">JPEG</option>

                        <option value="image/webp">WEBP</option>

                    </select>

                    <button id="imgConvButton">Convert</button>

                    <canvas id="imgConvCanvas" style="display:none;"></canvas>

                    <img id="imgConvPreview" src="#" alt="Preview" style="max-width:100%; margin-top:10px; display:none;">

                </div>

            `;

        }

        function initImageConverter() {

            const fileInput = document.getElementById('imgConvFile');

            const formatSelect = document.getElementById('imgConvFormat');

            const convertBtn = document.getElementById('imgConvButton');

            const canvas = document.getElementById('imgConvCanvas');

            const ctx = canvas.getContext('2d');

            const previewImg = document.getElementById('imgConvPreview');

            let originalFileName = '';

            let objectURL = null;


            fileInput.addEventListener('change', (e) => {

                const file = e.target.files[0];

                if (file) {

                    originalFileName = file.name.split('.')[0];

                    if (objectURL) URL.revokeObjectURL(objectURL); // Revoke previous

                    objectURL = URL.createObjectURL(file);

                    previewImg.src = objectURL;

                    previewImg.style.display = 'block';

                    clearOutput();

                }

            });


            convertBtn.addEventListener('click', () => {

                if (!fileInput.files[0] && !previewImg.src.startsWith('blob:')) { // check if a file is loaded

                    showOutput('Please upload an image file.', 'error');

                    return;

                }

                showProcessing();

                

                const img = new Image();

                img.onload = () => {

                    canvas.width = img.naturalWidth; // Use naturalWidth for original dimensions

                    canvas.height = img.naturalHeight;

                    ctx.drawImage(img, 0, 0);

                    const format = formatSelect.value;

                    let quality = (format === 'image/jpeg' || format === 'image/webp') ? 0.92 : undefined; 

                    

                    try {

                        const dataUrl = canvas.toDataURL(format, quality);

                        const extension = format.split('/')[1];

                        fetch(dataUrl)

                            .then(res => res.blob())

                            .then(blob => {

                                createDownloadLink(blob, `${originalFileName}_converted.${extension}`, `Download ${extension.toUpperCase()}`);

                                showOutput(`Image converted successfully to ${extension.toUpperCase()}.`, 'success');

                                // Update preview with converted image for visual confirmation

                                if (previewImg.src.startsWith('blob:')) URL.revokeObjectURL(previewImg.src); // Revoke old blob

                                previewImg.src = dataUrl; 

                                objectURL = null; // The new src is a dataURL, not a blob URL

                            })

                            .catch(err => showOutput(`Error creating blob: ${err.message}`, 'error'))

                            .finally(hideProcessing);

                    } catch (error) {

                        showOutput(`Conversion error: ${error.message}. Selected format might not be fully supported by your browser.`, 'error');

                        hideProcessing();

                    }

                };

                img.onerror = () => {

                    showOutput('Error loading image. Please try a different file or format.', 'error');

                    hideProcessing();

                };

                // Ensure img.src is set. If previewImg.src is already a dataURL (from previous conversion), use it.

                // Otherwise, use the blob URL.

                img.src = previewImg.src; 

            });

            

            // Assign cleanup function to the tool object if needed by openModal

            tools.find(t => t.id === 'imageConverter').cleanup = () => {

                if (objectURL) {

                    URL.revokeObjectURL(objectURL);

                    objectURL = null;

                }

                 // If previewImg holds a dataURL, no specific cleanup needed for it here.

                 // Canvas is part of the modal content and will be removed.

            };

        }


        // 2. Image Compressor

        function createImageCompressorUI() {

            return `

                <div class="tool-ui">

                    <label for="imgCompFile">Upload Image (JPG, PNG, WEBP):</label>

                    <input type="file" id="imgCompFile" accept="image/jpeg,image/png,image/webp">

                    <label for="imgCompQuality">Quality (0.1 - 1.0 for JPG/WEBP):</label>

                    <input type="range" id="imgCompQuality" min="0.1" max="1" step="0.05" value="0.7">

                    <span id="imgCompQualityValue">0.7</span>

                    <button id="imgCompButton">Compress</button>

                    <canvas id="imgCompCanvas" style="display:none;"></canvas>

                    <img id="imgCompPreview" src="#" alt="Preview" style="max-width:100%; margin-top:10px; display:none;">

                    <div id="imgCompSizeInfo" class="output-area" style="margin-top:10px;"></div>

                </div>

            `;

        }

        function initImageCompressor() {

            const fileInput = document.getElementById('imgCompFile');

            const qualitySlider = document.getElementById('imgCompQuality');

            const qualityValueDisplay = document.getElementById('imgCompQualityValue');

            const compressBtn = document.getElementById('imgCompButton');

            const canvas = document.getElementById('imgCompCanvas');

            const ctx = canvas.getContext('2d');

            const previewImg = document.getElementById('imgCompPreview');

            const sizeInfo = document.getElementById('imgCompSizeInfo');

            let originalFile = null;

            let objectURL = null;


            fileInput.addEventListener('change', (e) => {

                originalFile = e.target.files[0];

                if (originalFile) {

                    if (objectURL) URL.revokeObjectURL(objectURL);

                    objectURL = URL.createObjectURL(originalFile);

                    previewImg.src = objectURL;

                    previewImg.style.display = 'block';

                    clearOutput();

                    sizeInfo.textContent = `Original size: ${(originalFile.size / 1024).toFixed(2)} KB`;

                }

            });


            qualitySlider.addEventListener('input', () => {

                qualityValueDisplay.textContent = qualitySlider.value;

            });


            compressBtn.addEventListener('click', () => {

                if (!originalFile && !previewImg.src.startsWith('blob:')) {

                    showOutput('Please upload an image file.', 'error');

                    return;

                }

                showProcessing();


                const img = new Image();

                img.onload = () => {

                    canvas.width = img.naturalWidth;

                    canvas.height = img.naturalHeight;

                    ctx.drawImage(img, 0, 0);

                    

                    let outputFormat = originalFile ? originalFile.type : 'image/jpeg'; // Fallback if originalFile somehow lost

                    if (outputFormat !== 'image/jpeg' && outputFormat !== 'image/webp') {

                        outputFormat = 'image/jpeg'; 

                        showOutput('Note: For significant compression of PNG, output is JPEG or WEBP. Canvas PNG compression is lossless.', 'info');

                    }


                    const quality = parseFloat(qualitySlider.value);

                    

                    try {

                        const dataUrl = canvas.toDataURL(outputFormat, quality);

                        const extension = outputFormat.split('/')[1];


                        fetch(dataUrl)

                            .then(res => res.blob())

                            .then(blob => {

                                createDownloadLink(blob, `${(originalFile?.name || 'image').split('.')[0]}_compressed.${extension}`);

                                const originalSizeKB = originalFile ? (originalFile.size / 1024).toFixed(2) : 'N/A';

                                const compressedSizeKB = (blob.size / 1024).toFixed(2);

                                let reductionText = '';

                                if (originalFile) {

                                    const reductionPercent = ((1 - blob.size / originalFile.size) * 100).toFixed(1);

                                    reductionText = ` (${reductionPercent}% reduction)`;

                                }

                                sizeInfo.innerHTML = `Original: ${originalSizeKB} KB<br>Compressed: ${compressedSizeKB} KB${reductionText}`;

                                showOutput(`Image compressed successfully as ${extension.toUpperCase()}.`, 'success');

                                if (previewImg.src.startsWith('blob:')) URL.revokeObjectURL(previewImg.src);

                                previewImg.src = dataUrl; 

                                objectURL = null; 

                            })

                            .catch(err => showOutput(`Error creating blob: ${err.message}`, 'error'))

                            .finally(hideProcessing);

                    } catch (error) {

                         showOutput(`Compression error: ${error.message}.`, 'error');

                         hideProcessing();

                    }

                };

                img.onerror = () => {

                    showOutput('Error loading image.', 'error');

                    hideProcessing();

                };

                img.src = previewImg.src;

            });

            

            tools.find(t => t.id === 'imageCompressor').cleanup = () => {

                if (objectURL) {

                    URL.revokeObjectURL(objectURL);

                    objectURL = null;

                }

            };

        }


        // 3. Image Cropper

        function createImageCropperUI() {

            return `

                <div class="tool-ui">

                    <label for="imgCropFile">Upload Image:</label>

                    <input type="file" id="imgCropFile" accept="image/*">

                    <p style="font-size:0.8em; margin-bottom:5px;">Click and drag on the image to select crop area.</p>

                    <div id="imageCropperCanvasContainer" style="touch-action: none;"> <!-- touch-action for mobile drag -->

                        <canvas id="imageCropperCanvas"></canvas>

                    </div>

                    <button id="imgCropButton" style="margin-top:10px;">Crop & Download</button>

                    <h4>Preview:</h4>

                    <canvas id="imageCropperPreviewCanvas" style="border: 1px solid #555; max-width:200px; max-height:150px;"></canvas>

                </div>

            `;

        }

        function initImageCropper() {

            const fileInput = document.getElementById('imgCropFile');

            const canvas = document.getElementById('imageCropperCanvas');

            const ctx = canvas.getContext('2d');

            const previewCanvas = document.getElementById('imageCropperPreviewCanvas');

            const pCtx = previewCanvas.getContext('2d');

            const cropButton = document.getElementById('imgCropButton');

            const container = document.getElementById('imageCropperCanvasContainer');


            let img = new Image();

            let cropRect = { x: 0, y: 0, w: 0, h: 0 };

            let isDragging = false;

            let startX, startY;

            let originalFileName = 'cropped_image';

            let originalFileType = 'image/png';

            let objectURL = null;

            let displayScale = 1; // Scale of displayed image vs natural image


            fileInput.addEventListener('change', (e) => {

                const file = e.target.files[0];

                if (file) {

                    originalFileName = file.name.split('.')[0];

                    originalFileType = file.type;

                    if (objectURL) URL.revokeObjectURL(objectURL);

                    objectURL = URL.createObjectURL(file);

                    

                    img.onload = () => {

                        const containerWidth = container.clientWidth;

                        const maxContainerHeight = 400; 


                        displayScale = Math.min(containerWidth / img.naturalWidth, maxContainerHeight / img.naturalHeight, 1);

                        canvas.width = img.naturalWidth * displayScale;

                        canvas.height = img.naturalHeight * displayScale;

                        

                        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

                        cropRect = { x: 0, y: 0, w: 0, h: 0 }; 

                        clearPreview();

                        URL.revokeObjectURL(objectURL); // Revoke after loading into image object

                        objectURL = null; 

                    };

                    img.src = objectURL;

                    clearOutput();

                }

            });


            function drawSelection() {

                ctx.clearRect(0,0,canvas.width, canvas.height);

                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

                if (cropRect.w !== 0 && cropRect.h !== 0) { // Allow zero width/height for initial drag

                    ctx.strokeStyle = 'rgba(255, 215, 0, 0.8)'; 

                    ctx.lineWidth = 2;

                    ctx.strokeRect(cropRect.x, cropRect.y, cropRect.w, cropRect.h);

                    ctx.fillStyle = 'rgba(255, 215, 0, 0.2)';

                    ctx.fillRect(cropRect.x, cropRect.y, cropRect.w, cropRect.h);

                }

            }

            

            function updatePreview() {

                if (Math.abs(cropRect.w) > 0 && Math.abs(cropRect.h) > 0) {

                    const actualCropRect = normalizeRect(cropRect);

                    

                    const sourceX = actualCropRect.x / displayScale;

                    const sourceY = actualCropRect.y / displayScale;

                    const sourceWidth = actualCropRect.w / displayScale;

                    const sourceHeight = actualCropRect.h / displayScale;

                    

                    const maxPreviewSize = 150;

                    let previewScale = Math.min(maxPreviewSize / sourceWidth, maxPreviewSize / sourceHeight, 1);

                    previewCanvas.width = sourceWidth * previewScale;

                    previewCanvas.height = sourceHeight * previewScale;


                    pCtx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, previewCanvas.width, previewCanvas.height);

                } else {

                    clearPreview();

                }

            }

            

            function normalizeRect(rect) {

                // Handles negative width/height from dragging right-to-left or bottom-to-top

                let x = rect.x, y = rect.y, w = rect.w, h = rect.h;

                if (w < 0) { x = rect.x + w; w = -w; }

                if (h < 0) { y = rect.y + h; h = -h; }

                return { x, y, w, h };

            }


            function clearPreview() {

                pCtx.clearRect(0, 0, previewCanvas.width, previewCanvas.height);

                previewCanvas.width = 150; 

                previewCanvas.height = 100;

                pCtx.fillStyle = '#2B2D42';

                pCtx.fillRect(0,0,previewCanvas.width, previewCanvas.height);

                pCtx.fillStyle = '#EAEAEA';

                pCtx.textAlign = 'center';

                pCtx.textBaseline = 'middle';

                pCtx.fillText('Preview', previewCanvas.width/2, previewCanvas.height/2);

            }

            clearPreview(); 


            function getMousePos(canvasEl, evt) {

                const rect = canvasEl.getBoundingClientRect();

                const clientX = evt.clientX || (evt.touches && evt.touches[0].clientX);

                const clientY = evt.clientY || (evt.touches && evt.touches[0].clientY);

                return {

                    x: clientX - rect.left,

                    y: clientY - rect.top

                };

            }

            

            const onDragStart = (e) => {

                if (!img.src) return;

                e.preventDefault(); // Prevent page scroll on touch

                isDragging = true;

                const pos = getMousePos(canvas, e);

                startX = pos.x;

                startY = pos.y;

                cropRect = { x: startX, y: startY, w: 0, h: 0 };

            };


            const onDragMove = (e) => {

                if (!isDragging || !img.src) return;

                e.preventDefault();

                const pos = getMousePos(canvas, e);

                const currentX = pos.x;

                const currentY = pos.y;


                // Clamp coordinates to canvas boundaries

                cropRect.w = Math.max(-startX, Math.min(currentX - startX, canvas.width - startX));

                cropRect.h = Math.max(-startY, Math.min(currentY - startY, canvas.height - startY));

                

                drawSelection();

            };


            const onDragEnd = () => {

                if (!isDragging || !img.src) return;

                isDragging = false;

                

                // Normalize and clamp the final rectangle

                cropRect = normalizeRect(cropRect);

                cropRect.x = Math.max(0, cropRect.x);

                cropRect.y = Math.max(0, cropRect.y);

                cropRect.w = Math.min(cropRect.w, canvas.width - cropRect.x);

                cropRect.h = Math.min(cropRect.h, canvas.height - cropRect.y);


                drawSelection(); 

                updatePreview();

            };


            canvas.addEventListener('mousedown', onDragStart);

            canvas.addEventListener('mousemove', onDragMove);

            canvas.addEventListener('mouseup', onDragEnd);

            canvas.addEventListener('mouseleave', onDragEnd); // End drag if mouse leaves


            canvas.addEventListener('touchstart', onDragStart, { passive: false });

            canvas.addEventListener('touchmove', onDragMove, { passive: false });

            canvas.addEventListener('touchend', onDragEnd);

            canvas.addEventListener('touchcancel', onDragEnd);



            cropButton.addEventListener('click', () => {

                const finalCropRect = normalizeRect(cropRect);

                if (!img.src || finalCropRect.w === 0 || finalCropRect.h === 0) {

                    showOutput('Please upload an image and select a crop area.', 'error');

                    return;

                }

                showProcessing();


                const tempCanvas = document.createElement('canvas');

                const tCtx = tempCanvas.getContext('2d');

                

                const sourceX = finalCropRect.x / displayScale;

                const sourceY = finalCropRect.y / displayScale;

                const sourceWidth = finalCropRect.w / displayScale;

                const sourceHeight = finalCropRect.h / displayScale;


                tempCanvas.width = sourceWidth;

                tempCanvas.height = sourceHeight;


                tCtx.drawImage(img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight);


                try {

                    const dataUrl = tempCanvas.toDataURL(originalFileType, originalFileType.includes('jpeg') ? 0.92 : undefined);

                    const extension = (originalFileType.split('/')[1] || 'png').replace('jpeg', 'jpg');

                    fetch(dataUrl)

                        .then(res => res.blob())

                        .then(blob => {

                            createDownloadLink(blob, `${originalFileName}_cropped.${extension}`);

                            showOutput('Image cropped successfully.', 'success');

                        })

                        .catch(err => showOutput(`Error creating blob: ${err.message}`, 'error'))

                        .finally(hideProcessing);

                } catch (error) {

                    showOutput(`Cropping error: ${error.message}`, 'error');

                    hideProcessing();

                }

            });

            

            tools.find(t => t.id === 'imageCropper').cleanup = () => {

                if (objectURL) { // Should be null if img.onload completed

                    URL.revokeObjectURL(objectURL);

                    objectURL = null;

                }

                img.src = ''; // Release image resources

            };

        }


        // 4. Video Converter (Re-encoder to WebM)

        function createVideoConverterUI() {

            return `

                <div class="tool-ui">

                    <label for="vidConvFile">Upload Video (MP4, etc.):</label>

                    <input type="file" id="vidConvFile" accept="video/*">

                    <p style="font-size:0.8em; margin-bottom:5px;">This tool attempts to re-encode the video to WebM (VP8/Opus or VP9/Opus). Conversion can be slow for large files. Ensure video plays before converting.</p>

                    <button id="vidConvButton" disabled>Convert to WebM</button>

                    <video id="vidConvPreview" controls style="max-width:100%; margin-top:10px; display:none; background-color:black;"></video>

                </div>

            `;

        }

        function initVideoConverter() {

            const fileInput = document.getElementById('vidConvFile');

            const convertBtn = document.getElementById('vidConvButton');

            const videoPreview = document.getElementById('vidConvPreview');

            let mediaRecorder;

            let recordedChunks = [];

            let sourceVideoURL = null;

            let streamToStop = null;


            fileInput.addEventListener('change', (e) => {

                const file = e.target.files[0];

                if (file) {

                    if (sourceVideoURL) URL.revokeObjectURL(sourceVideoURL);

                    sourceVideoURL = URL.createObjectURL(file);

                    videoPreview.src = sourceVideoURL;

                    videoPreview.style.display = 'block';

                    convertBtn.disabled = false;

                    clearOutput();

                }

            });


            convertBtn.addEventListener('click', async () => {

                if (!videoPreview.src || !videoPreview.captureStream) {

                     showOutput('Please upload a video or your browser does not support captureStream/MediaRecorder.', 'error');

                    return;

                }

                if (!MediaRecorder) {

                    showOutput('MediaRecorder API not supported by your browser.', 'error');

                    return;

                }

                

                showProcessing('Preparing conversion...');

                convertBtn.disabled = true;

                recordedChunks = [];


                const mimeTypes = [

                    'video/webm;codecs=vp9,opus',

                    'video/webm;codecs=vp8,opus',

                    'video/webm;codecs=vp9', // some browsers might accept this

                    'video/webm;codecs=vp8',

                    'video/webm'

                ];

                let supportedMimeType = mimeTypes.find(type => MediaRecorder.isTypeSupported(type));


                if (!supportedMimeType) {

                    showOutput('No supported WebM MIME type found for MediaRecorder.', 'error');

                    hideProcessing();

                    convertBtn.disabled = false;

                    return;

                }

                showOutput(`Using MIME type: ${supportedMimeType}`, 'info');


                try {

                    streamToStop = videoPreview.captureStream();

                    mediaRecorder = new MediaRecorder(streamToStop, { mimeType: supportedMimeType });


                    mediaRecorder.ondataavailable = (event) => {

                        if (event.data.size > 0) {

                            recordedChunks.push(event.data);

                        }

                    };


                    mediaRecorder.onstop = () => {

                        const blob = new Blob(recordedChunks, { type: supportedMimeType });

                        const originalFileName = (fileInput.files[0]?.name || 'video').split('.')[0];

                        createDownloadLink(blob, `${originalFileName}_converted.webm`);

                        showOutput(`Video re-encoded to WebM. Size: ${(blob.size / (1024*1024)).toFixed(2)} MB.`, 'success');

                        hideProcessing();

                        convertBtn.disabled = false; // Re-enable after processing

                        

                        if (streamToStop) streamToStop.getTracks().forEach(track => track.stop());

                        streamToStop = null;

                        videoPreview.pause(); 

                        videoPreview.currentTime = 0; 

                    };

                    

                    mediaRecorder.onerror = (event) => {

                        let errorName = event.error ? event.error.name : 'Unknown error';

                        showOutput(`MediaRecorder error: ${errorName}`, 'error');

                        hideProcessing();

                        convertBtn.disabled = false;

                        if (streamToStop) streamToStop.getTracks().forEach(track => track.stop());

                        streamToStop = null;

                    };


                    videoPreview.onended = () => { // When original video finishes playing

                        if (mediaRecorder && mediaRecorder.state === "recording") {

                            mediaRecorder.stop();

                        }

                    };

                    

                    videoPreview.onloadedmetadata = () => { 

                        videoPreview.play().then(() => {

                            if(mediaRecorder.state !== "recording"){ mediaRecorder.start(); }

                            showProcessing('Re-encoding... Please wait. This may take time.');

                        }).catch(e => {

                            showOutput(`Error playing video for capture: ${e.message}`, 'error');

                            hideProcessing();

                            convertBtn.disabled = false;

                            if (streamToStop) streamToStop.getTracks().forEach(track => track.stop());

                            streamToStop = null;

                        });

                    };

                    

                    if (videoPreview.readyState >= videoPreview.HAVE_METADATA) { // HAVE_METADATA = 2

                         videoPreview.play().then(() => {

                            if(mediaRecorder.state !== "recording"){ mediaRecorder.start(); }

                            showProcessing('Re-encoding... Please wait. This may take time.');

                        }).catch(e => {

                            showOutput(`Error playing video for capture: ${e.message}`, 'error');

                            hideProcessing();

                            convertBtn.disabled = false;

                            if (streamToStop) streamToStop.getTracks().forEach(track => track.stop());

                            streamToStop = null;

                        });

                    } // else onloadedmetadata will handle it.

                } catch (error) {

                    showOutput(`Error setting up MediaRecorder: ${error.message}`, 'error');

                    hideProcessing();

                    convertBtn.disabled = false;

                    if (streamToStop) streamToStop.getTracks().forEach(track => track.stop());

                    streamToStop = null;

                }

            });

            

            tools.find(t => t.id === 'videoConverter').cleanup = () => {

                if (mediaRecorder && mediaRecorder.state === "recording") {

                    mediaRecorder.stop(); // This will trigger onstop

                } else if (streamToStop) {

                    streamToStop.getTracks().forEach(track => track.stop());

                }

                streamToStop = null;

                if (videoPreview.src && videoPreview.src.startsWith('blob:')) {

                    videoPreview.pause();

                    // Check if srcObject is used, though for blob URLs it's usually .src

                    const currentStream = videoPreview.srcObject; 

                    if (currentStream && typeof currentStream.getTracks === 'function') {

                         currentStream.getTracks().forEach(track => track.stop());

                    }

                    URL.revokeObjectURL(videoPreview.src);

                    videoPreview.srcObject = null;

                    videoPreview.src = '';

                }

                if (sourceVideoURL) { //This is the original file blob

                    URL.revokeObjectURL(sourceVideoURL);

                    sourceVideoURL = null;

                }

                recordedChunks = [];

            };

        }


        // 5. Audio to WAV Converter

        function createAudioConverterUI() {

            return `

                <div class="tool-ui">

                    <label for="audioConvFile">Upload Audio File (MP3, WAV, OGG, etc.):</label>

                    <input type="file" id="audioConvFile" accept="audio/*">

                    <button id="audioConvButton">Convert to WAV</button>

                    <audio id="audioConvPreview" controls style="width:100%; margin-top:10px; display:none;"></audio>

                </div>

            `;

        }

        function initAudioConverter() {

            const fileInput = document.getElementById('audioConvFile');

            const convertBtn = document.getElementById('audioConvButton');

            const audioPreview = document.getElementById('audioConvPreview');

            let audioFile = null;

            let objectURL = null;

            

            fileInput.addEventListener('change', (e) => {

                audioFile = e.target.files[0];

                if (audioFile) {

                    if (objectURL) URL.revokeObjectURL(objectURL);

                    objectURL = URL.createObjectURL(audioFile);

                    audioPreview.src = objectURL;

                    audioPreview.style.display = 'block';

                    clearOutput();

                }

            });


            convertBtn.addEventListener('click', async () => {

                if (!audioFile) {

                    showOutput('Please upload an audio file.', 'error');

                    return;

                }

                showProcessing();

                const audioCtx = getAudioContext();


                const reader = new FileReader();

                reader.onload = async (e_reader) => {

                    try {

                        const audioBuffer = await audioCtx.decodeAudioData(e_reader.target.result);

                        const wavBlob = audioBufferToWav(audioBuffer);

                        const originalFileName = audioFile.name.split('.')[0];

                        createDownloadLink(wavBlob, `${originalFileName}_converted.wav`);

                        showOutput('Audio converted to WAV successfully.', 'success');

                    } catch (err) {

                        showOutput(`Error converting audio: ${err.message}. Ensure the file is a valid audio format.`, 'error');

                    } finally {

                        hideProcessing();

                    }

                };

                reader.onerror = () => {

                     showOutput('Error reading file.', 'error');

                     hideProcessing();

                };

                reader.readAsArrayBuffer(audioFile);

            });

            

            tools.find(t => t.id === 'audioConverter').cleanup = () => {

                if (objectURL) {

                    URL.revokeObjectURL(objectURL);

                    objectURL = null;

                }

                if (audioPreview.src) {

                    audioPreview.pause();

                    audioPreview.src = '';

                }

            };

        }

        

        // Helper: AudioBuffer to WAV

        function audioBufferToWav(buffer) {

            let numOfChan = buffer.numberOfChannels,

                btwLength = buffer.length * numOfChan * 2 + 44, // 2 bytes per sample (16-bit)

                btwArrBuff = new ArrayBuffer(btwLength),

                btwView = new DataView(btwArrBuff),

                btwChnls = [],

                btwIndex,

                btwSample,

                btwOffset = 0,

                btwPos = 0;


            setUint32(0x46464952); // "RIFF"

            setUint32(btwLength - 8); // file length - 8

            setUint32(0x45564157); // "WAVE"

            setUint32(0x20746d66); // "fmt " chunk

            setUint32(16); // length = 16 for PCM

            setUint16(1); // PCM (uncompressed)

            setUint16(numOfChan);

            setUint32(buffer.sampleRate);

            setUint32(buffer.sampleRate * 2 * numOfChan); // avg. bytes/sec (SR * BytesPerSample * NumChannels)

            setUint16(numOfChan * 2); // block-align (BytesPerSample * NumChannels)

            setUint16(16); // 16-bit

            setUint32(0x61746164); // "data" - chunk

            setUint32(buffer.length * numOfChan * 2); // chunk length (NumSamples * NumChannels * BytesPerSample)


            for (btwIndex = 0; btwIndex < buffer.numberOfChannels; btwIndex++)

                btwChnls.push(buffer.getChannelData(btwIndex));


            // Interleave channels and convert to 16-bit PCM

            for (btwOffset = 0; btwOffset < buffer.length; btwOffset++) {

                for (btwIndex = 0; btwIndex < numOfChan; btwIndex++) {

                    btwSample = Math.max(-1, Math.min(1, btwChnls[btwIndex][btwOffset])); // clamp

                    // scale to 16-bit signed int

                    btwSample = btwSample < 0 ? btwSample * 0x8000 : btwSample * 0x7FFF; 

                    btwView.setInt16(btwPos, btwSample, true); // write 16-bit sample, little-endian

                    btwPos += 2;

                }

            }

            return new Blob([btwArrBuff], { type: "audio/wav" });


            function setUint16(data) {

                btwView.setUint16(btwPos, data, true); btwPos += 2;

            }

            function setUint32(data) {

                btwView.setUint32(btwPos, data, true); btwPos += 4;

            }

        }


        // 6. Audio Trimmer

        function createAudioTrimmerUI() {

            return `

                <div class="tool-ui">

                    <label for="audioTrimFile">Upload Audio File:</label>

                    <input type="file" id="audioTrimFile" accept="audio/*">

                    <audio id="audioTrimPreview" controls style="width:100%; margin-top:10px; display:none;"></audio>

                    <div style="display:flex; gap:10px; margin-top:10px;">

                        <div style="flex:1;">

                            <label for="audioTrimStart">Start Time (s):</label>

                            <input type="number" id="audioTrimStart" value="0" min="0" step="0.01">

                        </div>

                        <div style="flex:1;">

                            <label for="audioTrimEnd">End Time (s):</label>

                            <input type="number" id="audioTrimEnd" value="0" min="0" step="0.01">

                        </div>

                    </div>

                    <button id="audioTrimButton" style="margin-top:10px;">Trim and Download WAV</button>

                </div>

            `;

        }

        function initAudioTrimmer() {

            const fileInput = document.getElementById('audioTrimFile');

            const preview = document.getElementById('audioTrimPreview');

            const startTimeInput = document.getElementById('audioTrimStart');

            const endTimeInput = document.getElementById('audioTrimEnd');

            const trimBtn = document.getElementById('audioTrimButton');

            let audioFile = null;

            let originalAudioBuffer = null; // Store the full decoded buffer

            let objectURL = null;


            fileInput.addEventListener('change', async (e) => {

                audioFile = e.target.files[0];

                if (audioFile) {

                    if (objectURL) URL.revokeObjectURL(objectURL);

                    objectURL = URL.createObjectURL(audioFile);

                    preview.src = objectURL;

                    preview.style.display = 'block';

                    clearOutput();

                    originalAudioBuffer = null; // Reset buffer

                    showOutput('Decoding audio... please wait.', 'info');


                    const audioCtx = getAudioContext();

                    const reader = new FileReader();

                    reader.onload = async (ev_reader) => {

                        try {

                            originalAudioBuffer = await audioCtx.decodeAudioData(ev_reader.target.result);

                            endTimeInput.value = originalAudioBuffer.duration.toFixed(2);

                            startTimeInput.max = originalAudioBuffer.duration.toFixed(2);

                            endTimeInput.max = originalAudioBuffer.duration.toFixed(2);

                            showOutput(`Audio loaded. Duration: ${originalAudioBuffer.duration.toFixed(2)}s. Ready to trim.`, 'success');

                        } catch (err) {

                            showOutput(`Error decoding audio: ${err.message}`, 'error');

                            originalAudioBuffer = null;

                        } finally {

                             // No explicit hideProcessing as decode is part of loading user flow

                        }

                    };

                    reader.readAsArrayBuffer(audioFile);

                }

            });

            

            preview.onloadedmetadata = () => { // Fallback if decodeAudioData is slow or fails

                if (!originalAudioBuffer && preview.duration) { 

                    endTimeInput.value = preview.duration.toFixed(2);

                    startTimeInput.max = preview.duration.toFixed(2);

                    endTimeInput.max = preview.duration.toFixed(2);

                     if (!modalToolOutput.textContent.includes('success')) { // Avoid overwriting decode success

                        showOutput(`Preview loaded. Duration: ${preview.duration.toFixed(2)}s. For trimming, full decode is preferred.`, 'info');

                     }

                }

            };


            trimBtn.addEventListener('click', () => {

                if (!originalAudioBuffer) {

                    showOutput('Please upload and wait for audio to fully decode before trimming.', 'error');

                    return;

                }

                const audioCtx = getAudioContext(); // Ensure context is active

                const startTime = parseFloat(startTimeInput.value);

                const endTime = parseFloat(endTimeInput.value);


                if (isNaN(startTime) || isNaN(endTime) || startTime < 0 || endTime <= startTime || endTime > originalAudioBuffer.duration) {

                    showOutput('Invalid start or end time. Ensure End Time > Start Time and within audio duration.', 'error');

                    return;

                }

                showProcessing('Trimming audio...');


                try {

                    const startSample = Math.floor(startTime * originalAudioBuffer.sampleRate);

                    const endSample = Math.floor(endTime * originalAudioBuffer.sampleRate);

                    const trimmedLength = endSample - startSample;


                    if (trimmedLength <= 0) {

                        showOutput('Trimmed duration is zero or negative.', 'error');

                        hideProcessing();

                        return;

                    }


                    const trimmedBuffer = audioCtx.createBuffer(

                        originalAudioBuffer.numberOfChannels,

                        trimmedLength,

                        originalAudioBuffer.sampleRate

                    );


                    for (let i = 0; i < originalAudioBuffer.numberOfChannels; i++) {

                        const channelData = originalAudioBuffer.getChannelData(i);

                        const trimmedChannelData = trimmedBuffer.getChannelData(i);

                        // Use subarray for efficiency if available and correct, otherwise loop

                        // For AudioBuffer, getChannelData returns a Float32Array, which has subarray

                        trimmedChannelData.set(channelData.subarray(startSample, endSample));

                    }

                    

                    const wavBlob = audioBufferToWav(trimmedBuffer); // Use the helper

                    const originalFileName = audioFile.name.split('.')[0];

                    createDownloadLink(wavBlob, `${originalFileName}_trimmed.wav`);

                    showOutput('Audio trimmed successfully.', 'success');


                } catch (err) {

                    showOutput(`Error trimming audio: ${err.message}`, 'error');

                } finally {

                    hideProcessing();

                }

            });


            tools.find(t => t.id === 'audioTrimmer').cleanup = () => {

                if (objectURL) {

                    URL.revokeObjectURL(objectURL);

                    objectURL = null;

                }

                if (preview.src) {

                    preview.pause();

                    preview.src = '';

                }

                originalAudioBuffer = null; // Release the decoded buffer

            };

        }


        // 7. Age Calculator

        function createAgeCalculatorUI() {

            return `

                <div class="tool-ui">

                    <label for="dob">Date of Birth:</label>

                    <input type="date" id="dob" max="${new Date().toISOString().split('T')[0]}">

                    <button id="calculateAgeBtn">Calculate Age</button>

                    <div id="ageResult" class="output-area" style="margin-top:10px;"></div>

                </div>

            `;

        }

        function initAgeCalculator() {

            const dobInput = document.getElementById('dob');

            const calculateBtn = document.getElementById('calculateAgeBtn');

            const ageResultDiv = document.getElementById('ageResult');


            calculateBtn.addEventListener('click', () => {

                const dobString = dobInput.value;

                ageResultDiv.textContent = ''; // Clear previous result

                clearOutput();


                if (!dobString) {

                    ageResultDiv.textContent = 'Please select a date of birth.';

                    ageResultDiv.style.color = 'var(--error-color)';

                    showOutput('Please select a date of birth.', 'error');

                    return;

                }

                const dob = new Date(dobString);

                const today = new Date();


                if (dob > today) {

                    ageResultDiv.textContent = 'Date of birth cannot be in the future.';

                    ageResultDiv.style.color = 'var(--error-color)';

                    showOutput('Date of birth cannot be in the future.', 'error');

                    return;

                }


                let years = today.getFullYear() - dob.getFullYear();

                let months = today.getMonth() - dob.getMonth();

                let days = today.getDate() - dob.getDate();


                if (days < 0) {

                    months--;

                    const lastMonthDate = new Date(today.getFullYear(), today.getMonth(), 0).getDate();

                    days += lastMonthDate;

                }

                if (months < 0) {

                    years--;

                    months += 12;

                }

                const resultText = `You are ${years} years, ${months} months, and ${days} days old.`;

                ageResultDiv.textContent = resultText;

                ageResultDiv.style.color = 'var(--text-color)';

                showOutput(resultText, 'success');

            });

        }

        

        // 8. EMI Calculator

        function createEMICalculatorUI() {

            return `

                <div class="tool-ui">

                    <label for="loanAmount">Loan Amount (?):</label>

                    <input type="number" id="loanAmount" placeholder="e.g., 500000" min="0">

                    <label for="interestRate">Annual Interest Rate (%):</label>

                    <input type="number" id="interestRate" placeholder="e.g., 8.5" step="0.01" min="0">

                    <label for="loanTenure">Loan Tenure (Years):</label>

                    <input type="number" id="loanTenure" placeholder="e.g., 5" min="0">

                    <button id="calculateEMIBtn">Calculate EMI</button>

                    <div id="emiResult" class="output-area" style="margin-top:10px;"></div>

                </div>

            `;

        }

        function initEMICalculator() {

            const amountInput = document.getElementById('loanAmount');

            const rateInput = document.getElementById('interestRate');

            const tenureInput = document.getElementById('loanTenure');

            const calculateBtn = document.getElementById('calculateEMIBtn');

            const emiResultDiv = document.getElementById('emiResult');


            calculateBtn.addEventListener('click', () => {

                emiResultDiv.innerHTML = ''; clearOutput();

                const P = parseFloat(amountInput.value);

                const annualRate = parseFloat(rateInput.value);

                const tenureYears = parseFloat(tenureInput.value);


                if (isNaN(P) || P <= 0 || isNaN(annualRate) || annualRate <= 0 || isNaN(tenureYears) || tenureYears <= 0) {

                    emiResultDiv.textContent = 'Please enter valid positive values for all fields.';

                    showOutput('Invalid input for EMI calculation.', 'error');

                    return;

                }


                const r = (annualRate / 12) / 100; // Monthly interest rate

                const n = tenureYears * 12; // Tenure in months


                // EMI = P * r * (1+r)^n / ((1+r)^n - 1)

                const emi = (P * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);

                if (isNaN(emi) || !isFinite(emi)) {

                     emiResultDiv.textContent = 'Could not calculate EMI. Check inputs (e.g., rate might be too low for formula stability).';

                     showOutput('EMI calculation resulted in an invalid number.', 'error');

                     return;

                }

                const totalPayment = emi * n;

                const totalInterest = totalPayment - P;


                emiResultDiv.innerHTML = `

                    Monthly EMI: <strong>? ${emi.toFixed(2)}</strong><br>

                    Total Interest Payable: <strong>? ${totalInterest.toFixed(2)}</strong><br>

                    Total Payment (Principal + Interest): <strong>? ${totalPayment.toFixed(2)}</strong>

                `;

                showOutput('EMI calculation successful.', 'success');

            });

        }


        // 9. SIP Calculator

        function createSIPCalculatorUI() {

            return `

                <div class="tool-ui">

                    <label for="monthlyInvestment">Monthly Investment (?):</label>

                    <input type="number" id="monthlyInvestment" placeholder="e.g., 5000" min="0">

                    <label for="expectedReturnRate">Expected Annual Return Rate (%):</label>

                    <input type="number" id="expectedReturnRate" placeholder="e.g., 12" step="0.01" min="0">

                    <label for="investmentDuration">Investment Duration (Years):</label>

                    <input type="number" id="investmentDuration" placeholder="e.g., 10" min="0">

                    <button id="calculateSIPBtn">Calculate Future Value</button>

                    <div id="sipResult" class="output-area" style="margin-top:10px;"></div>

                </div>

            `;

        }

        function initSIPCalculator() {

            const monthlyInvestmentInput = document.getElementById('monthlyInvestment');

            const returnRateInput = document.getElementById('expectedReturnRate');

            const durationInput = document.getElementById('investmentDuration');

            const calculateBtn = document.getElementById('calculateSIPBtn');

            const sipResultDiv = document.getElementById('sipResult');


            calculateBtn.addEventListener('click', () => {

                sipResultDiv.innerHTML = ''; clearOutput();

                const P = parseFloat(monthlyInvestmentInput.value); 

                const annualRate = parseFloat(returnRateInput.value);

                const T = parseFloat(durationInput.value); 


                if (isNaN(P) || P <= 0 || isNaN(annualRate) || annualRate < 0 || isNaN(T) || T <= 0) { // Rate can be 0

                    sipResultDiv.textContent = 'Please enter valid positive values for investment and duration, and a non-negative rate.';

                     showOutput('Invalid input for SIP calculation.', 'error');

                    return;

                }


                const i = (annualRate / 12) / 100; // Monthly rate of return

                const n = T * 12; // Number of months


                // Future Value of SIP: FV = P * [((1+i)^n - 1) / i] * (1+i) (investment at beginning of month)

                // If i is 0, the formula ( (1+i)^n - 1 ) / i becomes n

                let M;

                if (i === 0) {

                    M = P * n; // Simple sum if rate is 0

                } else {

                    M = P * ( (Math.pow(1 + i, n) - 1) / i ) * (1 + i);

                }

                

                const totalInvested = P * n;

                const wealthGained = M - totalInvested;


                sipResultDiv.innerHTML = `

                    Invested Amount: <strong>? ${totalInvested.toFixed(2)}</strong><br>

                    Estimated Wealth Gained: <strong>? ${wealthGained.toFixed(2)}</strong><br>

                    Total Future Value: <strong>? ${M.toFixed(2)}</strong>

                `;

                 showOutput('SIP calculation successful.', 'success');

            });

        }

        

        // 10. QR Code Generator

        // The qrcode.js library (MIT license) logic is embedded here directly.

        // Source: https://github.com/kazuhikoarase/qrcode-generator (Adapted for this tool)

        var qrcode = function() {

            var qrcode = function(typeNumber, errorCorrectLevel) {

                this.typeNumber = typeNumber; this.errorCorrectLevel = errorCorrectLevel; this.modules = null; this.moduleCount = 0; this.dataCache = null; this.dataList = [];

            };

            qrcode.prototype = {

                addData: function(data) { this.dataList.push(new QR8bitByte(data)); this.dataCache = null; },

                isDark: function(row, col) { if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) throw new Error(row + "," + col); return this.modules[row][col]; },

                getModuleCount: function() { return this.moduleCount; },

                make: function() { this.makeImpl(false, this.getBestMaskPattern()); },

                makeImpl: function(test, maskPattern) {

                    this.moduleCount = this.typeNumber * 4 + 17; this.modules = new Array(this.moduleCount);

                    for (var r = 0; r < this.moduleCount; r++) { this.modules[r] = new Array(this.moduleCount); for (var c = 0; c < this.moduleCount; c++) this.modules[r][c] = null; }

                    this.setupPositionProbePattern(0, 0); this.setupPositionProbePattern(this.moduleCount - 7, 0); this.setupPositionProbePattern(0, this.moduleCount - 7);

                    this.setupPositionAdjustPattern(); this.setupTimingPattern(); this.setupTypeInfo(test, maskPattern);

                    if (this.typeNumber >= 7) this.setupTypeNumber(test);

                    if (this.dataCache == null) this.dataCache = qrcode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList);

                    this.mapData(this.dataCache, maskPattern);

                },

                setupPositionProbePattern: function(row, col) {

                    for (var r = -1; r <= 7; r++) { if (row + r <= -1 || this.moduleCount <= row + r) continue;

                        for (var c = -1; c <= 7; c++) { if (col + c <= -1 || this.moduleCount <= col + c) continue;

                            if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) this.modules[row + r][col + c] = true;

                            else this.modules[row + r][col + c] = false;

                        }}

                },

                getBestMaskPattern: function() {

                    var minLostPoint = 0, pattern = 0;

                    for (var i = 0; i < 8; i++) { this.makeImpl(true, i); var lostPoint = QRUtil.getLostPoint(this); if (i == 0 || minLostPoint > lostPoint) { minLostPoint = lostPoint; pattern = i; } }

                    return pattern;

                },

                setupTimingPattern: function() {

                    for (var r = 8; r < this.moduleCount - 8; r++) { if (this.modules[r][6] != null) continue; this.modules[r][6] = (r % 2 == 0); }

                    for (var c = 8; c < this.moduleCount - 8; c++) { if (this.modules[6][c] != null) continue; this.modules[6][c] = (c % 2 == 0); }

                },

                setupPositionAdjustPattern: function() {

                    var pos = QRUtil.PATTERN_POSITION_TABLE[this.typeNumber - 1];

                    for (var i = 0; i < pos.length; i++) for (var j = 0; j < pos.length; j++) {

                        var row = pos[i], col = pos[j]; if (this.modules[row][col] != null) continue;

                        for (var r = -2; r <= 2; r++) for (var c = -2; c <= 2; c++) 

                            this.modules[row + r][col + c] = (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0));

                    }

                },

                setupTypeNumber: function(test) {

                    var bits = QRUtil.getBCHTypeNumber(this.typeNumber);

                    for (var i = 0; i < 18; i++) { var mod = (!test && ((bits >> i) & 1) == 1); this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; }

                    for (var i = 0; i < 18; i++) { var mod = (!test && ((bits >> i) & 1) == 1); this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; }

                },

                setupTypeInfo: function(test, maskPattern) {

                    var data = (this.errorCorrectLevel << 3) | maskPattern, bits = QRUtil.getBCHTypeInfo(data);

                    for (var i = 0; i < 15; i++) { var mod = (!test && ((bits >> i) & 1) == 1);

                        if (i < 6) this.modules[i][8] = mod; else if (i < 8) this.modules[i + 1][8] = mod; else this.modules[this.moduleCount - 15 + i][8] = mod;

                    }

                    for (var i = 0; i < 15; i++) { var mod = (!test && ((bits >> i) & 1) == 1);

                        if (i < 8) this.modules[8][this.moduleCount - i - 1] = mod; else if (i < 9) this.modules[8][15 - i - 1 + 1] = mod; else this.modules[8][15 - i - 1] = mod;

                    }

                    this.modules[this.moduleCount - 8][8] = (!test);

                },

                mapData: function(data, maskPattern) {

                    var inc = -1, row = this.moduleCount - 1, bitIndex = 7, byteIndex = 0;

                    for (var col = this.moduleCount - 1; col > 0; col -= 2) { if (col == 6) col--;

                        while (true) { for (var c = 0; c < 2; c++) {

                                if (this.modules[row][col - c] == null) {

                                    var dark = false; if (byteIndex < data.length) dark = (((data[byteIndex] >>> bitIndex) & 1) == 1);

                                    if (QRUtil.getMask(maskPattern, row, col - c)) dark = !dark;

                                    this.modules[row][col - c] = dark; bitIndex--;

                                    if (bitIndex == -1) { byteIndex++; bitIndex = 7; }

                                }}

                            row += inc; if (row < 0 || this.moduleCount <= row) { row -= inc; inc = -inc; break; }

                        }}

                }

            };

            qrcode.PAD0 = 0xEC; qrcode.PAD1 = 0x11;

            qrcode.createData = function(typeNumber, errorCorrectLevel, dataList) {

                var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel), buffer = new QRBitBuffer();

                for (var i = 0; i < dataList.length; i++) { var data = dataList[i]; buffer.put(data.getMode(), 4); buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber, data.data)); data.write(buffer); }

                var totalDataCount = 0; for (var i = 0; i < rsBlocks.length; i++) totalDataCount += rsBlocks[i].dataCount;

                if (buffer.getLengthInBits() > totalDataCount * 8) throw new Error("code length overflow. (" + buffer.getLengthInBits() + ">" + totalDataCount * 8 + ")");

                if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) buffer.put(0, 4);

                while (buffer.getLengthInBits() % 8 != 0) buffer.putBit(false);

                while (true) { if (buffer.getLengthInBits() >= totalDataCount * 8) break; buffer.put(qrcode.PAD0, 8); if (buffer.getLengthInBits() >= totalDataCount * 8) break; buffer.put(qrcode.PAD1, 8); }

                return qrcode.createBytes(buffer, rsBlocks);

            };

            qrcode.createBytes = function(buffer, rsBlocks) {

                var offset = 0, maxDcCount = 0, maxEcCount = 0, dcdata = new Array(rsBlocks.length), ecdata = new Array(rsBlocks.length);

                for (var r = 0; r < rsBlocks.length; r++) {

                    var dcCount = rsBlocks[r].dataCount, ecCount = rsBlocks[r].totalCount - dcCount;

                    maxDcCount = Math.max(maxDcCount, dcCount); maxEcCount = Math.max(maxEcCount, ecCount);

                    dcdata[r] = new Array(dcCount); for (var i = 0; i < dcdata[r].length; i++) dcdata[r][i] = 0xff & buffer.buffer[i + offset];

                    offset += dcCount; var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount), rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1), modPoly = rawPoly.mod(rsPoly);

                    ecdata[r] = new Array(rsPoly.getLength() - 1); for (var i = 0; i < ecdata[r].length; i++) { var modIndex = i + modPoly.getLength() - ecdata[r].length; ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0; }

                }

                var totalCodeCount = 0; for (var i = 0; i < rsBlocks.length; i++) totalCodeCount += rsBlocks[i].totalCount;

                var data = new Array(totalCodeCount), index = 0;

                for (var i = 0; i < maxDcCount; i++) for (var r = 0; r < rsBlocks.length; r++) if (i < dcdata[r].length) data[index++] = dcdata[r][i];

                for (var i = 0; i < maxEcCount; i++) for (var r = 0; r < rsBlocks.length; r++) if (i < ecdata[r].length) data[index++] = ecdata[r][i];

                return data;

            };

            var QRMode = { MODE_NUMBER: 1 << 0, MODE_ALPHA_NUM: 1 << 1, MODE_8BIT_BYTE: 1 << 2, MODE_KANJI: 1 << 3 };

            var QRErrorCorrectLevel = { L: 1, M: 0, Q: 3, H: 2 };

            var QRMaskPattern = { PATTERN000: 0, PATTERN001: 1, PATTERN010: 2, PATTERN011: 3, PATTERN100: 4, PATTERN101: 5, PATTERN110: 6, PATTERN111: 7 };

            var QRUtil = {

                PATTERN_POSITION_TABLE: [ [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170] ],

                G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),

                getBCHTypeInfo: function(data) { var d = data << 10; while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15))); return ((data << 10) | d) ^ QRUtil.G15_MASK; },

                getBCHTypeNumber: function(data) { var d = data << 12; while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18))); return (data << 12) | d; },

                getBCHDigit: function(data) { var digit = 0; while (data != 0) { digit++; data >>>= 1; } return digit; },

                getPatternPosition: function(typeNumber) { return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; },

                getMask: function(maskPattern, i, j) {

                    switch (maskPattern) {

                    case QRMaskPattern.PATTERN000: return (i + j) % 2 == 0; case QRMaskPattern.PATTERN001: return i % 2 == 0; case QRMaskPattern.PATTERN010: return j % 3 == 0; case QRMaskPattern.PATTERN011: return (i + j) % 3 == 0;

                    case QRMaskPattern.PATTERN100: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; case QRMaskPattern.PATTERN101: return (i * j) % 2 + (i * j) % 3 == 0;

                    case QRMaskPattern.PATTERN110: return ((i * j) % 2 + (i * j) % 3) % 2 == 0; case QRMaskPattern.PATTERN111: return ((i * j) % 3 + (i + j) % 2) % 2 == 0; default: throw new Error("bad maskPattern:" + maskPattern);

                    }},

                getErrorCorrectPolynomial: function(errorCorrectLength) { var a = new QRPolynomial([1], 0); for (var i = 0; i < errorCorrectLength; i++) a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0)); return a; },

                getLengthInBits: function(mode, type, textData) {

                    // Simplified calculation (original is more complex with tables)

                    if (type < 1 || type > 40) throw new Error("bad type:" + type);

                    let len = textData ? new QR8bitByte(textData).getLength() : 20; // Approx length for estimation


                    if (mode === QRMode.MODE_NUMBER) {

                        if (type >= 1 && type <= 9) return 10;

                        if (type >= 10 && type <= 26) return 12;

                        if (type >= 27 && type <= 40) return 14;

                    } else if (mode === QRMode.MODE_ALPHA_NUM) {

                        if (type >= 1 && type <= 9) return 9;

                        if (type >= 10 && type <= 26) return 11;

                        if (type >= 27 && type <= 40) return 13;

                    } else if (mode === QRMode.MODE_8BIT_BYTE) {

                        if (type >= 1 && type <= 9) return 8;

                        if (type >= 10 && type <= 26) return 16; // This is often 16 for 8bit

                        if (type >= 27 && type <= 40) return 16;

                    } else if (mode === QRMode.MODE_KANJI) {

                         if (type >= 1 && type <= 9) return 8;

                         if (type >= 10 && type <= 26) return 10;

                         if (type >= 27 && type <= 40) return 12;

                    } else { throw new Error("mode:" + mode); }

                    return 0; // Fallback

                },

                getLostPoint: function(qrcode) {

                    var moduleCount = qrcode.getModuleCount(), lostPoint = 0;

                    for (var row = 0; row < moduleCount; row++) for (var col = 0; col < moduleCount; col++) { var sameCount = 0, dark = qrcode.isDark(row, col); for (var r = -1; r <= 1; r++) { if (row + r < 0 || moduleCount <= row + r) continue; for (var c = -1; c <= 1; c++) { if (col + c < 0 || moduleCount <= col + c) continue; if (r == 0 && c == 0) continue; if (dark == qrcode.isDark(row + r, col + c)) sameCount++; } } if (sameCount > 5) lostPoint += (3 + sameCount - 5); }

                    for (var row = 0; row < moduleCount - 1; row++) for (var col = 0; col < moduleCount - 1; col++) { var count = 0; if (qrcode.isDark(row, col)) count++; if (qrcode.isDark(row + 1, col)) count++; if (qrcode.isDark(row, col + 1)) count++; if (qrcode.isDark(row + 1, col + 1)) count++; if (count == 0 || count == 4) lostPoint += 3; }

                    for (var row = 0; row < moduleCount; row++) for (var col = 0; col < moduleCount - 6; col++) if (qrcode.isDark(row, col) && !qrcode.isDark(row, col + 1) && qrcode.isDark(row, col + 2) && qrcode.isDark(row, col + 3) && qrcode.isDark(row, col + 4) && !qrcode.isDark(row, col + 5) && qrcode.isDark(row, col + 6)) lostPoint += 40;

                    for (var col = 0; col < moduleCount; col++) for (var row = 0; row < moduleCount - 6; row++) if (qrcode.isDark(row, col) && !qrcode.isDark(row + 1, col) && qrcode.isDark(row + 2, col) && qrcode.isDark(row + 3, col) && qrcode.isDark(row + 4, col) && !qrcode.isDark(row + 5, col) && qrcode.isDark(row + 6, col)) lostPoint += 40;

                    var darkCount = 0; for (var col = 0; col < moduleCount; col++) for (var row = 0; row < moduleCount; row++) if (qrcode.isDark(row, col)) darkCount++;

                    var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; lostPoint += ratio * 10;

                    return lostPoint;

                }

            };

            var QRMath = { glog: function(n) { if (n < 1) throw new Error("glog(" + n + ")"); return QRMath.LOG_TABLE[n]; }, gexp: function(n) { while (n < 0) n += 255; while (n >= 256) n -= 255; return QRMath.EXP_TABLE[n]; }, EXP_TABLE: new Array(256), LOG_TABLE: new Array(256) };

            for (var i = 0; i < 8; i++) QRMath.EXP_TABLE[i] = 1 << i;

            for (var i = 8; i < 256; i++) QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8];

            for (var i = 0; i < 255; i++) QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i;

            function QRPolynomial(num, shift) {

                if (num.length == undefined) throw new Error(num.length + "/" + shift);

                var offset = 0; while (offset < num.length && num[offset] == 0) offset++;

                this.num = new Array(num.length - offset + shift); for (var i = 0; i < num.length - offset; i++) this.num[i] = num[i + offset];

            }

            QRPolynomial.prototype = { get: function(index) { return this.num[index]; }, getLength: function() { return this.num.length; },

                multiply: function(e) { var num = new Array(this.getLength() + e.getLength() - 1); for (var i = 0; i < this.getLength(); i++) for (var j = 0; j < e.getLength(); j++) num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j))); return new QRPolynomial(num, 0); },

                mod: function(e) { if (this.getLength() - e.getLength() < 0) return this; var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0)); var num = new Array(this.getLength()); for (var i = 0; i < this.getLength(); i++) num[i] = this.get(i); for (var i = 0; i < e.getLength(); i++) num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio); return new QRPolynomial(num, 0).mod(e); }

            };

            function QRRSBlock(totalCount, dataCount) { this.totalCount = totalCount; this.dataCount = dataCount; }

            QRRSBlock.RS_BLOCK_TABLE = [ // [totalCount, dataCount]

                [1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9],

                [1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16],

                [1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13],

                // ... Many more entries from the original library needed for full range of types/EC levels.

                // This is a truncated list for brevity.

                // For type 4 (default), EC M: [1, 100, 40] -> need [2, 50, 20] if two blocks

                // Type 4, M (ErrorCorrectLevel.M = 0) means index 1

                // The table in full QR spec is (version, EC level) -> (num_blocks_group1, codewords_group1, data_codewords_group1, num_blocks_group2, ...)

                // The original qrcode.js table is simpler: [[totalCodewords, dataCodewordsPerBlock_EC_L, M, Q, H], [total, data_L, data_M, data_Q, data_H for type 2]...]

                // Let's use a simplified version of getRSBlocks that works for common cases.

            ];

            // This getRSBlocks is simplified. A full implementation uses a large table.

            QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) {

                var rsBlock = [];

                // Example values for type 4 (common default), ErrorCorrectLevel.M

                if (typeNumber === 4 && errorCorrectLevel === QRErrorCorrectLevel.M) {

                     rsBlock.push(new QRRSBlock(20, 16)); // Data count for one block

                     rsBlock.push(new QRRSBlock(20, 16)); // This would be derived from a table: total 40 data words, 2 blocks.

                } else if (typeNumber === 1 && errorCorrectLevel === QRErrorCorrectLevel.L) {

                    rsBlock.push(new QRRSBlock(26, 19));

                } else if (typeNumber === 1 && errorCorrectLevel === QRErrorCorrectLevel.M) {

                     rsBlock.push(new QRRSBlock(26, 16));

                } else if (typeNumber === 10 && errorCorrectLevel === QRErrorCorrectLevel.M) { // for longer text

                    rsBlock.push(new QRRSBlock(32,24)); rsBlock.push(new QRRSBlock(32,24));

                    rsBlock.push(new QRRSBlock(32,24)); rsBlock.push(new QRRSBlock(32,24));

                }

                 else { // Fallback for other types/levels, may not be accurate

                    var dataCount = Math.floor((typeNumber * 4 + 17) * (typeNumber * 4 + 17) * 0.7 / 8) - 20; // very rough estimate

                    dataCount = Math.max(10, dataCount * (errorCorrectLevel === QRErrorCorrectLevel.L ? 0.75 : (errorCorrectLevel === QRErrorCorrectLevel.M ? 0.6 : 0.4)));

                    var totalCount = dataCount + Math.floor(dataCount * 0.3); // rough

                    rsBlock.push(new QRRSBlock(Math.max(20,totalCount), Math.max(15, Math.floor(dataCount))));

                }

                return rsBlock;

            };

            function QRBitBuffer() { this.buffer = []; this.length = 0; }

            QRBitBuffer.prototype = {

                get: function(index) { var bufIndex = Math.floor(index / 8); return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1; },

                put: function(num, length) { for (var i = 0; i < length; i++) this.putBit(((num >>> (length - i - 1)) & 1) == 1); },

                getLengthInBits: function() { return this.length; },

                putBit: function(bit) { var bufIndex = Math.floor(this.length / 8); if (this.buffer.length <= bufIndex) this.buffer.push(0); if (bit) this.buffer[bufIndex] |= (0x80 >>> (this.length % 8)); this.length++; }

            };

            function QR8bitByte(data) {

                this.mode = QRMode.MODE_8BIT_BYTE; this.data = data; this.parsedData = [];

                for (var i = 0, l = this.data.length; i < l; i++) {

                    var code = this.data.charCodeAt(i);

                    if (code > 0x00ff) { // Basic UTF-8 handling (single byte chars only for this simplified ver)

                         this.parsedData.push(0x3F); // Replace with '?' if multi-byte

                    } else {

                        this.parsedData.push(code);

                    }

                }

            }

            QR8bitByte.prototype = { getLength: function() { return this.parsedData.length; }, write: function(buffer) { for (var i = 0; i < this.parsedData.length; i++) buffer.put(this.parsedData[i], 8); }, getMode: function() { return this.mode; } };

            return qrcode;

        }();


        function createQRCodeGeneratorUI() {

            return `

                <div class="tool-ui">

                    <label for="qrText">Text or URL:</label>

                    <textarea id="qrText" placeholder="Enter text to encode"></textarea>

                    <label for="qrTypeNumber">Complexity (1-10, affects size/capacity):</label>

                    <input type="number" id="qrTypeNumber" value="4" min="1" max="10">

                    <button id="generateQRBtn">Generate QR Code</button>

                    <canvas id="qrCodeCanvas"></canvas>

                    <button id="downloadQRBtn" style="margin-top:10px; display:none;">Download QR Code (PNG)</button>

                </div>

            `;

        }

        function initQRCodeGenerator() {

            const textInput = document.getElementById('qrText');

            const typeNumberInput = document.getElementById('qrTypeNumber');

            const generateBtn = document.getElementById('generateQRBtn');

            const canvas = document.getElementById('qrCodeCanvas');

            const downloadBtn = document.getElementById('downloadQRBtn');


            generateBtn.addEventListener('click', () => {

                const text = textInput.value;

                let typeNumber = parseInt(typeNumberInput.value) || 4; 

                typeNumber = Math.max(1, Math.min(typeNumber, 10)); // Clamp between 1 and 10 for this simplified version


                if (!text) {

                    showOutput('Please enter text or URL.', 'error');

                    return;

                }

                showProcessing();

                clearOutput(); // Clear previous messages

                

                // Simple UTF-8 check for qrcode.js basic version

                let validText = true;

                for(let i=0; i < text.length; i++) {

                    if(text.charCodeAt(i) > 255) {

                        validText = false;

                        break;

                    }

                }

                if (!validText) {

                     showOutput('Warning: Input contains multi-byte characters. This basic QR generator handles single-byte (ASCII/Latin-1) characters best. Complex characters might not render correctly.', 'error');

                     // The embedded QR8bitByte was simplified, this is a heads up.

                }



                try {

                    const qr = new qrcode(typeNumber, QRErrorCorrectLevel.M); // M for medium error correction

                    qr.addData(text);

                    qr.make();


                    const moduleCount = qr.getModuleCount();

                    const cellSize = Math.max(1, Math.floor(250 / moduleCount)); 

                    canvas.width = canvas.height = moduleCount * cellSize;

                    const ctx = canvas.getContext('2d');

                    ctx.fillStyle = '#FFFFFF'; 

                    ctx.fillRect(0, 0, canvas.width, canvas.height);

                    ctx.fillStyle = '#000000'; 


                    for (let row = 0; row < moduleCount; row++) {

                        for (let col = 0; col < moduleCount; col++) {

                            if (qr.isDark(row, col)) {

                                ctx.fillRect(col * cellSize, row * cellSize, cellSize, cellSize);

                            }

                        }

                    }

                    downloadBtn.style.display = 'block';

                    if (!modalToolOutput.classList.contains('error')) { // Don't overwrite error with success

                        showOutput('QR Code generated.', 'success');

                    }

                } catch (e) {

                    showOutput(`Error generating QR Code: ${e.message}. Try simpler text, or a higher complexity number if text is long. Max length for type ${typeNumber} might be exceeded.`, 'error');

                    console.error("QR generation error:", e);

                    downloadBtn.style.display = 'none';

                    // Clear canvas on error

                    const ctx = canvas.getContext('2d');

                    ctx.clearRect(0,0,canvas.width,canvas.height);

                    canvas.width=0; canvas.height=0; // Hide it

                } finally {

                    hideProcessing();

                }

            });

            

            downloadBtn.addEventListener('click', () => {

                if (canvas.width > 0 && canvas.height > 0) { // Ensure canvas has content

                    try {

                        const dataUrl = canvas.toDataURL('image/png');

                        const a = document.createElement('a');

                        a.href = dataUrl;

                        a.download = 'qrcode.png';

                        document.body.appendChild(a);

                        a.click();

                        document.body.removeChild(a);

                        showOutput('QR Code download initiated.', 'success');

                    } catch (e) {

                        showOutput('Failed to prepare QR code for download. Canvas might be tainted if an external image was used (not applicable here).', 'error');

                    }

                } else {

                    showOutput('No QR code generated to download.', 'error');

                }

            });

        }


        // 11. Password Generator

        function createPasswordGeneratorUI() {

            return `

                <div class="tool-ui">

                    <label for="passLength">Password Length:</label>

                    <input type="number" id="passLength" value="16" min="4" max="128"> <!-- Min 4 to allow all types -->

                    <div class="options-group" style="margin: 10px 0;">

                        <label><input type="checkbox" id="passUpper" checked> Uppercase (A-Z)</label>

                        <label><input type="checkbox" id="passLower" checked> Lowercase (a-z)</label>

                        <label><input type="checkbox" id="passNumbers" checked> Numbers (0-9)</label>

                        <label><input type="checkbox" id="passSymbols" checked> Symbols (!@#$...)</label>

                    </div>

                    <button id="generatePassBtn">Generate Password</button>

                    <div style="display: flex; align-items: center; margin-top:10px;">

                        <input type="text" id="passwordOutput" readonly title="Generated Password" style="flex-grow:1; background-color: var(--bg-color); border-right:0; border-top-right-radius:0; border-bottom-right-radius:0;">

                        <button id="copyPassBtn" title="Copy to Clipboard" style="padding: 0.75rem; border-top-left-radius:0; border-bottom-left-radius:0;">??</button>

                    </div>

                </div>

            `;

        }

        function initPasswordGenerator() {

            const lengthInput = document.getElementById('passLength');

            const upperCheck = document.getElementById('passUpper');

            const lowerCheck = document.getElementById('passLower');

            const numbersCheck = document.getElementById('passNumbers');

            const symbolsCheck = document.getElementById('passSymbols');

            const generateBtn = document.getElementById('generatePassBtn');

            const outputInput = document.getElementById('passwordOutput'); // Changed to input for easier selection/copy

            const copyBtn = document.getElementById('copyPassBtn');



            const charSets = {

                upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',

                lower: 'abcdefghijklmnopqrstuvwxyz',

                numbers: '0123456789',

                symbols: '!@#$%^&*()_+-=[]{};\':",./<>?'

            };


            generateBtn.addEventListener('click', () => {

                outputInput.value = ''; clearOutput();

                const length = parseInt(lengthInput.value);

                let charset = '';

                let password = '';

                let guaranteedChars = ''; // To ensure at least one of each selected type


                if (upperCheck.checked) { charset += charSets.upper; guaranteedChars += getRandomChar(charSets.upper); }

                if (lowerCheck.checked) { charset += charSets.lower; guaranteedChars += getRandomChar(charSets.lower); }

                if (numbersCheck.checked) { charset += charSets.numbers; guaranteedChars += getRandomChar(charSets.numbers); }

                if (symbolsCheck.checked) { charset += charSets.symbols; guaranteedChars += getRandomChar(charSets.symbols); }


                if (charset === '') {

                    outputInput.value = 'Select char type(s)';

                    showOutput('Please select at least one character type.', 'error');

                    return;

                }

                

                if (length < guaranteedChars.length) {

                     outputInput.value = `Min length ${guaranteedChars.length}`;

                     showOutput(`Length too short for selected types. Minimum required is ${guaranteedChars.length}.`, 'error');

                     return;

                }

                if (length > 128) { // Safety, though input max is 128

                    outputInput.value = 'Length too long';

                    showOutput('Password length is too long (max 128).', 'error');

                    return;

                }


                // Fill remaining length

                for (let i = guaranteedChars.length; i < length; i++) {

                    password += getRandomChar(charset);

                }

                

                password = shuffleString(password + guaranteedChars);



                outputInput.value = password;

                showOutput('Password generated. Click ?? to copy.', 'success');

            });


            copyBtn.addEventListener('click', () => {

                if (outputInput.value && navigator.clipboard) {

                    navigator.clipboard.writeText(outputInput.value)

                        .then(() => showOutput('Password copied to clipboard!', 'success'))

                        .catch(err => {

                            showOutput('Failed to copy password. Manual copy may be needed.', 'error');

                            // Fallback for older browsers or if clipboard fails

                            outputInput.select(); // Select the text

                            outputInput.setSelectionRange(0, 99999); // For mobile devices

                        });

                } else if (outputInput.value) { // Fallback for no navigator.clipboard

                    try {

                        outputInput.select();

                        outputInput.setSelectionRange(0, 99999); // For mobile

                        document.execCommand('copy');

                        showOutput('Password copied (fallback method).', 'success');

                    } catch (err) {

                        showOutput('Copying failed. Please copy manually.', 'error');

                    }

                }

            });


            function getRandomChar(str) {

                return str[Math.floor(Math.random() * str.length)];

            }

            function shuffleString(str) {

                const arr = str.split('');

                for (let i = arr.length - 1; i > 0; i--) {

                    const j = Math.floor(Math.random() * (i + 1));

                    [arr[i], arr[j]] = [arr[j], arr[i]]; 

                }

                return arr.join('');

            }

        }


        // 12. Word Counter

        function createWordCounterUI() {

            return `

                <div class="tool-ui">

                    <label for="wordCountText">Enter Text:</label>

                    <textarea id="wordCountText" rows="8" placeholder="Paste or type your text here..."></textarea>

                    <div id="wordCountResult" class="output-area" style="margin-top:10px; padding: 10px; line-height:1.8;">

                        Words: 0 <br> Characters (with spaces): 0 <br> Characters (no spaces): 0 <br> Spaces: 0 <br> Reading Time: ~0 min

                    </div>

                </div>

            `;

        }

        function initWordCounter() {

            const textArea = document.getElementById('wordCountText');

            const resultDiv = document.getElementById('wordCountResult');

            const AVG_WPM = 200; 


            textArea.addEventListener('input', () => {

                const text = textArea.value;

                const charactersWithSpaces = text.length;

                // Words: split by whitespace, filter out empty strings that can result from multiple spaces.

                const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).filter(Boolean).length; 

                const spaces = (text.match(/\s/g) || []).length;

                const charsNoSpaces = charactersWithSpaces - spaces;

                const readingTime = words > 0 ? Math.ceil(words / AVG_WPM) : 0;


                resultDiv.innerHTML = `

                    Words: <strong>${words}</strong> <br> 

                    Characters (with spaces): <strong>${charactersWithSpaces}</strong> <br> 

                    Characters (no spaces): <strong>${charsNoSpaces}</strong> <br> 

                    Spaces: <strong>${spaces}</strong> <br> 

                    Reading Time: <strong>~${readingTime} min</strong>

                `;

            });

        }


        // 13. Base64 Encoder/Decoder

        function createBase64EncoderDecoderUI() {

            return `

                <div class="tool-ui">

                    <label for="base64Input">Input Text:</label>

                    <textarea id="base64Input" rows="5" placeholder="Text for encoding or Base64 for decoding"></textarea>

                    <select id="base64Operation" style="margin-top:10px; margin-bottom:10px;">

                        <option value="encode">Encode to Base64</option>

                        <option value="decode">Decode from Base64</option>

                    </select>

                    <button id="base64ProcessBtn">Process</button>

                    <label for="base64Output" style="margin-top:10px;">Output:</label>

                    <textarea id="base64Output" rows="5" readonly placeholder="Result will appear here"></textarea>

                </div>

            `;

        }

        function initBase64EncoderDecoder() {

            const inputArea = document.getElementById('base64Input');

            const outputArea = document.getElementById('base64Output');

            const operationSelect = document.getElementById('base64Operation');

            const processBtn = document.getElementById('base64ProcessBtn');


            processBtn.addEventListener('click', () => {

                const inputText = inputArea.value;

                const operation = operationSelect.value;

                outputArea.value = ''; 

                clearOutput();


                if (!inputText.trim()) {

                    showOutput('Input is empty.', 'info');

                    return;

                }


                try {

                    if (operation === 'encode') {

                        // Handle UTF-8 characters correctly using TextEncoder before btoa

                        const utf8Bytes = new TextEncoder().encode(inputText);

                        let binaryString = '';

                        utf8Bytes.forEach(byte => binaryString += String.fromCharCode(byte));

                        outputArea.value = btoa(binaryString);

                        showOutput('Encoded successfully.', 'success');

                    } else { // decode

                        const binaryString = atob(inputText);

                        // Convert binary string to Uint8Array for TextDecoder

                        const bytes = new Uint8Array(binaryString.length);

                        for (let i = 0; i < binaryString.length; i++) {

                            bytes[i] = binaryString.charCodeAt(i);

                        }

                        outputArea.value = new TextDecoder().decode(bytes); // Decodes as UTF-8

                        showOutput('Decoded successfully.', 'success');

                    }

                } catch (e) {

                    let errorMessage = e.message;

                    if (e.name === "InvalidCharacterError" && operation === 'decode') {

                        errorMessage = "Invalid Base64 string. Contains characters not part of Base64 alphabet or incorrect padding.";

                    }

                    outputArea.value = 'Error: ' + errorMessage;

                    showOutput('Error during Base64 operation: ' + errorMessage, 'error');

                }

            });

        }


        // 14. Color Picker Tool

        function createColorPickerUI() {

            return `

                <div class="tool-ui">

                    <label for="colorPickerInput">Choose Color:</label>

                    <div style="display:flex; align-items:center; gap:10px;">

                        <input type="color" id="colorPickerInput" value="#FFD700" style="width:60px; height:50px; padding:0; border:1px solid var(--input-border); border-radius:5px; cursor:pointer;">

                        <input type="text" id="hexColorValue" value="#FFD700" style="flex-grow:1;" title="Enter HEX color (e.g., #RRGGBB or #RGB)">

                    </div>

                    <div id="colorValues" class="output-area" style="margin-top:10px; line-height:1.8;">

                        HEX: #FFD700<br>

                        RGB: rgb(255, 215, 0)<br>

                        HSL: hsl(51, 100%, 50%)

                    </div>

                </div>

            `;

        }

        function initColorPicker() {

            const colorInput = document.getElementById('colorPickerInput');

            const hexInput = document.getElementById('hexColorValue');

            const valuesDiv = document.getElementById('colorValues');


            function updateColorValues(hex) {

                // Validate HEX input a bit

                if (!/^#([0-9A-F]{3}){1,2}$/i.test(hex)) {

                    // If invalid, don't update, or show error

                    // For now, let's just not update if clearly malformed

                    // Or, try to make it valid if possible (e.g. missing #)

                    if (hex.length === 6 && /^[0-9A-F]{6}$/i.test(hex)) hex = '#' + hex;

                    else if (hex.length === 3 && /^[0-9A-F]{3}$/i.test(hex)) hex = '#' + hex;

                    else return; // Don't proceed with invalid hex

                }


                const rgb = hexToRgb(hex);

                if (!rgb) { // hexToRgb might return null for invalid format

                    valuesDiv.innerHTML = "Invalid HEX format.";

                    return;

                }

                const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);

                valuesDiv.innerHTML = `

                    HEX: <strong>${hex.toUpperCase()}</strong><br>

                    RGB: <strong>rgb(${rgb.r}, ${rgb.g}, ${rgb.b})</strong><br>

                    HSL: <strong>hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)</strong>

                `;

                // Sync both inputs

                if(document.activeElement !== colorInput) colorInput.value = hex;

                if(document.activeElement !== hexInput) hexInput.value = hex.toUpperCase();

            }


            colorInput.addEventListener('input', (e) => {

                updateColorValues(e.target.value);

            });

            hexInput.addEventListener('input', (e) => { // Allow manual HEX input

                updateColorValues(e.target.value);

            });

            hexInput.addEventListener('change', (e) => { // On blur or enter, finalize

                updateColorValues(e.target.value);

            });



            // Initial value

            updateColorValues(colorInput.value);


            function hexToRgb(hex) {

                hex = hex.replace(/^#/, '');

                if (!/^[0-9A-F]+$/i.test(hex) || (hex.length !== 3 && hex.length !== 6)) {

                    return null; // Invalid hex string

                }

                if (hex.length === 3) {

                    hex = hex.split('').map(char => char + char).join('');

                }

                const bigint = parseInt(hex, 16);

                const r = (bigint >> 16) & 255;

                const g = (bigint >> 8) & 255;

                const b = bigint & 255;

                return { r, g, b };

            }


            function rgbToHsl(r, g, b) {

                r /= 255; g /= 255; b /= 255;

                const max = Math.max(r, g, b), min = Math.min(r, g, b);

                let h, s, l = (max + min) / 2;


                if (max === min) {

                    h = s = 0; 

                } else {

                    const d = max - min;

                    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

                    switch (max) {

                        case r: h = (g - b) / d + (g < b ? 6 : 0); break;

                        case g: h = (b - r) / d + 2; break;

                        case b: h = (r - g) / d + 4; break;

                    }

                    h /= 6;

                }

                return {

                    h: Math.round(h * 360),

                    s: Math.round(s * 100),

                    l: Math.round(l * 100)

                };

            }

        }


        // 15. Text to Speech

        function createTextToSpeechUI() {

            return `

                <div class="tool-ui">

                    <label for="ttsText">Text to Speak:</label>

                    <textarea id="ttsText" rows="5" placeholder="Enter text here..."></textarea>

                    <label for="ttsVoice">Voice:</label>

                    <select id="ttsVoice" style="margin-bottom:10px;"></select>

                    <div style="display:flex; gap:10px; align-items:center; margin-bottom:5px;">

                        <label for="ttsRate" style="margin-bottom:0; white-space:nowrap;">Rate:</label>

                        <input type="range" id="ttsRate" min="0.1" max="2" step="0.1" value="1" style="flex-grow:1;">

                        <span id="ttsRateValue" style="width:25px; text-align:right;">1</span>

                    </div>

                     <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">

                        <label for="ttsPitch" style="margin-bottom:0; white-space:nowrap;">Pitch:</label>

                        <input type="range" id="ttsPitch" min="0" max="2" step="0.1" value="1" style="flex-grow:1;">

                        <span id="ttsPitchValue" style="width:25px; text-align:right;">1</span>

                    </div>

                    <div style="display:flex; gap:10px; margin-top:10px;">

                        <button id="speakBtn" style="flex:1;">Speak</button>

                        <button id="pauseBtn" style="flex:1;" disabled>Pause</button>

                        <button id="resumeBtn" style="flex:1;" disabled>Resume</button>

                        <button id="stopBtn" style="flex:1;">Stop</button>

                    </div>

                </div>

            `;

        }

        function initTextToSpeech() {

            const textInput = document.getElementById('ttsText');

            const voiceSelect = document.getElementById('ttsVoice');

            const rateInput = document.getElementById('ttsRate');

            const rateValue = document.getElementById('ttsRateValue');

            const pitchInput = document.getElementById('ttsPitch');

            const pitchValue = document.getElementById('ttsPitchValue');

            const speakBtn = document.getElementById('speakBtn');

            const pauseBtn = document.getElementById('pauseBtn');

            const resumeBtn = document.getElementById('resumeBtn');

            const stopBtn = document.getElementById('stopBtn');

            

            const synth = window.speechSynthesis;

            if (!synth) {

                showOutput('Speech Synthesis API not supported by this browser.', 'error');

                [speakBtn, pauseBtn, resumeBtn, stopBtn, rateInput, pitchInput, voiceSelect].forEach(el => el.disabled = true);

                return;

            }

            let voices = [];

            let currentUtterance = null;


            function populateVoiceList() {

                voices = synth.getVoices().sort((a,b) => a.name.localeCompare(b.name)); // Sort voices by name

                const previouslySelected = voiceSelect.value;

                voiceSelect.innerHTML = '';

                voices.forEach((voice) => {

                    const option = document.createElement('option');

                    option.textContent = `${voice.name} (${voice.lang})`;

                    option.value = voice.name; // Use voice name as value for easier lookup

                    if (voice.default) option.selected = true;

                    voiceSelect.appendChild(option);

                });

                if (previouslySelected) voiceSelect.value = previouslySelected; // Restore selection if possible

            }


            populateVoiceList();

            if (speechSynthesis.onvoiceschanged !== undefined) {

                speechSynthesis.onvoiceschanged = populateVoiceList;

            }


            rateInput.addEventListener('input', () => rateValue.textContent = parseFloat(rateInput.value).toFixed(1));

            pitchInput.addEventListener('input', () => pitchValue.textContent = parseFloat(pitchInput.value).toFixed(1));


            function updateButtonStates() {

                if (synth.speaking && !synth.paused) { // Speaking

                    speakBtn.disabled = true; pauseBtn.disabled = false; resumeBtn.disabled = true; stopBtn.disabled = false;

                } else if (synth.paused) { // Paused

                    speakBtn.disabled = true; pauseBtn.disabled = true; resumeBtn.disabled = false; stopBtn.disabled = false;

                } else { // Idle or finished

                    speakBtn.disabled = false; pauseBtn.disabled = true; resumeBtn.disabled = true; stopBtn.disabled = true;

                }

            }

            updateButtonStates(); // Initial state


            speakBtn.addEventListener('click', () => {

                const text = textInput.value;

                if (!text.trim()) {

                    showOutput('Please enter some text to speak.', 'error');

                    return;

                }

                if (synth.speaking) synth.cancel(); // Stop current before starting new

                

                clearOutput();

                currentUtterance = new SpeechSynthesisUtterance(text);

                const selectedVoice = voices.find(voice => voice.name === voiceSelect.value);

                if (selectedVoice) currentUtterance.voice = selectedVoice;

                currentUtterance.pitch = parseFloat(pitchInput.value);

                currentUtterance.rate = parseFloat(rateInput.value);

                

                currentUtterance.onstart = () => { showOutput('Speaking...', 'info'); updateButtonStates(); };

                currentUtterance.onend = () => { showOutput('Finished speaking.', 'success'); updateButtonStates(); currentUtterance = null;};

                currentUtterance.onerror = (e) => { showOutput(`Error during speech: ${e.error}`, 'error'); updateButtonStates(); currentUtterance = null;};

                currentUtterance.onpause = () => { showOutput('Speech paused.', 'info'); updateButtonStates(); };

                currentUtterance.onresume = () => { showOutput('Speech resumed.', 'info'); updateButtonStates(); };

                

                synth.speak(currentUtterance);

            });

            

            pauseBtn.addEventListener('click', () => { if (synth.speaking) synth.pause(); updateButtonStates(); });

            resumeBtn.addEventListener('click', () => { if (synth.paused) synth.resume(); updateButtonStates(); });

            stopBtn.addEventListener('click', () => { if (synth.speaking || synth.paused) synth.cancel(); updateButtonStates(); showOutput('Speech stopped.', 'info'); currentUtterance = null; });


            tools.find(t => t.id === 'textToSpeech').cleanup = () => {

                if (synth && (synth.speaking || synth.paused)) {

                    synth.cancel();

                }

                currentUtterance = null;

            };

        }

        

        // 16. Speech to Text

        function createSpeechToTextUI() {

            return `

                <div class="tool-ui">

                    <label for="sttOutput">Recognized Text:</label>

                    <textarea id="sttOutput" rows="5" placeholder="Recognized text will appear here..."></textarea>

                    <div style="display:flex; gap:10px; margin-top:10px;">

                        <button id="startRecognitionBtn" style="flex:1;">Start Listening</button>

                        <button id="stopRecognitionBtn" style="flex:1;" disabled>Stop Listening</button>

                    </div>

                    <p id="sttStatus" style="margin-top:10px; font-style:italic;">Status: Idle</p>

                </div>

            `;

        }

        function initSpeechToText() {

            const outputArea = document.getElementById('sttOutput');

            const startBtn = document.getElementById('startRecognitionBtn');

            const stopBtn = document.getElementById('stopRecognitionBtn');

            const statusP = document.getElementById('sttStatus');


            const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

            if (!SpeechRecognition) {

                showOutput('Speech Recognition API not supported by this browser.', 'error');

                startBtn.disabled = true; stopBtn.disabled = true;

                statusP.textContent = 'Status: API Not Supported';

                return;

            }


            let recognition = new SpeechRecognition(); // Create new instance for each session

            recognition.continuous = true; 

            recognition.interimResults = true; 

            // recognition.lang = 'en-US'; // User's browser default usually works fine


            let finalTranscript = '';


            function setupRecognition() {

                recognition = new SpeechRecognition();

                recognition.continuous = true;

                recognition.interimResults = true;

                // recognition.lang = 'en-US'; // Set lang if specific needed


                recognition.onstart = () => {

                    statusP.textContent = 'Status: Listening...';

                    startBtn.disabled = true; stopBtn.disabled = false;

                    clearOutput();

                };


                recognition.onresult = (event) => {

                    let interimTranscript = '';

                    finalTranscript = ''; // Rebuild finalTranscript from all final results

                    for (let i = 0; i < event.results.length; ++i) {

                        if (event.results[i].isFinal) {

                            finalTranscript += event.results[i][0].transcript + ' ';

                        } else {

                            interimTranscript += event.results[i][0].transcript;

                        }

                    }

                    outputArea.value = finalTranscript.trim() + (interimTranscript ? ' ' + interimTranscript : '');

                };


                recognition.onerror = (event) => {

                    statusP.textContent = `Status: Error - ${event.error}`;

                    if (event.error === 'no-speech' || event.error === 'audio-capture' || event.error === 'not-allowed' || event.error === 'network') {

                        stopRecognitionInstance(); // Clean up and reset UI

                    }

                    showOutput(`Speech recognition error: ${event.error}. If 'not-allowed', check microphone permissions.`, 'error');

                };


                recognition.onend = () => {

                   stopRecognitionInstance(); // Ensure UI updates when recognition ends naturally or by stop()

                };

            }

            

            function stopRecognitionInstance(){

                if (recognition && (recognition.readyState === 1 /* listening */ || recognition.readyState === 2 /* processing */ )) {

                     // recognition.stop(); // This seems to trigger onend again, so be careful with direct calls

                }

                statusP.textContent = 'Status: Idle. Press Start to try again.';

                startBtn.disabled = false; stopBtn.disabled = true;

                if (finalTranscript.trim() && !modalToolOutput.classList.contains('error')) { // Don't overwrite error msg

                     showOutput('Speech recognition finished.', 'success');

                }

            }


            startBtn.addEventListener('click', () => {

                finalTranscript = ''; 

                outputArea.value = '';

                setupRecognition(); // Re-init for fresh start, handles permissions better

                try {

                    recognition.start();

                } catch (e) {

                     statusP.textContent = `Status: Error - ${e.message}`;

                     showOutput(`Could not start recognition: ${e.message}`, 'error');

                     startBtn.disabled = false; stopBtn.disabled = true;

                }

            });


            stopBtn.addEventListener('click', () => {

                if (recognition) recognition.stop(); // This will trigger onend handler

            });

            

            tools.find(t => t.id === 'speechToText').cleanup = () => {

                if (recognition) {

                    recognition.abort(); // Force stop without triggering all events if modal closes

                }

            };

            // Initial setup if needed, but startBtn click will set it up

        }


        // 17. JSON Formatter

        function createJSONFormatterUI() {

            return `

                <div class="tool-ui">

                    <label for="jsonInput">Paste JSON here:</label>

                    <textarea id="jsonInput" rows="8" placeholder='{"name": "John Doe", "age": 30, "isStudent": false, "courses": [{"title": "History", "credits": 3}, {"title": "Math", "credits": 4}]}'></textarea>

                    <button id="formatJsonBtn" style="margin-top:10px;">Format / Validate JSON</button>

                    <label for="jsonOutput" style="margin-top:10px;">Formatted JSON / Error:</label>

                    <textarea id="jsonOutput" rows="8" readonly></textarea>

                </div>

            `;

        }

        function initJSONFormatter() {

            const inputArea = document.getElementById('jsonInput');

            const outputArea = document.getElementById('jsonOutput');

            const formatBtn = document.getElementById('formatJsonBtn');


            formatBtn.addEventListener('click', () => {

                const inputText = inputArea.value.trim();

                outputArea.value = ''; clearOutput();

                if (!inputText) {

                    showOutput('Input is empty.', 'info');

                    return;

                }

                try {

                    const jsonObj = JSON.parse(inputText);

                    outputArea.value = JSON.stringify(jsonObj, null, 2); // 2 spaces for indentation

                    outputArea.style.color = 'var(--text-color)';

                    showOutput('JSON formatted and validated successfully.', 'success');

                } catch (e) {

                    outputArea.value = `Invalid JSON: ${e.message}`;

                    outputArea.style.color = 'var(--error-color)';

                    showOutput(`Invalid JSON: ${e.message}`, 'error');

                }

            });

             // Auto-format on paste (optional)

            inputArea.addEventListener('paste', (event) => {

                // Use a short timeout to allow the pasted content to actually appear in the textarea

                setTimeout(() => {

                    formatBtn.click(); // Simulate click to format/validate

                }, 0);

            });

        }

        

        // 18. Unit Converter

        function createUnitConverterUI() {

            return `

                <div class="tool-ui">

                    <label for="unitCategory">Category:</label>

                    <select id="unitCategory" style="margin-bottom:10px;"></select>

                    <div style="display:flex; gap:10px; align-items:flex-end;">

                        <div style="flex:2;">

                            <label for="unitInputValue">Value:</label>

                            <input type="number" id="unitInputValue" value="1">

                        </div>

                        <div style="flex:3;">

                            <label for="unitFrom">From:</label>

                            <select id="unitFrom"></select>

                        </div>

                        <div style="flex:0 0 auto; text-align:center; padding-bottom:0.75rem; font-size:1.5em;">&rarr;</div>

                         <div style="flex:3;">

                            <label for="unitTo">To:</label>

                            <select id="unitTo"></select>

                        </div>

                    </div>

                    <div id="unitResult" class="output-area" style="margin-top:15px; font-size:1.2em; text-align:center; padding:10px;"></div>

                </div>

            `;

        }

        function initUnitConverter() {

            const categorySelect = document.getElementById('unitCategory');

            const fromSelect = document.getElementById('unitFrom');

            const toSelect = document.getElementById('unitTo');

            const inputValue = document.getElementById('unitInputValue');

            const resultDiv = document.getElementById('unitResult');


            const units = {

                length: {

                    meter: { name: 'Meter (m)', factor: 1 }, kilometer: { name: 'Kilometer (km)', factor: 1000 }, centimeter: { name: 'Centimeter (cm)', factor: 0.01 }, millimeter: { name: 'Millimeter (mm)', factor: 0.001 },

                    mile: { name: 'Mile (mi)', factor: 1609.344 }, yard: { name: 'Yard (yd)', factor: 0.9144 }, foot: { name: 'Foot (ft)', factor: 0.3048 }, inch: { name: 'Inch (in)', factor: 0.0254 },

                },

                weight: {

                    kilogram: { name: 'Kilogram (kg)', factor: 1 }, gram: { name: 'Gram (g)', factor: 0.001 }, milligram: { name: 'Milligram (mg)', factor: 0.000001 }, metric_ton: { name: 'Metric Ton (t)', factor: 1000},

                    pound: { name: 'Pound (lb)', factor: 0.45359237 }, ounce: { name: 'Ounce (oz)', factor: 0.0283495231 },

                },

                temperature: { // Special handling, factors are not direct multipliers

                    celsius: { name: 'Celsius (°C)' }, fahrenheit: { name: 'Fahrenheit (°F)' }, kelvin: { name: 'Kelvin (K)' },

                },

                area: {

                    sq_meter: { name: 'Square Meter (m²)', factor: 1}, sq_kilometer: { name: 'Square Kilometer (km²)', factor: 1e6}, sq_centimeter: { name: 'Square Centimeter (cm²)', factor: 1e-4},

                    hectare: { name: 'Hectare (ha)', factor: 1e4}, sq_mile: { name: 'Square Mile (mi²)', factor: 2.59e6}, acre: { name: 'Acre', factor: 4046.86},

                },

                volume: {

                    liter: { name: 'Liter (L)', factor: 1}, milliliter: { name: 'Milliliter (mL)', factor: 0.001}, cubic_meter: { name: 'Cubic Meter (m³)', factor: 1000},

                    gallon_us: { name: 'US Gallon (gal)', factor: 3.78541}, pint_us: {name: 'US Pint (pt)', factor: 0.473176},

                }

            };


            function populateCategories() {

                for (const category in units) {

                    const option = document.createElement('option');

                    option.value = category;

                    option.textContent = category.charAt(0).toUpperCase() + category.slice(1).replace('_', ' ');

                    categorySelect.appendChild(option);

                }

            }


            function populateUnits(category) {

                fromSelect.innerHTML = ''; toSelect.innerHTML = '';

                const categoryUnits = units[category];

                let defaultFromSet = false, defaultToSet = false;

                let count = 0;

                for (const unitKey in categoryUnits) {

                    const option = document.createElement('option');

                    option.value = unitKey;

                    option.textContent = categoryUnits[unitKey].name;

                    

                    fromSelect.appendChild(option.cloneNode(true));

                    toSelect.appendChild(option.cloneNode(true));

                    count++;

                }

                // Set default selections (e.g., first for 'from', second for 'to')

                if (fromSelect.options.length > 0) fromSelect.selectedIndex = 0;

                if (toSelect.options.length > 1) toSelect.selectedIndex = 1;

                else if (toSelect.options.length > 0) toSelect.selectedIndex = 0;

            }

            

            function convert() {

                const category = categorySelect.value;

                const fromUnitKey = fromSelect.value;

                const toUnitKey = toSelect.value;

                const val = parseFloat(inputValue.value);


                resultDiv.textContent = ''; clearOutput();

                if (isNaN(val)) {

                    resultDiv.textContent = 'Invalid input value';

                    return;

                }

                if (!fromUnitKey || !toUnitKey) { // Should not happen if populated correctly

                    resultDiv.textContent = 'Select units';

                    return;

                }


                let result;


                if (category === 'temperature') {

                    if (fromUnitKey === toUnitKey) result = val;

                    else if (fromUnitKey === 'celsius') {

                        if (toUnitKey === 'fahrenheit') result = (val * 9/5) + 32;

                        else if (toUnitKey === 'kelvin') result = val + 273.15;

                    } else if (fromUnitKey === 'fahrenheit') {

                        if (toUnitKey === 'celsius') result = (val - 32) * 5/9;

                        else if (toUnitKey === 'kelvin') result = (val - 32) * 5/9 + 273.15;

                    } else if (fromUnitKey === 'kelvin') {

                        if (toUnitKey === 'celsius') result = val - 273.15;

                        else if (toUnitKey === 'fahrenheit') result = (val - 273.15) * 9/5 + 32;

                    }

                } else {

                    const fromUnit = units[category][fromUnitKey];

                    const toUnit = units[category][toUnitKey];

                    const valueInBase = val * fromUnit.factor; 

                    result = valueInBase / toUnit.factor; 

                }

                // Use toFixed for reasonable precision, avoid overly long decimals.

                // Precision can be dynamic based on number magnitude if needed.

                let precision = 5;

                if (Math.abs(result) > 1000) precision = 2;

                if (Math.abs(result) < 0.001 && result !== 0) precision = 7;


                const resultUnitName = units[category][toUnitKey].name.split('(')[0].trim();

                resultDiv.textContent = `${val} ${units[category][fromUnitKey].name.split('(')[0].trim()} = ${result.toFixed(precision)} ${resultUnitName}`;

            }


            categorySelect.addEventListener('change', (e) => {

                populateUnits(e.target.value);

                convert();

            });

            fromSelect.addEventListener('change', convert);

            toSelect.addEventListener('change', convert);

            inputValue.addEventListener('input', convert);


            populateCategories();

            if (categorySelect.options.length > 0) { // Ensure categories populated

                 populateUnits(categorySelect.value); 

                 convert(); 

            }

        }


        // 19. BMI Calculator

        function createBMICalculatorUI() {

            return `

                <div class="tool-ui">

                    <div style="display:flex; gap:10px;">

                        <div style="flex:1;">

                            <label for="bmiWeight">Weight (kg):</label>

                            <input type="number" id="bmiWeight" placeholder="e.g., 70" min="0">

                        </div>

                        <div style="flex:1;">

                            <label for="bmiHeight">Height (cm):</label>

                            <input type="number" id="bmiHeight" placeholder="e.g., 175" min="0">

                        </div>

                    </div>

                    <button id="calculateBmiBtn" style="margin-top:10px;">Calculate BMI</button>

                    <div id="bmiResult" class="output-area" style="margin-top:10px; line-height:1.8;"></div>

                </div>

            `;

        }

        function initBMICalculator() {

            const weightInput = document.getElementById('bmiWeight');

            const heightInput = document.getElementById('bmiHeight');

            const calculateBtn = document.getElementById('calculateBmiBtn');

            const resultDiv = document.getElementById('bmiResult');


            calculateBtn.addEventListener('click', () => {

                resultDiv.innerHTML = ''; clearOutput();

                const weight = parseFloat(weightInput.value);

                const heightCm = parseFloat(heightInput.value);


                if (isNaN(weight) || weight <= 0 || isNaN(heightCm) || heightCm <= 0) {

                    resultDiv.textContent = 'Please enter valid positive weight (kg) and height (cm).';

                    showOutput('Invalid input for BMI.', 'error');

                    return;

                }


                const heightM = heightCm / 100;

                const bmi = weight / (heightM * heightM);

                let category = '';

                let color = 'var(--text-color)'; // Default color


                if (bmi < 18.5) { category = 'Underweight'; color = 'lightblue'; }

                else if (bmi < 24.9) { category = 'Normal weight'; color = 'var(--success-color)'; }

                else if (bmi < 29.9) { category = 'Overweight'; color = 'orange';}

                else if (bmi < 34.9) { category = 'Obesity Class I'; color = 'var(--error-color)';}

                else if (bmi < 39.9) { category = 'Obesity Class II'; color = 'darkred';}

                else { category = 'Obesity Class III (Severe)'; color = 'purple';}


                resultDiv.innerHTML = `

                    Your BMI: <strong style="font-size:1.2em;">${bmi.toFixed(1)}</strong><br>

                    Category: <strong style="color:${color};">${category}</strong>

                `;

                showOutput(`BMI calculated: ${bmi.toFixed(1)} (${category})`, 'success');

            });

        }


        // 20. Timer / Stopwatch Tool

        function createTimerStopwatchUI() {

            return `

                <div class="tool-ui">

                    <div style="text-align:center; margin-bottom:20px; border-bottom: 1px solid var(--card-bg); padding-bottom:20px;">

                        <h4 style="color:var(--accent-color); margin-bottom:5px;">Stopwatch</h4>

                        <div id="stopwatchDisplay" style="font-size: 2.2em; margin:10px 0; font-family:monospace;">00:00:00.000</div>

                        <div style="display:flex; gap:10px; justify-content:center;">

                            <button id="swStartBtn" style="flex:1;">Start</button>

                            <button id="swStopBtn" style="flex:1;" disabled>Stop</button>

                            <button id="swResetBtn" style="flex:1;" disabled>Reset</button>

                        </div>

                    </div>

                    <div style="text-align:center;">

                        <h4 style="color:var(--accent-color); margin-bottom:5px;">Timer</h4>

                        <div style="display:flex; gap:5px; justify-content:center; margin-bottom:10px; align-items:center;">

                            <input type="number" id="timerHours" placeholder="HH" min="0" max="99" value="0" style="width:60px; text-align:center;"> :

                            <input type="number" id="timerMinutes" placeholder="MM" min="0" max="59" value="1" style="width:60px; text-align:center;"> :

                            <input type="number" id="timerSeconds" placeholder="SS" min="0" max="59" value="0" style="width:60px; text-align:center;">

                        </div>

                        <div id="timerDisplay" style="font-size: 2.2em; margin:10px 0; font-family:monospace;">00:01:00</div>

                        <div style="display:flex; gap:10px; justify-content:center;">

                            <button id="timerStartBtn" style="flex:1;">Start</button>

                            <button id="timerPauseBtn" style="flex:1;" disabled>Pause</button>

                            <button id="timerResetBtn" style="flex:1;">Reset</button>

                        </div>

                    </div>

                </div>

            `;

        }

        function initTimerStopwatch() {

            // Stopwatch

            const swDisplay = document.getElementById('stopwatchDisplay');

            const swStartBtn = document.getElementById('swStartBtn');

            const swStopBtn = document.getElementById('swStopBtn');

            const swResetBtn = document.getElementById('swResetBtn');

            let swInterval, swStartTime, swElapsedTime = 0;


            function formatSWTime(ms) {

                const totalSeconds = Math.floor(ms / 1000);

                const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');

                const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');

                const seconds = String(totalSeconds % 60).padStart(2, '0');

                const milliseconds = String(ms % 1000).padStart(3, '0');

                return `${hours}:${minutes}:${seconds}.${milliseconds}`;

            }

            

            swStartBtn.addEventListener('click', () => {

                if (swInterval) return; 

                swStartTime = Date.now() - swElapsedTime;

                swInterval = setInterval(() => {

                    swElapsedTime = Date.now() - swStartTime;

                    swDisplay.textContent = formatSWTime(swElapsedTime);

                }, 10); 

                swStartBtn.disabled = true; swStopBtn.disabled = false; swResetBtn.disabled = true;

            });

            swStopBtn.addEventListener('click', () => {

                clearInterval(swInterval); swInterval = null;

                swStartBtn.disabled = false; swStopBtn.disabled = true; swResetBtn.disabled = false;

            });

            swResetBtn.addEventListener('click', () => {

                clearInterval(swInterval); swInterval = null; swElapsedTime = 0;

                swDisplay.textContent = formatSWTime(0);

                swStartBtn.disabled = false; swStopBtn.disabled = true; swResetBtn.disabled = true;

            });


            // Timer

            const timerHoursInput = document.getElementById('timerHours');

            const timerMinutesInput = document.getElementById('timerMinutes');

            const timerSecondsInput = document.getElementById('timerSeconds');

            const timerDisplay = document.getElementById('timerDisplay');

            const timerStartBtn = document.getElementById('timerStartBtn');

            const timerPauseBtn = document.getElementById('timerPauseBtn');

            const timerResetBtn = document.getElementById('timerResetBtn');

            let timerInterval, timerRemainingSeconds;

            let timerAudio; // Declare, will initialize on first finish


            function initializeTimerAudio() {

                if (!timerAudio) {

                    timerAudio = new Audio("data:audio/wav;base64,UklGRl9vT19XQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YU"+Array(1e3).join("123"));

                }

            }



            function formatTimerDisplay(totalSeconds) {

                const h = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');

                const m = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');

                const s = String(totalSeconds % 60).padStart(2, '0');

                return `${h}:${m}:${s}`;

            }

            

            function setTimerInputsDisabled(isDisabled) {

                timerHoursInput.disabled = isDisabled;

                timerMinutesInput.disabled = isDisabled;

                timerSecondsInput.disabled = isDisabled;

            }


            function updateTimerDisplayFromInputs() {

                 const h = parseInt(timerHoursInput.value) || 0;

                 const m = parseInt(timerMinutesInput.value) || 0;

                 const s = parseInt(timerSecondsInput.value) || 0;

                 timerRemainingSeconds = (h * 3600) + (m * 60) + s;

                 timerDisplay.textContent = formatTimerDisplay(timerRemainingSeconds);

            }

            [timerHoursInput, timerMinutesInput, timerSecondsInput].forEach(input => {

                input.addEventListener('change', updateTimerDisplayFromInputs);

                input.addEventListener('input', () => { // For live update if user types

                    // Basic validation to prevent non-numeric or excessive values during typing

                    input.value = input.value.replace(/[^0-9]/g, '');

                    if (input.id === 'timerMinutes' || input.id === 'timerSeconds') {

                        if (parseInt(input.value) > 59) input.value = '59';

                    }

                    if (input.id === 'timerHours') {

                        if (parseInt(input.value) > 99) input.value = '99';

                    }

                    if (input.value === '') input.value = '0'; // Default to 0 if cleared

                });

            });

            

            timerStartBtn.addEventListener('click', () => {

                if (timerInterval) return; 

                updateTimerDisplayFromInputs(); // Get current values from inputs


                if (timerRemainingSeconds <= 0) {

                    showOutput('Please set a timer duration greater than 0.', 'error');

                    return;

                }

                clearOutput();

                setTimerInputsDisabled(true);

                timerStartBtn.disabled = true; timerPauseBtn.disabled = false; timerResetBtn.disabled = false;

                timerDisplay.style.color = 'var(--text-color)';


                timerInterval = setInterval(() => {

                    timerRemainingSeconds--;

                    timerDisplay.textContent = formatTimerDisplay(timerRemainingSeconds);

                    if (timerRemainingSeconds <= 0) {

                        clearInterval(timerInterval); timerInterval = null;

                        timerDisplay.textContent = '00:00:00';

                        timerDisplay.style.color = 'var(--accent-color)';

                        showOutput('Timer finished!', 'success');

                        initializeTimerAudio();

                        try { timerAudio.play(); } catch(e) { /* ignore if blocked */ }

                        timerStartBtn.disabled = false; // Allow restart with same values or new

                        timerPauseBtn.disabled = true;

                        setTimerInputsDisabled(false); // Re-enable inputs

                    }

                }, 1000);

            });


            timerPauseBtn.addEventListener('click', () => {

                clearInterval(timerInterval); timerInterval = null;

                timerStartBtn.disabled = false; timerPauseBtn.disabled = true;

                // Keep inputs disabled while paused to prevent confusion

            });


            timerResetBtn.addEventListener('click', () => {

                clearInterval(timerInterval); timerInterval = null;

                timerHoursInput.value = '0'; timerMinutesInput.value = '1'; timerSecondsInput.value = '0'; // Reset inputs

                updateTimerDisplayFromInputs(); // Update display based on reset inputs

                timerDisplay.style.color = 'var(--text-color)';

                setTimerInputsDisabled(false);

                timerStartBtn.disabled = false; timerPauseBtn.disabled = true; timerResetBtn.disabled = true; // Reset button itself often becomes disabled

                clearOutput();

            });

            

            // Initialize timer display & button states

            updateTimerDisplayFromInputs(); 

            timerResetBtn.disabled = true; // Initially, reset is not needed until started/paused

            

            tools.find(t => t.id === 'timerStopwatch').cleanup = () => { 

                clearInterval(swInterval);

                clearInterval(timerInterval);

            };

        }


    }); // End DOMContentLoaded

    </script>

</body>

</html>