Instagram Unhook

A userscript to help people escape Instagram addiction. After installation, a small gear icon will appear on Instagram in the bottom right corner. There, you can set various tools/schedules to reduce your exposure to the algorithm. Currently, you can enable/disable messaging, a chronological/algorithmic feed, and instagram stories, and you can disable the blocker entirely. You can also set a schedule for working hours to have certain features appear and dissapear. Best of luck and fuck you Meta!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Instagram Unhook
// @namespace    instagram-unhook
// @version      1.2
// @description  A userscript to help people escape Instagram addiction. After installation, a small gear icon will appear on Instagram in the bottom right corner. There, you can set various tools/schedules to reduce your exposure to the algorithm. Currently, you can enable/disable messaging, a chronological/algorithmic feed, and instagram stories, and you can disable the blocker entirely. You can also set a schedule for working hours to have certain features appear and dissapear. Best of luck and fuck you Meta!
// @match        https://www.instagram.com/*
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license      MIT-0
// ==/UserScript==

(() => {
  'use strict';

  /* ───────── GM poly + helpers ───────── */
  const GM = (typeof GM_getValue === 'function' && typeof GM_setValue === 'function') ? {
    get: (k, v) => { try { return GM_getValue(k, v); } catch { return v; } },
    set: (k, v) => { try { GM_setValue(k, v); } catch {} },
    addStyle: (css) => { try { GM_addStyle(css); } catch { const el = document.createElement('style'); el.textContent = css; document.documentElement.appendChild(el); } },
    registerMenu: (label, fn) => { try { GM_registerMenuCommand(label, fn); } catch {} },
  } : {
    get: (k, v) => { try { return JSON.parse(localStorage.getItem(k)) ?? v; } catch { return v; } },
    set: (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} },
    addStyle: (css) => { const el = document.createElement('style'); el.textContent = css; document.documentElement.appendChild(el); },
    registerMenu: () => {},
  };

  const now = () => new Date();
  const clamp = (n,min,max)=>Math.max(min,Math.min(max,n));
  const pad2 = (n)=>String(n).padStart(2,'0');
  const hhmmToMins = (s)=>{const m=/^(\d{1,2}):(\d{2})$/.exec(String(s).trim()); if(!m) return 0; return clamp(+m[1],0,23)*60+clamp(+m[2],0,59);};
  const dayOfWeek = (d=new Date())=>{const n=d.getDay(); return n===0?7:n;};

  const sameOrigin=(u)=>{ try{return new URL(u,location.origin).origin===location.origin;}catch{return false;} };
  const pathOf=(u)=>{ try{return new URL(u,location.origin).pathname;}catch{return '';} };

  const isRoot = ()=>location.pathname==='/';
  const onFollowing = ()=>location.search.startsWith('?variant=following');
  const isDMPath = (p=location.pathname)=>p.startsWith('/direct');
  const isLoginFlowPath = (p=location.pathname)=>/^\/(accounts|challenge|oauth)(\/|$)/.test(p);

  const DM_INBOX_URL='https://www.instagram.com/direct/inbox/';
  const FOLLOWING_URL='https://www.instagram.com/?variant=following';
  const STORY_FLAG_SS='iuStoriesOnly'; // per-tab only
  const STORY_HASH   ='#iu_so';
  const ALLOW_LOGIN_FLOWS = true;

  /* ───────── Settings ───────── */
  const SETTINGS_KEY='iu_settings_v2';
  const DEFAULTS={
    version:2,
    debug:true,
    persistOverrideAcrossSessions:true,
    schedule:{
      weekdays:{ rangeStart:'09:00', rangeEnd:'19:00',
        inRange:{messages:true,chronological:false,stories:false,unrestricted:false},
        outRange:{messages:true,chronological:true,stories:true,unrestricted:false} },
      weekends:{ rangeStart:'00:00', rangeEnd:'00:00',
        inRange:{messages:true,chronological:true,stories:true,unrestricted:false},
        outRange:{messages:true,chronological:true,stories:true,unrestricted:false} },
    },
    override:{active:false,features:{messages:false,chronological:false,stories:false,unrestricted:false},expiresAt:null},
  };
  let settings=(function(){const s=GM.get(SETTINGS_KEY,null); if(!s||s.version!==DEFAULTS.version){GM.set(SETTINGS_KEY,DEFAULTS); return JSON.parse(JSON.stringify(DEFAULTS));} return s;})();
  function saveSettings(){ GM.set(SETTINGS_KEY,settings); }

  /* ───────── Debug ───────── */
  const dbg = {
    on: !!settings.debug,
    log(tag, msg, extra){ if(!this.on) return; try{ console.log(`[IU] ${tag} :: ${msg}`, extra??''); }catch{} },
    group(tag, obj){ if(!this.on) return; try{ console.groupCollapsed(`[IU] ${tag}`); if(obj) console.log(obj); console.groupEnd(); }catch{} }
  };
  window.IU = {
    version:'1.1.3',
    get settings(){return JSON.parse(JSON.stringify(settings));},
    state(){return {features: current.features, nextBoundary: current.nextBoundary};},
    toggleDebug(on){settings.debug=!!on; saveSettings(); dbg.on=settings.debug; console.info('[IU] debug', dbg.on?'ENABLED':'disabled');},
    setOverride(f){const exp=computeNextSwitchTime(); settings.override={active:true,features:f,expiresAt:exp.toISOString()}; saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=exp; applyPolicyForLocation(); dbg.group('IU.setOverride',{f,exp});},
    clearOverride(){settings.override.active=false; settings.override.expiresAt=null; saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=computeNextSwitchTime(); applyPolicyForLocation(); dbg.group('IU.clearOverride');},
    forceApply(){applyPolicyForLocation(); dbg.group('IU.forceApply');}
  };

  /* ───────── Stories intent (per-tab) ───────── */
  const setSOIntent = ()=>{ try{ sessionStorage.setItem(STORY_FLAG_SS,'1'); }catch{} };
  const inSO        = ()=> sessionStorage.getItem(STORY_FLAG_SS)==='1';
  const clearSOIntent = ()=>{ try{ sessionStorage.removeItem(STORY_FLAG_SS); }catch{} };
  const hasAndConsumeSOFromURL = ()=>{ if (location.hash===STORY_HASH){ history.replaceState(history.state,'',location.pathname+location.search); return true; } return false; };
  const consumeSOIntent = ()=>{ const s=inSO() || hasAndConsumeSOFromURL(); if(s) sessionStorage.setItem(STORY_FLAG_SS,'1'); return s; };
  let lastSOJump = 0;

  /* ───────── Scheduler ───────── */
  const isWeekend=(n)=>n===6||n===7;
  const cloneF=(f)=>({messages:!!f.messages,chronological:!!f.chronological,stories:!!f.stories,unrestricted:!!f.unrestricted});
  const eqF=(a,b)=>a.messages===b.messages && a.chronological===b.chronological && a.stories===b.stories && !!a.unrestricted===!!b.unrestricted;

  function normalizeRangesForDay(kind){
    const conf=settings.schedule[kind];
    const s=hhmmToMins(conf.rangeStart), e=hhmmToMins(conf.rangeEnd);
    const inR=cloneF(conf.inRange), outR=cloneF(conf.outRange);
    if(s===e) return [{start:0,end:1440,f:outR}];
    if(s<e) return [{start:0,end:s,f:outR},{start:s,end:e,f:inR},{start:e,end:1440,f:outR}];
    return [{start:0,end:e,f:inR},{start:e,end:s,f:outR},{start:s,end:1440,f:inR}];
  }
  function getScheduledFAt(d=new Date()){
    const dn=dayOfWeek(d), mins=d.getHours()*60+d.getMinutes(), kind=isWeekend(dn)?'weekends':'weekdays';
    const blocks=normalizeRangesForDay(kind);
    let f=blocks.find(b=>mins>=b.start&&mins<b.end)?.f; if(!f) f=blocks[blocks.length-1].f; return cloneF(f);
  }
  function getActiveFeatures(){
    if(settings.override?.active){
      const exp=settings.override.expiresAt?new Date(settings.override.expiresAt):null;
      if(!exp || now()<exp) return cloneF(settings.override.features);
      settings.override.active=false; settings.override.expiresAt=null; saveSettings(); dbg.log('Override','expired');
    }
    return getScheduledFAt();
  }
  function getNextBoundaryAfter(d=new Date()){
    const start=new Date(d.getTime()); const curr=getScheduledFAt(start);
    for(let i=0;i<8;i++){
      const day=new Date(start.getTime()); day.setDate(start.getDate()+i);
      const kind=isWeekend(dayOfWeek(day))?'weekends':'weekdays';
      for(const b of normalizeRangesForDay(kind)){
        const t=new Date(day.getFullYear(),day.getMonth(),day.getDate(),Math.floor(b.start/60),b.start%60,0,0);
        if(t<=d) continue;
        if(!eqF(getScheduledFAt(t), curr)) return t;
      }
    }
    const fb=new Date(d.getTime()+86400000); fb.setSeconds(0,0); return fb;
  }
  function computeNextSwitchTime(){ return getNextBoundaryAfter(now()); }

  /* ───────── Current state & timers ───────── */
  let current={ features:getActiveFeatures(), nextBoundary:computeNextSwitchTime() };
  dbg.group('Startup', current);

  let boundaryTimer=null;
  function scheduleTick(){
    if(boundaryTimer) clearTimeout(boundaryTimer);
    const ms=Math.max(1000, Math.min(86400000, current.nextBoundary - now()));
    boundaryTimer=setTimeout(()=>{
      current.features=getActiveFeatures(); current.nextBoundary=computeNextSwitchTime();
      dbg.group('Boundary tick', current);
      applyPolicyForLocation();
      toast(`Instagram Unhook → ${describeF(current.features)} (until ${fmtTime(current.nextBoundary)})`);
      scheduleTick(); refreshUI();
    }, ms);
  }
  const fmtTime=(d)=>{const dd=new Date(d); return `${dd.toLocaleDateString()} ${pad2(dd.getHours())}:${pad2(dd.getMinutes())}`;};
  function describeF(f){ if(f.unrestricted) return 'Unrestricted (Reels allowed)'; const p=[]; if(f.messages)p.push('Messages'); if(f.chronological)p.push('Chronological'); if(f.stories)p.push('Stories'); return p.join(' + ')||'No features'; }

  /* ───────── Access policy (allow comments/post detail; safe Stories viewer) ───────── */
  const isPostDetail = (p) => /^\/p\/[^/]+/.test(p);
  const isStoriesViewer = (p) => /^\/stories(\/|$)/.test(p);
  const isReelsPath = (p) => /^\/reels(\/|$)/.test(p);

  function checkAccess(f, p=location.pathname){
    if (f.unrestricted) return {allowed:true, reason:'unrestricted'};
    if (isDMPath(p)) return {allowed:true, reason:'dm'};
    if (ALLOW_LOGIN_FLOWS && isLoginFlowPath(p)) return {allowed:true, reason:'login'};
    if ((f.chronological || f.stories) && isPostDetail(p)) return {allowed:true, reason:'post-detail'}; // NEW
    if (f.stories && isStoriesViewer(p)) return {allowed:true, reason:'stories-viewer'}; // NEW

    const hybrid = f.messages && (f.chronological || f.stories);
    if (hybrid) {
      if (p === '/') return {allowed:true, reason:'hybrid-home'};
      if (isReelsPath(p)) return {allowed:true, reason:'reels-mapping'};
      return {allowed:false, reason:'hybrid-block-nonhome'};
    }
    if (f.messages && !hybrid) return {allowed:false, reason:'pure-messages'};
    return {allowed:true, reason:'default-allow'};
  }
  const landingForHybrid = (f)=> f.chronological ? FOLLOWING_URL : '/';

  /* ───────── CSS ───────── */
  GM.addStyle(`
    .iu-block-all body { opacity:0 !important; pointer-events:none !important; }
    .iu-stories-only main article { display:none !important; }
    .iu-toast{position:fixed;z-index:2147483647;left:50%;transform:translateX(-50%);bottom:24px;background:#111;color:#fff;padding:10px 14px;border-radius:10px;font:12px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;box-shadow:0 6px 30px rgba(0,0,0,.35);opacity:.95;max-width:calc(100vw - 28px);text-align:center;}
    .iu-gear{position:fixed;z-index:2147483647;right:max(18px, env(safe-area-inset-right,18px));bottom:max(18px, env(safe-area-inset-bottom,18px));width:40px;height:40px;border-radius:50%;background:#111;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 6px 30px rgba(0,0,0,.35);}
    .iu-panel-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:2147483646;}
    .iu-panel{position:fixed;right:max(20px, env(safe-area-inset-right,20px));bottom:max(70px, env(safe-area-inset-bottom,70px));width:360px;max-width:min(420px, calc(100vw - 32px));background:#fff;color:#111;border-radius:14px;box-shadow:0 20px 50px rgba(0,0,0,.25);padding:16px;z-index:2147483647;font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;box-sizing:border-box;}
    .iu-panel *{box-sizing:border-box}
    .iu-row{display:flex;gap:8px;align-items:center;margin:6px 0;flex-wrap:wrap}
    .iu-sec{border-top:1px solid #eee;margin-top:10px;padding-top:10px}
    .iu-h{font-weight:700;font-size:14px;margin-bottom:6px}
    .iu-muted{opacity:.5;pointer-events:none}
    .iu-panel .iu-btn{all:unset;display:inline-block;padding:6px 10px;border-radius:8px;border:1px solid #ddd;background:#fafafa;cursor:pointer;white-space:nowrap;font:13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,sans-serif !important;color:#111 !important;-webkit-appearance:none;appearance:none}
    .iu-panel .iu-btn.primary{background:#111;border-color:#111;color:#fff !important}
    .iu-grid{display:grid;grid-template-columns:auto 1fr;gap:6px 10px}
    .iu-note{color:#666;font-size:12px}
    .iu-kbd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#f1f1f1;padding:1px 4px;border-radius:4px;border:1px solid #ddd}
  `);

  /* ───────── Early enforcement (ordered) ───────── */
  if (!getActiveFeatures().stories && sessionStorage.getItem(STORY_FLAG_SS)) {
    dbg.log('Early','Clearing stories intent (Stories OFF)');
    clearSOIntent();
  }

  if (!current.features.unrestricted && /^\/reels(\/|$)/.test(location.pathname)) {
    dbg.log('Early','Reels path → stories intent home');
    setSOIntent(); location.replace('/' + STORY_HASH);
  }

  {
    const chk = checkAccess(current.features, location.pathname);
    dbg.log('Early-Access', `${chk.allowed?'allow':'BLOCK'} @${location.pathname}`, chk.reason);
    if (!chk.allowed) {
      document.documentElement.classList.add('iu-block-all');
      const f=current.features;
      if (f.messages && !(f.chronological||f.stories)) {
        if (location.href !== DM_INBOX_URL) location.replace(DM_INBOX_URL);
      } else {
        const dest = landingForHybrid(f);
        if (location.href !== dest) location.replace(dest);
      }
    }
  }

  if (!current.features.unrestricted && current.features.chronological && isRoot() && !onFollowing()) {
    if (current.features.stories) {
      if (!inSO() && !hasAndConsumeSOFromURL()) {
        dbg.log('Early','Chrono+Stories but no intent → Following');
        location.replace(FOLLOWING_URL);
      }
    } else {
      if (inSO()) { dbg.log('Early','Chrono only → clear stories intent'); clearSOIntent(); }
      dbg.log('Early','Chrono only → Following');
      location.replace(FOLLOWING_URL);
    }
  }

  /* ───────── Nav enforcement ───────── */
  function gotoDM(reason=''){ try{sessionStorage.setItem('iu_reason',reason);}catch{} if(location.href!==DM_INBOX_URL) location.replace(DM_INBOX_URL); }
  const blockContent=(on)=>document.documentElement.classList.toggle('iu-block-all',!!on);
  const applySOClass=(on)=>document.documentElement.classList.toggle('iu-stories-only',!!on);

  function wireAnchors(root=document){
    root.querySelectorAll('a[href]:not([data-iu-wired])').forEach(a=>{
      a.dataset.iuWired='1';
      a.addEventListener('click',(ev)=>{
        const href=a.getAttribute('href')||''; if(!href||!sameOrigin(href)) return;
        const p=pathOf(href); const f=current.features;

        // Reels inside DMs: block in-place (no nav) unless Unrestricted
        if (!f.unrestricted && isDMPath(location.pathname) && /^\/reels(\/|$)/.test(p)) {
          ev.preventDefault(); ev.stopImmediatePropagation();
          toast('Reels blocked in Messages (enable Unrestricted to view)');
          dbg.log('Anchor','Reels in DM → blocked in place');
          return;
        }

        // Access control
        const chk = checkAccess(f, p);
        if (!chk.allowed) {
          ev.preventDefault(); ev.stopImmediatePropagation();
          dbg.log('Anchor','BLOCK', {p, reason: chk.reason});
          if (f.messages && !(f.chronological||f.stories)) { blockContent(true); gotoDM(`click:${p}`); }
          else { location.assign(landingForHybrid(f)); }
          return;
        }

        // Home behavior
        if (!f.unrestricted && p==='/') {
          ev.preventDefault(); ev.stopImmediatePropagation();
          if (f.chronological) {
            clearSOIntent(); applySOClass(false);
            dbg.log('Anchor','Home → Following (chronological preferred)');
            location.assign(FOLLOWING_URL);
          } else if (f.stories) {
            setSOIntent(); applySOClass(true);
            dbg.log('Anchor','Home → stories-only');
            location.assign('/');
          } else {
            dbg.log('Anchor','Home pass-through');
            location.assign('/');
          }
          return;
        }

        // Global Reels mapping (non-DM contexts)
        if (!f.unrestricted && /^\/reels(\/|$)/.test(p)) {
          ev.preventDefault(); ev.stopImmediatePropagation();
          const t = Date.now();
          if (t - lastSOJump > 800) {
            lastSOJump = t;
            dbg.log('Anchor','Reels → stories intent');
            setSOIntent(); location.assign('/'+STORY_HASH);
          } else {
            dbg.log('Anchor','Reels mapping throttled');
          }
          return;
        }
      }, {capture:true});
    });
  }

  wireAnchors();
  const bigMO=new MutationObserver(m=>{ for(const rec of m) for(const n of rec.addedNodes||[]) if(n.nodeType===1){ wireAnchors(n); relabelAndInterceptReels(!current.features.unrestricted); } });
  bigMO.observe(document.documentElement,{childList:true,subtree:true});

  (function hookHistory(){
    const wrap=(fn)=>function(...args){ const rv=fn.apply(this,args); queueMicrotask(()=>{ dbg.log('History',`${fn.name} → policy`); applyPolicyForLocation(); }); return rv; };
    history.pushState=wrap(history.pushState.bind(history));
    history.replaceState=wrap(history.replaceState.bind(history));
  })();
  window.addEventListener('popstate',()=>{ dbg.log('History','popstate → policy'); applyPolicyForLocation(); });
  window.addEventListener('DOMContentLoaded',()=>{ buildUI(); refreshUI(); });

  /* ───────── Per-location policy ───────── */
  function applyPolicyForLocation(){
    const f=current.features;
    dbg.group('ApplyPolicy', {path: location.pathname+location.search+location.hash, features: f, inSO: inSO()});

    if (!f.stories && inSO()) { dbg.log('Policy','Stories OFF → clear intent'); clearSOIntent(); applySOClass(false); }

    const chk = checkAccess(f);
    dbg.log('Policy-Access', `${chk.allowed?'allow':'BLOCK'}`, chk.reason);
    if (!chk.allowed) {
      if (f.messages && !(f.chronological||f.stories)) { blockContent(true); gotoDM(`policy:${location.pathname}`); return; }
      location.replace(landingForHybrid(f)); return;
    } else { blockContent(false); }

    if (!f.unrestricted && isRoot()) {
      if (f.stories && !onFollowing()) {
        const so = consumeSOIntent();
        if (so || inSO()) { applySOClass(true); startSOHiding(); dbg.log('Policy','stories-only on Home'); }
        else if (f.chronological) { applySOClass(false); dbg.log('Policy','chronological preferred on Home → Following'); location.replace(FOLLOWING_URL); return; }
        else { applySOClass(false); }
      } else if (f.chronological && !onFollowing()) {
        applySOClass(false); clearSOIntent();
        dbg.log('Policy','chronological only → Following'); location.replace(FOLLOWING_URL); return;
      } else { applySOClass(false); }
    } else { applySOClass(false); }

    relabelAndInterceptReels(!f.unrestricted);
  }

  /* ───────── Stories-only hider ───────── */
  let soObserver=null;
  function hidePostsOnce(){ if(!document.documentElement.classList.contains('iu-stories-only')) return;
    document.querySelectorAll('main article').forEach(el=>{ if(el.dataset.iuHidden) return; el.dataset.iuHidden='1'; el.style.display='none'; el.setAttribute('aria-hidden','true'); });
  }
  function startSOHiding(){ hidePostsOnce(); if(soObserver||!document.body) return; soObserver=new MutationObserver(()=>{ if(inSO()) hidePostsOnce(); }); soObserver.observe(document.body,{childList:true,subtree:true}); }
  function stopSOHiding(){ if(soObserver){ soObserver.disconnect(); soObserver=null; } document.querySelectorAll('main article[data-iu-hidden="1"]').forEach(el=>{ el.style.display=''; el.removeAttribute('aria-hidden'); delete el.dataset.iuHidden; }); }

  /* ───────── Reels → Stories label/mapping ───────── */
  function setAnchorLabel(a, text){
    let touched=false;
    const leaf=[...a.querySelectorAll('span,div')].find(n=>n.childElementCount===0 && /\S/.test(n.textContent));
    if(leaf){ leaf.textContent=text; touched=true; }
    if(a.getAttribute('aria-label')){ a.setAttribute('aria-label',text); touched=true; }
    const svg=a.querySelector('svg'); if(svg){ svg.setAttribute('aria-label',text); const t=svg.querySelector('title'); if(t) t.textContent=text; touched=true; }
    return touched;
  }
  function relabelAndInterceptReels(enableMapping){
    const root=document;

    root.querySelectorAll('a[href^="/reels"]:not([data-iu-wired-reels])').forEach(a=>{
      a.dataset.iuWiredReels='1';
      a.addEventListener('click',(ev)=>{ if(enableMapping && !isDMPath(location.pathname)){ ev.preventDefault(); ev.stopImmediatePropagation(); const t=Date.now(); if(t-lastSOJump>800){ lastSOJump=t; dbg.log('Reels','icon link → stories intent'); setSOIntent(); location.assign('/'+STORY_HASH);} } }, {capture:true});
    });
    root.querySelectorAll('a[href^="/reels"]:not([data-iu-wired-reels-text])').forEach(a=>{
      if(a.querySelector('svg')) return;
      a.dataset.iuWiredReelsText='1';
      a.addEventListener('click',(ev)=>{ if(enableMapping && !isDMPath(location.pathname)){ ev.preventDefault(); ev.stopImmediatePropagation(); const t=Date.now(); if(t-lastSOJump>800){ lastSOJump=t; dbg.log('Reels','text link → stories intent'); setSOIntent(); location.assign('/'+STORY_HASH);} } }, {capture:true});
    });

    root.querySelectorAll('a[href^="/reels"]').forEach(a=>{
      if(enableMapping){ if(a.dataset.iuLabeled!=='1'){ if(setAnchorLabel(a,'Stories')){ a.dataset.iuLabeled='1'; dbg.log('Reels','Relabeled'); } } }
      else { if(a.dataset.iuLabeled==='1'){ setAnchorLabel(a,'Reels'); a.dataset.iuLabeled=''; } }
    });
  }

  /* ───────── Toast & UI ───────── */
  let toastTimer=null;
  function toast(msg,dur=2500){ try{ const old=document.querySelector('.iu-toast'); if(old) old.remove(); const t=document.createElement('div'); t.className='iu-toast'; t.textContent=msg; document.documentElement.appendChild(t); clearTimeout(toastTimer); toastTimer=setTimeout(()=>t.remove(),dur); }catch{} }

  let gearBtn=null, panel=null, backdrop=null;
  function buildUI(){
    gearBtn=document.createElement('div'); gearBtn.className='iu-gear'; gearBtn.title='Instagram Unhook settings (Alt+U)'; gearBtn.innerHTML='⚙️';
    gearBtn.addEventListener('click',openPanel); document.documentElement.appendChild(gearBtn);
    window.addEventListener('keydown',(e)=>{ if(e.altKey&&!e.shiftKey&&!e.ctrlKey&&!e.metaKey&&e.key.toLowerCase()==='u'){ e.preventDefault(); openPanel(); }});
    GM.registerMenu('Instagram Unhook: Open settings', openPanel);
  }
  function openPanel(){
    if(panel){ refreshUI(); return; }
    backdrop=document.createElement('div'); backdrop.className='iu-panel-backdrop'; backdrop.addEventListener('click', closePanel);
    panel=document.createElement('div'); panel.className='iu-panel';
    panel.innerHTML=`
      <div class="iu-h">Instagram Unhook</div>
      <div class="iu-grid"><div>Current:</div><div id="iu-cur"></div><div>Next switch:</div><div id="iu-next"></div></div>
      <div class="iu-sec">
        <div class="iu-h">Override (until next boundary)</div>
        <div class="iu-row">
          <button id="iu-ovr-none" class="iu-btn">Clear override</button>
          <button id="iu-ovr-unr" class="iu-btn">Unrestricted</button>
          <label><input type="checkbox" id="iu-ovr-msg"> Messages</label>
          <label><input type="checkbox" id="iu-ovr-chron"> Chronological</label>
          <label><input type="checkbox" id="iu-ovr-sto"> Stories</label>
          <button id="iu-ovr-apply" class="iu-btn primary">Apply</button>
        </div>
        <div class="iu-row">
          <label><input type="checkbox" id="iu-ovr-persist"> Persist override across sessions</label>
          <label><input type="checkbox" id="iu-debug"> Enable debug logging</label>
        </div>
        <div class="iu-note">Enabling <b>Unrestricted</b> allows Reels and disables all protections.</div>
      </div>
      <div class="iu-sec">
        <div class="iu-h">Schedule — Weekdays</div>
        <div class="iu-row"><span>In-range:</span><input id="iu-wd-start" type="time" step="60" style="width:110px"><span>to</span><input id="iu-wd-end" type="time" step="60" style="width:110px"></div>
        <div id="iu-wd-in" class="iu-row"><span>Features in-range:</span>
          <label><input type="checkbox" id="iu-wd-in-msg"> Messages</label>
          <label><input type="checkbox" id="iu-wd-in-chron"> Chronological</label>
          <label><input type="checkbox" id="iu-wd-in-sto"> Stories</label>
        </div>
        <div id="iu-wd-out" class="iu-row"><span>Features out-of-range:</span>
          <label><input type="checkbox" id="iu-wd-out-msg"> Messages</label>
          <label><input type="checkbox" id="iu-wd-out-chron"> Chronological</label>
          <label><input type="checkbox" id="iu-wd-out-sto"> Stories</label>
        </div>
      </div>
      <div class="iu-sec">
        <div class="iu-h">Schedule — Weekends</div>
        <div class="iu-row"><span>In-range:</span><input id="iu-we-start" type="time" step="60" style="width:110px"><span>to</span><input id="iu-we-end" type="time" step="60" style="width:110px"></div>
        <div id="iu-we-in" class="iu-row"><span>Features in-range:</span>
          <label><input type="checkbox" id="iu-we-in-msg"> Messages</label>
          <label><input type="checkbox" id="iu-we-in-chron"> Chronological</label>
          <label><input type="checkbox" id="iu-we-in-sto"> Stories</label>
        </div>
        <div id="iu-we-out" class="iu-row"><span>Features out-of-range:</span>
          <label><input type="checkbox" id="iu-we-out-msg"> Messages</label>
          <label><input type="checkbox" id="iu-we-out-chron"> Chronological</label>
          <label><input type="checkbox" id="iu-we-out-sto"> Stories</label>
        </div>
        <div class="iu-note" id="iu-we-note" style="display:none">Note: Start = End → full-day uses <b>Out-of-range</b>; In-range is inactive.</div>
      </div>
      <div class="iu-sec iu-row" style="justify-content:space-between">
        <button id="iu-save" class="iu-btn primary">Save</button>
        <button id="iu-reset" class="iu-btn">Reset to defaults</button>
        <span class="iu-note">Tip: open with <span class="iu-kbd">Alt+U</span></span>
      </div>`;
    document.documentElement.appendChild(backdrop);
    document.documentElement.appendChild(panel);

    panel.querySelector('#iu-ovr-none').addEventListener('click', ()=>{ settings.override.active=false; settings.override.expiresAt=null; saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=computeNextSwitchTime(); applyPolicyForLocation(); refreshUI(); toast('Override cleared'); });
    panel.querySelector('#iu-ovr-unr').addEventListener('click', ()=>{ if(!confirm('Enable Unrestricted? Reels will be available and protections disabled until the next scheduled boundary.')) return; const exp=computeNextSwitchTime(); settings.override={active:true,features:{messages:false,chronological:false,stories:false,unrestricted:true},expiresAt:exp.toISOString()}; saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=exp; applyPolicyForLocation(); refreshUI(); toast('Unrestricted ON'); });
    panel.querySelector('#iu-ovr-apply').addEventListener('click', ()=>{ const f={messages:panel.querySelector('#iu-ovr-msg').checked, chronological:panel.querySelector('#iu-ovr-chron').checked, stories:panel.querySelector('#iu-ovr-sto').checked, unrestricted:false}; const exp=computeNextSwitchTime(); settings.override={active:true,features:f,expiresAt:exp.toISOString()}; saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=exp; applyPolicyForLocation(); refreshUI(); toast(`Override: ${describeF(f)}`); });
    panel.querySelector('#iu-ovr-persist').addEventListener('change',(e)=>{ settings.persistOverrideAcrossSessions=!!e.target.checked; saveSettings(); });
    panel.querySelector('#iu-debug').addEventListener('change',(e)=>{ settings.debug=!!e.target.checked; saveSettings(); dbg.on=settings.debug; console.info('[IU] debug', dbg.on?'ENABLED':'disabled'); });

    panel.querySelector('#iu-save').addEventListener('click', ()=>{
      const S=settings.schedule;
      S.weekdays.rangeStart=panel.querySelector('#iu-wd-start').value||'09:00';
      S.weekdays.rangeEnd  =panel.querySelector('#iu-wd-end').value  ||'19:00';
      S.weekdays.inRange   ={messages:panel.querySelector('#iu-wd-in-msg').checked, chronological:panel.querySelector('#iu-wd-in-chron').checked, stories:panel.querySelector('#iu-wd-in-sto').checked, unrestricted:false};
      S.weekdays.outRange  ={messages:panel.querySelector('#iu-wd-out-msg').checked, chronological:panel.querySelector('#iu-wd-out-chron').checked, stories:panel.querySelector('#iu-wd-out-sto').checked, unrestricted:false};
      S.weekends.rangeStart=panel.querySelector('#iu-we-start').value||'00:00';
      S.weekends.rangeEnd  =panel.querySelector('#iu-we-end').value  ||'00:00';
      S.weekends.inRange   ={messages:panel.querySelector('#iu-we-in-msg').checked, chronological:panel.querySelector('#iu-we-in-chron').checked, stories:panel.querySelector('#iu-we-in-sto').checked, unrestricted:false};
      S.weekends.outRange  ={messages:panel.querySelector('#iu-we-out-msg').checked, chronological:panel.querySelector('#iu-we-out-chron').checked, stories:panel.querySelector('#iu-we-out-sto').checked, unrestricted:false};
      saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=computeNextSwitchTime(); applyPolicyForLocation(); refreshUI(); toast('Schedule saved'); });

    panel.querySelector('#iu-reset').addEventListener('click', ()=>{ if(!confirm('Reset to defaults?')) return; settings=JSON.parse(JSON.stringify(DEFAULTS)); saveSettings(); current.features=getActiveFeatures(); current.nextBoundary=computeNextSwitchTime(); applyPolicyForLocation(); refreshUI(); toast('Defaults restored'); });

    [['#iu-wd-start','#iu-wd-end','#iu-wd-in',null], ['#iu-we-start','#iu-we-end','#iu-we-in','#iu-we-note']].forEach(([sId,eId,inId,noteId])=>{
      const s=panel.querySelector(sId), e=panel.querySelector(eId), row=panel.querySelector(inId), note=noteId?panel.querySelector(noteId):null;
      const up=()=>{ const same=(s.value||'00:00')===(e.value||'00:00'); row.classList.toggle('iu-muted',same); if(note) note.style.display=same?'':'none'; };
      s.addEventListener('input',up); e.addEventListener('input',up);
    });

    refreshUI();
  }
  function closePanel(){ if(panel) panel.remove(); panel=null; if(backdrop) backdrop.remove(); backdrop=null; }
  function setInput(id,val){ const i=panel.querySelector(id); if(i) i.checked=!!val; }
  function setTime(id,val){ const i=panel.querySelector(id); if(i) i.value=val; }
  function refreshUI(){
    if(!panel) return;
    panel.querySelector('#iu-cur').textContent=describeF(current.features);
    panel.querySelector('#iu-next').textContent=fmtTime(current.nextBoundary);

    const S=settings.schedule;
    setTime('#iu-wd-start',S.weekdays.rangeStart); setTime('#iu-wd-end',S.weekdays.rangeEnd);
    setInput('#iu-wd-in-msg',S.weekdays.inRange.messages); setInput('#iu-wd-in-chron',S.weekdays.inRange.chronological); setInput('#iu-wd-in-sto',S.weekdays.inRange.stories);
    setInput('#iu-wd-out-msg',S.weekdays.outRange.messages); setInput('#iu-wd-out-chron',S.weekdays.outRange.chronological); setInput('#iu-wd-out-sto',S.weekdays.outRange.stories);
    setTime('#iu-we-start',S.weekends.rangeStart); setTime('#iu-we-end',S.weekends.rangeEnd);
    setInput('#iu-we-in-msg',S.weekends.inRange.messages); setInput('#iu-we-in-chron',S.weekends.inRange.chronological); setInput('#iu-we-in-sto',S.weekends.inRange.stories);
    setInput('#iu-we-out-msg',S.weekends.outRange.messages); setInput('#iu-we-out-chron',S.weekends.outRange.chronological); setInput('#iu-we-out-sto',S.weekends.outRange.stories);

    const wdSame=S.weekdays.rangeStart===S.weekdays.rangeEnd;
    const weSame=S.weekends.rangeStart===S.weekends.rangeEnd;
    panel.querySelector('#iu-wd-in').classList.toggle('iu-muted',wdSame);
    panel.querySelector('#iu-we-in').classList.toggle('iu-muted',weSame);
    const weNote=panel.querySelector('#iu-we-note'); if(weNote) weNote.style.display=weSame?'':'none';

    setInput('#iu-ovr-persist',!!settings.persistOverrideAcrossSessions);
    setInput('#iu-debug',!!settings.debug);

    const ov=settings.override||{}; const f=ov.features||{};
    setInput('#iu-ovr-msg',!!f.messages); setInput('#iu-ovr-chron',!!f.chronological); setInput('#iu-ovr-sto',!!f.stories);
  }

  /* ───────── Start ───────── */
  scheduleTick();
  applyPolicyForLocation();
  new MutationObserver(()=>{ if(isRoot() && inSO()) startSOHiding(); else stopSOHiding(); })
    .observe(document.documentElement,{childList:true,subtree:true});
})();