JobFiller Pro

Auto-scroll job listings, fill applications, and submit your CV/Resume on Indeed, Jobs.cz, Prace.cz and more.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @license MIT
// @name         JobFiller Pro
// @namespace    https://github.com/jobfiller-pro
// @version      2.1.0
// @description  Auto-scroll job listings, fill applications, and submit your CV/Resume on Indeed, Jobs.cz, Prace.cz and more.
// @author       JobFiller Pro
// @match        *://*.indeed.com/*
// @match        *://*.indeed.co.uk/*
// @match        *://*.jobs.cz/*
// @match        *://*.prace.cz/*
// @match        *://*.linkedin.com/*
// @match        *://*.glassdoor.com/*
// @match        *://*.pracezarohem.cz/*
// @match        *://*.jobscanner.cz/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ═══════════════════════════════════════════════════════════════════════════
  //  STORAGE  (GM_getValue / GM_setValue wrappers)
  // ═══════════════════════════════════════════════════════════════════════════
  const Store = {
    get: (key, def) => GM_getValue(key, def),
    set: (key, val) => GM_setValue(key, val),
    del: (key) => GM_deleteValue(key),
    getProfile: () => GM_getValue('profile', {}),
    setProfile: (p) => GM_setValue('profile', p),
    getSettings: () => GM_getValue('settings', {
      autoScroll: false,
      autoFill: true,
      autoSubmit: false,
      confirmSubmit: true,
      highlightFields: true,
      scrollDelay: 2500,
      fillDelay: 800,
      filePriority: 'cv',
    }),
    setSettings: (s) => GM_setValue('settings', s),
    getCVMeta: () => GM_getValue('cvMeta', null),
    setCVMeta: (m) => GM_setValue('cvMeta', m),
    getCVData: () => GM_getValue('cvData', null),
    setCVData: (d) => GM_setValue('cvData', d),
    getResumeMeta: () => GM_getValue('resumeMeta', null),
    setResumeMeta: (m) => GM_setValue('resumeMeta', m),
    getResumeData: () => GM_getValue('resumeData', null),
    setResumeData: (d) => GM_setValue('resumeData', d),
    getLogs: () => GM_getValue('logs', []),
    addLog: (msg, type = 'info') => {
      const logs = GM_getValue('logs', []);
      logs.push({ msg, type, time: new Date().toTimeString().slice(0, 5) });
      if (logs.length > 100) logs.splice(0, logs.length - 100);
      GM_setValue('logs', logs);
    },
  };

  // ═══════════════════════════════════════════════════════════════════════════
  //  FIELD DETECTION ENGINE
  // ═══════════════════════════════════════════════════════════════════════════
  const FIELD_MAP = {
    firstName: {
      labels: ['first name', 'given name', 'jméno', 'kerstián'],
      attrs:  ['firstname', 'first_name', 'fname', 'given_name', 'applicant_first'],
    },
    lastName: {
      labels: ['last name', 'surname', 'příjmení', 'family name'],
      attrs:  ['lastname', 'last_name', 'lname', 'surname', 'family_name', 'applicant_last'],
    },
    fullName: {
      labels: ['full name', 'celé jméno', 'jméno a příjmení', 'your name', '^name$'],
      attrs:  ['fullname', 'full_name', 'name', 'applicant_name'],
    },
    email: {
      labels: ['email', 'e-mail', 'emailová adresa', 'email address'],
      attrs:  ['email', 'email_address', 'e_mail', 'applicant_email'],
      inputType: 'email',
    },
    phone: {
      labels: ['phone', 'telephone', 'telefon', 'tel', 'mobil', 'phone number'],
      attrs:  ['phone', 'tel', 'telephone', 'mobile', 'phone_number', 'applicant_phone'],
      inputType: 'tel',
    },
    street: {
      labels: ['street', 'address', 'ulice', 'adresa', 'street address'],
      attrs:  ['street', 'address', 'address1', 'addr1', 'street_address'],
    },
    city: {
      labels: ['city', 'town', 'město', 'obec'],
      attrs:  ['city', 'town', 'locality'],
    },
    zip: {
      labels: ['zip', 'postal', 'postcode', 'psč', 'psc', 'zip code'],
      attrs:  ['zip', 'zipcode', 'postal_code', 'postcode'],
    },
    country: {
      labels: ['country', 'stát', 'země', 'zeme'],
      attrs:  ['country', 'nation'],
    },
    linkedin: {
      labels: ['linkedin', 'linkedin url', 'linkedin profile'],
      attrs:  ['linkedin', 'linkedin_url'],
      inputType: 'url',
    },
    coverLetter: {
      labels: ['cover letter', 'motivační dopis', 'covering letter', 'motivation', 'message', 'about yourself', 'tell us', 'additional information', 'o sobě'],
      attrs:  ['cover_letter', 'coverletter', 'motivation', 'message', 'additional_info'],
      tag:    'textarea',
    },
  };

  function norm(s) { return (s || '').toLowerCase().replace(/[\s_\-\.]+/g, ' ').trim(); }

  function getLabelText(el) {
    if (el.id) {
      const lbl = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
      if (lbl) return lbl.innerText;
    }
    const wrap = el.closest('label');
    if (wrap) return wrap.innerText;
    let sib = el.previousElementSibling;
    while (sib) {
      if (sib.matches('label, [class*="label"], [class*="Label"]')) return sib.innerText;
      sib = sib.previousElementSibling;
    }
    const parent = el.parentElement;
    if (parent) {
      const lbl = parent.querySelector('label, [class*="label"]');
      if (lbl) return lbl.innerText;
    }
    return el.getAttribute('aria-label') || el.getAttribute('placeholder') || '';
  }

  function detectIntent(el) {
    const tag = el.tagName.toLowerCase();
    const type = (el.getAttribute('type') || '').toLowerCase();
    const nameId = norm(el.name + ' ' + el.id + ' ' + (el.getAttribute('autocomplete') || ''));
    const labelText = norm(getLabelText(el));
    const placeholder = norm(el.getAttribute('placeholder') || '');
    const combined = nameId + ' ' + labelText + ' ' + placeholder;

    for (const [intent, rules] of Object.entries(FIELD_MAP)) {
      if (rules.tag && tag !== rules.tag) continue;
      if (rules.inputType && type !== rules.inputType && type !== 'text' && type !== '') continue;
      if (tag === 'input' && ['hidden','file','submit','button','checkbox','radio','image'].includes(type)) continue;

      for (const attr of (rules.attrs || [])) {
        if (combined.includes(norm(attr))) return intent;
      }
      for (const lbl of (rules.labels || [])) {
        if (combined.includes(norm(lbl))) return intent;
      }
    }
    return null;
  }

  function buildValueMap(profile) {
    const full = [profile.firstName, profile.lastName].filter(Boolean).join(' ');
    return { ...profile, fullName: full };
  }

  function fillInput(el, value) {
    if (!value || el.disabled || el.readOnly) return false;
    const tag = el.tagName.toLowerCase();

    if (tag === 'select') {
      const v = value.toLowerCase();
      const opt = Array.from(el.options).find(o =>
        o.value.toLowerCase().includes(v) || o.text.toLowerCase().includes(v)
      );
      if (!opt) return false;
      el.value = opt.value;
      el.dispatchEvent(new Event('change', { bubbles: true }));
      return true;
    }

    // Native setter to bypass React/Vue controlled inputs
    const proto = tag === 'textarea' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
    const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
    if (setter) setter.call(el, value);
    else el.value = value;

    ['input', 'change', 'blur'].forEach(evt =>
      el.dispatchEvent(new Event(evt, { bubbles: true }))
    );
    el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'a' }));
    return true;
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  FILE INJECTION
  // ═══════════════════════════════════════════════════════════════════════════
  function b64toFile(b64, meta) {
    const binary = atob(b64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
    return new File([bytes], meta.name, { type: meta.type || 'application/pdf' });
  }

  function injectFile(input, b64, meta) {
    try {
      const file = b64toFile(b64, meta);
      const dt = new DataTransfer();
      dt.items.add(file);
      input.files = dt.files;
      input.dispatchEvent(new Event('change', { bubbles: true }));
      input.dispatchEvent(new Event('input', { bubbles: true }));
      return true;
    } catch (e) {
      return false;
    }
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  HIGHLIGHT
  // ═══════════════════════════════════════════════════════════════════════════
  function highlight(el) {
    if (!Store.getSettings().highlightFields) return;
    el.style.setProperty('outline', '2px solid #00e5a0', 'important');
    el.style.setProperty('outline-offset', '2px', 'important');
    setTimeout(() => { el.style.removeProperty('outline'); el.style.removeProperty('outline-offset'); }, 3000);
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  MAIN FILL FUNCTION
  // ═══════════════════════════════════════════════════════════════════════════
  function fillPage() {
    const profile = Store.getProfile();
    const settings = Store.getSettings();
    if (!profile || !profile.email) return { filled: 0, files: 0, error: 'No profile set up' };

    const valueMap = buildValueMap(profile);
    let filled = 0, filesInjected = 0;
    const done = new Set();

    // Text / select / textarea
    const els = document.querySelectorAll(
      'input:not([type="hidden"]):not([type="file"]):not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]), textarea, select'
    );

    for (const el of els) {
      if (done.has(el)) continue;
      if (el.value && el.value.trim().length > 3) continue; // already filled

      const intent = detectIntent(el);
      if (!intent) continue;
      const val = valueMap[intent];
      if (!val) continue;

      if (fillInput(el, val)) {
        highlight(el);
        done.add(el);
        filled++;
      }
    }

    // File inputs
    const cvData = Store.getCVData(), cvMeta = Store.getCVMeta();
    const resumeData = Store.getResumeData(), resumeMeta = Store.getResumeMeta();
    const fileInputs = Array.from(document.querySelectorAll('input[type="file"]'));
    const priority = settings.filePriority || 'cv';

    fileInputs.forEach((inp, idx) => {
      let data, meta;
      if (fileInputs.length === 1) {
        data = priority === 'cv' ? (cvData || resumeData) : (resumeData || cvData);
        meta = priority === 'cv' ? (cvMeta || resumeMeta) : (resumeMeta || cvMeta);
      } else {
        data = idx === 0 ? cvData : resumeData;
        meta = idx === 0 ? cvMeta : resumeMeta;
      }
      if (data && meta && injectFile(inp, data, meta)) {
        highlight(inp);
        filesInjected++;
      }
    });

    const total = filled + filesInjected;
    Store.addLog(`Filled ${filled} fields, ${filesInjected} files on ${location.hostname}`, total > 0 ? 'success' : 'info');
    return { filled, files: filesInjected };
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  AUTO-SCROLL + AUTO-APPLY ENGINE
  // ═══════════════════════════════════════════════════════════════════════════
  let scrollTimer = null;
  let isScrolling = false;

  // Site-specific: find job listing links on the current page
  const LISTING_SELECTORS = {
    'indeed.com':    'a.jcs-JobTitle, a[data-jk], .jobTitle a, a[id^="job_"]',
    'jobs.cz':       'a.SearchResultCard__titleLink, a[data-jobad-id], .job-listing a[href*="/detail/"]',
    'prace.cz':      'a.job-title, a[href*="/nabidka/"], .job-card a',
    'linkedin.com':  'a.job-card-list__title, a[href*="/jobs/view/"]',
    'glassdoor.com': 'a[data-test="job-link"], a[href*="/job-listing/"]',
  };

  function getListingSelector() {
    const host = location.hostname;
    for (const [domain, sel] of Object.entries(LISTING_SELECTORS)) {
      if (host.includes(domain)) return sel;
    }
    return 'a[href*="job"], a[href*="position"], a[href*="apply"], a[href*="career"]';
  }

  function getJobLinks() {
    const sel = getListingSelector();
    const links = Array.from(document.querySelectorAll(sel));
    return links.filter(l => {
      const href = l.href || '';
      return href && !href.includes('#') && href !== location.href;
    });
  }

  let scrollQueue = [];
  let scrollIndex = 0;
  let scrollActive = false;

  function startAutoScroll() {
    if (scrollActive) return;
    scrollActive = true;
    scrollQueue = getJobLinks();
    scrollIndex = 0;

    if (scrollQueue.length === 0) {
      showToast('⚠️ No job listings found on this page');
      scrollActive = false;
      return;
    }

    Store.set('scrollStep', 'detail'); // reset step state for fresh run
    showToast(`🔍 Found ${scrollQueue.length} job listings — starting...`);
    Store.addLog(`Auto-scroll started: ${scrollQueue.length} listings found`, 'info');
    processNextListing();
  }

  function stopAutoScroll() {
    scrollActive = false;
    clearTimeout(scrollTimer);
    updatePanel();
    showToast('⏹ Auto-scroll stopped');
    Store.addLog('Auto-scroll stopped by user', 'info');
  }

  function processNextListing() {
    if (!scrollActive || scrollIndex >= scrollQueue.length) {
      scrollActive = false;
      showToast(`✅ Done! Processed ${scrollIndex} listings`);
      Store.addLog(`Auto-scroll complete: ${scrollIndex} listings processed`, 'success');
      updatePanel();
      return;
    }

    const link = scrollQueue[scrollIndex];
    scrollIndex++;

    // Scroll to the link and highlight it
    link.scrollIntoView({ behavior: 'smooth', block: 'center' });
    link.style.setProperty('outline', '2px solid #7c5cfc', 'important');
    setTimeout(() => link.style.removeProperty('outline'), 1500);

    updatePanel();

    const settings = Store.getSettings();
    const delay = parseInt(settings.scrollDelay) || 2500;

    // Open the job in the same tab after delay
    scrollTimer = setTimeout(() => {
      if (!scrollActive) return;
      // Store queue state so we can resume on the listing page
      Store.set('scrollQueue', scrollQueue.map(l => l.href));
      Store.set('scrollIndex', scrollIndex);
      Store.set('scrollStep', 'detail'); // fresh start: look for reply button first
      Store.set('scrollActive', true);
      window.location.href = link.href;
    }, delay);
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  REPLY BUTTON DETECTION  (Czech + English job sites)
  // ═══════════════════════════════════════════════════════════════════════════

  // Keywords that mean "apply / reply" — NOT submit-after-filling
  const REPLY_KEYWORDS = [
    'odpovědět', 'odpovedet', 'reagovat', 'přihlásit se', 'prihlasit se',
    'apply now', 'apply for', 'quick apply', 'easy apply',
    'send application', 'submit application',
  ];

  // Keywords that mean "send/submit the filled form"
  const SUBMIT_KEYWORDS = [
    'odeslat', 'potvrdit', 'dokončit', 'dokoncit',
    'submit', 'send', 'confirm', 'finish', 'complete',
    'přihlásit', 'prihlasit',
  ];

  function findReplyButton() {
    // Look for <a> or <button> whose visible text matches reply keywords
    const candidates = Array.from(document.querySelectorAll(
      'a[href], button, input[type="button"], input[type="submit"], [role="button"]'
    ));
    for (const el of candidates) {
      if (el.offsetParent === null) continue; // skip hidden
      const txt = (el.textContent || el.value || el.getAttribute('aria-label') || '').trim().toLowerCase();
      if (REPLY_KEYWORDS.some(k => txt.includes(k))) return el;
    }
    return null;
  }

  function findSubmitButton() {
    const candidates = Array.from(document.querySelectorAll(
      'button[type="submit"], input[type="submit"], button, [role="button"]'
    ));
    // Prefer exact submit-type buttons first
    const submitType = candidates.find(el => {
      if (el.offsetParent === null) return false;
      const txt = (el.textContent || el.value || '').trim().toLowerCase();
      return SUBMIT_KEYWORDS.some(k => txt.includes(k));
    });
    if (submitType) return submitType;

    // Fallback: any submit-type input
    return document.querySelector('button[type="submit"], input[type="submit"]') || null;
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  SCROLL SESSION STATE MACHINE
  //  States: 'listing' → 'detail' → 'form' → back to 'listing'
  // ═══════════════════════════════════════════════════════════════════════════

  // Called every time a page loads during an active scroll session
  function resumeScrollMode() {
    const settings = Store.getSettings();
    const fillDelay = parseInt(settings.fillDelay) || 800;
    const scrollDelay = parseInt(settings.scrollDelay) || 2500;
    const step = Store.get('scrollStep', 'detail'); // 'detail' or 'form'

    if (step === 'detail') {
      // We just landed on the job detail page.
      // Look for a reply/apply button and click it automatically.
      showToast('🔍 Looking for reply button...');

      waitForElement(findReplyButton, 4000).then(btn => {
        if (btn) {
          btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
          const label = (btn.textContent || btn.value || '').trim();
          showToast(`👆 Clicking "${label}"...`);
          Store.addLog(`Clicking reply button: "${label}"`, 'info');

          setTimeout(() => {
            // If it's a link that navigates to a new page, set step to 'form'
            const href = btn.getAttribute('href');
            if (href && href !== '#' && !href.startsWith('javascript')) {
              Store.set('scrollStep', 'form');
              Store.set('scrollActive', true);
              btn.click();
            } else {
              // It might open a modal/inline form on the same page
              btn.click();
              Store.set('scrollStep', 'form');
              // Wait for form to appear then fill it
              setTimeout(() => {
                fillAndSubmitOrBack(settings, scrollDelay);
              }, fillDelay + 500);
            }
          }, 600);
        } else {
          // No reply button — maybe this IS the form already, or it's already applied
          showToast('⚠️ No reply button found — trying to fill directly');
          Store.addLog('No reply button found, attempting direct fill', 'info');
          Store.set('scrollStep', 'form');
          setTimeout(() => fillAndSubmitOrBack(settings, scrollDelay), fillDelay);
        }
      });

    } else if (step === 'form') {
      // We're on the application form page. Fill it.
      showToast('⚡ Filling application form...');

      setTimeout(() => {
        fillAndSubmitOrBack(settings, scrollDelay);
      }, fillDelay);
    }
  }

  function fillAndSubmitOrBack(settings, scrollDelay) {
    const result = fillPage();
    showToast(`✅ Filled ${result.filled} fields, ${result.files} files`);

    if (settings.autoSubmit) {
      setTimeout(() => {
        if (settings.confirmSubmit) {
          if (confirm('[JobFiller Pro]\nSubmit this application?\n\nOK = Submit & continue\nCancel = Skip & go back')) {
            clickSubmit();
          } else {
            goBackToListings();
          }
        } else {
          clickSubmit();
        }
      }, 800);
    } else {
      // No auto-submit: just go back after a pause so user can review
      const pause = Math.max(scrollDelay, 2000);
      showToast(`👀 Review & go back in ${Math.round(pause/1000)}s... (or stop scroll)`);
      Store.set('scrollStep', 'detail'); // reset for next listing
      setTimeout(goBackToListings, pause);
    }
  }

  function clickSubmit() {
    const btn = findSubmitButton();
    if (btn) {
      btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
      setTimeout(() => {
        btn.click();
        Store.addLog(`Submitted on ${location.hostname}`, 'success');
        showToast('📨 Application submitted!');
        Store.set('scrollStep', 'detail'); // reset for next listing
        setTimeout(goBackToListings, 2500);
      }, 400);
    } else {
      showToast('⚠️ Submit button not found — going back');
      Store.addLog('Submit button not found', 'error');
      Store.set('scrollStep', 'detail');
      setTimeout(goBackToListings, 2000);
    }
  }

  function goBackToListings() {
    Store.set('scrollActive', true);
    // scrollStep stays as 'detail' for the next job
    history.back();
  }

  // Helper: poll for an element up to `timeout` ms
  function waitForElement(finder, timeout = 4000) {
    return new Promise(resolve => {
      const found = finder();
      if (found) { resolve(found); return; }
      const start = Date.now();
      const interval = setInterval(() => {
        const el = finder();
        if (el) { clearInterval(interval); resolve(el); return; }
        if (Date.now() - start > timeout) { clearInterval(interval); resolve(null); }
      }, 250);
    });
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  TOAST NOTIFICATION
  // ═══════════════════════════════════════════════════════════════════════════
  let toastTimer;
  function showToast(msg, duration = 3000) {
    let el = document.getElementById('jfp-toast');
    if (!el) {
      el = document.createElement('div');
      el.id = 'jfp-toast';
      document.body.appendChild(el);
    }
    el.textContent = msg;
    el.style.cssText = `
      position:fixed!important;bottom:20px!important;right:20px!important;
      background:#0d0d0f!important;color:#00e5a0!important;
      font-family:'Courier New',monospace!important;font-size:13px!important;
      padding:10px 18px!important;border-radius:8px!important;
      border:1px solid #00e5a0!important;
      box-shadow:0 4px 24px rgba(0,229,160,0.25)!important;
      z-index:2147483647!important;
      animation:jfp-in 0.25s ease!important;
      max-width:320px!important;word-break:break-word!important;
    `;
    clearTimeout(toastTimer);
    toastTimer = setTimeout(() => el.remove(), duration);
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  FLOATING PANEL UI
  // ═══════════════════════════════════════════════════════════════════════════
  GM_addStyle(`
    @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@600;700;800&display=swap');
    @keyframes jfp-in { from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)} }
    @keyframes jfp-panel-in { from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)} }
    @keyframes jfp-pulse { 0%,100%{opacity:1}50%{opacity:0.5} }

    #jfp-root * { box-sizing:border-box; margin:0; padding:0; font-family:'Syne',sans-serif; }
    #jfp-root ::-webkit-scrollbar { width:3px }
    #jfp-root ::-webkit-scrollbar-thumb { background:#2a2a35; border-radius:2px }

    #jfp-fab {
      position:fixed!important; bottom:24px!important; right:24px!important;
      width:52px!important; height:52px!important;
      background:linear-gradient(135deg,#00e5a0,#7c5cfc)!important;
      border-radius:14px!important; cursor:pointer!important;
      display:flex!important; align-items:center!important; justify-content:center!important;
      font-size:22px!important; z-index:2147483646!important;
      box-shadow:0 4px 20px rgba(0,229,160,0.4)!important;
      transition:transform 0.2s,box-shadow 0.2s!important;
      user-select:none!important;
    }
    #jfp-fab:hover { transform:scale(1.08)!important; box-shadow:0 6px 28px rgba(0,229,160,0.5)!important; }

    #jfp-panel {
      position:fixed!important; bottom:88px!important; right:24px!important;
      width:360px!important;
      background:#0d0d0f!important;
      border:1px solid #2a2a35!important;
      border-radius:16px!important;
      z-index:2147483645!important;
      box-shadow:0 8px 40px rgba(0,0,0,0.6)!important;
      animation:jfp-panel-in 0.25s ease!important;
      overflow:hidden!important;
      max-height:90vh!important;
      display:flex!important;
      flex-direction:column!important;
    }

    .jfp-header {
      background:linear-gradient(135deg,#0f0f14,#1a1025)!important;
      padding:14px 16px!important;
      border-bottom:1px solid #2a2a35!important;
      display:flex!important; align-items:center!important; gap:10px!important;
    }
    .jfp-logo { font-size:16px!important; font-weight:800!important; color:#e8e8f0!important; }
    .jfp-logo span { color:#00e5a0!important; }
    .jfp-close {
      margin-left:auto!important; cursor:pointer!important; color:#6b6b80!important;
      font-size:18px!important; line-height:1!important; padding:2px 6px!important;
      border-radius:4px!important;
    }
    .jfp-close:hover { color:#e8e8f0!important; background:#1e1e24!important; }

    .jfp-tabs {
      display:flex!important; background:#0d0d0f!important;
      border-bottom:1px solid #2a2a35!important; flex-shrink:0!important;
    }
    .jfp-tab {
      flex:1!important; padding:9px 4px!important; text-align:center!important;
      font-size:10px!important; font-weight:700!important; color:#6b6b80!important;
      border-bottom:2px solid transparent!important; cursor:pointer!important;
      letter-spacing:0.3px!important; transition:all 0.2s!important;
    }
    .jfp-tab.active { color:#00e5a0!important; border-bottom-color:#00e5a0!important; background:rgba(0,229,160,0.04)!important; }
    .jfp-tab:hover:not(.active) { color:#e8e8f0!important; }

    .jfp-body { overflow-y:auto!important; flex:1!important; padding:14px 16px!important; }

    .jfp-section {
      font-size:9px!important; font-weight:700!important; letter-spacing:1.5px!important;
      color:#6b6b80!important; text-transform:uppercase!important;
      margin:12px 0 8px!important;
    }
    .jfp-section:first-child { margin-top:0!important; }

    .jfp-field { display:flex!important; flex-direction:column!important; gap:3px!important; margin-bottom:7px!important; }
    .jfp-field label { font-size:9px!important; font-weight:600!important; color:#6b6b80!important; letter-spacing:0.5px!important; }
    .jfp-field input, .jfp-field textarea, .jfp-field select {
      background:#16161a!important; border:1px solid #2a2a35!important;
      border-radius:6px!important; color:#e8e8f0!important;
      font-family:'DM Mono',monospace!important; font-size:11px!important;
      padding:7px 9px!important; outline:none!important; width:100%!important;
      transition:border-color 0.2s!important;
    }
    .jfp-field input:focus, .jfp-field textarea:focus { border-color:#00e5a0!important; }
    .jfp-field textarea { resize:vertical!important; min-height:55px!important; }

    .jfp-row { display:flex!important; gap:7px!important; }
    .jfp-row .jfp-field { flex:1!important; }

    .jfp-btn {
      display:flex!important; align-items:center!important; justify-content:center!important;
      gap:5px!important; padding:9px 14px!important; border-radius:7px!important;
      border:none!important; cursor:pointer!important; font-family:'Syne',sans-serif!important;
      font-weight:700!important; font-size:11px!important; transition:all 0.18s!important;
      letter-spacing:0.3px!important; width:100%!important; margin-top:10px!important;
    }
    .jfp-btn-primary { background:linear-gradient(135deg,#00e5a0,#00c488)!important; color:#000!important; }
    .jfp-btn-primary:hover { opacity:0.9!important; transform:translateY(-1px)!important; }
    .jfp-btn-scroll { background:linear-gradient(135deg,#7c5cfc,#5a3fd4)!important; color:#fff!important; }
    .jfp-btn-scroll:hover { opacity:0.9!important; transform:translateY(-1px)!important; }
    .jfp-btn-stop { background:rgba(255,77,106,0.15)!important; color:#ff4d6a!important; border:1px solid rgba(255,77,106,0.3)!important; }
    .jfp-btn-stop:hover { background:rgba(255,77,106,0.25)!important; }
    .jfp-btn-secondary { background:#1e1e24!important; color:#e8e8f0!important; border:1px solid #2a2a35!important; flex:1!important; }
    .jfp-btn-secondary:hover { border-color:#00e5a0!important; color:#00e5a0!important; }
    .jfp-btn-row { display:flex!important; gap:7px!important; margin-top:8px!important; }
    .jfp-btn-row .jfp-btn { margin-top:0!important; }

    .jfp-toggle-row {
      display:flex!important; align-items:center!important; justify-content:space-between!important;
      padding:8px 10px!important; background:#16161a!important;
      border-radius:7px!important; border:1px solid #2a2a35!important; margin-bottom:5px!important;
    }
    .jfp-toggle-label { font-size:11px!important; font-weight:600!important; color:#e8e8f0!important; }
    .jfp-toggle-desc { font-size:9px!important; color:#6b6b80!important; margin-top:1px!important; }
    .jfp-toggle { position:relative!important; width:34px!important; height:18px!important; flex-shrink:0!important; }
    .jfp-toggle input { opacity:0!important; width:0!important; height:0!important; }
    .jfp-slider {
      position:absolute!important; inset:0!important;
      background:#2a2a35!important; border-radius:18px!important; cursor:pointer!important; transition:background 0.2s!important;
    }
    .jfp-slider::before {
      content:''!important; position:absolute!important;
      width:12px!important; height:12px!important;
      left:3px!important; top:3px!important;
      background:#fff!important; border-radius:50%!important; transition:transform 0.2s!important;
    }
    .jfp-toggle input:checked + .jfp-slider { background:#00e5a0!important; }
    .jfp-toggle input:checked + .jfp-slider::before { transform:translateX(16px)!important; }

    .jfp-file-area {
      border:1.5px dashed #2a2a35!important; border-radius:7px!important;
      padding:10px 12px!important; cursor:pointer!important; transition:all 0.2s!important;
      display:flex!important; align-items:center!important; gap:9px!important;
      background:#16161a!important; margin-bottom:8px!important;
    }
    .jfp-file-area:hover { border-color:#00e5a0!important; background:rgba(0,229,160,0.04)!important; }
    .jfp-file-area.has-file { border-color:#00e5a0!important; border-style:solid!important; }
    .jfp-file-icon { font-size:20px!important; flex-shrink:0!important; }
    .jfp-file-name { font-family:'DM Mono',monospace!important; font-size:10px!important; color:#00e5a0!important; }
    .jfp-file-prompt { font-size:10px!important; color:#6b6b80!important; }

    .jfp-log {
      background:#16161a!important; border:1px solid #2a2a35!important;
      border-radius:7px!important; padding:8px!important;
      font-family:'DM Mono',monospace!important; font-size:10px!important;
      max-height:150px!important; overflow-y:auto!important; color:#6b6b80!important;
    }
    .jfp-log-entry { padding:2px 0!important; border-bottom:1px solid rgba(255,255,255,0.04)!important; display:flex!important; gap:7px!important; }
    .jfp-log-time { color:#7c5cfc!important; flex-shrink:0!important; }
    .jfp-log-success { color:#00e5a0!important; }
    .jfp-log-error { color:#ff4d6a!important; }

    .jfp-scroll-status {
      background:#16161a!important; border:1px solid #2a2a35!important;
      border-radius:7px!important; padding:10px!important; margin-bottom:8px!important;
    }
    .jfp-scroll-progress {
      font-family:'DM Mono',monospace!important; font-size:11px!important;
      color:#00e5a0!important; margin-bottom:6px!important;
    }
    .jfp-progress-bar-bg {
      background:#2a2a35!important; border-radius:4px!important; height:4px!important; overflow:hidden!important;
    }
    .jfp-progress-bar { height:4px!important; background:linear-gradient(90deg,#00e5a0,#7c5cfc)!important; border-radius:4px!important; transition:width 0.4s!important; }

    .jfp-divider { height:1px!important; background:#2a2a35!important; margin:10px 0!important; }

    .jfp-delay-row { display:flex!important; align-items:center!important; gap:8px!important; margin-bottom:6px!important; }
    .jfp-delay-row label { font-size:9px!important; color:#6b6b80!important; font-weight:600!important; white-space:nowrap!important; }
    .jfp-delay-row input { flex:1!important; }
  `);

  let panelVisible = false;
  let activeTab = 'apply';

  function buildPanel() {
    const profile = Store.getProfile();
    const settings = Store.getSettings();
    const cvMeta = Store.getCVMeta();
    const resumeMeta = Store.getResumeMeta();
    const logs = Store.getLogs();
    const queueLen = Store.get('scrollQueue', []).length;
    const queueIdx = Store.get('scrollIndex', 0);
    const isActive = Store.get('scrollActive', false) && scrollActive;
    const pct = queueLen > 0 ? Math.round((queueIdx / queueLen) * 100) : 0;

    return `
<div id="jfp-root">
  <div id="jfp-fab" title="JobFiller Pro">⚡</div>

  <div id="jfp-panel" style="display:none">
    <div class="jfp-header">
      <span style="font-size:20px">⚡</span>
      <div class="jfp-logo">Job<span>Filler</span> Pro</div>
      <div class="jfp-close" id="jfp-close">✕</div>
    </div>

    <div class="jfp-tabs">
      <div class="jfp-tab ${activeTab==='apply'?'active':''}" data-tab="apply">⚡ Apply</div>
      <div class="jfp-tab ${activeTab==='scroll'?'active':''}" data-tab="scroll">🔍 Scroll</div>
      <div class="jfp-tab ${activeTab==='profile'?'active':''}" data-tab="profile">👤 Profile</div>
      <div class="jfp-tab ${activeTab==='files'?'active':''}" data-tab="files">📎 Files</div>
      <div class="jfp-tab ${activeTab==='settings'?'active':''}" data-tab="settings">⚙</div>
    </div>

    <div class="jfp-body">

      <!-- APPLY TAB -->
      <div class="jfp-pane" id="jfp-pane-apply" style="display:${activeTab==='apply'?'block':'none'}">
        <div class="jfp-section">Current Page</div>
        <p style="font-size:10px;color:#6b6b80;margin-bottom:10px;line-height:1.5;">
          Detects and fills all form fields on this page using your saved profile.
        </p>
        <button class="jfp-btn jfp-btn-primary" id="jfp-fill-now">⚡ Fill This Page Now</button>
        <div class="jfp-btn-row">
          <button class="jfp-btn jfp-btn-secondary" id="jfp-submit-btn">📨 Submit Form</button>
          <button class="jfp-btn jfp-btn-secondary" id="jfp-clear-btn">🧹 Clear Fields</button>
        </div>
        <div class="jfp-divider"></div>
        <div class="jfp-section">Recent Activity</div>
        <div class="jfp-log" id="jfp-mini-log">
          ${logs.slice(-5).reverse().map(l => `
            <div class="jfp-log-entry">
              <span class="jfp-log-time">${l.time}</span>
              <span class="jfp-log-${l.type}">${l.msg}</span>
            </div>`).join('') || '<div style="color:#6b6b80;font-size:10px;padding:4px 0">No activity yet</div>'}
        </div>
      </div>

      <!-- SCROLL TAB -->
      <div class="jfp-pane" id="jfp-pane-scroll" style="display:${activeTab==='scroll'?'block':'none'}">
        <div class="jfp-section">Auto-Scroll & Apply</div>
        <p style="font-size:10px;color:#6b6b80;margin-bottom:10px;line-height:1.5;">
          Scans job listings on this page, opens each one, fills the form, and optionally submits. Stay on a job listing/search page.
        </p>

        <div class="jfp-scroll-status" id="jfp-scroll-status">
          <div class="jfp-scroll-progress" id="jfp-scroll-progress">
            ${isActive ? `Processing ${queueIdx} / ${queueLen}` : 'Ready to scan'}
          </div>
          <div class="jfp-progress-bar-bg">
            <div class="jfp-progress-bar" id="jfp-progress-bar" style="width:${pct}%"></div>
          </div>
        </div>

        ${isActive
          ? `<button class="jfp-btn jfp-btn-stop" id="jfp-stop-scroll">⏹ Stop Auto-Scroll</button>`
          : `<button class="jfp-btn jfp-btn-scroll" id="jfp-start-scroll">🔍 Start Auto-Scroll</button>`
        }

        <div class="jfp-divider"></div>
        <div class="jfp-section">Timing</div>
        <div class="jfp-delay-row">
          <label>Scroll delay (ms)</label>
          <input type="number" id="jfp-scroll-delay" value="${settings.scrollDelay || 2500}" min="500" max="10000" step="250" style="background:#16161a;border:1px solid #2a2a35;border-radius:5px;color:#e8e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:5px 8px;width:90px" />
        </div>
        <div class="jfp-delay-row">
          <label>Fill delay (ms)</label>
          <input type="number" id="jfp-fill-delay" value="${settings.fillDelay || 800}" min="200" max="5000" step="100" style="background:#16161a;border:1px solid #2a2a35;border-radius:5px;color:#e8e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:5px 8px;width:90px" />
        </div>

        <div class="jfp-divider"></div>
        <div class="jfp-toggle-row">
          <div>
            <div class="jfp-toggle-label">Auto-submit</div>
            <div class="jfp-toggle-desc">Clicks submit button after filling</div>
          </div>
          <label class="jfp-toggle">
            <input type="checkbox" id="jfp-auto-submit" ${settings.autoSubmit ? 'checked' : ''} />
            <span class="jfp-slider"></span>
          </label>
        </div>
        <div class="jfp-toggle-row">
          <div>
            <div class="jfp-toggle-label">Confirm before submit</div>
            <div class="jfp-toggle-desc">Show a dialog before each submit</div>
          </div>
          <label class="jfp-toggle">
            <input type="checkbox" id="jfp-confirm-submit" ${settings.confirmSubmit ? 'checked' : ''} />
            <span class="jfp-slider"></span>
          </label>
        </div>
        <button class="jfp-btn jfp-btn-primary" id="jfp-save-scroll-settings" style="margin-top:10px">💾 Save Scroll Settings</button>
      </div>

      <!-- PROFILE TAB -->
      <div class="jfp-pane" id="jfp-pane-profile" style="display:${activeTab==='profile'?'block':'none'}">
        <div class="jfp-section">Personal Info</div>
        <div class="jfp-row">
          <div class="jfp-field"><label>First Name</label><input id="jfp-firstName" value="${esc(profile.firstName)}" placeholder="Jan" /></div>
          <div class="jfp-field"><label>Last Name</label><input id="jfp-lastName" value="${esc(profile.lastName)}" placeholder="Novák" /></div>
        </div>
        <div class="jfp-field"><label>Email</label><input id="jfp-email" type="email" value="${esc(profile.email)}" placeholder="[email protected]" /></div>
        <div class="jfp-row">
          <div class="jfp-field"><label>Phone</label><input id="jfp-phone" value="${esc(profile.phone)}" placeholder="+420 123 456 789" /></div>
          <div class="jfp-field"><label>LinkedIn</label><input id="jfp-linkedin" value="${esc(profile.linkedin)}" placeholder="linkedin.com/in/..." /></div>
        </div>
        <div class="jfp-section">Address</div>
        <div class="jfp-field"><label>Street</label><input id="jfp-street" value="${esc(profile.street)}" placeholder="Náměstí Míru 1" /></div>
        <div class="jfp-row">
          <div class="jfp-field"><label>City</label><input id="jfp-city" value="${esc(profile.city)}" placeholder="Praha" /></div>
          <div class="jfp-field"><label>ZIP</label><input id="jfp-zip" value="${esc(profile.zip)}" placeholder="110 00" /></div>
        </div>
        <div class="jfp-field"><label>Country</label><input id="jfp-country" value="${esc(profile.country)}" placeholder="Czech Republic" /></div>
        <div class="jfp-section">Cover Letter</div>
        <div class="jfp-field"><textarea id="jfp-coverLetter" placeholder="Your cover letter / motivation text...">${esc(profile.coverLetter)}</textarea></div>
        <button class="jfp-btn jfp-btn-primary" id="jfp-save-profile">💾 Save Profile</button>
      </div>

      <!-- FILES TAB -->
      <div class="jfp-pane" id="jfp-pane-files" style="display:${activeTab==='files'?'block':'none'}">
        <div class="jfp-section">CV / Životopis</div>
        <label for="jfp-cv-input">
          <div class="jfp-file-area ${cvMeta ? 'has-file' : ''}" id="jfp-cv-area">
            <div class="jfp-file-icon">📄</div>
            <div>
              <div class="jfp-file-name" id="jfp-cv-name">${cvMeta ? cvMeta.name : 'Click to upload CV'}</div>
              <div class="jfp-file-prompt">${cvMeta ? '✓ Saved · ' + formatSize(cvMeta.size) : 'PDF, DOC, DOCX · max 5MB'}</div>
            </div>
          </div>
        </label>
        <input type="file" id="jfp-cv-input" accept=".pdf,.doc,.docx" style="display:none" />

        <div class="jfp-section">Resume (alternate)</div>
        <label for="jfp-resume-input">
          <div class="jfp-file-area ${resumeMeta ? 'has-file' : ''}" id="jfp-resume-area">
            <div class="jfp-file-icon">📋</div>
            <div>
              <div class="jfp-file-name" id="jfp-resume-name">${resumeMeta ? resumeMeta.name : 'Click to upload Resume'}</div>
              <div class="jfp-file-prompt">${resumeMeta ? '✓ Saved · ' + formatSize(resumeMeta.size) : 'PDF, DOC, DOCX · max 5MB'}</div>
            </div>
          </div>
        </label>
        <input type="file" id="jfp-resume-input" accept=".pdf,.doc,.docx" style="display:none" />

        <div class="jfp-section">Priority (single upload slot)</div>
        <div class="jfp-field">
          <select id="jfp-file-priority" style="background:#16161a;border:1px solid #2a2a35;border-radius:6px;color:#e8e8f0;font-family:'DM Mono',monospace;font-size:11px;padding:7px 9px;width:100%">
            <option value="cv" ${settings.filePriority==='cv'?'selected':''}>CV (primary)</option>
            <option value="resume" ${settings.filePriority==='resume'?'selected':''}>Resume (alternate)</option>
          </select>
        </div>
        <button class="jfp-btn jfp-btn-primary" id="jfp-save-files">💾 Save File Preferences</button>
        <div class="jfp-btn-row">
          <button class="jfp-btn jfp-btn-secondary" id="jfp-clear-cv">🗑 CV</button>
          <button class="jfp-btn jfp-btn-secondary" id="jfp-clear-resume">🗑 Resume</button>
        </div>
      </div>

      <!-- SETTINGS TAB -->
      <div class="jfp-pane" id="jfp-pane-settings" style="display:${activeTab==='settings'?'block':'none'}">
        <div class="jfp-section">Behaviour</div>
        <div class="jfp-toggle-row">
          <div>
            <div class="jfp-toggle-label">Auto-fill on page load</div>
            <div class="jfp-toggle-desc">Fill forms as soon as an apply page opens</div>
          </div>
          <label class="jfp-toggle">
            <input type="checkbox" id="jfp-auto-fill" ${settings.autoFill ? 'checked' : ''} />
            <span class="jfp-slider"></span>
          </label>
        </div>
        <div class="jfp-toggle-row">
          <div>
            <div class="jfp-toggle-label">Highlight filled fields</div>
            <div class="jfp-toggle-desc">Green outline on auto-filled inputs</div>
          </div>
          <label class="jfp-toggle">
            <input type="checkbox" id="jfp-highlight" ${settings.highlightFields !== false ? 'checked' : ''} />
            <span class="jfp-slider"></span>
          </label>
        </div>
        <button class="jfp-btn jfp-btn-primary" id="jfp-save-settings">💾 Save Settings</button>
        <div class="jfp-divider"></div>
        <div class="jfp-section">Data</div>
        <div class="jfp-btn-row">
          <button class="jfp-btn jfp-btn-secondary" id="jfp-export">↑ Export Profile</button>
          <button class="jfp-btn jfp-btn-secondary" id="jfp-import">↓ Import Profile</button>
        </div>
        <button class="jfp-btn jfp-btn-stop" style="margin-top:7px" id="jfp-reset-all">🗑 Reset All Data</button>
      </div>

    </div>
  </div>
</div>`;
  }

  function esc(s) { return (s || '').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
  function formatSize(b) {
    if (!b) return '';
    if (b < 1024) return b + ' B';
    if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
    return (b/1048576).toFixed(1) + ' MB';
  }

  function mount() {
    const old = document.getElementById('jfp-root');
    if (old) old.remove();
    const div = document.createElement('div');
    div.innerHTML = buildPanel();
    document.body.appendChild(div.firstElementChild);
    attachEvents();
  }

  function updatePanel() {
    if (panelVisible) mount();
  }

  function togglePanel() {
    panelVisible = !panelVisible;
    const panel = document.getElementById('jfp-panel');
    if (panel) panel.style.display = panelVisible ? 'flex' : 'none';
  }

  function switchTab(tab) {
    activeTab = tab;
    document.querySelectorAll('.jfp-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
    document.querySelectorAll('.jfp-pane').forEach(p => p.style.display = 'none');
    const pane = document.getElementById('jfp-pane-' + tab);
    if (pane) pane.style.display = 'block';
  }

  function attachEvents() {
    const $ = id => document.getElementById(id);

    $('jfp-fab').onclick = togglePanel;
    $('jfp-close').onclick = togglePanel;

    document.querySelectorAll('.jfp-tab').forEach(t => {
      t.onclick = () => switchTab(t.dataset.tab);
    });

    // Fill now
    const fillNow = $('jfp-fill-now');
    if (fillNow) fillNow.onclick = () => {
      const result = fillPage();
      showToast(`✅ Filled ${result.filled} fields, ${result.files} files`);
      updatePanel();
    };

    // Submit
    const submitBtn = $('jfp-submit-btn');
    if (submitBtn) submitBtn.onclick = () => clickSubmit();

    // Clear fields
    const clearBtn = $('jfp-clear-btn');
    if (clearBtn) clearBtn.onclick = () => {
      document.querySelectorAll('input:not([type="hidden"]):not([type="file"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"]), textarea').forEach(el => {
        if (!el.disabled && !el.readOnly) { el.value = ''; el.dispatchEvent(new Event('input', {bubbles:true})); }
      });
      showToast('🧹 Fields cleared');
    };

    // Scroll controls
    const startBtn = $('jfp-start-scroll');
    if (startBtn) startBtn.onclick = () => { startAutoScroll(); updatePanel(); };
    const stopBtn = $('jfp-stop-scroll');
    if (stopBtn) stopBtn.onclick = () => { stopAutoScroll(); updatePanel(); };

    // Save scroll settings
    const saveScroll = $('jfp-save-scroll-settings');
    if (saveScroll) saveScroll.onclick = () => {
      const s = Store.getSettings();
      s.scrollDelay = parseInt($('jfp-scroll-delay')?.value) || 2500;
      s.fillDelay = parseInt($('jfp-fill-delay')?.value) || 800;
      s.autoSubmit = $('jfp-auto-submit')?.checked || false;
      s.confirmSubmit = $('jfp-confirm-submit')?.checked !== false;
      Store.setSettings(s);
      showToast('✅ Scroll settings saved');
    };

    // Save profile
    const saveProfile = $('jfp-save-profile');
    if (saveProfile) saveProfile.onclick = () => {
      Store.setProfile({
        firstName: $('jfp-firstName')?.value.trim(),
        lastName:  $('jfp-lastName')?.value.trim(),
        email:     $('jfp-email')?.value.trim(),
        phone:     $('jfp-phone')?.value.trim(),
        linkedin:  $('jfp-linkedin')?.value.trim(),
        street:    $('jfp-street')?.value.trim(),
        city:      $('jfp-city')?.value.trim(),
        zip:       $('jfp-zip')?.value.trim(),
        country:   $('jfp-country')?.value.trim(),
        coverLetter: $('jfp-coverLetter')?.value.trim(),
      });
      showToast('✅ Profile saved!');
      Store.addLog('Profile updated', 'success');
    };

    // File uploads
    const cvInput = $('jfp-cv-input');
    if (cvInput) cvInput.onchange = (e) => handleFileUpload(e.target.files[0], 'cv');
    const resumeInput = $('jfp-resume-input');
    if (resumeInput) resumeInput.onchange = (e) => handleFileUpload(e.target.files[0], 'resume');

    // Save file prefs
    const saveFiles = $('jfp-save-files');
    if (saveFiles) saveFiles.onclick = () => {
      const s = Store.getSettings();
      s.filePriority = $('jfp-file-priority')?.value || 'cv';
      Store.setSettings(s);
      showToast('✅ File preferences saved');
    };

    // Clear files
    $('jfp-clear-cv')?.addEventListener('click', () => { Store.del('cvData'); Store.del('cvMeta'); showToast('CV cleared'); updatePanel(); });
    $('jfp-clear-resume')?.addEventListener('click', () => { Store.del('resumeData'); Store.del('resumeMeta'); showToast('Resume cleared'); updatePanel(); });

    // Settings
    const saveSettings = $('jfp-save-settings');
    if (saveSettings) saveSettings.onclick = () => {
      const s = Store.getSettings();
      s.autoFill = $('jfp-auto-fill')?.checked || false;
      s.highlightFields = $('jfp-highlight')?.checked !== false;
      Store.setSettings(s);
      showToast('✅ Settings saved');
    };

    // Export
    $('jfp-export')?.addEventListener('click', () => {
      const data = { profile: Store.getProfile(), settings: Store.getSettings() };
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'jobfiller-profile.json';
      a.click();
      showToast('📤 Profile exported');
    });

    // Import
    $('jfp-import')?.addEventListener('click', () => {
      const inp = document.createElement('input');
      inp.type = 'file'; inp.accept = '.json';
      inp.onchange = async (e) => {
        try {
          const text = await e.target.files[0].text();
          const d = JSON.parse(text);
          if (d.profile) Store.setProfile(d.profile);
          if (d.settings) Store.setSettings(d.settings);
          updatePanel();
          showToast('✅ Profile imported');
        } catch { showToast('❌ Invalid file'); }
      };
      inp.click();
    });

    // Reset
    $('jfp-reset-all')?.addEventListener('click', () => {
      if (!confirm('Reset ALL JobFiller Pro data?')) return;
      GM_listValues().forEach(k => GM_deleteValue(k));
      updatePanel();
      showToast('🗑 All data cleared');
    });
  }

  function handleFileUpload(file, type) {
    if (!file) return;
    if (file.size > 5 * 1024 * 1024) { showToast('❌ File too large (max 5MB)'); return; }
    const reader = new FileReader();
    reader.onload = (ev) => {
      const b64 = ev.target.result.split(',')[1];
      const meta = { name: file.name, size: file.size, type: file.type };
      if (type === 'cv') { Store.setCVData(b64); Store.setCVMeta(meta); }
      else { Store.setResumeData(b64); Store.setResumeMeta(meta); }
      showToast(`✅ ${type.toUpperCase()} uploaded: ${file.name}`);
      Store.addLog(`${type.toUpperCase()} uploaded: ${file.name}`, 'success');
      updatePanel();
    };
    reader.readAsDataURL(file);
  }

  // ═══════════════════════════════════════════════════════════════════════════
  //  TAMPERMONKEY MENU COMMANDS
  // ═══════════════════════════════════════════════════════════════════════════
  GM_registerMenuCommand('⚡ Fill This Page', () => {
    const r = fillPage();
    showToast(`✅ Filled ${r.filled} fields, ${r.files} files`);
  });
  GM_registerMenuCommand('🔍 Start Auto-Scroll', () => startAutoScroll());
  GM_registerMenuCommand('⏹ Stop Auto-Scroll', () => stopAutoScroll());
  GM_registerMenuCommand('🪟 Toggle Panel', () => { panelVisible = !panelVisible; updatePanel(); });

  // ═══════════════════════════════════════════════════════════════════════════
  //  AUTO-FILL ON LOAD
  // ═══════════════════════════════════════════════════════════════════════════
  const JOB_APPLY_PATTERN = /\/(apply|application|job|position|prihlasit|prihlaska|reagovat|odpovedet|nabidka)/i;

  function init() {
    mount();

    // Check if we're returning from a scroll session
    const wasScrolling = Store.get('scrollActive', false);
    if (wasScrolling) {
      scrollActive = true;
      scrollQueue = Store.get('scrollQueue', []);
      scrollIndex = Store.get('scrollIndex', 0);
      Store.set('scrollActive', false); // clear flag; resumeScrollMode sets it again when needed
      resumeScrollMode();
      return;
    }

    // Auto-fill on apply pages
    const settings = Store.getSettings();
    if (settings.autoFill && JOB_APPLY_PATTERN.test(location.pathname)) {
      setTimeout(() => {
        const r = fillPage();
        if (r.filled + r.files > 0) showToast(`⚡ Auto-filled ${r.filled} fields, ${r.files} files`);
      }, 1000);
    }
  }

  // Wait for DOM to be ready enough
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

})();