// TT1 — Tech Tools v1.1 (qualified free_forever)
// Pure vanilla. Offline-local. Outputs are inert text only.
// Storage/cache scoped to tt1_v1_1.

(function () {
  'use strict';

  var VERSION = '1.1.0';
  var SHORT_CODE = 'TT1';
  var STORAGE_PREFIX = 'tt1_v1_1::';
  var MAX_TEXT = 200000;
  var MAX_FIELD = 2000;

  var lang = 'en';
  var activeCore = 'pw';
  var activeAdded = null;
  var tallyN = 0;
  var timerStart = 0;
  var timerAccum = 0;
  var timerHandle = null;
  var els = {};

  var STRINGS = {
    en: {
      title: 'TECH TOOLS',
      sub: 'Useful local tools. Everything runs on your device. Nothing leaves your device.',
      free: 'FREE FOREVER',
      core: 'Core tools',
      added: 'Added tools',
      advanced: 'Advanced',
      password: 'Password',
      pad: 'Scratch Pad',
      tally: 'Tally',
      timer: 'Stopwatch',
      words: 'Word Count',
      pick: 'Random Pick',
      fmt: 'Text Formatter',
      generate: 'Generate',
      copy: 'Copy',
      copied: 'Copied',
      clear: 'Clear pad',
      length: 'Length',
      charset: 'Charset',
      padLabel: 'Scratch pad (saved in this browser only)',
      tallyLabel: 'Tally counter (persists)',
      timerLabel: 'Stopwatch (count up)',
      start: 'Start',
      stop: 'Stop',
      reset: 'Reset',
      pasteText: 'Paste text',
      choices: 'Choices — one per line',
      pickOne: 'Pick one',
      text: 'Text',
      ready: 'Ready'
    },
    es: {
      title: 'HERRAMIENTAS TÉCNICAS',
      sub: 'Herramientas locales. Todo corre en tu dispositivo. Nada sale de tu dispositivo.',
      free: 'GRATIS PARA SIEMPRE',
      core: 'Herramientas base',
      added: 'Herramientas añadidas',
      advanced: 'Avanzadas',
      password: 'Contraseña',
      pad: 'Notas',
      tally: 'Contador',
      timer: 'Cronómetro',
      words: 'Palabras',
      pick: 'Elegir al azar',
      fmt: 'Formato de texto',
      generate: 'Generar',
      copy: 'Copiar',
      copied: 'Copiado',
      clear: 'Borrar notas',
      length: 'Longitud',
      charset: 'Juego de caracteres',
      padLabel: 'Bloc de notas (solo en este navegador)',
      tallyLabel: 'Contador (se guarda)',
      timerLabel: 'Cronómetro (cuenta hacia arriba)',
      start: 'Iniciar',
      stop: 'Parar',
      reset: 'Reiniciar',
      pasteText: 'Pega texto',
      choices: 'Opciones — una por línea',
      pickOne: 'Elegir una',
      text: 'Texto',
      ready: 'Listo'
    }
  };

  function t(k) {
    var p = STRINGS[lang] || STRINGS.en;
    return p[k] != null ? p[k] : (STRINGS.en[k] || k);
  }

  function sanitizeLang(c) {
    return c === 'es' || c === 'en' ? c : 'en';
  }

  function clampText(s, max) {
    if (s == null) return '';
    s = String(s);
    if (s.length > max) s = s.slice(0, max);
    return s;
  }

  function safeGet(key, fallback) {
    try {
      var v = localStorage.getItem(STORAGE_PREFIX + key);
      return v == null ? fallback : v;
    } catch (e) {
      return fallback;
    }
  }

  function safeSet(key, val) {
    try {
      localStorage.setItem(STORAGE_PREFIX + key, val);
      return true;
    } catch (e) {
      return false;
    }
  }

  function el(tag, attrs, children) {
    var n = document.createElement(tag);
    if (attrs) {
      Object.keys(attrs).forEach(function (k) {
        if (k === 'className') n.className = attrs[k];
        else if (k === 'text') n.textContent = attrs[k];
        else if (k === 'htmlFor') n.htmlFor = attrs[k];
        else if (k.indexOf('on') === 0 && typeof attrs[k] === 'function') n.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
        else if (attrs[k] != null) n.setAttribute(k, attrs[k]);
      });
    }
    if (children) {
      (Array.isArray(children) ? children : [children]).forEach(function (c) {
        if (c == null) return;
        if (typeof c === 'string') n.appendChild(document.createTextNode(c));
        else n.appendChild(c);
      });
    }
    return n;
  }

  function setOut(node, text) {
    if (node) node.textContent = text == null ? '' : String(text);
  }

  function copyText(text, btn) {
    text = String(text || '');
    if (!text || text === '—') return Promise.resolve(false);
    var done = function (ok) {
      if (btn) {
        var prev = btn.textContent;
        btn.textContent = ok ? t('copied') : 'Copy failed';
        setTimeout(function () { btn.textContent = prev; }, 1200);
      }
      return ok;
    };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      return navigator.clipboard.writeText(text).then(function () { return done(true); }).catch(function () {
        return fallbackCopy(text) ? done(true) : done(false);
      });
    }
    return Promise.resolve(fallbackCopy(text) ? done(true) : done(false));
  }

  function fallbackCopy(text) {
    try {
      var ta = document.createElement('textarea');
      ta.value = text;
      ta.setAttribute('readonly', '');
      ta.style.position = 'fixed';
      ta.style.left = '-9999px';
      document.body.appendChild(ta);
      ta.select();
      var ok = document.execCommand('copy');
      document.body.removeChild(ta);
      return !!ok;
    } catch (e) {
      return false;
    }
  }

  var SETS = {
    all: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{};:,.?/',
    alnum: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
    alpha: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  };

  function rand(charset, n) {
    var out = '';
    var buf = new Uint32Array(n);
    if (window.crypto && crypto.getRandomValues) crypto.getRandomValues(buf);
    else for (var i = 0; i < n; i++) buf[i] = Math.floor(Math.random() * 4294967295);
    for (var j = 0; j < n; j++) out += charset.charAt(buf[j] % charset.length);
    return out;
  }

  function fmtMs(ms) {
    var tms = Math.max(0, ms);
    var m = Math.floor(tms / 60000);
    var s = Math.floor((tms % 60000) / 1000);
    var cs = Math.floor((tms % 1000) / 10);
    return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) + '.' + (cs < 10 ? '0' + cs : cs);
  }

  function titleCase(s) {
    return s.replace(/\w\S*/g, function (w) {
      return w.charAt(0).toUpperCase() + w.substr(1).toLowerCase();
    });
  }

  function decodeHtmlEntitiesSafe(s) {
    // Map common entities only — never assign to innerHTML
    return String(s)
      .replace(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&quot;/g, '"')
      .replace(/&#39;/g, "'")
      .replace(/&amp;/g, '&');
  }

  function encodeHtmlEntities(s) {
    return String(s)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function mdPreviewText(src) {
    // Text-only markdown preview — no HTML execution
    return String(src)
      .replace(/\r\n/g, '\n')
      .split('\n')
      .map(function (line) {
        if (/^#\s+/.test(line)) return line.replace(/^#\s+/, '【H1】 ');
        if (/^##\s+/.test(line)) return line.replace(/^##\s+/, '【H2】 ');
        if (/^-\s+/.test(line)) return line.replace(/^-\s+/, '• ');
        line = line.replace(/\*\*(.+?)\*\*/g, '「$1」');
        line = line.replace(/\*(.+?)\*/g, '‹$1›');
        return line;
      })
      .join('\n');
  }

  function validateEmergency(obj) {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return {};
    var out = {};
    var keys = ['name', 'contact', 'allergy', 'meds', 'notes'];
    keys.forEach(function (k) {
      if (Object.prototype.hasOwnProperty.call(obj, k) && typeof obj[k] === 'string') {
        out[k] = clampText(obj[k], MAX_FIELD);
      } else {
        out[k] = '';
      }
    });
    // reject prototype pollution paths by never reading __proto__/constructor
    return out;
  }

  function parseEmergencyStored() {
    try {
      var raw = safeGet('emergency', '{}');
      var obj = JSON.parse(raw);
      return validateEmergency(obj);
    } catch (e) {
      return { name: '', contact: '', allergy: '', meds: '', notes: '' };
    }
  }

  function buildUI() {
    var root = document.getElementById('root');
    if (!root) return;
    while (root.firstChild) root.removeChild(root.firstChild);

    var scope = el('div', { className: 'scope-tt' });
    els.scope = scope;

    var header = el('div', null, [
      el('h1', { id: 'tt-title', text: t('title') }),
      el('p', { className: 'tt-sub', id: 'tt-sub', text: t('sub') })
    ]);
    var tagRow = el('div', null, [
      el('span', { className: 'tt-tag', id: 'tt-free', text: t('free') }),
      el('div', { className: 'lang-toggle', role: 'group', 'aria-label': 'Language' }, [
        el('button', { type: 'button', id: 'lang-en', className: 'lang-btn' + (lang === 'en' ? ' on' : ''), 'aria-pressed': lang === 'en' ? 'true' : 'false', text: 'EN' }),
        el('button', { type: 'button', id: 'lang-es', className: 'lang-btn' + (lang === 'es' ? ' on' : ''), 'aria-pressed': lang === 'es' ? 'true' : 'false', text: 'ES' })
      ])
    ]);
    scope.appendChild(header);
    scope.appendChild(tagRow);

    // Core tabs
    var tabs = el('div', { className: 'tt-tabs', role: 'tablist', 'aria-label': t('core') });
    var coreDefs = [
      ['pw', 'password'],
      ['pad', 'pad'],
      ['tally', 'tally'],
      ['timer', 'timer'],
      ['words', 'words'],
      ['pick', 'pick'],
      ['fmt', 'fmt']
    ];
    coreDefs.forEach(function (d) {
      var btn = el('button', {
        type: 'button',
        className: d[0] === activeCore ? 'on' : '',
        'data-tt-tab': d[0],
        role: 'tab',
        'aria-selected': d[0] === activeCore ? 'true' : 'false',
        text: t(d[1])
      });
      tabs.appendChild(btn);
    });
    scope.appendChild(tabs);

    // Password pane
    var pwPane = pane('pw', true);
    pwPane.appendChild(row2(
      field('tt-pw-len', t('length'), el('input', { id: 'tt-pw-len', type: 'number', min: '8', max: '128', value: '20', 'aria-label': t('length') })),
      field('tt-pw-set', t('charset'), (function () {
        var s = el('select', { id: 'tt-pw-set', 'aria-label': t('charset') });
        [['all', 'letters + numbers + symbols'], ['alnum', 'letters + numbers'], ['alpha', 'letters only']].forEach(function (o) {
          s.appendChild(el('option', { value: o[0], text: o[1] }));
        });
        return s;
      })())
    ));
    pwPane.appendChild(btnRow([
      btn('tt-pw-go', t('generate'), true),
      btn('tt-pw-copy', t('copy'), false)
    ]));
    pwPane.appendChild(el('div', { className: 'out', id: 'tt-pw-out', role: 'status', text: '—' }));
    scope.appendChild(pwPane);

    // Scratch pad
    var padPane = pane('pad', false);
    padPane.appendChild(el('label', { htmlFor: 'tt-pad', text: t('padLabel') }));
    padPane.appendChild(el('textarea', { id: 'tt-pad', 'aria-label': t('padLabel'), placeholder: 'Notes…' }));
    padPane.appendChild(btnRow([
      btn('tt-pad-clear', t('clear'), false),
      el('span', { className: 'hint', text: 'key: ' }),
      el('span', { className: 'kbd', text: STORAGE_PREFIX + 'pad' })
    ]));
    scope.appendChild(padPane);

    // Tally
    var taPane = pane('tally', false);
    taPane.appendChild(el('label', { text: t('tallyLabel') }));
    taPane.appendChild(el('div', { className: 'out', id: 'tt-tally-out', role: 'status', style: 'text-align:center;font-size:28px', text: '0' }));
    taPane.appendChild(btnRow([
      btn('tt-tally-inc', '+1', true),
      btn('tt-tally-dec', '−1', false),
      btn('tt-tally-reset', t('reset'), false)
    ]));
    scope.appendChild(taPane);

    // Stopwatch
    var tiPane = pane('timer', false);
    tiPane.appendChild(el('label', { text: t('timerLabel') }));
    tiPane.appendChild(el('div', { className: 'out', id: 'tt-timer-out', role: 'timer', style: 'text-align:center;font-size:28px;font-variant-numeric:tabular-nums', text: '00:00.00' }));
    tiPane.appendChild(btnRow([
      btn('tt-timer-start', t('start'), true),
      btn('tt-timer-stop', t('stop'), false),
      btn('tt-timer-reset', t('reset'), false)
    ]));
    scope.appendChild(tiPane);

    // Word count
    var wcPane = pane('words', false);
    wcPane.appendChild(el('label', { htmlFor: 'tt-wc-in', text: t('pasteText') }));
    wcPane.appendChild(el('textarea', { id: 'tt-wc-in', 'aria-label': t('pasteText') }));
    wcPane.appendChild(el('div', { className: 'out', id: 'tt-wc-out', role: 'status', text: 'words: 0 · chars: 0 · lines: 0' }));
    scope.appendChild(wcPane);

    // Random pick
    var pkPane = pane('pick', false);
    pkPane.appendChild(el('label', { htmlFor: 'tt-pick-in', text: t('choices') }));
    pkPane.appendChild(el('textarea', { id: 'tt-pick-in', 'aria-label': t('choices'), text: 'Pizza\nTacos\nCurry' }));
    pkPane.appendChild(btnRow([btn('tt-pick-go', t('pickOne'), true)]));
    pkPane.appendChild(el('div', { className: 'out', id: 'tt-pick-out', role: 'status', text: '—' }));
    scope.appendChild(pkPane);

    // Formatter
    var fmPane = pane('fmt', false);
    fmPane.appendChild(el('label', { htmlFor: 'tt-fmt-in', text: t('text') }));
    fmPane.appendChild(el('textarea', { id: 'tt-fmt-in', 'aria-label': t('text') }));
    var fmtBtns = btnRow([]);
    [['upper', 'UPPER'], ['lower', 'lower'], ['title', 'Title'], ['trim', 'Trim Lines'], ['reverse', 'Reverse']].forEach(function (f) {
      fmtBtns.appendChild(el('button', { type: 'button', className: 'b', 'data-tt-fmt': f[0], text: f[1] }));
    });
    fmPane.appendChild(fmtBtns);
    fmPane.appendChild(el('div', { className: 'out', id: 'tt-fmt-out', role: 'status', text: '—' }));
    scope.appendChild(fmPane);

    // Added tools section
    scope.appendChild(el('h2', { className: 'section-h', id: 'tt-added-h', text: t('added') }));
    var addedRow = btnRow([]);
    var added = [
      ['qr', 'QR / Text Share'],
      ['clipboard', 'Clipboard Cleaner'],
      ['urlclean', 'URL Cleaner'],
      ['urlencode', 'URL Encode/Decode'],
      ['base64', 'Base64'],
      ['json', 'JSON Formatter'],
      ['htmlent', 'HTML Entity'],
      ['md', 'Markdown Preview'],
      ['emergency', 'Emergency Card'],
      ['device', 'Device Inspector']
    ];
    added.forEach(function (a) {
      addedRow.appendChild(el('button', { type: 'button', className: 'b', 'data-added': a[0], text: a[1] }));
    });
    scope.appendChild(addedRow);
    var addedArea = el('div', { id: 'added-tool-area', className: 'pane on' });
    scope.appendChild(addedArea);
    els.addedArea = addedArea;

    // Advanced
    scope.appendChild(el('h2', { className: 'section-h', id: 'tt-adv-h', text: t('advanced') }));
    var advRow = btnRow([]);
    [['sha', 'SHA-256'], ['regex', 'Regex Tester'], ['occ', 'OCC Inspector']].forEach(function (a) {
      advRow.appendChild(el('button', { type: 'button', className: 'b', 'data-added': a[0], text: a[1] }));
    });
    scope.appendChild(advRow);

    root.appendChild(scope);
    wire();
    applyLabels();
    showCore(activeCore);
  }

  function pane(id, on) {
    return el('div', { className: 'pane' + (on ? ' on' : ''), 'data-tt-pane': id, role: 'tabpanel' });
  }

  function field(id, label, control) {
    var wrap = el('div');
    wrap.appendChild(el('label', { htmlFor: id, text: label }));
    wrap.appendChild(control);
    return wrap;
  }

  function row2(a, b) {
    var r = el('div', { className: 'row' });
    r.appendChild(a);
    r.appendChild(b);
    return r;
  }

  function btn(id, text, gold) {
    return el('button', { type: 'button', id: id, className: 'b' + (gold ? ' gold' : ''), text: text });
  }

  function btnRow(nodes) {
    var r = el('div', { className: 'btn-row' });
    nodes.forEach(function (n) { r.appendChild(n); });
    return r;
  }

  function q(sel) {
    return els.scope ? els.scope.querySelector(sel) : null;
  }

  function qa(sel) {
    return els.scope ? Array.prototype.slice.call(els.scope.querySelectorAll(sel)) : [];
  }

  function showCore(key) {
    activeCore = key;
    qa('[data-tt-tab]').forEach(function (b) {
      var on = b.getAttribute('data-tt-tab') === key;
      b.classList.toggle('on', on);
      b.setAttribute('aria-selected', on ? 'true' : 'false');
    });
    qa('[data-tt-pane]').forEach(function (p) {
      p.classList.toggle('on', p.getAttribute('data-tt-pane') === key);
    });
  }

  function wire() {
    qa('[data-tt-tab]').forEach(function (btnEl) {
      btnEl.addEventListener('click', function () {
        showCore(btnEl.getAttribute('data-tt-tab'));
      });
    });

    document.getElementById('lang-en').addEventListener('click', function () { setLang('en'); });
    document.getElementById('lang-es').addEventListener('click', function () { setLang('es'); });

    // Password
    q('#tt-pw-go').addEventListener('click', function () {
      var n = Math.max(8, Math.min(128, parseInt(q('#tt-pw-len').value, 10) || 20));
      var s = SETS[q('#tt-pw-set').value] || SETS.all;
      setOut(q('#tt-pw-out'), rand(s, n));
    });
    q('#tt-pw-copy').addEventListener('click', function () {
      copyText(q('#tt-pw-out').textContent, q('#tt-pw-copy'));
    });

    // Pad
    var pad = q('#tt-pad');
    pad.value = clampText(safeGet('pad', ''), MAX_TEXT);
    pad.addEventListener('input', function () {
      safeSet('pad', clampText(pad.value, MAX_TEXT));
    });
    q('#tt-pad-clear').addEventListener('click', function () {
      pad.value = '';
      safeSet('pad', '');
    });

    // Tally
    tallyN = parseInt(safeGet('tally', '0'), 10);
    if (!isFinite(tallyN)) tallyN = 0;
    function tallyRender() {
      setOut(q('#tt-tally-out'), String(tallyN));
      safeSet('tally', String(tallyN));
    }
    tallyRender();
    q('#tt-tally-inc').addEventListener('click', function () { tallyN++; tallyRender(); });
    q('#tt-tally-dec').addEventListener('click', function () { tallyN--; tallyRender(); });
    q('#tt-tally-reset').addEventListener('click', function () { tallyN = 0; tallyRender(); });

    // Timer
    function timerTick() {
      setOut(q('#tt-timer-out'), fmtMs(timerAccum + (timerStart ? (Date.now() - timerStart) : 0)));
    }
    q('#tt-timer-start').addEventListener('click', function () {
      if (timerHandle) return;
      timerStart = Date.now();
      timerHandle = setInterval(timerTick, 50);
    });
    q('#tt-timer-stop').addEventListener('click', function () {
      if (!timerHandle) return;
      clearInterval(timerHandle);
      timerHandle = null;
      timerAccum += (Date.now() - timerStart);
      timerStart = 0;
      timerTick();
    });
    q('#tt-timer-reset').addEventListener('click', function () {
      if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }
      timerStart = 0;
      timerAccum = 0;
      timerTick();
    });

    // Word count
    var wcIn = q('#tt-wc-in');
    wcIn.addEventListener('input', function () {
      var v = clampText(wcIn.value, MAX_TEXT);
      if (v !== wcIn.value) wcIn.value = v;
      var words = v.trim() ? v.trim().split(/\s+/).length : 0;
      setOut(q('#tt-wc-out'), 'words: ' + words + ' · chars: ' + v.length + ' · lines: ' + (v ? v.split(/\r?\n/).length : 0));
    });

    // Pick
    q('#tt-pick-go').addEventListener('click', function () {
      var lines = (q('#tt-pick-in').value || '').split(/\r?\n/).map(function (s) { return s.trim(); }).filter(Boolean);
      if (!lines.length) { setOut(q('#tt-pick-out'), '—'); return; }
      var i = Math.floor(Math.random() * lines.length);
      setOut(q('#tt-pick-out'), lines[i]);
    });

    // Format
    qa('[data-tt-fmt]').forEach(function (btnEl) {
      btnEl.addEventListener('click', function () {
        var op = btnEl.getAttribute('data-tt-fmt');
        var v = q('#tt-fmt-in').value || '';
        var out = v;
        if (op === 'upper') out = v.toUpperCase();
        else if (op === 'lower') out = v.toLowerCase();
        else if (op === 'title') out = titleCase(v);
        else if (op === 'trim') out = v.split(/\r?\n/).map(function (l) { return l.trim(); }).join('\n');
        else if (op === 'reverse') out = v.split('').reverse().join('');
        setOut(q('#tt-fmt-out'), out || '—');
      });
    });

    // Added / advanced
    qa('[data-added]').forEach(function (btnEl) {
      btnEl.addEventListener('click', function () {
        showAddedTool(btnEl.getAttribute('data-added'));
      });
    });
  }

  function clearNode(n) {
    while (n.firstChild) n.removeChild(n.firstChild);
  }

  function showAddedTool(type) {
    activeAdded = type;
    var area = els.addedArea;
    if (!area) return;
    clearNode(area);
    area.classList.add('on');

    function addLabel(forId, text) {
      area.appendChild(el('label', { htmlFor: forId, text: text }));
    }
    function addTa(id, rows) {
      var ta = el('textarea', { id: id, rows: String(rows || 3), 'aria-label': id });
      area.appendChild(ta);
      return ta;
    }
    function addIn(id, ph) {
      var inp = el('input', { id: id, 'aria-label': ph || id, placeholder: ph || '' });
      area.appendChild(inp);
      return inp;
    }
    function addOut(id) {
      var o = el('div', { className: 'out', id: id, role: 'status', text: '' });
      area.appendChild(o);
      return o;
    }
    function addBtns(defs) {
      var row = el('div', { className: 'btn-row' });
      defs.forEach(function (d) {
        row.appendChild(el('button', { type: 'button', id: d[0], className: 'b' + (d[2] ? ' gold' : ''), text: d[1] }));
      });
      area.appendChild(row);
      return row;
    }

    if (type === 'qr') {
      addLabel('tt-qr-in', 'Text to share (local payload only — not transmitted)');
      addTa('tt-qr-in', 3);
      addBtns([['tt-qr-go', 'Generate Payload', true], ['tt-qr-copy', 'Copy', false]]);
      addOut('tt-qr-out');
      area.appendChild(el('p', { className: 'hint', text: 'Payload is plain text. Use any offline QR app if needed. Nothing is sent from this cartridge.' }));
      q('#tt-qr-go').onclick = function () {
        var txt = clampText(q('#tt-qr-in').value.trim(), MAX_TEXT);
        setOut(q('#tt-qr-out'), txt ? 'TT1_SHARE:' + txt : '');
      };
      q('#tt-qr-copy').onclick = function () { copyText(q('#tt-qr-out').textContent, q('#tt-qr-copy')); };
    } else if (type === 'clipboard') {
      addLabel('tt-clip-in', 'Paste text');
      addTa('tt-clip-in', 4);
      addBtns([['tt-clip-space', 'Remove Extra Spaces', false], ['tt-clip-breaks', 'Remove Line Breaks', false]]);
      addOut('tt-clip-out');
      q('#tt-clip-space').onclick = function () {
        setOut(q('#tt-clip-out'), q('#tt-clip-in').value.replace(/\s+/g, ' ').trim());
      };
      q('#tt-clip-breaks').onclick = function () {
        setOut(q('#tt-clip-out'), q('#tt-clip-in').value.replace(/[\r\n]+/g, ' ').trim());
      };
    } else if (type === 'urlclean') {
      addLabel('tt-url-in', 'URL');
      addIn('tt-url-in', 'https://example.com/?utm_source=x');
      addBtns([['tt-url-go', 'Clean', true]]);
      addOut('tt-url-out');
      q('#tt-url-go').onclick = function () {
        try {
          var u = new URL(q('#tt-url-in').value);
          if (u.protocol !== 'http:' && u.protocol !== 'https:') {
            setOut(q('#tt-url-out'), 'Only http/https URLs');
            return;
          }
          ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid'].forEach(function (k) {
            u.searchParams.delete(k);
          });
          setOut(q('#tt-url-out'), u.toString());
        } catch (e) {
          setOut(q('#tt-url-out'), 'Invalid URL');
        }
      };
    } else if (type === 'urlencode') {
      addLabel('tt-urlenc-in', 'Text');
      addTa('tt-urlenc-in', 2);
      addBtns([['tt-urlenc-enc', 'Encode', false], ['tt-urlenc-dec', 'Decode', false]]);
      addOut('tt-urlenc-out');
      q('#tt-urlenc-enc').onclick = function () {
        setOut(q('#tt-urlenc-out'), encodeURIComponent(clampText(q('#tt-urlenc-in').value, MAX_TEXT)));
      };
      q('#tt-urlenc-dec').onclick = function () {
        try {
          setOut(q('#tt-urlenc-out'), decodeURIComponent(q('#tt-urlenc-in').value));
        } catch (e) {
          setOut(q('#tt-urlenc-out'), 'Decode error');
        }
      };
    } else if (type === 'base64') {
      addLabel('tt-b64-in', 'Text');
      addTa('tt-b64-in', 2);
      addBtns([['tt-b64-enc', 'Encode', false], ['tt-b64-dec', 'Decode', false]]);
      addOut('tt-b64-out');
      q('#tt-b64-enc').onclick = function () {
        try {
          setOut(q('#tt-b64-out'), btoa(unescape(encodeURIComponent(clampText(q('#tt-b64-in').value, MAX_TEXT)))));
        } catch (e) {
          setOut(q('#tt-b64-out'), 'Encode error');
        }
      };
      q('#tt-b64-dec').onclick = function () {
        try {
          setOut(q('#tt-b64-out'), decodeURIComponent(escape(atob(q('#tt-b64-in').value))));
        } catch (e) {
          setOut(q('#tt-b64-out'), 'Decode error');
        }
      };
    } else if (type === 'json') {
      addLabel('tt-json-in', 'JSON');
      addTa('tt-json-in', 4);
      addBtns([['tt-json-go', 'Format', true]]);
      addOut('tt-json-out');
      q('#tt-json-go').onclick = function () {
        try {
          var parsed = JSON.parse(q('#tt-json-in').value);
          setOut(q('#tt-json-out'), JSON.stringify(parsed, null, 2));
        } catch (e) {
          setOut(q('#tt-json-out'), 'Invalid JSON: ' + e.message);
        }
      };
    } else if (type === 'htmlent') {
      addLabel('tt-he-in', 'Text');
      addTa('tt-he-in', 2);
      addBtns([['tt-he-enc', 'Encode', false], ['tt-he-dec', 'Decode', false]]);
      addOut('tt-he-out');
      q('#tt-he-enc').onclick = function () {
        setOut(q('#tt-he-out'), encodeHtmlEntities(q('#tt-he-in').value));
      };
      q('#tt-he-dec').onclick = function () {
        setOut(q('#tt-he-out'), decodeHtmlEntitiesSafe(q('#tt-he-in').value));
      };
    } else if (type === 'md') {
      addLabel('tt-md-in', 'Markdown (text preview only — not HTML)');
      addTa('tt-md-in', 4);
      addOut('tt-md-out');
      q('#tt-md-in').addEventListener('input', function () {
        setOut(q('#tt-md-out'), mdPreviewText(clampText(q('#tt-md-in').value, MAX_TEXT)) || '—');
      });
    } else if (type === 'emergency') {
      area.appendChild(row2(
        field('tt-em-name', 'Name', el('input', { id: 'tt-em-name', 'aria-label': 'Name' })),
        field('tt-em-contact', 'Emergency Contact', el('input', { id: 'tt-em-contact', 'aria-label': 'Emergency Contact' }))
      ));
      area.appendChild(row2(
        field('tt-em-allergy', 'Allergies', el('input', { id: 'tt-em-allergy', 'aria-label': 'Allergies' })),
        field('tt-em-meds', 'Meds', el('input', { id: 'tt-em-meds', 'aria-label': 'Meds' }))
      ));
      addLabel('tt-em-notes', 'Notes');
      addTa('tt-em-notes', 2);
      addBtns([['tt-em-save', 'Save', true], ['tt-em-copy', 'Copy Card', false]]);
      area.appendChild(el('div', { className: 'status-line', id: 'tt-em-status', role: 'status', text: '' }));
      var em = parseEmergencyStored();
      ['name', 'contact', 'allergy', 'meds', 'notes'].forEach(function (f) {
        var node = q('#tt-em-' + f);
        if (node) node.value = em[f] || '';
      });
      q('#tt-em-save').onclick = function () {
        var obj = {};
        ['name', 'contact', 'allergy', 'meds', 'notes'].forEach(function (f) {
          obj[f] = clampText((q('#tt-em-' + f).value || '').trim(), MAX_FIELD);
        });
        obj = validateEmergency(obj);
        safeSet('emergency', JSON.stringify(obj));
        setOut(q('#tt-em-status'), 'Saved locally (this browser only)');
      };
      q('#tt-em-copy').onclick = function () {
        var obj = parseEmergencyStored();
        var txt = 'EMERGENCY CARD\nName: ' + obj.name + '\nContact: ' + obj.contact +
          '\nAllergies: ' + obj.allergy + '\nMeds: ' + obj.meds + '\nNotes: ' + obj.notes;
        copyText(txt, q('#tt-em-copy')).then(function (ok) {
          setOut(q('#tt-em-status'), ok ? 'Copied' : 'Copy denied or failed');
        });
      };
    } else if (type === 'device') {
      var dout = addOut('tt-dev-out');
      var lsOk = false;
      try {
        localStorage.setItem(STORAGE_PREFIX + '_probe', '1');
        localStorage.removeItem(STORAGE_PREFIX + '_probe');
        lsOk = true;
      } catch (e) { lsOk = false; }
      setOut(dout,
        'Screen: ' + window.innerWidth + 'x' + window.innerHeight +
        '\nOnline: ' + navigator.onLine +
        '\nUA: ' + String(navigator.userAgent || '').slice(0, 80) +
        '\nlocalStorage: ' + (lsOk ? 'YES' : 'NO') +
        '\nServiceWorker: ' + ('serviceWorker' in navigator ? 'YES' : 'NO') +
        '\nCrypto: ' + (window.crypto && crypto.getRandomValues ? 'YES' : 'NO') +
        '\nVersion: ' + VERSION +
        '\nStorage prefix: ' + STORAGE_PREFIX
      );
    } else if (type === 'sha') {
      addLabel('tt-sha-in', 'Text to hash (local only)');
      addTa('tt-sha-in', 3);
      addBtns([['tt-sha-go', 'SHA-256', true]]);
      addOut('tt-sha-out');
      q('#tt-sha-go').onclick = function () {
        var txt = clampText(q('#tt-sha-in').value, MAX_TEXT);
        if (!(window.crypto && crypto.subtle)) {
          setOut(q('#tt-sha-out'), 'Web Crypto unavailable');
          return;
        }
        var data = new TextEncoder().encode(txt);
        crypto.subtle.digest('SHA-256', data).then(function (buf) {
          var arr = Array.prototype.slice.call(new Uint8Array(buf));
          setOut(q('#tt-sha-out'), arr.map(function (b) {
            return (b < 16 ? '0' : '') + b.toString(16);
          }).join(''));
        }).catch(function () {
          setOut(q('#tt-sha-out'), 'Hash failed');
        });
      };
    } else if (type === 'regex') {
      addLabel('tt-re-pat', 'Pattern');
      addIn('tt-re-pat', 'e.g. \\w+');
      addLabel('tt-re-flags', 'Flags');
      addIn('tt-re-flags', 'g');
      addLabel('tt-re-in', 'Test text');
      addTa('tt-re-in', 3);
      addBtns([['tt-re-go', 'Test', true]]);
      addOut('tt-re-out');
      q('#tt-re-go').onclick = function () {
        try {
          var flags = String(q('#tt-re-flags').value || '').replace(/[^gimsuy]/g, '').slice(0, 6);
          var re = new RegExp(q('#tt-re-pat').value, flags);
          var text = clampText(q('#tt-re-in').value, MAX_TEXT);
          var matches = text.match(re);
          setOut(q('#tt-re-out'), matches ? ('matches: ' + matches.length + '\n' + matches.slice(0, 50).join('\n')) : 'No matches');
        } catch (e) {
          setOut(q('#tt-re-out'), 'Invalid regex: ' + e.message);
        }
      };
    } else if (type === 'occ') {
      addLabel('tt-occ-in', 'OCC / package text (inspect only — never executed)');
      addTa('tt-occ-in', 5);
      addBtns([['tt-occ-go', 'Inspect', true]]);
      addOut('tt-occ-out');
      q('#tt-occ-go').onclick = function () {
        var v = clampText(q('#tt-occ-in').value, MAX_TEXT);
        var lines = v.split(/\r?\n/).length;
        var hasScript = /<script/i.test(v);
        var hasB64 = /[A-Za-z0-9+/]{40,}={0,2}/.test(v);
        setOut(q('#tt-occ-out'),
          'chars: ' + v.length +
          '\nlines: ' + lines +
          '\ncontains <script: ' + (hasScript ? 'YES (inert — not run)' : 'no') +
          '\nlong base64-like run: ' + (hasB64 ? 'yes' : 'no') +
          '\nnote: inspection only; no decode/exec network'
        );
      };
    }
  }

  function setLang(code) {
    lang = sanitizeLang(code);
    safeSet('lang', lang);
    applyLabels();
    // rebuild tab labels without wiping tool state: update known labels
    var map = {
      pw: 'password', pad: 'pad', tally: 'tally', timer: 'timer',
      words: 'words', pick: 'pick', fmt: 'fmt'
    };
    qa('[data-tt-tab]').forEach(function (b) {
      var k = b.getAttribute('data-tt-tab');
      if (map[k]) b.textContent = t(map[k]);
    });
    var title = document.getElementById('tt-title');
    if (title) title.textContent = t('title');
    var sub = document.getElementById('tt-sub');
    if (sub) sub.textContent = t('sub');
    var free = document.getElementById('tt-free');
    if (free) free.textContent = t('free');
    var ah = document.getElementById('tt-added-h');
    if (ah) ah.textContent = t('added');
    var adv = document.getElementById('tt-adv-h');
    if (adv) adv.textContent = t('advanced');
    var en = document.getElementById('lang-en');
    var es = document.getElementById('lang-es');
    if (en) {
      en.className = 'lang-btn' + (lang === 'en' ? ' on' : '');
      en.setAttribute('aria-pressed', lang === 'en' ? 'true' : 'false');
    }
    if (es) {
      es.className = 'lang-btn' + (lang === 'es' ? ' on' : '');
      es.setAttribute('aria-pressed', lang === 'es' ? 'true' : 'false');
    }
    document.documentElement.lang = lang;
    document.title = lang === 'es' ? 'Herramientas Técnicas' : 'Tech Tools';
  }

  function applyLabels() {
    document.documentElement.lang = lang;
    document.title = lang === 'es' ? 'Herramientas Técnicas' : 'Tech Tools';
  }

  function getState() {
    return {
      version: VERSION,
      shortCode: SHORT_CODE,
      lang: lang,
      activeCore: activeCore,
      activeAdded: activeAdded,
      tally: tallyN,
      storagePrefix: STORAGE_PREFIX,
      timerRunning: !!timerHandle
    };
  }

  // Public API for tests
  window.TT = {
    VERSION: VERSION,
    SHORT_CODE: SHORT_CODE,
    setLang: setLang,
    showCore: showCore,
    showAddedTool: showAddedTool,
    getState: getState,
    encodeHtmlEntities: encodeHtmlEntities,
    decodeHtmlEntitiesSafe: decodeHtmlEntitiesSafe,
    mdPreviewText: mdPreviewText,
    validateEmergency: validateEmergency,
    STORAGE_PREFIX: STORAGE_PREFIX
  };

  function init() {
    lang = sanitizeLang(safeGet('lang', 'en'));
    buildUI();
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('sw.js').catch(function () {});
    }
  }

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