Last active 1 day ago

Revision be10023904e71240c7067c66b1a756228d6008f0

sister-day.html Raw
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<title>Sister Day Calculator</title>
7<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,500;0,700;1,300;1,500&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
8<style>
9 :root {
10 --spring: #2d8a56;
11 --fall: #b85c2a;
12 --cold: #3a7ab5;
13 --sol: #7c5cbf;
14 --hot: #c9960a;
15 --bg: #faf9f7;
16 --surface: #ffffff;
17 --surface2: #f2f0ed;
18 --text: #1a1a1a;
19 --muted: #6b6b6b;
20 --border: rgba(0,0,0,0.08);
21 }
22 * { margin: 0; padding: 0; box-sizing: border-box; }
23 body {
24 background: var(--bg); color: var(--text);
25 font-family: 'DM Sans', sans-serif; font-weight: 400;
26 min-height: 100vh; overflow-x: hidden;
27 -webkit-font-smoothing: antialiased;
28 }
29 .container {
30 max-width: 760px; margin: 0 auto; padding: 3.5rem 1.5rem;
31 }
32 h1 {
33 font-family: 'Cormorant Garamond', serif;
34 font-weight: 500; font-size: clamp(3rem, 8vw, 4.8rem);
35 line-height: 1.05; letter-spacing: -0.02em; margin-bottom: 0.25em;
36 background: linear-gradient(135deg, var(--spring), var(--fall));
37 -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
38 }
39 .subtitle {
40 font-family: 'Cormorant Garamond', serif;
41 font-weight: 300; font-style: italic;
42 font-size: 1.35rem; color: var(--muted);
43 margin-bottom: 2.5rem; line-height: 1.5;
44 }
45 .definition {
46 border-left: 3px solid var(--cold);
47 padding: 1.1rem 1.4rem; margin-bottom: 2.5rem;
48 background: rgba(58,122,181,0.04); border-radius: 0 10px 10px 0;
49 font-size: 1.05rem; line-height: 1.75; color: var(--muted);
50 }
51 .definition strong { color: var(--text); font-weight: 500; }
52 .controls {
53 display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 2rem;
54 }
55 label { display: flex; flex-direction: column; gap: 0.45rem; }
56 label span {
57 font-size: 0.85rem; font-weight: 500; text-transform: uppercase;
58 letter-spacing: 0.07em; color: var(--muted);
59 }
60 input, select {
61 background: var(--surface); border: 1.5px solid var(--border);
62 color: var(--text); border-radius: 10px; padding: 0.75rem 1rem;
63 font-family: 'DM Sans', sans-serif; font-size: 1.05rem; font-weight: 400;
64 transition: border-color 0.2s; appearance: none; -webkit-appearance: none;
65 box-shadow: 0 1px 3px rgba(0,0,0,0.04);
66 }
67 select {
68 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236b6b6b' fill='none' stroke-width='1.5'/%3E%3C/svg%3E");
69 background-repeat: no-repeat; background-position: right 1rem center;
70 padding-right: 2.5rem;
71 }
72 input:focus, select:focus { outline: none; border-color: rgba(0,0,0,0.2); }
73 .coldest-note {
74 font-size: 0.85rem; color: var(--cold); margin-top: 0.15rem; min-height: 1.2em; font-weight: 500;
75 }
76 .result-card {
77 background: var(--surface); border-radius: 18px; padding: 2.25rem;
78 border: 1.5px solid var(--border); margin-bottom: 1.5rem;
79 box-shadow: 0 2px 12px rgba(0,0,0,0.04);
80 animation: fadeUp 0.5s ease both;
81 }
82 @keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
83 .pair {
84 display: flex; align-items: center; justify-content: center; gap: 1.5rem;
85 margin-bottom: 1.5rem; flex-wrap: wrap;
86 }
87 .day-badge {
88 display: flex; flex-direction: column; align-items: center;
89 padding: 1.1rem 1.75rem; border-radius: 14px; min-width: 150px;
90 }
91 .day-badge.spring { background: rgba(45,138,86,0.08); border: 1.5px solid rgba(45,138,86,0.2); }
92 .day-badge.fall { background: rgba(184,92,42,0.08); border: 1.5px solid rgba(184,92,42,0.2); }
93 .day-badge .season-label {
94 font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
95 letter-spacing: 0.1em; margin-bottom: 0.3rem;
96 }
97 .day-badge.spring .season-label { color: var(--spring); }
98 .day-badge.fall .season-label { color: var(--fall); }
99 .day-badge .date {
100 font-family: 'Cormorant Garamond', serif; font-size: 1.85rem; font-weight: 600;
101 }
102 .mirror-symbol { font-size: 1.5rem; color: var(--muted); opacity: 0.4; }
103 .meta { text-align: center; font-size: 1rem; color: var(--muted); line-height: 1.6; }
104 .meta em { font-style: normal; color: var(--cold); font-weight: 500; }
105 .orbital { position: relative; width: 100%; max-width: 420px; margin: 1.75rem auto 0; aspect-ratio: 1; }
106 .orbital svg { width: 100%; height: 100%; }
107
108 .legend {
109 display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem 1.5rem;
110 margin-top: 1.25rem; font-size: 0.82rem; color: var(--muted); font-weight: 400;
111 }
112 .legend-item { display: flex; align-items: center; gap: 0.4rem; }
113 .legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
114
115 .lag-card {
116 background: var(--surface); border-radius: 18px; padding: 1.75rem 2.25rem;
117 border: 1.5px solid var(--border); margin-bottom: 2rem;
118 box-shadow: 0 2px 12px rgba(0,0,0,0.04);
119 animation: fadeUp 0.5s ease both; animation-delay: 0.1s;
120 }
121 .lag-card h3 {
122 font-family: 'Cormorant Garamond', serif;
123 font-weight: 600; font-size: 1.35rem; margin-bottom: 0.65rem;
124 color: var(--sol);
125 }
126 .lag-card p {
127 font-size: 1rem; line-height: 1.7; color: var(--muted);
128 }
129 .lag-card p strong { color: var(--text); font-weight: 500; }
130 .lag-card .lag-stats {
131 display: grid; grid-template-columns: 1fr 1fr; gap: 0.85rem; margin-top: 1.15rem;
132 }
133 .lag-stat {
134 background: var(--surface2); border-radius: 12px; padding: 0.85rem 1.1rem; text-align: center;
135 }
136 .lag-stat .val {
137 font-family: 'Cormorant Garamond', serif; font-size: 1.7rem; font-weight: 600;
138 }
139 .lag-stat .lbl {
140 font-size: 0.78rem; font-weight: 500; text-transform: uppercase;
141 letter-spacing: 0.05em; color: var(--muted); margin-top: 0.2rem;
142 }
143 .lag-stat.sol .val { color: var(--sol); }
144 .lag-stat.cold .val { color: var(--cold); }
145
146 footer {
147 text-align: center; font-size: 0.85rem; color: var(--muted); padding: 2rem 0 1rem;
148 border-top: 1.5px solid var(--border); margin-top: 1.5rem;
149 }
150 @media (max-width: 480px) {
151 .controls { grid-template-columns: 1fr; }
152 .pair { gap: 0.75rem; }
153 .day-badge { min-width: 125px; padding: 0.85rem 1.25rem; }
154 .lag-card .lag-stats { grid-template-columns: 1fr; }
155 .container { padding: 2rem 1.25rem; }
156 }
157</style>
158</head>
159<body>
160<div class="container">
161 <h1>Sister Day</h1>
162 <p class="subtitle">The mirror date across winter's coldest point</p>
163
164 <div class="definition">
165 <strong>Sister Day</strong> — For a given location, let <em>C</em> denote the calendar date with the lowest long-term average daily temperature. The <em>sister day</em> of a date <em>D</em> is the unique date <em>D′</em> equidistant from <em>C</em> on the opposite side of the annual temperature cycle — if <em>D</em> falls <em>n</em> days after <em>C</em>, then <em>D′</em> falls <em>n</em> days before <em>C</em>, and vice versa.
166 </div>
167
168 <div class="controls">
169 <label>
170 <span>Your date</span>
171 <input type="date" id="dateInput">
172 </label>
173 <label>
174 <span>Location</span>
175 <select id="locationSelect"></select>
176 <div class="coldest-note" id="coldestNote"></div>
177 </label>
178 </div>
179
180 <div class="result-card" id="resultCard" style="display:none;">
181 <div class="pair">
182 <div class="day-badge spring" id="badgeA">
183 <div class="season-label" id="labelA">warming</div>
184 <div class="date" id="dateA"></div>
185 </div>
186 <div class="mirror-symbol"></div>
187 <div class="day-badge fall" id="badgeB">
188 <div class="season-label" id="labelB">cooling</div>
189 <div class="date" id="dateB"></div>
190 </div>
191 </div>
192 <div class="meta" id="metaLine"></div>
193 <div class="orbital">
194 <svg viewBox="0 0 440 440" id="orbitalSvg"></svg>
195 </div>
196 <div class="legend">
197 <div class="legend-item"><div class="legend-dot" style="background:var(--spring)"></div>Warming side</div>
198 <div class="legend-item"><div class="legend-dot" style="background:var(--fall)"></div>Cooling side</div>
199 <div class="legend-item"><div class="legend-dot" style="background:var(--cold)"></div>Coldest day</div>
200 <div class="legend-item"><div class="legend-dot" style="background:var(--hot)"></div>Warmest day</div>
201 <div class="legend-item"><div class="legend-dot" style="background:var(--sol)"></div>Winter solstice</div>
202 <div class="legend-item"><div class="legend-dot" style="background:var(--hot);opacity:0.45"></div>Summer solstice</div>
203 </div>
204 </div>
205
206 <div class="lag-card" id="lagCard" style="display:none;">
207 <h3>Why not the solstice?</h3>
208 <p>
209 The winter solstice marks the day the sun sits lowest in the sky — the most oblique angle of incidence and the least solar energy per square metre of ground. Yet the coldest day arrives weeks later. This is <strong>thermal lag</strong>: the earth and atmosphere keep radiating stored heat even after the sun begins its climb back. It's the same reason late afternoon is hotter than solar noon, scaled to an entire hemisphere.
210 </p>
211 <div class="lag-stats">
212 <div class="lag-stat sol">
213 <div class="val" id="solDate"></div>
214 <div class="lbl">Winter solstice (min sun angle)</div>
215 </div>
216 <div class="lag-stat cold">
217 <div class="val" id="coldDate"></div>
218 <div class="lbl">Coldest day (min temperature)</div>
219 </div>
220 </div>
221 <p style="margin-top:0.95rem;">
222 For <strong id="lagCity"></strong>, this thermal lag is <strong id="lagDays"></strong> — the atmosphere needs that long to exhaust its stored warmth after the solstice turns the corner.
223 </p>
224 </div>
225
226 <footer>A concept by Erik Kalviainen · Built with Claude</footer>
227</div>
228
229<script>
230const $ = id => document.getElementById(id);
231
232const locations = [
233 { name: "Waterloo, ON", cold: [1,29], hot: [7,21], h: 'N' },
234 { name: "Toronto, ON", cold: [1,28], hot: [7,20], h: 'N' },
235 { name: "Ottawa, ON", cold: [1,15], hot: [7,18], h: 'N' },
236 { name: "Montreal, QC", cold: [1,14], hot: [7,18], h: 'N' },
237 { name: "Vancouver, BC", cold: [12,28], hot: [8,2], h: 'N' },
238 { name: "Calgary, AB", cold: [1,10], hot: [7,17], h: 'N' },
239 { name: "Edmonton, AB", cold: [1,8], hot: [7,15], h: 'N' },
240 { name: "Winnipeg, MB", cold: [1,10], hot: [7,19], h: 'N' },
241 { name: "Halifax, NS", cold: [2,2], hot: [7,28], h: 'N' },
242 { name: "Sudbury, ON", cold: [1,18], hot: [7,18], h: 'N' },
243 { name: "New York, NY", cold: [1,29], hot: [7,22], h: 'N' },
244 { name: "Chicago, IL", cold: [1,21], hot: [7,20], h: 'N' },
245 { name: "Boston, MA", cold: [1,25], hot: [7,22], h: 'N' },
246 { name: "Minneapolis, MN", cold: [1,13], hot: [7,19], h: 'N' },
247 { name: "Denver, CO", cold: [12,25], hot: [7,14], h: 'N' },
248 { name: "Detroit, MI", cold: [1,27], hot: [7,20], h: 'N' },
249 { name: "Philadelphia, PA", cold: [1,28], hot: [7,22], h: 'N' },
250 { name: "Seattle, WA", cold: [12,28], hot: [8,3], h: 'N' },
251 { name: "Portland, OR", cold: [12,26], hot: [8,1], h: 'N' },
252 { name: "San Francisco, CA", cold: [1,1], hot: [9,15], h: 'N' },
253 { name: "Los Angeles, CA", cold: [12,22], hot: [8,15], h: 'N' },
254 { name: "Miami, FL", cold: [1,15], hot: [7,25], h: 'N' },
255 { name: "Washington, DC", cold: [1,27], hot: [7,21], h: 'N' },
256 { name: "London, UK", cold: [2,8], hot: [7,25], h: 'N' },
257 { name: "Paris, France", cold: [1,17], hot: [7,23], h: 'N' },
258 { name: "Berlin, Germany", cold: [1,18], hot: [7,22], h: 'N' },
259 { name: "Stockholm, Sweden", cold: [2,2], hot: [7,22], h: 'N' },
260 { name: "Helsinki, Finland", cold: [2,5], hot: [7,20], h: 'N' },
261 { name: "Moscow, Russia", cold: [1,22], hot: [7,20], h: 'N' },
262 { name: "Amsterdam, NL", cold: [2,3], hot: [7,25], h: 'N' },
263 { name: "Zurich, Switzerland", cold: [1,12], hot: [7,20], h: 'N' },
264 { name: "Tokyo, Japan", cold: [1,25], hot: [8,5], h: 'N' },
265 { name: "Seoul, S. Korea", cold: [1,15], hot: [8,1], h: 'N' },
266 { name: "Beijing, China", cold: [1,14], hot: [7,18], h: 'N' },
267 { name: "Sydney, Australia", cold: [7,17], hot: [1,22], h: 'S' },
268 { name: "Melbourne, Australia", cold: [7,20], hot: [1,28], h: 'S' },
269 { name: "Auckland, NZ", cold: [7,22], hot: [2,2], h: 'S' },
270 { name: "Buenos Aires, Argentina", cold: [7,15], hot: [1,18], h: 'S' },
271 { name: "São Paulo, Brazil", cold: [7,18], hot: [2,5], h: 'S' },
272 { name: "Cape Town, S. Africa", cold: [7,20], hot: [2,8], h: 'S' },
273];
274
275const sel = $('locationSelect');
276locations.forEach((loc, i) => {
277 const o = document.createElement('option');
278 o.value = i; o.textContent = loc.name; sel.appendChild(o);
279});
280sel.value = 0;
281$('dateInput').value = new Date().toISOString().slice(0, 10);
282
283function shortMonth(d) { return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }
284function dayOfYear(d) {
285 const s = new Date(d.getFullYear(), 0, 0);
286 return Math.round((d - s) / 86400000);
287}
288function winterSolsticeDoy(h) { return h === 'N' ? 355 : 172; }
289function summerSolsticeDoy(h) { return h === 'N' ? 172 : 355; }
290
291function polar(deg, cx, cy, r) {
292 const rad = deg * Math.PI / 180;
293 return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
294}
295function doyToAng(doy) { return (doy / 365) * 360 - 90; }
296
297function svgArc(cx, cy, r, a1, a2) {
298 let sweep = a2 - a1;
299 if (sweep < 0) sweep += 360;
300 if (sweep <= 0.1) return '';
301 const large = sweep > 180 ? 1 : 0;
302 const [x1,y1] = polar(a1,cx,cy,r);
303 const [x2,y2] = polar(a2,cx,cy,r);
304 return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
305}
306
307function thermalLag(solDoy, coldDoy) {
308 let d = coldDoy - solDoy;
309 if (d < 0) d += 365;
310 if (d > 182) d -= 365;
311 return Math.abs(d);
312}
313
314function compute() {
315 const dVal = $('dateInput').value;
316 if (!dVal) { $('resultCard').style.display='none'; $('lagCard').style.display='none'; return; }
317
318 const loc = locations[parseInt(sel.value)];
319 const D = new Date(dVal + 'T12:00:00');
320 const yr = D.getFullYear();
321 const C = new Date(yr, loc.cold[0]-1, loc.cold[1], 12);
322 const H = new Date(yr, loc.hot[0]-1, loc.hot[1], 12);
323 const cDoy = dayOfYear(C), dDoy = dayOfYear(D), hDoy = dayOfYear(H);
324
325 $('coldestNote').textContent = `Coldest avg day: ${shortMonth(C)}`;
326
327 let n = dDoy - cDoy;
328 if (n > 182) n -= 365;
329 if (n < -182) n += 365;
330 let sDoy = cDoy - n;
331 if (sDoy < 1) sDoy += 365;
332 if (sDoy > 365) sDoy -= 365;
333 const sister = new Date(yr, 0, sDoy, 12);
334 const absN = Math.abs(n);
335
336 let warmD, coolD;
337 if (n >= 0) { warmD = D; coolD = sister; } else { warmD = sister; coolD = D; }
338
339 $('resultCard').style.display = '';
340 $('dateA').textContent = shortMonth(warmD);
341 $('dateB').textContent = shortMonth(coolD);
342 $('metaLine').innerHTML = `Both <em>${absN} days</em> from the coldest day (${shortMonth(C)})`;
343
344 const wSolDoy = winterSolsticeDoy(loc.h);
345 const sSolDoy = summerSolsticeDoy(loc.h);
346 const wSolDate = new Date(yr, 0, wSolDoy, 12);
347 const lag = thermalLag(wSolDoy, cDoy);
348
349 $('lagCard').style.display = '';
350 $('solDate').textContent = shortMonth(wSolDate);
351 $('coldDate').textContent = shortMonth(C);
352 $('lagCity').textContent = loc.name;
353 $('lagDays').textContent = lag + ' days';
354
355 // --- Orbital ---
356 const CX = 220, CY = 220, R = 155;
357 const cA = doyToAng(cDoy);
358 const wA = doyToAng(dayOfYear(warmD));
359 const fA = doyToAng(dayOfYear(coolD));
360 const hA = doyToAng(hDoy);
361 const wSolA = doyToAng(wSolDoy);
362 const sSolA = doyToAng(sSolDoy);
363
364 const arcW = svgArc(CX,CY,R, cA, wA);
365 const arcF = svgArc(CX,CY,R, fA, cA);
366 const lagArc = svgArc(CX,CY,R+14, wSolA, cA);
367
368 // Lag label midpoint
369 let lagSweep = cA - wSolA; if (lagSweep < 0) lagSweep += 360;
370 let lagMidA = lagSweep > 180 ? wSolA + (lagSweep - 360) / 2 : wSolA + lagSweep / 2;
371 const [llx,lly] = polar(lagMidA, CX, CY, R + 28);
372
373 // Month ticks
374 const mNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
375 let ticks = '';
376 for (let m = 0; m < 12; m++) {
377 const md = new Date(yr, m, 15);
378 const a = doyToAng(dayOfYear(md));
379 const [t1x,t1y] = polar(a,CX,CY,R-6);
380 const [t2x,t2y] = polar(a,CX,CY,R+6);
381 const [tlx,tly] = polar(a,CX,CY,R-26);
382 ticks += `<line x1="${t1x}" y1="${t1y}" x2="${t2x}" y2="${t2y}" stroke="rgba(0,0,0,0.08)" stroke-width="1"/>`;
383 ticks += `<text x="${tlx}" y="${tly+4}" text-anchor="middle" font-size="11" fill="rgba(0,0,0,0.25)" font-family="DM Sans,sans-serif" font-weight="400">${mNames[m]}</text>`;
384 }
385
386 // Sun rays at summer solstice
387 const [ssx,ssy] = polar(sSolA,CX,CY,R);
388 let rays = '';
389 for (let i = -2; i <= 2; i++) {
390 const rA = sSolA + i * 4;
391 const [rx1,ry1] = polar(rA,CX,CY,R+5);
392 const [rx2,ry2] = polar(rA,CX,CY,R+16+(i===0?5:0));
393 rays += `<line x1="${rx1}" y1="${ry1}" x2="${rx2}" y2="${ry2}" stroke="${'var(--hot)'}" stroke-width="${i===0?2:1}" opacity="${i===0?0.5:0.3}" stroke-linecap="round"/>`;
394 }
395
396 // All key points
397 const pts = {
398 c: { xy: polar(cA,CX,CY,R), lbl: polar(cA,CX,CY,R+30) },
399 h: { xy: polar(hA,CX,CY,R), lbl: polar(hA,CX,CY,R+30) },
400 w: { xy: polar(wA,CX,CY,R), lbl: polar(wA,CX,CY,R+30) },
401 f: { xy: polar(fA,CX,CY,R), lbl: polar(fA,CX,CY,R+30) },
402 ws: { xy: polar(wSolA,CX,CY,R), lbl: polar(wSolA,CX,CY,R+30) },
403 ss: { xy: polar(sSolA,CX,CY,R), lbl: polar(sSolA,CX,CY,R+30) },
404 };
405
406 // Solstice axis
407 const [sa1x,sa1y] = polar(wSolA,CX,CY,R-10);
408 const [sa2x,sa2y] = polar(sSolA,CX,CY,R-10);
409
410 $('orbitalSvg').innerHTML = `
411 ${ticks}
412 <circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="rgba(0,0,0,0.07)" stroke-width="1.5"/>
413
414 <!-- Solstice axis -->
415 <line x1="${sa1x}" y1="${sa1y}" x2="${sa2x}" y2="${sa2y}" stroke="rgba(124,92,191,0.1)" stroke-width="1" stroke-dasharray="4 8"/>
416
417 <!-- Cold–center axis -->
418 <line x1="${pts.c.xy[0]}" y1="${pts.c.xy[1]}" x2="${CX}" y2="${CY}" stroke="rgba(58,122,181,0.1)" stroke-width="1" stroke-dasharray="4 6"/>
419 <!-- Hot–center axis -->
420 <line x1="${pts.h.xy[0]}" y1="${pts.h.xy[1]}" x2="${CX}" y2="${CY}" stroke="rgba(201,150,10,0.1)" stroke-width="1" stroke-dasharray="4 6"/>
421
422 <!-- Thermal lag arc -->
423 <path d="${lagArc}" fill="none" stroke="rgba(124,92,191,0.35)" stroke-width="1.5" stroke-dasharray="4 4" stroke-linecap="round"/>
424 <text x="${llx}" y="${lly+4}" text-anchor="middle" font-size="11" fill="rgba(124,92,191,0.7)" font-family="DM Sans,sans-serif" font-weight="500">${lag}d lag</text>
425
426 <!-- Sister day arcs -->
427 <path d="${arcW}" fill="none" stroke="var(--spring)" stroke-width="3.5" stroke-linecap="round" opacity="0.85"/>
428 <path d="${arcF}" fill="none" stroke="var(--fall)" stroke-width="3.5" stroke-linecap="round" opacity="0.85"/>
429
430 <!-- Sun rays -->
431 ${rays}
432
433 <!-- Summer solstice -->
434 <circle cx="${ssx}" cy="${ssy}" r="5.5" fill="var(--hot)" opacity="0.45"/>
435 <text x="${pts.ss.lbl[0]}" y="${pts.ss.lbl[1]+5}" text-anchor="middle" font-size="11" fill="var(--hot)" font-family="DM Sans,sans-serif" opacity="0.6" font-weight="500">${loc.h==='N'?'Jun':'Dec'} solstice</text>
436
437 <!-- Winter solstice -->
438 <circle cx="${pts.ws.xy[0]}" cy="${pts.ws.xy[1]}" r="5.5" fill="var(--sol)" opacity="0.7"/>
439 <text x="${pts.ws.lbl[0]}" y="${pts.ws.lbl[1]+5}" text-anchor="middle" font-size="11" fill="var(--sol)" font-family="DM Sans,sans-serif" font-weight="500" opacity="0.8">${loc.h==='N'?'Dec':'Jun'} solstice</text>
440
441 <!-- Warmest day -->
442 <circle cx="${pts.h.xy[0]}" cy="${pts.h.xy[1]}" r="6" fill="var(--hot)" opacity="0.75"/>
443 <text x="${pts.h.lbl[0]}" y="${pts.h.lbl[1]+5}" text-anchor="middle" font-size="12" fill="var(--hot)" font-family="DM Sans,sans-serif" font-weight="600">${shortMonth(H)}</text>
444
445 <!-- Coldest day -->
446 <circle cx="${pts.c.xy[0]}" cy="${pts.c.xy[1]}" r="6.5" fill="var(--cold)"/>
447 <text x="${pts.c.lbl[0]}" y="${pts.c.lbl[1]+5}" text-anchor="middle" font-size="12" fill="var(--cold)" font-family="DM Sans,sans-serif" font-weight="600">${shortMonth(C)}</text>
448
449 <!-- Warming dot -->
450 <circle cx="${pts.w.xy[0]}" cy="${pts.w.xy[1]}" r="7" fill="var(--spring)"/>
451 <text x="${pts.w.lbl[0]}" y="${pts.w.lbl[1]+5}" text-anchor="middle" font-size="12.5" fill="var(--spring)" font-family="DM Sans,sans-serif" font-weight="600">${shortMonth(warmD)}</text>
452
453 <!-- Cooling dot -->
454 <circle cx="${pts.f.xy[0]}" cy="${pts.f.xy[1]}" r="7" fill="var(--fall)"/>
455 <text x="${pts.f.lbl[0]}" y="${pts.f.lbl[1]+5}" text-anchor="middle" font-size="12.5" fill="var(--fall)" font-family="DM Sans,sans-serif" font-weight="600">${shortMonth(coolD)}</text>
456 `;
457}
458
459$('dateInput').addEventListener('change', compute);
460sel.addEventListener('change', compute);
461compute();
462</script>
463</body>
464</html>
465