GeoFS Mod Menu -cool-

Mod Menu for GeoFS flight model variables using console-modifiable input fields

// ==UserScript==
// @name         GeoFS Mod Menu -cool-
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Mod Menu for GeoFS flight model variables using console-modifiable input fields
// @author       Jasp
// @match        https://www.geo-fs.com/*
// @grant        none
// @license      MIT
// ==/UserScript==


(function () {
  'use strict';

  /* ===========================
     Config & variable lists
     =========================== */

  const allVars = [
    "maxRPM","minRPM","starterRPM","idleThrottle","fuelFlow","enginePower","brakeRPM",
    "wingArea","dragFactor","liftFactor","CD0","CLmax","elevatorFactor","rudderFactor","aileronFactor",
    "mass","emptyWeight","maxWeight","inertia","pitchMoment","yawMoment","rollMoment",
    "gearDrag","gearCompression","gearLength"
  ];

  const explanations = {
    maxRPM: "Maximum revolutions per minute of the engine.",
    minRPM: "Minimum engine RPM (idle lower bound).",
    starterRPM: "RPM used when starting the engine.",
    idleThrottle: "Throttle percentage at idle.",
    fuelFlow: "Fuel flow rate scaling.",
    enginePower: "Base engine power factor.",
    brakeRPM: "RPM threshold for brakes.",
    wingArea: "Wing area used for lift calculations.",
    dragFactor: "General drag multiplier.",
    liftFactor: "Lift multiplier applied to wing calculations.",
    CD0: "Parasitic drag coefficient.",
    CLmax: "Maximum lift coefficient before stall.",
    elevatorFactor: "Elevator control effectiveness.",
    rudderFactor: "Rudder control effectiveness.",
    aileronFactor: "Aileron control effectiveness.",
    mass: "Aircraft mass.",
    emptyWeight: "Empty weight of aircraft.",
    maxWeight: "Maximum allowable weight.",
    inertia: "Rotational inertia parameter.",
    pitchMoment: "Pitch moment (stability).",
    yawMoment: "Yaw moment (stability).",
    rollMoment: "Roll moment (stability).",
    gearDrag: "Drag contribution from gear.",
    gearCompression: "Suspension stiffness / compression.",
    gearLength: "Landing gear strut length."
  };

  /* ===========================
     Theme palettes & helpers
     =========================== */

  const PALETTE_DARK = {
    bg: "#0f1720", panel: "#111827", accent: "#2b9af3",
    text: "#e6eef8", muted: "#9aa8b6"
  };
  const PALETTE_LIGHT = {
    bg: "#f6fbff", panel: "#e9f2fb", accent: "#1e6fb8",
    text: "#07263a", muted: "#3f6b83"
  };

  function hexToRgb(hex) {
    const h = hex.replace("#", "");
    if (h.length === 3) return [parseInt(h[0]+h[0],16), parseInt(h[1]+h[1],16), parseInt(h[2]+h[2],16)];
    const bigint = parseInt(h,16); return [(bigint>>16)&255, (bigint>>8)&255, bigint&255];
  }
  function rgbToHex(rgb){ return "#" + rgb.map(v=>{ const s=Math.round(v).toString(16); return s.length===1?"0"+s:s; }).join(""); }
  function lerpColor(a,b,t){ const A=hexToRgb(a), B=hexToRgb(b); return rgbToHex([A[0]+(B[0]-A[0])*t, A[1]+(B[1]-A[1])*t, A[2]+(B[2]-A[2])*t]); }

  /* ===========================
     Backdrop tint helpers
     =========================== */

  // dark tint (near-black) and light tint (near-white)
  const DARK_TINT_RGB = [4,8,12];   // near-black tint base
  const LIGHT_TINT_RGB = [255,255,255]; // light tint base

  function lerpRgb(a, b, t) {
    return [ Math.round(a[0] + (b[0]-a[0]) * t), Math.round(a[1] + (b[1]-a[1]) * t), Math.round(a[2] + (b[2]-a[2]) * t) ];
  }

  /* ===========================
     Injection core (per-document)
     =========================== */

  const injectedDocs = new WeakSet();

  function injectIntoWindow(targetWin) {
    if (!targetWin || !targetWin.document) return;
    const doc = targetWin.document;
    if (injectedDocs.has(doc)) return;
    injectedDocs.add(doc);

    // CSS (menu opaque; backdrop separate)
    const css = `
      :root{ --mod-panel:${PALETTE_DARK.panel}; --mod-text:${PALETTE_DARK.text}; --mod-accent:${PALETTE_DARK.accent}; --mod-muted:${PALETTE_DARK.muted}; }
      #geofsModMenu{ position:fixed; top:80px; right:18px; width:360px; max-height:86vh; overflow-y:auto; color:var(--mod-text); border-radius:14px; padding:12px; box-shadow:0 10px 40px rgba(2,6,23,0.6); z-index:2147484001; border:1px solid rgba(0,0,0,0.12); display:none; font-family:"Segoe UI",Roboto,Arial; font-size:13px; background:var(--mod-panel); }
      #geofsModMenu .panel{ background:var(--mod-panel); border-radius:10px; padding:10px; box-shadow:inset 0 1px 0 rgba(255,255,255,0.02); }
      header{ display:flex; align-items:center; gap:10px; margin-bottom:8px; }
      #geofsModMenu h1{ font-size:15px; margin:0; color:var(--mod-text); font-weight:600; }
      #themeControl{ margin-left:auto; display:flex; align-items:center; gap:8px; }
      .theme-label{ font-size:12px; color:var(--mod-muted); width:36px; text-align:center; }
      input[type="range"].theme-range{ width:140px; appearance:none; height:8px; border-radius:999px; background:linear-gradient(90deg,var(--mod-accent),#00b4ff 50%,#9ad6ff); outline:none; box-shadow:inset 0 -1px 0 rgba(0,0,0,0.2); }
      input[type="range"].theme-range::-webkit-slider-thumb{ -webkit-appearance:none; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 2px 6px rgba(0,0,0,0.35); border:2px solid rgba(0,0,0,0.2); cursor:pointer; }
      #actionRow{ display:flex; gap:8px; margin-bottom:10px; }
      .action-button{ background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(0,0,0,0.03)); border:1px solid rgba(255,255,255,0.02); color:var(--mod-text); padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:600; font-size:13px; }
      .action-button.primary{ background:linear-gradient(180deg,rgba(255,255,255,0.02),rgba(0,0,0,0.06)); border:1px solid rgba(255,255,255,0.04); }
      h2.section-title{ margin:12px 0 6px; font-size:13px; color:var(--mod-accent); text-transform:uppercase; letter-spacing:0.6px; }
      .var-row{ display:flex; flex-direction:column; gap:6px; margin-bottom:8px; }
      .label-row{ display:flex; justify-content:space-between; align-items:center; gap:12px; }
      label.name{ font-weight:600; color:var(--mod-text); font-size:13px; }
      .explain{ font-size:12px; color:var(--mod-muted); margin-top:-4px; }
      input[type="number"].num-input{ width:160px; background:transparent; border:1px solid rgba(255,255,255,0.04); color:var(--mod-text); padding:6px 8px; border-radius:8px; text-align:right; }
      #menuToggleBtn{ position:fixed; top:18px; right:18px; width:46px; height:46px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:var(--mod-panel); color:var(--mod-text); border:1px solid rgba(255,255,255,0.03); box-shadow:0 6px 20px rgba(2,6,23,0.5); z-index:2147484002; cursor:pointer; }
      #menuBackdrop{ position:fixed; inset:0; z-index:2147484000; display:none; pointer-events:auto; transition: background-color 220ms ease, backdrop-filter 220ms ease, opacity 220ms ease; }
      #helpOverlay{ position:fixed; top:0; left:0; width:100%; height:100%; background: rgba(2,6,23,0.9); color:#eaf6ff; padding:28px; z-index:2147484003; display:none; overflow-y:auto; font-size:14px; }
      #closeHelp{ position:absolute; top:18px; right:24px; font-size:22px; cursor:pointer; }
      .collapse-toggle{ cursor:pointer; font-size:13px; color:var(--mod-accent); background:none; border:none; padding:6px 8px; border-radius:6px; }
      .visual-row{ display:flex; align-items:center; gap:10px; margin-bottom:8px; }
      .visual-label{ width:140px; color:var(--mod-text); font-weight:600; font-size:13px; }
      @media (max-width:520px){ #geofsModMenu{ width:92%; left:4%; right:4%; top:80px; } }
    `;
    const styleEl = doc.createElement('style');
    styleEl.textContent = css;
    doc.head.appendChild(styleEl);

    // Create DOM elements
    const menu = doc.createElement('div'); menu.id = 'geofsModMenu'; menu.className = 'panel';
    const toggleBtn = doc.createElement('div'); toggleBtn.id = 'menuToggleBtn'; toggleBtn.title = 'Toggle GeoFS Mod Menu'; toggleBtn.textContent = '⚙️';
    const backdrop = doc.createElement('div'); backdrop.id = 'menuBackdrop';
    const helpOverlay = doc.createElement('div'); helpOverlay.id = 'helpOverlay';
    helpOverlay.innerHTML = `<div id="closeHelp">✕</div><h1>GeoFS Mod Menu — Help</h1><div id="helpContent"></div>`;

    // Header & theme control
    const header = doc.createElement('header');
    header.innerHTML = `<h1>GeoFS Mod Menu</h1>`;
    const themeControl = doc.createElement('div'); themeControl.id = 'themeControl';
    themeControl.innerHTML = `<div class="theme-label">Dark</div><input id="themeSlider" class="theme-range" type="range" min="0" max="100" value="0" /><div class="theme-label">Light</div>`;
    header.appendChild(themeControl);

    // Action row (no presets)
    const actionRow = doc.createElement('div'); actionRow.id = 'actionRow';
    const resetBtn = doc.createElement('button'); resetBtn.className = 'action-button'; resetBtn.textContent = 'Reset';
    const helpBtn = doc.createElement('button'); helpBtn.className = 'action-button primary'; helpBtn.textContent = 'Help';
    actionRow.appendChild(resetBtn); actionRow.appendChild(helpBtn);

    // Body container
    const bodyWrap = doc.createElement('div'); bodyWrap.className = 'panel';

    // Visual settings collapsible
    const visualToggle = doc.createElement('button'); visualToggle.className = 'collapse-toggle'; visualToggle.textContent = 'Visual Settings ▾';
    const visualContainer = doc.createElement('div'); visualContainer.style.display = 'none'; visualContainer.style.marginTop = '8px';
    // Visual sliders: blur and tint
    const blurRow = doc.createElement('div'); blurRow.className = 'visual-row';
    blurRow.innerHTML = `<div class="visual-label">Background Blur</div><input id="blurSlider" type="range" min="0" max="100" value="40" class="theme-range" />`;
    const tintRow = doc.createElement('div'); tintRow.className = 'visual-row';
    tintRow.innerHTML = `<div class="visual-label">Blur Tint</div><input id="tintSlider" type="range" min="0" max="100" value="50" class="theme-range" />`;

    visualContainer.appendChild(blurRow); visualContainer.appendChild(tintRow);

    // Assemble menu
    menu.appendChild(header);
    menu.appendChild(actionRow);
    bodyWrap.appendChild(visualToggle);
    bodyWrap.appendChild(visualContainer);

    // Add variable groups
    const groups = {
      Engine: ["maxRPM","minRPM","starterRPM","idleThrottle","fuelFlow","enginePower","brakeRPM"],
      Aerodynamics: ["wingArea","dragFactor","liftFactor","CD0","CLmax","elevatorFactor","rudderFactor","aileronFactor"],
      "Flight Model": ["mass","emptyWeight","maxWeight","inertia","pitchMoment","yawMoment","rollMoment"],
      "Landing Gear": ["gearDrag","gearCompression","gearLength"]
    };

    // mapping for inputs
    const inputsMap = {};

    function createNumberControl(varName) {
      const container = doc.createElement('div'); container.className = 'var-row';
      const labelRow = doc.createElement('div'); labelRow.className = 'label-row';
      const label = doc.createElement('label'); label.className = 'name'; label.textContent = varName;
      const input = doc.createElement('input'); input.type='number'; input.className='num-input'; input.setAttribute('inputmode','decimal'); input.placeholder='0';
      // enforce 9-digit cap: digits count excluding '.' and '-'
      input.addEventListener('input', ()=> {
        const original = input.value;
        const enforced = enforceNineDigitCap(original);
        if (enforced !== original) input.value = enforced;
        const parsed = parseFloat(input.value);
        if (!Number.isNaN(parsed)) updateModelInDoc(doc, varName, parsed);
      }, {passive:true});
      input.addEventListener('paste', (ev)=> {
        ev.preventDefault();
        const text = (ev.clipboardData || window.clipboardData).getData('text') || '';
        const enforced = enforceNineDigitCap(text);
        input.value = enforced;
        const parsed = parseFloat(enforced);
        if (!Number.isNaN(parsed)) updateModelInDoc(doc, varName, parsed);
      });
      input.addEventListener('blur', ()=> {
        if (input.value === '' || input.value === '-' || input.value === '.' || input.value === '-.') {
          input.value = '0'; updateModelInDoc(doc, varName, 0);
        } else {
          const parsed = parseFloat(input.value);
          if (Number.isNaN(parsed)) { input.value = '0'; updateModelInDoc(doc, varName, 0); }
          else updateModelInDoc(doc, varName, parsed);
        }
      });

      labelRow.appendChild(label); labelRow.appendChild(input); container.appendChild(labelRow);
      const explain = doc.createElement('div'); explain.className='explain'; explain.textContent = explanations[varName] || '';
      container.appendChild(explain);
      inputsMap[varName] = { input, container };
      return container;
    }

    for (const [groupName, vars] of Object.entries(groups)) {
      const h = doc.createElement('h2'); h.className='section-title'; h.textContent = groupName;
      bodyWrap.appendChild(h);
      for (const v of vars) {
        if (!allVars.includes(v)) continue;
        const ctrl = createNumberControl(v);
        bodyWrap.appendChild(ctrl);
      }
    }

    menu.appendChild(bodyWrap);

    // Append to document
    try { doc.body.appendChild(backdrop); doc.body.appendChild(menu); doc.body.appendChild(toggleBtn); doc.body.appendChild(helpOverlay); }
    catch (e) { console.error('[ModMenu] append failed', e); return; }

    // Help populating
    const helpContent = helpOverlay.querySelector('#helpContent');
    helpContent.innerHTML = '<p>Type numeric values (up to 9 digits) into the boxes; use Reset to set all to 0. Adjust Theme (top) and Visual Settings to control backdrop blur/tint.</p>';
    for (const [k,v] of Object.entries(explanations)) {
      const p = doc.createElement('p'); p.innerHTML = `<strong>${k}</strong>: ${v}`; helpContent.appendChild(p);
    }
    helpOverlay.querySelector('#closeHelp').addEventListener('click', ()=> { helpOverlay.style.display='none'; menu.style.display='block'; toggleBackdrop(false); });

    helpBtn.addEventListener('click', ()=> { menu.style.display='none'; helpOverlay.style.display='block'; toggleBackdrop(false); });

    // Reset button behavior
    resetBtn.addEventListener('click', ()=> {
      for (const [k,entry] of Object.entries(inputsMap)) {
        entry.input.value = '0';
        updateModelInDoc(doc, k, 0);
      }
    });

    // Visual settings collapse toggle
    let visualOpen = false;
    visualToggle.addEventListener('click', ()=> {
      visualOpen = !visualOpen;
      visualContainer.style.display = visualOpen ? 'block' : 'none';
      visualToggle.textContent = `Visual Settings ${visualOpen ? '▴' : '▾'}`;
    });

    // enforce 9-digit cap helper (digits only count)
    function enforceNineDigitCap(str) {
      let cleaned = String(str || '');
      // remove non digit/.- characters
      cleaned = cleaned.replace(/[^\d\.\-]/g,'');
      // sanitize minus: only one at start
      const minusMatches = cleaned.match(/-/g);
      if (minusMatches && minusMatches.length > 1) {
        cleaned = cleaned.replace(/-/g,'');
        cleaned = '-' + cleaned;
      }
      if (cleaned.indexOf('-') > 0) cleaned = cleaned.replace('-', '');
      // handle multiple dots
      const parts = cleaned.split('.');
      if (parts.length > 2) {
        cleaned = parts.shift() + '.' + parts.join('');
      }
      // count digits and truncate if necessary
      const digitsOnly = cleaned.replace(/[^0-9]/g,'');
      if (digitsOnly.length > 9) {
        const allowed = digitsOnly.slice(0,9);
        if (cleaned.includes('.')) {
          const [intPart, fracPart=''] = cleaned.split('.');
          const intDigits = intPart.replace(/[^0-9]/g,'').replace('-','');
          const keepInt = Math.min(intDigits.length, allowed.length);
          const newInt = intDigits.slice(0, keepInt) || '0';
          const newFrac = allowed.slice(keepInt);
          cleaned = (cleaned.startsWith('-')?'-':'') + newInt + (newFrac ? '.' + newFrac : '');
        } else {
          const wasNeg = cleaned.startsWith('-');
          cleaned = (wasNeg ? '-' : '') + allowed;
        }
      }
      return cleaned;
    }

    // Apply theme to doc (smooth via CSS variables)
    function applyThemeToDoc(docRef, t) {
      const panel = lerpColor(PALETTE_DARK.panel, PALETTE_LIGHT.panel, t);
      const text = lerpColor(PALETTE_DARK.text, PALETTE_LIGHT.text, t);
      const accent = lerpColor(PALETTE_DARK.accent, PALETTE_LIGHT.accent, t);
      const muted = lerpColor(PALETTE_DARK.muted, PALETTE_LIGHT.muted, t);
      const root = docRef.documentElement;
      root.style.setProperty('--mod-panel', panel);
      root.style.setProperty('--mod-text', text);
      root.style.setProperty('--mod-accent', accent);
      root.style.setProperty('--mod-muted', muted);
    }

    // Backdrop control: compute blur px and tint rgba and apply
    // blurSlider value 0..100 -> blurPx 0..maxBlurPx
    const maxBlurPx = 32; // adjustable for stronger blur
    function applyBackdropSettings(docRef, blurPercent, tintPercent) {
      // blur:
      const blurPx = (blurPercent/100) * maxBlurPx;
      // tint color between dark and light
      const tintRgb = lerpRgb(DARK_TINT_RGB, LIGHT_TINT_RGB, tintPercent/100);
      // opacity scales with blurPercent (so when blur=0 it's transparent)
      const maxOpacity = 0.85; // when blur=100, opacity cap
      const opacity = (blurPercent/100) * maxOpacity;
      backdrop.style.backdropFilter = `blur(${blurPx}px)`;
      backdrop.style.webkitBackdropFilter = `blur(${blurPx}px)`;
      backdrop.style.backgroundColor = `rgba(${tintRgb[0]},${tintRgb[1]},${tintRgb[2]},${opacity})`;
      // also set opacity to 1 when visible for smooth transitions
      backdrop.style.opacity = blurPercent>0 ? '1' : '0';
    }

    // Theme slider smoothing (easing)
    const themeSlider = doc.getElementById('themeSlider');
    let themeT = parseFloat(themeSlider.value)/100;
    applyThemeToDoc(doc, themeT);
    let themeAnim = null;
    function animateThemeTo(targetT, duration = 320) {
      const startT = themeT, delta = targetT - startT, startTime = performance.now();
      if (themeAnim) cancelAnimationFrame(themeAnim);
      function step(now) {
        const elapsed = now - startTime;
        const p = Math.min(1, elapsed / duration);
        const eased = p < 0.5 ? 2*p*p : -1 + (4-2*p)*p;
        themeT = startT + delta * eased;
        applyThemeToDoc(doc, themeT);
        if (p < 1) themeAnim = requestAnimationFrame(step); else themeAnim = null;
      }
      themeAnim = requestAnimationFrame(step);
    }
    themeSlider.addEventListener('input', (e) => {
      const val = parseInt(e.target.value,10)/100;
      animateThemeTo(val, 320);
    });

    // Visual sliders: blur & tint with smoothing
    const blurSlider = doc.getElementById('blurSlider');
    const tintSlider = doc.getElementById('tintSlider');

    let backdropBlurT = parseFloat(blurSlider.value)/100;
    let backdropTintT = parseFloat(tintSlider.value)/100;
    // apply initial
    applyBackdropSettings(doc, backdropBlurT*100, backdropTintT*100);

    let backdropAnim = null;
    function animateBackdropTo(targetBlurT, targetTintT, duration = 220) {
      const startBlur = backdropBlurT, startTint = backdropTintT;
      const dBlur = targetBlurT - startBlur, dTint = targetTintT - startTint;
      const startTime = performance.now();
      if (backdropAnim) cancelAnimationFrame(backdropAnim);
      function step(now) {
        const elapsed = now - startTime;
        const p = Math.min(1, elapsed / duration);
        const eased = p < 0.5 ? 2*p*p : -1 + (4-2*p)*p;
        backdropBlurT = startBlur + dBlur * eased;
        backdropTintT = startTint + dTint * eased;
        applyBackdropSettings(doc, backdropBlurT*100, backdropTintT*100);
        if (p < 1) backdropAnim = requestAnimationFrame(step); else backdropAnim = null;
      }
      backdropAnim = requestAnimationFrame(step);
    }

    blurSlider.addEventListener('input', (e) => {
      const t = parseInt(e.target.value,10)/100;
      animateBackdropTo(t, backdropTintT, 180);
    });
    tintSlider.addEventListener('input', (e) => {
      const t = parseInt(e.target.value,10)/100;
      animateBackdropTo(backdropBlurT, t, 180);
    });

    // Toggle backdrop visibility when menu opens/closes
    function toggleBackdrop(show) {
      if (show) {
        // ensure it's visible (but blur/opacity may be zero depending on slider)
        backdrop.style.display = 'block';
        // apply current slider settings smoothly
        const bT = parseInt(blurSlider.value,10)/100;
        const ti = parseInt(tintSlider.value,10)/100;
        animateBackdropTo(bT, ti, 220);
      } else {
        // hide gradually then set display:none
        if (backdropAnim) cancelAnimationFrame(backdropAnim);
        // fade out by setting blur to 0 and opacity to 0
        animateBackdropTo(0, backdropTintT, 160);
        setTimeout(()=> { try { backdrop.style.display='none'; } catch(e) {} }, 240);
      }
    }

    // Toggle menu button behavior
    toggleBtn.addEventListener('click', ()=> {
      const open = menu.style.display === 'block';
      menu.style.display = open ? 'none' : 'block';
      if (!open) { toggleBtn.style.boxShadow = '0 10px 26px rgba(2,6,23,0.6)'; toggleBackdrop(true); }
      else { toggleBtn.style.boxShadow = '0 6px 20px rgba(2,6,23,0.5)'; toggleBackdrop(false); }
    });

    // keybinding
    doc.addEventListener('keydown', (e) => { if (e.key === '#') toggleBtn.click(); });

    // clicking backdrop does not close by default — but let's allow alt-click to close
    backdrop.addEventListener('click', (ev)=> {
      // if user holds Alt/Option while clicking backdrop, close menu
      if (ev.altKey) {
        menu.style.display = 'none';
        toggleBackdrop(false);
      }
    });

    // Model update helper (writes to window.geofs.* in that doc's window)
    function updateModelInDoc(docRef, variable, value) {
      const win = docRef.defaultView || window;
      try {
        const def = win.geofs?.aircraft?.instance?.definition || win.geofs?.aircraft?.definition;
        if (def && variable in def) {
          def[variable] = value;
        } else {
          // fallback nested attempt
          if (win.geofs && win.geofs.aircraft && win.geofs.aircraft.instance && win.geofs.aircraft.instance.definition) {
            win.geofs.aircraft.instance.definition[variable] = value;
          }
        }
      } catch (err) {
        // ignore silently
      }
    }

    // initialize inputs to 0
    for (const key of Object.keys(inputsMap)) {
      inputsMap[key].input.value = '0';
      updateModelInDoc(doc, key, 0);
    }

    // Build mapping (after creating inputs)
    // We already created inputs when building groups; ensure they are set to 0
    for (const v of allVars) {
      if (!inputsMap[v] && doc.querySelector) {
        // should not happen: but guard
      }
    }

    // initial backdrop hidden
    backdrop.style.display = 'none';
    backdrop.style.backdropFilter = 'blur(0px)';
    backdrop.style.backgroundColor = 'rgba(0,0,0,0)';

    // ensure menu hidden initially
    menu.style.display = 'none';

    console.log('[ModMenu] injected overlay into', doc.location && doc.location.href ? doc.location.href : 'unknown');
  } // end injectIntoWindow

  /* ===========================
     Injection orchestrator (main document + iframes)
     =========================== */

  // We must create inputsMap per doc; to avoid overcomplicating closure scope, we'll regenerate per injection.
  // The injectIntoWindow function creates and appends elements and wires events.

  function attemptInjectionAll() {
    try { injectIntoWindow(window); } catch(e){/* ignore */ }

    const iframes = Array.from(document.getElementsByTagName('iframe'));
    for (const iframe of iframes) {
      try {
        const src = (iframe.getAttribute('src') || '').toLowerCase();
        if (src.includes('geofs.php') || src.includes('geo-fs') || (iframe.id && /geofs|map|game/i.test(iframe.id))) {
          try {
            if (iframe.contentWindow && iframe.contentWindow.document) injectIntoWindow(iframe.contentWindow);
          } catch (err) {
            // cross-origin or not ready
          }
        }
      } catch (err) {}
    }
  }

  // initial attempt & observer
  attemptInjectionAll();

  const observer = new MutationObserver(muts => {
    for (const m of muts) {
      if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) {
        for (const n of m.addedNodes) {
          if (n.tagName && n.tagName.toLowerCase() === 'iframe') {
            const iframe = n;
            setTimeout(()=> {
              try { if (iframe.contentWindow && iframe.contentWindow.document) injectIntoWindow(iframe.contentWindow); } catch(e){}
            }, 300);
          }
        }
      }
      if (m.type === 'attributes' && m.target && m.target.tagName && m.target.tagName.toLowerCase() === 'iframe') {
        const iframe = m.target;
        setTimeout(()=> { try { if (iframe.contentWindow && iframe.contentWindow.document) injectIntoWindow(iframe.contentWindow); } catch(e){} }, 300);
      }
    }
  });

  observer.observe(document.documentElement || document.body, { childList:true, subtree:true, attributes:true, attributeFilter:['src'] });

  // Poller fallback
  const poll = setInterval(()=> { attemptInjectionAll(); }, 1200);
  setTimeout(()=> clearInterval(poll), 90000);

  console.log('[ModMenu] overlay injector running — click cog in flight view to open panel.');

})();