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!

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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});
})();