<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>World Market Clock</title>
<style>
:root {
--bg-color: #0a0a0a;
--card-closed: #1e1e1e;
--card-open: #064e3b; /* Richer Emerald Green */
--card-lunch: #78350f; /* Amber/Brown for Lunch */
--text-main: #ffffff;
--text-muted: #9ca3af;
--accent: #3b82f6;
--danger: #ef4444;
--success: #10b981;
/* Cleaner Fonts */
--font-mono: 'SF Mono', 'Roboto Mono', 'Menlo', monospace;
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: var(--font-sans);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* --- Header --- */
header {
background-color: #111;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #333;
height: 60px;
flex-shrink: 0;
}
h1 { font-size: 1.4rem; font-weight: 800; letter-spacing: -0.5px; background: linear-gradient(90deg, #fff, #aaa); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.controls { display: flex; gap: 16px; align-items: center; }
.view-toggle {
background: #222;
border-radius: 6px;
padding: 3px;
display: flex;
}
.view-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 8px 16px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
border-radius: 4px;
transition: all 0.2s;
font-family: var(--font-sans);
}
.view-btn.active {
background: var(--accent);
color: white;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.icon-btn {
background: transparent;
border: 1px solid #333;
color: #ddd;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
font-family: var(--font-sans);
font-size: 0.9rem;
}
.icon-btn:hover { background: #222; border-color: #555; }
/* --- Main Content Area --- */
main {
flex-grow: 1;
position: relative;
overflow: hidden;
padding: 24px;
height: calc(95vh - 60px); /* Subtract header and ad strip */
}
/* --- Clock Grid View --- */
#clock-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
height: 100%;
overflow-y: auto;
padding-bottom: 20px;
}
.market-card {
background-color: var(--card-closed);
border-radius: 16px;
padding: 24px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
transition: background-color 0.6s ease, transform 0.2s;
position: relative;
min-height: 200px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.05);
}
.market-card.open { background-color: var(--card-open); border-color: rgba(255,255,255,0.1); }
.market-card.lunch { background-color: var(--card-lunch); }
.card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
.market-name { font-size: 1.4rem; font-weight: 800; letter-spacing: -0.5px; }
.market-sub { font-size: 0.85rem; color: rgba(255,255,255,0.6); margin-top: 4px; font-weight: 400; }
.status-badge {
font-size: 0.7rem;
padding: 6px 10px;
border-radius: 20px;
background: rgba(0,0,0,0.4);
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
backdrop-filter: blur(4px);
}
/* BIGGER DIGITS */
.time-display {
font-family: var(--font-mono);
font-size: 3.5rem; /* Increased size */
font-weight: 500;
letter-spacing: -2px;
text-align: center;
margin: 10px 0;
transition: opacity 0.3s;
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.countdown-display {
font-family: var(--font-mono);
font-size: 1.8rem;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.hours-info {
font-size: 0.75rem;
color: rgba(255,255,255,0.5);
text-align: center;
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 12px;
font-family: var(--font-mono);
}
/* Flip Logic Classes */
.market-card.flipped .time-display { opacity: 0; }
.market-card.flipped .countdown-display { opacity: 1; }
/* Warning Colors */
.countdown-text.warn-close { color: var(--danger); font-weight: 700; text-shadow: 0 0 10px rgba(239, 68, 68, 0.4); }
.countdown-text.warn-open { color: var(--success); font-weight: 700; text-shadow: 0 0 10px rgba(16, 185, 129, 0.4); }
/* --- Map View --- */
#map-view {
display: none;
width: 100%;
height: 100%;
position: relative;
justify-content: center;
align-items: center;
background: #111;
border-radius: 16px;
overflow: hidden;
box-shadow: inset 0 0 50px rgba(0,0,0,0.5);
}
.map-svg {
width: 100%;
height: 100%;
max-width: 1400px;
fill: #2a2a2a;
stroke: #1a1a1a;
stroke-width: 1;
}
.map-marker {
position: absolute;
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
box-shadow: 0 0 0 4px rgba(255,255,255,0.1);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 10;
}
.map-marker:hover {
transform: translate(-50%, -50%) scale(2);
z-index: 20;
box-shadow: 0 0 0 6px rgba(255,255,255,0.2);
}
.map-marker.open { background-color: var(--success); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2); }
.map-marker.open:hover { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0.3), 0 0 20px var(--success); }
.map-marker.lunch { background-color: #f59e0b; }
.map-marker.closed { background-color: #525252; }
.map-tooltip {
position: absolute;
background: rgba(15, 15, 15, 0.95);
border: 1px solid #333;
padding: 16px;
border-radius: 8px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 100;
min-width: 220px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
}
.map-tooltip h3 { margin-bottom: 2px; color: var(--accent); font-size: 1.1rem; }
.map-tooltip .country-name { color: #888; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; font-weight: 600; }
.map-tooltip p { font-size: 0.9rem; margin-bottom: 4px; color: #e5e5e5; }
/* --- Settings Modal --- */
#settings-modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.85);
z-index: 999;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
}
.modal-content {
background: #1a1a1a;
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 450px;
max-height: 80vh;
overflow-y: auto;
border: 1px solid #333;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.modal-header { display: flex; justify-content: space-between; margin-bottom: 20px; align-items: center; }
.market-toggle-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #333;
transition: background 0.2s;
}
.market-toggle-item:hover { background: rgba(255,255,255,0.03); }
.market-toggle-item label { margin-left: 12px; cursor: pointer; flex-grow: 1; font-weight: 500;}
.close-modal { background: #333; border: none; color: white; padding: 6px 14px; cursor: pointer; border-radius: 6px; font-weight: 600; }
.close-modal:hover { background: #444; }
/* --- AD STRIP --- */
#ad-strip {
height: 5vh;
min-height: 40px;
width: 100%;
background: #111;
border-top: 1px solid #333;
display: flex;
justify-content: center;
align-items: center;
color: #444;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 2px;
flex-shrink: 0;
font-weight: 700;
}
/* Scrollbar */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #0a0a0a; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #444; }
</style>
</head>
<body>
<header>
<h1>WORLD MARKET CLOCK</h1>
<div class="controls">
<div class="view-toggle">
<button class="view-btn active" onclick="switchView('clock')">Grid</button>
<button class="view-btn" onclick="switchView('map')">Map</button>
</div>
<button class="icon-btn" onclick="openSettings()" title="Select Markets">⚙️ Markets</button>
<button class="icon-btn" onclick="toggleFullscreen()" id="fs-btn" title="Fullscreen">⛶</button>
</div>
</header>
<main>
<!-- Grid View -->
<div id="clock-view">
<!-- Cards injected by JS -->
</div>
<!-- Map View -->
<div id="map-view">
<!-- SVG Map -->
<svg class="map-svg" viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="2" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<!-- Simplified World Map Path -->
<path d="M226,145 L200,160 L180,150 L150,180 L160,220 L190,250 L200,280 L230,320 L240,380 L260,390 L270,360 L280,300 L320,260 L350,230 L320,190 L280,160 L250,140 Z" /> <!-- South America -->
<path d="M150,80 L200,80 L250,100 L300,80 L350,60 L320,120 L300,150 L250,130 L180,130 L120,100 Z" /> <!-- North America -->
<path d="M420,100 L450,80 L480,80 L500,100 L480,130 L450,140 L430,120 Z" /> <!-- Europe -->
<path d="M420,150 L460,150 L500,160 L520,200 L500,250 L450,280 L420,250 L400,200 Z" /> <!-- Africa -->
<path d="M520,100 L600,80 L700,80 L800,100 L850,150 L800,200 L750,250 L700,220 L650,200 L600,180 L550,150 Z" /> <!-- Asia -->
<path d="M750,300 L800,300 L850,350 L800,400 L750,380 Z" /> <!-- Australia -->
<!-- Decorative Context Lines -->
<g opacity="0.3">
<path d="M158,118 C158,118 245,69 320,60 C364,54 353,130 315,149 C277,168 238,136 238,136" stroke="none" fill="#333" />
<path d="M228,162 C228,162 289,286 261,399 C255,423 234,319 220,290 C206,261 169,215 228,162" stroke="none" fill="#333" />
<path d="M438,138 C438,138 493,127 500,146 C507,165 471,200 460,227 C449,254 456,290 440,290 C424,290 405,210 408,187 C411,164 438,138 438,138" stroke="none" fill="#333" />
<path d="M447,117 C447,117 495,80 550,100 C605,120 634,166 612,192 C590,218 554,180 527,150" stroke="none" fill="#333" />
<path d="M600,180 C600,180 675,128 750,130 C825,132 850,180 820,200 C790,220 730,250 700,230" stroke="none" fill="#333" />
<path d="M780,310 C780,310 860,300 860,360 C860,360 810,400 780,370 C750,340 780,310 780,310" stroke="none" fill="#333" />
<path d="M880,120 C880,120 920,130 900,150 C880,170 860,140 880,120" stroke="none" fill="#333" />
</g>
</svg>
<div id="map-markers-container"></div>
<div id="map-tooltip" class="map-tooltip"></div>
</div>
</main>
<div id="ad-strip">
<!-- Placeholder for Ad Unit -->
ADVERTISEMENT SPACE (5% HEIGHT)
</div>
<!-- Settings Modal -->
<div id="settings-modal">
<div class="modal-content">
<div class="modal-header">
<h3>Select Markets</h3>
<button class="close-modal" onclick="closeSettings()">Close</button>
</div>
<div id="market-list"></div>
</div>
</div>
<script>
/**
* EXTENDED MARKET DATA
*/
const markets = [
{
id: 'nyse',
name: 'NYSE / NASDAQ',
short: 'New York',
country: 'USA',
tz: 'America/New_York',
hours: [{ start: '09:30', end: '16:00' }],
coords: { x: 26, y: 28 }
},
{
id: 'tsx',
name: 'Toronto Stock Ex.',
short: 'Toronto',
country: 'Canada',
tz: 'America/Toronto',
hours: [{ start: '09:30', end: '16:00' }],
coords: { x: 24, y: 25 }
},
{
id: 'b3',
name: 'B3 (Brasil Bolsa Balcão)',
short: 'São Paulo',
country: 'Brazil',
tz: 'America/Sao_Paulo',
hours: [{ start: '10:00', end: '17:00' }],
coords: { x: 33, y: 65 }
},
{
id: 'lse',
name: 'London Stock Ex.',
short: 'London',
country: 'UK',
tz: 'Europe/London',
hours: [{ start: '08:00', end: '16:30' }],
coords: { x: 47, y: 23 }
},
{
id: 'xetra',
name: 'XETRA (Frankfurt)',
short: 'Frankfurt',
country: 'Germany',
tz: 'Europe/Berlin',
hours: [{ start: '09:00', end: '17:30' }],
coords: { x: 50, y: 24 }
},
{
id: 'six',
name: 'SIX Swiss Ex.',
short: 'Zurich',
country: 'Switzerland',
tz: 'Europe/Zurich',
hours: [{ start: '09:00', end: '17:30' }],
coords: { x: 49, y: 25.5 }
},
{
id: 'jse',
name: 'Johannesburg SE',
short: 'Johannesburg',
country: 'South Africa',
tz: 'Africa/Johannesburg',
hours: [{ start: '09:00', end: '17:00' }],
coords: { x: 53, y: 70 }
},
{
id: 'dfm',
name: 'Dubai Financial Mkt',
short: 'Dubai',
country: 'UAE',
tz: 'Asia/Dubai',
hours: [{ start: '10:00', end: '15:00' }],
coords: { x: 60, y: 35 }
},
{
id: 'nse',
name: 'National Stock Ex.',
short: 'Mumbai',
country: 'India',
tz: 'Asia/Kolkata',
hours: [{ start: '09:15', end: '15:30' }],
coords: { x: 68, y: 40 }
},
{
id: 'sgx',
name: 'Singapore Ex.',
short: 'Singapore',
country: 'Singapore',
tz: 'Asia/Singapore',
hours: [{ start: '09:00', end: '17:00' }],
coords: { x: 78, y: 55 }
},
{
id: 'hkex',
name: 'Hong Kong Ex.',
short: 'Hong Kong',
country: 'Hong Kong',
tz: 'Asia/Hong_Kong',
hours: [
{ start: '09:30', end: '12:00' },
{ start: '13:00', end: '16:00' }
],
coords: { x: 80, y: 40 }
},
{
id: 'sse',
name: 'Shanghai Stock Ex.',
short: 'Shanghai',
country: 'China',
tz: 'Asia/Shanghai',
hours: [
{ start: '09:30', end: '11:30' },
{ start: '13:00', end: '15:00' }
],
coords: { x: 81, y: 36 }
},
{
id: 'twse',
name: 'Taiwan Stock Ex.',
short: 'Taipei',
country: 'Taiwan',
tz: 'Asia/Taipei',
hours: [{ start: '09:00', end: '13:30' }],
coords: { x: 83, y: 38 }
},
{
id: 'krx',
name: 'Korea Exchange',
short: 'Seoul',
country: 'South Korea',
tz: 'Asia/Seoul',
hours: [{ start: '09:00', end: '15:30' }],
coords: { x: 85, y: 33 }
},
{
id: 'tse',
name: 'Tokyo Stock Ex.',
short: 'Tokyo',
country: 'Japan',
tz: 'Asia/Tokyo',
hours: [
{ start: '09:00', end: '11:30' },
{ start: '12:30', end: '15:00' }
],
coords: { x: 88, y: 34 }
},
{
id: 'asx',
name: 'Australian Securities',
short: 'Sydney',
country: 'Australia',
tz: 'Australia/Sydney',
hours: [{ start: '10:00', end: '16:00' }],
coords: { x: 90, y: 75 }
},
{
id: 'nzx',
name: 'New Zealand Ex.',
short: 'Wellington',
country: 'New Zealand',
tz: 'Pacific/Auckland',
hours: [{ start: '10:00', end: '16:45' }],
coords: { x: 96, y: 85 }
}
];
// State
// Default: Show major global centers initially to avoid overcrowding
const defaultIds = ['nyse','lse','xetra','tse','hkex','nse','asx','b3'];
let userSelection = JSON.parse(localStorage.getItem('wmc_selection')) || defaultIds;
let flipState = false;
// --- TIME UTILITIES ---
function getMarketStatus(market) {
const now = new Date();
const marketTimeString = now.toLocaleString('en-US', { timeZone: market.tz, hour12: false });
const marketDate = new Date(marketTimeString);
const dayOfWeek = new Date(now.toLocaleString('en-US', { timeZone: market.tz })).getDay();
// 0 = Sun, 6 = Sat
if (dayOfWeek === 0 || dayOfWeek === 6) {
return { status: 'CLOSED', label: 'WEEKEND', nextChange: null, secondsToChange: 999999 };
}
const currentMinutes = marketDate.getHours() * 60 + marketDate.getMinutes();
const currentSecondsTotal = currentMinutes * 60 + marketDate.getSeconds();
let isOpen = false;
let activeSession = null;
const sessions = market.hours.map(h => {
const [sh, sm] = h.start.split(':').map(Number);
const [eh, em] = h.end.split(':').map(Number);
return { start: sh * 60 + sm, end: eh * 60 + em };
});
for (let i = 0; i < sessions.length; i++) {
const s = sessions[i];
if (currentMinutes >= s.start && currentMinutes < s.end) {
isOpen = true;
activeSession = s;
break;
}
}
if (isOpen) {
return {
status: 'OPEN',
label: 'OPEN',
secondsToChange: (activeSession.end * 60) - currentSecondsTotal
};
} else {
let nextStart = null;
for (let s of sessions) {
if (currentMinutes < s.start) {
nextStart = s;
break;
}
}
if (nextStart) {
const isLunchTime = sessions.length > 1 && currentMinutes >= sessions[0].end && currentMinutes < sessions[1].start;
return {
status: isLunchTime ? 'LUNCH' : 'CLOSED',
label: isLunchTime ? 'BREAK' : 'CLOSED',
secondsToChange: (nextStart.start * 60) - currentSecondsTotal
};
} else {
return { status: 'CLOSED', label: 'CLOSED', secondsToChange: 999999 };
}
}
}
function formatTimeSeconds(seconds) {
if (seconds > 86400) return "> 24h";
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
}
// --- DOM GENERATION ---
const gridContainer = document.getElementById('clock-view');
const mapMarkersContainer = document.getElementById('map-markers-container');
const marketListContainer = document.getElementById('market-list');
function init() {
renderGrid();
renderMapMarkers();
renderSettings();
startLoops();
}
function renderGrid() {
gridContainer.innerHTML = '';
const selectedMarkets = markets.filter(m => userSelection.includes(m.id));
if (selectedMarkets.length === 0) {
gridContainer.innerHTML = `<div style="text-align:center; grid-column: 1/-1; padding: 40px; color:#666;">No markets selected. Click ⚙️ Markets to add some.</div>`;
return;
}
selectedMarkets.forEach(m => {
const card = document.createElement('div');
card.className = 'market-card';
card.id = `card-${m.id}`;
const hoursStr = m.hours.map(h => `${h.start}-${h.end}`).join(' / ');
card.innerHTML = `
<div class="card-header">
<div>
<div class="market-name">${m.short}</div>
<div class="market-sub">${m.name}</div>
</div>
<div class="status-badge" id="badge-${m.id}">...</div>
</div>
<div class="time-display" id="time-${m.id}">--:--:--</div>
<div class="countdown-display" id="count-box-${m.id}">
<div style="font-size:0.8rem; text-transform:uppercase; margin-bottom:5px; color:#aaa;" id="lbl-${m.id}">Time to...</div>
<div class="countdown-text" id="count-${m.id}">--:--:--</div>
</div>
<div class="hours-info">
Local Time • Hours: ${hoursStr}
</div>
`;
gridContainer.appendChild(card);
});
}
function renderMapMarkers() {
mapMarkersContainer.innerHTML = '';
markets.forEach(m => {
const marker = document.createElement('div');
marker.className = 'map-marker';
marker.id = `marker-${m.id}`;
marker.style.left = m.coords.x + '%';
marker.style.top = m.coords.y + '%';
marker.addEventListener('mouseenter', (e) => showTooltip(e, m));
marker.addEventListener('click', (e) => showTooltip(e, m));
marker.addEventListener('mouseleave', hideTooltip);
mapMarkersContainer.appendChild(marker);
});
}
function renderSettings() {
marketListContainer.innerHTML = '';
markets.forEach(m => {
const div = document.createElement('div');
div.className = 'market-toggle-item';
const checked = userSelection.includes(m.id) ? 'checked' : '';
div.innerHTML = `
<input type="checkbox" id="chk-${m.id}" ${checked} onchange="toggleMarket('${m.id}')">
<label for="chk-${m.id}">${m.name} (${m.country})</label>
`;
marketListContainer.appendChild(div);
});
}
// --- UPDATES & LOGIC ---
function updateAll() {
markets.forEach(m => {
const isSelected = userSelection.includes(m.id);
const statusData = getMarketStatus(m);
const now = new Date();
const timeStr = now.toLocaleTimeString('en-US', { timeZone: m.tz, hour12: false });
// --- UPDATE GRID CARDS ---
if (isSelected) {
const card = document.getElementById(`card-${m.id}`);
const badge = document.getElementById(`badge-${m.id}`);
const timeDisp = document.getElementById(`time-${m.id}`);
const countDisp = document.getElementById(`count-${m.id}`);
const labelDisp = document.getElementById(`lbl-${m.id}`);
if (card) {
timeDisp.innerText = timeStr;
badge.innerText = statusData.label;
card.classList.remove('open', 'lunch', 'closed', 'flipped');
if (statusData.status === 'OPEN') card.classList.add('open');
else if (statusData.status === 'LUNCH') card.classList.add('lunch');
if (flipState) {
card.classList.add('flipped');
}
let msg = "";
let val = "";
if (statusData.status === 'OPEN') {
msg = "Time to Close";
val = formatTimeSeconds(statusData.secondsToChange);
} else if (statusData.secondsToChange < 86400) {
msg = "Time to Open";
val = formatTimeSeconds(statusData.secondsToChange);
} else {
msg = "Opens";
val = "Next Work Day";
}
labelDisp.innerText = msg;
countDisp.innerText = val;
countDisp.className = 'countdown-text';
if (statusData.secondsToChange <= 10) {
if (statusData.status === 'OPEN') {
countDisp.classList.add('warn-close');
} else {
countDisp.classList.add('warn-open');
}
}
}
}
// --- UPDATE MAP MARKERS ---
const marker = document.getElementById(`marker-${m.id}`);
if (marker) {
marker.classList.remove('open', 'lunch', 'closed');
if (statusData.status === 'OPEN') marker.classList.add('open');
else if (statusData.status === 'LUNCH') marker.classList.add('lunch');
else marker.classList.add('closed');
}
});
}
function startLoops() {
// Update Grid/Time every second
updateAll();
setInterval(updateAll, 1000);
// FLIP CYCLE LOGIC:
// 10 seconds Normal Time, then 5 seconds Countdown
// Total Cycle = 15 seconds
setInterval(() => {
// Start Flip (show countdown)
flipState = true;
updateAll(); // Apply immediate CSS change
// Wait 5 seconds, then flip back
setTimeout(() => {
flipState = false;
updateAll();
}, 5000);
}, 15000); // Repeat every 15 seconds
}
// --- INTERACTION ---
function switchView(view) {
document.getElementById('clock-view').style.display = view === 'clock' ? 'grid' : 'none';
document.getElementById('map-view').style.display = view === 'map' ? 'flex' : 'none';
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
document.getElementById('fs-btn').innerText = "❌";
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
document.getElementById('fs-btn').innerText = "⛶";
}
}
}
function openSettings() {
document.getElementById('settings-modal').style.display = 'flex';
}
function closeSettings() {
document.getElementById('settings-modal').style.display = 'none';
renderGrid();
updateAll();
}
function toggleMarket(id) {
if (userSelection.includes(id)) {
userSelection = userSelection.filter(x => x !== id);
} else {
userSelection.push(id);
}
localStorage.setItem('wmc_selection', JSON.stringify(userSelection));
}
// --- MAP TOOLTIP ---
const tooltip = document.getElementById('map-tooltip');
function showTooltip(e, market) {
const status = getMarketStatus(market);
const now = new Date();
const localTime = now.toLocaleTimeString('en-US', { timeZone: market.tz, hour12: false });
let remainingText = "";
if(status.secondsToChange < 86400) {
remainingText = (status.status === 'OPEN' ? "Closes in: " : "Opens in: ") + formatTimeSeconds(status.secondsToChange);
} else {
remainingText = "Opens: Next Work Day";
}
tooltip.innerHTML = `
<div class="country-name">${market.country}</div>
<h3>${market.short}</h3>
<p>${market.name}</p>
<p style="color:white; font-family:var(--font-mono); font-size:1.2rem; margin: 8px 0;">${localTime}</p>
<p><span style="color:${status.status === 'OPEN' ? 'var(--success)' : '#aaa'}; font-weight:bold;">● ${status.label}</span></p>
<p style="font-size:0.8rem; margin-top:8px; border-top:1px solid #444; padding-top:8px; color:#aaa;">${remainingText}</p>
`;
const rect = e.target.getBoundingClientRect();
// Dynamic Positioning to keep tooltip on screen
let left = rect.left + window.scrollX + 20;
let top = rect.top + window.scrollY - 20;
// Simple boundary check (basic)
if (left + 250 > window.innerWidth) left -= 270;
if (top + 150 > window.innerHeight) top -= 150;
tooltip.style.left = left + 'px';
tooltip.style.top = top + 'px';
tooltip.style.opacity = 1;
}
function hideTooltip() {
tooltip.style.opacity = 0;
}
init();
</script>
</body>
</html>