Universal Horizon Sync

CHRONOS STATION

High-precision temporal tracking for global synchronization. Retro aesthetics meeting cutting-edge real-time data.

0
0
0
0
:
0
0
0
0
:
0
0
0
0
· ·
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

gsap.registerPlugin(ScrollTrigger);

    const STORAGE_KEY = 'tp_world_clocks';
    const DEFAULT_CLOCKS = [
      { id: '1', name: 'LONDON', tz: 'Europe/London' },
      { id: '2', name: 'NEW YORK', tz: 'America/New_York' },
      { id: '3', name: 'TOKYO', tz: 'Asia/Tokyo' },
      { id: '4', name: 'KOLKATA', tz: 'Asia/Kolkata' }
    ];

    const TZ_ALIASES = {
      // Asia
      'Asia/Kolkata': 'India Kolkata Delhi Mumbai Chennai IST',
      'Asia/Calcutta': 'India Kolkata Delhi Mumbai Chennai IST',
      'Asia/Tokyo': 'Japan JST Tokyo Osaka Kyoto',
      'Asia/Shanghai': 'China Shanghai Beijing CST',
      'Asia/Hong_Kong': 'Hong Kong HK',
      'Asia/Singapore': 'Singapore SGT',
      'Asia/Seoul': 'Korea South Korea Seoul KST',
      'Asia/Dubai': 'UAE United Arab Emirates Dubai GST Abu Dhabi',
      'Asia/Jakarta': 'Indonesia Jakarta WIB',
      'Asia/Bangkok': 'Thailand Bangkok ICT',
      'Asia/Manila': 'Philippines Manila PHT',
      'Asia/Ho_Chi_Minh': 'Vietnam Ho Chi Minh City Saigon',
      'Asia/Kuala_Lumpur': 'Malaysia Kuala Lumpur MYT',
      'Asia/Taipei': 'Taiwan Taipei CST',
      'Asia/Jerusalem': 'Israel Jerusalem IST',
      'Asia/Riyadh': 'Saudi Arabia Riyadh AST',
      'Asia/Tehran': 'Iran Tehran IRST',
      'Asia/Kabul': 'Afghanistan Kabul AFT',
      'Asia/Tashkent': 'Uzbekistan Tashkent UZT',

      // Europe
      'Europe/London': 'UK United Kingdom London Great Britain BST GMT GB',
      'Europe/Paris': 'France Paris CET CEST',
      'Europe/Berlin': 'Germany Berlin CET CEST Munich Frankfurt Hamburg',
      'Europe/Rome': 'Italy Rome Milan CET CEST',
      'Europe/Madrid': 'Spain Madrid Barcelona CET CEST',
      'Europe/Amsterdam': 'Netherlands Amsterdam CET CEST',
      'Europe/Brussels': 'Belgium Brussels CET CEST',
      'Europe/Zurich': 'Switzerland Zurich CET CEST',
      'Europe/Vienna': 'Austria Vienna CET CEST',
      'Europe/Stockholm': 'Sweden Stockholm CET CEST',
      'Europe/Oslo': 'Norway Oslo CET CEST',
      'Europe/Copenhagen': 'Denmark Copenhagen CET CEST',
      'Europe/Helsinki': 'Finland Helsinki EET EEST',
      'Europe/Athens': 'Greece Athens EET EEST',
      'Europe/Istanbul': 'Turkey Istanbul TRT',
      'Europe/Moscow': 'Russia Moscow MSK Saint Petersburg',
      'Europe/Kiev': 'Ukraine Kyiv Kiev EET EEST',
      'Europe/Warsaw': 'Poland Warsaw CET CEST',
      'Europe/Prague': 'Czechia Prague CET CEST',
      'Europe/Lisbon': 'Portugal Lisbon WET WEST',
      'Europe/Dublin': 'Ireland Dublin GMT IST',

      // Americas
      'America/New_York': 'USA United States NYC NY East Coast EST EDT Washington',
      'America/Chicago': 'USA United States Chicago Central Time CST CDT',
      'America/Denver': 'USA United States Denver Mountain Time MST MDT',
      'America/Los_Angeles': 'USA United States LA California CA West Coast PST PDT',
      'America/Anchorage': 'USA Alaska AK AKT AKST',
      'America/Honolulu': 'USA Hawaii HI HST',
      'America/Toronto': 'Canada Toronto Ontario EST EDT',
      'America/Vancouver': 'Canada Vancouver BC PST PDT',
      'America/Mexico_City': 'Mexico Mexico City CST CDT',
      'America/Sao_Paulo': 'Brazil Sao Paulo BRT Sao Paulo Rio Janeiro',
      'America/Argentina/Buenos_Aires': 'Argentina Buenos Aires ART',
      'America/Santiago': 'Chile Santiago CLT CLST',
      'America/Bogota': 'Colombia Bogota COT',
      'America/Lima': 'Peru Lima PET',
      'America/Caracas': 'Venezuela Caracas VET',

      // Oceania
      'Australia/Sydney': 'Australia Sydney Melbourne Canberra AEDT AEST Victoria NSW',
      'Australia/Perth': 'Australia Perth AWST Western Australia',
      'Australia/Adelaide': 'Australia Adelaide ACDT ACST South Australia',
      'Pacific/Auckland': 'New Zealand Auckland NZDT NZST',
      'Pacific/Fiji': 'Fiji FJT FJST',

      // Africa
      'Africa/Johannesburg': 'South Africa Johannesburg Cape Town SAST',
      'Africa/Cairo': 'Egypt Cairo EET EEST',
      'Africa/Nairobi': 'Kenya Nairobi East Africa EAT',
      'Africa/Lagos': 'Nigeria Lagos WAT',
      'Africa/Casablanca': 'Morocco Casablanca WET WEST',
      'Africa/Addis_Ababa': 'Ethiopia Addis Ababa EAT'
    };

    const DISPLAY_NAMES = {
      'Asia/Kolkata': 'KOLKATA',
      'Asia/Calcutta': 'KOLKATA',
      'Asia/Saigon': 'HO CHI MINH CITY',
      'Asia/Katmandu': 'KATHMANDU',
      'America/New_York': 'NEW YORK',
      'Europe/London': 'LONDON'
    };

    const ALL_TIMEZONES = Intl.supportedValuesOf('timeZone');
    
    // Ensure common timezones are present (some browsers/environments omit aliases)
    ['Asia/Kolkata', 'Asia/Calcutta', 'Asia/Shanghai', 'America/New_York', 'Europe/London'].forEach(tz => {
      if (!ALL_TIMEZONES.includes(tz)) {
        try {
          new Intl.DateTimeFormat('en-US', { timeZone: tz });
          ALL_TIMEZONES.push(tz);
        } catch (e) {}
      }
    });

    // Deduplicate Kolkata/Calcutta and other common aliases to avoid double entries in search
    if (ALL_TIMEZONES.includes('Asia/Kolkata') && ALL_TIMEZONES.includes('Asia/Calcutta')) {
      const idx = ALL_TIMEZONES.indexOf('Asia/Calcutta');
      if (idx > -1) ALL_TIMEZONES.splice(idx, 1);
    }
    let activeClocks = JSON.parse(localStorage.getItem(STORAGE_KEY)) || DEFAULT_CLOCKS;
    let heroTz = 'UTC';
    let selectedClockId = null;

    // Pre-calculate short codes for searchability
    const TZ_SHORT_CODES = {};
    ALL_TIMEZONES.forEach(tz => {
      try {
        const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'short' }).formatToParts(new Date());
        const short = parts.find(p => p.type === 'timeZoneName')?.value;
        if (short) TZ_SHORT_CODES[tz] = short;
      } catch (e) {}
    });

    const clockGrid = document.getElementById('clockGrid');
    const tzSearch = document.getElementById('tzSearch');
    const searchResults = document.getElementById('searchResults');
    const bgGlow = document.getElementById('bg-glow');

    // Mouse Glow
    document.addEventListener('mousemove', (e) => {
      gsap.to(bgGlow, { x: e.clientX, y: e.clientY, duration: 2, ease: 'power2.out' });
    });

    // Three.js Scene Setup (Big Ben Background)
    const canvasBen = document.querySelector('#big-ben-canvas');
    const renderer = new THREE.WebGLRenderer({ canvas: canvasBen, alpha: true, antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x0a0a0a, 0.005); // High-end mist for realism
    
    const cameraBen = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    cameraBen.position.set(0, 35, 75);
    cameraBen.lookAt(0, 40, 0); // Explicitly set initial focus

    // Glowing River Thames (Winding through terrain)
    const riverPoints = [];
    for (let i = 0; i < 20; i++) {
        const x = -200 + i * 20;
        const z = Math.sin(i * 0.5) * 40 - 50;
        riverPoints.push(new THREE.Vector3(x, -2, z));
    }
    const riverCurve = new THREE.CatmullRomCurve3(riverPoints);
    const riverGeo = new THREE.TubeGeometry(riverCurve, 64, 22, 2, false);
    const riverMat = new THREE.MeshPhongMaterial({ 
        color: 0x0088ff, 
        emissive: 0x0044ff, 
        emissiveIntensity: 1.8,
        transparent: true,
        opacity: 0.6,
        shininess: 100 
    });
    const river = new THREE.Mesh(riverGeo, riverMat);
    river.scale.y = 0.08; 
    scene.add(river);

    const riverGlowMat = new THREE.MeshBasicMaterial({ color: 0x0044ff, transparent: true, opacity: 0.15 });
    const riverGlow = new THREE.Mesh(new THREE.TubeGeometry(riverCurve, 64, 30, 2, false), riverGlowMat);
    riverGlow.scale.y = 0.04;
    scene.add(riverGlow);

    // Refined Starry Sky System
    const starCount = 4000;
    const starGeo = new THREE.BufferGeometry();
    const starPos = new Float32Array(starCount * 3);
    const starColors = new Float32Array(starCount * 3);
    
    for (let i = 0; i < starCount; i++) {
        const i3 = i * 3;
        const r = 350 + Math.random() * 250;
        const theta = Math.acos(Math.random() * 2 - 1);
        const phi = Math.random() * Math.PI * 2;
        
        starPos[i3] = r * Math.sin(theta) * Math.cos(phi);
        starPos[i3+1] = Math.abs(r * Math.sin(theta) * Math.sin(phi)); 
        starPos[i3+2] = r * Math.cos(theta);

        const color = new THREE.Color();
        const rand = Math.random();
        if (rand > 0.94) color.setHex(0xa0c0ff); // Vibrant Blue
        else if (rand > 0.88) color.setHex(0xffd0a0); // Stellar Orange
        else if (rand > 0.82) color.setHex(0xd0a0ff); // Cosmic Purple
        else color.setHex(0xffffff); // Pure White
        
        starColors[i3] = color.r;
        starColors[i3+1] = color.g;
        starColors[i3+2] = color.b;
    }
    
    starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
    starGeo.setAttribute('color', new THREE.BufferAttribute(starColors, 3));
    
    const starMat = new THREE.PointsMaterial({ 
      size: 4.2, 
      vertexColors: true, 
      transparent: true, 
      opacity: 0.8,
      sizeAttenuation: true,
      fog: false 
    });
    const stars = new THREE.Points(starGeo, starMat);
    scene.add(stars);

    // Subtle Sky Rotation for depth
    function animateStars() {
      stars.rotation.y += 0.0002;
      stars.rotation.x += 0.0001;
      // Twinkle effect
      starMat.opacity = 0.6 + Math.random() * 0.4;
    }

    // Premium Procedural 3D Moon
    const moonCanvas = document.createElement('canvas');
    moonCanvas.width = 1024; moonCanvas.height = 1024;
    const mCtx = moonCanvas.getContext('2d');
    mCtx.fillStyle = '#f0f0f5'; // Bright Moon Base
    mCtx.fillRect(0, 0, 1024, 1024);
    
    // Draw Lunar Maria & Craters
    for(let i=0; i<100; i++) {
        const x = Math.random() * 1024;
        const y = Math.random() * 1024;
        const r = 10 + Math.random() * 60;
        const grad = mCtx.createRadialGradient(x, y, 0, x, y, r);
        grad.addColorStop(0, 'rgba(0,0,0,0.25)');
        grad.addColorStop(0.8, 'rgba(0,0,0,0.1)');
        grad.addColorStop(1, 'rgba(255,255,255,0.05)');
        mCtx.fillStyle = grad;
        mCtx.beginPath();
        mCtx.arc(x, y, r, 0, Math.PI * 2);
        mCtx.fill();
    }
    // Fine Surface Grain
    for(let i=0; i<10000; i++) {
        mCtx.fillStyle = `rgba(0,0,0,${Math.random() * 0.1})`;
        mCtx.fillRect(Math.random()*1024, Math.random()*1024, 1, 1);
    }
    const moonTexture = new THREE.CanvasTexture(moonCanvas);

    const moonGeo = new THREE.SphereGeometry(18, 64, 64);
    const moonMat = new THREE.MeshPhongMaterial({ 
        map: moonTexture,
        emissive: 0xffffff, 
        emissiveMap: moonTexture,
        emissiveIntensity: 0.7,
        shininess: 0,
        fog: false 
    });
    const moon = new THREE.Mesh(moonGeo, moonMat);
    moon.position.set(-200, 180, -350);
    scene.add(moon);

    // Atmospheric Moon Halo (The Glow)
    const moonHaloGeo = new THREE.SphereGeometry(22, 32, 32);
    const moonHaloMat = new THREE.MeshBasicMaterial({ 
        color: 0xdae8ff, 
        transparent: true, 
        opacity: 0.15, 
        side: THREE.BackSide,
        blending: THREE.AdditiveBlending
    });
    const moonHalo = new THREE.Mesh(moonHaloGeo, moonHaloMat);
    moon.add(moonHalo);

    // Soft Moon Glow Light
    const moonLightSrc = new THREE.PointLight(0xd0e0ff, 2, 800);
    moonLightSrc.position.copy(moon.position);
    scene.add(moonLightSrc);

    const benGroup = new THREE.Group();
    scene.add(benGroup);

    // Realistic Ground Base for Stability
    const groundGeo = new THREE.CircleGeometry(200, 32);
    const groundMat = new THREE.MeshPhongMaterial({ color: 0x0a0a0a, shininess: 5, flatShading: true });
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -2;
    scene.add(ground);

    // Procedural Nature System (High Reliability)
    function createPineTree(x, z) {
      const treeGrp = new THREE.Group();
      const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.4, 2), new THREE.MeshPhongMaterial({ color: 0x1a0f0a }));
      trunk.position.y = 1;
      treeGrp.add(trunk);
      for(let i=0; i<3; i++) {
        const foliage = new THREE.Mesh(new THREE.ConeGeometry(2 - i*0.5, 3, 8), new THREE.MeshPhongMaterial({ color: 0x0a220a, flatShading: true }));
        foliage.position.y = 2 + i*1.5;
        treeGrp.add(foliage);
      }
      treeGrp.position.set(x, -2, z);
      treeGrp.scale.setScalar(0.5 + Math.random()*1.5);
      scene.add(treeGrp);
    }

    for (let i = 0; i < 150; i++) {
      const rad = 25 + Math.random() * 100;
      const ang = Math.random() * Math.PI * 2;
      createPineTree(Math.cos(ang) * rad, Math.sin(ang) * rad);
    }

    // Materials
    const bodyMat = new THREE.MeshPhongMaterial({ color: 0x4d4532, flatShading: true, shininess: 30 });
    
    // Canvas-based Clock Face Texture for Realism
    const faceCanvas = document.createElement('canvas');
    faceCanvas.width = 512;
    faceCanvas.height = 512;
    const ctx = faceCanvas.getContext('2d');
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, 512, 512);
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = '#000000';
    ctx.font = 'bold 50px "Times New Roman", serif';
    const romans = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
    for(let i=0; i<12; i++) {
        const angle = (i * 30) * Math.PI / 180;
        const x = 256 + Math.sin(angle) * 200;
        const y = 256 - Math.cos(angle) * 200;
        ctx.fillText(romans[i], x, y);
    }
    const faceTexture = new THREE.CanvasTexture(faceCanvas);

    const faceMat = new THREE.MeshPhongMaterial({ 
        map: faceTexture,
        emissive: 0xffffff, 
        emissiveMap: faceTexture,
        emissiveIntensity: 0.6, 
        shininess: 100 
    });
    const handMat = new THREE.MeshPhongMaterial({ color: 0x000000, shininess: 50 });

    // High-Detail Elizabeth Tower (Big Ben)
    const towerBody = new THREE.Mesh(new THREE.BoxGeometry(6, 45, 6), bodyMat);
    towerBody.position.y = 17.5;
    benGroup.add(towerBody);

    // Corner Detailing (Pillars)
    const pillarGeo = new THREE.BoxGeometry(0.8, 45.2, 0.8);
    const pillarPositions = [[3,17.5,3], [3,17.5,-3], [-3,17.5,3], [-3,17.5,-3]];
    pillarPositions.forEach(p => {
        const pillar = new THREE.Mesh(pillarGeo, bodyMat);
        pillar.position.set(p[0], p[1], p[2]);
        benGroup.add(pillar);
    });

    // Clock Platform (Wider)
    const clockSection = new THREE.Mesh(new THREE.BoxGeometry(7.5, 8, 7.5), bodyMat);
    clockSection.position.y = 40;
    benGroup.add(clockSection);

    // Clock Face Pinnacles (Decorative corner spikes)
    const pinGeo = new THREE.ConeGeometry(0.6, 3, 4);
    const pinPos = [[3.5,41.5,3.5], [3.5,41.5,-3.5], [-3.5,41.5,3.5], [-3.5,41.5,-3.5]];
    pinPos.forEach(p => {
        const pin = new THREE.Mesh(pinGeo, bodyMat);
        pin.position.set(p[0], p[1]+2.5, p[2]);
        pin.rotation.y = Math.PI / 4;
        benGroup.add(pin);
    });

    // Lantern Room (Above clock)
    const lantern = new THREE.Mesh(new THREE.BoxGeometry(4, 5, 4), bodyMat);
    lantern.position.y = 46.5;
    benGroup.add(lantern);

    // Faces & Hands
    const faceHands = [];
    for(let i=0; i<4; i++) {
        const faceGroup = new THREE.Group();
        faceGroup.rotation.y = (Math.PI / 2) * i;
        const d = 3.76;
        if (i===0) faceGroup.position.z = d;
        if (i===1) faceGroup.position.x = d;
        if (i===2) faceGroup.position.z = -d;
        if (i===3) faceGroup.position.x = -d;
        faceGroup.position.y = 40;
        benGroup.add(faceGroup);

        const face = new THREE.Mesh(new THREE.CircleGeometry(3, 32), faceMat);
        faceGroup.add(face);

        const hHand = new THREE.Mesh(new THREE.BoxGeometry(0.2, 1.5, 0.05), handMat);
        hHand.position.y = 0.75;
        const hPivot = new THREE.Group();
        hPivot.add(hHand);
        hPivot.position.z = 0.1;
        faceGroup.add(hPivot);

        const mHand = new THREE.Mesh(new THREE.BoxGeometry(0.12, 2.2, 0.05), handMat);
        mHand.position.y = 1.1;
        const mPivot = new THREE.Group();
        mPivot.add(mHand);
        mPivot.position.z = 0.15;
        faceGroup.add(mPivot);
        faceHands.push({ h: hPivot, m: mPivot });
    }

    // Main Spire
    const roofMesh = new THREE.Mesh(new THREE.ConeGeometry(5.5, 12, 4), bodyMat);
    roofMesh.position.y = 55;
    roofMesh.rotation.y = Math.PI / 4;
    benGroup.add(roofMesh);

    // High-End Natural Lighting System
    const hemiLight = new THREE.HemisphereLight(0x444488, 0x111122, 1.2); // Sky-Ground soft fill
    scene.add(hemiLight);

    const moonLight = new THREE.DirectionalLight(0xd0d0ff, 0.8); // Subtle, cool moonlit key
    moonLight.position.set(40, 100, 50);
    scene.add(moonLight);

    const rimLight = new THREE.SpotLight(0x223344, 5, 200, 0.5, 1, 1);
    rimLight.position.set(-30, 20, -50);
    rimLight.target = towerBody;
    scene.add(rimLight);

    // Dynamic Searchlights System
    const searchlights = [];
    const beamCount = 4;
    const beamColors = [0xffffff, 0xd0e0ff, 0xffffff, 0xe0f0ff];
    for (let i = 0; i < beamCount; i++) {
        const ang = (i / beamCount) * Math.PI * 2;
        const x = Math.cos(ang) * 15;
        const z = Math.sin(ang) * 15;
        
        const spot = new THREE.SpotLight(beamColors[i], 12, 180, 0.1, 0.6, 1.5);
        spot.position.set(x, -1, z);
        scene.add(spot);
        
        const targetObj = new THREE.Object3D();
        scene.add(targetObj);
        spot.target = targetObj;

        // Grounded Bulb Glow
        const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.8, 16, 16), new THREE.MeshBasicMaterial({ color: beamColors[i] }));
        bulb.position.set(x, -1.8, z);
        scene.add(bulb);

        // Volumetric Beam Mesh (Subtler)
        const beamGeo = new THREE.ConeGeometry(2, 120, 32, 1, true);
        const beamMat = new THREE.MeshBasicMaterial({ 
          color: beamColors[i], 
          transparent: true, 
          opacity: 0.08, 
          blending: THREE.AdditiveBlending,
          side: THREE.DoubleSide,
          depthWrite: false
        });
        const beam = new THREE.Mesh(beamGeo, beamMat);
        scene.add(beam);
        
        searchlights.push({ spot, target: targetObj, beam });
    }

    function animateBeam(s) {
        const tx = (Math.random() - 0.5) * 180;
        const ty = 30 + Math.random() * 90;
        const tz = (Math.random() - 0.5) * 180;
        
        gsap.to(s.target.position, { 
          x: tx, y: ty, z: tz, 
          duration: 3 + Math.random() * 4, // Faster, more cinematic sweep
          ease: "sine.inOut",
          onUpdate: () => {
             const pos = new THREE.Vector3(s.spot.position.x, -1.8, s.spot.position.z);
             const target = new THREE.Vector3(s.target.position.x, s.target.position.y, s.target.position.z);
             const mid = new THREE.Vector3().addVectors(pos, target).multiplyScalar(0.5);
             s.beam.position.copy(mid);
             s.beam.lookAt(target);
             s.beam.rotateX(Math.PI / 2);
             const dist = pos.distanceTo(target);
             s.beam.scale.set(1, dist / 120, 1);
          },
          onComplete: () => animateBeam(s)
        });
    }
    searchlights.forEach(s => animateBeam(s));

    const camAngles = [
      { pos: [60, 45, 60], look: [0, 35, 0], up: [0.3, 1, 0] },       // Wide Landscape Hero
      { pos: [12, 42, 12], look: [0, 40, 0], up: [0.2, 1, 0] },       // Balanced Face Macro
      { pos: [-80, 20, 40], look: [0, 30, 0], up: [-0.1, 1, 0] },      // River-Side Panorama
      { pos: [150, 40, 150], look: [-100, 80, -150], up: [0, 1, 0] },  // Lunar Silhouette View
      { pos: [45, 65, 45], look: [0, 30, 0], up: [0.5, 1, 0.5] },     // Side-Top Terrain View
      { pos: [-45, 55, -45], look: [5, 40, 5], up: [-0.3, 1, 0] },    // Angled Forest Observer
      { pos: [0, 65, 120], look: [0, 20, 0], up: [0, 1, 0] },         // The Grand Journey
      { pos: [40, 20, -120], look: [-150, 140, -300], up: [0, 1, 0] }, // Celestial Framing
      { pos: [-15, 42, -8], look: [0, 40, 0], up: [-0.2, 1, 0] },     // Western Face Silhouette
      { pos: [55, 50, -55], look: [10, 20, 10], up: [0.4, 1, -0.4] }, // Aerial River Sweep
      { pos: [70, 12, 70], look: [0, 45, 0], up: [-0.1, 1, -0.1] },   // Distant Majesty
      { pos: [5, 43, 8], look: [0, 40, 0], up: [0, 1, 0.2] },         // Close Architectural Glow
      { pos: [100, 60, 0], look: [0, 30, 0], up: [0.6, 1, 0] },       // God-View Panorama
      { pos: [0, 5, -130], look: [0, 50, 0], up: [0, 1, 0] }          // Long-Distance Cinematic
    ];

    // Persistent Camera State to prevent Jitter
    const currentLook = { x: 0, y: 40, z: 0 };
    const currentUp = { x: 0, y: 1, z: 0 };

    function updateBigBenHands() {
      const now = new Date();
      const heroDate = new Date(now.toLocaleString('en-US', { timeZone: heroTz }));
      const h = heroDate.getHours() % 12;
      const m = heroDate.getMinutes();
      const s = heroDate.getSeconds();
      faceHands.forEach(f => {
        f.h.rotation.z = -((h + m/60) * (Math.PI * 2 / 12));
        f.m.rotation.z = -((m + s/60) * (Math.PI * 2 / 60));
      });
    }

    function changeBenAngle() {
      const a = camAngles[Math.floor(Math.random() * camAngles.length)];
      
      // Kill any active transitions
      gsap.killTweensOf(cameraBen.position);
      gsap.killTweensOf(currentLook);
      gsap.killTweensOf(currentUp);

      const duration = 1.8 + Math.random() * 1.2;
      const ease = "expo.inOut";

      gsap.to(cameraBen.position, { x: a.pos[0], y: a.pos[1], z: a.pos[2], duration, ease });
      
      gsap.to(currentLook, { 
        x: a.look[0], y: a.look[1], z: a.look[2], 
        duration, ease, 
        onUpdate: () => {
          cameraBen.lookAt(currentLook.x, currentLook.y, currentLook.z);
        }
      });

      gsap.to(currentUp, {
        x: a.up[0], y: a.up[1], z: a.up[2],
        duration, ease,
        onUpdate: () => {
          cameraBen.up.set(currentUp.x, currentUp.y, currentUp.z);
        }
      });
    }

    function animateBen() {
      requestAnimationFrame(animateBen);
      updateBigBenHands();
      animateStars();
      benGroup.rotation.y += 0.003;
      renderer.render(scene, cameraBen);
    }
    animateBen();

    window.addEventListener('resize', () => {
      cameraBen.aspect = window.innerWidth / window.innerHeight;
      cameraBen.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });

    // Flip Clock Logic
    function updateFlipDigit(id, val) {
      const unit = document.getElementById(id);
      if(!unit) return;
      const topStatic = unit.querySelector('.flip-card-top');
      const bottomStatic = unit.querySelector('.flip-card-bottom');
      const currentVal = topStatic.innerText;
      if (currentVal === val) return;

      const leaf = document.createElement('div');
      leaf.className = 'flip-leaf';
      leaf.innerHTML = `<div class="flip-leaf-front">${currentVal}</div><div class="flip-leaf-back">${val}</div>`;
      unit.appendChild(leaf);

      topStatic.innerText = val;
      gsap.to(leaf, {
        rotateX: -180, duration: 0.3, force3D: true, z: 0.1, ease: "power2.inOut",
        onComplete: () => { bottomStatic.innerText = val; leaf.remove(); }
      });
      
      if (id === 's2' && parseInt(val) % 6 === 0) changeBenAngle();
    }

    function updateClocks() {
      const now = new Date();
      const heroDate = new Date(now.toLocaleString('en-US', { timeZone: heroTz }));
      
      // Update Flip Hero
      const h = heroDate.getHours().toString().padStart(2, '0');
      const m = heroDate.getMinutes().toString().padStart(2, '0');
      const s = heroDate.getSeconds().toString().padStart(2, '0');
      updateFlipDigit('h1', h[0]); updateFlipDigit('h2', h[1]);
      updateFlipDigit('m1', m[0]); updateFlipDigit('m2', m[1]);
      updateFlipDigit('s1', s[0]); updateFlipDigit('s2', s[1]);

      document.getElementById('localDay').innerText = heroDate.toLocaleDateString('en-US', { weekday: 'long' }).toUpperCase();
      document.getElementById('localDate').innerText = heroDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toUpperCase();
      document.getElementById('localTz').innerText = heroTz === 'UTC' ? 'UNIVERSAL COORDINATED TIME' : heroTz.split('/').pop().replace(/_/g, ' ').toUpperCase();

      // Update Cards
      activeClocks.forEach(clock => {
        const card = document.getElementById(`clock-${clock.id}`);
        if (!card) return;
        const clockDate = new Date(now.toLocaleString('en-US', { timeZone: clock.tz }));
        card.querySelector('.card-time').innerText = clockDate.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
        card.querySelector('.card-date').innerText = clockDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }).toUpperCase();
        
        const hC = clockDate.getHours(); const mC = clockDate.getMinutes(); const sC = clockDate.getSeconds();
        card.querySelector('.hand-hour').style.transform = `translateX(-50%) rotate(${(hC % 12) * 30 + mC * 0.5}deg)`;
        card.querySelector('.hand-minute').style.transform = `translateX(-50%) rotate(${mC * 6}deg)`;
        card.querySelector('.hand-second').style.transform = `translateX(-50%) rotate(${sC * 6}deg)`;
      });
    }

    function renderGrid() {
      clockGrid.innerHTML = '';
      activeClocks.forEach((clock, index) => {
        const card = document.createElement('div');
        card.className = `clock-card ${selectedClockId === clock.id ? 'active' : ''}`;
        card.id = `clock-${clock.id}`;
        card.onclick = (e) => {
          if (e.target.closest('.remove-btn')) return;
          selectClock(clock.id, clock.tz);
        };
        card.innerHTML = `
          <button class="remove-btn" onclick="removeClock('${clock.id}')" title="Remove Horizon">
            <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-linecap="round" stroke-linejoin="round">
              <line x1="18" y1="6" x2="6" y2="18"></line>
              <line x1="6" y1="6" x2="18" y2="18"></line>
            </svg>
          </button>
          <div class="card-city">${clock.name}<span class="card-short-tz">${TZ_SHORT_CODES[clock.tz] || ''}</span></div>
          <div class="card-timezone">${clock.tz.replace(/_/g, ' ')}</div>
          <div class="card-time">00:00:00</div>
          <div class="card-date">—</div>
          <div><span class="card-offset">—</span></div>
          <div class="analog-clock-face">
            <div class="analog-hand hand-hour"></div>
            <div class="analog-hand hand-minute"></div>
            <div class="analog-hand hand-second"></div>
            <div class="analog-hand hand-center"></div>
          </div>
        `;
        clockGrid.appendChild(card);
        
        // Manual Offset calc for render time
        const now = new Date();
        const localOffset = -now.getTimezoneOffset();
        const tzOffset = (new Date(now.toLocaleString('en-US', { timeZone: clock.tz })) - new Date(now.toLocaleString('en-US', { timeZone: 'UTC' }))) / 60000;
        const diff = tzOffset - localOffset;
        const dH = Math.floor(Math.abs(diff) / 60); const dM = Math.abs(diff) % 60;
        card.querySelector('.card-offset').innerText = diff === 0 ? 'SYNCHRONIZED' : `${diff > 0 ? '+' : '-'}${dH}${dM ? `:${dM}` : ''} HRS FROM LOCAL`;
      });

      gsap.from(".clock-card", {
        y: 20, rotateX: -10, duration: 0.6, stagger: 0.05, ease: "power4.out",
        scrollTrigger: { trigger: ".clock-grid", start: "top 95%" }
      });
      updateClocks();
    }

    function addClock(tz) {
      const name = DISPLAY_NAMES[tz] || tz.split('/').pop().replace(/_/g, ' ').toUpperCase();
      const newClock = { id: Date.now().toString(), name, tz };
      activeClocks.push(newClock);
      saveClocks();
      renderGrid();
      tzSearch.value = '';
      searchResults.style.display = 'none';
    }

    function selectClock(id, tz) {
      if (selectedClockId === id) {
        selectedClockId = null;
        heroTz = 'UTC';
      } else {
        selectedClockId = id;
        heroTz = tz;
      }
      renderGrid();
    }

    function removeClock(id) {
      const el = document.getElementById(`clock-${id}`);
      gsap.to(el, { 
        scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.in", 
        onComplete: () => {
          el.remove();
          activeClocks = activeClocks.filter(c => c.id !== id);
          saveClocks();
        } 
      });
    }

    function saveClocks() {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(activeClocks));
    }

    tzSearch.addEventListener('input', (e) => {
      const query = e.target.value.toLowerCase().trim();
      if (query.length < 2) { searchResults.style.display = 'none'; return; }
      const matches = ALL_TIMEZONES.filter(tz => {
        const alias = TZ_ALIASES[tz] || '';
        const short = TZ_SHORT_CODES[tz] || '';
        return tz.toLowerCase().includes(query) || 
               tz.split('/').pop().toLowerCase().includes(query) ||
               short.toLowerCase().includes(query) ||
               alias.toLowerCase().includes(query);
      }).slice(0, 15);
      if (matches.length > 0) {
        searchResults.innerHTML = matches.map(tz => {
          const display = DISPLAY_NAMES[tz] || tz.split('/').pop().replace(/_/g, ' ').toUpperCase();
          const short = TZ_SHORT_CODES[tz] || '';
          return `
            <div class="search-item" onclick="addClock('${tz}')">
              <div style="display:flex; align-items:center; width:100%; justify-content:between;">
                <span style="flex:1;">${display}</span>
                <span style="font-size:10px; color:var(--accent); background:var(--accent-dim); padding:2px 8px; border-radius:4px; margin-left:12px; font-family:var(--mono); opacity: 0.9;">${short}</span>
              </div>
              <span style="opacity: 0.3; margin-left: 20px; font-size: 11px;">${tz.split('/')[0]}</span>
            </div>
          `;
        }).join('');
        searchResults.style.display = 'block';
        gsap.from(".search-item", { x: -10, duration: 0.3 });
      } else { searchResults.style.display = 'none'; }
    });

    document.addEventListener('click', (e) => { if (!tzSearch.contains(e.target) && !searchResults.contains(e.target)) searchResults.style.display = 'none'; });

    const fsBtn = document.getElementById('fullscreen-btn');
    fsBtn.addEventListener('click', () => {
      document.body.classList.toggle('cinematic-mode');
      const isCinematic = document.body.classList.contains('cinematic-mode');
      fsBtn.querySelector('span').innerText = isCinematic ? 'EXIT CINEMATIC' : 'CHANGE VIEW';
      
      if (isCinematic) {
        if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(e => {});
      } else {
        if (document.fullscreenElement) document.exitFullscreen();
      }
    });

    // Handle ESC key or other ways out 
    document.addEventListener('fullscreenchange', () => {
       if (!document.fullscreenElement) {
           document.body.classList.remove('cinematic-mode');
           fsBtn.querySelector('span').innerText = 'CHANGE VIEW';
       }
    });

    renderGrid();
    setInterval(updateClocks, 1000);