0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Pick two to four cities and their working days stack onto one 24-hour strip, with the slice where everyone is genuinely at a desk marked in green and written out in each city's own clock. It opens on New York, London and Berlin with the answer already on screen — the only shared stretch is the tail of Berlin's afternoon, which is first thing in New York. Nothing is hardcoded: it asks your own browser for each zone's real offset on the date you pick, so daylight saving is already applied and half-hour zones like Bangalore (+5:30) and Kathmandu (+5:45) land where they should. Try the "hard one" preset — San Francisco, London, Bangalore, Sydney — and watch it refuse to invent a window that doesn't exist, naming the best 2-of-4 slot and who gets left out instead.
Source — the code you see is the code that just ran 1310 lines 55.9 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<title>Meeting Overlap Finder — sloprun.dev</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end tokens ───────────────────────────────────────────────────────────── */
/* demo-local additions: alpha values + on-color pairs the tokens don't carry */
:root {
--on-accent: #FFFFFF; /* on solid --accent #C05600 → 5.1 */
--on-run: #FFFFFF; /* on solid --run #1A7F37 → 5.0 */
--shadow: 0 1px 2px rgba(26,29,33,.06), 0 8px 24px rgba(26,29,33,.05);
--recess: #FBFBF9; /* track floor, just off the surface */
--grid: rgba(26,29,33,.10); /* 3-hour rules inside a track */
--hair: rgba(26,29,33,.06);
--work-fill: rgba(192,86,0,.16);
--work-edge: rgba(192,86,0,.48);
--run-fill: rgba(26,127,55,.16);
--run-edge: rgba(26,127,55,.55);
--near-fill: rgba(192,86,0,.10);
--near-edge: rgba(192,86,0,.50);
--night: rgba(26,29,33,.055); /* 22:00–06:00 wash */
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--on-accent: #14161A; /* on solid --accent #FFB454 → 10.9 */
--on-run: #071B0C; /* on solid --run #3FB950 → 9.8 */
--shadow: 0 1px 2px rgba(0,0,0,.40), 0 8px 26px rgba(0,0,0,.35);
--recess: #16181D;
--grid: rgba(232,230,225,.10);
--hair: rgba(232,230,225,.06);
--work-fill: rgba(255,180,84,.15);
--work-edge: rgba(255,180,84,.48);
--run-fill: rgba(63,185,80,.20);
--run-edge: rgba(63,185,80,.62);
--near-fill: rgba(255,180,84,.12);
--near-edge: rgba(255,180,84,.55);
--night: rgba(0,0,0,.15);
}
}
:root[data-theme="dark"] {
--on-accent: #14161A;
--on-run: #071B0C;
--shadow: 0 1px 2px rgba(0,0,0,.40), 0 8px 26px rgba(0,0,0,.35);
--recess: #16181D;
--grid: rgba(232,230,225,.10);
--hair: rgba(232,230,225,.06);
--work-fill: rgba(255,180,84,.15);
--work-edge: rgba(255,180,84,.48);
--run-fill: rgba(63,185,80,.20);
--run-edge: rgba(63,185,80,.62);
--near-fill: rgba(255,180,84,.12);
--near-edge: rgba(255,180,84,.55);
--night: rgba(0,0,0,.15);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
min-height: 100%;
line-height: 1.5;
overflow-x: hidden;
-webkit-text-size-adjust: 100%;
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 22px 16px 28px; }
/* ── header ──────────────────────────────────────────────────────────────── */
header { margin-bottom: 16px; }
h1 { margin: 0 0 4px; font-size: 1.35rem; font-weight: 620; letter-spacing: -.015em; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 66ch; }
/* ── layout ──────────────────────────────────────────────────────────────── */
.grid { display: grid; gap: 16px; grid-template-columns: 1fr; align-items: start; }
@media (min-width: 900px) {
.grid { grid-template-columns: minmax(0,1.4fr) minmax(288px,.6fr); gap: 18px; }
}
.side { display: grid; gap: 16px; }
.main { display: grid; gap: 16px; min-width: 0; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
min-width: 0;
}
.card > h2 {
margin: 0;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
font-family: var(--font-mono);
font-size: .66rem;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.card > h2 .spacer { flex: 1; }
.body { padding: 14px; }
/* ── verdict ─────────────────────────────────────────────────────────────── */
.verdict {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 13px 14px;
background: var(--bg);
}
.verdict.is-ok { background: var(--run-soft); border-color: var(--run-edge); }
.verdict.is-part { background: var(--accent-soft); border-color: var(--near-edge); }
.verdict.is-none { background: var(--bg); border-color: var(--line); }
.vhead { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.vbig {
font-family: var(--font-mono);
font-size: 1.6rem;
font-weight: 660;
letter-spacing: -.02em;
line-height: 1.15;
}
.is-ok .vbig { color: var(--run); }
.is-part .vbig { color: var(--accent); }
.is-none .vbig { color: var(--muted); }
.vtag {
font-family: var(--font-mono);
font-size: .64rem;
letter-spacing: .1em;
text-transform: uppercase;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid currentColor;
}
.is-ok .vtag { color: var(--run); }
.is-part .vtag { color: var(--accent); }
.is-none .vtag { color: var(--muted); }
.vsub { margin: 5px 0 0; font-size: .95rem; }
.vsub b { font-family: var(--font-mono); font-weight: 650; letter-spacing: -.01em; }
.vnote { margin: 6px 0 0; font-size: .82rem; color: var(--muted); }
.vlist { margin: 12px 0 0; border-top: 1px dashed var(--line); padding-top: 10px; display: grid; gap: 3px; }
.vrow {
display: grid;
grid-template-columns: minmax(0,1fr) auto;
gap: 10px;
align-items: baseline;
font-size: .86rem;
}
.vrow .vc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vrow .vt { font-family: var(--font-mono); font-size: .84rem; font-variant-numeric: tabular-nums; }
.vrow.out .vc, .vrow.out .vt { color: var(--muted); }
.vrow .ref {
font-family: var(--font-mono); font-size: .58rem; letter-spacing: .08em;
color: var(--accent); border: 1px solid var(--work-edge); border-radius: 4px;
padding: 0 4px; margin-left: 6px; vertical-align: 1px;
}
.day { color: var(--muted); font-size: .78em; }
/* ── timeline ────────────────────────────────────────────────────────────── */
.tl { --lab: 128px; --gap: 10px; --axisH: 20px; --railH: 24px; position: relative; }
.tl .lane { display: grid; grid-template-columns: var(--lab) minmax(0,1fr); gap: var(--gap); }
.axis { height: var(--axisH); }
.tl .axl {
font-family: var(--font-mono); font-size: .6rem; letter-spacing: .08em;
color: var(--muted); text-transform: uppercase; align-self: end; line-height: 1;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.axtrack { position: relative; }
.axtick {
position: absolute; bottom: 0; font-family: var(--font-mono); font-size: .62rem;
color: var(--muted); font-variant-numeric: tabular-nums; line-height: 1; white-space: nowrap;
}
.rail { height: var(--railH); }
.railtrack { position: relative; }
.handle {
position: absolute; top: 0;
min-width: 52px; height: 22px; padding: 0 7px;
display: inline-flex; align-items: center; justify-content: center;
font-family: var(--font-mono); font-size: .72rem; font-weight: 650;
font-variant-numeric: tabular-nums;
color: var(--on-accent); background: var(--accent);
border: 0; border-radius: 6px; cursor: ew-resize;
box-shadow: 0 1px 3px rgba(0,0,0,.22);
touch-action: none;
}
.handle::after {
content: ""; position: absolute; left: 50%; bottom: -5px; width: 0; height: 0;
transform: translateX(-50%);
border-left: 5px solid transparent; border-right: 5px solid transparent;
border-top: 5px solid var(--accent);
}
.rows { display: grid; gap: 4px; }
.row { align-items: center; padding: 3px 0; border-radius: 6px; }
.row.anchored { background: var(--hair); }
.lab { min-width: 0; position: relative; padding-right: 22px; }
.namebtn {
display: block; width: 100%; text-align: left;
background: none; border: 0; padding: 0; margin: 0;
font: inherit; font-size: .82rem; font-weight: 600; color: var(--ink);
cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
border-radius: 4px;
}
.namebtn:hover { color: var(--accent); }
.namebtn .pin { color: var(--accent); font-size: .8em; }
.labtime {
font-family: var(--font-mono); font-size: .88rem; font-weight: 600;
font-variant-numeric: tabular-nums; line-height: 1.25; letter-spacing: -.01em;
}
.laboff {
font-family: var(--font-mono); font-size: .6rem; color: var(--muted);
letter-spacing: .02em; line-height: 1.3;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.xbtn {
position: absolute; top: -1px; right: -2px;
width: 22px; height: 22px; display: grid; place-items: center;
background: none; border: 1px solid transparent; border-radius: 6px;
color: var(--muted); font-family: var(--font-mono); font-size: .8rem;
cursor: pointer; padding: 0; line-height: 1;
}
.xbtn:hover:not(:disabled) { color: var(--danger); border-color: var(--line); background: var(--bg); }
.xbtn:disabled { opacity: .3; cursor: not-allowed; }
.track {
position: relative; height: 30px; border-radius: 6px;
background: var(--recess);
border: 1px solid var(--line);
overflow: hidden;
}
.track .grid3 {
position: absolute; inset: 0;
background-image: repeating-linear-gradient(to right,
var(--grid) 0 1px, transparent 1px 12.5%);
background-position: 0 0;
}
.seg { position: absolute; top: 0; bottom: 0; background: var(--night); }
.work {
position: absolute; top: 0; bottom: 0; overflow: hidden;
background: var(--work-fill);
border-left: 1px solid var(--work-edge);
border-right: 1px solid var(--work-edge);
display: grid; place-items: center;
}
.work.nol { border-left-color: transparent; }
.work.nor { border-right-color: transparent; }
.worktxt {
font-family: var(--font-mono); font-size: .58rem; font-weight: 650;
color: var(--accent); letter-spacing: .02em; white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.hrs { position: absolute; inset: 0; pointer-events: none; }
.hr {
position: absolute; top: 50%; transform: translate(-50%,-50%);
font-family: var(--font-mono); font-size: .58rem; color: var(--muted);
font-variant-numeric: tabular-nums; line-height: 1;
}
/* overlay columns, aligned to the track column */
.ovl { position: absolute; left: calc(var(--lab) + var(--gap)); right: 0; pointer-events: none; }
.ovl-band { top: calc(var(--axisH) + var(--railH)); bottom: 0; }
.ovl-cur { top: var(--axisH); bottom: 0; }
.band {
position: absolute; top: 0; bottom: 0; border-radius: 4px;
background: var(--run-fill);
border-left: 2px solid var(--run-edge);
border-right: 2px solid var(--run-edge);
}
.band.partial { background: var(--near-fill); border-left-style: dashed; border-right-style: dashed; border-color: var(--near-edge); }
/* a window that runs past local midnight is drawn as two pieces; the seam
between them gets no border and no rounding so it reads as one band */
.band.nol { border-left-color: transparent; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.band.nor { border-right-color: transparent; border-top-right-radius: 0; border-bottom-right-radius: 0; }
.band.hidden { display: none; }
.cline { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px dashed var(--accent); opacity: .85; }
.scrub { touch-action: pan-y; cursor: crosshair; }
.legend {
margin-top: 12px; padding-top: 10px; border-top: 1px dashed var(--line);
display: flex; flex-wrap: wrap; gap: 6px 14px;
font-family: var(--font-mono); font-size: .64rem; color: var(--muted);
align-items: center;
}
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.sw { width: 16px; height: 10px; border-radius: 2px; display: inline-block; }
.sw.w { background: var(--work-fill); border: 1px solid var(--work-edge); }
.sw.o { background: var(--run-fill); border: 1px solid var(--run-edge); }
.sw.c { width: 0; height: 12px; border-left: 1px dashed var(--accent); border-radius: 0; }
/* ── controls ────────────────────────────────────────────────────────────── */
.field { display: grid; gap: 5px; }
.field + .field { margin-top: 12px; }
.flabel {
font-family: var(--font-mono); font-size: .62rem; letter-spacing: .1em;
text-transform: uppercase; color: var(--muted); font-weight: 600;
}
input[type="date"], input[type="text"], select {
font: inherit; font-size: .88rem; color: var(--ink);
background: var(--surface); border: 1px solid var(--line);
border-radius: 8px; padding: 7px 9px; width: 100%; min-width: 0;
}
input[type="date"], select { font-family: var(--font-mono); font-size: .84rem; }
select { cursor: pointer; }
.duo { display: grid; grid-template-columns: 1fr auto 1fr; gap: 6px; align-items: center; }
.duo .to { font-family: var(--font-mono); font-size: .72rem; color: var(--muted); }
.btnrow { display: flex; flex-wrap: wrap; gap: 6px; }
.btn {
font: inherit; font-size: .8rem; font-weight: 550;
color: var(--ink); background: var(--surface);
border: 1px solid var(--line); border-radius: 999px;
padding: 5px 11px; cursor: pointer; line-height: 1.35;
}
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.mono { font-family: var(--font-mono); font-size: .74rem; }
.picker {
margin-top: 8px; max-height: 268px; overflow-y: auto;
border: 1px solid var(--line); border-radius: 8px; background: var(--bg);
padding: 4px;
}
.cbtn {
display: grid; grid-template-columns: 16px minmax(0,1fr) auto;
gap: 8px; align-items: center; width: 100%;
font: inherit; font-size: .84rem; text-align: left;
background: none; border: 1px solid transparent; border-radius: 6px;
padding: 5px 7px; color: var(--ink); cursor: pointer;
}
.cbtn:hover:not(:disabled) { background: var(--surface); border-color: var(--line); }
.cbtn:disabled { opacity: .42; cursor: not-allowed; }
.cbtn.sel { background: var(--accent-soft); border-color: var(--work-edge); }
.cbtn .tick { font-family: var(--font-mono); font-size: .8rem; color: var(--accent); text-align: center; }
.cbtn .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cbtn .nm small { color: var(--muted); font-size: .78em; }
.cbtn .off {
font-family: var(--font-mono); font-size: .68rem; color: var(--muted);
font-variant-numeric: tabular-nums; white-space: nowrap;
}
.you {
font-family: var(--font-mono); font-size: .55rem; letter-spacing: .08em;
border: 1px solid var(--work-edge); color: var(--accent);
border-radius: 4px; padding: 0 3px; margin-left: 5px; vertical-align: 1px;
}
.empty { padding: 14px 8px; text-align: center; color: var(--muted); font-size: .82rem; }
.status {
margin-top: 10px; font-family: var(--font-mono); font-size: .66rem;
color: var(--muted); line-height: 1.5;
}
.status b { color: var(--ink); font-weight: 600; }
.status.ok b { color: var(--run); }
.status.warn b { color: var(--accent); }
.caveat { margin: 10px 0 0; font-size: .78rem; color: var(--muted); line-height: 1.5; }
kbd {
font-family: var(--font-mono); font-size: .72em; border: 1px solid var(--line);
border-bottom-width: 2px; border-radius: 4px; padding: 0 4px;
background: var(--bg); color: var(--ink);
}
footer {
margin-top: 18px; padding-top: 12px; border-top: 1px solid var(--line);
display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
font-family: var(--font-mono); font-size: .7rem; color: var(--muted);
}
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.mono { font-family: var(--font-mono); }
.vcap {
font-family: var(--font-mono); font-size: .58rem; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted); margin-bottom: 5px;
}
[hidden] { display: none !important; }
.sr {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
@media (max-width: 560px) {
.tl { --lab: 92px; --gap: 8px; }
.hrs { display: none; }
.axtick.minor { display: none; }
}
@media (min-width: 561px) {
.worktxt { display: none; }
}
@media (max-width: 400px) {
.wrap { padding: 16px 12px 22px; }
.body { padding: 12px; }
h1 { font-size: 1.2rem; }
.vbig { font-size: 1.35rem; }
}
@media (prefers-reduced-motion: no-preference) {
.band, .work { transition: left .16s ease, width .16s ease, background-color .16s ease; }
.btn, .cbtn, .namebtn { transition: background-color .12s ease, border-color .12s ease, color .12s ease; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Meeting Overlap Finder</h1>
<p class="lede">Pick two to four cities. Their working days stack onto one 24-hour strip, and the slice where
everyone is actually at a desk gets marked — with the answer written out in each city's own clock.</p>
</header>
<div class="grid">
<div class="main">
<section class="card" aria-labelledby="vh">
<h2 id="vh">Verdict<span class="spacer"></span><span id="dateTag" class="mono"></span></h2>
<div class="body">
<div class="verdict" id="verdict" role="status" aria-live="polite">
<div class="vhead">
<span class="vbig" id="vbig">—</span>
<span class="vtag" id="vtag">overlap</span>
</div>
<p class="vsub" id="vsub"></p>
<p class="vnote" id="vnote"></p>
<div class="vlist" id="vlist"></div>
</div>
<div class="btnrow" style="margin-top:12px">
<button type="button" class="btn" id="copyBtn">Copy summary</button>
<button type="button" class="btn mono" id="jumpBtn">Cursor → best window</button>
</div>
<div class="status" id="copyStatus" aria-live="polite"></div>
</div>
</section>
<section class="card" aria-labelledby="th">
<h2 id="th">The day, side by side<span class="spacer"></span><span id="anchorTag"></span></h2>
<div class="body">
<div class="tl" id="tl">
<div class="lane axis">
<div class="axl" id="axLabel">local hour</div>
<div class="axtrack scrub" id="axTrack"></div>
</div>
<div class="lane rail" id="railLane">
<div class="axl">cursor</div>
<div class="railtrack scrub" id="railTrack">
<button type="button" class="handle" id="handle" role="slider"
aria-valuemin="0" aria-valuemax="1439" aria-valuenow="720"
aria-label="Time cursor">12:00</button>
</div>
</div>
<div class="rows" id="rows"></div>
<div class="ovl ovl-band"><div class="band hidden" id="band"></div><div class="band hidden" id="band2"></div></div>
<div class="ovl ovl-cur"><div class="cline" id="cline"></div></div>
</div>
<div class="legend">
<span><i class="sw w"></i> working hours <b id="legWork" style="font-weight:600">09:00–17:00</b></span>
<span><i class="sw o"></i> everyone free</span>
<span><i class="sw c"></i> cursor</span>
<span>drag the strip · <kbd>←</kbd><kbd>→</kbd> to nudge</span>
</div>
<p class="caveat">Click a city's name to read the whole strip in that city's clock.</p>
</div>
</section>
</div>
<div class="side">
<section class="card" aria-labelledby="sh">
<h2 id="sh">Setup</h2>
<div class="body">
<div class="field">
<label class="flabel" for="dateIn">Date</label>
<input type="date" id="dateIn" min="1970-01-01" max="2100-12-31">
</div>
<div class="field">
<span class="flabel" id="whLabel">Working hours (local, everywhere)</span>
<div class="duo" role="group" aria-labelledby="whLabel">
<select id="wsIn" aria-label="Working day starts"></select>
<span class="to">to</span>
<select id="weIn" aria-label="Working day ends"></select>
</div>
</div>
<div class="field">
<span class="flabel" id="presetLabel">Presets</span>
<div class="btnrow" role="group" aria-labelledby="presetLabel" id="presets"></div>
</div>
<div class="status" id="tzStatus"></div>
</div>
</section>
<section class="card" aria-labelledby="ch">
<h2 id="ch">Cities<span class="spacer"></span><span id="countTag">3 / 4</span></h2>
<div class="body">
<label class="sr" for="searchIn">Filter cities</label>
<input type="text" id="searchIn" placeholder="Filter — try tokyo, india, +5" autocomplete="off" spellcheck="false">
<div class="picker" id="picker"></div>
<div class="status" id="pickStatus"></div>
</div>
</section>
</div>
</div>
<footer>
<span>demo · sloprun.dev</span>
<span id="engineTag">overlap finder · 24-hour clock · no storage</span>
</footer>
</div>
<script>
(function () {
"use strict";
/* ---------- platform theme bridge ---------- */
addEventListener("message", function (e) {
var d = e && e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
document.documentElement.setAttribute("data-theme", d.theme);
}
});
var $ = function (id) { return document.getElementById(id); };
/* ---------- city table ----------------------------------------------------
base = standard-time offset in minutes from UTC, used only when the browser
cannot answer time-zone questions itself. When it can, the real offset for
the chosen date is read from the browser's own data instead. */
var CITIES = [
{ id: "honolulu", name: "Honolulu", region: "United States", zone: "Pacific/Honolulu", base: -600 },
{ id: "anchorage", name: "Anchorage", region: "United States", zone: "America/Anchorage", base: -540 },
{ id: "san-francisco",name: "San Francisco", region: "United States", zone: "America/Los_Angeles", base: -480 },
{ id: "denver", name: "Denver", region: "United States", zone: "America/Denver", base: -420 },
{ id: "mexico-city", name: "Mexico City", region: "Mexico", zone: "America/Mexico_City", base: -360 },
{ id: "chicago", name: "Chicago", region: "United States", zone: "America/Chicago", base: -360 },
{ id: "new-york", name: "New York", region: "United States", zone: "America/New_York", base: -300 },
{ id: "toronto", name: "Toronto", region: "Canada", zone: "America/Toronto", base: -300 },
{ id: "sao-paulo", name: "São Paulo",region: "Brazil", zone: "America/Sao_Paulo", base: -180 },
{ id: "buenos-aires", name: "Buenos Aires", region: "Argentina", zone: "America/Argentina/Buenos_Aires", base: -180 },
{ id: "reykjavik", name: "Reykjavík",region: "Iceland", zone: "Atlantic/Reykjavik", base: 0 },
{ id: "london", name: "London", region: "United Kingdom", zone: "Europe/London", base: 0 },
{ id: "dublin", name: "Dublin", region: "Ireland", zone: "Europe/Dublin", base: 0 },
{ id: "lisbon", name: "Lisbon", region: "Portugal", zone: "Europe/Lisbon", base: 0 },
{ id: "paris", name: "Paris", region: "France", zone: "Europe/Paris", base: 60 },
{ id: "berlin", name: "Berlin", region: "Germany", zone: "Europe/Berlin", base: 60 },
{ id: "madrid", name: "Madrid", region: "Spain", zone: "Europe/Madrid", base: 60 },
{ id: "lagos", name: "Lagos", region: "Nigeria", zone: "Africa/Lagos", base: 60 },
{ id: "cairo", name: "Cairo", region: "Egypt", zone: "Africa/Cairo", base: 120 },
{ id: "johannesburg", name: "Johannesburg", region: "South Africa", zone: "Africa/Johannesburg", base: 120 },
{ id: "athens", name: "Athens", region: "Greece", zone: "Europe/Athens", base: 120 },
{ id: "istanbul", name: "Istanbul", region: "Türkiye", zone: "Europe/Istanbul", base: 180 },
{ id: "moscow", name: "Moscow", region: "Russia", zone: "Europe/Moscow", base: 180 },
{ id: "nairobi", name: "Nairobi", region: "Kenya", zone: "Africa/Nairobi", base: 180 },
{ id: "dubai", name: "Dubai", region: "UAE", zone: "Asia/Dubai", base: 240 },
{ id: "karachi", name: "Karachi", region: "Pakistan", zone: "Asia/Karachi", base: 300 },
{ id: "bangalore", name: "Bangalore", region: "India", zone: "Asia/Kolkata", base: 330 },
{ id: "kathmandu", name: "Kathmandu", region: "Nepal", zone: "Asia/Kathmandu", base: 345 },
{ id: "dhaka", name: "Dhaka", region: "Bangladesh", zone: "Asia/Dhaka", base: 360 },
{ id: "bangkok", name: "Bangkok", region: "Thailand", zone: "Asia/Bangkok", base: 420 },
{ id: "jakarta", name: "Jakarta", region: "Indonesia", zone: "Asia/Jakarta", base: 420 },
{ id: "singapore", name: "Singapore", region: "Singapore", zone: "Asia/Singapore", base: 480 },
{ id: "hong-kong", name: "Hong Kong", region: "Hong Kong", zone: "Asia/Hong_Kong", base: 480 },
{ id: "shanghai", name: "Shanghai", region: "China", zone: "Asia/Shanghai", base: 480 },
{ id: "seoul", name: "Seoul", region: "South Korea", zone: "Asia/Seoul", base: 540 },
{ id: "tokyo", name: "Tokyo", region: "Japan", zone: "Asia/Tokyo", base: 540 },
{ id: "adelaide", name: "Adelaide", region: "Australia", zone: "Australia/Adelaide", base: 570 },
{ id: "sydney", name: "Sydney", region: "Australia", zone: "Australia/Sydney", base: 600 },
{ id: "auckland", name: "Auckland", region: "New Zealand", zone: "Pacific/Auckland", base: 720 }
];
var PRESETS = [
{ label: "US ↔ Europe", sel: ["new-york", "london", "berlin"], anchor: "berlin" },
{ label: "The hard one", sel: ["san-francisco", "london", "bangalore", "sydney"], anchor: "london" },
{ label: "Asia–Pacific",sel: ["singapore", "tokyo", "sydney"], anchor: "singapore" },
{ label: "Follow the sun", sel: ["san-francisco", "berlin", "tokyo"], anchor: "berlin" }
];
/* ---------- small helpers ---------- */
function pad2(n) { return (n < 10 ? "0" : "") + n; }
function mod(n, m) { return ((n % m) + m) % m; }
function hhmm(mins) { var v = mod(mins, 1440); return pad2(Math.floor(v / 60)) + ":" + pad2(v % 60); }
function endLabel(m) { return m === 1440 ? "24:00" : hhmm(m); }
function offLabel(m) {
if (m === 0) return "UTC±00:00";
var s = m < 0 ? "−" : "+", a = Math.abs(m);
return "UTC" + s + pad2(Math.floor(a / 60)) + ":" + pad2(a % 60);
}
function durLabel(m) {
var h = Math.floor(m / 60), mm = m % 60;
if (h && mm) return h + "h " + pad2(mm) + "m";
if (h) return h + "h";
return mm + "m";
}
/* "09–17" — the compact form that fits inside a narrow working block */
function shortHours() {
return pad2(Math.floor(st.ws / 60)) + "–" + pad2(Math.floor(st.we / 60));
}
function windowLabel(a, b, len) {
return len >= 1440 ? "all day" : hhmm(a) + "–" + hhmm(b);
}
function dayTag(shift) { return shift === 0 ? "" : (shift > 0 ? "+" + shift + "d" : shift + "d"); }
function popcount(x) { var c = 0; while (x) { x &= x - 1; c++; } return c; }
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function listNames(arr) {
if (arr.length === 0) return "";
if (arr.length === 1) return arr[0];
return arr.slice(0, -1).join(", ") + " and " + arr[arr.length - 1];
}
/* ---------- time-zone plumbing ---------- */
var fmtCache = {};
function zoneOffset(zone, ms) {
try {
var f = fmtCache[zone];
if (!f) {
f = new Intl.DateTimeFormat("en-US", {
timeZone: zone, hour12: false,
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", second: "2-digit"
});
fmtCache[zone] = f;
}
var parts = f.formatToParts(new Date(ms)), m = {}, i;
for (i = 0; i < parts.length; i++) m[parts[i].type] = parts[i].value;
var h = +m.hour; if (h === 24) h = 0;
var asUTC = Date.UTC(+m.year, +m.month - 1, +m.day, h, +m.minute, +m.second);
if (isNaN(asUTC)) return null;
return Math.round((asUTC - ms) / 60000);
} catch (err) { return null; }
}
var TZ_OK = false;
try { TZ_OK = zoneOffset("Asia/Kolkata", Date.UTC(2024, 0, 15, 12, 0, 0)) === 330; } catch (err) { TZ_OK = false; }
/* your own clock, if the browser will name it */
var youId = null;
(function () {
var tz = null;
try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || null; } catch (err) { tz = null; }
if (!tz) return;
for (var i = 0; i < CITIES.length; i++) {
if (CITIES[i].zone === tz) { youId = CITIES[i].id; return; }
}
var leaf = tz.split("/").pop().replace(/_/g, " ");
var here = { id: "__you", name: leaf, region: "your clock", zone: tz, you: true,
base: -new Date().getTimezoneOffset() };
CITIES.unshift(here);
youId = "__you";
})();
var byId = {};
CITIES.forEach(function (c) { byId[c.id] = c; });
/* ---------- state ---------- */
function todayStr() {
var d = new Date();
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
}
var st = {
date: todayStr(),
sel: PRESETS[0].sel.slice(),
anchor: PRESETS[0].anchor,
ws: 9 * 60,
we: 17 * 60,
cursor: 720
};
function sampleMs() {
var p = /^(\d{4})-(\d{2})-(\d{2})$/.exec(st.date);
if (!p) return Date.UTC(2026, 0, 1, 12, 0, 0);
return Date.UTC(+p[1], +p[2] - 1, +p[3], 12, 0, 0);
}
function offsetOf(id) {
var c = byId[id];
if (!c) return 0;
if (TZ_OK) {
var o = zoneOffset(c.zone, sampleMs());
if (o !== null) return o;
}
return c.base;
}
function dateLabel(shift) {
var ms = sampleMs() + (shift || 0) * 86400000;
try {
return new Intl.DateTimeFormat(undefined, {
weekday: "short", day: "numeric", month: "short", timeZone: "UTC"
}).format(new Date(ms));
} catch (err) {
var d = new Date(ms);
return ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getUTCDay()] + " " +
d.getUTCDate() + " " +
["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][d.getUTCMonth()];
}
}
/* ---------- the actual maths -------------------------------------------
Axis position p = minutes since local midnight in the anchor city.
A city whose offset differs from the anchor by dc minutes reads
(p + dc) on its own clock. Working iff that lands inside [ws, we). */
function arcSegs(dc, from, to) {
var len = mod(to - from, 1440); if (len === 0) len = 1440;
var s = mod(from - dc, 1440), out = [];
if (s + len <= 1440) out.push({ a: s, b: s + len, head: true, tail: true });
else { out.push({ a: s, b: 1440, head: true, tail: false });
out.push({ a: 0, b: s + len - 1440, head: false, tail: true }); }
return out;
}
function circularRuns(mask) {
var n = 1440, i, same = true;
for (i = 1; i < n; i++) { if (mask[i] !== mask[0]) { same = false; break; } }
if (same) return [{ start: 0, len: n, mask: mask[0] }];
var b = 0;
for (i = 0; i < n; i++) { if (mask[i] !== mask[mod(i - 1, n)]) { b = i; break; } }
var runs = [], cs = b, cm = mask[b], k, idx;
for (k = 1; k <= n; k++) {
idx = (b + k) % n;
if (k === n || mask[idx] !== cm) {
runs.push({ start: cs, len: k - mod(cs - b, n), mask: cm });
cs = idx; cm = mask[idx];
}
}
return runs;
}
function solve() {
var ids = st.sel.slice();
var offs = {}, i, p;
ids.forEach(function (id) { offs[id] = offsetOf(id); });
ids.sort(function (a, b) {
if (offs[a] !== offs[b]) return offs[a] - offs[b];
return byId[a].name.localeCompare(byId[b].name);
});
if (ids.indexOf(st.anchor) < 0) st.anchor = ids[0];
var A = offs[st.anchor];
var dc = {};
ids.forEach(function (id) { dc[id] = offs[id] - A; });
var n = ids.length;
var mask = new Int32Array(1440);
for (i = 0; i < n; i++) {
var d = dc[ids[i]], bit = 1 << i;
var segs = arcSegs(d, st.ws, st.we);
for (var s = 0; s < segs.length; s++) {
for (p = segs[s].a; p < segs[s].b; p++) mask[p] |= bit;
}
}
var maxCount = 0;
for (p = 0; p < 1440; p++) { var pc = popcount(mask[p]); if (pc > maxCount) maxCount = pc; }
var band = null;
if (maxCount >= 2) {
var runs = circularRuns(mask), best = null;
var abit = 1 << ids.indexOf(st.anchor);
for (i = 0; i < runs.length; i++) {
var rr = runs[i];
if (popcount(rr.mask) !== maxCount) continue;
if (!best) { best = rr; continue; }
/* equally good windows are common; prefer the one the reference city
is actually inside, then the longer one */
var ra = (rr.mask & abit) ? 1 : 0, ba = (best.mask & abit) ? 1 : 0;
if (ra !== ba) { if (ra > ba) best = rr; continue; }
if (rr.len > best.len) best = rr;
}
if (best) {
var members = [];
for (i = 0; i < n; i++) if (best.mask & (1 << i)) members.push(ids[i]);
band = { start: best.start, len: best.len, count: maxCount, members: members,
full: maxCount === n };
}
}
return { ids: ids, offs: offs, dc: dc, anchorOff: A, band: band, maxCount: maxCount };
}
/* ---------- element handles ---------- */
var tl = $("tl"), axTrack = $("axTrack"), rowsEl = $("rows"), bandEl = $("band"),
bandEl2 = $("band2"),
clineEl = $("cline"), handle = $("handle"), axLabel = $("axLabel"),
anchorTag = $("anchorTag"), dateTag = $("dateTag"),
verdict = $("verdict"), vbig = $("vbig"), vtag = $("vtag"), vsub = $("vsub"),
vnote = $("vnote"), vlist = $("vlist"),
dateIn = $("dateIn"), wsIn = $("wsIn"), weIn = $("weIn"), presetsEl = $("presets"),
searchIn = $("searchIn"), picker = $("picker"), pickStatus = $("pickStatus"),
tzStatus = $("tzStatus"), countTag = $("countTag"), legWork = $("legWork"),
copyBtn = $("copyBtn"), copyStatus = $("copyStatus"), jumpBtn = $("jumpBtn");
var current = null; /* last solve() result */
var pickRefs = {}; /* id -> { btn, tick, off } */
var labTimes = []; /* live clock readouts, one per visible row */
/* ---------- build the fixed controls ---------- */
(function buildControls() {
dateIn.value = st.date;
var h, o;
for (h = 0; h <= 22; h++) {
o = document.createElement("option");
o.value = String(h * 60); o.textContent = pad2(h) + ":00";
wsIn.appendChild(o);
}
for (h = 1; h <= 24; h++) {
o = document.createElement("option");
o.value = String(h * 60); o.textContent = pad2(h % 24) + ":00";
if (h === 24) o.textContent = "24:00";
weIn.appendChild(o);
}
wsIn.value = String(st.ws); weIn.value = String(st.we);
PRESETS.forEach(function (p, i) {
var b = document.createElement("button");
b.type = "button"; b.className = "btn"; b.textContent = p.label;
b.dataset.preset = String(i);
presetsEl.appendChild(b);
});
CITIES.forEach(function (c) {
var b = document.createElement("button");
b.type = "button"; b.className = "cbtn"; b.dataset.city = c.id;
var tick = document.createElement("span"); tick.className = "tick"; tick.setAttribute("aria-hidden", "true");
var nm = document.createElement("span"); nm.className = "nm";
nm.innerHTML = esc(c.name) + " <small>" + esc(c.region) + "</small>" +
(c.id === youId ? ' <span class="you">YOU</span>' : "");
var off = document.createElement("span"); off.className = "off";
b.appendChild(tick); b.appendChild(nm); b.appendChild(off);
picker.appendChild(b);
pickRefs[c.id] = { btn: b, tick: tick, off: off };
});
tzStatus.className = "status" + (TZ_OK ? "" : " warn");
tzStatus.innerHTML = TZ_OK
? "offsets: <b>your browser's time-zone data</b><br>daylight saving is already applied for the date above."
: "offsets: <b>fixed standard values</b><br>this browser can't do time-zone lookups, so daylight saving is not applied.";
})();
/* ---------- rendering ---------- */
function placeBand(el, a, b, partial, nol, nor) {
el.className = "band" + (partial ? " partial" : "") + (nol ? " nol" : "") + (nor ? " nor" : "");
el.style.left = (a / 14.4) + "%";
el.style.width = ((b - a) / 14.4) + "%";
}
function render() {
var r = solve();
current = r;
var ids = r.ids, dc = r.dc, band = r.band;
/* header bits */
var anchorCity = byId[st.anchor];
axLabel.textContent = anchorCity.name.length > 12 ? "local hour" : anchorCity.name.toLowerCase();
anchorTag.textContent = "ref · " + anchorCity.name;
dateTag.textContent = dateLabel(0);
legWork.textContent = hhmm(st.ws) + "–" + endLabel(st.we);
countTag.textContent = ids.length + " / 4";
/* axis ticks */
var ax = "", h, f;
for (h = 0; h <= 24; h += 3) {
f = h / 24 * 100;
var tx = h === 0 ? "translateX(0)" : (h === 24 ? "translateX(-100%)" : "translateX(-50%)");
ax += '<span class="axtick' + (h % 6 ? " minor" : "") +
'" style="left:' + f + '%;transform:' + tx + '">' + (h === 24 ? "24" : pad2(h)) + "</span>";
}
axTrack.innerHTML = ax;
/* rows */
var html = "", i;
for (i = 0; i < ids.length; i++) {
var id = ids[i], c = byId[id], d = dc[id], isA = id === st.anchor;
var loc = st.cursor + d, shift = Math.floor(loc / 1440), tag = dayTag(shift);
var track = '<div class="grid3"></div>';
arcSegs(d, 22 * 60, 6 * 60).forEach(function (s) {
track += '<div class="seg" style="left:' + (s.a / 14.4) + '%;width:' + ((s.b - s.a) / 14.4) + '%"></div>';
});
var wsegs = arcSegs(d, st.ws, st.we), widest = 0;
wsegs.forEach(function (s, k) { if (s.b - s.a > wsegs[widest].b - wsegs[widest].a) widest = k; });
wsegs.forEach(function (s, k) {
var w = (s.b - s.a) / 1440;
var cls = "work" + (s.head ? "" : " nol") + (s.tail ? "" : " nor");
track += '<div class="' + cls + '" style="left:' + (s.a / 14.4) + '%;width:' + (w * 100) + '%">' +
(k === widest && w >= 0.24
? '<span class="worktxt">' + shortHours() + "</span>" : "") +
"</div>";
});
var hrs = "";
for (h = 0; h < 24; h += 3) {
var p = mod(h * 60 - d, 1440), fr = p / 1440;
if (fr < 0.022 || fr > 0.978) continue;
hrs += '<span class="hr" style="left:' + (fr * 100) + '%">' + pad2(h) + "</span>";
}
track += '<div class="hrs">' + hrs + "</div>";
html +=
'<div class="lane row' + (isA ? " anchored" : "") + '">' +
'<div class="lab">' +
'<button type="button" class="namebtn" data-anchor="' + id + '" aria-pressed="' + isA + '" ' +
'title="Read the strip in ' + esc(c.name) + '’s clock">' + esc(c.name) +
(isA ? ' <span class="pin" aria-hidden="true">◉</span>' : "") + "</button>" +
'<div class="labtime">' + hhmm(loc) + (tag ? ' <span class="day">' + tag + "</span>" : "") + "</div>" +
'<div class="laboff">' + offLabel(r.offs[id]) + "</div>" +
'<button type="button" class="xbtn" data-remove="' + id + '" aria-label="Remove ' + esc(c.name) + '"' +
(ids.length <= 2 ? " disabled" : "") + ">✕</button>" +
"</div>" +
'<div class="track scrub">' + track + "</div>" +
"</div>";
}
rowsEl.innerHTML = html;
labTimes = [];
var timeEls = rowsEl.querySelectorAll(".labtime");
for (i = 0; i < timeEls.length && i < ids.length; i++) {
labTimes.push({ el: timeEls[i], d: dc[ids[i]] });
}
/* overlay band — a window that crosses the anchor's midnight is drawn as
two pieces, one running to the right edge and one resuming at 00:00 */
if (band) {
var bEnd = band.start + band.len;
if (bEnd <= 1440) {
placeBand(bandEl, band.start, bEnd, !band.full, false, false);
bandEl2.className = "band hidden";
} else {
placeBand(bandEl, band.start, 1440, !band.full, false, true);
placeBand(bandEl2, 0, bEnd - 1440, !band.full, true, false);
}
} else {
bandEl.className = "band hidden";
bandEl2.className = "band hidden";
}
jumpBtn.disabled = !band;
anchorName = anchorCity.name;
updateCursorUI();
renderVerdict(r);
renderPicker(r);
}
/* the cursor is the only thing that moves during a drag, so it gets its own
cheap pass instead of rebuilding the strip on every pointer sample */
var anchorName = "";
function updateCursorUI() {
var cf = st.cursor / 1440 * 100;
clineEl.style.left = cf + "%";
handle.style.left = cf + "%";
handle.style.transform = cf < 4 ? "translateX(0)" : (cf > 96 ? "translateX(-100%)" : "translateX(-50%)");
handle.textContent = hhmm(st.cursor);
handle.setAttribute("aria-valuenow", String(st.cursor));
handle.setAttribute("aria-valuetext", hhmm(st.cursor) + " in " + anchorName);
for (var i = 0; i < labTimes.length; i++) {
var loc = st.cursor + labTimes[i].d, tag = dayTag(Math.floor(loc / 1440));
labTimes[i].el.innerHTML = hhmm(loc) + (tag ? ' <span class="day">' + tag + "</span>" : "");
}
}
function renderVerdict(r) {
var band = r.band, ids = r.ids, dc = r.dc, anchorName = byId[st.anchor].name;
if (band && band.full) {
verdict.className = "verdict is-ok";
vbig.textContent = durLabel(band.len) + " overlap";
vtag.textContent = "all " + ids.length;
vsub.innerHTML = "<b>" + windowLabel(band.start, band.start + band.len, band.len) +
"</b> in " + esc(anchorName) + ", " + esc(dateLabel(0));
vnote.textContent = "Every city on the strip is inside its own " +
hhmm(st.ws) + "–" + endLabel(st.we) + " for that whole stretch.";
} else if (band) {
verdict.className = "verdict is-part";
vbig.textContent = "no full overlap";
vtag.textContent = "best " + band.count + " of " + ids.length;
vsub.innerHTML = "<b>" + durLabel(band.len) + "</b> for " + band.count + " of " + ids.length +
" · " + windowLabel(band.start, band.start + band.len, band.len) + " in " + esc(anchorName);
var out = ids.filter(function (id) { return band.members.indexOf(id) < 0; })
.map(function (id) { return byId[id].name; });
vnote.textContent = listNames(out) + (out.length === 1 ? " is" : " are") +
" off the clock then. Widen the working hours, or split into two calls.";
} else {
verdict.className = "verdict is-none";
vbig.textContent = "no overlap";
vtag.textContent = "none";
vsub.textContent = "No two of these cities share a single working minute on this day.";
vnote.textContent = "The strip below shows why — the shaded blocks never touch.";
}
var cap = band
? (band.full ? "the shared window, in each city’s clock"
: "that window, in each city’s clock")
: "each working day, in " + anchorName + "’s clock";
var rows = '<div class="vcap">' + esc(cap) + "</div>";
ids.forEach(function (id) {
var c = byId[id], d = dc[id], txt, out = false;
if (band) {
var a = band.start + d, b = band.start + band.len + d;
out = band.members.indexOf(id) < 0;
txt = windowLabel(a, b, band.len);
var sh = band.len >= 1440 ? "" : dayTag(Math.floor(a / 1440));
if (sh) txt += ' <span class="day">' + sh + "</span>";
if (out) txt += ' <span class="day">off</span>';
} else {
var s0 = mod(st.ws - d, 1440), s1 = s0 + (st.we - st.ws);
txt = hhmm(s0) + "–" + endLabel(s1);
if (s1 > 1440) txt += ' <span class="day">+1d</span>';
}
rows += '<div class="vrow' + (out ? " out" : "") + '">' +
'<span class="vc">' + esc(c.name) +
(id === st.anchor ? '<span class="ref">REF</span>' : "") + "</span>" +
'<span class="vt">' + txt + "</span>" +
"</div>";
});
vlist.innerHTML = rows;
}
function renderPicker(r) {
var q = searchIn.value.trim().toLowerCase();
var full = st.sel.length >= 4, shown = 0;
CITIES.forEach(function (c) {
var ref = pickRefs[c.id], on = st.sel.indexOf(c.id) >= 0;
var off = offsetOf(c.id);
ref.off.textContent = offLabel(off).replace("UTC", "");
ref.tick.textContent = on ? "✓" : "";
ref.btn.className = "cbtn" + (on ? " sel" : "");
ref.btn.setAttribute("aria-pressed", String(on));
ref.btn.disabled = (!on && full) || (on && st.sel.length <= 2);
/* offsets are searchable both as printed (UTC+05:30) and as typed
(+5, +5:30, -8) — nobody types the zero pad or the minus sign U+2212 */
var sg = off < 0 ? "-" : "+", oh = Math.floor(Math.abs(off) / 60), om = Math.abs(off) % 60;
var hay = (c.name + " " + c.region + " " + c.zone + " " + offLabel(off) +
" " + sg + oh + " " + sg + oh + ":" + pad2(om)).toLowerCase();
var hit = !q || hay.indexOf(q) >= 0;
ref.btn.hidden = !hit;
if (hit) shown++;
});
var empty = picker.querySelector(".empty");
if (shown === 0 && !empty) {
var e = document.createElement("div");
e.className = "empty"; e.textContent = "No city matches “" + searchIn.value.trim() + "”";
picker.appendChild(e);
} else if (shown > 0 && empty) {
empty.parentNode.removeChild(empty);
} else if (empty) {
empty.textContent = "No city matches “" + searchIn.value.trim() + "”";
}
pickStatus.textContent = full
? "4 of 4 chosen · drop one to swap it out"
: st.sel.length + " chosen · room for " + (4 - st.sel.length) + " more";
var sig = st.sel.slice().sort().join(",");
Array.prototype.forEach.call(presetsEl.children, function (b, i) {
var on = PRESETS[i].sel.slice().sort().join(",") === sig;
b.className = "btn" + (on ? " on" : "");
b.setAttribute("aria-pressed", String(on));
});
}
/* ---------- cursor ---------- */
function centreCursor() {
var b = current && current.band;
st.cursor = b ? mod(b.start + Math.floor(b.len / 2), 1440) : 12 * 60;
}
function setCursor(p) {
st.cursor = Math.max(0, Math.min(1439, Math.round(p)));
updateCursorUI();
}
function xToP(clientX) {
var rect = axTrack.getBoundingClientRect();
if (!rect.width) return st.cursor;
var f = (clientX - rect.left) / rect.width;
f = Math.max(0, Math.min(0.999999, f));
return Math.round(f * 1440 / 5) * 5;
}
var dragging = false;
tl.addEventListener("pointerdown", function (e) {
var onHandle = e.target === handle;
if (!onHandle) {
if (e.target.closest("button")) return;
if (!e.target.closest(".scrub")) return;
}
dragging = true;
e.preventDefault();
try { tl.setPointerCapture(e.pointerId); } catch (err) { /* capture optional */ }
handle.focus();
if (!onHandle) setCursor(xToP(e.clientX));
});
tl.addEventListener("pointermove", function (e) {
if (!dragging) return;
e.preventDefault();
setCursor(xToP(e.clientX));
});
function endDrag(e) {
if (!dragging) return;
dragging = false;
try { tl.releasePointerCapture(e.pointerId); } catch (err) { /* fine */ }
}
tl.addEventListener("pointerup", endDrag);
tl.addEventListener("pointercancel", endDrag);
handle.addEventListener("keydown", function (e) {
var step = e.shiftKey ? 60 : 15, p = st.cursor, used = true;
switch (e.key) {
case "ArrowLeft": case "ArrowDown": p -= step; break;
case "ArrowRight": case "ArrowUp": p += step; break;
case "PageDown": p -= 180; break;
case "PageUp": p += 180; break;
case "Home": p = 0; break;
case "End": p = 1439; break;
default: used = false;
}
if (!used) return;
e.preventDefault();
setCursor(Math.max(0, Math.min(1439, p)));
});
/* ---------- controls ---------- */
dateIn.addEventListener("change", function () {
/* an empty or out-of-range date (typed years like 0002 are possible in
some browsers) would push Intl into pre-standard-time offsets */
var p = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateIn.value);
var yr = p ? +p[1] : 0;
if (!p || yr < 1970 || yr > 2100) { dateIn.value = st.date; return; }
st.date = dateIn.value;
current = solve(); centreCursor(); render();
});
function hoursChanged(which) {
var ws = +wsIn.value, we = +weIn.value;
if (we <= ws) {
if (which === "start") { we = Math.min(1440, ws + 60); weIn.value = String(we); }
else { ws = Math.max(0, we - 60); wsIn.value = String(ws); }
}
st.ws = ws; st.we = we;
current = solve(); centreCursor(); render();
}
wsIn.addEventListener("change", function () { hoursChanged("start"); });
weIn.addEventListener("change", function () { hoursChanged("end"); });
presetsEl.addEventListener("click", function (e) {
var b = e.target.closest("button[data-preset]");
if (!b) return;
var p = PRESETS[+b.dataset.preset];
st.sel = p.sel.slice(); st.anchor = p.anchor;
current = solve(); centreCursor(); render();
});
picker.addEventListener("click", function (e) {
var b = e.target.closest("button[data-city]");
if (!b || b.disabled) return;
var id = b.dataset.city, i = st.sel.indexOf(id);
if (i >= 0) {
if (st.sel.length <= 2) return;
st.sel.splice(i, 1);
if (st.anchor === id) st.anchor = st.sel[0];
} else {
if (st.sel.length >= 4) return;
st.sel.push(id);
}
current = solve(); centreCursor(); render();
});
searchIn.addEventListener("input", function () { renderPicker(current); });
/* the rows are rebuilt wholesale, so the button that was clicked no longer
exists afterwards — put the keyboard back on its replacement */
function refocusRow(sel, fallbackIdx) {
var el = sel ? rowsEl.querySelector(sel) : null;
if (!el || el.disabled) {
var all = rowsEl.querySelectorAll("button[data-anchor]");
el = all[Math.min(fallbackIdx || 0, Math.max(0, all.length - 1))];
}
if (el) el.focus();
}
rowsEl.addEventListener("click", function (e) {
var rem = e.target.closest("button[data-remove]");
if (rem) {
if (st.sel.length <= 2) return;
var id = rem.dataset.remove, i = st.sel.indexOf(id);
var rowIdx = Array.prototype.indexOf.call(
rowsEl.querySelectorAll("button[data-remove]"), rem);
if (i >= 0) st.sel.splice(i, 1);
if (st.anchor === id) st.anchor = st.sel[0];
current = solve(); centreCursor(); render();
refocusRow(null, rowIdx);
return;
}
var an = e.target.closest("button[data-anchor]");
if (an) {
var nid = an.dataset.anchor;
if (nid === st.anchor) return;
/* keep the cursor pinned to the same instant while the frame moves */
st.cursor = mod(st.cursor + (offsetOf(nid) - offsetOf(st.anchor)), 1440);
st.anchor = nid;
render();
refocusRow('button[data-anchor="' + nid + '"]', 0);
}
});
jumpBtn.addEventListener("click", function () { centreCursor(); render(); });
/* ---------- copy ---------- */
function summaryText() {
var r = current, band = r.band, lines = [];
lines.push("Meeting overlap · " + dateLabel(0));
if (band && band.full) {
lines.push("Everyone free for " + durLabel(band.len) + ":");
} else if (band) {
lines.push("No full overlap. Best is " + band.count + " of " + r.ids.length +
" for " + durLabel(band.len) + ":");
} else {
lines.push("No shared working time at all.");
}
var width = 0;
r.ids.forEach(function (id) { width = Math.max(width, byId[id].name.length); });
r.ids.forEach(function (id) {
var c = byId[id], d = r.dc[id], name = c.name, pad = "";
while (name.length + pad.length < width) pad += " ";
var txt;
if (band) {
var a = band.start + d, b = band.start + band.len + d;
txt = band.len >= 1440 ? "all day" : hhmm(a) + "-" + hhmm(b);
var sh = band.len >= 1440 ? "" : dayTag(Math.floor(a / 1440));
if (sh) txt += " " + sh;
if (band.members.indexOf(id) < 0) txt += " (off hours)";
} else {
txt = hhmm(st.ws) + "-" + endLabel(st.we) + " local";
}
lines.push(" " + name + pad + " " + txt + " " + offLabel(r.offs[id]).replace("−", "-"));
});
lines.push("");
lines.push("working hours " + hhmm(st.ws) + "-" + endLabel(st.we) + " local · sloprun.dev");
return lines.join("\n");
}
var copyTimer = null;
function flash(msg, good) {
copyStatus.className = "status" + (good ? " ok" : " warn");
copyStatus.innerHTML = "<b>" + esc(msg) + "</b>";
if (copyTimer) clearTimeout(copyTimer);
copyTimer = setTimeout(function () { copyStatus.innerHTML = ""; copyStatus.className = "status"; }, 3200);
}
copyBtn.addEventListener("click", function () {
var text = summaryText();
function fallback() {
try {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed"; ta.style.top = "-1000px"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
var ok = document.execCommand("copy");
document.body.removeChild(ta);
if (ok) flash("copied to clipboard", true);
else flash("clipboard blocked here", false);
} catch (err) { flash("clipboard blocked here", false); }
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
flash("copied to clipboard", true);
}, fallback);
} else fallback();
});
/* ---------- go ---------- */
current = solve();
centreCursor();
render();
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.