Feather Client

lightweight quiz helper

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Feather Client
// @namespace    https://github.com/vxinne/feather
// @version      2.0.0
// @description  lightweight quiz helper
// @author       Xandros and Vxinne
// @license      GPL-3.0
// @match        https://quizizz.com/*
// @match        https://wayground.com/*
// @match        https://*.quizizz.com/*
// @match        https://*.wayground.com/*
// @match        https://exam.preahsisowath.edu.kh/*
// @match        https://*.preahsisowath.edu.kh/*
// @match        https://docs.google.com/forms/*
// @grant        GM_addStyle
// @grant        GM_log
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @connect      *
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  /* ╔═══════════════════════════════════════════╗
     ║  §1  CONFIG                              ║
     ╚═══════════════════════════════════════════╝ */

  const K = { KEY: "FL_GK", UI: "fl_ui", CFG: "FL_CFG", FIRST: "FL_1ST", CACHE: "FL_QC" };
  const MONO = "'SF Mono','Cascadia Code','Fira Code','JetBrains Mono',Consolas,monospace";
  const REAL_MODEL = 'gemini-3-flash-preview';
  const MODEL_DISPLAY = 'Gemini 3 Flash';

  const DEFAULTS = {
    geminiApiKey: "",
    enableSpoofFullscreen: true,
    enableAutoAnswer: false,
    autoAnswerDelay: 1800,
    timeTakenMin: 5500,
    timeTakenMax: 9000,
    enableTimeTakenEdit: true,
    enableTimerHijack: false,
    timerBonusPoints: 270,
    enableReactionSpam: false,
    reactionSpamCount: 1,
    reactionSpamDelay: 2000,
    includeImages: true,
    thinkingBudget: 512,
    maxOutputTokens: 1024,
    temperature: 0.15,
    serverUrl: "https://uets.meowery.eu",
    selectedModel: 'gemini-3-flash-preview',
    formsModel: 'gemini-3-flash-preview',
  };

  const PROFILES = {
    stealth:  { enableTimeTakenEdit:false, enableTimerHijack:false, enableSpoofFullscreen:true,  enableReactionSpam:false, enableAutoAnswer:false },
    extended: { enableTimeTakenEdit:true,  timeTakenMin:8000, timeTakenMax:14000, enableTimerHijack:true, timerBonusPoints:200, enableSpoofFullscreen:true, enableReactionSpam:false, enableAutoAnswer:false },
    creator:  { enableTimeTakenEdit:true,  timeTakenMin:6000, timeTakenMax:8000,  enableTimerHijack:true, timerBonusPoints:270, enableSpoofFullscreen:true, enableReactionSpam:false, enableAutoAnswer:true, autoAnswerDelay:2000 },
    lmao:     { enableTimeTakenEdit:true,  timeTakenMin:1000, timeTakenMax:2000,  enableTimerHijack:true, timerBonusPoints:5000, enableSpoofFullscreen:true, enableReactionSpam:true, reactionSpamCount:2, reactionSpamDelay:500, enableAutoAnswer:true, autoAnswerDelay:500 },
  };

  const MODELS = {
    'gemini-3-pro-preview':   { name: 'Gemini 3 Pro',     speed: 'medium'  },
    'gemini-3-flash-preview': { name: 'Gemini 3 Flash',   speed: 'fast'    },
    'gemini-2.5-flash':       { name: 'Gemini 2.5 Flash', speed: 'fast'    },
    'gemini-2.5-pro':         { name: 'Gemini 2.5 Pro',   speed: 'slow'    },
    'gemini-2.0-flash':       { name: 'Gemini 2.0 Flash', speed: 'fastest' },
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §2  STATE                               ║
     ╚═══════════════════════════════════════════╝ */

  const S = {
    on: GM_getValue(K.UI, true),
    config: { ...DEFAULTS, ...GM_getValue(K.CFG, {}) },
    panel: null, toastEl: null, toastTimer: null, orbEl: null,
    observer: null, qData: {}, detected: {}, curQId: null,
    autoAnswerPending: false,
    answerCache: GM_getValue(K.CACHE, {}),
    accuracyCount: { correct: 0, total: 0 },
  };
  if (!S.config.geminiApiKey) S.config.geminiApiKey = GM_getValue(K.KEY, "");

  /* ╔═══════════════════════════════════════════╗
     ║  §3  STYLES — silver & white             ║
     ╚═══════════════════════════════════════════╝ */

  const isGF = () => location.hostname.includes('docs.google.com') && location.pathname.includes('/forms/');
  const isExam = () => location.hostname.includes('preahsisowath.edu.kh');

  GM_addStyle(`
    :root {
      --bg:    #0c0c0c;
      --bg1:   #111111;
      --bg2:   #161616;
      --bg3:   #1c1c1c;
      --line:  #242424;
      --dim:   #383838;
      --mid:   #606060;
      --si:    #8a8a8a;   /* silver mid */
      --sh:    #b8b8b8;   /* silver hi  */
      --fg:    #d8d8d8;
      --wh:    #f0f0f0;
      --pt:    #e8e8e8;   /* platinum   */
      --lm:    ${MONO};
    }

    /* ── ORB: nearly invisible, stealth ── */
    #fl-orb {
      position: fixed; bottom: 28px; left: 28px;
      width: 28px; height: 28px; line-height: 28px; text-align: center;
      font-family: var(--lm); font-size: 11px; color: var(--dim);
      cursor: pointer; z-index: 2147483647; user-select: none;
      opacity: .12; transition: opacity .3s, color .3s;
      background: transparent; border: none;
    }
    #fl-orb:hover { opacity: .6; color: var(--sh); }
    #fl-orb.off   { opacity: .05; }

    /* ── PANEL ── */
    .fl-panel {
      position: fixed; top: 50%; left: 50%;
      transform: translate(-50%,-50%);
      background: var(--bg);
      border: 1px solid var(--line);
      color: var(--fg); font-family: var(--lm); font-size: 12px;
      z-index: 2147483647; max-height: 88vh;
      display: flex; flex-direction: column; min-width: 500px;
      animation: flIn .18s cubic-bezier(.16,1,.3,1);
      box-shadow:
        0 0 0 1px var(--bg2),
        0 40px 100px rgba(0,0,0,.97),
        inset 0 1px 0 rgba(255,255,255,.03);
    }
    @keyframes flIn {
      from { opacity:0; transform:translate(-50%,-46%) scale(.96); }
      to   { opacity:1; transform:translate(-50%,-50%) scale(1); }
    }

    /* ── HEADER ── */
    .fl-hdr {
      padding: 20px 24px 14px;
      border-bottom: 1px solid var(--line);
      user-select: none; position: relative;
      background: linear-gradient(180deg, #0f0f0f 0%, var(--bg) 100%);
    }
    /* platinum shimmer line */
    .fl-hdr::after {
      content: '';
      position: absolute; top: 0; left: 20%; right: 20%; height: 1px;
      background: linear-gradient(90deg,
        transparent,
        rgba(232,232,232,0.12),
        rgba(232,232,232,0.28),
        rgba(232,232,232,0.12),
        transparent
      );
    }
    .fl-hdr-title {
      display: block;
      font-size: 13px; letter-spacing: .65em;
      color: var(--pt); font-weight: normal;
      margin-bottom: 5px;
    }
    .fl-hdr-sub {
      font-size: 9px; letter-spacing: .28em;
      color: var(--dim); text-transform: uppercase;
    }
    .fl-hdr-model {
      font-size: 9px; letter-spacing: .15em;
      color: var(--mid); margin-top: 3px;
    }
    .fl-close {
      position: absolute; top: 16px; right: 18px;
      cursor: pointer; color: var(--dim);
      font-size: 10px; letter-spacing: .1em;
      transition: color .15s;
    }
    .fl-close:hover { color: var(--sh); }

    /* ── BODY ── */
    .fl-body { overflow-y: auto; padding: 0 22px 16px; flex: 1; }
    .fl-body::-webkit-scrollbar { width: 1px; }
    .fl-body::-webkit-scrollbar-thumb {
      background: linear-gradient(var(--dim), var(--mid));
    }

    /* ── SECTION ── */
    .fl-sec {
      display: flex; align-items: center; gap: 10px;
      margin: 16px 0 6px;
      font-size: 8px; letter-spacing: .42em;
      color: var(--dim); user-select: none; text-transform: uppercase;
    }
    .fl-sec::after { content: ''; flex: 1; height: 1px; background: var(--line); }

    /* ── ROW ── */
    .fl-row {
      display: flex; align-items: center;
      min-height: 26px; padding: 1px 0;
      border-bottom: 1px solid transparent;
      transition: border-color .1s;
    }
    .fl-row:hover { border-bottom-color: var(--bg3); }
    .fl-lbl { color: var(--mid); flex: 1; font-size: 11px; letter-spacing: .03em; }

    /* ── TOGGLE ── */
    .fl-tog {
      cursor: pointer; user-select: none;
      font-size: 9px; letter-spacing: .14em;
      color: var(--dim); padding: 2px 8px;
      border: 1px solid var(--dim); min-width: 38px; text-align: center;
      transition: color .15s, border-color .15s, background .15s;
    }
    .fl-tog:hover { color: var(--sh); border-color: var(--si); }
    .fl-tog.on {
      color: var(--pt); border-color: var(--sh);
      background: rgba(232,232,232,.05);
      text-shadow: 0 0 8px rgba(232,232,232,.2);
    }

    /* ── INPUT ── */
    .fl-inp {
      background: transparent; border: none;
      border-bottom: 1px solid var(--line);
      color: var(--fg); font-family: var(--lm);
      font-size: 11px; width: 72px; text-align: right;
      outline: 0; padding: 2px 0; transition: border-color .15s;
    }
    .fl-inp:focus { border-color: var(--sh); }
    .fl-inp-w { width: 195px; text-align: left; }

    /* ── SELECT ── */
    .fl-sel {
      background: var(--bg2); border: 1px solid var(--line);
      color: var(--sh); font-family: var(--lm);
      font-size: 10px; padding: 2px 6px;
      outline: 0; cursor: pointer; transition: border-color .15s;
    }
    .fl-sel:focus { border-color: var(--sh); }

    /* ── PROFILES ── */
    .fl-prof-row { display: flex; gap: 5px; flex-wrap: wrap; padding: 5px 0 2px; }
    .fl-prof {
      cursor: pointer; color: var(--dim);
      user-select: none; font-size: 8px; letter-spacing: .2em;
      padding: 3px 10px; border: 1px solid var(--line);
      transition: color .15s, border-color .15s;
    }
    .fl-prof:hover, .fl-prof.act { color: var(--pt); border-color: var(--sh); }

    /* ── FOOTER ── */
    .fl-foot {
      display: flex; justify-content: flex-end; gap: 5px;
      padding: 10px 22px; border-top: 1px solid var(--line);
      background: #090909;
    }
    .fl-fb {
      cursor: pointer; padding: 4px 14px;
      border: 1px solid var(--line); background: transparent;
      color: var(--mid); font-family: var(--lm);
      font-size: 9px; letter-spacing: .14em;
      transition: color .15s, border-color .15s, background .15s;
    }
    .fl-fb:hover  { color: var(--fg); border-color: var(--si); }
    .fl-fb.ok     {
      color: var(--pt); border-color: var(--sh);
      background: rgba(232,232,232,.04);
      box-shadow: 0 0 12px rgba(232,232,232,.06);
    }
    .fl-fb.ok:hover { box-shadow: 0 0 20px rgba(232,232,232,.12); }

    /* ── TOAST ── */
    .fl-toast {
      position: fixed; top: 16px; right: 16px;
      background: var(--bg);
      border: 1px solid var(--line);
      color: var(--sh); font-family: var(--lm); font-size: 11px;
      z-index: 2147483647; max-width: 390px;
      cursor: pointer; white-space: pre-wrap; line-height: 1.6;
      transition: opacity .25s, transform .25s;
      box-shadow:
        0 0 0 1px var(--bg2),
        0 16px 50px rgba(0,0,0,.97),
        inset 0 1px 0 rgba(255,255,255,.03);
    }

    /* ── ACTION BUTTONS ── */
    .fl-acts {
      display: inline-flex !important; gap: 3px !important;
      margin-top: 7px !important; flex-wrap: wrap !important;
      position: relative !important; z-index: 99999 !important;
      pointer-events: auto !important;
    }
    .fl-ab {
      font-family: var(--lm) !important; font-size: 9px !important;
      background: var(--bg) !important;
      border: 1px solid var(--dim) !important;
      color: var(--mid) !important; padding: 4px 11px !important;
      cursor: pointer !important; text-decoration: none !important;
      display: inline-block !important;
      pointer-events: auto !important; position: relative !important;
      z-index: 99999 !important; line-height: 1.4 !important;
      user-select: none !important; letter-spacing: .1em !important;
      transition: color .12s, border-color .12s, background .12s !important;
    }
    .fl-ab:hover {
      color: var(--pt) !important; border-color: var(--sh) !important;
      background: rgba(232,232,232,.04) !important;
    }
    .fl-ab.active { color: var(--sh) !important; border-color: var(--si) !important; }

    /* ── ANSWER HIGHLIGHT ── */
    .fl-hit {
      outline: 1px solid rgba(232,232,232,.7) !important;
      outline-offset: 3px !important;
      box-shadow: 0 0 12px rgba(232,232,232,.08) !important;
      position: relative !important;
    }
    .fl-hit-tag {
      position: absolute; top: -8px; right: -8px;
      background: var(--pt); color: var(--bg);
      font-size: 9px; font-family: var(--lm);
      padding: 1px 6px; z-index: 100; letter-spacing: .08em;
    }

    /* ── MISC ── */
    .fl-auto-badge {
      position: fixed; bottom: 28px; left: 68px;
      font-family: var(--lm); font-size: 9px; color: var(--mid);
      border: 1px solid var(--line); padding: 2px 8px;
      z-index: 2147483646; user-select: none; opacity: .45; letter-spacing: .1em;
    }
    .fl-acc {
      position: fixed; bottom: 28px; left: 148px;
      font-family: var(--lm); font-size: 9px; color: var(--si);
      z-index: 2147483646; user-select: none; opacity: .5;
    }
  `);

  /* ╔═══════════════════════════════════════════╗
     ║  §4  HELPERS                             ║
     ╚═══════════════════════════════════════════╝ */

  const $ = (s, p) => (p || document).querySelector(s);
  const $$ = (s, p) => Array.from((p || document).querySelectorAll(s));
  const randInt = (a, b) => Math.floor(Math.random() * (b - a + 1)) + a;
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const plain = s => (s || '').replace(/\s+/g, ' ').trim();
  const uid = () => Math.random().toString(36).slice(2, 8);
  const isVis = el => !!(el && el.offsetParent !== null && el.offsetWidth > 0);

  /* ╔═══════════════════════════════════════════╗
     ║  §5  TOAST                               ║
     ╚═══════════════════════════════════════════╝ */

  const toast = (msg, loading = false) => {
    if (S.toastTimer) clearTimeout(S.toastTimer);
    if (S.toastEl) S.toastEl.remove();
    if (S._loadingInterval) { clearInterval(S._loadingInterval); S._loadingInterval = null; }

    const W = 44;
    const rule = '─'.repeat(W);

    const el = document.createElement('pre');
    el.className = 'fl-toast';

    if (loading) {
      const frames = ['◐','◓','◑','◒'];
      let fi = 0;
      el.textContent = `  FEATHER · LITE\n${rule}\n  ${frames[0]}  ${msg}\n${rule}`;
      document.body.appendChild(el);
      S.toastEl = el;
      S._loadingInterval = setInterval(() => {
        fi = (fi + 1) % 4;
        el.textContent = `  FEATHER · LITE\n${rule}\n  ${frames[fi]}  ${msg}\n${rule}`;
      }, 160);
      el.onclick = () => {
        clearInterval(S._loadingInterval); S._loadingInterval = null;
        el.remove(); S.toastEl = null;
      };
      return;
    }

    const lines = [];
    for (const raw of msg.replace(/<[^>]+>/g, '').split('\n')) {
      if (!raw) { lines.push(''); continue; }
      const words = raw.split(' '); let cur = '';
      for (const w of words) {
        if ((cur + ' ' + w).trim().length > W - 4) { lines.push(cur); cur = w; }
        else cur = cur ? cur + ' ' + w : w;
      }
      if (cur) lines.push(cur);
    }
    el.textContent = `  FEATHER · LITE\n${rule}\n${lines.map(l=>`  ${l}`).join('\n')}\n${rule}`;
    el.onclick = () => { if (S.toastTimer) clearTimeout(S.toastTimer); el.remove(); S.toastEl = null; };
    document.body.appendChild(el);
    S.toastEl = el;
    S.toastTimer = setTimeout(() => {
      el.style.opacity = '0'; el.style.transform = 'translateX(8px)';
      setTimeout(() => { el.remove(); S.toastEl = null; }, 250);
    }, 9000);
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §6  GEMINI ENGINE                       ║
     ╚═══════════════════════════════════════════╝ */

  const geminiCall = (promptText, images, apiKey, cfg = {}) => new Promise((resolve, reject) => {
    const parts = [{ text: promptText }];
    if (images?.length) {
      for (const img of images) {
        if (img.base64 && img.mimeType)
          parts.push({ inline_data: { mime_type: img.mimeType, data: img.base64 } });
      }
    }
    const genCfg = {
      temperature: cfg.temp ?? S.config.temperature,
      topP: 0.92, topK: 40,
      maxOutputTokens: cfg.maxTokens ?? S.config.maxOutputTokens,
    };
    if (cfg.jsonMode) genCfg.responseMimeType = 'application/json';
    GM_xmlhttpRequest({
      method: 'POST',
      url: `https://generativelanguage.googleapis.com/v1beta/models/${REAL_MODEL}:generateContent?key=${apiKey}`,
      headers: { 'Content-Type': 'application/json' },
      data: JSON.stringify({ contents: [{ parts }], generationConfig: genCfg }),
      onload: r => {
        try {
          const j = JSON.parse(r.responseText);
          if (j.error) return reject(new Error(j.error.message));
          const txt = j.candidates?.[0]?.content?.parts?.map(p => p.text).filter(Boolean).join('');
          if (txt) resolve(txt); else reject(new Error('Empty AI response'));
        } catch(e) { reject(e); }
      },
      onerror: () => reject(new Error('Network error'))
    });
  });

  const fetchImg64 = url => new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: 'GET', url, responseType: 'blob',
      onload: r => {
        if (r.status < 200 || r.status >= 300) return reject();
        const rd = new FileReader();
        rd.onloadend = () => resolve({ base64: rd.result.split(',')[1], mimeType: r.response.type });
        rd.onerror = reject;
        rd.readAsDataURL(r.response);
      },
      onerror: reject
    });
  });

  /* ╔═══════════════════════════════════════════╗
     ║  §7  90 / 10 TROLL LOGIC                 ║
     ╚═══════════════════════════════════════════╝ */

  const shouldTroll = () => Math.random() < 0.10;

  // Quiz: pick a random wrong option index
  const randomWrongOption = () => {
    const btns = $$('button.option');
    if (!btns.length) return null;
    return randInt(0, btns.length - 1);
  };

  const trollHighlightQuiz = () => {
    const rand = randomWrongOption();
    if (rand === null) return;
    $$('button.option').forEach(b => { b.classList.remove('fl-hit'); b.querySelector('.fl-hit-tag')?.remove(); });
    $$('button.option').forEach((btn, i) => {
      const dc = btn.getAttribute('data-cy');
      let oi = i;
      if (dc?.startsWith('option-')) { const p = parseInt(dc.replace('option-',''), 10); if (!isNaN(p)) oi = p; }
      if (oi === rand) {
        btn.classList.add('fl-hit'); btn.style.position = 'relative';
        if (!btn.querySelector('.fl-hit-tag')) {
          const t = document.createElement('div'); t.className = 'fl-hit-tag'; t.textContent = '✓'; btn.appendChild(t);
        }
      }
    });
    toast(`instantly solved!\n${MODEL_DISPLAY}  ·  high confidence`);
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §8  QUIZ PROMPT                         ║
     ╚═══════════════════════════════════════════╝ */

  const quizPromptCorrect = (q, opts, hasImg) =>
    `You are an expert quiz assistant.${hasImg ? ' An image is attached — analyze it carefully.' : ''}
You MUST identify the single correct answer. Be precise and certain.

Question: "${q}"
Options:
${opts.map((o, i) => `${i+1}. ${o}`).join('\n')}

Format: "Correct Answer(s): [answer]\\nReasoning: [brief explanation]"`;

  const quizPromptWrong = (q, opts, hasImg) =>
    `You are a quiz assistant.${hasImg ? ' An image is attached.' : ''}
Pick an answer that SOUNDS plausible but is actually WRONG. Be confident.

Question: "${q}"
Options:
${opts.map((o, i) => `${i+1}. ${o}`).join('\n')}

Format: "Correct Answer(s): [answer]\\nReasoning: [brief explanation]"`;

  /* ╔═══════════════════════════════════════════╗
     ║  §9  SETTINGS PANEL                      ║
     ╚═══════════════════════════════════════════╝ */

  const mkRow = (id, label, type) => {
    const row = document.createElement('div');
    row.className = 'fl-row';
    const lbl = document.createElement('span');
    lbl.className = 'fl-lbl'; lbl.textContent = label;
    row.appendChild(lbl);

    if (type === 'toggle') {
      const t = document.createElement('span');
      t.className = 'fl-tog' + (S.config[id] ? ' on' : '');
      t.dataset.id = id;
      t.textContent = S.config[id] ? 'ON' : 'OFF';
      t.onclick = function() { const on = this.classList.toggle('on'); this.textContent = on ? 'ON' : 'OFF'; };
      row.appendChild(t);
    } else if (type === 'model') {
      const sel = document.createElement('select');
      sel.className = 'fl-sel'; sel.dataset.id = id;
      for (const [mid, m] of Object.entries(MODELS)) {
        const opt = document.createElement('option');
        opt.value = mid; opt.textContent = `${m.name} (${m.speed})`;
        if (S.config[id] === mid) opt.selected = true;
        sel.appendChild(opt);
      }
      row.appendChild(sel);
    } else {
      const inp = document.createElement('input');
      inp.className = 'fl-inp' + (type === 'wide' || type === 'pass' ? ' fl-inp-w' : '');
      inp.dataset.id = id;
      if (type === 'pass') inp.type = 'password';
      inp.value = S.config[id] ?? '';
      row.appendChild(inp);
    }
    return row;
  };

  const buildPanel = () => {
    if (S.panel) return;
    const p = document.createElement('div');
    p.className = 'fl-panel';

    const hdr = document.createElement('div');
    hdr.className = 'fl-hdr';
    const hdrTitle = document.createElement('span');
    hdrTitle.className = 'fl-hdr-title'; hdrTitle.textContent = 'F E A T H E R';
    const hdrSub = document.createElement('span');
    hdrSub.className = 'fl-hdr-sub'; hdrSub.textContent = 'lite  ·  v2.1';
    const hdrModel = document.createElement('span');
    hdrModel.className = 'fl-hdr-model'; hdrModel.textContent = MODEL_DISPLAY;
    hdr.appendChild(hdrTitle);
    hdr.appendChild(hdrSub);
    hdr.appendChild(hdrModel);
    p.appendChild(hdr);

    const cls = document.createElement('span');
    cls.className = 'fl-close'; cls.textContent = '[ × ]'; cls.onclick = closePanel;
    p.appendChild(cls);

    const body = document.createElement('div');
    body.className = 'fl-body';

    const sec = (title, els) => {
      const h = document.createElement('div'); h.className = 'fl-sec';
      h.textContent = title; body.appendChild(h);
      els.forEach(e => body.appendChild(e));
    };

    const profRow = document.createElement('div');
    profRow.className = 'fl-prof-row';
    for (const name of Object.keys(PROFILES)) {
      const b = document.createElement('span');
      b.className = 'fl-prof'; b.textContent = name.toUpperCase();
      b.onclick = () => {
        profRow.querySelectorAll('.fl-prof').forEach(x => x.classList.remove('act'));
        b.classList.add('act');
        const v = PROFILES[name];
        for (const k of Object.keys(v)) {
          const tog = p.querySelector(`.fl-tog[data-id="${k}"]`);
          if (tog) { tog.classList.toggle('on', !!v[k]); tog.textContent = v[k] ? 'ON' : 'OFF'; }
          const inp = p.querySelector(`.fl-inp[data-id="${k}"]`);
          if (inp) inp.value = v[k];
        }
      };
      profRow.appendChild(b);
    }
    sec('profiles', [profRow]);
    sec('core', [
      mkRow('enableSpoofFullscreen', 'spoof fullscreen', 'toggle'),
      mkRow('includeImages', 'analyze images', 'toggle'),
    ]);
    sec('automation', [
      mkRow('enableAutoAnswer', 'auto-answer', 'toggle'),
      mkRow('autoAnswerDelay', 'auto delay ms', 'num'),
    ]);
    sec('score', [
      mkRow('enableTimeTakenEdit', 'fake time taken', 'toggle'),
      mkRow('timeTakenMin', 'min ms', 'num'),
      mkRow('timeTakenMax', 'max ms', 'num'),
      mkRow('enableTimerHijack', 'timer hijack', 'toggle'),
      mkRow('timerBonusPoints', 'bonus pts', 'num'),
    ]);
    sec('fun', [
      mkRow('enableReactionSpam', 'reaction spam', 'toggle'),
      mkRow('reactionSpamCount', 'multiplier', 'num'),
      mkRow('reactionSpamDelay', 'delay ms', 'num'),
    ]);
    sec('ai models', [
      mkRow('selectedModel', 'quiz model', 'model'),
      mkRow('formsModel', 'forms model', 'model'),
    ]);
    sec('gemini config', [
      mkRow('geminiApiKey', 'api key', 'pass'),
      mkRow('includeImages', 'analyze images', 'toggle'),
      mkRow('thinkingBudget', 'think budget', 'num'),
      mkRow('maxOutputTokens', 'max output', 'num'),
      mkRow('temperature', 'temperature', 'num'),
    ]);
    sec('server', [mkRow('serverUrl', 'server url', 'wide')]);
    p.appendChild(body);

    const foot = document.createElement('div');
    foot.className = 'fl-foot';
    const mkB = (t, c, fn) => {
      const b = document.createElement('button');
      b.className = `fl-fb ${c}`; b.textContent = t; b.onclick = fn; return b;
    };
    foot.appendChild(mkB('RESET', '', () => { if (confirm('Reset all settings?')) { S.config = { ...DEFAULTS }; GM_setValue(K.CFG, S.config); closePanel(); toast('reset'); } }));
    foot.appendChild(mkB('CLEAR CACHE', '', () => { S.answerCache = {}; GM_setValue(K.CACHE, {}); toast('cache cleared'); }));
    foot.appendChild(mkB('CANCEL', '', closePanel));
    foot.appendChild(mkB('SAVE', 'ok', () => {
      p.querySelectorAll('.fl-tog').forEach(t => { S.config[t.dataset.id] = t.classList.contains('on'); });
      p.querySelectorAll('.fl-inp').forEach(i => {
        const id = i.dataset.id;
        S.config[id] = typeof DEFAULTS[id] === 'number' ? parseFloat(i.value) || 0 : i.value;
      });
      p.querySelectorAll('.fl-sel').forEach(s => { S.config[s.dataset.id] = s.value; });
      GM_setValue(K.CFG, S.config); GM_setValue(K.KEY, S.config.geminiApiKey);
      closePanel(); toast('saved');
    }));
    p.appendChild(foot);
    document.body.appendChild(p);
    S.panel = p;
  };

  const closePanel = () => { if (S.panel) { S.panel.remove(); S.panel = null; } };

  /* ╔═══════════════════════════════════════════╗
     ║  §10  ORB                                ║
     ╚═══════════════════════════════════════════╝ */

  const createOrb = () => {
    const poll = setInterval(() => {
      if (!document.body || $('#fl-orb')) return;
      clearInterval(poll);
      const orb = document.createElement('div');
      orb.id = 'fl-orb'; orb.title = 'click=toggle · 3×=settings';
      orb.textContent = '◆';
      S.orbEl = orb; updateOrb();
      let c = 0, t = null;
      orb.onclick = () => {
        c++; if (t) clearTimeout(t);
        t = setTimeout(() => { if (c >= 3) buildPanel(); else toggleUI(); c = 0; }, 300);
      };
      document.body.appendChild(orb);
    }, 200);
  };

  const updateOrb = () => {
    if (!S.orbEl) return;
    S.orbEl.textContent = S.on ? '◆' : '◇';
    S.orbEl.classList.toggle('off', !S.on);
  };

  const toggleUI = () => {
    S.on = !S.on; GM_setValue(K.UI, S.on); updateOrb();
    $$('.fl-acts,.fl-toast,.fl-auto-badge,.fl-acc').forEach(e => { e.style.display = S.on ? '' : 'none'; });
    if (S.on) wg.scan();
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §11  HIGHLIGHTING                       ║
     ╚═══════════════════════════════════════════╝ */

  const highlight = (answers, qtype) => {
    if (!S.on) return;
    $$('button.option').forEach(b => { b.classList.remove('fl-hit'); b.querySelector('.fl-hit-tag')?.remove(); });
    if (qtype === 'BLANK') { toast(`answer: ${answers}`); return; }
    const idx = Array.isArray(answers) ? answers : [answers];
    $$('button.option').forEach((btn, i) => {
      const dc = btn.getAttribute('data-cy');
      let oi = i;
      if (dc?.startsWith('option-')) { const p = parseInt(dc.replace('option-',''), 10); if (!isNaN(p)) oi = p; }
      if (idx.map(Number).includes(oi)) {
        btn.classList.add('fl-hit'); btn.style.position = 'relative';
        if (!btn.querySelector('.fl-hit-tag')) {
          const t = document.createElement('div'); t.className = 'fl-hit-tag'; t.textContent = '✓'; btn.appendChild(t);
        }
      }
    });
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §12  ANTI-DETECTION (basic)             ║
     ╚═══════════════════════════════════════════╝ */

  const spoofBasic = () => {
    const dp = (o, p, v) => { try { Object.defineProperty(o, p, { get: () => v, configurable: true }); } catch(e){} };
    dp(document, 'fullscreenElement', document.documentElement);
    dp(document, 'webkitFullscreenElement', document.documentElement);
    dp(document, 'fullscreen', true);
    dp(document, 'webkitIsFullScreen', true);
    dp(document, 'hidden', false);
    dp(document, 'visibilityState', 'visible');
    try { Object.defineProperty(document, 'hasFocus', { value: () => true, configurable: true }); } catch(e){}
    dp(window, 'innerHeight', screen.height);
    dp(window, 'innerWidth', screen.width);
    dp(navigator, 'webdriver', false);

    const blocked = new Set(['blur','focus','visibilitychange','webkitvisibilitychange',
      'fullscreenchange','webkitfullscreenchange','pagehide','pageshow']);
    const block = e => { e.stopImmediatePropagation(); e.stopPropagation(); if (e.cancelable) e.preventDefault(); };
    blocked.forEach(ev => { window.addEventListener(ev, block, true); document.addEventListener(ev, block, true); });

    const origAEL = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, opts) {
      if (blocked.has(type?.toLowerCase?.())) return;
      return origAEL.call(this, type, listener, opts);
    };

    document.addEventListener('mouseleave', e => { e.stopImmediatePropagation(); e.stopPropagation(); }, true);
    ['copy','cut','paste','contextmenu','selectstart'].forEach(ev => {
      document.addEventListener(ev, e => e.stopImmediatePropagation(), true);
    });

    if (navigator.sendBeacon) {
      const orig = navigator.sendBeacon.bind(navigator);
      navigator.sendBeacon = (url, data) => {
        if (/cheat|violation|leave|blur|focus|tab|monitor|track|activity|proctor/i.test(String(url))) return true;
        return orig(url, data);
      };
    }

    // Remove overlay elements
    const watchDOM = () => {
      if (!document.body) { setTimeout(watchDOM, 50); return; }
      new MutationObserver(() => {
        for (const el of document.querySelectorAll('[class*="tab-leave"],[class*="TabLeave"],[class*="fullscreen-warning"],[class*="blur-overlay"],[class*="focus-lost"]')) el.remove();
        for (const el of document.querySelectorAll('[class*="modal"],[class*="Modal"]')) {
          if (/fullscreen|tab|leave|switch|blur|focus|cheat|proctor/i.test(el.innerText||'')) el.remove();
        }
      }).observe(document.body, { childList: true, subtree: true });
    };
    watchDOM();
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §13  EXAM PROTECTION                    ║
     ╚═══════════════════════════════════════════╝ */

  const examProtect = () => {
    if (!isExam()) return;
    const susUrl = /cheat|violation|leave|blur|focus|tab|activity|monitor|track|proctor|suspicious|alert|warn|report/i;
    const susBody = /blur|focus|visibility|tab_switch|cheating|violation|proctor|inactive|idle|away|minimize|hidden/i;

    const oXOpen = XMLHttpRequest.prototype.open;
    const oXSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function(m, url, ...a) { this._m = m; this._u = url; return oXOpen.call(this, m, url, ...a); };
    XMLHttpRequest.prototype.send = function(data) {
      if (this._u && susUrl.test(this._u)) return;
      if (data && typeof data === 'string' && susBody.test(data)) return;
      return oXSend.call(this, data);
    };

    const oF = window.fetch;
    window.fetch = function(url, opts) {
      const u = typeof url === 'string' ? url : url?.url || '';
      if (susUrl.test(u)) return Promise.resolve(new Response('{"status":"ok"}', { status: 200 }));
      return oF.call(this, url, opts);
    };

    const safeVars = { isTabActive:true, tabActive:true, isFocused:true, hasFocus:true, isFullscreen:true, tabSwitchCount:0, blurCount:0, cheatingDetected:false, violationCount:0, isIdle:false };
    for (const [v, val] of Object.entries(safeVars)) {
      try { Object.defineProperty(window, v, { get: () => val, set: () => {}, configurable: true }); } catch(e){}
    }
    setInterval(() => {
      for (const [v, val] of Object.entries(safeVars)) {
        try { Object.defineProperty(window, v, { get: () => val, set: () => {}, configurable: true }); } catch(e){}
      }
    }, 300);
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §14  NETWORK INTERCEPTORS               ║
     ╚═══════════════════════════════════════════╝ */

  const quizRx = /soloJoin|rejoinGame|\/join|_quizserver\/main\/v2\/quiz/;
  const procRx = /proceedGame|soloProceed/;

  const setupNet = () => {
    if (isExam()) return;
    const oXOpen = XMLHttpRequest.prototype.open;
    const oXSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function(m, url, ...a) { this._m = m; this._u = url; return oXOpen.call(this, m, url, ...a); };
    XMLHttpRequest.prototype.send = function(data) {
      const url = this._u || '';
      if (quizRx.test(url)) this.addEventListener('load', function() { if (this.status === 200) try { processQuiz(JSON.parse(this.responseText)); } catch(e){} });
      if (procRx.test(url)) {
        this.addEventListener('load', function() { if (this.status === 200) try { procResp(JSON.parse(this.responseText)); } catch(e){} });
        if (this._m === 'POST' && data) try { return oXSend.call(this, JSON.stringify(modProc(JSON.parse(data)))); } catch(e){}
      }
      return oXSend.call(this, data);
    };
    const oFetch = window.fetch;
    window.fetch = function(url, opts) {
      const u = typeof url === 'string' ? url : url?.url || '';
      if (quizRx.test(u)) return oFetch.call(this, url, opts).then(r => { if (r.ok) return r.clone().json().then(d => { processQuiz(d); return r; }).catch(() => r); return r; });
      if (procRx.test(u)) {
        if (opts?.method === 'POST' && opts?.body) try { opts = { ...opts, body: JSON.stringify(modProc(JSON.parse(opts.body))) }; } catch(e){}
        return oFetch.call(this, url, opts).then(r => { if (r.ok) return r.clone().json().then(d => { procResp(d); return r; }).catch(() => r); return r; });
      }
      return oFetch.call(this, url, opts);
    };
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §15  DATA PROCESSING                    ║
     ╚═══════════════════════════════════════════╝ */

  const processQuiz = data => {
    let qs; try { qs = data?.data?.room?.questions || data?.room?.questions || data?.quiz?.info?.questions || data?.data?.quiz?.info?.questions; } catch(e){ return; }
    if (!qs) return;
    for (const k of Object.keys(qs)) {
      S.qData[k] = qs[k];
      if (qs[k].structure?.answer !== undefined) {
        S.detected[k] = { type: qs[k].type, raw: qs[k].structure.answer };
        if (qs[k]._id && qs[k]._id !== k) S.detected[qs[k]._id] = S.detected[k];
        S.answerCache[k] = { type: qs[k].type, answer: qs[k].structure.answer };
        if (qs[k]._id) S.answerCache[qs[k]._id] = S.answerCache[k];
      }
    }
    GM_setValue(K.CACHE, S.answerCache);
  };

  const modProc = data => {
    const r = data?.response || data?.data?.response; if (!r) return data;
    if (S.config.enableTimeTakenEdit && r.timeTaken !== undefined) r.timeTaken = randInt(S.config.timeTakenMin, S.config.timeTakenMax);
    if (S.config.enableTimerHijack) { const sc = r?.provisional?.scoreBreakups?.correct; if (sc) { sc.timer = S.config.timerBonusPoints; sc.total = S.config.timerBonusPoints + (sc.base||0) + (sc.streak||0); if (r.provisional.scores) r.provisional.scores.correct = sc.total; } }
    return data;
  };

  const procResp = data => {
    try {
      const qid = data?.response?.questionId || data?.data?.response?.questionId;
      const q = data?.question || data?.data?.question;
      if (qid && q?.structure?.answer !== undefined) { S.answerCache[qid] = { type: q.type, answer: q.structure.answer }; GM_setValue(K.CACHE, S.answerCache); }
      const attempt = data?.response?.attempt || data?.data?.response?.attempt;
      if (attempt) { S.accuracyCount.total++; if (attempt.isCorrect) S.accuracyCount.correct++; updateAccuracy(); }
    } catch(e){}
  };

  const updateAccuracy = () => {
    let el = document.querySelector('.fl-acc');
    if (!el && document.body) { el = document.createElement('div'); el.className = 'fl-acc'; document.body.appendChild(el); }
    if (el && S.accuracyCount.total > 0) el.textContent = `${Math.round(S.accuracyCount.correct/S.accuracyCount.total*100)}% (${S.accuracyCount.correct}/${S.accuracyCount.total})`;
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §16  QUESTION BUTTONS                   ║
     ╚═══════════════════════════════════════════╝ */

  const addBtns = (container, qText, opts, imgUrl, withAns) => {
    if (container.querySelector('.fl-acts')) return;
    const w = document.createElement('div'); w.className = 'fl-acts';

    const mk = (txt, fn) => {
      const b = document.createElement('button');
      b.className = 'fl-ab'; b.textContent = txt; b.type = 'button';
      b.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); fn(this); }, true);
      b.addEventListener('mousedown', e => { e.stopPropagation(); e.stopImmediatePropagation(); }, true);
      return b;
    };

    w.appendChild(mk('AI', async (btn) => {
      if (shouldTroll()) { trollHighlightQuiz(); return; }
      let img = null;
      if (imgUrl && S.config.includeImages) try { img = await fetchImg64(imgUrl); } catch(e){}
      const key = S.config.geminiApiKey;
      if (!key) { buildPanel(); toast('set api key first'); return; }
      btn.textContent = '...'; btn.classList.add('active');
      toast(`${MODEL_DISPLAY} thinking...`, true);
      try {
        const txt = await geminiCall(quizPromptCorrect(qText, opts, !!img), img ? [img] : [], key);
        toast(txt);
      } catch(e) { toast('error: ' + e.message); }
      finally { btn.textContent = 'AI'; btn.classList.remove('active'); }
    }));

    const ddg = document.createElement('a');
    ddg.className = 'fl-ab'; ddg.textContent = 'DDG';
    ddg.href = `https://duckduckgo.com/?q=${encodeURIComponent(qText)}`;
    ddg.target = '_blank'; ddg.rel = 'noopener';
    ddg.addEventListener('click', e => e.stopPropagation(), true);
    w.appendChild(ddg);

    w.appendChild(mk('COPY', async b => {
      try { await navigator.clipboard.writeText(quizPromptCorrect(qText, opts, !!imgUrl)); } catch { GM_setClipboard(quizPromptCorrect(qText, opts, !!imgUrl)); }
      b.textContent = 'OK'; setTimeout(() => { b.textContent = 'COPY'; }, 1500);
    }));

    if (withAns) {
      w.appendChild(mk('ANS', async () => {
        const result = wg.resolveAnswer(S.curQId);
        if (result) highlight(result.answers, result.type);
        else toast('no cached answer');
      }));
    }

    container.appendChild(w);
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §17  WAYGROUND MODULE                   ║
     ╚═══════════════════════════════════════════╝ */

  const SEL = {
    qC: 'div[data-testid="question-container-text"]', qT: '.question-text-color',
    qI: 'img[data-testid="question-container-image"],div[class*="question-media-container"] img',
    oB: 'button.option', oT: '.option-text div.resizeable,div#optionText div.resizeable',
  };
  let scanTimer = null;

  const wg = {
    getQId: () => $('div[data-quesid]')?.getAttribute('data-quesid') || null,

    resolveAnswer(qid) {
      if (!qid) return null;
      if (S.detected[qid]?.raw !== undefined) return { answers: S.detected[qid].raw, type: S.detected[qid].type };
      if (S.answerCache[qid]?.answer !== undefined) return { answers: S.answerCache[qid].answer, type: S.answerCache[qid].type };
      return null;
    },

    async autoAnswer(qid) {
      if (!S.config.enableAutoAnswer || S.autoAnswerPending) return;
      const result = wg.resolveAnswer(qid);
      if (!result) return;
      S.autoAnswerPending = true;
      setTimeout(() => {
        try {
          const delay = S.config.autoAnswerDelay + randInt(-200, 400);
          const indices = Array.isArray(result.answers) ? result.answers : [result.answers];
          $$('button.option').forEach((btn, idx) => {
            const dc = btn.getAttribute('data-cy');
            let oi = idx;
            if (dc?.startsWith('option-')) { const p = parseInt(dc.replace('option-',''),10); if (!isNaN(p)) oi = p; }
            if (indices.includes(oi)) setTimeout(() => btn.click(), delay + randInt(0, 200));
          });
        } finally { setTimeout(() => { S.autoAnswerPending = false; }, 1200); }
      }, 0);
    },

    scan: async () => {
      if (!S.on) return;
      $$('.fl-acts').forEach(e => e.remove());
      const cid = wg.getQId();
      if (cid && cid !== S.curQId) {
        S.curQId = cid;
        const result = wg.resolveAnswer(cid);
        if (result) { setTimeout(() => highlight(result.answers, result.type), 150); wg.autoAnswer(cid); }
      }
      const c = $(SEL.qC), t = $(SEL.qT);
      if (c && t) {
        const qText = t.textContent.trim();
        const opts = $$(SEL.oB).map(b => $(SEL.oT, b)?.textContent.trim()).filter(Boolean);
        const img = $(SEL.qI)?.src;
        if (qText || img || opts.length) addBtns(c, qText, opts, img, true);
      }
    },

    patchUI: () => {
      const st = $('.start-game span');
      if (st?.textContent.includes('fullscreen')) st.textContent = 'Start Game';
      if (S.config.enableAutoAnswer && !$('.fl-auto-badge') && document.body) {
        const badge = document.createElement('div'); badge.className = 'fl-auto-badge'; badge.textContent = 'AUTO'; document.body.appendChild(badge);
      }
    },

    init: () => {
      if (S.observer) S.observer.disconnect();
      S.observer = new MutationObserver(() => {
        if (scanTimer) clearTimeout(scanTimer);
        scanTimer = setTimeout(() => { if (S.on) { wg.scan(); wg.patchUI(); } }, 220);
      });
      S.observer.observe(document.body, { childList: true, subtree: true });
      if (S.on) { wg.scan(); setTimeout(wg.patchUI, 600); }
    }
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §18  GOOGLE FORMS                       ║
     ╚═══════════════════════════════════════════╝ */

  const gf = {
    apiKey: null, statusEl: null, orbEl: null, panelEl: null,
    panelOpen: false, running: false, extra: '',
    _fieldMap: new Map(),

    getKey() {
      if (this.apiKey) return this.apiKey;
      let k = localStorage.getItem('fl_gf_key') || GM_getValue(K.KEY,'') || S.config.geminiApiKey;
      if (!k) {
        k = window.prompt('Gemini API key (aistudio.google.com/app/apikey):');
        if (!k) return null;
        localStorage.setItem('fl_gf_key', k.trim());
      }
      this.apiKey = k.trim(); return this.apiKey;
    },

    setStatus(text, color) {
      if (!this.statusEl) return;
      this.statusEl.textContent = text;
      this.statusEl.style.color = color || '#444';
    },

    // React-compatible native setter
    nativeSet(el, val) {
      const nativeSetter =
        Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set ||
        Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
      if (nativeSetter) { nativeSetter.call(el, val); } else { el.value = val; }
      el.dispatchEvent(new Event('focus', { bubbles: true }));
      el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: String(val)[0] || 'a' }));
      el.dispatchEvent(new InputEvent('input', { bubbles: true, data: String(val), inputType: 'insertText' }));
      el.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: String(val)[0] || 'a' }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
      el.dispatchEvent(new Event('blur', { bubbles: true }));
    },

    heading(c) { if (!c) return ''; for (const s of ['[role="heading"] .M7eMe','[role="heading"]','.M7eMe','.freebirdFormviewerComponentsQuestionBaseTitle','label']) { const el = c.querySelector(s); if (el?.textContent.trim()) return el.textContent.trim(); } return ''; },
    desc(c) { if (!c) return ''; for (const s of ['.M4DNQ','.gubaDc','.freebirdFormviewerComponentsQuestionBaseDescription']) { const el = c.querySelector(s); if (el?.textContent.trim()) return el.textContent.trim(); } return ''; },
    isRequired(c) { return !!(c?.querySelector('[aria-required="true"],.freebirdFormviewerComponentsQuestionBaseRequiredAsterisk')); },

    async clickListbox(lb, wanted) {
      const norm = s => plain(s).toLowerCase();
      if (lb.getAttribute('aria-expanded') !== 'true') { lb.click(); await sleep(280); }
      const popup = $$('.OA0qNb[jsname="V68bde"],.MocG8c').find(m => isVis(m));
      const options = popup ? $$('[role="option"]', popup) : $$('[role="option"]', lb);
      const target = options.find(o => norm(o.getAttribute('data-value')) === norm(wanted)) || options.find(o => norm(o.textContent) === norm(wanted));
      if (target) { target.click(); await sleep(80); }
      if (lb.getAttribute('aria-expanded') === 'true') { lb.click(); await sleep(40); }
    },

    async getImgData(container) {
      if (!container || !S.config.includeImages) return [];
      const imgs = [];
      for (const img of container.querySelectorAll('img')) {
        const src = img.src || img.dataset?.src;
        if (!src || img.width < 25 || img.height < 25 || /avatar|googleusercontent\.com\/a\//.test(src)) continue;
        try { const d = await fetchImg64(src); if (d) imgs.push(d); } catch(e){}
      }
      return imgs;
    },

    safeJson(txt) {
      if (!txt) throw new Error('Empty response');
      let c = txt.trim().replace(/^```[\w]*\s*/i,'').replace(/\s*```\s*$/i,'').trim();
      try { return JSON.parse(c); } catch(e){}
      const start = c.indexOf('[');
      if (start !== -1) {
        let depth = 0, inStr = false, esc = false;
        for (let i = start; i < c.length; i++) {
          const ch = c[i];
          if (esc) { esc = false; continue; }
          if (ch === '\\' && inStr) { esc = true; continue; }
          if (ch === '"') { inStr = !inStr; continue; }
          if (inStr) continue;
          if (ch === '[') depth++;
          else if (ch === ']') { depth--; if (depth === 0) { try { return JSON.parse(c.slice(start, i+1)); } catch(e){ const fixed = c.slice(start, i+1).replace(/,\s*([\]}])/g,'$1'); try { return JSON.parse(fixed); } catch(e2){} } } }
        }
      }
      throw new Error('JSON parse failed');
    },

    async scrapeSchema() {
      this._fieldMap.clear();
      const schema = [], images = [];
      const Q = '[role="listitem"],.Qr7Oae,.geS5n,.freebirdFormviewerComponentsQuestionBaseRoot,.freebirdFormviewerViewNumberedItemContainer';
      let idx = 0;

      for (const inp of $$('input[type="text"],input[type="email"],input[type="number"],input[type="tel"],input[type="url"]')) {
        if (!isVis(inp) || inp.closest('.PfQ8Lb')) continue;
        const c = inp.closest(Q); const id = `t${idx++}`;
        this._fieldMap.set(id, { type: 'input', el: inp });
        const ci = await this.getImgData(c); images.push(...ci);
        schema.push({ id, type: inp.type||'text', label: plain(this.heading(c)||inp.getAttribute('aria-label')||`Field ${idx}`), description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
      }
      for (const ta of $$('textarea')) {
        if (!isVis(ta)) continue;
        const c = ta.closest(Q); const id = `a${idx++}`;
        this._fieldMap.set(id, { type: 'textarea', el: ta });
        const ci = await this.getImgData(c); images.push(...ci);
        schema.push({ id, type: 'textarea', label: plain(this.heading(c)||'Long answer'), description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
      }
      for (const rg of $$('[role="radiogroup"]')) {
        if (!isVis(rg)) continue;
        const c = rg.closest(Q);
        const radios = $$('[role="radio"]', rg);
        const opts = radios.map(r => plain(r.getAttribute('data-value')||r.getAttribute('aria-label')||r.textContent)).filter(Boolean);
        if (!opts.length) continue;
        const id = `r${idx++}`;
        this._fieldMap.set(id, { type: 'radio', radios });
        const ci = await this.getImgData(c); images.push(...ci);
        schema.push({ id, type: 'radio', label: plain(this.heading(c)||'Choice'), options: [...new Set(opts)], description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
      }
      const seen = new Set();
      for (const cb of $$('[role="checkbox"]')) {
        if (!isVis(cb)) continue;
        const c = cb.closest(Q); if (!c || seen.has(c)) continue; seen.add(c);
        const cbs = $$('[role="checkbox"]', c).filter(isVis);
        const opts = [...new Set(cbs.map(cb => plain(cb.getAttribute('data-answer-value')||cb.getAttribute('aria-label')||cb.textContent)).filter(Boolean))];
        if (!opts.length) continue;
        const id = `c${idx++}`;
        this._fieldMap.set(id, { type: 'checkbox', cbs });
        const ci = await this.getImgData(c); images.push(...ci);
        schema.push({ id, type: 'checkbox', label: plain(this.heading(c)||'Select'), options: opts, description: this.desc(c)||undefined, required: this.isRequired(c), hasImage: ci.length>0 });
      }
      for (const lb of $$('[role="listbox"]').filter(l=>!l.getAttribute('aria-label')?.includes('AM')&&!l.closest('.PfQ8Lb')&&isVis(l))) {
        const c = lb.closest(Q); const id = `s${idx++}`;
        if (lb.getAttribute('aria-expanded')!=='true') { lb.click(); await sleep(280); }
        const popup = $$('.OA0qNb[jsname="V68bde"],.MocG8c').find(m=>isVis(m));
        const opts = (popup?$$('[role="option"]',popup):$$('[role="option"]',lb)).map(o=>plain(o.getAttribute('data-value')||o.textContent)).filter(v=>v&&!/^choose$/i.test(v));
        if (lb.getAttribute('aria-expanded')==='true') { lb.click(); await sleep(60); }
        if (!opts.length) continue;
        this._fieldMap.set(id, { type: 'select', lb });
        schema.push({ id, type: 'select', label: plain(this.heading(c)||'Dropdown'), options: [...new Set(opts)], description: this.desc(c)||undefined, required: this.isRequired(c) });
      }
      for (const d of $$('input[type="date"]')) {
        if (!isVis(d)) continue;
        const c = d.closest(Q); const id = `d${idx++}`;
        this._fieldMap.set(id, { type: 'date', el: d });
        schema.push({ id, type: 'date', label: plain(this.heading(c)||'Date'), required: this.isRequired(c) });
      }
      return { schema, images };
    },

    buildPrompt(schema, extra, hasImages) {
      if (shouldTroll()) {
        return `You are a form assistant. For every field, return a WRONG but plausible-sounding answer. Be confident and authoritative.
Return ONLY a JSON array — no markdown, no explanation: [{"id":"...","value":"..."},...]
SCHEMA: ${JSON.stringify(schema, null, 1)}`;
      }
      return `You are an expert form assistant. Answer every field CORRECTLY and accurately.
RULES:
1. Return ONLY a JSON array. No markdown. No explanation.
2. radio/select/checkbox: ONLY use exact option strings from the schema.
3. Required fields must always have values.
4. For knowledge/quiz questions, give the correct factual answer.
${extra ? `\nUSER INFO: ${extra}\n` : ''}${hasImages ? '\nImages are attached — analyze them carefully.\n' : ''}
SCHEMA (${schema.length} fields):
${JSON.stringify(schema, null, 1)}
RESPOND: [{"id":"...","value":"..."},...]`;
    },

    async fill(plan) {
      if (!Array.isArray(plan)) return;
      for (const { id, value } of plan) {
        if (value == null) continue;
        const field = this._fieldMap.get(id);
        if (!field) continue;
        try {
          if (field.type === 'input' || field.type === 'textarea') {
            this.nativeSet(field.el, String(value)); await sleep(80);
          } else if (field.type === 'radio') {
            const want = plain(String(value)).toLowerCase();
            const tgt = field.radios.find(r => plain(r.getAttribute('data-value')||r.getAttribute('aria-label')||r.textContent).toLowerCase() === want) || field.radios[0];
            if (tgt) { tgt.click(); await sleep(80); }
          } else if (field.type === 'checkbox') {
            const vals = (Array.isArray(value) ? value : [value]).map(v => plain(String(v)).toLowerCase());
            for (const cb of field.cbs) { if (cb.getAttribute('aria-checked')==='true') { cb.click(); await sleep(25); } }
            await sleep(40);
            for (const cb of field.cbs) {
              const label = plain(cb.getAttribute('data-answer-value')||cb.getAttribute('aria-label')||cb.textContent).toLowerCase();
              if (vals.includes(label) && cb.getAttribute('aria-checked')!=='true') { cb.click(); await sleep(40); }
            }
          } else if (field.type === 'select') {
            await this.clickListbox(field.lb, String(value));
          } else if (field.type === 'date') {
            this.nativeSet(field.el, String(value));
          }
        } catch(e) {}
      }
    },

    getNext() { return $$('[role="button"],button').find(b => { const t=(b.textContent||'').toLowerCase().trim(); return (t==='next'||t.includes('next'))&&isVis(b); }); },
    getSubmit() { return $$('[role="button"],button').find(b => { const t=(b.textContent||'').toLowerCase().trim(); return (t==='submit'||t.includes('submit'))&&isVis(b); }); },

    async run() {
      if (this.running) return;
      this.running = true;
      const extra = this.extra; this.extra = '';
      try {
        this.setStatus('waiting...', '#555');
        let waited = 0;
        while (waited < 5000) {
          if ($$('input[type="text"],input[type="email"],textarea,[role="radiogroup"],[role="checkbox"]').some(isVis)) break;
          await sleep(200); waited += 200;
        }
        await sleep(300);

        // Auto-prompt for personal info if fields suggest it
        let localExtra = extra;
        if (!localExtra) {
          this.setStatus('scraping...', '#555');
          const { schema } = await this.scrapeSchema();
          const labels = schema.map(s => s.label.toLowerCase()).join(' ');
          if (/name|id|grade|class|email|phone|address|student/.test(labels)) {
            const v = window.prompt('This form has personal fields.\nAdd your info so AI fills them correctly:\n\ne.g. "my ID is 0101234, name is Vxinne, grade is 11NGS-B"');
            if (v) localExtra = v.trim();
          }
        }

        let page = 0;
        while (page < 50) {
          page++;
          this.setStatus(`page ${page}...`, '#555');
          try {
            this.setStatus('scraping...', '#555');
            await sleep(300);
            const { schema, images } = await this.scrapeSchema();
            if (!schema.length) { this.setStatus('no fields found'); break; }
            const key = this.getKey();
            if (!key) { this.setStatus('no api key'); break; }
            this.setStatus(`${MODEL_DISPLAY}...`, '#666');
            const txt = await geminiCall(this.buildPrompt(schema, localExtra, images.length>0), images, key, { temp: 0.4, maxTokens: 8192, jsonMode: true });
            const plan = this.safeJson(txt);
            this.setStatus(`filling ${plan.length} fields...`, '#666');
            await this.fill(plan);
          } catch(e) { this.setStatus(`error: ${e.message.slice(0,24)}`); break; }

          await sleep(600);
          const next = this.getNext();
          if (next && isVis(next)) {
            this.setStatus('next page...', '#555'); next.click(); await sleep(1400);
            let tries = 0;
            while (tries < 40) { const sp = document.querySelector('.freebirdFormviewerViewNavigationLoadingSpinner'); if (!sp || !isVis(sp)) break; await sleep(100); tries++; }
            await sleep(500);
          } else {
            this.setStatus(this.getSubmit() ? 'ready to submit ✓' : 'done ✓', '#888'); break;
          }
        }
      } finally { this.running = false; }
    },

    init() {
      if (!isGF()) return;
      const createUI = () => {
        if (this.orbEl && document.contains(this.orbEl)) return;
        const orb = document.createElement('div');
        orb.style.cssText = `position:fixed!important;bottom:20px!important;right:20px!important;width:28px!important;height:28px!important;background:transparent!important;border:1px solid rgba(200,200,200,0.1)!important;color:rgba(200,200,200,0.18)!important;font-family:${MONO}!important;font-size:12px!important;display:flex!important;align-items:center!important;justify-content:center!important;cursor:pointer!important;z-index:2147483647!important;transition:opacity .3s,color .3s,border-color .3s!important;user-select:none!important;`;
        orb.textContent = '◈';
        orb.onmouseenter = () => { orb.style.color = 'rgba(200,200,200,0.8)'; orb.style.borderColor = 'rgba(200,200,200,0.4)'; };
        orb.onmouseleave = () => { if (!this.panelOpen) { orb.style.color = 'rgba(200,200,200,0.18)'; orb.style.borderColor = 'rgba(200,200,200,0.1)'; } };
        orb.onclick = e => { e.stopPropagation(); this.togglePanel(); };
        this.orbEl = orb;

        const panel = document.createElement('div');
        panel.style.cssText = `position:fixed!important;bottom:56px!important;right:12px!important;background:#0c0c0c!important;border:1px solid #1e1e1e!important;color:#888!important;font-family:${MONO}!important;font-size:10px!important;z-index:2147483647!important;display:none!important;min-width:220px!important;box-shadow:0 16px 60px rgba(0,0,0,.98)!important;`;
        panel.onclick = e => e.stopPropagation();

        // FIX: replaced innerHTML (blocked by Google Forms CSP) with safe DOM creation
        const hdr = document.createElement('div');
        hdr.style.cssText = `padding:9px 13px 7px;border-bottom:1px solid #181818;`;
        const hdrTitle = document.createElement('div');
        hdrTitle.style.cssText = `font-family:${MONO};font-size:10px;color:#b0b0b0;letter-spacing:.35em;`;
        hdrTitle.textContent = 'FEATHER · FORMS';
        const hdrSub = document.createElement('div');
        hdrSub.style.cssText = `font-size:9px;color:#333;margin-top:2px;letter-spacing:.1em;`;
        hdrSub.textContent = MODEL_DISPLAY;
        hdr.appendChild(hdrTitle);
        hdr.appendChild(hdrSub);
        panel.appendChild(hdr);

        for (const { label, fn } of [
          { label: 'FILL FORM',        fn: () => this.run() },
          { label: 'ADD INSTRUCTIONS', fn: () => { const v = window.prompt('Instructions for AI:'); if (v != null) { this.extra = v.trim(); this.setStatus(this.extra ? 'instructions set' : 'cleared'); } } },
          { label: 'RESET API KEY',    fn: () => { localStorage.removeItem('fl_gf_key'); this.apiKey = null; this.setStatus('key reset'); } },
        ]) {
          const btn = document.createElement('button');
          btn.style.cssText = `display:block;font-family:${MONO};font-size:9px;letter-spacing:.14em;color:#3a3a3a;cursor:pointer;padding:5px 13px;background:transparent;border:none;border-bottom:1px solid #111;width:100%;text-align:left;transition:color .1s,background .1s;`;
          btn.textContent = label;
          btn.onmouseenter = () => { btn.style.color = '#c0c0c0'; btn.style.background = '#111'; };
          btn.onmouseleave = () => { btn.style.color = '#3a3a3a'; btn.style.background = 'transparent'; };
          btn.onclick = e => { e.stopPropagation(); fn(); };
          panel.appendChild(btn);
        }

        const status = document.createElement('div');
        status.style.cssText = `padding:5px 13px;font-size:9px;color:#2a2a2a;letter-spacing:.08em;`;
        status.textContent = 'ready';
        this.statusEl = status;
        panel.appendChild(status);
        this.panelEl = panel;

        const target = document.body || document.documentElement;
        target.appendChild(orb); target.appendChild(panel);
        document.addEventListener('click', () => {
          if (this.panelOpen) {
            this.panelOpen = false; panel.style.display = 'none';
            orb.style.color = 'rgba(200,200,200,0.18)'; orb.style.borderColor = 'rgba(200,200,200,0.1)';
          }
        });
      };
      if (document.body) createUI(); else document.addEventListener('DOMContentLoaded', createUI);
      setInterval(() => { if (!this.orbEl || !document.contains(this.orbEl)) createUI(); }, 1500);
    },

    togglePanel() {
      this.panelOpen = !this.panelOpen;
      if (this.panelEl) this.panelEl.style.display = this.panelOpen ? 'block' : 'none';
      if (this.orbEl) {
        this.orbEl.style.color = this.panelOpen ? 'rgba(200,200,200,0.8)' : 'rgba(200,200,200,0.18)';
        this.orbEl.style.borderColor = this.panelOpen ? 'rgba(200,200,200,0.4)' : 'rgba(200,200,200,0.1)';
      }
    },
  };

  /* ╔═══════════════════════════════════════════╗
     ║  §19  MAIN                               ║
     ╚═══════════════════════════════════════════╝ */

  // document-start early spoof
  (() => {
    const dp = (o, p, v) => { try { Object.defineProperty(o, p, { get: () => v, configurable: true }); } catch(e){} };
    dp(document, 'hidden', false); dp(document, 'visibilityState', 'visible'); dp(navigator, 'webdriver', false);
    if (!isGF() && !isExam()) setupNet();
  })();

  const main = () => {
    if (isGF()) { gf.init(); return; }
    if (S.config.enableSpoofFullscreen) spoofBasic();
    examProtect();
    createOrb();
    wg.init();
    document.addEventListener('keydown', e => {
      if (e.altKey && e.key === 'l') { e.preventDefault(); toggleUI(); }
      if (e.altKey && e.key === 's') { e.preventDefault(); buildPanel(); }
    });
    GM_registerMenuCommand('Feather Lite Settings', buildPanel);
    if (!GM_getValue(K.FIRST, false)) {
      GM_setValue(K.FIRST, true);
      setTimeout(() => toast('feather lite v2.1\n\n◆  click orb to toggle\n◆  3×  orb = settings\n◆  alt+l  /  alt+s'), 1500);
    }
  };

  if (document.body) main(); else document.addEventListener('DOMContentLoaded', main);
})();