commit f017c684dad34724b42cb428761db66504aa51cd Author: planetrenox@protonmail.com Date: Mon Oct 19 21:08:59 2026 +0000 Live PRT dot map of Earth with every ortus unit Static page: day/night lit dot map, PRT in ms/s/m/h/d/w, PRT day and week progress, milestones, next equinox/solstice, Date <-> PRT converter. Co-Authored-By: Claude Opus 5.5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..75a8f9d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# time.planetrenox.com + +Live **PRT (Planet Renox Time)**, milliseconds since `1998-03-14T03:00:00.000Z`, in every [ortus](https://github.com/planetrenox/ortus) unit (`ms s m h d w`), over a dot map of Earth lit by the real-time sun. + +Also on the page: PRT day/week progress, upcoming round-number milestones, next equinox/solstice, a subsolar marker with hover readout (lat/lon, sun altitude, solar time), and a Date ⇄ PRT converter. + +Static site, no build step. `ortus` loads from jsDelivr via an import map. Land dots come from Natural Earth 50m land, rasterized to a half-degree RLE mask in `land.js`. + +```sh +python3 -m http.server # then open http://localhost:8000 +``` + +Cloudflare Pages: no build command, output directory `/`. diff --git a/app.js b/app.js new file mode 100644 index 0000000..70663cc --- /dev/null +++ b/app.js @@ -0,0 +1,215 @@ +import { PRT } from 'ortus' +import { R, LAND } from './land.js' + +const $ = s => document.getElementById(s) +const { sin, cos, asin, atan2, max, min, floor, abs, PI } = Math +const D = PI / 180, TAU = 2 * PI +const E = PRT.toUnixMs(0) +const UNITS = { ms: 'milliseconds', s: 'seconds', m: 'minutes', h: 'hours', d: 'days', w: 'weeks' } +const nf = new Intl.NumberFormat('en-US') +const num = n => nf.format(n).replace(/,/g, ',') +const pad = n => String(n).padStart(2, '0') +const dur = t => { + const s = floor(t / 1e3), d = floor(s / 86400) + return (d ? d + 'd ' : '') + [floor(s / 3600) % 24, floor(s / 60) % 60, s % 60].map(pad).join(':') +} +const SHORT = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' } +const LONG = { ...SHORT, second: '2-digit', timeZoneName: 'short' } +const loc = (t, o = SHORT) => new Date(t).toLocaleString(undefined, o) +const iso = t => new Date(t).toISOString().replace('T', ' ').slice(0, 19) + ' UTC' +const set = (id, h) => { const e = $(id); e.innerHTML != h && (e.innerHTML = h) } +const ll = (v, p, n) => abs(v).toFixed(1) + '°' + (v < 0 ? n : p) + +// Low-precision solar ephemeris (~0.01°): declination + subsolar longitude, radians +const sun = t => { + const d = t / 864e5 - 10957.5, g = (357.529 + .98560028 * d) * D, q = 280.459 + .98564736 * d + const L = (q + 1.915 * sin(g) + .02 * sin(2 * g)) * D, e = (23.439 - 3.6e-7 * d) * D + const ra = atan2(cos(e) * sin(L), cos(L)), gmst = (280.46061837 + 360.98564736629 * d) * D + return { dec: asin(sin(e) * sin(L)), lon: (((ra - gmst) % TAU) + 3 * PI) % TAU - PI, L: ((L / D) % 360 + 360) % 360 } +} +// Next equinox/solstice: Newton-step the ecliptic longitude onto the next multiple of 90° +const SEASONS = ['March equinox', 'June solstice', 'September equinox', 'December solstice'] +const season = now => { + const q = floor(sun(now).L / 90 + 1e-9) + 1, g = q * 90 % 360 + let t = now + for (let i = 0; i < 6; i++) t += (((g - sun(t).L) % 360 + 540) % 360 - 180 || 0) / .98565 * 864e5 + t < now && (t += 365.2422 * 864e5 / 4) + return [SEASONS[q % 4], t] +} +const alt = (lat, lon, s) => asin(sin(lat) * sin(s.dec) + cos(lat) * cos(s.dec) * cos(lon - s.lon)) + +// Land mask: RLE rows, 90N → 90S +const mask = LAND.split(' ').map(r => { + const a = new Uint8Array(360 * R) + let i = 0, v = 0 + for (const n of r.split('.')) a.fill(v, i, i += parseInt(n, 36)), v ^= 1 + return a +}) +const isLand = (lat, lon) => mask[min(180 * R - 1, max(0, floor((90 - lat) * R)))][floor((lon + 180) * R) % (360 * R)] + +// Map +const LAT0 = 84, LAT1 = -58, NL = 16 +const cv = $('map'), cx = cv.getContext('2d'), sunEl = $('sun'), tip = $('tip') +let W, H, dpr, dots = [], P +const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * t) +const ramp = (st, t) => { + const i = min(st.length - 2, floor(t * (st.length - 1))) + return mix(st[i], st[i + 1], t * (st.length - 1) - i) +} +const rgba = c => `rgba(${c[0] | 0},${c[1] | 0},${c[2] | 0},${c[3].toFixed(3)})` +const LANDC = [[34, 66, 160, .62], [44, 96, 215, .8], [70, 150, 255, .92], [150, 222, 255, 1]] +const SEAC = [[30, 60, 150, .1], [60, 130, 255, .2]] +const COL = [...Array(NL)].map((_, i) => rgba(ramp(SEAC, i / (NL - 1)))).concat([...Array(NL)].map((_, i) => rgba(ramp(LANDC, i / (NL - 1))))) +const smooth = (a, b, x) => (x = min(1, max(0, (x - a) / (b - a))), x * x * (3 - 2 * x)) + +const layout = () => { + W = cv.clientWidth, H = cv.clientHeight, dpr = min(devicePixelRatio || 1, 2) + cv.width = W * dpr, cv.height = H * dpr + P = max(4.2, W / 200) + const rh = P * .866 + dots = [] + for (let j = 0, y = rh / 2; y < H; y += rh, j++) + for (let x = j & 1 ? P : P / 2; x < W; x += P) { + const lat = LAT0 - y / H * (LAT0 - LAT1), lon = x / W * 360 - 180 + dots.push({ x, y, s: sin(lat * D), c: cos(lat * D), l: lon * D, k: isLand(lat, lon) }) + } + draw() +} + +const draw = () => { + const s = sun(Date.now()), sd = sin(s.dec), cd = cos(s.dec) + const sx = (s.lon / D + 180) / 360 * W, sy = (LAT0 - s.dec / D) / (LAT0 - LAT1) * H + cx.setTransform(dpr, 0, 0, dpr, 0, 0) + cx.clearRect(0, 0, W, H) + for (const o of [-W, 0, W]) { + const g = cx.createRadialGradient(sx + o, sy, 0, sx + o, sy, W * .32) + g.addColorStop(0, 'rgba(60,140,255,.22)'), g.addColorStop(1, 'rgba(60,140,255,0)') + cx.fillStyle = g, cx.fillRect(0, 0, W, H) + } + const B = [...Array(2 * NL)].map(() => new Path2D()) + for (const d of dots) { + const a = d.s * sd + d.c * cd * cos(d.l - s.lon) + const t = smooth(-.31, .1, a) * .7 + max(a, 0) * .3 + const b = d.k * NL + min(NL - 1, floor(t * NL)), r = d.k ? P * (.27 + .11 * t) : P * .2 + B[b].moveTo(d.x + r, d.y), B[b].arc(d.x, d.y, r, 0, TAU) + } + B.forEach((p, i) => (cx.fillStyle = COL[i], cx.fill(p))) + sunEl.style.left = sx / W * 100 + '%', sunEl.style.top = sy / H * 100 + '%' +} + +cv.addEventListener('pointermove', e => { + const r = cv.getBoundingClientRect(), x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height + const lat = LAT0 - y * (LAT0 - LAT1), lon = x * 360 - 180, s = sun(Date.now()) + const a = alt(lat * D, lon * D, s) / D, st = ((12 + (lon - s.lon / D) / 15) % 24 + 24) % 24 + tip.hidden = false + tip.style.left = x * 100 + '%', tip.style.top = y * 100 + '%' + tip.classList.toggle('l', x > .7) + tip.innerHTML = `${ll(lat, 'N', 'S')} ${ll(lon, 'E', 'W')}${a > 0 ? 'sun ' + a.toFixed(0) + '° up' : a > -18 ? 'twilight' : 'night'} · solar ${pad(floor(st))}:${pad(floor(st % 1 * 60))}` +}) +cv.addEventListener('pointerleave', () => tip.hidden = true) +new ResizeObserver(layout).observe(cv) +setInterval(draw, 5e3) + +// Unit cards +const KS = Object.keys(UNITS).slice(1) +$('units').innerHTML = KS.map(u => ``).join('') +const last = {} +const toast = m => { const t = $('toast'); t.textContent = m, t.hidden = false, clearTimeout(toast.t), toast.t = setTimeout(() => t.hidden = true, 1400) } +const copy = v => navigator.clipboard?.writeText(String(v)).then(() => toast('Copied ' + nf.format(v))) +document.addEventListener('click', e => { + const u = e.target.closest('[data-u]')?.dataset.u + u && copy(PRT.now(u)) +}) +$('ms').addEventListener('click', () => copy(PRT.now())) +$('npm').addEventListener('click', () => navigator.clipboard?.writeText('npm i ortus').then(() => toast('Copied npm i ortus'))) + +// Static bits +$('ep').textContent = `A Saturday · ${loc(E, LONG)} local` +$('pips').innerHTML = ''.repeat(7) +const pips = [...$('pips').children] + +// Milestones: next round number per unit (2 significant digits), plus the trillion +const miles = now => { + const m = new Map(), add = (u, n) => { + const t = PRT.toUnixMs(n, u) + t > now && m.set(t, [...(m.get(t) || []), `${num(n)} ${u}`]) + } + for (const u in UNITS) { + const v = PRT.fromUnixMs(now, u), k = 10 ** max(1, floor(Math.log10(abs(v) || 1)) - 1) + add(u, (floor(v / k) + 1) * k) + } + add('ms', 1e12), add('s', 1e9), add('d', 11111) + return [...m].sort((a, b) => a[0] - b[0]).slice(0, 6) +} + +const age = now => { + const at = (y, mo) => { const d = new Date(E); d.setUTCFullYear(d.getUTCFullYear() + y, d.getUTCMonth() + mo); return +d } + let y = 0, mo = 0 + while (at(y + 1, 0) <= now) y++ + while (at(y, mo + 1) <= now) mo++ + const r = now - at(y, mo) + return `${y}y ${mo}mo ${floor(r / 864e5)}d ${dur(r % 864e5)}` +} + +const slow = now => { + set('utc', iso(now)) + set('loc', loc(now, LONG)) + set('age', age(now)) + set('unix', `Unix ${num(floor(now / 1e3))} s · epoch ${num(E)} ms`) + const d = PRT.fromUnixMs(now, 'd'), d0 = PRT.toUnixMs(d, 'd'), d1 = PRT.toUnixMs(d + 1, 'd') + set('day', `#${num(d)}`) + $('dayb').style.transform = `scaleX(${(now - d0) / (d1 - d0)})` + set('daym', `began ${loc(d0, { hour: '2-digit', minute: '2-digit' })} · ends in ${dur(d1 - now)}`) + const w = PRT.fromUnixMs(now, 'w'), wd = ((d % 7) + 7) % 7 + set('wk', `#${num(w)} · day ${wd + 1}/7`) + set('wkm', `Weeks start Saturday 03:00 UTC · next in ${dur(PRT.toUnixMs(w + 1, 'w') - now)}`) + pips.forEach((p, i) => p.className = i < wd ? 'on' : i == wd ? 'cur' : '') + const s = sun(now) + set('ss', `${ll(s.dec / D, 'N', 'S')} ${ll(s.lon / D, 'E', 'W')}`) + const [sn, st] = season(now) + set('ssm', `Subsolar point · declination ${(s.dec / D).toFixed(2)}°
${sn} ≈ ${loc(st)} · in ${dur(st - now)}`) + set('miles', miles(now).map(([t, l]) => `
  • ${l.join(' = ')}${loc(t)}in ${dur(t - now)}
  • `).join('')) +} + +// Main loop +let sec +const tick = () => { + const now = Date.now() + const ms = PRT.fromUnixMs(now) + last.ms != ms && ($('ms').innerHTML = num(last.ms = ms)) + for (const u of KS) { + const v = PRT.fromUnixMs(now, u), a = PRT.toUnixMs(v, u), b = PRT.toUnixMs(v + 1, u) + last[u] != v && ($('v' + u).innerHTML = num(last[u] = v)) + $('b' + u).style.transform = `scaleX(${(now - a) / (b - a)})` + set('n' + u, u == 's' ? `next in ${((b - now) / 1e3).toFixed(1)}s` : `next in ${dur(b - now)}`) + } + floor(now / 1e3) != sec && slow(now, sec = floor(now / 1e3)) + requestAnimationFrame(tick) +} +tick() + +// Converter +$('pu').innerHTML = Object.entries(UNITS).map(([u, n]) => ``).join('') +const dl = rows => rows.map(([k, v]) => `
    ${k}
    ${v}
    `).join('') +const err = e => `
    !
    ${e.message}
    ` +const toLocalInput = t => { const d = new Date(t - new Date(t).getTimezoneOffset() * 6e4); return d.toISOString().slice(0, 23) } +const conv = () => { + const v = $('cin').value + if (!v) return $('cout').innerHTML = '' + try { + const dt = new Date(v + ($('cz').checked ? 'Z' : '')) + $('cout').innerHTML = dl(Object.keys(UNITS).map(u => [u, num(PRT.fromDate(dt, u))])) + } catch (e) { $('cout').innerHTML = err(e) } +} +const pconv = () => { + try { + const n = Number($('pin').value.replace(/[\s,_]/g, '')), t = PRT.toUnixMs(n, $('pu').value) + $('pout').innerHTML = dl([['UTC', iso(t)], ['local', loc(t, LONG)], ['day', new Date(t).toLocaleDateString(undefined, { weekday: 'long' })], ['unix', num(t) + ' ms']]) + } catch (e) { $('pout').innerHTML = err(e) } +} +const fill = () => $('cin').value = $('cz').checked ? new Date().toISOString().slice(0, 23) : toLocalInput(Date.now()) +$('cnow').onclick = () => (fill(), conv()) +$('cz').onchange = () => (fill(), conv()) +$('cin').oninput = conv +$('pin').oninput = $('pu').onchange = pconv +fill(), conv(), pconv() diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..2027f4f --- /dev/null +++ b/favicon.svg @@ -0,0 +1 @@ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..2bd1079 --- /dev/null +++ b/index.html @@ -0,0 +1,78 @@ + + + + + +PRT — Planet Renox Time + + + + + + + + + + + + + + + + +
    + PRT + +
    + +
    +
    +

    Planet Renox Time

    +

    Milliseconds since 1998-03-14 03:00:00 UTC

    +
    + +
    + +
    +
    +
    –
    +
    ms official unit
    +
    +
    + +
    + +
    +

    Epoch

    1998-03-14 03:00:00 UTC

    +

    Now

    –

    +

    Elapsed

    –

    +

    PRT day

    –

    +

    PRT week

    –

    +

    Sun

    –

    +

    Upcoming milestones

      +
      +

      Convert

      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      + +
      +

      Integers, floored toward −∞ · negative before the epoch · 1 d = 86,400,000 ms · 1 w = 7 d · leap seconds ignored

      + +
      + + + + diff --git a/land.js b/land.js new file mode 100644 index 0000000..2ecdbf9 --- /dev/null +++ b/land.js @@ -0,0 +1,2 @@ +// Natural Earth 50m land, 2 cells/deg, rows 90N→90S, RLE sea/land runs (base36) +export const R=2,LAND='k0 k0 k0 k0 k0 k0 k0 k0 k0 k0 k0 k0 k0 7i.10.2.2.be 5g.1.1.10.v.1g.b7 58.6.1.17.5.a.1.2.2.1l.b9 4w.2.1.1i.4.29.2.3.3.a.45.3.6g 4t.4.3.1e.3.2w.3g.1.a.4.1.2.9.3.1d.3.6.4.4n 4n.c.2.18.2.2x.3a.5.3.5.5.g.1.6.r.3.o.a.4l 4g.2.4.2.1.g.2.7.1.o.1.1.5.2u.1w.8.1.9.9.2.t.7.4.2.4.8.1s.d.4k 4h.2.4.1f.c.2n.1k.5.2.q.2q.2.s.j.4f 45.6.e.2.1.i.1.k.e.2p.1o.j.4.6.3t.c.2.4.1.1.48 3t.4.9.1.1.9.2.5.4.13.5.2w.1p.l.e.1.1.1.3r.5.1.b.45 3p.9.8.c.2.6.5.8.2.p.4.2w.3.1.1p.1.1.e.2.6.48.b.3.3.40 3m.a.8.2.5.2.2.3.4.8.4.1.5.4.1.f.d.2u.1.2.1t.a.5.7.4d.4.46 3c.9.6.2.c.2.r.3.3.j.c.2.1.2y.1s.7.a.1.42.1.9.a.44 39.b.3.2.7.3.8.1.5.1.1.2.1.1.3.8.1.3.1.p.e.2s.3.2.1w.3.2q.6.1k.1.7.d.1.7.1.1.3r 37.8.1.2.1.8.4.5.7.4.1.9.4.b.8.4.6.4.j.2s.1.1.4f.f.1g.3.4.v.1d.2.r.1.1i 3b.j.2.9.4.c.2.1.2.1.4.6.1.1.7.c.13.28.4b.f.1e.1b.17.2.1.b.1.4.1y 3h.o.1.3.5.7.1.7.2.q.16.2a.42.d.13.2.9.1i.1a.g.2.a.1m 3n.a.r.5.3.p.18.24.46.a.1f.1h.1f.3.1.1.3.2.a.5.1n 33.f.19.4.1y.25.24.1.1z.7.1b.3.5.1d.3.3.18.1.8.2.26 33.i.h.5.7.8.2.c.4.6.3.4.1.6.18.1z.45.8.1a.1k.1.8.3.4.d.4.u.6.21 32.m.b.8.6.a.2.9.3.q.15.1x.45.8.q.3.5.1.c.25.1.2.5.c.l.1.2.5.21 32.s.1.3.2.6.4.d.2.8.4.6.1.b.1.8.14.1.1.1u.45.8.p.8.3.2.4.1.1.2.2.2p.n.8.1z 30.d.1.l.1.6.6.b.2.4.7.8.1.l.13.1v.43.8.q.9.4.2.2.7.1.2p.j.m.1o 32.a.2.t.9.7.1.5.7.x.1.1.x.1w.43.9.o.9.1.5.1.2y.6.2.5.1.7.l.1o 0.5.14.4.1s.3.8.s.9.3.3.7.7.11.w.1.1.1.1.1r.q.1.1v.1.1h.8.k.c.1.36.3.15.1h.3 14.f.1w.u.5.2.7.9.5.16.s.1m.2.6.p.1.1p.h.1a.8.i.d.2.i.1.49.1.1.z.1.2 z.y.t.1.1.1.2.3.4.1.f.w.9.9.7.3.1.12.p.1r.2d.2.1.1.1.j.1j.3.e.b.2.4t.14 y.19.2.1.9.1.2.d.2.3.1.6.6.x.5.3.2.8.1.1.b.7.1.1.1.2.2.1.1.l.o.6.1.1l.2a.u.1h.9.5.c.2.4v.2.5.5.5.1.f.5 w.1g.4.z.4.u.4.8.3.8.1.2.6.9.4.2.1.2.1.i.p.4.3.1h.28.1.2.13.n.5.k.b.4.9.2.5b.3.i.1 0.3.o.2x.1.a.1.5.3.3.1.1.2.1.1.9.1.8.1.4.4.a.3.1.5.i.t.2.1.1g.28.1b.b.1.9.3.5.3.3.2.2.k.2.a.2.5.1.5p 0.6.l.2x.a.9.c.5.2.f.1.1.1.8.a.5.1.e.1.1.p.1e.2b.2.1.1e.9.5.7.1b.2.5v 0.a.k.34.1.p.1.f.2.b.8.8.1.i.k.17.2i.1.4.1g.6.6.2.1f.2.5v 0.a.2.1.j.4m.7.4.6.k.h.15.2n.1j.4.4.4.1e.3.5w 0.i.h.4h.h.p.f.13.2m.1l.6.1i.2.5z 0.l.6.6.3.48.1.3.j.f.1.a.f.13.m.4.5.9.1j.17.2.a.2.7p 0.j.5.4p.j.d.4.6.i.z.o.k.1f.1b.a.7s 0.1.7.8.a.4g.1.6.c.l.4.4.l.q.z.h.1f.j.7.k.1.1.1.2.4.7t 9.5.d.4f.1.9.7.r.p.p.w.l.1c.m.6.l.3.7y.1 c.2.n.43.3.a.7.4.5.h.q.n.11.f.1d.n.4.q.2.7u.3 g.1.1.2.i.3y.5.8.2.4.6.1.9.g.q.m.z.3.1.9.1e.1.1.n.3.8p.2 g.2.1.2.a.2.1.41.a.2.c.2.b.8.1.6.q.l.2p.o.4.8s.1 u.44.e.4.f.1.5.a.2.1.t.i.2o.o.5.8t.1 s.43.h.3.4.3.1.c.7.6.w.g.1x.2.n.p.7.7x.1.p.4.2.1 r.43.p.2.3.d.c.2.u.f.2m.p.8.8i.b s.40.w.e.c.1.v.d.2m.q.8.7j.6.4.3.j.e t.z.3.2x.v.i.16.c.2a.1.c.p.8.7h.8.2.3.j.g e.2.9.4.1.q.1.7.1.3.1.2u.x.h.2.2.5.1.12.5.2b.1.c.r.2.1.3.1.1.7.5.71.d.c.4.2.j q.2.4.2.2.i.2.5.3.1.c.2m.x.h.8.3.14.1.2p.s.8.1.4.1.4.71.c.6.2.1.u 10.g.4.3.m.2i.w.i.7.5.3c.1.g.r.a.6p.3.7.4.9.8.8.1.1.v 10.3.1.e.u.2i.t.l.3.7.3b.1.h.8.3.d.9.6o.i.2.e.8.1.2.v 19.7.1.2.v.2h.t.v.33.2.1.5.i.6.4.c.a.2.1.6j.z.7.10 18.6.1.5.v.2g.v.u.31.2.1.8.o.1.2.a.3.3.5.2.2.6h.x.d.x 17.4.3.5.x.4.1.2a.1.4.q.v.30.b.l.4.3.9.3.2.5.4.1.6f.y.d.y 15.5.6.1.10.2m.o.u.32.9.k.5.4.9.8.6j.z.f.x 13.4.1b.2n.f.2.4.w.31.8.l.6.3.7.a.6i.z.g.x z.7.6.1.18.2o.b.1.4.z.2z.a.j.d.d.6h.10.d.10 x.5.1i.2v.8.13.2t.6.1.7.k.8.1.1.2.2.b.6f.2.1.z.d.8.1.r u.3.2.1.1m.2.2.2q.4.19.2q.f.i.5.1.2.2.1.7.3.2.6h.2.2.z.d.8.1.2.1.o s.1.1t.1.4.2p.5.18.2n.9.1.2.1.7.h.5.1.5.2.6t.2.3.5.1.p.a.13 q.2.1u.3.2.2p.6.1a.2k.8.6.6.e.2.1.7d.3.1.q.8.14 m.4.1x.2.2.2r.4.1b.2j.8.3.a.8.1.1.7o.p.8.14 i.1.24.2.4.2q.3.1b.2j.8.2.1.1.c.5.7q.p.5.s.2.d a.3.2c.1.4.2o.5.1a.2j.7.3.e.4.7r.q.4.17 3.1.1.2.2o.2o.3.1a.2j.5.5.e.5.7r.q.4.11.1.5 2w.3w.2.3.2u.c.2.7p.2.3.q.2.17.1.1 2v.3.1.3r.3.3.2u.c.1.7q.3.4.n.1.1c 2w.3t.3.3.2t.5.a.7q.3.4.m.2.1c 2y.4.1.37.5.4.9.5.2.1.2w.2.2.7t.3.5.k.1.1e 30.3a.1.4.2.4.6.a.2v.7x.3.5.k.1.1e 32.3e.a.c.2p.82.3.2.3.1.i.1.1f 32.3d.b.c.2o.82.4.2.22 33.3c.a.d.2p.80.5.1.23 33.3b.6.1.c.2.1.3.2s.7v.6.2.22 34.3c.6.1.c.1.1.1.2u.26.1.q.3.1.1.4r.6.4.g.1.1k 34.3g.1.4.38.1t.2.7.5.n.7.4r.6.2.1.1.d.1.1n 34.3k.3a.1r.5.4.5.m.8.4r.n.1.1o 34.39.1.7.3c.s.2.x.5.u.7.4s.9.2.a.1.1.1.1q 34.36.2.7.3e.s.2.2.1.t.8.4.3.k.7.4s.a.3.8.1.1t 34.34.3.5.3h.s.5.s.i.j.5.4s.b.5.2.3.1v 33.32.6.4.34.3.2.1.8.j.4.7.4.q.l.h.7.4p.c.9.1w 33.30.3e.q.3.4.7.7.5.n.o.f.7.4o.b.a.1x 33.30.3e.p.c.1.2.7.7.l.q.e.8.4f.2.3.c.8.20 33.30.3f.p.a.2.4.7.7.j.r.e.8.4d.h.2.3.3.21 33.30.3f.p.a.2.5.9.5.j.7.8.b.f.7.1.2.48.k.2.26 33.2y.1.1.3f.n.d.1.7.8.5.10.6.i.9.48.k.3.25 33.2w.3j.k.e.4.8.8.2.c.1.4.3.16.5.4b.k.3.25 33.2t.3m.j.g.3.a.7.1.a.3.1e.4.3s.3.e.m.4.24 34.2s.3m.i.5.2.a.3.b.3.2.1.2.7.4.1.1.1b.8.3o.4.d.o.4.24 34.2r.3m.j.5.2.a.2.d.3.5.7.5.1b.7.3p.4.2.5.6.o.4.24 34.2q.3n.j.2.1.t.2.7.8.4.19.9.3l.6.2.6.7.m.5.24 36.2o.3o.h.r.1.2.4.8.8.3.1a.9.3m.c.9.i.1.2.4.25 37.2n.3o.h.q.6.b.9.1.1.1.19.8.3n.2.3.7.8.i.6.26 37.2m.3p.f.l.3.5.5.c.4.7.1b.7.3u.7.6.e.2.1.6.26 38.2k.3v.9.9.i.6.2.c.3.a.59.7.7.e.9.26 38.2k.3v.3.a.l.y.2.1.3.3.4.3.4r.b.6.d.9.27 39.2k.3v.2.8.o.6.1.p.1.g.4p.d.6.a.2.1.a.26 3a.2i.3v.8.1.q.p.6.c.3.4.4n.d.7.6.g.27 3a.2h.3w.z.16.3.5.4n.d.4.1.2.4.f.2a 3d.2c.3x.z.1e.4q.h.1.3.c.2e 3f.28.3y.11.1d.4q.j.d.2f 3f.1.1.25.3w.16.1a.4s.a.2.5.8.1.1.2j 3h.24.3w.1c.c.5.o.4s.i.4.1.1.2m 3i.21.3y.1e.9.8.l.4u.h.4.2o 3i.20.3y.1f.9.b.i.4v.g.3.2p 3j.4.1.1u.3y.1j.5.g.3.6.1.4y.g.2.2q 3j.4.3.1r.3z.1l.2.5p.38 3k.3.3.1g.1.b.3y.7b.1.1.g.1.2q 3k.4.2.1c.7.2.1.6.3y.39.3.41.37 3f.1.6.4.1.z.7.5.b.5.3x.3a.4.3z.38 3n.3.1.y.p.5.3h.1.7.1.4.3c.4.3z.38 3n.4.2.u.r.5.3i.4.2.1.4.3e.4.3x.39 3m.5.4.r.s.5.3l.1.4.2m.1.1.1.s.5.3v.3a 3n.5.3.q.u.5.3p.2n.3.s.6.3s.3b 3o.5.3.q.t.5.2.3.3j.2o.4.s.7.6.1.3j.f.1.2v 3q.4.3.p.u.4.5.1.3h.2q.3.t.b.1.1.3i.3c 3r.3.3.p.v.3.3m.2r.4.u.8.2.1.3i.3c 3s.2.4.o.w.2.3.1.3.1.3e.2s.4.t.7.3.6.d.1.2y.3.2.38 3r.4.5.l.12.2.3.1.3d.2s.4.t.6.4.k.2x.3.3.38 3t.3.5.k.13.1.3g.2u.4.t.1.2.1.6.k.2u.5.3.3.2.33 3u.3.5.j.13.1.3f.2v.5.15.j.2s.4.3.39 3v.2.6.i.u.3.b.1.39.2w.6.15.j.2p.6.3.39 44.h.r.9.b.1.36.2y.5.15.j.2m.8.3.39 14.2.2y.h.q.3.2.8.9.2.33.30.4.16.i.2h.1.1.c.1.3a 42.1.2.h.o.2.2.1.4.7.3c.30.4.15.j.17.2.17.3q 17.4.2u.h.d.8.g.5.5.1.34.31.3.15.k.z.a.w.3.3.3u 1a.2.2t.h.d.8.g.8.37.31.3.14.n.2.2.t.a.u.5.2.3v 1c.1.2r.j.c.7.j.7.37.30.4.11.t.t.b.s.7.1.3v 1c.2.2r.i.b.7.j.2.7.7.2z.30.6.z.t.s.d.q.6.4.3u 1c.2.2s.i.a.7.t.7.2y.30.7.y.t.p.h.p.5.5.3u 48.i.4.2.1.8.q.1.1.b.2v.31.6.w.w.n.i.p.5.4.m.1.38 49.w.i.5.3.9.2.1.2.5.2q.31.6.u.x.n.j.p.5.2.l.4.37 4c.s.k.2.1.1.8.2.32.32.5.s.z.l.m.o.r.5.37 4d.r.3h.1.h.33.6.r.z.k.n.p.r.4.37 4g.o.3j.1.f.34.4.p.12.j.n.r.p.5.37 4j.k.1h.2.24.1.c.34.6.k.15.i.o.5.2.m.m.6.37 4m.3.3.k.3q.35.1.1.4.k.16.f.s.1.4.m.n.3.39 4u.k.17.1.22.1.d.36.5.j.17.e.y.n.m.3.39 4v.j.39.1.d.3a.3.f.1b.d.y.n.m.4.38 4w.i.3o.39.3.c.1f.c.z.n.m.5.36 4z.e.3p.3b.1.a.1h.c.z.n.m.2.1.5.33 52.b.1b.1.2d.3b.1.6.1l.c.o.1.b.3.2.h.m.3.1.3.34 54.9.3p.3g.h.2.14.c.o.1.a.4.2.h.m.2.1.1.2.2.33 56.7.m.3.1.1.2.1.d.1.2h.3d.k.2.15.a.10.4.4.f.l.1.1.1.2.6.31 57.6.l.4.1.2.2y.3b.e.3.1c.9.p.1.b.3.5.e.p.1.4.2.31 58.5.i.6.1.5.9.1.2m.1.1.3a.7.8.1d.9.p.1.b.2.7.c.q.3.1.3.31 58.5.g.f.3.1.3.5.1.1.2k.3a.2.d.1d.8.11.2.8.9.m.2.4.4.1.2.31 58.6.f.q.1.2.2l.3n.1e.8.11.2.a.6.m.2.6.7.30 59.6.d.8.2.k.2m.3m.1e.7.1.1.10.2.a.4.n.2.7.4.2.1.30 5c.5.2.5.3.w.2m.3l.1e.6.2.2.y.4.9.3.n.2.8.3.3.2.2z 5c.9.1.10.2n.3k.1g.4.3.3.x.4.9.2.n.2.b.1.1.4.2z 5e.6.2.14.2j.3k.1h.2.3.4.x.5.18.8.2z 5h.3.3.14.2j.3i.1n.5.w.1.1.3.17.9.g.1.2i 5g.1.1.2.4.13.2k.3g.1o.5.n.1.a.4.16.2.2.5.1e.1.12.1.h 5o.15.2k.3e.1o.5.n.1.b.5.t.2.8.2.3.3.1.1.2z 5p.15.2k.s.2.2i.1q.3.10.6.r.4.c.4.30 5p.1c.2f.m.8.2g.2u.6.q.5.d.1.31 5p.1e.2e.i.c.2e.2l.6.5.6.o.9.3c 5p.1g.2d.6.7.2.e.2e.2m.5.5.6.l.9.3f 5p.1h.2e.1.r.2.4.26.2n.6.4.6.9.1.a.a.g.1.2y 15.1.4j.1h.3b.1.1.24.2q.5.3.6.9.1.9.a.3g 5p.1h.3b.1.1.23.2s.6.2.5.j.a.3g 5o.1j.3c.22.2q.2.2.6.1.6.e.e.3g 5m.1m.3b.21.2s.1.2.8.1.4.e.e.k.2.2u 19.1.4c.1m.36.1.4.1z.2z.7.1.4.9.3.1.f.d.1.4.1.2w 5l.1n.3b.1y.2x.2.1.c.9.k.3.2.5.2.5.3.2g.1.d 5k.1o.3a.1y.2z.1.2.9.1.1.9.k.2.a.5.2.2v 5k.1p.34.1.4.1x.33.a.1.1.8.h.4.2.5.2.7.1.1.1.2u 4w.2.l.1s.37.1w.32.1.2.9.1.1.8.h.4.1.e.2.4.3.1.2.2m 4x.3.j.1w.32.1w.37.9.9.h.4.2.2.4.9.1.4.7.3.1.2g 5i.1z.31.1u.35.1.3.8.a.f.4.9.8.1.5.8.2.2.q.1.1o 5i.21.30.1s.36.2.2.8.1.3.4.1.2.d.5.6.4.5.2.2.2.2.3.5.2.2.1.4.2a 5i.21.30.1r.39.1.1.c.7.d.5.6.k.5.3.9.c.2.1t 5j.26.1.2.2r.1q.3b.1.1.c.1.2.3.d.4.8.b.4.4.6.2.c.g.2.1m 5j.2b.2r.1o.3b.1.2.9.4.1.6.a.4.4.1.3.7.3.1.6.3.m.g.3.1i 5i.2e.2q.1m.3g.8.h.4.6.2.1.3.7.3.1.1.3.2.3.o.f.2.1i 5h.2g.2p.1m.3h.7.r.2.2.4.m.m.c.3.1i 5h.2h.2p.1k.3j.6.r.2.2.4.p.k.b.3.1i 5h.2l.2m.1i.3i.1.2.5.q.3.2.4.j.1.8.h.2.1.7.3.4.1.1e 1c.1.44.2l.2m.1i.3m.4.r.1.s.2.6.i.1.8.6.2.1d 5i.2k.2m.1j.3o.6.1f.2.6.k.2.4.8.2.1c 5j.2j.2n.1i.3o.9.1.3.3.1.2.1.11.1.8.j.e.2.1.1.1a 5k.2j.2m.1i.3q.g.s.1.5.1.d.i.i.2.19 5l.2i.2n.1h.3s.e.m.4.4.1.2.1.c.l.h.2.2.1.16 5m.2g.2o.1h.3y.d.1.3.2.2.3.5.3.3.j.c.5.5.h.2.2.1.2.1.12 27.1.3e.2g.2o.1h.45.1.1.1.1.6.1.7.4.4.r.6.6.4.5.1.i.1.12 27.1.3f.2f.2o.1i.4e.2.8.4.u.4.8.5.6.1.d.4.11 5n.2e.2p.1i.4e.4.5.3.18.6.1.1.h.3.12 5n.2d.2r.1i.4f.2.5.2.z.1.b.5.1.1.k.1.10 5o.2b.2s.1i.4k.2.11.1.f.2.l.2.z 5o.2a.2t.1i.50.2.1.1.j.2.m.1.1f 5p.28.2u.1i.5.1.4t.3.2.3.g.2.y.1.13 5q.27.2u.1i.h.1.4i.d.9.4.21 5q.26.2u.1j.9.1.6.3.4g.e.9.4.21 5r.24.2u.1k.g.3.4g.d.a.4.21 e.3.5a.23.2v.1k.f.5.48.1.5.d.1.1.9.4.21 5r.23.2u.1m.d.6.46.5.3.d.1.1.9.6.1z 5s.22.2u.1m.c.7.45.8.1.c.c.8.16.1.q 5t.21.2u.1l.c.8.44.o.a.8.16.4.n 5u.20.2t.1m.a.a.44.p.9.8.17.1.p 5w.1y.2t.1l.9.b.44.s.6.9.17.3.l.2 20.1.3x.1w.2t.1k.9.c.41.y.3.a.1t.3 60.1u.2t.1i.b.b.41.z.2.b.1s.1.3 1o.2.4b.1t.2t.1g.d.b.41.1c.18.1.h.3.3 62.1r.2u.1f.e.b.41.1c.1r.2.3 63.1q.2v.1d.f.b.40.1e.19.1.l 63.1q.2w.1b.g.a.40.1g.1q.1.3 63.1q.2w.1a.i.9.3y.1k.16.1.l 63.1p.2y.18.i.a.g.2.3c.1q.u.1.v 63.1o.2z.18.i.9.3r.1u.u.3.t 64.1m.31.18.g.a.d.2.3b.1w.u.3.2.1.p 63.1n.32.17.f.b.3n.1z.w.2.r 63.1n.32.17.f.a.3n.23.u.2.q 63.1l.35.16.f.a.3n.23.1m 63.1g.3a.16.g.9.3n.23.1m 63.1f.3b.16.g.8.3o.24.1l 63.1c.3e.16.g.8.3o.25.1k 63.1b.3f.15.i.7.3o.26.1j 63.19.3h.13.k.6.3p.28.1h 62.19.3i.11.4g.28.1i 62.19.3j.10.4g.29.1h 62.19.3j.10.4h.28.1h 3x.1.24.19.3j.10.4i.27.1h 62.19.3k.z.4i.27.1h 61.1a.3k.y.4j.27.1h 61.19.3n.w.4k.26.1h 61.18.3o.u.4m.26.1h 61.17.3q.t.4n.25.1h 60.18.3q.s.4o.25.1h 60.17.3s.q.4p.24.1i 60.17.3s.q.4p.24.1i 61.15.3u.n.4s.s.4.17.1i 61.13.3w.m.4t.p.a.13.1j 61.12.3w.m.4u.k.h.11.1j 60.13.3x.k.4v.i.j.7.1.s.1k 5m.1.d.12.3z.h.4w.i.m.x.1l 60.11.3z.9.4.1.50.9.v.3.2.s.1l 5z.u.1.6.43.2.5a.7.w.2.3.1.1.p.17.2.d 5z.v.as.s.18.3.b 5y.w.ar.4.1.n.1a.2.b 5y.x.aw.m.1b.3.9 5x.y.aw.l.1c.2.1.1.8 5w.z.aw.l.1d.3.8 5w.y.ay.k.1d.5.1.2.3 5x.w.b1.d.1i.8.3 5x.u.b7.2.3.3.1j.8.3 5x.n.bk.1.1i.9.4 5x.n.bf.1.7.1.1g.6.6 5w.n.bp.1.1h.4.6 5w.o.bh.2.4.2.1b.2.1.1.2.3.7 5w.i.2.2.bj.8.1b.8.8 5w.i.bo.7.1a.6.b 5v.2.1.h.1.1.bl.6.1a.6.c 5v.2.1.j.bl.7.18.6.d 5v.2.1.h.bo.5.18.6.e 5v.j.d0.8.e 5w.i.cy.7.h 5v.i.cx.9.h 5v.g.cz.8.i 5u.g.cz.9.i 5t.g.d0.8.j 5s.i.5t.1.77.2.1.1.l 5v.i.cy.1.o 5u.j.dn 5t.j.do 5s.i.7j.1.66 5t.g.7k.4.63 5t.g.7k.1.1.2.63 5t.e.dt 5t.d.d2.1.r 5t.d.h.1.1.2.d9 5u.d.g.6.d7 5u.d.f.4.da 5w.c.ds 5w.1.1.4.1.5.7v.1.5w 5x.c.dr 5y.1.1.b.1l.4.c0 61.e.1k.1.at.1.16 63.7.dq 67.3.dq k0 k0 k0 k0 k0 k0 k0 k0 k0 7g.2.ci 6x.2.d1 k0 6o.1.1.1.d9 6l.3.dc 6s.2.1.1.d4 6m.1.1.7.d5 6g.4.1.9.d6 6f.8.dd 6g.5.df 6c.1.1.6.6f.1.2k.2.m.3.3p 6a.1.1.a.67.c.27.2.7.c.8.8.m.1.8.f.1d.1.z 67.1.1.1.1.6.1.1.69.f.1j.2.3.17.1.c.d.a.5.q.23 66.9.65.3.2.l.1d.3e.1z 66.1.2.4.64.1f.m.3n.1v 6a.5.5x.1k.h.3u.6.1.5.1.1g 61.3.5.8.5c.3.c.1o.g.4a.1e 5z.6.2.e.58.5.9.1o.e.4l.16 5s.3.3.8.1.d.4b.2.w.c.1.1p.7.4v.13 5t.2.5.k.39.2.7.1.5.1.8.3.5.9.3.2.4.1.8.2.5.25.1.2.6.4x.2.2.y 5r.6.4.m.2x.5.5.2.a.x.2.9.4.2d.4.50.1.c.o 5r.1.5.b.1.d.2p.2.2.3.2.c.1.1.2.3u.4.5h.l 4f.1.2.2.2.2.15.f.1.d.2q.4g.5.5l.i 4b.e.16.2.1.9.3.d.2n.1.2.4e.4.5l.k 49.3.d.2.1.1.3.3.1.4.o.3.4.5.4.f.2g.1.3.4j.2.5o.k 2x.3.16.1.2.11.7.5.6.3.1.1.3.o.2f.ac.l 2x.7.2.8.5.1.q.2d.27.1.8.a5.s 2o.1.7.1.4.4.1.5.6.2.3.3.3.4.h.2a.26.2.6.3.1.a2.t 2f.1a.1.6.j.25.2g.a2.2.1.v 27.27.1.23.2g.a3.z 1w.4k.25.ah.y 1p.4k.25.ao.y 1t.3x.4.a.26.as.y 17.a.7.7.1.3x.2b.az.6.2.p 17.4s.1d.5.k.b4.4.5.m 19.4g.3.4.d.2.x.d.f.b9.1.2.o w.5.a.44.2.8.e.7.x.e.e.b6.3.2.q v.a.c.41.1.9.8.6.1.3.x.f.e.2.2.au.12 z.7.7.1.b.43.9.4.3.5.8.4.g.h.l.ap.14 1n.46.i.1.6.7.b.l.o.an.14 1p.48.1.6.a.3.1.7.a.b.q.ax.13 w.5.a.51.1c.ba.12 16.1.8.55.12.bg.y 1g.55.d.i.3.bk.2.1.u c.1.2.2.f.3.e.57.9.ci.p f.g.1.4.8.5e.3.cp.m n.j4.9 0.4.p.j7 0.v.6.iz 0.k0 0.k0 0.k0 0.k0 0.k0 0.k0 0.k0 0.k0 0.k0 0.k0' diff --git a/og.png b/og.png new file mode 100644 index 0000000..f642286 Binary files /dev/null and b/og.png differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..4167994 --- /dev/null +++ b/style.css @@ -0,0 +1,121 @@ +:root { + --bg: #030815; --ink: #e4eeff; --mute: #7b8fb8; --dim: #3d5180; + --cyan: #7fd6ff; --blue: #3d8bff; --line: rgba(110, 160, 255, .13); + --card: linear-gradient(160deg, rgba(30, 60, 140, .22), rgba(10, 22, 55, .35)); + --mono: 'JetBrains Mono', ui-monospace, monospace; + --sans: 'Space Grotesk', system-ui, sans-serif; +} +* { box-sizing: border-box; margin: 0 } +html { background: var(--bg); color: var(--ink); font: 15px/1.5 var(--sans); -webkit-font-smoothing: antialiased } +body { + min-height: 100vh; + background: + radial-gradient(ellipse 80% 50% at 50% -10%, rgba(40, 100, 255, .18), transparent 70%), + radial-gradient(rgba(120, 170, 255, .07) 1px, transparent 1.5px) 0 0 / 22px 22px; +} +a { color: inherit; text-decoration: none } +button, input, select { font: inherit; color: inherit } +.wrap { width: min(1240px, 100% - 40px); margin-inline: auto } + +.top { display: flex; align-items: center; justify-content: space-between; padding: 22px 0 } +.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: .14em } +.orb { width: 14px; height: 14px; border-radius: 50%; background: radial-gradient(circle at 35% 35%, #c9efff, var(--blue) 55%, #0b2a7a); box-shadow: 0 0 14px var(--blue) } +nav { display: flex; gap: 22px; color: var(--mute); font-size: 14px } +nav a:hover { color: var(--cyan) } + +.hero { text-align: center; padding-top: 18px } +h1 { font-weight: 500; font-size: clamp(28px, 4.4vw, 48px); letter-spacing: -.02em; background: linear-gradient(90deg, #cfe9ff, #7fb6ff 60%, #5d8cff); -webkit-background-clip: text; background-clip: text; color: transparent } +.sub { color: var(--mute); margin-top: 6px } +.sub b { color: var(--ink); font-weight: 500; font-family: var(--mono); font-size: .92em } + +.map { + position: relative; margin: 34px 0 0; + --fy: linear-gradient(transparent, #000 6%, #000 88%, transparent); + --fx: linear-gradient(90deg, transparent, #000 3%, #000 97%, transparent); + -webkit-mask: var(--fy), var(--fx); -webkit-mask-composite: source-in; + mask: var(--fy), var(--fx); mask-composite: intersect; +} +#map { display: block; width: 100%; aspect-ratio: 360 / 142; cursor: crosshair; touch-action: pan-y } +#sun { position: absolute; width: 10px; height: 10px; margin: -5px 0 0 -5px; border-radius: 50%; background: #fff; box-shadow: 0 0 10px 3px #bfe8ff, 0 0 40px 12px rgba(80, 170, 255, .55); pointer-events: none } +#sun::after { content: ''; position: absolute; inset: -5px; border-radius: 50%; border: 1px solid rgba(180, 225, 255, .8); animation: ping 2.6s ease-out infinite } +@keyframes ping { to { inset: -26px; opacity: 0 } } +#tip { position: absolute; transform: translate(14px, -50%); padding: 7px 10px; border-radius: 8px; background: rgba(6, 16, 42, .88); border: 1px solid var(--line); backdrop-filter: blur(8px); font: 12px/1.45 var(--mono); color: var(--mute); white-space: nowrap; pointer-events: none } +#tip.l { transform: translate(calc(-100% - 14px), -50%) } +#tip b { display: block; color: var(--ink); font-weight: 500 } + +.now { margin-top: -6px; cursor: copy } +.big { font: 300 clamp(30px, 8.4vw, 104px)/1 var(--mono); letter-spacing: -.03em; font-variant-numeric: tabular-nums; color: #e3f3ff; text-shadow: 0 0 22px rgba(80, 160, 255, .55), 0 0 60px rgba(40, 110, 255, .35) } +.big i, .uv i, .mv i, dd i { font-style: normal; opacity: .38 } +.big i { opacity: 1; color: #2f63d6; text-shadow: none; margin: 0 -.12em } +.big-u { margin-top: 12px; font: 500 14px var(--mono); color: var(--cyan); letter-spacing: .2em; text-transform: uppercase } +.big-u span { color: var(--dim); letter-spacing: .1em; margin-left: 8px } + +.units { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 14px; margin-top: 54px } +.unit, .card { position: relative; border: 1px solid var(--line); border-radius: 16px; background: var(--card); backdrop-filter: blur(6px) } +.unit { display: flex; flex-direction: column; gap: 10px; padding: 18px 18px 16px; text-align: left; cursor: copy; transition: border-color .2s, transform .2s } +.unit:hover { border-color: rgba(127, 214, 255, .45); transform: translateY(-2px) } +.ul { display: flex; justify-content: space-between; color: var(--mute); font-size: 13px; letter-spacing: .06em; text-transform: uppercase } +.ul em { font: 500 12px var(--mono); color: var(--cyan); text-transform: none } +.uv { font: 400 clamp(18px, 1.9vw, 26px)/1.1 var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -.02em; white-space: nowrap } +.un { font: 12px var(--mono); color: var(--dim) } +.bar { display: block; height: 3px; border-radius: 3px; background: rgba(90, 140, 255, .14); overflow: hidden } +.bar i { display: block; height: 100%; transform-origin: left; transform: scaleX(0); background: linear-gradient(90deg, #2f6bff, var(--cyan)); box-shadow: 0 0 10px var(--cyan) } + +.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 14px 0 0 } +.card { padding: 18px 20px 20px } +.wide { grid-column: span 3 } +h2 { font: 500 12px var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--mute); margin-bottom: 10px } +.v { font: 400 18px/1.35 var(--mono); font-variant-numeric: tabular-nums } +.m { margin-top: 8px; font-size: 13px; color: var(--mute) } +.card .bar { margin-top: 12px } +.pips { display: flex; gap: 5px; margin-top: 12px } +.pips i { flex: 1; height: 3px; border-radius: 3px; background: rgba(90, 140, 255, .14) } +.pips .on { background: #2f6bff } +.pips .cur { background: var(--cyan); box-shadow: 0 0 10px var(--cyan) } + +#miles { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px 26px } +#miles li { display: flex; flex-direction: column; gap: 2px; padding: 10px 0 10px 14px; border-left: 2px solid rgba(110, 160, 255, .25) } +#miles li:first-child { border-color: var(--cyan) } +.mv { font: 15px var(--mono); font-variant-numeric: tabular-nums } +.mv em, dd em { font-style: normal; color: var(--cyan) } +.mv s { text-decoration: none; color: var(--dim) } +.mt { font-size: 13px; color: var(--mute) } +.mc { font: 12px var(--mono); color: var(--dim) } + +.cv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 30px } +.cv label { display: flex; justify-content: space-between; font-size: 13px; color: var(--mute); margin-bottom: 8px } +.tz { display: flex; align-items: center; gap: 6px; cursor: pointer } +.tz input { accent-color: var(--blue) } +.row { display: flex; gap: 8px } +.row input, .row select, .row button { height: 40px; padding: 0 12px; border-radius: 10px; border: 1px solid var(--line); background: rgba(4, 12, 32, .7); font: 14px var(--mono); outline: none; color-scheme: dark } +.row input { flex: 1; min-width: 0 } +.row input:focus, .row select:focus { border-color: rgba(127, 214, 255, .5) } +.row button { cursor: pointer; color: var(--cyan) } +.row button:hover { border-color: rgba(127, 214, 255, .5) } +dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin-top: 14px; font: 14px var(--mono); font-variant-numeric: tabular-nums } +dt { color: var(--cyan); text-align: right } +dd { overflow-wrap: anywhere } +.err { color: #ff9db0 } + +footer { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 34px 0 44px; color: var(--dim); font-size: 13px } +.npm { flex-shrink: 0; white-space: nowrap; padding: 8px 14px; border-radius: 10px; border: 1px solid var(--line); background: rgba(4, 12, 32, .6); font: 13px var(--mono); color: var(--cyan); cursor: copy } +#toast { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%); padding: 9px 16px; border-radius: 10px; background: rgba(10, 30, 80, .92); border: 1px solid rgba(127, 214, 255, .35); font: 13px var(--mono); box-shadow: 0 10px 40px rgba(0, 0, 0, .5) } + +@media (max-width: 960px) { + .units { grid-template-columns: repeat(6, minmax(0, 1fr)) } + .unit { grid-column: span 2 } + .unit:nth-child(n+4) { grid-column: span 3 } + .grid { grid-template-columns: repeat(2, minmax(0, 1fr)) } + .wide { grid-column: span 2 } + #miles { grid-template-columns: repeat(2, minmax(0, 1fr)) } +} +@media (max-width: 620px) { + .units { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 36px } + .unit, .unit:nth-child(n+4) { grid-column: auto } + .unit:last-child { grid-column: span 2 } + .grid, .cv, #miles { grid-template-columns: minmax(0, 1fr) } + .wide { grid-column: auto } + .map { margin: 22px -20px 0 } + footer { flex-direction: column; align-items: flex-start } +} +@media (prefers-reduced-motion: reduce) { #sun::after { animation: none } .unit { transition: none } }