<?php
// ═══════════════════════════════════════════════════════════════════════════════
//  OpenDNS Home Remote Control — v2.34
//  Created by Clive Thompson (clivethompson.com) — free to self-host
//  PHP 7.4+ · cURL · sessions · no database
//
//  SCHEDULING: Schedules are stored in opendns-schedules.json in the same
//  directory. For 24/7 scheduling without a browser tab, add a cron job:
//    * * * * * curl -s "https://yoursite.com/opendns-remote.php?cron=1&key=ODNS2026CRON"
//  SECURITY: Protect opendns-schedules.json from web access (see .htaccess note below).
// ═══════════════════════════════════════════════════════════════════════════════
session_start();

define('SCHED_FILE', __DIR__ . '/opendns-schedules.json');
define('CRON_KEY',   'ODNS2026CRON');  // change this if you self-host
define('SCHEDULE_OWNER', '');          // set to your OpenDNS email to restrict server scheduling to yourself only

// ── Protected always-allow domains — cannot be removed by any action ──────────
$protectedAllowDomains = ['clivethompson.com', 'opendns.com'];

// ── Built-in profiles ─────────────────────────────────────────────────────────
// Adware and Advertisements are included in every profile except 'open' (the explicit unblock)
// ALL and ALL_EXCEPT profiles already cover them
$profiles = [
    'ads_adware'=> ['label'=>'Block Ads & Adware', 'desc'=>'Blocks ad servers and adware sites — recommended always-on baseline',
                    'icon'=>'🛡️','color'=>'#0891b2','cats'=>['Adware','Advertisements'],'domains'=>[]],
    'open'      => ['label'=>'Everything Open',    'desc'=>'No filtering — removes all blocks including ads and adware',
                    'icon'=>'🌐','color'=>'#16a34a','cats'=>[],'domains'=>[]],
    'kids'      => ['label'=>'Kids Mode',          'desc'=>'Blocks adult content, drugs, gambling, ads and adware',
                    'icon'=>'👶','color'=>'#0891b2',
                    'cats'=>['Adware','Advertisements','Adult Themes','Pornography','Sexuality','Nudity','Lingerie/Bikini','Drugs','Weapons','Hate/Discrimination','Tasteless','Gambling'],'domains'=>[]],
    'study'     => ['label'=>'Study Mode',         'desc'=>'Blocks social, video, games, chat, ads and adware',
                    'icon'=>'📚','color'=>'#7c3aed',
                    'cats'=>['Adware','Advertisements','Social Networking','Games','Video Sharing','Movies','Television','Music','Radio','Photo Sharing','Instant Messaging','Chat'],'domains'=>[]],
    'no_social' => ['label'=>'No Social Media',    'desc'=>'Blocks social networks, ads and adware',
                    'icon'=>'📵','color'=>'#ca8a04','cats'=>['Adware','Advertisements','Social Networking'],'domains'=>[]],
    'evening'   => ['label'=>'Evening Mode',       'desc'=>'Blocks video, social, games, movies, ads and adware',
                    'icon'=>'🌙','color'=>'#ea580c',
                    'cats'=>['Adware','Advertisements','Video Sharing','Social Networking','Games','Movies','Television','Music','Radio','Photo Sharing'],'domains'=>[]],
    'bedtime'   => ['label'=>'Bedtime Mode',       'desc'=>'Blocks Roblox and YouTube only — everything else stays open',
                    'icon'=>'🛌','color'=>'#4f46e5','cats'=>[],'domains'=>['roblox.com','youtube.com']],
    'no_media'  => ['label'=>'No Media',           'desc'=>'Blocks social media, video and games categories',
                    'icon'=>'📵','color'=>'#7c3aed','cats'=>['Adware','Advertisements','Social Networking','Video Sharing','Games'],'domains'=>[]],
    'lockdown'  => ['label'=>'Full Lockdown',      'desc'=>'Blocks all categories',
                    'icon'=>'🔒','color'=>'#dc2626','cats'=>'ALL','domains'=>[]],
    'wife'      => ['label'=>'Wife Mode',          'desc'=>'Blocks Facebook, ads and adware',
                    'icon'=>'👩','color'=>'#db2777','cats'=>['Adware','Advertisements'],'domains'=>['facebook.com']],
    'husband'   => ['label'=>'Husband Mode',       'desc'=>'Blocks adult content, ads and adware',
                    'icon'=>'👨','color'=>'#1d4ed8',
                    'cats'=>['Adware','Advertisements','Pornography','Adult Themes','Sexuality','Nudity','Lingerie/Bikini'],'domains'=>[]],
    'edu_only'  => ['label'=>'Education Only',     'desc'=>'Blocks everything except education and research sites',
                    'icon'=>'🏫','color'=>'#059669','cats'=>'ALL_EXCEPT',
                    'cats_except'=>['Educational Institutions','Research/Reference'],'domains'=>[]],
    'kindergpt' => ['label'=>'KinderGPT Only',     'desc'=>'Full lockdown — only KinderGPT and always-authorised sites accessible',
                    'icon'=>'🧒','color'=>'#7c3aed','cats'=>'ALL','domains'=>[],'allow_domains'=>['kindergpt.com']],
];

$catIds = [
    'Academic Fraud'=>72,'Adult Themes'=>58,'Advertisements'=>414,'Adware'=>1,
    'Alcohol'=>2,'Anime/Manga/Webcomic'=>76,'Auctions'=>3,'Automotive'=>70,
    'Blogs'=>4,'Business Services'=>56,'Chat'=>5,'Classifieds'=>6,'Dating'=>7,
    'Drugs'=>8,'Ecommerce/Shopping'=>9,'Educational Institutions'=>52,
    'File Storage'=>10,'Financial Institutions'=>55,'Forums/Message boards'=>67,
    'Gambling'=>11,'Games'=>12,'German Youth Protection'=>74,'Government'=>49,
    'Hate/Discrimination'=>13,'Health and Fitness'=>14,'Humor'=>15,
    'Instant Messaging'=>16,'Jobs/Employment'=>17,'Lingerie/Bikini'=>60,
    'Movies'=>19,'Music'=>50,'News/Media'=>33,'Non-Profits'=>69,'Nudity'=>63,
    'P2P/File sharing'=>20,'Parked Domains'=>57,'Photo Sharing'=>48,
    'Podcasts'=>71,'Politics'=>66,'Pornography'=>64,'Portals'=>21,
    'Proxy/Anonymizer'=>61,'Radio'=>22,'Religious'=>65,'Research/Reference'=>54,
    'Search Engines'=>23,'Sexuality'=>62,'Social Networking'=>24,
    'Software/Technology'=>47,'Sports'=>51,'Tasteless'=>59,'Television'=>34,
    'Tobacco'=>73,'Travel'=>68,'Video Sharing'=>26,'Visual Search Engines'=>27,
    'Weapons'=>28,'Web Spam'=>77,'Webmail'=>29,
];

// ── Helpers ───────────────────────────────────────────────────────────────────
function getAllProfiles($builtins) {
    $custom = $_SESSION['odns_custom'] ?? [];
    return array_merge($builtins, $custom);
}
function _ua(){return'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';}
function curlGet($url, $jar, $ref='') {
    $h=['User-Agent: '._ua()];if($ref)$h[]='Referer: '.$ref;
    $ch=curl_init($url);
    curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>true,
        CURLOPT_COOKIEJAR=>$jar,CURLOPT_COOKIEFILE=>$jar,CURLOPT_SSL_VERIFYPEER=>false,
        CURLOPT_TIMEOUT=>20,CURLOPT_HTTPHEADER=>$h]);
    $body=curl_exec($ch);$info=curl_getinfo($ch);curl_close($ch);
    return['body'=>$body,'url'=>$info['url'],'code'=>$info['http_code']];
}
function curlPost($url, $fields, $jar, $ref='') {
    $h=['User-Agent: '._ua()];if($ref)$h[]='Referer: '.$ref;
    $ch=curl_init($url);
    curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,
        CURLOPT_POSTFIELDS=>http_build_query($fields),CURLOPT_FOLLOWLOCATION=>true,
        CURLOPT_COOKIEJAR=>$jar,CURLOPT_COOKIEFILE=>$jar,CURLOPT_SSL_VERIFYPEER=>false,
        CURLOPT_TIMEOUT=>20,CURLOPT_HTTPHEADER=>$h]);
    $body=curl_exec($ch);$info=curl_getinfo($ch);curl_close($ch);
    return['body'=>$body,'url'=>$info['url'],'code'=>$info['http_code']];
}
function odnsLogin($email,$pass) {
    $jar=tempnam(sys_get_temp_dir(),'odns_');
    $dash='https://dashboard.opendns.com/';
    $r=curlGet('https://login.opendns.com/?return_to='.urlencode($dash),$jar);
    preg_match('/name="formtoken"\s+value="([^"]+)"/',$r['body'],$m);
    $r=curlPost('https://login.opendns.com',[
        'username'=>$email,'password'=>$pass,'return_to'=>$dash,
        'formtoken'=>($m[1]??''),'loginToken'=>'',
    ],$jar);
    if(strpos($r['url'],'login.opendns.com')!==false){@unlink($jar);return['ok'=>false,'error'=>'Incorrect email or password.'];}
    preg_match_all('/\/settings\/(\d{6,12})\//',$r['body'],$nm);
    $ids=array_unique($nm[1]??[]);
    if(empty($ids)){@unlink($jar);return['ok'=>false,'error'=>'Logged in but no networks found.'];}
    $nets=[];
    foreach($ids as $id){
        $pat='/option[^>]*value=["\']?'.preg_quote($id,'/').'["\']?[^>]*>([^<]+)</';
        $name=preg_match($pat,$r['body'],$nx)?trim($nx[1]):'Network '.$id;
        $nets[]=['id'=>$id,'name'=>$name];
    }
    $cookies=file_get_contents($jar);@unlink($jar);
    return['ok'=>true,'cookies'=>$cookies,'networks'=>$nets];
}
function applyProfile($networkId,$profileKey,$builtins,$catIds,$cookies) {
    $all=getAllProfiles($builtins);
    if(!isset($all[$profileKey]))return['ok'=>false,'message'=>'Profile not found (ID: '.$profileKey.'). Try signing out and signing in again to reload.'];
    $p=$all[$profileKey];
    if(!$networkId)return['ok'=>false,'message'=>'No network selected — sign out and sign in again.'];
    $jar=tempnam(sys_get_temp_dir(),'odns_');
    file_put_contents($jar,$cookies);
    $cfUrl="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
    // Fetch settings page: get CSRF token + discover form action
    $pg=curlGet($cfUrl,$jar);
    if(strpos($pg['url']??'','login.opendns.com')!==false){
        @unlink($jar);
        return['ok'=>false,'message'=>'⚠️ Could not reach OpenDNS settings — login may have failed.'];
    }
    $catToken='';
    if(preg_match('/name=["\']?formtoken["\']?[^>]*value=["\']?([^"\'>\s]+)/',$pg['body'],$m)||
       preg_match('/value=["\']?([^"\'>\s]+)["\']?[^>]*name=["\']?formtoken["\']?/',$pg['body'],$m))
        $catToken=$m[1];
    // Use form action from the page if found, otherwise fall back to known endpoint
    $ajaxUrl='https://dashboard.opendns.com/dashboard_ajax.php';
    if(preg_match('/<form[^>]+action=["\']([^"\']+)["\'][^>]*(?:id|name)=["\']content[_-]?filtering["\']/',$pg['body'],$fm)||
       preg_match('/<form[^>]+(?:id|name)=["\']content[_-]?filtering["\'][^>]*action=["\']([^"\']+)["\']/',$pg['body'],$fm)){
        $found=$fm[1];
        $ajaxUrl=preg_match('/^https?:\/\//',$found)?$found:'https://dashboard.opendns.com'.$found;
    }
    $cats=$p['cats'];
    $post=['n'=>$networkId,'action'=>'save_blocking_categories',
           'return'=>"/settings/{$networkId}/content_filtering",'save-categories'=>'Apply','applytoall'=>''];
    if($catToken)$post['formtoken']=$catToken; // categories need the CSRF token; domain calls do not
    if($cats==='ALL'){foreach($catIds as $n=>$id)$post["dt_category[$id]"]=$id;}
    elseif($cats==='ALL_EXCEPT'){$except=$p['cats_except']??[];foreach($catIds as $n=>$id){if(!in_array($n,$except))$post["dt_category[$id]"]=$id;}}
    else{foreach((array)$cats as $n){if(isset($catIds[$n])){$id=$catIds[$n];$post["dt_category[$id]"]=$id;}}}
    $ar=curlPost($ajaxUrl,$post,$jar,$cfUrl);
    // Keep $jar alive — reuse the same session for domain calls so updated cookies carry over
    $catCount=$cats==='ALL'?count($catIds):($cats==='ALL_EXCEPT'?count($catIds)-count($p['cats_except']??[])
        :count(array_filter((array)$cats,fn($n)=>isset($catIds[$n]))));
    $domCount=count($p['domains']??[]);
    $domResults=[];
    foreach($p['domains']??[] as $d){
        if(!empty($domResults))usleep(500000);
        $dr1=curlPost('https://dashboard.opendns.com/dashboard_ajax.php',
            ['step1'=>'true','blocked_domain'=>$d,'action'=>'add_blocked_domain','n'=>$networkId],$jar,$cfUrl);
        $d1Body=json_decode($dr1['body']??'',true);
        if($d1Body['success']??false){
            $domOk='ok';
        } elseif(array_key_exists('has_categories',$d1Body??[])){
            usleep(300000);
            $dr2=curlPost('https://dashboard.opendns.com/dashboard_ajax.php',
                ['step2'=>'true','blocked_domain'=>$d,'action'=>'add_blocked_domain','n'=>$networkId],$jar,$cfUrl);
            $d2Body=json_decode($dr2['body']??'',true);
            $domOk=($d2Body['success']??false)?'ok':'FAIL2:'.$dr2['code'].':'.substr($d2Body['message']??($dr2['body']??''),0,40);
        } else {
            $domOk='FAIL:'.$dr1['code'].':'.substr($d1Body['message']??($dr1['body']??''),0,40);
        }
        $domResults[]=$d.':'.$domOk;
        $bl=$_SESSION['odns_blocked']??[];
        if(!in_array($d,$bl)){$bl[]=$d;$_SESSION['odns_blocked']=$bl;}
    }
    // Always-allow domains (e.g. KinderGPT mode)
    $allowDomains=$p['allow_domains']??[];
    $allowResults=[];
    foreach($allowDomains as $d){
        if(!empty($allowResults))usleep(500000);
        $ar2=addAllowDomain($networkId,$d,$jar,$cfUrl);
        $allowResults[]=$d.':'.$ar2;
        $al=$_SESSION['odns_always_allow']??[];
        if(!in_array($d,$al)){$al[]=$d;$_SESSION['odns_always_allow']=$al;}
    }
    // Always re-apply protected domains regardless of profile
    foreach($GLOBALS['protectedAllowDomains'] as $pd){
        if(in_array($pd,$allowDomains))continue;
        if(!empty($allowResults))usleep(400000);
        addAllowDomain($networkId,$pd,$jar,$cfUrl);
        $al=$_SESSION['odns_always_allow']??[];
        if(!in_array($pd,$al)){$al[]=$pd;$_SESSION['odns_always_allow']=$al;}
    }
    @unlink($jar);
    $domDiag=empty($domResults)?'':' ['.implode(', ',$domResults).']';
    $allowDiag=empty($allowResults)?'':' [allow:'.implode(', ',$allowResults).']';
    $allowCount=count($allowDomains);
    if($catCount===0&&$domCount===0&&$allowCount===0)
        return['ok'=>true,'message'=>'✓ '.$p['label'].' applied — no blocks set'];
    $detail=($catCount?' '.$catCount.' cat'.($catCount===1?'':'s'):'')
        .($domCount?' + '.$domCount.' blocked':'').($allowCount?' + '.$allowCount.' always-allowed':'');
    return['ok'=>true,'message'=>'✓ '.$p['label'].' ('.$detail.')'.$domDiag.$allowDiag];
}
function addAllowDomain($networkId,$domain,$jar,$cfUrl) {
    $dr1=curlPost('https://dashboard.opendns.com/dashboard_ajax.php',
        ['step1'=>'true','allowed_domain'=>$domain,'action'=>'add_allowed_domain','n'=>$networkId],$jar,$cfUrl);
    $d1Body=json_decode($dr1['body']??'',true);
    if($d1Body['success']??false)return'ok';
    if(array_key_exists('has_categories',$d1Body??[])){
        usleep(300000);
        $dr2=curlPost('https://dashboard.opendns.com/dashboard_ajax.php',
            ['step2'=>'true','allowed_domain'=>$domain,'action'=>'add_allowed_domain','n'=>$networkId],$jar,$cfUrl);
        $d2Body=json_decode($dr2['body']??'',true);
        return($d2Body['success']??false)?'ok':'FAIL2:'.$dr2['code'].':'.substr($d2Body['message']??($dr2['body']??''),0,20);
    }
    return'FAIL:'.$dr1['code'].':'.substr($d1Body['message']??($dr1['body']??''),0,20);
}
function toggleDomain($networkId,$domain,$block,$cookies) {
    $jar=tempnam(sys_get_temp_dir(),'odns_');
    file_put_contents($jar,$cookies);
    $cfUrl="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
    if($block){
        curlPost('https://dashboard.opendns.com/dashboard_ajax.php',
            ['step1'=>'true','blocked_domain'=>$domain,'action'=>'add_blocked_domain','n'=>$networkId],$jar,$cfUrl);
    }
    @unlink($jar);
}
function resetAll($networkId,$builtins,$catIds,$cookies) {
    $jar=tempnam(sys_get_temp_dir(),'odns_');
    file_put_contents($jar,$cookies);
    $cfUrl="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
    // Fetch the settings page to discover always-blocked domains
    $pg=curlGet($cfUrl,$jar);
    // Parse: OpenDNS uses <label for="NUM_ID">domain.com</label> + checkbox value=NUM_ID
    // Deletion requires action=delete_blocked_domains with bdomain_id[ID]=ID (confirmed from browser)
    preg_match_all('/<label\s+for=["\'](\d+)["\']>\s*([a-zA-Z0-9][a-zA-Z0-9.\-]*\.[a-zA-Z]{2,})\s*<\/label>/i',$pg['body'],$m);
    $domIds=[];
    for($i=0;$i<count($m[1]);$i++) $domIds[$m[1][$i]]=$m[2][$i]; // [numeric_id => domain_name]
    // Apply open profile (clear all categories)
    $post=['n'=>$networkId,'action'=>'save_blocking_categories',
           'return'=>"/settings/{$networkId}/content_filtering",'save-categories'=>'Apply','applytoall'=>''];
    curlPost('https://dashboard.opendns.com/dashboard_ajax.php',$post,$jar,$cfUrl);
    // Also parse always-allowed domain IDs from page
    preg_match_all('/class=["\'][^"\']*allowed-domain[^"\']*["\'][^>]*value=["\'](\d+)["\']/',$pg['body'],$ad1);
    preg_match_all('/value=["\'](\d+)["\'][^>]*class=["\'][^"\']*allowed-domain[^"\']*["\']/',$pg['body'],$ad2);
    $allowIds=array_unique(array_merge($ad1[1]??[],$ad2[1]??[]));
    // Batch-delete all always-blocked domains in one request using confirmed API format
    $delMsg='no blocked domains found';
    $n=count($domIds);
    if($n>0){
        $delPost=['n'=>$networkId,'applytoall'=>'true','action'=>'delete_blocked_domains'];
        foreach($domIds as $domId=>$d) $delPost["bdomain_id[$domId]"]=$domId;
        $dr=curlPost('https://dashboard.opendns.com/dashboard_ajax.php',$delPost,$jar,$cfUrl);
        $drBody=json_decode($dr['body']??'',true);
        $delOk=($drBody['success']??false)?'ok':'FAIL:'.$dr['code'].':'.substr($drBody['message']??($dr['body']??''),0,40);
        $delMsg=implode(', ',array_values($domIds)).' → '.$delOk;
    }
    // Batch-delete always-allowed domains (excluding protected ones)
    $unprotectedAllowIds=[];
    if(!empty($allowIds)){
        // Rebuild label map for allowed domains to find which IDs are protected
        $protectedIds=[];
        foreach($allowIds as $aId){
            $domName=$labelMap[$aId]??'';
            if(in_array($domName,$GLOBALS['protectedAllowDomains']))$protectedIds[]=$aId;
            else $unprotectedAllowIds[]=$aId;
        }
        if(!empty($unprotectedAllowIds)){
            $delAllowPost=['n'=>$networkId,'applytoall'=>'true','action'=>'delete_allowed_domains'];
            foreach($unprotectedAllowIds as $aId)$delAllowPost["adomain_id[$aId]"]=$aId;
            curlPost('https://dashboard.opendns.com/dashboard_ajax.php',$delAllowPost,$jar,$cfUrl);
        }
    }
    // Re-apply protected domains to ensure they survive the reset
    foreach($GLOBALS['protectedAllowDomains'] as $pd){
        usleep(300000);
        addAllowDomain($networkId,$pd,$jar,$cfUrl);
        $pal=$_SESSION['odns_always_allow']??[];
        if(!in_array($pd,$pal)){$pal[]=$pd;$_SESSION['odns_always_allow']=$pal;}
    }
    @unlink($jar);
    $_SESSION['odns_blocked']=[];
    $protectedStr=' · '.implode(', ',$GLOBALS['protectedAllowDomains']).' kept on always-allow';
    return['ok'=>true,'message'=>'✓ Reset complete — categories cleared, '.$delMsg.(count($unprotectedAllowIds)?', '.count($unprotectedAllowIds).' always-allowed removed':'').$protectedStr];
}

function freshLogin() {
    $e=$_SESSION['odns_email']??'';$p=$_SESSION['odns_pass']??'';
    if(!$e||!$p)return null;
    $r=odnsLogin($e,$p);
    if(!$r['ok'])return null;
    $_SESSION['odns_cookies']=$r['cookies'];
    return $r['cookies'];
}

function getStatus($networkId,$cookies) {
    $jar=tempnam(sys_get_temp_dir(),'odns_');
    file_put_contents($jar,$cookies);
    $cfUrl="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
    $pg=curlGet($cfUrl,$jar);@unlink($jar);
    $body=$pg['body']??'';
    if(strpos($pg['url']??'','login.opendns.com')!==false)return['ok'=>false,'message'=>'Session expired'];
    // Filtering level — parse each radio input individually to handle multi-line HTML
    $level='Custom'; // default to Custom when categories are set; only override if a named level found
    preg_match_all('/<input[^>]*type=["\']radio["\'][^>]*>/is',$body,$radioInputs);
    foreach($radioInputs[0]??[] as $ri){
        if(!preg_match('/\bchecked\b/i',$ri))continue;
        foreach(['none','low','moderate','high','custom'] as $lv){
            if(preg_match('/value=["\']'.$lv.'["\']|\bvalue='.$lv.'\b/i',$ri)){$level=ucfirst($lv);break 2;}
        }
    }
    // Fallback: search for checked radio with level value anywhere in proximity
    if($level==='Custom'){
        foreach(['none','low','moderate','high'] as $lv){
            if(preg_match('/value=["\']?'.$lv.'["\']?[^>]{0,60}checked|checked[^>]{0,60}value=["\']?'.$lv.'["\']?/is',$body))
                {$level=ucfirst($lv);break;}
        }
    }
    // Blocked categories — parse each checkbox individually
    // German Youth Protection is automatically forced checked by OpenDNS on some accounts regardless of user settings — exclude it
    $autoForcedCats=['German Youth Protection'];
    $catMap=array_flip($GLOBALS['catIds']);
    $blockedCats=[];
    preg_match_all('/<input[^>]*name=["\']?dt_category\[(\d+)\]["\']?[^>]*>/i',$body,$catInputs);
    foreach($catInputs[0]??[] as $i=>$inp){
        if(preg_match('/\bchecked\b/i',$inp)){
            $cid=(int)($catInputs[1][$i]??0);
            if(isset($catMap[$cid])&&!in_array($catMap[$cid],$autoForcedCats))$blockedCats[]=$catMap[$cid];
        }
    }
    // Domain labels: all <label for="ID">domain</label> entries
    preg_match_all('/<label\s+for=["\'](\d+)["\']>\s*([a-zA-Z0-9][a-zA-Z0-9.\-]*\.[a-zA-Z]{2,})\s*<\/label>/i',$body,$lms);
    $labelMap=[];for($i=0;$i<count($lms[1]);$i++)$labelMap[$lms[1][$i]]=$lms[2][$i];
    // Blocked domains (class="...blocked-domain...")
    preg_match_all('/class=["\'][^"\']*blocked-domain[^"\']*["\'][^>]*value=["\'](\d+)["\']/',$body,$bd1);
    preg_match_all('/value=["\'](\d+)["\'][^>]*class=["\'][^"\']*blocked-domain[^"\']*["\']/',$body,$bd2);
    $bIds=array_unique(array_merge($bd1[1]??[],$bd2[1]??[]));
    $alwaysBlocked=array_values(array_filter(array_map(fn($id)=>$labelMap[$id]??null,$bIds)));
    // Allowed domains (class="...allowed-domain...")
    preg_match_all('/class=["\'][^"\']*allowed-domain[^"\']*["\'][^>]*value=["\'](\d+)["\']/',$body,$ad1);
    preg_match_all('/value=["\'](\d+)["\'][^>]*class=["\'][^"\']*allowed-domain[^"\']*["\']/',$body,$ad2);
    $aIds=array_unique(array_merge($ad1[1]??[],$ad2[1]??[]));
    $alwaysAllowed=array_values(array_filter(array_map(fn($id)=>$labelMap[$id]??null,$aIds)));
    return['ok'=>true,'level'=>$level,'blocked_cats'=>$blockedCats,'blocked_cats_count'=>count($blockedCats),
           'always_blocked'=>$alwaysBlocked,'always_allowed'=>$alwaysAllowed];
}

// ── Schedule file helpers ─────────────────────────────────────────────────────
function loadScheds() {
    if(!file_exists(SCHED_FILE))return[];
    $d=json_decode(file_get_contents(SCHED_FILE),true);
    return is_array($d)?$d:[];
}
function saveScheds($d){file_put_contents(SCHED_FILE,json_encode($d,JSON_PRETTY_PRINT));}
function uHash($email){return substr(md5(strtolower(trim($email))),0,16);}
function activeSchedule($schedules) {
    $day=(int)date('w');$now=date('H:i');$found=null;
    foreach($schedules as $s){
        if(in_array($day,$s['days'])&&$now>=$s['start']&&$now<$s['end'])$found=$s;
    }
    return $found;
}

// ── Cron endpoint — call every minute via server cron ────────────────────────
//    Add to cPanel: * * * * * curl -s "https://yoursite.com/opendns-remote.php?cron=1&key=ODNS2026CRON"
if(isset($_GET['cron'])&&($_GET['key']??'')===CRON_KEY){
    header('Content-Type: application/json');
    $all=loadScheds();$out=[];
    foreach($all as $hash=>$u){
        $scheds=$u['schedules']??[];
        $active=activeSchedule($scheds);
        $target=$active?$active['profile']:'open';
        if($target===($u['last_applied']??null)){$out[$hash]=['switched'=>false,'current'=>$target];continue;}
        $lr=odnsLogin($u['email'],$u['password']);
        if(!$lr['ok']){$out[$hash]=['error'=>'Login failed'];continue;}
        $r=applyProfile($u['network_id'],$target,$GLOBALS['profiles'],$GLOBALS['catIds'],$lr['cookies']);
        if($r['ok'])$all[$hash]['last_applied']=$target;
        $out[$hash]=['switched'=>true,'to'=>$target,'result'=>$r];
    }
    saveScheds($all);
    echo json_encode($out);exit;
}

// ── Download ──────────────────────────────────────────────────────────────────
if(isset($_GET['download'])){
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="opendns-remote.php"');
    readfile(__FILE__);exit;
}

// ── POST API ──────────────────────────────────────────────────────────────────
if($_SERVER['REQUEST_METHOD']==='POST'){
    header('Content-Type: application/json');
    $in=json_decode(file_get_contents('php://input'),true)??[];
    $action=$in['action']??'';

    // Login
    if($action==='login'){
        $email=trim($in['email']??'');$pass=trim($in['password']??'');
        if(!$email||!$pass){echo json_encode(['ok'=>false,'error'=>'Enter email and password.']);exit;}
        $r=odnsLogin($email,$pass);
        if($r['ok']){
            $_SESSION['odns_cookies']=$r['cookies'];
            $_SESSION['odns_networks']=$r['networks'];
            $_SESSION['odns_email']=$email;
            $_SESSION['odns_pass']=$pass;      // stored server-side only, used for schedule cron
            $_SESSION['odns_blocked']=$_SESSION['odns_blocked']??[];
        }
        unset($r['cookies']);echo json_encode($r);exit;
    }

    if($action==='logout'){session_destroy();echo json_encode(['ok'=>true]);exit;}

    if($action==='check_dns'){
        $primary='208.67.222.222';$secondary='208.67.220.220';
        // Step 1: try TCP port 53 (works on VPS/dedicated; often blocked on shared hosting)
        $t1=@stream_socket_client("tcp://{$primary}:53",$e1,$es1,2);
        $ok1=(bool)$t1;if($t1)fclose($t1);
        $t2=@stream_socket_client("tcp://{$secondary}:53",$e2,$es2,2);
        $ok2=(bool)$t2;if($t2)fclose($t2);
        $method='tcp';
        // Step 2: if TCP blocked (common on shared hosting), fall back to DNS hostname resolution.
        // resolver1/2.opendns.com are authoritative names that ONLY resolve correctly if
        // OpenDNS's own infrastructure is responding — a reliable proxy test.
        if(!$ok1){$r1=@gethostbyname('resolver1.opendns.com');$ok1=($r1===$primary);$method='dns';}
        if(!$ok2){$r2=@gethostbyname('resolver2.opendns.com');$ok2=($r2===$secondary);$method='dns';}
        $fwd=trim(explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']??'')[0]);
        $userIp=$fwd?:($_SERVER['REMOTE_ADDR']??'');
        echo json_encode(['ok'=>true,
            'primary'  =>['ip'=>$primary,  'reachable'=>$ok1,'label'=>'Primary'],
            'secondary'=>['ip'=>$secondary,'reachable'=>$ok2,'label'=>'Secondary'],
            'your_ip'  =>trim($userIp),
            'method'   =>$method
        ]);exit;
    }

    if(empty($_SESSION['odns_cookies'])){
        echo json_encode(['ok'=>false,'message'=>'Session expired — please log in again.']);exit;
    }
    $cookies=$_SESSION['odns_cookies'];
    $networkId=$in['network']??'';

    switch($action){

        case 'check_connection':
            $fresh=freshLogin();
            if(!$fresh){echo json_encode(['ok'=>false,'message'=>'⚠️ Re-login failed — check your credentials or sign out and sign in again.']);break;}
            echo json_encode(['ok'=>true,'message'=>'✓ OpenDNS login successful — network ID: '.$networkId]);
            break;

        case 'switch':
            $fresh=freshLogin();
            if($fresh)$cookies=$fresh;
            elseif(!$cookies){echo json_encode(['ok'=>false,'message'=>'Not logged in.']);break;}
            $r=applyProfile($networkId,$in['profile']??'',$profiles,$catIds,$cookies);
            echo json_encode($r);break;

        case 'reset_all':
            $fresh=freshLogin();
            if($fresh)$cookies=$fresh;
            $r=resetAll($networkId,$profiles,$catIds,$cookies);
            echo json_encode($r);break;

        case 'get_status':
            $fresh=freshLogin();if($fresh)$cookies=$fresh;
            echo json_encode(getStatus($networkId,$cookies));break;

        case 'add_always_allow':
            $d=strtolower(preg_replace('/[^a-zA-Z0-9.\-]/','', $in['domain']??''));
            if(!$d){echo json_encode(['ok'=>false,'message'=>'No domain provided']);break;}
            $fresh=freshLogin();if($fresh)$cookies=$fresh;
            $jar2=tempnam(sys_get_temp_dir(),'odns_');file_put_contents($jar2,$cookies);
            $cfUrl2="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
            $ar2=addAllowDomain($networkId,$d,$jar2,$cfUrl2);@unlink($jar2);
            $al=$_SESSION['odns_always_allow']??[];
            if(!in_array($d,$al)){$al[]=$d;$_SESSION['odns_always_allow']=$al;}
            echo json_encode(['ok'=>$ar2==='ok','message'=>$ar2==='ok'?"✓ $d added to Always Authorised":"Failed to allow $d: $ar2"]);break;

        case 'apply_protected_domains':
            $fresh=freshLogin();if($fresh)$cookies=$fresh;
            $jar4=tempnam(sys_get_temp_dir(),'odns_');file_put_contents($jar4,$cookies);
            $cfUrl4="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
            $presults=[];
            foreach($protectedAllowDomains as $pd){
                if(!empty($presults))usleep(400000);
                $pr=addAllowDomain($networkId,$pd,$jar4,$cfUrl4);
                $presults[]=$pd.':'.$pr;
                $pal=$_SESSION['odns_always_allow']??[];
                if(!in_array($pd,$pal)){$pal[]=$pd;$_SESSION['odns_always_allow']=$pal;}
            }
            @unlink($jar4);
            echo json_encode(['ok'=>true,'message'=>'✓ Protected domains applied: '.implode(', ',$protectedAllowDomains)]);break;

        case 'remove_always_allow':
            $d=strtolower(preg_replace('/[^a-zA-Z0-9.\-]/','', $in['domain']??''));
            if(!$d){echo json_encode(['ok'=>false,'message'=>'No domain provided']);break;}
            if(in_array($d,$protectedAllowDomains)){
                echo json_encode(['ok'=>false,'message'=>"⛔ $d is a protected domain and cannot be removed."]);break;
            }
            $fresh=freshLogin();if($fresh)$cookies=$fresh;
            $jar3=tempnam(sys_get_temp_dir(),'odns_');file_put_contents($jar3,$cookies);
            $cfUrl3="https://dashboard.opendns.com/settings/{$networkId}/content_filtering";
            $pg3=curlGet($cfUrl3,$jar3);$b3=$pg3['body']??'';
            preg_match_all('/<label\s+for=["\'](\d+)["\']>\s*'.preg_quote($d,'/').'["\']?\s*<\/label>/i',$b3,$dIdsM);
            if(!empty($dIdsM[1])){
                $dId=$dIdsM[1][0];
                curlPost('https://dashboard.opendns.com/dashboard_ajax.php',
                    ['n'=>$networkId,'applytoall'=>'true','action'=>'delete_allowed_domains',"adomain_id[$dId]"=>$dId],$jar3,$cfUrl3);
            }
            @unlink($jar3);
            $_SESSION['odns_always_allow']=array_values(array_filter($_SESSION['odns_always_allow']??[],fn($x)=>$x!==$d));
            echo json_encode(['ok'=>true,'message'=>"✓ $d removed from Always Authorised"]);break;

        case 'block_domain':
            $d=strtolower(preg_replace('/[^a-zA-Z0-9.\-]/','', $in['domain']??''));
            if(!$d){echo json_encode(['ok'=>false,'message'=>'No domain provided']);break;}
            $fresh=freshLogin();if($fresh)$cookies=$fresh;
            toggleDomain($networkId,$d,true,$cookies);
            $bl=$_SESSION['odns_blocked']??[];
            if(!in_array($d,$bl)){$bl[]=$d;$_SESSION['odns_blocked']=$bl;}
            echo json_encode(['ok'=>true,'message'=>"✓ $d blocked"]);break;

        case 'unblock_domain':
            $d=strtolower(preg_replace('/[^a-zA-Z0-9.\-]/','', $in['domain']??''));
            $fresh=freshLogin();if($fresh)$cookies=$fresh;
            toggleDomain($networkId,$d,false,$cookies);
            $_SESSION['odns_blocked']=array_values(array_filter($_SESSION['odns_blocked']??[],fn($x)=>$x!==$d));
            echo json_encode(['ok'=>true,'message'=>"✓ $d unblocked"]);break;

        case 'save_profile':
            $p=$in['profile']??[];
            $editId=trim($p['id']??'');
            $label=trim($p['label']??'Custom');
            $id=$editId?:preg_replace('/[^a-z0-9_]/','_',strtolower($label));
            if(!$editId&&isset($profiles[$id]))$id.='_custom';
            $_SESSION['odns_custom'][$id]=[
                'label'=>htmlspecialchars($label),
                'desc' =>htmlspecialchars(trim($p['desc']??'')),
                'icon' =>mb_substr(trim($p['icon']??'⚙️'),0,4),
                'color'=>preg_replace('/[^#a-fA-F0-9]/','', $p['color']??'#64748b'),
                'cats' =>array_values(array_filter(array_map('strval',$p['cats']??[]))),
                'domains'=>array_values(array_filter(array_map(fn($x)=>strtolower(preg_replace('/[^a-zA-Z0-9.\-]/','',trim($x))),$p['domains']??[]))),
                'custom'=>true,
            ];
            echo json_encode(['ok'=>true,'message'=>'✓ Profile saved','id'=>$id]);break;

        case 'delete_profile':
            $id=$in['id']??'';
            unset($_SESSION['odns_custom'][$id]);
            echo json_encode(['ok'=>true,'message'=>'✓ Profile deleted']);break;

        // ── Schedule actions ──
        case 'save_schedule':
            if(SCHEDULE_OWNER!==''&&strtolower($_SESSION['odns_email']??'')!==strtolower(SCHEDULE_OWNER)){
                echo json_encode(['ok'=>false,'message'=>'Server scheduling on this site is restricted to the site owner. Download the tool from the Advanced tab and run it on your own server to use scheduling.']);break;
            }
            $s=$in['schedule']??[];
            $email=$_SESSION['odns_email']??'';
            if(!$email){echo json_encode(['ok'=>false,'message'=>'Not logged in']);break;}
            $hash=uHash($email);
            $data=loadScheds();
            if(!isset($data[$hash])){
                $data[$hash]=['email'=>$email,'password'=>$_SESSION['odns_pass']??'',
                              'network_id'=>$networkId,'schedules'=>[],'last_applied'=>null];
            } else {
                // Update credentials and network in case they changed
                $data[$hash]['network_id']=$networkId;
                if(!empty($_SESSION['odns_pass']))$data[$hash]['password']=$_SESSION['odns_pass'];
            }
            $entry=['id'=>'s'.time().rand(10,99),
                    'profile'=>trim($s['profile']??'open'),
                    'days'=>array_map('intval',$s['days']??[]),
                    'start'=>preg_replace('/[^0-9:]/','', $s['start']??'00:00'),
                    'end'=>preg_replace('/[^0-9:]/','', $s['end']??'23:59')];
            $data[$hash]['schedules'][]=$entry;
            saveScheds($data);
            echo json_encode(['ok'=>true,'message'=>'✓ Schedule saved','id'=>$entry['id']]);break;

        case 'delete_schedule':
            $sid=$in['id']??'';
            $hash=uHash($_SESSION['odns_email']??'');
            $data=loadScheds();
            if(isset($data[$hash])){
                $data[$hash]['schedules']=array_values(array_filter($data[$hash]['schedules']??[],fn($s)=>$s['id']!==$sid));
                saveScheds($data);
            }
            echo json_encode(['ok'=>true,'message'=>'✓ Schedule removed']);break;

        case 'get_schedules':
            $hash=uHash($_SESSION['odns_email']??'');
            $data=loadScheds();
            $u=$data[$hash]??[];
            echo json_encode(['ok'=>true,'schedules'=>$u['schedules']??[],'network_id'=>$u['network_id']??'']);break;

        case 'clear_schedules':
            $hash=uHash($_SESSION['odns_email']??'');
            $data=loadScheds();
            if(isset($data[$hash]))$data[$hash]['schedules']=[];
            saveScheds($data);
            echo json_encode(['ok'=>true,'message'=>'✓ All schedules cleared']);break;

        default:
            echo json_encode(['ok'=>false,'error'=>'Unknown action']);
    }
    exit;
}

// ── State for JS ──────────────────────────────────────────────────────────────
$isLoggedIn   = !empty($_SESSION['odns_cookies']);
$networks     = $isLoggedIn ? json_encode($_SESSION['odns_networks']) : 'null';
$protectedAllowJson = json_encode($protectedAllowDomains);
// Only built-ins in profilesJson — custom profiles are in customJson and added by JS
$profileList  = [];
foreach($profiles as $k=>$p) $profileList[]=array_merge(['id'=>$k,'custom'=>false],$p);
$profilesJson = json_encode($profileList);
$allCatsJson  = json_encode(array_keys($catIds));
$customJson   = json_encode($_SESSION['odns_custom']??[]);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>OpenDNS Home Remote Control</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0a0a0f;--surf:#13131a;--surf2:#1c1c28;--bord:#252535;--text:#e2e8f0;--muted:#475569;--acc:#3b82f6}
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;min-height:100vh;line-height:1.5}

.tabs{display:flex;background:var(--surf);border-bottom:1px solid var(--bord);position:sticky;top:0;z-index:10}
.tab{flex:1;padding:.65rem .1rem;text-align:center;font-size:.63rem;font-weight:700;color:var(--muted);cursor:pointer;border-bottom:3px solid transparent;transition:color .15s,border-color .15s;user-select:none}
.tab.active{color:var(--acc);border-bottom-color:var(--acc)}

.page{display:none;max-width:680px;margin:0 auto;padding:1.4rem 1.2rem 3rem}
.page.active{display:block}

h1{font-size:clamp(1.3rem,4vw,1.9rem);font-weight:800;margin-bottom:.4rem}
h2{font-size:1rem;font-weight:700;margin:1.5rem 0 .5rem}
h3{font-size:.88rem;font-weight:700;color:var(--acc);margin:1rem 0 .3rem}
p{color:#94a3b8;font-size:.87rem;margin-bottom:.65rem}
a{color:var(--acc)}
code{background:var(--surf2);padding:.1rem .35rem;border-radius:4px;font-size:.8rem;color:#93c5fd;font-family:monospace}

.step{background:var(--surf);border:1px solid var(--bord);border-left:4px solid var(--acc);border-radius:12px;padding:.95rem 1.15rem;margin-bottom:.8rem}
.step__num{font-size:.6rem;font-weight:800;text-transform:uppercase;letter-spacing:.12em;color:var(--acc);margin-bottom:.2rem}
.step__title{font-size:.92rem;font-weight:700;margin-bottom:.3rem}
.step__body{font-size:.82rem;color:#94a3b8;line-height:1.6}
.dns-box{background:#0f1f10;border:1px solid #1a3a1a;border-radius:10px;padding:.85rem 1rem;margin:.6rem 0;font-family:monospace;font-size:.92rem;color:#4ade80;display:flex;flex-direction:column;gap:.3rem;overflow-x:auto}
.dns-box span{display:flex;justify-content:space-between;align-items:center;min-width:0}
.dns-label{font-size:.65rem;color:#475569}
.callout{background:#0c1a2e;border:1px solid #1e3a5f;border-radius:10px;padding:.75rem .95rem;font-size:.8rem;color:#7dd3fc;margin:.6rem 0;line-height:1.5}
.callout strong{color:#93c5fd}
.router-table{width:100%;border-collapse:collapse;font-size:.78rem;margin:.6rem 0}
.router-table th{background:var(--surf2);padding:.5rem .65rem;text-align:left;font-size:.65rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);border-bottom:1px solid var(--bord)}
.router-table td{padding:.5rem .65rem;border-bottom:1px solid var(--bord);color:#94a3b8;vertical-align:top}
.router-table tr:last-child td{border-bottom:none}
.mono{font-family:monospace;color:#e2e8f0}

/* Login */
.login-card{background:var(--surf);border:1px solid var(--bord);border-radius:16px;padding:1.8rem 1.4rem;max-width:400px;margin:0 auto}
.login-logo{text-align:center;font-size:2.8rem;margin-bottom:.45rem}
.login-title{text-align:center;font-size:1.1rem;font-weight:800;margin-bottom:.2rem}
.login-sub{text-align:center;font-size:.78rem;color:var(--muted);margin-bottom:1.3rem}
.field{margin-bottom:.85rem}
.field label{display:block;font-size:.7rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.28rem}
.field input{width:100%;background:var(--surf2);border:1px solid var(--bord);border-radius:10px;padding:.65rem .9rem;font-size:.92rem;color:var(--text);outline:none;transition:border-color .15s;font-family:inherit}
.field input:focus{border-color:var(--acc)}
.login-error{background:#1f0a0a;border:1px solid #7f1d1d;border-radius:8px;padding:.55rem .8rem;font-size:.8rem;color:#f87171;margin-bottom:.85rem;display:none}

/* Shared buttons */
.btn-primary{width:100%;background:var(--acc);color:#fff;border:none;border-radius:10px;padding:.75rem;font-size:.9rem;font-weight:700;cursor:pointer;font-family:inherit;transition:opacity .15s;margin-top:.35rem}
.btn-primary:hover{opacity:.88}
.btn-primary:disabled{opacity:.5;cursor:not-allowed}
.btn-reset{width:100%;background:#450a0a;color:#fca5a5;border:none;border-radius:12px;padding:.75rem;font-size:.85rem;font-weight:800;cursor:pointer;font-family:inherit;transition:background .15s;margin-top:.7rem}
.btn-reset:hover{background:#7f1d1d}
.btn-full{width:100%;padding:.7rem;border:none;border-radius:11px;font-size:.85rem;font-weight:800;cursor:pointer;font-family:inherit;transition:opacity .15s}
.btn-full.blue{background:#1d4ed8;color:#fff}
.btn-full.green{background:#15803d;color:#fff}
.btn-full.orange{background:#92400e;color:#fcd34d}
.btn-full:active{opacity:.8}
.btn-sm{background:var(--surf2);border:1px solid var(--bord);border-radius:7px;padding:.26rem .6rem;font-size:.7rem;font-weight:700;color:#94a3b8;cursor:pointer;font-family:inherit;transition:background .12s;white-space:nowrap}
.btn-sm:hover{background:#2a2a3a}
.btn-sm.danger{background:#1f0a0a;border-color:#7f1d1d;color:#f87171}
.btn-sm.create{background:#052e16;border-color:#14532d;color:#4ade80}
.logout-btn{background:none;border:1px solid var(--bord);border-radius:8px;padding:.28rem .65rem;font-size:.72rem;color:var(--muted);cursor:pointer;font-family:inherit}
.logout-btn:hover{color:var(--text)}

/* Control */
.ctrl-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:.9rem;flex-wrap:wrap;gap:.4rem}
.ctrl-hdr h2{margin:0}
.net-sel{width:100%;background:var(--surf2);border:1px solid var(--bord);border-radius:10px;padding:.58rem .9rem;font-size:.86rem;color:var(--text);margin-bottom:.9rem;font-family:inherit;outline:none}
.pgrid{display:flex;flex-direction:column;gap:.6rem;margin-bottom:.7rem}
.prow{display:flex;align-items:center;gap:.45rem}
.pbtn{display:flex;align-items:center;gap:.8rem;padding:.85rem 1rem;border:1px solid rgba(255,255,255,.06);border-left:5px solid var(--c,#334155);border-radius:14px;background:rgba(0,0,0,.3);cursor:pointer;font-family:inherit;text-align:left;flex:1;transition:opacity .15s}
.pbtn:hover{opacity:.85}
.pbtn__icon{font-size:1.5rem;flex-shrink:0}
.pbtn__label{font-size:.9rem;font-weight:800;color:#fff}
.pbtn__desc{font-size:.7rem;color:rgba(255,255,255,.38);margin-top:.08rem}
.sbox{background:var(--surf);border:1px solid var(--bord);border-radius:12px;padding:.75rem 1rem;font-size:.83rem;color:var(--muted);text-align:center;min-height:2.6rem;display:flex;align-items:center;justify-content:center;gap:.4rem;transition:color .2s}
.sbox.ok{color:#4ade80}.sbox.error{color:#f87171}.sbox.busy{color:#fbbf24}
.spinner{width:14px;height:14px;border:2px solid #fbbf24;border-top-color:transparent;border-radius:50%;animation:spin .7s linear infinite;flex-shrink:0}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}

/* Profiles tab */
.pcard{background:var(--surf);border:1px solid var(--bord);border-left:4px solid var(--c,#334155);border-radius:14px;padding:.9rem 1rem;margin-bottom:.65rem}
.pcard__top{display:flex;align-items:flex-start;gap:.65rem;margin-bottom:.5rem}
.pcard__icon{font-size:1.5rem;line-height:1;flex-shrink:0;margin-top:.05rem}
.pcard__info{flex:1;min-width:0}
.pcard__label{font-size:.9rem;font-weight:800;margin-bottom:.12rem}
.pcard__desc{font-size:.72rem;color:var(--muted)}
.pcard__btns{display:flex;gap:.35rem;flex-wrap:wrap}
.badge{font-size:.58rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:.08rem .38rem;border-radius:4px;background:#1e293b;color:#475569;margin-left:.3rem;vertical-align:middle}
.badge.custom{background:#0c1a2e;color:#7dd3fc}

/* Form elements */
.flabel{display:block;font-size:.68rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:.28rem}
.finput{width:100%;background:var(--surf2);border:1px solid var(--bord);border-radius:9px;padding:.58rem .82rem;color:var(--text);font-size:.86rem;font-family:inherit;outline:none;transition:border-color .15s;margin-bottom:.8rem}
.finput:focus{border-color:var(--acc)}
select.finput{cursor:pointer}
.frow{display:flex;gap:.4rem;margin-bottom:.8rem}
.frow .finput{margin-bottom:0;flex:1}
.swatches{display:flex;gap:.38rem;flex-wrap:wrap;margin-bottom:.8rem}
.swatch{width:26px;height:26px;border-radius:7px;border:2px solid transparent;cursor:pointer;transition:transform .12s,border-color .12s}
.swatch.on{border-color:#fff;transform:scale(1.2)}
.emoji-sw{width:26px;height:26px;background:var(--surf2);border:none;border-radius:6px;font-size:.9rem;cursor:pointer}
.cat-grid{display:grid;grid-template-columns:1fr 1fr;gap:.15rem;max-height:250px;overflow-y:auto;border:1px solid var(--bord);border-radius:9px;padding:.45rem;margin-bottom:.55rem}
.cat-lbl{display:flex;align-items:center;gap:.32rem;cursor:pointer;padding:.17rem .28rem;border-radius:5px}
.cat-lbl:hover{background:rgba(255,255,255,.04)}
.cat-lbl input{width:12px;height:12px;accent-color:var(--acc);cursor:pointer;flex-shrink:0}
.cat-lbl span{font-size:.66rem;color:#94a3b8;line-height:1.2}
.domain-list{display:flex;flex-direction:column;gap:.28rem;margin-bottom:.55rem}
.dchip{display:flex;align-items:center;justify-content:space-between;background:var(--surf2);border-radius:7px;padding:.28rem .58rem;font-size:.76rem;color:#94a3b8}
.dchip button{background:none;border:none;color:#334155;cursor:pointer;font-size:.82rem;padding:0 .1rem}
.dchip button:hover{color:#f87171}
.form-card{background:var(--surf);border:1px solid var(--bord);border-radius:14px;padding:1.1rem}
.sec-label{font-size:.65rem;font-weight:800;color:#334155;text-transform:uppercase;letter-spacing:.07em;margin:1.1rem 0 .5rem}

/* Schedule tab */
.sched-card{background:var(--surf);border:1px solid var(--bord);border-radius:14px;padding:.9rem 1rem;margin-bottom:.65rem}
.day-picker{display:flex;gap:.3rem;flex-wrap:wrap;margin-bottom:.8rem}
.day-btn{background:var(--surf2);border:1px solid var(--bord);border-radius:8px;width:36px;height:36px;font-size:.7rem;font-weight:700;color:var(--muted);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .12s;font-family:inherit}
.day-btn.on{background:#1e3a8a;border-color:#3b82f6;color:#93c5fd}
.litem{background:var(--surf);border:1px solid var(--bord);border-radius:12px;padding:.7rem .95rem;margin-bottom:.5rem;display:flex;align-items:flex-start;gap:.6rem}
.litem__body{flex:1;min-width:0}
.litem__title{font-size:.83rem;font-weight:700;margin-bottom:.12rem}
.litem__sub{font-size:.7rem;color:var(--muted)}
.litem__del{background:none;border:none;color:#334155;font-size:1rem;cursor:pointer;padding:.1rem;flex-shrink:0;line-height:1;margin-top:.1rem}
.litem__del:hover{color:#f87171}
.info-box{background:#0f172a;border:1px solid #1e293b;border-radius:10px;padding:.65rem .9rem;font-size:.72rem;color:#64748b;line-height:1.6;margin-bottom:.9rem}
.warn-box{background:#1c0f00;border:1px solid #78350f;border-radius:10px;padding:.65rem .9rem;font-size:.72rem;color:#d97706;line-height:1.6;margin-bottom:.9rem}

/* Modal */
#modal-bg{display:none;position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:50;align-items:center;justify-content:center;padding:1.5rem}
#modal-bg.open{display:flex}
.modal{background:#111;border:1px solid var(--bord);border-radius:16px;padding:1.2rem;width:100%;max-width:440px;max-height:85vh;overflow-y:auto}
.modal__title{font-size:1rem;font-weight:800;margin-bottom:.6rem}
.modal__body{font-size:.8rem;color:#94a3b8;line-height:1.6}
.modal__body ul{padding-left:1rem;margin-top:.25rem}
.modal__close{display:block;width:100%;margin-top:.9rem;padding:.5rem;background:var(--surf2);border:none;border-radius:10px;color:#94a3b8;font-size:.8rem;font-weight:700;cursor:pointer;font-family:inherit}

/* Download */
.dl-card{background:linear-gradient(135deg,#0f1f3a,#1a1f3a);border:1px solid #1e3a5f;border-radius:14px;padding:1.4rem;text-align:center;margin-top:1.8rem}
.dl-card h2{color:#93c5fd;margin-top:0}
.dl-btn{display:inline-block;background:#1d4ed8;color:#fff;padding:.65rem 1.7rem;border-radius:10px;text-decoration:none;font-weight:700;font-size:.88rem;margin-top:.6rem;transition:background .15s}
.dl-btn:hover{background:#2563eb}
.privacy-note{font-size:.68rem;color:#334155;text-align:center;margin-top:.85rem;line-height:1.5}

/* Status card */
.status-wrap-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
.status-wrap-title{font-size:.72rem;font-weight:800;color:#7dd3fc;text-transform:uppercase;letter-spacing:.08em}
.status-card{background:#07142a;border:1px solid #1e3a5f;border-radius:12px;padding:.8rem 1rem;margin-bottom:.9rem}
.status-row{display:flex;justify-content:space-between;align-items:flex-start;padding:.38rem 0;border-bottom:1px solid rgba(30,58,95,.55);font-size:.84rem;gap:.6rem}
.status-row:last-child{border-bottom:none}
.status-lbl{color:#7dd3fc;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap;flex-shrink:0;padding-top:.08rem}
.status-val{text-align:right;word-break:break-word;color:#e2e8f0;font-weight:600}
.status-val.green{color:#4ade80}.status-val.amber{color:#fbbf24}.status-val.red{color:#f87171}
.chip-row{display:flex;flex-wrap:wrap;gap:.25rem;justify-content:flex-end;margin-top:.15rem}
.chip{background:#0f2240;border:1px solid #1e3a5f;border-radius:5px;padding:.1rem .42rem;font-size:.65rem;color:#93c5fd;font-weight:600}
/* Allow-domain list */
.allow-chip{display:flex;align-items:center;gap:.5rem;background:#052e16;border:1px solid #14532d;border-radius:8px;padding:.28rem .65rem;font-size:.78rem;color:#4ade80;margin-bottom:.3rem}
.allow-chip button{background:none;border:none;color:#14532d;cursor:pointer;font-size:.82rem;padding:0;line-height:1}
.allow-chip button:hover{color:#f87171}
/* Cache flush banner */
.flush-banner{background:#071a0e;border:1px solid #166534;border-radius:12px;padding:.85rem 1rem .7rem;margin-top:.55rem;position:relative;animation:fadeIn .25s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
.flush-banner__close{position:absolute;top:.45rem;right:.55rem;background:none;border:none;color:#1e3a2e;cursor:pointer;font-size:.95rem;line-height:1;padding:.1rem}
.flush-banner__close:hover{color:#f87171}
.flush-banner__title{font-size:.76rem;font-weight:800;color:#4ade80;margin-bottom:.35rem;padding-right:1.2rem}
.flush-banner__sub{font-size:.7rem;color:#334155;line-height:1.5;margin-bottom:.5rem}
.flush-steps{display:flex;flex-direction:column;gap:.22rem;margin-bottom:.4rem}
.flush-step{display:flex;align-items:flex-start;gap:.45rem;font-size:.69rem;color:#94a3b8;line-height:1.4;background:#040f07;border-radius:7px;padding:.28rem .5rem}
.flush-device{font-weight:800;color:#4ade80;flex-shrink:0;min-width:100px}
.flush-auto{font-size:.66rem;color:#1a3a25;line-height:1.5}
/* DNS status panel */
.dns-panel{background:#07142a;border:1px solid #1e3a5f;border-radius:12px;padding:.75rem 1rem;margin-bottom:.9rem}
.dns-panel-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}
.dns-panel-title{font-size:.68rem;font-weight:800;color:#7dd3fc;text-transform:uppercase;letter-spacing:.08em}
.dns-row{display:flex;align-items:center;gap:.55rem;padding:.26rem 0;font-size:.79rem}
.dns-row+.dns-row{border-top:1px solid rgba(30,58,95,.35)}
.dns-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.dns-dot.on{background:#4ade80;box-shadow:0 0 5px rgba(74,222,128,.5)}
.dns-dot.off{background:#f87171;box-shadow:0 0 4px rgba(248,113,113,.4)}
.dns-dot.neutral{background:#334155}
.dns-ip{font-family:monospace;color:#93c5fd;font-size:.77rem;min-width:108px;letter-spacing:.02em}
.dns-lbl{color:#475569;font-size:.71rem;flex:1}
.dns-state.ok{color:#4ade80;font-size:.72rem;font-weight:700}
.dns-state.fail{color:#f87171;font-size:.72rem;font-weight:700}
.dns-your-ip-row{display:flex;align-items:center;gap:.55rem;padding-top:.38rem;margin-top:.1rem;border-top:1px solid rgba(30,58,95,.35);font-size:.75rem;color:#475569}
.dns-footer-note{font-size:.67rem;color:#1e3a5f;line-height:1.55;margin-top:.38rem;padding-top:.38rem;border-top:1px solid rgba(30,58,95,.25)}
@media(max-width:480px){.page{padding:1rem 1rem 3rem}.login-card{padding:1.4rem 1rem}}
</style>
</head>
<body>

<div class="tabs">
    <div class="tab active" onclick="showTab('guide')">📖 Guide</div>
    <div class="tab" onclick="showTab('control')">🎛 Control</div>
    <div class="tab" onclick="showTab('profiles')">⚙️ Profiles</div>
    <div class="tab" onclick="showTab('schedule')">⏰ Schedule</div>
    <div class="tab" onclick="showTab('advanced')">🛠️ Advanced</div>
    <div class="tab" onclick="showTab('security')">🛡️ Safety</div>
</div>

<!-- ══ GUIDE ══════════════════════════════════════════════════════════════════ -->
<div class="page active" id="tab-guide">
    <h1>🏠 OpenDNS Home Remote Control</h1>
    <p>Control what websites are accessible on your home Wi-Fi — from any phone or computer. Block social media, games, adult content, or everything, and switch back with one tap.</p>
    <div class="callout"><strong>How it works:</strong> This tool uses your free OpenDNS account to filter websites at the DNS level. Every device on your home network is affected automatically — no app needed on individual devices.</div>

    <h2>What you need (all free)</h2>
    <div class="step">
        <div class="step__num">Step 1</div>
        <div class="step__title">Create a free OpenDNS account</div>
        <div class="step__body">Go to <strong>opendns.com</strong> → <em>Get Started Free</em> → <em>OpenDNS Home</em>. Sign up with your email. No credit card required.</div>
    </div>
    <div class="step">
        <div class="step__num">Step 2</div>
        <div class="step__title">Change your router's DNS servers</div>
        <div class="step__body">
            <p>Log in to your router (usually <span class="mono">192.168.1.1</span>) and find DNS Settings. Replace existing DNS numbers with:</p>
            <div class="dns-box">
                <span><span class="dns-label">Primary DNS</span><strong>208.67.222.222</strong></span>
                <span><span class="dns-label">Secondary DNS</span><strong>208.67.220.220</strong></span>
            </div>
            <h3>Where to find DNS settings</h3>
            <table class="router-table">
                <tr><th>Router</th><th>Where to look</th></tr>
                <tr><td><strong>Most routers</strong></td><td><span class="mono">192.168.1.1</span> → WAN / Internet / DNS</td></tr>
                <tr><td><strong>BT / Virgin</strong></td><td><span class="mono">192.168.1.254</span> → Advanced → DNS</td></tr>
                <tr><td><strong>Sky</strong></td><td><span class="mono">192.168.0.1</span> → Security → Advanced → DNS</td></tr>
                <tr><td><strong>ASUS</strong></td><td><span class="mono">192.168.1.1</span> → WAN → Internet Connection → DNS</td></tr>
                <tr><td><strong>TP-Link</strong></td><td><span class="mono">192.168.0.1</span> → Advanced → Network → Internet</td></tr>
                <tr><td><strong>Netgear</strong></td><td><span class="mono">routerlogin.net</span> → Advanced → Setup → Internet</td></tr>
                <tr><td><strong>Swisscom / Sunrise</strong></td><td>Some ISP boxes block DNS changes — connect a second router between the box and your devices</td></tr>
            </table>
        </div>
    </div>
    <div class="step">
        <div class="step__num">Step 3</div>
        <div class="step__title">Register your home network with OpenDNS</div>
        <div class="step__body">
            <p>Log in at <strong>dashboard.opendns.com</strong>. It will detect your IP and ask you to register it. Click <em>Add This Network</em>.</p>
            <div class="callout"><strong>Dynamic IP note:</strong> Download the free <strong>OpenDNS Updater</strong> app on one always-on computer to keep your IP registered automatically.</div>
        </div>
    </div>
    <div class="step">
        <div class="step__num">Step 4</div>
        <div class="step__title">Control your network from here</div>
        <div class="step__body">Click the <strong>Control</strong> tab, log in with your OpenDNS credentials, and tap any profile to switch your network instantly.</div>
    </div>

    <h2>Filtering profiles</h2>
    <?php foreach($profiles as $k=>$p):
        $cats=$p['cats'];
        if($cats==='ALL') $cs='Blocks all '.count($catIds).' categories';
        elseif($cats==='ALL_EXCEPT') $cs='Blocks all categories except: '.implode(', ',$p['cats_except']??[]);
        else $cs=empty($cats)?'No category filtering':'Blocks '.count((array)$cats).' categories: '.implode(', ',(array)$cats);
        $ds=empty($p['domains'])?'':' · Also blocks: '.implode(', ',$p['domains']);
        $as=empty($p['allow_domains']??[])?'':' · Always allows: '.implode(', ',$p['allow_domains']);
    ?>
    <div class="step" style="border-left-color:<?=htmlspecialchars($p['color'])?>">
        <div class="step__title"><?=$p['icon']?> <?=htmlspecialchars($p['label'])?></div>
        <div class="step__body"><?=htmlspecialchars($cs.$ds.$as)?></div>
    </div>
    <?php endforeach;?>

    <div class="callout" style="margin-top:1.2rem"><strong>Privacy:</strong> Your credentials are used only to contact OpenDNS. They are held in your browser session and discarded when you close the tab — never permanently stored on this server unless you set up a schedule (see the Schedule tab for details).</div>

    <div class="callout" style="border-color:#78350f;background:#1c0f00;color:#d97706;margin-top:1.2rem">
        <strong>⚠️ Important:</strong> OpenDNS is a useful first layer of protection but it is not foolproof — a motivated teenager can bypass it. Read the <a onclick="showTab('security')" style="cursor:pointer;color:#fbbf24;text-decoration:underline">🛡️ Safety &amp; Limitations tab</a> to understand what it does and does not protect against, and what additional measures to put in place.
    </div>

    <div style="text-align:center;margin-top:1.3rem"><button class="btn-primary" style="max-width:240px" onclick="showTab('control')">Go to Control Panel →</button></div>
</div>

<!-- ══ SAFETY ═════════════════════════════════════════════════════════════════ -->
<div class="page" id="tab-security">
    <h1>🛡️ Safety &amp; Limitations</h1>
    <div class="callout" style="border-color:#78350f;background:#1c0f00;color:#d97706;margin-bottom:1rem">
        <strong>Honest assessment:</strong> OpenDNS filtering is a useful first layer of protection — but it is not a complete solution on its own. A motivated teenager can bypass it in under a minute. Understanding how it can be defeated helps you decide what additional measures to put in place.
    </div>

    <h2>How it can be bypassed</h2>

    <div class="step" style="border-left-color:#dc2626">
        <div class="step__num" style="color:#dc2626">Easiest bypass — no technical knowledge needed</div>
        <div class="step__title">Switch to mobile data</div>
        <div class="step__body">OpenDNS only filters your home Wi-Fi. Any smartphone on 4G or 5G goes through the phone network instead, bypassing your router entirely. This is the most common workaround and works instantly on any phone with a SIM.</div>
    </div>

    <div class="step" style="border-left-color:#dc2626">
        <div class="step__num" style="color:#dc2626">Easy bypass — 30 seconds</div>
        <div class="step__title">Change DNS on the device</div>
        <div class="step__body">In the Wi-Fi settings on any phone or laptop, you can type in a different DNS address (such as Google's <strong>8.8.8.8</strong> or Cloudflare's <strong>1.1.1.1</strong>). This takes about 30 seconds, leaves no trace, and completely bypasses OpenDNS.</div>
    </div>

    <div class="step" style="border-left-color:#dc2626">
        <div class="step__num" style="color:#dc2626">Easy bypass — free app</div>
        <div class="step__title">Use a VPN</div>
        <div class="step__body">Dozens of free VPN apps are available in the App Store and Google Play. A VPN encrypts all traffic and routes it through its own servers, bypassing DNS filtering completely. Many work with a single tap.</div>
    </div>

    <div class="step" style="border-left-color:#ea580c">
        <div class="step__num" style="color:#ea580c">Easy bypass — browser setting</div>
        <div class="step__title">DNS over HTTPS (DoH)</div>
        <div class="step__body">Firefox uses Cloudflare's encrypted DNS by default, bypassing your router's DNS entirely. Chrome can be configured the same way. This means filtering can be bypassed without any app or VPN — just by using a different browser or changing one setting.</div>
    </div>

    <div class="step" style="border-left-color:#ea580c">
        <div class="step__num" style="color:#ea580c">Easy bypass</div>
        <div class="step__title">Connect to a friend's hotspot</div>
        <div class="step__body">Any device can connect to another phone's mobile hotspot instead of your home Wi-Fi. OpenDNS has no involvement in that connection.</div>
    </div>

    <h2 style="margin-top:1.4rem">What actually works</h2>
    <div class="callout" style="margin-bottom:.7rem">OpenDNS is most effective for younger children who do not yet know about these workarounds. For older children and teenagers, it works best as one layer among several.</div>

    <div class="step" style="border-left-color:#16a34a">
        <div class="step__num" style="color:#16a34a">Strong countermeasure</div>
        <div class="step__title">Screen Time (iPhone) / Digital Wellbeing (Android)</div>
        <div class="step__body">Built into the phone's operating system and protected by a separate passcode. Controls which apps can be installed (blocking VPN apps), limits screen time, and filters content at the device level — works on both Wi-Fi and mobile data. This is the most effective single measure for smartphone use.</div>
    </div>

    <div class="step" style="border-left-color:#16a34a">
        <div class="step__num" style="color:#16a34a">Strong countermeasure</div>
        <div class="step__title">Parental controls on the mobile network account</div>
        <div class="step__body">EE, O2, Vodafone, Three, and most other carriers offer free parental controls that apply at the network level — filtering mobile data as well as Wi-Fi, and blocking adult content even on 4G/5G. Set up through your online account or by calling the carrier.</div>
    </div>

    <div class="step" style="border-left-color:#0891b2">
        <div class="step__num" style="color:#0891b2">Router-level countermeasure</div>
        <div class="step__title">Force all DNS through your router</div>
        <div class="step__body">Some routers can be configured to intercept all DNS traffic on port 53 and redirect it to your chosen DNS — meaning even if a device manually sets a different DNS address, the router overrides it. This blocks the "change DNS on device" bypass. Check your router's firewall or DNS interception settings.</div>
    </div>

    <div class="step" style="border-left-color:#0891b2">
        <div class="step__num" style="color:#0891b2">Router-level countermeasure</div>
        <div class="step__title">Block VPN protocols at the router</div>
        <div class="step__body">Most home routers can block common VPN protocols (OpenVPN, WireGuard, L2TP). Combined with app installation controls through Screen Time, this makes VPN bypass significantly harder.</div>
    </div>

    <div class="callout" style="margin-top:1rem"><strong>The bottom line:</strong> Use OpenDNS for home Wi-Fi filtering, Screen Time or Digital Wellbeing on the phone itself, and your carrier's parental controls for mobile data. All three together provide meaningful protection. Any single layer alone can be bypassed.</div>

    <div style="text-align:center;margin-top:1.3rem"><button class="btn-primary" style="max-width:240px" onclick="showTab('control')">Go to Control Panel →</button></div>
</div>

<!-- ══ CONTROL ════════════════════════════════════════════════════════════════ -->
<div class="page" id="tab-control">
    <div id="login-section" style="<?=$isLoggedIn?'display:none':''?>">
        <div class="login-card">
            <div class="login-logo">🔐</div>
            <div class="login-title">Sign in to OpenDNS</div>
            <div class="login-sub">Use your free OpenDNS account credentials</div>
            <div class="login-error" id="login-error"></div>
            <div class="field"><label>OpenDNS email</label><input type="email" id="login-email" placeholder="you@example.com" autocomplete="username"></div>
            <div class="field"><label>Password</label><input type="password" id="login-pass" placeholder="••••••••" autocomplete="current-password" onkeydown="if(event.key==='Enter')doLogin()"></div>
            <button class="btn-primary" id="login-btn" onclick="doLogin()">Sign In</button>
            <p class="privacy-note">Credentials sent over HTTPS only · never stored unless you set up a schedule</p>
        </div>
        <p style="text-align:center;margin-top:.8rem;font-size:.8rem">No account? <a onclick="showTab('guide')" style="cursor:pointer">Read the guide →</a></p>
    </div>

    <div id="ctrl-section" style="<?=$isLoggedIn?'':'display:none'?>">
        <div class="ctrl-hdr"><h2>Your Network</h2><button class="logout-btn" onclick="doLogout()">Sign out</button></div>
        <select class="net-sel" id="net-sel">
            <?php if($isLoggedIn):foreach($_SESSION['odns_networks'] as $n):?>
            <option value="<?=htmlspecialchars($n['id'])?>"><?=htmlspecialchars($n['name'])?></option>
            <?php endforeach;endif;?>
        </select>

        <div id="status-wrap" style="<?=$isLoggedIn?'':'display:none'?>">
            <div class="status-wrap-hdr">
                <span class="status-wrap-title">📡 Current Network Status</span>
                <button class="btn-sm" onclick="refreshStatus()">↺ Refresh</button>
            </div>
            <div class="status-card" id="status-card">
                <div style="text-align:center;color:#334155;font-size:.8rem;padding:.5rem">Loading…</div>
            </div>
            <div class="dns-panel" id="dns-panel" style="display:none">
                <div class="dns-panel-hdr">
                    <span class="dns-panel-title">🌐 DNS Server Check</span>
                    <span id="dns-overall-badge" style="font-size:.68rem;font-weight:700"></span>
                </div>
                <div id="dns-panel-body">
                    <div style="color:#334155;font-size:.78rem;text-align:center;padding:.3rem">Checking…</div>
                </div>
            </div>
        </div>

        <h2 style="margin-bottom:.5rem">Switch Profile</h2>
        <div class="pgrid" id="ctrl-grid"></div>
        <div class="sbox" id="sbox">Tap a profile to apply it to your network</div>
        <div id="flush-banner" style="display:none">
            <div class="flush-banner">
                <button class="flush-banner__close" onclick="dismissFlushBanner()" title="Dismiss">✕</button>
                <div class="flush-banner__title">⚡ Settings applied — clear DNS cache for instant effect on all devices</div>
                <div class="flush-banner__sub">Devices that already looked up a blocked site may bypass the block for a few minutes until their cache expires. To make it take effect immediately:</div>
                <div class="flush-steps">
                    <div class="flush-step"><span class="flush-device">📱 iPhone / iPad</span><span>Toggle Aeroplane Mode on → wait 5 sec → off. Or: Settings → Wi-Fi → ⓘ → Renew Lease</span></div>
                    <div class="flush-step"><span class="flush-device">🤖 Android</span><span>Settings → Connections → Wi-Fi → toggle off, then on</span></div>
                    <div class="flush-step"><span class="flush-device">💻 Chrome / Edge</span><span>Go to <strong style="color:#7dd3fc">chrome://net-internals/#dns</strong> → Clear host cache</span></div>
                    <div class="flush-step"><span class="flush-device">🦊 Firefox</span><span>about:config → set <strong style="color:#7dd3fc">network.dnsCacheExpiration</strong> to 0, then back to 60</span></div>
                    <div class="flush-step"><span class="flush-device">🍎 Mac Terminal</span><span><strong style="color:#7dd3fc">sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder</strong></span></div>
                    <div class="flush-step"><span class="flush-device">🖥️ Windows CMD</span><span><strong style="color:#7dd3fc">ipconfig /flushdns</strong></span></div>
                    <div class="flush-step"><span class="flush-device">🔄 Router</span><span>Log in at <a href="http://internetbox.home" target="_blank" rel="noopener" style="color:#7dd3fc">internetbox.home</a> (home Wi-Fi only) and reboot — or use the button below</span></div>
                </div>
                <div class="flush-auto">If you do nothing, DNS cache entries expire on their own — typically within <strong style="color:#334155">5 minutes</strong> for most sites including YouTube.</div>
            </div>
        </div>
        <div style="display:flex;justify-content:center;align-items:center;gap:1.4rem;flex-wrap:wrap;margin-top:.35rem">
            <a onclick="checkConn()" style="cursor:pointer;font-size:.72rem;color:#475569">Test OpenDNS connection</a>
            <a href="http://internetbox.home" target="_blank" rel="noopener" style="font-size:.72rem;color:#475569;text-decoration:none" title="Swisscom Internet-Box admin — home Wi-Fi only">🔄 Router Admin <span style="font-size:.6rem;color:#1e3a5f">(home Wi-Fi only)</span></a>
        </div>
        <button class="btn-reset" onclick="doReset()">↺ Reset All — Open Everything &amp; Clear All Blocks</button>
    </div>
</div>

<!-- ══ PROFILES ══════════════════════════════════════════════════════════════ -->
<div class="page" id="tab-profiles">
    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.9rem;flex-wrap:wrap;gap:.4rem">
        <h2 style="margin:0">All Profiles</h2>
        <button class="btn-sm create" onclick="openCreate()">+ Create Profile</button>
    </div>
    <div id="profiles-list"></div>

    <div class="sec-label" style="margin-top:1.5rem">Always Authorised Domains</div>
    <div class="callout" style="margin-bottom:.7rem">These domains are <strong>never blocked</strong>, regardless of which profile is active. Use this for sites that should always be accessible on your network.</div>
    <div id="allow-list"><div style="color:#334155;font-size:.82rem;text-align:center;padding:.8rem">Sign in on the Control tab to manage always-authorised domains</div></div>
    <div class="frow" style="margin-bottom:.4rem;margin-top:.4rem">
        <input type="text" class="finput" id="allow-domain-input" placeholder="e.g. kindergpt.com" autocapitalize="none" autocorrect="off" style="margin:0" onkeydown="if(event.key==='Enter')addAlwaysAllow()">
        <button class="btn-sm create" onclick="addAlwaysAllow()" style="flex-shrink:0">+ Allow</button>
    </div>
    <div id="allow-msg" style="font-size:.76rem;min-height:1.2rem;margin-bottom:.9rem"></div>

    <div id="create-form" style="display:none;margin-top:1.4rem">
        <h2 id="form-title" style="margin-top:0;margin-bottom:.7rem">Create New Profile</h2>
        <div class="form-card">
            <input type="hidden" id="fp-edit-id">
            <label class="flabel">Profile Name</label>
            <input type="text" class="finput" id="fp-name" placeholder="e.g. Wife Mode">
            <label class="flabel">Short Description</label>
            <input type="text" class="finput" id="fp-desc" placeholder="e.g. Blocks Facebook">
            <label class="flabel">Icon</label>
            <div style="display:flex;align-items:center;gap:.65rem;margin-bottom:.8rem">
                <input type="text" id="fp-icon" placeholder="👩" maxlength="4" style="width:56px;text-align:center;font-size:1.3rem;background:var(--surf2);border:1px solid var(--bord);border-radius:9px;padding:.45rem;color:var(--text);outline:none">
                <div class="swatches" style="margin:0">
                    <?php foreach(['👩','👨','👦','👧','👶','🎮','📚','🎬','🌙','💼','🏠','⚙️','🎵','🚗','🐶','🌍'] as $e):?>
                    <button class="emoji-sw" onclick="document.getElementById('fp-icon').value='<?=$e?>'"><?=$e?></button>
                    <?php endforeach;?>
                </div>
            </div>
            <label class="flabel">Colour</label>
            <div class="swatches" id="fp-colors">
                <?php foreach(['#dc2626'=>'Red','#ea580c'=>'Orange','#ca8a04'=>'Amber','#16a34a'=>'Green','#0891b2'=>'Cyan','#1d4ed8'=>'Blue','#7c3aed'=>'Purple','#db2777'=>'Pink','#64748b'=>'Grey'] as $hex=>$name):?>
                <div class="swatch" style="background:<?=$hex?>" data-c="<?=$hex?>" title="<?=$name?>" onclick="pickColor(this)"></div>
                <?php endforeach;?>
            </div>
            <input type="hidden" id="fp-color" value="#7c3aed">
            <label class="flabel">Block These Categories</label>
            <div class="cat-grid" id="fp-cats">
                <?php foreach(array_keys($catIds) as $cat):?>
                <label class="cat-lbl"><input type="checkbox" value="<?=htmlspecialchars($cat)?>"><span><?=htmlspecialchars($cat)?></span></label>
                <?php endforeach;?>
            </div>
            <div style="display:flex;gap:.35rem;margin-bottom:.8rem">
                <button class="btn-sm" onclick="setAllCats(true)">Select All</button>
                <button class="btn-sm" onclick="setAllCats(false)">Clear All</button>
            </div>
            <label class="flabel">Block Specific Domains</label>
            <div class="domain-list" id="fp-domain-list"></div>
            <div class="frow" style="margin-bottom:.8rem">
                <input type="text" class="finput" id="fp-domain-input" placeholder="e.g. facebook.com" autocapitalize="none" autocorrect="off" style="margin:0">
                <button class="btn-sm" onclick="addFormDomain()" style="flex-shrink:0">+ Add</button>
            </div>
            <div style="display:flex;gap:.45rem">
                <button class="btn-full green" onclick="saveProfile()">Save Profile</button>
                <button class="btn-sm" onclick="closeCreate()" style="flex-shrink:0;align-self:stretch">Cancel</button>
            </div>
        </div>
    </div>
</div>

<!-- ══ SCHEDULE ═══════════════════════════════════════════════════════════════ -->
<div class="page" id="tab-schedule">

    <div id="sched-login-msg" style="display:none">
        <div class="callout">Please <a onclick="showTab('control')" style="cursor:pointer">sign in on the Control tab</a> before setting up schedules.</div>
    </div>

    <div id="sched-main">

        <!-- ── Browser Schedules ───────────────────────────────────────────── -->
        <div class="sec-label" style="margin-top:0">Browser Schedules <span class="badge">tab must be open</span></div>
        <div class="callout" style="margin-bottom:.7rem"><strong>No server needed.</strong> These rules are stored on this device only — nothing is sent to any server. The tab must be open and you must be signed in for them to fire. When a scheduled window ends, the network automatically switches back to Everything Open.</div>
        <div id="browser-scheduler-bar" style="display:none;background:#052e16;border:1px solid #14532d;border-radius:8px;padding:.38rem .75rem;font-size:.72rem;color:#4ade80;margin-bottom:.65rem;display:flex;align-items:center;gap:.5rem"><span style="width:7px;height:7px;background:#4ade80;border-radius:50%;animation:pulse 1.8s ease-in-out infinite;flex-shrink:0"></span> Browser scheduler active — checking every minute</div>
        <div id="browser-scheds-list"></div>

        <div class="sec-label">Add Browser Rule</div>
        <div class="sched-card">
            <label class="flabel">Profile to activate</label>
            <select class="finput" id="bc-profile" style="margin-bottom:.8rem"></select>
            <label class="flabel">Days of the week</label>
            <div class="day-picker" id="bc-days">
                <button class="day-btn" data-d="1" onclick="toggleDay(this)">Mo</button>
                <button class="day-btn" data-d="2" onclick="toggleDay(this)">Tu</button>
                <button class="day-btn" data-d="3" onclick="toggleDay(this)">We</button>
                <button class="day-btn" data-d="4" onclick="toggleDay(this)">Th</button>
                <button class="day-btn" data-d="5" onclick="toggleDay(this)">Fr</button>
                <button class="day-btn" data-d="6" onclick="toggleDay(this)">Sa</button>
                <button class="day-btn" data-d="0" onclick="toggleDay(this)">Su</button>
            </div>
            <label class="flabel">Active from → until</label>
            <div class="frow">
                <input type="time" class="finput" id="bc-start" value="08:00" style="margin:0">
                <input type="time" class="finput" id="bc-end"   value="22:00" style="margin:0">
            </div>
            <div style="margin-top:.8rem;display:flex;gap:.45rem">
                <button class="btn-full blue" onclick="addBrowserSchedule()">+ Add Browser Rule</button>
                <button class="btn-sm danger" onclick="clearAllBrowserScheds()" style="flex-shrink:0;align-self:stretch">Clear All</button>
            </div>
        </div>

        <div style="border-top:1px solid var(--bord);margin:1.5rem 0;opacity:.35"></div>

        <!-- ── Server Schedules ────────────────────────────────────────────── -->
        <div class="sec-label" style="margin-top:0">Server Schedules <span class="badge">requires own server &amp; cron job</span></div>
        <div class="warn-box">
            <strong>⚠️ Own server required.</strong> Server schedules run 24/7 even when no browser is open — but they require you to download this tool and host it on your own web server with a cron job. They are not available on clivethompson.com to other users. See the <a onclick="showTab('advanced');return false" href="#" style="color:#d97706">Advanced tab</a> for setup instructions.
        </div>
        <div id="scheds-list"><div style="color:#334155;font-size:.82rem;text-align:center;padding:1.2rem">No server schedules set</div></div>

        <div class="sec-label">Add Server Rule</div>
        <div class="sched-card">
            <label class="flabel">Profile to activate</label>
            <select class="finput" id="sc-profile" style="margin-bottom:.8rem"></select>
            <label class="flabel">Days of the week</label>
            <div class="day-picker" id="sc-days">
                <button class="day-btn" data-d="1" onclick="toggleDay(this)">Mo</button>
                <button class="day-btn" data-d="2" onclick="toggleDay(this)">Tu</button>
                <button class="day-btn" data-d="3" onclick="toggleDay(this)">We</button>
                <button class="day-btn" data-d="4" onclick="toggleDay(this)">Th</button>
                <button class="day-btn" data-d="5" onclick="toggleDay(this)">Fr</button>
                <button class="day-btn" data-d="6" onclick="toggleDay(this)">Sa</button>
                <button class="day-btn" data-d="0" onclick="toggleDay(this)">Su</button>
            </div>
            <label class="flabel">Active from → until</label>
            <div class="frow">
                <input type="time" class="finput" id="sc-start" value="08:00" style="margin:0">
                <input type="time" class="finput" id="sc-end"   value="22:00" style="margin:0">
            </div>
            <div style="margin-top:.8rem;display:flex;gap:.45rem">
                <button class="btn-full blue" onclick="addSchedule()">+ Add Server Rule</button>
                <button class="btn-sm danger" onclick="clearAllScheds()" style="flex-shrink:0;align-self:stretch">Clear All</button>
            </div>
        </div>
    </div>
</div>

<!-- ══ ADVANCED ═════════════════════════════════════════════════════════════════ -->
<div class="page" id="tab-advanced">
    <h1>🛠️ Advanced Users Only</h1>
    <p style="color:#7dd3fc;font-weight:700">This section is for people who have their own web server or web hosting account.</p>
    <p>If you are just visiting this tool on clivethompson.com to control your home filter, you do not need any of this — everything on the Control, Profiles, and Schedule tabs already works for you as-is.</p>

    <div class="callout"><strong>Who this is for:</strong> You have a web hosting account (like SiteGround, Namecheap, Bluehost, etc.) or a VPS, and you want to run your own private copy of this tool — with full 24/7 automatic scheduling that works even when no browser tab is open.</div>

    <h2>Part 1 — Download &amp; Install Your Own Copy</h2>

    <div class="step">
        <div class="step__num">What you get</div>
        <div class="step__title">A single PHP file — the entire tool in one download</div>
        <div class="step__body">No database, no config files, no installation wizard. Just one file you upload to your server and it works.</div>
    </div>
    <div class="step">
        <div class="step__num">Requirements</div>
        <div class="step__title">What your server needs</div>
        <div class="step__body">Any web host running <strong>PHP 7.4 or newer</strong> with <strong>cURL</strong> enabled. This is standard on virtually all shared hosting plans — if your host runs WordPress sites, it can run this too.</div>
    </div>
    <div class="step">
        <div class="step__num">How to install</div>
        <div class="step__title">Three steps</div>
        <div class="step__body">
            <p><strong>1.</strong> Download the file using the button below.</p>
            <p><strong>2.</strong> Upload it to your web server using FTP (or your host's file manager).</p>
            <p><strong>3.</strong> Visit it in your browser — for example <span class="mono">yoursite.com/opendns-remote.php</span> — and it will work immediately.</p>
        </div>
    </div>

    <div class="dl-card">
        <h2>📄 opendns-remote.php</h2>
        <p>Single file · PHP 7.4+ · No database</p>
        <a href="opendns-remote.php?download=1" class="dl-btn">⬇️ Download Free</a>
        <p style="margin-top:.6rem;font-size:.7rem">Created by <a href="https://clivethompson.com">Clive Thompson</a> · Free to use and modify</p>
    </div>

    <h2 style="margin-top:2rem">Part 2 — Setting Up 24/7 Automatic Scheduling</h2>
    <p>This is the part that makes your schedules fire on their own — even when you are asleep and no browser tab is open.</p>

    <div class="step">
        <div class="step__num">The problem it solves</div>
        <div class="step__title">Why schedules need a cron job</div>
        <div class="step__body">A normal webpage can only do things while you are looking at it. The moment you close the tab, it goes completely quiet and cannot do anything on its own. So if you set a schedule to switch on Kids Mode at 8pm, something else needs to check the clock and act on it for you.</div>
    </div>
    <div class="step">
        <div class="step__num">What a cron job is</div>
        <div class="step__title">An alarm clock on your web server</div>
        <div class="step__body">Your web server runs 24 hours a day, 7 days a week, even while you sleep. A <strong>cron job</strong> is simply an alarm you set on that server. You tell it: <em>"every minute, run this small task."</em> That task visits a special address on your site, which wakes the PHP file up to check whether any schedule should be active right now — and if so, logs into OpenDNS and switches the filter automatically.</div>
    </div>
    <div class="step">
        <div class="step__num">How to set it up</div>
        <div class="step__title">Add one line in your hosting control panel</div>
        <div class="step__body">
            <p><strong>1.</strong> Log in to your web hosting control panel (most hosts call it <strong>cPanel</strong>).</p>
            <p><strong>2.</strong> Find the section called <strong>Cron Jobs</strong>.</p>
            <p><strong>3.</strong> Set the frequency to <strong>Every Minute</strong> (usually shown as five asterisks: <code>* * * * *</code>).</p>
            <p><strong>4.</strong> Paste this as the command, replacing the URL with your own server address:</p>
            <div class="dns-box" style="font-size:.76rem;word-break:break-all"><span>* * * * * curl -s "https://yoursite.com/opendns-remote.php?cron=1&amp;key=ODNS2026CRON"</span></div>
            <p style="margin-top:.5rem">The five asterisks mean <em>every minute, every hour, every day</em>. The <code>?cron=1</code> tells the PHP file this is the automatic check. The <code>key=ODNS2026CRON</code> is a secret password so random people on the internet cannot trigger it.</p>
            <p><strong>5.</strong> Save the cron job. That is it — the server handles everything from this point on.</p>
        </div>
    </div>
    <div class="step">
        <div class="step__num">Without the cron job</div>
        <div class="step__title">Schedules do nothing on their own</div>
        <div class="step__body">Your rules will be saved, but nothing will ever check them. They will sit there doing nothing until you open the page yourself and manually switch profiles. The cron job is what makes them truly automatic.</div>
    </div>

    <h2>Part 3 — Security</h2>
    <div class="step">
        <div class="step__num">Protect the schedule file</div>
        <div class="step__title">Stop others from downloading your credentials</div>
        <div class="step__body">
            <p>When you save a schedule, this tool stores your OpenDNS password in a file called <code>opendns-schedules.json</code> so the cron job can log in automatically. By default, anyone who knows the filename could download it. Prevent that by creating a file called <code>.htaccess</code> in the same folder as the PHP file, containing:</p>
            <code style="display:block;background:#0a0a0f;padding:.7rem;border-radius:8px;margin:.6rem 0;font-size:.76rem;line-height:1.8">&lt;Files "opendns-schedules.json"&gt;<br>&nbsp; Order allow,deny<br>&nbsp; Deny from all<br>&lt;/Files&gt;</code>
            <p>This tells your web server: <em>never serve that file to any browser — ever.</em> Most hosting control panels let you create files directly; alternatively upload the <code>.htaccess</code> file via FTP.</p>
        </div>
    </div>

    <h2>Part 4 — Customising Profiles &amp; the Cron Key</h2>
    <div class="step">
        <div class="step__num">Built-in profiles</div>
        <div class="step__title">Edit them in the PHP file</div>
        <div class="step__body">Open the downloaded file in any text editor (Notepad, TextEdit, VS Code). Near the top you will find an array called <code>$profiles</code>. Each profile lists the categories it blocks (<code>cats</code>) and any specific websites (<code>domains</code>). The full list of available category names is in the <code>$catIds</code> array just below it.</div>
    </div>
    <div class="step">
        <div class="step__num">Cron key</div>
        <div class="step__title">Change it to something private</div>
        <div class="step__body">The default key is <code>ODNS2026CRON</code>. Find the line near the top of the file that reads <code>define('CRON_KEY', 'ODNS2026CRON');</code> and change it to anything you like. Then update the URL in your cron job to match your new key.</div>
    </div>
</div>

<!-- Modal -->
<div id="modal-bg" onclick="closeModal()">
    <div class="modal" onclick="event.stopPropagation()">
        <div class="modal__title" id="modal-title"></div>
        <div class="modal__body"  id="modal-body"></div>
        <button class="modal__close" onclick="closeModal()">Close</button>
    </div>
</div>

<script>
const BUILTIN_IDS       = <?= json_encode(array_keys($profiles)) ?>;
const ALL_PROFILES      = <?= $profilesJson ?>;
const ALL_CATS          = <?= $allCatsJson ?>;
const PROTECTED_DOMAINS = <?= $protectedAllowJson ?>;
let   customProfiles = <?= $customJson ?>;
const IS_LOGGED_IN   = <?= $isLoggedIn ? 'true' : 'false' ?>;
const DAY_LABELS     = ['Su','Mo','Tu','We','Th','Fr','Sa'];

// ── Tabs ──────────────────────────────────────────────────────────────────────
function showTab(name){
    document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
    document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
    document.getElementById('tab-'+name).classList.add('active');
    const tabs=['guide','control','profiles','schedule','advanced','security'];
    document.querySelectorAll('.tab')[tabs.indexOf(name)]?.classList.add('active');
    if(name==='profiles'){renderProfilesTab();if(IS_LOGGED_IN)initAllowList();}
    if(name==='control'){renderCtrlGrid();if(IS_LOGGED_IN){showStatusWrap();refreshStatus();}}
    if(name==='schedule')initScheduleTab();
}

// ── Profiles data ─────────────────────────────────────────────────────────────
function getMerged(){
    const cp=Object.entries(customProfiles).map(([id,p])=>({...p,id,custom:true}));
    return[...ALL_PROFILES,...cp];
}

// ── Control grid ──────────────────────────────────────────────────────────────
function renderCtrlGrid(){
    const g=document.getElementById('ctrl-grid');if(!g)return;
    g.innerHTML=getMerged().map(p=>`
        <div class="prow">
            <button class="pbtn" style="--c:${p.color||'#334155'}" onclick="doSwitch('${p.id}')">
                <span class="pbtn__icon">${p.icon||'⚙️'}</span>
                <div><div class="pbtn__label">${p.label}</div><div class="pbtn__desc">${p.desc||''}</div></div>
            </button>
            <button class="btn-sm" onclick="showDetails('${p.id}')">ℹ</button>
        </div>`).join('');
}

// ── Login / Logout ────────────────────────────────────────────────────────────
function doLogin(){
    const email=document.getElementById('login-email').value.trim();
    const pass =document.getElementById('login-pass').value;
    const btn  =document.getElementById('login-btn');
    const err  =document.getElementById('login-error');
    if(!email||!pass){showLoginErr('Enter email and password.');return;}
    btn.disabled=true;btn.textContent='Signing in…';err.style.display='none';
    api({action:'login',email,password:pass}).then(d=>{
        if(d.ok){
            const sel=document.getElementById('net-sel');
            sel.innerHTML=d.networks.map(n=>`<option value="${n.id}">${n.name}</option>`).join('');
            document.getElementById('login-section').style.display='none';
            document.getElementById('ctrl-section').style.display='block';
            renderCtrlGrid();showStatusWrap();
            api({action:'apply_protected_domains',network:sel.value})
                .finally(()=>refreshStatus());
        } else showLoginErr(d.error||'Login failed.');
    }).catch(()=>showLoginErr('Network error — try again.'))
      .finally(()=>{btn.disabled=false;btn.textContent='Sign In';});
}
function showLoginErr(m){const e=document.getElementById('login-error');e.textContent=m;e.style.display='block';}
function doLogout(){
    api({action:'logout'}).finally(()=>{
        document.getElementById('ctrl-section').style.display='none';
        document.getElementById('login-section').style.display='block';
        ['login-email','login-pass'].forEach(id=>document.getElementById(id).value='');
        setStatus('','Tap a profile to apply it to your network');
    });
}

// ── Cache flush banner ────────────────────────────────────────────────────────
let _flushTimer=null;
function showFlushBanner(){
    const b=document.getElementById('flush-banner');
    if(!b)return;
    b.style.display='block';
    if(_flushTimer)clearTimeout(_flushTimer);
    _flushTimer=setTimeout(dismissFlushBanner,90000);
}
function dismissFlushBanner(){
    const b=document.getElementById('flush-banner');
    if(b)b.style.display='none';
    if(_flushTimer){clearTimeout(_flushTimer);_flushTimer=null;}
}

// ── Switch + Reset ────────────────────────────────────────────────────────────
function netId(){return document.getElementById('net-sel').value;}
function doSwitch(profile){
    setStatus('busy','Connecting to OpenDNS…');
    api({action:'switch',network:netId(),profile}).then(d=>{
        setStatus(d.ok?'ok':'error',d.message);
        if(d.ok){showStatusWrap();refreshStatus();showFlushBanner();}
    }).catch(()=>setStatus('error','Network error — try again'));
}
function checkConn(){
    setStatus('busy','Checking OpenDNS connection…');
    api({action:'check_connection',network:netId()}).then(d=>setStatus(d.ok?'ok':'error',d.message))
        .catch(()=>setStatus('error','Network error — try again'));
}
function doReset(){
    if(!confirm('Remove all blocks and set your network to Everything Open?'))return;
    setStatus('busy','Resetting…');
    api({action:'reset_all',network:netId()}).then(d=>{
        setStatus(d.ok?'ok':'error',d.message);
        if(d.ok){showStatusWrap();refreshStatus();alwaysAllowList=[];renderAllowList();}
    }).catch(()=>setStatus('error','Network error — try again'));
}
function showStatusWrap(){const w=document.getElementById('status-wrap');if(w)w.style.display='block';}
function setStatus(type,msg){
    const b=document.getElementById('sbox');
    b.className='sbox '+type;
    b.innerHTML=type==='busy'?`<div class="spinner"></div> ${msg}`:msg;
}

// ── Details modal ─────────────────────────────────────────────────────────────
function showDetails(id){
    if(id==='ads_adware'){
        showModal('🛡️ Block Ads & Adware — What it does',`
<p style="margin-bottom:.7rem"><em>This profile blocks two categories. They are quite different from each other — it is worth understanding both.</em></p>
<p style="font-size:.85rem;font-weight:800;color:#7dd3fc;margin:.2rem 0 .25rem">Adware</p>
<p>This is a <strong>security category</strong>, not an ad category. It covers sites that host or distribute adware and similar unwanted software: pop-up generators, browser hijackers, bundled installers, and "your PC is infected" scareware pages.</p>
<p style="margin-top:.4rem">OpenDNS's own category description is explicit that it does not include sites which serve advertising. Switching it on will <em>not</em> clean up ads on a normal news site — it protects you from downloading junk software. <strong>Recommended: leave this on permanently.</strong></p>
<p style="font-size:.85rem;font-weight:800;color:#7dd3fc;margin:.8rem 0 .25rem">Advertisements</p>
<p>This is the one aimed at actual ads. It targets ad servers — the domains that deliver ads to pages: DoubleClick, googlesyndication, Criteo, Taboola, Outbrain, Pubmatic and hundreds of others.</p>
<p style="margin-top:.4rem">Blocking it means your device never receives the address for those servers, so the ad simply fails to load. Many banner and display ads do disappear.</p>
<p style="font-size:.85rem;font-weight:800;color:#f87171;margin:.8rem 0 .25rem">What it will not do</p>
<ul>
<li>It cannot remove ads served from the <strong>same domain as the site itself</strong> — first-party ads still appear.</li>
<li><strong>YouTube ads</strong> survive, because they use the same domains as the videos.</li>
<li>Sponsored articles and promoted content baked into the page stay put.</li>
<li>You often get <strong>blank gaps or broken layout</strong> rather than a tidy page, because the space is still reserved.</li>
<li>Some sites detect failed ad calls and show an "please disable your ad blocker" wall.</li>
<li>Only works on devices using OpenDNS on your home network — a phone on mobile data is unaffected.</li>
</ul>
<p style="font-size:.85rem;font-weight:800;color:#4ade80;margin:.8rem 0 .25rem">Practical suggestion</p>
<p><strong>Turn Adware on and leave it on</strong> — it costs you nothing and adds genuine security.</p>
<p style="margin-top:.4rem"><strong>Try Advertisements too</strong>, but expect the occasional site to misbehave. You can add specific domains to the Always Authorised list on the Profiles tab if something you use regularly breaks.</p>
<p style="margin-top:.4rem">For proper ad removal in the browser, a DNS filter is a blunt instrument. An extension such as <strong>uBlock Origin</strong> removes the ad and the empty space it leaves behind, and handles first-party ads that DNS cannot touch. The two work well together.</p>`);
        return;
    }
    const p=getMerged().find(x=>x.id===id);if(!p)return;
    const cats=p.cats==='ALL'?'<em>All categories (complete lockdown)</em>'
        :p.cats==='ALL_EXCEPT'?'<em>All categories except: '+((p.cats_except||[]).join(', '))+'</em>'
        :(!p.cats||!p.cats.length)?'<em>None — everything accessible</em>'
        :'<ul>'+p.cats.map(c=>`<li>${c}</li>`).join('')+'</ul>';
    const doms=p.domains&&p.domains.length
        ?'<br><strong>Always blocks these domains:</strong><ul>'+p.domains.map(d=>`<li>${d}</li>`).join('')+'</ul>':'';
    const allowed=p.allow_domains&&p.allow_domains.length
        ?'<br><strong>Always allows these domains:</strong><ul>'+p.allow_domains.map(d=>`<li>${d}</li>`).join('')+'</ul>':'';
    showModal(p.icon+' '+p.label,'<strong>Blocked categories:</strong>'+cats+doms+allowed);
}

// ── Profiles tab ──────────────────────────────────────────────────────────────
function renderProfilesTab(){
    const merged=getMerged();
    let html='<div class="sec-label" style="margin-top:0">Built-in Profiles</div>';
    html+=merged.filter(p=>!p.custom).map(profileCard).join('');
    const custom=merged.filter(p=>p.custom);
    if(custom.length){
        html+='<div class="sec-label" style="margin-top:1.2rem">Your Custom Profiles</div>';
        html+=custom.map(profileCard).join('');
    }
    document.getElementById('profiles-list').innerHTML=html;
}
function catSummary(p){
    if(p.cats==='ALL')return'Blocks all categories';
    if(p.cats==='ALL_EXCEPT')return'Blocks all except '+(p.cats_except||[]).join(', ');
    return(!p.cats||!p.cats.length)?'Nothing blocked':p.cats.length+' categories blocked';
}
function profileCard(p){
    const domNote=p.domains&&p.domains.length?` · ${p.domains.length} domain(s) blocked`:'';
    const allowNote=p.allow_domains&&p.allow_domains.length?` · ${p.allow_domains.length} always-allowed`:'';
    return`<div class="pcard" style="--c:${p.color||'#334155'}">
        <div class="pcard__top">
            <span class="pcard__icon">${p.icon||'⚙️'}</span>
            <div class="pcard__info">
                <div class="pcard__label">${p.label}<span class="badge ${p.custom?'custom':''}">${p.custom?'custom':'built-in'}</span></div>
                <div class="pcard__desc">${catSummary(p)}${domNote}${allowNote}</div>
            </div>
        </div>
        <div class="pcard__btns">
            <button class="btn-sm" onclick="showDetails('${p.id}')">ℹ View Contents</button>
            ${p.custom?`<button class="btn-sm" onclick="openEdit('${p.id}')">✏️ Edit</button>`:''}
            ${p.custom?`<button class="btn-sm danger" onclick="deleteProfile('${p.id}')">🗑️ Delete</button>`:''}
        </div>
    </div>`;
}

// ── Create / Edit form ────────────────────────────────────────────────────────
let formDomains=[];
function openCreate(){
    document.getElementById('form-title').textContent='Create New Profile';
    document.getElementById('fp-edit-id').value='';
    document.getElementById('fp-name').value='';
    document.getElementById('fp-desc').value='';
    document.getElementById('fp-icon').value='';
    setAllCats(false);formDomains=[];renderFormDomains();
    pickColor(document.querySelector('[data-c="#7c3aed"]'));
    document.getElementById('create-form').style.display='block';
    document.getElementById('create-form').scrollIntoView({behavior:'smooth'});
}
function openEdit(id){
    const p=customProfiles[id];if(!p)return;
    document.getElementById('form-title').textContent='Edit Profile';
    document.getElementById('fp-edit-id').value=id;
    document.getElementById('fp-name').value=p.label||'';
    document.getElementById('fp-desc').value=p.desc||'';
    document.getElementById('fp-icon').value=p.icon||'';
    setAllCats(false);
    (p.cats||[]).forEach(c=>{const cb=document.querySelector(`#fp-cats input[value="${CSS.escape(c)}"]`);if(cb)cb.checked=true;});
    formDomains=[...(p.domains||[])];renderFormDomains();
    const sw=document.querySelector(`[data-c="${p.color}"]`);
    pickColor(sw||null);if(!sw)document.getElementById('fp-color').value=p.color||'#7c3aed';
    document.getElementById('create-form').style.display='block';
    document.getElementById('create-form').scrollIntoView({behavior:'smooth'});
}
function closeCreate(){document.getElementById('create-form').style.display='none';}
function addFormDomain(){
    const inp=document.getElementById('fp-domain-input');
    const d=inp.value.trim().toLowerCase().replace(/[^a-z0-9.\-]/g,'');
    if(!d)return;if(!formDomains.includes(d))formDomains.push(d);inp.value='';renderFormDomains();
}
function removeFormDomain(d){formDomains=formDomains.filter(x=>x!==d);renderFormDomains();}
function renderFormDomains(){
    document.getElementById('fp-domain-list').innerHTML=formDomains.map(d=>
        `<div class="dchip">${d}<button onclick="removeFormDomain('${d}')">✕</button></div>`).join('');
}
function setAllCats(v){document.querySelectorAll('#fp-cats input').forEach(c=>c.checked=v);}
function pickColor(el){
    document.querySelectorAll('#fp-colors .swatch').forEach(s=>s.classList.remove('on'));
    if(el){el.classList.add('on');document.getElementById('fp-color').value=el.dataset.c;}
}
function saveProfile(){
    const label=document.getElementById('fp-name').value.trim();
    if(!label){alert('Enter a profile name.');return;}
    const payload={action:'save_profile',profile:{
        id:document.getElementById('fp-edit-id').value||undefined,
        label,desc:document.getElementById('fp-desc').value.trim(),
        icon:document.getElementById('fp-icon').value.trim()||'⚙️',
        color:document.getElementById('fp-color').value||'#7c3aed',
        cats:Array.from(document.querySelectorAll('#fp-cats input:checked')).map(c=>c.value),
        domains:formDomains,
    }};
    api(payload).then(d=>{
        if(d.ok){
            const id=d.id;
            customProfiles[id]={...payload.profile,id,custom:true};
            closeCreate();renderProfilesTab();renderCtrlGrid();populateSchedDrop();
        } else alert(d.message||'Save failed.');
    });
}
function deleteProfile(id){
    if(!confirm('Delete this profile?'))return;
    api({action:'delete_profile',id}).then(d=>{
        if(d.ok){delete customProfiles[id];renderProfilesTab();renderCtrlGrid();populateSchedDrop();}
    });
}

// ── Schedule tab ──────────────────────────────────────────────────────────────
function initScheduleTab(){
    const loggedIn=document.getElementById('ctrl-section')?.style.display!=='none'||IS_LOGGED_IN;
    document.getElementById('sched-login-msg').style.display=loggedIn?'none':'block';
    document.getElementById('sched-main').style.display=loggedIn?'block':'none';
    if(loggedIn){populateSchedDrop();loadSchedules();renderBrowserSchedules();startBrowserScheduler();}
}
function populateSchedDrop(){
    ['sc-profile','bc-profile'].forEach(selId=>{
        const sel=document.getElementById(selId);if(!sel)return;
        const cur=sel.value;
        sel.innerHTML=getMerged().map(p=>`<option value="${p.id}">${p.icon||''} ${p.label}</option>`).join('');
        if(cur)sel.value=cur;
    });
}
function toggleDay(b){b.classList.toggle('on');}
function selectedDays(){return Array.from(document.querySelectorAll('#sc-days .day-btn.on')).map(b=>+b.dataset.d);}
function selectedBrowserDays(){return Array.from(document.querySelectorAll('#bc-days .day-btn.on')).map(b=>+b.dataset.d);}

function addSchedule(){
    const profile=document.getElementById('sc-profile').value;
    const days=selectedDays();
    const start=document.getElementById('sc-start').value;
    const end=document.getElementById('sc-end').value;
    if(!days.length){alert('Select at least one day.');return;}
    if(!start||!end||start>=end){alert('Check the time range — start must be before end.');return;}
    api({action:'save_schedule',network:netId()||'',schedule:{profile,days,start,end}}).then(d=>{
        if(d.ok){loadSchedules();document.querySelectorAll('#sc-days .day-btn').forEach(b=>b.classList.remove('on'));}
        else alert(d.message||'Failed to save.');
    });
}
function loadSchedules(){
    api({action:'get_schedules',network:netId()||''}).then(d=>{
        if(d.ok)renderSchedules(d.schedules);
    });
}
function renderSchedules(scheds){
    const el=document.getElementById('scheds-list');
    if(!scheds||!scheds.length){el.innerHTML='<div style="color:#334155;font-size:.82rem;text-align:center;padding:1.2rem">No schedules set</div>';return;}
    el.innerHTML=scheds.map(s=>{
        const p=getMerged().find(x=>x.id===s.profile);
        const dayStr=s.days.slice().sort((a,b)=>a-b).map(d=>DAY_LABELS[d]).join(' ');
        return`<div class="litem">
            <span style="font-size:1.3rem">${p?p.icon:'⚙️'}</span>
            <div class="litem__body">
                <div class="litem__title">${p?p.label:s.profile}</div>
                <div class="litem__sub">${dayStr} &nbsp;·&nbsp; ${s.start} – ${s.end}</div>
            </div>
            <button class="litem__del" onclick="deleteSchedule('${s.id}')">✕</button>
        </div>`;
    }).join('');
}
function deleteSchedule(id){
    api({action:'delete_schedule',id}).then(d=>{if(d.ok)loadSchedules();});
}
function clearAllScheds(){
    if(!confirm('Clear all server schedule rules?'))return;
    api({action:'clear_schedules'}).then(d=>{if(d.ok)loadSchedules();});
}

// ── Browser Schedules ─────────────────────────────────────────────────────────
let localSchedules=JSON.parse(localStorage.getItem('odns_browser_scheds')||'[]');
let lastBrowserTarget=null;
let browserSchedulerInterval=null;

function saveBrowserScheds(){localStorage.setItem('odns_browser_scheds',JSON.stringify(localSchedules));}

function addBrowserSchedule(){
    const profile=document.getElementById('bc-profile').value;
    const days=selectedBrowserDays();
    const start=document.getElementById('bc-start').value;
    const end=document.getElementById('bc-end').value;
    if(!days.length){alert('Select at least one day.');return;}
    if(!start||!end||start>=end){alert('Check the time range — start must be before end.');return;}
    localSchedules.push({id:'b'+Date.now(),profile,days,start,end});
    saveBrowserScheds();renderBrowserSchedules();
    document.querySelectorAll('#bc-days .day-btn').forEach(b=>b.classList.remove('on'));
}
function deleteBrowserSchedule(id){
    localSchedules=localSchedules.filter(s=>s.id!==id);
    saveBrowserScheds();renderBrowserSchedules();
}
function clearAllBrowserScheds(){
    if(!confirm('Clear all browser schedule rules?'))return;
    localSchedules=[];saveBrowserScheds();renderBrowserSchedules();
}
function renderBrowserSchedules(){
    const el=document.getElementById('browser-scheds-list');if(!el)return;
    if(!localSchedules.length){
        el.innerHTML='<div style="color:#334155;font-size:.82rem;text-align:center;padding:1.2rem">No browser schedules set</div>';
        return;
    }
    el.innerHTML=localSchedules.map(s=>{
        const p=getMerged().find(x=>x.id===s.profile);
        const dayStr=s.days.slice().sort((a,b)=>a-b).map(d=>DAY_LABELS[d]).join(' ');
        return`<div class="litem">
            <span style="font-size:1.3rem">${p?p.icon:'⚙️'}</span>
            <div class="litem__body">
                <div class="litem__title">${p?p.label:s.profile}</div>
                <div class="litem__sub">${dayStr} &nbsp;·&nbsp; ${s.start} – ${s.end}</div>
            </div>
            <button class="litem__del" onclick="deleteBrowserSchedule('${s.id}')">✕</button>
        </div>`;
    }).join('');
}
function checkBrowserSchedules(){
    if(!localSchedules.length)return;
    const now=new Date();
    const day=now.getDay();
    const time=now.getHours().toString().padStart(2,'0')+':'+now.getMinutes().toString().padStart(2,'0');
    let target=null;
    for(const s of localSchedules){
        if(s.days.includes(day)&&time>=s.start&&time<s.end){target=s.profile;break;}
    }
    if(target!==null&&target!==lastBrowserTarget){
        lastBrowserTarget=target;
        api({action:'switch',network:netId()||'',profile:target})
            .then(d=>{if(d.ok){showStatusWrap();refreshStatus();}})
            .catch(()=>{});
    } else if(target===null&&lastBrowserTarget!==null){
        lastBrowserTarget=null;
        api({action:'switch',network:netId()||'',profile:'open'})
            .then(d=>{if(d.ok){showStatusWrap();refreshStatus();}})
            .catch(()=>{});
    }
}
function startBrowserScheduler(){
    if(browserSchedulerInterval)return;
    checkBrowserSchedules();
    browserSchedulerInterval=setInterval(checkBrowserSchedules,60000);
    const bar=document.getElementById('browser-scheduler-bar');
    if(bar&&localSchedules.length)bar.style.display='flex';
}

// ── DNS status check ──────────────────────────────────────────────────────────
function checkDns(){
    const panel=document.getElementById('dns-panel');
    if(!panel)return;
    panel.style.display='block';
    api({action:'check_dns'}).then(d=>{if(d.ok)renderDnsStatus(d);}).catch(()=>{});
}
function renderDnsStatus(d){
    const body=document.getElementById('dns-panel-body');
    const badge=document.getElementById('dns-overall-badge');
    if(!body)return;
    const p=d.primary,s=d.secondary;
    const bothOk=p.reachable&&s.reachable;
    const noneOk=!p.reachable&&!s.reachable;
    if(badge){
        badge.textContent=bothOk?'✓ Both servers online':noneOk?'✗ Servers unreachable':'⚠ Partial';
        badge.style.color=bothOk?'#4ade80':noneOk?'#f87171':'#fbbf24';
    }
    const row=(srv)=>`
        <div class="dns-row">
            <span class="dns-dot ${srv.reachable?'on':'off'}"></span>
            <span class="dns-ip">${srv.ip}</span>
            <span class="dns-lbl">${srv.label} DNS</span>
            <span class="dns-state ${srv.reachable?'ok':'fail'}">${srv.reachable?'✓ Online':'✗ Unreachable'}</span>
        </div>`;
    body.innerHTML=row(p)+row(s)+`
        <div class="dns-your-ip-row">
            <span class="dns-dot neutral"></span>
            <span>Your IP: <strong style="color:#94a3b8;font-family:monospace;font-size:.77rem">${d.your_ip||'—'}</strong></span>
        </div>
        <div class="dns-footer-note">${bothOk
            ?'Both OpenDNS servers are online ✓ — set your router\'s DNS to the two IPs above. If your filter changes take effect, your DNS is correctly configured.'
            :'⚠️ One or more OpenDNS servers could not be reached from this server — check your OpenDNS account and router DNS settings. Note: if you can log in and apply filters successfully, OpenDNS is working and this warning may be a server firewall limitation.'
        }</div>`;
}

// ── Status card ───────────────────────────────────────────────────────────────
function refreshStatus(){
    checkDns();
    const card=document.getElementById('status-card');
    if(card)card.innerHTML='<div style="text-align:center;color:#475569;font-size:.82rem;padding:.6rem">Loading status…</div>';
    api({action:'get_status',network:netId()}).then(d=>{
        if(!d.ok){
            if(card)card.innerHTML=`<div class="status-row"><span class="status-lbl">Error</span><span class="status-val red">${d.message||'Failed to load'}</span></div>`;
            return;
        }
        const catCount=d.blocked_cats_count||0;
        const levelColor=d.level==='None'?'green':d.level==='Unknown'?'':'amber';
        const catChips=d.blocked_cats&&d.blocked_cats.length
            ?`<div class="chip-row" style="margin-top:.3rem">${d.blocked_cats.map(c=>`<span class="chip">${c}</span>`).join('')}</div>`:'';
        const blockedDoms=d.always_blocked&&d.always_blocked.length
            ?`<div class="chip-row">${d.always_blocked.map(x=>`<span class="chip">${x}</span>`).join('')}</div>`
            :'<span class="status-val" style="color:#475569">None</span>';
        const allowedDoms=d.always_allowed&&d.always_allowed.length
            ?`<div class="chip-row">${d.always_allowed.map(x=>`<span class="chip" style="color:#4ade80;background:#052e16;border-color:#14532d">${x}</span>`).join('')}</div>`
            :'<span class="status-val" style="color:#475569">None</span>';
        if(card)card.innerHTML=`
            <div class="status-row"><span class="status-lbl">Filter Level</span><span class="status-val ${levelColor}">${d.level}</span></div>
            <div class="status-row" style="${catChips?'flex-direction:column;align-items:flex-start':''}">
                <div style="display:flex;justify-content:space-between;width:100%">
                    <span class="status-lbl">Blocked Categories</span>
                    ${catCount?`<span class="status-val amber">${catCount} blocked</span>`:'<span class="status-val green">None</span>'}
                </div>${catChips}
            </div>
            <div class="status-row"><span class="status-lbl">Always Blocked Domains</span>${blockedDoms}</div>
            <div class="status-row" style="border-bottom:none"><span class="status-lbl">Always Allowed Domains</span>${allowedDoms}</div>`;
        if(d.always_allowed){alwaysAllowList=d.always_allowed;renderAllowList();}
    }).catch(()=>{
        if(card)card.innerHTML='<div class="status-row"><span class="status-val red">Connection error</span></div>';
    });
}

// ── Always Authorised list ────────────────────────────────────────────────────
let alwaysAllowList=[];
function initAllowList(){
    const el=document.getElementById('allow-list');
    if(!el)return;
    if(!IS_LOGGED_IN){el.innerHTML='<div style="color:#334155;font-size:.82rem;text-align:center;padding:.8rem">Sign in on the Control tab to manage always-authorised domains</div>';return;}
    el.innerHTML='<div style="color:#334155;font-size:.82rem;text-align:center;padding:.8rem">Loading…</div>';
    api({action:'get_status',network:netId()||''}).then(d=>{
        if(d.ok){alwaysAllowList=d.always_allowed||[];}
        renderAllowList();
    }).catch(()=>renderAllowList());
}
function renderAllowList(){
    const el=document.getElementById('allow-list');if(!el)return;
    if(!alwaysAllowList.length){
        el.innerHTML='<div style="color:#334155;font-size:.82rem;text-align:center;padding:.8rem">No always-authorised domains set</div>';
        return;
    }
    // Show protected domains first, then others
    const sorted=[...alwaysAllowList].sort((a,b)=>{
        const ap=PROTECTED_DOMAINS.includes(a),bp=PROTECTED_DOMAINS.includes(b);
        return ap===bp?0:ap?-1:1;
    });
    el.innerHTML=sorted.map(d=>{
        const locked=PROTECTED_DOMAINS.includes(d);
        return`<div class="allow-chip">${d}${locked
            ?'<span title="Protected — cannot be removed" style="color:#4ade80;font-size:.75rem;margin-left:auto">🔒</span>'
            :`<button onclick="removeAlwaysAllow('${d}')" title="Remove">✕</button>`}</div>`;
    }).join('');
}
function addAlwaysAllow(){
    const inp=document.getElementById('allow-domain-input');
    const d=inp.value.trim().toLowerCase().replace(/[^a-z0-9.\-]/g,'');
    const msg=document.getElementById('allow-msg');
    if(!d)return;
    if(!IS_LOGGED_IN){if(msg)msg.style.color='#f87171',msg.textContent='Sign in first.';return;}
    if(msg)msg.textContent='';
    api({action:'add_always_allow',network:netId()||'',domain:d}).then(r=>{
        if(r.ok){
            if(!alwaysAllowList.includes(d))alwaysAllowList.push(d);
            renderAllowList();inp.value='';
            if(msg){msg.style.color='#4ade80';msg.textContent='✓ '+d+' added';}
        } else {if(msg){msg.style.color='#f87171';msg.textContent=r.message||'Failed to add.';}}
    });
}
function removeAlwaysAllow(d){
    api({action:'remove_always_allow',network:netId()||'',domain:d}).then(r=>{
        if(r.ok){alwaysAllowList=alwaysAllowList.filter(x=>x!==d);renderAllowList();}
        else alert(r.message||'Failed to remove.');
    });
}

// ── Modal ─────────────────────────────────────────────────────────────────────
function showModal(title,body){document.getElementById('modal-title').textContent=title;document.getElementById('modal-body').innerHTML=body;document.getElementById('modal-bg').classList.add('open');}
function closeModal(){document.getElementById('modal-bg').classList.remove('open');}

// ── API ───────────────────────────────────────────────────────────────────────
function api(data){
    return fetch(location.href,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}).then(r=>r.json());
}

// ── Init ──────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded',()=>{
    pickColor(document.querySelector('[data-c="#7c3aed"]'));
    if(IS_LOGGED_IN||document.getElementById('ctrl-section')?.style.display!=='none'){
        renderCtrlGrid();showStatusWrap();
        api({action:'apply_protected_domains',network:netId()}).finally(()=>refreshStatus());
    }
});
</script>

<footer style="width:100%;text-align:center;padding:.5rem;font-size:.62rem;color:#1a1a2e">
    clivethompson.com/opendns-remote &middot; version 2.34 — <?= date('Ymd:H:i:s') ?>
</footer>
</body>
</html>
