// METASPEAK Tweaks — Replikant-inspired tabbed dev panel
// Replaces the simple TweaksPanel with a richer interface:
//   - Tab navigation (Characters · Behaviour · Environment · API · Audio)
//   - Per-character upload (.glb), name, color, position/scale, personality
//   - Environment presets, lighting, fog
//   - API key fields
// Wires to the same protocol as TweaksPanel: __activate_edit_mode / __deactivate_edit_mode

const __MS_TWEAK_STYLE = `
.mst-panel{position:fixed;right:16px;top:16px;bottom:16px;z-index:2147483646;
  width:360px;display:flex;flex-direction:column;
  background:rgba(14,16,22,.92);color:#e8e9ee;
  -webkit-backdrop-filter:blur(20px) saturate(140%);backdrop-filter:blur(20px) saturate(140%);
  border:1px solid rgba(120,140,180,.18);border-radius:12px;
  box-shadow:0 24px 60px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.04);
  font:12px/1.4 ui-sans-serif,system-ui,-apple-system,sans-serif;overflow:hidden}
.mst-hd{display:flex;align-items:center;justify-content:space-between;
  padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.06);
  background:linear-gradient(180deg,rgba(255,255,255,.04),transparent)}
.mst-hd-title{display:flex;align-items:center;gap:8px;font-weight:600;letter-spacing:.02em}
.mst-hd-title svg{opacity:.7}
.mst-hd-x{appearance:none;border:0;background:transparent;color:rgba(232,233,238,.55);
  width:24px;height:24px;border-radius:6px;cursor:pointer;font-size:14px}
.mst-hd-x:hover{background:rgba(255,255,255,.06);color:#fff}

.mst-tabs{display:flex;padding:4px;gap:2px;border-bottom:1px solid rgba(255,255,255,.06);
  background:rgba(0,0,0,.3)}
.mst-tab{flex:1;appearance:none;border:0;background:transparent;color:rgba(232,233,238,.55);
  font:inherit;font-size:11px;font-weight:500;padding:7px 6px;border-radius:6px;
  cursor:pointer;letter-spacing:.02em;text-transform:uppercase}
.mst-tab:hover{color:rgba(232,233,238,.85)}
.mst-tab.active{background:rgba(95,165,225,.18);color:#9ec6f0}

.mst-body{flex:1;overflow-y:auto;padding:14px 14px 18px;
  scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.15) transparent}
.mst-body::-webkit-scrollbar{width:8px}
.mst-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:4px;
  border:2px solid transparent;background-clip:content-box}

.mst-section{margin-bottom:18px}
.mst-section:last-child{margin-bottom:0}
.mst-sec-h{display:flex;align-items:center;justify-content:space-between;
  font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
  color:rgba(232,233,238,.4);margin:0 0 10px;padding-bottom:6px;
  border-bottom:1px solid rgba(255,255,255,.06)}
.mst-sec-h .mst-sec-tag{font-size:9.5px;color:rgba(255,255,255,.35);
  background:rgba(255,255,255,.05);padding:2px 6px;border-radius:4px;letter-spacing:.04em}

.mst-row{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
.mst-row.h{flex-direction:row;align-items:center;justify-content:space-between;gap:10px}
.mst-lbl{display:flex;justify-content:space-between;align-items:baseline;
  font-size:11.5px;color:rgba(232,233,238,.7)}
.mst-lbl b{font-weight:500}
.mst-lbl .mst-val{color:rgba(232,233,238,.45);font-variant-numeric:tabular-nums;font-size:11px}

.mst-input{appearance:none;width:100%;height:30px;padding:0 10px;
  border:1px solid rgba(255,255,255,.08);border-radius:7px;
  background:rgba(0,0,0,.35);color:#e8e9ee;font:inherit;font-size:11.5px;outline:none;
  transition:border-color .15s,background .15s}
.mst-input:focus{border-color:rgba(120,170,230,.5);background:rgba(0,0,0,.5)}
textarea.mst-input{height:auto;min-height:64px;padding:8px 10px;line-height:1.45;resize:vertical}

.mst-slider{appearance:none;-webkit-appearance:none;width:100%;height:3px;
  border-radius:999px;background:rgba(255,255,255,.1);outline:none;margin:6px 0 4px}
.mst-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
  width:12px;height:12px;border-radius:50%;background:#9ec6f0;
  border:2px solid rgba(0,0,0,.4);cursor:pointer;box-shadow:0 0 0 1px rgba(120,170,230,.3)}
.mst-slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;
  background:#9ec6f0;border:2px solid rgba(0,0,0,.4);cursor:pointer}

.mst-up{display:flex;align-items:center;gap:8px;padding:10px;border-radius:8px;
  background:rgba(0,0,0,.35);border:1px dashed rgba(255,255,255,.12);
  cursor:pointer;transition:border-color .15s,background .15s}
.mst-up:hover{border-color:rgba(120,170,230,.5);background:rgba(120,170,230,.05)}
.mst-up.has-file{border-style:solid;border-color:rgba(120,200,140,.4);
  background:rgba(120,200,140,.06)}
.mst-up-icon{width:28px;height:28px;display:flex;align-items:center;justify-content:center;
  border-radius:6px;background:rgba(255,255,255,.06);flex-shrink:0}
.mst-up-text{flex:1;min-width:0}
.mst-up-title{font-weight:500;color:#e8e9ee;font-size:11.5px}
.mst-up-sub{font-size:10.5px;color:rgba(232,233,238,.5);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mst-up input{display:none}
.mst-up-clear{appearance:none;border:0;background:rgba(255,255,255,.05);
  color:rgba(232,233,238,.6);width:22px;height:22px;border-radius:5px;cursor:pointer;
  font-size:12px;flex-shrink:0}
.mst-up-clear:hover{background:rgba(255,80,80,.2);color:#ff8888}

.mst-color{appearance:none;-webkit-appearance:none;width:36px;height:24px;
  border:1px solid rgba(255,255,255,.1);border-radius:5px;padding:0;background:transparent;
  cursor:pointer;flex-shrink:0}
.mst-color::-webkit-color-swatch-wrapper{padding:0}
.mst-color::-webkit-color-swatch{border:0;border-radius:4px}
.mst-color::-moz-color-swatch{border:0;border-radius:4px}

.mst-grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
.mst-grid2{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.mst-pill{display:flex;flex-direction:column;align-items:center;gap:2px;padding:5px 4px;
  background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.06);border-radius:6px}
.mst-pill label{font-size:9.5px;color:rgba(232,233,238,.5);text-transform:uppercase;letter-spacing:.04em}
.mst-pill input{width:100%;height:22px;text-align:center;background:transparent;border:0;
  color:#e8e9ee;font:inherit;font-size:11px;font-variant-numeric:tabular-nums;outline:none;
  -moz-appearance:textfield}
.mst-pill input::-webkit-inner-spin-button,.mst-pill input::-webkit-outer-spin-button{
  -webkit-appearance:none;margin:0}

.mst-presets{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.mst-preset{appearance:none;padding:8px 4px;border:1px solid rgba(255,255,255,.08);
  border-radius:7px;background:rgba(0,0,0,.3);color:#e8e9ee;font:inherit;font-size:10.5px;
  cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:5px;
  transition:border-color .15s,background .15s}
.mst-preset:hover{border-color:rgba(120,170,230,.4)}
.mst-preset.active{border-color:rgba(120,170,230,.6);background:rgba(95,165,225,.12)}
.mst-preset-swatch{width:100%;height:24px;border-radius:4px}

.mst-toggle{position:relative;width:32px;height:18px;border:0;border-radius:999px;
  background:rgba(255,255,255,.1);cursor:pointer;padding:0;flex-shrink:0;transition:background .15s}
.mst-toggle[data-on="1"]{background:#5e9bd6}
.mst-toggle i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
  background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.4);transition:transform .15s}
.mst-toggle[data-on="1"] i{transform:translateX(14px)}

.mst-char-card{padding:12px;border-radius:9px;background:rgba(0,0,0,.25);
  border:1px solid rgba(255,255,255,.06);margin-bottom:10px}
.mst-char-card-h{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.mst-char-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;
  box-shadow:0 0 8px currentColor}
.mst-char-card-name{font-weight:600;color:#e8e9ee;font-size:13px}

.mst-help{font-size:10.5px;color:rgba(232,233,238,.45);line-height:1.5;
  padding:8px 10px;background:rgba(120,170,230,.06);border-radius:6px;
  border-left:2px solid rgba(120,170,230,.3);margin-top:6px}
.mst-help b{color:rgba(232,233,238,.7)}

.mst-edit-toggle{position:fixed;right:16px;top:16px;z-index:2147483645;
  appearance:none;border:1px solid rgba(255,255,255,.12);
  background:rgba(20,24,32,.85);color:#e8e9ee;padding:8px 12px;border-radius:8px;
  font:11.5px/1 ui-sans-serif,system-ui,sans-serif;font-weight:500;cursor:pointer;
  display:flex;align-items:center;gap:6px;letter-spacing:.02em;
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
.mst-edit-toggle:hover{background:rgba(30,36,48,.95);border-color:rgba(120,170,230,.4)}
`;

// ── Hook ──────────────────────────────────────────────────────────────
function useMSTweaks(defaults) {
  const [values, setValues] = React.useState(defaults);

  // Persist directly to localStorage on every change. The parent-frame
  // postMessage path (used when METASPEAK runs embedded inside an editor
  // host) is kept as a secondary signal. In standalone mode, postMessage
  // posts to window itself and is a no-op for storage.
  const persistToLS = React.useCallback((next) => {
    try {
      window.PresetUtils?.saveToLocalStorage?.(next);
    } catch (e) { /* silent */ }
  }, []);

  const setKey = React.useCallback((key, val) => {
    setValues((p) => {
      const next = { ...p, [key]: val };
      persistToLS(next);
      return next;
    });
    if (typeof val !== 'object' || val === null) {
      try {
        window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: val } }, '*');
      } catch (e) { /* silent */ }
    }
  }, [persistToLS]);

  const setMulti = React.useCallback((edits) => {
    setValues((p) => {
      const next = { ...p, ...edits };
      persistToLS(next);
      return next;
    });
    const persistable = {};
    for (const k in edits) {
      if (typeof edits[k] !== 'object' || edits[k] === null) persistable[k] = edits[k];
    }
    if (Object.keys(persistable).length) {
      try {
        window.parent.postMessage({ type: '__edit_mode_set_keys', edits: persistable }, '*');
      } catch (e) { /* silent */ }
    }
  }, [persistToLS]);

  return [values, setKey, setMulti];
}

// ── Tabbed shell ──────────────────────────────────────────────────────
function MSTweaksShell({ tabs, activeTab, onTab, children, onClose, mode }) {
  // In 'admin' mode, the dashboard provides its own chrome (header, tabs).
  // Just render the panel contents directly without the floating panel
  // wrapper or close button.
  if (mode === 'admin') {
    return <>{children}</>;
  }
  return (
    <>
      <style>{__MS_TWEAK_STYLE}</style>
      <div className="mst-panel">
        <div className="mst-hd">
          <div className="mst-hd-title">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <circle cx="12" cy="12" r="3"/>
              <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
            </svg>
            METASPEAK Tweaks
          </div>
          <button className="mst-hd-x" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="mst-tabs">
          {tabs.map((t) => (
            <button key={t.id} className={`mst-tab ${activeTab === t.id ? 'active' : ''}`}
                    onClick={() => onTab(t.id)}>{t.label}</button>
          ))}
        </div>
        <div className="mst-body">{children}</div>
      </div>
    </>
  );
}

// ── Generic field components ──────────────────────────────────────────
function MSField({ label, value, children, hint }) {
  return (
    <div className="mst-row">
      {label && (
        <div className="mst-lbl">
          <b>{label}</b>
          {value != null && <span className="mst-val">{value}</span>}
        </div>
      )}
      {children}
      {hint && <div className="mst-help">{hint}</div>}
    </div>
  );
}

function MSText({ label, value, placeholder, onChange, hint }) {
  return (
    <MSField label={label} hint={hint}>
      <input className="mst-input" type="text" value={value || ''} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </MSField>
  );
}

function MSPassword({ label, value, placeholder, onChange, hint }) {
  return (
    <MSField label={label} hint={hint}>
      <input className="mst-input" type="password" value={value || ''} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </MSField>
  );
}

function MSTextarea({ label, value, placeholder, rows = 3, onChange, hint }) {
  return (
    <MSField label={label} hint={hint}>
      <textarea className="mst-input" rows={rows} value={value || ''} placeholder={placeholder}
             onChange={(e) => onChange(e.target.value)} />
    </MSField>
  );
}

function MSSlider({ label, value, min, max, step, unit = '', onChange, fmt }) {
  const display = fmt ? fmt(value) : `${(+value).toFixed(step < 0.1 ? 2 : step < 1 ? 1 : 0)}${unit}`;
  return (
    <MSField label={label} value={display}>
      <input className="mst-slider" type="range" min={min} max={max} step={step}
             value={value} onChange={(e) => onChange(parseFloat(e.target.value))} />
    </MSField>
  );
}

function MSToggle({ label, value, onChange }) {
  return (
    <div className="mst-row h">
      <span className="mst-lbl"><b>{label}</b></span>
      <button className="mst-toggle" data-on={value ? '1' : '0'}
              onClick={() => onChange(!value)}><i/></button>
    </div>
  );
}

function MSColor({ label, value, onChange }) {
  return (
    <div className="mst-row h">
      <span className="mst-lbl"><b>{label}</b></span>
      <input type="color" className="mst-color" value={value}
             onChange={(e) => onChange(e.target.value)} />
    </div>
  );
}

function MSXYZ({ label, x, y, z, step = 0.05, onChange }) {
  return (
    <MSField label={label}>
      <div className="mst-grid3">
        {[['x', x], ['y', y], ['z', z]].map(([k, v]) => (
          <div key={k} className="mst-pill">
            <label>{k.toUpperCase()}</label>
            <input type="number" step={step} value={v?.toFixed?.(2) ?? v}
                   onChange={(e) => onChange({ [k]: parseFloat(e.target.value) || 0 })} />
          </div>
        ))}
      </div>
    </MSField>
  );
}

// File upload component for GLB
function MSFileUpload({ label, fileName, onFile, onClear, accept = '.glb,.gltf' }) {
  const inputRef = React.useRef();
  return (
    <MSField label={label}>
      <label className={`mst-up ${fileName ? 'has-file' : ''}`}>
        <div className="mst-up-icon">
          {fileName ? (
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#7fd17f" strokeWidth="2.5">
              <path d="M20 6L9 17l-5-5"/>
            </svg>
          ) : (
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>
            </svg>
          )}
        </div>
        <div className="mst-up-text">
          <div className="mst-up-title">{fileName ? 'Loaded' : 'Drop or click to upload'}</div>
          <div className="mst-up-sub">{fileName || `.glb or .gltf file`}</div>
        </div>
        <input ref={inputRef} type="file" accept={accept}
               onChange={(e) => { const f = e.target.files?.[0]; if (f) onFile(f); }} />
        {fileName && (
          <button
            type="button"
            className="mst-up-clear"
            onClick={(e) => {
              // Prevent the wrapping <label> from re-opening the file
              // picker when clicking ✕. Both preventDefault AND
              // stopPropagation are needed: without stopPropagation, the
              // event still bubbles to the label which is the file input
              // trigger; without preventDefault, some browsers also
              // trigger the file dialog.
              e.preventDefault();
              e.stopPropagation();
              // Also reset the underlying file input so the same file
              // can be re-selected after a clear (otherwise the input
              // still holds the stale file name and onChange won't fire).
              if (inputRef.current) inputRef.current.value = '';
              onClear();
            }}
          >✕</button>
        )}
      </label>
    </MSField>
  );
}

// ── Setup the protocol-aware shell that switches tabs ────────────────
function MSPanel({ defaults, render }) {
  // METASPEAK_MODE controls how the panel surfaces:
  //   'kiosk'  → no gear button, panel never opens (client-facing chatbot.html)
  //   'admin'  → panel always rendered inline, no gear button (admin.html)
  //   'combo'  → original behaviour: gear toggles panel (METASPEAK.html)
  const mode = window.METASPEAK_MODE || 'combo';
  const [open, setOpen] = React.useState(mode === 'admin');
  const [activeTab, setActiveTab] = React.useState('characters');
  const [values, setKey, setMulti] = useMSTweaks(defaults);

  React.useEffect(() => {
    const onMsg = (e) => {
      const t = e?.data?.type;
      if (t === '__activate_edit_mode') setOpen(true);
      else if (t === '__deactivate_edit_mode') setOpen(false);
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  const close = () => {
    setOpen(false);
    window.parent.postMessage({ type: '__edit_mode_dismissed' }, '*');
  };

  const tabs = [
    { id: 'characters', label: 'Cast' },
    { id: 'behaviour', label: 'Voice' },
    { id: 'environment', label: 'Scene' },
    { id: 'camera', label: 'Camera' },
    { id: 'dialogue', label: 'Dialogue' },
    { id: 'api', label: 'API' },
  ];

  // Always render the values context for the app to use, but only render the
  // panel UI when open. The gear button is hidden in 'kiosk' mode (no settings
  // access for end users) and in 'admin' mode (panel is always docked open).
  return (
    <>
      {render({ values, setKey, setMulti, panel: { role: 'canvas', activeTab, setActiveTab, open, mode } })}

      {/* Standalone gear button — only in 'combo' mode */}
      {mode === 'combo' && (
        <button
          onClick={() => open ? close() : setOpen(true)}
          title={open ? 'Close settings' : 'Open settings'}
          style={{
          position: 'fixed',
          left: '18px',
          bottom: '18px',
          width: '44px',
          height: '44px',
          borderRadius: '50%',
          border: '1px solid rgba(255,255,255,0.12)',
          background: open ? 'rgba(75,163,199,0.25)' : 'rgba(20,24,32,0.85)',
          color: '#cfe5ff',
          cursor: 'pointer',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          backdropFilter: 'blur(8px)',
          WebkitBackdropFilter: 'blur(8px)',
          boxShadow: '0 4px 14px rgba(0,0,0,0.4)',
          transition: 'transform 0.15s ease, background 0.15s ease',
          zIndex: 9999,
        }}
        onMouseEnter={(e) => e.currentTarget.style.transform = 'scale(1.06) rotate(20deg)'}
        onMouseLeave={(e) => e.currentTarget.style.transform = 'scale(1) rotate(0deg)'}
      >
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="3"/>
          <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
        </svg>
      </button>
      )}

      {open && (
        <MSTweaksShell tabs={tabs} activeTab={activeTab} onTab={setActiveTab} onClose={close} mode={mode}>
          {render({ values, setKey, setMulti, panel: { role: 'panel', activeTab, setActiveTab, mode } })}
        </MSTweaksShell>
      )}
    </>
  );
}

// Per-tab Save button. Settings already auto-save to localStorage on every
// change (see App's render mirror), so this is primarily for UX feedback —
// flashes green for 1.2s on click to confirm "yes your settings are
// persisted". Pinned to the bottom of any tab.
function MSSaveButton({ label = 'Save settings', values, onSave }) {
  const [saved, setSaved] = React.useState(false);
  const onClick = () => {
    try {
      // Custom save hook (e.g. apply tree to TopicTree) before persisting
      onSave?.();
      // Force a localStorage write through PresetUtils
      if (values && window.PresetUtils?.saveToLocalStorage) {
        window.PresetUtils.saveToLocalStorage(values);
      }
    } catch (e) { console.warn('Save failed', e); }
    setSaved(true);
    setTimeout(() => setSaved(false), 1200);
  };
  return (
    <div style={{
      position: 'sticky', bottom: 0,
      marginTop: '14px', padding: '10px 0 4px',
      background: 'linear-gradient(to top, rgba(13,18,27,0.96) 50%, transparent)',
      zIndex: 5,
    }}>
      <button onClick={onClick} style={{
        width: '100%', padding: '10px 14px',
        background: saved ? 'rgba(120,255,160,0.18)' : 'rgba(120,180,255,0.12)',
        border: '1px solid ' + (saved ? 'rgba(120,255,160,0.5)' : 'rgba(120,180,255,0.4)'),
        borderRadius: '6px',
        color: saved ? '#9be0a8' : '#cfe5ff',
        fontSize: '12px', fontWeight: 600, cursor: 'pointer',
        transition: 'background 0.2s, border-color 0.2s, color 0.2s',
        letterSpacing: '0.3px',
      }}>
        {saved ? '✓ Saved' : '💾 ' + label}
      </button>
    </div>
  );
}

Object.assign(window, {
  useMSTweaks, MSTweaksShell, MSPanel,
  MSField, MSText, MSPassword, MSTextarea, MSSlider, MSToggle, MSColor, MSXYZ, MSFileUpload,
  MSSaveButton,
});
