aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/prototypes/2026-07-23-world-clock-horizontal.html190
-rw-r--r--docs/prototypes/2026-07-23-world-clock-vertical-centered.html191
-rw-r--r--docs/prototypes/2026-07-23-world-clock-vertical-even.html128
-rw-r--r--docs/prototypes/2026-07-23-world-clock-vertical-longitude.html177
4 files changed, 686 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-23-world-clock-horizontal.html b/docs/prototypes/2026-07-23-world-clock-horizontal.html
new file mode 100644
index 0000000..ef45657
--- /dev/null
+++ b/docs/prototypes/2026-07-23-world-clock-horizontal.html
@@ -0,0 +1,190 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Dupre World — horizontal, true longitude</title>
+<style>
+ :root{
+ --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
+ --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
+ --night:#39435a; --line:#2c2823;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin:0; height:100%; }
+ body {
+ background:var(--ground); font-family:var(--mono); color:var(--silver);
+ cursor:none; overflow:hidden; position:relative;
+ }
+ .masthead {
+ position:absolute; top:30px; left:50%; transform:translateX(-50%);
+ color:var(--steel); font-size:12px; letter-spacing:0.35em;
+ text-transform:uppercase; z-index:3;
+ }
+ .masthead b { color:var(--gold); font-weight:400; }
+ .mode { position:absolute; top:30px; right:34px; color:var(--dim);
+ font-size:11px; letter-spacing:0.2em; z-index:3; }
+ svg { position:absolute; inset:0; width:100%; height:100%; z-index:1; }
+ /* Each label anchors at the leader's end; above blocks sit up, below sit down. */
+ .lbl { position:absolute; z-index:2; text-align:center;
+ transform:translateX(-50%); white-space:nowrap; }
+ .lbl.up { transform:translate(-50%,-100%); }
+ .lbl .city { color:var(--steel); font-size:11px; letter-spacing:0.18em;
+ text-transform:uppercase; }
+ .lbl .time { color:var(--cream); font-size:24px; letter-spacing:0.04em;
+ line-height:1.1; }
+ .lbl .meta { color:var(--dim); font-size:11px; letter-spacing:0.1em; }
+ .lbl .meta .tz { color:var(--steel); }
+ .lbl.night .time { color:var(--slate); }
+ .lbl.night .city::after { content:" ☾"; color:var(--night); }
+ .lbl.home .time { color:var(--gold-hi); }
+ .lbl.home .city { color:var(--gold); }
+</style>
+</head>
+<body>
+ <div class="masthead"><b>DUPRE</b> · WORLD WATCH &nbsp;·&nbsp; WEST → EAST</div>
+ <div class="mode" id="mode">12H · click to toggle</div>
+ <svg id="wires"></svg>
+ <div id="labels"></div>
+<script>
+const ROSTER = [
+ { tz:"Pacific/Auckland", label:"Wellington", lon:174.78 },
+ { tz:"Australia/Sydney", label:"Sydney", lon:151.21 },
+ { tz:"Asia/Tokyo", label:"Tokyo", lon:139.69 },
+ { tz:"Asia/Shanghai", label:"Shanghai", lon:121.47 },
+ { tz:"Asia/Kolkata", label:"Delhi", lon:77.21 },
+ { tz:"Asia/Yerevan", label:"Yerevan", lon:44.51 },
+ { tz:"Europe/Istanbul", label:"Istanbul", lon:28.98 },
+ { tz:"Europe/Athens", label:"Athens", lon:23.73 },
+ { tz:"Europe/Paris", label:"Paris", lon:2.35 },
+ { tz:"Europe/London", label:"London", lon:-0.13 },
+ { tz:"America/New_York", label:"New York", lon:-74.01 },
+ { tz:"America/Chicago", label:"New Orleans", lon:-90.07 },
+ { tz:"America/Los_Angeles", label:"Berkeley", lon:-122.27},
+ { tz:"America/Anchorage", label:"Anchorage", lon:-149.90},
+ { tz:"Pacific/Honolulu", label:"Honolulu", lon:-157.86},
+];
+const TZNAME = {
+ "Pacific/Honolulu":"US Hawaii", "America/Anchorage":"US Alaska",
+ "America/Los_Angeles":"US Pacific", "America/Chicago":"US Central",
+ "America/New_York":"US Eastern", "Europe/London":"UK",
+ "Europe/Paris":"Central Europe", "Europe/Athens":"Eastern Europe",
+ "Europe/Istanbul":"Türkiye", "Asia/Yerevan":"Armenia",
+ "Asia/Kolkata":"India", "Asia/Shanghai":"China", "Asia/Tokyo":"Japan",
+ "Australia/Sydney":"Australia East", "Pacific/Auckland":"New Zealand",
+};
+function tzName(tz) {
+ if (TZNAME[tz]) return TZNAME[tz];
+ try {
+ const p = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date())
+ .find((x) => x.type === "timeZoneName");
+ return p ? p.value : tz.split("/").pop().replace(/_/g, " ");
+ } catch (e) { return tz.split("/").pop().replace(/_/g, " "); }
+}
+const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
+let hour12 = true;
+
+/* Spine across the centre; longitude maps to x (west left, east right). Labels
+ alternate above and below so near-longitude cities split across the line. */
+const SIDE = 150, MIN_LEAD = 7, ROW_H = 92, COL_W = 176;
+const lons = ROSTER.map((c) => c.lon);
+const maxLon = Math.max(...lons), minLon = Math.min(...lons);
+const svgNS = "http://www.w3.org/2000/svg";
+const wires = document.getElementById("wires");
+const labels = document.getElementById("labels");
+let nodes = [];
+
+function layout() {
+ const H = window.innerHeight, W = window.innerWidth, cy = Math.round(H / 2);
+ const drawW = W - SIDE * 2;
+ wires.innerHTML = ""; labels.innerHTML = ""; nodes = [];
+
+ const spine = document.createElementNS(svgNS, "line");
+ spine.setAttribute("y1", cy); spine.setAttribute("y2", cy);
+ spine.setAttribute("x1", SIDE - 24); spine.setAttribute("x2", W - SIDE + 24);
+ spine.setAttribute("stroke", "var(--line)"); spine.setAttribute("stroke-width", "2");
+ wires.appendChild(spine);
+
+ const lastX = {}; // slot "side:row" -> last x placed
+ // west->east order (left to right) for stable slotting
+ const ordered = [...ROSTER].sort((a, b) => a.lon - b.lon);
+ ordered.forEach((c, i) => {
+ const x = SIDE + (c.lon - minLon) / (maxLon - minLon) * drawW;
+ const order = [];
+ for (let row = 0; row < 4; row++) {
+ const first = (i % 2 === 0) ? "up" : "dn";
+ const second = first === "up" ? "dn" : "up";
+ order.push(first + ":" + row, second + ":" + row);
+ }
+ let slot = order.find((s) => lastX[s] === undefined || x - lastX[s] >= COL_W)
+ || order[order.length - 1];
+ lastX[slot] = x;
+ const [side, rowStr] = slot.split(":");
+ const row = parseInt(rowStr, 10);
+ const labelY = side === "up"
+ ? cy - MIN_LEAD - row * ROW_H
+ : cy + MIN_LEAD + row * ROW_H;
+
+ const lead = document.createElementNS(svgNS, "line");
+ lead.setAttribute("x1", x); lead.setAttribute("x2", x);
+ lead.setAttribute("y1", cy); lead.setAttribute("y2", labelY);
+ lead.setAttribute("stroke", "var(--line)"); lead.setAttribute("stroke-width", "1.4");
+ wires.appendChild(lead);
+ const dot = document.createElementNS(svgNS, "circle");
+ dot.setAttribute("cx", x); dot.setAttribute("cy", cy);
+ dot.setAttribute("r", "3.5");
+ dot.setAttribute("fill", c.tz === home ? "var(--gold)" : "var(--steel)");
+ wires.appendChild(dot);
+
+ const lbl = document.createElement("div");
+ lbl.className = "lbl " + side;
+ lbl.style.left = x + "px";
+ lbl.style.top = (side === "up" ? labelY - 6 : labelY + 6) + "px";
+ lbl.innerHTML = "<div class='city'>" + c.label + "</div>"
+ + "<div class='time'></div><div class='meta'></div>";
+ labels.appendChild(lbl);
+ nodes.push({ c, lbl,
+ time: lbl.querySelector(".time"), meta: lbl.querySelector(".meta") });
+ });
+}
+function fmt(tz) {
+ const now = new Date();
+ const p = new Intl.DateTimeFormat("en-US", {
+ timeZone: tz, hour12, weekday: "short",
+ day: "numeric", hour: hour12 ? "numeric" : "2-digit", minute: "2-digit",
+ }).formatToParts(now);
+ const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
+ const h24 = parseInt(new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
+ .find((x) => x.type === "hour").value, 10) % 24;
+ const time = hour12
+ ? g("hour") + ":" + g("minute") + " " + g("dayPeriod")
+ : g("hour") + ":" + g("minute");
+ return { time, date: g("weekday") + " " + g("day"), night: h24 >= 19 || h24 < 7 };
+}
+function tick() {
+ for (const e of nodes) {
+ const f = fmt(e.c.tz);
+ e.time.textContent = f.time;
+ e.meta.innerHTML = f.date + " &nbsp;·&nbsp; <span class='tz'>"
+ + tzName(e.c.tz) + "</span>";
+ const isHome = e.c.tz === home;
+ e.lbl.classList.toggle("home", isHome);
+ e.lbl.classList.toggle("night", f.night && !isHome);
+ }
+}
+document.body.addEventListener("click", () => {
+ hour12 = !hour12;
+ document.getElementById("mode").textContent =
+ (hour12 ? "12H" : "24H") + " · click to toggle";
+ tick();
+});
+layout(); tick();
+setInterval(tick, 5000);
+let rt; window.addEventListener("resize", () => {
+ clearTimeout(rt); rt = setTimeout(() => { layout(); tick(); }, 150);
+});
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-23-world-clock-vertical-centered.html b/docs/prototypes/2026-07-23-world-clock-vertical-centered.html
new file mode 100644
index 0000000..114c4b8
--- /dev/null
+++ b/docs/prototypes/2026-07-23-world-clock-vertical-centered.html
@@ -0,0 +1,191 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Dupre World — vertical, centered spine, both sides</title>
+<style>
+ :root{
+ --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
+ --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
+ --night:#39435a; --line:#2c2823;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin:0; height:100%; }
+ body {
+ background:var(--ground); font-family:var(--mono); color:var(--silver);
+ cursor:none; overflow:hidden; position:relative;
+ }
+ .masthead {
+ position:absolute; top:30px; left:50%; transform:translateX(-50%);
+ color:var(--steel); font-size:12px; letter-spacing:0.35em;
+ text-transform:uppercase; z-index:3; text-align:center;
+ }
+ .masthead b { color:var(--gold); font-weight:400; }
+ .mode { position:absolute; top:30px; right:34px; color:var(--dim);
+ font-size:11px; letter-spacing:0.2em; z-index:3; }
+ svg { position:absolute; inset:0; width:100%; height:100%; z-index:1; }
+ .lbl { position:absolute; z-index:2; transform:translateY(-50%);
+ white-space:nowrap; }
+ .lbl.r { text-align:left; }
+ .lbl.l { text-align:right; transform:translate(-100%,-50%); }
+ .lbl .city { color:var(--steel); font-size:12px; letter-spacing:0.2em;
+ text-transform:uppercase; }
+ .lbl .time { color:var(--cream); font-size:26px; letter-spacing:0.04em;
+ line-height:1.05; }
+ .lbl .meta { color:var(--dim); font-size:11px; letter-spacing:0.11em; }
+ .lbl .meta .tz { color:var(--steel); }
+ .lbl.night .time { color:var(--slate); }
+ .lbl.night .city::after { content:" ☾"; color:var(--night); }
+ .lbl.home .time { color:var(--gold-hi); }
+ .lbl.home .city { color:var(--gold); }
+</style>
+</head>
+<body>
+ <div class="masthead"><b>DUPRE</b> · WORLD WATCH &nbsp;·&nbsp; EAST → WEST</div>
+ <div class="mode" id="mode">12H · click to toggle</div>
+ <svg id="wires"></svg>
+ <div id="labels"></div>
+<script>
+const ROSTER = [
+ { tz:"Pacific/Auckland", label:"Wellington", lon:174.78 },
+ { tz:"Australia/Sydney", label:"Sydney", lon:151.21 },
+ { tz:"Asia/Tokyo", label:"Tokyo", lon:139.69 },
+ { tz:"Asia/Shanghai", label:"Shanghai", lon:121.47 },
+ { tz:"Asia/Kolkata", label:"Delhi", lon:77.21 },
+ { tz:"Asia/Yerevan", label:"Yerevan", lon:44.51 },
+ { tz:"Europe/Istanbul", label:"Istanbul", lon:28.98 },
+ { tz:"Europe/Athens", label:"Athens", lon:23.73 },
+ { tz:"Europe/Paris", label:"Paris", lon:2.35 },
+ { tz:"Europe/London", label:"London", lon:-0.13 },
+ { tz:"America/New_York", label:"New York", lon:-74.01 },
+ { tz:"America/Chicago", label:"New Orleans", lon:-90.07 },
+ { tz:"America/Los_Angeles", label:"Berkeley", lon:-122.27},
+ { tz:"America/Anchorage", label:"Anchorage", lon:-149.90},
+ { tz:"Pacific/Honolulu", label:"Honolulu", lon:-157.86},
+];
+const TZNAME = {
+ "Pacific/Honolulu":"US Hawaii", "America/Anchorage":"US Alaska",
+ "America/Los_Angeles":"US Pacific", "America/Chicago":"US Central",
+ "America/New_York":"US Eastern", "Europe/London":"UK",
+ "Europe/Paris":"Central Europe", "Europe/Athens":"Eastern Europe",
+ "Europe/Istanbul":"Türkiye", "Asia/Yerevan":"Armenia",
+ "Asia/Kolkata":"India", "Asia/Shanghai":"China", "Asia/Tokyo":"Japan",
+ "Australia/Sydney":"Australia East", "Pacific/Auckland":"New Zealand",
+};
+function tzName(tz) {
+ if (TZNAME[tz]) return TZNAME[tz];
+ try {
+ const p = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date())
+ .find((x) => x.type === "timeZoneName");
+ return p ? p.value : tz.split("/").pop().replace(/_/g, " ");
+ } catch (e) { return tz.split("/").pop().replace(/_/g, " "); }
+}
+const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
+let hour12 = true;
+
+/* Spine down the centre; labels alternate to left and right so near-longitude
+ cities split across the line instead of piling into one far column. */
+const TOP = 104, BOT = 104, MIN_LEAD = 7, COL_W = 224, ROW_H = 68;
+const lons = ROSTER.map((c) => c.lon);
+const maxLon = Math.max(...lons), minLon = Math.min(...lons);
+const svgNS = "http://www.w3.org/2000/svg";
+const wires = document.getElementById("wires");
+const labels = document.getElementById("labels");
+let nodes = [];
+
+function layout() {
+ const H = window.innerHeight, W = window.innerWidth, cx = Math.round(W / 2);
+ const drawH = H - TOP - BOT;
+ wires.innerHTML = ""; labels.innerHTML = ""; nodes = [];
+
+ const spine = document.createElementNS(svgNS, "line");
+ spine.setAttribute("x1", cx); spine.setAttribute("x2", cx);
+ spine.setAttribute("y1", TOP - 18); spine.setAttribute("y2", H - BOT + 18);
+ spine.setAttribute("stroke", "var(--line)"); spine.setAttribute("stroke-width", "2");
+ wires.appendChild(spine);
+
+ // slots keyed "side:col" -> last y placed there
+ const lastY = {};
+ ROSTER.forEach((c, i) => {
+ const y = TOP + (maxLon - c.lon) / (maxLon - minLon) * drawH;
+ // try sides in an order that alternates by index, then widen columns
+ const order = [];
+ for (let col = 0; col < 6; col++) {
+ const first = (i % 2 === 0) ? "r" : "l";
+ const second = first === "r" ? "l" : "r";
+ order.push(first + ":" + col, second + ":" + col);
+ }
+ let slot = order.find((s) => lastY[s] === undefined || y - lastY[s] >= ROW_H)
+ || order[order.length - 1];
+ lastY[slot] = y;
+ const [side, colStr] = slot.split(":");
+ const col = parseInt(colStr, 10);
+
+ const labelX = side === "r"
+ ? cx + MIN_LEAD + col * COL_W
+ : cx - MIN_LEAD - col * COL_W;
+
+ const lead = document.createElementNS(svgNS, "line");
+ lead.setAttribute("x1", cx); lead.setAttribute("x2", labelX);
+ lead.setAttribute("y1", y); lead.setAttribute("y2", y);
+ lead.setAttribute("stroke", "var(--line)"); lead.setAttribute("stroke-width", "1.4");
+ wires.appendChild(lead);
+ const dot = document.createElementNS(svgNS, "circle");
+ dot.setAttribute("cx", cx); dot.setAttribute("cy", y);
+ dot.setAttribute("r", "3.5");
+ dot.setAttribute("fill", c.tz === home ? "var(--gold)" : "var(--steel)");
+ wires.appendChild(dot);
+
+ const lbl = document.createElement("div");
+ lbl.className = "lbl " + side;
+ lbl.style.left = (side === "r" ? labelX + 8 : labelX - 8) + "px";
+ lbl.style.top = y + "px";
+ lbl.innerHTML = "<div class='city'>" + c.label + "</div>"
+ + "<div class='time'></div><div class='meta'></div>";
+ labels.appendChild(lbl);
+ nodes.push({ c, lbl,
+ time: lbl.querySelector(".time"), meta: lbl.querySelector(".meta") });
+ });
+}
+function fmt(tz) {
+ const now = new Date();
+ const p = new Intl.DateTimeFormat("en-US", {
+ timeZone: tz, hour12, weekday: "short",
+ day: "numeric", hour: hour12 ? "numeric" : "2-digit", minute: "2-digit",
+ }).formatToParts(now);
+ const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
+ const h24 = parseInt(new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
+ .find((x) => x.type === "hour").value, 10) % 24;
+ const time = hour12
+ ? g("hour") + ":" + g("minute") + " " + g("dayPeriod")
+ : g("hour") + ":" + g("minute");
+ return { time, date: g("weekday") + " " + g("day"), night: h24 >= 19 || h24 < 7 };
+}
+function tick() {
+ for (const e of nodes) {
+ const f = fmt(e.c.tz);
+ e.time.textContent = f.time;
+ e.meta.innerHTML = f.date + " &nbsp;·&nbsp; <span class='tz'>"
+ + tzName(e.c.tz) + "</span>";
+ const isHome = e.c.tz === home;
+ e.lbl.classList.toggle("home", isHome);
+ e.lbl.classList.toggle("night", f.night && !isHome);
+ }
+}
+document.body.addEventListener("click", () => {
+ hour12 = !hour12;
+ document.getElementById("mode").textContent =
+ (hour12 ? "12H" : "24H") + " · click to toggle";
+ tick();
+});
+layout(); tick();
+setInterval(tick, 5000);
+let rt; window.addEventListener("resize", () => {
+ clearTimeout(rt); rt = setTimeout(() => { layout(); tick(); }, 150);
+});
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-23-world-clock-vertical-even.html b/docs/prototypes/2026-07-23-world-clock-vertical-even.html
new file mode 100644
index 0000000..58d24a1
--- /dev/null
+++ b/docs/prototypes/2026-07-23-world-clock-vertical-even.html
@@ -0,0 +1,128 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Dupre World — vertical, even spacing</title>
+<style>
+ :root{
+ --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
+ --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
+ --night:#39435a;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
+ }
+ * { box-sizing: border-box; }
+ body {
+ margin:0; min-height:100vh; background:var(--ground);
+ display:flex; flex-direction:column; justify-content:center;
+ padding:48px 64px; gap:22px;
+ font-family:var(--mono); color:var(--silver);
+ cursor:none; overflow:hidden;
+ }
+ .masthead {
+ color:var(--steel); font-size:12px; letter-spacing:0.35em;
+ text-transform:uppercase; margin-bottom:8px;
+ }
+ .masthead b { color:var(--gold); font-weight:400; }
+ .row { display:flex; align-items:baseline; gap:16px; }
+ .city { color:var(--steel); font-size:13px; letter-spacing:0.22em;
+ text-transform:uppercase; min-width:150px; }
+ .time { color:var(--cream); font-size:30px; letter-spacing:0.05em;
+ min-width:150px; }
+ .meta { color:var(--dim); font-size:12px; letter-spacing:0.12em; }
+ .meta .tz { color:var(--steel); }
+ .row.night .time { color:var(--slate); }
+ .row.night .city::after { content:" ☾"; color:var(--night); }
+ .row.home .time { color:var(--gold-hi); }
+ .row.home .city { color:var(--gold); }
+</style>
+</head>
+<body>
+ <div class="masthead"><b>DUPRE</b> · WORLD WATCH</div>
+ <div id="stack"></div>
+<script>
+/* Roster mirrors ~/.config/waybar/worldclock.conf (tz|label|lat|lon). The
+ real face receives this as ?cities=<json>; embedded here for the prototype. */
+const ROSTER = [
+ { tz:"Pacific/Auckland", label:"Wellington", lon:174.78 },
+ { tz:"Australia/Sydney", label:"Sydney", lon:151.21 },
+ { tz:"Asia/Tokyo", label:"Tokyo", lon:139.69 },
+ { tz:"Asia/Shanghai", label:"Shanghai", lon:121.47 },
+ { tz:"Asia/Kolkata", label:"Delhi", lon:77.21 },
+ { tz:"Asia/Yerevan", label:"Yerevan", lon:44.51 },
+ { tz:"Europe/Istanbul", label:"Istanbul", lon:28.98 },
+ { tz:"Europe/Athens", label:"Athens", lon:23.73 },
+ { tz:"Europe/Paris", label:"Paris", lon:2.35 },
+ { tz:"Europe/London", label:"London", lon:-0.13 },
+ { tz:"America/New_York", label:"New York", lon:-74.01 },
+ { tz:"America/Chicago", label:"New Orleans", lon:-90.07 },
+ { tz:"America/Los_Angeles", label:"Berkeley", lon:-122.27},
+ { tz:"America/Anchorage", label:"Anchorage", lon:-149.90},
+ { tz:"Pacific/Honolulu", label:"Honolulu", lon:-157.86},
+];
+/* Region names in Craig's "US Central" style, not the tz's major city.
+ Intl longGeneric is the fallback for a zone not in the map. */
+const TZNAME = {
+ "Pacific/Honolulu":"US Hawaii", "America/Anchorage":"US Alaska",
+ "America/Los_Angeles":"US Pacific", "America/Chicago":"US Central",
+ "America/New_York":"US Eastern", "Europe/London":"UK",
+ "Europe/Paris":"Central Europe", "Europe/Athens":"Eastern Europe",
+ "Europe/Istanbul":"Türkiye", "Asia/Yerevan":"Armenia",
+ "Asia/Kolkata":"India", "Asia/Shanghai":"China", "Asia/Tokyo":"Japan",
+ "Australia/Sydney":"Australia East", "Pacific/Auckland":"New Zealand",
+};
+function tzName(tz) {
+ if (TZNAME[tz]) return TZNAME[tz];
+ try {
+ const parts = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date());
+ const p = parts.find((x) => x.type === "timeZoneName");
+ return p ? p.value : tz.split("/").pop().replace(/_/g, " ");
+ } catch (e) { return tz.split("/").pop().replace(/_/g, " "); }
+}
+const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
+const stack = document.getElementById("stack");
+stack.className = "";
+const rows = ROSTER.map((c) => {
+ const row = document.createElement("div");
+ row.className = "row";
+ row.style.marginBottom = "18px";
+ const city = document.createElement("div"); city.className = "city";
+ city.textContent = c.label;
+ const time = document.createElement("div"); time.className = "time";
+ const meta = document.createElement("div"); meta.className = "meta";
+ row.append(city, time, meta);
+ stack.appendChild(row);
+ return { c, row, time, meta };
+});
+function fmt(tz) {
+ const now = new Date();
+ const p = new Intl.DateTimeFormat("en-US", {
+ timeZone: tz, hour12: true, weekday: "short",
+ day: "numeric", hour: "numeric", minute: "2-digit",
+ }).formatToParts(now);
+ const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
+ const h24 = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
+ .find((x) => x.type === "hour").value;
+ return {
+ time: g("hour") + ":" + g("minute") + " " + g("dayPeriod"),
+ date: g("weekday") + " " + g("day"),
+ night: (parseInt(h24, 10) % 24) >= 19 || (parseInt(h24, 10) % 24) < 7,
+ };
+}
+function tick() {
+ for (const e of rows) {
+ const f = fmt(e.c.tz);
+ e.time.textContent = f.time;
+ e.meta.innerHTML = f.date + " &nbsp;·&nbsp; <span class='tz'>"
+ + tzName(e.c.tz) + "</span>";
+ const isHome = e.c.tz === home;
+ e.row.classList.toggle("home", isHome);
+ e.row.classList.toggle("night", f.night && !isHome);
+ }
+}
+tick();
+setInterval(tick, 5000);
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html b/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html
new file mode 100644
index 0000000..a897755
--- /dev/null
+++ b/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html
@@ -0,0 +1,177 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Dupre World — vertical, true longitude</title>
+<style>
+ :root{
+ --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
+ --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
+ --night:#39435a; --line:#2c2823;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin:0; height:100%; }
+ body {
+ background:var(--ground); font-family:var(--mono); color:var(--silver);
+ cursor:none; overflow:hidden; position:relative;
+ }
+ .masthead {
+ position:absolute; top:34px; left:64px;
+ color:var(--steel); font-size:12px; letter-spacing:0.35em;
+ text-transform:uppercase; z-index:3;
+ }
+ .masthead b { color:var(--gold); font-weight:400; }
+ svg { position:absolute; inset:0; width:100%; height:100%; z-index:1; }
+ .lbl {
+ position:absolute; z-index:2; transform:translateY(-50%);
+ white-space:nowrap;
+ }
+ .lbl .city { color:var(--steel); font-size:12px; letter-spacing:0.2em;
+ text-transform:uppercase; }
+ .lbl .time { color:var(--cream); font-size:26px; letter-spacing:0.04em;
+ line-height:1.05; }
+ .lbl .meta { color:var(--dim); font-size:11px; letter-spacing:0.11em; }
+ .lbl .meta .tz { color:var(--steel); }
+ .lbl.night .time { color:var(--slate); }
+ .lbl.night .city::after { content:" ☾"; color:var(--night); }
+ .lbl.home .time { color:var(--gold-hi); }
+ .lbl.home .city { color:var(--gold); }
+</style>
+</head>
+<body>
+ <div class="masthead"><b>DUPRE</b> · WORLD WATCH &nbsp;·&nbsp; EAST → WEST</div>
+ <svg id="wires"></svg>
+ <div id="labels"></div>
+<script>
+const ROSTER = [
+ { tz:"Pacific/Auckland", label:"Wellington", lon:174.78 },
+ { tz:"Australia/Sydney", label:"Sydney", lon:151.21 },
+ { tz:"Asia/Tokyo", label:"Tokyo", lon:139.69 },
+ { tz:"Asia/Shanghai", label:"Shanghai", lon:121.47 },
+ { tz:"Asia/Kolkata", label:"Delhi", lon:77.21 },
+ { tz:"Asia/Yerevan", label:"Yerevan", lon:44.51 },
+ { tz:"Europe/Istanbul", label:"Istanbul", lon:28.98 },
+ { tz:"Europe/Athens", label:"Athens", lon:23.73 },
+ { tz:"Europe/Paris", label:"Paris", lon:2.35 },
+ { tz:"Europe/London", label:"London", lon:-0.13 },
+ { tz:"America/New_York", label:"New York", lon:-74.01 },
+ { tz:"America/Chicago", label:"New Orleans", lon:-90.07 },
+ { tz:"America/Los_Angeles", label:"Berkeley", lon:-122.27},
+ { tz:"America/Anchorage", label:"Anchorage", lon:-149.90},
+ { tz:"Pacific/Honolulu", label:"Honolulu", lon:-157.86},
+];
+const TZNAME = {
+ "Pacific/Honolulu":"US Hawaii", "America/Anchorage":"US Alaska",
+ "America/Los_Angeles":"US Pacific", "America/Chicago":"US Central",
+ "America/New_York":"US Eastern", "Europe/London":"UK",
+ "Europe/Paris":"Central Europe", "Europe/Athens":"Eastern Europe",
+ "Europe/Istanbul":"Türkiye", "Asia/Yerevan":"Armenia",
+ "Asia/Kolkata":"India", "Asia/Shanghai":"China", "Asia/Tokyo":"Japan",
+ "Australia/Sydney":"Australia East", "Pacific/Auckland":"New Zealand",
+};
+function tzName(tz) {
+ if (TZNAME[tz]) return TZNAME[tz];
+ try {
+ const parts = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date());
+ const p = parts.find((x) => x.type === "timeZoneName");
+ return p ? p.value : tz.split("/").pop().replace(/_/g, " ");
+ } catch (e) { return tz.split("/").pop().replace(/_/g, " "); }
+}
+const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+/* Geometry: longitude maps to y (east/high-lon at top, west/low-lon at bottom).
+ Dots sit at the true longitude position on a vertical line. Labels are
+ staggered into columns so near-longitude cities don't overwrite each other;
+ a horizontal leader (>= MIN_LEAD px) runs from each dot to its label. */
+const LINE_X = 84, TOP = 104, BOT = 104, MIN_LEAD = 7;
+const COL_W = 216, ROW_H = 68; // label footprint (3 lines + breathing room)
+const lons = ROSTER.map((c) => c.lon);
+const maxLon = Math.max(...lons), minLon = Math.min(...lons);
+const wires = document.getElementById("wires");
+const labels = document.getElementById("labels");
+let nodes = [];
+
+function layout() {
+ const H = window.innerHeight, W = window.innerWidth;
+ const drawH = H - TOP - BOT;
+ wires.innerHTML = ""; labels.innerHTML = ""; nodes = [];
+ const svgNS = "http://www.w3.org/2000/svg";
+
+ // the spine
+ const spine = document.createElementNS(svgNS, "line");
+ spine.setAttribute("x1", LINE_X); spine.setAttribute("x2", LINE_X);
+ spine.setAttribute("y1", TOP - 18); spine.setAttribute("y2", H - BOT + 18);
+ spine.setAttribute("stroke", "var(--line)"); spine.setAttribute("stroke-width", "2");
+ wires.appendChild(spine);
+
+ // true y per city, sorted east->west (already the roster order)
+ const placed = []; // per column: last y used
+ ROSTER.forEach((c) => {
+ const y = TOP + (maxLon - c.lon) / (maxLon - minLon) * drawH;
+ // leftmost column whose last label is >= ROW_H away
+ let col = 0;
+ while (placed[col] !== undefined && y - placed[col] < ROW_H) col++;
+ placed[col] = y;
+ const x = LINE_X + MIN_LEAD + col * COL_W;
+
+ // leader (horizontal from dot to label)
+ const lead = document.createElementNS(svgNS, "line");
+ lead.setAttribute("x1", LINE_X); lead.setAttribute("x2", x);
+ lead.setAttribute("y1", y); lead.setAttribute("y2", y);
+ lead.setAttribute("stroke", "var(--line)"); lead.setAttribute("stroke-width", "1.4");
+ wires.appendChild(lead);
+ // dot on the spine
+ const dot = document.createElementNS(svgNS, "circle");
+ dot.setAttribute("cx", LINE_X); dot.setAttribute("cy", y);
+ dot.setAttribute("r", "3.5");
+ dot.setAttribute("fill", c.tz === home ? "var(--gold)" : "var(--steel)");
+ wires.appendChild(dot);
+
+ const lbl = document.createElement("div");
+ lbl.className = "lbl";
+ lbl.style.left = (x + 8) + "px"; lbl.style.top = y + "px";
+ lbl.innerHTML = "<div class='city'>" + c.label + "</div>"
+ + "<div class='time'></div>"
+ + "<div class='meta'></div>";
+ labels.appendChild(lbl);
+ nodes.push({ c, lbl,
+ time: lbl.querySelector(".time"), meta: lbl.querySelector(".meta") });
+ });
+}
+function fmt(tz) {
+ const now = new Date();
+ const p = new Intl.DateTimeFormat("en-US", {
+ timeZone: tz, hour12: true, weekday: "short",
+ day: "numeric", hour: "numeric", minute: "2-digit",
+ }).formatToParts(now);
+ const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
+ const h24 = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
+ .find((x) => x.type === "hour").value;
+ return {
+ time: g("hour") + ":" + g("minute") + " " + g("dayPeriod"),
+ date: g("weekday") + " " + g("day"),
+ night: (parseInt(h24, 10) % 24) >= 19 || (parseInt(h24, 10) % 24) < 7,
+ };
+}
+function tick() {
+ for (const e of nodes) {
+ const f = fmt(e.c.tz);
+ e.time.textContent = f.time;
+ e.meta.innerHTML = f.date + " &nbsp;·&nbsp; <span class='tz'>"
+ + tzName(e.c.tz) + "</span>";
+ const isHome = e.c.tz === home;
+ e.lbl.classList.toggle("home", isHome);
+ e.lbl.classList.toggle("night", f.night && !isHome);
+ }
+}
+layout(); tick();
+setInterval(tick, 5000);
+let rt; window.addEventListener("resize", () => {
+ clearTimeout(rt); rt = setTimeout(() => { layout(); tick(); }, 150);
+});
+</script>
+</body>
+</html>