SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,338
Folders
408
Scanned Size
3.84 GB
PHP Files
890
Editable Text Files
12,599
File viewer
guarded to /htdocs
/gaylord/index3.php
<?php declare(strict_types=1); const GL_APP = 'GayLord'; const GL_MIN_AGE = 18; const GL_UPLOAD_MAX = 20_000_000; ini_set('display_errors', isset($_GET['debug']) ? '1' : '0'); ini_set('log_errors', '1'); error_reporting(E_ALL); $privateDir = __DIR__ . '/_gaylord_private'; $uploadDir = $privateDir . '/uploads'; foreach ([$privateDir,$uploadDir] as $dir) { if (!is_dir($dir)) @mkdir($dir, 0700, true); } session_name('GAYLORDFRESH'); session_start(); header('X-Content-Type-Options: nosniff'); header('Referrer-Policy: strict-origin-when-cross-origin'); header('Permissions-Policy: geolocation=(self), camera=(self), microphone=()'); function e(?string $v): string { return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function csrf(): string { if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(24)); return $_SESSION['csrf']; } function verify_csrf(): void { if (!hash_equals((string)($_SESSION['csrf'] ?? ''), (string)($_POST['csrf'] ?? ''))) { http_response_code(419); exit('Session expired. Reload.'); } } function redirect_to(string $url): never { header('Location: '.$url, true, 303); exit; } function flash(string $type,string $message): void { $_SESSION['flash'][]=['type'=>$type,'message'=>$message]; } function pull_flashes(): array { $x=$_SESSION['flash']??[]; unset($_SESSION['flash']); return is_array($x)?$x:[]; } function age_from_birthdate(string $birth): int { try { return (new DateTimeImmutable($birth))->diff(new DateTimeImmutable('today'))->y; } catch(Throwable) { return 0; } } function db(): PDO { static $pdo; if ($pdo instanceof PDO) return $pdo; $dsn = getenv('GAYLORD_DSN') ?: 'sqlite:' . __DIR__ . '/_gaylord_private/gaylord_fresh.sqlite'; $pdo = new PDO($dsn, getenv('GAYLORD_DB_USER') ?: null, getenv('GAYLORD_DB_PASS') ?: null, [ PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES=>false, ]); if (str_starts_with($dsn,'sqlite:')) { $pdo->exec('PRAGMA foreign_keys=ON'); $pdo->exec('PRAGMA journal_mode=WAL'); } migrate($pdo); return $pdo; } function migrate(PDO $pdo): void { $mysql=$pdo->getAttribute(PDO::ATTR_DRIVER_NAME)==='mysql'; $id=$mysql?'BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY':'INTEGER PRIMARY KEY AUTOINCREMENT'; $dt=$mysql?'TIMESTAMP DEFAULT CURRENT_TIMESTAMP':"TEXT DEFAULT (datetime('now'))"; $pdo->exec("CREATE TABLE IF NOT EXISTS users(id $id, username VARCHAR(40) UNIQUE NOT NULL, email VARCHAR(190) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, display_name VARCHAR(90) NOT NULL, birthdate VARCHAR(10) NOT NULL, created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS profiles(user_id BIGINT PRIMARY KEY, pronouns VARCHAR(50) DEFAULT '', orientation VARCHAR(60) DEFAULT '', relationship_status VARCHAR(60) DEFAULT '', relationship_structure VARCHAR(60) DEFAULT '', looking_for VARCHAR(160) DEFAULT '', personality VARCHAR(60) DEFAULT '', communication_style VARCHAR(60) DEFAULT '', social_energy VARCHAR(60) DEFAULT '', love_language VARCHAR(160) DEFAULT '', occupation VARCHAR(100) DEFAULT '', education VARCHAR(100) DEFAULT '', body_type VARCHAR(60) DEFAULT '', height VARCHAR(40) DEFAULT '', exercise VARCHAR(60) DEFAULT '', smoking VARCHAR(60) DEFAULT '', drinking VARCHAR(60) DEFAULT '', pets VARCHAR(60) DEFAULT '', children VARCHAR(60) DEFAULT '', future_children VARCHAR(60) DEFAULT '', travel_style VARCHAR(80) DEFAULT '', nightlife VARCHAR(60) DEFAULT '', about_me TEXT DEFAULT '', interests TEXT DEFAULT '', boundaries TEXT DEFAULT '', privacy_level VARCHAR(40) DEFAULT 'members', city VARCHAR(100) DEFAULT '', latitude DECIMAL(10,7) NULL, longitude DECIMAL(10,7) NULL, share_location INTEGER DEFAULT 0, theme VARCHAR(40) DEFAULT 'auto', cover_media_id BIGINT NULL, avatar_media_id BIGINT NULL)"); $pdo->exec("CREATE TABLE IF NOT EXISTS profile_answers(id $id,user_id BIGINT NOT NULL,question_key VARCHAR(80) NOT NULL,answer TEXT NOT NULL,visibility VARCHAR(30) DEFAULT 'members',updated_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS threads(id $id,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS thread_members(thread_id BIGINT NOT NULL,user_id BIGINT NOT NULL,last_read_id BIGINT DEFAULT 0,PRIMARY KEY(thread_id,user_id))"); $pdo->exec("CREATE TABLE IF NOT EXISTS messages(id $id,thread_id BIGINT NOT NULL,sender_id BIGINT NOT NULL,body TEXT NOT NULL,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS groups(id $id,owner_id BIGINT NOT NULL,name VARCHAR(100) NOT NULL,tagline VARCHAR(160) DEFAULT '',description TEXT DEFAULT '',category VARCHAR(60) DEFAULT 'Community',privacy VARCHAR(30) DEFAULT 'public',vibe VARCHAR(60) DEFAULT 'Cosmic',art_style VARCHAR(40) DEFAULT 'cosmic',image_media_id BIGINT NULL,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS group_members(group_id BIGINT NOT NULL,user_id BIGINT NOT NULL,role VARCHAR(30) DEFAULT 'member',joined_at $dt,PRIMARY KEY(group_id,user_id))"); $pdo->exec("CREATE TABLE IF NOT EXISTS group_posts(id $id,group_id BIGINT NOT NULL,user_id BIGINT NOT NULL,body TEXT NOT NULL,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS media(id $id,user_id BIGINT NOT NULL,kind VARCHAR(30) DEFAULT 'profile',storage_name VARCHAR(255) NOT NULL,original_name VARCHAR(255) NOT NULL,mime VARCHAR(90) NOT NULL,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS notifications(id $id,user_id BIGINT NOT NULL,title VARCHAR(140) NOT NULL,body VARCHAR(500) NOT NULL,is_read INTEGER DEFAULT 0,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS events(id $id,user_id BIGINT NOT NULL,title VARCHAR(140) NOT NULL,description TEXT DEFAULT '',location VARCHAR(180) DEFAULT '',starts_at VARCHAR(32) NOT NULL,ends_at VARCHAR(32) DEFAULT '',visibility VARCHAR(30) DEFAULT 'members',created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS albums(id $id,user_id BIGINT NOT NULL,title VARCHAR(140) NOT NULL,description TEXT DEFAULT '',visibility VARCHAR(30) DEFAULT 'members',cover_media_id BIGINT NULL,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS album_media(album_id BIGINT NOT NULL,media_id BIGINT NOT NULL,PRIMARY KEY(album_id,media_id))"); $pdo->exec("CREATE TABLE IF NOT EXISTS reels(id $id,user_id BIGINT NOT NULL,caption VARCHAR(500) DEFAULT '',media_id BIGINT NOT NULL,visibility VARCHAR(30) DEFAULT 'members',created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS applications(id $id,user_id BIGINT NOT NULL,name VARCHAR(120) NOT NULL,api_key VARCHAR(120) NOT NULL,callback_url VARCHAR(255) DEFAULT '',is_enabled INTEGER DEFAULT 1,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS developer_logs(id $id,user_id BIGINT NOT NULL,level VARCHAR(20) DEFAULT 'info',event_name VARCHAR(120) NOT NULL,message TEXT DEFAULT '',created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS marketplace_ads(id $id,user_id BIGINT NOT NULL,title VARCHAR(160) NOT NULL,category VARCHAR(80) DEFAULT 'General',price VARCHAR(60) DEFAULT '',location VARCHAR(160) DEFAULT '',contact VARCHAR(190) DEFAULT '',description TEXT DEFAULT '',image_media_id BIGINT NULL,status VARCHAR(30) DEFAULT 'active',created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS safety_settings(user_id BIGINT PRIMARY KEY,email_verified INTEGER DEFAULT 0,phone_verified INTEGER DEFAULT 0,selfie_verified INTEGER DEFAULT 0,government_id_verified INTEGER DEFAULT 0,photo_verified INTEGER DEFAULT 0,age_verified INTEGER DEFAULT 0,two_factor_enabled INTEGER DEFAULT 0,show_distance INTEGER DEFAULT 1,online_status INTEGER DEFAULT 1,last_seen INTEGER DEFAULT 1,incognito INTEGER DEFAULT 0,hide_from_search INTEGER DEFAULT 0,screenshot_alerts INTEGER DEFAULT 1,private_albums INTEGER DEFAULT 0,live_location INTEGER DEFAULT 0,meet_safely_mode INTEGER DEFAULT 0,safety_timer_minutes INTEGER DEFAULT 0,updated_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS trusted_contacts(id $id,user_id BIGINT NOT NULL,name VARCHAR(120) NOT NULL,contact VARCHAR(190) NOT NULL,created_at $dt)"); $pdo->exec("CREATE TABLE IF NOT EXISTS account_sessions(id $id,user_id BIGINT NOT NULL,device VARCHAR(190) DEFAULT '',ip_address VARCHAR(80) DEFAULT '',last_seen_at $dt,is_current INTEGER DEFAULT 0)"); $pdo->exec("CREATE TABLE IF NOT EXISTS app_preferences(user_id BIGINT PRIMARY KEY,notification_messages INTEGER DEFAULT 1,notification_events INTEGER DEFAULT 1,notification_marketplace INTEGER DEFAULT 1,notification_safety INTEGER DEFAULT 1,appearance_motion INTEGER DEFAULT 1,appearance_glow INTEGER DEFAULT 1,ai_assistant INTEGER DEFAULT 1,ai_suggestions INTEGER DEFAULT 1,data_personalization INTEGER DEFAULT 1,accessibility_large_text INTEGER DEFAULT 0,accessibility_high_contrast INTEGER DEFAULT 0,accessibility_reduced_motion INTEGER DEFAULT 0,updated_at $dt)"); if (!$mysql) { $pdo->exec('CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id,id)'); $pdo->exec('CREATE INDEX IF NOT EXISTS idx_group_posts_group ON group_posts(group_id,id)'); } } function current_user(): ?array { if (empty($_SESSION['uid'])) return null; $s=db()->prepare('SELECT u.*,p.* FROM users u LEFT JOIN profiles p ON p.user_id=u.id WHERE u.id=?'); $s->execute([(int)$_SESSION['uid']]); return $s->fetch() ?: null; } function require_user(): array { $u=current_user(); if(!$u){ flash('error','Please sign in first.'); redirect_to('./'); } return $u; } function media_url(int $id): string { return './?action=media&id='.$id; } function save_upload(string $field,int $uid,string $kind='profile',bool $allowVideo=false): ?int { if (empty($_FILES[$field]) || !is_array($_FILES[$field]) || (int)$_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null; $f=$_FILES[$field]; if ((int)$f['error']!==UPLOAD_ERR_OK || (int)$f['size']<=0 || (int)$f['size']>GL_UPLOAD_MAX) throw new RuntimeException('Upload failed or is too large. Maximum size is 20 MB.'); $mime=(string)(new finfo(FILEINFO_MIME_TYPE))->file((string)$f['tmp_name']); $allowed=['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp']; if($allowVideo)$allowed+=['video/mp4'=>'mp4','video/webm'=>'webm','video/quicktime'=>'mov']; if(!isset($allowed[$mime])) throw new RuntimeException($allowVideo?'Use JPEG, PNG, WebP, MP4, WebM, or MOV.':'Only JPEG, PNG, or WebP images are allowed.'); $name=bin2hex(random_bytes(20)).'.'.$allowed[$mime]; $path=$GLOBALS['uploadDir'].'/'.$name; if(!move_uploaded_file((string)$f['tmp_name'],$path)) throw new RuntimeException('Could not save the upload.'); @chmod($path,0600); $s=db()->prepare('INSERT INTO media(user_id,kind,storage_name,original_name,mime) VALUES(?,?,?,?,?)'); $s->execute([$uid,$kind,$name,basename((string)$f['name']),$mime]); return (int)db()->lastInsertId(); } $action=(string)($_GET['action']??''); if($action==='media'){ $me=require_user(); $s=db()->prepare('SELECT * FROM media WHERE id=?'); $s->execute([(int)($_GET['id']??0)]); $m=$s->fetch(); if(!$m){http_response_code(404);exit('Not found');} $path=$uploadDir.'/'.basename((string)$m['storage_name']); if(!is_file($path)){http_response_code(404);exit('Not found');} header('Content-Type: '.$m['mime']); header('Content-Length: '.filesize($path)); header('Cache-Control: private, max-age=900'); readfile($path); exit; } if($_SERVER['REQUEST_METHOD']==='POST'){ verify_csrf(); $pa=(string)($_POST['action']??''); try{ if($pa==='register'){ $username=trim((string)($_POST['username']??''));$email=strtolower(trim((string)($_POST['email']??'')));$name=trim((string)($_POST['display_name']??''));$birth=(string)($_POST['birthdate']??'');$password=(string)($_POST['password']??''); if(!preg_match('/^[A-Za-z0-9_]{3,40}$/',$username)) throw new RuntimeException('Username must be 3–40 letters, numbers, or underscores.'); if(!filter_var($email,FILTER_VALIDATE_EMAIL)) throw new RuntimeException('Enter a valid email.'); if(age_from_birthdate($birth)<GL_MIN_AGE) throw new RuntimeException('GayLord is for adults 18+ only.'); if(strlen($password)<10) throw new RuntimeException('Password must be at least 10 characters.'); $s=db()->prepare('INSERT INTO users(username,email,password_hash,display_name,birthdate) VALUES(?,?,?,?,?)'); $s->execute([$username,$email,password_hash($password,PASSWORD_DEFAULT),$name,$birth]); $uid=(int)db()->lastInsertId(); db()->prepare('INSERT INTO profiles(user_id) VALUES(?)')->execute([$uid]); db()->prepare('INSERT INTO safety_settings(user_id) VALUES(?)')->execute([$uid]); db()->prepare('INSERT INTO app_preferences(user_id) VALUES(?)')->execute([$uid]); $_SESSION['uid']=$uid; session_regenerate_id(true); redirect_to('./?view=profile'); } if($pa==='login'){ $id=strtolower(trim((string)($_POST['identity']??'')));$password=(string)($_POST['password']??''); $s=db()->prepare('SELECT * FROM users WHERE lower(email)=? OR lower(username)=? LIMIT 1');$s->execute([$id,$id]);$u=$s->fetch(); if(!$u || !password_verify($password,(string)$u['password_hash'])) throw new RuntimeException('Sign-in information did not match.'); $_SESSION['uid']=(int)$u['id'];session_regenerate_id(true);redirect_to('./?view=home'); } if($pa==='logout'){$_SESSION=[];session_destroy();redirect_to('./');} $me=require_user();$uid=(int)$me['id']; if($pa==='save_profile'){ $fields=['pronouns','orientation','relationship_status','relationship_structure','looking_for','personality','communication_style','social_energy','love_language','occupation','education','body_type','height','exercise','smoking','drinking','pets','children','future_children','travel_style','nightlife','about_me','interests','boundaries','privacy_level','city','theme']; $values=[];foreach($fields as $f)$values[$f]=trim((string)($_POST[$f]??'')); $avatar=save_upload('avatar',$uid,'avatar');$cover=save_upload('cover',$uid,'cover'); $set=[];$params=[];foreach($fields as $f){$set[]="$f=?";$params[]=$values[$f];} if($avatar){$set[]='avatar_media_id=?';$params[]=$avatar;} if($cover){$set[]='cover_media_id=?';$params[]=$cover;} $params[]=$uid;db()->prepare('UPDATE profiles SET '.implode(',',$set).' WHERE user_id=?')->execute($params); flash('success','Profile saved.');redirect_to('./?view=profile'); } if($pa==='save_interview'){ $answers=json_decode((string)($_POST['answers_json']??'{}'),true); if(!is_array($answers))$answers=[]; db()->prepare('DELETE FROM profile_answers WHERE user_id=?')->execute([$uid]); $ins=db()->prepare('INSERT INTO profile_answers(user_id,question_key,answer,visibility) VALUES(?,?,?,?)'); foreach($answers as $k=>$v){$ins->execute([$uid,(string)$k,is_array($v)?implode(', ',$v):(string)$v,'matches']);} flash('success','Guided interview saved and profile suggestions updated.');redirect_to('./?view=profile'); } if($pa==='create_group'){ $name=trim((string)($_POST['name']??'')); if($name==='') throw new RuntimeException('Group name is required.'); $image=save_upload('group_image',$uid,'group'); $s=db()->prepare('INSERT INTO groups(owner_id,name,tagline,description,category,privacy,vibe,art_style,image_media_id) VALUES(?,?,?,?,?,?,?,?,?)'); $s->execute([$uid,$name,trim((string)$_POST['tagline']),trim((string)$_POST['description']),trim((string)$_POST['category']),trim((string)$_POST['privacy']),trim((string)$_POST['vibe']),trim((string)$_POST['art_style']),$image]); $gid=(int)db()->lastInsertId();db()->prepare("INSERT INTO group_members(group_id,user_id,role) VALUES(?,?,'owner')")->execute([$gid,$uid]); flash('success','Group created.');redirect_to('./?view=groups&group='.$gid); } if($pa==='join_group'){ $gid=(int)($_POST['group_id']??0);$driver=db()->getAttribute(PDO::ATTR_DRIVER_NAME); if($driver==='mysql')db()->prepare('INSERT IGNORE INTO group_members(group_id,user_id) VALUES(?,?)')->execute([$gid,$uid]); else db()->prepare('INSERT OR IGNORE INTO group_members(group_id,user_id) VALUES(?,?)')->execute([$gid,$uid]); redirect_to('./?view=groups&group='.$gid); } if($pa==='group_post'){ $gid=(int)($_POST['group_id']??0);$body=trim((string)($_POST['body']??''));if($body==='')throw new RuntimeException('Write a post first.'); db()->prepare('INSERT INTO group_posts(group_id,user_id,body) VALUES(?,?,?)')->execute([$gid,$uid,$body]);redirect_to('./?view=groups&group='.$gid); } if($pa==='start_thread'){ $other=(int)($_POST['other_id']??0);if($other<=0||$other===$uid)throw new RuntimeException('Choose another member.'); db()->beginTransaction();db()->exec('INSERT INTO threads DEFAULT VALUES');$tid=(int)db()->lastInsertId();$s=db()->prepare('INSERT INTO thread_members(thread_id,user_id) VALUES(?,?)');$s->execute([$tid,$uid]);$s->execute([$tid,$other]);db()->commit();redirect_to('./?view=messages&thread='.$tid); } if($pa==='send_message'){ $tid=(int)($_POST['thread_id']??0);$body=trim((string)($_POST['body']??''));if($body==='')throw new RuntimeException('Message is empty.'); $s=db()->prepare('SELECT 1 FROM thread_members WHERE thread_id=? AND user_id=?');$s->execute([$tid,$uid]);if(!$s->fetchColumn())throw new RuntimeException('Conversation unavailable.'); db()->prepare('INSERT INTO messages(thread_id,sender_id,body) VALUES(?,?,?)')->execute([$tid,$uid,$body]);redirect_to('./?view=messages&thread='.$tid); } if($pa==='create_event'){ $title=trim((string)($_POST['title']??''));$starts=trim((string)($_POST['starts_at']??''));if($title===''||$starts==='')throw new RuntimeException('Event title and start time are required.'); db()->prepare('INSERT INTO events(user_id,title,description,location,starts_at,ends_at,visibility) VALUES(?,?,?,?,?,?,?)')->execute([$uid,$title,trim((string)($_POST['description']??'')),trim((string)($_POST['location']??'')),$starts,trim((string)($_POST['ends_at']??'')),trim((string)($_POST['visibility']??'members'))]); db()->prepare('INSERT INTO notifications(user_id,title,body) VALUES(?,?,?)')->execute([$uid,'Event created',$title.' was added to your calendar.']);flash('success','Event created.');redirect_to('./?view=events'); } if($pa==='create_album'){ $title=trim((string)($_POST['title']??''));if($title==='')throw new RuntimeException('Album title is required.');$cover=save_upload('cover',$uid,'album'); db()->prepare('INSERT INTO albums(user_id,title,description,visibility,cover_media_id) VALUES(?,?,?,?,?)')->execute([$uid,$title,trim((string)($_POST['description']??'')),trim((string)($_POST['visibility']??'members')),$cover]);flash('success','Album created.');redirect_to('./?view=albums'); } if($pa==='upload_reel'){ $mid=save_upload('reel',$uid,'reel',true);if(!$mid)throw new RuntimeException('Choose a reel video or image.'); db()->prepare('INSERT INTO reels(user_id,caption,media_id,visibility) VALUES(?,?,?,?)')->execute([$uid,trim((string)($_POST['caption']??'')),$mid,trim((string)($_POST['visibility']??'members'))]); db()->prepare('INSERT INTO notifications(user_id,title,body) VALUES(?,?,?)')->execute([$uid,'Reel uploaded','Your reel is now available.']);flash('success','Reel uploaded.');redirect_to('./?view=reels'); } if($pa==='mark_alerts_read'){db()->prepare('UPDATE notifications SET is_read=1 WHERE user_id=?')->execute([$uid]);redirect_to('./?view=notifications');} if($pa==='save_settings'){ $theme=trim((string)($_POST['theme']??'auto'));$privacy=trim((string)($_POST['privacy_level']??'members'));db()->prepare('UPDATE profiles SET theme=?,privacy_level=? WHERE user_id=?')->execute([$theme,$privacy,$uid]);flash('success','Application settings saved.');redirect_to('./?view=settings'); } if($pa==='create_application'){ $name=trim((string)($_POST['name']??''));if($name==='')throw new RuntimeException('Application name is required.');$key='gl_'.bin2hex(random_bytes(24)); db()->prepare('INSERT INTO applications(user_id,name,api_key,callback_url) VALUES(?,?,?,?)')->execute([$uid,$name,$key,trim((string)($_POST['callback_url']??''))]);flash('success','Application created. Copy its API key below.');redirect_to('./?view=developer'); } if($pa==='developer_message'){ $event=trim((string)($_POST['event_name']??''));$message=trim((string)($_POST['message']??''));if($event===''||$message==='')throw new RuntimeException('Event name and message are required.'); db()->prepare('INSERT INTO developer_logs(user_id,level,event_name,message) VALUES(?,?,?,?)')->execute([$uid,trim((string)($_POST['level']??'info')),$event,$message]); db()->prepare('INSERT INTO notifications(user_id,title,body) VALUES(?,?,?)')->execute([$uid,'Developer event: '.$event,$message]);flash('success','Developer message sent and event logged.');redirect_to('./?view=developer'); } if($pa==='create_marketplace_ad'){ $title=trim((string)($_POST['title']??'')); if($title==='') throw new RuntimeException('Ad title is required.'); $image=save_upload('ad_image',$uid,'marketplace'); db()->prepare('INSERT INTO marketplace_ads(user_id,title,category,price,location,contact,description,image_media_id) VALUES(?,?,?,?,?,?,?,?)')->execute([ $uid,$title,trim((string)($_POST['category']??'General')),trim((string)($_POST['price']??'')), trim((string)($_POST['location']??'')),trim((string)($_POST['contact']??'')), trim((string)($_POST['description']??'')),$image ]); db()->prepare('INSERT INTO notifications(user_id,title,body) VALUES(?,?,?)')->execute([$uid,'Marketplace ad posted',$title.' is now live.']); flash('success','Marketplace ad posted.');redirect_to('./?view=marketplace'); } if($pa==='save_safety'){ $bools=['email_verified','phone_verified','selfie_verified','government_id_verified','photo_verified','age_verified','two_factor_enabled','show_distance','online_status','last_seen','incognito','hide_from_search','screenshot_alerts','private_albums','live_location','meet_safely_mode']; $vals=[]; foreach($bools as $b) $vals[$b]=isset($_POST[$b])?1:0; $timer=max(0,min(240,(int)($_POST['safety_timer_minutes']??0))); $driver=db()->getAttribute(PDO::ATTR_DRIVER_NAME); if($driver==='mysql'){ $sql='INSERT INTO safety_settings(user_id,'.implode(',',$bools).',safety_timer_minutes) VALUES('.implode(',',array_fill(0,count($bools)+2,'?')).') ON DUPLICATE KEY UPDATE '.implode(',',array_map(fn($b)=>"$b=VALUES($b)",$bools)).',safety_timer_minutes=VALUES(safety_timer_minutes)'; }else{ $sql='INSERT INTO safety_settings(user_id,'.implode(',',$bools).',safety_timer_minutes) VALUES('.implode(',',array_fill(0,count($bools)+2,'?')).') ON CONFLICT(user_id) DO UPDATE SET '.implode(',',array_map(fn($b)=>"$b=excluded.$b",$bools)).',safety_timer_minutes=excluded.safety_timer_minutes'; } db()->prepare($sql)->execute(array_merge([$uid],array_values($vals),[$timer])); flash('success','Safety preferences saved.');redirect_to('./?view=safety'); } if($pa==='add_trusted_contact'){ $name=trim((string)($_POST['name']??''));$contact=trim((string)($_POST['contact']??'')); if($name===''||$contact==='') throw new RuntimeException('Trusted contact name and phone or email are required.'); db()->prepare('INSERT INTO trusted_contacts(user_id,name,contact) VALUES(?,?,?)')->execute([$uid,$name,$contact]); flash('success','Trusted contact added.');redirect_to('./?view=safety'); } if($pa==='panic_exit'){ redirect_to('https://www.google.com/'); } if($pa==='save_central_settings'){ $theme=trim((string)($_POST['theme']??'auto')); $privacy=trim((string)($_POST['privacy_level']??'members')); db()->prepare('UPDATE profiles SET theme=?,privacy_level=? WHERE user_id=?')->execute([$theme,$privacy,$uid]); $fields=['notification_messages','notification_events','notification_marketplace','notification_safety','appearance_motion','appearance_glow','ai_assistant','ai_suggestions','data_personalization','accessibility_large_text','accessibility_high_contrast','accessibility_reduced_motion']; $values=[];foreach($fields as $f)$values[$f]=isset($_POST[$f])?1:0; $driver=db()->getAttribute(PDO::ATTR_DRIVER_NAME); if($driver==='mysql'){ $sql='INSERT INTO app_preferences(user_id,'.implode(',',$fields).') VALUES('.implode(',',array_fill(0,count($fields)+1,'?')).') ON DUPLICATE KEY UPDATE '.implode(',',array_map(fn($f)=>"$f=VALUES($f)",$fields)); }else{ $sql='INSERT INTO app_preferences(user_id,'.implode(',',$fields).') VALUES('.implode(',',array_fill(0,count($fields)+1,'?')).') ON CONFLICT(user_id) DO UPDATE SET '.implode(',',array_map(fn($f)=>"$f=excluded.$f",$fields)); } db()->prepare($sql)->execute(array_merge([$uid],array_values($values))); flash('success','Settings saved.');redirect_to('./?view=settings'); } }catch(Throwable $ex){flash('error',$ex->getMessage());redirect_to($_SERVER['HTTP_REFERER']??'./');} } $me=current_user();$flashes=pull_flashes();$view=(string)($_GET['view']??($me?'home':'welcome')); $allowed=['welcome','home','discover','messages','groups','events','marketplace','albums','reels','spatial','profile','progress','notifications','settings','safety','developer'];if(!in_array($view,$allowed,true))$view=$me?'home':'welcome'; $users=[];$groups=[];$threads=[];$threadMessages=[];$selectedThread=0;$selectedGroup=null;$groupPosts=[];$events=[];$albums=[];$reels=[];$alerts=[];$applications=[];$devLogs=[];$marketAds=[];$safety=[];$trustedContacts=[];$sessions=[];$preferences=[];$avatarUrl='';$coverUrl=''; if($me){ $uid=(int)$me['id']; $users=db()->query("SELECT u.id,u.display_name,u.username,p.city,p.looking_for,p.personality,p.avatar_media_id FROM users u LEFT JOIN profiles p ON p.user_id=u.id WHERE u.id<>$uid ORDER BY u.id DESC LIMIT 24")->fetchAll(); $groups=db()->query("SELECT g.*,u.display_name owner_name,(SELECT COUNT(*) FROM group_members gm WHERE gm.group_id=g.id) member_count FROM groups g JOIN users u ON u.id=g.owner_id ORDER BY g.id DESC LIMIT 50")->fetchAll(); $s=db()->prepare("SELECT t.id,MAX(m.created_at) last_message,(SELECT body FROM messages m2 WHERE m2.thread_id=t.id ORDER BY m2.id DESC LIMIT 1) last_body,(SELECT u.display_name FROM thread_members tm2 JOIN users u ON u.id=tm2.user_id WHERE tm2.thread_id=t.id AND tm2.user_id<>? LIMIT 1) other_name FROM threads t JOIN thread_members tm ON tm.thread_id=t.id LEFT JOIN messages m ON m.thread_id=t.id WHERE tm.user_id=? GROUP BY t.id ORDER BY last_message DESC");$s->execute([$uid,$uid]);$threads=$s->fetchAll(); $selectedThread=(int)($_GET['thread']??($threads[0]['id']??0)); if($selectedThread){$s=db()->prepare('SELECT m.*,u.display_name FROM messages m JOIN users u ON u.id=m.sender_id WHERE m.thread_id=? ORDER BY m.id');$s->execute([$selectedThread]);$threadMessages=$s->fetchAll();} $gid=(int)($_GET['group']??0);if($gid){$s=db()->prepare('SELECT g.*,u.display_name owner_name,(SELECT COUNT(*) FROM group_members gm WHERE gm.group_id=g.id) member_count FROM groups g JOIN users u ON u.id=g.owner_id WHERE g.id=?');$s->execute([$gid]);$selectedGroup=$s->fetch()?:null;if($selectedGroup){$s=db()->prepare('SELECT gp.*,u.display_name FROM group_posts gp JOIN users u ON u.id=gp.user_id WHERE gp.group_id=? ORDER BY gp.id DESC');$s->execute([$gid]);$groupPosts=$s->fetchAll();}} $s=db()->prepare('SELECT * FROM events WHERE user_id=? ORDER BY starts_at ASC');$s->execute([$uid]);$events=$s->fetchAll(); $s=db()->prepare('SELECT * FROM albums WHERE user_id=? ORDER BY id DESC');$s->execute([$uid]);$albums=$s->fetchAll(); $s=db()->prepare('SELECT r.*,m.mime FROM reels r JOIN media m ON m.id=r.media_id WHERE r.user_id=? ORDER BY r.id DESC');$s->execute([$uid]);$reels=$s->fetchAll(); $s=db()->prepare('SELECT * FROM notifications WHERE user_id=? ORDER BY id DESC LIMIT 100');$s->execute([$uid]);$alerts=$s->fetchAll(); $s=db()->prepare('SELECT * FROM applications WHERE user_id=? ORDER BY id DESC');$s->execute([$uid]);$applications=$s->fetchAll(); $s=db()->prepare('SELECT * FROM developer_logs WHERE user_id=? ORDER BY id DESC LIMIT 100');$s->execute([$uid]);$devLogs=$s->fetchAll(); $s=db()->prepare('SELECT * FROM marketplace_ads ORDER BY id DESC LIMIT 100');$s->execute();$marketAds=$s->fetchAll(); $s=db()->prepare('SELECT * FROM safety_settings WHERE user_id=?');$s->execute([$uid]);$safety=$s->fetch()?:[]; if(!$safety){db()->prepare('INSERT INTO safety_settings(user_id) VALUES(?)')->execute([$uid]);$safety=['user_id'=>$uid];} $s=db()->prepare('SELECT * FROM trusted_contacts WHERE user_id=? ORDER BY id DESC');$s->execute([$uid]);$trustedContacts=$s->fetchAll(); $s=db()->prepare('SELECT * FROM account_sessions WHERE user_id=? ORDER BY id DESC LIMIT 20');$s->execute([$uid]);$sessions=$s->fetchAll(); if(!$sessions){db()->prepare('INSERT INTO account_sessions(user_id,device,ip_address,is_current) VALUES(?,?,?,1)')->execute([$uid,(string)($_SERVER['HTTP_USER_AGENT']??'Current browser'),(string)($_SERVER['REMOTE_ADDR']??'')]);$s=db()->prepare('SELECT * FROM account_sessions WHERE user_id=? ORDER BY id DESC LIMIT 20');$s->execute([$uid]);$sessions=$s->fetchAll();} $s=db()->prepare('SELECT * FROM app_preferences WHERE user_id=?');$s->execute([$uid]);$preferences=$s->fetch()?:[]; if(!$preferences){db()->prepare('INSERT INTO app_preferences(user_id) VALUES(?)')->execute([$uid]);$preferences=['user_id'=>$uid];} if(!empty($me['avatar_media_id']))$avatarUrl=media_url((int)$me['avatar_media_id']); if(!empty($me['cover_media_id']))$coverUrl=media_url((int)$me['cover_media_id']); } ?> <!doctype html> <html lang="en" data-theme="<?=e($me['theme']??'auto')?>"> <head> <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#090914"> <title><?=e(GL_APP)?> Spatial</title> <link rel="preconnect" href="https://unpkg.com"><link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"> <style> :root{color-scheme:dark;--bg:#050711;--panel:rgba(14,18,32,.72);--panel2:rgba(24,29,49,.62);--line:rgba(255,255,255,.13);--text:#f7f9ff;--muted:#9ca9c1;--a1:#6f5cff;--a2:#2fd8ff;--a3:#ff4fa3;--a4:#ffd85a;--good:#56e58a;--bad:#ff647d;--nav:240px;--top:72px;--safe:env(safe-area-inset-bottom);--radius:24px} *{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}button,input,textarea,select{font:inherit}a{color:inherit;text-decoration:none}button{cursor:pointer}.muted{color:var(--muted)} .bg{position:fixed;inset:0;z-index:-5;background:radial-gradient(circle at 15% 12%,color-mix(in srgb,var(--a2) 18%,transparent),transparent 30%),radial-gradient(circle at 82% 18%,color-mix(in srgb,var(--a1) 22%,transparent),transparent 34%),radial-gradient(circle at 50% 90%,color-mix(in srgb,var(--a3) 16%,transparent),transparent 40%),linear-gradient(150deg,#03040a,#080b18 55%,#060612);overflow:hidden}.bg:before{content:"";position:absolute;inset:-30%;background:conic-gradient(from 20deg,transparent,color-mix(in srgb,var(--a1) 10%,transparent),transparent,color-mix(in srgb,var(--a3) 10%,transparent),transparent);animation:spin 35s linear infinite}.bg:after{content:"";position:absolute;inset:0;background-image:radial-gradient(2px 2px at 10% 20%,#fff,transparent 70%),radial-gradient(1.5px 1.5px at 70% 25%,var(--a2),transparent 70%),radial-gradient(2px 2px at 85% 70%,var(--a4),transparent 70%);background-size:240px 240px,320px 320px,420px 420px;opacity:.35;animation:drift 28s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@keyframes drift{to{transform:translateY(60px)}} .app{display:grid;grid-template-columns:var(--nav) minmax(0,1fr);grid-template-rows:var(--top) minmax(0,1fr);height:100%}.side{grid-row:1/3;padding:15px 12px;display:flex;flex-direction:column;gap:12px;background:rgba(5,8,16,.9);backdrop-filter:blur(22px);border-right:1px solid var(--line);overflow:auto}.brandbox{display:flex;align-items:center;gap:11px;padding:5px 5px 16px;border-bottom:1px solid var(--line)}.mark{width:50px;height:50px;border-radius:17px;display:grid;place-items:center;background:linear-gradient(145deg,var(--a4),var(--a3),var(--a2));color:#08080c;font-weight:1000;box-shadow:0 0 32px color-mix(in srgb,var(--a2) 25%,transparent)}.brandname{font-size:20px;font-weight:1000}.nav{display:grid;gap:5px}.nav a{display:grid;grid-template-columns:40px 1fr;gap:9px;align-items:center;padding:9px 10px;border-radius:15px;color:#a9b3c5;border:1px solid transparent}.nav a:hover,.nav a.active{color:#fff;background:linear-gradient(90deg,color-mix(in srgb,var(--a1) 20%,transparent),color-mix(in srgb,var(--a2) 8%,transparent));border-color:color-mix(in srgb,var(--a1) 35%,transparent);box-shadow:inset 3px 0 0 var(--a4)}.ni{width:36px;height:36px;border-radius:12px;display:grid;place-items:center;background:rgba(255,255,255,.06);color:var(--a4);font-weight:900}.sidefoot{margin-top:auto;padding:12px;border-top:1px solid var(--line);font-size:12px;color:var(--muted)} .top{grid-column:2;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 20px;background:rgba(6,9,17,.68);backdrop-filter:blur(22px);border-bottom:1px solid var(--line)}.wordmark{font-size:28px;font-weight:1000;background:linear-gradient(90deg,var(--a4),var(--a3),var(--a1),var(--a2));-webkit-background-clip:text;color:transparent;background-size:220% auto;animation:flow 8s linear infinite}@keyframes flow{to{background-position:220% 0}}.topactions{display:flex;align-items:center;gap:8px}.chip,.btn{border:1px solid var(--line);background:rgba(18,24,41,.72);color:#fff;border-radius:14px;padding:10px 13px;font-weight:800}.btn.primary{background:linear-gradient(135deg,var(--a4),color-mix(in srgb,var(--a4) 70%,var(--a3)));color:#121216;border:0}.btn.accent{background:linear-gradient(135deg,var(--a1),var(--a2));border:0}.workspace{grid-column:2;min-width:0;min-height:0;overflow:auto;padding:clamp(14px,2vw,28px)}.glass{background:linear-gradient(180deg,rgba(22,28,47,.74),rgba(9,13,24,.64));border:1px solid var(--line);border-radius:var(--radius);backdrop-filter:blur(22px) saturate(135%);box-shadow:0 26px 80px rgba(0,0,0,.34)}.section{padding:20px}.hero{min-height:290px;padding:clamp(26px,4vw,54px);position:relative;overflow:hidden;background:linear-gradient(145deg,color-mix(in srgb,var(--a1) 25%,rgba(7,10,18,.92)),color-mix(in srgb,var(--a3) 18%,rgba(7,10,18,.9)))}.hero:after{content:"∞";position:absolute;right:3%;bottom:-16%;font-size:min(30vw,420px);font-weight:1000;color:rgba(255,255,255,.035)}.hero h1{font-size:clamp(44px,7vw,96px);line-height:.9;letter-spacing:-.065em;margin:10px 0 16px}.hero p{max-width:760px;color:#c7d0df;font-size:18px}.kicker{font-size:11px;font-weight:900;letter-spacing:.16em;color:var(--a2)}.grid{display:grid;gap:16px}.g4{grid-template-columns:repeat(4,minmax(0,1fr))}.g3{grid-template-columns:repeat(3,minmax(0,1fr))}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}.sectionhead{display:flex;align-items:end;justify-content:space-between;gap:16px;margin:24px 0 13px}.sectionhead h2{margin:0;font-size:clamp(25px,3vw,40px)}.metric{font-size:42px;font-weight:1000}.card{padding:18px;min-width:0}.field{display:grid;gap:7px;margin:12px 0}.field label{font-size:13px;font-weight:800}.field input,.field textarea,.field select{width:100%;border:1px solid var(--line);background:rgba(5,8,15,.72);color:#fff;border-radius:14px;padding:12px}.field textarea{min-height:100px;resize:vertical}.formgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.profilehero{position:relative;min-height:390px;overflow:hidden}.profilecover{position:absolute;inset:0;background:radial-gradient(circle at 70% 30%,color-mix(in srgb,var(--a2) 36%,transparent),transparent 30%),linear-gradient(145deg,#18172a,#08101b)}.profilecover img{width:100%;height:100%;object-fit:cover;filter:saturate(1.2) contrast(1.04);opacity:.72}.profilecover:after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,transparent 20%,rgba(5,7,13,.92) 92%)}.profileblend{position:absolute;left:clamp(22px,4vw,52px);bottom:28px;display:flex;align-items:end;gap:20px}.portrait{width:clamp(150px,18vw,240px);aspect-ratio:4/5;border-radius:42% 42% 26px 26px;overflow:hidden;background:linear-gradient(145deg,var(--a3),var(--a1),var(--a2));box-shadow:0 0 0 4px rgba(255,255,255,.3),0 0 60px color-mix(in srgb,var(--a2) 36%,transparent);mask-image:linear-gradient(to bottom,#000 76%,transparent 100%)}.portrait img{width:100%;height:100%;object-fit:cover}.profilecopy h1{font-size:clamp(36px,5vw,68px);margin:0}.tabs{display:flex;gap:8px;overflow:auto;padding-bottom:4px}.tabs a,.tabs button{white-space:nowrap;border:1px solid var(--line);background:rgba(255,255,255,.05);color:#dbe2ef;padding:9px 12px;border-radius:999px}.tabs .active{background:linear-gradient(135deg,var(--a1),var(--a2));border:0}.profilelayout{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(300px,.6fr);gap:16px}.profileart{min-height:320px;display:grid;place-items:center;position:relative;overflow:hidden}.profileart:before{content:"";position:absolute;width:85%;height:55%;border:4px solid var(--a4);border-radius:50%;transform:rotate(12deg);box-shadow:0 0 35px var(--a4),0 0 70px var(--a2);filter:blur(.3px)}.profileart:after{content:"∞";font-size:160px;color:transparent;-webkit-text-stroke:3px var(--a2);filter:drop-shadow(0 0 20px var(--a2))}.interview{max-width:760px;margin:auto;padding:26px}.question{font-size:clamp(28px,4vw,48px);line-height:1.05;margin:10px 0 24px}.answers{display:grid;gap:10px}.answer{padding:14px;border-radius:16px;border:1px solid var(--line);background:rgba(255,255,255,.05);text-align:left}.answer.selected{background:linear-gradient(135deg,color-mix(in srgb,var(--a1) 40%,transparent),color-mix(in srgb,var(--a2) 22%,transparent));border-color:var(--a2)}.progressbar{height:8px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden}.progressbar i{display:block;height:100%;background:linear-gradient(90deg,var(--a1),var(--a2),var(--a3));width:0}.profilegrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}.person{min-height:320px;position:relative;overflow:hidden;padding:0}.personart{height:100%;background:linear-gradient(145deg,var(--a1),var(--a2));position:absolute;inset:0}.personart img{width:100%;height:100%;object-fit:cover}.personart:after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,transparent 35%,rgba(3,5,10,.95) 92%)}.personinfo{position:absolute;left:16px;right:16px;bottom:15px}.groupgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}.groupcard{overflow:hidden;padding:0}.groupart{height:180px;position:relative;background:linear-gradient(135deg,var(--a1),var(--a3),var(--a2));overflow:hidden}.groupart img{width:100%;height:100%;object-fit:cover}.groupart.cosmic:after{content:"";position:absolute;inset:0;background:radial-gradient(circle at 30% 30%,rgba(255,255,255,.25),transparent 20%),conic-gradient(from 120deg,transparent,rgba(255,255,255,.18),transparent);mix-blend-mode:screen;animation:spin 14s linear infinite}.groupbody{padding:16px}.chatlayout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:14px;min-height:620px}.threadlist{padding:10px;overflow:auto}.thread{display:block;padding:12px;border-radius:15px;border:1px solid var(--line);background:rgba(255,255,255,.04);margin-bottom:8px}.thread.active{border-color:var(--a2);background:color-mix(in srgb,var(--a1) 18%,transparent)}.chat{display:flex;flex-direction:column;overflow:hidden}.messages{flex:1;overflow:auto;padding:18px;display:flex;flex-direction:column;gap:10px}.bubble{max-width:76%;padding:11px 13px;border-radius:17px;background:rgba(255,255,255,.09)}.bubble.mine{align-self:flex-end;background:linear-gradient(135deg,var(--a1),var(--a2));color:#fff}.composer{display:flex;gap:8px;padding:12px;border-top:1px solid var(--line)}.composer textarea{flex:1;min-height:52px;max-height:120px;border:1px solid var(--line);background:rgba(5,8,15,.75);color:#fff;border-radius:14px;padding:12px}.mapwrap{height:620px;position:relative;overflow:hidden}.map{height:100%;background:#07111a}.mapcontrols{position:absolute;z-index:600;left:14px;top:14px;right:14px;display:flex;justify-content:space-between;gap:10px;pointer-events:none}.mapcontrols>*{pointer-events:auto}.mapbottom{position:absolute;z-index:600;left:14px;right:14px;bottom:14px;display:flex;gap:10px;overflow:auto;padding:10px;background:rgba(6,10,18,.72);backdrop-filter:blur(18px);border:1px solid var(--line);border-radius:18px}.mapmember{flex:0 0 190px;padding:10px;border-radius:15px;background:rgba(255,255,255,.07)}.screensaver{position:fixed;inset:0;z-index:5000;display:none;place-items:center;background:radial-gradient(circle at 50% 45%,color-mix(in srgb,var(--a1) 30%,#02030a),#02030a 70%);overflow:hidden}.screensaver.active{display:grid}.ssorb{width:min(58vw,580px);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle at 32% 25%,#fff,var(--a2) 7%,var(--a1) 32%,var(--a3) 57%,#03040a 75%);box-shadow:0 0 80px var(--a2),0 0 160px color-mix(in srgb,var(--a3) 50%,transparent);animation:ssfloat 5s ease-in-out infinite}.ssorb span{font-size:clamp(80px,15vw,180px);font-weight:1000}.sstime{position:absolute;bottom:8%;text-align:center}.sstime strong{font-size:clamp(40px,8vw,100px);display:block}.sshint{position:absolute;top:4%;font-size:12px;letter-spacing:.16em;color:#b7c2d5}@keyframes ssfloat{50%{transform:translateY(-12px) rotateY(10deg);filter:hue-rotate(130deg)}}.flash{padding:12px 14px;border-radius:14px;margin-bottom:12px;background:rgba(77,212,120,.14);border:1px solid rgba(77,212,120,.34)}.flash.error{background:rgba(255,80,100,.14);border-color:rgba(255,80,100,.34)} @media(max-width:1100px){.g4{grid-template-columns:repeat(2,minmax(0,1fr))}.g3{grid-template-columns:repeat(2,minmax(0,1fr))}.profilelayout{grid-template-columns:1fr}.formgrid{grid-template-columns:repeat(2,minmax(0,1fr))}} @media(max-width:900px){:root{--nav:0px;--top:60px}.app{grid-template-columns:1fr;grid-template-rows:var(--top) minmax(0,1fr) 78px}.top{grid-column:1}.workspace{grid-column:1;padding:10px 10px 18px}.side{grid-column:1;grid-row:3;flex-direction:row;padding:7px;overflow-x:auto;overflow-y:hidden;border-right:0;border-top:1px solid var(--line)}.brandbox,.sidefoot{display:none}.nav{display:flex}.nav a{flex:0 0 72px;display:grid;grid-template-columns:1fr;grid-template-rows:36px auto;place-items:center;gap:2px;padding:5px;font-size:9px}.ni{width:34px;height:34px}.nav a.active{box-shadow:inset 0 3px 0 var(--a4)}.topactions .chip{display:none}.hero{min-height:340px}.chatlayout{grid-template-columns:1fr}.threadlist{max-height:220px}.profilehero{min-height:500px}.profileblend{left:18px;right:18px;bottom:22px;display:grid;grid-template-columns:130px 1fr}.portrait{width:130px}.mapwrap{height:62dvh;min-height:470px}} @media(max-width:620px){.g4,.g3,.g2,.formgrid{grid-template-columns:1fr}.wordmark{font-size:23px}.top{padding:0 10px}.topactions .btn{padding:8px 10px;font-size:12px}.hero h1{font-size:48px}.section{padding:14px}.profilegrid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.person{min-height:265px}.groupgrid{grid-template-columns:1fr}.profileblend{grid-template-columns:110px 1fr;gap:12px}.portrait{width:110px}.profilecopy h1{font-size:34px}.mapwrap{height:58dvh;min-height:420px}} .media-preview{width:100%;aspect-ratio:16/10;object-fit:cover;border-radius:18px;background:#050711}.eventrow{display:grid;grid-template-columns:120px minmax(0,1fr) auto;gap:16px;align-items:center}.eventdate{font-size:20px;font-weight:1000}.statusdot{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--good);margin-right:7px}.codebox{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all;background:#050711;border:1px solid var(--line);padding:12px;border-radius:12px}.logline{border-left:3px solid var(--a2);padding:10px 12px;margin:8px 0;background:rgba(255,255,255,.04);border-radius:0 12px 12px 0}@media(max-width:620px){.eventrow{grid-template-columns:1fr}.eventrow .btn{width:100%}} @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}} /* 2026-07 animated brand + iPhone readability pass */ :root{ --tap:48px; --mobile-font:16px; } /* Animated GayLord identity */ .wordmark,.brandname{position:relative;isolation:isolate;display:inline-flex;align-items:center;letter-spacing:-.055em;transform-style:preserve-3d} .wordmark span,.brandname span{display:inline-block;background:linear-gradient(180deg,#fff 0 14%,var(--a4) 28%,var(--a3) 48%,var(--a1) 68%,var(--a2) 88%,#fff 100%);background-size:100% 240%;-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 10px color-mix(in srgb,var(--a2) 32%,transparent));animation:glBrandWave 3.8s ease-in-out infinite,glBrandColor 8s linear infinite;transform-origin:50% 85%} .wordmark span:nth-child(2),.brandname span:nth-child(2){animation-delay:-.22s}.wordmark span:nth-child(3),.brandname span:nth-child(3){animation-delay:-.44s}.wordmark span:nth-child(4),.brandname span:nth-child(4){animation-delay:-.66s}.wordmark span:nth-child(5),.brandname span:nth-child(5){animation-delay:-.88s}.wordmark span:nth-child(6),.brandname span:nth-child(6){animation-delay:-1.1s}.wordmark span:nth-child(7),.brandname span:nth-child(7){animation-delay:-1.32s} .wordmark:after{content:"";position:absolute;left:-4%;right:-4%;bottom:-7px;height:3px;border-radius:999px;background:linear-gradient(90deg,transparent,var(--a4),var(--a3),var(--a1),var(--a2),transparent);background-size:220% 100%;box-shadow:0 0 18px color-mix(in srgb,var(--a2) 48%,transparent);animation:glBrandLine 4.6s linear infinite} @keyframes glBrandWave{0%,100%{transform:translate3d(0,0,0) rotateX(0) scale(1)}45%{transform:translate3d(0,-3px,18px) rotateX(10deg) scale(1.035)}60%{transform:translate3d(0,1px,0) rotateX(-4deg) scale(.995)}} @keyframes glBrandColor{to{background-position:0 240%}} @keyframes glBrandLine{to{background-position:220% 0}} .brandbox{position:relative;overflow:hidden;border-radius:20px;background:linear-gradient(145deg,rgba(255,255,255,.045),rgba(255,255,255,.008));box-shadow:inset 0 0 0 1px rgba(255,255,255,.04)} .brandbox:after{content:"";position:absolute;inset:-60% -35%;pointer-events:none;background:linear-gradient(110deg,transparent 38%,rgba(255,255,255,.22) 50%,transparent 62%);transform:translateX(-75%) rotate(8deg);animation:glBrandSweep 5.2s ease-in-out infinite} @keyframes glBrandSweep{0%,45%{transform:translateX(-75%) rotate(8deg)}75%,100%{transform:translateX(78%) rotate(8deg)}} .mark{position:relative;isolation:isolate;overflow:visible;animation:glMarkFloat 3.6s ease-in-out infinite,glMarkHue 13s linear infinite} .mark:before,.mark:after{content:"";position:absolute;inset:-7px;border-radius:23px;border:1px solid color-mix(in srgb,var(--a2) 55%,transparent);box-shadow:0 0 18px color-mix(in srgb,var(--a2) 35%,transparent);animation:glMarkOrbit 4.4s linear infinite;pointer-events:none} .mark:after{inset:-12px;border-color:color-mix(in srgb,var(--a4) 48%,transparent);animation-direction:reverse;animation-duration:6.2s} @keyframes glMarkFloat{50%{transform:translateY(-3px) rotateZ(2deg) scale(1.04)}} @keyframes glMarkHue{to{filter:hue-rotate(360deg)}} @keyframes glMarkOrbit{to{transform:rotate(360deg)}} /* More motion, without compromising layout */ .glass{position:relative;overflow:hidden;transition:transform .28s ease,border-color .28s ease,box-shadow .28s ease} .glass:before{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(120deg,rgba(255,255,255,.07),transparent 26%,transparent 72%,color-mix(in srgb,var(--a2) 10%,transparent));mix-blend-mode:screen;opacity:.6} @media(hover:hover){.glass:hover{transform:translateY(-3px);border-color:color-mix(in srgb,var(--a2) 30%,var(--line));box-shadow:0 32px 90px rgba(0,0,0,.42),0 0 36px color-mix(in srgb,var(--a1) 12%,transparent)}} .hero{background-size:180% 180%;animation:glHeroShift 16s ease-in-out infinite alternate} .hero:after{animation:glInfinityFloat 8s ease-in-out infinite} @keyframes glHeroShift{to{background-position:100% 100%}} @keyframes glInfinityFloat{50%{transform:translateY(-14px) rotate(-5deg);filter:drop-shadow(0 0 24px color-mix(in srgb,var(--a2) 30%,transparent))}} .profileart:before{animation:glCompatOrbit 8s linear infinite;transform-origin:center} .profileart:after{animation:glInfinityPulse 3.4s ease-in-out infinite} @keyframes glCompatOrbit{to{transform:rotate(372deg)}} @keyframes glInfinityPulse{50%{transform:scale(1.08);filter:drop-shadow(0 0 34px var(--a2)) drop-shadow(0 0 20px var(--a4))}} /* Accessible touch targets everywhere */ .btn,.chip,.tabs a,.tabs button,.answer{min-height:var(--tap);display:inline-flex;align-items:center;justify-content:center;line-height:1.2} .field input,.field select{min-height:52px}.field textarea{font-size:16px;line-height:1.45}.field input,.field select{font-size:16px} .nav a{min-height:48px}.ni{flex:0 0 auto} /* iPhone / narrow mobile: larger type, buttons and single-column cards */ @media(max-width:900px){ :root{--top:70px;--tap:50px} .app{grid-template-rows:var(--top) minmax(0,1fr) calc(90px + var(--safe))} .top{padding:0 12px;padding-top:env(safe-area-inset-top);min-height:var(--top)} .workspace{padding:14px 12px 24px;font-size:var(--mobile-font);line-height:1.45} .side{height:calc(90px + var(--safe));padding:8px 8px calc(8px + var(--safe));gap:6px} .nav a{flex-basis:82px;grid-template-rows:42px auto;min-height:72px;font-size:11px;font-weight:800;line-height:1.05;padding:6px 4px} .ni{width:42px;height:42px;border-radius:14px;font-size:15px} .topactions{gap:7px} .topactions .btn{min-width:50px;padding:11px 14px;font-size:14px} .wordmark{font-size:28px} .sectionhead{align-items:flex-start;gap:12px}.sectionhead h2{font-size:clamp(30px,8vw,40px)} .muted{font-size:15px;line-height:1.45;color:#b9c4d8} .kicker{font-size:12px;letter-spacing:.14em} .btn{padding:13px 16px;font-size:15px;border-radius:15px} .chip{padding:12px 14px;font-size:14px} .field{gap:9px;margin:15px 0}.field label{font-size:15px;line-height:1.25}.field input,.field select,.field textarea{padding:14px 15px;border-radius:16px} .tabs{gap:10px;padding-bottom:8px}.tabs a,.tabs button{padding:12px 16px;font-size:15px} .hero p{font-size:17px;line-height:1.5} .profilehero{min-height:540px} .profileblend{align-items:end} .profilecopy p{font-size:15px;line-height:1.45} .chatlayout{min-height:calc(100dvh - 190px)} .composer{padding:12px;gap:10px}.composer textarea{font-size:16px;min-height:56px} .mapcontrols{left:10px;right:10px;top:10px}.mapbottom{left:10px;right:10px;bottom:10px} } @media(max-width:620px){ :root{--radius:22px} .top{padding-inline:10px}.wordmark{font-size:26px}.topactions .btn{font-size:13px;padding:10px 12px} .workspace{padding:12px 10px 22px} .section{padding:17px}.card{padding:17px} .hero{padding:24px 20px;min-height:390px}.hero h1{font-size:clamp(44px,14vw,62px);line-height:.94}.hero p{font-size:16px} .profilegrid{grid-template-columns:1fr;gap:14px}.person{min-height:430px}.personinfo{left:18px;right:18px;bottom:18px}.personinfo h3{font-size:26px;margin-bottom:8px} .groupgrid{grid-template-columns:1fr}.groupart{height:250px}.groupbody h3{font-size:24px}.groupbody{padding:18px} .profileblend{grid-template-columns:120px minmax(0,1fr);left:16px;right:16px;bottom:18px}.portrait{width:120px}.profilecopy h1{font-size:38px}.profilecopy .chip{margin-top:8px} .question{font-size:34px}.answer{font-size:16px;padding:15px 16px;justify-content:flex-start} .thread{padding:14px}.bubble{max-width:88%;font-size:16px;line-height:1.45} .mapmember{flex-basis:220px;padding:12px} } @media(max-width:390px){ .wordmark{font-size:24px}.topactions .btn{padding:9px 10px;min-width:46px} .nav a{flex-basis:76px;font-size:10px} .profileblend{grid-template-columns:96px minmax(0,1fr)}.portrait{width:96px}.profilecopy h1{font-size:32px} } @supports(-webkit-touch-callout:none){ .workspace,.side{-webkit-overflow-scrolling:touch} input,select,textarea{font-size:16px!important} } /* SIMON cognitive sphere: launch, living menu background, and screensaver */ .simon-canvas{position:fixed;inset:0;width:100%;height:100%;display:block;pointer-events:none;z-index:-4;opacity:.18;transition:opacity .9s ease,filter .9s ease;filter:saturate(.8) brightness(.65)} #simon-c1,#simon-c2{mix-blend-mode:screen} #simonBoot{position:fixed;inset:0;z-index:6100;display:grid;place-items:center;pointer-events:none;opacity:0;transition:opacity .9s ease;color:#eaffff;text-align:center} #simonBoot .boot-copy{position:absolute;bottom:9%;font-weight:900;letter-spacing:.24em;text-transform:uppercase;text-shadow:0 0 24px #20dfff} #simonBoot strong{display:block;font-size:clamp(28px,5vw,72px);letter-spacing:.05em;margin-bottom:8px} #simonHint{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);font:600 9px/2 'Courier New',monospace;letter-spacing:.25em;color:rgba(0,200,255,.28);text-transform:uppercase;text-align:center;pointer-events:none;z-index:6200;transition:opacity 2s;opacity:0} body.simon-boot{cursor:none} body.simon-boot .simon-canvas{z-index:6000;opacity:1;filter:none;pointer-events:auto} body.simon-boot #simonBoot{opacity:1} body.simon-boot #simonHint{opacity:1} body.simon-boot > .app,body.simon-boot > div:not(.bg):not(#simonBoot):not(#simonHint):not(.screensaver){opacity:0} body.simon-boot-fade .simon-canvas{opacity:.18;filter:saturate(.8) brightness(.65)} body.simon-boot-fade #simonBoot,body.simon-boot-fade #simonHint{opacity:0} body.simon-background .side,body.simon-background .top,body.simon-background .workspace{position:relative;z-index:2} body.simon-background .bg{opacity:.68} body.simon-screensaver{cursor:none} body.simon-screensaver .simon-canvas{z-index:5001;opacity:1;filter:none;pointer-events:auto} body.simon-screensaver .screensaver{display:grid;background:transparent;z-index:5002;pointer-events:none} body.simon-screensaver .app{filter:brightness(.12) blur(4px)} body.simon-screensaver #simonHint{z-index:5003;opacity:1} .screensaver .ssorb{display:none} @media(prefers-reduced-motion:reduce){.simon-canvas{opacity:.08!important}.simon-canvas:not(#simon-c0){display:none}body.simon-boot .simon-canvas{opacity:.35!important}} /* === GayLord Living Interface v1.1 — additive enhancement, no modules removed === */ :root{--simon-signal:#8b5cff;--mx:0;--my:0} .bg{transform:translate3d(calc(var(--mx)*-5px),calc(var(--my)*-4px),0) scale(1.025);transition:transform .18s ease-out} .bg:before{animation-duration:54s}.bg:after{animation-duration:44s} .workspace{perspective:1200px} .glass{will-change:transform;animation:glAmbientFloat 9s ease-in-out infinite;animation-delay:calc(var(--float-index,0)*-.6s)} .glass:nth-of-type(3n){--float-index:2}.glass:nth-of-type(4n){--float-index:4} @keyframes glAmbientFloat{0%,100%{translate:0 0}50%{translate:0 -3px}} .btn{position:relative;isolation:isolate;overflow:hidden;transition:transform .22s ease,box-shadow .22s ease,filter .22s ease} .btn:after{content:"";position:absolute;inset:-60%;z-index:-1;background:radial-gradient(circle,rgba(255,255,255,.35),transparent 52%);opacity:0;transform:scale(.45);transition:opacity .25s ease,transform .35s ease} @media(hover:hover){.btn:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 0 28px color-mix(in srgb,var(--a2) 24%,transparent)}.btn:hover:after{opacity:.55;transform:scale(1)}} /* SIMON remains present as a compact living navigation star */ .simon-nav-orb{--orb:var(--simon-signal);width:46px;height:46px;border:1px solid color-mix(in srgb,var(--orb) 72%,white);border-radius:50%;padding:0;background:radial-gradient(circle at 32% 26%,#fff 0 3%,color-mix(in srgb,var(--orb) 82%,#48dfff) 11%,var(--orb) 35%,#080b20 72%);box-shadow:0 0 18px var(--orb),inset 0 0 18px rgba(255,255,255,.18);position:relative;animation:simonNavBreathe 3s ease-in-out infinite;flex:0 0 auto} .simon-nav-orb:before,.simon-nav-orb:after{content:"";position:absolute;inset:-5px;border:1px solid color-mix(in srgb,var(--orb) 65%,transparent);border-radius:50%;animation:glMarkOrbit 7s linear infinite}.simon-nav-orb:after{inset:8px;animation-direction:reverse;animation-duration:4.5s} .simon-nav-orb.has-alerts{animation:simonNavBreathe 1.3s ease-in-out infinite} @keyframes simonNavBreathe{50%{transform:scale(1.08);filter:brightness(1.25)}} /* Collectible cosmic profile cards */ .person{transform-style:preserve-3d;transition:transform .35s ease,box-shadow .35s ease} .person:before{content:"";position:absolute;inset:0;z-index:2;pointer-events:none;background-image:radial-gradient(circle at 15% 19%,#fff 0 1px,transparent 2px),radial-gradient(circle at 78% 24%,var(--a2) 0 1px,transparent 2px),radial-gradient(circle at 61% 58%,var(--a3) 0 1.5px,transparent 2.5px),linear-gradient(28deg,transparent 49.5%,rgba(117,220,255,.22) 50%,transparent 50.5%);opacity:.46;background-size:100% 100%;animation:constellationReveal 3.8s ease both} .personart img{transition:transform 7s ease,filter .5s ease}.person:hover .personart img{transform:scale(1.09);filter:saturate(1.14)} .person .compat-ring{position:absolute;right:14px;top:14px;z-index:4;width:64px;height:64px;border-radius:50%;display:grid;place-items:center;background:conic-gradient(var(--a2) var(--compat,82%),rgba(255,255,255,.1) 0);box-shadow:0 0 22px color-mix(in srgb,var(--a2) 38%,transparent);font-size:12px;font-weight:900} .person .compat-ring:before{content:"";position:absolute;inset:6px;border-radius:50%;background:#080c18}.person .compat-ring span{position:relative} @keyframes constellationReveal{from{opacity:0;filter:blur(6px);transform:scale(.94)}to{opacity:.46;filter:none;transform:scale(1)}} @media(hover:hover){.person:hover{transform:rotateX(2deg) rotateY(-2deg) translateY(-7px);box-shadow:0 34px 90px rgba(0,0,0,.5),0 0 38px color-mix(in srgb,var(--a1) 23%,transparent)}} /* Cinematic albums */ .groupgrid.album-cinema{perspective:1300px}.album-card{cursor:pointer;transform-origin:50% 90%;transition:transform .45s cubic-bezier(.2,.8,.2,1),filter .35s ease}.album-card:nth-child(3n+1){transform:rotateY(1.7deg)}.album-card:nth-child(3n+2){transform:translateY(6px)}.album-card:nth-child(3n){transform:rotateY(-1.7deg)} @media(hover:hover){.album-card:hover{transform:translateY(-9px) rotateY(0) scale(1.025);filter:brightness(1.1)}} .album-viewer{position:fixed;inset:0;z-index:7200;display:none;place-items:center;background:rgba(1,3,12,.88);backdrop-filter:blur(18px);touch-action:none}.album-viewer.open{display:grid;animation:viewerIn .35s ease both}.album-stage{width:min(1100px,92vw);height:min(76vh,760px);display:grid;place-items:center;position:relative}.album-stage img{max-width:100%;max-height:100%;object-fit:contain;border-radius:26px;box-shadow:0 40px 120px #000,0 0 55px color-mix(in srgb,var(--a2) 30%,transparent);transition:transform .2s ease}.album-viewer-controls{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);display:flex;gap:10px;z-index:2}.album-viewer-title{position:fixed;top:26px;left:50%;transform:translateX(-50%);font-weight:900;letter-spacing:.08em}.album-close{position:fixed;right:24px;top:22px;z-index:2}.album-fan{animation:albumFan .7s cubic-bezier(.2,.8,.2,1) both}@keyframes viewerIn{from{opacity:0}to{opacity:1}}@keyframes albumFan{from{opacity:0;transform:translateY(50px) rotate(-4deg) scale(.85)}to{opacity:1;transform:none}} /* SIMON assistant */ .simon-assistant{position:fixed;right:20px;top:84px;z-index:7100;width:min(390px,calc(100vw - 24px));max-height:calc(100dvh - 110px);display:none;overflow:auto;padding:18px;background:linear-gradient(180deg,rgba(13,18,39,.96),rgba(4,7,18,.96));border:1px solid color-mix(in srgb,var(--simon-signal) 55%,white);border-radius:24px;box-shadow:0 30px 100px #000,0 0 45px color-mix(in srgb,var(--simon-signal) 26%,transparent);backdrop-filter:blur(24px)}.simon-assistant.open{display:block;animation:viewerIn .25s ease}.simon-assistant h3{margin:0 0 6px}.simon-signal-list{display:grid;gap:8px;margin-top:14px}.simon-signal-list a{padding:12px;border:1px solid var(--line);border-radius:15px;background:rgba(255,255,255,.05)} @media(max-width:900px){.simon-nav-orb{width:42px;height:42px}.simon-assistant{top:76px;right:12px}} @media(prefers-reduced-motion:reduce){.glass,.simon-nav-orb,.person:before{animation:none!important}.bg{transform:none!important}} .market-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px}.market-card{overflow:hidden}.market-card img{width:100%;height:210px;object-fit:cover;display:block}.market-card .market-body{padding:17px}.price{font-size:26px;font-weight:1000;color:var(--a4)}.toggle-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}.toggle-card{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:15px;border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.045)}.toggle-card input{width:22px;height:22px}.safety-score{display:grid;grid-template-columns:180px 1fr;gap:22px;align-items:center}.score-ring{width:170px;aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:conic-gradient(var(--good) 0 86%,rgba(255,255,255,.08) 86%);box-shadow:0 0 45px rgba(86,229,138,.25)}.score-ring:before{content:"";position:absolute;width:132px;aspect-ratio:1;border-radius:50%;background:#09101d}.score-ring strong{position:relative;font-size:42px}.settings-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:18px}.settings-panel{display:none}.settings-panel.active{display:block}.danger{background:linear-gradient(135deg,#ff355d,#b00035)!important;color:#fff!important}.verification-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.verification-item{padding:15px;border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.04)}@media(max-width:700px){.safety-score{grid-template-columns:1fr}.score-ring{margin:auto}} </style> </head> <body> <canvas id="simon-c0" class="simon-canvas" aria-hidden="true"></canvas> <canvas id="simon-c1" class="simon-canvas" aria-hidden="true"></canvas> <canvas id="simon-c2" class="simon-canvas" aria-hidden="true"></canvas> <div id="simonBoot" aria-hidden="true"><div class="boot-copy"><strong>SIMON</strong>GayLord Spatial Network · Initializing</div></div> <div id="simonHint">DRAG — rotate | SCROLL — zoom | CLICK — pulse | DBL CLICK — nova</div> <div class="bg"></div> <?php if(!$me): ?> <div style="height:100%;display:grid;place-items:center;padding:20px;overflow:auto"> <div class="glass" style="width:min(1040px,100%);display:grid;grid-template-columns:1.1fr .9fr;overflow:hidden"> <section class="hero" style="border-radius:0"><div class="kicker">GAYLORD SPATIAL · 18+</div><h1>Enter your universe.</h1><p>A fresh social experience with artistic profiles, meaningful questions, groups, messaging, spatial discovery, and a built-in cosmic screensaver.</p><div class="ssorb" style="width:230px;margin-top:24px"><span>GL</span></div></section> <section class="section"> <?php foreach($flashes as $f): ?><div class="flash <?=e($f['type'])?>"><?=e($f['message'])?></div><?php endforeach; ?> <div class="tabs"><button class="active" data-auth="login">Sign in</button><button data-auth="register">Create account</button></div> <form method="post" data-auth-panel="login"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="login"><div class="field"><label>Email or username</label><input name="identity" required></div><div class="field"><label>Password</label><input name="password" type="password" required></div><button class="btn primary" style="width:100%">Enter GayLord</button></form> <form method="post" data-auth-panel="register" hidden><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="register"><div class="field"><label>Display name</label><input name="display_name" required></div><div class="field"><label>Username</label><input name="username" required></div><div class="field"><label>Email</label><input name="email" type="email" required></div><div class="field"><label>Birthdate</label><input name="birthdate" type="date" required></div><div class="field"><label>Password</label><input name="password" type="password" minlength="10" required></div><button class="btn primary" style="width:100%">Create account</button></form> </section> </div> </div> <?php else: ?> <div class="app"> <aside class="side"><div class="brandbox"><div class="mark">GL</div><div><div class="brandname" aria-label="GayLord"><span>G</span><span>a</span><span>y</span><span>L</span><span>o</span><span>r</span><span>d</span></div><small class="muted">SPATIAL NETWORK</small></div></div> <nav class="nav"> <?php $nav=[['home','⌂','Home'],['discover','◇','Discover'],['messages','✉','Messages'],['groups','◎','Groups'],['events','□','Events'],['marketplace','M','Marketplace'],['albums','▣','Albums'],['reels','18+','Reels'],['spatial','◉','Spatial'],['profile','P','Profile'],['progress','⚡','Progress'],['notifications','●','Alerts'],['settings','⚙','Settings'],['safety','!','Safety'],['developer','</>','Developer']];foreach($nav as $n):?><a href="?view=<?=$n[0]?>" class="<?=$view===$n[0]?'active':''?>"><span class="ni"><?=$n[1]?></span><span><?=$n[2]?></span></a><?php endforeach; ?> </nav><div class="sidefoot">● Network online<br><?=e($me['display_name'])?></div></aside> <header class="top"><div class="wordmark" aria-label="GayLord"><span>G</span><span>a</span><span>y</span><span>L</span><span>o</span><span>r</span><span>d</span></div><div class="topactions"><button class="simon-nav-orb" id="simonOrb" type="button" aria-label="Open SIMON assistant"></button><div class="chip" id="clock">LIVE</div><button class="btn" id="themeBtn" type="button" aria-label="Change color theme">Color</button><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="logout"><button class="btn">Sign out</button></form></div></header> <main class="workspace"> <?php foreach($flashes as $f): ?><div class="flash <?=e($f['type'])?>"><?=e($f['message'])?></div><?php endforeach; ?> <?php if($view==='home'): ?> <section class="glass hero"><div class="kicker">GAYLORD SPATIAL NETWORK</div><h1>Where connection becomes art.</h1><p>Discover people, build expressive groups, explore the spatial field, and let the guided profile engine reveal who you are—without external AI.</p><div style="display:flex;gap:10px;flex-wrap:wrap"><a class="btn primary" href="?view=profile">Build profile</a><a class="btn accent" href="?view=discover">Explore members</a><a class="btn" href="?view=groups">Find groups</a></div></section> <div class="sectionhead"><div><h2>Your universe</h2><div class="muted">Sectional glass modules that adapt to phone and desktop.</div></div></div> <div class="grid g4"><article class="glass card"><div class="kicker">PROFILE DEPTH</div><div class="metric">72%</div><p class="muted">Finish the guided questions.</p></article><article class="glass card"><div class="kicker">DISCOVERY</div><div class="metric"><?=count($users)?></div><p class="muted">Members in your network.</p></article><article class="glass card"><div class="kicker">GROUPS</div><div class="metric"><?=count($groups)?></div><p class="muted">Communities ready to explore.</p></article><article class="glass card"><div class="kicker">STREAK</div><div class="metric">5</div><p class="muted">Days of profile progress.</p></article></div> <div class="sectionhead"><div><h2>Featured members</h2><div class="muted">Art-forward profiles.</div></div><a class="btn" href="?view=discover">View all</a></div> <div class="profilegrid"><?php foreach(array_slice($users,0,6) as $u):?><article class="glass person" data-cosmic-profile><div class="compat-ring" style="--compat:82%"><span>82%</span></div><div class="personart"><?php if(!empty($u['avatar_media_id'])):?><img src="<?=e(media_url((int)$u['avatar_media_id']))?>" alt=""><?php endif;?></div><div class="personinfo"><h3><?=e($u['display_name'])?></h3><div class="muted"><?=e($u['city']?:'Somewhere in the Kosmos')?> · <?=e($u['personality']?:'Open to connection')?></div><form method="post" style="margin-top:10px"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="start_thread"><input type="hidden" name="other_id" value="<?=$u['id']?>"><button class="btn primary">Message</button></form></div></article><?php endforeach;?></div> <?php elseif($view==='discover'): ?> <div class="sectionhead"><div><div class="kicker">DISCOVERY</div><h2>Discover</h2><div class="muted">Profiles blended like living artwork.</div></div></div><div class="profilegrid"><?php foreach($users as $u):?><article class="glass person" data-cosmic-profile><div class="compat-ring" style="--compat:82%"><span>82%</span></div><div class="personart"><?php if(!empty($u['avatar_media_id'])):?><img src="<?=e(media_url((int)$u['avatar_media_id']))?>" alt=""><?php endif;?></div><div class="personinfo"><h3><?=e($u['display_name'])?></h3><div class="muted"><?=e($u['looking_for']?:'Open to connection')?></div><form method="post" style="margin-top:10px"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="start_thread"><input type="hidden" name="other_id" value="<?=$u['id']?>"><button class="btn primary">Message</button></form></div></article><?php endforeach;?></div> <?php elseif($view==='messages'): ?> <div class="sectionhead"><div><div class="kicker">MESSAGES</div><h2>Inbox</h2></div></div><div class="chatlayout"><aside class="glass threadlist"><?php if(!$threads):?><div class="muted">Start a conversation from Discover.</div><?php endif;foreach($threads as $t):?><a class="thread <?=$selectedThread==(int)$t['id']?'active':''?>" href="?view=messages&thread=<?=$t['id']?>"><strong><?=e($t['other_name']?:'Conversation')?></strong><div class="muted"><?=e($t['last_body']?:'No messages yet')?></div></a><?php endforeach;?></aside><section class="glass chat"><?php if($selectedThread):?><div class="messages"><?php foreach($threadMessages as $m):?><div class="bubble <?=((int)$m['sender_id']===(int)$me['id'])?'mine':''?>"><strong><?=e($m['display_name'])?></strong><div><?=nl2br(e($m['body']))?></div></div><?php endforeach;?></div><form method="post" class="composer"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="send_message"><input type="hidden" name="thread_id" value="<?=$selectedThread?>"><textarea name="body" placeholder="Write a message..."></textarea><button class="btn primary">Send</button></form><?php else:?><div style="display:grid;place-items:center;height:100%" class="muted">Select a conversation.</div><?php endif;?></section></div> <?php elseif($view==='groups'): ?> <div class="sectionhead"><div><div class="kicker">GROUPS</div><h2>Groups with identity</h2><div class="muted">Create artistic group covers, events, posts, and community spaces.</div></div><button class="btn primary" onclick="document.getElementById('createGroup').showModal()">Create group</button></div> <?php if($selectedGroup):?><section class="glass profilehero"><div class="profilecover"><?php if(!empty($selectedGroup['image_media_id'])):?><img src="<?=e(media_url((int)$selectedGroup['image_media_id']))?>" alt=""><?php endif;?></div><div class="profileblend"><div class="portrait" style="aspect-ratio:1;border-radius:30px"><?php if(!empty($selectedGroup['image_media_id'])):?><img src="<?=e(media_url((int)$selectedGroup['image_media_id']))?>" alt=""><?php endif;?></div><div class="profilecopy"><div class="kicker"><?=e($selectedGroup['category'])?></div><h1><?=e($selectedGroup['name'])?></h1><p><?=e($selectedGroup['tagline'])?></p><div class="chip"><?=e((string)$selectedGroup['member_count'])?> members</div></div></div></section><div class="grid g2" style="margin-top:16px"><section class="glass section"><h3>About</h3><p><?=nl2br(e($selectedGroup['description']))?></p><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="join_group"><input type="hidden" name="group_id" value="<?=$selectedGroup['id']?>"><button class="btn primary">Join group</button></form></section><section class="glass section"><h3>Post to group</h3><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="group_post"><input type="hidden" name="group_id" value="<?=$selectedGroup['id']?>"><div class="field"><textarea name="body" placeholder="Share something..."></textarea></div><button class="btn primary">Post</button></form></section></div><div class="sectionhead"><h2>Group activity</h2></div><div class="grid g2"><?php foreach($groupPosts as $p):?><article class="glass card"><strong><?=e($p['display_name'])?></strong><p><?=nl2br(e($p['body']))?></p></article><?php endforeach;?></div><?php else:?><div class="groupgrid"><?php foreach($groups as $g):?><a class="glass groupcard" href="?view=groups&group=<?=$g['id']?>"><div class="groupart <?=e($g['art_style'])?>"><?php if(!empty($g['image_media_id'])):?><img src="<?=e(media_url((int)$g['image_media_id']))?>" alt=""><?php endif;?></div><div class="groupbody"><div class="kicker"><?=e($g['category'])?></div><h3><?=e($g['name'])?></h3><p class="muted"><?=e($g['tagline'])?></p><div class="chip"><?=$g['member_count']?> members</div></div></a><?php endforeach;?></div><?php endif;?> <dialog id="createGroup" class="glass" style="width:min(700px,94vw);color:#fff;background:#0b1020;border:1px solid var(--line);border-radius:24px"><form method="post" enctype="multipart/form-data"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="create_group"><h2>Create a group</h2><div class="formgrid"><div class="field"><label>Name</label><input name="name" required></div><div class="field"><label>Tagline</label><input name="tagline"></div><div class="field"><label>Category</label><select name="category"><option>Community</option><option>Events</option><option>Creative</option><option>Nightlife</option><option>Travel</option><option>Dating</option><option>Support</option><option>Adult</option></select></div><div class="field"><label>Privacy</label><select name="privacy"><option>public</option><option>approval</option><option>invite</option><option>hidden</option></select></div><div class="field"><label>Vibe</label><select name="vibe"><option>Cosmic</option><option>Chill</option><option>Wild</option><option>Artsy</option><option>Flirty</option><option>Deep talk</option></select></div><div class="field"><label>Art style</label><select name="art_style"><option value="cosmic">Cosmic</option><option value="neon">Neon</option><option value="editorial">Editorial</option><option value="dream">Dream blur</option><option value="industrial">Dark industrial</option></select></div></div><div class="field"><label>Description</label><textarea name="description"></textarea></div><div class="field"><label>Group profile art</label><input type="file" name="group_image" accept="image/*"></div><div style="display:flex;gap:8px;justify-content:flex-end"><button type="button" class="btn" onclick="this.closest('dialog').close()">Cancel</button><button class="btn primary">Create</button></div></form></dialog> <?php elseif($view==='spatial'): ?> <div class="sectionhead"><div><div class="kicker">SPATIAL FIELD</div><h2>Nearby now</h2><div class="muted">Uses the template map layout with controls inside the map.</div></div></div><section class="glass mapwrap"><div class="mapcontrols"><div class="chip" id="mapStatus">Demo field</div><button class="btn primary" id="locateBtn">Use my location</button></div><div id="map" class="map"></div><div class="mapbottom"><?php foreach(array_slice($users,0,8) as $u):?><div class="mapmember"><strong><?=e($u['display_name'])?></strong><div class="muted"><?=e($u['city']?:'Nearby')?> · <?=e($u['looking_for']?:'Connect')?></div></div><?php endforeach;?></div></section> <?php elseif($view==='profile'): ?> <section class="glass profilehero"><div class="profilecover"><?php if($coverUrl):?><img src="<?=e($coverUrl)?>" alt=""><?php endif;?></div><div class="profileblend"><div class="portrait"><?php if($avatarUrl):?><img src="<?=e($avatarUrl)?>" alt=""><?php endif;?></div><div class="profilecopy"><div class="kicker">YOUR UNIVERSE</div><h1><?=e($me['display_name'])?></h1><p><?=e($me['about_me']?:'Your story becomes art here.')?></p><div class="tabs"><button class="active" data-profile-tab="edit">Edit profile</button><button data-profile-tab="interview">Guided interview</button><button data-profile-tab="preview">Preview art</button></div></div></div></section> <section data-profile-panel="edit" class="glass section" style="margin-top:16px"><form method="post" enctype="multipart/form-data"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="save_profile"><div class="sectionhead"><div><h2>Deep profile</h2><div class="muted">Dropdown-first, complete, and phone friendly.</div></div><button class="btn primary">Save profile</button></div><div class="formgrid"> <?php $selects=['pronouns'=>['He/Him','She/Her','They/Them','Any pronouns','Ask me'],'orientation'=>['Gay','Bisexual','Queer','Pansexual','Questioning','Other'],'relationship_status'=>['Single','Dating','Partnered','Married','It is complicated'],'relationship_structure'=>['Monogamous','Open','Polyamorous','Exploring','Undecided'],'looking_for'=>['Relationship','Dating','Friends','Casual','Community','Creative collaboration','Travel partner'],'personality'=>['Introvert','Ambivert','Extrovert','Adventurous','Reflective','Playful'],'communication_style'=>['Direct','Gentle','Detailed','Playful','Low-key'],'social_energy'=>['Quiet','Balanced','Highly social','Depends on the day'],'love_language'=>['Quality time','Physical touch','Words of affirmation','Acts of service','Gifts'],'education'=>['High school','Trade school','Some college','Associate','Bachelor','Master','Doctorate','Other'],'body_type'=>['Slim','Average','Athletic','Muscular','Stocky','Large','Other'],'exercise'=>['Never','Sometimes','Weekly','Regularly','Daily'],'smoking'=>['Never','Socially','Regularly','Quitting'],'drinking'=>['Never','Socially','Regularly','Sober'],'pets'=>['None','Dogs','Cats','Both','Other'],'children'=>['No','Yes','Prefer not to say'],'future_children'=>['No','Yes','Maybe','Undecided'],'travel_style'=>['Homebody','Occasional trips','Frequent traveler','Digital nomad'],'nightlife'=>['Not my thing','Sometimes','Weekends','Frequently'],'privacy_level'=>['public','members','matches','friends','private'],'theme'=>['auto','cosmic-blue','neon-violet','solar-gold','electric-pink','aurora-green','midnight']];foreach($selects as $name=>$opts):?><div class="field"><label><?=e(ucwords(str_replace('_',' ',$name)))?></label><select name="<?=$name?>"><option value="">Choose...</option><?php foreach($opts as $o):?><option value="<?=e($o)?>" <?=($me[$name]??'')===$o?'selected':''?>><?=e($o)?></option><?php endforeach;?></select></div><?php endforeach;?></div> <div class="formgrid"><div class="field"><label>Occupation</label><input name="occupation" value="<?=e($me['occupation'])?>"></div><div class="field"><label>Height</label><input name="height" value="<?=e($me['height'])?>"></div><div class="field"><label>City / general area</label><input name="city" value="<?=e($me['city'])?>"></div></div><div class="field"><label>About me</label><textarea name="about_me"><?=e($me['about_me'])?></textarea></div><div class="field"><label>Interests</label><textarea name="interests" placeholder="Art, music, travel, games..."><?=e($me['interests'])?></textarea></div><div class="field"><label>Boundaries and consent expectations</label><textarea name="boundaries"><?=e($me['boundaries'])?></textarea></div><div class="grid g2"><div class="field"><label>Profile portrait</label><input type="file" name="avatar" accept="image/*"></div><div class="field"><label>Cover artwork</label><input type="file" name="cover" accept="image/*"></div></div></form></section> <section data-profile-panel="interview" class="glass interview" style="margin-top:16px" hidden><div class="progressbar"><i id="interviewProgress"></i></div><div class="kicker" style="margin-top:18px">GUIDED PROFILE ENGINE · NO EXTERNAL AI</div><div class="question" id="questionText"></div><div class="answers" id="answerList"></div><div style="display:flex;justify-content:space-between;gap:8px;margin-top:18px"><button class="btn" id="prevQuestion">Back</button><button class="btn primary" id="nextQuestion">Continue</button></div><form method="post" id="interviewForm"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="save_interview"><input type="hidden" name="answers_json" id="answersJson"></form></section> <section data-profile-panel="preview" class="profilelayout" style="margin-top:16px" hidden><article class="glass profileart"></article><article class="glass section"><div class="kicker">COMPATIBILITY SIGNALS</div><h2>Profile balance</h2><p class="muted">Communication 88% · Relationship goals 91% · Lifestyle 76%</p><div class="progressbar"><i style="width:86%"></i></div><h3 style="margin-top:20px">Local summary</h3><p><?=e($me['about_me']?:'Complete the interview to generate a hardwired profile summary.')?></p></article></section> <?php elseif($view==='progress'): ?> <div class="sectionhead"><div><div class="kicker">SIMON MISSION ENGINE</div><h2>Progress and achievements</h2><div class="muted">Hardwired recommendations, no external AI.</div></div></div><div class="grid g3"><article class="glass card"><div class="metric">🔥 5</div><h3>Day streak</h3><p class="muted">Keep exploring your profile.</p></article><article class="glass card"><div class="metric">82%</div><h3>Profile depth</h3><p class="muted">Answer three compatibility questions.</p></article><article class="glass card"><div class="metric">12</div><h3>Achievements</h3><p class="muted">Community Explorer unlocked.</p></article></div><div class="sectionhead"><h2>Daily missions</h2></div><div class="grid g2"><article class="glass card"><h3>Complete 3 profile questions</h3><div class="progressbar"><i style="width:66%"></i></div></article><article class="glass card"><h3>Join a group</h3><div class="progressbar"><i style="width:25%"></i></div></article><article class="glass card"><h3>Review privacy settings</h3><div class="progressbar"><i style="width:100%"></i></div></article><article class="glass card"><h3>Add profile cover art</h3><div class="progressbar"><i style="width:40%"></i></div></article></div> <?php elseif($view==='events'): ?> <div class="sectionhead"><div><div class="kicker">EVENTS</div><h2>Events</h2><div class="muted">Create, schedule, and log events.</div></div><button class="btn primary" onclick="document.getElementById('eventDialog').showModal()">Add event</button></div> <div class="grid"><?php if(!$events):?><section class="glass hero"><h1>No events yet.</h1><p>Add your first event using the button above.</p></section><?php endif;foreach($events as $ev):?><article class="glass card eventrow"><div class="eventdate"><?=e(date('M j, Y',strtotime((string)$ev['starts_at'])))?><div class="muted"><?=e(date('g:i A',strtotime((string)$ev['starts_at'])))?></div></div><div><h3><?=e($ev['title'])?></h3><div class="muted"><?=e($ev['location']?:'Location not set')?> · <?=e($ev['visibility'])?></div><p><?=nl2br(e($ev['description']))?></p></div><span class="chip"><span class="statusdot"></span>Scheduled</span></article><?php endforeach;?></div> <dialog id="eventDialog" class="glass" style="width:min(700px,94vw);color:#fff;background:#0b1020;border:1px solid var(--line);border-radius:24px"><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="create_event"><h2>Add event</h2><div class="field"><label>Title</label><input name="title" required></div><div class="formgrid"><div class="field"><label>Starts</label><input type="datetime-local" name="starts_at" required></div><div class="field"><label>Ends</label><input type="datetime-local" name="ends_at"></div><div class="field"><label>Visibility</label><select name="visibility"><option>members</option><option>public</option><option>private</option></select></div></div><div class="field"><label>Location</label><input name="location"></div><div class="field"><label>Description</label><textarea name="description"></textarea></div><div style="display:flex;justify-content:flex-end;gap:8px"><button type="button" class="btn" onclick="this.closest('dialog').close()">Cancel</button><button class="btn primary">Save event</button></div></form></dialog> <?php elseif($view==='albums'): ?> <div class="sectionhead"><div><div class="kicker">ALBUMS</div><h2>Albums</h2><div class="muted">Create visual collections with cover art.</div></div><button class="btn primary" onclick="document.getElementById('albumDialog').showModal()">Add album</button></div><div class="groupgrid album-cinema"><?php foreach($albums as $a):?><article class="glass groupcard album-card" data-album-title="<?=e($a['title'])?>" data-album-image="<?= $a['cover_media_id'] ? e(media_url((int)$a['cover_media_id'])) : '' ?>"><div class="groupart"><?php if($a['cover_media_id']):?><img src="<?=e(media_url((int)$a['cover_media_id']))?>" alt=""><?php endif;?></div><div class="groupbody"><div class="kicker"><?=e($a['visibility'])?></div><h3><?=e($a['title'])?></h3><p class="muted"><?=e($a['description'])?></p></div></article><?php endforeach;?></div> <dialog id="albumDialog" class="glass" style="width:min(650px,94vw);color:#fff;background:#0b1020;border:1px solid var(--line);border-radius:24px"><form method="post" enctype="multipart/form-data"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="create_album"><h2>Create album</h2><div class="field"><label>Title</label><input name="title" required></div><div class="field"><label>Description</label><textarea name="description"></textarea></div><div class="field"><label>Cover image</label><input type="file" name="cover" accept="image/*"></div><div class="field"><label>Visibility</label><select name="visibility"><option>members</option><option>public</option><option>private</option></select></div><button class="btn primary">Create album</button></form></dialog> <?php elseif($view==='reels'): ?> <div class="sectionhead"><div><div class="kicker">18+ REELS</div><h2>Reels</h2><div class="muted">Upload short video or image reels, up to 20 MB.</div></div><button class="btn primary" onclick="document.getElementById('reelDialog').showModal()">Upload reel</button></div><div class="grid g3"><?php foreach($reels as $r):?><article class="glass card"><?php if(str_starts_with((string)$r['mime'],'video/')):?><video class="media-preview" controls playsinline preload="metadata" src="<?=e(media_url((int)$r['media_id']))?>"></video><?php else:?><img class="media-preview" src="<?=e(media_url((int)$r['media_id']))?>" alt=""><?php endif;?><p><?=e($r['caption'])?></p><div class="muted"><?=e($r['visibility'])?> · <?=e($r['created_at'])?></div></article><?php endforeach;?></div> <dialog id="reelDialog" class="glass" style="width:min(650px,94vw);color:#fff;background:#0b1020;border:1px solid var(--line);border-radius:24px"><form method="post" enctype="multipart/form-data"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="upload_reel"><h2>Upload reel</h2><div class="field"><label>Video or image</label><input type="file" name="reel" accept="video/mp4,video/webm,video/quicktime,image/*" required></div><div class="field"><label>Caption</label><textarea name="caption"></textarea></div><div class="field"><label>Visibility</label><select name="visibility"><option>members</option><option>public</option><option>private</option></select></div><button class="btn primary">Upload</button></form></dialog> <?php elseif($view==='notifications'): ?> <div class="sectionhead"><div><div class="kicker">ALERTS</div><h2>Alerts</h2><div class="muted">System, social, event, and developer notifications.</div></div><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="mark_alerts_read"><button class="btn">Mark all read</button></form></div><div class="grid"><?php if(!$alerts):?><section class="glass card muted">No alerts yet.</section><?php endif;foreach($alerts as $a):?><article class="glass card"><div class="kicker"><?=$a['is_read']?'READ':'NEW'?></div><h3><?=e($a['title'])?></h3><p><?=e($a['body'])?></p><div class="muted"><?=e($a['created_at'])?></div></article><?php endforeach;?></div> <?php elseif($view==='marketplace'): ?> <div class="sectionhead"><div><div class="kicker">MARKETPLACE</div><h2>Community Market</h2><div class="muted">Post personal ads, housing, jobs, services, tickets, travel, businesses, and community listings.</div></div><button class="btn primary" onclick="document.getElementById('marketDialog').showModal()">Enter an ad</button></div> <div class="market-grid"> <?php if(!$marketAds):?><section class="glass hero"><h1>No ads yet.</h1><p>Use “Enter an ad” to post the first marketplace listing.</p></section><?php endif;?> <?php foreach($marketAds as $ad):?><article class="glass market-card"><?php if(!empty($ad['image_media_id'])):?><img src="<?=e(media_url((int)$ad['image_media_id']))?>" alt=""><?php else:?><div style="height:210px;background:linear-gradient(135deg,var(--a1),var(--a3),var(--a2));display:grid;place-items:center;font-size:64px">M</div><?php endif;?><div class="market-body"><div class="kicker"><?=e($ad['category'])?></div><h3><?=e($ad['title'])?></h3><?php if($ad['price']!==''):?><div class="price"><?=e($ad['price'])?></div><?php endif;?><p><?=nl2br(e($ad['description']))?></p><div class="muted"><?=e($ad['location']?:'Location not listed')?><?php if($ad['contact']!==''):?> · <?=e($ad['contact'])?><?php endif;?></div></div></article><?php endforeach;?> </div> <dialog id="marketDialog" class="glass" style="width:min(760px,95vw);color:#fff;background:#0b1020;border:1px solid var(--line);border-radius:24px"><form method="post" enctype="multipart/form-data"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="create_marketplace_ad"><h2>Enter a marketplace ad</h2><div class="formgrid"><div class="field"><label>Title</label><input name="title" required></div><div class="field"><label>Category</label><select name="category"><option>Personal Ad</option><option>Housing</option><option>Jobs</option><option>Services</option><option>Businesses</option><option>Travel</option><option>Tickets</option><option>Events</option><option>Books</option><option>Electronics</option><option>Furniture</option><option>Collectibles</option><option>Free Stuff</option><option>Wanted</option><option>General</option></select></div><div class="field"><label>Price</label><input name="price" placeholder="$25, Free, Negotiable"></div></div><div class="formgrid"><div class="field"><label>Location</label><input name="location"></div><div class="field"><label>Contact</label><input name="contact" placeholder="Email, phone, username"></div><div class="field"><label>Picture</label><input type="file" name="ad_image" accept="image/*"></div></div><div class="field"><label>Description</label><textarea name="description" required></textarea></div><div style="display:flex;justify-content:flex-end;gap:8px"><button type="button" class="btn" onclick="this.closest('dialog').close()">Cancel</button><button class="btn primary">Post ad</button></div></form></dialog> <?php elseif($view==='safety'): ?> <div class="sectionhead"><div><div class="kicker">SAFETY CENTER</div><h2>Protection, verification, and emergency tools</h2><div class="muted">Control what you share, verify your account, review devices, and prepare for safer meetups.</div></div><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="panic_exit"><button class="btn danger">Panic exit</button></form></div> <section class="glass section safety-score"><div class="score-ring"><strong><?=array_sum(array_map(fn($k)=>(int)($safety[$k]??0),['email_verified','phone_verified','selfie_verified','government_id_verified','photo_verified','age_verified','two_factor_enabled']))*12+14?>%</strong></div><div><div class="kicker">SIMON SAFETY STATUS</div><h2>Protected</h2><p class="muted">Complete verification and security steps to strengthen your account. Verification controls below are test controls until connected to real providers.</p></div></section> <form method="post" class="glass section" style="margin-top:16px"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="save_safety"> <h2>Verification</h2><div class="verification-grid"><?php foreach(['email_verified'=>'Email','phone_verified'=>'Phone','selfie_verified'=>'Selfie','government_id_verified'=>'Government ID','photo_verified'=>'Photo','age_verified'=>'Age'] as $key=>$label):?><label class="verification-item"><strong><?=e($label)?></strong><div class="muted">Verification status</div><input type="checkbox" name="<?=e($key)?>" <?=!empty($safety[$key])?'checked':''?>></label><?php endforeach;?></div> <div class="sectionhead"><h2>Security</h2></div><div class="toggle-grid"><label class="toggle-card"><span><strong>Two-factor authentication</strong><small class="muted">Require a second login step</small></span><input type="checkbox" name="two_factor_enabled" <?=!empty($safety['two_factor_enabled'])?'checked':''?>></label><a class="toggle-card" href="?view=settings"><span><strong>Password</strong><small class="muted">Manage password and account access</small></span><span>›</span></a><div class="toggle-card"><span><strong>Active devices</strong><small class="muted"><?=count($sessions)?> known session(s)</small></span><span>›</span></div><div class="toggle-card"><span><strong>Login history</strong><small class="muted">Review recent access</small></span><span>›</span></div><div class="toggle-card"><span><strong>Sessions</strong><small class="muted">Current and previous sessions</small></span><span>›</span></div></div> <div class="sectionhead"><h2>Privacy</h2></div><div class="toggle-grid"><?php foreach(['show_distance'=>'Show Distance','online_status'=>'Online Status','last_seen'=>'Last Seen','incognito'=>'Incognito','hide_from_search'=>'Hide From Search','screenshot_alerts'=>'Screenshot Alerts','private_albums'=>'Private Albums'] as $key=>$label):?><label class="toggle-card"><span><strong><?=e($label)?></strong></span><input type="checkbox" name="<?=e($key)?>" <?=!empty($safety[$key])?'checked':''?>></label><?php endforeach;?></div> <div class="sectionhead"><h2>Emergency and meet safely</h2></div><div class="toggle-grid"><label class="toggle-card"><span><strong>Live location</strong><small class="muted">Temporary location-sharing preference</small></span><input type="checkbox" name="live_location" <?=!empty($safety['live_location'])?'checked':''?>></label><label class="toggle-card"><span><strong>Meet safely mode</strong><small class="muted">Enable check-in tools</small></span><input type="checkbox" name="meet_safely_mode" <?=!empty($safety['meet_safely_mode'])?'checked':''?>></label><label class="toggle-card"><span><strong>Safety timer</strong><small class="muted">Check-in duration</small></span><select name="safety_timer_minutes"><option value="0">Off</option><?php foreach([15,30,60,120,240] as $m):?><option value="<?=$m?>" <?=((int)($safety['safety_timer_minutes']??0)===$m)?'selected':''?>><?=$m?> minutes</option><?php endforeach;?></select></label></div><button class="btn primary" style="margin-top:18px">Save safety settings</button></form> <div class="grid g2" style="margin-top:16px"><section class="glass section"><h2>Trusted contacts</h2><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="add_trusted_contact"><div class="field"><label>Name</label><input name="name" required></div><div class="field"><label>Phone or email</label><input name="contact" required></div><button class="btn primary">Add trusted contact</button></form><?php foreach($trustedContacts as $tc):?><div class="toggle-card" style="margin-top:10px"><span><strong><?=e($tc['name'])?></strong><small class="muted"><?=e($tc['contact'])?></small></span></div><?php endforeach;?></section><section class="glass section"><h2>Sessions and devices</h2><?php foreach($sessions as $session):?><div class="toggle-card" style="margin-top:10px"><span><strong><?=e($session['is_current']?'Current session':'Previous session')?></strong><small class="muted"><?=e($session['device'])?><br><?=e($session['ip_address'])?> · <?=e($session['last_seen_at'])?></small></span></div><?php endforeach;?></section></div> <?php elseif($view==='settings'): ?> <div class="sectionhead"><div><div class="kicker">SETTINGS</div><h2>Everything centralized</h2><div class="muted">Privacy, notifications, appearance, AI, security, devices, data, and accessibility.</div></div></div> <section class="glass section"><div class="settings-tabs"><?php foreach(['privacy'=>'Privacy','notifications'=>'Notifications','appearance'=>'Appearance','ai'=>'AI','security'=>'Security','devices'=>'Devices','data'=>'Data','accessibility'=>'Accessibility'] as $key=>$label):?><button type="button" class="btn <?=$key==='privacy'?'primary':''?>" data-settings-tab="<?=e($key)?>"><?=e($label)?></button><?php endforeach;?></div> <form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="save_central_settings"> <div class="settings-panel active" data-settings-panel="privacy"><h2>Privacy</h2><div class="field"><label>Profile visibility</label><select name="privacy_level"><?php foreach(['public','members','matches','friends','private'] as $o):?><option value="<?=e($o)?>" <?=($me['privacy_level']??'')===$o?'selected':''?>><?=e(ucfirst($o))?></option><?php endforeach;?></select></div><a class="btn" href="?view=safety">Open detailed safety and privacy controls</a></div> <div class="settings-panel" data-settings-panel="notifications"><h2>Notifications</h2><div class="toggle-grid"><?php foreach(['notification_messages'=>'Messages','notification_events'=>'Events','notification_marketplace'=>'Marketplace','notification_safety'=>'Safety alerts'] as $k=>$label):?><label class="toggle-card"><strong><?=e($label)?></strong><input type="checkbox" name="<?=e($k)?>" <?=!isset($preferences[$k])||!empty($preferences[$k])?'checked':''?>></label><?php endforeach;?></div></div> <div class="settings-panel" data-settings-panel="appearance"><h2>Appearance</h2><div class="field"><label>Theme</label><select name="theme"><?php foreach(['auto','cosmic-blue','neon-violet','solar-gold','electric-pink','aurora-green','midnight'] as $o):?><option value="<?=e($o)?>" <?=($me['theme']??'')===$o?'selected':''?>><?=e(ucwords(str_replace('-',' ',$o)))?></option><?php endforeach;?></select></div><div class="toggle-grid"><label class="toggle-card"><strong>Motion effects</strong><input type="checkbox" name="appearance_motion" <?=!isset($preferences['appearance_motion'])||!empty($preferences['appearance_motion'])?'checked':''?>></label><label class="toggle-card"><strong>Glow effects</strong><input type="checkbox" name="appearance_glow" <?=!isset($preferences['appearance_glow'])||!empty($preferences['appearance_glow'])?'checked':''?>></label></div></div> <div class="settings-panel" data-settings-panel="ai"><h2>AI</h2><div class="toggle-grid"><label class="toggle-card"><strong>SIMON assistant</strong><input type="checkbox" name="ai_assistant" <?=!isset($preferences['ai_assistant'])||!empty($preferences['ai_assistant'])?'checked':''?>></label><label class="toggle-card"><strong>AI suggestions</strong><input type="checkbox" name="ai_suggestions" <?=!isset($preferences['ai_suggestions'])||!empty($preferences['ai_suggestions'])?'checked':''?>></label></div></div> <div class="settings-panel" data-settings-panel="security"><h2>Security</h2><a class="btn primary" href="?view=safety">Verification, 2FA, password, login history, and sessions</a></div> <div class="settings-panel" data-settings-panel="devices"><h2>Devices</h2><?php foreach($sessions as $session):?><div class="toggle-card" style="margin-bottom:10px"><span><strong><?=e($session['is_current']?'Current device':'Known device')?></strong><small class="muted"><?=e($session['device'])?></small></span></div><?php endforeach;?></div> <div class="settings-panel" data-settings-panel="data"><h2>Data</h2><div class="toggle-grid"><label class="toggle-card"><span><strong>Personalized experience</strong><small class="muted">Use app activity for recommendations</small></span><input type="checkbox" name="data_personalization" <?=!isset($preferences['data_personalization'])||!empty($preferences['data_personalization'])?'checked':''?>></label></div></div> <div class="settings-panel" data-settings-panel="accessibility"><h2>Accessibility</h2><div class="toggle-grid"><label class="toggle-card"><strong>Large text</strong><input type="checkbox" name="accessibility_large_text" <?=!empty($preferences['accessibility_large_text'])?'checked':''?>></label><label class="toggle-card"><strong>High contrast</strong><input type="checkbox" name="accessibility_high_contrast" <?=!empty($preferences['accessibility_high_contrast'])?'checked':''?>></label><label class="toggle-card"><strong>Reduced motion</strong><input type="checkbox" name="accessibility_reduced_motion" <?=!empty($preferences['accessibility_reduced_motion'])?'checked':''?>></label></div></div> <button class="btn primary" style="margin-top:20px">Save all settings</button></form></section> <?php elseif($view==='developer'): ?> <div class="sectionhead"><div><div class="kicker">DEVELOPER MODE</div><h2>Applications, messages, and event logs</h2><div class="muted">Create an application, issue an API key, send a developer message, and log an event.</div></div></div><div class="grid g3"><article class="glass card"><div class="kicker">PHP</div><div class="metric"><?=e(PHP_VERSION)?></div></article><article class="glass card"><div class="kicker">DATABASE</div><div class="metric"><?=e(db()->getAttribute(PDO::ATTR_DRIVER_NAME))?></div></article><article class="glass card"><div class="kicker">LOGS</div><div class="metric"><?=count($devLogs)?></div></article></div> <div class="grid g2" style="margin-top:16px"><section class="glass section"><h3>Create application</h3><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="create_application"><div class="field"><label>Application name</label><input name="name" required></div><div class="field"><label>Callback URL</label><input name="callback_url" type="url" placeholder="https://example.com/callback"></div><button class="btn primary">Create application</button></form></section><section class="glass section"><h3>Send message + log event</h3><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf())?>"><input type="hidden" name="action" value="developer_message"><div class="formgrid"><div class="field"><label>Level</label><select name="level"><option>info</option><option>warning</option><option>error</option><option>success</option></select></div><div class="field"><label>Event name</label><input name="event_name" placeholder="album.created" required></div></div><div class="field"><label>Message</label><textarea name="message" required></textarea></div><button class="btn accent">Send and log</button></form></section></div> <div class="sectionhead"><h2>Applications</h2></div><div class="grid g2"><?php if(!$applications):?><article class="glass card muted">No applications yet.</article><?php endif;foreach($applications as $app):?><article class="glass card"><div class="kicker"><?=$app['is_enabled']?'ACTIVE':'DISABLED'?></div><h3><?=e($app['name'])?></h3><div class="codebox"><?=e($app['api_key'])?></div><p class="muted"><?=e($app['callback_url']?:'No callback URL')?></p></article><?php endforeach;?></div> <div class="sectionhead"><h2>Event log</h2></div><section class="glass section"><?php if(!$devLogs):?><p class="muted">No developer events logged.</p><?php endif;foreach($devLogs as $log):?><div class="logline"><strong><?=e(strtoupper($log['level']))?> · <?=e($log['event_name'])?></strong><div><?=nl2br(e($log['message']))?></div><small class="muted"><?=e($log['created_at'])?></small></div><?php endforeach;?></section> <?php else: ?> <div class="sectionhead"><div><div class="kicker"><?=e(strtoupper($view))?></div><h2><?=e(ucwords($view))?></h2><div class="muted">This fresh module has its production placeholder and shares the new single-shell layout.</div></div></div><section class="glass hero"><h1><?=e(ucwords($view))?></h1><p>The route, mobile layout, navigation state, theme system, and database foundation are ready for the next functional pass.</p></section> <?php endif;?> </main></div> <div class="screensaver" id="screensaver"><div class="sshint">MOVE · TAP · PRESS A KEY TO RETURN</div><div class="ssorb"><span>GL</span></div><div class="sstime"><strong id="ssClock">00:00</strong><span id="ssDate"></span></div></div> <?php endif;?> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script> (()=>{'use strict'; const themePalettes=[['#6f5cff','#2fd8ff','#ff4fa3','#ffd85a'],['#00c8ff','#5b7cff','#a855f7','#ff4fa3'],['#ff3b6b','#ff8a3d','#ffd84d','#55e6c1'],['#14f1d9','#00a8ff','#7048ff','#ff4fb8'],['#f4c95d','#ff6d7a','#6d5dfc','#55d7ff']];let ti=0;function applyTheme(p){const r=document.documentElement;r.style.setProperty('--a1',p[0]);r.style.setProperty('--a2',p[1]);r.style.setProperty('--a3',p[2]);r.style.setProperty('--a4',p[3]);localStorage.setItem('gl_palette',JSON.stringify(p));}try{const s=JSON.parse(localStorage.getItem('gl_palette')||'null');if(Array.isArray(s))applyTheme(s);}catch(e){} const tb=document.getElementById('themeBtn');if(tb)tb.onclick=()=>{ti=(ti+1)%themePalettes.length;applyTheme(themePalettes[ti]);}; setInterval(()=>{const c=document.getElementById('clock');if(c)c.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});const s=document.getElementById('ssClock');if(s)s.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});const d=document.getElementById('ssDate');if(d)d.textContent=new Date().toLocaleDateString([],{weekday:'long',month:'long',day:'numeric'});},1000); const ss=document.getElementById('screensaver');let idle;function resetIdle(){document.body.classList.remove('simon-screensaver');if(ss)ss.classList.remove('active');clearTimeout(idle);idle=setTimeout(()=>{document.body.classList.add('simon-screensaver');if(ss)ss.classList.add('active');},120000);}['mousemove','mousedown','keydown','touchstart','scroll'].forEach(ev=>addEventListener(ev,resetIdle,{passive:true}));resetIdle(); document.querySelectorAll('[data-auth]').forEach(b=>b.onclick=()=>{document.querySelectorAll('[data-auth]').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('[data-auth-panel]').forEach(p=>p.hidden=p.dataset.authPanel!==b.dataset.auth);}); document.querySelectorAll('[data-profile-tab]').forEach(b=>b.onclick=()=>{document.querySelectorAll('[data-profile-tab]').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('[data-profile-panel]').forEach(p=>p.hidden=p.dataset.profilePanel!==b.dataset.profileTab);}); document.querySelectorAll('[data-settings-tab]').forEach(b=>b.onclick=()=>{document.querySelectorAll('[data-settings-tab]').forEach(x=>x.classList.toggle('primary',x===b));document.querySelectorAll('[data-settings-panel]').forEach(p=>p.classList.toggle('active',p.dataset.settingsPanel===b.dataset.settingsTab));}); const q=[ {k:'goal',q:'What kind of connection are you hoping to build?',o:['Relationship','Dating','Friendship','Casual','Community','Creative collaboration']}, {k:'structure',q:'Which relationship structure feels most honest for you?',o:['Monogamous','Open','Polyamorous','Exploring','Undecided']}, {k:'communication',q:'How do you prefer difficult conversations to happen?',o:['Direct and immediate','Gentle and gradual','Detailed and thoughtful','After time to process']}, {k:'energy',q:'What social rhythm feels most natural?',o:['Quiet and intimate','Balanced','Highly social','Depends on the day']}, {k:'affection',q:'What makes you feel most valued?',o:['Quality time','Physical touch','Words of affirmation','Acts of service','Thoughtful gifts']}, {k:'independence',q:'How much independence do you want in a relationship?',o:['A lot','Balanced','Mostly shared life','It depends']}, {k:'conflict',q:'What usually causes you to pull away?',o:['Feeling ignored','Pressure','Dishonesty','Conflict','Loss of freedom','Emotional distance']}, {k:'privacy',q:'Who should see your deeper answers?',o:['Everyone','Members','Matches only','Friends only','Private']} ];let qi=0,ans={};const qt=document.getElementById('questionText'),al=document.getElementById('answerList'),pg=document.getElementById('interviewProgress');function draw(){if(!qt)return;const item=q[qi];qt.textContent=item.q;al.innerHTML='';item.o.forEach(o=>{const b=document.createElement('button');b.type='button';b.className='answer'+(ans[item.k]===o?' selected':'');b.textContent=o;b.onclick=()=>{ans[item.k]=o;draw();};al.appendChild(b);});pg.style.width=((qi+1)/q.length*100)+'%';}draw();const prev=document.getElementById('prevQuestion'),next=document.getElementById('nextQuestion');if(prev)prev.onclick=()=>{qi=Math.max(0,qi-1);draw();};if(next)next.onclick=()=>{if(qi<q.length-1){qi++;draw();}else{document.getElementById('answersJson').value=JSON.stringify(ans);document.getElementById('interviewForm').submit();}}; if(document.getElementById('map')){const map=L.map('map',{zoomControl:true}).setView([32.7767,-96.7970],10);L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{maxZoom:18}).addTo(map);const pts=[[32.79,-96.8],[32.85,-96.9],[32.72,-96.78],[32.95,-96.75],[32.68,-96.92]];pts.forEach((p,i)=>L.circleMarker(p,{radius:11,color:'#fff',weight:3,fillColor:themePalettes[0][i%4],fillOpacity:.95}).addTo(map));document.getElementById('locateBtn').onclick=()=>navigator.geolocation?navigator.geolocation.getCurrentPosition(pos=>{map.setView([pos.coords.latitude,pos.coords.longitude],13);L.circleMarker([pos.coords.latitude,pos.coords.longitude],{radius:12,color:'#fff',fillColor:'#ffd85a',fillOpacity:1}).addTo(map).bindPopup('You').openPopup();document.getElementById('mapStatus').textContent='Approximate location active';},()=>document.getElementById('mapStatus').textContent='Location unavailable'):null;setTimeout(()=>map.invalidateSize(),300);} })(); </script> <script> (() => { 'use strict'; // ═══════════════════════════════════════════════════════ // UTILITY // ═══════════════════════════════════════════════════════ const clamp = (v,lo,hi) => v<lo?lo:v>hi?hi:v; const lerp = (a,b,t) => a+(b-a)*t; const PI2 = Math.PI*2; // ═══════════════════════════════════════════════════════ // CANVAS // ═══════════════════════════════════════════════════════ const c0=document.getElementById('simon-c0'); const c1=document.getElementById('simon-c1'); const c2=document.getElementById('simon-c2'); const ctx=c0.getContext('2d'); const g1 =c1.getContext('2d'); // lighter — particles/nodes const g2 =c2.getContext('2d'); // screen — glow FX let W,H,cx,cy,DPR=Math.min(devicePixelRatio||1,2); function fitCanvas(c){ c.width =Math.floor(innerWidth *DPR); c.height=Math.floor(innerHeight*DPR); c.style.width =innerWidth +'px'; c.style.height=innerHeight+'px'; c.getContext('2d').setTransform(DPR,0,0,DPR,0,0); } function resize(){ DPR=Math.min(devicePixelRatio||1,2); W=innerWidth; H=innerHeight; cx=W/2; cy=H/2; [c0,c1,c2].forEach(fitCanvas); buildStars(); } window.addEventListener('resize',resize); // ═══════════════════════════════════════════════════════ // MOUSE / INPUT // ═══════════════════════════════════════════════════════ const M={x:0,y:0,nx:0,ny:0,down:false,lx:0,ly:0,vx:0,vy:0}; let rotX=.28, rotY=0, tZoom=1, zoom=1; let pulseAmt=0, novaAmt=0, shellP=0; let gT=0; // Figure-8 Lissajous auto-rotation let autoAngle=0; window.addEventListener('mousemove',e=>{ M.x=e.clientX; M.y=e.clientY; M.nx=(e.clientX/W-.5)*2; M.ny=(e.clientY/H-.5)*2; if(M.down){ M.vx=(e.clientX-M.lx)*.013; M.vy=(e.clientY-M.ly)*.013; rotY+=M.vx; rotX+=M.vy; } M.lx=e.clientX; M.ly=e.clientY; document.getElementById('simonHint').style.opacity='0'; }); window.addEventListener('mousedown',e=>{ M.down=true; M.lx=e.clientX; M.ly=e.clientY; }); window.addEventListener('mouseup', ()=>M.down=false); window.addEventListener('click', ()=>{ pulseAmt=1; shellP=1; spawnSparks(90); }); window.addEventListener('dblclick', ()=>{ novaAmt=1; shellP=1.5; spawnSparks(240); }); window.addEventListener('wheel', e=>{ tZoom=clamp(tZoom-e.deltaY*.0012,.3,2.8); },{passive:true}); // ═══════════════════════════════════════════════════════ // 3D MATH // ═══════════════════════════════════════════════════════ function rX(y,z,a){return{y:y*Math.cos(a)-z*Math.sin(a),z:y*Math.sin(a)+z*Math.cos(a)};} function rY(x,z,a){return{x:x*Math.cos(a)+z*Math.sin(a),z:-x*Math.sin(a)+z*Math.cos(a)};} const FOV=480; function proj(x,y,z){ const sc=FOV/(FOV+z+320); return{sx:cx+x*sc, sy:cy+y*sc, sc, z}; } // ═══════════════════════════════════════════════════════ // SPHERE NODES (Fibonacci distribution) // ═══════════════════════════════════════════════════════ const N=2000; const GOLD=Math.PI*(1+Math.sqrt(5)); const nodes=[]; const DIG='0123456789'; for(let i=0;i<N;i++){ const t=Math.acos(1-2*(i+.5)/N); const f=GOLD*i; const ox=Math.sin(t)*Math.cos(f); const oy=Math.sin(t)*Math.sin(f); const oz=Math.cos(t); const nd=Math.random()<.55?1:Math.random()<.72?2:3; let num=''; for(let d=0;d<nd;d++) num+=DIG[(Math.random()*10)|0]; nodes.push({ ox,oy,oz,num, hue: 182+Math.random()*60, spLen: .10+Math.random()*.28, fs: 7 +Math.random()*11, ph: Math.random()*PI2, drift: (Math.random()-.5)*.0045, w: .6+Math.random()*1.4, // weight/brightness // color wave phase (distance from north pole) lat: Math.acos(oz) // 0=north, PI=south }); } // ═══════════════════════════════════════════════════════ // PLEXUS CONNECTIONS // ═══════════════════════════════════════════════════════ const links=[]; const THRESH=.22; for(let i=0;i<N&&links.length<7500;i++) for(let j=i+1;j<N&&links.length<7500;j++){ const d=nodes[i].ox*nodes[j].ox+nodes[i].oy*nodes[j].oy+nodes[i].oz*nodes[j].oz; if(d>1-THRESH*.44) links.push([i,j]); } // ═══════════════════════════════════════════════════════ // ENERGY RIPPLES (click spawned rings on sphere surface) // ═══════════════════════════════════════════════════════ const ripples=[]; function spawnRipple(){ ripples.push({lat:Math.random()*Math.PI, t:0, life:2.2, hue:185+Math.random()*50}); } // ═══════════════════════════════════════════════════════ // SPARKS // ═══════════════════════════════════════════════════════ const sparks=[]; function spawnSparks(n){ spawnRipple(); for(let i=0;i<n;i++){ const a=Math.random()*PI2, b=Math.acos(2*Math.random()-1); const spd=1.5+Math.random()*8; sparks.push({ x:cx, y:cy, vx:Math.sin(b)*Math.cos(a)*spd, vy:Math.sin(b)*Math.sin(a)*spd, life:1+Math.random()*.6, decay:.010+Math.random()*.022, hue:182+Math.random()*60, sz:.7+Math.random()*2.4, trail:[] }); } } // ═══════════════════════════════════════════════════════ // STAR FIELD // ═══════════════════════════════════════════════════════ const stars=[]; function buildStars(){ stars.length=0; for(let i=0;i<280;i++) stars.push({ x:Math.random()*W, y:Math.random()*H, r:.2+Math.random()*1.4, a:.04+Math.random()*.3, ph:Math.random()*PI2, sp:.004+Math.random()*.02 }); } // ═══════════════════════════════════════════════════════ // COLOR WAVE ENGINE // Sweeping latitude bands of hue shift across sphere // ═══════════════════════════════════════════════════════ function waveHue(lat, t){ // three overlapping sine waves sweeping pole-to-pole const w1=Math.sin(lat*2.5 - t*1.1)*30; const w2=Math.sin(lat*1.2 + t*.7) *20; const w3=Math.sin(lat*4 - t*1.8)*12; return w1+w2+w3; } // ═══════════════════════════════════════════════════════ // SHOOTING METEORS around sphere // ═══════════════════════════════════════════════════════ const meteors=[]; function spawnMeteor(){ const ang=Math.random()*PI2; const R0=Math.min(W,H)*.48; meteors.push({ x:cx+Math.cos(ang)*R0, y:cy+Math.sin(ang)*R0*.72, vx:(Math.random()-.5)*6, vy:(Math.random()-.5)*4, life:1, decay:.008+Math.random()*.012, hue:185+Math.random()*55, len:40+Math.random()*80 }); } let meteorTimer=0; // ═══════════════════════════════════════════════════════ // MAIN LOOP // ═══════════════════════════════════════════════════════ function loop(ts){ gT=ts*.001; // ── auto figure-8 rotation ── autoAngle+=.0042; if(!M.down){ const asx=Math.sin(autoAngle*.7)*.0025; const asy=Math.sin(autoAngle)* .0058; rotY += asy + M.vx*.88; rotX += asx + M.vy*.88; M.vx*=.93; M.vy*=.93; } // magnetic mouse tilt (gentle lean toward cursor when not dragging) if(!M.down){ rotY=lerp(rotY, rotY+M.nx*.012, .08); rotX=lerp(rotX, rotX+M.ny*.008, .06); } rotX=clamp(rotX,-.95,.95); zoom=lerp(zoom,tZoom,.08); pulseAmt*=.93; novaAmt *=.92; if(novaAmt<.01) novaAmt=0; shellP *=.91; const R=Math.min(W,H)*.38*zoom; const breathe=1+.022*Math.sin(gT*1.35)+pulseAmt*.055; // ── clear layers ── ctx.fillStyle='rgba(0,1,8,.18)'; ctx.fillRect(0,0,W,H); g1.clearRect(0,0,W,H); g2.clearRect(0,0,W,H); g1.globalCompositeOperation='lighter'; g2.globalCompositeOperation='screen'; // ── deep BG glow ── const bg=ctx.createRadialGradient(cx,cy,0,cx,cy,R*2.2); bg.addColorStop(0,'rgba(0,20,60,.08)'); bg.addColorStop(.5,'rgba(0,10,40,.05)'); bg.addColorStop(1,'rgba(0,0,0,0)'); ctx.fillStyle=bg; ctx.fillRect(0,0,W,H); // ── stars ── for(const s of stars){ s.ph+=s.sp; const a=s.a+Math.sin(s.ph)*.06; ctx.beginPath(); ctx.arc(s.x,s.y,s.r,0,PI2); ctx.fillStyle=`rgba(255,255,255,${clamp(a,0,.4)})`; ctx.fill(); } // ── atmospheric rim ── const rp=1+shellP*.18+.04*Math.sin(gT*2.1); const rim=g2.createRadialGradient(cx,cy,R*.76,cx,cy,R*1.24*rp); rim.addColorStop(0,'rgba(0,160,255,0)'); rim.addColorStop(.42,'rgba(0,210,255,.20)'); rim.addColorStop(.70,'rgba(80,235,255,.52)'); rim.addColorStop(.88,'rgba(60,100,255,.15)'); rim.addColorStop(1,'rgba(0,0,0,0)'); g2.fillStyle=rim; g2.beginPath(); g2.arc(cx,cy,R*1.26*rp,0,PI2); g2.fill(); // secondary shimmer ring for(let k=0;k<3;k++){ const kr=R*(.92+k*.055+Math.sin(gT*.85+k)*.018); g2.beginPath(); g2.arc(cx,cy,kr,0,PI2); g2.strokeStyle=`rgba(100,230,255,${.07-k*.018})`; g2.lineWidth=1.2-k*.3; g2.stroke(); } // ── horizontal scan bands ── for(let b=0;b<4;b++){ const by=cy+Math.sin(gT*.7+b*1.4)*R*.5; const bh=12+b*5; const bl=g2.createLinearGradient(cx-R*1.2,by,cx+R*1.2,by); bl.addColorStop(0,'rgba(0,0,0,0)'); bl.addColorStop(.5,`rgba(0,200,255,${.04+.02*Math.sin(gT+b)})`); bl.addColorStop(1,'rgba(0,0,0,0)'); g2.fillStyle=bl; g2.fillRect(cx-R*1.2,by-bh*.5,R*2.4,bh); } // ── rotate & project all nodes ── const pj=new Array(N); for(let i=0;i<N;i++){ const nd=nodes[i]; nd.ph+=nd.drift; const nova=1+novaAmt*1.9; const wb=breathe*nova; const px=nd.ox*wb, py=nd.oy*wb, pz=nd.oz*wb; const ry=rY(px,pz,rotY); const rx=rX(py,ry.z,rotX); const fx=ry.x*R, fy=rx.y*R, fz=rx.z*R; pj[i]={...proj(fx,fy,fz), fx,fy,fz,nd,i}; } pj.sort((a,b)=>a.z-b.z); const idxMap=new Array(N); pj.forEach(p=>idxMap[p.i]=p); // ── plexus links with color wave ── ctx.globalCompositeOperation='source-over'; ctx.lineCap='round'; for(const [ai,bi] of links){ const a=idxMap[ai], b=idxMap[bi]; if(!a||!b) continue; if(a.z<-R*.7&&b.z<-R*.7) continue; const depth=clamp(((a.z+b.z)*.5/R+1)*.5,0,1); const al=depth*.20+pulseAmt*.06; if(al<.01) continue; const wh=waveHue(a.nd.lat,gT)*.5+waveHue(b.nd.lat,gT)*.5; const hue=clamp(190+wh,140,280); ctx.beginPath(); ctx.moveTo(a.sx,a.sy); ctx.lineTo(b.sx,b.sy); ctx.strokeStyle=`hsla(${hue},100%,${55+depth*30}%,${al})`; ctx.lineWidth=.3+depth*.55; ctx.stroke(); } // ── energy ripples (latitude rings sweeping across sphere) ── for(let ri=ripples.length-1;ri>=0;ri--){ const rp2=ripples[ri]; rp2.t+=.016; if(rp2.t>rp2.life){ripples.splice(ri,1);continue;} const prog=rp2.t/rp2.life; // sweep latitude from 0→PI over lifetime const lat=prog*Math.PI; const sinLat=Math.sin(lat); // ring radius in 3D const rRad=sinLat*R*breathe; const rY2 =Math.cos(lat)*R*breathe; // project ring as ellipse const ry2=rX(rY2,0,rotX); const prY=proj(0,ry2.y,ry2.z); const ellA=rRad*proj(rRad,0,0).sc; const ellB=Math.abs(rRad*Math.cos(rotX)*.45)*proj(0,0,0).sc+1; const al2=(1-prog)*.55; ctx.beginPath(); ctx.ellipse(prY.sx,prY.sy,Math.max(1,ellA),Math.max(1,ellB),0,0,PI2); ctx.strokeStyle=`hsla(${rp2.hue},100%,75%,${al2})`; ctx.lineWidth=1.5*(1-prog)+.5; ctx.stroke(); // glow on g2 g2.beginPath(); g2.ellipse(prY.sx,prY.sy,Math.max(1,ellA),Math.max(1,ellB),0,0,PI2); g2.strokeStyle=`hsla(${rp2.hue},100%,80%,${al2*.5})`; g2.lineWidth=8*(1-prog); g2.stroke(); } // ── nodes + spines + numbers ── for(const {sx,sy,sc,z,fx,fy,fz,nd} of pj){ const depth=clamp((z/R+1)*.5,0,1); if(depth<.04) continue; nd.ph+=nd.drift*.3; const pulse=pulseAmt*Math.sin(gT*13+nd.ph); const wh=waveHue(nd.lat,gT); const hue=clamp(nd.hue+wh+pulse*22+gT*9,100,310); const al=clamp(.15+depth*.85,0,1)*(1-novaAmt*.3); // spine const nx=fx/(R*breathe), ny=fy/(R*breathe), nz=fz/(R*breathe); const sp=nd.spLen*R*(1+pulse*.45+novaAmt*.65)*breathe; const tp=proj(fx+nx*sp, fy+ny*sp, fz+nz*sp); const sg=ctx.createLinearGradient(sx,sy,tp.sx,tp.sy); sg.addColorStop(0,`hsla(${hue},100%,55%,${al*.38})`); sg.addColorStop(.6,`hsla(${hue},100%,78%,${al*.75})`); sg.addColorStop(1,`hsla(${hue+12},100%,96%,${al})`); ctx.beginPath(); ctx.moveTo(sx,sy); ctx.lineTo(tp.sx,tp.sy); ctx.strokeStyle=sg; ctx.lineWidth=Math.max(.2,sc*.7*(1+pulse*.4)*nd.w); ctx.stroke(); // node dot const nr=Math.max(.5,sc*(2.5+pulse*.7)*nd.w); ctx.beginPath(); ctx.arc(sx,sy,nr,0,PI2); ctx.fillStyle=`hsla(${hue},100%,88%,${al})`; ctx.fill(); // node glow (g1 lighter) if(depth>.32){ const gr2=sc*8*(1+pulse*.5); const ng=g1.createRadialGradient(sx,sy,0,sx,sy,gr2); ng.addColorStop(0,`hsla(${hue},100%,80%,${depth*.22})`); ng.addColorStop(1,'rgba(0,0,0,0)'); g1.fillStyle=ng; g1.beginPath(); g1.arc(sx,sy,gr2,0,PI2); g1.fill(); } // number label if(depth>.20&&sc>.12){ const fs=clamp(nd.fs*sc*zoom*(1+pulse*.2),5.5,28); ctx.font=`700 ${fs.toFixed(1)}px "Courier New"`; const ta=(.22+depth*.78)*al; ctx.fillStyle=`hsla(${hue},90%,88%,${ta})`; ctx.fillText(nd.num, tp.sx-(fs*.28), tp.sy+(fs*.35)); } } // ── core multi-ring glow ── const cp=1+pulseAmt*1.3+novaAmt*2.8; [[R*.055*cp,'rgba(220,255,255,.95)'],[R*.14*cp,'rgba(0,240,255,.5)'], [R*.32*cp,'rgba(0,160,255,.18)'],[R*.6*cp,'rgba(0,80,200,.06)']].forEach(([r,col])=>{ const cg=g2.createRadialGradient(cx,cy,0,cx,cy,r); cg.addColorStop(0,col); cg.addColorStop(1,'rgba(0,0,0,0)'); g2.fillStyle=cg; g2.beginPath(); g2.arc(cx,cy,r,0,PI2); g2.fill(); }); // ── meteors ── meteorTimer++; if(meteorTimer>90){ meteorTimer=0; spawnMeteor(); } for(let i=meteors.length-1;i>=0;i--){ const m=meteors[i]; m.life-=m.decay; if(m.life<=0){meteors.splice(i,1);continue;} const tail=ctx.createLinearGradient(m.x,m.y,m.x-m.vx*m.len*.2,m.y-m.vy*m.len*.2); tail.addColorStop(0,`hsla(${m.hue},100%,90%,${m.life*.7})`); tail.addColorStop(1,'rgba(0,0,0,0)'); ctx.beginPath(); ctx.moveTo(m.x,m.y); ctx.lineTo(m.x-m.vx*m.len*.2, m.y-m.vy*m.len*.2); ctx.strokeStyle=tail; ctx.lineWidth=m.life*2; ctx.stroke(); m.x+=m.vx; m.y+=m.vy; } // ── sparks with trails ── for(let i=sparks.length-1;i>=0;i--){ const s=sparks[i]; s.trail.push({x:s.x,y:s.y}); if(s.trail.length>10) s.trail.shift(); s.x+=s.vx*(R*.014); s.y+=s.vy*(R*.014); s.vx*=.97; s.vy*=.97; s.life-=s.decay; if(s.life<=0){sparks.splice(i,1);continue;} for(let ti=1;ti<s.trail.length;ti++){ const ta=(ti/s.trail.length)*s.life*.5; ctx.beginPath(); ctx.moveTo(s.trail[ti-1].x,s.trail[ti-1].y); ctx.lineTo(s.trail[ti].x,s.trail[ti].y); ctx.strokeStyle=`hsla(${s.hue},100%,82%,${ta})`; ctx.lineWidth=s.sz*ti/s.trail.length; ctx.stroke(); } ctx.beginPath(); ctx.arc(s.x,s.y,s.sz*clamp(s.life,0,1),0,PI2); ctx.fillStyle=`hsla(${s.hue},100%,92%,${clamp(s.life,0,1)*.85})`; ctx.fill(); } // ── composite all layers ── ctx.save(); ctx.globalCompositeOperation='screen'; ctx.drawImage(c1,0,0); ctx.drawImage(c2,0,0); ctx.restore(); // ── custom cursor (boot/screensaver only) ── if(document.body.classList.contains('simon-boot') || document.body.classList.contains('simon-screensaver')){ const mx=M.x||cx, my=M.y||cy; const ch=(192+gT*25)%360; const cr=9+shellP*5; ctx.globalCompositeOperation='source-over'; // rotating ring ctx.save(); ctx.translate(mx,my); ctx.rotate(gT*.8); ctx.beginPath(); ctx.arc(0,0,cr,0,PI2); ctx.strokeStyle=`hsla(${ch},100%,72%,.82)`; ctx.lineWidth=1.1; ctx.stroke(); [0,.5,1,1.5].forEach(f=>{ const a=f*Math.PI; ctx.beginPath(); ctx.moveTo(Math.cos(a)*(cr+3),Math.sin(a)*(cr+3)); ctx.lineTo(Math.cos(a)*(cr+8),Math.sin(a)*(cr+8)); ctx.strokeStyle=`hsla(${ch},100%,82%,.6)`; ctx.lineWidth=1; ctx.stroke(); }); ctx.restore(); ctx.beginPath(); ctx.arc(mx,my,1.8,0,PI2); ctx.fillStyle=`hsla(${ch},100%,98%,1)`; ctx.fill(); } // ── HUD ── ctx.font='700 9px "Courier New"'; ctx.letterSpacing='1px'; ctx.fillStyle='rgba(0,190,255,.24)'; ctx.textAlign='left'; ctx.fillText('SIMON // COGNITIVE SPHERE 8K',22,24); ctx.fillText(`NODES ${N} LINKS ${links.length} ZOOM ${zoom.toFixed(2)}x`,22,H-20); ctx.textAlign='right'; const n=new Date(),p=v=>String(v).padStart(2,'0'); ctx.fillText(`${n.getFullYear()}.${p(n.getMonth()+1)}.${p(n.getDate())} ${p(n.getHours())}:${p(n.getMinutes())}:${p(n.getSeconds())}`,W-22,H-20); ctx.textAlign='left'; requestAnimationFrame(loop); } // ═══════════════════════════════════════════════════════ // BOOT // ═══════════════════════════════════════════════════════ resize(); M.x=cx; M.y=cy; setTimeout(()=>spawnSparks(40),600); // intro burst requestAnimationFrame(loop); // Show the three-second cinematic launch only once per browser tab. // Normal menu clicks reload PHP pages, but they no longer replay the intro. const bootKey='gaylord_simon_intro_seen'; let shouldRunIntro=false; try{ shouldRunIntro=sessionStorage.getItem(bootKey)!=='1'; if(shouldRunIntro) sessionStorage.setItem(bootKey,'1'); }catch(e){ // If sessionStorage is unavailable, avoid replaying the intro between clicks. shouldRunIntro=false; } document.body.classList.add('simon-background'); if(shouldRunIntro){ document.body.classList.remove('simon-background'); document.body.classList.add('simon-boot'); setTimeout(()=>{ document.body.classList.add('simon-boot-fade'); setTimeout(()=>{ document.body.classList.remove('simon-boot','simon-boot-fade'); document.body.classList.add('simon-background'); },900); },3000); } })(); </script> <div class="simon-assistant" id="simonAssistant" role="dialog" aria-label="SIMON assistant" aria-modal="false"> <div style="display:flex;justify-content:space-between;gap:12px;align-items:start"><div><div class="kicker">SIMON · LIVING INTERFACE</div><h3>Your universe is active.</h3><p class="muted">The sphere changes signal color with the current area and unread activity.</p></div><button class="btn" type="button" id="closeSimon">×</button></div> <div class="simon-signal-list"> <a href="?view=messages">🟣 Messages signal</a><a href="?view=groups">🔵 Groups signal</a><a href="?view=marketplace">🟡 Marketplace signal</a><a href="?view=events">🟢 Events signal</a><a href="?view=notifications">🔴 Alerts signal</a> </div> </div> <div class="album-viewer" id="albumViewer" role="dialog" aria-modal="true" aria-label="Cinematic album viewer"> <div class="album-viewer-title" id="albumViewerTitle">Album</div><button class="btn album-close" type="button" id="albumClose">Close</button> <div class="album-stage"><img id="albumViewerImage" alt="Album cover"></div> <div class="album-viewer-controls"><button class="btn" type="button" id="albumPrev">Previous</button><button class="btn primary" type="button" id="albumPlay">Play highlight</button><button class="btn" type="button" id="albumNext">Next</button></div> </div> <script> (()=>{'use strict'; const root=document.documentElement; addEventListener('pointermove',e=>{root.style.setProperty('--mx',((e.clientX/innerWidth)-.5).toFixed(3));root.style.setProperty('--my',((e.clientY/innerHeight)-.5).toFixed(3));},{passive:true}); const view=new URLSearchParams(location.search).get('view')||'home'; const signals={messages:'#a855f7',groups:'#2fd8ff',marketplace:'#ffd85a',events:'#56e58a',notifications:'#ff5068'}; root.style.setProperty('--simon-signal',signals[view]||'#8b5cff'); const orb=document.getElementById('simonOrb'),panel=document.getElementById('simonAssistant'); if(orb&&panel){orb.addEventListener('click',()=>panel.classList.toggle('open'));document.getElementById('closeSimon')?.addEventListener('click',()=>panel.classList.remove('open'));} const unread=[...document.querySelectorAll('.kicker')].some(x=>x.textContent.trim()==='NEW'); if(unread)orb?.classList.add('has-alerts'); document.querySelectorAll('.progressbar i').forEach((bar,i)=>{const target=bar.style.width||getComputedStyle(bar).width;bar.style.width='0';setTimeout(()=>bar.style.width=target,160+i*35);bar.style.transition='width 1.1s cubic-bezier(.2,.8,.2,1)';}); document.querySelectorAll('[data-cosmic-profile]').forEach((card,i)=>{card.style.setProperty('--float-index',String(i%5));}); const cards=[...document.querySelectorAll('.album-card')].filter(c=>c.dataset.albumImage); const viewer=document.getElementById('albumViewer'),img=document.getElementById('albumViewerImage'),title=document.getElementById('albumViewerTitle');let ai=0,timer=null,scale=1; function show(i){if(!cards.length)return;ai=(i+cards.length)%cards.length;img.src=cards[ai].dataset.albumImage;title.textContent=cards[ai].dataset.albumTitle||'Album';img.classList.remove('album-fan');requestAnimationFrame(()=>img.classList.add('album-fan'));scale=1;img.style.transform='scale(1)';} cards.forEach((c,i)=>c.addEventListener('click',()=>{viewer.classList.add('open');show(i);})); document.getElementById('albumClose')?.addEventListener('click',()=>{viewer.classList.remove('open');clearInterval(timer);timer=null;}); document.getElementById('albumPrev')?.addEventListener('click',()=>show(ai-1));document.getElementById('albumNext')?.addEventListener('click',()=>show(ai+1)); document.getElementById('albumPlay')?.addEventListener('click',e=>{if(timer){clearInterval(timer);timer=null;e.currentTarget.textContent='Play highlight';}else{show(ai);timer=setInterval(()=>show(ai+1),2200);e.currentTarget.textContent='Pause highlight';}}); viewer?.addEventListener('wheel',e=>{e.preventDefault();scale=Math.max(1,Math.min(4,scale-e.deltaY*.002));img.style.transform=`scale(${scale})`;},{passive:false}); let pinch=0;viewer?.addEventListener('touchstart',e=>{if(e.touches.length===2)pinch=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);},{passive:true}); viewer?.addEventListener('touchmove',e=>{if(e.touches.length===2&&pinch){const d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);scale=Math.max(1,Math.min(4,scale*d/pinch));pinch=d;img.style.transform=`scale(${scale})`;e.preventDefault();}},{passive:false}); })(); </script> </body></html>
Save file
Quick jump
open a path
Open