diff --git a/index.html b/index.html index 9e53332..2847df4 100644 --- a/index.html +++ b/index.html @@ -55,8 +55,34 @@
-

AI DEATH CLOCK

-

The True Environmental Cost of Global AI Token Consumption

+

HUMAN EXTINCTION COUNTDOWN

+
+
+ — + YRS +
+ +
+ — + DAYS +
+ +
+ — + HRS +
+ +
+ — + MINS +
+ +
+ — + SECS +
+
+

at current global AI token consumption rate · see milestones ↓

diff --git a/src/js/02-counter.js b/src/js/02-counter.js index b326c3f..b71765c 100644 --- a/src/js/02-counter.js +++ b/src/js/02-counter.js @@ -158,3 +158,57 @@ if (el) el.textContent = value; } + // ---- Extinction countdown (header) ----------------------- + // Module-level constants and helpers so they are created once, not on every + // call to updateExtinctionCountdown() (which runs every second). + const _EXT_SECS_PER_YEAR = 365.25 * 24 * 3600; + const _EXT_SECS_PER_DAY = 24 * 3600; + const _EXT_SECS_PER_HOUR = 3600; + const _EXT_SECS_PER_MIN = 60; + const _EXT_UNIT_IDS = ['extYears', 'extDays', 'extHours', 'extMins', 'extSecs']; + const _extPad2 = (n) => String(Math.floor(n)).padStart(2, '0'); + const _extPad3 = (n) => String(Math.floor(n)).padStart(3, '0'); + + // Finds the milestone flagged `extinctionMarker: true`, computes the + // remaining seconds at the current dynamic rate, and updates the header + // countdown display once per second. + function updateExtinctionCountdown() { + const extinctionMilestone = MILESTONES.find(m => m.extinctionMarker); + if (!extinctionMilestone) return; + + const tokens = getCurrentTokens(); + const currentRate = getDynamicRate(new Date()); + const tokensRemaining = extinctionMilestone.tokens - tokens; + + if (tokensRemaining <= 0) { + // Extinction threshold reached — show zeroed-out timer + _EXT_UNIT_IDS.forEach(id => { + const el = document.getElementById(id); + if (el) el.textContent = '00'; + }); + return; + } + + const secsRemaining = tokensRemaining / currentRate; + const years = Math.floor(secsRemaining / _EXT_SECS_PER_YEAR); + const remAfterY = secsRemaining % _EXT_SECS_PER_YEAR; + const days = Math.floor(remAfterY / _EXT_SECS_PER_DAY); + const remAfterD = remAfterY % _EXT_SECS_PER_DAY; + const hours = Math.floor(remAfterD / _EXT_SECS_PER_HOUR); + const remAfterH = remAfterD % _EXT_SECS_PER_HOUR; + const mins = Math.floor(remAfterH / _EXT_SECS_PER_MIN); + const secs = Math.floor(remAfterH % _EXT_SECS_PER_MIN); + + const yrsEl = document.getElementById('extYears'); + const daysEl = document.getElementById('extDays'); + const hrsEl = document.getElementById('extHours'); + const minsEl = document.getElementById('extMins'); + const secsEl = document.getElementById('extSecs'); + + if (yrsEl) yrsEl.textContent = String(years); + if (daysEl) daysEl.textContent = _extPad3(days); + if (hrsEl) hrsEl.textContent = _extPad2(hours); + if (minsEl) minsEl.textContent = _extPad2(mins); + if (secsEl) secsEl.textContent = _extPad2(secs); + } + diff --git a/src/js/21-boot.js b/src/js/21-boot.js index 2de1dac..be5a48c 100644 --- a/src/js/21-boot.js +++ b/src/js/21-boot.js @@ -69,10 +69,14 @@ // Kick off the live counter RAF loop requestAnimationFrame(updateCounters); + // Populate extinction countdown immediately, then refresh every second + updateExtinctionCountdown(); + // Check time-based badges and milestone alert every second setInterval(() => { checkTimeBadges(); checkMilestoneAlert(); + updateExtinctionCountdown(); }, 1000); } diff --git a/styles/hero-tabs.css b/styles/hero-tabs.css index 198c094..af2cc20 100644 --- a/styles/hero-tabs.css +++ b/styles/hero-tabs.css @@ -33,11 +33,80 @@ header::after { text-shadow: 0 0 20px var(--accent-glow); } -.site-subtitle { +/* ---- Extinction Countdown Timer ---- */ +.extinction-timer { + display: flex; + justify-content: center; + align-items: flex-start; + gap: 0.15rem; + margin-top: 0.75rem; + flex-wrap: wrap; +} + +.extinction-unit { + display: flex; + flex-direction: column; + align-items: center; + min-width: 3rem; +} + +.extinction-value { + font-family: 'Orbitron', monospace; + font-size: clamp(1.8rem, 5vw, 3rem); + font-weight: 900; + color: var(--accent); + text-shadow: 0 0 18px var(--accent-glow); + line-height: 1; + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; +} + +.extinction-unit-label { + font-family: 'Orbitron', monospace; + font-size: 0.55rem; + letter-spacing: 0.18em; color: var(--text-dim); - font-size: 1rem; - margin-top: 0.5rem; - letter-spacing: 0.1em; + text-transform: uppercase; + margin-top: 0.25rem; +} + +.extinction-sep { + font-family: 'Orbitron', monospace; + font-size: clamp(1.4rem, 4vw, 2.4rem); + font-weight: 900; + color: var(--accent); + opacity: 0.35; + line-height: 1; + padding-top: 0.05em; + align-self: flex-start; +} + +.extinction-caption { + font-size: 0.7rem; + color: var(--text-dim); + letter-spacing: 0.08em; + margin-top: 0.75rem; + text-transform: uppercase; +} + +.extinction-link { + color: var(--accent); + text-decoration: none; + opacity: 0.7; +} + +.extinction-link:hover { + opacity: 1; + text-decoration: underline; +} + +@media (max-width: 480px) { + .extinction-unit { + min-width: 2.25rem; + } + .extinction-unit-label { + font-size: 0.48rem; + } } /* ---- Tab Bar ---- */ diff --git a/tests/e2e/death-clock.spec.js b/tests/e2e/death-clock.spec.js index 014829c..b3545fb 100644 --- a/tests/e2e/death-clock.spec.js +++ b/tests/e2e/death-clock.spec.js @@ -41,9 +41,16 @@ test.describe('AI Death Clock — end-to-end', () => { await expect(page).toHaveTitle(/Token Deathclock/i); }); - test('renders main header', async ({ page }) => { + test('renders main header with extinction countdown', async ({ page }) => { await expect(page.locator('h1.site-title')).toBeVisible(); - await expect(page.locator('h1.site-title')).toContainText('AI DEATH CLOCK'); + await expect(page.locator('h1.site-title')).toContainText('HUMAN EXTINCTION COUNTDOWN'); + // Countdown timer should be present and populated after init + await page.waitForFunction(() => { + const el = document.getElementById('extYears'); + return el && el.textContent.trim() !== '—' && el.textContent.trim() !== ''; + }, { timeout: 3000 }); + const years = await page.locator('#extYears').textContent(); + expect(parseInt(years, 10)).toBeGreaterThan(0); }); // ── Live counters ─────────────────────────────────────────────────────────