ChatGPT Bubble Theme Pro

自定义 ChatGPT 气泡:纯色/线性/放射/弥散光渐变、磨砂、外发光、描边、圆角/内边距……支持 Alt+G 打开面板;移动端优化与节流。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Bubble Theme Pro
// @namespace    https://greasyfork.org/zh-CN/users/1503226-loom29
// @version      1.9.3
// @author       Ech0
// @description  自定义 ChatGPT 气泡:纯色/线性/放射/弥散光渐变、磨砂、外发光、描边、圆角/内边距……支持 Alt+G 打开面板;移动端优化与节流。
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license      MIT
// ==/UserScript==
(function () {
  "use strict";

  const IS_MOBILE = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) ||
                    (window.matchMedia && matchMedia("(max-width: 820px)").matches);
  const SAFE_MODE = !!IS_MOBILE;

  /*** 性能:样式节流 + 归拢开关 ***/
  let NEED_ENSURE = true;
  let styleTimer = null;
  const requestStyleUpdate = () => {
    if (styleTimer) return;
    styleTimer = setTimeout(() => { styleTimer = null; applyStyle(); }, IS_MOBILE ? 90 : 45);
  };

  /*** 主题与默认值 ***/
  const THEMES = {
    "海盐": { mode:"diffuse", angle:135, colors:["#89d0d2","#a8ced7","#88b9ce"], weights:[34,33,33], alpha:0.25, blur:10, glow:0, outlineColor:"#78afb0", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2d3d43", soft:10, autoText:false },
    "春日": { mode:"diffuse", angle:130, colors:["#d5e7ab","#e2e29d","#b4d4ab"], weights:[34,33,33], alpha:0.45, blur:9, glow:0, outlineColor:"#b0cda7", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2e3f27", autoText:false },
    "蜜桃": { mode:"diffuse", angle:125, colors:["#f2d8d4","#ebbeb3","#f4d5b8"], weights:[34,33,33], alpha:0.40, blur:8, glow:0, outlineColor:"#e6a08e", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#47352e", autoText:false },
    "洋芋": { mode:"diffuse", angle:125, colors:["#9898c5","#b7cfe5","#b9b5c5"], weights:[34,33,33], alpha:0.30, blur:8, glow:0, outlineColor:"#9189be", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#4d4860", autoText:false },
    "奶油": { mode:"diffuse", angle:125, colors:["#f0e6d0","#e2dfca","#ecd8c1"], weights:[34,33,33], alpha:0.30, blur:8, glow:0, outlineColor:"#c8b493", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#5c5951", autoText:false },
    "湖畔": { mode:"diffuse", angle:135, colors:["#76b9d5","#a9d3d6","#34b7a1"], weights:[34,33,33], alpha:0.70, blur:10, glow:0, outlineColor:"#65b3b8", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#09161b", soft:20, autoText:false },
    "热异常": { mode:"linear", angle:180, colors:["#dcd6d6","#eab06d","#e95f28"], weights:[27,47,26], alpha:1, blur:9, glow:0, outlineColor:"#610505", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#2e3f27", soft:20, autoText:true },
    "水泥": { mode:"diffuse", angle:135, colors:["#4d5b6f","#626e7a","#4f5863"], weights:[34,33,33], alpha:0.85, blur:10, glow:0, outlineColor:"#0d3f5e", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true },
    "墨玉": { mode:"diffuse", angle:135, colors:["#3c5639","#274b37","#2a5026"], weights:[34,33,33], alpha:0.75, blur:10, glow:0, outlineColor:"#0d631f", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true },
    "红酒": { mode:"diffuse", angle:135, colors:["#640707","#2f0909","#570a29"], weights:[34,33,33], alpha:0.90, blur:10, glow:0, outlineColor:"#651406", outlineAlpha:0, outlineWidth:1, radius:16, padV:8, padH:12, textColor:"#ffffff", autoText:true },
    "纯黑": { mode:"solid", angle:135, colors:["#000000","#000000","#000000"], weights:[100,0,0], alpha:1.00, blur:10, glow:0, outlineColor:"#000000", outlineAlpha:0, outlineWidth:0, radius:16, padV:8, padH:12, textColor:"#ffffff", soft:10, autoText:true }
  };
  const DEFAULTS = {
    targets: { user:true, assistant:false, sendbtn:true },
    followOfSendBtn: "user",
    user: structuredClone(THEMES["海盐"]),
    assistant: structuredClone(THEMES["海盐"]),
    lastTheme: { user:"海盐", assistant:"海盐" },
    overrides: { user:{}, assistant:{} },
    customThemes: {},
    galleryOrder: Object.keys(THEMES),
    panel: { x:null, y:null, w:560, h:560 }
  };

  // —— 这里只声明一次,避免重复声明告警 ——
  const PANEL_ID = "ech0-theme-panel";
  const STYLE_ID = "ech0-theme-style";

  const clone = (x)=>JSON.parse(JSON.stringify(x));
  const clamp=(v,min,max)=>Math.min(max,Math.max(min,v));
  const pad2=(n)=>n.toString(16).padStart(2,"0");
  const hexToRgb=(hex)=>{ let h=(hex||"").replace("#","").trim(); if(h.length===3) h=h.split("").map(x=>x+x).join(""); const n=parseInt(h||"000000",16); return { r:(n>>16)&255, g:(n>>8)&255, b:n&255 }; };
  const rgba=(hex,a)=>{ const { r,g,b }=hexToRgb(hex); return `rgba(${r},${g},${b},${clamp(a,0,1)})`; };
  const load=()=>{ try{ const s=GM_getValue("ech0_theme_pro"); if(s) return Object.assign(clone(DEFAULTS), s);}catch{} return clone(DEFAULTS); };
  const save=(cfg)=>{ try{ GM_setValue("ech0_theme_pro", cfg); }catch{} };
  let CFG = load(); // 提前加载,保证后续调用可用

  function hslToRgb(h,s,l){
    h=((h%360)+360)%360; s/=100; l/=100;
    const c=(1-Math.abs(2*l-1))*s, x=c*(1-Math.abs((h/60)%2-1)), m=l-c/2;
    let r=0,g=0,b=0;
    if(h<60){ r=c; g=x; }
    else if(h<120){ r=x; g=c; }
    else if(h<180){ g=c; b=x; }
    else if(h<240){ g=x; b=c; }
    else if(h<300){ r=x; b=c; }
    else { r=c; b=x; }
    return { r:Math.round((r+m)*255), g:Math.round((g+m)*255), b:Math.round((b+m)*255) };
  }
  function parseColorToHex(s){
    if(!s) return null;
    s=s.trim();
    let m=s.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
    if(m){ let h=m[1]; if(h.length===3) h=h.split("").map(x=>x+x).join(""); return "#"+h.toLowerCase(); }
    m=s.match(/^rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i);
    if(m){ const r=clamp(Math.round(+m[1]),0,255), g=clamp(Math.round(+m[2]),0,255), b=clamp(Math.round(+m[3]),0,255); return "#"+pad2(r)+pad2(g)+pad2(b); }
    m=s.match(/^hsla?\s*\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%(?:\s*,\s*([\d.]+))?\s*\)$/i);
    if(m){ const { r,g,b }=hslToRgb(+m[1],+m[2],+m[3]); return "#"+pad2(r)+pad2(g)+pad2(b); }
    return null;
  }
  function relLuma(hex){
    const { r,g,b }=hexToRgb(hex);
    const sr=[r,g,b].map(v=>v/255).map(v=>v<=0.04045? v/12.92 : Math.pow((v+0.055)/1.055,2.4));
    return 0.2126*sr[0]+0.7152*sr[1]+0.0722*sr[2];
  }
  function themeLuma(s){
    const w=s.weights||[1,1,1], sum=w.reduce((a,b)=>a+b,0)||1;
    const cols=(s.colors||[]).filter(Boolean); let L=0;
    cols.forEach((c,i)=>{ L+=relLuma(c)*(w[i]||0)/sum; });
    return L;
  }
  const pickAutoTextColor=(s)=> themeLuma(s)>0.55 ? "#000000" : "#ffffff";

  const SOFT=10, MAX_SOFT=50;
  function makeBackground(s){
    const cols=(s.colors||[]).filter(Boolean);
    const c1=cols[0]||"#000000", c2=cols[1]||c1, c3=cols[2]||c2;
    const a1=rgba(c1,s.alpha), a2=rgba(c2,s.alpha), a3=rgba(c3,s.alpha);
    const w=(s.weights||[100,0,0]).map(n=>Math.max(0,+n||0));
    const sum=w.reduce((a,b)=>a+b,0);
    if(s.mode==="solid") return a1;
    const softBase=Math.max(0,Math.min((s.soft??SOFT),MAX_SOFT));
    if(sum<=0) return a1;

    if(s.mode==="diffuse"){
      const k1=w[0]/sum, k2=w[1]/sum, k3=w[2]/sum;
      const angle=typeof s.angle==="number"?s.angle:135;
      return [
        `radial-gradient(60% 60% at 0% 0%, ${a1} ${Math.round(k1*60)}%, transparent 60%)`,
        `radial-gradient(60% 60% at 100% 0%, ${a2} ${Math.round(k2*60)}%, transparent 60%)`,
        `radial-gradient(60% 60% at 0% 100%, ${a3} ${Math.round(k3*60)}%, transparent 60%)`,
        `linear-gradient(${angle}deg, ${a1}, ${a2}, ${a3})`
      ].join(",");
    }

    if(s.mode==="radial"){
      const r1=w[0]/sum*100, r2=(w[0]+w[1])/sum*100;
      const t1=Math.min(softBase,r1,(r2-r1)/2), t2=Math.min(softBase,(r2-r1)/2,100-r2);
      return `radial-gradient(120% 100% at 0% 0%,
        ${a1} 0%, ${a1} ${Math.max(0,r1-t1)}%,
        ${a2} ${Math.min(100,r1+t1)}%, ${a2} ${Math.max(0,r2-t2)}%,
        ${a3} ${Math.min(100,r2+t2)}%, ${a3} 100%)`;
    }

    const p1=w[0]*100/sum, p2=(w[0]+w[1])*100/sum;
    const t1=Math.min(softBase,p1,(p2-p1)/2,100-p1);
    const t2=Math.min(softBase,(p2-p1)/2,100-p2);
    const angle=typeof s.angle==="number"?s.angle:135;
    if(p1<=0 && p2<=0) return `linear-gradient(${angle}deg, ${a3} 0%, ${a3} 100%)`;
    if(p1>=100) return `linear-gradient(${angle}deg, ${a1} 0%, ${a1} 100%)`;
    return `linear-gradient(${angle}deg,
      ${a1} 0%, ${a1} ${Math.max(0,p1-t1)}%,
      ${a2} ${Math.min(100,p1+t1)}%, ${a2} ${Math.max(0,p2-t2)}%,
      ${a3} ${Math.min(100,p2+t2)}%, ${a3} 100%)`;
  }

  const CANDIDATES=[":scope > .markdown",":scope .markdown",":scope .prose",":scope .whitespace-pre-wrap"];
  const getCandidateBlocks=(el)=> Array.from(el.querySelectorAll(CANDIDATES.join(","))).filter(n=>!n.closest(".ech0-bubble"));
  const containsAll=(c,nodes)=> nodes.every(n=>c.contains(n));
  function findCommonContainer(msgEl,nodes){
    if(!nodes.length) return null;
    for(let c=nodes[0].parentElement; c && c!==msgEl; c=c.parentElement){
      if(containsAll(c,nodes)) return c;
    }
    return msgEl;
  }
  function wrapIntoBubble(container,nodes){
    if(!container) return;
    let shell=container.querySelector(":scope > .ech0-bubble");
    if(!shell){ shell=document.createElement("div"); shell.className="ech0-bubble"; container.insertBefore(shell,container.firstChild); }
    const tops=[];
    nodes.forEach(n=>{ let t=n; while(t.parentElement && t.parentElement!==container) t=t.parentElement; if(!tops.includes(t)) tops.push(t); });
    tops.sort((a,b)=>a.compareDocumentPosition(b)&Node.DOCUMENT_POSITION_FOLLOWING?-1:1);
    tops.forEach(t=>{ if(t!==shell) shell.appendChild(t); });
  }
  function glueOneMessage(msgEl){
    const nodes=getCandidateBlocks(msgEl);
    if(!nodes.length) return;
    const c=findCommonContainer(msgEl,nodes);
    if (SAFE_MODE){ c.classList.add("ech0-bubble-s"); return; }
    wrapIntoBubble(c,nodes);
  }
  function ensureShells(role){
    document.querySelectorAll(`[data-message-author-role="${role}"]`).forEach(glueOneMessage);
  }

  const SEND_BTN_SEL=[
    'button[data-testid="send-button"]',
    'form button[type="submit"]',
    'button[aria-label="Send"]',
    '[data-testid="composer-send-button"] button'
  ].join(",");
  const makeGlow=(hex,str)=> str>0?`0 6px 18px ${rgba(hex,clamp(str*0.55,0,1))}, 0 0 18px ${rgba(hex,clamp(str*0.35,0,1))}`:"none";

  function cssForRole(role,s){
    const bg=makeBackground(s);
    const outline=rgba(s.outlineColor,s.outlineAlpha);
    const border=(s.outlineAlpha<=0 || s.outlineWidth<=0)?"none":`${s.outlineWidth}px solid ${outline}`;
    const shadow=makeGlow(s.outlineColor,s.glow);
    const pfx=role==="user"?"user":"asst";
    const isLight=themeLuma(s)>0.55;
    const preBg=isLight?"rgba(0,0,0,.08)":"rgba(255,255,255,.12)";
    const preBorder=isLight?"1px solid rgba(0,0,0,.12)":"1px solid rgba(255,255,255,.16)";
    const text=s.autoText?pickAutoTextColor(s):(s.textColor||"#101418");
    const blurPx = IS_MOBILE ? Math.min(s.blur, 4) : s.blur; // 移动端 blur 限幅

    const bubbleSel = SAFE_MODE ? `.ech0-bubble-s` : `.ech0-bubble`;
    const clearHost = SAFE_MODE ? "" : `
[data-message-author-role="${role}"], [data-message-author-role="${role}"] > div,
[data-message-author-role="${role}"] .text-message, [data-message-author-role="${role}"] .relative{
  background:transparent!important; box-shadow:none!important; border-color:transparent!important;
}`;

    return `
:root{ --ech0-${pfx}-text:${text}; }
${clearHost}
[data-message-author-role="${role}"] ${bubbleSel}{
  display:inline-block; width:fit-content; max-width:100%;
  background:${bg}!important; color:var(--ech0-${pfx}-text)!important;
  border:${border}!important; border-radius:${s.radius}px!important;
  padding:${s.padV}px ${s.padH}px!important; box-shadow:${shadow}!important;
  -webkit-backdrop-filter:blur(${blurPx}px)!important; backdrop-filter:blur(${blurPx}px)!important;
}
[data-message-author-role="${role}"] ${bubbleSel} :not(pre):not(code){
  color:var(--ech0-${pfx}-text)!important;
  -webkit-text-fill-color:var(--ech0-${pfx}-text)!important;
}
[data-message-author-role="${role}"] ${bubbleSel} pre{
  background:${preBg}!important; border:${preBorder}!important; border-radius:10px!important;
}`;
  }

  function applyStyle(){
    if (NEED_ENSURE){
      if (CFG.targets.user) ensureShells("user");
      if (CFG.targets.assistant) ensureShells("assistant");
      NEED_ENSURE = false;
    }

    let css=`
.ech0-bubble{ background:transparent; background-clip:padding-box; }
.ech0-bubble-s{ background:transparent; background-clip:padding-box; }
#${PANEL_ID} .hd{ position:sticky; top:0; z-index:5; }
#${PANEL_ID} .previewRow{ position:relative; z-index:1; }
`;

    if(CFG.targets.user) css+=cssForRole("user",CFG.user);
    if(CFG.targets.assistant) css+=cssForRole("assistant",CFG.assistant);

    if(CFG.targets.sendbtn){
      const s=CFG.followOfSendBtn==="assistant"?CFG.assistant:CFG.user;
      const bg=makeBackground(s);
      const outline=rgba(s.outlineColor,s.outlineAlpha);
      const border=(s.outlineAlpha<=0 || s.outlineWidth<=0)?"none":`${s.outlineWidth}px solid ${outline}`;
      const textColor = s.autoText ? pickAutoTextColor(s) : (s.textColor || "#101418");
      const blurPx = IS_MOBILE ? Math.min(s.blur, 4) : s.blur;
      css+=`
${SEND_BTN_SEL}{
  background:${bg}!important; border:${border}!important; border-radius:9999px!important;
  -webkit-backdrop-filter:blur(${blurPx}px)!important; backdrop-filter:blur(${blurPx}px)!important;
  color:${textColor}!important;
}
${SEND_BTN_SEL} svg{
  color:${textColor}!important;
  --send-stroke: 0.10px;
}
${SEND_BTN_SEL} svg *{
  filter:none!important; mix-blend-mode:normal!important; opacity:1!important;
  -webkit-mask-image:none!important; mask:none!important;
  fill:none!important; stroke:currentColor!important; stroke-linecap:round!important; stroke-linejoin:round!important;
  vector-effect:non-scaling-stroke!important; stroke-width:var(--send-stroke)!important; shape-rendering:geometricPrecision; paint-order:stroke fill markers;
}
`;
    }

    let node=document.getElementById(STYLE_ID);
    if(!node){ node=document.createElement("style"); node.id=STYLE_ID; document.head.appendChild(node); }
    node.textContent=css;
    updatePreview();
  }

  function openPanel(){
    if(document.getElementById(PANEL_ID)) return;
    const w=document.createElement("div"); w.id=PANEL_ID;
    w.innerHTML=`
<div class="hd" id="ech0-hd"><strong>Bubble Theme Pro</strong><div class="sp"></div><button data-act="close" title="关闭">✕</button></div>

<div class="bar">
  <label><input type="checkbox" id="t-user"> 用户气泡</label>
  <label><input type="checkbox" id="t-assistant"> AI 气泡</label>
  <label><input type="checkbox" id="t-sendbtn"> 发送按钮</label>
  <span class="sep"></span>
  <label>发送按钮跟随
    <select id="followSel"><option value="user">用户</option><option value="assistant">AI</option></select>
  </label>
</div>

<div class="tabs">
  <button class="tab active" data-role="user">编辑:用户</button>
  <button class="tab" data-role="assistant">编辑:AI</button>
  <div class="sp"></div>
  <button class="chipEdit" id="editChipsBtn">编辑主题</button>
</div>

<div class="chipWall" id="chipWall"></div>

<div class="previewRow">
  <div class="previewLabel">预览:</div>
  <div class="previewWrap"><div id="ech0-preview" class="previewBubble">正在输入中.............</div></div>
</div>

<div class="grid">
  <div class="col">
    <div class="item"><label>背景类型</label>
      <select id="mode"><option value="linear">线性渐变</option><option value="radial">放射渐变</option><option value="diffuse">弥散光渐变</option><option value="solid">纯色</option></select>
    </div>
    <div class="item"><label>角度(线性)</label><div class="dual"><input type="range" id="angleR" min="0" max="360" step="1"><input type="number" id="angle" min="0" max="360" step="1"></div></div>

    <div class="item"><label>文字颜色</label>
      <div class="dualColor">
        <input type="text" id="textHex" class="hex" placeholder="#RRGGBB">
        <input type="color" id="textColor">
        <label class="inline"><input type="checkbox" id="autoText"> 自动</label>
      </div>
    </div>

    <div class="item"><label>颜色1</label><div class="dualColor"><input type="text" id="h1" class="hex" placeholder="#RRGGBB"><input type="color" id="c1"></div></div>
    <div class="item"><label>颜色2</label><div class="dualColor"><input type="text" id="h2" class="hex" placeholder="#RRGGBB"><input type="color" id="c2"></div></div>
    <div class="item"><label>颜色3</label><div class="dualColor"><input type="text" id="h3" class="hex" placeholder="#RRGGBB"><input type="color" id="c3"></div></div>

    <div class="item"><label>配重1/2/3(%)</label>
      <div class="weights">
        <div class="row"><input type="range" id="w1r" min="0" max="100" step="1"><input type="number" id="w1" min="0" max="100" step="1"></div>
        <div class="row"><input type="range" id="w2r" min="0" max="100" step="1"><input type="number" id="w2" min="0" max="100" step="1"></div>
        <div class="row"><input type="range" id="w3r" min="0" max="100" step="1" disabled><input type="number" id="w3" min="0" max="100" step="1" readonly></div>
      </div>
    </div>

    <div class="hint" id="weightHint">配重合计:0%(建议=100%)</div>
  </div>

  <div class="col">
    <div class="item"><label>过渡羽化(%)</label><div class="dual"><input type="range" id="softR" min="0" max="50" step="1"><input type="number" id="soft" min="0" max="50" step="1"></div></div>
    <div class="item"><label>透明度</label><div class="dual"><input type="range" id="alphaR" min="0" max="1" step="0.01"><input type="number" id="alpha" min="0" max="1" step="0.01"></div></div>
    <div class="item"><label>磨砂(blur)</label><div class="dual"><input type="range" id="blurR" min="0" max="30" step="1"><input type="number" id="blur" min="0" max="30" step="1"></div></div>
    <div class="item"><label>发光强度</label><div class="dual"><input type="range" id="glowR" min="0" max="1" step="0.01"><input type="number" id="glow" min="0" max="1" step="0.01"></div></div>

    <div class="item"><label>描边色</label>
      <div class="dualColor">
        <input type="text" id="outlineHex" class="hex" placeholder="#RRGGBB">
        <input type="color" id="outlineColor">
      </div>
    </div>

    <div class="item"><label>描边透明</label><div class="dual"><input type="range" id="outlineAlphaR" min="0" max="1" step="0.01"><input type="number" id="outlineAlpha" min="0" max="1" step="0.01"></div></div>
    <div class="item"><label>描边宽(px)</label><div class="dual"><input type="range" id="outlineWidthR" min="0" max="6" step="1"><input type="number" id="outlineWidth" min="0" max="6" step="1"></div></div>

    <div class="item"><label>圆角(px)</label><div class="dual"><input type="range" id="radiusR" min="0" max="40" step="1"><input type="number" id="radius" min="0" max="40" step="1"></div></div>
    <div class="item"><label>内边距-垂直</label><div class="dual"><input type="range" id="padVR" min="0" max="40" step="1"><input type="number" id="padV" min="0" max="40" step="1"></div></div>
    <div class="item"><label>内边距-水平</label><div class="dual"><input type="range" id="padHR" min="0" max="40" step="1"><input type="number" id="padH" min="0" max="40" step="1"></div></div>
  </div>
</div>

<div class="presets">
  <div class="left">
    <input id="presetName" placeholder="预设名称">
    <button data-act="savePreset">另存为</button>
  </div>
  <div class="right">
    <button data-act="save">保存</button>
    <button data-act="reset">重置默认</button>
  </div>
</div>
`;
    document.body.appendChild(w);

    w.querySelector('[data-act="close"]').addEventListener("click",(ev)=>{ ev.stopPropagation(); w.remove(); });

    GM_addStyle(`
#${PANEL_ID}{ position:fixed; z-index:999999; left:50%; top:60px; transform:translateX(-50%);
  width:${CFG.panel.w||560}px; height:${CFG.panel.h||560}px; background:#fff; color:#1b1f24;
  border:1px solid rgba(0,0,0,.12); border-radius:14px; box-shadow:0 16px 48px rgba(0,0,0,.16);
  overflow:auto; resize:both; font:13px/1.35 system-ui, Segoe UI, Arial; cursor:default; }
#${PANEL_ID} .hd{ position:sticky; top:0; z-index:5; display:flex; align-items:center; gap:8px; padding:8px 10px;
  user-select:none; background:#fff; border-bottom:1px solid rgba(0,0,0,.06); border-top-left-radius:14px; border-top-right-radius:14px; cursor:default; }
#${PANEL_ID} .sp{ flex:1; }
#${PANEL_ID} .bar{ display:flex; align-items:center; gap:8px; padding:6px 10px; flex-wrap:wrap; cursor:default; }
#${PANEL_ID} .sep{ width:1px; height:16px; background:rgba(0,0,0,.08); margin:0 4px; }
#${PANEL_ID} .tabs{ display:flex; align-items:center; gap:6px; padding:6px 10px; border-top:1px solid rgba(0,0,0,.06); cursor:default; }
#${PANEL_ID} .tab{ border:1px solid rgba(0,0,0,.12); background:#f5f7fb; border-radius:8px; padding:5px 8px; cursor:default; }
#${PANEL_ID} .tab.active{ background:#e9eef9; }
#${PANEL_ID} .chipEdit{ border:1px solid rgba(0,0,0,.12); background:#fff; border-radius:999px; padding:5px 10px; cursor:default; }
#${PANEL_ID} .chipWall{ display:flex; flex-wrap:wrap; gap:6px; padding:0 10px 6px; border-bottom:1px solid rgba(0,0,0,.06); cursor:default; }
#${PANEL_ID} .chip{ display:flex; align-items:center; gap:6px; padding:5px 8px; border:1px solid rgba(0,0,0,.12); background:#f7f9fc; border-radius:999px; cursor:default; user-select:none; }
#${PANEL_ID} .chip .dot{ width:14px; height:14px; border-radius:999px; border:1px solid rgba(0,0,0,.12); background:linear-gradient(135deg,var(--c1),var(--c2),var(--c3)); }
#${PANEL_ID} .chip .x{ display:none; width:16px; height:16px; border-radius:50%; border:1px solid rgba(0,0,0,.25); background:#fff; line-height:14px; text-align:center; font-size:11px; }
#${PANEL_ID}.editing .chip .x{ display:inline-block; }
#${PANEL_ID} .chip.active{ background:#e9eef9; border-color:rgba(59,130,246,.35); box-shadow: inset 0 0 0 2px rgba(59,130,246,.18); }
#${PANEL_ID} .previewRow{ display:grid; grid-template-columns:56px 1fr; align-items:center; gap:8px; padding:6px 10px; }
#${PANEL_ID} .previewBubble{ display:inline-block; padding:6px 10px; border-radius:12px; border:1px solid rgba(0,0,0,.12); width:fit-content; max-width:100%; white-space:nowrap; }
#${PANEL_ID} .grid{ display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:6px 10px; }
#${PANEL_ID} .col{ display:flex; flex-direction:column; gap:8px; }
#${PANEL_ID} .item{ display:flex; align-items:center; gap:8px; cursor:default; }
#${PANEL_ID} label{ width:116px; color:#3b4250; }
#${PANEL_ID} .bar label{ width:auto!important; display:inline-flex; align-items:center; gap:8px; margin:0; white-space:nowrap; }
#${PANEL_ID} #followSel{ min-width:92px; padding-right:22px; }
#${PANEL_ID} .dual{ display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .dual input[type="range"]{ width:128px; }
#${PANEL_ID} .dual input[type="number"]{ width:56px; height:26px; line-height:26px; font-size:13px; padding:4px 6px; border-radius:7px; cursor:default; }
#${PANEL_ID} .dualColor{ display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .dualColor input.hex{ width:96px; height:26px; line-height:26px; font-size:13px; }
#${PANEL_ID} .dualColor .inline{ display:inline-flex; align-items:center; gap:4px; font-size:12px; color:#64748b; margin-left:6px; }
#${PANEL_ID} .dualColor input:disabled{ opacity:.6; }
#${PANEL_ID} input[type="color"]{ width:34px; height:24px; border:none; background:transparent; cursor:default; }
#${PANEL_ID} input[type="number"], #${PANEL_ID} select, #${PANEL_ID} input[type="text"]{ font-size:13px; height:26px; line-height:26px; padding:4px 6px; border-radius:7px; border:1px solid rgba(0,0,0,.14); background:#fff; cursor:default; }
#${PANEL_ID} input[type="range"]{ -webkit-appearance:none; appearance:none; background:transparent; height:16px; cursor:default; }
#${PANEL_ID} input[type="range"]::-webkit-slider-runnable-track{ height:3px; border-radius:2px; background:#d1d5db; }
#${PANEL_ID} input[type="range"]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; width:12px; height:12px; border-radius:50%; background:#4b5563; border:none; margin-top:-4.5px; }
#${PANEL_ID} .weights .row{ display:flex; align-items:center; gap:6px; margin-bottom:2px; }
#${PANEL_ID} .weights input[type="range"]{ width:128px; }
#${PANEL_ID} .weights input[type="number"]{ width:56px; height:26px; line-height:26px; font-size:13px; }
#${PANEL_ID} .hint{ padding-left:116px; color:#6b7280; }
#${PANEL_ID} .presets{ display:flex; justify-content:space-between; align-items:center; gap:8px; padding:8px 10px; border-top:1px solid rgba(0,0,0,.06); cursor:default; }
#${PANEL_ID} .presets .left{ display:flex; align-items:center; gap:6px; }
#${PANEL_ID} .presets input{ width:140px; cursor:default; }
#${PANEL_ID} button{ background:#f5f7fb; border:1px solid rgba(0,0,0,.12); border-radius:8px; padding:5px 10px; cursor:default; }
#${PANEL_ID} #followSel, #${PANEL_ID} #mode{ font-size:12px; height:26px; line-height:26px; padding:0 24px 0 8px; text-align:center; text-align-last:center; cursor:default; }
#${PANEL_ID} #mode{ min-width:128px; }
#${PANEL_ID} #followSel option, #${PANEL_ID} #mode option{ text-align:center; }
@media (max-width: 820px){
  #${PANEL_ID}{ left:50%!important; top:10px!important; transform:translateX(-50%)!important;
    width:min(94vw, ${CFG.panel.w||560}px)!important; height:min(78vh, ${CFG.panel.h||560}px)!important;
    max-width:96vw; max-height:82vh; -webkit-overflow-scrolling:touch; overscroll-behavior:contain; touch-action:auto; }
  #${PANEL_ID} .hd{ touch-action:none; }
  #${PANEL_ID} .grid{ grid-template-columns:1fr; }
  #${PANEL_ID} .previewRow{ grid-template-columns:48px 1fr; }
}`);
    if(CFG.panel.x!=null && CFG.panel.y!=null){ w.style.left=CFG.panel.x+"px"; w.style.top=CFG.panel.y+"px"; w.style.transform="none"; }

    // 拖动
    (function enableDrag(){
      const hd = w.querySelector("#ech0-hd");
      let dragging = false, sx = 0, sy = 0, ox = 0, oy = 0;
      const clampToViewport = (nx,ny)=>{
        const pad=8, r=w.getBoundingClientRect(), vw=innerWidth, vh=innerHeight;
        nx = Math.max(pad, Math.min(vw - r.width - pad, nx));
        ny = Math.max(pad, Math.min(vh - r.height - pad, ny));
        return { nx, ny };
      };
      const getPoint = (e) => (e.touches && e.touches[0]) || e;
      const isInteractive = (el) => el && el.closest && el.closest('[data-act="close"],button,select,input,textarea,a');
      function onStart(e){ if (isInteractive(e.target)) return;
        const p=getPoint(e); dragging=true; const r=w.getBoundingClientRect(); sx=p.clientX; sy=p.clientY; ox=r.left; oy=r.top;
        if (hd.setPointerCapture && e.pointerId!=null) hd.setPointerCapture(e.pointerId); e.preventDefault(); }
      function onMove(e){ if(!dragging) return; const p=getPoint(e); let nx=ox+(p.clientX-sx), ny=oy+(p.clientY-sy);
        ({nx,ny}=clampToViewport(nx,ny)); w.style.left=nx+"px"; w.style.top=ny+"px"; w.style.transform="none"; e.preventDefault(); }
      function onEnd(){ if(!dragging) return; dragging=false; const r=w.getBoundingClientRect();
        CFG.panel.x=Math.round(r.left); CFG.panel.y=Math.round(r.top); CFG.panel.w=Math.round(r.width); CFG.panel.h=Math.round(r.height); save(CFG); }
      if (window.PointerEvent){ hd.addEventListener("pointerdown", onStart); addEventListener("pointermove", onMove); addEventListener("pointerup", onEnd); }
      else { hd.addEventListener("mousedown", onStart); addEventListener("mousemove", onMove); addEventListener("mouseup", onEnd);
             hd.addEventListener("touchstart", onStart, { passive:false }); addEventListener("touchmove", onMove, { passive:false }); addEventListener("touchend", onEnd); }
      addEventListener("resize", ()=>{ const r=w.getBoundingClientRect(), {nx,ny}=clampToViewport(r.left,r.top); w.style.left=Math.round(nx)+"px"; w.style.top=Math.round(ny)+"px";});
    })();

    const $=(s)=>w.querySelector(s);
    $("#t-user").checked=!!CFG.targets.user;
    $("#t-assistant").checked=!!CFG.targets.assistant;
    $("#t-sendbtn").checked=!!CFG.targets.sendbtn;
    $("#followSel").value=CFG.followOfSendBtn;
    ["t-user","t-assistant","t-sendbtn","followSel"].forEach(id=>{
      $("#"+id).addEventListener("change",()=>{
        CFG.targets.user=$("#t-user").checked;
        CFG.targets.assistant=$("#t-assistant").checked;
        CFG.targets.sendbtn=$("#t-sendbtn").checked;
        CFG.followOfSendBtn=$("#followSel").value;
        save(CFG); NEED_ENSURE = true; requestStyleUpdate();
      });
    });

    let editMode=false, ACTIVE="user";
    const isBuiltin=(name)=> name in THEMES;
    const chipTheme=(name)=> isBuiltin(name)?THEMES[name]:(CFG.customThemes[name]||THEMES["海盐"]);
    function galleryNames(){ const builtins=Object.keys(THEMES), customs=Object.keys(CFG.customThemes||{}), set=new Set([...builtins,...customs]);
      const arr=(CFG.galleryOrder||[]).filter(n=>set.has(n)); for(const n of set){ if(!arr.includes(n)) arr.push(n); } CFG.galleryOrder=arr; save(CFG); return arr; }
    function renderChips(){ const wall=$("#chipWall"); wall.innerHTML=""; const current=CFG.lastTheme[ACTIVE];
      galleryNames().forEach(name=>{ const st=chipTheme(name), btn=document.createElement("div");
        btn.className="chip"; btn.draggable=true; btn.dataset.name=name; if(name===current) btn.classList.add("active");
        btn.innerHTML=`<span class="dot" style="--c1:${st.colors[0]};--c2:${st.colors[1]};--c3:${st.colors[2]}"></span><span class="nm">${name}</span>${isBuiltin(name)?"":'<span class="x" title="删除">×</span>'}`;
        wall.appendChild(btn); }); wall.parentElement.classList.toggle("editing",editMode); }
    function pickThemeByName(name){ CFG.lastTheme[ACTIVE]=name; const over=(CFG.overrides[ACTIVE]||{})[name];
      CFG[ACTIVE]=clone(over||chipTheme(name)); fillForm(); save(CFG); renderChips(); }

    // 标签:用户 / AI
    w.querySelector(".tabs").addEventListener("click",(e)=>{
      const t = e.target.closest(".tab"); if(!t) return;
      const role = t.dataset.role; if(!role || role === ACTIVE) return;
      w.querySelectorAll(".tabs .tab").forEach(x=>x.classList.remove("active")); t.classList.add("active");
      ACTIVE = role; renderChips(); fillForm(); updatePreview();
    });

    $("#editChipsBtn").addEventListener("click",()=>{ editMode=!editMode; $("#editChipsBtn").textContent=editMode?"完成":"编辑主题"; renderChips(); });
    $("#chipWall").addEventListener("click",(e)=>{ const chip=e.target.closest(".chip"); if(!chip) return; const name=chip.dataset.name;
      if(editMode){ if(e.target.classList.contains("x")){ if(isBuiltin(name)) return;
        if(confirm(`删除主题「${name}」?`)){ delete CFG.customThemes[name]; CFG.galleryOrder=CFG.galleryOrder.filter(n=>n!==name); save(CFG); renderChips(); } }
      }else{ pickThemeByName(name); }});
    let dragName=null;
    $("#chipWall").addEventListener("dragstart",(e)=>{ const c=e.target.closest(".chip"); if(!c) return; dragName=c.dataset.name; e.dataTransfer.effectAllowed="move"; });
    $("#chipWall").addEventListener("dragover",(e)=>{ if(!dragName) return; e.preventDefault(); });
    $("#chipWall").addEventListener("drop",(e)=>{ e.preventDefault(); const c=e.target.closest(".chip"); if(!c || !dragName) return;
      const target=c.dataset.name; if(target===dragName) return; const arr=galleryNames(), a=arr.indexOf(dragName), b=arr.indexOf(target);
      arr.splice(b,0,arr.splice(a,1)[0]); CFG.galleryOrder=arr; save(CFG); dragName=null; renderChips(); });
    $("#chipWall").addEventListener("dragend",()=>{ dragName=null; renderChips(); });
    renderChips();

    function bindHex(hexId,colorId){
      const hex=$(hexId), col=$(colorId);
      const setHex = () => { hex.value = (col.value || "#000000").toLowerCase(); };
      const setCol=()=>{ const p=parseColorToHex(hex.value); if(p){ col.value=p; hex.value=p; } };
      setHex(); col.addEventListener("input",()=>{ setHex(); readForm(); requestStyleUpdate(); });
      hex.addEventListener("input",()=>{ setCol(); readForm(); requestStyleUpdate(); });
      hex.addEventListener("change",()=>{ setCol(); readForm(); requestStyleUpdate(); });
      hex.addEventListener("paste",()=>{ setTimeout(()=>{ setCol(); readForm(); requestStyleUpdate(); },0); });
    }
    function bindPair(rangeId,numId,onChange){
      const r=$(rangeId), n=$(numId);
      function sync(from){ if(from==="r"){ n.value=r.value; } else { r.value=n.value; } onChange&&onChange(+n.value); }
      r.addEventListener("input",()=>sync("r")); n.addEventListener("input",()=>sync("n")); sync("n");
    }
    function applyWeightUI(w1,w2){ const w3=clamp(100-w1-w2,0,100); $("#w1r").value=w1; $("#w1").value=w1; $("#w2r").value=w2; $("#w2").value=w2; $("#w3r").value=w3; $("#w3").value=w3; $("#weightHint").textContent=`配重合计:${w1+w2+w3}%(建议=100%)`; }
    function readWeightUIToModel(){ let w1=+$("#w1").value||0, w2=+$("#w2").value||0; if(w1<0) w1=0; if(w1>100) w1=100; if(w2<0) w2=0; if(w1+w2>100) w2=100-w1; const w3=100-w1-w2; applyWeightUI(w1,w2); const s=CFG[ACTIVE]; s.weights=[w1,w2,w3]; return s.weights; }

    $("#w1r").addEventListener("input",()=>{ $("#w1").value=$("#w1r").value; readWeightUIToModel(); requestStyleUpdate(); });
    $("#w2r").addEventListener("input",()=>{ $("#w2").value=$("#w2r").value; readWeightUIToModel(); requestStyleUpdate(); });
    $("#w1").addEventListener("input",()=>{ readWeightUIToModel(); requestStyleUpdate(); });
    $("#w2").addEventListener("input",()=>{ readWeightUIToModel(); requestStyleUpdate(); });

    function fillForm(){
      const s=CFG[ACTIVE];
      $("#mode").value=s.mode; $("#angle").value=s.angle; $("#angleR").value=s.angle;

      $("#textColor").value=s.textColor||"#101418"; $("#textHex").value=$("#textColor").value.toLowerCase();
      $("#autoText").checked=!!s.autoText; const dis=!!s.autoText;
      $("#textHex").disabled=dis; $("#textColor").disabled=dis;
      $("#autoText").onchange=()=>{ const on=$("#autoText").checked; $("#textHex").disabled=on; $("#textColor").disabled=on; readForm(); requestStyleUpdate(); };

      $("#c1").value=s.colors[0]||"#000000"; $("#h1").value=$("#c1").value.toLowerCase();
      $("#c2").value=s.colors[1]||"#000000"; $("#h2").value=$("#c2").value.toLowerCase();
      $("#c3").value=s.colors[2]||"#000000"; $("#h3").value=$("#c3").value.toLowerCase();

      const w1=s.weights?.[0]??34, w2=s.weights?.[1]??33; applyWeightUI(w1,w2);

      $("#soft").value=s.soft??SOFT; $("#softR").value=$("#soft").value;
      $("#alpha").value=s.alpha; $("#alphaR").value=s.alpha;
      $("#blur").value=s.blur; $("#blurR").value=s.blur;
      $("#glow").value=s.glow; $("#glowR").value=$("#glow").value;

      $("#outlineColor").value=s.outlineColor; $("#outlineHex").value=$("#outlineColor").value.toLowerCase();
      $("#outlineAlpha").value=s.outlineAlpha; $("#outlineAlphaR").value=$("#outlineAlpha").value;
      $("#outlineWidth").value=s.outlineWidth; $("#outlineWidthR").value=$("#outlineWidth").value;

      $("#radius").value=s.radius; $("#radiusR").value=s.radius;
      $("#padV").value=s.padV; $("#padVR").value=$("#padV").value;
      $("#padH").value=s.padH; $("#padHR").value=$("#padH").value;

      bindHex("#h1","#c1"); bindHex("#h2","#c2"); bindHex("#h3","#c3"); bindHex("#textHex","#textColor"); bindHex("#outlineHex","#outlineColor");
      bindPair("#angleR","#angle",()=>{ CFG[ACTIVE].angle=+$("#angle").value; });
      bindPair("#softR","#soft",()=>{ CFG[ACTIVE].soft=+$("#soft").value; });
      bindPair("#alphaR","#alpha",()=>{ CFG[ACTIVE].alpha=+$("#alpha").value; });
      bindPair("#blurR","#blur",()=>{ CFG[ACTIVE].blur=+$("#blur").value; });
      bindPair("#glowR","#glow",()=>{ CFG[ACTIVE].glow=+$("#glow").value; });
      bindPair("#outlineAlphaR","#outlineAlpha",()=>{ CFG[ACTIVE].outlineAlpha=+$("#outlineAlpha").value; });
      bindPair("#outlineWidthR","#outlineWidth",()=>{ CFG[ACTIVE].outlineWidth=+$("#outlineWidth").value; });
      bindPair("#radiusR","#radius",()=>{ CFG[ACTIVE].radius=+$("#radius").value; });
      bindPair("#padVR","#padV",()=>{ CFG[ACTIVE].padV=+$("#padV").value; });
      bindPair("#padHR","#padH",()=>{ CFG[ACTIVE].padH=+$("#padH").value; });

      requestStyleUpdate();
    }
    function readForm(){
      const s=CFG[ACTIVE];
      s.mode=$("#mode").value; s.angle=+$("#angle").value;
      s.autoText=$("#autoText").checked;
      const parsedText=parseColorToHex($("#textHex").value)||$("#textColor").value; s.textColor=parsedText||s.textColor||"#101418";
      s.colors=[$("#c1").value,$("#c2").value,$("#c3").value].filter(Boolean);
      s.weights=readWeightUIToModel();
      s.soft=+$("#soft").value; s.alpha=+$("#alpha").value; s.blur=+$("#blur").value; s.glow=+$("#glow").value;
      const parsedOutline=parseColorToHex($("#outlineHex").value)||$("#outlineColor").value; s.outlineColor=parsedOutline||"#000000";
      s.outlineAlpha=+$("#outlineAlpha").value; s.outlineWidth=+$("#outlineWidth").value;
      s.radius=+$("#radius").value; s.padV=+$("#padV").value; s.padH=+$("#padH").value;

      const tname=CFG.lastTheme[ACTIVE]; if(!CFG.overrides[ACTIVE]) CFG.overrides[ACTIVE]={};
      CFG.overrides[ACTIVE][tname]=clone(s);
      save(CFG);
    }

    w.querySelector(".grid").addEventListener("input",()=>{ readForm(); requestStyleUpdate(); });
    w.querySelector(".grid").addEventListener("change",()=>{ readForm(); requestStyleUpdate(); });

    w.querySelector('[data-act="save"]').addEventListener("click",()=>{ save(CFG); alert("已保存配置"); });
    w.querySelector('[data-act="savePreset"]').addEventListener("click",()=>{
      const name=$("#presetName").value.trim(); if(!name){ alert("请输入预设名称"); return; }
      if(THEMES[name] && !confirm("同名内置主题已存在,是否覆盖为自定义?")) return;
      if(!CFG.customThemes) CFG.customThemes={};
      CFG.customThemes[name]=clone(CFG[ACTIVE]); CFG.galleryOrder=[...new Set([name,...CFG.galleryOrder])];
      CFG.lastTheme[ACTIVE]=name; delete (CFG.overrides[ACTIVE]||{})[name]; save(CFG); renderChips(); pickThemeByName(name); alert("已另存为自定义主题");
    });
    w.querySelector('[data-act="reset"]').addEventListener("click",()=>{
      const name=CFG.lastTheme[ACTIVE];
      const source=isBuiltin(name)?THEMES[name]:CFG.customThemes[name];
      if(!source){ alert("未找到默认主题定义"); return; }
      if(CFG.overrides[ACTIVE]) delete CFG.overrides[ACTIVE][name];
      CFG[ACTIVE]=clone(source); save(CFG); fillForm(); requestStyleUpdate(); alert("已重置为默认");
    });

    fillForm();
  }

  function updatePreview(){
    const wp = document.getElementById("ech0-preview");
    if (!wp) return;
    const active = document.querySelector(`#${PANEL_ID} .tab.active`);
    const role = active ? active.dataset.role : "user";
    const s = role === "assistant" ? CFG.assistant : CFG.user;

    const bg = makeBackground(s);
    const outline = rgba(s.outlineColor, s.outlineAlpha);
    const border = (s.outlineAlpha <= 0 || s.outlineWidth <= 0) ? "none" : `${s.outlineWidth}px solid ${outline}`;
    const text = s.autoText ? (themeLuma(s)>0.55 ? "#000" : "#fff") : (s.textColor || "#111");
    const blurPx = IS_MOBILE ? Math.min(s.blur, 4) : s.blur;

    wp.style.background = bg;
    wp.style.border = border;
    wp.style.borderRadius = s.radius + "px";
    wp.style.padding = `${s.padV}px ${s.padH}px`;
    wp.style.color = text;
    wp.style.webkitTextFillColor = text;
    wp.style.webkitBackdropFilter = `blur(${blurPx}px)`;
    wp.style.backdropFilter = `blur(${blurPx}px)`;
    wp.style.boxShadow = makeGlow(s.outlineColor, s.glow);
    wp.textContent = "正在输入中.............";
  }

  // 面板开关(只此一处声明)
  const togglePanel = () => {
    const p = document.getElementById(PANEL_ID);
    if (p) { p.remove(); } else { openPanel(); }
  };
  document.addEventListener("keydown", (e) => {
    if (e.altKey && e.key.toLowerCase() === "g") { e.preventDefault(); togglePanel(); }
  });
  GM_registerMenuCommand("打开/关闭设置面板 (Alt+G)", togglePanel);

  // 初次渲染
  NEED_ENSURE = true;
  requestStyleUpdate();

  // 监听新节点:仅置位“需要归拢”,由节流器统一更新
  const mo = new MutationObserver(() => { NEED_ENSURE = true; requestStyleUpdate(); });
  mo.observe(document.querySelector("main") || document.documentElement, { childList: true, subtree: true });

})();